Cream Finance: A Protocol Exploited Three Times in One Year

Cream Finance holds an unwanted record: exploited three separate times in 2021, each attack using a different technique. Total losses exceeded $190 million. The protocol had been audited. The team was experienced. And yet three distinct attacker groups each found a way in.

That pattern—repeated compromise through different vectors—is more instructive than any single hack.

February 2021: $37.5 Million via Reentrancy

The first attack came through an integration with Alpha Homora, a leveraged yield farming protocol. The reentrancy bug lived at the boundary between the two protocols.

The vulnerable pattern was a classic: Cream’s borrow function transferred tokens to the caller before updating the borrow accounting. That ordering—external call before state update—is the defining signature of reentrancy risk.

function borrow(uint amount) external {
    require(collateral[msg.sender] >= amount * ratio);
    token.transfer(msg.sender, amount);  // External call first
    debt[msg.sender] += amount;          // State updated after — reentrancy window here
}

An attacker controlling the receiving contract could call back into borrow during that transfer, borrowing again against the same collateral before the first debt was recorded. The protocol followed the Checks-Effects-Interactions pattern in its documentation but not in this code path.

August 2021: $18.8 Million via ERC-1820 Hooks

The second attack was subtler. It exploited AMP, a token that implements the ERC-777/ERC-1820 standard. Unlike standard ERC-20 tokens, ERC-777 tokens call a hook on the recipient during transferFrom. This hook runs before the transfer completes.

Cream allowed AMP as collateral. When the protocol called transferFrom to accept AMP deposits, AMP’s transfer hook fired—giving the attacker’s contract a chance to call back into Cream’s borrow function before the deposit was recorded:

function deposit(address token, uint amount) external {
    // For ERC-777 tokens, this line triggers a callback to the sender
    IERC20(token).transferFrom(msg.sender, address(this), amount);

    // The attacker's callback ran during the line above
    // and borrowed against collateral not yet registered
    collateral[msg.sender][token] += amount;
}

The Cream team had not accounted for tokens with transfer hooks when designing the collateral logic. It is the same fundamental mistake as classic reentrancy, just triggered through a token feature rather than direct ETH transfer.

October 2021: $130 Million via Oracle Manipulation

The third attack was the largest and most operationally complex. The attacker:

  1. Created a custom token and bootstrapped a lending market for it on Cream
  2. Flash-borrowed roughly $500 million in DAI and $2 billion in ETH from multiple sources
  3. Used the borrowed capital to manipulate the spot price of their custom token in a Uniswap pool
  4. Borrowed against the inflated collateral value
  5. Walked away with $130 million across 68 different assets

The oracle vulnerability was using spot reserves from a DEX pool as a price feed:

function getPrice(address token) public view returns (uint) {
    uint reserve0 = pair.reserve0();
    uint reserve1 = pair.reserve1();
    return reserve0 * 1e18 / reserve1;
}

Spot price from a thin pool can be moved dramatically within a single transaction using flash-loaned capital. A time-weighted average price (TWAP) over multiple blocks would have been uneconomical to manipulate at scale—requiring sustained capital lock-up rather than a single atomic transaction.

The Compounding Problem

What makes the Cream saga worth studying is not that any one of these bugs was especially novel. Each is a well-documented vulnerability class. The issue is that the same protocol was not hardened against known attack patterns after the first or second exploit.

Source code audits were conducted, but:

  • The reentrancy in the Alpha Homora integration was in the interaction between two audited systems, not within either one alone
  • The AMP token hook issue required understanding the behavior of an external token contract, not just Cream’s own code
  • The oracle manipulation required modeling what an adversary could do with flash-loaned capital at scale—a threat model most auditors do not apply by default

After each attack, fixes were applied to the specific vulnerable paths. But the broader vulnerability class was never eliminated at the root.

What Changes

For reentrancy, the fix is ordering. State must be updated before external calls are made. Where that is not possible structurally, a reentrancy guard (a mutex stored in contract storage) prevents recursive entry:

uint private locked = 1;

modifier nonReentrant() {
    require(locked == 1, "Reentrant call");
    locked = 2;
    _;
    locked = 1;
}

For token hook risks, the solution is either using pull-based accounting (track deposits separately from transfers) or explicitly blocklisting tokens with non-standard callback behavior. ERC-777 tokens should not be accepted as collateral in protocols not designed to handle hooks.

For oracle manipulation, the standard defense is a TWAP with a sufficiently long window. The tradeoff is price staleness, but for collateral valuation, a slightly lagged price is far safer than a manipulable spot price.

The Auditor’s Limitation

Cream had audits from respected firms. The audits were not useless—they likely caught other issues. But audits are point-in-time assessments of the code as written. They do not model:

  • What happens when two separately-audited protocols interact
  • What external token contracts do during callbacks
  • What an adversary with $2 billion in temporary capital can accomplish

None of these require exotic tooling to understand. They require asking the right adversarial questions during design, not just checking the existing code for known patterns.


References