Sturdy

QuantstampFiles named in the report

Quantstamp reviewed Sturdy on 13 Oct 2022: 26 findings, 0 critical; 12 marked fixed, 12 acknowledged, 2 open.

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

Report hosted on certificate.quantstamp.com.

Trustblock does not review code.

Findings26

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

SeverityOpenAcknowledgedFixed
Critical000
High000
Medium205
Low0127
MediumCannot Unregister VaultFixed

A vault can be registered through the `LendingPoolConfigurator.registerVault()` function but cannot currently be unregistered in any way. Registered vaults have a high level of access to the `LendingPool` contract through the `withdrawFrom()`, `getYield()`, `deposit()` and `depositYield()` functions. Thus, if a vault is compromised in any way, it is important to have the ability to unregister it quickly.

MediumReplay Attack on Forked ChainFixed

The `AToken` and `ATokenForCollateral` contracts implement EIP-712 to accept off-chain signatures. While the signatures must include a `chainId`, we note that the `DOMAIN_SEPARATOR` for both contracts is defined as follows within the `initialize()` function: ``` assembly { chainId := chainid() } DOMAIN_SEPARATOR = keccak256( abi.encode( EIP712_DOMAIN, keccak256(bytes(aTokenName)), keccak256(EIP712_REVISION), chainId, address(this) ) ); ``` Note that the `chainId` is only obtained during the initial computation of the `DOMAIN_SEPARATOR` during the initialization of the contract. If the chain is forked, then the signature could be replayed across both forks of the chain.

MediumUser May Be Unable to Withdraw AssetsFixed

By design, the protocol requires users to withdraw their collateral from a vault. However, it is technically possible for users to withdraw directly from the `LendingPool` contract directly using the `LendingPool.withdraw()` function. For some tokens, such as stETH, directly withdrawing from the `LendingPool` contract does not appear to cause any issues. However, if a `SturdyInternalAsset` token is withdrawn directly from the `LendingPool` contract, there will be no way for a user to redeem the `SturdyInternalAsset` for the actual token it represents. The user will be left holding the valueless `SturdyInternalAsset` token and will not be able to withdraw their original assets.

MediumViolation of Checks-Effects-Interactions Pattern in `ConvexCurveLPVault`Fixed

