Sturdy security record

Ethereum, Fantom, Optimismsturdy.financeX

As of 21 Oct 2026, Sturdy has 4 audit reports by 3 security firms on Trustblock; the latest is by ChainSecurity, dated 13 Oct 2023. Those reports list no open critical findings.

Critical findings
No open critical findings
Across 4 reports.
Open the latest report
Your label, live
Get the label for your site

Record last updated . It holds audits and findings, contracts and incidents up to Oct 2026. Trustblock does not review code.

Audits4

Newest first

AuditSecurity firmDateScopeCritical findingsReport
Sturdy AggregatorSecurity firm: ChainSecurityDate: Scope: Files named in the reportNo open critical findingsOpen · chainsecurity.com
Sturdy security assessmentSecurity firm: ZellicDate: Scope: Not recorded (see the report)No open critical findingsOpen · GitHub
Sturdy (Aura Integration And Leverage)Security firm: QuantstampDate: Scope: Files named in the reportNo open critical findingsOpen · certificate.quantstamp.com
SturdySecurity firm: QuantstampDate: Scope: Files named in the reportNo open critical findingsOpen · certificate.quantstamp.com

Findings65

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

SeverityOpenAcknowledgedFixed
Critical000
High101
Medium3210
Low12522
HighDraining Collateral with Reentrancy AttackSturdy (Aura Integration And Leverage) · Quantstamp · 24 Jan 2023Fixed

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.

Open the report
MediumIdle Assets Not Used for requestLiquiditySturdy Aggregator · ChainSecurity · 13 Oct 2023Fixed

The report gives no description here.

Open the report
MediumSorting of the Lenders Is IncorrectSturdy Aggregator · ChainSecurity · 13 Oct 2023Fixed

The report gives no description here.

Open the report
MediumUtilization Limit Does Not Take Into Account JIT LiquiditySturdy Aggregator · ChainSecurity · 13 Oct 2023Fixed

The report gives no description here.

Open the report
MediumUtilization Limit Only Enforced on Requesting LenderSturdy Aggregator · ChainSecurity · 13 Oct 2023Fixed

The report gives no description here.

Open the report
MediumutilizationLimit Is Not Always EnforcedSturdy Aggregator · ChainSecurity · 13 Oct 2023Fixed

The report gives no description here.

Open the report
MediumCannot Unregister VaultSturdy · Quantstamp · 13 Oct 2022Fixed

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.

Open the report
MediumReplay Attack on Forked ChainSturdy · Quantstamp · 13 Oct 2022Fixed

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.

Open the report
MediumUser May Be Unable to Withdraw AssetsSturdy · Quantstamp · 13 Oct 2022Fixed

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.

Open the report
MediumViolation of Checks-Effects-Interactions Pattern in `ConvexCurveLPVault`Sturdy · Quantstamp · 13 Oct 2022Fixed

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.

Open the report
Show 10 more
MediumViolation of Checks-Effects-Interactions Pattern in `LendingPoolCollateralManager`Sturdy · Quantstamp · 13 Oct 2022Fixed

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.

Open the report
LowIncorrect Code CommentSturdy Aggregator · ChainSecurity · 13 Oct 2023Fixed

The report gives no description here.

Open the report
LowReentrancy Guards Applied InconsistentlySturdy Aggregator · ChainSecurity · 13 Oct 2023Fixed

The report gives no description here.

Open the report
LowMissing test suite code coverageSturdy security assessment · Zellic · 15 Sep 2023Fixed

The report gives no description here.

Open the report
Low`getYieldAmount()` Returns Incorrect Value for `AuraBalancerLPVault`Sturdy (Aura Integration And Leverage) · Quantstamp · 24 Jan 2023Fixed

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.

Open the report
Low`vaultYieldInPrice()` Returns Incorrect Value for All VaultsSturdy (Aura Integration And Leverage) · Quantstamp · 24 Jan 2023Fixed

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; } ```

Open the report
LowAllowing More than 10x LeverageSturdy (Aura Integration And Leverage) · Quantstamp · 24 Jan 2023Fixed

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.

Open the report
LowExtreme Fee Setup Can Block the Yield FunctionSturdy (Aura Integration And Leverage) · Quantstamp · 24 Jan 2023Fixed

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,

Open the report
LowHardcoded Slippage May Result in Swap FailureSturdy (Aura Integration And Leverage) · Quantstamp · 24 Jan 2023Fixed

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.

Open the report
LowIncorrect `_vaultFee` Could Cause ` _transferYield()` to FailSturdy (Aura Integration And Leverage) · Quantstamp · 24 Jan 2023Fixed

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.

Open the report
Show 10 more
LowMissing Input ValidationSturdy (Aura Integration And Leverage) · Quantstamp · 24 Jan 2023Fixed

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.

Open the report
LowPrecision Loss Due to Division Before MultiplicationSturdy (Aura Integration And Leverage) · Quantstamp · 24 Jan 2023Fixed

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.

Open the report
LowReturn Values Not VerifiedSturdy (Aura Integration And Leverage) · Quantstamp · 24 Jan 2023Fixed

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);`

Open the report
LowStrict Inequality Could Cause Validation to FailSturdy (Aura Integration And Leverage) · Quantstamp · 24 Jan 2023Fixed

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.

Open the report
LowUnlimited Allowance Given to VaultSturdy (Aura Integration And Leverage) · Quantstamp · 24 Jan 2023Fixed

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.

Open the report
LowUsage of `BALWSTETHWETHOracle` Is UnclearSturdy (Aura Integration And Leverage) · Quantstamp · 24 Jan 2023Fixed

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.

Open the report
Low`getYieldAmount()` Returns Incorrect Value for `ConvexCurveLPVault`Sturdy · Quantstamp · 13 Oct 2022Fixed

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.

Open the report
Low`YearnRETHWstETHVault` May Claim Less Yield if Curve Pool Is ManipulatedSturdy · Quantstamp · 13 Oct 2022Fixed

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.

Open the report
LowAssets with More Decimal Places than `18` Are Implicitly DisallowedSturdy · Quantstamp · 13 Oct 2022Fixed

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.

Open the report
LowCan Enable Borrowing and Collateral on Same ReserveSturdy · Quantstamp · 13 Oct 2022Fixed

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`.

Open the report
Show 3 more
LowCan Receive Less than Expected Amount From SwapSturdy · Quantstamp · 13 Oct 2022Fixed

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.

Open the report
LowMisleading Behavior of `withdrawOnLiquidation()` in `LidoVault`Sturdy · Quantstamp · 13 Oct 2022Fixed

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.

Open the report
LowMissing Input ValidationSturdy · Quantstamp · 13 Oct 2022Fixed

<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.

Open the report

Contracts

No deployed address is recorded for these audits.

The reports name a repository or files. Open a report to see its scope.

The repositories and files the reports name

Show this on your site

Is this your project? Show this record on your own website. The label states what the record holds, links to this page and updates by itself. It is free and needs no sign-in.

Content

Facts

Automatic: the three strongest facts of this project’s own record (its last audit and two more), highlighted. They update with the record. Picking a fact turns Automatic off.

Fixed as
Language

Style

Layout
Theme
Accent
Corners

Format

Size
Date
Embed as
HTML
<head>
<script src="https://widget.trustblock.run/project-widget.min.js" defer></script>
</head>
<body>
<div
data-tb-project-widget
data-tb-project-slug="sturdy"
></div>
</body>

Paste the code where the label should appear. It uses no API credit and updates by itself.

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