Skip to content

Latest commit

 

History

History

README.md

title description type network date loss_usd returned_usd tags subcategory vulnerable_contracts tokens_lost attacker_addresses malicious_token attack_block reproduction_command attack_txs sources
Cream Finance
Bypassing reentrancy guards through cross-contract token hooks
Exploit
ethereum
2021-08-30
18000000
0
reentrancy
0xD06527D5e56A3495252A528C4987003b712860eE
0x2Db6c82CE72C8d7D770ba1b5F5Ed0b6E075066d6
AMP
WETH
crETH
0xcE1F4B4F17224ec6df16Eeb1e3e5321c54Ff6EDe
13125071
forge test --match-contract Exploit_CreamFinance -vvv
0xa9a1b8ea288eb9ad315088f17f7c7386b9989c95b4d13c81b69d5ddad7ffe61e

Step-by-step

  1. Add the contract to the universal interface registry
  2. Request a Flashloan
  3. Swap WETH for ETH
  4. Mint crETH tokens
  5. Enter Markets using crETH as collateral
  6. Borrow crAMP against crETH
  7. Deploy a minion contract
  8. Reenter borrowing crETH in the AMP receive hook
  9. The minion liquidates the main contract (commander).
  10. The liquidated amount is transferred from the minion to the commander.
  11. Selfdestruct the minion
  12. Swap ETH for WETH
  13. Repay the loan

Detailed Description

The attacker reentered multiple pools borrowing WETH and AMP repeatedly over 17 txns.

This was possible mainly because the lending protocol transfers borrowed tokens before updating the internal accountancy values. In addition to this, as hookable tokens were used, the attacker was able to trigger a reentrant call to different contract which state was related with the first contract's.

    function borrow(uint borrowAmount) external returns (uint) {
        return borrowInternal(borrowAmount);
    }

    function borrowInternal(uint borrowAmount) internal nonReentrant returns (uint) {
        ...

        return borrowFresh(msg.sender, borrowAmount);
    }

    function borrowFresh(address payable borrower, uint borrowAmount) internal returns (uint) {
        ...

        doTransferOut(borrower, borrowAmount);

        // We write the previously calculated values into storage
        accountBorrows[borrower].principal = vars.accountBorrowsNew;
        accountBorrows[borrower].interestIndex = borrowIndex;
        totalBorrows = vars.totalBorrowsNew;

        // We emit a Borrow event
        emit Borrow(borrower, borrowAmount, vars.accountBorrowsNew, vars.totalBorrowsNew);

        // We call the defense hook
        comptroller.borrowVerify(address(this), borrower, borrowAmount);
        return uint(Error.NO_ERROR);
    }

Because the reentrancy mutex only protects functions that include that modifier, the attacker was able to call another contract borrowing undercollateralized amount.

Possible mitigations

  • Respect the checks-effects-interactions pattern whenever it's possible taking into account that a reentrancy mutex does not protect against cross-contract attacks.

Related

  • Fei Protocol - Same borrowFresh pattern with cross-function reentrancy
  • Hundred Finance - Same borrowFresh pattern exploited via ERC667 token hooks
  • DFX Finance - Reentrancy through flash loan callback to manipulate balances