From 8cb7245a4cd49064165d0203e1071da9d13eaaea Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Sat, 9 Sep 2023 14:48:03 +0800 Subject: [PATCH] test: reproduction of the staking and locked stakes bug --- contracts/test/arbitration/draw.ts | 233 +++++++++++++++++++++++++---- cspell.json | 1 + 2 files changed, 206 insertions(+), 28 deletions(-) diff --git a/contracts/test/arbitration/draw.ts b/contracts/test/arbitration/draw.ts index 40840832b..5e835857b 100644 --- a/contracts/test/arbitration/draw.ts +++ b/contracts/test/arbitration/draw.ts @@ -1,17 +1,16 @@ import { anyValue } from "@nomicfoundation/hardhat-chai-matchers/withArgs"; import { deployments, ethers, getNamedAccounts, network } from "hardhat"; -import { BigNumber, ContractTransaction, Wallet } from "ethers"; +import { BigNumber, ContractReceipt, ContractTransaction, Wallet } from "ethers"; import { PNK, KlerosCore, ArbitrableExample, HomeGateway, DisputeKitClassic, - RandomizerRNG, - RandomizerMock, SortitionModule, } from "../../typechain-types"; import { expect } from "chai"; +import { DrawEvent } from "../../typechain-types/src/kleros-v1/kleros-liquid-xdai/XKlerosLiquidV2"; /* eslint-disable no-unused-vars */ /* eslint-disable no-unused-expressions */ // https://github.com/standard/standard/issues/690#issuecomment-278533482 @@ -49,7 +48,11 @@ describe("Draw Benchmark", async () => { let homeGateway; let sortitionModule; let rng; - let randomizer; + let parentCourtMinStake: BigNumber; + let childCourtMinStake: BigNumber; + const RANDOM = BigNumber.from("61688911660239508166491237672720926005752254046266901728404745669596507231249"); + const PARENT_COURT = 1; + const CHILD_COURT = 2; beforeEach("Setup", async () => { ({ deployer, relayer } = await getNamedAccounts()); @@ -63,11 +66,25 @@ describe("Draw Benchmark", async () => { core = (await ethers.getContract("KlerosCore")) as KlerosCore; homeGateway = (await ethers.getContract("HomeGatewayToEthereum")) as HomeGateway; arbitrable = (await ethers.getContract("ArbitrableExample")) as ArbitrableExample; - rng = (await ethers.getContract("RandomizerRNG")) as RandomizerRNG; - randomizer = (await ethers.getContract("RandomizerMock")) as RandomizerMock; sortitionModule = (await ethers.getContract("SortitionModule")) as SortitionModule; - // CourtId 2 + parentCourtMinStake = await core + .GENERAL_COURT() + .then((courtId) => core.courts(courtId)) + .then((court) => court.minStake); + + childCourtMinStake = BigNumber.from(10).pow(20).mul(3); // 300 PNK + + // Make the tests more deterministic with this dummy RNG + rng = await deployments.deploy("IncrementalNG", { + from: deployer, + args: [RANDOM], + log: true, + }); + + await sortitionModule.changeRandomNumberGenerator(rng.address, 20); + + // CourtId 2 = CHILD_COURT const minStake = BigNumber.from(10).pow(20).mul(3); // 300 PNK const alpha = 10000; const feeForJuror = BigNumber.from(10).pow(17); @@ -84,16 +101,24 @@ describe("Draw Benchmark", async () => { ); }); + type CountedDraws = { [address: string]: number }; type SetStake = (wallet: Wallet) => Promise; type ExpectFromDraw = (drawTx: Promise) => Promise; - const draw = async (setStake: SetStake, createDisputeCourtId: string, expectFromDraw: ExpectFromDraw) => { + const draw = async ( + stake: SetStake, + createDisputeCourtId: number, + expectFromDraw: ExpectFromDraw, + unstake: SetStake + ) => { const arbitrationCost = ONE_TENTH_ETH.mul(3); const [bridger] = await ethers.getSigners(); + const wallets: Wallet[] = []; // Stake some jurors for (let i = 0; i < 16; i++) { const wallet = ethers.Wallet.createRandom().connect(ethers.provider); + wallets.push(wallet); await bridger.sendTransaction({ to: wallet.address, @@ -106,7 +131,7 @@ describe("Draw Benchmark", async () => { await pnk.connect(wallet).approve(core.address, ONE_THOUSAND_PNK.mul(10), { gasLimit: 300000 }); - await setStake(wallet); + await stake(wallet); } // Create a dispute @@ -144,75 +169,227 @@ describe("Draw Benchmark", async () => { await network.provider.send("evm_mine"); } - await randomizer.relay(rng.address, 0, ethers.utils.randomBytes(32)); await sortitionModule.passPhase(); // Generating -> Drawing await expectFromDraw(core.draw(0, 1000, { gasLimit: 1000000 })); + + await network.provider.send("evm_increaseTime", [2000]); // Wait for maxDrawingTime + await sortitionModule.passPhase(); // Drawing -> Staking + expect(await sortitionModule.phase()).to.equal(Phase.staking); + + // Unstake jurors + for (const wallet of wallets) { + await unstake(wallet); + } + }; + + const countDraws = async (blockNumber: number) => { + const draws: Array = await core.queryFilter(core.filters.Draw(), blockNumber, blockNumber); + return draws.reduce((acc: { [address: string]: number }, draw) => { + const address = draw.args._address; + acc[address] = acc[address] ? acc[address] + 1 : 1; + return acc; + }, {}); }; it("Stakes in parent court and should draw jurors in parent court", async () => { - const setStake = async (wallet: Wallet) => { - await core.connect(wallet).setStake(1, ONE_THOUSAND_PNK.mul(5), { gasLimit: 5000000 }); + const stake = async (wallet: Wallet) => { + await core.connect(wallet).setStake(PARENT_COURT, ONE_THOUSAND_PNK.mul(5), { gasLimit: 5000000 }); + + expect(await core.getJurorBalance(wallet.address, 1)).to.deep.equal([ + ONE_THOUSAND_PNK.mul(5), // stakedInCourt + 0, // lockedInCourt + PARENT_COURT, // nbOfCourts + ]); }; - + let countedDraws: CountedDraws; const expectFromDraw = async (drawTx: Promise) => { - await expect(drawTx) + const tx = await (await drawTx).wait(); + expect(tx) .to.emit(core, "Draw") .withArgs(anyValue, 0, 0, 0) .to.emit(core, "Draw") .withArgs(anyValue, 0, 0, 1) .to.emit(core, "Draw") .withArgs(anyValue, 0, 0, 2); + + countedDraws = await countDraws(tx.blockNumber); + for (const [address, draws] of Object.entries(countedDraws)) { + expect(await core.getJurorBalance(address, PARENT_COURT)).to.deep.equal([ + ONE_THOUSAND_PNK.mul(5), // stakedInCourt + parentCourtMinStake.mul(draws), // lockedInCourt + 1, // nbOfCourts + ]); + expect(await core.getJurorBalance(address, CHILD_COURT)).to.deep.equal([ + 0, // stakedInCourt + 0, // lockedInCourt + 1, // nbOfCourts + ]); + } }; - await draw(setStake, "1", expectFromDraw); + const unstake = async (wallet: Wallet) => { + await core.connect(wallet).setStake(PARENT_COURT, 0, { gasLimit: 5000000 }); + const locked = parentCourtMinStake.mul(countedDraws[wallet.address] ?? 0); + expect( + await core.getJurorBalance(wallet.address, PARENT_COURT), + "Drawn jurors have a locked stake in the parent court" + ).to.deep.equal([ + 0, // stakedInCourt + locked, // lockedInCourt + 0, // nbOfCourts + ]); + expect( + await core.getJurorBalance(wallet.address, CHILD_COURT), + "No locked stake in the child court" + ).to.deep.equal([ + 0, // stakedInCourt + 0, // lockedInCourt + 0, // nbOfCourts + ]); + }; + + await draw(stake, PARENT_COURT, expectFromDraw, unstake); }); it("Stakes in parent court and should draw nobody in subcourt", async () => { - const setStake = async (wallet: Wallet) => { - await core.connect(wallet).setStake(1, ONE_THOUSAND_PNK.mul(5), { gasLimit: 5000000 }); + const stake = async (wallet: Wallet) => { + await core.connect(wallet).setStake(PARENT_COURT, ONE_THOUSAND_PNK.mul(5), { gasLimit: 5000000 }); }; const expectFromDraw = async (drawTx: Promise) => { await expect(drawTx).to.not.emit(core, "Draw"); }; - await draw(setStake, "2", expectFromDraw); + const unstake = async (wallet: Wallet) => { + await core.connect(wallet).setStake(PARENT_COURT, 0, { gasLimit: 5000000 }); + expect( + await core.getJurorBalance(wallet.address, PARENT_COURT), + "No locked stake in the parent court" + ).to.deep.equal([ + 0, // stakedInCourt + 0, // lockedInCourt + 0, // nbOfCourts + ]); + expect( + await core.getJurorBalance(wallet.address, CHILD_COURT), + "No locked stake in the child court" + ).to.deep.equal([ + 0, // stakedInCourt + 0, // lockedInCourt + 0, // nbOfCourts + ]); + }; + + await draw(stake, CHILD_COURT, expectFromDraw, unstake); }); it("Stakes in subcourt and should draw jurors in parent court", async () => { - const setStake = async (wallet: Wallet) => { - await core.connect(wallet).setStake(2, ONE_THOUSAND_PNK.mul(5), { gasLimit: 5000000 }); + const stake = async (wallet: Wallet) => { + await core.connect(wallet).setStake(CHILD_COURT, ONE_THOUSAND_PNK.mul(5), { gasLimit: 5000000 }); }; - + let countedDraws: CountedDraws; const expectFromDraw = async (drawTx: Promise) => { - await expect(drawTx) + const tx = await (await drawTx).wait(); + expect(tx) .to.emit(core, "Draw") .withArgs(anyValue, 0, 0, 0) .to.emit(core, "Draw") .withArgs(anyValue, 0, 0, 1) .to.emit(core, "Draw") .withArgs(anyValue, 0, 0, 2); + + countedDraws = await countDraws(tx.blockNumber); + for (const [address, draws] of Object.entries(countedDraws)) { + expect(await core.getJurorBalance(address, PARENT_COURT)).to.deep.equal([ + 0, // stakedInCourt + parentCourtMinStake.mul(draws), // lockedInCourt + 1, // nbOfCourts + ]); + expect(await core.getJurorBalance(address, CHILD_COURT)).to.deep.equal([ + ONE_THOUSAND_PNK.mul(5), // stakedInCourt + 0, // lockedInCourt + 1, // nbOfCourts + ]); + } }; - await draw(setStake, "1", expectFromDraw); + const unstake = async (wallet: Wallet) => { + await core.connect(wallet).setStake(CHILD_COURT, 0, { gasLimit: 5000000 }); + const locked = parentCourtMinStake.mul(countedDraws[wallet.address] ?? 0); + console.log(`draws for ${wallet.address}: ${countedDraws[wallet.address] ?? 0}, locked: ${locked}`); + expect( + await core.getJurorBalance(wallet.address, PARENT_COURT), + "No locked stake in the parent court" + ).to.deep.equal([ + 0, // stakedInCourt + 0, // lockedInCourt + 0, // nbOfCourts + ]); + expect( + await core.getJurorBalance(wallet.address, CHILD_COURT), + "Drawn jurors have a locked stake in the child court" + ).to.deep.equal([ + 0, // stakedInCourt + locked, // lockedInCourt + 0, // nbOfCourts + ]); + }; + + await draw(stake, PARENT_COURT, expectFromDraw, unstake); }); it("Stakes in subcourt and should draw jurors in subcourt", async () => { - const setStake = async (wallet: Wallet) => { - await core.connect(wallet).setStake(2, ONE_THOUSAND_PNK.mul(5), { gasLimit: 5000000 }); + const stake = async (wallet: Wallet) => { + await core.connect(wallet).setStake(CHILD_COURT, ONE_THOUSAND_PNK.mul(5), { gasLimit: 5000000 }); }; - + let countedDraws: CountedDraws; const expectFromDraw = async (drawTx: Promise) => { - await expect(drawTx) + const tx = await (await drawTx).wait(); + expect(tx) .to.emit(core, "Draw") .withArgs(anyValue, 0, 0, 0) .to.emit(core, "Draw") .withArgs(anyValue, 0, 0, 1) .to.emit(core, "Draw") .withArgs(anyValue, 0, 0, 2); + + countedDraws = await countDraws(tx.blockNumber); + for (const [address, draws] of Object.entries(countedDraws)) { + expect(await core.getJurorBalance(address, PARENT_COURT)).to.deep.equal([ + 0, // stakedInCourt + 0, // lockedInCourt + 1, // nbOfCourts + ]); + expect(await core.getJurorBalance(address, CHILD_COURT)).to.deep.equal([ + ONE_THOUSAND_PNK.mul(5), // stakedInCourt + childCourtMinStake.mul(draws), // lockedInCourt + 1, // nbOfCourts + ]); + } + }; + + const unstake = async (wallet: Wallet) => { + await core.connect(wallet).setStake(CHILD_COURT, 0, { gasLimit: 5000000 }); + const locked = childCourtMinStake.mul(countedDraws[wallet.address] ?? 0); + expect( + await core.getJurorBalance(wallet.address, PARENT_COURT), + "No locked stake in the parent court" + ).to.deep.equal([ + 0, // stakedInCourt + 0, // lockedInCourt + 0, // nbOfCourts + ]); + expect( + await core.getJurorBalance(wallet.address, CHILD_COURT), + "Drawn jurors have a locked stake in the child court" + ).to.deep.equal([ + 0, // stakedInCourt + locked, // lockedInCourt + 0, // nbOfCourts + ]); }; - await draw(setStake, "2", expectFromDraw); + await draw(stake, CHILD_COURT, expectFromDraw, unstake); }); }); diff --git a/cspell.json b/cspell.json index 73009f268..1aa319e95 100644 --- a/cspell.json +++ b/cspell.json @@ -36,6 +36,7 @@ "typechain", "uncommify", "Unslashed", + "unstake", "viem", "wagmi" ],