Root Cause
The vulnerability lives in the cook() multi-action entrypoint of PrivilegedCauldronV4 and PrivilegedCheckpointCauldronV4 contracts. When ACTION_BORROW (action code 5) runs, it sets an internal needsSolvencyCheck flag to true. However, an unhandled else branch — triggered by any other action code, including the no-op ACTION_0 — calls the empty _additionalCookAction() override and then unconditionally resets needsSolvencyCheck back to false. When the attacker passed actions = [5, 0], the borrow executed and the flag was immediately cleared before the final solvency check.
// Simplified vulnerable logic inside cook()
function cook(
uint8[] calldata actions,
uint256[] calldata values,
bytes[] calldata datas
) external payable {
bool needsSolvencyCheck;
for (uint256 i = 0; i < actions.length; i++) {
uint8 action = actions[i];
if (action == ACTION_BORROW) {
_borrow(msg.sender, abi.decode(datas[i], (int256)));
needsSolvencyCheck = true;
} else {
// _additionalCookAction is empty in the deprecated fork;
// the else-branch then falls through and resets the flag:
_additionalCookAction(action, values[i], datas[i]);
needsSolvencyCheck = false; // <-- unintended reset
}
}
if (needsSolvencyCheck) {
_checkUserSolvency(msg.sender); // never reached
}
}
Attack Steps
| Step | Action |
|---|---|
| 1 | Identify six deprecated CauldronV4 contracts still live on Ethereum mainnet |
| 2 | Call cook([5, 0], ...) on each cauldron |
| 3 | ACTION_BORROW (5) executes an unsecured borrow, sets needsSolvencyCheck = true |
| 4 | ACTION_0 triggers the empty _additionalCookAction() override, resets flag to false |
| 5 | Loop ends; solvency check is skipped entirely |
| 6 | Repeat across all six cauldron instances, accumulating 1,724,494 MIM |
| 7 | Swap MIM → DAI → USDC → WETH → ~395 ETH and exit |
Impact
Approximately 1,724,494 MIM (~$1.8M USD) was drained across six deprecated Cauldron contracts on Ethereum mainnet. Aave-linked collateral held in active cauldrons was unaffected; only the wind-down instances were exploited. The attacker netted roughly 395 ETH after swapping.
Lessons for Auditors
- Multi-action entrypoints with accumulator flags are a critical review target. Any action sequence that can reset a "check required" sentinel mid-loop defeats the protection for all preceding operations.
- Deprecated contracts must be formally disabled. Contracts marked deprecated but left live and unpaused carry the same exploit surface as active ones. Audit wind-down procedures and enforce disabling or self-destruct (or at minimum a global pause).
- Fork audits must diff every override. This flaw was introduced (or reintroduced) in a fork of an earlier Cauldron variant. Empty virtual function overrides (
_additionalCookAction) can silently change flag-accumulation semantics; trace every override path through the else-branch logic. - Audit unaudited legacy code — the affected cauldrons had not been reviewed since November 2023 yet remained callable and held real user collateral.