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 sibling crETH market (0xD06527D5e56A3495252A528C4987003b712860eE) before the initial borrow path had sealed its accounting.
  • 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:
    1. The attacker deposited ETH into crETH to mint crETH collateral and enter the market as a borrower.
    2. The attacker called crAMP.borrow() to pull a large AMP balance from the AMP market.
    3. During that outbound AMP transfer, AMP invoked its ERC-777-style recipient hook on the attacker contract.
    4. 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 into crETH.borrow() for ETH.
    5. 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.
    6. 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.
    7. 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.
  • 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 crAMP had 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 from crETH while 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 on into a tighter two-contract structure that reclaims seized collateral before the protocol can socialize the loss.
  • Representative transactions and contracts:
    • AMP token: 0xff20817765cb7f73d4bde2e66e067e58d11095c2
    • crAMP market: 0x2Db6c82CE72C8d7D770ba1b5F5Ed0b6E075066d6
    • crETH market: 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
  • 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 sibling crETH market 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 make crETH trust 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 seized crETH and 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

  1. 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, not the borrow path is already economically settled.
  2. 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 borrow were true.
    • The dangerous jump was from AMP transfer started to the protocol may already treat the borrower's cross-market liquidity as coherent enough for other markets to trust.
  3. A sibling market was still allowed to spend against that half-published story before the frame closed

    • During the hook, crETH still 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 crETH and redeemed ETH.

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 before doTransferOut() or any token transfer that could hand execution away.
  • Fuzz borrow(callback-capable asset) -> callback -> borrow sibling market and 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? but what 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 collateral sequences 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 inert before 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 last should 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 debt into leave protocol with bad debt and also reclaim seized collateral once the first path works.
  • Token-standard compatibility is not binary. ERC-20 enough to list is a dangerous fiction when transfer semantics import hooks, registries, or callback dispatch that the core protocol never modeled.

Internal linkages

Control surface