Description
A logic flaw in Abracadabra’scook()
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 freshCookStatus
with all fields set tofalse
- Call
cook()
with [Action 5 (borrow), Action 0] - Borrow MIM tokens without collateral
- Repeat across six cauldron contracts
- Total stolen: 1,793,755 MIM (~$1.8M)
Proposed Solution
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
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:
cook()
call and:
- Identifies the caller using
getCallInputs
- Compares their borrow amount before (
forkPreTx
) and after (forkPostTx
) the transaction - If borrowing increased, calculates their collateral value using oracle prices
- Validates that collateral meets the required threshold for their debt
- Reverts if insufficient collateral is detected
- 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