This is a demo to replay the Rari & Fei protocol expoloit happened on Apr-30-2022
https://etherscan.io/tx/0xadbe5cf9269a001d50990d0c29075b402bcc3a0b0f3258821881621b787b35c6
npm install
npx hardhat run scripts/execute.js
This is a typical reentrance exploit where the exitMarket
is implemented without reentrance lock and the transfer of ETH is using call
instead of transfer
which only limit the gas to 2300.
- exploite first flashloan 80000 wstETH and 50000 WETH
- deposit 80000 wstETH into the wstETH vault and borrow 2392 ETH from fETH vault
- exit the market and withdraw 80000 wstETH from the vault.
- the exploiter repeat the above step to drain the fund.
Now, why the exploiter can exit the market while still have borrow balance ?
- The source code of transfering fund to user is using
call
instead oftransfer
which limit gas usage to 2300
function doTransferOut(address payable to, uint amount) internal {
// Send the Ether and revert on failure
(bool success, ) = to.call.value(amount)("");
require(success, "doTransferOut failed");
}
- There is no reentrance lock in
exitMarket
function exitMarket(address cTokenAddress) external returns (uint) {
...........
}
- The borrowing source code is not following Checks-Effects-Interactions pattern which is the major reason making this exploit possible. Checks-Effects-Interactions pattern is one of the most effective way to protect against re-entrancy exploit without side effect.
function borrowFresh(address payable borrower, uint borrowAmount) internal returns (uint) {
.........more codes
doTransferOut(borrower, borrowAmount);
/* We write the previously calculated values into storage */
accountBorrows[borrower].principal = vars.accountBorrowsNew;
accountBorrows[borrower].interestIndex = borrowIndex;
totalBorrows = vars.totalBorrowsNew;
return uint(Error.NO_ERROR);
}
This codebase is for demonstration purposes only