Calculated from recorded token losses using historical USD prices at the incident time.
0x842aae91c89a9e5043e64af34f53dc66daf0f033ad8afbf35ef0c93f99a9e5e60xd96f48665a1410c0cd669a88898eca36b9fc2cceEthereum0x46f54d434063e5f1a2b2cc6d9aaa657b1b9ff82cEthereum0x289424add4a1a503870eb475fd8bf1d586b134edEthereumOn Ethereum mainnet at block 23504546, an unprivileged adversary executed a single, attacker-crafted transaction (0x842aae91c89a9e5043e64af34f53dc66daf0f033ad8afbf35ef0c93f99a9e5e6) that deployed an ephemeral helper contract and immediately used it to exploit a logic flaw in two Abracadabra CauldronV4 markets integrated with the DegenBox vault.
Within this transaction, the helper contract invoked CauldronV4.cook on two DegenBox-backed Cauldron clones, borrowed essentially all available MIM from each without posting any collateral, withdrew the borrowed MIM from DegenBox, and routed the funds through Curve and Uniswap V3 into WETH. WETH9 then sent the ETH proceeds to the helper, which self-destructed and forwarded the ETH back to the creator EOA.
Native balance diffs for the adversary EOA 0x1aaade3e9062d124b7deb0ed6ddc7055efa7354d show its ETH balance increasing from 173504352583541000 wei to 395232980108896905054 wei, a net gain of 395059475756313364054 wei (approximately 395.0595 ETH), while paying only 124347733527784 wei (0.0001243 ETH) in gas for the exploit transaction. DegenBox’s on-chain MIM balance decreased by roughly 1,793,766.1335 MIM, which was moved into the Curve MIM-3CRV pool and ultimately converted into the adversary’s ETH profit.
The root cause is a solvency-check bypass in CauldronV4.cook. After an ACTION_BORROW step sets a CookStatus.needsSolvencyCheck flag and increases the borrower’s userBorrowPart, a subsequent custom action routed through returns a default-initialized struct. Assigning clears back to false. As a result, the final is skipped even when and , allowing fully uncollateralized borrowing of MIM from the affected Cauldrons.
_additionalCookActionCookStatusstatus = returnStatusneedsSolvencyCheckrequire(_isSolvent(msg.sender, _exchangeRate))userBorrowPart > 0userCollateralShare == 0Abracadabra’s DegenBox is a BentoBox-style vault that tracks ERC‑20 balances and shares. Protocols such as CauldronV4 hold both collateral and borrowed assets inside DegenBox, using share accounting while interacting with external AMMs and strategies.
CauldronV4 is a collateralized borrowing primitive that integrates with DegenBox to manage per-user collateral and debt. Users interact through a generic cook(uint8[] actions, uint256[] values, bytes[] datas) entry point, which sequences actions like deposits, borrows, repayments, and arbitrary external calls. This design allows flexible compositions but pushes significant responsibility into the cook action dispatcher.
Each CauldronV4 clone relies on an oracle (via ProxyOracle) to price collateral against debt. However, the fundamental solvency invariant is captured directly in _isSolvent: a borrower with non‑zero debt and zero collateral should always be considered insolvent, regardless of oracle prices. The relevant part of the verified CauldronV4 implementation is:
function _isSolvent(address user, uint256 _exchangeRate) internal view returns (bool) {
uint256 borrowPart = userBorrowPart[user];
if (borrowPart == 0) return true;
uint256 collateralShare = userCollateralShare[user];
if (collateralShare == 0) return false;
// ... compare collateral value vs. borrowPart using exchangeRate ...
}
In the affected deployment, the two exploited Cauldron clones were configured with effectively unlimited borrow limits and non-restrictive parameters at block 23504546. Contract-call snapshots at that block show:
borrowLimit.total and borrowLimit.borrowPartPerAddress set to the maximum uint128 (3.402823669209384634633746074317682e38), making borrow caps non-binding.The exploitable design pattern is that CauldronV4.cook tracks whether a solvency check is required via a CookStatus struct, but then delegates to _additionalCookAction in a way that allows this struct to be overwritten and the flag cleared after a borrow.
In the verified CauldronV4 source (artifacts/root_cause/data_collector/iter_3/contract/1/0x46f54d4.../source/src/cauldrons/CauldronV4.sol), the relevant definitions are:
struct CookStatus {
bool needsSolvencyCheck;
bool hasAccrued;
}
function _additionalCookAction(
uint8 action,
CookStatus memory,
uint256 value,
bytes memory data,
uint256 value1,
uint256 value2
) internal virtual returns (bytes memory, uint8, CookStatus memory) {}
function cook(
uint8[] calldata actions,
uint256[] calldata values,
bytes[] calldata datas
) external payable returns (uint256 value1, uint256 value2) {
CookStatus memory status;
for (uint256 i = 0; i < actions.length; i++) {
uint8 action = actions[i];
if (!status.hasAccrued && action < 10) {
accrue();
status.hasAccrued = true;
}
// ...
} else if (action == ACTION_REMOVE_COLLATERAL) {
// ...
status.needsSolvencyCheck = true;
} else if (action == ACTION_BORROW) {
// ...
(value1, value2) = _borrow(to, _num(amount, value1, value2));
status.needsSolvencyCheck = true;
// ...
} else {
(bytes memory returnData, uint8 returnValues, CookStatus memory returnStatus) =
_additionalCookAction(action, status, values[i], datas[i], value1, value2);
status = returnStatus;
// decode returnData into value1/value2 if requested
}
}
if (status.needsSolvencyCheck) {
(, uint256 _exchangeRate) = updateExchangeRate();
require(_isSolvent(msg.sender, _exchangeRate), "Cauldron: user insolvent");
}
}
Key points:
ACTION_BORROW set status.needsSolvencyCheck = true after calling _borrow, which increases userBorrowPart[msg.sender] and transfers MIM-denominated DegenBox shares to the borrower.cook calls _additionalCookAction(action, status, ...) and then assigns status = returnStatus._additionalCookAction ignores the incoming status and returns a default-initialized CookStatus struct with all fields set to zero, including needsSolvencyCheck.As a result, a cook sequence of the form:
ACTION_BORROW (set needsSolvencyCheck = true, increase userBorrowPart, move shares to borrower);_additionalCookAction;will end the loop with status.needsSolvencyCheck == false, because the borrowed state persists in storage, but status has been overwritten with a zeroed struct returned from _additionalCookAction. The final if (status.needsSolvencyCheck) guard is then skipped entirely.
Given the _isSolvent definition, any user with userBorrowPart > 0 and userCollateralShare == 0 should be rejected as insolvent. Instead, the caller exits cook successfully in exactly this state.
Contract-call snapshots at block 23504546 confirm that the exploited helper contract is left with debt and no collateral in both Cauldron clones. For the clone at 0x289424add4a1a503870eb475fd8bf1d586b134ed, calls.json shows:
userCollateralShare(0xB8e0A4758Df2954063Ca4ba3d094f2d6EdA9B993) = 0.userBorrowPart(0xB8e0A4758Df2954063Ca4ba3d094f2d6EdA9B993) = 60557920478553930467368.borrowLimit() returns the maximum uint128 in both fields, so borrow caps are not constraining this position.The corresponding file for 0x46f54d434063e5f1a2b2cc6d9aaa657b1b9ff82c shows the same pattern: userCollateralShare for the helper contract is zero and userBorrowPart is very large. Under _isSolvent, these states are definitively insolvent but are not rejected because the final solvency check was skipped via the flawed CookStatus handling.
The root cause is a protocol-level logic error in CauldronV4’s cook and _additionalCookAction design:
_additionalCookAction implementation returns a fresh CookStatus struct instead of threading through the existing one, causing loss of the needsSolvencyCheck intent set by previous actions.This aligns with the ATTACK root cause category: an unprivileged adversary exploited a deterministic protocol bug to drain protocol-held liquidity for profit.
The adversary cluster consists of:
EOA 0x1aaade3e9062d124b7deb0ed6ddc7055efa7354d
0x842a....artifacts/root_cause/data_collector/iter_1/address/1/0x1aaade.../txlist_normal.json) shows inbound funding transfers immediately before block 23504546.Helper contract 0xB8e0A4758Df2954063Ca4ba3d094f2d6EdA9B993
0x842a... (metadata shows to = null and from = 0x1aaade...).0xd96f48665a1410c0cd669a88898eca36b9fc2cce, CauldronV4 clones 0x46f54d4... and 0x289424a..., Curve MIM-3CRV 0x5a6a4d5..., Curve 3pool 0xbebc447..., Uniswap V3 router/pools, and WETH9 0xc02aaa3....395059753040555107478 wei from WETH9 before self-destructing and returning value to the creator EOA.There is no evidence of privileged roles or governance rights associated with either address; both behave as unprivileged participants in public contracts, satisfying the ACT adversary model.
The combined traces from debug_trace_callTracer.json and the MIM-focused subtrace (mim_subtrace_abi_decoded.json) illustrate the token flows. A representative MIM transfer segment is:
{
"from": "0xd96f48665a1410c0cd669a88898eca36b9fc2cce",
"to": "0x99D8a9C45b2ecA8864373A26D1459e3Dff1e17F3",
"function": "transfer",
"decoded_args": [
{
"name": "to",
"type": "address",
"value": "0xb8e0a4758df2954063ca4ba3d094f2d6eda9b993"
},
{
"name": "amount",
"type": "uint256",
"value": 1793766133547645000000000
}
]
}
This corresponds to DegenBox transferring approximately 1,793,766.1335 MIM worth of shares to the helper contract, which then approves and transfers those tokens through an intermediate address into the Curve MIM-3CRV pool.
The same traces show WETH9 ultimately sending 395059753040555107478 wei to the helper, and the helper’s self-destruct sending ETH back to 0x1aaade.... Native balance diffs in balance_diff.json confirm that there are no matching outbound ETH transfers from the adversary addresses that would offset this gain within the transaction.
Adversary initial funding
0x592468ca5c..., 0x01c0fb6d37..., 0x50af0b9f23...) transfer ETH into 0x1aaade... in blocks 23504393–23504404, providing sufficient balance to cover the gas cost of the exploit transaction.txlist_normal.json for 0x1aaade... in artifacts/root_cause/data_collector/iter_1/address/1/0x1aaade.../.Helper contract deployment
0x842a...) with 0 ETH value.seed/1/0x842a.../metadata.json) shows from = 0x1aaade... and to = null, confirming contract creation.debug_trace_callTracer.json identifies the created contract as 0xB8e0A4758Df2954063Ca4ba3d094f2d6EdA9B993.Exploit execution and profit realization (within the same tx)
CauldronV4.cook on clones 0x46f54d4... and 0x289424a... with action sequences that:
ACTION_BORROW, increasing userBorrowPart for the helper address and transferring DegenBox-held MIM shares from the Cauldrons to the helper._additionalCookAction, causing CookStatus.needsSolvencyCheck to be reset to false.status.needsSolvencyCheck is false by the end of the loop, the final require(_isSolvent(msg.sender, _exchangeRate)) is not executed, even though userBorrowPart > 0 and userCollateralShare == 0 for the helper in both Cauldrons.395059753040555107478 wei from WETH9.0x1aaade..., which remains with a net balance increase of 395.0595 ETH after gas.Balance diffs in seed/1/0x842a.../balance_diff.json quantify the impact precisely:
Protocol loss in MIM
1793766133547645000000000 MIM units, corresponding to 1,793,766.13354764508484412 MIM at 18 decimals.Adversary profit in ETH
0x1aaade...:
before_wei: 173504352583541000after_wei: 395232980108896905054delta_wei: 395059475756313364054 (≈395.059475756313364054 ETH)0x842a... shows gasUsed = 940264 and gasPrice = 132247681 wei, yielding a fee of 124347733527784 wei (0.000124347733527784 ETH).The Cauldron clones retain large userBorrowPart entries for the now-destroyed helper contract and zero corresponding collateral, leaving a persistent accounting hole: protocol-owned MIM liquidity has been drained from DegenBox, and outstanding debt is effectively uncollectible. Absent out-of-band remediation, this loss must be borne by the protocol or its broader stakeholder set.
Under the ACT definition, this incident represents an “anyone-can-take” opportunity present on Ethereum mainnet at block 23504546.
System state (pre-state σ_B)
0xd96f4..., CauldronV4 master and clones 0x46f54d4... and 0x289424a..., MagicInternetMoneyV1 (MIM) 0x99D8a9C4..., Curve MIM‑3CRV 0x5a6a4d5..., Curve 3pool 0xbebc447..., Uniswap V3 router/pools, WETH9 0xc02aaa3....artifacts/root_cause/data_collector/iter_2/contract/1/.../DegenBox.sol, iter_3/contract/1/.../CauldronV4.sol, and iter_4/contract_call/1/*/calls.json) show the deployed code and parameters, including the vulnerable cook/_additionalCookAction pattern and effectively unlimited borrow limits.Adversary transaction sequence (b)
txhash: 0x842aae91c89a9e5043e64af34f53dc66daf0f033ad8afbf35ef0c93f99a9e5e60x1aaade3e9062d124b7deb0ed6ddc7055efa7354d (EOA)0xB8e0A4..., followed by CauldronV4 cook calls on 0x46f54d4... and 0x289424a..., DegenBox interactions, and AMM swaps.cook and AMM interactions is fully reconstructible from public ABI signatures, verified contract sources, and the recorded trace; no private order flow or privileged calls are required.Inclusion feasibility
maxFeePerGas and maxPriorityFeePerGas, submitted to the public mempool and included on-chain.Success predicate (profit)
0x1aaade3e9062d124b7deb0ed6ddc7055efa7354d.All components of this exploit—contract code, configuration, traces, and balances—are derived from canonical on-chain data and verified sources, and the adversary’s actions rely only on permissionless interactions. Consequently, this is a clear ACT opportunity arising from a deterministic CauldronV4 logic bug.
0x842a... (artifacts/root_cause/seed/1/0x842aae91c89a9e5043e64af34f53dc66daf0f033ad8afbf35ef0c93f99a9e5e6).artifacts/root_cause/data_collector/iter_2/contract/1/0xd96f48665a1410c0cd669a88898eca36b9fc2cce/source/src/flat/DegenBox.sol).artifacts/root_cause/data_collector/iter_3/contract/1/0x46f54d434063e5f1a2b2cc6d9aaa657b1b9ff82c/source/src/cauldrons/CauldronV4.sol).artifacts/root_cause/data_collector/iter_4/contract_call/1/*/calls.json).artifacts/root_cause/data_collector/iter_1/address/1/0x1aaade3e9062d124b7deb0ed6ddc7055efa7354d/txlist_normal.json).