Curve crvUSD sDOLA Market In-Tx Oracle Refresh Liquidation Attack
Exploit Transactions
0xb93506af8f1a39f6a31e2d34f5f6a262c2799fef6e338640f42ab8737ed3d8a4Victim Addresses
0xad444663c6c92b497225c6ce65fee2e7f78bfb86Ethereum0x0079885e248b572cdc4559a8b156745e2d8ea1f7Ethereum0x966cbdecefb60a289b0460f7638f4a75f432ca06EthereumLoss Breakdown
Similar Incidents
Ploutos Market Oracle Feed Misconfiguration Enabled Undercollateralized WETH Borrow
33%bZx/Fulcrum WBTC market manipulation drains ETH liquidity
31%OMPxContract bonding-curve loop exploit drains ETH reserves
30%Euler DAI Reserve Donation
28%Aave AMM LP Oracle Manipulation Through Delegated Recursive LP Looping
28%bZx/Fulcrum iETH oracle manipulation enables undercollateralized WETH borrowing
27%Root Cause Analysis
Curve crvUSD sDOLA Market In-Tx Oracle Refresh Liquidation Attack
1. Incident Overview TL;DR
An unprivileged adversary executed a single Ethereum mainnet transaction (0xb93506af8f1a39f6a31e2d34f5f6a262c2799fef6e338640f42ab8737ed3d8a4, block 24566937) that refreshed LLAMMA oracle state in-transaction and then liquidated 27 borrowers in the same transaction.
The root cause is that liquidation eligibility in the crvUSD Controller depends on oracle-influenced health(..., full=true), while LLAMMA allows any caller to trigger _price_oracle_w() through exchange(0,1,0,1). Because _price_oracle_w() is executed before the zero-amount early return, a permissionless caller can atomically steer oracle state and then immediately consume that state in users_to_liquidate() / liquidate() checks.
Primary ACT success predicate is non-monetary and is satisfied: users healthy at pre-state sigma_B become liquidatable (health < 0) and are hard-liquidated in the same attacker-crafted transaction.
2. Key Background
- Victim protocol components:
Controller(0xad444663c6c92b497225c6ce65fee2e7f78bfb86)LLAMMA(0x0079885e248b572cdc4559a8b156745e2d8ea1f7)- Oracle path contracts including
0x966cbdecefb60a289b0460f7638f4a75f432ca06and0x18672b1b0c623a30089a280ed9256379fb0e4e62
- Liquidation guard in Controller is health-based:
users_to_liquidate()computes_health(user, debt, True, ...)and includes users wherehealth < 0._liquidate(...)asserts_health(user, debt, True, health_limit) < 0when not self-liquidating.
- LLAMMA oracle write path is caller-reachable via exchange:
_exchange(...)calls_price_oracle_w()first.- It then returns
[0,0]ifamount == 0.
- Oracle contracts implement writable
price_w()methods that update state such aslast_timestamp,last_tvl, andlast_pricein-transaction.
3. Vulnerability Analysis & Root Cause Summary
This is an ATTACK-class issue caused by compositional coupling between liquidation checks and a permissionless in-transaction oracle write path. The vulnerable property is not that liquidations exist, but that liquidation eligibility can be attacker-steered within the same transaction before checks execute. In LLAMMA, _exchange updates oracle state via _price_oracle_w() even for zero-amount calls and only then exits with [0,0]. In Controller, both users_to_liquidate() and liquidate() consume _health(..., full=true) and gate behavior on health < 0. The combination permits a same-tx state transition from healthy to liquidatable without requiring privileged access. Trace and state evidence show this transition occurred and was immediately followed by a 27-user liquidation sweep. The explicit safety invariant violated is: positions healthy in sigma_B should not become hard-liquidatable solely via attacker-controlled same-tx oracle refresh sequencing.
4. Detailed Root Cause Analysis
- Pre-state at block 24566936 (
sigma_B) All 27 later-liquidated users were healthy when evaluated withfull=true.
{
"block": 24566936,
"users": [
{"user":"0x145e305a6e8979cbefcb75993f7ae5270856c1d2","health_full_true":73765408062853040},
{"user":"0x21ab0875611da0235bc5b6405b8a08268d859700","health_full_true":63530956274070320},
{"user":"0x2b083a0aa6b808a31e9ac749772a285f5cd34fbe","health_full_true":178440128037523520}
]
}
- Code-level breakpoint in LLAMMA LLAMMA executes oracle write before zero-amount exit:
p_o: uint256[2] = self._price_oracle_w() # Let's update the oracle even if we exchange 0
if amount == 0:
return [0, 0]
- Oracle write path is stateful
Representative oracle excerpts show
price_w()mutates oracle state in-tx:
@external
def price_w() -> uint256:
tvls: uint256[N_POOLS] = self._ema_tvl()
if self.last_timestamp < block.timestamp:
self.last_timestamp = block.timestamp
self.last_tvl = tvls
return self._raw_price(tvls, STABLESWAP_AGGREGATOR.price_w())
@external
def price_w() -> uint256:
if self.last_timestamp == block.timestamp:
return self.last_price
else:
ema_tvl: DynArray[uint256, MAX_PAIRS] = self._ema_tvl()
self.last_timestamp = block.timestamp
...
self.last_price = p
return p
- Exploit trace ordering
The focused trace shows attacker path reaches
DolaSavings::stake, thenLLAMMA::exchange(0,1,0,1), thenController::users_to_liquidate()in one transaction.
... DolaSavings::stake(...)
... LLAMMA - crvUSD AMM::exchange(0, 1, 0, 1)
... <- [Return] [0, 0]
... crvUSD Controller::users_to_liquidate() [staticcall]
- Controller consumption of refreshed state
Controller liquidation logic consumes
health(..., True, ...):
if health_limit != 0:
assert self._health(user, debt, True, health_limit) < 0, "Not enough rekt"
health: int256 = self._health(user, debt, True, self.liquidation_discounts[user])
if health < 0:
out.append(Position({...}))
- Observed post-trigger effect
users_to_liquidate()returned 27 tuples with negative health, and 27Liquidateevents were emitted by helper liquidator0xc6c2fcdf688baeb7b03d9d9c088c183dbb499ac0.
{
"count": 27,
"tuples": [
{"user":"0x2b083a0aa6b808a31e9ac749772a285f5cd34fbe","health":"-185624752928262771"},
{"user":"0xcbcc2b2ecd195ebef03fcb7c7564e4e906485a14","health":"-311490290847575722"}
]
}
-
State-change corroboration LLAMMA storage slots around incident changed across blocks (
slot12andslot13), consistent with in-tx oracle state update effects. -
Deterministic value accounting (supplementary to non-monetary predicate) From collector balance diffs:
+227325565940517368498878DOLA (to0xd8...) and+38937269897759257032249sDOLA (to0xc6c2...). Using on-chainsDOLA.convertToAssets(1e18)=1353066283233106054, sDOLA value is52684707059805421280866DOLA-wei, total measured cluster value280010273000322789779744DOLA-wei. Sender gas outflow is23556974000000000wei ETH.
5. Adversary Flow Analysis
- Adversary cluster
- EOA:
0x33a0aab2642c78729873786e5903cc30f9a94be2 - Orchestrator:
0xd8e8544e0c808641b9b89dfb285b5655bd5b6982 - Helper liquidator:
0xc6c2fcdf688baeb7b03d9d9c088c183dbb499ac0
- EOA:
- Stage 1: Preparation
0x813bb6f5d23811cad0d0ff6440313550a36856a07ff55c7ffaea9b18940eaf97deploys/sets up orchestrator before exploit block.
- Stage 2: In-tx oracle steering
- In exploit tx
0xb93506...d8a4, attacker flow performs stake + LLAMMA exchange path, including zero-amount exchange that still refreshes oracle-linked state.
- In exploit tx
- Stage 3: Liquidation sweep and settlement
- Same transaction calls
users_to_liquidate()and performs 27 hard liquidations. - Borrower states are cleared and value is realized to adversary-controlled contracts before flashloan repayment.
- Same transaction calls
6. Impact & Losses
- Protocol/user impact
- 27 borrower positions became hard-liquidatable intra-transaction and were liquidated.
- This demonstrates deterministic liquidation harm under attacker-controlled oracle-refresh sequencing.
- Observed token flows to adversary cluster (seed tx)
DOLA:227325565940517368498878sDOLA:38937269897759257032249
- Supplementary DOLA-equivalent accounting
value_before_in_reference_asset = 0value_after_in_reference_asset = 280010273000322789779744(DOLA-wei)value_delta_in_reference_asset = 280010273000322789779744(DOLA-wei)- ETH gas fee paid by sender:
23556974000000000wei.
7. References
- Seed tx metadata:
0xb93506af8f1a39f6a31e2d34f5f6a262c2799fef6e338640f42ab8737ed3d8a4. - Focused stake->exchange->users_to_liquidate trace (shows call order and
[0,0]zero-exchange return). - Pre-block health snapshot (
health_full_true/false) at block24566936. users_to_liquidatedecoded return tuples (count=27, negative health values).- Decoded
Liquidateevents (27events, helper liquidator0xc6c2...). - Controller source (
_health,users_to_liquidate,_liquidate,liquidate). - LLAMMA source (
_exchange,_price_oracle_w). - Oracle source
0x966c...(price_w). - Stable aggregator source
0x1867...(price_w). - LLAMMA storage slots (
12,13) around incident blocks (24566936->24566937). - Seed transaction balance diff (native + ERC20 deltas, including DOLA/sDOLA recipient deltas and sender gas delta).
- Related tx set from analysis context:
0x813bb6f5d23811cad0d0ff6440313550a36856a07ff55c7ffaea9b18940eaf970xb93506af8f1a39f6a31e2d34f5f6a262c2799fef6e338640f42ab8737ed3d8a40x8378e263995565bc3e7d42ff129c7e3148015f7b7ecb0b73502906c8bf651da40x594f589235b35531e9b6cb67e09d892861454422c9e83f323b29a40e39bf777e