Fuse's cEther sent the borrowed ETH to the borrower before writing the loan into storage — the classic violation of checks-effects-interactions. The ETH transfer hands control to the borrower's contract, and from inside that callback the attacker called exitMarket(). Since his borrow had not been recorded yet, the collateral check saw an account with no debt and released 100% of his collateral. He kept the loan and the collateral, and repeated it across pool after pool.
function borrowFresh(address payable borrower, uint borrowAmount) internal returns (uint) { ... doTransferOut(borrower, borrowAmount); // ❶ external call FIRST accountBorrows[borrower].principal = vars.accountBorrowsNew; // ❷ state after accountBorrows[borrower].interestIndex = borrowIndex; totalBorrows = vars.totalBorrowsNew; ... } // inside the attacker's receive(): comptroller.exitMarket(cToken); → "does this account have outstanding borrows?" → not written yet → no → collateral released // ✅ two independent fixes, and you want both: // 1. move doTransferOut() to the LAST line (checks-effects-interactions) // 2. a shared reentrancy guard that also covers exitMarket()
Entries in the SAFE database that describe this failure. These share its failure class.
Every figure on this page comes from the post-mortems above, not from us. Losses are US dollars at the time of the incident.
Reentrancy is one of the 203 classes the SaferICO scanner checks for. It will not review your signing process — but it will read your Solidity.