diff --git a/.github/workflows/contracts-testing.yml b/.github/workflows/contracts-testing.yml index 3b5dd04c0..72d261633 100644 --- a/.github/workflows/contracts-testing.yml +++ b/.github/workflows/contracts-testing.yml @@ -40,14 +40,16 @@ jobs: 54.185.253.63:443 - name: Setup Node.js environment - uses: actions/setup-node@v4 + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 with: node-version: 18.x - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + submodules: recursive - name: Cache node modules - uses: actions/cache@v4 + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 env: cache-name: cache-node-modules with: @@ -57,28 +59,22 @@ jobs: key: ${{ runner.os }}-build-${{ secrets.CACHE_VERSION }}-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }} restore-keys: | ${{ runner.os }}-build-${{ secrets.CACHE_VERSION }}-${{ env.cache-name }}- - - #- name: Install parent dependencies - # run: | - # echo "current dir: $PWD" - # yarn install - + - name: Install contracts dependencies - run: | - yarn workspace @kleros/kleros-v2-contracts install - - - name: Compile - run: | - yarn hardhat compile - working-directory: contracts - - - name: Test with coverage - run: | - yarn hardhat coverage --solcoverjs ./.solcover.js --temp artifacts --testfiles './test/**/*.ts' --show-stack-traces - working-directory: contracts + run: yarn workspace @kleros/kleros-v2-contracts install + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@8f1998e9878d786675189ef566a2e4bf24869773 # v1.2.0 + - name: Install lcov + run: sudo apt-get install -y lcov + + - name: Run Hardhat and Foundry tests with coverage + run: yarn coverage + working-directory: contracts + - name: Upload a build artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: code-coverage-report path: contracts/coverage diff --git a/contracts/.solcover.js b/contracts/.solcover.js index 460f8400c..8fcb481f2 100644 --- a/contracts/.solcover.js +++ b/contracts/.solcover.js @@ -5,7 +5,7 @@ const shell = require("shelljs"); // The environment variables are loaded in hardhat.config.ts module.exports = { - istanbulReporter: ["html"], + istanbulReporter: ["lcov"], onCompileComplete: async function (_config) { await run("typechain"); }, @@ -14,7 +14,7 @@ module.exports = { shell.rm("-rf", "./artifacts"); shell.rm("-rf", "./typechain"); }, - skipFiles: ["mocks", "test"], + skipFiles: ["test", "token", "kleros-v1", "proxy/mock", "gateway/mock", "rng/mock"], mocha: { timeout: 20000, grep: "@skip-on-coverage", // Find everything with this tag diff --git a/contracts/lib/forge-std b/contracts/lib/forge-std index 066ff16c5..8f24d6b04 160000 --- a/contracts/lib/forge-std +++ b/contracts/lib/forge-std @@ -1 +1 @@ -Subproject commit 066ff16c5c03e6f931cd041fd366bc4be1fae82a +Subproject commit 8f24d6b04c92975e0795b5868aa0d783251cdeaa diff --git a/contracts/package.json b/contracts/package.json index d9729185b..e2441d609 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -22,6 +22,7 @@ "clean": "hardhat clean", "check": "hardhat check", "test": "TS_NODE_TRANSPILE_ONLY=1 hardhat test", + "coverage": "scripts/coverage.sh", "start": "hardhat node --tags nop", "start-local": "hardhat node --tags Arbitration,HomeArbitrable --hostname 0.0.0.0", "deploy": "hardhat deploy", diff --git a/contracts/scripts/coverage.sh b/contracts/scripts/coverage.sh new file mode 100755 index 000000000..4f7327a91 --- /dev/null +++ b/contracts/scripts/coverage.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash + +set -e # exit on error + +rm -rf coverage +mkdir -p coverage + +# Generate the Forge coverage report +forge clean +if [ "$CI" != "true" ]; then + forge coverage --report summary --report lcov --report-file coverage/lcov-forge.info +else + # FIXME: Temporarily workaround a CI issue + touch coverage/lcov-forge.info +fi + +# Generate the Hardhat coverage report +yarn clean +yarn hardhat coverage --solcoverjs ./.solcover.js --temp artifacts --show-stack-traces --testfiles "test/**/*.ts" +mv coverage/lcov.info coverage/lcov-hardhat.info + +# Make the Hardhat report paths relative for consistency with Forge coverage report +sed -i -e 's/\/.*\/kleros-v2\/contracts\///g' coverage/lcov-hardhat.info + +# Merge the two reports +lcov \ + --ignore-errors format \ + --ignore-errors inconsistent \ + --ignore-errors empty \ + --rc max_message_count=3 \ + --rc derive_function_end_line=0 \ + --rc branch_coverage=1 \ + --add-tracefile coverage/lcov-hardhat.info \ + --add-tracefile coverage/lcov-forge.info \ + --output-file coverage/merged-lcov.info + +# Filter out unnecessary contracts from the report +lcov \ + --ignore-errors format \ + --ignore-errors inconsistent \ + --ignore-errors empty \ + --ignore-errors unused \ + --rc max_message_count=3 \ + --rc branch_coverage=1 \ + --rc derive_function_end_line=0 \ + --remove coverage/merged-lcov.info \ + --output-file coverage/filtered-lcov.info \ + "../node_modules" "src/test" "src/token" "src/kleros-v1" "src/proxy/mock" "src/gateway/mock" "src/rng/mock" + +# Open more granular breakdown in browser +if [ "$CI" != "true" ]; then + # Generate the HTML report + genhtml coverage/filtered-lcov.info \ + --ignore-errors format \ + --ignore-errors inconsistent \ + --ignore-errors empty \ + --ignore-errors category \ + --rc branch_coverage=1 \ + --rc max_message_count=3 \ + -o coverage + open coverage/index.html +fi diff --git a/contracts/src/arbitration/KlerosCoreBase.sol b/contracts/src/arbitration/KlerosCoreBase.sol index ce6ed6202..2e062e473 100644 --- a/contracts/src/arbitration/KlerosCoreBase.sol +++ b/contracts/src/arbitration/KlerosCoreBase.sol @@ -113,7 +113,7 @@ abstract contract KlerosCoreBase is IArbitratorV2 { event AppealDecision(uint256 indexed _disputeID, IArbitrableV2 indexed _arbitrable); event Draw(address indexed _address, uint256 indexed _disputeID, uint256 _roundID, uint256 _voteID); event CourtCreated( - uint256 indexed _courtID, + uint96 indexed _courtID, uint96 indexed _parent, bool _hiddenVotes, uint256 _minStake, @@ -237,8 +237,10 @@ abstract contract KlerosCoreBase is IArbitratorV2 { sortitionModule.createTree(bytes32(uint256(GENERAL_COURT)), _sortitionExtraData); + uint256[] memory supportedDisputeKits = new uint256[](1); + supportedDisputeKits[0] = DISPUTE_KIT_CLASSIC; emit CourtCreated( - 1, + GENERAL_COURT, court.parent, _hiddenVotes, _courtParameters[0], @@ -246,7 +248,7 @@ abstract contract KlerosCoreBase is IArbitratorV2 { _courtParameters[2], _courtParameters[3], _timesPerPeriod, - new uint256[](0) + supportedDisputeKits ); _enableDisputeKit(GENERAL_COURT, DISPUTE_KIT_CLASSIC, true); } @@ -351,7 +353,7 @@ abstract contract KlerosCoreBase is IArbitratorV2 { if (_supportedDisputeKits[i] == 0 || _supportedDisputeKits[i] >= disputeKits.length) { revert WrongDisputeKitIndex(); } - court.supportedDisputeKits[_supportedDisputeKits[i]] = true; + _enableDisputeKit(uint96(courtID), _supportedDisputeKits[i], true); } // Check that Classic DK support was added. if (!court.supportedDisputeKits[DISPUTE_KIT_CLASSIC]) revert MustSupportDisputeKitClassic(); @@ -370,7 +372,7 @@ abstract contract KlerosCoreBase is IArbitratorV2 { // Update the parent. courts[_parent].children.push(courtID); emit CourtCreated( - courtID, + uint96(courtID), _parent, _hiddenVotes, _minStake, @@ -1061,7 +1063,7 @@ abstract contract KlerosCoreBase is IArbitratorV2 { bool _alreadyTransferred, OnError _onError ) internal returns (bool) { - if (_courtID == FORKING_COURT || _courtID > courts.length) { + if (_courtID == FORKING_COURT || _courtID >= courts.length) { _stakingFailed(_onError, StakingResult.CannotStakeInThisCourt); // Staking directly into the forking court is not allowed. return false; } @@ -1102,6 +1104,7 @@ abstract contract KlerosCoreBase is IArbitratorV2 { if (_result == StakingResult.CannotStakeInMoreCourts) revert StakingInTooManyCourts(); if (_result == StakingResult.CannotStakeInThisCourt) revert StakingNotPossibeInThisCourt(); if (_result == StakingResult.CannotStakeLessThanMinStake) revert StakingLessThanCourtMinStake(); + if (_result == StakingResult.CannotStakeZeroWhenNoStake) revert StakingZeroWhenNoStake(); } /// @dev Gets a court ID, the minimum number of jurors and an ID of a dispute kit from a specified extra data bytes array. @@ -1147,13 +1150,11 @@ abstract contract KlerosCoreBase is IArbitratorV2 { error SortitionModuleOnly(); error UnsuccessfulCall(); error InvalidDisputKitParent(); - error DepthLevelMax(); error MinStakeLowerThanParentCourt(); error UnsupportedDisputeKit(); error InvalidForkingCourtAsParent(); error WrongDisputeKitIndex(); error CannotDisableClassicDK(); - error ArraysLengthMismatch(); error StakingInTooManyCourts(); error StakingNotPossibeInThisCourt(); error StakingLessThanCourtMinStake(); @@ -1177,4 +1178,5 @@ abstract contract KlerosCoreBase is IArbitratorV2 { error TransferFailed(); error WhenNotPausedOnly(); error WhenPausedOnly(); + error StakingZeroWhenNoStake(); } diff --git a/contracts/src/arbitration/SortitionModuleBase.sol b/contracts/src/arbitration/SortitionModuleBase.sol index 24e32e6ea..642f9b627 100644 --- a/contracts/src/arbitration/SortitionModuleBase.sol +++ b/contracts/src/arbitration/SortitionModuleBase.sol @@ -206,13 +206,14 @@ abstract contract SortitionModuleBase is ISortitionModule { DelayedStake storage delayedStake = delayedStakes[i]; // Delayed stake could've been manually removed already. In this case simply move on to the next item. if (delayedStake.account != address(0)) { + // Nullify the index so the delayed stake won't get deleted before its own execution. + delete latestDelayedStakeIndex[delayedStake.account][delayedStake.courtID]; core.setStakeBySortitionModule( delayedStake.account, delayedStake.courtID, delayedStake.stake, delayedStake.alreadyTransferred ); - delete latestDelayedStakeIndex[delayedStake.account][delayedStake.courtID]; delete delayedStakes[i]; } } @@ -265,13 +266,16 @@ abstract contract SortitionModuleBase is ISortitionModule { uint256 currentStake = stakeOf(_account, _courtID); uint256 nbCourts = juror.courtIDs.length; - if (_newStake == 0 && (nbCourts >= MAX_STAKE_PATHS || currentStake == 0)) { + if (currentStake == 0 && nbCourts >= MAX_STAKE_PATHS) { return (0, 0, StakingResult.CannotStakeInMoreCourts); // Prevent staking beyond MAX_STAKE_PATHS but unstaking is always allowed. } - if (phase != Phase.staking) { - pnkWithdrawal = _deleteDelayedStake(_courtID, _account); + if (currentStake == 0 && _newStake == 0) { + return (0, 0, StakingResult.CannotStakeZeroWhenNoStake); // Forbid staking 0 amount when current stake is 0 to avoid flaky behaviour. + } + pnkWithdrawal = _deleteDelayedStake(_courtID, _account); + 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; diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitClassic.sol b/contracts/src/arbitration/dispute-kits/DisputeKitClassic.sol index 0aa857091..12d4e45c5 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitClassic.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitClassic.sol @@ -238,7 +238,6 @@ contract DisputeKitClassic is IDisputeKit, Initializable, UUPSProxiable { (uint96 courtID, , , , ) = core.disputes(_coreDisputeID); bytes32 key = bytes32(uint256(courtID)); // Get the ID of the tree. - // TODO: Handle the situation when no one has staked yet. drawnAddress = sortitionModule.draw(key, _coreDisputeID, _nonce); if (_postDrawCheck(_coreDisputeID, drawnAddress)) { @@ -603,6 +602,10 @@ contract DisputeKitClassic is IDisputeKit, Initializable, UUPSProxiable { /// @param _coreDisputeID ID of the dispute in the core contract. /// @param _juror Chosen address. /// @return Whether the address can be drawn or not. + /// Note that we don't check the minStake requirement here because of the implicit staking in parent courts. + /// minStake is checked directly during staking process however it's possible for the juror to get drawn + /// while having < minStake if it is later increased by governance. + /// This issue is expected and harmless since we check for insolvency anyway. function _postDrawCheck(uint256 _coreDisputeID, address _juror) internal view returns (bool) { (uint96 courtID, , , , ) = core.disputes(_coreDisputeID); uint256 lockedAmountPerJuror = core diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitSybilResistant.sol b/contracts/src/arbitration/dispute-kits/DisputeKitSybilResistant.sol index 40b805c05..dc571cfbf 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitSybilResistant.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitSybilResistant.sol @@ -255,7 +255,6 @@ contract DisputeKitSybilResistant is IDisputeKit, Initializable, UUPSProxiable { (uint96 courtID, , , , ) = core.disputes(_coreDisputeID); bytes32 key = bytes32(uint256(courtID)); // Get the ID of the tree. - // TODO: Handle the situation when no one has staked yet. drawnAddress = sortitionModule.draw(key, _coreDisputeID, _nonce); if (_postDrawCheck(_coreDisputeID, drawnAddress) && !round.alreadyDrawn[drawnAddress]) { @@ -621,6 +620,10 @@ contract DisputeKitSybilResistant is IDisputeKit, Initializable, UUPSProxiable { /// @param _coreDisputeID ID of the dispute in the core contract. /// @param _juror Chosen address. /// @return Whether the address can be drawn or not. + /// Note that we don't check the minStake requirement here because of the implicit staking in parent courts. + /// minStake is checked directly during staking process however it's possible for the juror to get drawn + /// while having < minStake if it is later increased by governance. + /// This issue is expected and harmless since we check for insolvency anyway. function _postDrawCheck(uint256 _coreDisputeID, address _juror) internal view returns (bool) { (uint96 courtID, , , , ) = core.disputes(_coreDisputeID); uint256 lockedAmountPerJuror = core diff --git a/contracts/src/arbitration/interfaces/IDisputeKit.sol b/contracts/src/arbitration/interfaces/IDisputeKit.sol index 86430f663..32e0bb7fb 100644 --- a/contracts/src/arbitration/interfaces/IDisputeKit.sol +++ b/contracts/src/arbitration/interfaces/IDisputeKit.sol @@ -41,6 +41,7 @@ interface IDisputeKit { /// @param _coreDisputeID The ID of the dispute in Kleros Core, not in the Dispute Kit. /// @param _numberOfChoices Number of choices of the dispute /// @param _extraData Additional info about the dispute, for possible use in future dispute kits. + /// @param _nbVotes Maximal number of votes this dispute can get. DEPRECATED as we don't need to pass it now. KC handles the count. function createDispute( uint256 _coreDisputeID, uint256 _numberOfChoices, diff --git a/contracts/src/libraries/Constants.sol b/contracts/src/libraries/Constants.sol index 44e9b4cd6..f393b4792 100644 --- a/contracts/src/libraries/Constants.sol +++ b/contracts/src/libraries/Constants.sol @@ -9,7 +9,7 @@ uint96 constant FORKING_COURT = 0; // Index of the forking court. uint96 constant GENERAL_COURT = 1; // Index of the default (general) court. // Dispute Kits -uint256 constant NULL_DISPUTE_KIT = 0; // Null pattern to indicate a top-level DK which has no parent. +uint256 constant NULL_DISPUTE_KIT = 0; // Null pattern to indicate a top-level DK which has no parent. DEPRECATED, as its main purpose was to accommodate forest structure which is not used now. uint256 constant DISPUTE_KIT_CLASSIC = 1; // Index of the default DK. 0 index is skipped. // Sortition Module @@ -33,5 +33,6 @@ enum StakingResult { CannotStakeInThisCourt, CannotStakeLessThanMinStake, CannotStakeMoreThanMaxStakePerJuror, - CannotStakeMoreThanMaxTotalStaked + CannotStakeMoreThanMaxTotalStaked, + CannotStakeZeroWhenNoStake } diff --git a/contracts/src/test/KlerosCoreMock.sol b/contracts/src/test/KlerosCoreMock.sol new file mode 100644 index 000000000..76bb75f2c --- /dev/null +++ b/contracts/src/test/KlerosCoreMock.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT + +/// @custom:authors: [@unknownunknown1] +/// @custom:reviewers: [] +/// @custom:auditors: [] +/// @custom:bounties: [] +/// @custom:deployments: [] + +pragma solidity 0.8.24; + +import "../arbitration/KlerosCore.sol"; + +/// @title KlerosCoreMock +/// KlerosCore with view functions to use in Foundry tests. +contract KlerosCoreMock is KlerosCore { + function getCourtChildren(uint256 _courtId) external view returns (uint256[] memory children) { + children = courts[_courtId].children; + } + + function extraDataToCourtIDMinJurorsDisputeKit( + bytes memory _extraData + ) external view returns (uint96 courtID, uint256 minJurors, uint256 disputeKitID) { + (courtID, minJurors, disputeKitID) = _extraDataToCourtIDMinJurorsDisputeKit(_extraData); + } +} diff --git a/contracts/src/test/SortitionModuleMock.sol b/contracts/src/test/SortitionModuleMock.sol new file mode 100644 index 000000000..bfe911dfe --- /dev/null +++ b/contracts/src/test/SortitionModuleMock.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT + +/** + * @custom:authors: [unknownunknown1] + * @custom:reviewers: [] + * @custom:auditors: [] + * @custom:bounties: [] + * @custom:deployments: [] + */ + +pragma solidity 0.8.24; + +import "../arbitration/SortitionModule.sol"; + +/// @title SortitionModuleMock +/// @dev Adds getter functions to sortition module for Foundry tests. +contract SortitionModuleMock is SortitionModule { + function getSortitionProperties(bytes32 _key) external view returns (uint256 K, uint256 nodeLength) { + SortitionSumTree storage tree = sortitionSumTrees[_key]; + K = tree.K; + nodeLength = tree.nodes.length; + } +} diff --git a/contracts/test/arbitration/index.ts b/contracts/test/arbitration/index.ts index 5c861c537..fa2bc24f3 100644 --- a/contracts/test/arbitration/index.ts +++ b/contracts/test/arbitration/index.ts @@ -30,7 +30,7 @@ describe("DisputeKitClassic", async () => { expect(events2[0].args._feeForJuror).to.equal(ethers.parseUnits("0.1", 18)); expect(events2[0].args._jurorsForCourtJump).to.equal(256); expect(events2[0].args._timesPerPeriod).to.deep.equal([0, 0, 0, 10]); - expect(events2[0].args._supportedDisputeKits).to.deep.equal([]); + expect(events2[0].args._supportedDisputeKits).to.deep.equal([1]); const events3 = await core.queryFilter(core.filters.DisputeKitEnabled()); expect(events3.length).to.equal(1); diff --git a/contracts/test/foundry/Contract.t.sol b/contracts/test/foundry/Contract.t.sol deleted file mode 100644 index e69de29bb..000000000 diff --git a/contracts/test/foundry/KlerosCore.t.sol b/contracts/test/foundry/KlerosCore.t.sol new file mode 100644 index 000000000..56e33c9c5 --- /dev/null +++ b/contracts/test/foundry/KlerosCore.t.sol @@ -0,0 +1,2706 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import {Test} from "forge-std/Test.sol"; +import {console} from "forge-std/console.sol"; // Import the console for logging +import {KlerosCoreMock, KlerosCoreBase, IArbitratorV2} from "../../src/test/KlerosCoreMock.sol"; +import {IDisputeKit} from "../../src/arbitration/interfaces/IDisputeKit.sol"; +import {DisputeKitClassic} from "../../src/arbitration/dispute-kits/DisputeKitClassic.sol"; +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 {UUPSProxy} from "../../src/proxy/UUPSProxy.sol"; +import {BlockHashRNG} from "../../src/rng/BlockHashRNG.sol"; +import {PNK} from "../../src/token/PNK.sol"; +import {TestERC20} from "../../src/token/TestERC20.sol"; +import {ArbitrableExample, IArbitrableV2} from "../../src/arbitration/arbitrables/ArbitrableExample.sol"; +import {DisputeTemplateRegistry} from "../../src/arbitration/DisputeTemplateRegistry.sol"; +import "../../src/libraries/Constants.sol"; + +contract KlerosCoreTest is Test { + event Initialized(uint64 version); + + KlerosCoreMock core; + DisputeKitClassic disputeKit; + SortitionModuleMock sortitionModule; + BlockHashRNG rng; + PNK pinakion; + TestERC20 feeToken; + ArbitrableExample arbitrable; + DisputeTemplateRegistry registry; + address governor; + address guardian; + address staker1; + address staker2; + address disputer; + address crowdfunder1; + address crowdfunder2; + address other; + address jurorProsecutionModule; + uint256 minStake; + uint256 alpha; + uint256 feeForJuror; + uint256 jurorsForCourtJump; + bytes sortitionExtraData; + bytes arbitratorExtraData; + uint256[4] timesPerPeriod; + bool hiddenVotes; + + uint256 totalSupply = 1000000 ether; + + uint256 minStakingTime; + uint256 maxDrawingTime; + uint256 rngLookahead; + + string templateData; + string templateDataMappings; + + function setUp() public { + KlerosCoreMock coreLogic = new KlerosCoreMock(); + SortitionModuleMock smLogic = new SortitionModuleMock(); + DisputeKitClassic dkLogic = new DisputeKitClassic(); + DisputeTemplateRegistry registryLogic = new DisputeTemplateRegistry(); + rng = new BlockHashRNG(); + pinakion = new PNK(); + feeToken = new TestERC20("Test", "TST"); + + governor = msg.sender; + guardian = vm.addr(1); + staker1 = vm.addr(2); + staker2 = vm.addr(3); + disputer = vm.addr(4); + crowdfunder1 = vm.addr(5); + crowdfunder2 = vm.addr(6); + vm.deal(disputer, 10 ether); + vm.deal(crowdfunder1, 10 ether); + vm.deal(crowdfunder2, 10 ether); + jurorProsecutionModule = vm.addr(8); + other = vm.addr(9); + minStake = 1000; + alpha = 10000; + feeForJuror = 0.03 ether; + jurorsForCourtJump = 511; + timesPerPeriod = [60, 120, 180, 240]; + + pinakion.transfer(msg.sender, totalSupply - 2 ether); + pinakion.transfer(staker1, 1 ether); + pinakion.transfer(staker2, 1 ether); + + sortitionExtraData = abi.encode(uint256(5)); + minStakingTime = 18; + maxDrawingTime = 24; + rngLookahead = 20; + hiddenVotes = false; + + UUPSProxy proxyCore = new UUPSProxy(address(coreLogic), ""); + + bytes memory initDataDk = abi.encodeWithSignature("initialize(address,address)", governor, address(proxyCore)); + + UUPSProxy proxyDk = new UUPSProxy(address(dkLogic), initDataDk); + disputeKit = DisputeKitClassic(address(proxyDk)); + + bytes memory initDataSm = abi.encodeWithSignature( + "initialize(address,address,uint256,uint256,address,uint256)", + governor, + address(proxyCore), + minStakingTime, + maxDrawingTime, + rng, + rngLookahead + ); + + UUPSProxy proxySm = new UUPSProxy(address(smLogic), initDataSm); + sortitionModule = SortitionModuleMock(address(proxySm)); + + core = KlerosCoreMock(address(proxyCore)); + core.initialize( + governor, + guardian, + pinakion, + jurorProsecutionModule, + disputeKit, + hiddenVotes, + [minStake, alpha, feeForJuror, jurorsForCourtJump], + timesPerPeriod, + sortitionExtraData, + sortitionModule + ); + vm.prank(staker1); + pinakion.approve(address(core), 1 ether); + vm.prank(staker2); + pinakion.approve(address(core), 1 ether); + + templateData = "AAA"; + templateDataMappings = "BBB"; + arbitratorExtraData = abi.encodePacked(uint256(GENERAL_COURT), DEFAULT_NB_OF_JURORS, DISPUTE_KIT_CLASSIC); + + bytes memory initDataRegistry = abi.encodeWithSignature("initialize(address)", governor); + UUPSProxy proxyRegistry = new UUPSProxy(address(registryLogic), initDataRegistry); + registry = DisputeTemplateRegistry(address(proxyRegistry)); + + arbitrable = new ArbitrableExample( + core, + templateData, + templateDataMappings, + arbitratorExtraData, + registry, + feeToken + ); + } + + function test_initialize() public { + assertEq(core.governor(), msg.sender, "Wrong governor"); + assertEq(core.guardian(), guardian, "Wrong guardian"); + assertEq(address(core.pinakion()), address(pinakion), "Wrong pinakion address"); + assertEq(core.jurorProsecutionModule(), jurorProsecutionModule, "Wrong jurorProsecutionModule address"); + assertEq(address(core.sortitionModule()), address(sortitionModule), "Wrong sortitionModule address"); + assertEq(core.getDisputeKitsLength(), 2, "Wrong DK array length"); + ( + uint96 courtParent, + bool courtHiddenVotes, + uint256 courtMinStake, + uint256 courtAlpha, + uint256 courtFeeForJuror, + uint256 courtJurorsForCourtJump, + bool courtDisabled + ) = core.courts(FORKING_COURT); + assertEq(courtParent, FORKING_COURT, "Wrong court parent"); + assertEq(courtHiddenVotes, false, "Wrong hiddenVotes value"); + assertEq(courtMinStake, 0, "Wrong minStake value"); + assertEq(courtAlpha, 0, "Wrong alpha value"); + assertEq(courtFeeForJuror, 0, "Wrong feeForJuror value"); + assertEq(courtJurorsForCourtJump, 0, "Wrong jurorsForCourtJump value"); + assertEq(courtDisabled, false, "Court should not be disabled"); + ( + courtParent, + courtHiddenVotes, + courtMinStake, + courtAlpha, + courtFeeForJuror, + courtJurorsForCourtJump, + courtDisabled + ) = core.courts(GENERAL_COURT); + assertEq(courtParent, FORKING_COURT, "Wrong court parent"); + assertEq(courtHiddenVotes, false, "Wrong hiddenVotes value"); + assertEq(courtMinStake, 1000, "Wrong minStake value"); + assertEq(courtAlpha, 10000, "Wrong alpha value"); + assertEq(courtFeeForJuror, 0.03 ether, "Wrong feeForJuror value"); + assertEq(courtJurorsForCourtJump, 511, "Wrong jurorsForCourtJump value"); + assertEq(courtDisabled, false, "Court should not be disabled"); + + uint256[] memory children = core.getCourtChildren(GENERAL_COURT); + assertEq(children.length, 0, "No children"); + uint256[4] memory courtTimesPerPeriod = core.getTimesPerPeriod(GENERAL_COURT); + for (uint256 i = 0; i < 4; i++) { + assertEq(courtTimesPerPeriod[i], timesPerPeriod[i], "Wrong times per period"); + } + + assertEq(address(core.disputeKits(NULL_DISPUTE_KIT)), address(0), "Wrong address NULL_DISPUTE_KIT"); + assertEq( + address(core.disputeKits(DISPUTE_KIT_CLASSIC)), + address(disputeKit), + "Wrong address DISPUTE_KIT_CLASSIC" + ); + assertEq(core.isSupported(FORKING_COURT, NULL_DISPUTE_KIT), false, "Forking court null dk should be false"); + assertEq( + core.isSupported(FORKING_COURT, DISPUTE_KIT_CLASSIC), + false, + "Forking court classic dk should be false" + ); + assertEq(core.isSupported(GENERAL_COURT, NULL_DISPUTE_KIT), false, "General court null dk should be false"); + assertEq(core.isSupported(GENERAL_COURT, DISPUTE_KIT_CLASSIC), true, "General court classic dk should be true"); + assertEq(core.paused(), false, "Wrong paused value"); + + assertEq(pinakion.name(), "Pinakion", "Wrong token name"); + assertEq(pinakion.symbol(), "PNK", "Wrong token symbol"); + assertEq(pinakion.totalSupply(), 1000000 ether, "Wrong total supply"); + assertEq(pinakion.balanceOf(msg.sender), 999998 ether, "Wrong token balance of governor"); + assertEq(pinakion.balanceOf(staker1), 1 ether, "Wrong token balance of staker1"); + assertEq(pinakion.allowance(staker1, address(core)), 1 ether, "Wrong allowance for staker1"); + assertEq(pinakion.balanceOf(staker2), 1 ether, "Wrong token balance of staker2"); + assertEq(pinakion.allowance(staker2, address(core)), 1 ether, "Wrong allowance for staker2"); + + assertEq(disputeKit.governor(), msg.sender, "Wrong DK governor"); + assertEq(address(disputeKit.core()), address(core), "Wrong core in DK"); + + assertEq(sortitionModule.governor(), msg.sender, "Wrong SM governor"); + assertEq(address(sortitionModule.core()), address(core), "Wrong core in SM"); + assertEq(uint256(sortitionModule.phase()), uint256(ISortitionModule.Phase.staking), "Phase should be 0"); + assertEq(sortitionModule.minStakingTime(), 18, "Wrong minStakingTime"); + assertEq(sortitionModule.maxDrawingTime(), 24, "Wrong maxDrawingTime"); + assertEq(sortitionModule.lastPhaseChange(), block.timestamp, "Wrong lastPhaseChange"); + assertEq(sortitionModule.randomNumberRequestBlock(), 0, "randomNumberRequestBlock should be 0"); + 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.rngLookahead(), 20, "Wrong rngLookahead"); + assertEq(sortitionModule.delayedStakeWriteIndex(), 0, "delayedStakeWriteIndex should be 0"); + assertEq(sortitionModule.delayedStakeReadIndex(), 1, "Wrong delayedStakeReadIndex"); + + (uint256 K, uint256 nodeLength) = sortitionModule.getSortitionProperties(bytes32(uint256(FORKING_COURT))); + assertEq(K, 5, "Wrong tree K FORKING_COURT"); + assertEq(nodeLength, 1, "Wrong node length for created tree FORKING_COURT"); + + (K, nodeLength) = sortitionModule.getSortitionProperties(bytes32(uint256(GENERAL_COURT))); + assertEq(K, 5, "Wrong tree K GENERAL_COURT"); + assertEq(nodeLength, 1, "Wrong node length for created tree GENERAL_COURT"); + } + + function test_initialize_events() public { + KlerosCoreMock coreLogic = new KlerosCoreMock(); + SortitionModuleMock smLogic = new SortitionModuleMock(); + DisputeKitClassic dkLogic = new DisputeKitClassic(); + rng = new BlockHashRNG(); + pinakion = new PNK(); + + governor = msg.sender; + guardian = vm.addr(1); + staker1 = vm.addr(2); + other = vm.addr(9); + jurorProsecutionModule = vm.addr(8); + minStake = 1000; + alpha = 10000; + feeForJuror = 0.03 ether; + jurorsForCourtJump = 511; + timesPerPeriod = [60, 120, 180, 240]; + + pinakion.transfer(msg.sender, totalSupply - 1 ether); + pinakion.transfer(staker1, 1 ether); + + sortitionExtraData = abi.encode(uint256(5)); + minStakingTime = 18; + maxDrawingTime = 24; + rngLookahead = 20; + hiddenVotes = false; + + UUPSProxy proxyCore = new UUPSProxy(address(coreLogic), ""); + + bytes memory initDataDk = abi.encodeWithSignature("initialize(address,address)", governor, address(proxyCore)); + + UUPSProxy proxyDk = new UUPSProxy(address(dkLogic), initDataDk); + disputeKit = DisputeKitClassic(address(proxyDk)); + + bytes memory initDataSm = abi.encodeWithSignature( + "initialize(address,address,uint256,uint256,address,uint256)", + governor, + address(proxyCore), + minStakingTime, + maxDrawingTime, + rng, + rngLookahead + ); + + UUPSProxy proxySm = new UUPSProxy(address(smLogic), initDataSm); + sortitionModule = SortitionModuleMock(address(proxySm)); + + core = KlerosCoreMock(address(proxyCore)); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.DisputeKitCreated(DISPUTE_KIT_CLASSIC, disputeKit); + vm.expectEmit(true, true, true, true); + + uint256[] memory supportedDK = new uint256[](1); + supportedDK[0] = DISPUTE_KIT_CLASSIC; + emit KlerosCoreBase.CourtCreated( + GENERAL_COURT, + FORKING_COURT, + false, + 1000, + 10000, + 0.03 ether, + 511, + [uint256(60), uint256(120), uint256(180), uint256(240)], // Explicitly convert otherwise it throws + supportedDK + ); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.DisputeKitEnabled(GENERAL_COURT, DISPUTE_KIT_CLASSIC, true); + core.initialize( + governor, + guardian, + pinakion, + jurorProsecutionModule, + disputeKit, + hiddenVotes, + [minStake, alpha, feeForJuror, jurorsForCourtJump], + timesPerPeriod, + sortitionExtraData, + sortitionModule + ); + } + + // ****************************************** // + // * Governance test * // + // ****************************************** // + + function test_pause() public { + vm.expectRevert(KlerosCoreBase.GuardianOrGovernorOnly.selector); + vm.prank(other); + core.pause(); + // Note that we must explicitly switch to the governor/guardian address to make the call, otherwise Foundry treats UUPS proxy as msg.sender. + vm.prank(guardian); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.Paused(); + core.pause(); + assertEq(core.paused(), true, "Wrong paused value"); + // Switch between governor and guardian to test both. WhenNotPausedOnly modifier is triggered after governor's check. + vm.prank(governor); + vm.expectRevert(KlerosCoreBase.WhenNotPausedOnly.selector); + core.pause(); + } + + function test_unpause() public { + vm.expectRevert(KlerosCoreBase.GovernorOnly.selector); + vm.prank(other); + core.unpause(); + + vm.expectRevert(KlerosCoreBase.WhenPausedOnly.selector); + vm.prank(governor); + core.unpause(); + + vm.prank(governor); + core.pause(); + vm.prank(governor); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.Unpaused(); + core.unpause(); + assertEq(core.paused(), false, "Wrong paused value"); + } + + function test_executeGovernorProposal() public { + bytes memory data = abi.encodeWithSignature("changeGovernor(address)", other); + vm.expectRevert(KlerosCoreBase.GovernorOnly.selector); + vm.prank(other); + core.executeGovernorProposal(address(core), 0, data); + + vm.expectRevert(KlerosCoreBase.UnsuccessfulCall.selector); + vm.prank(governor); + core.executeGovernorProposal(address(core), 0, data); // It'll fail because the core is not its own governor + + vm.prank(governor); + core.changeGovernor(payable(address(core))); + vm.prank(address(core)); + core.executeGovernorProposal(address(core), 0, data); + assertEq(core.governor(), other, "Wrong governor"); + } + + function test_changeGovernor() public { + vm.expectRevert(KlerosCoreBase.GovernorOnly.selector); + vm.prank(other); + core.changeGovernor(payable(other)); + vm.prank(governor); + core.changeGovernor(payable(other)); + assertEq(core.governor(), other, "Wrong governor"); + } + + function test_changeGuardian() public { + vm.expectRevert(KlerosCoreBase.GovernorOnly.selector); + vm.prank(other); + core.changeGuardian(other); + vm.prank(governor); + core.changeGuardian(other); + assertEq(core.guardian(), other, "Wrong guardian"); + } + + function test_changePinakion() public { + PNK fakePNK = new PNK(); + vm.expectRevert(KlerosCoreBase.GovernorOnly.selector); + vm.prank(other); + core.changePinakion(fakePNK); + vm.prank(governor); + core.changePinakion(fakePNK); + assertEq(address(core.pinakion()), address(fakePNK), "Wrong PNK"); + } + + function test_changeJurorProsecutionModule() public { + vm.expectRevert(KlerosCoreBase.GovernorOnly.selector); + vm.prank(other); + core.changeJurorProsecutionModule(other); + vm.prank(governor); + core.changeJurorProsecutionModule(other); + assertEq(core.jurorProsecutionModule(), other, "Wrong jurorProsecutionModule"); + } + + function test_changeSortitionModule() public { + SortitionModuleMock fakeSM = new SortitionModuleMock(); + vm.expectRevert(KlerosCoreBase.GovernorOnly.selector); + vm.prank(other); + core.changeSortitionModule(fakeSM); + vm.prank(governor); + core.changeSortitionModule(fakeSM); + assertEq(address(core.sortitionModule()), address(fakeSM), "Wrong sortitionModule"); + } + + function test_addNewDisputeKit() public { + DisputeKitSybilResistant newDK = new DisputeKitSybilResistant(); + vm.expectRevert(KlerosCoreBase.GovernorOnly.selector); + vm.prank(other); + core.addNewDisputeKit(newDK); + vm.prank(governor); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.DisputeKitCreated(2, newDK); + core.addNewDisputeKit(newDK); + assertEq(address(core.disputeKits(2)), address(newDK), "Wrong address of new DK"); + assertEq(core.getDisputeKitsLength(), 3, "Wrong DK array length"); + } + + function test_createCourt() public { + vm.expectRevert(KlerosCoreBase.GovernorOnly.selector); + vm.prank(other); + uint256[] memory supportedDK = new uint256[](2); + supportedDK[0] = DISPUTE_KIT_CLASSIC; + supportedDK[1] = 2; // New DK is added below. + core.createCourt( + GENERAL_COURT, + true, // Hidden votes + 2000, // min stake + 10000, // alpha + 0.03 ether, // fee for juror + 50, // jurors for jump + [uint256(10), uint256(20), uint256(30), uint256(40)], // Times per period + abi.encode(uint256(4)), // Sortition extra data + supportedDK + ); + + vm.expectRevert(KlerosCoreBase.MinStakeLowerThanParentCourt.selector); + vm.prank(governor); + core.createCourt( + GENERAL_COURT, + true, // Hidden votes + 800, // min stake + 10000, // alpha + 0.03 ether, // fee for juror + 50, // jurors for jump + [uint256(10), uint256(20), uint256(30), uint256(40)], // Times per period + abi.encode(uint256(4)), // Sortition extra data + supportedDK + ); + + vm.expectRevert(KlerosCoreBase.UnsupportedDisputeKit.selector); + vm.prank(governor); + uint256[] memory emptySupportedDK = new uint256[](0); + core.createCourt( + GENERAL_COURT, + true, // Hidden votes + 2000, // min stake + 10000, // alpha + 0.03 ether, // fee for juror + 50, // jurors for jump + [uint256(10), uint256(20), uint256(30), uint256(40)], // Times per period + abi.encode(uint256(4)), // Sortition extra data + emptySupportedDK + ); + + vm.expectRevert(KlerosCoreBase.InvalidForkingCourtAsParent.selector); + vm.prank(governor); + core.createCourt( + FORKING_COURT, + true, // Hidden votes + 2000, // min stake + 10000, // alpha + 0.03 ether, // fee for juror + 50, // jurors for jump + [uint256(10), uint256(20), uint256(30), uint256(40)], // Times per period + abi.encode(uint256(4)), // Sortition extra data + supportedDK + ); + + uint256[] memory badSupportedDK = new uint256[](2); + badSupportedDK[0] = NULL_DISPUTE_KIT; // Include NULL_DK to check that it reverts + badSupportedDK[1] = DISPUTE_KIT_CLASSIC; + vm.expectRevert(KlerosCoreBase.WrongDisputeKitIndex.selector); + vm.prank(governor); + core.createCourt( + GENERAL_COURT, + true, // Hidden votes + 2000, // min stake + 10000, // alpha + 0.03 ether, // fee for juror + 50, // jurors for jump + [uint256(10), uint256(20), uint256(30), uint256(40)], // Times per period + abi.encode(uint256(4)), // Sortition extra data + badSupportedDK + ); + + badSupportedDK[0] = DISPUTE_KIT_CLASSIC; + badSupportedDK[1] = 2; // Check out of bounds index + vm.expectRevert(KlerosCoreBase.WrongDisputeKitIndex.selector); + vm.prank(governor); + core.createCourt( + GENERAL_COURT, + true, // Hidden votes + 2000, // min stake + 10000, // alpha + 0.03 ether, // fee for juror + 50, // jurors for jump + [uint256(10), uint256(20), uint256(30), uint256(40)], // Times per period + abi.encode(uint256(4)), // Sortition extra data + badSupportedDK + ); + + // Add new DK to check the requirement for classic DK + DisputeKitSybilResistant newDK = new DisputeKitSybilResistant(); + vm.prank(governor); + core.addNewDisputeKit(newDK); + badSupportedDK = new uint256[](1); + badSupportedDK[0] = 2; // Include only sybil resistant dk + vm.expectRevert(KlerosCoreBase.MustSupportDisputeKitClassic.selector); + vm.prank(governor); + core.createCourt( + GENERAL_COURT, + true, // Hidden votes + 2000, // min stake + 10000, // alpha + 0.03 ether, // fee for juror + 50, // jurors for jump + [uint256(10), uint256(20), uint256(30), uint256(40)], // Times per period + abi.encode(uint256(4)), // Sortition extra data + badSupportedDK + ); + + vm.prank(governor); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.DisputeKitEnabled(2, DISPUTE_KIT_CLASSIC, true); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.DisputeKitEnabled(2, 2, true); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.CourtCreated( + 2, + GENERAL_COURT, + true, + 2000, + 20000, + 0.04 ether, + 50, + [uint256(10), uint256(20), uint256(30), uint256(40)], // Explicitly convert otherwise it throws + supportedDK + ); + core.createCourt( + GENERAL_COURT, + true, // Hidden votes + 2000, // min stake + 20000, // alpha + 0.04 ether, // fee for juror + 50, // jurors for jump + [uint256(10), uint256(20), uint256(30), uint256(40)], // Times per period + abi.encode(uint256(4)), // Sortition extra data + supportedDK + ); + + ( + uint96 courtParent, + bool courtHiddenVotes, + uint256 courtMinStake, + uint256 courtAlpha, + uint256 courtFeeForJuror, + uint256 courtJurorsForCourtJump, + bool courtDisabled + ) = core.courts(2); + assertEq(courtParent, GENERAL_COURT, "Wrong court parent"); + assertEq(courtHiddenVotes, true, "Wrong hiddenVotes value"); + assertEq(courtMinStake, 2000, "Wrong minStake value"); + assertEq(courtAlpha, 20000, "Wrong alpha value"); + assertEq(courtFeeForJuror, 0.04 ether, "Wrong feeForJuror value"); + assertEq(courtJurorsForCourtJump, 50, "Wrong jurorsForCourtJump value"); + assertEq(courtDisabled, false, "Court should not be disabled"); + + uint256[] memory children = core.getCourtChildren(2); + assertEq(children.length, 0, "No children"); + uint256[4] memory courtTimesPerPeriod = core.getTimesPerPeriod(2); + assertEq(courtTimesPerPeriod[0], uint256(10), "Wrong times per period 0"); + assertEq(courtTimesPerPeriod[1], uint256(20), "Wrong times per period 1"); + assertEq(courtTimesPerPeriod[2], uint256(30), "Wrong times per period 2"); + assertEq(courtTimesPerPeriod[3], uint256(40), "Wrong times per period 3"); + + children = core.getCourtChildren(GENERAL_COURT); // Check that parent updated children + assertEq(children.length, 1, "Wrong children count"); + assertEq(children[0], 2, "Wrong child id"); + + (uint256 K, uint256 nodeLength) = sortitionModule.getSortitionProperties(bytes32(uint256(2))); + assertEq(K, 4, "Wrong tree K of the new court"); + assertEq(nodeLength, 1, "Wrong node length for created tree of the new court"); + } + + function test_changeCourtParameters() public { + // Create a 2nd court to check the minStake requirements + vm.prank(governor); + uint96 newCourtID = 2; + uint256[] memory supportedDK = new uint256[](1); + supportedDK[0] = DISPUTE_KIT_CLASSIC; + core.createCourt( + GENERAL_COURT, + true, // Hidden votes + 2000, // min stake + 20000, // alpha + 0.04 ether, // fee for juror + 50, // jurors for jump + [uint256(10), uint256(20), uint256(30), uint256(40)], // Times per period + abi.encode(uint256(4)), // Sortition extra data + supportedDK + ); + + vm.expectRevert(KlerosCoreBase.GovernorOnly.selector); + vm.prank(other); + core.changeCourtParameters( + GENERAL_COURT, + true, // Hidden votes + 2000, // min stake + 10000, // alpha + 0.03 ether, // fee for juror + 50, // jurors for jump + [uint256(10), uint256(20), uint256(30), uint256(40)] // Times per period + ); + vm.expectRevert(KlerosCoreBase.MinStakeLowerThanParentCourt.selector); + vm.prank(governor); + // Min stake of a parent became higher than of a child + core.changeCourtParameters( + GENERAL_COURT, + true, // Hidden votes + 3000, // min stake + 10000, // alpha + 0.03 ether, // fee for juror + 50, // jurors for jump + [uint256(10), uint256(20), uint256(30), uint256(40)] // Times per period + ); + // Min stake of a child became lower than of a parent + vm.expectRevert(KlerosCoreBase.MinStakeLowerThanParentCourt.selector); + vm.prank(governor); + core.changeCourtParameters( + newCourtID, + true, // Hidden votes + 800, // min stake + 10000, // alpha + 0.03 ether, // fee for juror + 50, // jurors for jump + [uint256(10), uint256(20), uint256(30), uint256(40)] // Times per period + ); + + vm.prank(governor); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.CourtModified( + GENERAL_COURT, + true, + 2000, + 20000, + 0.04 ether, + 50, + [uint256(10), uint256(20), uint256(30), uint256(40)] // Explicitly convert otherwise it throws + ); + core.changeCourtParameters( + GENERAL_COURT, + true, // Hidden votes + 2000, // min stake + 20000, // alpha + 0.04 ether, // fee for juror + 50, // jurors for jump + [uint256(10), uint256(20), uint256(30), uint256(40)] // Times per period + ); + + ( + uint96 courtParent, + bool courtHiddenVotes, + uint256 courtMinStake, + uint256 courtAlpha, + uint256 courtFeeForJuror, + uint256 courtJurorsForCourtJump, + bool courtDisabled + ) = core.courts(GENERAL_COURT); + assertEq(courtHiddenVotes, true, "Wrong hiddenVotes value"); + assertEq(courtMinStake, 2000, "Wrong minStake value"); + assertEq(courtAlpha, 20000, "Wrong alpha value"); + assertEq(courtFeeForJuror, 0.04 ether, "Wrong feeForJuror value"); + assertEq(courtJurorsForCourtJump, 50, "Wrong jurorsForCourtJump value"); + assertEq(courtDisabled, false, "Court should not be disabled"); + + uint256[4] memory courtTimesPerPeriod = core.getTimesPerPeriod(GENERAL_COURT); + assertEq(courtTimesPerPeriod[0], uint256(10), "Wrong times per period 0"); + assertEq(courtTimesPerPeriod[1], uint256(20), "Wrong times per period 1"); + assertEq(courtTimesPerPeriod[2], uint256(30), "Wrong times per period 2"); + assertEq(courtTimesPerPeriod[3], uint256(40), "Wrong times per period 3"); + } + + function test_enableDisputeKits() public { + DisputeKitSybilResistant newDK = new DisputeKitSybilResistant(); + uint256 newDkID = 2; + vm.prank(governor); + core.addNewDisputeKit(newDK); + + vm.expectRevert(KlerosCoreBase.GovernorOnly.selector); + vm.prank(other); + uint256[] memory supportedDK = new uint256[](1); + supportedDK[0] = newDkID; + core.enableDisputeKits(GENERAL_COURT, supportedDK, true); + + vm.expectRevert(KlerosCoreBase.WrongDisputeKitIndex.selector); + vm.prank(governor); + supportedDK[0] = NULL_DISPUTE_KIT; + core.enableDisputeKits(GENERAL_COURT, supportedDK, true); + + vm.expectRevert(KlerosCoreBase.WrongDisputeKitIndex.selector); + vm.prank(governor); + supportedDK[0] = 3; // Out of bounds + core.enableDisputeKits(GENERAL_COURT, supportedDK, true); + + vm.expectRevert(KlerosCoreBase.CannotDisableClassicDK.selector); + vm.prank(governor); + supportedDK[0] = DISPUTE_KIT_CLASSIC; + core.enableDisputeKits(GENERAL_COURT, supportedDK, false); + + vm.prank(governor); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.DisputeKitEnabled(GENERAL_COURT, newDkID, true); + supportedDK[0] = newDkID; + core.enableDisputeKits(GENERAL_COURT, supportedDK, true); + assertEq(core.isSupported(GENERAL_COURT, newDkID), true, "New DK should be supported by General court"); + + vm.prank(governor); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.DisputeKitEnabled(GENERAL_COURT, newDkID, false); + core.enableDisputeKits(GENERAL_COURT, supportedDK, false); + assertEq(core.isSupported(GENERAL_COURT, newDkID), false, "New DK should be disabled in General court"); + } + + function test_changeAcceptedFeeTokens() public { + vm.expectRevert(KlerosCoreBase.GovernorOnly.selector); + vm.prank(other); + core.changeAcceptedFeeTokens(feeToken, true); + + (bool accepted, , ) = core.currencyRates(feeToken); + assertEq(accepted, false, "Token should not be accepted yet"); + + vm.prank(governor); + vm.expectEmit(true, true, true, true); + emit IArbitratorV2.AcceptedFeeToken(feeToken, true); + core.changeAcceptedFeeTokens(feeToken, true); + (accepted, , ) = core.currencyRates(feeToken); + assertEq(accepted, true, "Token should be accepted"); + } + + function test_changeCurrencyRates() public { + vm.expectRevert(KlerosCoreBase.GovernorOnly.selector); + vm.prank(other); + core.changeCurrencyRates(feeToken, 100, 200); + + (, uint256 rateInEth, uint256 rateDecimals) = core.currencyRates(feeToken); + assertEq(rateInEth, 0, "rateInEth should be 0"); + assertEq(rateDecimals, 0, "rateDecimals should be 0"); + + vm.prank(governor); + vm.expectEmit(true, true, true, true); + emit IArbitratorV2.NewCurrencyRate(feeToken, 100, 200); + core.changeCurrencyRates(feeToken, 100, 200); + + (, rateInEth, rateDecimals) = core.currencyRates(feeToken); + assertEq(rateInEth, 100, "rateInEth is incorrect"); + assertEq(rateDecimals, 200, "rateDecimals is incorrect"); + } + + function test_extraDataToCourtIDMinJurorsDisputeKit() public { + // Standard values + bytes memory extraData = abi.encodePacked(uint256(GENERAL_COURT), DEFAULT_NB_OF_JURORS, DISPUTE_KIT_CLASSIC); + + (uint96 courtID, uint256 minJurors, uint256 disputeKitID) = core.extraDataToCourtIDMinJurorsDisputeKit( + extraData + ); + assertEq(courtID, GENERAL_COURT, "Wrong courtID"); + assertEq(minJurors, DEFAULT_NB_OF_JURORS, "Wrong minJurors"); + assertEq(disputeKitID, DISPUTE_KIT_CLASSIC, "Wrong disputeKitID"); + + // Botched extraData. Values should fall into standard + extraData = "0xfa"; + + (courtID, minJurors, disputeKitID) = core.extraDataToCourtIDMinJurorsDisputeKit(extraData); + assertEq(courtID, GENERAL_COURT, "Wrong courtID"); + assertEq(minJurors, DEFAULT_NB_OF_JURORS, "Wrong minJurors"); + assertEq(disputeKitID, DISPUTE_KIT_CLASSIC, "Wrong disputeKitID"); + + // Custom values. + vm.startPrank(governor); + core.addNewDisputeKit(disputeKit); + core.addNewDisputeKit(disputeKit); + core.addNewDisputeKit(disputeKit); + core.addNewDisputeKit(disputeKit); + core.addNewDisputeKit(disputeKit); + extraData = abi.encodePacked(uint256(50), uint256(41), uint256(6)); + + (courtID, minJurors, disputeKitID) = core.extraDataToCourtIDMinJurorsDisputeKit(extraData); + assertEq(courtID, GENERAL_COURT, "Wrong courtID"); // Value in extra data is out of scope so fall back + assertEq(minJurors, 41, "Wrong minJurors"); + assertEq(disputeKitID, 6, "Wrong disputeKitID"); + } + + // *************************************** // + // * Staking test * // + // *************************************** // + + function test_setStake_increase() public { + vm.prank(governor); + core.pause(); + vm.expectRevert(KlerosCoreBase.WhenNotPausedOnly.selector); + vm.prank(staker1); + core.setStake(GENERAL_COURT, 1000); + vm.prank(governor); + core.unpause(); + + vm.expectRevert(KlerosCoreBase.StakingNotPossibeInThisCourt.selector); + vm.prank(staker1); + core.setStake(FORKING_COURT, 1000); + + uint96 badCourtID = 2; + vm.expectRevert(KlerosCoreBase.StakingNotPossibeInThisCourt.selector); + vm.prank(staker1); + core.setStake(badCourtID, 1000); + + vm.expectRevert(KlerosCoreBase.StakingLessThanCourtMinStake.selector); + vm.prank(staker1); + core.setStake(GENERAL_COURT, 800); + + vm.expectRevert(KlerosCoreBase.StakingZeroWhenNoStake.selector); + vm.prank(staker1); + core.setStake(GENERAL_COURT, 0); + + vm.prank(staker1); + vm.expectEmit(true, true, true, true); + emit SortitionModuleBase.StakeSet(staker1, GENERAL_COURT, 1001); + core.setStake(GENERAL_COURT, 1001); + + (uint256 totalStaked, uint256 totalLocked, uint256 stakedInCourt, uint256 nbCourts) = sortitionModule + .getJurorBalance(staker1, GENERAL_COURT); + assertEq(totalStaked, 1001, "Wrong amount total staked"); + assertEq(totalLocked, 0, "Wrong amount locked"); + assertEq(stakedInCourt, 1001, "Wrong amount staked in court"); + assertEq(nbCourts, 1, "Wrong number of courts"); + + uint96[] memory courts = sortitionModule.getJurorCourtIDs(staker1); + assertEq(courts.length, 1, "Wrong courts count"); + assertEq(courts[0], GENERAL_COURT, "Wrong court id"); + assertEq(sortitionModule.isJurorStaked(staker1), true, "Juror should be staked"); + + assertEq(pinakion.balanceOf(address(core)), 1001, "Wrong token balance of the core"); + assertEq(pinakion.balanceOf(staker1), 999999999999998999, "Wrong token balance of staker1"); // 1 eth - 1001 wei + assertEq(pinakion.allowance(staker1, address(core)), 999999999999998999, "Wrong allowance for staker1"); + + vm.expectRevert(KlerosCoreBase.StakingTransferFailed.selector); // This error will be caught because governor didn't approve any tokens for KlerosCore + vm.prank(governor); + core.setStake(GENERAL_COURT, 1000); + + // Increase stake one more time to verify the correct behavior + vm.prank(staker1); + vm.expectEmit(true, true, true, true); + emit SortitionModuleBase.StakeSet(staker1, GENERAL_COURT, 2000); + core.setStake(GENERAL_COURT, 2000); + + (totalStaked, totalLocked, stakedInCourt, nbCourts) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); + assertEq(totalStaked, 2000, "Wrong amount total staked"); + assertEq(totalLocked, 0, "Wrong amount locked"); + assertEq(stakedInCourt, 2000, "Wrong amount staked in court"); + assertEq(nbCourts, 1, "Number of courts should not increase"); + + assertEq(pinakion.balanceOf(address(core)), 2000, "Wrong token balance of the core"); + assertEq(pinakion.balanceOf(staker1), 999999999999998000, "Wrong token balance of staker1"); // 1 eth - 2000 wei + assertEq(pinakion.allowance(staker1, address(core)), 999999999999998000, "Wrong allowance for staker1"); + } + + function test_setStake_decrease() public { + vm.prank(staker1); + core.setStake(GENERAL_COURT, 2000); + assertEq(pinakion.balanceOf(address(core)), 2000, "Wrong token balance of the core"); + assertEq(pinakion.balanceOf(staker1), 999999999999998000, "Wrong token balance of staker1"); + assertEq(pinakion.allowance(staker1, address(core)), 999999999999998000, "Wrong allowance for staker1"); + + vm.prank(staker1); + core.setStake(GENERAL_COURT, 1500); // Decrease the stake to see if it's reflected correctly + (uint256 totalStaked, uint256 totalLocked, uint256 stakedInCourt, uint256 nbCourts) = sortitionModule + .getJurorBalance(staker1, GENERAL_COURT); + assertEq(totalStaked, 1500, "Wrong amount total staked"); + assertEq(totalLocked, 0, "Wrong amount locked"); + assertEq(stakedInCourt, 1500, "Wrong amount staked in court"); + assertEq(nbCourts, 1, "Wrong number of courts"); + + uint96[] memory courts = sortitionModule.getJurorCourtIDs(staker1); + assertEq(courts.length, 1, "Wrong courts count"); + assertEq(courts[0], GENERAL_COURT, "Wrong court id"); + assertEq(sortitionModule.isJurorStaked(staker1), true, "Juror should be staked"); + + assertEq(pinakion.balanceOf(address(core)), 1500, "Wrong token balance of the core"); + assertEq(pinakion.balanceOf(staker1), 999999999999998500, "Wrong token balance of staker1"); + assertEq( + pinakion.allowance(staker1, address(core)), + 999999999999998000, + "Allowance should not change during withdrawal" + ); + + vm.prank(address(core)); + pinakion.transfer(staker1, 1); // Manually send 1 token to make the withdrawal fail + + vm.expectRevert(KlerosCoreBase.UnstakingTransferFailed.selector); + vm.prank(staker1); + core.setStake(GENERAL_COURT, 0); + + vm.prank(address(staker1)); + pinakion.transfer(address(core), 1); // Manually give the token back + vm.prank(staker1); + core.setStake(GENERAL_COURT, 0); + + (totalStaked, totalLocked, stakedInCourt, nbCourts) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); + assertEq(totalStaked, 0, "Wrong amount total staked"); + assertEq(totalLocked, 0, "Wrong amount locked"); + assertEq(stakedInCourt, 0, "Wrong amount staked in court"); + assertEq(nbCourts, 0, "Wrong number of courts"); + + courts = sortitionModule.getJurorCourtIDs(staker1); + assertEq(courts.length, 0, "Wrong courts count"); + assertEq(sortitionModule.isJurorStaked(staker1), false, "Juror should not be staked"); + + assertEq(pinakion.balanceOf(address(core)), 0, "Wrong token balance of the core"); + assertEq(pinakion.balanceOf(staker1), 1 ether, "Wrong token balance of staker1"); + assertEq( + pinakion.allowance(staker1, address(core)), + 999999999999998000, + "Allowance should not change during withdrawal" + ); + } + + function test_setStake_maxStakePathCheck() public { + uint256[] memory supportedDK = new uint256[](1); + supportedDK[0] = DISPUTE_KIT_CLASSIC; + + // Create 4 courts to check the require + for (uint96 i = GENERAL_COURT; i <= 4; i++) { + vm.prank(governor); + core.createCourt( + GENERAL_COURT, + true, + 2000, + 20000, + 0.04 ether, + 50, + [uint256(10), uint256(20), uint256(30), uint256(40)], + abi.encode(uint256(4)), + supportedDK + ); + vm.prank(staker1); + core.setStake(i, 2000); + } + + uint96[] memory courts = sortitionModule.getJurorCourtIDs(staker1); + assertEq(courts.length, 4, "Wrong courts count"); + + uint96 excessiveCourtID = 5; + vm.expectRevert(KlerosCoreBase.StakingInTooManyCourts.selector); + vm.prank(staker1); + core.setStake(excessiveCourtID, 2000); + } + + function test_setStake_increaseDrawingPhase() public { + // Set the stake and create a dispute to advance the phase + vm.prank(staker1); + core.setStake(GENERAL_COURT, 1000); + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + assertEq(sortitionModule.disputesWithoutJurors(), 1, "Wrong disputesWithoutJurors count"); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.roll(block.number + rngLookahead + 1); + sortitionModule.passPhase(); // Drawing phase + + assertEq(pinakion.balanceOf(address(core)), 1000, "Wrong token balance of the core"); + assertEq(pinakion.balanceOf(staker1), 999999999999999000, "Wrong token balance of staker1"); + assertEq(uint256(sortitionModule.phase()), uint256(ISortitionModule.Phase.drawing), "Wrong phase"); + + vm.prank(staker1); + vm.expectEmit(true, true, true, true); + emit SortitionModuleBase.StakeDelayedAlreadyTransferred(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, true, "Should be flagged as transferred"); + + (uint256 totalStaked, uint256 totalLocked, uint256 stakedInCourt, uint256 nbCourts) = sortitionModule + .getJurorBalance(staker1, GENERAL_COURT); + assertEq(totalStaked, 1500, "Wrong amount total staked"); + assertEq(totalLocked, 0, "Wrong amount locked"); + assertEq(stakedInCourt, 1000, "Amount staked in court should not change until delayed stake is executed"); + assertEq(nbCourts, 1, "Wrong number of courts"); + + uint96[] memory courts = sortitionModule.getJurorCourtIDs(staker1); + assertEq(courts.length, 1, "Wrong courts count"); + assertEq(courts[0], GENERAL_COURT, "Wrong court id"); + assertEq(sortitionModule.isJurorStaked(staker1), true, "Juror should be staked"); + + assertEq(pinakion.balanceOf(address(core)), 1500, "Wrong token balance of the core"); + assertEq(pinakion.balanceOf(staker1), 999999999999998500, "Wrong token balance of staker1"); + assertEq(pinakion.allowance(staker1, address(core)), 999999999999998500, "Wrong allowance amount"); + } + + function test_setStake_decreaseDrawingPhase() public { + // Set the stake and create a dispute to advance the phase + vm.prank(staker1); + core.setStake(GENERAL_COURT, 2000); + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.roll(block.number + rngLookahead + 1); + sortitionModule.passPhase(); // Drawing phase + + assertEq(pinakion.balanceOf(address(core)), 2000, "Wrong token balance of the core"); + assertEq(pinakion.balanceOf(staker1), 999999999999998000, "Wrong token balance of staker1"); + + vm.prank(staker1); + vm.expectEmit(true, true, true, true); + emit SortitionModuleBase.StakeDelayedNotTransferred(staker1, GENERAL_COURT, 1800); + core.setStake(GENERAL_COURT, 1800); + + (uint256 totalStaked, , uint256 stakedInCourt, ) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); + assertEq(totalStaked, 2000, "Total staked amount should not change"); + assertEq(stakedInCourt, 2000, "Amount staked in court should not change"); + + assertEq(pinakion.balanceOf(address(core)), 2000, "Token balance of the core should not change"); + assertEq(pinakion.balanceOf(staker1), 999999999999998000, "Wrong token balance of staker1"); + } + + function test_setStake_LockedTokens() public { + // Check that correct amount is taken when locked tokens amount exceeds the staked amount + vm.prank(staker1); + core.setStake(GENERAL_COURT, 10000); + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.roll(block.number + rngLookahead + 1); + sortitionModule.passPhase(); // Drawing phase + + uint256 disputeID = 0; + core.draw(disputeID, DEFAULT_NB_OF_JURORS); + (uint256 totalStaked, uint256 totalLocked, uint256 stakedInCourt, uint256 nbCourts) = sortitionModule + .getJurorBalance(staker1, GENERAL_COURT); + assertEq(totalStaked, 10000, "Wrong amount total staked"); + assertEq(totalLocked, 3000, "Wrong amount locked"); // 1000 per draw and the juror was drawn 3 times + assertEq(stakedInCourt, 10000, "Wrong amount staked in court"); + + sortitionModule.passPhase(); // Staking + + assertEq(pinakion.balanceOf(address(core)), 10000, "Wrong token balance of the core"); + assertEq(pinakion.balanceOf(staker1), 999999999999990000, "Wrong token balance of staker1"); + + // Unstake to check that locked tokens will remain + vm.prank(staker1); + core.setStake(GENERAL_COURT, 0); + + (totalStaked, totalLocked, stakedInCourt, nbCourts) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); + assertEq(totalStaked, 0, "Wrong amount total staked"); + assertEq(totalLocked, 3000, "Wrong amount locked"); + assertEq(stakedInCourt, 0, "Wrong amount staked in court"); + assertEq(nbCourts, 0, "Wrong amount staked in court"); + + assertEq(pinakion.balanceOf(address(core)), 3000, "Wrong token balance of the core"); + assertEq(pinakion.balanceOf(staker1), 999999999999997000, "Wrong token balance of staker1"); + + // Stake again to see that locked tokens will count when increasing the stake. We check that the court won't take the full stake + // but only the remaining part. + vm.prank(staker1); + core.setStake(GENERAL_COURT, 5000); + + (totalStaked, totalLocked, stakedInCourt, nbCourts) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); + assertEq(totalStaked, 5000, "Wrong amount total staked"); + assertEq(totalLocked, 3000, "Wrong amount locked"); + assertEq(stakedInCourt, 5000, "Wrong amount staked in court"); + assertEq(nbCourts, 1, "Wrong amount staked in court"); + + assertEq(pinakion.balanceOf(address(core)), 5000, "Locked tokens should stay in the core"); + assertEq(pinakion.balanceOf(staker1), 999999999999995000, "Wrong token balance of staker1"); + } + + function test_executeDelayedStakes() public { + // Stake as staker2 as well to diversify the execution of delayed stakes + vm.prank(staker2); + core.setStake(GENERAL_COURT, 10000); + + vm.expectRevert(bytes("No delayed stake to execute.")); + sortitionModule.executeDelayedStakes(5); + + // Set the stake and create a dispute to advance the phase + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.roll(block.number + rngLookahead + 1); + sortitionModule.passPhase(); // Drawing phase + uint256 disputeID = 0; + core.draw(disputeID, DEFAULT_NB_OF_JURORS); + + vm.expectRevert(bytes("Should be in Staking phase.")); + sortitionModule.executeDelayedStakes(5); + + vm.prank(staker1); + core.setStake(GENERAL_COURT, 1500); + assertEq(pinakion.balanceOf(address(core)), 11500, "Wrong token balance of the core"); + assertEq(pinakion.balanceOf(staker1), 999999999999998500, "Wrong token balance of staker1"); + + vm.prank(staker2); + core.setStake(GENERAL_COURT, 0); + assertEq(pinakion.balanceOf(staker2), 999999999999990000, "Wrong token balance of staker2"); // Balance should not change since wrong phase + + vm.prank(staker1); + vm.expectEmit(true, true, true, true); + emit SortitionModuleBase.StakeDelayedAlreadyTransferredWithdrawn(staker1, GENERAL_COURT, 1500); + 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); + + // First delayed stake should be nullified + 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"); + + (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 for staker2"); + + (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, true, "Should be true for staker1"); + + assertEq(pinakion.balanceOf(address(core)), 11800, "Wrong token balance of the core"); + assertEq(pinakion.balanceOf(staker1), 999999999999998200, "Wrong token balance of staker1"); + assertEq(pinakion.balanceOf(staker2), 999999999999990000, "Wrong token balance of staker2"); + + (uint256 totalStaked, uint256 totalLocked, uint256 stakedInCourt, uint256 nbCourts) = sortitionModule + .getJurorBalance(staker1, GENERAL_COURT); // Only check the first staker since he has consecutive delayed stakes + assertEq(totalStaked, 1800, "Wrong amount total staked"); + assertEq(totalLocked, 0, "Wrong amount locked"); + assertEq(stakedInCourt, 0, "Wrong amount staked in court"); + assertEq(nbCourts, 1, "Wrong amount staked in court"); + + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Staking. Delayed stakes can be executed now + + vm.prank(address(core)); + pinakion.transfer(governor, 10000); // Dispose of the tokens of 2nd staker to make the execution fail for the 2nd delayed stake + assertEq(pinakion.balanceOf(address(core)), 1800, "Wrong token balance of the core"); + + vm.expectEmit(true, true, true, true); + emit SortitionModuleBase.StakeSet(staker1, GENERAL_COURT, 1800); + sortitionModule.executeDelayedStakes(20); // Deliberately ask for more iterations than needed + + assertEq(sortitionModule.delayedStakeWriteIndex(), 3, "Wrong delayedStakeWriteIndex"); + assertEq(sortitionModule.delayedStakeReadIndex(), 4, "Wrong delayedStakeReadIndex"); + + // Check that delayed stakes are nullified + for (uint i = 2; i <= sortitionModule.delayedStakeWriteIndex(); i++) { + (account, courtID, stake, alreadyTransferred) = 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(pinakion.balanceOf(staker1), 999999999999998200, "Wrong token balance of staker1"); + + (totalStaked, totalLocked, stakedInCourt, nbCourts) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); + assertEq(totalStaked, 1800, "Wrong amount total staked"); + assertEq(totalLocked, 0, "Wrong amount locked"); + assertEq(stakedInCourt, 1800, "Wrong amount staked in court"); + assertEq(nbCourts, 1, "Wrong amount staked in court"); + + // Staker2 not getting the tokens back indicates that his delayed stake was skipped and the flow wasn't disrupted + assertEq(pinakion.balanceOf(staker2), 999999999999990000, "Wrong token balance of staker2"); + } + + function test_deleteDelayedStake() public { + // Check that the delayed stake gets deleted without execution if the juror changed his stake in staking phase before its execution. + vm.prank(staker1); + core.setStake(GENERAL_COURT, 1000); + + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.roll(block.number + rngLookahead + 1); + sortitionModule.passPhase(); // Drawing phase + + vm.prank(staker1); + core.setStake(GENERAL_COURT, 1500); // Create delayed stake + + (uint256 totalStaked, , uint256 stakedInCourt, ) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); + assertEq(totalStaked, 1500, "Wrong amount total staked"); + assertEq(stakedInCourt, 1000, "Wrong amount staked in court"); + assertEq(pinakion.balanceOf(staker1), 999999999999998500, "Wrong token balance of the staker1"); + assertEq(pinakion.balanceOf(address(core)), 1500, "Wrong token balance of the core"); + + (address account, uint96 courtID, uint256 stake, bool alreadyTransferred) = sortitionModule.delayedStakes(1); + assertEq(account, staker1, "Wrong account"); + assertEq(courtID, GENERAL_COURT, "Wrong court id"); + assertEq(stake, 1500, "Wrong amount for delayed stake"); + assertEq(alreadyTransferred, true, "Should be true"); + + vm.warp(block.timestamp + maxDrawingTime); + sortitionModule.passPhase(); // Staking phase + + vm.prank(staker1); + core.setStake(GENERAL_COURT, 1700); // Set stake 2nd time, this time in staking phase to see that the delayed stake will be nullified. + + (totalStaked, , stakedInCourt, ) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); + assertEq(totalStaked, 1700, "Wrong amount total staked"); + assertEq(stakedInCourt, 1700, "Wrong amount staked in court"); + assertEq(pinakion.balanceOf(staker1), 999999999999998300, "Wrong token balance of the staker1"); + assertEq(pinakion.balanceOf(address(core)), 1700, "Wrong token balance of the core"); + + sortitionModule.executeDelayedStakes(1); + (account, courtID, stake, alreadyTransferred) = sortitionModule.delayedStakes(1); + // Check that delayed stake is deleted + 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"); + } + + function test_setStakeBySortitionModule() public { + // Note that functionality of this function was checked during delayed stakes execution + vm.expectRevert(KlerosCoreBase.SortitionModuleOnly.selector); + vm.prank(governor); + core.setStakeBySortitionModule(staker1, GENERAL_COURT, 1000, false); + } + + // *************************************** // + // * Disputes * // + // *************************************** // + + function test_createDispute_eth() public { + // Create a new court and DK to test non-standard extra data + uint256 newFee = 0.01 ether; + uint96 newCourtID = 2; + uint256 newNbJurors = 4; + uint256 newDkID = 2; + uint256[] memory supportedDK = new uint256[](1); + supportedDK[0] = DISPUTE_KIT_CLASSIC; + bytes memory newExtraData = abi.encodePacked(uint256(newCourtID), newNbJurors, newDkID); + + vm.prank(governor); + core.addNewDisputeKit(disputeKit); // Just add the same dk to avoid dealing with initialization + vm.prank(governor); + core.createCourt( + GENERAL_COURT, + true, // Hidden votes + 2000, // min stake + 20000, // alpha + newFee, // fee for juror + 50, // jurors for jump + [uint256(10), uint256(20), uint256(30), uint256(40)], // Times per period + abi.encode(uint256(4)), // Sortition extra data + supportedDK + ); + + arbitrable.changeArbitratorExtraData(newExtraData); + + vm.expectRevert(KlerosCoreBase.ArbitrationFeesNotEnough.selector); + vm.prank(disputer); + arbitrable.createDispute{value: newFee * newNbJurors - 1}("Action"); + + vm.expectRevert(KlerosCoreBase.DisputeKitNotSupportedByCourt.selector); + vm.prank(disputer); + arbitrable.createDispute{value: 0.04 ether}("Action"); + + vm.prank(governor); + supportedDK = new uint256[](1); + supportedDK[0] = newDkID; + core.enableDisputeKits(newCourtID, supportedDK, true); + + uint256 disputeID = 0; + uint256 nbChoices = 2; + vm.prank(disputer); + vm.expectEmit(true, true, true, true); + emit DisputeKitClassic.DisputeCreation(disputeID, nbChoices, newExtraData); + vm.expectEmit(true, true, true, true); + emit IArbitratorV2.DisputeCreation(disputeID, arbitrable); + arbitrable.createDispute{value: 0.04 ether}("Action"); + + assertEq(sortitionModule.disputesWithoutJurors(), 1, "Wrong disputesWithoutJurors count"); + ( + uint96 courtID, + IArbitrableV2 arbitrated, + KlerosCoreBase.Period period, + bool ruled, + uint256 lastPeriodChange + ) = core.disputes(disputeID); + + assertEq(courtID, newCourtID, "Wrong court ID"); + assertEq(address(arbitrated), address(arbitrable), "Wrong arbitrable"); + assertEq(uint256(period), uint256(KlerosCoreBase.Period.evidence), "Wrong period"); + assertEq(ruled, false, "Should not be ruled"); + assertEq(lastPeriodChange, block.timestamp, "Wrong lastPeriodChange"); + + KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 0); + assertEq(round.disputeKitID, newDkID, "Wrong DK ID"); + assertEq(round.pnkAtStakePerJuror, 4000, "Wrong pnkAtStakePerJuror"); // minStake * alpha / divisor = 2000 * 20000/10000 + assertEq(round.totalFeesForJurors, 0.04 ether, "Wrong totalFeesForJurors"); + assertEq(round.nbVotes, 4, "Wrong nbVotes"); + assertEq(round.repartitions, 0, "repartitions should be 0"); + assertEq(round.pnkPenalties, 0, "pnkPenalties should be 0"); + assertEq(round.sumFeeRewardPaid, 0, "sumFeeRewardPaid should be 0"); + assertEq(round.sumPnkRewardPaid, 0, "sumPnkRewardPaid should be 0"); + assertEq(address(round.feeToken), address(0), "feeToken should be 0"); + assertEq(round.drawIterations, 0, "drawIterations should be 0"); + + (uint256 numberOfChoices, bool jumped, bytes memory extraData) = disputeKit.disputes(disputeID); + + assertEq(numberOfChoices, 2, "Wrong numberOfChoices"); + assertEq(jumped, false, "jumped should be false"); + assertEq(extraData, newExtraData, "Wrong extra data"); + assertEq(disputeKit.coreDisputeIDToLocal(0), disputeID, "Wrong local disputeID"); + + ( + uint256 winningChoice, + bool tied, + uint256 totalVoted, + uint256 totalCommited, + uint256 nbVoters, + uint256 choiceCount + ) = disputeKit.getRoundInfo(0, 0, 0); + assertEq(winningChoice, 0, "winningChoice should be 0"); + assertEq(tied, true, "tied should be true"); + assertEq(totalVoted, 0, "totalVoted should be 0"); + assertEq(totalCommited, 0, "totalCommited should be 0"); + assertEq(nbVoters, 0, "nbVoters should be 0"); + assertEq(choiceCount, 0, "choiceCount should be 0"); + } + + function test_createDispute_tokens() public { + feeToken.transfer(disputer, 1 ether); + vm.prank(disputer); + feeToken.approve(address(arbitrable), 1 ether); + + vm.expectRevert(KlerosCoreBase.TokenNotAccepted.selector); + vm.prank(disputer); + arbitrable.createDispute("Action", 0.18 ether); + + vm.prank(governor); + core.changeAcceptedFeeTokens(feeToken, true); + vm.prank(governor); + core.changeCurrencyRates(feeToken, 500, 3); + + vm.expectRevert(KlerosCoreBase.ArbitrationFeesNotEnough.selector); + vm.prank(disputer); + arbitrable.createDispute("Action", 0.18 ether - 1); + + vm.expectRevert(KlerosCoreBase.TransferFailed.selector); + vm.prank(address(arbitrable)); // Bypass createDispute in arbitrable to avoid transfer checks there and make the arbitrable call KC directly + core.createDispute(2, arbitratorExtraData, feeToken, 0.18 ether); + + assertEq(core.arbitrationCost(arbitratorExtraData, feeToken), 0.18 ether, "Wrong token cost"); + vm.prank(disputer); + arbitrable.createDispute("Action", 0.18 ether); + + KlerosCoreBase.Round memory round = core.getRoundInfo(0, 0); + assertEq(round.totalFeesForJurors, 0.18 ether, "Wrong totalFeesForJurors"); + assertEq(round.nbVotes, 3, "Wrong nbVotes"); + assertEq(address(round.feeToken), address(feeToken), "Wrong feeToken"); + + assertEq(feeToken.balanceOf(address(core)), 0.18 ether, "Wrong token balance of the core"); + assertEq(feeToken.balanceOf(disputer), 0.82 ether, "Wrong token balance of the disputer"); + } + + function test_draw() public { + uint256 disputeID = 0; + uint256 roundID = 0; + + vm.prank(staker1); + core.setStake(GENERAL_COURT, 1500); + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.roll(block.number + rngLookahead + 1); + sortitionModule.passPhase(); // Drawing phase + + vm.expectEmit(true, true, true, true); + emit SortitionModuleBase.StakeLocked(staker1, 1000, false); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.Draw(staker1, disputeID, roundID, 0); // VoteID = 0 + + core.draw(disputeID, DEFAULT_NB_OF_JURORS); // Do 3 iterations, but the current stake will only allow 1. + + (uint256 totalStaked, uint256 totalLocked, uint256 stakedInCourt, ) = sortitionModule.getJurorBalance( + staker1, + GENERAL_COURT + ); + assertEq(totalStaked, 1500, "Wrong amount total staked"); + assertEq(totalLocked, 1000, "Wrong amount locked"); // 1000 per draw + assertEq(stakedInCourt, 1500, "Wrong amount staked in court"); + assertEq(sortitionModule.disputesWithoutJurors(), 1, "Wrong disputesWithoutJurors count"); + + KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 0); + assertEq(round.drawIterations, 3, "Wrong drawIterations number"); + + vm.prank(staker1); + core.setStake(GENERAL_COURT, 3000); // Set stake to the minimal amount to cover the full dispute. The stake will be updated in Drawing phase since it's an increase. + + vm.expectEmit(true, true, true, true); + emit SortitionModuleBase.StakeLocked(staker1, 1000, false); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.Draw(staker1, disputeID, roundID, 1); // VoteID = 1 + vm.expectEmit(true, true, true, true); + emit SortitionModuleBase.StakeLocked(staker1, 1000, false); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.Draw(staker1, disputeID, roundID, 2); // VoteID = 2 + + core.draw(disputeID, DEFAULT_NB_OF_JURORS); + + (totalStaked, totalLocked, stakedInCourt, ) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); + assertEq(totalStaked, 3000, "Wrong amount total staked"); + assertEq(totalLocked, 3000, "Wrong amount locked"); // 1000 per draw and the juror was drawn 3 times + assertEq(stakedInCourt, 1500, "Wrong amount staked in court"); + assertEq(sortitionModule.disputesWithoutJurors(), 0, "Wrong disputesWithoutJurors count"); + + round = core.getRoundInfo(disputeID, roundID); + assertEq(round.drawIterations, 5, "Wrong drawIterations number"); // It's 5 because we needed only 2 iterations to draw the rest of the jurors + + for (uint256 i = 0; i < DEFAULT_NB_OF_JURORS; i++) { + (address account, bytes32 commit, uint256 choice, bool voted) = disputeKit.getVoteInfo(0, 0, i); + assertEq(account, staker1, "Wrong drawn account"); + assertEq(commit, bytes32(0), "Commit should be empty"); + assertEq(choice, 0, "Choice should be empty"); + assertEq(voted, false, "Voted should be false"); + } + } + + function test_draw_parentCourts() public { + uint96 newCourtID = 2; + uint256 disputeID = 0; + uint256 roundID = 0; + + // Create a child court and stake exclusively there to check that parent courts hold drawing power. + vm.prank(governor); + uint256[] memory supportedDK = new uint256[](1); + supportedDK[0] = DISPUTE_KIT_CLASSIC; + core.createCourt( + GENERAL_COURT, + true, // Hidden votes + 1000, // min stake + 10000, // alpha + 0.03 ether, // fee for juror + 50, // jurors for jump + [uint256(10), uint256(20), uint256(30), uint256(40)], // Times per period + sortitionExtraData, // Sortition extra data + supportedDK + ); + + uint256[] memory children = core.getCourtChildren(GENERAL_COURT); + assertEq(children.length, 1, "Wrong children count"); + assertEq(children[0], 2, "Wrong child ID"); + + vm.prank(staker1); + core.setStake(newCourtID, 3000); + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); // Dispute uses general court by default + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.roll(block.number + rngLookahead + 1); + sortitionModule.passPhase(); // Drawing phase + + (uint96 courtID, , , , ) = core.disputes(disputeID); + assertEq(courtID, GENERAL_COURT, "Wrong court ID of the dispute"); + + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.Draw(staker1, disputeID, roundID, 0); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.Draw(staker1, disputeID, roundID, 1); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.Draw(staker1, disputeID, roundID, 2); + core.draw(disputeID, DEFAULT_NB_OF_JURORS); + + assertEq(sortitionModule.disputesWithoutJurors(), 0, "Wrong disputesWithoutJurors count"); + + KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, roundID); + assertEq(round.drawIterations, 3, "Wrong drawIterations number"); + + (, , , , uint256 nbVoters, ) = disputeKit.getRoundInfo(disputeID, roundID, 0); + assertEq(nbVoters, 3, "nbVoters should be 3"); + } + + function test_castCommit() public { + // Change hidden votes in general court + uint256 disputeID = 0; + vm.prank(governor); + core.changeCourtParameters( + GENERAL_COURT, + true, // Hidden votes + 1000, // min stake + 10000, // alpha + 0.03 ether, // fee for juror + 511, // jurors for jump + [uint256(60), uint256(120), uint256(180), uint256(240)] // Times per period + ); + + vm.prank(staker1); + core.setStake(GENERAL_COURT, 10000); + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.roll(block.number + rngLookahead + 1); + sortitionModule.passPhase(); // Drawing phase + core.draw(disputeID, DEFAULT_NB_OF_JURORS); + + uint256 YES = 1; + uint256 salt = 123455678; + uint256[] memory voteIDs = new uint256[](1); + voteIDs[0] = 0; + bytes32 commit; + vm.prank(staker1); + vm.expectRevert(bytes("The dispute should be in Commit period.")); + disputeKit.castCommit(disputeID, voteIDs, commit); + + vm.expectRevert(KlerosCoreBase.EvidenceNotPassedAndNotAppeal.selector); + core.passPeriod(disputeID); + vm.warp(block.timestamp + timesPerPeriod[0]); + + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.commit); + core.passPeriod(disputeID); + + (, , KlerosCoreBase.Period period, , uint256 lastPeriodChange) = core.disputes(disputeID); + + assertEq(uint256(period), uint256(KlerosCoreBase.Period.commit), "Wrong period"); + assertEq(lastPeriodChange, block.timestamp, "Wrong lastPeriodChange"); + + vm.prank(staker1); + vm.expectRevert(bytes("Empty commit.")); + disputeKit.castCommit(disputeID, voteIDs, commit); + + commit = keccak256(abi.encodePacked(YES, salt)); + + vm.prank(other); + vm.expectRevert(bytes("The caller has to own the vote.")); + disputeKit.castCommit(disputeID, voteIDs, commit); + + vm.prank(staker1); + vm.expectEmit(true, true, true, true); + emit DisputeKitClassic.CommitCast(disputeID, staker1, voteIDs, commit); + disputeKit.castCommit(disputeID, voteIDs, commit); + + (, , , uint256 totalCommited, uint256 nbVoters, uint256 choiceCount) = disputeKit.getRoundInfo(disputeID, 0, 0); + assertEq(totalCommited, 1, "totalCommited should be 1"); + assertEq(disputeKit.areCommitsAllCast(disputeID), false, "Commits should not all be cast"); + + (, bytes32 commitStored, , ) = disputeKit.getVoteInfo(0, 0, 0); + assertEq(commitStored, keccak256(abi.encodePacked(YES, salt)), "Incorrect commit"); + + voteIDs = new uint256[](2); // Create the leftover votes subset + voteIDs[0] = 1; + voteIDs[1] = 2; + + vm.prank(staker1); + vm.expectEmit(true, true, true, true); + emit DisputeKitClassic.CommitCast(disputeID, staker1, voteIDs, commit); + disputeKit.castCommit(disputeID, voteIDs, commit); + + (, , , totalCommited, nbVoters, choiceCount) = disputeKit.getRoundInfo(disputeID, 0, 0); + assertEq(totalCommited, DEFAULT_NB_OF_JURORS, "totalCommited should be 3"); + assertEq(disputeKit.areCommitsAllCast(disputeID), true, "Commits should all be cast"); + + for (uint256 i = 1; i < DEFAULT_NB_OF_JURORS; i++) { + (, commitStored, , ) = disputeKit.getVoteInfo(0, 0, i); + assertEq(commitStored, keccak256(abi.encodePacked(YES, salt)), "Incorrect commit"); + } + + // Check reveal in the next period + core.passPeriod(disputeID); + + // Check the require with the wrong choice and then with the wrong salt + vm.prank(staker1); + vm.expectRevert(bytes("The commit must match the choice in courts with hidden votes.")); + disputeKit.castVote(disputeID, voteIDs, 2, salt, "XYZ"); + + vm.prank(staker1); + vm.expectRevert(bytes("The commit must match the choice in courts with hidden votes.")); + disputeKit.castVote(disputeID, voteIDs, YES, salt - 1, "XYZ"); + + vm.prank(staker1); + disputeKit.castVote(disputeID, voteIDs, YES, salt, "XYZ"); + + for (uint256 i = 1; i < DEFAULT_NB_OF_JURORS; i++) { + // 0 voteID was skipped when casting a vote + (address account, , uint256 choice, bool voted) = disputeKit.getVoteInfo(0, 0, i); + assertEq(account, staker1, "Wrong drawn account"); + assertEq(choice, YES, "Wrong choice"); + assertEq(voted, true, "Voted should be true"); + } + } + + function test_castCommit_timeoutCheck() public { + // Change hidden votes in general court + uint256 disputeID = 0; + vm.prank(governor); + core.changeCourtParameters( + GENERAL_COURT, + true, // Hidden votes + 1000, // min stake + 10000, // alpha + 0.03 ether, // fee for juror + 511, // jurors for jump + [uint256(60), uint256(120), uint256(180), uint256(240)] // Times per period + ); + + vm.prank(staker1); + core.setStake(GENERAL_COURT, 10000); + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.roll(block.number + rngLookahead + 1); + sortitionModule.passPhase(); // Drawing phase + core.draw(disputeID, DEFAULT_NB_OF_JURORS); + + vm.warp(block.timestamp + timesPerPeriod[0]); + core.passPeriod(disputeID); // Commit + + vm.expectRevert(KlerosCoreBase.CommitPeriodNotPassed.selector); + core.passPeriod(disputeID); + + vm.warp(block.timestamp + timesPerPeriod[1]); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.vote); + core.passPeriod(disputeID); + } + + function test_castVote() public { + uint256 disputeID = 0; + + vm.prank(staker1); + core.setStake(GENERAL_COURT, 10000); + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.roll(block.number + rngLookahead + 1); + sortitionModule.passPhase(); // Drawing phase + + core.draw(disputeID, DEFAULT_NB_OF_JURORS - 1); // Draw less to check the require later + vm.warp(block.timestamp + timesPerPeriod[0]); + + uint256[] memory voteIDs = new uint256[](0); + vm.prank(staker1); + vm.expectRevert(bytes("The dispute should be in Vote period.")); + disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); // Leave salt empty as not needed + + vm.expectRevert(KlerosCoreBase.DisputeStillDrawing.selector); + core.passPeriod(disputeID); + + core.draw(disputeID, 1); // Draw the last juror + + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.vote); + core.passPeriod(disputeID); // Vote + + (, , KlerosCoreBase.Period period, , uint256 lastPeriodChange) = core.disputes(disputeID); + + assertEq(uint256(period), uint256(KlerosCoreBase.Period.vote), "Wrong period"); + assertEq(lastPeriodChange, block.timestamp, "Wrong lastPeriodChange"); + + vm.prank(staker1); + vm.expectRevert(bytes("No voteID provided")); + disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); + + voteIDs = new uint256[](1); + voteIDs[0] = 0; // Split vote IDs to see how the winner changes + vm.prank(staker1); + vm.expectRevert(bytes("Choice out of bounds")); + disputeKit.castVote(disputeID, voteIDs, 2 + 1, 0, "XYZ"); + + vm.prank(other); + vm.expectRevert(bytes("The caller has to own the vote.")); + disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); + + vm.prank(staker1); + vm.expectEmit(true, true, true, true); + emit IDisputeKit.VoteCast(disputeID, staker1, voteIDs, 2, "XYZ"); + disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); + + vm.prank(staker1); + vm.expectRevert(bytes("Vote already cast.")); + disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); + + ( + uint256 winningChoice, + bool tied, + uint256 totalVoted, + uint256 totalCommited, + , + uint256 choiceCount + ) = disputeKit.getRoundInfo(disputeID, 0, 2); + assertEq(winningChoice, 2, "Wrong winning choice"); + assertEq(tied, false, "tied should be false"); + assertEq(totalVoted, 1, "totalVoted should be 1"); + assertEq(totalCommited, 0, "totalCommited should be 0"); + assertEq(choiceCount, 1, "choiceCount should be 1"); + + (address account, bytes32 commit, uint256 choice, bool voted) = disputeKit.getVoteInfo(0, 0, 0); // Dispute - Round - VoteID + assertEq(account, staker1, "Wrong drawn account"); + assertEq(commit, bytes32(0), "Commit should be empty"); + assertEq(choice, 2, "Choice should be empty"); + assertEq(voted, true, "Voted should be true"); + + assertEq(disputeKit.isVoteActive(0, 0, 0), true, "Vote should be active"); // Dispute - Round - VoteID + + voteIDs = new uint256[](1); + voteIDs[0] = 1; // Cast another vote to check the tie. + + vm.prank(staker1); + vm.expectEmit(true, true, true, true); + emit IDisputeKit.VoteCast(disputeID, staker1, voteIDs, 1, "XYZZ"); + disputeKit.castVote(disputeID, voteIDs, 1, 0, "XYZZ"); + + (, tied, totalVoted, , , choiceCount) = disputeKit.getRoundInfo(disputeID, 0, 1); + assertEq(tied, true, "tied should be true"); + assertEq(totalVoted, 2, "totalVoted should be 2"); + assertEq(choiceCount, 1, "choiceCount should be 1 for first choice"); + + voteIDs = new uint256[](1); + voteIDs[0] = 2; // Cast another vote to declare a new winner. + + vm.prank(staker1); + vm.expectEmit(true, true, true, true); + emit IDisputeKit.VoteCast(disputeID, staker1, voteIDs, 1, "XYZZ"); + disputeKit.castVote(disputeID, voteIDs, 1, 0, "XYZZ"); + + (winningChoice, tied, totalVoted, , , choiceCount) = disputeKit.getRoundInfo(disputeID, 0, 1); + assertEq(winningChoice, 1, "Wrong winning choice"); + assertEq(tied, false, "tied should be false"); + assertEq(totalVoted, 3, "totalVoted should be 3"); + assertEq(choiceCount, 2, "choiceCount should be 2 for first choice"); + assertEq(disputeKit.areVotesAllCast(disputeID), true, "Votes should all be cast"); + } + + function test_castVote_timeoutCheck() public { + // Change hidden votes in general court + uint256 disputeID = 0; + vm.prank(staker1); + core.setStake(GENERAL_COURT, 10000); + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.roll(block.number + rngLookahead + 1); + sortitionModule.passPhase(); // Drawing phase + core.draw(disputeID, DEFAULT_NB_OF_JURORS); + + vm.warp(block.timestamp + timesPerPeriod[0]); + core.passPeriod(disputeID); // Votes + + vm.expectRevert(KlerosCoreBase.VotePeriodNotPassed.selector); + core.passPeriod(disputeID); + + vm.warp(block.timestamp + timesPerPeriod[2]); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.AppealPossible(disputeID, arbitrable); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.appeal); + core.passPeriod(disputeID); + } + + function test_castVote_rulingCheck() public { + // Change hidden votes in general court + uint256 disputeID = 0; + vm.prank(staker1); + core.setStake(GENERAL_COURT, 10000); + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.roll(block.number + rngLookahead + 1); + sortitionModule.passPhase(); // Drawing phase + core.draw(disputeID, DEFAULT_NB_OF_JURORS); + + vm.warp(block.timestamp + timesPerPeriod[0]); + core.passPeriod(disputeID); // Votes + + uint256[] memory voteIDs = new uint256[](3); + voteIDs[0] = 0; + voteIDs[1] = 1; + voteIDs[2] = 2; + + vm.prank(staker1); + disputeKit.castVote(disputeID, voteIDs, 1, 0, "XYZZ"); + + (uint256 ruling, bool tied, bool overridden) = disputeKit.currentRuling(disputeID); + assertEq(ruling, 1, "Wrong ruling"); + assertEq(tied, false, "Not tied"); + assertEq(overridden, false, "Not overridden"); + } + + function test_appeal_fundOneSide() public { + uint256 disputeID = 0; + vm.deal(address(disputeKit), 1 ether); + vm.deal(staker1, 1 ether); + + vm.prank(staker1); + core.setStake(GENERAL_COURT, 10000); + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.roll(block.number + rngLookahead + 1); + sortitionModule.passPhase(); // Drawing phase + + core.draw(disputeID, DEFAULT_NB_OF_JURORS); + vm.warp(block.timestamp + timesPerPeriod[0]); + core.passPeriod(disputeID); // Vote + + uint256[] memory voteIDs = new uint256[](3); + voteIDs[0] = 0; + voteIDs[1] = 1; + voteIDs[2] = 2; + + vm.prank(staker1); + disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); + + (uint256 start, uint256 end) = core.appealPeriod(0); + assertEq(start, 0, "Appeal period start should be 0"); + assertEq(end, 0, "Appeal period end should be 0"); + + // Simulate the call from dispute kit to check the requires unrelated to caller + vm.prank(address(disputeKit)); + vm.expectRevert(KlerosCoreBase.DisputeNotAppealable.selector); + core.appeal{value: 0.21 ether}(disputeID, 2, arbitratorExtraData); + + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.AppealPossible(disputeID, arbitrable); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.appeal); + core.passPeriod(disputeID); + + (, , KlerosCoreBase.Period period, , uint256 lastPeriodChange) = core.disputes(disputeID); + (start, end) = core.appealPeriod(0); + assertEq(uint256(period), uint256(KlerosCoreBase.Period.appeal), "Wrong period"); + assertEq(lastPeriodChange, block.timestamp, "Wrong lastPeriodChange"); + assertEq(core.appealCost(0), 0.21 ether, "Wrong appealCost"); + assertEq(start, lastPeriodChange, "Appeal period start is incorrect"); + assertEq(end, lastPeriodChange + timesPerPeriod[3], "Appeal period end is incorrect"); + + vm.expectRevert(KlerosCoreBase.AppealPeriodNotPassed.selector); + core.passPeriod(disputeID); + + // Simulate the call from dispute kit to check the requires unrelated to caller + vm.prank(address(disputeKit)); + vm.expectRevert(KlerosCoreBase.AppealFeesNotEnough.selector); + core.appeal{value: 0.21 ether - 1}(disputeID, 2, arbitratorExtraData); + vm.deal(address(disputeKit), 0); // Nullify the balance so it doesn't get in the way. + + vm.prank(staker1); + vm.expectRevert(KlerosCoreBase.DisputeKitOnly.selector); + core.appeal{value: 0.21 ether}(disputeID, 2, arbitratorExtraData); + + vm.prank(crowdfunder1); + vm.expectRevert(bytes("There is no such ruling to fund.")); + disputeKit.fundAppeal(disputeID, 3); + + vm.prank(crowdfunder1); + vm.expectEmit(true, true, true, true); + emit DisputeKitClassic.Contribution(disputeID, 0, 1, crowdfunder1, 0.21 ether); + disputeKit.fundAppeal{value: 0.21 ether}(disputeID, 1); // Fund the losing choice. Total cost will be 0.63 (0.21 + 0.21 * (20000/10000)) + + assertEq(crowdfunder1.balance, 9.79 ether, "Wrong balance of the crowdfunder"); + assertEq(address(disputeKit).balance, 0.21 ether, "Wrong balance of the DK"); + assertEq((disputeKit.getFundedChoices(disputeID)).length, 0, "No funded choices"); + + vm.prank(crowdfunder1); + vm.expectEmit(true, true, true, true); + emit DisputeKitClassic.Contribution(disputeID, 0, 1, crowdfunder1, 0.42 ether); + vm.expectEmit(true, true, true, true); + emit DisputeKitClassic.ChoiceFunded(disputeID, 0, 1); + disputeKit.fundAppeal{value: 5 ether}(disputeID, 1); // Deliberately overpay to check reimburse + + assertEq(crowdfunder1.balance, 9.37 ether, "Wrong balance of the crowdfunder"); + assertEq(address(disputeKit).balance, 0.63 ether, "Wrong balance of the DK"); + assertEq((disputeKit.getFundedChoices(disputeID)).length, 1, "One choice should be funded"); + assertEq((disputeKit.getFundedChoices(disputeID))[0], 1, "Incorrect funded choice"); + + vm.prank(crowdfunder1); + vm.expectRevert(bytes("Appeal fee is already paid.")); + disputeKit.fundAppeal(disputeID, 1); + } + + function test_appeal_timeoutCheck() public { + uint256 disputeID = 0; + + vm.prank(staker1); + core.setStake(GENERAL_COURT, 10000); + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.roll(block.number + rngLookahead + 1); + sortitionModule.passPhase(); // Drawing phase + + core.draw(disputeID, DEFAULT_NB_OF_JURORS); + vm.warp(block.timestamp + timesPerPeriod[0]); + core.passPeriod(disputeID); // Vote + + uint256[] memory voteIDs = new uint256[](3); + voteIDs[0] = 0; + voteIDs[1] = 1; + voteIDs[2] = 2; + + vm.prank(staker1); + disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); + + vm.prank(crowdfunder1); + vm.expectRevert(bytes("Appeal period is over.")); // Appeal period not started yet + disputeKit.fundAppeal{value: 0.1 ether}(disputeID, 1); + core.passPeriod(disputeID); + + (uint256 start, uint256 end) = core.appealPeriod(0); + + vm.prank(crowdfunder1); + vm.warp(block.timestamp + ((end - start) / 2 + 1)); + vm.expectRevert(bytes("Appeal period is over for loser")); + disputeKit.fundAppeal{value: 0.1 ether}(disputeID, 1); // Losing choice + + disputeKit.fundAppeal(disputeID, 2); // Winning choice funding should not revert yet + + vm.prank(crowdfunder1); + vm.warp(block.timestamp + (end - start) / 2); // Warp one more to cover the whole period + vm.expectRevert(bytes("Appeal period is over.")); + disputeKit.fundAppeal{value: 0.1 ether}(disputeID, 2); + } + + function test_appeal_fullFundingNoSwitch() public { + uint256 disputeID = 0; + + vm.prank(staker1); + core.setStake(GENERAL_COURT, 20000); + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.roll(block.number + rngLookahead + 1); + sortitionModule.passPhase(); // Drawing phase + + core.draw(disputeID, DEFAULT_NB_OF_JURORS); + vm.warp(block.timestamp + timesPerPeriod[0]); + core.passPeriod(disputeID); // Vote + + uint256[] memory voteIDs = new uint256[](3); + voteIDs[0] = 0; + voteIDs[1] = 1; + voteIDs[2] = 2; + + vm.prank(staker1); + disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); + + core.passPeriod(disputeID); // Appeal + + vm.prank(crowdfunder1); + disputeKit.fundAppeal{value: 0.63 ether}(disputeID, 1); + + vm.prank(crowdfunder2); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.AppealDecision(disputeID, arbitrable); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.evidence); + disputeKit.fundAppeal{value: 0.42 ether}(disputeID, 2); + + assertEq((disputeKit.getFundedChoices(disputeID)).length, 0, "No funded choices in the fresh round"); + (uint256 ruling, bool tied, bool overridden) = disputeKit.currentRuling(disputeID); + assertEq(ruling, 0, "Should be 0 ruling in the fresh round"); + assertEq(tied, true, "Should be tied"); + assertEq(overridden, false, "Not overridden"); + + assertEq(address(disputeKit).balance, 0.84 ether, "Wrong balance of the DK"); // 0.63 + 0.42 - 0.21 + assertEq(address(core).balance, 0.3 ether, "Wrong balance of the core"); // 0.09 arbFee + 0.21 appealFee + + assertEq(sortitionModule.disputesWithoutJurors(), 1, "Wrong disputesWithoutJurors count after appeal"); + assertEq(core.getNumberOfRounds(disputeID), 2, "Wrong number of rounds"); + + (, , KlerosCoreBase.Period period, , uint256 lastPeriodChange) = core.disputes(disputeID); + assertEq(uint256(period), uint256(KlerosCoreBase.Period.evidence), "Wrong period"); + assertEq(lastPeriodChange, block.timestamp, "Wrong lastPeriodChange"); + + KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 1); // Check the new round + assertEq(round.pnkAtStakePerJuror, 1000, "Wrong pnkAtStakePerJuror"); + assertEq(round.totalFeesForJurors, 0.21 ether, "Wrong totalFeesForJurors"); + assertEq(round.nbVotes, 7, "Wrong nbVotes"); + + core.draw(disputeID, 7); + emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.vote); // Check that we don't have to wait for the timeout to pass the evidence period after appeal + core.passPeriod(disputeID); + } + + function test_appeal_fullFundingDKCourtSwitch() public { + uint256 disputeID = 0; + DisputeKitClassic dkLogic = new DisputeKitClassic(); + // Create a new DK and court to check the switch + bytes memory initDataDk = abi.encodeWithSignature("initialize(address,address)", governor, address(core)); + + UUPSProxy proxyDk = new UUPSProxy(address(dkLogic), initDataDk); + DisputeKitClassic newDisputeKit = DisputeKitClassic(address(proxyDk)); + + uint96 newCourtID = 2; + uint256 newDkID = 2; + uint256[] memory supportedDK = new uint256[](1); + supportedDK[0] = DISPUTE_KIT_CLASSIC; + bytes memory newExtraData = abi.encodePacked(uint256(newCourtID), DEFAULT_NB_OF_JURORS, newDkID); + + vm.prank(governor); + core.addNewDisputeKit(newDisputeKit); + vm.prank(governor); + core.createCourt( + GENERAL_COURT, + hiddenVotes, + minStake, + alpha, + feeForJuror, + 3, // jurors for jump. Low number to ensure jump after the first appeal + [uint256(60), uint256(120), uint256(180), uint256(240)], // Times per period + sortitionExtraData, + supportedDK + ); + + arbitrable.changeArbitratorExtraData(newExtraData); + + vm.prank(governor); + supportedDK = new uint256[](1); + supportedDK[0] = newDkID; + core.enableDisputeKits(newCourtID, supportedDK, true); + + vm.prank(staker1); + core.setStake(newCourtID, 20000); + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.roll(block.number + rngLookahead + 1); + sortitionModule.passPhase(); // Drawing phase + + KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 0); + assertEq(round.disputeKitID, newDkID, "Wrong DK ID"); + + core.draw(disputeID, DEFAULT_NB_OF_JURORS); + vm.warp(block.timestamp + timesPerPeriod[0]); + core.passPeriod(disputeID); // Vote + + uint256[] memory voteIDs = new uint256[](3); + voteIDs[0] = 0; + voteIDs[1] = 1; + voteIDs[2] = 2; + + vm.prank(staker1); + newDisputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); + + core.passPeriod(disputeID); // Appeal + + vm.prank(crowdfunder1); + newDisputeKit.fundAppeal{value: 0.63 ether}(disputeID, 1); + vm.prank(crowdfunder2); + + assertEq(core.isDisputeKitJumping(disputeID), true, "Should be jumping"); + + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.CourtJump(disputeID, 1, newCourtID, GENERAL_COURT); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.DisputeKitJump(disputeID, 1, newDkID, DISPUTE_KIT_CLASSIC); + vm.expectEmit(true, true, true, true); + emit DisputeKitClassic.DisputeCreation(disputeID, 2, newExtraData); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.AppealDecision(disputeID, arbitrable); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.evidence); + newDisputeKit.fundAppeal{value: 0.42 ether}(disputeID, 2); + + (, bool jumped, ) = newDisputeKit.disputes(disputeID); + assertEq(jumped, true, "jumped should be true"); + assertEq( + (newDisputeKit.getFundedChoices(disputeID)).length, + 2, + "No fresh round created so the number of funded choices should be 2" + ); + + round = core.getRoundInfo(disputeID, 1); + assertEq(round.disputeKitID, DISPUTE_KIT_CLASSIC, "Wrong DK ID"); + assertEq(sortitionModule.disputesWithoutJurors(), 1, "Wrong disputesWithoutJurors count"); + (uint96 courtID, , , , ) = core.disputes(disputeID); + assertEq(courtID, GENERAL_COURT, "Wrong court ID"); + + (, jumped, ) = disputeKit.disputes(disputeID); + assertEq(jumped, false, "jumped should be false in the DK that dispute jumped to"); + + // Check jump modifier + vm.prank(address(core)); + vm.expectRevert(bytes("Dispute jumped to a parent DK!")); + newDisputeKit.draw(disputeID, 1); + + // And check that draw in the new round works + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.Draw(staker1, disputeID, 1, 0); // roundID = 1 VoteID = 0 + core.draw(disputeID, 1); + + (address account, , , ) = disputeKit.getVoteInfo(disputeID, 1, 0); + assertEq(account, staker1, "Wrong drawn account in the classic DK"); + } + + function test_execute() public { + uint256 disputeID = 0; + + vm.prank(staker1); + core.setStake(GENERAL_COURT, 1500); + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.roll(block.number + rngLookahead + 1); + sortitionModule.passPhase(); // Drawing phase + + // Split the stakers' votes. The first staker will get VoteID 0 and the second will take the rest. + core.draw(disputeID, 1); + + vm.warp(block.timestamp + maxDrawingTime); + sortitionModule.passPhase(); // Staking phase to stake the 2nd voter + vm.prank(staker2); + core.setStake(GENERAL_COURT, 20000); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.roll(block.number + rngLookahead + 1); + sortitionModule.passPhase(); // Drawing phase + + core.draw(disputeID, 2); // Assign leftover votes to staker2 + + vm.warp(block.timestamp + timesPerPeriod[0]); + core.passPeriod(disputeID); // Vote + + uint256[] memory voteIDs = new uint256[](1); + voteIDs[0] = 0; + vm.prank(staker1); + disputeKit.castVote(disputeID, voteIDs, 1, 0, "XYZ"); // Staker1 only got 1 vote because of low stake + + voteIDs = new uint256[](2); + voteIDs[0] = 1; + voteIDs[1] = 2; + vm.prank(staker2); + disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); + core.passPeriod(disputeID); // Appeal + + vm.expectRevert(KlerosCoreBase.NotExecutionPeriod.selector); + core.execute(disputeID, 0, 1); + + vm.warp(block.timestamp + timesPerPeriod[3]); + core.passPeriod(disputeID); // Execution + + vm.prank(governor); + core.pause(); + vm.expectRevert(KlerosCoreBase.WhenNotPausedOnly.selector); + core.execute(disputeID, 0, 1); + vm.prank(governor); + core.unpause(); + + assertEq(disputeKit.getCoherentCount(disputeID, 0), 2, "Wrong coherent count"); + // dispute, round, voteID, feeForJuror (not used in classic DK), pnkPerJuror (not used in classic DK) + assertEq(disputeKit.getDegreeOfCoherence(disputeID, 0, 0, 0, 0), 0, "Wrong degree of coherence 0 vote ID"); + assertEq(disputeKit.getDegreeOfCoherence(disputeID, 0, 1, 0, 0), 10000, "Wrong degree of coherence 1 vote ID"); + assertEq(disputeKit.getDegreeOfCoherence(disputeID, 0, 2, 0, 0), 10000, "Wrong degree of coherence 2 vote ID"); + + vm.expectEmit(true, true, true, true); + emit SortitionModuleBase.StakeLocked(staker1, 1000, true); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.TokenAndETHShift(staker1, disputeID, 0, 0, -int256(1000), 0, IERC20(address(0))); + // Check iterations for the winning staker to see the shifts + vm.expectEmit(true, true, true, true); + emit SortitionModuleBase.StakeLocked(staker2, 0, true); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.TokenAndETHShift(staker2, disputeID, 0, 10000, 0, 0, IERC20(address(0))); + vm.expectEmit(true, true, true, true); + emit SortitionModuleBase.StakeLocked(staker2, 0, true); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.TokenAndETHShift(staker2, disputeID, 0, 10000, 0, 0, IERC20(address(0))); + core.execute(disputeID, 0, 3); // Do 3 iterations to check penalties first + + (uint256 totalStaked, uint256 totalLocked, , ) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); + assertEq(totalStaked, 500, "totalStaked should be penalized"); // 1500 - 1000 + assertEq(totalLocked, 0, "Tokens should be released for staker1"); + (, totalLocked, , ) = sortitionModule.getJurorBalance(staker2, GENERAL_COURT); + assertEq(totalLocked, 2000, "Tokens should still be locked for staker2"); + + KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 0); + assertEq(round.repartitions, 3, "Wrong repartitions"); + assertEq(round.pnkPenalties, 1000, "Wrong pnkPenalties"); + + vm.expectEmit(true, true, true, true); + emit SortitionModuleBase.StakeLocked(staker1, 0, true); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.TokenAndETHShift(staker1, disputeID, 0, 0, 0, 0, IERC20(address(0))); + // Check iterations for the winning staker to see the shifts + vm.expectEmit(true, true, true, true); + emit SortitionModuleBase.StakeLocked(staker2, 1000, true); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.TokenAndETHShift(staker2, disputeID, 0, 10000, 500, 0.045 ether, IERC20(address(0))); + vm.expectEmit(true, true, true, true); + emit SortitionModuleBase.StakeLocked(staker2, 1000, true); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.TokenAndETHShift(staker2, disputeID, 0, 10000, 500, 0.045 ether, IERC20(address(0))); + core.execute(disputeID, 0, 10); // Finish the iterations. We need only 3 but check that it corrects the count. + + (, totalLocked, , ) = sortitionModule.getJurorBalance(staker2, GENERAL_COURT); + assertEq(totalLocked, 0, "Tokens should be unlocked for staker2"); + + round = core.getRoundInfo(disputeID, 0); + assertEq(round.repartitions, 6, "Wrong repartitions"); + assertEq(round.pnkPenalties, 1000, "Wrong pnkPenalties"); + assertEq(round.sumFeeRewardPaid, 0.09 ether, "Wrong sumFeeRewardPaid"); + assertEq(round.sumPnkRewardPaid, 1000, "Wrong sumPnkRewardPaid"); + + assertEq(address(core).balance, 0, "Wrong balance of the core"); + assertEq(staker1.balance, 0, "Wrong balance of the staker1"); + assertEq(staker2.balance, 0.09 ether, "Wrong balance of the staker2"); + + assertEq(pinakion.balanceOf(address(core)), 20500, "Wrong token balance of the core"); // Was 21500. 1000 was transferred to staker2 + assertEq(pinakion.balanceOf(staker1), 999999999999998500, "Wrong token balance of staker1"); + assertEq(pinakion.balanceOf(staker2), 999999999999981000, "Wrong token balance of staker2"); // 20k stake and 1k added as a reward, thus -19k from the default + } + + function test_execute_NoCoherence() public { + uint256 disputeID = 0; + + vm.prank(staker1); + core.setStake(GENERAL_COURT, 20000); + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.roll(block.number + rngLookahead + 1); + sortitionModule.passPhase(); // Drawing phase + + core.draw(disputeID, DEFAULT_NB_OF_JURORS); + + vm.warp(block.timestamp + timesPerPeriod[0]); + core.passPeriod(disputeID); // Vote + + vm.warp(block.timestamp + timesPerPeriod[2]); // Don't vote at all so no one is coherent + core.passPeriod(disputeID); // Appeal + + vm.warp(block.timestamp + timesPerPeriod[3]); + core.passPeriod(disputeID); // Execution + + assertEq(disputeKit.getCoherentCount(disputeID, 0), 0, "Wrong coherent count"); + // dispute, round, voteID, feeForJuror (not used in classic DK), pnkPerJuror (not used in classic DK) + assertEq(disputeKit.getDegreeOfCoherence(disputeID, 0, 0, 0, 0), 0, "Wrong degree of coherence 0 vote ID"); + assertEq(disputeKit.getDegreeOfCoherence(disputeID, 0, 1, 0, 0), 0, "Wrong degree of coherence 1 vote ID"); + assertEq(disputeKit.getDegreeOfCoherence(disputeID, 0, 2, 0, 0), 0, "Wrong degree of coherence 2 vote ID"); + + uint256 governorBalance = governor.balance; + uint256 governorTokenBalance = pinakion.balanceOf(governor); + + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.LeftoverRewardSent(disputeID, 0, 3000, 0.09 ether, IERC20(address(0))); + core.execute(disputeID, 0, 3); + + KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 0); + assertEq(round.pnkPenalties, 3000, "Wrong pnkPenalties"); + assertEq(round.sumFeeRewardPaid, 0, "Wrong sumFeeRewardPaid"); + assertEq(round.sumPnkRewardPaid, 0, "Wrong sumPnkRewardPaid"); + + assertEq(address(core).balance, 0, "Wrong balance of the core"); + assertEq(staker1.balance, 0, "Wrong balance of the staker1"); + assertEq(governor.balance, governorBalance + 0.09 ether, "Wrong balance of the governor"); + + assertEq(pinakion.balanceOf(address(core)), 17000, "Wrong token balance of the core"); + assertEq(pinakion.balanceOf(staker1), 999999999999980000, "Wrong token balance of staker1"); + assertEq(pinakion.balanceOf(governor), governorTokenBalance + 3000, "Wrong token balance of governor"); + } + + function test_execute_UnstakeInactive() public { + // Create a 2nd court so unstaking is done in multiple courts. + vm.prank(governor); + uint256[] memory supportedDK = new uint256[](1); + supportedDK[0] = DISPUTE_KIT_CLASSIC; + core.createCourt( + GENERAL_COURT, + true, // Hidden votes + 1000, // min stake + 10000, // alpha + 0.03 ether, // fee for juror + 50, // jurors for jump + [uint256(10), uint256(20), uint256(30), uint256(40)], // Times per period + sortitionExtraData, // Sortition extra data + supportedDK + ); + + uint256 disputeID = 0; + uint96 newCourtID = 2; + + vm.prank(staker1); + core.setStake(GENERAL_COURT, 20000); + vm.prank(staker1); + core.setStake(newCourtID, 20000); + (, , , uint256 nbCourts) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); + assertEq(nbCourts, 2, "Wrong number of courts"); + + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.roll(block.number + rngLookahead + 1); + sortitionModule.passPhase(); // Drawing phase + + core.draw(disputeID, DEFAULT_NB_OF_JURORS); + + sortitionModule.passPhase(); // Staking phase. Change to staking so we don't have to deal with delayed stakes. + + vm.warp(block.timestamp + timesPerPeriod[0]); + core.passPeriod(disputeID); // Vote + + vm.warp(block.timestamp + timesPerPeriod[2]); // Don't vote at all so no one is coherent + core.passPeriod(disputeID); // Appeal + + vm.warp(block.timestamp + timesPerPeriod[3]); + core.passPeriod(disputeID); // Execution + + uint256 governorTokenBalance = pinakion.balanceOf(governor); + + vm.expectEmit(true, true, true, true); + emit SortitionModuleBase.StakeSet(staker1, newCourtID, 0); + vm.expectEmit(true, true, true, true); + emit SortitionModuleBase.StakeSet(staker1, GENERAL_COURT, 0); + core.execute(disputeID, 0, 3); + + assertEq(pinakion.balanceOf(address(core)), 0, "Wrong token balance of the core"); + assertEq(pinakion.balanceOf(staker1), 999999999999997000, "Wrong token balance of staker1"); + assertEq(pinakion.balanceOf(governor), governorTokenBalance + 3000, "Wrong token balance of governor"); + + (, , , nbCourts) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); + assertEq(nbCourts, 0, "Should unstake from all courts"); + } + + function test_execute_RewardUnstaked() public { + // Reward the juror who fully unstaked earlier. Return the locked tokens + uint256 disputeID = 0; + + vm.prank(staker1); + core.setStake(GENERAL_COURT, 20000); + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.roll(block.number + rngLookahead + 1); + sortitionModule.passPhase(); // Drawing phase + + core.draw(disputeID, DEFAULT_NB_OF_JURORS); + + sortitionModule.passPhase(); // Staking. Pass the phase so the juror can unstake before execution + + vm.warp(block.timestamp + timesPerPeriod[0]); + core.passPeriod(disputeID); // Vote + + uint256[] memory voteIDs = new uint256[](3); + voteIDs[0] = 0; + voteIDs[1] = 1; + voteIDs[2] = 2; + vm.prank(staker1); + disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); + + core.passPeriod(disputeID); // Appeal + + vm.warp(block.timestamp + timesPerPeriod[3]); + core.passPeriod(disputeID); // Execution + + vm.prank(staker1); + core.setStake(GENERAL_COURT, 0); + + (uint256 totalStaked, uint256 totalLocked, uint256 stakedInCourt, uint256 nbCourts) = sortitionModule + .getJurorBalance(staker1, GENERAL_COURT); + assertEq(totalStaked, 0, "Should be unstaked"); + assertEq(totalLocked, 3000, "Wrong amount locked"); + assertEq(stakedInCourt, 0, "Should be unstaked"); + assertEq(nbCourts, 0, "Should be 0 courts"); + + assertEq(pinakion.balanceOf(address(core)), 3000, "Wrong token balance of the core"); + assertEq(pinakion.balanceOf(staker1), 999999999999997000, "Wrong token balance of staker1"); + + core.execute(disputeID, 0, 6); + + KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 0); + assertEq(round.pnkPenalties, 0, "Wrong pnkPenalties"); + assertEq(round.sumFeeRewardPaid, 0.09 ether, "Wrong sumFeeRewardPaid"); + assertEq(round.sumPnkRewardPaid, 0, "Wrong sumPnkRewardPaid"); // No penalty so no rewards in pnk + + assertEq(pinakion.balanceOf(address(core)), 0, "Wrong token balance of the core"); + assertEq(pinakion.balanceOf(staker1), 1 ether, "Wrong token balance of staker1"); + } + + function test_execute_feeToken() public { + uint256 disputeID = 0; + + feeToken.transfer(disputer, 1 ether); + vm.prank(disputer); + feeToken.approve(address(arbitrable), 1 ether); + + vm.prank(governor); + core.changeAcceptedFeeTokens(feeToken, true); + vm.prank(governor); + core.changeCurrencyRates(feeToken, 500, 3); + + vm.prank(disputer); + arbitrable.createDispute("Action", 0.18 ether); + + vm.prank(staker1); + core.setStake(GENERAL_COURT, 20000); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.roll(block.number + rngLookahead + 1); + sortitionModule.passPhase(); // Drawing phase + + core.draw(disputeID, DEFAULT_NB_OF_JURORS); + + vm.warp(block.timestamp + timesPerPeriod[0]); + core.passPeriod(disputeID); // Vote + + uint256[] memory voteIDs = new uint256[](3); + voteIDs[0] = 0; + voteIDs[1] = 1; + voteIDs[2] = 2; + vm.prank(staker1); + disputeKit.castVote(disputeID, voteIDs, 1, 0, "XYZ"); // Staker1 only got 1 vote because of low stake + + core.passPeriod(disputeID); // Appeal + + vm.warp(block.timestamp + timesPerPeriod[3]); + core.passPeriod(disputeID); // Execution + + // Check only once per penalty and per reward + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.TokenAndETHShift(staker1, disputeID, 0, 10000, 0, 0, feeToken); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.TokenAndETHShift(staker1, disputeID, 0, 10000, 0, 0.06 ether, feeToken); + core.execute(disputeID, 0, 6); + + KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 0); + assertEq(round.sumFeeRewardPaid, 0.18 ether, "Wrong sumFeeRewardPaid"); + + assertEq(feeToken.balanceOf(address(core)), 0, "Wrong fee token balance of the core"); + assertEq(feeToken.balanceOf(staker1), 0.18 ether, "Wrong fee token balance of staker1"); + assertEq(feeToken.balanceOf(disputer), 0.82 ether, "Wrong fee token balance of disputer"); + } + + function test_execute_NoCoherence_feeToken() public { + uint256 disputeID = 0; + + feeToken.transfer(disputer, 1 ether); + vm.prank(disputer); + feeToken.approve(address(arbitrable), 1 ether); + + vm.prank(governor); + core.changeAcceptedFeeTokens(feeToken, true); + vm.prank(governor); + core.changeCurrencyRates(feeToken, 500, 3); + + vm.prank(disputer); + arbitrable.createDispute("Action", 0.18 ether); + + vm.prank(staker1); + core.setStake(GENERAL_COURT, 20000); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.roll(block.number + rngLookahead + 1); + sortitionModule.passPhase(); // Drawing phase + + core.draw(disputeID, DEFAULT_NB_OF_JURORS); + + vm.warp(block.timestamp + timesPerPeriod[0]); + core.passPeriod(disputeID); // Vote + + vm.warp(block.timestamp + timesPerPeriod[2]); // Don't vote at all so no one is coherent + core.passPeriod(disputeID); // Appeal + + vm.warp(block.timestamp + timesPerPeriod[3]); + core.passPeriod(disputeID); // Execution + + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.LeftoverRewardSent(disputeID, 0, 3000, 0.18 ether, feeToken); + core.execute(disputeID, 0, 10); // Put more iterations to check that they're capped + + KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 0); + assertEq(round.pnkPenalties, 3000, "Wrong pnkPenalties"); + assertEq(round.sumFeeRewardPaid, 0, "Wrong sumFeeRewardPaid"); + assertEq(round.sumPnkRewardPaid, 0, "Wrong sumPnkRewardPaid"); + assertEq(round.repartitions, 3, "Wrong repartitions"); + + assertEq(feeToken.balanceOf(address(core)), 0, "Wrong token balance of the core"); + assertEq(feeToken.balanceOf(staker1), 0, "Wrong token balance of staker1"); + assertEq(feeToken.balanceOf(disputer), 0.82 ether, "Wrong token balance of disputer"); + assertEq(feeToken.balanceOf(governor), 0.18 ether, "Wrong token balance of governor"); + } + + function test_executeRuling() public { + uint256 disputeID = 0; + + vm.prank(staker1); + core.setStake(GENERAL_COURT, 20000); + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.roll(block.number + rngLookahead + 1); + sortitionModule.passPhase(); // Drawing phase + + core.draw(disputeID, DEFAULT_NB_OF_JURORS); + vm.warp(block.timestamp + timesPerPeriod[0]); + core.passPeriod(disputeID); // Vote + + uint256[] memory voteIDs = new uint256[](3); + voteIDs[0] = 0; + voteIDs[1] = 1; + voteIDs[2] = 2; + + vm.prank(staker1); + disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); + core.passPeriod(disputeID); // Appeal + + vm.expectRevert(KlerosCoreBase.NotExecutionPeriod.selector); + core.executeRuling(disputeID); + + vm.warp(block.timestamp + timesPerPeriod[3]); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.execution); + core.passPeriod(disputeID); // Execution + + (, , KlerosCoreBase.Period period, , uint256 lastPeriodChange) = core.disputes(disputeID); + assertEq(uint256(period), uint256(KlerosCoreBase.Period.execution), "Wrong period"); + assertEq(lastPeriodChange, block.timestamp, "Wrong lastPeriodChange"); + + vm.expectRevert(KlerosCoreBase.DisputePeriodIsFinal.selector); + core.passPeriod(disputeID); + + vm.expectEmit(true, true, true, true); + emit IArbitratorV2.Ruling(arbitrable, disputeID, 2); // Winning choice = 2 + vm.expectEmit(true, true, true, true); + emit IArbitrableV2.Ruling(core, disputeID, 2); + core.executeRuling(disputeID); + + vm.expectRevert(KlerosCoreBase.RulingAlreadyExecuted.selector); + core.executeRuling(disputeID); + + (, , , bool ruled, ) = core.disputes(disputeID); + assertEq(ruled, true, "Should be ruled"); + } + + function test_executeRuling_appealSwitch() public { + // Check that the ruling switches if only one side was funded + uint256 disputeID = 0; + + vm.prank(staker1); + core.setStake(GENERAL_COURT, 20000); + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.roll(block.number + rngLookahead + 1); + sortitionModule.passPhase(); // Drawing phase + + core.draw(disputeID, DEFAULT_NB_OF_JURORS); + vm.warp(block.timestamp + timesPerPeriod[0]); + core.passPeriod(disputeID); // Vote + + uint256[] memory voteIDs = new uint256[](3); + voteIDs[0] = 0; + voteIDs[1] = 1; + voteIDs[2] = 2; + + vm.prank(staker1); + disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); + core.passPeriod(disputeID); // Appeal + + vm.prank(crowdfunder1); + disputeKit.fundAppeal{value: 0.63 ether}(disputeID, 1); // Fund the losing choice + + vm.warp(block.timestamp + timesPerPeriod[3]); + core.passPeriod(disputeID); // Execution + + vm.expectEmit(true, true, true, true); + emit IArbitratorV2.Ruling(arbitrable, disputeID, 1); // Winning choice is switched to 1 + vm.expectEmit(true, true, true, true); + emit IArbitrableV2.Ruling(core, disputeID, 1); + core.executeRuling(disputeID); + + (uint256 ruling, bool tied, bool overridden) = disputeKit.currentRuling(disputeID); + assertEq(ruling, 1, "Wrong ruling"); + assertEq(tied, false, "Not tied"); + assertEq(overridden, true, "Should be overridden"); + } + + function test_withdrawFeesAndRewards() public { + uint256 disputeID = 0; + + vm.prank(staker1); + core.setStake(GENERAL_COURT, 20000); + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.roll(block.number + rngLookahead + 1); + sortitionModule.passPhase(); // Drawing phase + + core.draw(disputeID, DEFAULT_NB_OF_JURORS); + vm.warp(block.timestamp + timesPerPeriod[0]); + core.passPeriod(disputeID); // Vote + + uint256[] memory voteIDs = new uint256[](3); + voteIDs[0] = 0; + voteIDs[1] = 1; + voteIDs[2] = 2; + + vm.prank(staker1); + disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); + core.passPeriod(disputeID); // Appeal + + vm.prank(crowdfunder1); + disputeKit.fundAppeal{value: 0.63 ether}(disputeID, 1); // Fund the losing choice. The ruling will be overridden here + vm.prank(crowdfunder2); + disputeKit.fundAppeal{value: 0.41 ether}(disputeID, 2); // Underpay a bit to not create an appeal and withdraw the funded sum fully + + vm.warp(block.timestamp + timesPerPeriod[3]); + core.passPeriod(disputeID); // Execution + + vm.expectRevert(bytes("Dispute should be resolved.")); + disputeKit.withdrawFeesAndRewards(disputeID, payable(staker1), 0, 1); + + core.executeRuling(disputeID); + + vm.prank(governor); + core.pause(); + vm.expectRevert(bytes("Core is paused")); + disputeKit.withdrawFeesAndRewards(disputeID, payable(staker1), 0, 1); + vm.prank(governor); + core.unpause(); + + assertEq(crowdfunder1.balance, 9.37 ether, "Wrong balance of the crowdfunder1"); + assertEq(crowdfunder2.balance, 9.59 ether, "Wrong balance of the crowdfunder2"); + assertEq(address(disputeKit).balance, 1.04 ether, "Wrong balance of the DK"); + + vm.expectEmit(true, true, true, true); + emit DisputeKitClassic.Withdrawal(disputeID, 0, 1, crowdfunder1, 0.63 ether); + disputeKit.withdrawFeesAndRewards(disputeID, payable(crowdfunder1), 0, 1); + + vm.expectEmit(true, true, true, true); + emit DisputeKitClassic.Withdrawal(disputeID, 0, 2, crowdfunder2, 0.41 ether); + disputeKit.withdrawFeesAndRewards(disputeID, payable(crowdfunder2), 0, 2); + + assertEq(crowdfunder1.balance, 10 ether, "Wrong balance of the crowdfunder1"); + assertEq(crowdfunder2.balance, 10 ether, "Wrong balance of the crowdfunder2"); + assertEq(address(disputeKit).balance, 0, "Wrong balance of the DK"); + } +} diff --git a/contracts/test/rng/index.ts b/contracts/test/rng/index.ts index 6fec0e12b..3d4906721 100644 --- a/contracts/test/rng/index.ts +++ b/contracts/test/rng/index.ts @@ -45,6 +45,7 @@ describe("BlockHashRNG", async () => { await tx.wait(); const [rn] = abiCoder.decode(["uint"], ethers.getBytes(`${trace.returnValue}`)); expect(rn).to.not.equal(0); + await tx.wait(); }); it("Should return zero for a block number in the future", async () => { @@ -53,6 +54,7 @@ describe("BlockHashRNG", async () => { await tx.wait(); const [rn] = abiCoder.decode(["uint"], ethers.getBytes(`${trace.returnValue}`)); expect(rn).to.equal(0); + await tx.wait(); }); }); @@ -83,6 +85,7 @@ describe("ChainlinkRNG", async () => { const rn = await rng.receiveRandomness(0); expect(rn).to.equal(expectedRn); + await tx.wait(); }); it("Should return only the last random number when multiple requests are made", async () => { @@ -116,6 +119,7 @@ describe("ChainlinkRNG", async () => { // Should return only the last random number const rn = await rng.receiveRandomness(0); expect(rn).to.equal(expectedRn2); + await tx.wait(); }); }); @@ -145,6 +149,7 @@ describe("RandomizerRNG", async () => { const rn = await rng.receiveRandomness(0); expect(rn).to.equal(expectedRn); + await tx.wait(); }); it("Should return only the last random number when multiple requests are made", async () => { @@ -177,5 +182,6 @@ describe("RandomizerRNG", async () => { // Should return only the last random number const rn = await rng.receiveRandomness(0); expect(rn).to.equal(expectedRn2); + await tx.wait(); }); }); diff --git a/cspell.json b/cspell.json index c671ffe5b..0ab3e541c 100644 --- a/cspell.json +++ b/cspell.json @@ -21,6 +21,7 @@ "commitlint", "consts", "COOLDOWN", + "crowdfunder", "datetime", "devnet", "Devnet", @@ -47,6 +48,8 @@ "uncommify", "Unslashed", "unstake", + "unstaked", + "Unstaking", "Upgradability", "UUPS", "viem",