Skip to main content
This case study explains how the Abracadabra CauldronV4 cook function exploit worked and which assertion pattern could have constrained the invalid state transition.

What Happened

A logic flaw in Abracadabra’s cook() function allowed an attacker to borrow $1.8M in MIM tokens without posting any collateral. The Bug: The CauldronV4 contract’s cook() function executes multiple actions in sequence while tracking a CookStatus struct. This struct contains a flag indicating whether solvency checks are required.
  • Action 5 (borrow) sets the solvency check flag to true
  • Action 0 calls _additionalCookAction(), which returns a fresh CookStatus with all fields set to false
By chaining [Action 5, Action 0], the borrow executes and marks that a solvency check is needed. But Action 0 immediately clears this flag before any validation runs. Exploitation:
  1. Call cook() with [Action 5 (borrow), Action 0]
  2. Borrow MIM tokens without collateral
  3. Repeat across six cauldron contracts
  4. Total stolen: 1,793,755 MIM (~$1.8M)

Assertion Pattern

This exploit could have been prevented by enforcing a fundamental invariant: users cannot borrow without sufficient collateral. The beauty of this approach is that it doesn’t require understanding the exploit’s specific mechanism. Developers don’t need to know about:
  • The CookStatus struct and how it tracks solvency checks
  • The _additionalCookAction() helper function that resets flags
  • The specific action sequence [Action 5, Action 0] that triggers the bug
  • How the internal solvency check logic works
Instead, an assertion simply states the protocol’s core rule: after any transaction, if a user’s borrow increased, their collateral must cover the debt. This works regardless of which code path led to the borrow or what internal checks were bypassed. Why This Matters: Complex protocols like Abracadabra have many functions and execution paths. The cook() function alone can execute dozens of different action combinations. Trying to secure each path individually is error-prone - as this exploit demonstrates, one forgotten check or edge case can drain millions. Assertions flip this model. Rather than validating each execution path, you validate the outcome. The protocol can have 100 different ways to borrow, but only one invariant to check: collateral >= required collateral. Implementation:
How It Works: The assertion triggers on every cook() call and:
  1. Identifies the caller using getCallInputs
  2. Compares their borrow amount before (forkPreTx) and after (forkPostTx) the transaction
  3. If borrowing increased, calculates their collateral value using oracle prices
  4. Validates that collateral meets the required threshold for their debt
  5. Reverts if insufficient collateral is detected
This approach validates the outcome rather than the execution path:
  • Without assertion: Developers must remember to add solvency checks in every function that modifies debt, handle complex state transitions, and guard against flag manipulation
  • With assertion: Define the invariant once. It catches any bug that results in undercollateralized positions, whether from flag manipulation, reentrancy, rounding errors, or future code changes