Abracadabra Cauldron V4 repayForAll() Base/Elastic Desynchronization Borrow Inflation (2024-01-30)

  • Name: Abracadabra Cauldron V4 repayForAll() base/elastic desynchronization borrow inflation

  • Date: 2024-01-30

  • Protocol: Abracadabra Money / Cauldron V4

  • Chain: Ethereum

  • Category: lending-protocol accounting desynchronization / debt-rebase mismatch / helper-path invariant failure / bad-debt drain

  • Loss: about 6.5M of unbacked MIM, with the exploit also knocking MIM off peg

  • Summary: Abracadabra’s legacy Ethereum Cauldron V4 logic let repayForAll() zero out totalBorrow.elastic without also zeroing the matching totalBorrow.base. That left the market in an impossible debt state: aggregate debt amount was treated as 0, but aggregate debt shares were still nonzero. The attacker first used flash-loaned MIM to drive the cauldron into that broken state, then repaid outstanding borrowers and used a helper contract to loop tiny borrow() / repay() calls against fresh collateral. Because the base/elastic relationship was already corrupted, those tiny loops made later debt-part calculations economically meaningless. The attacker then borrowed essentially all remaining MIM in the cauldron while the protocol’s accounting no longer described a real liability state.

  • Trigger / entry point:

    • Legacy Cauldron V4 markets exposed repayForAll(uint128 amount, bool skim) as a public helper that proportionally repaid all borrowers.
    • Public analysis and replay PoCs show that this path could drive totalBorrow.elastic to 0 while leaving totalBorrow.base nonzero.
    • Once the debt rebase was in that impossible state, later borrow() / repay() flows still used the corrupted aggregate variables for debt-part accounting and solvency checks.
    • The attacker targeted an older Ethereum Cauldron V4 clone at 0x7259e152103756e1616A77Ae982353c3751A6a90, while Abracadabra later said borrow limits were set to zero across the affected older V4 cauldrons.
  • Root cause:

    • Abracadabra treated repayForAll() like a benign debt-management helper, but it could mutate one side of the debt rebase without preserving the invariant between aggregate amount (elastic) and aggregate shares (base).
    • The protocol implicitly relied on totalBorrow.elastic and totalBorrow.base staying synchronized enough that later debt-part math remained meaningful.
    • After that invariant broke, tiny follow-on borrows and repays no longer represented real borrower liability changes, yet the cauldron still let the attacker withdraw real MIM.
    • In short: a helper path corrupted the debt state machine, and the main borrow path trusted the corrupted state as if it were valid.
  • Exploit path:

    1. The attacker EOA 0x87f585809ce79ae39a5fa0c7c96d0d159eb678c9 and attack contract 0xE1091D17473b049CcCD65c54f71677Da85b77A45 targeted Cauldron V4 market 0x7259e152103756e1616A77Ae982353c3751A6a90 in main exploit transaction 0x26a83db7e28838dd9fee6fb7314ae58dcc6aee9a20bf224c386ff5e80f7e4cf2.
    2. Using DegenBox 0xd96f48665a1410C0cd669A88898ecA36B9Fc2cce, the attacker flash-loaned 300,000 MIM.
    3. They deposited part of that MIM into DegenBox, transferred 240,000 MIM directly to the Cauldron, and called repayForAll(240000e18, true).
    4. Public replay code shows the attacker then iterated through existing borrower addresses, repaying each borrow part until the market reached the critical broken state where totalBorrow.elastic == 0 while debt-share state remained nonzero.
    5. The attacker swapped some MIM through Curve, built yvCurve_3Crypto_f collateral, deposited it back through DegenBox, and deployed a helper contract to run repeated borrow(1) / repay(1) cycles against the corrupted debt accounting.
    6. Those tiny cycles inflated the borrow-part math enough that a later large borrow no longer reflected a real proportional liability increase.
    7. With the accounting already desynchronized, the attacker added the prepared collateral, borrowed essentially the cauldron’s remaining MIM balance, withdrew it from DegenBox, repaid the flash loan, and kept the surplus.
    8. The stolen MIM was then routed into Curve and Uniswap liquidity for exit into stronger assets, contributing to MIM’s temporary depeg.
  • What the attacker needed to be true:

    • repayForAll() had to be callable in a way that could zero market debt amount without also resetting the matching debt-share state.
    • The affected market still needed meaningful MIM liquidity to drain after the accounting state was corrupted.
    • Follow-on borrow() / repay() logic had to trust the corrupted totalBorrow rebase rather than rejecting or repairing the impossible state.
    • The attacker needed enough capital, via flash loan, to push the market through the repayForAll() transition and clean up legacy borrower positions into the exploitable state.
    • The protocol had to accept new collateral and borrowing after the desynchronization instead of pausing on impossible debt-book values.
  • Technical anatomy worth remembering:

    • This was not an oracle exploit and not a classic reentrancy issue. The theft primitive lived in Abracadabra’s own debt accounting.
    • The critical invariant was not merely debt should not go negative; it was aggregate debt amount and aggregate debt shares must remain mutually meaningful after helper-path mutations.
    • The exploit sits in the same general lending family as other accounting-desync attacks, but the distinctive preserved lesson is the debt-rebase mismatch: elastic could be cleared while base still poisoned future borrow math.
    • It is also a clean helper path stopped reusing the canonical accounting boundary case: a bulk debt-management wrapper was allowed to leave aggregate debt in a state the ordinary borrow path would never have authored, yet later user-facing borrow math still trusted that broken state as canonical truth.
    • The public replay PoC is especially useful because it preserves the exploit choreography in concrete terms: mass borrower cleanup, state forcing through repayForAll(), then tiny looped borrows/repays before the final drain.
    • Abracadabra’s own immediate response is also analytically important: the team emphasized that the bug lived in older V4 code and mitigated by forcing affected cauldron borrow limits to 0, which is strong evidence that the dangerous surface was market-specific helper logic rather than systemwide collateral pricing.
  • Representative transactions and artifacts:

    • Attacker EOA: 0x87f585809ce79ae39a5fa0c7c96d0d159eb678c9
    • Main attack contract: 0xE1091D17473b049CcCD65c54f71677Da85b77A45
    • Contract creator: 0x193E045BeE45C7573Ff89b12601C745AF739CE67
    • Vulnerable Cauldron V4 clone: 0x7259e152103756e1616A77Ae982353c3751A6a90
    • DegenBox: 0xd96f48665a1410C0cd669A88898ecA36B9Fc2cce
    • Main exploit transaction: 0x26a83db7e28838dd9fee6fb7314ae58dcc6aee9a20bf224c386ff5e80f7e4cf2
    • MIM token: 0x99D8a9C45b2ecA8864373A26D1459e3Dff1e17F3
  • Response / mitigation:

    • Abracadabra said the vulnerability affected certain older Cauldron V4 code paths, set borrow limits to 0 on affected cauldrons, and said user collateral was not directly at risk.
    • The DAO announced treasury support and later planned more detailed follow-up, while outside analysts published exploit reconstructions and PoCs.
    • The incident became a useful case study in why debt-management helper functions must preserve the same core accounting invariants as user-facing borrow and repay paths.

