-
Notifications
You must be signed in to change notification settings - Fork 49
fix(SortitionModule): fix staking logic and remove instant staking #2004
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Conversation
WalkthroughThis update refactors staking and penalty logic across arbitration contracts by removing the deprecated Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant KlerosCore
participant SortitionModule
User->>KlerosCore: setStakeBySortitionModule(account, courtID, newStake)
KlerosCore->>SortitionModule: validateStake(account, courtID, newStake)
SortitionModule-->>KlerosCore: (pnkDeposit, pnkWithdrawal, actualNewStake, StakingResult)
alt StakingResult == Delayed
KlerosCore-->>User: return early (true)
else
KlerosCore->>SortitionModule: setStake(account, courtID, actualNewStake)
end
sequenceDiagram
participant KlerosCore
participant SortitionModule
KlerosCore->>SortitionModule: penalizeStake(account, courtID, relativeAmount)
SortitionModule-->>KlerosCore: (pnkBalance, availablePenalty)
KlerosCore->>KlerosCore: update penalty accounting, set juror inactive if needed
sequenceDiagram
participant User
participant SortitionModule
User->>SortitionModule: withdrawLeftoverPNK(account)
SortitionModule-->>User: Transfer leftover PNK, emit event
Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🔭 Outside diff range comments (1)
contracts/src/arbitration/university/SortitionModuleUniversity.sol (1)
238-248
:⚠️ Potential issueFunction signature updated but missing return values
While the function signature has been updated to match the interface, the function doesn't actually set or return the declared values
pnkBalance
andavailablePenalty
. This needs to be fixed to properly implement the interface.Update the function to return the required values:
function penalizeStake( address _account, uint256 _relativeAmount ) external override onlyByCore returns (uint256 pnkBalance, uint256 availablePenalty) { Juror storage juror = jurors[_account]; if (juror.stakedPnk >= _relativeAmount) { juror.stakedPnk -= _relativeAmount; + availablePenalty = _relativeAmount; } else { + availablePenalty = juror.stakedPnk; juror.stakedPnk = 0; // stakedPnk might become lower after manual unstaking, but lockedPnk will always cover the difference. } + pnkBalance = juror.stakedPnk; + return (pnkBalance, availablePenalty); }
🧹 Nitpick comments (12)
contracts/src/arbitration/university/SortitionModuleUniversity.sol (1)
264-265
: Empty implementation of withdrawLeftoverPNKThis is an empty implementation of the required interface function. While technically compliant with the interface, this implementation doesn't actually withdraw any leftover PNK. If this is intentional for the University module, consider adding a comment explaining why.
Consider adding a clarifying comment:
-function withdrawLeftoverPNK(address _account) external override {} +function withdrawLeftoverPNK(address _account) external override { + // No implementation needed for University module as it doesn't track leftover PNK +}contracts/src/arbitration/KlerosCoreBase.sol (2)
474-482
: Deprecate the unused_alreadyTransferred
parameter to save deployment & call gas
setStakeBySortitionModule
keeps the parameter in the ABI but never uses it (/* _alreadyTransferred */
).
If no externally-deployed SortitionModule still relies on the 4-argument signature, consider removing the parameter entirely (and adjusting the interface) or adding a dedicated deprecated overload to keep backwards compatibility. Eliminating the dead parameter shaves 3-5 gas per call and simplifies the API.
1083-1099
: Follow-up: remove deprecated boolean in call tosetStake
sortitionModule.setStake(..., false)
passes a hard-coded value that is now unused.
After cleaning up the interface (see comment above), this fourth argument and the trailing comment can be dropped to avoid confusion:- (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) = sortitionModule.setStake( - _account, - _courtID, - _newStake, - false // Unused parameter. - ); + (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) = + sortitionModule.setStake(_account, _courtID, _newStake);contracts/test/foundry/KlerosCore.t.sol (5)
1019-1026
: Clarify assertion description to match the expected flag value
alreadyTransferred
is asserted to befalse
, yet the accompanying message says “Should be flagged as transferred”.
This wording is confusing because a value offalse
actually means “not transferred”. Renaming the
message to something like “Should be not flagged as transferred” (or the opposite, depending on the intended
semantics) will prevent mis-interpretation when the test fails.
1094-1119
: Replace magic numbers with expressions for better readability & resilienceThe balance assertions use large hard-coded literals such as
999999999999998200
.
Although they are numerically correct (1 ether - 1800
), the intent is obscured and any future change
to the staked amount will require hunting down and editing multiple constants.Consider computing the expected value on-the-fly, e.g.
uint256 expected = 1 ether - 1_800; assertEq(pinakion.balanceOf(staker1), expected, "Wrong token balance of staker1");or even
assertEq( pinakion.balanceOf(staker1), 1 ether - (1500 /* first stake */ + 300 /* diff */), "Wrong token balance of staker1" );This keeps the arithmetic obvious, avoids copy-paste mistakes, and makes the test less brittle.
1203-1210
: Impersonating the Core to self-transfer can hide real-world constraints
vm.prank(address(core)); pinakion.transfer(governor, 10000);
works in the
unit test, but in productionKlerosCore
has no such method and cannot trigger ERC-20transfer
directly. If the goal is only to zero the Core’s balance, consider minting a fresh token or using
deal(address(pinakion), address(core), 0)
(forge cheat-code) instead.This keeps the test closer to real behaviour and avoids accidentally relying on “the contract calls
transfer on itself” paths that will never exist on-chain.
2454-2510
: Hard-coded iteration counts & stake figures make new tests brittle
core.execute(disputeID, 0, 6)
assumes that exactly six repartition iterations are always required.
The actual number depends on internal constants such asDEFAULT_NB_OF_JURORS
, future changes to
penalty logic, or even gas-cap tweaks. The same applies to the literal3000
penalty math and the
final balance check for a full1 ether
.Suggestion:
uint256 iterations = DEFAULT_NB_OF_JURORS * 2; // worst-case upper bound core.execute(disputeID, 0, iterations);and compute expected balances from recorded pre-/post-state instead of fixed literals.
Doing so will prevent spurious failures when business rules evolve while preserving
the intent of proving “juror becomes insolvent and is automatically unstaked”.
2532-2574
: Leverage helper functions to minimise duplicated balance assertions
withdrawLeftoverPNK
is tested with a long sequence ofassertEq
calls that
repeat the “balance before / after” pattern already present in several other stake-related tests.
Extracting a small internal helper:function _assertPnkBalances( uint256 coreBal, uint256 jurorBal, string memory msgSuffix ) internal { assertEq(pinakion.balanceOf(address(core)), coreBal, string.concat("Core ", msgSuffix)); assertEq(pinakion.balanceOf(staker1), jurorBal, string.concat("Juror ", msgSuffix)); }and re-using it across the suite will reduce noise, ease future refactors, and make the intent of
each check clearer.contracts/src/arbitration/SortitionModuleBase.sol (4)
69-72
: Consider explicitly annotating deprecated storage to reduce cognitive overhead
latestDelayedStakeIndex
is kept for storage-layout compatibility, but it now serves no functional purpose.
Adding an/// @deprecated
NatSpec tag (or// DEPRECATED: kept for storage-layout
) immediately above the declaration would make the intent clearer to future maintainers and external auditors.
84-100
: Mark legacy events with @deprecated to avoid accidental reuseThe three
StakeDelayed*
events are retained but unused. Emitting them elsewhere by mistake would silently revive dead semantics.
Consider:-/// @notice DEPRECATED Emitted when a juror's stake is delayed and tokens are not transferred yet. +/// @deprecated Replaced by `StakeDelayed`. Kept for ABI compatibility with historic logs.Doing so on all three legacy events keeps the ABI unchanged while signalling that integrators should subscribe only to
StakeDelayed
.
242-253
: Guard against zero-iteration calls inexecuteDelayedStakes
Passing
_iterations == 0
wastes gas and returns silently. A tiny require improves UX and prevents accidental no-op transactions:function executeDelayedStakes(uint256 _iterations) external { require(phase == Phase.staking, "Should be in Staking phase."); require(delayedStakeWriteIndex >= delayedStakeReadIndex, "No delayed stake to execute."); + require(_iterations > 0, "Iterations must be > 0");
411-423
: Minor polish forwithdrawLeftoverPNK
The function is callable by anyone for any
_account
, which is harmless but unusual.
If the goal is to let third parties “dust-sweep” for jurors, document this explicitly; otherwise considerrequire(msg.sender == _account, "Only juror");
.Replace the
if / else { revert(...) }
pattern with a singlerequire
to save gas and keep style consistent:-function withdrawLeftoverPNK(address _account) external override { +function withdrawLeftoverPNK(address _account) external override { Juror storage juror = jurors[_account]; - if (juror.stakedPnk > 0 && juror.courtIDs.length == 0 && juror.lockedPnk == 0) { - ... - } else { - revert("Not eligible for withdrawal."); - } + require( + juror.stakedPnk > 0 && juror.courtIDs.length == 0 && juror.lockedPnk == 0, + "Not eligible for withdrawal" + ); + ... }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
contracts/src/arbitration/KlerosCoreBase.sol
(4 hunks)contracts/src/arbitration/SortitionModuleBase.sol
(8 hunks)contracts/src/arbitration/SortitionModuleNeo.sol
(2 hunks)contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol
(2 hunks)contracts/src/arbitration/interfaces/ISortitionModule.sol
(2 hunks)contracts/src/arbitration/university/SortitionModuleUniversity.sol
(2 hunks)contracts/test/foundry/KlerosCore.t.sol
(10 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (3)
- GitHub Check: Analyze (javascript)
- GitHub Check: SonarCloud
- GitHub Check: contracts-testing
🔇 Additional comments (9)
contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol (1)
609-630
: Simplified juror eligibility check improves code clarityThe
_postDrawCheck
function has been simplified to only check if the juror has already been drawn whensingleDrawPerJuror
is enabled, removing previous stake sufficiency checks. This aligns with the broader refactoring to simplify staking logic across the arbitration system.contracts/src/arbitration/interfaces/ISortitionModule.sol (2)
30-34
: Enhanced penalty accounting with detailed return valuesThe updated return signature for
penalizeStake
now provides more detailed information by returning both the juror's updated PNK balance and the actual penalty applied. This change enables more precise accounting of penalties in the core arbitration contract.
52-53
: New function for withdrawing leftover PNK tokensThis new function enables jurors to withdraw any remaining PNK tokens after fully unstaking and clearing their locked tokens, improving the user experience by ensuring no funds are trapped in the contract.
contracts/src/arbitration/SortitionModuleNeo.sol (3)
87-92
: Properly marking deprecated parameterThe
_alreadyTransferred
parameter has been marked as unused with a comment, clearly indicating its deprecation as part of removing instant staking functionality.
97-104
: Simplified conditional logic for stake increasesThe conditional logic has been streamlined to only check maximum stake limits when the stake is being increased, making the code more focused and easier to understand.
112-117
: Explicit handling of deprecated parameterThe code now explicitly passes
false
for the deprecated_alreadyTransferred
parameter with a clarifying comment, ensuring proper handling while maintaining compatibility with the base contract.contracts/src/arbitration/KlerosCoreBase.sol (1)
789-803
: Double-unlock edge case: verify that unlocked but un-penalised PNK cannot be withdrawnThe flow is:
unlockStake(account, penalty);
penalizeStake(account, penalty)
→ returnsavailablePenalty
(may be < penalty).If the juror’s locked stake is smaller than
penalty
, the surplus tokens get unlocked in step 1 but are not slashed in step 2, letting the juror immediately withdraw them-–potentially rewarding under-collateralised behaviour.Please confirm that
sortitionModule.unlockStake
prevents unlocking more than currently locked or thatpenalizeStake
internally adjusts the unlocked amount. If not, swap the order (penalise first, then unlock the actualavailablePenalty
) or unlock onlyavailablePenalty
.contracts/src/arbitration/SortitionModuleBase.sol (2)
327-334
: Double-check tree consistency when withdrawals are limited by locked PNKIf
lockedPnk
prevents a full withdrawal,pnkWithdrawal
is trimmed but_newStake
remains unchanged.
This deliberately produces “left-over” PNK that can later be withdrawn viawithdrawLeftoverPNK
, so it may be correct by design.
However, it temporarily breaks the invariantjuror.stakedPnk == Σ(tree stakes)
until the leftover is withdrawn.Please verify that all downstream logic (drawing, penalties, etc.) tolerates this mismatch; otherwise consider updating
_newStake
after trimming:- // _newStake is still the user-requested target (might be < effective stake) + // Align stake recorded in the tree with the actual post-withdrawal amount. + _newStake = currentStake - pnkWithdrawal;
372-389
: Potential invariant break:lockedPnk
can exceedstakedPnk
after penalties
penalizeStake
reducesstakedPnk
but never toucheslockedPnk
.
IflockedPnk > stakedPnk
afterwards, subsequent calls tounlockStake
could underflow or leavestakedPnk
negative in intent (though uint256 prevents it).Please confirm that
lockedPnk ≤ stakedPnk
is not relied upon elsewhere (e.g., when drawing, displaying balances, or executing further penalties).
An inexpensive safeguard could be:assert(juror.stakedPnk >= juror.lockedPnk);or clamping the penalty to
stakedPnk - lockedPnk
.
/// @dev Transfers PNK to the juror by SortitionModule. | ||
/// @param _account The account of the juror whose PNK to transfer. | ||
/// @param _amount The amount to transfer. | ||
function transferBySortitionModule(address _account, uint256 _amount) external { | ||
if (msg.sender != address(sortitionModule)) revert SortitionModuleOnly(); | ||
// Note eligibility is checked in SortitionModule. | ||
pinakion.safeTransfer(_account, _amount); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Emit an event for PNK moved via transferBySortitionModule
transferBySortitionModule
performs an external token transfer but leaves no on-chain trace inside Core.
For transparency, accounting and easier off-chain indexing, emit a lightweight event such as:
+ event SortitionPNKTransfer(address indexed account, uint256 amount);
...
function transferBySortitionModule(address _account, uint256 _amount) external {
if (msg.sender != address(sortitionModule)) revert SortitionModuleOnly();
pinakion.safeTransfer(_account, _amount);
+ emit SortitionPNKTransfer(_account, _amount);
}
This incurs negligible gas yet gives jurors and auditors full visibility.
🤖 Prompt for AI Agents
In contracts/src/arbitration/KlerosCoreBase.sol around lines 483 to 490, the
function transferBySortitionModule transfers PNK tokens without emitting any
event, which reduces transparency and traceability. Add an event declaration for
PNK transfers by the SortitionModule and emit this event inside the function
after the safeTransfer call, including the recipient address and the amount
transferred to provide on-chain visibility and facilitate off-chain indexing.
✅ Deploy Preview for kleros-v2-testnet ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
❌ Deploy Preview for kleros-v2-university failed. Why did it fail? →
|
✅ Deploy Preview for kleros-v2-testnet-devtools ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for kleros-v2-neo ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🔭 Outside diff range comments (1)
subgraph/core/abi-migrations/SortitionModule.json (1)
823-843
: 💡 Verification agent🧩 Analysis chain
Invalid unnamed boolean input in setStake
The input list forsetStake
still contains an unnamedbool
parameter (formerly_alreadyTransferred
), which is invalid in the ABI and will break code generation. You should either remove this entry entirely or assign it a proper name to match the contract signature.Apply this diff to remove it:
--- before { "internalType": "bool", - "name": "", + "name": "", // <–– remove this block if the parameter is deprecated "type": "bool" } --- after // block removed
🏁 Script executed:
#!/usr/bin/env bash # Extract the full ABI entry for the `setStake` function to verify its inputs jq '.[] | select(.name == "setStake")' subgraph/core/abi-migrations/SortitionModule.jsonLength of output: 198
🏁 Script executed:
#!/usr/bin/env bash # Attempt to extract the `setStake` entry from the ABI jq '.abi[] | select(.name=="setStake")' subgraph/core/abi-migrations/SortitionModule.json \ || rg -n '"name": "setStake"' -A 10 -B 10 subgraph/core/abi-migrations/SortitionModule.jsonLength of output: 984
🏁 Script executed:
#!/usr/bin/env bash # Locate the Solidity definition of setStake to confirm the parameter name rg -n "function setStake" -g "*.sol" -C 3Length of output: 6473
Remove Invalid Unnamed Boolean from
setStake
ABI
The ABI forsetStake
currently includes a fourth, unnamedbool
parameter that isn’t part of the contract’s external signature (address
,uint96
,uint256
only). This extra input breaks codegen and must be removed.• File:
subgraph/core/abi-migrations/SortitionModule.json
(inputs around lines 823–843)
• Function:setStake(address _account, uint96 _courtID, uint256 _newStake)
Apply this diff to drop the stray boolean block:
--- a/subgraph/core/abi-migrations/SortitionModule.json +++ b/subgraph/core/abi-migrations/SortitionModule.json @@ -823,10 +823,6 @@ { "internalType": "uint256", "name": "_newStake", "type": "uint256" }, - { - "internalType": "bool", - "name": "", - "type": "bool" - }, { "internalType": "uint256", "name": "pnkDeposit", "type": "uint256" },
🧹 Nitpick comments (2)
subgraph/core/abi-migrations/SortitionModule.json (2)
67-81
: Added LeftoverPNKWithdrawn event
Introduces theLeftoverPNKWithdrawn(address _account, uint256 _amount)
event for leftover PNK withdrawals. Make sure your subgraph’s GraphQL schema and mapping handlers are updated to index this new event.
954-961
: New withdrawLeftoverPNK function
AddswithdrawLeftoverPNK(address _account)
for direct PNK withdrawals. Update any subgraph custom handlers or scripts that invoke this function if you intend to index these calls.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
subgraph/core-neo/abi-migrations/SortitionModuleNeo.json
(6 hunks)subgraph/core-neo/subgraph.yaml
(1 hunks)subgraph/core/abi-migrations/SortitionModule.json
(7 hunks)subgraph/core/src/SortitionModule.ts
(1 hunks)subgraph/core/subgraph.yaml
(1 hunks)subgraph/temp-older-events-addition.txt
(1 hunks)
✅ Files skipped from review due to trivial changes (1)
- subgraph/temp-older-events-addition.txt
⏰ Context from checks skipped due to timeout of 90000ms (7)
- GitHub Check: Redirect rules - kleros-v2-testnet-devtools
- GitHub Check: Header rules - kleros-v2-testnet-devtools
- GitHub Check: Pages changed - kleros-v2-testnet-devtools
- GitHub Check: Redirect rules - kleros-v2-testnet
- GitHub Check: Header rules - kleros-v2-testnet
- GitHub Check: Pages changed - kleros-v2-testnet
- GitHub Check: contracts-testing
🔇 Additional comments (14)
subgraph/core/subgraph.yaml (1)
162-163
: LGTM! Event consolidation simplifies the handling logic.The consolidation of multiple
StakeDelayed
event variants into a single unified event handler aligns well with the contract changes that removed the deprecated_alreadyTransferred
parameter. This simplification reduces complexity and maintains consistency across the system.subgraph/core-neo/subgraph.yaml (1)
161-162
: LGTM! Consistent event handling across networks.The changes mirror those in the core network subgraph, ensuring consistent event handling across both arbitrum-sepolia and arbitrum-one networks. The unified
StakeDelayed
event signature maintains compatibility while simplifying the codebase.subgraph/core/src/SortitionModule.ts (2)
1-1
: LGTM! Import statement correctly reflects the consolidated events.The import statement now includes only the necessary events after the consolidation, removing the deprecated variants. This keeps the imports clean and aligned with the contract changes.
7-9
: LGTM! Unified handler simplifies event processing.The single
handleStakeDelayed
function correctly extracts all necessary parameters (_address
,_courtID
,_amount
) from the consolidated event. This approach eliminates code duplication while maintaining full functionality.subgraph/core-neo/abi-migrations/SortitionModuleNeo.json (6)
69-80
: LGTM! Event rename clarifies its purpose.The rename from
StakeDelayedAlreadyTransferredDeposited
toLeftoverPNKWithdrawn
makes the event name more descriptive and aligns with its actual functionality of withdrawing leftover PNK tokens.
87-93
: LGTM! Simplified NewPhase event reduces complexity.The
NewPhase
event now only includes the essential_phase
parameter as auint8
enum, removing unnecessary court ID and amount parameters that were previously included. This simplification makes the event more focused and easier to handle.
117-118
: LGTM! Consolidated StakeDelayed event improves consistency.The unified
StakeDelayed
event with indexedaddress
anduint96
court ID parameters, along with theuint256
amount, provides a clean interface that eliminates the need for multiple event variants.
742-753
: LGTM! Enhanced penalizeStake function provides better accountability.The updated
penalizeStake
function now returns bothpnkBalance
andavailablePenalty
values, which improves transparency by providing information about the juror's remaining balance and the actual penalty applied. This is valuable for accurate penalty accounting.
885-905
: LGTM! setStake function improvements enhance functionality.The changes to
setStake
include:
- Unnamed boolean parameter (formerly
_alreadyTransferred
) indicating deprecation- Additional
oldStake
return value for better state trackingThese changes improve the function's utility while maintaining backward compatibility through the unnamed parameter.
1024-1037
: LGTM! New withdrawLeftoverPNK function adds important functionality.The addition of
withdrawLeftoverPNK
function provides jurors with a mechanism to withdraw any remaining PNK tokens after full unstaking. This is an important feature for complete token recovery and aligns with the overall staking logic improvements.subgraph/core/abi-migrations/SortitionModule.json (4)
4-7
: Explicit no-args constructor declared
The ABI now includes a parameterless constructor (type: "constructor"
) and has removed any fallback/receive entries to align with the updated contract deployment.
87-93
: Consolidated NewPhase event
Replaces legacy phase‐related events with a singleNewPhase(uint8 _phase)
event. Verify that your subgraph mappings now listen toNewPhase
instead of the old events.
114-118
: Updated StakeDelayed event
All delayed‐stake events have been merged intoStakeDelayed(address _address, uint96 _courtID, uint256 _amount)
. Ensure your subgraph only handles this consolidated event going forward.
679-691
: penalizeStake now returns actual balances
ThepenalizeStake
function signature was changed to return(uint256 pnkBalance, uint256 availablePenalty)
. If your subgraph makes contract calls to this function, update the ABICall definitions accordingly.
d128f10
to
f7591d2
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
subgraph/core-neo/abi-migrations/SortitionModuleNeo.json
(6 hunks)subgraph/core-neo/subgraph.yaml
(1 hunks)subgraph/core/abi-migrations/SortitionModule.json
(7 hunks)subgraph/core/src/SortitionModule.ts
(1 hunks)subgraph/core/subgraph.yaml
(1 hunks)subgraph/temp-older-events-addition.txt
(1 hunks)
✅ Files skipped from review due to trivial changes (1)
- subgraph/temp-older-events-addition.txt
⏰ Context from checks skipped due to timeout of 90000ms (12)
- GitHub Check: Redirect rules - kleros-v2-testnet-devtools
- GitHub Check: Header rules - kleros-v2-testnet-devtools
- GitHub Check: Pages changed - kleros-v2-testnet-devtools
- GitHub Check: Redirect rules - kleros-v2-university
- GitHub Check: Redirect rules - kleros-v2-neo
- GitHub Check: Header rules - kleros-v2-university
- GitHub Check: Header rules - kleros-v2-neo
- GitHub Check: Pages changed - kleros-v2-neo
- GitHub Check: Pages changed - kleros-v2-university
- GitHub Check: SonarCloud
- GitHub Check: contracts-testing
- GitHub Check: Analyze (javascript)
🔇 Additional comments (18)
subgraph/core-neo/subgraph.yaml (1)
161-162
: Event handler consolidation looks good.The consolidation of multiple delayed stake event handlers into a single
StakeDelayed
event handler simplifies the subgraph configuration and aligns with the contract changes. This improves maintainability and reduces complexity.subgraph/core/subgraph.yaml (1)
162-163
: Consistent event handler consolidation across networks.The changes maintain consistency with the core-neo configuration, properly consolidating the delayed stake event handlers. This ensures uniform behavior across different network deployments.
subgraph/core/src/SortitionModule.ts (2)
1-1
: Import consolidation is correctly implemented.The import statement has been properly updated to reflect the consolidated event structure, removing the multiple specific delayed stake events in favor of the unified
StakeDelayed
event.
7-9
: Event handler consolidation maintains correct logic.The unified
handleStakeDelayed
function properly callsupdateJurorDelayedStake
with the correct parameters (address, courtID, amount), maintaining the same functionality as the previous separate handlers.subgraph/core/abi-migrations/SortitionModule.json (8)
4-6
: Constructor definition updated correctly.The constructor has been properly updated to a standard nonpayable constructor with no inputs, removing the previous fallback/receive function structure.
69-69
: Event parameter naming standardized.The parameter naming has been standardized from
_address
to_account
in theLeftoverPNKWithdrawn
event, improving consistency across the ABI.Also applies to: 79-79
87-93
: NewPhase event structure improved.The
NewPhase
event has been updated with proper enum typing (enum ISortitionModule.Phase
) and parameter naming (_phase
), which provides better type safety and clarity.
117-117
: StakeDelayed event consolidation aligns with contract changes.The unified
StakeDelayed
event with parameters (address, uint96, uint256) correctly replaces the multiple variant delayed stake events, simplifying the event structure while maintaining necessary information.
680-691
: penalizeStake function returns enhanced penalty information.The function now returns two values (
pnkBalance
andavailablePenalty
) instead of none, providing better transparency into penalty execution and available penalty amounts for reward distribution.
823-823
: setStake parameter handling updated.The deprecated
_alreadyTransferred
parameter has been removed (parameter name is now empty), aligning with the PR objective to remove instant staking and simplify the staking logic.
839-843
: setStake returns additional stake information.The function now returns an additional
oldStake
value, providing better visibility into stake changes for external integrations and event processing.
954-961
: withdrawLeftoverPNK function properly defined.The function has been correctly updated from a constructor-type to a regular function with simplified parameters, enabling jurors to withdraw leftover PNK tokens as described in the PR objectives.
subgraph/core-neo/abi-migrations/SortitionModuleNeo.json (6)
67-80
: New LeftoverPNKWithdrawn event looks correct
Introducing theLeftoverPNKWithdrawn
event with an indexed_account
parameter allows efficient filtering. Ensure your subgraph mappings handle this new event.Please verify that the subgraph’s event handlers have been updated to consume
LeftoverPNKWithdrawn
.
85-93
: UpdatedNewPhase
event signature is in place
The previous variant with an indexed address is removed, leaving only the_phase
payload. Confirm that downstream consumers (subgraph, frontends) are updated for this change.Run a search in your subgraph mappings for
NewPhase
to ensure the filtering logic aligns with this new signature.
109-118
: ConsolidatedStakeDelayed
event is correct
Combining the three prior delayed-stake events into this single, indexed version simplifies processing. Confirm that your subgraph and any listeners now reference only thisStakeDelayed
event.Ensure all old event handlers are removed and that
StakeDelayed
is fully integrated.
739-753
:penalizeStake
now returns two values
The signature update to emit bothpnkBalance
andavailablePenalty
aligns with the new core logic. Verify that any ABI consumers (e.g., subgraph or UI) handle both return values in the correct order.You may grep for
.penalizeStake
calls in your mappings to confirm compatibility.
901-905
:oldStake
output correctly added tosetStake
CapturingoldStake
enables downstream logic to reconcile previous state. The new ordering aligns with the updatedStakingResult
enum.
1024-1037
: AddedwithdrawLeftoverPNK
function
This new method enables jurors to reclaim unused tokens. Ensure that your subgraph ABI and mappings include this function, and that theLeftoverPNKWithdrawn
event is emitted accordingly in the contract.Double-check your graph schema to include
withdrawLeftoverPNK
and that calls to it are indexed.
{ | ||
"internalType": "bool", | ||
"name": "_alreadyTransferred", | ||
"name": "", | ||
"type": "bool" | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Remove the unnamed boolean input in setStake
The boolean parameter is now deprecated and left unnamed—this will break ABI consumers. It should be removed entirely.
Apply this diff to eliminate the unused boolean param:
@@ -883,4 +883,0
- {
- "internalType": "bool",
- "name": "",
- "type": "bool"
- },
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In subgraph/core-neo/abi-migrations/SortitionModuleNeo.json around lines 883 to
887, there is an unnamed boolean input parameter in the setStake function that
is deprecated and breaks ABI consumers. Remove this entire boolean parameter
object from the ABI to eliminate the unused and unnamed boolean input.
…ule and SM.withdrawLeftoverPNK
Code Climate has analyzed commit 3faccb6 and detected 0 issues on this pull request. View more on Code Climate. |
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🔭 Outside diff range comments (2)
contracts/src/arbitration/university/KlerosCoreUniversity.sol (1)
740-776
:⚠️ Potential issuePenalty accounting bug – returned values ignored.
penalizeStake
now yields(pnkBalance, availablePenalty)
, but the code:_params.pnkPenaltiesInRound += penalty; ... penalizeStake(account, dispute.courtID, penalty);never uses
availablePenalty
. If the juror’s stake is insufficient, you will over-credit penalties, breaking reward maths.- uint256 penalty = ...; - _params.pnkPenaltiesInRound += penalty; - ... - sortitionModule.penalizeStake(account, dispute.courtID, penalty); + uint256 penalty = ...; + (uint256 pnkBalance, uint256 applied) = + sortitionModule.penalizeStake(account, dispute.courtID, penalty); + _params.pnkPenaltiesInRound += applied; + ... + if (pnkBalance == 0) { + sortitionModule.setJurorInactive(account); + }This aligns behaviour with the Neo/Core version.
contracts/src/arbitration/university/SortitionModuleUniversity.sol (1)
247-258
:⚠️ Potential issueMissing return statement – contract will not compile.
penalizeStake
promises(uint256 pnkBalance, uint256 availablePenalty)
but neverreturn
s.- ) external override onlyByCore returns (uint256 pnkBalance, uint256 availablePenalty) { + ) external override onlyByCore returns (uint256 pnkBalance, uint256 availablePenalty) { Juror storage juror = jurors[_account]; if (juror.stakedPnk >= _relativeAmount) { juror.stakedPnk -= _relativeAmount; - } else { + availablePenalty = _relativeAmount; + } else { juror.stakedPnk = 0; + availablePenalty = juror.stakedPnk; // whatever could actually be taken } - } + pnkBalance = juror.stakedPnk; + return (pnkBalance, availablePenalty); + }Without this, compilation fails and upstream contracts depending on the returned values break.
🧹 Nitpick comments (11)
contracts/src/arbitration/SortitionModuleNeo.sol (1)
87-120
: Minor gas-save opportunity in_validateStake
.The arithmetic inside
if (phase == Phase.staking)
is overflow-checked twice by default.
Wrapping thetotalStaked
updates in anunchecked { ... }
block (after confirming bounds with the earlierif
checks) will shave a few hundred gas per stake adjustment without changing behaviour.contracts/src/arbitration/university/SortitionModuleUniversity.sol (2)
142-181
:validateStake
always reports Success – consider max-stake caps.Unlike the Neo module, no upper-bound checks are applied. If instructional simplicity is intended that’s fine; otherwise replicate
maxStakePerJuror
/maxTotalStaked
validation to keep examples consistent.
183-185
:setStake
is still TODO.Core now calls this method after deposits succeed. Implement the function (even a thin wrapper around tree updates) to avoid silent no-ops once the classroom moves past validation.
contracts/src/arbitration/SortitionModuleBase.sol (8)
38-43
:alreadyTransferred
field is dead-code – drop it to slim the structThe flag is now documented as DEPRECATED and is never read anywhere in the contract after this PR.
Keeping it around:
- Bloats storage layout and therefore deployment/runtime gas.
- Confuses auditors who must still reason about its invariants.
If backward-compatibility is not strictly required at the storage level, delete the field altogether; otherwise at least add an inline comment that the slot is intentionally preserved for storage-layout compatibility.
48-50
: Nice addition, but document the per-court locking invariantIntroducing
lockedInCourt
clarifies where tokens are frozen.
Please extend NatSpec onJuror.lockedPnk
/lockedInCourt
to state the intended invariant (e.g.sum(lockedInCourt) == lockedPnk
) so future maintainers cannot break it inadvertently.
72-73
: Unused mapping – consider pruning
latestDelayedStakeIndex
is marked DEPRECATED but is still instantiated.
Unless an external migration script still relies on it, removing it (and its writes in legacy code, if any) will save ~20 000 gas at deploy and 5 000 gas per cold SLOAD.
85-101
: Legacy delayed-stake events still emitted nowhereAll three
StakeDelayed*
legacy events remain declared but are never emitted after this PR.
Leaving them around:
- Creates an ABI surface that dApp/indexer devs might listen to by mistake.
- Adds 96 bytes to the runtime bytecode.
Recommend deleting the declarations once subgraphs have been migrated.
156-163
: ModifieronlyByCoreOrThis
enables external self-calls – be deliberateUsing
this.setStake(...)
(seepenalizeStake
) incurs an external call, extra gas, and re-entrancy surface (albeit to self).
An internal helper (_applyStake
) would avoid this.-function setStake(address _account, uint96 _courtID, uint256 _newStake) public ... +function _applyStake(address _account, uint96 _courtID, uint256 _newStake) internal ...Then expose a thin external wrapper guarded by the modifier if needed.
348-388
: Micro-optimisations & typo insidesetStake
currenCourtID
is misspelled – minor, but typo leaks into public bytecode.- The for-loop used for array cleanup is O(n). Keeping a mapping
courtID => index
would make deletions O(1) and save gas for jurors with many courts.Not blocking, but worth tracking.
403-414
:penalizeStake
performs an external self-callCalling
this.setStake
costs an extra 700–1 000 gas and bypasses Solidity’s internal call optimisations.
Extract the core logic into an internal function and reuse it here.Also, since penalty burns stake while
lockedPnk
stays unchanged untilunlockStake
, make sure downstream code never assumesstakedPnk ≥ lockedPnk
between these two calls (see earlier comment).
377-377
: Nit: variable name typo
uint96 currenCourtID
→uint96 currentCourtID
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (8)
contracts/src/arbitration/KlerosCoreBase.sol
(7 hunks)contracts/src/arbitration/KlerosCoreNeo.sol
(1 hunks)contracts/src/arbitration/SortitionModuleBase.sol
(8 hunks)contracts/src/arbitration/SortitionModuleNeo.sol
(2 hunks)contracts/src/arbitration/interfaces/ISortitionModule.sol
(1 hunks)contracts/src/arbitration/university/KlerosCoreUniversity.sol
(7 hunks)contracts/src/arbitration/university/SortitionModuleUniversity.sol
(5 hunks)contracts/test/foundry/KlerosCore.t.sol
(11 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- contracts/src/arbitration/interfaces/ISortitionModule.sol
- contracts/test/foundry/KlerosCore.t.sol
⏰ Context from checks skipped due to timeout of 90000ms (15)
- GitHub Check: Redirect rules - kleros-v2-neo
- GitHub Check: Redirect rules - kleros-v2-university
- GitHub Check: Header rules - kleros-v2-neo
- GitHub Check: Header rules - kleros-v2-university
- GitHub Check: Pages changed - kleros-v2-neo
- GitHub Check: Pages changed - kleros-v2-university
- GitHub Check: Redirect rules - kleros-v2-testnet
- GitHub Check: Redirect rules - kleros-v2-testnet
- GitHub Check: Analyze (javascript)
- GitHub Check: contracts-testing
- GitHub Check: SonarCloud
- GitHub Check: Header rules - kleros-v2-testnet
- GitHub Check: Header rules - kleros-v2-testnet
- GitHub Check: Pages changed - kleros-v2-testnet
- GitHub Check: Pages changed - kleros-v2-testnet
🔇 Additional comments (14)
contracts/src/arbitration/KlerosCoreNeo.sol (1)
108-108
: _setStake call matches the new interface – looks good.contracts/src/arbitration/KlerosCoreBase.sol (5)
466-476
: Interface alignment looks correct.The updated invocations of
_setStake
now supplyOnError.*
instead of the removed_alreadyTransferred
flag – consistent with the refactor. No further action required.
606-607
: Court-scoped locking call adopted correctly.
lockStake(drawnAddress, dispute.courtID, …)
propagates the court context expected by the new SortitionModule ABI.
775-798
: Good use of real penalty returned bypenalizeStake
, but double-check ordering.Unlocking the stake before calling
penalizeStake
is necessary, yet it briefly dropslockedInCourt
below the eventual penalty size. Confirm thatpenalizeStake
itself never relies onlockedInCourt
for invariants to avoid under-penalisation.
845-846
: Court-scoped unlock reflected here as well – looks fine.
1084-1108
: Delayed-stake branch returns before token transfer – verify follow-up call.When
stakingResult == Delayed
, the function exits without moving tokens or updating the SortitionModule. Ensure the follow-upsetStakeBySortitionModule
call executes in all cases so the deposit is eventually pulled exactly once.contracts/src/arbitration/university/KlerosCoreUniversity.sol (4)
459-460
: Updated_setStake
invocation aligns with new signature.
467-469
:setStakeBySortitionModule
call updated – OK.
595-596
: Court-aware locking adopted correctly.
824-828
: Court-aware unlock reflected in reward path – looks OK.contracts/src/arbitration/university/SortitionModuleUniversity.sol (2)
34-35
: Per-court locking map added – nice touch.
235-245
: Court-scoped lock/unlock bookkeeping looks correct.contracts/src/arbitration/SortitionModuleBase.sol (2)
103-108
: 👍 UnifiedStakeDelayed
eventClear, single source of truth with both indexed topics – good choice.
115-119
: Event defined but never fired in this contract
LeftoverPNKWithdrawn
is declared yet noemit
is present.
Confirm the emission happens in a derived contract; otherwise add it wherewithdrawLeftoverPNK
is implemented.
function _validateStake( | ||
address _account, | ||
uint96 _courtID, | ||
uint256 _newStake, | ||
bool _alreadyTransferred | ||
) internal virtual returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { | ||
uint256 _newStake | ||
) | ||
internal | ||
virtual | ||
returns (uint256 pnkDeposit, uint256 pnkWithdrawal, uint256 actualNewStake, StakingResult stakingResult) | ||
{ | ||
Juror storage juror = jurors[_account]; | ||
uint256 currentStake = stakeOf(_account, _courtID); | ||
|
||
uint256 nbCourts = juror.courtIDs.length; | ||
if (currentStake == 0 && nbCourts >= MAX_STAKE_PATHS) { | ||
return (0, 0, StakingResult.CannotStakeInMoreCourts); // Prevent staking beyond MAX_STAKE_PATHS but unstaking is always allowed. | ||
return (0, 0, currentStake, StakingResult.CannotStakeInMoreCourts); // Prevent staking beyond MAX_STAKE_PATHS but unstaking is always allowed. | ||
} | ||
|
||
if (currentStake == 0 && _newStake == 0) { | ||
return (0, 0, StakingResult.CannotStakeZeroWhenNoStake); // Forbid staking 0 amount when current stake is 0 to avoid flaky behaviour. | ||
return (0, 0, currentStake, StakingResult.CannotStakeZeroWhenNoStake); // Forbid staking 0 amount when current stake is 0 to avoid flaky behaviour. | ||
} | ||
|
||
pnkWithdrawal = _deleteDelayedStake(_courtID, _account); | ||
if (phase != Phase.staking) { | ||
// Store the stake change as delayed, to be applied when the phase switches back to Staking. | ||
DelayedStake storage delayedStake = delayedStakes[++delayedStakeWriteIndex]; | ||
delayedStake.account = _account; | ||
delayedStake.courtID = _courtID; | ||
delayedStake.stake = _newStake; | ||
latestDelayedStakeIndex[_account][_courtID] = delayedStakeWriteIndex; | ||
if (_newStake > currentStake) { | ||
// PNK deposit: tokens are transferred now. | ||
delayedStake.alreadyTransferred = true; | ||
pnkDeposit = _increaseStake(juror, _courtID, _newStake, currentStake); | ||
emit StakeDelayedAlreadyTransferredDeposited(_account, _courtID, _newStake); | ||
} else { | ||
// PNK withdrawal: tokens are not transferred yet. | ||
emit StakeDelayedNotTransferred(_account, _courtID, _newStake); | ||
} | ||
return (pnkDeposit, pnkWithdrawal, StakingResult.Successful); | ||
emit StakeDelayed(_account, _courtID, _newStake); | ||
return (0, 0, currentStake, StakingResult.Delayed); | ||
} | ||
|
||
// Current phase is Staking: set normal stakes or delayed stakes (which may have been already transferred). | ||
// Current phase is Staking: set normal stakes or delayed stakes. | ||
actualNewStake = _newStake; | ||
if (_newStake >= currentStake) { | ||
if (!_alreadyTransferred) { | ||
pnkDeposit = _increaseStake(juror, _courtID, _newStake, currentStake); | ||
pnkDeposit = _newStake - currentStake; | ||
} else { | ||
pnkWithdrawal = currentStake - _newStake; | ||
// Ensure locked tokens remain in the contract. They can only be released during Execution. | ||
uint256 possibleWithdrawal = currentStake > juror.lockedInCourt[_courtID] | ||
? currentStake - juror.lockedInCourt[_courtID] | ||
: 0; | ||
if (pnkWithdrawal > possibleWithdrawal) { | ||
pnkWithdrawal = possibleWithdrawal; | ||
actualNewStake = currentStake - pnkWithdrawal; | ||
} | ||
} | ||
return (pnkDeposit, pnkWithdrawal, actualNewStake, StakingResult.Successful); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Withdrawal cap ignores locks in other courts – can break lockedPnk ≤ stakedPnk
invariant
possibleWithdrawal
only considers lockedInCourt[_courtID]
.
A juror can therefore withdraw from court A while tokens remain locked in court B, ending with stakedPnk < lockedPnk
.
That violates the implicit invariant and may later revert on unlockStake
or allow drawing under-collateralised jurors.
Proposed fix (illustrative):
- uint256 possibleWithdrawal = currentStake > juror.lockedInCourt[_courtID]
- ? currentStake - juror.lockedInCourt[_courtID]
- : 0;
+ uint256 globallyAvailable = juror.stakedPnk - juror.lockedPnk;
+ // remove the portion we are about to touch
+ globallyAvailable += currentStake;
+ uint256 possibleWithdrawal = globallyAvailable > currentStake
+ ? globallyAvailable - currentStake
+ : 0;
Ensures total stake never drops below total locked across all courts.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In contracts/src/arbitration/SortitionModuleBase.sol between lines 299 and 346,
the withdrawal cap calculation only accounts for locked tokens in the current
court (_courtID), ignoring locks in other courts. This can cause the total
staked tokens to fall below the total locked tokens, violating the invariant
lockedPnk ≤ stakedPnk. To fix this, modify the calculation of possibleWithdrawal
to consider the total locked tokens across all courts for the juror, ensuring
that the new stake never drops below the sum of all locked tokens. This prevents
under-collateralization and maintains the invariant.
PR-Codex overview
This PR focuses on refactoring the staking mechanism in the Kleros arbitration system, introducing a new
Delayed
staking result, and streamlining the handling of stakes without transferring tokens prematurely.Detailed summary
Delayed
toStakingResult
enum.setStake
tovalidateStake
inISortitionModule
.setStake
methods to remove_alreadyTransferred
parameter.withdrawLeftoverPNK
function.Summary by CodeRabbit
New Features
Improvements
Bug Fixes
Tests