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 |
The approach taken to calculate the Balancer LP Token value involves multiplying the price of the cheaper of the two tokens (`WETH` and `STETH`) by the value returned by `BALWSTETHWETH.getRate()`. This price calculation would be most accurate when the price of `WETH` is the same as `STETH`. However, the `WETH/STETH` price can fluctuate significantly, and the further they diverge, the less accurate the price calculation will be. While the calculation provides a reliable lower bound for the price, it is unclear how different this lower bound will be from the actual price. If the price calculation is too low, it could result in premature liquidation of users who have deposited the Balancer LP token as collateral.
The function `enterPositionWithFlashloan()` assumes the maximum amount of slippage when calculating the amount to borrow for the flash loan. This can be seen in the code below from the helper function `_leverageWithFlashloan()` ``` amounts[0] = ((((_principal * _getAssetPrice(COLLATERAL)) / 10**DECIMALS) * 10**borrowAssetDecimals) / _getAssetPrice(_borrowAsset)).percentMul(_leverage).percentMul( PercentageMath.PERCENTAGE_FACTOR + _slippage ); ``` This borrowed amount is then completely swapped to collateral tokens and deposited on behalf of the user as shown in the code below from`_enterPositionWithFlashloan()`: ``` //swap borrowing asset to collateral uint256 collateralAmount = _swapTo(_borrowingAsset, _borrowedAmount); require(collateralAmount >= _minAmount, Errors.LS_SUPPLY_FAILED); //deposit collateral _supply(collateralAmount, _user); //borrow borrowing asset _borrow(_borrowingAsset, _borrowedAmount + _fee, _user); ``` A user may expect that they will only deposit `_principal + _principal*_leverage` amount of collateral if the swap slippage is zero. They may be surprised to see that they have a more leveraged position than they expected. The `zapLeverageWithFlashloan()` suffers from the same issue.
The `BALWSTETHWETHOracle` contract's primary purpose is to provide a price for the `B-stETH-STABLE ` token. However, the correct calculation of the price is highly dependent on having the correct `STETH` price. Currently, the only source of this data is the Chainlink price feed. In the event of any large-scale attack/disruption of the Chainlink network, the protocol could be severely impacted.
As the `_swapTo()` and `_swapFrom()` functions in the `AURAWSTETHWETHLevSwap` contract return the whole contract balance of Balancer LP Tokens and WETH, respectively, such tokens, which were sent directly to the contract by accident, will be returned to the next user swapping to that asset. So, under such conditions, users will unexpectedly receive more tokens than perhaps anticipated from swaps. This of course does not seem problematic but could lead to unexpected behavior. The inflated amount of collateral would be used to create leverage, possibly resulting in a much larger position than desired. Of course, a user could immediately withdraw again and make a profit, but the user may also be confused by the protocol's behavior.
The `registerLevSwapper()` function represents a critical state change for the protocol, as it changes or disables the leverage swap functionality for a particular collateral asset. The function should emit an event, so this key state transition can be monitored and responded to.
The functions `setConfiguration()` and `processExtraYield()` in the `AuraBalancerLPVault` contract are marked as `payable`, yet we were unable to find a reason for why that should be the case. It seems like there would be no use for ETH transferred via these methods and the funds would become stuck.
The definition of leverage the protocol uses might differ from a user's expectations. For example, most users would expect when using "2X" leverage that an additional 100% of the provided collateral would be borrowed. Yet in this protocol, a leverage of `2` will result in an additional `200%` being borrowed, which for some users might lead to unexpectedly creating higher leverage than desired. While the used definition differs, the documentation properly describes how the leverage is set and calculated in the protocol.
The `LeverageSwapManager` possesses a function `registerLevSwapper()` which is callable by the administrator role of the contract. This function allows the admin to change the official implementation of the `*LevSwap` contract for a particular collateral to an entirely different one. They may also disable the leveraged swap functionality for the collateral by calling `registerLevSwapper()` with the zero address for the `swapper` parameter. Users should be made aware of the consequences of this privileged role and its consequences. The team should also warn users in advance if the leveraged swapped functionality is being disabled or changed for a collateral asset.
To avoid reentrancy risks and reduce the attack surface, it is highly recommended to inherit from OpenZeppelin's `ReentrancyGuard` contract and add the `nonReentrant` modifier to all functions that: 1. Involve asset manipulation. 2. Could be accessed by any external address. 3. Are not expected to be reentered The following is a non-exhaustive list of functions we recommend adding a `nonReentrant` modifier to: * `GeneralVault`/`AuraBalancerLPVault.sol` * `depositCollateral()` * `depositCollateralFrom()` * `withdrawCollateral()` * `processYield()`
The platform generates yields by sending the collateral tokens stored by borrowers to other protocols such as Aura. If any of these yield-generating protocols got hacked, the protocol users would also lose their funds.
The exploit scenario described in QSP-2 demonstrates that the effect of swap slippage can be greatly amplified. In that scenario, the swap slippage was only `10%,` and yet the user lost `45%` of their position's value. The amplification of slippage is primarily caused by the need to pay back the exact amount of borrowed tokens (plus any flash loan fees) to the flash loan provider. The flash loan provider absorbs no fraction of the loss of value due to slippage, so the entire cost is absorbed by the user. It follows that using higher leverage, and thus a higher borrow amount, leads to a higher amplification of the swap slippage rate.
Every Solidity file specifies in the header a version number of the format `pragma solidity ^0.8.0`. The `^` before the version number implies an unlocked pragma, meaning that the compiler will use the specified version and above, hence the term "unlocked".
There are multiple instances of unsafe casts within the contracts. These could lead to overflow or underflow of values and have unpredictable results. We have listed all instances of unsafe casts below: 1. `BALWSTETHWETHOracle`` 1. ``L25:uint256 minValue = Math.min(**uint256**(wstETHPrice), 1e18);` 2. `L39:return (true, int256(_get()));` 3. ``L45:return int256(_get());` 2. AURAWSTETHWETHLevSwap` 1. `L45:uint256 joinKind = **uint256**(IBalancerVault.JoinKind.EXACT_TOKENS_IN_FOR_BPT_OUT);` 2. `L75:uint256 exitKind = **uint256**(IBalancerVault.ExitKind.EXACT_BPT_IN_FOR_ONE_TOKEN_OUT);`
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