Why this incident matters

  • It preserves a lending exploit family that is easy to miss if reviews focus only on collateral-side solvency checks. Debt-side helper paths can be just as dangerous.
  • It adds a clear debt rebase corrupted, then tiny loops turn that corruption into a full pool drain case to the corpus.
  • It is also a reminder that legacy or older market code remains live attack surface as long as liquidity and callability remain live.

Pattern tags

  • zero-share borrow inflation
  • Solvency-check bypass
  • Helper-path invariant reuse and fail-closed discipline
  • The preserved shape is sharper than generic legacy market got hacked: repayForAll() was a production debt-ledger mutation path, not harmless maintenance, and it left elastic/base in a state later borrow math was never designed to price safely.
  • This is also a clean helper path authored a debt state the canonical borrow path would never have authored case. The exploit only became cash-outable because the cauldron kept treating that helper-authored state as borrowable truth instead of halting on impossible aggregate debt semantics.

Why the exploit worked

  • repayForAll() was allowed to mutate aggregate debt as if it were an administrative convenience function rather than a canonical solvency path. Once it could zero totalBorrow.elastic without retiring the matching base, the protocol had already lost the ledger truth that later borrow-part math depended on.
  • The broken state was not self-quarantining. Abracadabra left collateral addition, tiny borrow() / repay() loops, and later large borrows available after the helper-authored desynchronization instead of treating elastic == 0 && base > 0 as an impossible state that should freeze the market.
  • Tiny follow-on loops mattered because debt-share math was still trying to interpret a poisoned ratio. After repayForAll() broke the rebase, one-wei-scale borrow/repay cycles no longer represented real liability changes, but they still let the attacker shape later borrow-part calculations in their favor.
  • DegenBox and the cauldron integration externalized the accounting lie into real MIM immediately. This was not a cosmetic internal mismatch; the protocol still let the attacker add collateral, withdraw borrowed MIM, and repay the flash loan against the corrupted ledger.
  • The older V4 code detail matters because it preserves a recurring review failure: teams often treat legacy helpers as operational leftovers, but if liquidity and callability remain live then old helper semantics are still part of the active solvency perimeter.

