All incidents

Curve crvUSD sDOLA Market In-Tx Oracle Refresh Liquidation Attack

Share
Mar 02, 2026 03:00 UTCAttackLoss: 227,325.57 DOLA, 38,937.27 sDOLAManually checked1 exploit txWindow: Atomic
Estimated Impact
227,325.57 DOLA, 38,937.27 sDOLA
Label
Attack
Exploit Tx
1
Addresses
3
Attack Window
Atomic
Mar 02, 2026 03:00 UTC → Mar 02, 2026 03:00 UTC

Exploit Transactions

TX 1Ethereum
0xb93506af8f1a39f6a31e2d34f5f6a262c2799fef6e338640f42ab8737ed3d8a4
Mar 02, 2026 03:00 UTCExplorer

Victim Addresses

0xad444663c6c92b497225c6ce65fee2e7f78bfb86Ethereum
0x0079885e248b572cdc4559a8b156745e2d8ea1f7Ethereum
0x966cbdecefb60a289b0460f7638f4a75f432ca06Ethereum

Loss Breakdown

227,325.57DOLA
38,937.27sDOLA

Similar Incidents

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 0x966cbdecefb60a289b0460f7638f4a75f432ca06 and 0x18672b1b0c623a30089a280ed9256379fb0e4e62
  • Liquidation guard in Controller is health-based:
    • users_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.
  • LLAMMA oracle write path is caller-reachable via exchange:
    • _exchange(...) calls _price_oracle_w() first.
    • It then returns [0,0] if amount == 0.
  • Oracle contracts implement writable price_w() methods that update state such as last_timestamp, last_tvl, and last_price in-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

  1. Pre-state at block 24566936 (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}
  ]
}
  1. 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]
  1. 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
  1. Exploit trace ordering The focused trace shows attacker path reaches 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]
  1. 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({...}))
  1. Observed post-trigger effect 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"}
  ]
}
  1. State-change corroboration LLAMMA storage slots around incident changed across blocks (slot12 and slot13), consistent with in-tx oracle state update effects.

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

5. Adversary Flow Analysis

  • Adversary cluster
    • EOA: 0x33a0aab2642c78729873786e5903cc30f9a94be2
    • Orchestrator: 0xd8e8544e0c808641b9b89dfb285b5655bd5b6982
    • Helper liquidator: 0xc6c2fcdf688baeb7b03d9d9c088c183dbb499ac0
  • Stage 1: Preparation
    • 0x813bb6f5d23811cad0d0ff6440313550a36856a07ff55c7ffaea9b18940eaf97 deploys/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.
  • 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.

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: 227325565940517368498878
    • sDOLA: 38937269897759257032249
  • Supplementary DOLA-equivalent accounting
    • value_before_in_reference_asset = 0
    • value_after_in_reference_asset = 280010273000322789779744 (DOLA-wei)
    • value_delta_in_reference_asset = 280010273000322789779744 (DOLA-wei)
    • ETH gas fee paid by sender: 23556974000000000 wei.

7. References

  1. Seed tx metadata: 0xb93506af8f1a39f6a31e2d34f5f6a262c2799fef6e338640f42ab8737ed3d8a4.
  2. Focused stake->exchange->users_to_liquidate trace (shows call order and [0,0] zero-exchange return).
  3. Pre-block health snapshot (health_full_true/false) at block 24566936.
  4. users_to_liquidate decoded return tuples (count=27, negative health values).
  5. Decoded Liquidate events (27 events, helper liquidator 0xc6c2...).
  6. Controller source (_health, users_to_liquidate, _liquidate, liquidate).
  7. LLAMMA source (_exchange, _price_oracle_w).
  8. Oracle source 0x966c... (price_w).
  9. Stable aggregator source 0x1867... (price_w).
  10. LLAMMA storage slots (12, 13) around incident blocks (24566936 -> 24566937).
  11. Seed transaction balance diff (native + ERC20 deltas, including DOLA/sDOLA recipient deltas and sender gas delta).
  12. Related tx set from analysis context:
  • 0x813bb6f5d23811cad0d0ff6440313550a36856a07ff55c7ffaea9b18940eaf97
  • 0xb93506af8f1a39f6a31e2d34f5f6a262c2799fef6e338640f42ab8737ed3d8a4
  • 0x8378e263995565bc3e7d42ff129c7e3148015f7b7ecb0b73502906c8bf651da4
  • 0x594f589235b35531e9b6cb67e09d892861454422c9e83f323b29a40e39bf777e