From 1006d82054817db97250de8a8a16844412be633b Mon Sep 17 00:00:00 2001 From: unknownunknown1 Date: Sun, 30 Nov 2025 02:55:08 +1000 Subject: [PATCH 1/8] fix(SortitionModule): total stake update --- contracts/src/arbitration/KlerosCore.sol | 1 + contracts/src/arbitration/SortitionModule.sol | 12 +++++++-- .../interfaces/ISortitionModule.sol | 5 ++++ .../university/SortitionModuleUniversity.sol | 5 ++++ .../test/foundry/KlerosCore_Staking.t.sol | 26 +++++++++++++++++++ 5 files changed, 47 insertions(+), 2 deletions(-) diff --git a/contracts/src/arbitration/KlerosCore.sol b/contracts/src/arbitration/KlerosCore.sol index eb301237a..aaa6e4ade 100644 --- a/contracts/src/arbitration/KlerosCore.sol +++ b/contracts/src/arbitration/KlerosCore.sol @@ -1363,6 +1363,7 @@ contract KlerosCore is IArbitratorV2, Initializable, UUPSProxiable { } } sortitionModule.setStake(_account, _courtID, pnkDeposit, pnkWithdrawal, _newStake); + sortitionModule.updateTotalStake(pnkDeposit, pnkWithdrawal); return true; } diff --git a/contracts/src/arbitration/SortitionModule.sol b/contracts/src/arbitration/SortitionModule.sol index 7a65327f5..d253414ad 100644 --- a/contracts/src/arbitration/SortitionModule.sol +++ b/contracts/src/arbitration/SortitionModule.sol @@ -311,7 +311,6 @@ contract SortitionModule is ISortitionModule, Initializable, UUPSProxiable { // Current phase is Staking: set stakes. if (stakeIncrease) { pnkDeposit = stakeChange; - totalStaked += stakeChange; } else { pnkWithdrawal = stakeChange; uint256 possibleWithdrawal = juror.stakedPnk > juror.lockedPnk ? juror.stakedPnk - juror.lockedPnk : 0; @@ -319,11 +318,20 @@ contract SortitionModule is ISortitionModule, Initializable, UUPSProxiable { // Ensure locked tokens remain in the contract. They can only be released during Execution. pnkWithdrawal = possibleWithdrawal; } - totalStaked -= pnkWithdrawal; } return (pnkDeposit, pnkWithdrawal, StakingResult.Successful); } + /// @inheritdoc ISortitionModule + function updateTotalStake(uint256 _pnkDeposit, uint256 _pnkWithdrawal) external override onlyByCore { + // Note that we don't update totalStake in setStake() function because it doesn't always change total (e.g. during rewards/penalties). + if (_pnkDeposit > 0) { + totalStaked += _pnkDeposit; + } else { + totalStaked -= _pnkWithdrawal; + } + } + /// @inheritdoc ISortitionModule function setStake( address _account, diff --git a/contracts/src/arbitration/interfaces/ISortitionModule.sol b/contracts/src/arbitration/interfaces/ISortitionModule.sol index ccc66a2f5..6a84089db 100644 --- a/contracts/src/arbitration/interfaces/ISortitionModule.sol +++ b/contracts/src/arbitration/interfaces/ISortitionModule.sol @@ -57,6 +57,11 @@ interface ISortitionModule { bool _noDelay ) external returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult); + /// @notice Updates the total amount staked in all courts. + /// @param _pnkDeposit The amount of PNK that increases total stake. + /// @param _pnkWithdrawal The amount of PNK that decreases total stake. + function updateTotalStake(uint256 _pnkDeposit, uint256 _pnkWithdrawal) external; + /// @notice Update the state of the stakes, called by KC at the end of setStake flow. /// /// @dev `O(n + p * log_k(j))` where diff --git a/contracts/src/arbitration/university/SortitionModuleUniversity.sol b/contracts/src/arbitration/university/SortitionModuleUniversity.sol index c24e8811c..5ce7f9e00 100644 --- a/contracts/src/arbitration/university/SortitionModuleUniversity.sol +++ b/contracts/src/arbitration/university/SortitionModuleUniversity.sol @@ -130,6 +130,11 @@ contract SortitionModuleUniversity is ISortitionModuleUniversity, UUPSProxiable, // NOP } + /// @inheritdoc ISortitionModule + function updateTotalStake(uint256 _pnkDeposit, uint256 _pnkWithdrawal) external override onlyByCore { + // NOP + } + /// @inheritdoc ISortitionModule function createDisputeHook(uint256 /*_disputeID*/, uint256 /*_roundID*/) external override onlyByCore { disputesWithoutJurors++; diff --git a/contracts/test/foundry/KlerosCore_Staking.t.sol b/contracts/test/foundry/KlerosCore_Staking.t.sol index b44bbfab2..a0e0dc8a5 100644 --- a/contracts/test/foundry/KlerosCore_Staking.t.sol +++ b/contracts/test/foundry/KlerosCore_Staking.t.sol @@ -141,6 +141,32 @@ contract KlerosCore_StakingTest is KlerosCore_TestBase { ); } + function test_setStake_totalStaked() public { + // Increase + vm.prank(staker1); + core.setStake(GENERAL_COURT, 4000); + vm.prank(staker1); + core.setStake(GENERAL_COURT, 5001); + vm.prank(staker2); + core.setStake(GENERAL_COURT, 1000); + vm.prank(staker2); + core.setStake(GENERAL_COURT, 1500); + + assertEq(sortitionModule.totalStaked(), 6501, "Wrong totalStaked"); + + // Decrease + vm.prank(staker1); + core.setStake(GENERAL_COURT, 3000); + vm.prank(staker1); + core.setStake(GENERAL_COURT, 2500); + vm.prank(staker2); + core.setStake(GENERAL_COURT, 1400); + vm.prank(staker2); + core.setStake(GENERAL_COURT, 1200); + + assertEq(sortitionModule.totalStaked(), 3700, "Wrong totalStaked"); + } + function test_setStake_maxStakePathCheck() public { uint256[] memory supportedDK = new uint256[](1); supportedDK[0] = DISPUTE_KIT_CLASSIC; From a2b5d76a2281d1b1a690733c06675262784fe34d Mon Sep 17 00:00:00 2001 From: unknownunknown1 Date: Wed, 3 Dec 2025 00:00:05 +1000 Subject: [PATCH 2/8] feat(SortitionModule): add certora PoC to Foundry --- .../test/foundry/KlerosCore_Execution.t.sol | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/contracts/test/foundry/KlerosCore_Execution.t.sol b/contracts/test/foundry/KlerosCore_Execution.t.sol index 81dd723a0..dbeebbeb1 100644 --- a/contracts/test/foundry/KlerosCore_Execution.t.sol +++ b/contracts/test/foundry/KlerosCore_Execution.t.sol @@ -8,6 +8,7 @@ import {DisputeKitClassicBase} from "../../src/arbitration/dispute-kits/DisputeK import {IArbitratorV2, IArbitrableV2} from "../../src/arbitration/KlerosCore.sol"; import {IERC20} from "../../src/libraries/SafeERC20.sol"; import "../../src/libraries/Constants.sol"; +import {console} from "forge-std/console.sol"; /// @title KlerosCore_ExecutionTest /// @dev Tests for KlerosCore execution, rewards, and ruling finalization @@ -749,6 +750,52 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { assertEq(address(disputeKit).balance, 0, "Wrong balance of the DK"); } + function test_inflatedTotalStaked_whenDelayedStakeExecute_whenJurorHasNoFunds() public { + // pre conditions + // 1. there is a dispute in drawing phase + // 2. juror call setStake with an amount greater than his PNK balance + // 3. draw jurors, move to voting phase and execute voting + // 4. move sortition to staking phase + uint256 disputeID = 0; + uint256 amountToStake = 20000; + _stakePnk_createDispute_moveToDrawingPhase(disputeID, staker1, amountToStake); + + KlerosCore.Round memory round = core.getRoundInfo(disputeID, 0); + uint256 pnkAtStakePerJuror = round.pnkAtStakePerJuror; + _stakeBalanceForJuror(staker1, type(uint256).max); + _drawJurors_advancePeriodToVoting(disputeID); + _vote_execute(disputeID, staker1); + sortitionModule.passPhase(); // set it to staking phase + _assertJurorBalance( + disputeID, + staker1, + amountToStake, + pnkAtStakePerJuror * DEFAULT_NB_OF_JURORS, + amountToStake, + 1 + ); + + console.log("totalStaked before: %e", sortitionModule.totalStaked()); + + // execution: execute delayed stake + sortitionModule.executeDelayedStakes(1); + + // post condition: inflated totalStaked + console.log("totalStaked after: %e", sortitionModule.totalStaked()); + _assertJurorBalance( + disputeID, + staker1, + amountToStake, + pnkAtStakePerJuror * DEFAULT_NB_OF_JURORS, + amountToStake, + 1 + ); + + // new juror tries to stake but totalStaked already reached type(uint256).max + // it reverts with "arithmetic underflow or overflow (0x11)" + _stakeBalanceForJuror(staker2, 20000); + } + function testFuzz_executeIterations(uint256 iterations) public { uint256 disputeID = 0; uint256 roundID = 0; @@ -847,4 +894,61 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { assertEq(totalLocked, (pnkAtStake * nbJurors) - unlockedTokens, "Wrong amount locked"); assertEq(stakedInCourt, 2000, "Wrong amount staked in court"); } + + ///////// Internal ////////// + + function _assertJurorBalance( + uint256 disputeID, + address juror, + uint256 totalStakedPnk, + uint256 totalLocked, + uint256 stakedInCourt, + uint256 nbCourts + ) internal { + (uint256 totalStakedPnk, uint256 totalLocked, uint256 stakedInCourt, uint256 nbCourts) = sortitionModule + .getJurorBalance(juror, GENERAL_COURT); + assertEq(totalStakedPnk, totalStakedPnk, "Wrong totalStakedPnk"); // jurors total staked a.k.a juror.stakedPnk + assertEq(totalLocked, totalLocked, "Wrong totalLocked"); + assertEq(stakedInCourt, stakedInCourt, "Wrong stakedInCourt"); // juror staked in court a.k.a _stakeOf + assertEq(nbCourts, nbCourts, "Wrong nbCourts"); + } + + function _stakeBalanceForJuror(address juror, uint256 amount) internal { + console.log("actual juror PNK balance before staking: %e", pinakion.balanceOf(juror)); + vm.prank(juror); + core.setStake(GENERAL_COURT, amount); + } + + function _stakePnk_createDispute_moveToDrawingPhase(uint256 disputeID, address juror, uint256 amount) internal { + vm.prank(juror); + core.setStake(GENERAL_COURT, amount); + 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 + + assertEq(sortitionModule.totalStaked(), amount, "!totalStaked"); + } + + function _drawJurors_advancePeriodToVoting(uint256 disputeID) internal { + core.draw(disputeID, DEFAULT_NB_OF_JURORS); + vm.warp(block.timestamp + timesPerPeriod[0]); + core.passPeriod(disputeID); // Vote + } + + function _vote_execute(uint256 disputeID, address juror) internal { + uint256[] memory voteIDs = new uint256[](3); + voteIDs[0] = 0; + voteIDs[1] = 1; + voteIDs[2] = 2; + + vm.prank(juror); + disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); + core.passPeriod(disputeID); // Appeal + + vm.warp(block.timestamp + timesPerPeriod[3]); + core.passPeriod(disputeID); // Execution + } } From 719ea3ee1ab19bedb64b390f6bde70feda76e281 Mon Sep 17 00:00:00 2001 From: unknownunknown1 Date: Sun, 7 Dec 2025 05:35:36 +1000 Subject: [PATCH 3/8] fix(KC): malicious arbitrable revert --- contracts/src/arbitration/KlerosCore.sol | 9 ++- .../arbitrables/ArbitrableExample.sol | 2 +- .../dispute-kits/DisputeKitClassicBase.sol | 4 +- .../src/test/MaliciousArbitrableMock.sol | 52 ++++++++++++++++ .../test/foundry/KlerosCore_Execution.t.sol | 60 ++++++++++++++++++- 5 files changed, 121 insertions(+), 6 deletions(-) create mode 100644 contracts/src/test/MaliciousArbitrableMock.sol diff --git a/contracts/src/arbitration/KlerosCore.sol b/contracts/src/arbitration/KlerosCore.sol index aaa6e4ade..b470fc102 100644 --- a/contracts/src/arbitration/KlerosCore.sol +++ b/contracts/src/arbitration/KlerosCore.sol @@ -139,6 +139,11 @@ contract KlerosCore is IArbitratorV2, Initializable, UUPSProxiable { /// @param _voteID ID of the vote given to the drawn juror. event Draw(address indexed _address, uint256 indexed _disputeID, uint256 _roundID, uint256 _voteID); + /// @notice Emitted when the dispute is successfully appealed. + /// @param _disputeID ID of the related dispute. + /// @param _arbitrable The arbitrable contract. + event ArbitrableReverted(uint256 indexed _disputeID, IArbitrableV2 indexed _arbitrable); + /// @notice Emitted when a new court is created. /// @param _courtID ID of the new court. /// @param _parent ID of the parent court. @@ -1068,7 +1073,9 @@ contract KlerosCore is IArbitratorV2, Initializable, UUPSProxiable { (uint256 winningChoice, , ) = currentRuling(_disputeID); dispute.ruled = true; emit Ruling(dispute.arbitrated, _disputeID, winningChoice); - dispute.arbitrated.rule(_disputeID, winningChoice); + try dispute.arbitrated.rule(_disputeID, winningChoice) {} catch { + emit ArbitrableReverted(_disputeID, dispute.arbitrated); + } } // ************************************* // diff --git a/contracts/src/arbitration/arbitrables/ArbitrableExample.sol b/contracts/src/arbitration/arbitrables/ArbitrableExample.sol index 3811bcb59..dc075028e 100644 --- a/contracts/src/arbitration/arbitrables/ArbitrableExample.sol +++ b/contracts/src/arbitration/arbitrables/ArbitrableExample.sol @@ -149,7 +149,7 @@ contract ArbitrableExample is IArbitrableV2 { } /// @inheritdoc IArbitrableV2 - function rule(uint256 _arbitratorDisputeID, uint256 _ruling) external override { + function rule(uint256 _arbitratorDisputeID, uint256 _ruling) external virtual { uint256 localDisputeID = externalIDtoLocalID[_arbitratorDisputeID]; DisputeStruct storage dispute = disputes[localDisputeID]; if (msg.sender != address(arbitrator)) revert ArbitratorOnly(); diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol index 643690693..252e7b562 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol @@ -475,8 +475,8 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi address payable _beneficiary, uint256 _choice ) external returns (uint256 amount) { - (, , , bool isRuled, ) = core.disputes(_coreDisputeID); - if (!isRuled) revert DisputeNotResolved(); + (, , KlerosCore.Period period, , ) = core.disputes(_coreDisputeID); + if (period != KlerosCore.Period.execution) revert DisputeNotResolved(); if (core.paused()) revert CoreIsPaused(); if (!coreDisputeIDToActive[_coreDisputeID].dispute) revert DisputeUnknownInThisDisputeKit(); diff --git a/contracts/src/test/MaliciousArbitrableMock.sol b/contracts/src/test/MaliciousArbitrableMock.sol new file mode 100644 index 000000000..6d0935c2f --- /dev/null +++ b/contracts/src/test/MaliciousArbitrableMock.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.24; + +import {IArbitratorV2, IDisputeTemplateRegistry, IERC20, ArbitrableExample} from "../arbitration/arbitrables/ArbitrableExample.sol"; + +/// @title MaliciousArbitrableMock +/// A mock contract to check intentional rule() revert. +contract MaliciousArbitrableMock is ArbitrableExample { + bool public doRevert; + + function changeBehaviour(bool _doRevert) external { + doRevert = _doRevert; + } + + constructor( + IArbitratorV2 _arbitrator, + string memory _templateData, + string memory _templateDataMappings, + bytes memory _arbitratorExtraData, + IDisputeTemplateRegistry _templateRegistry, + IERC20 _weth + ) + ArbitrableExample( + _arbitrator, + _templateData, + _templateDataMappings, + _arbitratorExtraData, + _templateRegistry, + _weth + ) + { + doRevert = true; + } + + function rule(uint256 _arbitratorDisputeID, uint256 _ruling) external override { + if (doRevert) revert RuleReverted(); + + uint256 localDisputeID = externalIDtoLocalID[_arbitratorDisputeID]; + DisputeStruct storage dispute = disputes[localDisputeID]; + if (msg.sender != address(arbitrator)) revert ArbitratorOnly(); + if (_ruling > dispute.numberOfRulingOptions) revert RulingOutOfBounds(); + if (dispute.isRuled) revert DisputeAlreadyRuled(); + + dispute.isRuled = true; + dispute.ruling = _ruling; + + emit Ruling(IArbitratorV2(msg.sender), _arbitratorDisputeID, dispute.ruling); + } + + error RuleReverted(); +} diff --git a/contracts/test/foundry/KlerosCore_Execution.t.sol b/contracts/test/foundry/KlerosCore_Execution.t.sol index dbeebbeb1..e5cc900df 100644 --- a/contracts/test/foundry/KlerosCore_Execution.t.sol +++ b/contracts/test/foundry/KlerosCore_Execution.t.sol @@ -9,6 +9,7 @@ import {IArbitratorV2, IArbitrableV2} from "../../src/arbitration/KlerosCore.sol import {IERC20} from "../../src/libraries/SafeERC20.sol"; import "../../src/libraries/Constants.sol"; import {console} from "forge-std/console.sol"; +import {MaliciousArbitrableMock} from "../../src/test/MaliciousArbitrableMock.sol"; /// @title KlerosCore_ExecutionTest /// @dev Tests for KlerosCore execution, rewards, and ruling finalization @@ -644,6 +645,60 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { assertEq(ruled, true, "Should be ruled"); } + function test_executeRuling_arbitrableRevert() public { + MaliciousArbitrableMock maliciousArbitrable = new MaliciousArbitrableMock( + core, + templateData, + templateDataMappings, + arbitratorExtraData, + registry, + feeToken + ); + uint256 disputeID = 0; + + vm.prank(staker1); + core.setStake(GENERAL_COURT, 20000); + vm.prank(disputer); + maliciousArbitrable.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, 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.warp(block.timestamp + timesPerPeriod[3]); + core.passPeriod(disputeID); // Execution + + core.executeRuling(disputeID); + + (, , , bool ruled, ) = core.disputes(disputeID); + assertEq(ruled, true, "Should be ruled"); + + (bool isRuled, , ) = maliciousArbitrable.disputes(disputeID); + assertEq(isRuled, false, "Should be false"); + + vm.expectRevert(KlerosCore.RulingAlreadyExecuted.selector); + core.executeRuling(disputeID); + + maliciousArbitrable.changeBehaviour(false); + + // If the first revert was accidental arbitrable will be locked out. + vm.expectRevert(KlerosCore.RulingAlreadyExecuted.selector); + core.executeRuling(disputeID); + } + function test_executeRuling_appealSwitch() public { // Check that the ruling switches if only one side was funded uint256 disputeID = 0; @@ -719,12 +774,13 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { 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(DisputeKitClassicBase.DisputeNotResolved.selector); disputeKit.withdrawFeesAndRewards(disputeID, payable(staker1), 1); - core.executeRuling(disputeID); + core.passPeriod(disputeID); // Execution + // executeRuling() should be irrelevant for withdrawals in case malicious arbitrable reverts rule() + //core.executeRuling(disputeID); vm.prank(owner); core.pause(); From 5799a3bacae8c202acc1d53d116fd95e9310107c Mon Sep 17 00:00:00 2001 From: unknownunknown1 Date: Sun, 7 Dec 2025 06:27:52 +1000 Subject: [PATCH 4/8] fix(SortitionModule): sync totalStaked with stakedPnk --- contracts/src/arbitration/KlerosCore.sol | 1 - contracts/src/arbitration/SortitionModule.sol | 13 +++---------- .../arbitration/interfaces/ISortitionModule.sol | 5 ----- .../university/SortitionModuleUniversity.sol | 5 ----- contracts/test/foundry/KlerosCore_Execution.t.sol | 15 +++++++++++++-- 5 files changed, 16 insertions(+), 23 deletions(-) diff --git a/contracts/src/arbitration/KlerosCore.sol b/contracts/src/arbitration/KlerosCore.sol index b470fc102..5519c4e70 100644 --- a/contracts/src/arbitration/KlerosCore.sol +++ b/contracts/src/arbitration/KlerosCore.sol @@ -1370,7 +1370,6 @@ contract KlerosCore is IArbitratorV2, Initializable, UUPSProxiable { } } sortitionModule.setStake(_account, _courtID, pnkDeposit, pnkWithdrawal, _newStake); - sortitionModule.updateTotalStake(pnkDeposit, pnkWithdrawal); return true; } diff --git a/contracts/src/arbitration/SortitionModule.sol b/contracts/src/arbitration/SortitionModule.sol index d253414ad..d0f2d09f3 100644 --- a/contracts/src/arbitration/SortitionModule.sol +++ b/contracts/src/arbitration/SortitionModule.sol @@ -322,16 +322,6 @@ contract SortitionModule is ISortitionModule, Initializable, UUPSProxiable { return (pnkDeposit, pnkWithdrawal, StakingResult.Successful); } - /// @inheritdoc ISortitionModule - function updateTotalStake(uint256 _pnkDeposit, uint256 _pnkWithdrawal) external override onlyByCore { - // Note that we don't update totalStake in setStake() function because it doesn't always change total (e.g. during rewards/penalties). - if (_pnkDeposit > 0) { - totalStaked += _pnkDeposit; - } else { - totalStaked -= _pnkWithdrawal; - } - } - /// @inheritdoc ISortitionModule function setStake( address _account, @@ -399,8 +389,10 @@ contract SortitionModule is ISortitionModule, Initializable, UUPSProxiable { } // Increase juror's balance by deposited amount. juror.stakedPnk += _pnkDeposit; + totalStaked += _pnkDeposit; } else { juror.stakedPnk -= _pnkWithdrawal; + totalStaked -= _pnkWithdrawal; if (_newStake == 0) { // Cleanup for (uint256 i = juror.courtIDs.length; i > 0; i--) { @@ -468,6 +460,7 @@ contract SortitionModule is ISortitionModule, Initializable, UUPSProxiable { uint256 amount = getJurorLeftoverPNK(_account); if (amount == 0) revert NotEligibleForWithdrawal(); jurors[_account].stakedPnk = 0; + totalStaked -= amount; core.transferBySortitionModule(_account, amount); emit LeftoverPNKWithdrawn(_account, amount); } diff --git a/contracts/src/arbitration/interfaces/ISortitionModule.sol b/contracts/src/arbitration/interfaces/ISortitionModule.sol index 6a84089db..ccc66a2f5 100644 --- a/contracts/src/arbitration/interfaces/ISortitionModule.sol +++ b/contracts/src/arbitration/interfaces/ISortitionModule.sol @@ -57,11 +57,6 @@ interface ISortitionModule { bool _noDelay ) external returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult); - /// @notice Updates the total amount staked in all courts. - /// @param _pnkDeposit The amount of PNK that increases total stake. - /// @param _pnkWithdrawal The amount of PNK that decreases total stake. - function updateTotalStake(uint256 _pnkDeposit, uint256 _pnkWithdrawal) external; - /// @notice Update the state of the stakes, called by KC at the end of setStake flow. /// /// @dev `O(n + p * log_k(j))` where diff --git a/contracts/src/arbitration/university/SortitionModuleUniversity.sol b/contracts/src/arbitration/university/SortitionModuleUniversity.sol index 5ce7f9e00..c24e8811c 100644 --- a/contracts/src/arbitration/university/SortitionModuleUniversity.sol +++ b/contracts/src/arbitration/university/SortitionModuleUniversity.sol @@ -130,11 +130,6 @@ contract SortitionModuleUniversity is ISortitionModuleUniversity, UUPSProxiable, // NOP } - /// @inheritdoc ISortitionModule - function updateTotalStake(uint256 _pnkDeposit, uint256 _pnkWithdrawal) external override onlyByCore { - // NOP - } - /// @inheritdoc ISortitionModule function createDisputeHook(uint256 /*_disputeID*/, uint256 /*_roundID*/) external override onlyByCore { disputesWithoutJurors++; diff --git a/contracts/test/foundry/KlerosCore_Execution.t.sol b/contracts/test/foundry/KlerosCore_Execution.t.sol index e5cc900df..4748d5f6a 100644 --- a/contracts/test/foundry/KlerosCore_Execution.t.sol +++ b/contracts/test/foundry/KlerosCore_Execution.t.sol @@ -98,6 +98,9 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { "Wrong penalty coherence 2 vote ID" ); + assertEq(pinakion.balanceOf(address(core)), 22000, "Wrong token balance of the core"); + assertEq(sortitionModule.totalStaked(), 22000, "Total staked should be equal to the balance in this test"); + vm.expectEmit(true, true, true, true); emit SortitionModule.StakeLocked(staker1, 1000, true); vm.expectEmit(true, true, true, true); @@ -143,9 +146,11 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { 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)), 22000, "Wrong token balance of the core"); // Was 21500. 1000 was transferred to staker2 + assertEq(pinakion.balanceOf(address(core)), 22000, "Token balance of the core shouldn't change after rewards"); + assertEq(sortitionModule.totalStaked(), 22000, "Total staked shouldn't change after rewards"); + assertEq(pinakion.balanceOf(staker1), 999999999999998000, "Wrong token balance of staker1"); - assertEq(pinakion.balanceOf(staker2), 999999999999980000, "Wrong token balance of staker2"); // 20k stake and 1k added as a reward, thus -19k from the default + assertEq(pinakion.balanceOf(staker2), 999999999999980000, "Wrong token balance of staker2"); } function test_execute_NoCoherence() public { @@ -479,6 +484,9 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { vm.prank(owner); core.transferBySortitionModule(staker1, 1000); + assertEq(pinakion.balanceOf(address(core)), 1000, "Wrong token balance of the core"); + assertEq(sortitionModule.totalStaked(), 1000, "Wrong totalStaked before withdrawal"); + vm.expectEmit(true, true, true, true); emit SortitionModule.LeftoverPNKWithdrawn(staker1, 1000); sortitionModule.withdrawLeftoverPNK(staker1); @@ -486,6 +494,9 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { (totalStaked, , , ) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); assertEq(totalStaked, 0, "Should be unstaked fully"); + assertEq(pinakion.balanceOf(address(core)), 0, "Wrong token balance of the core"); + assertEq(sortitionModule.totalStaked(), 0, "Wrong totalStaked after withdrawal"); + // Check that everything is withdrawn now assertEq(pinakion.balanceOf(address(core)), 0, "Core balance should be empty"); assertEq(pinakion.balanceOf(staker1), 1 ether, "All PNK should be withdrawn"); From cb492fd0e04fdfa1ec5c374be034bf1d57e1ceaa Mon Sep 17 00:00:00 2001 From: unknownunknown1 Date: Mon, 8 Dec 2025 18:13:45 +1000 Subject: [PATCH 5/8] fix(KC): check safeTransfer for withdrawLeftover --- contracts/src/arbitration/KlerosCore.sol | 2 +- contracts/test/foundry/KlerosCore_Execution.t.sol | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/contracts/src/arbitration/KlerosCore.sol b/contracts/src/arbitration/KlerosCore.sol index 5519c4e70..e545c762d 100644 --- a/contracts/src/arbitration/KlerosCore.sol +++ b/contracts/src/arbitration/KlerosCore.sol @@ -632,7 +632,7 @@ contract KlerosCore is IArbitratorV2, Initializable, UUPSProxiable { function transferBySortitionModule(address _account, uint256 _amount) external { if (msg.sender != address(sortitionModule)) revert SortitionModuleOnly(); // Note eligibility is checked in SortitionModule. - pinakion.safeTransfer(_account, _amount); + if (!pinakion.safeTransfer(_account, _amount)) revert TransferFailed(); } /// @inheritdoc IArbitratorV2 diff --git a/contracts/test/foundry/KlerosCore_Execution.t.sol b/contracts/test/foundry/KlerosCore_Execution.t.sol index 4748d5f6a..73f35fcc5 100644 --- a/contracts/test/foundry/KlerosCore_Execution.t.sol +++ b/contracts/test/foundry/KlerosCore_Execution.t.sol @@ -487,6 +487,15 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { assertEq(pinakion.balanceOf(address(core)), 1000, "Wrong token balance of the core"); assertEq(sortitionModule.totalStaked(), 1000, "Wrong totalStaked before withdrawal"); + vm.prank(address(core)); + pinakion.transfer(staker2, 1000); // Manually send the balance to trigger the revert + + vm.expectRevert(KlerosCore.TransferFailed.selector); + sortitionModule.withdrawLeftoverPNK(staker1); + + vm.prank(staker2); + pinakion.transfer(address(core), 1000); // Transfer the tokens back to execute withdrawal + vm.expectEmit(true, true, true, true); emit SortitionModule.LeftoverPNKWithdrawn(staker1, 1000); sortitionModule.withdrawLeftoverPNK(staker1); From cae435e505353b20281e5cb8a185e2044de3884d Mon Sep 17 00:00:00 2001 From: unknownunknown1 Date: Mon, 8 Dec 2025 18:27:35 +1000 Subject: [PATCH 6/8] fix(KC): remove try catch --- contracts/src/arbitration/KlerosCore.sol | 9 +--- .../test/foundry/KlerosCore_Execution.t.sol | 54 ------------------- 2 files changed, 1 insertion(+), 62 deletions(-) diff --git a/contracts/src/arbitration/KlerosCore.sol b/contracts/src/arbitration/KlerosCore.sol index e545c762d..1b7514105 100644 --- a/contracts/src/arbitration/KlerosCore.sol +++ b/contracts/src/arbitration/KlerosCore.sol @@ -139,11 +139,6 @@ contract KlerosCore is IArbitratorV2, Initializable, UUPSProxiable { /// @param _voteID ID of the vote given to the drawn juror. event Draw(address indexed _address, uint256 indexed _disputeID, uint256 _roundID, uint256 _voteID); - /// @notice Emitted when the dispute is successfully appealed. - /// @param _disputeID ID of the related dispute. - /// @param _arbitrable The arbitrable contract. - event ArbitrableReverted(uint256 indexed _disputeID, IArbitrableV2 indexed _arbitrable); - /// @notice Emitted when a new court is created. /// @param _courtID ID of the new court. /// @param _parent ID of the parent court. @@ -1073,9 +1068,7 @@ contract KlerosCore is IArbitratorV2, Initializable, UUPSProxiable { (uint256 winningChoice, , ) = currentRuling(_disputeID); dispute.ruled = true; emit Ruling(dispute.arbitrated, _disputeID, winningChoice); - try dispute.arbitrated.rule(_disputeID, winningChoice) {} catch { - emit ArbitrableReverted(_disputeID, dispute.arbitrated); - } + dispute.arbitrated.rule(_disputeID, winningChoice); } // ************************************* // diff --git a/contracts/test/foundry/KlerosCore_Execution.t.sol b/contracts/test/foundry/KlerosCore_Execution.t.sol index 73f35fcc5..9d4dfdd18 100644 --- a/contracts/test/foundry/KlerosCore_Execution.t.sol +++ b/contracts/test/foundry/KlerosCore_Execution.t.sol @@ -665,60 +665,6 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { assertEq(ruled, true, "Should be ruled"); } - function test_executeRuling_arbitrableRevert() public { - MaliciousArbitrableMock maliciousArbitrable = new MaliciousArbitrableMock( - core, - templateData, - templateDataMappings, - arbitratorExtraData, - registry, - feeToken - ); - uint256 disputeID = 0; - - vm.prank(staker1); - core.setStake(GENERAL_COURT, 20000); - vm.prank(disputer); - maliciousArbitrable.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, 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.warp(block.timestamp + timesPerPeriod[3]); - core.passPeriod(disputeID); // Execution - - core.executeRuling(disputeID); - - (, , , bool ruled, ) = core.disputes(disputeID); - assertEq(ruled, true, "Should be ruled"); - - (bool isRuled, , ) = maliciousArbitrable.disputes(disputeID); - assertEq(isRuled, false, "Should be false"); - - vm.expectRevert(KlerosCore.RulingAlreadyExecuted.selector); - core.executeRuling(disputeID); - - maliciousArbitrable.changeBehaviour(false); - - // If the first revert was accidental arbitrable will be locked out. - vm.expectRevert(KlerosCore.RulingAlreadyExecuted.selector); - core.executeRuling(disputeID); - } - function test_executeRuling_appealSwitch() public { // Check that the ruling switches if only one side was funded uint256 disputeID = 0; From 8c256aa24cdc947eb5061306b435b64536546e93 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Tue, 9 Dec 2025 18:39:52 +0000 Subject: [PATCH 7/8] test: adding back the reverting arbitrable test, small nitpicks --- .../arbitrables/ArbitrableExample.sol | 2 +- .../test/foundry/KlerosCore_Execution.t.sol | 87 ++++++++++++------- 2 files changed, 58 insertions(+), 31 deletions(-) diff --git a/contracts/src/arbitration/arbitrables/ArbitrableExample.sol b/contracts/src/arbitration/arbitrables/ArbitrableExample.sol index dc075028e..fe55ba2e7 100644 --- a/contracts/src/arbitration/arbitrables/ArbitrableExample.sol +++ b/contracts/src/arbitration/arbitrables/ArbitrableExample.sol @@ -149,7 +149,7 @@ contract ArbitrableExample is IArbitrableV2 { } /// @inheritdoc IArbitrableV2 - function rule(uint256 _arbitratorDisputeID, uint256 _ruling) external virtual { + function rule(uint256 _arbitratorDisputeID, uint256 _ruling) external virtual override { uint256 localDisputeID = externalIDtoLocalID[_arbitratorDisputeID]; DisputeStruct storage dispute = disputes[localDisputeID]; if (msg.sender != address(arbitrator)) revert ArbitratorOnly(); diff --git a/contracts/test/foundry/KlerosCore_Execution.t.sol b/contracts/test/foundry/KlerosCore_Execution.t.sol index 9d4dfdd18..5eee52ccf 100644 --- a/contracts/test/foundry/KlerosCore_Execution.t.sol +++ b/contracts/test/foundry/KlerosCore_Execution.t.sol @@ -665,6 +665,48 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { assertEq(ruled, true, "Should be ruled"); } + function test_executeRuling_arbitrableRevert() public { + MaliciousArbitrableMock maliciousArbitrable = new MaliciousArbitrableMock( + core, + templateData, + templateDataMappings, + arbitratorExtraData, + registry, + feeToken + ); + uint256 disputeID = 0; + + vm.prank(staker1); + core.setStake(GENERAL_COURT, 20000); + vm.prank(disputer); + maliciousArbitrable.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, 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.warp(block.timestamp + timesPerPeriod[3]); + core.passPeriod(disputeID); // Execution + + vm.expectRevert(MaliciousArbitrableMock.RuleReverted.selector); + core.executeRuling(disputeID); // Reverts + + disputeKit.withdrawFeesAndRewards(disputeID, payable(staker1), 2); // Should not revert even if executeRuling() reverted + } + function test_executeRuling_appealSwitch() public { // Check that the ruling switches if only one side was funded uint256 disputeID = 0; @@ -780,7 +822,7 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { // 4. move sortition to staking phase uint256 disputeID = 0; uint256 amountToStake = 20000; - _stakePnk_createDispute_moveToDrawingPhase(disputeID, staker1, amountToStake); + _stakePnk_createDispute_moveToDrawingPhase(staker1, amountToStake); KlerosCore.Round memory round = core.getRoundInfo(disputeID, 0); uint256 pnkAtStakePerJuror = round.pnkAtStakePerJuror; @@ -788,14 +830,7 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { _drawJurors_advancePeriodToVoting(disputeID); _vote_execute(disputeID, staker1); sortitionModule.passPhase(); // set it to staking phase - _assertJurorBalance( - disputeID, - staker1, - amountToStake, - pnkAtStakePerJuror * DEFAULT_NB_OF_JURORS, - amountToStake, - 1 - ); + _assertJurorBalance(staker1, amountToStake, pnkAtStakePerJuror * DEFAULT_NB_OF_JURORS, amountToStake, 1); console.log("totalStaked before: %e", sortitionModule.totalStaked()); @@ -804,14 +839,7 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { // post condition: inflated totalStaked console.log("totalStaked after: %e", sortitionModule.totalStaked()); - _assertJurorBalance( - disputeID, - staker1, - amountToStake, - pnkAtStakePerJuror * DEFAULT_NB_OF_JURORS, - amountToStake, - 1 - ); + _assertJurorBalance(staker1, amountToStake, pnkAtStakePerJuror * DEFAULT_NB_OF_JURORS, amountToStake, 1); // new juror tries to stake but totalStaked already reached type(uint256).max // it reverts with "arithmetic underflow or overflow (0x11)" @@ -920,19 +948,18 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { ///////// Internal ////////// function _assertJurorBalance( - uint256 disputeID, - address juror, - uint256 totalStakedPnk, - uint256 totalLocked, - uint256 stakedInCourt, - uint256 nbCourts - ) internal { + address _juror, + uint256 _totalStakedPnk, + uint256 _totalLocked, + uint256 _stakedInCourt, + uint256 _nbCourts + ) internal view { (uint256 totalStakedPnk, uint256 totalLocked, uint256 stakedInCourt, uint256 nbCourts) = sortitionModule - .getJurorBalance(juror, GENERAL_COURT); - assertEq(totalStakedPnk, totalStakedPnk, "Wrong totalStakedPnk"); // jurors total staked a.k.a juror.stakedPnk - assertEq(totalLocked, totalLocked, "Wrong totalLocked"); - assertEq(stakedInCourt, stakedInCourt, "Wrong stakedInCourt"); // juror staked in court a.k.a _stakeOf - assertEq(nbCourts, nbCourts, "Wrong nbCourts"); + .getJurorBalance(_juror, GENERAL_COURT); + assertEq(_totalStakedPnk, totalStakedPnk, "Wrong totalStakedPnk"); // jurors total staked a.k.a juror.stakedPnk + assertEq(_totalLocked, totalLocked, "Wrong totalLocked"); + assertEq(_stakedInCourt, stakedInCourt, "Wrong stakedInCourt"); // juror staked in court a.k.a _stakeOf + assertEq(_nbCourts, nbCourts, "Wrong nbCourts"); } function _stakeBalanceForJuror(address juror, uint256 amount) internal { @@ -941,7 +968,7 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { core.setStake(GENERAL_COURT, amount); } - function _stakePnk_createDispute_moveToDrawingPhase(uint256 disputeID, address juror, uint256 amount) internal { + function _stakePnk_createDispute_moveToDrawingPhase(address juror, uint256 amount) internal { vm.prank(juror); core.setStake(GENERAL_COURT, amount); vm.prank(disputer); From a0167407cc7371bd6e7c06b61de69d96e7ec1fe0 Mon Sep 17 00:00:00 2001 From: unknownunknown1 Date: Sat, 13 Dec 2025 06:07:39 +1000 Subject: [PATCH 8/8] feat(KC): add event for failed transfer --- contracts/src/libraries/SafeERC20.sol | 10 +++- .../test/foundry/KlerosCore_Execution.t.sol | 57 ++++++++++++++++++- 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/contracts/src/libraries/SafeERC20.sol b/contracts/src/libraries/SafeERC20.sol index 724ec4f5b..2db6469f5 100644 --- a/contracts/src/libraries/SafeERC20.sol +++ b/contracts/src/libraries/SafeERC20.sol @@ -15,6 +15,12 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract, /// which allows you to call the safe operations as `token.safeTransfer(...)`, etc. library SafeERC20 { + /// @notice Emits when safeTransfer fails. + /// @param _token Token to transfer. + /// @param _to Recipient address. + /// @param _value Amount transferred. + event SafeTransferFailed(IERC20 _token, address _to, uint256 _value); + /// @notice Increases the allowance granted to `spender` by the caller. /// @param _token Token to transfer. /// @param _spender The address which will spend the funds. @@ -31,7 +37,9 @@ library SafeERC20 { /// @return Whether transfer succeeded or not. function safeTransfer(IERC20 _token, address _to, uint256 _value) internal returns (bool) { (bool success, bytes memory data) = address(_token).call(abi.encodeCall(IERC20.transfer, (_to, _value))); - return (success && (data.length == 0 || abi.decode(data, (bool)))); + bool ok = success && (data.length == 0 || abi.decode(data, (bool))); + if (!ok) emit SafeTransferFailed(_token, _to, _value); + return ok; } /// @notice Calls transferFrom() without reverting. diff --git a/contracts/test/foundry/KlerosCore_Execution.t.sol b/contracts/test/foundry/KlerosCore_Execution.t.sol index 5eee52ccf..a7607c8ed 100644 --- a/contracts/test/foundry/KlerosCore_Execution.t.sol +++ b/contracts/test/foundry/KlerosCore_Execution.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, SafeERC20} from "../../src/arbitration/KlerosCore.sol"; import {SortitionModule} from "../../src/arbitration/SortitionModule.sol"; import {DisputeKitClassicBase} from "../../src/arbitration/dispute-kits/DisputeKitClassicBase.sol"; import {IArbitratorV2, IArbitrableV2} from "../../src/arbitration/KlerosCore.sol"; @@ -543,7 +543,7 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { 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 + disputeKit.castVote(disputeID, voteIDs, 1, 0, "XYZ"); core.passPeriod(disputeID); // Appeal @@ -563,6 +563,59 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { assertEq(feeToken.balanceOf(disputer), 0.82 ether, "Wrong fee token balance of disputer"); } + function test_execute_feeToken_failedTransfer() public { + uint256 disputeID = 0; + + feeToken.transfer(disputer, 1 ether); + vm.prank(disputer); + feeToken.approve(address(arbitrable), 1 ether); + + vm.prank(owner); + core.changeAcceptedFeeTokens(feeToken, true); + vm.prank(owner); + 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.warp(block.timestamp + rngLookahead); + 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"); + + core.passPeriod(disputeID); // Appeal + + vm.warp(block.timestamp + timesPerPeriod[3]); + core.passPeriod(disputeID); // Execution + + vm.prank(address(core)); + feeToken.transfer(disputer, 0.18 ether); // Manually send all balance to make rewards fail + assertEq(feeToken.balanceOf(staker1), 0, "Wrong fee token balance of staker1"); + assertEq(feeToken.balanceOf(disputer), 1 ether, "Wrong fee token balance of disputer"); + + vm.expectEmit(true, true, true, true); + emit SafeERC20.SafeTransferFailed(feeToken, staker1, 0.06 ether); // One failed iteration has 0.06 eth + core.execute(disputeID, 0, 6); + + KlerosCore.Round memory round = core.getRoundInfo(disputeID, 0); + assertEq(round.repartitions, 6, "Wrong repartitions"); + assertEq(feeToken.balanceOf(staker1), 0, "Staker1 still has no balance"); + } + function test_execute_NoCoherence_feeToken() public { uint256 disputeID = 0;