The `ConvexCurveLPVault._withdraw()` function does not follow the [Check-Effects-Interactions pattern](https://docs.soliditylang.org/en/v0.8.1/security-considerations.html#re-entrancy). Specifically, `L239` should be performed after `L242` instead of the current order.

MediumViolation of Checks-Effects-Interactions Pattern in `LendingPoolCollateralManager`Fixed

The `LendingPoolCollateralManager.liquidationCall()` function does not follow the [Check-Effects-Interactions pattern](https://docs.soliditylang.org/en/v0.8.1/security-considerations.html#re-entrancy). This is because the function obtains the debt reserve from the liquidator after completing the internal accounting process in regard to the repayment of debt. It may be possible for the liquidator to sell the obtained collateral into debt tokens and then reenter the contract and finish the final external call.

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

The `ConvexCurveLPVault.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 `ConvexCurveLPVault`, which retrieves yield based on how much the Convex pool awards it.

Low`YearnRETHWstETHVault` May Claim Less Yield if Curve Pool Is ManipulatedFixed

The `YearnRETHWstETHVault.processYield()` function involves taking many steps to convert the yield token into WETH tokens. One of these steps is withdrawing liquidity from a Curve pool to retrieve wstETH as seen on `L125-L129`: ``` amountWstETH = ICurvePool(_poolAddress).remove_liquidity_one_coin( _amount, 1, minWstETHAmount, address(this) ); ``` If a Curve pool is imbalanced, then the liquidity provider may have to pay a withdrawal fee when removing liquidity. A malicious user may cause an imbalance in the Curve pool before calling `YearnRETHWstETHVault.processYield()` to force the function to pay the penalty by withdrawing liquidity from the Curve pool.

LowAssets with More Decimal Places than `18` Are Implicitly DisallowedFixed

Multiple areas within the code implicitly assume that assets with more decimal places than `18` may not be used. The assumption is best illustrated by `ATokenFromCollateral#L159-L160`: ``` if (decimal < 18) amount = (share * 10**(18 - decimal)).rayMul(index); else amount = share.rayMul(index); ``` We can see that the first condition accounts for when an asset token has less than `18` decimal places. The second `else` statement assumes that the asset has exactly `18` decimal places. However, no condition dealing with the case for greater than `18` decimal places exists. The same set of conditions can be found in multiple contracts, including `LendingPoolCollateralManager` and `LendingPool`. There is currently no explicit validation check to ensure that the external asset has less than or equal to `18` decimal places. An ideal place to add this check would be when a reserve is initialized within the `LendingPoolConfigurator.batchInitReserve()` function.

LowCan Enable Borrowing and Collateral on Same ReserveFixed

The `LendingPoolConfigurator.enableBorrowingOnReserve()` and `LendingPoolConfigurator.enableCollateralOnReserve()` functions enable borrowing and collateral on a reserve respectively. A reserve which can be borrowed should never be used as collateral and vice versa. However, it is currently possible to enable borrowing and collateral on the same reserver by calling the two functions with the same reserve `asset`.

LowCan Receive Less than Expected Amount From SwapFixed

The `YearnVault.processYield()` function performs multiple swaps to obtain debt assets from FTM. There is a broken validation check performed after the swap to ensure that the correct amount of debt asset has been received on `L126-L129`: ``` require( IERC20(_tokenOut).balanceOf(address(this)) >= receivedAmounts[1], Errors.VT_PROCESS_YIELD_INVALID ); ``` The current check validates that the contract's current token balance exceeds the expected amount returned by the swap. This check is insufficient if the contract had an existing balance of the token before the swap.

Show 2 more
LowMisleading Behavior of `withdrawOnLiquidation()` in `LidoVault`Fixed

The `GeneralVault.withdrawOnLiquidation()` function is not overridden in `LidoVault`. This means that the `LidoVault.withdrawOnLiquidation()` will have the exact same code as `GeneralVault.withdrawOnLiquidation()`. It will thus be callable by anyone and will return the `_amount` passed in as a parameter without doing anything else. This could result in callers of the function being misled into believing that a liquidation has occurred successfully. Furthermore, the `withdrawOnLiquidation()` function is generally only meant to be callable by the `LendingPool` contract. However, `LidoVault.withdrawOnLiquidation()` is callable by anyone.

LowMissing Input ValidationFixed

<br> 1. `LendingPool.initialize()`: `provider` should not be zero. 1. `LendingPoolConfigurator.initialize()`: `provider` should not be zero. 1. `AToken.sol.initialize()`: `aTokenDecimals`, `pool`, `treasury`, `underlyingAsset`, `incentivesController` should not be zero. 1. `GeneralVault.initialize()`: `_provider` should not be zero. 1. `ReserveLogic.init()`: `yieldAddress`, `stableDebtTokenAddress`, `variableDebtTokenAddress`, `interestRateStrategyAddress` should not be zero.

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 (24)

  • LendingPoolAddressesProvider.sol
  • LidoVault.sol
  • LendingPoolAddressesProviderRegistry.sol
  • VariableDebtToken.sol
  • ConvexCurveLPVault.sol
  • DebtTokenBase.sol
  • SturdyInternalAsset.sol
  • LendingPool.sol
  • DefaultReserveInterestRateStrategy.sol
  • AToken.sol
  • YearnVault.sol
  • LendingPoolCollateralManager.sol
  • GenericLogic.sol
  • BaseImmutableAdminUpgradeabilityProxy.sol
  • ValidationLogic.sol
  • CollateralAdapter.sol
  • VersionedInitializable.sol
  • ReserveLogic.sol
  • InitializableImmutableAdminUpgradeabilityProxy.sol
  • YearnRETHWstETHVault.sol
  • IncentiveVault.sol
  • ATokenForCollateral.sol
  • GeneralVault.sol
  • LendingPoolConfigurator.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