We do not have a reliable USD price for the recorded assets yet.
0x2db6c82ce72c8d7d770ba1b5f5ed0b6e075066d6Ethereum0xd06527d5e56a3495252a528c4987003b712860eeEthereum0xff20817765cb7f73d4bde2e66e067e58d11095c2Ethereum0x3d5bc3c8d13dcb8bf317092d84783c2697ae9258EthereumOn Ethereum mainnet between blocks 13124721 and 13125173, an adversary-controlled EOA and two helper contracts exploited Cream Finance’s Amp-backed cToken market (cAmp). By combining Amp’s ERC-1820 AmpTokensRecipient hook with Cream’s cToken cash-transfer logic, the attacker executed a single borrow/liquidation transaction that moved 9,740,000 AMP out of the cAmp contract to the attacker while Cream’s internal accounting still reported the market as fully backed. Across three attacker-crafted transactions, the adversary’s portfolio increased by exactly 9,740,000 AMP and 1,341.307701197557 ETH after gas, making this a clear anyone-can-take (ACT) opportunity.
The root cause is that cAmp’s CCollateralCapErc20CheckRepay implementation assumes underlying token transfers are atomic and non-reentrant. It calls Amp.transferFrom/transfer inside doTransferIn/doTransferOut and only afterwards updates the internalCash and related accounting. Amp, however, invokes an AmpTokensRecipient hook on the adversary helper contract during these transfers. This reentrancy window allows borrow and liquidation logic to run while internalCash and Amp balances still reflect pre-transfer state, causing the backing term
// CToken backing invariant (informal)
E = internalCash + totalBorrows - totalReserves;
to increase even though Amp.balanceOf(cAmp) and internalCash both decrease by 9,740,000 AMP.
Cream Finance’s markets follow a Compound-style cToken design. For each market (including cAmp and CEther), the protocol tracks at least the following aggregate variables:
internalCash: underlying tokens held by the cToken contract (Amp for cAmp, ETH for CEther).totalBorrows: the total principal borrowed by users, adjusted by a borrow index.totalReserves: accumulated reserves taken from interest or fees.totalSupply: total number of cTokens in circulation.0x2506926d1e547ebcfdc276cee9e1115f2399c4f211cdb53a5bb67df1e601d908totalCollateralTokens (for cAmp): amount of underlying counted as collateral.These are intended to obey a backing relation
E = internalCash + totalBorrows - totalReserves;
such that, with totalSupply and totalCollateralTokens fixed during a transaction, changes in E reflect real changes in the underlying token backing (plus interest/reserve updates), and never create backing out of thin air.
The cAmp market is deployed as a CErc20Delegator proxy at address 0x2db6c82ce72c8d7d770ba1b5f5ed0b6e075066d6, pointing to the CCollateralCapErc20CheckRepayDelegate implementation 0x96cc0f947b6c8f4675159ea03144f8c17d5a2fc8. The verified source for this implementation is stored under the data collector artifacts:
artifacts/root_cause/data_collector/iter_3/contract/1/0x96cc0f947b6c8f4675159ea03144f8c17d5a2fc8/source/src
The relevant victim-side logic comes from two files:
CCollateralCapErc20CheckRepay.sol (cAmp’s concrete implementation).CTokenCheckRepay.sol / CToken.sol (shared cToken accounting, including exchangeRateStored and borrow/liquidation flows).The CEther market used as collateral in the exploit is at 0xd06527d5e56a3495252a528c4987003b712860ee, with state diffs captured in:
artifacts/root_cause/data_collector/iter_5/state/1/0xd06527d5e56a3495252a528c4987003b712860ee/0xa9a1b8ea288eb9ad315088f17f7c7386b9989c95b4d13c81b69d5ddad7ffe61e_state_diff.json
The Comptroller/Unitroller governs both cAmp and CEther:
Comptroller.sol in the same contract source tree as cAmp.Amp (the underlying) is an ERC-20–compatible token deployed at 0xff20817765cb7f73d4bde2e66e067e58d11095c2. It additionally integrates with the ERC-1820 registry to support Amp-specific hooks. A key artifact is the ERC-1820 snapshot for Amp hooks:
// ERC-1820 Amp hook snapshot
{
"0x38c40427efbaae566407e4cde2a91947df0bd22b": {
"AmpTokensSender": {
"implementer": "0x0000000000000000000000000000000000000000"
},
"AmpTokensRecipient": {
"implementer": "0x38c40427efbAAe566407e4CdE2A91947dF0bD22B"
}
}
}
This is taken from:
artifacts/root_cause/data_collector/iter_3/erc1820/erc1820_amp_hooks.json
It shows that the adversary helper contract 0x38c40427efbaae566407e4cde2a91947df0bd22b is registered as an AmpTokensRecipient implementer, while cAmp and the Comptroller are not. Whenever Amp transfers tokens to 0x38c4…, Amp will invoke the helper’s recipient hook.
The ACT opportunity is evaluated around the exploit transaction
tx 0xa9a1b8ea288eb9ad315088f17f7c7386b9989c95b4d13c81b69d5ddad7ffe61e
block 13125071 (Ethereum mainnet)
using pre-state approximated by end-of-block 13125070 and post-state at end-of-block 13125071. This approximation is made explicit in the state diff artifacts and is numerically sufficient to show the invariant violation.
The vulnerability is a reentrancy issue arising from Amp’s ERC-1820 hooks interacting with cAmp’s cash-transfer helpers. cAmp’s doTransferIn/doTransferOut functions wrap Amp.transferFrom/transfer calls and then update internalCash based on the observed change in Amp.balanceOf(address(this)), under the assumption that the external token call completes atomically and cannot reenter protocol logic.
Amp, however, invokes an AmpTokensRecipient hook on contracts registered in the ERC-1820 registry. The adversary helper contract 0x38c4… is such a recipient. During Amp.transfer or Amp.transferFrom to 0x38c4…, Amp executes 0x38c4…’s hook, which in this exploit calls back into CEther, the Comptroller, and cAmp while cAmp’s internalCash and aggregate accounting variables still reflect pre-transfer values.
The intended backing invariant for cAmp is:
totalSupply and totalCollateralTokens fixed in a transaction, E = internalCash + totalBorrows - totalReserves must track the true Amp backing and should change only due to interest accrual and reserve-factor logic, in a way that matches net Amp inflows/outflows.E must not increase.In the exploit transaction, state diffs for cAmp show that internalCash decreases by 9,740,000 AMP while totalBorrows and totalReserves both increase, causing E to increase by 1,887,895,286,798,755,706,575 Amp units despite no Amp inflow. This is only possible if the borrow and liquidation flows execute on stale internalCash and Amp balances due to reentrant execution inside Amp.transfer/transferFrom, which is precisely what the call trace and ERC-1820 hook configuration show.
In short:
The concrete cAmp implementation CCollateralCapErc20CheckRepay is responsible for moving Amp in and out of the market. The doTransferIn helper is implemented as:
// CCollateralCapErc20CheckRepay.sol (simplified excerpt)
function doTransferIn(
address from,
uint256 amount,
bool isNative
) internal returns (uint256) {
isNative; // unused
EIP20NonStandardInterface token = EIP20NonStandardInterface(underlying);
uint256 balanceBefore = EIP20Interface(underlying).balanceOf(address(this));
token.transferFrom(from, address(this), amount);
bool success;
assembly {
switch returndatasize()
case 0 { success := not(0) }
case 32 { returndatacopy(0, 0, 32) success := mload(0) }
default { revert(0, 0) }
}
require(success, "TOKEN_TRANSFER_IN_FAILED");
uint256 balanceAfter = EIP20Interface(underlying).balanceOf(address(this));
uint256 transferredIn = sub_(balanceAfter, balanceBefore);
internalCash = add_(internalCash, transferredIn);
return transferredIn;
}
Similarly, doTransferOut calls Amp.transfer(to, amount) and only afterwards decrements internalCash by amount. This pattern assumes that the token transfer is atomic: the contract records the old balance, calls the token, then records the new balance and updates internalCash.
In the base cToken code (CTokenCheckRepay.sol and CToken.sol), borrow and liquidation flows read and update totalBorrows, totalReserves, and other market variables on the assumption that doTransferIn/doTransferOut safely encapsulate the cash movement. For example, exchangeRateStoredInternal() calculates:
uint256 totalCash = getCashPrior();
uint256 cashPlusBorrowsMinusReserves = sub_(add_(totalCash, totalBorrows), totalReserves);
uint256 exchangeRate = div_(cashPlusBorrowsMinusReserves, Exp({mantissa: _totalSupply}));
and the broader design expects totalCash (and thus internalCash) to track the true underlying token balances.
Amp supports custom hooks via the ERC-1820 registry. The snapshot in erc1820_amp_hooks.json shows that the adversary helper contract 0x38c4… is registered as an AmpTokensRecipient implementer. cAmp and the Comptroller have AmpTokensRecipient implementers set to the zero address, so only 0x38c4… receives callbacks.
Whenever Amp transfers tokens to 0x38c4… (either via transfer or transferFrom), Amp queries ERC-1820 and calls 0x38c4…’s recipient hook. This callback happens inside the execution of the Amp.transfer/transferFrom call triggered by cAmp’s doTransferOut/doTransferIn.
The call trace for the exploit transaction
artifacts/root_cause/data_collector/iter_4/tx/1/0xa9a1b8ea288eb9ad315088f17f7c7386b9989c95b4d13c81b69d5ddad7ffe61e/call_trace.debug.json
shows that 0x38c4… performs complex interactions with CEther, the Comptroller, and cAmp during the exploit, including borrow and liquidation flows.
The core exploit occurs in:
tx 0xa9a1b8ea288eb9ad315088f17f7c7386b9989c95b4d13c81b69d5ddad7ffe61e
block 13125071
from EOA 0xce1f4b4f17224ec6df16eeb1e3e5321c54ff6ede
call to 0x38c40427efbaae566407e4cde2a91947df0bd22b (helper contract)
A high-level view from the call trace shows the following steps:
The balance diff for this transaction is recorded in:
// balance_diff for tx 0xa9a1…e61e
{
"erc20_balance_deltas": [
{
"token": "0xff20817765cb7f73d4bde2e66e067e58d11095c2",
"holder": "0x2db6c82ce72c8d7d770ba1b5f5ed0b6e075066d6",
"before": "33663858581302237334110531",
"after": "23923858581302237334110531",
"delta": "-9740000000000000000000000",
"contract_name": "Amp"
},
{
"token": "0xff20817765cb7f73d4bde2e66e067e58d11095c2",
"holder": "0xce1f4b4f17224ec6df16eeb1e3e5321c54ff6ede",
"before": "332557977063785006705096485",
"after": "342297977063785006705096485",
"delta": "9740000000000000000000000",
"contract_name": "Amp"
}
]
}
(from artifacts/root_cause/data_collector/iter_4/tx/1/0xa9a1…e61e/balance_diff.json). This shows that Amp.balanceOf(cAmp) decreases by exactly 9,740,000 AMP while the adversary EOA’s Amp balance increases by the same amount.
The cAmp state diff across the transaction is:
// cAmp state diff (scalars only)
{
"scalars": {
"totalBorrows": {
"before": "470166088159676190211552469",
"after": "479909520696561278858291695"
},
"totalSupply": {
"before": "2515641748500199266",
"after": "2515641748500199266"
},
"totalReserves": {
"before": "207252914123973988114735",
"after": "208797555722263879147386"
},
"internalCash": {
"before": "33663858581302237334110531",
"after": "23923858581302237334110531"
},
"totalCollateralTokens": {
"before": "1218382802034500652",
"after": "1218382802034500652"
}
}
}
(from artifacts/root_cause/data_collector/iter_5/state/1/0x2db6…66d6/0xa9a1…e61e_state_diff.json). From these values, the backing term E = internalCash + totalBorrows - totalReserves is:
At the same time:
totalSupply and totalCollateralTokens are constant.Therefore, cAmp’s internal accounting claims more Amp backing per cAmp after the transaction even though the contract holds fewer Amp tokens. This is an explicit violation of the intended backing invariant.
The only way to reconcile this with the code and traces is that, during Amp.transfer/transferFrom calls inside doTransferIn/doTransferOut, Amp’s AmpTokensRecipient hook on 0x38c4… reenters Cream’s contracts and invokes borrow/liquidation logic that increases totalBorrows and totalReserves as if additional Amp had been repaid or reserved, while internalCash (and the on-chain Amp balances) reflect a pure outflow of 9,740,000 AMP.
The CEther state diff for the same transaction is recorded in:
artifacts/root_cause/data_collector/iter_5/state/1/0xd06527d5e56a3495252a528c4987003b712860ee/0xa9a1…e61e_state_diff.json
It shows:
totalBorrows increasing slightly, consistent with normal borrow usage.totalReserves increasing modestly from interest/reserve-factor logic.totalSupply decreasing, consistent with redemptions.The native balance diff for CEther from the transaction’s balance_diff.json shows CEther losing 42.583432458943489116 ETH, while WETH and the miner balances adjust accordingly. These values align with expected interest and premium flows and do not show anomalous ETH creation. This indicates that the exploit’s economic impact is concentrated in the Amp-backed cAmp market, not in CEther.
The adversary executes three attacker-crafted transactions from the same EOA 0xce1f4…:
Stage 1 – Amp allowance setup
artifacts/root_cause/data_collector/iter_2/address/1/0xce1f4…ede/tx_receipts/0x8dd9…b2c6.json show an Amp Approval event for the router. The balance diff at artifacts/root_cause/data_collector/iter_4/tx/1/0x8dd9…b2c6/balance_diff.json shows no Amp balance change and a small ETH gas cost, consistent with a pure approval.Stage 2 – Core Cream/Amp reentrancy exploit
E = internalCash + totalBorrows - totalReserves increases by 1,887,895,286,798,755,706,575 Amp units with totalSupply unchanged.call_trace.debug.json and balance_diff.json for this transaction, plus the cAmp and CEther state diffs, as cited above.Stage 3 – WETH to ETH profit realization
artifacts/root_cause/data_collector/iter_2/address/1/0xce1f4…ede/tx_receipts/0x2506…d908.json and balance diff at
artifacts/root_cause/data_collector/iter_4/tx/1/0x2506…d908/balance_diff.json show WETH9’s ETH balance decreasing and the EOA’s ETH balance increasing by 1,341.877732027261864457 ETH, with the difference attributed to miner compensation and gas.Across these three transactions, the adversary cluster consists of:
The flow uses only public interfaces and standard DeFi components: Uniswap V2, WETH9, CEther, Comptroller, cAmp, and Amp. There is no privileged access or whitelisting, so any unprivileged adversary can deploy equivalent helper contracts and submit the same calldata, satisfying the ACT criteria.
The direct on-chain loss to the Cream cAmp market is 9,740,000 AMP, as measured by the change in Amp.balanceOf(cAmp) across the exploit transaction. This is captured in the Amp ERC-20 balance diff for tx 0xa9a1…e61e, where cAmp’s Amp balance decreases by exactly 9,740,000 × 10¹⁸ units and the adversary EOA’s Amp balance increases by the same amount.
From the viewpoint of the adversary’s ETH-denominated portfolio, the Uniswap AMP/ETH valuation report shows:
// per-tx ETH portfolio changes (from uniswap_amp_eth_valuation_report.json)
{
"per_tx": [
{
"txhash": "0x2506926d1e547ebcfdc276cee9e1115f2399c4f211cdb53a5bb67df1e601d908",
"net_portfolio_delta_eth": 1341.872788649557
},
{
"txhash": "0xa9a1b8ea288eb9ad315088f17f7c7386b9989c95b4d13c81b69d5ddad7ffe61e",
"net_portfolio_delta_eth": -0.55130724
},
{
"txhash": "0x8dd9fa7e5d1ea4fb145c9c42513281e53d252590822d2f09b2c0266e8666b2c6",
"net_portfolio_delta_eth": -0.013780212
}
],
"net_portfolio_delta_eth_total": 1341.307701197557
}
(from artifacts/root_cause/data_collector/iter_4/address/1/0xce1f4…ede/pricing/uniswap_amp_eth_valuation_report.json). The per-transaction values already account for gas costs via gas_cost_wei. Summing them yields a net ETH-denominated portfolio increase of exactly 1,341.307701197557 ETH for the adversary across the three attacker-crafted transactions.
CEther’s behavior is consistent with normal interest and reserve accounting and does not show anomalous ETH creation; the observed ETH movements reflect legitimate collateral flows and flash swap repayment. The economic loss is therefore concentrated in the Amp collateral pool backing cAmp, manifesting as a deficit between the promised value of cAmp and the actual Amp tokens held by the protocol.
Key artifacts supporting this root cause analysis include:
artifacts/root_cause/data_collector/iter_4/tx/1/0xa9a1…e61e/call_trace.debug.json.artifacts/root_cause/data_collector/iter_4/tx/1/0xa9a1…e61e/balance_diff.json.artifacts/root_cause/data_collector/iter_5/state/1/0x2db6…66d6/0xa9a1…e61e_state_diff.json.artifacts/root_cause/data_collector/iter_5/state/1/0xd065…60ee/0xa9a1…e61e_state_diff.json.artifacts/root_cause/data_collector/iter_3/contract/1/0x96cc0f947b6c8f4675159ea03144f8c17d5a2fc8/source/src/CCollateralCapErc20CheckRepay.sol.artifacts/root_cause/data_collector/iter_3/contract/1/0x96cc0f947b6c8f4675159ea03144f8c17d5a2fc8/source/src/CTokenCheckRepay.sol.artifacts/root_cause/data_collector/iter_3/contract/1/0x96cc0f947b6c8f4675159ea03144f8c17d5a2fc8/source/src/CToken.sol.artifacts/root_cause/data_collector/iter_3/contract/1/0x7aa375f1fe5e04e18a6b02b4294cfd57ca9f53ba/source/src/Comptroller.sol.artifacts/root_cause/data_collector/iter_3/erc1820/erc1820_amp_hooks.json.artifacts/root_cause/data_collector/iter_4/address/1/0xce1f4b4f17224ec6df16eeb1e3e5321c54ff6ede/pricing/uniswap_amp_eth_valuation_report.json.artifacts/root_cause/data_collector/data_collection_summary.json.artifacts/root_cause/root_cause_analyzer/iter_6/current_analysis_result.json.These artifacts collectively demonstrate that the Cream/Amp integration permits AmpTokensRecipient-based reentrancy during doTransferIn/doTransferOut, violates the backing invariant E = internalCash + totalBorrows - totalReserves, and enables a deterministic ACT opportunity that yields 9,740,000 AMP and 1,341.307701197557 ETH to an unprivileged adversary.