C.R.E.A.M. Finance AMP Reentrancy (2021-08-31)
- Name: C.R.E.A.M. Finance AMP reentrancy
- Date: 2021-08-31
- Protocol: C.R.E.A.M. Finance
- Chain: Ethereum
- Category: lending-market integration failure / ERC-777 hook reentrancy / bad-debt extraction
- Loss: 462,079,976 AMP and 2,804.96 ETH, per C.R.E.A.M.’s postmortem headline figures
- Summary: C.R.E.A.M. integrated AMP into its lending markets in a way that treated token transfer like a passive ERC-20 move. It was not passive. In
crAMP.borrow(), the protocol transferred AMP out before finalizing borrower-debt storage, and AMP’s ERC-777-style post-transfer hook let the recipient run attacker code during that transfer. The attacker used that callback window to re-enter C.R.E.A.M., borrow against the same collateral while the first borrow was still mid-flight, and later refined the playbook into a two-contract self-liquidation loop. Across 17 exploit transactions, that integration mistake turned the AMP listing itself into a bad-debt extraction surface. - Trigger / entry point:
- C.R.E.A.M.’s
crAMP.borrow()path transferred the borrowed AMP out before sealing borrower-debt storage. - AMP (
0xff20817765cb7f73d4bde2e66e067e58d11095c2) exposed ERC-777-style hook behavior through_callPostTransferHooks/tokensReceived()via the ERC-1820 registry. - The attacker borrowed AMP from
crAMP(0x2Db6c82CE72C8d7D770ba1b5F5Ed0b6E075066d6), received the hook callback during transfer, and re-entered the siblingcrETHmarket (0xD06527D5e56A3495252A528C4987003b712860eE) before the initial borrow path had sealed its accounting.
- C.R.E.A.M.’s
- Root cause:
- The official postmortem says the root cause was not a bug in AMP itself, but an error in how C.R.E.A.M. integrated AMP into the lending protocol.
- The decisive mistake was ordering: transfer first, storage update second.
- Once a callback-capable asset sat in that path, the system’s assumption that transfer was inert collapsed.
- Exploit path:
- The attacker deposited ETH into
crETHto mintcrETHcollateral and enter the market as a borrower. - The attacker called
crAMP.borrow()to pull a large AMP balance from the AMP market. - During that outbound AMP transfer, AMP invoked its ERC-777-style recipient hook on the attacker contract.
- The hook re-entered C.R.E.A.M. before the outer
crAMP.borrow()had updated debt storage, and used the still-generous intermediate state to call back intocrETH.borrow()for ETH. - Once control returned, the original AMP borrow finalized, leaving the attacker with both the AMP and the re-entrant ETH while C.R.E.A.M. only later recognized the now-undercollateralized position.
- In the first wave, the attacker treated each attacking contract as disposable: one contract executed the nested-borrow path, finished underwater, and was abandoned after extracting value.
- In the second wave, the attacker improved the loop by moving part of the borrowed AMP into a second attacker-controlled contract, using that contract to liquidate the first one, seizing
crETH, redeeming it for ETH, and recovering more value before the bad debt settled on the protocol.
- The attacker deposited ETH into
- What the attacker needed to be true:
- A callback-capable asset had to be listed in a borrow path that updated state too late.
- The attacker needed a controllable recipient contract to receive AMP and execute logic during the post-transfer hook.
- The borrow path and the sibling market logic had to trust pre-update state during the callback window.
- Enough AMP liquidity had to be present in the market to make the attack economically worthwhile; C.R.E.A.M. explicitly notes the market had not been exploited earlier because
crAMPhad lower supplied liquidity before a later influx.
- Technical anatomy worth remembering:
- This was not generic “flash-loan magic.” The heart of the exploit was a real callback surface hidden inside a token integration and an ordering bug in the lending market’s own
borrow()implementation. - The exploit was cross-market: the vulnerable outbound AMP transfer happened in
crAMP, but the monetized re-entry borrowed ETH fromcrETHwhile the overall solvency picture was still mid-update. - AMP’s ERC-777-style hook path mattered operationally because it converted what looked like a normal token transfer into an attacker-controlled call boundary mediated by ERC-1820 registration.
- The postmortem notes the attack played out over 17 exploit transactions and multiple attacker contracts because a successful run intentionally left the primary borrower undercollateralized.
- C.R.E.A.M.’s appendix gives two clean exploit phases:
- First wave: nested borrow, leaving the attacking contract undercollateralized and liquidatable.
- Second wave: nested borrow, then attacker-controlled liquidation and collateral seizure.
- The second-wave example preserved by C.R.E.A.M. is especially useful because it shows the exploit maturing from
take bad debt and move oninto a tighter two-contract structure that reclaims seized collateral before the protocol can socialize the loss.
- This was not generic “flash-loan magic.” The heart of the exploit was a real callback surface hidden inside a token integration and an ordering bug in the lending market’s own
- Representative transactions and contracts:
- AMP token:
0xff20817765cb7f73d4bde2e66e067e58d11095c2 crAMPmarket:0x2Db6c82CE72C8d7D770ba1b5F5Ed0b6E075066d6crETHmarket:0xD06527D5e56A3495252A528C4987003b712860eE- ERC-1820 registry used by the hook-dispatch path:
0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24 - Main exploiter wallet highlighted by rekt.news:
0xce1f4b4f17224ec6df16eeb1e3e5321c54ff6ede - Example attacker contract A from rekt.news:
0x38c40427efbaae566407e4cde2a91947df0bd22b - Example attacker contract B from rekt.news:
0x0ec306d7634314d35139d1df4a630d829475a125 - Initial exploit transaction cited by C.R.E.A.M.:
0x0016745693d68d734faa408b94cdf2d6c95f511b50f47b03909dc599c1dd9ff6 - Copy-cat exploit transaction cited by C.R.E.A.M.:
0xc90468d698700757f33543039c7cb10d4ca49d57b5417789e7656e73019de674 - Example first-wave nested-borrow transaction from C.R.E.A.M.’s appendix:
0xba468e260588e659d308413bf14c5b466fb13ee6423a96587ad1eee4a81bb802 - Example second-wave nested-borrow-and-self-liquidation transaction from C.R.E.A.M.’s appendix:
0x487364b6de9e00015c571017c00fd5078c795822a4387af4d95962e50112bc06 - Another second-wave liquidation-assisted example from C.R.E.A.M.’s appendix:
0xa9a1b8ea288eb9ad315088f17f7c7386b9989c95b4d13c81b69d5ddad7ffe61e
- AMP token:
- Response / mitigation:
- C.R.E.A.M. paused AMP supply and borrow functions.
- The team stated it would patch the market before re-enabling it and work with auditors to harden against similar attacks.
- The protocol also committed to replacing the stolen ETH and AMP and allocating a share of protocol fees toward repayment.
Why this incident matters
- It is one of the cleanest modern examples of token-hook reentrancy breaking a lending protocol.
- It shows why treating all token transfers as ERC-20-like is a category error.
- It also shows that reentrancy analysis has to cover market interactions and liquidation paths, not just a single
withdraw()function.
Pattern tags
- Receiver-side callback-capable borrow asset: the exploitable control transfer happened in AMP’s ERC-777-style
tokensReceived()path during outbound loan delivery, not in a plain ETH send or a passive ERC-20 transfer. - Cross-market nested borrow monetization: the callback window opened in
crAMP, but the profit leg came from re-entering the siblingcrETHmarket before the first borrow had finalized its debt state. - Self-liquidation bad-debt extraction: the later attack wave mattered because the attacker did not only leave underwater debt behind; they recycled borrowed AMP into an attacker-controlled liquidation path to reclaim more ETH collateral before the protocol socialized the loss.
- ERC1820-mediated semantic drift: the deeper integration failure was admitting a token whose transfer semantics included externally dispatched hook logic while the borrow path still modeled transfer as inert value delivery.
Why the exploit worked
- C.R.E.A.M.’s
borrow()path transferred AMP before sealing borrower-debt storage, so the protocol surrendered control while solvency still depended on pre-update accounting. - AMP preserved ERC-777-style post-transfer hook behavior through the ERC1820 registry, which meant outbound loan delivery itself became an attacker-controlled callback edge.
- The protocol’s market composition made the callback profitable: while
crAMP.borrow()was mid-flight, the attacker could still makecrETHtrust collateral state that did not yet reflect the full debt picture. - The second-wave transactions show the loss was not limited to
borrow too much once; C.R.E.A.M. also let the attacker-controlled liquidation path convert the intentionally undercollateralized position back into seizedcrETHand then ETH. - Operationally, the vulnerability had existed since AMP was listed, but only became economically live once enough AMP liquidity accumulated in the market to make the nested-borrow path worth repeating across many disposable contracts.
Three borrow-side callback promotions this incident collapsed
-
A callback-capable borrow-delivery edge existed at all
crAMP.borrow()did not just move an ERC-20 balance. Once AMP’s ERC-777 / ERC1820 hook path fired during outbound delivery, the loan-disbursement step itself became an attacker-controlled execution edge.- The weak first fact was only
borrowed AMP is being delivered right now, notthe borrow path is already economically settled.
-
That in-flight borrow was treated as if it had already published one coherent debt / collateral story across markets
- C.R.E.A.M. had not yet finished sealing the borrower-debt state for the AMP leg, but the system still let the callback window exist as though
this account's solvency picture already reflects the AMP borrowwere true. - The dangerous jump was from
AMP transfer startedtothe protocol may already treat the borrower's cross-market liquidity as coherent enough for other markets to trust.
- C.R.E.A.M. had not yet finished sealing the borrower-debt state for the AMP leg, but the system still let the callback window exist as though
-
A sibling market was still allowed to spend against that half-published story before the frame closed
- During the hook,
crETHstill honored a more generous collateral picture and let the attacker borrow ETH against the same collateral before the first AMP borrow had fully landed. - The second-wave self-liquidation loop then shows the further promotion: even the intentionally broken position still retained enough ordinary liquidation semantics for the attacker to recycle borrowed AMP into seized
crETHand redeemed ETH.
- During the hook,
Detection and audit pivots
- Inventory listed borrow assets for ERC-777-style hooks, ERC1820 lookups, or any transfer path that can invoke recipient-controlled code during loan delivery.
- In every lending
borrow()path, trace whether debt, account liquidity, and market-level borrow state are finalized beforedoTransferOut()or any token transfer that could hand execution away. - Fuzz
borrow(callback-capable asset) -> callback -> borrow sibling marketand nearby sequences, then check whether the combined debt picture stays solvent at every callback edge rather than only at function exit. - Review liquidation paths as part of the same exploit family: if an attacker can deliberately create an underwater position during reentrancy, can a second attacker-controlled account liquidate it and reclaim collateral before losses settle on the protocol?
- Treat market-listing review as solvency review, not only token-admission hygiene. The critical question is not
does transfer succeed?butwhat control-flow and callback authority does this asset import into borrow, repay, and liquidation paths? - Monitor for profitability regime changes in already-listed exotic assets. A callback-capable market that looked harmless at low liquidity can become immediately exploitable once inventory or borrow demand crosses the threshold where bad-debt extraction pays.
Failure-prevention checklist
- Finalize debt, account-liquidity, and market-borrow state before
doTransferOut()or any other asset-delivery edge that can run recipient-controlled code. - Treat asset listing as callback-surface admission. If a token can invoke ERC777 / ERC1820 hooks, wrapper callbacks, or other externally dispatched logic during transfer, prove the lending path remains solvent at that edge or reject the asset.
- Reuse the same callback and solvency invariant across sibling markets. A borrow in one market should not be able to observe a more generous collateral picture in another market while the first borrow is still mid-flight.
- Harden liquidation paths against attacker-controlled self-liquidation loops. If reentrancy can intentionally create bad debt, the design also needs to prevent attacker-owned liquidators from reclaiming collateral before losses settle.
- Keep exotic-asset review live after listing, not only at launch. A callback-capable market that is harmless at thin liquidity can become immediately exploitable once inventory and borrow demand make the bad-debt loop profitable.
- Regression-test
borrow(callback-capable asset) -> callback -> borrow sibling market -> self-liquidate -> redeem collateralsequences in CI, not just same-function reentry on a toy withdraw path. - Prefer token-semantics allowlists or explicit adapter contracts for borrowable assets rather than assuming every listed token is ERC-20-passive enough for the core lending code.
Audit prompts
- For every listed borrow asset, what exact code can execute during outbound loan delivery, and where is that fact documented in listing review?
- Does
borrow()finish borrower debt, market borrow totals, and account-liquidity effects before any token transfer, hook dispatch, or external helper call can occur? - If a callback re-enters a sibling market, what invariant proves the second market already sees the full debt picture from the first one?
- Could an attacker deliberately leave an account underwater during reentrancy and then use a second contract to liquidate that account for additional collateral recovery before the protocol socializes the loss?
- Which listed assets rely on ERC1820 lookups, ERC777 hooks, wrapper callbacks, or other non-passive transfer semantics, and do the current borrow / repay / liquidation tests explicitly cover them?
- What risk review would catch
this token transfer is not inertbefore listing, and who owns re-review when market liquidity or borrow caps change? - If a token’s callback behavior only becomes profitable after liquidity grows, what monitoring or supply-cap policy prevents a dormant design bug from turning live without a code change?
- Are there any markets where transfer semantics, liquidation incentives, and collateral factors were reviewed separately even though they form one bad-debt extraction object?
Lessons that should already be standard
- Listing a token is security-sensitive code admission, not just market expansion. AMP did not need to be buggy; C.R.E.A.M. only needed to misunderstand what AMP transfers were allowed to do.
Borrow transfers out lastshould be a default lending invariant. If the loan is already leaving while debt is still provisional, the protocol has turned delivery itself into an attack surface.- Cross-market composability cuts both ways. Even when only one market hands away control, the profitable exploit path may live in a different market that still trusts the old solvency picture.
- Thin liquidity can hide real bugs. A callback-capable market may look safe for months and then become instantly exploitable when inventory rises enough for bad-debt extraction to pay.
- Liquidation is part of the same exploit surface as borrow. Attackers will often refine
leave protocol with bad debtintoleave protocol with bad debt and also reclaim seized collateralonce the first path works. - Token-standard compatibility is not binary.
ERC-20 enough to listis a dangerous fiction when transfer semantics import hooks, registries, or callback dispatch that the core protocol never modeled.
Internal linkages
- Core recurring pattern: reentrancy.
- Asset-integration lens: interface-conformance-semantic-compatibility-failure — the exploit only existed because C.R.E.A.M. listed a callback-capable asset in a borrow path that assumed token transfer was inert.
- Positive-design companion: actual-receipt-accounting-and-token-semantics-admission-discipline — the healthier design answer was to treat
what exact token behavior are we admitting into borrow-side delivery and reserve accounting?as an explicit listing boundary instead of a hidden assumption.
Control surface
-
The leverage sat in asset-listing policy, token-standard assumptions, borrow-path ordering, and the fact that C.R.E.A.M. let a callback-capable token live inside a path that had not yet finalized debt accounting.
-
The exploit was not “AMP broke C.R.E.A.M.” It was C.R.E.A.M. mis-modeling what AMP transfers were allowed to do.
-
The sharper review split is
a callback-capable delivery edge exists,that edge is treated as if one settled cross-market debt story already exists, anda sibling market may still spend against that unfinished story right now. -
Sources:
- https://medium.com/cream-finance/c-r-e-a-m-finance-post-mortem-amp-exploit-6ceb20a630c5
- https://rekt.news/cream-rekt
- https://raw.githubusercontent.com/SunWeb3Sec/DeFiHackLabs/main/src/test/2021-08/Cream_exp.sol
- https://eips.ethereum.org/EIPS/eip-777
- https://docs.soliditylang.org/en/latest/security-considerations.html#reentrancy
-
Last reviewed: 2026-07-30 UTC