Quantstamp reviewed Sturdy (Sturdy (Aura Integration And Leverage)) on 24 Jan 2023: 29 findings, 0 critical; 13 marked fixed, 13 acknowledged, 3 open.
Report hosted on certificate.quantstamp.com.
Trustblock does not review code.
Statuses are the security firm’s own, from its report.
| Severity | Open | Acknowledged | Fixed |
|---|---|---|---|
| Critical | 0 | 0 | 0 |
| High | 1 | 0 | 1 |
| Medium | 1 | 1 | 0 |
| Low | 1 | 12 | 12 |
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.
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.
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; } ```
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.
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,
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.
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.
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.
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.
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);`
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.
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.
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.
The report names a repository or files. Open the report to see its scope.
Repository and files the report names (5)
236 projects reviewed · 332 reports · latest Oct 2026
See Quantstamp’s auditsSomething 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