Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion contracts/src/arbitration/KlerosCore.sol
Original file line number Diff line number Diff line change
Expand Up @@ -627,7 +627,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
Expand Down
5 changes: 3 additions & 2 deletions contracts/src/arbitration/SortitionModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -311,15 +311,13 @@ 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;
if (pnkWithdrawal > possibleWithdrawal) {
// Ensure locked tokens remain in the contract. They can only be released during Execution.
pnkWithdrawal = possibleWithdrawal;
}
totalStaked -= pnkWithdrawal;
}
return (pnkDeposit, pnkWithdrawal, StakingResult.Successful);
}
Expand Down Expand Up @@ -391,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--) {
Expand Down Expand Up @@ -460,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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
52 changes: 52 additions & 0 deletions contracts/src/test/MaliciousArbitrableMock.sol
Original file line number Diff line number Diff line change
@@ -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();
}
134 changes: 130 additions & 4 deletions contracts/test/foundry/KlerosCore_Execution.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ 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";
import {MaliciousArbitrableMock} from "../../src/test/MaliciousArbitrableMock.sol";

/// @title KlerosCore_ExecutionTest
/// @dev Tests for KlerosCore execution, rewards, and ruling finalization
Expand Down Expand Up @@ -96,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);
Expand Down Expand Up @@ -141,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 {
Expand Down Expand Up @@ -477,13 +484,28 @@ 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.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);

(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");
Expand Down Expand Up @@ -718,12 +740,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();
Expand All @@ -749,6 +772,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);
}
Comment on lines +775 to +819
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Incomplete test: Missing assertions or revert expectation.

The test comment on line 795 states that staker2's stake attempt "reverts with arithmetic underflow or overflow," but line 796 calls _stakeBalanceForJuror(staker2, 20000) without a preceding vm.expectRevert() call and without any subsequent assertions. This leaves the test's behavior and purpose unclear.

Additionally, the test relies on the _assertJurorBalance helper which currently has a critical bug (see separate comment), so the existing assertions at lines 769-776 and 785-792 are not functioning correctly.

Please clarify the test's intent and add appropriate assertions:

If the stake should revert:

     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)"
+    vm.expectRevert(); // or specify the exact error
     _stakeBalanceForJuror(staker2, 20000);

If the stake should succeed:

     // new juror tries to stake but totalStaked already reached type(uint256).max
-    // it reverts with "arithmetic underflow or overflow (0x11)"
     _stakeBalanceForJuror(staker2, 20000);
+    // Add assertions about staker2's balance and totalStaked
+    assertEq(sortitionModule.totalStaked(), expectedValue, "totalStaked should be...");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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 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
vm.expectRevert();
_stakeBalanceForJuror(staker2, 20000);
}
Suggested change
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 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
);
_stakeBalanceForJuror(staker2, 20000);
// Add assertions about staker2's balance and totalStaked
assertEq(sortitionModule.totalStaked(), expectedValue, "totalStaked should be...");
}


function testFuzz_executeIterations(uint256 iterations) public {
uint256 disputeID = 0;
uint256 roundID = 0;
Expand Down Expand Up @@ -847,4 +916,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");
}
Comment on lines +922 to +936
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Variable shadowing makes all assertions useless.

The tuple destructuring on lines 908-909 redeclares variables with the same names as the function parameters, causing variable shadowing. This means all assertions on lines 910-913 compare values against themselves and will always pass, regardless of the actual vs. expected values. The expected values passed as parameters are never used.

Apply this diff to fix the variable shadowing:

 function _assertJurorBalance(
     uint256 disputeID,
     address juror,
-    uint256 totalStakedPnk,
-    uint256 totalLocked,
-    uint256 stakedInCourt,
-    uint256 nbCourts
+    uint256 expectedTotalStakedPnk,
+    uint256 expectedTotalLocked,
+    uint256 expectedStakedInCourt,
+    uint256 expectedNbCourts
 ) 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");
