Skip to content

Commit b36d08a

Browse files
committed
fix: staking miscalculation + test coverage
1 parent 19339aa commit b36d08a

File tree

2 files changed

+167
-40
lines changed

2 files changed

+167
-40
lines changed

contracts/src/arbitration/SortitionModule.sol

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// SPDX-License-Identifier: MIT
22

33
/**
4-
* @custom:authors: [@epiqueras, @unknownunknown1, @shotaronowhere]
4+
* @custom:authors: [@epiqueras, @unknownunknown1, @jaybuidl, @shotaronowhere]
55
* @custom:reviewers: []
66
* @custom:auditors: []
77
* @custom:bounties: []
@@ -281,9 +281,9 @@ contract SortitionModule is ISortitionModule, UUPSProxiable, Initializable {
281281
return (0, 0, false); // Prevent staking beyond MAX_STAKE_PATHS but unstaking is always allowed.
282282
}
283283

284-
pnkWithdrawal = _deleteDelayedStake(_courtID, _account);
285-
286284
if (phase != Phase.staking) {
285+
pnkWithdrawal = _deleteDelayedStake(_courtID, _account);
286+
287287
// Store the stake change as delayed, to be applied when the phase switches back to Staking.
288288
DelayedStake storage delayedStake = delayedStakes[++delayedStakeWriteIndex];
289289
delayedStake.account = _account;
@@ -302,7 +302,7 @@ contract SortitionModule is ISortitionModule, UUPSProxiable, Initializable {
302302
return (pnkDeposit, pnkWithdrawal, true);
303303
}
304304

305-
// Staking phase: set normal stakes or delayed stakes (which may have been already transferred).
305+
// Current phase is Staking: set normal stakes or delayed stakes (which may have been already transferred).
306306
if (_newStake >= currentStake) {
307307
if (!_alreadyTransferred) {
308308
pnkDeposit = _increaseStake(juror, _courtID, _newStake, currentStake);

contracts/test/arbitration/staking.ts

Lines changed: 163 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,7 @@
11
import { ethers, getNamedAccounts, network, deployments } from "hardhat";
22
const { anyValue } = require("@nomicfoundation/hardhat-chai-matchers/withArgs");
33
import { BigNumber } from "ethers";
4-
import {
5-
PNK,
6-
KlerosCore,
7-
DisputeKitClassic,
8-
SortitionModule,
9-
RandomizerRNG,
10-
RandomizerMock,
11-
} from "../../typechain-types";
4+
import { PNK, KlerosCore, SortitionModule, RandomizerRNG, RandomizerMock } from "../../typechain-types";
125
import { expect } from "chai";
136

147
/* eslint-disable no-unused-vars */
@@ -23,7 +16,6 @@ describe("Staking", async () => {
2316
"0x000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000001";
2417

2518
let deployer;
26-
let disputeKit;
2719
let pnk;
2820
let core;
2921
let sortition;
@@ -36,7 +28,6 @@ describe("Staking", async () => {
3628
fallbackToGlobal: true,
3729
keepExistingDeployments: false,
3830
});
39-
disputeKit = (await ethers.getContract("DisputeKitClassic")) as DisputeKitClassic;
4031
pnk = (await ethers.getContract("PNK")) as PNK;
4132
core = (await ethers.getContract("KlerosCore")) as KlerosCore;
4233
sortition = (await ethers.getContract("SortitionModule")) as SortitionModule;
@@ -80,6 +71,136 @@ describe("Staking", async () => {
8071
await sortition.passPhase(); // Drawing -> Staking
8172
};
8273

74+
describe("When stake is increased once", async () => {
75+
before("Setup", async () => {
76+
await deploy();
77+
await reachDrawingPhase();
78+
});
79+
80+
it("Should be outside the Staking phase", async () => {
81+
expect(await sortition.phase()).to.be.equal(1); // Drawing
82+
expect(await sortition.getJurorBalance(deployer, 2)).to.be.deep.equal([PNK(4000), 0, PNK(2000), 2]);
83+
});
84+
85+
describe("When stake is increased", () => {
86+
it("Should transfer PNK but delay the stake increase", async () => {
87+
expect(await sortition.delayedStakeWriteIndex()).to.be.equal(0);
88+
expect(await sortition.delayedStakeReadIndex()).to.be.equal(1);
89+
await pnk.approve(core.address, PNK(1000));
90+
expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0);
91+
await expect(core.setStake(2, PNK(3000)))
92+
.to.emit(sortition, "StakeDelayedAlreadyTransferred")
93+
.withArgs(deployer, 2, PNK(3000));
94+
expect(await sortition.getJurorBalance(deployer, 2)).to.be.deep.equal([PNK(5000), 0, PNK(2000), 2]); // stake does not change
95+
});
96+
97+
it("Should transfer some PNK out of the juror's account", async () => {
98+
expect(await pnk.balanceOf(deployer)).to.be.equal(balanceBefore.sub(PNK(1000))); // PNK is transferred out of the juror's account
99+
});
100+
101+
it("Should store the delayed stake for later", async () => {
102+
expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(1);
103+
expect(await sortition.delayedStakeWriteIndex()).to.be.equal(1);
104+
expect(await sortition.delayedStakeReadIndex()).to.be.equal(1);
105+
expect(await sortition.delayedStakes(1)).to.be.deep.equal([deployer, 2, PNK(3000), true]);
106+
});
107+
});
108+
109+
describe("When the Phase passes back to Staking", () => {
110+
before("Setup", async () => {
111+
await reachStakingPhaseAfterDrawing();
112+
balanceBefore = await pnk.balanceOf(deployer);
113+
});
114+
115+
it("Should execute the delayed stakes", async () => {
116+
await expect(sortition.executeDelayedStakes(10))
117+
.to.emit(sortition, "StakeSet")
118+
.withArgs(deployer, 2, PNK(3000))
119+
.to.not.emit(sortition, "StakeDelayedNotTransferred")
120+
.to.not.emit(sortition, "StakeDelayedAlreadyTransferred")
121+
.to.not.emit(sortition, "StakeDelayedAlreadyTransferredWithdrawn");
122+
expect(await sortition.getJurorBalance(deployer, 2)).to.be.deep.equal([
123+
PNK(5000),
124+
PNK(300), // we're the only juror so we are drawn 3 times
125+
PNK(3000),
126+
2,
127+
]); // stake unchanged, delayed
128+
expect(await sortition.delayedStakeWriteIndex()).to.be.equal(1);
129+
expect(await sortition.delayedStakeReadIndex()).to.be.equal(2);
130+
expect(await sortition.delayedStakes(1)).to.be.deep.equal([ethers.constants.AddressZero, 0, 0, false]); // the 1st delayed stake got deleted
131+
expect(await sortition.delayedStakes(2)).to.be.deep.equal([ethers.constants.AddressZero, 0, 0, false]); // the 2nd delayed stake got deleted
132+
expect(await sortition.latestDelayedStakeIndex(deployer, 1)).to.be.equal(0); // no delayed stakes left
133+
});
134+
135+
it("Should not transfer any PNK", async () => {
136+
expect(await pnk.balanceOf(deployer)).to.be.equal(balanceBefore); // No PNK transfer
137+
});
138+
});
139+
});
140+
141+
describe("When stake is decreased once", async () => {
142+
before("Setup", async () => {
143+
await deploy();
144+
await reachDrawingPhase();
145+
});
146+
147+
it("Should be outside the Staking phase", async () => {
148+
expect(await sortition.phase()).to.be.equal(1); // Drawing
149+
expect(await sortition.getJurorBalance(deployer, 2)).to.be.deep.equal([PNK(4000), 0, PNK(2000), 2]);
150+
});
151+
152+
describe("When stake is decreased", async () => {
153+
it("Should delay the stake decrease", async () => {
154+
expect(await sortition.delayedStakeWriteIndex()).to.be.equal(0);
155+
expect(await sortition.delayedStakeReadIndex()).to.be.equal(1);
156+
expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0);
157+
await expect(core.setStake(2, PNK(1000)))
158+
.to.emit(sortition, "StakeDelayedNotTransferred")
159+
.withArgs(deployer, 2, PNK(1000));
160+
expect(await sortition.getJurorBalance(deployer, 2)).to.be.deep.equal([PNK(4000), 0, PNK(2000), 2]); // stake unchanged, delayed
161+
});
162+
163+
it("Should not transfer any PNK", async () => {
164+
expect(await pnk.balanceOf(deployer)).to.be.equal(balanceBefore); // No PNK transfer yet
165+
});
166+
167+
it("Should store the delayed stake for later", async () => {
168+
expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(1);
169+
expect(await sortition.delayedStakeWriteIndex()).to.be.equal(1);
170+
expect(await sortition.delayedStakeReadIndex()).to.be.equal(1);
171+
expect(await sortition.delayedStakes(1)).to.be.deep.equal([deployer, 2, PNK(1000), false]);
172+
});
173+
});
174+
175+
describe("When the Phase passes back to Staking", () => {
176+
before("Setup", async () => {
177+
await reachStakingPhaseAfterDrawing();
178+
balanceBefore = await pnk.balanceOf(deployer);
179+
});
180+
181+
it("Should execute the delayed stakes by withdrawing PNK and reducing the stakes", async () => {
182+
await expect(sortition.executeDelayedStakes(10))
183+
.to.emit(sortition, "StakeSet")
184+
.withArgs(deployer, 2, PNK(1000));
185+
expect(await sortition.getJurorBalance(deployer, 2)).to.be.deep.equal([
186+
PNK(3000),
187+
PNK(300), // we're the only juror so we are drawn 3 times
188+
PNK(1000),
189+
2,
190+
]); // stake unchanged, delayed
191+
expect(await sortition.delayedStakeWriteIndex()).to.be.equal(1);
192+
expect(await sortition.delayedStakeReadIndex()).to.be.equal(2);
193+
expect(await sortition.delayedStakes(1)).to.be.deep.equal([ethers.constants.AddressZero, 0, 0, false]); // the 1st delayed stake got deleted
194+
expect(await sortition.delayedStakes(2)).to.be.deep.equal([ethers.constants.AddressZero, 0, 0, false]); // the 2nd delayed stake got deleted
195+
expect(await sortition.latestDelayedStakeIndex(deployer, 1)).to.be.equal(0); // no delayed stakes left
196+
});
197+
198+
it("Should withdraw some PNK", async () => {
199+
expect(await pnk.balanceOf(deployer)).to.be.equal(balanceBefore.add(PNK(1000))); // No PNK transfer yet
200+
});
201+
});
202+
});
203+
83204
describe("When stake is decreased then increased back", async () => {
84205
before("Setup", async () => {
85206
await deploy();
@@ -139,9 +260,12 @@ describe("Staking", async () => {
139260
});
140261

141262
describe("When the Phase passes back to Staking", () => {
142-
it("Should execute the delayed stakes but the stakes should remain the same", async () => {
263+
before("Setup", async () => {
143264
await reachStakingPhaseAfterDrawing();
144265
balanceBefore = await pnk.balanceOf(deployer);
266+
});
267+
268+
it("Should execute the delayed stakes but the stakes should remain the same", async () => {
145269
await expect(sortition.executeDelayedStakes(10))
146270
.to.emit(sortition, "StakeSet")
147271
.withArgs(deployer, 2, PNK(2000));
@@ -223,33 +347,36 @@ describe("Staking", async () => {
223347
expect(await sortition.delayedStakes(2)).to.be.deep.equal([deployer, 2, PNK(2000), false]);
224348
});
225349
});
226-
});
227350

228-
describe("When the Phase passes back to Staking", () => {
229-
it("Should execute the delayed stakes but the stakes should remain the same", async () => {
230-
await reachStakingPhaseAfterDrawing();
231-
balanceBefore = await pnk.balanceOf(deployer);
232-
await expect(sortition.executeDelayedStakes(10))
233-
.to.emit(sortition, "StakeSet")
234-
.withArgs(deployer, 2, PNK(2000))
235-
.to.not.emit(sortition, "StakeDelayedNotTransferred")
236-
.to.not.emit(sortition, "StakeDelayedAlreadyTransferred")
237-
.to.not.emit(sortition, "StakeDelayedAlreadyTransferredWithdrawn");
238-
expect(await sortition.getJurorBalance(deployer, 2)).to.be.deep.equal([
239-
PNK(4000),
240-
PNK(300), // we're the only juror so we are drawn 3 times
241-
PNK(2000),
242-
2,
243-
]); // stake unchanged, delayed
244-
expect(await sortition.delayedStakeWriteIndex()).to.be.equal(2);
245-
expect(await sortition.delayedStakeReadIndex()).to.be.equal(3);
246-
expect(await sortition.delayedStakes(1)).to.be.deep.equal([ethers.constants.AddressZero, 0, 0, false]); // the 1st delayed stake got deleted
247-
expect(await sortition.delayedStakes(2)).to.be.deep.equal([ethers.constants.AddressZero, 0, 0, false]); // the 2nd delayed stake got deleted
248-
expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // no delayed stakes left
249-
});
351+
describe("When the Phase passes back to Staking", () => {
352+
before("Setup", async () => {
353+
await reachStakingPhaseAfterDrawing();
354+
balanceBefore = await pnk.balanceOf(deployer);
355+
});
250356

251-
it("Should not transfer any PNK", async () => {
252-
expect(await pnk.balanceOf(deployer)).to.be.equal(balanceBefore); // No PNK transfer yet
357+
it("Should execute the delayed stakes but the stakes should remain the same", async () => {
358+
await expect(sortition.executeDelayedStakes(10))
359+
.to.emit(sortition, "StakeSet")
360+
.withArgs(deployer, 2, PNK(2000))
361+
.to.not.emit(sortition, "StakeDelayedNotTransferred")
362+
.to.not.emit(sortition, "StakeDelayedAlreadyTransferred")
363+
.to.not.emit(sortition, "StakeDelayedAlreadyTransferredWithdrawn");
364+
expect(await sortition.getJurorBalance(deployer, 2)).to.be.deep.equal([
365+
PNK(4000),
366+
PNK(300), // we're the only juror so we are drawn 3 times
367+
PNK(2000),
368+
2,
369+
]); // stake unchanged, delayed
370+
expect(await sortition.delayedStakeWriteIndex()).to.be.equal(2);
371+
expect(await sortition.delayedStakeReadIndex()).to.be.equal(3);
372+
expect(await sortition.delayedStakes(1)).to.be.deep.equal([ethers.constants.AddressZero, 0, 0, false]); // the 1st delayed stake got deleted
373+
expect(await sortition.delayedStakes(2)).to.be.deep.equal([ethers.constants.AddressZero, 0, 0, false]); // the 2nd delayed stake got deleted
374+
expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // no delayed stakes left
375+
});
376+
377+
it("Should not transfer any PNK", async () => {
378+
expect(await pnk.balanceOf(deployer)).to.be.equal(balanceBefore); // No PNK transfer yet
379+
});
253380
});
254381
});
255382
});

0 commit comments

Comments
 (0)