diff --git a/contracts/src/arbitration/SortitionModuleBase.sol b/contracts/src/arbitration/SortitionModuleBase.sol index 69bdefc8b..387ee1b44 100644 --- a/contracts/src/arbitration/SortitionModuleBase.sol +++ b/contracts/src/arbitration/SortitionModuleBase.sol @@ -8,11 +8,14 @@ import {IDisputeKit} from "./interfaces/IDisputeKit.sol"; import {Initializable} from "../proxy/Initializable.sol"; import {UUPSProxiable} from "../proxy/UUPSProxiable.sol"; import {IRNG} from "../rng/IRNG.sol"; +import {DelayedStakes} from "../libraries/DelayedStakes.sol"; import "../libraries/Constants.sol"; /// @title SortitionModuleBase /// @dev A factory of trees that keeps track of staked values for sortition. abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSProxiable { + using DelayedStakes for DelayedStakes.Queue; + // ************************************* // // * Enums / Structs * // // ************************************* // @@ -26,13 +29,6 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr mapping(uint256 nodeIndex => bytes32 stakePathID) nodeIndexesToIDs; } - struct DelayedStake { - address account; // The address of the juror. - uint96 courtID; // The ID of the court. - uint256 stake; // The new stake. - bool alreadyTransferred; // DEPRECATED. True if tokens were already transferred before delayed stake's execution. - } - struct Juror { uint96[] courtIDs; // The IDs of courts where the juror's stake path ends. A stake path is a path from the general court to a court the juror directly staked in using `_setStake`. uint256 stakedPnk; // The juror's total amount of tokens staked in subcourts. PNK balance including locked PNK and penalty deductions. @@ -54,12 +50,9 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr IRNG public rng; // The random number generator. uint256 public randomNumber; // Random number returned by RNG. uint256 public rngLookahead; // DEPRECATED: to be removed in the next redeploy - uint256 public delayedStakeWriteIndex; // The index of the last `delayedStake` item that was written to the array. 0 index is skipped. - uint256 public delayedStakeReadIndex; // The index of the next `delayedStake` item that should be processed. Starts at 1 because 0 index is skipped. + DelayedStakes.Queue public delayedStakesQueue; // Queue for managing delayed stakes during Drawing phase. mapping(bytes32 treeHash => SortitionSumTree) sortitionSumTrees; // The mapping trees by keys. mapping(address account => Juror) public jurors; // The jurors. - mapping(uint256 => DelayedStake) public delayedStakes; // Stores the stakes that were changed during Drawing phase, to update them when the phase is switched to Staking. - mapping(address jurorAccount => mapping(uint96 courtId => uint256)) public latestDelayedStakeIndex; // DEPRECATED. Maps the juror to its latest delayed stake. If there is already a delayed stake for this juror then it'll be replaced. latestDelayedStakeIndex[juror][courtID]. // ************************************* // // * Events * // @@ -111,7 +104,7 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr maxDrawingTime = _maxDrawingTime; lastPhaseChange = block.timestamp; rng = _rng; - delayedStakeReadIndex = 1; + delayedStakesQueue.initialize(); } // ************************************* // @@ -200,19 +193,9 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr /// @param _iterations The number of delayed stakes to execute. function executeDelayedStakes(uint256 _iterations) external { if (phase != Phase.staking) revert NotStakingPhase(); - if (delayedStakeWriteIndex < delayedStakeReadIndex) revert NoDelayedStakeToExecute(); - - uint256 actualIterations = (delayedStakeReadIndex + _iterations) - 1 > delayedStakeWriteIndex - ? (delayedStakeWriteIndex - delayedStakeReadIndex) + 1 - : _iterations; - uint256 newDelayedStakeReadIndex = delayedStakeReadIndex + actualIterations; - - for (uint256 i = delayedStakeReadIndex; i < newDelayedStakeReadIndex; i++) { - DelayedStake storage delayedStake = delayedStakes[i]; - core.setStakeBySortitionModule(delayedStake.account, delayedStake.courtID, delayedStake.stake); - delete delayedStakes[i]; + if (!delayedStakesQueue.execute(core, _iterations)) { + revert NoDelayedStakeToExecute(); } - delayedStakeReadIndex = newDelayedStakeReadIndex; } function createDisputeHook(uint256 /*_disputeID*/, uint256 /*_roundID*/) external override onlyByCore { @@ -262,10 +245,7 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr if (phase != Phase.staking) { // Store the stake change as delayed, to be applied when the phase switches back to Staking. - DelayedStake storage delayedStake = delayedStakes[++delayedStakeWriteIndex]; - delayedStake.account = _account; - delayedStake.courtID = _courtID; - delayedStake.stake = _newStake; + delayedStakesQueue.add(_account, _courtID, _newStake); emit StakeDelayed(_account, _courtID, _newStake); return (pnkDeposit, pnkWithdrawal, StakingResult.Delayed); } @@ -547,6 +527,16 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr } } + /// @dev Get a specific delayed stake by index + function delayedStakes(uint256 index) external view returns (DelayedStakes.Stake memory) { + return delayedStakesQueue.stakes[index]; + } + + /// @dev Get the number of pending delayed stakes + function pendingDelayedStakesCount() external view returns (uint256) { + return delayedStakesQueue.pendingStakesCount(); + } + // ************************************* // // * Internal * // // ************************************* // diff --git a/contracts/src/libraries/DelayedStakes.sol b/contracts/src/libraries/DelayedStakes.sol new file mode 100644 index 000000000..4c79dce5b --- /dev/null +++ b/contracts/src/libraries/DelayedStakes.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.24; + +import {KlerosCore} from "../arbitration/KlerosCore.sol"; + +/// @title DelayedStakes +/// @notice Library for managing delayed stakes in the sortition module +library DelayedStakes { + struct Stake { + address account; // The address of the juror. + uint96 courtID; // The ID of the court. + uint256 stake; // The new stake. + bool alreadyTransferred; // DEPRECATED. True if tokens were already transferred before delayed stake's execution. + } + + struct Queue { + uint256 writeIndex; // The index of the last `delayedStake` item that was written to the array. 0 index is skipped. + uint256 readIndex; // The index of the next `delayedStake` item that should be processed. Starts at 1 because 0 index is skipped. + mapping(uint256 => Stake) stakes; // The delayed stakes storage + mapping(address jurorAccount => mapping(uint96 courtId => uint256)) latestDelayedStakeIndex; // DEPRECATED. Maps the juror to its latest delayed stake. + } + + /// @notice Initialize the queue + /// @param queue The queue to initialize + function initialize(Queue storage queue) internal { + queue.readIndex = 1; // Skip index 0 + } + + /// @notice Add a new delayed stake to the queue + /// @param queue The delayed stakes queue + /// @param account The address of the juror + /// @param courtID The ID of the court + /// @param stake The new stake amount + function add(Queue storage queue, address account, uint96 courtID, uint256 stake) internal { + uint256 newWriteIndex = ++queue.writeIndex; + Stake storage delayedStake = queue.stakes[newWriteIndex]; + delayedStake.account = account; + delayedStake.courtID = courtID; + delayedStake.stake = stake; + } + + /// @notice Execute pending delayed stakes + /// @param queue The delayed stakes queue + /// @param core The KlerosCore instance + /// @param iterations The maximum number of stakes to execute + /// @return success True if there were stakes to execute, false otherwise + function execute(Queue storage queue, KlerosCore core, uint256 iterations) internal returns (bool success) { + if (queue.writeIndex < queue.readIndex) { + return false; // Nothing to execute. + } + + uint256 actualIterations = calculateIterations(queue.writeIndex, queue.readIndex, iterations); + uint256 newReadIndex = queue.readIndex + actualIterations; + + for (uint256 i = queue.readIndex; i < newReadIndex; i++) { + Stake storage delayedStake = queue.stakes[i]; + core.setStakeBySortitionModule(delayedStake.account, delayedStake.courtID, delayedStake.stake); + delete queue.stakes[i]; + } + + queue.readIndex = newReadIndex; + return true; + } + + /// @notice Calculate the actual number of iterations to process + /// @param writeIndex The current write index + /// @param readIndex The current read index + /// @param requestedIterations The requested number of iterations + /// @return The actual number of iterations that can be processed + function calculateIterations( + uint256 writeIndex, + uint256 readIndex, + uint256 requestedIterations + ) internal pure returns (uint256) { + if (readIndex + requestedIterations - 1 > writeIndex) { + return writeIndex - readIndex + 1; + } else { + return requestedIterations; + } + } + + /// @notice Get the number of pending stakes + /// @param queue The delayed stakes queue + /// @return The number of stakes waiting to be executed + function pendingStakesCount(Queue storage queue) internal view returns (uint256) { + if (queue.writeIndex < queue.readIndex) { + return 0; + } + return queue.writeIndex - queue.readIndex + 1; + } +} diff --git a/contracts/test/arbitration/staking-neo.ts b/contracts/test/arbitration/staking-neo.ts index 696935b40..293c0216a 100644 --- a/contracts/test/arbitration/staking-neo.ts +++ b/contracts/test/arbitration/staking-neo.ts @@ -428,10 +428,10 @@ describe("Staking", async () => { describe("When stake is increased", () => { it("Should delay the stake increase", async () => { - expect(await sortition.delayedStakeWriteIndex()).to.be.equal(0); - expect(await sortition.delayedStakeReadIndex()).to.be.equal(1); + let delayedStakesQueue = await sortition.delayedStakesQueue(); + expect(delayedStakesQueue.writeIndex).to.be.equal(0); + expect(delayedStakesQueue.readIndex).to.be.equal(1); await pnk.approve(core.target, PNK(1000)); - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 await expect(core.setStake(2, PNK(3000))) .to.emit(sortition, "StakeDelayed") .withArgs(deployer, 2, PNK(3000)); @@ -443,9 +443,9 @@ describe("Staking", async () => { }); it("Should store the delayed stake for later", async () => { - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 - expect(await sortition.delayedStakeWriteIndex()).to.be.equal(1); - expect(await sortition.delayedStakeReadIndex()).to.be.equal(1); + let delayedStakesQueue = await sortition.delayedStakesQueue(); + expect(delayedStakesQueue.writeIndex).to.be.equal(1); + expect(delayedStakesQueue.readIndex).to.be.equal(1); expect(await sortition.delayedStakes(1)).to.be.deep.equal([deployer, 2, PNK(3000), false]); }); }); @@ -467,11 +467,11 @@ describe("Staking", async () => { PNK(3000), 2, ]); // stake unchanged, delayed - expect(await sortition.delayedStakeWriteIndex()).to.be.equal(1); - expect(await sortition.delayedStakeReadIndex()).to.be.equal(2); + let delayedStakesQueue = await sortition.delayedStakesQueue(); + expect(delayedStakesQueue.writeIndex).to.be.equal(1); + expect(delayedStakesQueue.readIndex).to.be.equal(2); expect(await sortition.delayedStakes(1)).to.be.deep.equal([ethers.ZeroAddress, 0, 0, false]); // the 1st delayed stake got deleted expect(await sortition.delayedStakes(2)).to.be.deep.equal([ethers.ZeroAddress, 0, 0, false]); // the 2nd delayed stake got deleted - expect(await sortition.latestDelayedStakeIndex(deployer, 1)).to.be.equal(0); // Deprecated. Always 0 }); it("Should transfer PNK after delayed stake execution", async () => { @@ -495,9 +495,9 @@ describe("Staking", async () => { describe("When stake is decreased", async () => { it("Should delay the stake decrease", async () => { - expect(await sortition.delayedStakeWriteIndex()).to.be.equal(0); - expect(await sortition.delayedStakeReadIndex()).to.be.equal(1); - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 + let delayedStakesQueue = await sortition.delayedStakesQueue(); + expect(delayedStakesQueue.writeIndex).to.be.equal(0); + expect(delayedStakesQueue.readIndex).to.be.equal(1); await expect(core.setStake(2, PNK(1000))) .to.emit(sortition, "StakeDelayed") .withArgs(deployer, 2, PNK(1000)); @@ -509,9 +509,9 @@ describe("Staking", async () => { }); it("Should store the delayed stake for later", async () => { - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 - expect(await sortition.delayedStakeWriteIndex()).to.be.equal(1); - expect(await sortition.delayedStakeReadIndex()).to.be.equal(1); + let delayedStakesQueue = await sortition.delayedStakesQueue(); + expect(delayedStakesQueue.writeIndex).to.be.equal(1); + expect(delayedStakesQueue.readIndex).to.be.equal(1); expect(await sortition.delayedStakes(1)).to.be.deep.equal([deployer, 2, PNK(1000), false]); }); }); @@ -532,11 +532,11 @@ describe("Staking", async () => { PNK(1000), 2, ]); // stake unchanged, delayed - expect(await sortition.delayedStakeWriteIndex()).to.be.equal(1); - expect(await sortition.delayedStakeReadIndex()).to.be.equal(2); + let delayedStakesQueue = await sortition.delayedStakesQueue(); + expect(delayedStakesQueue.writeIndex).to.be.equal(1); + expect(delayedStakesQueue.readIndex).to.be.equal(2); expect(await sortition.delayedStakes(1)).to.be.deep.equal([ethers.ZeroAddress, 0, 0, false]); // the 1st delayed stake got deleted expect(await sortition.delayedStakes(2)).to.be.deep.equal([ethers.ZeroAddress, 0, 0, false]); // the 2nd delayed stake got deleted - expect(await sortition.latestDelayedStakeIndex(deployer, 1)).to.be.equal(0); // Deprecated. Always 0 }); it("Should withdraw some PNK", async () => { @@ -560,9 +560,9 @@ describe("Staking", async () => { describe("When stake is decreased", async () => { it("Should delay the stake decrease", async () => { - expect(await sortition.delayedStakeWriteIndex()).to.be.equal(0); - expect(await sortition.delayedStakeReadIndex()).to.be.equal(1); - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 + let delayedStakesQueue = await sortition.delayedStakesQueue(); + expect(delayedStakesQueue.writeIndex).to.be.equal(0); + expect(delayedStakesQueue.readIndex).to.be.equal(1); await expect(core.setStake(2, PNK(1000))) .to.emit(sortition, "StakeDelayed") .withArgs(deployer, 2, PNK(1000)); @@ -574,9 +574,9 @@ describe("Staking", async () => { }); it("Should store the delayed stake for later", async () => { - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 - expect(await sortition.delayedStakeWriteIndex()).to.be.equal(1); - expect(await sortition.delayedStakeReadIndex()).to.be.equal(1); + let delayedStakesQueue = await sortition.delayedStakesQueue(); + expect(delayedStakesQueue.writeIndex).to.be.equal(1); + expect(delayedStakesQueue.readIndex).to.be.equal(1); expect(await sortition.delayedStakes(1)).to.be.deep.equal([deployer, 2, PNK(1000), false]); }); }); @@ -584,7 +584,6 @@ describe("Staking", async () => { describe("When stake is increased back to the previous amount", () => { it("Should delay the stake increase", async () => { balanceBefore = await pnk.balanceOf(deployer); - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 await expect(core.setStake(2, PNK(2000))) .to.emit(sortition, "StakeDelayed") .withArgs(deployer, 2, PNK(2000)); @@ -596,9 +595,9 @@ describe("Staking", async () => { }); it("Should store the delayed stake for later", async () => { - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 - expect(await sortition.delayedStakeWriteIndex()).to.be.equal(2); - expect(await sortition.delayedStakeReadIndex()).to.be.equal(1); + let delayedStakesQueue = await sortition.delayedStakesQueue(); + expect(delayedStakesQueue.writeIndex).to.be.equal(2); + expect(delayedStakesQueue.readIndex).to.be.equal(1); expect(await sortition.delayedStakes(1)).to.be.deep.equal([deployer, 2, PNK(1000), false]); expect(await sortition.delayedStakes(2)).to.be.deep.equal([deployer, 2, PNK(2000), false]); }); @@ -623,11 +622,11 @@ describe("Staking", async () => { PNK(2000), 2, ]); // stake unchanged, delayed - expect(await sortition.delayedStakeWriteIndex()).to.be.equal(2); - expect(await sortition.delayedStakeReadIndex()).to.be.equal(3); + let delayedStakesQueue = await sortition.delayedStakesQueue(); + expect(delayedStakesQueue.writeIndex).to.be.equal(2); + expect(delayedStakesQueue.readIndex).to.be.equal(3); expect(await sortition.delayedStakes(1)).to.be.deep.equal([ethers.ZeroAddress, 0, 0, false]); // the 1st delayed stake got deleted expect(await sortition.delayedStakes(2)).to.be.deep.equal([ethers.ZeroAddress, 0, 0, false]); // the 2nd delayed stake got deleted - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 }); it("Should not transfer any PNK", async () => { @@ -651,10 +650,10 @@ describe("Staking", async () => { describe("When stake is increased", () => { it("Should delay the stake increase", async () => { - expect(await sortition.delayedStakeWriteIndex()).to.be.equal(0); - expect(await sortition.delayedStakeReadIndex()).to.be.equal(1); + let delayedStakesQueue = await sortition.delayedStakesQueue(); + expect(delayedStakesQueue.writeIndex).to.be.equal(0); + expect(delayedStakesQueue.readIndex).to.be.equal(1); await pnk.approve(core.target, PNK(1000)); - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 await expect(core.setStake(2, PNK(3000))) .to.emit(sortition, "StakeDelayed") .withArgs(deployer, 2, PNK(3000)); @@ -666,9 +665,9 @@ describe("Staking", async () => { }); it("Should store the delayed stake for later", async () => { - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 - expect(await sortition.delayedStakeWriteIndex()).to.be.equal(1); - expect(await sortition.delayedStakeReadIndex()).to.be.equal(1); + let delayedStakesQueue = await sortition.delayedStakesQueue(); + expect(delayedStakesQueue.writeIndex).to.be.equal(1); + expect(delayedStakesQueue.readIndex).to.be.equal(1); expect(await sortition.delayedStakes(1)).to.be.deep.equal([deployer, 2, PNK(3000), false]); }); }); @@ -676,7 +675,6 @@ describe("Staking", async () => { describe("When stake is decreased back to the previous amount", () => { it("Should cancel out the stake decrease back", async () => { balanceBefore = await pnk.balanceOf(deployer); - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 await expect(core.setStake(2, PNK(2000))) .to.emit(sortition, "StakeDelayed") .withArgs(deployer, 2, PNK(2000)); @@ -688,9 +686,9 @@ describe("Staking", async () => { }); it("Should store the delayed stake for later", async () => { - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 - expect(await sortition.delayedStakeWriteIndex()).to.be.equal(2); - expect(await sortition.delayedStakeReadIndex()).to.be.equal(1); + let delayedStakesQueue = await sortition.delayedStakesQueue(); + expect(delayedStakesQueue.writeIndex).to.be.equal(2); + expect(delayedStakesQueue.readIndex).to.be.equal(1); expect(await sortition.delayedStakes(1)).to.be.deep.equal([deployer, 2, PNK(3000), false]); expect(await sortition.delayedStakes(2)).to.be.deep.equal([deployer, 2, PNK(2000), false]); }); @@ -713,11 +711,11 @@ describe("Staking", async () => { PNK(2000), 2, ]); // stake unchanged, delayed - expect(await sortition.delayedStakeWriteIndex()).to.be.equal(2); - expect(await sortition.delayedStakeReadIndex()).to.be.equal(3); + let delayedStakesQueue = await sortition.delayedStakesQueue(); + expect(delayedStakesQueue.writeIndex).to.be.equal(2); + expect(delayedStakesQueue.readIndex).to.be.equal(3); expect(await sortition.delayedStakes(1)).to.be.deep.equal([ethers.ZeroAddress, 0, 0, false]); // the 1st delayed stake got deleted expect(await sortition.delayedStakes(2)).to.be.deep.equal([ethers.ZeroAddress, 0, 0, false]); // the 2nd delayed stake got deleted - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 }); it("Should not transfer any PNK", async () => { diff --git a/contracts/test/arbitration/staking.ts b/contracts/test/arbitration/staking.ts index d27a5f10e..8fefa948f 100644 --- a/contracts/test/arbitration/staking.ts +++ b/contracts/test/arbitration/staking.ts @@ -79,10 +79,10 @@ describe("Staking", async () => { describe("When stake is increased", () => { it("Should delay the stake increase", async () => { - expect(await sortition.delayedStakeWriteIndex()).to.be.equal(0); - expect(await sortition.delayedStakeReadIndex()).to.be.equal(1); + let delayedStakesQueue = await sortition.delayedStakesQueue(); + expect(delayedStakesQueue.writeIndex).to.be.equal(0); + expect(delayedStakesQueue.readIndex).to.be.equal(1); await pnk.approve(core.target, PNK(1000)); - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 await expect(core.setStake(2, PNK(3000))) .to.emit(sortition, "StakeDelayed") .withArgs(deployer, 2, PNK(3000)); @@ -94,9 +94,9 @@ describe("Staking", async () => { }); it("Should store the delayed stake for later", async () => { - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 - expect(await sortition.delayedStakeWriteIndex()).to.be.equal(1); - expect(await sortition.delayedStakeReadIndex()).to.be.equal(1); + let delayedStakesQueue = await sortition.delayedStakesQueue(); + expect(delayedStakesQueue.writeIndex).to.be.equal(1); + expect(delayedStakesQueue.readIndex).to.be.equal(1); expect(await sortition.delayedStakes(1)).to.be.deep.equal([deployer, 2, PNK(3000), false]); }); }); @@ -118,11 +118,11 @@ describe("Staking", async () => { PNK(3000), 2, ]); // stake unchanged, delayed - expect(await sortition.delayedStakeWriteIndex()).to.be.equal(1); - expect(await sortition.delayedStakeReadIndex()).to.be.equal(2); + let delayedStakesQueue = await sortition.delayedStakesQueue(); + expect(delayedStakesQueue.writeIndex).to.be.equal(1); + expect(delayedStakesQueue.readIndex).to.be.equal(2); expect(await sortition.delayedStakes(1)).to.be.deep.equal([ethers.ZeroAddress, 0, 0, false]); // the 1st delayed stake got deleted expect(await sortition.delayedStakes(2)).to.be.deep.equal([ethers.ZeroAddress, 0, 0, false]); // the 2nd delayed stake got deleted - expect(await sortition.latestDelayedStakeIndex(deployer, 1)).to.be.equal(0); // Deprecated. Always 0 }); it("Should transfer PNK after delayed stake execution", async () => { @@ -144,9 +144,9 @@ describe("Staking", async () => { describe("When stake is decreased", async () => { it("Should delay the stake decrease", async () => { - expect(await sortition.delayedStakeWriteIndex()).to.be.equal(0); - expect(await sortition.delayedStakeReadIndex()).to.be.equal(1); - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 + let delayedStakesQueue = await sortition.delayedStakesQueue(); + expect(delayedStakesQueue.writeIndex).to.be.equal(0); + expect(delayedStakesQueue.readIndex).to.be.equal(1); await expect(core.setStake(2, PNK(1000))) .to.emit(sortition, "StakeDelayed") .withArgs(deployer, 2, PNK(1000)); @@ -158,9 +158,9 @@ describe("Staking", async () => { }); it("Should store the delayed stake for later", async () => { - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 - expect(await sortition.delayedStakeWriteIndex()).to.be.equal(1); - expect(await sortition.delayedStakeReadIndex()).to.be.equal(1); + let delayedStakesQueue = await sortition.delayedStakesQueue(); + expect(delayedStakesQueue.writeIndex).to.be.equal(1); + expect(delayedStakesQueue.readIndex).to.be.equal(1); expect(await sortition.delayedStakes(1)).to.be.deep.equal([deployer, 2, PNK(1000), false]); }); }); @@ -181,11 +181,11 @@ describe("Staking", async () => { PNK(1000), 2, ]); // stake unchanged, delayed - expect(await sortition.delayedStakeWriteIndex()).to.be.equal(1); - expect(await sortition.delayedStakeReadIndex()).to.be.equal(2); + let delayedStakesQueue = await sortition.delayedStakesQueue(); + expect(delayedStakesQueue.writeIndex).to.be.equal(1); + expect(delayedStakesQueue.readIndex).to.be.equal(2); expect(await sortition.delayedStakes(1)).to.be.deep.equal([ethers.ZeroAddress, 0, 0, false]); // the 1st delayed stake got deleted expect(await sortition.delayedStakes(2)).to.be.deep.equal([ethers.ZeroAddress, 0, 0, false]); // the 2nd delayed stake got deleted - expect(await sortition.latestDelayedStakeIndex(deployer, 1)).to.be.equal(0); // Deprecated. Always 0 }); it("Should withdraw some PNK", async () => { @@ -207,9 +207,9 @@ describe("Staking", async () => { describe("When stake is decreased", async () => { it("Should delay the stake decrease", async () => { - expect(await sortition.delayedStakeWriteIndex()).to.be.equal(0); - expect(await sortition.delayedStakeReadIndex()).to.be.equal(1); - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 + let delayedStakesQueue = await sortition.delayedStakesQueue(); + expect(delayedStakesQueue.writeIndex).to.be.equal(0); + expect(delayedStakesQueue.readIndex).to.be.equal(1); await expect(core.setStake(2, PNK(1000))) .to.emit(sortition, "StakeDelayed") .withArgs(deployer, 2, PNK(1000)); @@ -221,9 +221,9 @@ describe("Staking", async () => { }); it("Should store the delayed stake for later", async () => { - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 - expect(await sortition.delayedStakeWriteIndex()).to.be.equal(1); - expect(await sortition.delayedStakeReadIndex()).to.be.equal(1); + let delayedStakesQueue = await sortition.delayedStakesQueue(); + expect(delayedStakesQueue.writeIndex).to.be.equal(1); + expect(delayedStakesQueue.readIndex).to.be.equal(1); expect(await sortition.delayedStakes(1)).to.be.deep.equal([deployer, 2, PNK(1000), false]); }); }); @@ -231,7 +231,6 @@ describe("Staking", async () => { describe("When stake is increased back to the previous amount", () => { it("Should delay the stake increase", async () => { balanceBefore = await pnk.balanceOf(deployer); - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 await expect(core.setStake(2, PNK(2000))).to.emit(sortition, "StakeDelayed"); expect(await sortition.getJurorBalance(deployer, 2)).to.be.deep.equal([PNK(4000), 0, PNK(2000), 2]); // stake unchanged, delayed }); @@ -241,9 +240,9 @@ describe("Staking", async () => { }); it("Should store the delayed stake for later", async () => { - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 - expect(await sortition.delayedStakeWriteIndex()).to.be.equal(2); - expect(await sortition.delayedStakeReadIndex()).to.be.equal(1); + let delayedStakesQueue = await sortition.delayedStakesQueue(); + expect(delayedStakesQueue.writeIndex).to.be.equal(2); + expect(delayedStakesQueue.readIndex).to.be.equal(1); expect(await sortition.delayedStakes(1)).to.be.deep.equal([deployer, 2, PNK(1000), false]); expect(await sortition.delayedStakes(2)).to.be.deep.equal([deployer, 2, PNK(2000), false]); }); @@ -268,11 +267,11 @@ describe("Staking", async () => { PNK(2000), 2, ]); // stake unchanged, delayed - expect(await sortition.delayedStakeWriteIndex()).to.be.equal(2); - expect(await sortition.delayedStakeReadIndex()).to.be.equal(3); + let delayedStakesQueue = await sortition.delayedStakesQueue(); + expect(delayedStakesQueue.writeIndex).to.be.equal(2); + expect(delayedStakesQueue.readIndex).to.be.equal(3); expect(await sortition.delayedStakes(1)).to.be.deep.equal([ethers.ZeroAddress, 0, 0, false]); // the 1st delayed stake got deleted expect(await sortition.delayedStakes(2)).to.be.deep.equal([ethers.ZeroAddress, 0, 0, false]); // the 2nd delayed stake got deleted - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 }); it("Should not transfer any PNK", async () => { @@ -294,10 +293,10 @@ describe("Staking", async () => { describe("When stake is increased", () => { it("Should delay the stake increase", async () => { - expect(await sortition.delayedStakeWriteIndex()).to.be.equal(0); - expect(await sortition.delayedStakeReadIndex()).to.be.equal(1); + let delayedStakesQueue = await sortition.delayedStakesQueue(); + expect(delayedStakesQueue.writeIndex).to.be.equal(0); + expect(delayedStakesQueue.readIndex).to.be.equal(1); await pnk.approve(core.target, PNK(1000)); - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 await expect(core.setStake(2, PNK(3000))) .to.emit(sortition, "StakeDelayed") .withArgs(deployer, 2, PNK(3000)); @@ -309,9 +308,9 @@ describe("Staking", async () => { }); it("Should store the delayed stake for later", async () => { - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 - expect(await sortition.delayedStakeWriteIndex()).to.be.equal(1); - expect(await sortition.delayedStakeReadIndex()).to.be.equal(1); + let delayedStakesQueue = await sortition.delayedStakesQueue(); + expect(delayedStakesQueue.writeIndex).to.be.equal(1); + expect(delayedStakesQueue.readIndex).to.be.equal(1); expect(await sortition.delayedStakes(1)).to.be.deep.equal([deployer, 2, PNK(3000), false]); }); }); @@ -319,7 +318,6 @@ describe("Staking", async () => { describe("When stake is decreased back to the previous amount", () => { it("Should cancel out the stake decrease back", async () => { balanceBefore = await pnk.balanceOf(deployer); - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 await expect(core.setStake(2, PNK(2000))) .to.emit(sortition, "StakeDelayed") .withArgs(deployer, 2, PNK(2000)); @@ -331,9 +329,9 @@ describe("Staking", async () => { }); it("Should store the delayed stake for later", async () => { - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 - expect(await sortition.delayedStakeWriteIndex()).to.be.equal(2); - expect(await sortition.delayedStakeReadIndex()).to.be.equal(1); + let delayedStakesQueue = await sortition.delayedStakesQueue(); + expect(delayedStakesQueue.writeIndex).to.be.equal(2); + expect(delayedStakesQueue.readIndex).to.be.equal(1); expect(await sortition.delayedStakes(1)).to.be.deep.equal([deployer, 2, PNK(3000), false]); expect(await sortition.delayedStakes(2)).to.be.deep.equal([deployer, 2, PNK(2000), false]); }); @@ -356,11 +354,11 @@ describe("Staking", async () => { PNK(2000), 2, ]); // stake unchanged, delayed - expect(await sortition.delayedStakeWriteIndex()).to.be.equal(2); - expect(await sortition.delayedStakeReadIndex()).to.be.equal(3); + let delayedStakesQueue = await sortition.delayedStakesQueue(); + expect(delayedStakesQueue.writeIndex).to.be.equal(2); + expect(delayedStakesQueue.readIndex).to.be.equal(3); expect(await sortition.delayedStakes(1)).to.be.deep.equal([ethers.ZeroAddress, 0, 0, false]); // the 1st delayed stake got deleted expect(await sortition.delayedStakes(2)).to.be.deep.equal([ethers.ZeroAddress, 0, 0, false]); // the 2nd delayed stake got deleted - expect(await sortition.latestDelayedStakeIndex(deployer, 2)).to.be.equal(0); // Deprecated. Always 0 }); it("Should not transfer any PNK", async () => { diff --git a/contracts/test/foundry/KlerosCore.t.sol b/contracts/test/foundry/KlerosCore.t.sol index 5b60aad7e..6e7d069e5 100644 --- a/contracts/test/foundry/KlerosCore.t.sol +++ b/contracts/test/foundry/KlerosCore.t.sol @@ -10,6 +10,7 @@ import {DisputeKitClassic, DisputeKitClassicBase} from "../../src/arbitration/di import {DisputeKitSybilResistant} from "../../src/arbitration/dispute-kits/DisputeKitSybilResistant.sol"; import {ISortitionModule} from "../../src/arbitration/interfaces/ISortitionModule.sol"; import {SortitionModuleMock, SortitionModuleBase} from "../../src/test/SortitionModuleMock.sol"; +import {DelayedStakes} from "../../src/libraries/DelayedStakes.sol"; import {UUPSProxy} from "../../src/proxy/UUPSProxy.sol"; import {BlockHashRNG} from "../../src/rng/BlockHashRNG.sol"; import {RNGWithFallback, IRNG} from "../../src/rng/RNGWithFallback.sol"; @@ -246,8 +247,9 @@ contract KlerosCoreTest is Test { assertEq(sortitionModule.disputesWithoutJurors(), 0, "disputesWithoutJurors should be 0"); assertEq(address(sortitionModule.rng()), address(rng), "Wrong RNG address"); assertEq(sortitionModule.randomNumber(), 0, "randomNumber should be 0"); - assertEq(sortitionModule.delayedStakeWriteIndex(), 0, "delayedStakeWriteIndex should be 0"); - assertEq(sortitionModule.delayedStakeReadIndex(), 1, "Wrong delayedStakeReadIndex"); + (uint256 writeIndex, uint256 readIndex) = sortitionModule.delayedStakesQueue(); + assertEq(writeIndex, 0, "delayedStakeWriteIndex should be 0"); + assertEq(readIndex, 1, "Wrong delayedStakeReadIndex"); (uint256 K, uint256 nodeLength) = sortitionModule.getSortitionProperties(bytes32(uint256(FORKING_COURT))); assertEq(K, 5, "Wrong tree K FORKING_COURT"); @@ -1031,16 +1033,15 @@ contract KlerosCoreTest is Test { emit SortitionModuleBase.StakeDelayed(staker1, GENERAL_COURT, 1500); core.setStake(GENERAL_COURT, 1500); - uint256 delayedStakeId = sortitionModule.delayedStakeWriteIndex(); - assertEq(delayedStakeId, 1, "Wrong delayedStakeWriteIndex"); - assertEq(sortitionModule.delayedStakeReadIndex(), 1, "Wrong delayedStakeReadIndex"); - (address account, uint96 courtID, uint256 stake, bool alreadyTransferred) = sortitionModule.delayedStakes( - delayedStakeId - ); - assertEq(account, staker1, "Wrong staker account"); - assertEq(courtID, GENERAL_COURT, "Wrong court id"); - assertEq(stake, 1500, "Wrong amount staked in court"); - assertEq(alreadyTransferred, false, "Should be flagged as transferred"); + (uint256 writeIndex, uint256 readIndex) = sortitionModule.delayedStakesQueue(); + assertEq(writeIndex, 1, "Wrong delayedStakeWriteIndex"); + assertEq(readIndex, 1, "Wrong delayedStakeReadIndex"); + + DelayedStakes.Stake memory stake = sortitionModule.delayedStakes(writeIndex); + assertEq(stake.account, staker1, "Wrong staker account"); + assertEq(stake.courtID, GENERAL_COURT, "Wrong court id"); + assertEq(stake.stake, 1500, "Wrong amount staked in court"); + assertEq(stake.alreadyTransferred, false, "Should be flagged as transferred"); (uint256 totalStaked, uint256 totalLocked, uint256 stakedInCourt, uint256 nbCourts) = sortitionModule .getJurorBalance(staker1, GENERAL_COURT); @@ -1179,28 +1180,28 @@ contract KlerosCoreTest is Test { emit SortitionModuleBase.StakeDelayed(staker1, GENERAL_COURT, 1800); core.setStake(GENERAL_COURT, 1800); - assertEq(sortitionModule.delayedStakeWriteIndex(), 3, "Wrong delayedStakeWriteIndex"); - assertEq(sortitionModule.delayedStakeReadIndex(), 1, "Wrong delayedStakeReadIndex"); - - (address account, uint96 courtID, uint256 stake, bool alreadyTransferred) = sortitionModule.delayedStakes(1); + (uint256 writeIndex, uint256 readIndex) = sortitionModule.delayedStakesQueue(); + assertEq(writeIndex, 3, "Wrong delayedStakeWriteIndex"); + assertEq(readIndex, 1, "Wrong delayedStakeReadIndex"); // Check each delayed stake - assertEq(account, staker1, "Wrong staker account for the first delayed stake"); - assertEq(courtID, GENERAL_COURT, "Wrong court ID"); - assertEq(stake, 1500, "Wrong staking amount"); - assertEq(alreadyTransferred, false, "Should be false"); - - (account, courtID, stake, alreadyTransferred) = sortitionModule.delayedStakes(2); - assertEq(account, staker2, "Wrong staker2 account"); - assertEq(courtID, GENERAL_COURT, "Wrong court id for staker2"); - assertEq(stake, 0, "Wrong amount for delayed stake of staker2"); - assertEq(alreadyTransferred, false, "Should be false"); - - (account, courtID, stake, alreadyTransferred) = sortitionModule.delayedStakes(3); - assertEq(account, staker1, "Wrong staker1 account"); - assertEq(courtID, GENERAL_COURT, "Wrong court id for staker1"); - assertEq(stake, 1800, "Wrong amount for delayed stake of staker1"); - assertEq(alreadyTransferred, false, "Should be false"); + DelayedStakes.Stake memory stake = sortitionModule.delayedStakes(1); + assertEq(stake.account, staker1, "Wrong staker account for the first delayed stake"); + assertEq(stake.courtID, GENERAL_COURT, "Wrong court ID"); + assertEq(stake.stake, 1500, "Wrong staking amount"); + assertEq(stake.alreadyTransferred, false, "Should be false"); + + stake = sortitionModule.delayedStakes(2); + assertEq(stake.account, staker2, "Wrong staker2 account"); + assertEq(stake.courtID, GENERAL_COURT, "Wrong court id for staker2"); + assertEq(stake.stake, 0, "Wrong amount for delayed stake of staker2"); + assertEq(stake.alreadyTransferred, false, "Should be false"); + + stake = sortitionModule.delayedStakes(3); + assertEq(stake.account, staker1, "Wrong staker1 account"); + assertEq(stake.courtID, GENERAL_COURT, "Wrong court id for staker1"); + assertEq(stake.stake, 1800, "Wrong amount for delayed stake of staker1"); + assertEq(stake.alreadyTransferred, false, "Should be false"); // So far the only amount transferred was 10000 by staker2. Staker 1 has two delayed stakes, for 1500 and 1800 pnk. assertEq(pinakion.balanceOf(address(core)), 10000, "Wrong token balance of the core"); @@ -1229,17 +1230,18 @@ contract KlerosCoreTest is Test { emit SortitionModuleBase.StakeSet(staker1, GENERAL_COURT, 1800, 1800); sortitionModule.executeDelayedStakes(20); // Deliberately ask for more iterations than needed - assertEq(sortitionModule.delayedStakeWriteIndex(), 3, "Wrong delayedStakeWriteIndex"); - assertEq(sortitionModule.delayedStakeReadIndex(), 4, "Wrong delayedStakeReadIndex"); + (writeIndex, readIndex) = sortitionModule.delayedStakesQueue(); + assertEq(writeIndex, 3, "Wrong delayedStakeWriteIndex"); + assertEq(readIndex, 4, "Wrong delayedStakeReadIndex"); // Check that delayed stakes are nullified - for (uint i = 2; i <= sortitionModule.delayedStakeWriteIndex(); i++) { - (account, courtID, stake, alreadyTransferred) = sortitionModule.delayedStakes(i); + for (uint i = 2; i <= writeIndex; i++) { + DelayedStakes.Stake memory stake = sortitionModule.delayedStakes(i); - assertEq(account, address(0), "Wrong staker account after delayed stake deletion"); - assertEq(courtID, 0, "Court id should be nullified"); - assertEq(stake, 0, "No amount to stake"); - assertEq(alreadyTransferred, false, "Should be false"); + assertEq(stake.account, address(0), "Wrong staker account after delayed stake deletion"); + assertEq(stake.courtID, 0, "Court id should be nullified"); + assertEq(stake.stake, 0, "No amount to stake"); + assertEq(stake.alreadyTransferred, false, "Should be false"); } assertEq(pinakion.balanceOf(staker1), 999999999999998200, "Wrong token balance of staker1");