We do not have a reliable USD price for the recorded assets yet.
0xb93506af8f1a39f6a31e2d34f5f6a262c2799fef6e338640f42ab8737ed3d8a40xad444663c6c92b497225c6ce65fee2e7f78bfb86Ethereum0x0079885e248b572cdc4559a8b156745e2d8ea1f7Ethereum0x966cbdecefb60a289b0460f7638f4a75f432ca06EthereumAn 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.
Controller (0xad444663c6c92b497225c6ce65fee2e7f78bfb86)LLAMMA (0x0079885e248b572cdc4559a8b156745e2d8ea1f7)0x966cbdecefb60a289b0460f7638f4a75f432ca06 and 0x18672b1b0c623a30089a280ed9256379fb0e4e62users_to_liquidate() computes _health(user, debt, True, ...) and includes users where health < 0._liquidate(...) asserts _health(user, debt, True, health_limit) < 0 when not self-liquidating._exchange(...) calls _price_oracle_w() first.[0,0] if amount == 0.price_w() methods that update state such as last_timestamp, last_tvl, and last_price in-transaction.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.
sigma_B)
All 27 later-liquidated users were healthy when evaluated with full=true.{
"block": 24566936,
"users": [
{"user":"0x145e305a6e8979cbefcb75993f7ae5270856c1d2","health_full_true":73765408062853040},
{"user":"0x21ab0875611da0235bc5b6405b8a08268d859700","health_full_true":63530956274070320},
{"user":"0x2b083a0aa6b808a31e9ac749772a285f5cd34fbe","health_full_true":178440128037523520}
]
}
p_o: uint256[2] = self._price_oracle_w() # Let's update the oracle even if we exchange 0
if amount == 0:
return [0, 0]
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
DolaSavings::stake, then LLAMMA::exchange(0,1,0,1), then Controller::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]
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({...}))
users_to_liquidate() returned 27 tuples with negative health, and 27 Liquidate events were emitted by helper liquidator 0xc6c2fcdf688baeb7b03d9d9c088c183dbb499ac0.{
"count": 27,
"tuples": [
{"user":"0x2b083a0aa6b808a31e9ac749772a285f5cd34fbe","health":"-185624752928262771"},
{"user":"0xcbcc2b2ecd195ebef03fcb7c7564e4e906485a14","health":"-311490290847575722"}
]
}
State-change corroboration
LLAMMA storage slots around incident changed across blocks (slot12 and slot13), consistent with in-tx oracle state update effects.
Deterministic value accounting (supplementary to non-monetary predicate)
From collector balance diffs: +227325565940517368498878 DOLA (to 0xd8...) and +38937269897759257032249 sDOLA (to 0xc6c2...). Using on-chain sDOLA.convertToAssets(1e18)=1353066283233106054, sDOLA value is 52684707059805421280866 DOLA-wei, total measured cluster value 280010273000322789779744 DOLA-wei. Sender gas outflow is 23556974000000000 wei ETH.
0x33a0aab2642c78729873786e5903cc30f9a94be20xd8e8544e0c808641b9b89dfb285b5655bd5b69820xc6c2fcdf688baeb7b03d9d9c088c183dbb499ac00x813bb6f5d23811cad0d0ff6440313550a36856a07ff55c7ffaea9b18940eaf97 deploys/sets up orchestrator before exploit block.0xb93506...d8a4, attacker flow performs stake + LLAMMA exchange path, including zero-amount exchange that still refreshes oracle-linked state.users_to_liquidate() and performs 27 hard liquidations.DOLA: 227325565940517368498878sDOLA: 38937269897759257032249value_before_in_reference_asset = 0value_after_in_reference_asset = 280010273000322789779744 (DOLA-wei)value_delta_in_reference_asset = 280010273000322789779744 (DOLA-wei)23556974000000000 wei.0xb93506af8f1a39f6a31e2d34f5f6a262c2799fef6e338640f42ab8737ed3d8a4.[0,0] zero-exchange return).health_full_true/false) at block 24566936.users_to_liquidate decoded return tuples (count=27, negative health values).Liquidate events (27 events, helper liquidator 0xc6c2...)._health, users_to_liquidate, _liquidate, liquidate)._exchange, _price_oracle_w).0x966c... (price_w).0x1867... (price_w).12, 13) around incident blocks (24566936 -> 24566937).0x813bb6f5d23811cad0d0ff6440313550a36856a07ff55c7ffaea9b18940eaf970xb93506af8f1a39f6a31e2d34f5f6a262c2799fef6e338640f42ab8737ed3d8a40x8378e263995565bc3e7d42ff129c7e3148015f7b7ecb0b73502906c8bf651da40x594f589235b35531e9b6cb67e09d892861454422c9e83f323b29a40e39bf777e