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 outtotalBorrow.elasticwithout also zeroing the matchingtotalBorrow.base. That left the market in an impossible debt state: aggregate debt amount was treated as0, 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 tinyborrow()/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.elasticto0while leavingtotalBorrow.basenonzero. - 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.
- Legacy Cauldron V4 markets exposed
-
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.elasticandtotalBorrow.basestaying 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.
- Abracadabra treated
-
Exploit path:
- The attacker EOA
0x87f585809ce79ae39a5fa0c7c96d0d159eb678c9and attack contract0xE1091D17473b049CcCD65c54f71677Da85b77A45targeted Cauldron V4 market0x7259e152103756e1616A77Ae982353c3751A6a90in main exploit transaction0x26a83db7e28838dd9fee6fb7314ae58dcc6aee9a20bf224c386ff5e80f7e4cf2. - Using DegenBox
0xd96f48665a1410C0cd669A88898ecA36B9Fc2cce, the attacker flash-loaned300,000 MIM. - They deposited part of that MIM into DegenBox, transferred
240,000 MIMdirectly to the Cauldron, and calledrepayForAll(240000e18, true). - 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 == 0while debt-share state remained nonzero. - The attacker swapped some MIM through Curve, built
yvCurve_3Crypto_fcollateral, deposited it back through DegenBox, and deployed a helper contract to run repeatedborrow(1)/repay(1)cycles against the corrupted debt accounting. - Those tiny cycles inflated the borrow-part math enough that a later large borrow no longer reflected a real proportional liability increase.
- 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.
- The stolen MIM was then routed into Curve and Uniswap liquidity for exit into stronger assets, contributing to MIM’s temporary depeg.
- The attacker EOA
-
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 corruptedtotalBorrowrebase 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 wasaggregate 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:
elasticcould be cleared whilebasestill poisoned future borrow math. - It is also a clean
helper path stopped reusing the canonical accounting boundarycase: 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
- Attacker EOA:
-
Response / mitigation:
- Abracadabra said the vulnerability affected certain older Cauldron V4 code paths, set borrow limits to
0on 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.
- Abracadabra said the vulnerability affected certain older Cauldron V4 code paths, set borrow limits to
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 draincase to the corpus. - It is also a reminder that
legacyoroldermarket 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 leftelastic/basein 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 authoredcase. 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 zerototalBorrow.elasticwithout retiring the matchingbase, 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 treatingelastic == 0 && base > 0as 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 codedetail 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 authorbase/elasticcombinations 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 equivalentamount says zero while shares say nonzerostate, 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()andrepay(): if a helper would leavetotalBorrow.baseandtotalBorrow.elasticno 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 equivalentamount says zero while shares remain livecombination. 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-onborrow()/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 codeas 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.baseandtotalBorrow.elasticstill 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 nonzerois 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
- Closest recurring pattern: debt-share-desynchronization-zero-share-borrow-inflation.
- Broader lending umbrella: solvency-check-bypass.
- Sharper helper-path design companion: helper-path-invariant-reuse-and-fail-closed-discipline —
repayForAll()should have preserved the same aggregate debt invariants laterborrow()/repay()logic relied on, or else failed closed once it could not do so. - Adjacent borrower-side empty-state exemplar: alpha-homora-v2-hidden-susd-pool-zero-share-debt-inflation-2021-02-13 — useful when the sharper contrast is
public reserve helper inflated debt amount without debt shares in an empty borrower marketversus Abracadabra’sbulk repayment helper corrupted an already-live debt rebase.
Control surface
-
The leverage sat in
repayForAll(), thetotalBorrowrebase (elasticversusbase), DegenBox-assisted market funding, and the borrow/repay math that continued trusting an impossible aggregate debt state. -
Abracadabra did not fail because the market moved. It failed because a helper path broke the debt ledger and the rest of the cauldron kept lending against the broken ledger.
-
Sources:
- https://paragraph.com/@abracadabra-money/resilience-rising-abracadabra-dao-next-steps-full-speed-ahead
- https://dev.abracadabra.money/core-contracts/cauldrons/cauldron-v4
- https://extropy-io.medium.com/breaking-down-the-6-5m-abracadabra-money-hack-a-detailed-analysis-of-the-cauldron-v4-exploit-44100da10896
- https://raw.githubusercontent.com/SunWeb3Sec/DeFiHackLabs/main/src/test/2024-01/MIMSpell2_exp.sol
- https://app.blocksec.com/explorer/tx/eth/0x26a83db7e28838dd9fee6fb7314ae58dcc6aee9a20bf224c386ff5e80f7e4cf2
-
Last reviewed: 2026-07-26 UTC