Detection and audit pivots

  • Trace every path that can mutate aggregate debt outside plain borrow() / repay(), especially socialized repayment helpers, reserve-resolution helpers, bad-debt tooling, and emergency management functions. If any of them can author base/elastic combinations the main path would never create, they are solvency-critical code.
  • Add explicit impossible-state assertions around debt rebases. elastic == 0 && base > 0, or any equivalent amount says zero while shares say nonzero state, should halt borrowing and helper execution rather than becoming a new accounting baseline.
  • Fuzz tiny borrow/repay loops after helper-path mutations, not only normal user paths. The dangerous question is whether dust-scale calls can keep amplifying a state that was already made economically nonsensical by an earlier helper.
  • Review legacy and market-specific clones as first-class attack surface. If a protocol response says only older markets were affected, that is usually evidence that clone lineage, leftover helper semantics, and per-market borrow authority still deserve active auditing.
  • Follow the externalization path after the accounting break. Ask what lets the attacker convert poisoned debt math into assets that can leave the system now: flash loans, vault integrations, shared liquidity boxes, or protocol-owned stablecoin balances.

Failure-prevention checklist

  • Make aggregate debt-rebase coherence a hard invariant on every path, not just on borrow() and repay(): if a helper would leave totalBorrow.base and totalBorrow.elastic no longer describing the same market, revert.
  • Treat repayForAll()-style socialized repayment helpers as solvency code. They should reuse the same debt-accounting core as ordinary debt retirement instead of open-coding a cheaper aggregate shortcut.
  • Fail closed on impossible debt-book states such as elastic == 0 && base > 0, or any equivalent amount says zero while shares remain live combination. Once the ledger reaches that state, collateral addition and new borrowing should stop immediately.
  • Require helper-path postconditions, not just successful execution. A bulk repayment path should prove that later debt-part math will still price new borrows meaningfully before it returns.
  • Fuzz and invariant-test legacy clones at dust states: base == 1, elastic == 1, post-helper near-zero debt, and tiny follow-on borrow() / repay() loops after socialized repayment.
  • Put blast-radius controls under older markets anyway: per-cauldron borrow limits, fast market-specific pause paths, and isolated liquidity should stop one legacy debt-ledger bug from becoming protocolwide bad debt.
  • Review DegenBox-style shared-liquidity integrations as externalization rails. If local debt math breaks, shared vault liquidity turns that accounting lie into immediately withdrawable stablecoins.
  • Treat older V4 code as a security diagnosis, not a comfort signal. If a helper remains callable and the market still holds liquidity, the old code is still inside the live solvency perimeter.

Audit prompts

  • Can repayForAll() or any similar helper mutate one side of the debt rebase without mutating the other side proportionally?
  • What exact invariant proves that totalBorrow.base and totalBorrow.elastic still describe the same liability set after bulk repayment, bad-debt handling, reserve resolution, or emergency debt tooling?
  • If aggregate debt amount reaches zero, what code guarantees that the corresponding debt-share state is also extinguished before any further borrow becomes possible?
  • Are there dust-scale follow-on paths — borrow(1), repay(1), collateral top-ups, helper multicalls — that become economically dangerous only after a helper-authored desynchronization has already happened?
  • Which legacy clones or market-specific variants still expose helper semantics that newer deployments no longer rely on, and are those clones still liquid enough to matter?
  • What downstream module or liquidity box turns a corrupted local debt ledger into real assets that can leave the system immediately?
  • Does the protocol monitor impossible debt-book states or only user-facing symptoms like depegs and large withdrawals?
  • If a helper cannot preserve canonical debt invariants in one edge case, does the market freeze automatically or does it keep lending against the corrupted state?

Lessons that should already be standard

  • Debt-side helpers are not accounting housekeeping. If they can mutate aggregate borrower state, they are as security-critical as borrow() itself.
  • Amount zero, shares nonzero is not a quirky corner case. It is the ledger admitting it no longer knows what borrower liability means.
  • Tiny loops only become lethal because a bigger invariant already broke first. Reviewers should look for the earlier state corruption, not only the final borrow(1) choreography.
  • Legacy markets are not safer because the current product narrative moved on. If liquidity and callability remain live, old helper semantics are still live attack surface.
  • Shared-liquidity vaults amplify local accounting bugs. A broken market ledger becomes a protocol-level stablecoin event once bad debt can be withdrawn through a common box.
  • Bulk socialization functions need stricter, not looser, invariant discipline. A helper that touches everyone’s debt at once deserves more paranoia than an ordinary single-user repay path.

Internal linkages

Control surface