+    assertEq(totalStakedPnk, expectedTotalStakedPnk, "Wrong totalStakedPnk");
+    assertEq(totalLocked, expectedTotalLocked, "Wrong totalLocked");
+    assertEq(stakedInCourt, expectedStakedInCourt, "Wrong stakedInCourt");
+    assertEq(nbCourts, expectedNbCourts, "Wrong nbCourts");
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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 _assertJurorBalance(
uint256 disputeID,
address juror,
uint256 expectedTotalStakedPnk,
uint256 expectedTotalLocked,
uint256 expectedStakedInCourt,
uint256 expectedNbCourts
) internal {
(uint256 totalStakedPnk, uint256 totalLocked, uint256 stakedInCourt, uint256 nbCourts) = sortitionModule
.getJurorBalance(juror, GENERAL_COURT);
assertEq(totalStakedPnk, expectedTotalStakedPnk, "Wrong totalStakedPnk"); // jurors total staked a.k.a juror.stakedPnk
assertEq(totalLocked, expectedTotalLocked, "Wrong totalLocked");
assertEq(stakedInCourt, expectedStakedInCourt, "Wrong stakedInCourt"); // juror staked in court a.k.a _stakeOf
assertEq(nbCourts, expectedNbCourts, "Wrong nbCourts");
}
🤖 Prompt for AI Agents
In contracts/test/foundry/KlerosCore_Execution.t.sol around lines 900-914, the
tuple destructuring currently redeclares variables with the same names as the
function parameters causing shadowing and useless self-comparisons; change the
destructuring to capture the returned values into distinct "actual" variables
(e.g. actualTotalStakedPnk, actualTotalLocked, actualStakedInCourt,
actualNbCourts) or remove the types to assign into the existing parameter names,
then compare expected parameters to those actual variables in the assertEq calls
(assertEq(actual..., expected..., "...")). Ensure you do not redeclare names so
assertions validate the expected vs actual values.


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
}
}
26 changes: 26 additions & 0 deletions contracts/test/foundry/KlerosCore_Staking.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Comment on lines +144 to +168
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, search for totalStaked references in the test file
rg -n "totalStaked" contracts/test/foundry/KlerosCore_Staking.t.sol -B2 -A2

Repository: kleros/kleros-v2

Length of output: 7231


🏁 Script executed:

#!/bin/bash
# Search for tests that combine delayed stakes with totalStaked checks
rg -n "delayedStake.*totalStaked|totalStaked.*delayedStake" contracts/test/foundry/ -i

Repository: kleros/kleros-v2

Length of output: 42


🏁 Script executed:

#!/bin/bash
# Search for all test functions that mention delayed stake
rg -n "function test.*[Dd]elayed" contracts/test/foundry/KlerosCore_Staking.t.sol

Repository: kleros/kleros-v2

Length of output: 182


🏁 Script executed:

#!/bin/bash
# Check the entire context around delayed stake tests to see if totalStaked is tested
rg -n "test_delayedStake" contracts/test/foundry/KlerosCore_Staking.t.sol -A 50 | head -100

Repository: kleros/kleros-v2

Length of output: 42


🏁 Script executed:

#!/bin/bash
# Read the test_executeDelayedStakes function
sed -n '324,450p' contracts/test/foundry/KlerosCore_Staking.t.sol

Repository: kleros/kleros-v2

Length of output: 6960


🏁 Script executed:

#!/bin/bash
# Find the SortitionModule contract file
fd -t f "SortitionModule\.sol" contracts/

Repository: kleros/kleros-v2

Length of output: 163


🏁 Script executed:

#!/bin/bash
# Search for the totalStaked function/variable definition
rg -n "totalStaked" contracts/src/ -B2 -A5 | head -100

Repository: kleros/kleros-v2

Length of output: 7695


🏁 Script executed:

#!/bin/bash
# Search for executeDelayedStakes implementation
rg -n "function executeDelayedStakes" contracts/src/ -A 30

Repository: kleros/kleros-v2

Length of output: 9796


🏁 Script executed:

#!/bin/bash
# Search for setStakeBySortitionModule in KlerosCore
rg -n "function setStakeBySortitionModule" contracts/src/ -A 15

Repository: kleros/kleros-v2

Length of output: 3466


🏁 Script executed:

#!/bin/bash
# Confirm _setStake calls the totalStaked update
rg -n "function _setStake" contracts/src/arbitration/KlerosCore.sol -A 40 | head -60

Repository: kleros/kleros-v2

Length of output: 2059


Test coverage for delayed stakes affecting totalStaked should be added.

The verification confirms the review comment's concern is valid. While the current test_setStake_totalStaked() correctly validates immediate stakes, there is a test coverage gap:

  • Delayed stakes do NOT immediately update sortitionModule.totalStaked() (they return early in _setStake() at line 1351)
  • When executeDelayedStakes() is called during the Staking phase, it invokes setStakeBySortitionModule() which calls _setStake() with _noDelay=true, which then calls updateTotalStake() to update the global total
  • The existing test_executeDelayedStakes() validates delayed stake execution but never checks that sortitionModule.totalStaked() is correctly updated after execution

A test case should verify that executing delayed stakes properly increments the global totalStaked metric.

🤖 Prompt for AI Agents
In contracts/test/foundry/KlerosCore_Staking.t.sol around lines 144 to 168, add
a test that verifies delayed stakes do not immediately affect
sortitionModule.totalStaked and that calling executeDelayedStakes during the
Staking phase updates totalStaked appropriately: create delayed stakes (use
callers that will trigger the delayed path), assert
sortitionModule.totalStaked() remains unchanged, advance the contract to the
Staking phase (or ensure executeDelayedStakes is callable), call
core.executeDelayedStakes(), then assert sortitionModule.totalStaked() equals
the expected new total after delayed stakes are applied.


function test_setStake_maxStakePathCheck() public {
uint256[] memory supportedDK = new uint256[](1);
supportedDK[0] = DISPUTE_KIT_CLASSIC;
Expand Down
Loading