From 6752cadba741b98f46c802a7b3b945410af63dee Mon Sep 17 00:00:00 2001 From: unknownunknown1 Date: Wed, 24 Sep 2025 04:01:00 +1000 Subject: [PATCH] feat: fuzzing tests --- .../arbitrables/ArbitrableExample.sol | 8 +- .../test/foundry/KlerosCore_Appeals.t.sol | 126 ++++++++++++ .../test/foundry/KlerosCore_Disputes.t.sol | 47 ++++- .../test/foundry/KlerosCore_Drawing.t.sol | 86 +++++++++ .../test/foundry/KlerosCore_Execution.t.sol | 99 ++++++++++ .../test/foundry/KlerosCore_Staking.t.sol | 179 +++++++++++++++++- .../test/foundry/KlerosCore_Voting.t.sol | 57 +++++- 7 files changed, 597 insertions(+), 5 deletions(-) diff --git a/contracts/src/arbitration/arbitrables/ArbitrableExample.sol b/contracts/src/arbitration/arbitrables/ArbitrableExample.sol index c0f052202..1282876e5 100644 --- a/contracts/src/arbitration/arbitrables/ArbitrableExample.sol +++ b/contracts/src/arbitration/arbitrables/ArbitrableExample.sol @@ -40,6 +40,8 @@ contract ArbitrableExample is IArbitrableV2 { mapping(uint256 => uint256) public externalIDtoLocalID; // Maps external (arbitrator side) dispute IDs to local dispute IDs. DisputeStruct[] public disputes; // Stores the disputes' info. disputes[disputeID]. + uint256 public numberOfRulingOptions = 2; + // ************************************* // // * Function Modifiers * // // ************************************* // @@ -100,6 +102,10 @@ contract ArbitrableExample is IArbitrableV2 { templateId = templateRegistry.setDisputeTemplate("", _templateData, _templateDataMappings); } + function changeNumberOfRulingOptions(uint256 _numberOfRulingOptions) external onlyByOwner { + numberOfRulingOptions = _numberOfRulingOptions; + } + // ************************************* // // * State Modifiers * // // ************************************* // @@ -111,7 +117,6 @@ contract ArbitrableExample is IArbitrableV2 { function createDispute(string calldata _action) external payable returns (uint256 disputeID) { emit Action(_action); - uint256 numberOfRulingOptions = 2; uint256 localDisputeID = disputes.length; disputes.push(DisputeStruct({isRuled: false, ruling: 0, numberOfRulingOptions: numberOfRulingOptions})); @@ -130,7 +135,6 @@ contract ArbitrableExample is IArbitrableV2 { function createDispute(string calldata _action, uint256 _feeInWeth) external returns (uint256 disputeID) { emit Action(_action); - uint256 numberOfRulingOptions = 2; uint256 localDisputeID = disputes.length; disputes.push(DisputeStruct({isRuled: false, ruling: 0, numberOfRulingOptions: numberOfRulingOptions})); diff --git a/contracts/test/foundry/KlerosCore_Appeals.t.sol b/contracts/test/foundry/KlerosCore_Appeals.t.sol index c5016f23c..79e575e81 100644 --- a/contracts/test/foundry/KlerosCore_Appeals.t.sol +++ b/contracts/test/foundry/KlerosCore_Appeals.t.sol @@ -500,4 +500,130 @@ contract KlerosCore_AppealsTest is KlerosCore_TestBase { emit KlerosCore.NewPeriod(disputeID, KlerosCore.Period.execution); core.passPeriod(disputeID); } + + function testFuzz_appeal(uint256 numberOfOptions, uint256 choice1, uint256 choice2, uint256 choice3) public { + uint256 disputeID = 0; + + arbitrable.changeNumberOfRulingOptions(numberOfOptions); + + // Have only 2 options for 3 jurors to create a majority + vm.assume(choice1 <= numberOfOptions); + vm.assume(choice2 <= numberOfOptions); + vm.assume(choice3 <= numberOfOptions); // Will be used for appeal + + vm.prank(staker1); + core.setStake(GENERAL_COURT, 2000); + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + + (uint256 numberOfChoices, , ) = disputeKit.disputes(disputeID); + + assertEq(numberOfChoices, numberOfOptions, "Wrong numberOfChoices"); + + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.warp(block.timestamp + rngLookahead); + 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.warp(block.timestamp + rngLookahead); + 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, choice1, 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, choice2, 0, "XYZ"); + core.passPeriod(disputeID); // Appeal + + vm.assume(choice3 != choice2); + vm.prank(crowdfunder1); + disputeKit.fundAppeal{value: 0.63 ether}(disputeID, choice3); // Fund the losing choice. Total cost will be 0.63 (0.21 + 0.21 * (20000/10000)) + + assertEq((disputeKit.getFundedChoices(disputeID)).length, 1, "1 choice should be funded"); + + vm.prank(crowdfunder1); + disputeKit.fundAppeal{value: 0.42 ether}(disputeID, choice2); // Fund the winning choice. Total cost will be 0.42 (0.21 + 0.21 * (10000/10000)) + + assertEq((disputeKit.getFundedChoices(disputeID)).length, 0, "No funded choices in a fresh round"); + } + + function testFuzz_fundAppeal_msgValue(uint256 appealValue) public { + uint256 disputeID = 0; + + vm.assume(appealValue <= 10 ether); + vm.deal(crowdfunder1, 10 ether); + + 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.warp(block.timestamp + rngLookahead); + 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.warp(block.timestamp + rngLookahead); + 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.prank(crowdfunder1); + disputeKit.fundAppeal{value: appealValue}(disputeID, 1); // Fund the losing choice + + if (appealValue >= 0.63 ether) { + // 0.63 eth is the required amount for losing side. + assertEq((disputeKit.getFundedChoices(disputeID)).length, 1, "One choice should be funded"); + // Dispute kit shouldn't demand more value than necessary + assertEq(crowdfunder1.balance, 9.37 ether, "Wrong balance of the crowdfunder"); + assertEq(address(disputeKit).balance, 0.63 ether, "Wrong balance of the DK"); + } else { + assertEq((disputeKit.getFundedChoices(disputeID)).length, 0, "No choices should be funded"); + assertEq(crowdfunder1.balance, 10 ether - appealValue, "Wrong balance of the crowdfunder"); + assertEq(address(disputeKit).balance, appealValue, "Wrong balance of the DK"); + } + } } diff --git a/contracts/test/foundry/KlerosCore_Disputes.t.sol b/contracts/test/foundry/KlerosCore_Disputes.t.sol index 5b61c93cb..2d029ddcf 100644 --- a/contracts/test/foundry/KlerosCore_Disputes.t.sol +++ b/contracts/test/foundry/KlerosCore_Disputes.t.sol @@ -94,7 +94,7 @@ contract KlerosCore_DisputesTest is KlerosCore_TestBase { assertEq(jumped, false, "jumped should be false"); assertEq(extraData, newExtraData, "Wrong extra data"); assertEq(disputeKit.coreDisputeIDToLocal(0), disputeID, "Wrong local disputeID"); - assertEq(disputeKit.coreDisputeIDToActive(0), true, "Wrong disputes length"); + assertEq(disputeKit.coreDisputeIDToActive(0), true, "Dispute should be active in this DK"); ( uint256 winningChoice, @@ -146,4 +146,49 @@ contract KlerosCore_DisputesTest is KlerosCore_TestBase { 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 testFuzz_createDispute_msgValue(uint256 disputeValue) public { + uint256 disputeID = 0; + uint256 arbitrationCost = core.arbitrationCost(arbitratorExtraData); + + // Cap it to 10 eth, so the number of jurors is not astronomical. + vm.assume(disputeValue >= arbitrationCost && disputeValue <= 10 ether); + vm.deal(disputer, 10 ether); + + vm.prank(staker1); + core.setStake(GENERAL_COURT, 2000); + vm.prank(disputer); + arbitrable.createDispute{value: disputeValue}("Action"); + + KlerosCore.Round memory round = core.getRoundInfo(disputeID, 0); + assertEq(round.totalFeesForJurors, disputeValue, "Wrong totalFeesForJurors"); + assertEq(round.nbVotes, disputeValue / feeForJuror, "Wrong nbVotes"); + + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.warp(block.timestamp + rngLookahead); + sortitionModule.passPhase(); // Drawing phase + + core.draw(disputeID, disputeValue / feeForJuror); + + vm.warp(block.timestamp + timesPerPeriod[0]); + core.passPeriod(disputeID); // Vote + + uint256[] memory voteIDs = new uint256[](disputeValue / feeForJuror); + for (uint256 i = 0; i < voteIDs.length; i++) { + voteIDs[i] = i; + } + + vm.prank(staker1); + disputeKit.castVote(disputeID, voteIDs, 1, 0, "XYZ"); + + core.passPeriod(disputeID); // Appeal + + vm.warp(block.timestamp + timesPerPeriod[3]); + core.passPeriod(disputeID); // Execution + + vm.expectEmit(true, true, true, true); + emit IArbitrableV2.Ruling(IArbitratorV2(address(core)), disputeID, 1); + core.executeRuling(disputeID); + } } diff --git a/contracts/test/foundry/KlerosCore_Drawing.t.sol b/contracts/test/foundry/KlerosCore_Drawing.t.sol index 9bffbc27d..2db69e612 100644 --- a/contracts/test/foundry/KlerosCore_Drawing.t.sol +++ b/contracts/test/foundry/KlerosCore_Drawing.t.sol @@ -121,4 +121,90 @@ contract KlerosCore_DrawingTest is KlerosCore_TestBase { (, , , , uint256 nbVoters, ) = disputeKit.getRoundInfo(disputeID, roundID, 0); assertEq(nbVoters, 3, "nbVoters should be 3"); } + + function testFuzz_drawIterations(uint256 iterations) public { + uint256 disputeID = 0; + uint256 roundID = 0; + + 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.warp(block.timestamp + rngLookahead); + sortitionModule.passPhase(); // Drawing phase + + core.draw(disputeID, iterations); + + uint256 iterationsCount; + uint256 disputesWithoutJurors; + if (iterations < DEFAULT_NB_OF_JURORS) { + iterationsCount = iterations; + disputesWithoutJurors = 1; + } else { + iterationsCount = DEFAULT_NB_OF_JURORS; + disputesWithoutJurors = 0; + } + + KlerosCore.Round memory round = core.getRoundInfo(disputeID, roundID); + assertEq(round.drawIterations, iterationsCount, "Wrong drawIterations number"); + assertEq(round.nbVotes, DEFAULT_NB_OF_JURORS, "Wrong nbVotes"); + + (uint256 totalStaked, uint256 totalLocked, uint256 stakedInCourt, ) = sortitionModule.getJurorBalance( + staker1, + GENERAL_COURT + ); + uint256 pnkAtStake = (minStake * alpha) / ONE_BASIS_POINT; + assertEq(totalStaked, 2000, "Wrong amount total staked"); + assertEq(totalLocked, pnkAtStake * iterationsCount, "Wrong amount locked"); // 1000 per draw + assertEq(stakedInCourt, 2000, "Wrong amount staked in court"); + assertEq(sortitionModule.disputesWithoutJurors(), disputesWithoutJurors, "Wrong disputesWithoutJurors count"); + } + + function testFuzz_drawIterations_nbJurors(uint256 iterations, uint256 disputeValue) public { + uint256 disputeID = 0; + uint256 roundID = 0; + + uint256 arbitrationCost = core.arbitrationCost(arbitratorExtraData); + // Cap it to 10 eth, so the number of jurors is not astronomical. + vm.assume(disputeValue >= arbitrationCost && disputeValue <= 10 ether); + vm.deal(disputer, 10 ether); + + vm.prank(staker1); + core.setStake(GENERAL_COURT, 2000); + vm.prank(disputer); + arbitrable.createDispute{value: disputeValue}("Action"); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.warp(block.timestamp + rngLookahead); + sortitionModule.passPhase(); // Drawing phase + + core.draw(disputeID, iterations); + + KlerosCore.Round memory round = core.getRoundInfo(disputeID, roundID); + assertEq(round.totalFeesForJurors, disputeValue, "Wrong totalFeesForJurors"); + assertEq(round.nbVotes, disputeValue / feeForJuror, "Wrong nbVotes"); + + uint256 iterationsCount; + uint256 disputesWithoutJurors; + if (iterations < round.nbVotes) { + iterationsCount = iterations; + disputesWithoutJurors = 1; + } else { + iterationsCount = round.nbVotes; + disputesWithoutJurors = 0; + } + + assertEq(round.drawIterations, iterationsCount, "Wrong drawIterations number"); + (uint256 totalStaked, uint256 totalLocked, uint256 stakedInCourt, ) = sortitionModule.getJurorBalance( + staker1, + GENERAL_COURT + ); + uint256 pnkAtStake = (minStake * alpha) / ONE_BASIS_POINT; + assertEq(totalStaked, 2000, "Wrong amount total staked"); + assertEq(totalLocked, pnkAtStake * iterationsCount, "Wrong amount locked"); + assertEq(stakedInCourt, 2000, "Wrong amount staked in court"); + assertEq(sortitionModule.disputesWithoutJurors(), disputesWithoutJurors, "Wrong disputesWithoutJurors count"); + } } diff --git a/contracts/test/foundry/KlerosCore_Execution.t.sol b/contracts/test/foundry/KlerosCore_Execution.t.sol index c6f03ac49..bbce3234a 100644 --- a/contracts/test/foundry/KlerosCore_Execution.t.sol +++ b/contracts/test/foundry/KlerosCore_Execution.t.sol @@ -748,4 +748,103 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { assertEq(crowdfunder2.balance, 10 ether, "Wrong balance of the crowdfunder2"); assertEq(address(disputeKit).balance, 0, "Wrong balance of the DK"); } + + function testFuzz_executeIterations(uint256 iterations) public { + uint256 disputeID = 0; + uint256 roundID = 0; + + 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.warp(block.timestamp + rngLookahead); + sortitionModule.passPhase(); // Drawing phase + + core.draw(disputeID, 3); + + 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, roundID, "XYZ"); + + core.passPeriod(disputeID); // Appeal + + vm.warp(block.timestamp + timesPerPeriod[3]); + core.passPeriod(disputeID); // Execution + + core.execute(disputeID, roundID, iterations); + + uint256 iterationsCount = iterations < DEFAULT_NB_OF_JURORS * 2 ? iterations : DEFAULT_NB_OF_JURORS * 2; + + KlerosCore.Round memory round = core.getRoundInfo(disputeID, roundID); + assertEq(round.repartitions, iterationsCount, "Wrong repartitions"); + } + + function testFuzz_executeIterations_nbJurors(uint256 iterations, uint256 disputeValue) public { + uint256 disputeID = 0; + uint256 roundID = 0; + + uint256 arbitrationCost = core.arbitrationCost(arbitratorExtraData); + // Cap it to 10 eth, so the number of jurors is not astronomical. + vm.assume(disputeValue >= arbitrationCost && disputeValue <= 10 ether); + vm.deal(disputer, 10 ether); + + vm.prank(staker1); + core.setStake(GENERAL_COURT, 2000); + vm.prank(disputer); + arbitrable.createDispute{value: disputeValue}("Action"); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.warp(block.timestamp + rngLookahead); + sortitionModule.passPhase(); // Drawing phase + + uint256 nbJurors = disputeValue / feeForJuror; + + core.draw(disputeID, nbJurors); + + KlerosCore.Round memory round = core.getRoundInfo(disputeID, roundID); + assertEq(round.totalFeesForJurors, disputeValue, "Wrong totalFeesForJurors"); + assertEq(round.nbVotes, nbJurors, "Wrong nbVotes"); + + vm.warp(block.timestamp + timesPerPeriod[0]); + core.passPeriod(disputeID); // Vote + + uint256[] memory voteIDs = new uint256[](nbJurors); + for (uint256 i = 0; i < voteIDs.length; i++) { + voteIDs[i] = i; + } + + vm.prank(staker1); + disputeKit.castVote(disputeID, voteIDs, 1, 0, "XYZ"); + + core.passPeriod(disputeID); // Appeal + + vm.warp(block.timestamp + timesPerPeriod[3]); + core.passPeriod(disputeID); // Execution + + core.execute(disputeID, roundID, iterations); + + uint256 iterationsCount = iterations < nbJurors * 2 ? iterations : nbJurors * 2; + round = core.getRoundInfo(disputeID, roundID); + + assertEq(round.repartitions, iterationsCount, "Wrong repartitions"); + + (uint256 totalStaked, uint256 totalLocked, uint256 stakedInCourt, ) = sortitionModule.getJurorBalance( + staker1, + GENERAL_COURT + ); + + uint256 pnkAtStake = (minStake * alpha) / ONE_BASIS_POINT; + uint256 unlockedTokens = iterationsCount < nbJurors ? 0 : (iterationsCount - nbJurors) * pnkAtStake; + assertEq(totalStaked, 2000, "Wrong amount total staked"); + assertEq(totalLocked, (pnkAtStake * nbJurors) - unlockedTokens, "Wrong amount locked"); + assertEq(stakedInCourt, 2000, "Wrong amount staked in court"); + } } diff --git a/contracts/test/foundry/KlerosCore_Staking.t.sol b/contracts/test/foundry/KlerosCore_Staking.t.sol index a7cb96bef..b44bbfab2 100644 --- a/contracts/test/foundry/KlerosCore_Staking.t.sol +++ b/contracts/test/foundry/KlerosCore_Staking.t.sol @@ -7,6 +7,7 @@ import {SortitionModule} from "../../src/arbitration/SortitionModule.sol"; import {ISortitionModule} from "../../src/arbitration/interfaces/ISortitionModule.sol"; import {IKlerosCore, KlerosCoreSnapshotProxy} from "../../src/arbitration/view/KlerosCoreSnapshotProxy.sol"; import "../../src/libraries/Constants.sol"; +import {console} from "forge-std/console.sol"; /// @title KlerosCore_StakingTest /// @dev Tests for KlerosCore staking mechanics and stake management @@ -370,7 +371,7 @@ contract KlerosCore_StakingTest is KlerosCore_TestBase { assertEq(stakedInCourt, 0, "Wrong amount staked in court"); assertEq(nbCourts, 0, "Wrong number of courts"); - vm.warp(block.timestamp + minStakingTime); + vm.warp(block.timestamp + maxDrawingTime); sortitionModule.passPhase(); // Staking. Delayed stakes can be executed now vm.prank(address(core)); @@ -441,4 +442,180 @@ contract KlerosCore_StakingTest is KlerosCore_TestBase { snapshotProxy.changeOwner(other); assertEq(snapshotProxy.owner(), other, "Wrong owner after change"); } + + function testFuzz_setStake(uint256 firstStake, uint256 secondStake) public { + uint256 stakerSupply = totalSupply / 10; + + vm.prank(owner); + pinakion.transfer(staker1, stakerSupply - 1 ether); // 1 eth was transferred in the initial setup so offset that value. + vm.assume(firstStake >= minStake && firstStake <= stakerSupply); + vm.assume(secondStake >= minStake && secondStake <= stakerSupply); + + vm.prank(staker1); + pinakion.approve(address(core), firstStake); + + vm.prank(staker1); + vm.expectEmit(true, true, true, true); + emit SortitionModule.StakeSet(staker1, GENERAL_COURT, firstStake, firstStake); + core.setStake(GENERAL_COURT, firstStake); + + (uint256 totalStaked, uint256 totalLocked, uint256 stakedInCourt, uint256 nbCourts) = sortitionModule + .getJurorBalance(staker1, GENERAL_COURT); + assertEq(totalStaked, firstStake, "Wrong amount total staked"); + assertEq(totalLocked, 0, "Wrong amount locked"); + assertEq(stakedInCourt, firstStake, "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)), firstStake, "Wrong token balance of the core"); + assertEq(pinakion.balanceOf(staker1), stakerSupply - firstStake, "Wrong token balance of staker1"); + assertEq(pinakion.allowance(staker1, address(core)), 0, "Allowance should be spent for staker1"); + + // Change the stake and see if everything is correct. + vm.prank(staker1); + pinakion.approve(address(core), secondStake); + + vm.prank(staker1); + vm.expectEmit(true, true, true, true); + emit SortitionModule.StakeSet(staker1, GENERAL_COURT, secondStake, secondStake); + core.setStake(GENERAL_COURT, secondStake); + + (totalStaked, totalLocked, stakedInCourt, nbCourts) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); + assertEq(totalStaked, secondStake, "Wrong amount total staked secondStake"); + assertEq(totalLocked, 0, "Wrong amount locked secondStake"); + assertEq(stakedInCourt, secondStake, "Wrong amount staked in court secondStake"); + assertEq(nbCourts, 1, "Number of courts should not increase secondStake"); + + assertEq(pinakion.balanceOf(address(core)), secondStake, "Wrong token balance of the core secondStake"); + assertEq(pinakion.balanceOf(staker1), stakerSupply - secondStake, "Wrong token balance of staker1 secondStake"); + + bool stakeIncrease = secondStake > firstStake; + // If stake decrease new allowance won't be spent, if it increases it will only spent (secondStake - firstStake) difference. + uint256 newAllowance = stakeIncrease ? firstStake : secondStake; + assertEq(pinakion.allowance(staker1, address(core)), newAllowance, "Incorrect allowance secondStake"); + } + + function testFuzz_setStake_differentCourts(uint256 firstStake, uint256 secondStake) public { + uint256 stakerSupply = totalSupply / 10; + + vm.prank(owner); + uint256[] memory supportedDK = new uint256[](1); + supportedDK[0] = DISPUTE_KIT_CLASSIC; + core.createCourt( + GENERAL_COURT, + hiddenVotes, + minStake, + alpha, + feeForJuror, + jurorsForCourtJump, + timesPerPeriod, // Times per period + sortitionExtraData, // Sortition extra data + supportedDK + ); + + uint96 newCourtID = 2; + + vm.prank(owner); + pinakion.transfer(staker1, stakerSupply - 1 ether); // 1 eth was transferred in the initial setup so offset that value. + vm.assume(firstStake >= minStake && firstStake <= stakerSupply / 2); // Split the supply into two because courts are different now + vm.assume(secondStake >= minStake && secondStake <= stakerSupply / 2); + + vm.prank(staker1); + pinakion.approve(address(core), firstStake); + + vm.prank(staker1); + vm.expectEmit(true, true, true, true); + emit SortitionModule.StakeSet(staker1, GENERAL_COURT, firstStake, firstStake); + core.setStake(GENERAL_COURT, firstStake); + + (uint256 totalStaked, uint256 totalLocked, uint256 stakedInCourt, uint256 nbCourts) = sortitionModule + .getJurorBalance(staker1, GENERAL_COURT); + assertEq(totalStaked, firstStake, "Wrong amount total staked"); + assertEq(totalLocked, 0, "Wrong amount locked"); + assertEq(stakedInCourt, firstStake, "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)), firstStake, "Wrong token balance of the core"); + assertEq(pinakion.balanceOf(staker1), stakerSupply - firstStake, "Wrong token balance of staker1"); + assertEq(pinakion.allowance(staker1, address(core)), 0, "Allowance should be spent for staker1"); + + // Stake the juror in a different court. + vm.prank(staker1); + pinakion.approve(address(core), secondStake); + + vm.prank(staker1); + vm.expectEmit(true, true, true, true); + emit SortitionModule.StakeSet(staker1, newCourtID, secondStake, firstStake + secondStake); + core.setStake(newCourtID, secondStake); + + (totalStaked, totalLocked, stakedInCourt, nbCourts) = sortitionModule.getJurorBalance(staker1, newCourtID); + assertEq(totalStaked, secondStake + firstStake, "Wrong amount total staked secondStake"); + assertEq(totalLocked, 0, "Wrong amount locked secondStake"); + assertEq(stakedInCourt, secondStake, "Wrong amount staked in court secondStake"); + assertEq(nbCourts, 2, "Number of courts should increase"); + + assertEq( + pinakion.balanceOf(address(core)), + secondStake + firstStake, + "Wrong token balance of the core secondStake" + ); + assertEq( + pinakion.balanceOf(staker1), + stakerSupply - secondStake - firstStake, + "Wrong token balance of staker1 secondStake" + ); + + assertEq(pinakion.allowance(staker1, address(core)), 0, "Allowance should be spent for staker1 secondStake"); + } + + function testFuzz_delayedStakes(uint256 iterations) public { + // Test with large numbers but do not trigger possible overflow + vm.assume(iterations < 2 ** 128); + + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.warp(block.timestamp + rngLookahead); + sortitionModule.passPhase(); // Drawing phase + uint256 disputeID = 0; + core.draw(disputeID, DEFAULT_NB_OF_JURORS); + + // Create delayed stakes + vm.prank(staker1); + core.setStake(GENERAL_COURT, 1500); + + vm.prank(staker1); + core.setStake(GENERAL_COURT, 2500); + + vm.prank(staker1); + core.setStake(GENERAL_COURT, 3500); + + // Create delayed stake for another staker + vm.prank(staker2); + core.setStake(GENERAL_COURT, 1000); + + assertEq(sortitionModule.delayedStakeWriteIndex(), 4, "Wrong delayedStakeWriteIndex"); + assertEq(sortitionModule.delayedStakeReadIndex(), 1, "Wrong delayedStakeReadIndex"); + + vm.warp(block.timestamp + maxDrawingTime); + sortitionModule.passPhase(); // Staking. Delayed stakes can be executed now + + sortitionModule.executeDelayedStakes(iterations); + + uint256 actualIterations = iterations > 4 ? sortitionModule.delayedStakeWriteIndex() : iterations; + uint256 newDelayedStakeReadIndex = 1 + actualIterations; + + assertEq(sortitionModule.delayedStakeWriteIndex(), 4, "Wrong delayedStakeWriteIndex"); + assertEq(sortitionModule.delayedStakeReadIndex(), newDelayedStakeReadIndex, "Wrong delayedStakeReadIndex"); + } } diff --git a/contracts/test/foundry/KlerosCore_Voting.t.sol b/contracts/test/foundry/KlerosCore_Voting.t.sol index 504c9c644..41f8e3973 100644 --- a/contracts/test/foundry/KlerosCore_Voting.t.sol +++ b/contracts/test/foundry/KlerosCore_Voting.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.24; import {KlerosCore_TestBase} from "./KlerosCore_TestBase.sol"; -import {KlerosCore} from "../../src/arbitration/KlerosCore.sol"; +import {KlerosCore, IArbitratorV2, IArbitrableV2} from "../../src/arbitration/KlerosCore.sol"; import {DisputeKitClassic, DisputeKitClassicBase} from "../../src/arbitration/dispute-kits/DisputeKitClassic.sol"; import {IDisputeKit} from "../../src/arbitration/interfaces/IDisputeKit.sol"; import {UUPSProxy} from "../../src/proxy/UUPSProxy.sol"; @@ -483,4 +483,59 @@ contract KlerosCore_VotingTest is KlerosCore_TestBase { assertEq(totalCommited, 0, "totalCommited should be 0"); assertEq(choiceCount, 3, "choiceCount should be 3"); } + + function testFuzz_castVote(uint256 numberOfOptions, uint256 choice1, uint256 choice2) public { + uint256 disputeID = 0; + + arbitrable.changeNumberOfRulingOptions(numberOfOptions); + + // Have only 2 options for 3 jurors to create a majority + vm.assume(choice1 <= numberOfOptions); + vm.assume(choice2 <= numberOfOptions); + + 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.warp(block.timestamp + rngLookahead); + 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.warp(block.timestamp + rngLookahead); + 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, choice1, 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, choice2, 0, "XYZ"); + core.passPeriod(disputeID); // Appeal + + vm.warp(block.timestamp + timesPerPeriod[3]); + core.passPeriod(disputeID); // Execution + + vm.expectEmit(true, true, true, true); + emit IArbitrableV2.Ruling(IArbitratorV2(address(core)), disputeID, choice2); + core.executeRuling(disputeID); + } }