Sturdy (Aura Integration And Leverage)

QuantstampFiles named in the report

Quantstamp reviewed Sturdy (Sturdy (Aura Integration And Leverage)) on 24 Jan 2023: 29 findings, 0 critical; 13 marked fixed, 13 acknowledged, 3 open.

No open critical findings
Open the reportOpen Sturdy’s security record

Report hosted on certificate.quantstamp.com.

Trustblock does not review code.

Findings29

Statuses are the security firm’s own, from its report.

SeverityOpenAcknowledgedFixed
Critical000
High101
Medium110
Low11212
HighDraining Collateral with Reentrancy AttackFixed

In this audit, we found an exploit path in which the attacker can drain the collateral of other users. The `GeneralLevSwap` contract does not use the `ReentrancyGuard` (for example, OpenZeppelin's contract: [link](https://docs.openzeppelin.com/contracts/4.x/api/security#ReentrancyGuard)) nor follows the [checks-effects-interactions pattern](https://fravoll.github.io/solidity-patterns/checks_effects_interactions.html). Thus, the token transfer calls might trigger reentrancy attacks. However, most of the time, the code only interacts with allow-listed tokens. The tokens are either from the Sturdy platform itself or integrated platforms, such as Aave and Balancer. Those tokens do not have the hook to trigger a reentrancy attack. Unfortunately, in the `GeneralLevSwap._withdrawWithFlashloan()` function, the line `IERC20(_sAsset).safeTransferFrom(_user, address(this), withdrawalAmount)` can be a valid entry point of a reentrancy attack. The reason is that `_sAsset` is from user input when calling the `GeneralLevSwap.withdrawWithFlashloan()` function. Therefore, the attacker can pass in a malicious `_sAsset` contract address and trigger the attack. Once triggered, the attacker can call the `IBalancerVault(BALANCER_VAULT).flashLoan()` with arbitrary `userData` (see: [balancer's flashLoan code](https://github.com/balancer-labs/balancer-v2-monorepo/blob/d2794ef7d8f6d321cde36b7c536e8d51971688bd/pkg/vault/contracts/FlashLoans.sol#L42)). This allows the Balancer to call the `GeneralLevSwap.receiveFlashLoan()` function with unexpected data. The `_balancerFlashLoanLock == 2` validation on `L131` will also be bypassed since it is inside a reentrancy call. Finally, the transaction calls the `GeneralLevSwap._withdrawWithFlashloan()` function with arbitrary inputs to redeem the aTokens (`_sAsset`) of the victims.

Low`getYieldAmount()` Returns Incorrect Value for `AuraBalancerLPVault`Fixed

The `AuraBalancerLPVault.getYieldAmount()` function should return the amount of pending yield for the vault. However, it is currently returning the result of the `GeneralVault._getYieldAmount()` function, which returns the difference between the `ATokenForCollateral` supply and the amount of underlying token held within the `ATokenForCollateral` contract. This calculation does not apply to the `AuraBalancerLPVault`, which retrieves yield based on how much the AURA pool awards it.

Low`vaultYieldInPrice()` Returns Incorrect Value for All VaultsFixed

The `GeneralVault.vaultYieldInPrice()` function always returns `0` and is not overridden by any vault contracts. As such, this function always returns the incorrect value. This can be seen in the function definition below: ``` function vaultYieldInPrice() external view virtual returns (uint256) { return 0; } ```

LowAllowing More than 10x LeverageFixed

In the `GeneralLevSwap` contract, the `zapLeverageWithFlashloan()` and `enterPositionWithFlashloan()` functions does not validate the `_leverage` input to be less than or equal to nine. If the user calls the functions with `_leverage > 9`, it will have the effect of having a leverage `> 10x` of the user's principle. This breaks the spec from the [Sturdy documentation](https://docs.sturdy.finance/overview/what-is-sturdy), which is states that "DeFi's best yield farmers use Sturdy to access up to 10x leverage to farm with". Note that due to the liquidation threshold, the leverage is likely to fail as one cannot borrow such that their position is liquidatable.

LowExtreme Fee Setup Can Block the Yield FunctionFixed

In the case where `_vaultFee + _incentiveRatio == PercentageMath.PERCENTAGE_FACTOR` in the `AuraBalancerLPVault` contract, the `AuraBalancerLPVault._transferYield()` function might fail due to overflow. The reason is that the `PercentageMath.percentMul()` function rounds up when the decimal is larger than or equal to `0.5`. Consequently, in the case `_vaultFee + _incentiveRatio == PercentageMath.PERCENTAGE_FACTOR`, the `incentiveAmount` and `treasuryAmount` might be larger than the `yieldAmount`. The transfer can fail unexpectedly,

LowHardcoded Slippage May Result in Swap FailureFixed

When swapping from the Balancer LP token to the borrowing asset through the `_swapFrom()` function, there is a hard-coded slippage value of `10%` as seen in `L93:uint256 minAmountOut = ((_amountToSwap * fromAssetPrice) / 1e18).percentMul(9000); //10% slippage`. If a user ever wanted to allow more than `10%` slippage due to unusual market conditions, it would not be possible. In these cases, any attempt to swap through `_swapFrom()` would fail.

LowIncorrect `_vaultFee` Could Cause ` _transferYield()` to FailFixed

The `AuraBalancerLPVault` contract introduces another fee parameter, namely `_incentiveRatio`. It is set by the function `setIncentiveRatio()`, which properly makes sure that the sum of `_vaultFee` and `_incentiveRatio` is less than `100%`. However, while the function `GeneralVault.setTreasuryInfo()`, which sets the treasury address and the `_vaultFee`, checks that the `_vaultFee` is less than or equal to `30%`, it does not perform a sanity check that `_vaultFee` and `_incentiveRatio` (which is only introduced in `AuraBalancerLPVault.sol`) is less than `100%`. So, the contract could be erroneously set up where first the `_incentiveRatio` is set to `71%` via `setIncentiveRatio()`, followed by a setting of the `_vaultFee` of `30%` via `setTreasuryInfo()`, leading to `_incentiveRatio` + `_vaultFee` > `100%`. This would cause the `_transferYield()` function to revert, leading to the yield being stuck in the contract, as more than `100%` of the contract balance is attempted to be transferred.

LowMissing Input ValidationFixed

Several functions within the protocol lack sufficient validation of their input parameters. We have listed all missing checks below: 1. `GeneralLevSwap` 1. `executeOperation()` 1. Validate that the array parameters have the same length. 2. Validate that `amounts[0]` is greater than zero. 3. Validate that `assets[0]` is not the zero address. 2. `receiveFlashLoan()` 1. Validate that the array parameters have the same length. 2. Validate that `amounts[0]` is greater than zero. 3. Validate that `tokens[0]` is not the zero address. 3. `_executeOperation()` 1. Validate that `arg1` is greater than zero. 2. Validate that `arg2` is not the zero address. 3. If `isEnterPosition` is `false`, then validate that `arg0` is greater than zero. 4. If `isEnterPosition` is `false`, then validate that `arg3` is not the zero address. 4. `enterPositionWithFlashloan()` 1. Validate that `_borrowingAsset` is not the zero address. 5. `withdrawWithFlashloan()` 1. Validate that `_borrowingAsset` is not the zero address. 6. `zapLeverageWithFlashloan()` 1. Validate that `_borrowingAsset` is not the zero address. 2. Validate that `_zappingAsset` is not the zero address. 7. `zapDeposit()` 1. Validate that `_zappingAsset` is not the zero address. 2. `LeverageSwapManager` 1. `initialize()` 1. Validate that `_provider` is not the zero address. 2. `registerLevSwapper()` 1. Validate that `collateral` is not the zero address. 3. `AURAWSTETHWETHLevSwap` 1. `_swapTo()` 1. Validate that the `_borrowingAsset` is WETH. 2. `_swapFrom()` 1. Validate that the `_borrowingAsset` is WETH.

LowPrecision Loss Due to Division Before MultiplicationFixed

There are two calculations in which precision is lost due to division before multiplication. The first is the calculation for the withdrawal amounts from ``_withdrawWithFlashloan()` below: ``` uint256 withdrawalAmountETH = (((totalCollateralETH * currentLiquidationThreshold) / PercentageMath.PERCENTAGE_FACTOR - totalDebtETH) * PercentageMath.PERCENTAGE_FACTOR) / assetLiquidationThreshold; uint256 withdrawalAmount = Math.min( IERC20(_sAsset).balanceOf(_user), (withdrawalAmountETH * (10**DECIMALS)) / _getAssetPrice(COLLATERAL) ); ``` In the calculation for `withdrawalAmountETH` , a number is divided by `PercentageMath.PERCENTAGE_FACTOR` and then multiplied by another number. `withdrawalAmountETH` is also first divided by `assetLiquidationThreshold` and then multiplied by another number. The second example is the calculation for the amount to borrow with the flash loan in `_leverageWithFlashloan()`: ``` amounts[0] = ((((_principal * _getAssetPrice(COLLATERAL)) / 10**DECIMALS) * 10**borrowAssetDecimals) / _getAssetPrice(_borrowAsset)).percentMul(_leverage).percentMul( PercentageMath.PERCENTAGE_FACTOR + _slippage ); ``` Above, a number is divided by `10**DECIMALS` and then multiplied by another number.

LowReturn Values Not VerifiedFixed

Several contracts within the protocol make calls to functions on other contracts with return values that indicate the result of those functions. However, many of these return values are not being validated within the code. We have listed all function calls for which return values are being ignored: 1. `GeneralLevSwap` 1. `L83:IERC20(COLLATERAL).approve(_vault, type(**uint256**).max);` 2. `L367:LENDING_POOL.repay(_borrowingAsset, _amount, USE_VARIABLE_DEBT, borrower);` 2. `AuraBalancerLPVault` 1. `L197:AURA_BOOSTER.deposit(auraPoolId, _amount, true);` 2. `L202:SturdyInternalAsset(internalAsset).mint(address(this), _amount);` 3. ``L236:IConvexBaseRewardPool(baseRewardPool).withdrawAndUnwrap(_amount, false);`

Show 3 more
LowStrict Inequality Could Cause Validation to FailFixed

A validation check within `_withdrawWithFlashloan()` incorrectly uses strict inequality, which may cause the function to fail for valid data: `require(withdrawalAmount > _requiredAmount, Errors.LS_SUPPLY_NOT_ALLOWED);`. If the `withdrawalAmount` equals the `_requiredAmount`, the validation check will fail, despite meeting the user's requirements.

LowUnlimited Allowance Given to VaultFixed

In the constructor of the `GeneralLevSwap` contract, the `_vault` is set to have a maximum allowance of the `COLLATERAL`. While the `GeneralLevSwap` contract is not designed to hold any collateral assets directly, setting an unlimited allowance is a pattern that should generally be avoided.

LowUsage of `BALWSTETHWETHOracle` Is UnclearFixed

It is unclear how exactly `BALWSTETHWETHOracle.sol` will be integrated with the remainder of the system. The `AURAWSTETHWETHLevSwap` contract inherits from the `GeneralLevSwap` contract, which internally uses `SturdyOracle` contract as its `ORACLE`. The `SturdyOracle` contract seems to be designed to make direct calls to chainlink oracles, but their function signatures differ compared to the `BALWSTETHWETHOracle` contract (which inherits from an `IOracle` interface, unused in the `SturyOracle` contract). Therefore, the `BALWSTETHWETHOracle` contract could not be integrated as another `assetSource` of the `SturdyOracle` contract, resulting in non-trivial setup on how it would actually be used in the protocol.

Contracts in scope

No deployed address is recorded for this audit.

The report names a repository or files. Open the report to see its scope.

Repository and files the report names (5)

  • contracts/protocol/vault/ethereum/AuraVault/AuraBalancerLPVault.sol
  • contracts/protocol/leverage/GeneralLevSwap.sol
  • contracts/misc/BALWSTETHWETHOracle.sol
  • contracts/protocol/leverage/LeverageSwapManager.sol
  • contracts/protocol/leverage/ethereum-eth/AURAWSTETHWETHLevSwap.sol

Part of Sturdy’s security record

Sturdy
Audits4 audits by 3 firmsLast audit Oct 2023, by ChainSecurity.
Critical findingsNo open critical findingsAcross 4 reports.
Open Sturdy’s security record

Security firm

Quantstamp

236 projects reviewed · 332 reports · latest Oct 2026

See Quantstamp’s audits

Something wrong on this page? A missing report, a wrong date, a finding that was fixed since. Tell us and we check it against its source.

Tell us