Sturdy LP Oracle Manipulation
Exploit Transactions
0xeb87ebc0a18aca7d2a9ffcabf61aa69c9e8d3c6efade9e2303f8857717fb9eb7Victim Addresses
0x9f72dc67cec672bb99e3d02cbea0a21536a2b657Ethereum0xe5d78eb340627b8d5bcff63590ebec1ef9118c89Ethereum0xa36be47700c079bd94adc09f35b0fa93a55297bcEthereum0x6ae5fd07c0bb2264b1f60b33f65920a2b912151cEthereumLoss Breakdown
Similar Incidents
Aave AMM LP Oracle Manipulation Through Delegated Recursive LP Looping
43%Themis Oracle Manipulation
41%Conic crvUSD Oracle Exploit
39%Conic ETH Oracle Reentrancy
39%bZx/Fulcrum iETH oracle manipulation enables undercollateralized WETH borrowing
37%Ploutos Market Oracle Feed Misconfiguration Enabled Undercollateralized WETH Borrow
34%Root Cause Analysis
Sturdy LP Oracle Manipulation
1. Incident Overview TL;DR
On Ethereum mainnet block 17460610, EOA 0x1e8419e724d51e87f78e222d935fbbdeb631a08b sent transaction 0xeb87ebc0a18aca7d2a9ffcabf61aa69c9e8d3c6efade9e2303f8857717fb9eb7 to attacker contract 0x0b09c86260c12294e3b967f0d523b4b2bcdfbeab. That transaction used public Aave flashloans plus public Curve ETH/stETH and Balancer B-stETH-STABLE liquidity to temporarily inflate the oracle prices of Sturdy's LP-backed collateral reserves. While those inflated prices were live, the attacker opened WETH debt across five helper contracts, removed the collateral in the same transaction, unwound the pool distortions, repaid the flashloans, and finished with 442.117602378820516740 ETH of net profit.
The root cause is a protocol-side oracle design failure. Sturdy trusted same-transaction spot values from Curve.get_virtual_price() and Balancer.getRate() for both borrow-capacity checks and collateral-removal checks. That let an unprivileged adversary create WETH debt against transiently inflated LP collateral and then withdraw the collateral before the manipulated prices normalized.
2. Key Background
Sturdy listed LP-backed collateral through vault wrappers. The exploited reserves were csteCRV at 0x901247d08bebfd449526da92941b35d756873bcd and cB-stETH-STABLE at 0x10aa9eea35a3102cc47d4d93bc0ba9ae45557746. The lending pool was 0x9f72dc67cec672bb99e3d02cbea0a21536a2b657, and its oracle was 0xe5d78eb340627b8d5bcff63590ebec1ef9118c89.
The verified oracle sources show that Sturdy did not use a TWAP or delayed reference price for these LP reserves. Instead, the reserve oracle forwarded live adapter outputs, and those adapters directly consumed mutable pool state:
function getAssetPrice(address asset) public view override returns (uint256) {
address source = assetsSources[asset];
int256 price = IChainlinkAggregator(source).latestAnswer();
if (price > 0) {
return uint256(price);
}
return _fallbackOracle.getAssetPrice(asset);
}
function _get() internal view returns (uint256) {
uint256 minValue = Math.min(uint256(stETHPrice), 1e18);
return (ETHSTETH.get_virtual_price() * minValue) / 1e18;
}
function _get() internal view returns (uint256) {
uint256 minValue = Math.min(uint256(stETHPrice), 1e18);
return (BALWSTETHWETH.getRate() * minValue) / 1e18;
}
That design matters because both get_virtual_price() and getRate() are derived from current pool balances. An attacker who can borrow enough liquidity can move those values inside one transaction, use the inflated prices to satisfy Sturdy's checks, and then unwind before the transaction completes.
3. Vulnerability Analysis & Root Cause Summary
This incident is an ATTACK, not a passive MEV opportunity. The violated invariant is: collateral valuation used for debt creation and collateral-removal checks must remain manipulation-resistant during the transaction that consumes it. Sturdy broke that invariant by valuing LP-backed reserves from same-transaction spot functions on Curve and Balancer.
The vulnerable path is explicit in the lending-pool implementation. GenericLogic.calculateUserAccountData computes total collateral and total debt from IPriceOracleGetter(oracle).getAssetPrice(...). ValidationLogic.validateBorrow relies on that account data to approve borrows, and GenericLogic.balanceDecreaseAllowed recomputes health after a withdrawal using the same oracle path. Because Sturdy reused the same manipulable price source for both borrow validation and collateral-removal validation, the attacker could borrow against inflated collateral and then withdraw the collateral while the inflated prices were still in force.
The vault wrappers did not fix this. They changed how aTokens were minted and burned through reserve.getIndexFromPricePerShare(), but they still relied on the reserve asset oracle for credit checks. The result was a full same-transaction loop: distort LP state, deposit overvalued collateral, borrow WETH, disable collateral usage and withdraw, then repay the flashloan and keep the residual ETH.
4. Detailed Root Cause Analysis
The critical code path in the Sturdy lending-pool implementation is the direct use of oracle prices in both account-data calculation and collateral-decrease checks:
vars.reserveUnitPrice = IPriceOracleGetter(oracle).getAssetPrice(vars.currentReserveAddress);
vars.amountToDecreaseInETH =
(IPriceOracleGetter(oracle).getAssetPrice(asset) * amount) /
10**vars.decimals;
(
vars.userCollateralBalanceETH,
vars.userBorrowBalanceETH,
vars.currentLtv,
vars.currentLiquidationThreshold,
vars.healthFactor
) = GenericLogic.calculateUserAccountData(
userAddress,
reservesData,
userConfig,
reserves,
reservesCount,
oracle
);
The seed transaction trace shows that this was exactly the path exercised on-chain. A representative segment for the first helper contract shows the flashloan, both collateral deposits, the WETH borrow, and the collateral-removal sequence while the manipulated oracle calls were still happening:
Pool::flashLoan(... [wstETH, WETH], [50000000000000000000000, 60000000000000000000000], ...)
ConvexCurveLPVault2::depositCollateralFrom(..., 1000000000000000000000, 0x555003433d6a51e9cc7798752dbdd4ed28d61de5)
AuraBalancerLPVault::depositCollateralFrom(..., 233348773557117598739, 0x555003433d6a51e9cc7798752dbdd4ed28d61de5)
LendingPool::borrow(WETH9, 513367301825658717226, 2, 0, 0x555003433d6a51e9cc7798752dbdd4ed28d61de5)
SturdyOracle::getAssetPrice(csteCRV) -> ETHSTETHOracle::latestAnswer() -> Vyper_contract::get_virtual_price()
SturdyOracle::getAssetPrice(cB-stETH-STABLE) -> BALWSTETHWETHOracle::latestAnswer() -> MetaStablePool::getRate()
LendingPool::setUserUseReserveAsCollateral(csteCRV, false)
ConvexCurveLPVault2::withdrawCollateral(..., 1000000000000000000000, 10, 0x555003433d6a51e9cc7798752dbdd4ed28d61de5)
The same trace pattern repeats for all five helper contracts. Each helper borrows WETH after Sturdy reads the manipulated oracle values, then disables collateral usage and withdraws collateral while those same oracle adapters are still being queried inside the withdrawal-safety path. This is the concrete breakpoint: the protocol trusts prices that are manipulable by the very transaction being validated.
The post-incident state observations reinforce the same conclusion. All five helper addresses identified in the attack cluster had zero collateral and positive variableDebtWETH after the exploit path completed. That combination is only possible if Sturdy accepted the collateral value during borrowing and again during collateral removal, then finalized the transaction after the pool state normalized.
5. Adversary Flow Analysis
- The attacker funded a single public transaction from EOA
0x1e8419e724d51e87f78e222d935fbbdeb631a08bto orchestrator0x0b09c86260c12294e3b967f0d523b4b2bcdfbeab. - The orchestrator took public Aave flashloans of
50,000wstETHand60,000WETH, then distorted the Curve ETH/stETH and Balancer B-stETH-STABLE pools so that Sturdy's LP-collateral oracle adapters returned inflated prices. - The orchestrator funded five helper contracts:
0x555003433d6a51e9cc7798752dbdd4ed28d61de5,0xa6181b779dc2f93033e82584b692b3714b184163,0x89676bef260bbd56dbe97c81300d7a4f63d344d8,0x7750f94314a4fe3c78337dd8c9661864c4c77ad6, and0xf2ea7eacf7b042fd85690d71b9d2957830bdcf02. - Each helper deposited LP-backed collateral through the public Sturdy vault routes, borrowed WETH while the manipulated oracle prices were live, then removed the collateral in the same transaction.
- After all five helpers had been used, the attacker unwound the Curve and Balancer positions, repaid the flashloans plus fees, and transferred the remaining ETH profit back to the originating EOA.
The exploit required no privileged keys, no private orderflow, and no protocol-admin capability. Every component in the sequence was public and callable by an unprivileged actor.
6. Impact & Losses
The seed transaction minted 499.518233991704150000 WETH of new variable debt across the five helper contracts, measured directly from the transaction balance diff for variableDebtWETH token 0xe1ee5b789ba4ea5287999034fdf293361b53e024. That debt was left behind without matching collateral, creating protocol bad debt for Sturdy lenders.
The attacker EOA's native balance increased from 0.932308820528904726 ETH to 443.049911199349421466 ETH during the transaction, for a net gain of 442.117602378820516740 ETH. The recorded fee burden was 1.357731600829719304 ETH, consisting of gas plus a direct ETH transfer captured in the balance diff. The economic loss to the protocol therefore came from unbacked WETH debt, while the attacker realized profit in native ETH.
7. References
- Seed transaction:
0xeb87ebc0a18aca7d2a9ffcabf61aa69c9e8d3c6efade9e2303f8857717fb9eb7 - Block number:
17460610 - Attacker EOA:
0x1e8419e724d51e87f78e222d935fbbdeb631a08b - Orchestrator:
0x0b09c86260c12294e3b967f0d523b4b2bcdfbeab - Sturdy LendingPool:
0x9f72dc67cec672bb99e3d02cbea0a21536a2b657 - Sturdy Oracle:
0xe5d78eb340627b8d5bcff63590ebec1ef9118c89 - ETHSTETHOracle:
0xd783c7ff0666bb8245229946d595a819f5b21170 - BALWSTETHWETHOracle:
0xa49dff9fca35a7ede24f505c955424efc31645b5 - Curve collateral vault:
0xa36be47700c079bd94adc09f35b0fa93a55297bc - Aura collateral vault:
0x6ae5fd07c0bb2264b1f60b33f65920a2b912151c - Curve LP token:
0x06325440d014e39736583c165c2963ba99faf14e - Balancer B-stETH-STABLE pool:
0x32296969ef14eb0c6d29669c550d4a0449130230 - Evidence used by the validator: seed transaction trace, seed balance diff, live RPC reserve and helper-account observations, and verified source for the Sturdy oracle adapters and lending-pool implementation