Skip to content

Staking refactor including bug fixes #2021

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

Draft
wants to merge 24 commits into
base: dev
Choose a base branch
from
Draft

Conversation

jaybuidl
Copy link
Member

@jaybuidl jaybuidl commented Jun 9, 2025

PR-Codex overview

This PR focuses on refactoring the Kleros arbitration system by replacing the SortitionModule with a new SortitionSumTree architecture, enhancing the Vault and StakeController, and updating interfaces and contracts for improved functionality and clarity.

Detailed summary

  • Deleted SortitionModule.sol, SortitionModuleNeo.sol, and SortitionModuleBase.sol.
  • Introduced SortitionSumTree.sol for sortition operations.
  • Updated SortitionModuleMock to inherit from SortitionSumTree.
  • Refactored KlerosCore and KlerosCoreNeo to use StakeController.
  • Created Vault and VaultNeo for handling deposits with NFT-gated features.
  • Updated interfaces to reflect new architecture (IStakeController, IVault).
  • Modified changeCurrencyRate to include KlerosCore in its parameters.
  • Enhanced error handling and governance functions in the new contracts.
  • Updated deployment scripts to accommodate the new architecture and dependencies.

The following files were skipped due to too many changes: contracts/src/arbitration/SortitionSumTree.sol, contracts/src/arbitration/StakeController.sol, contracts/src/arbitration/KlerosCoreBase.sol

✨ Ask PR-Codex anything about this PR by commenting with /codex {your question}

Summary by CodeRabbit

  • New Features

    • Introduced a modular V2 arbitration architecture with new contracts for stake management, vaults, and sortition sum trees.
    • Added NFT-gated and capped deposit vaults, allowing governance over deposit eligibility and limits.
    • Implemented phased staking with delayed stake execution and random juror selection.
    • Provided new interfaces for stake controllers, vaults, and sortition sum trees to enhance modularity.
  • Refactor

    • Replaced the legacy sortition module with dedicated StakeController and SortitionSumTree contracts.
    • Updated core contracts to interact with the new stake and vault architecture.
    • Removed NFT-based juror eligibility in favor of whitelist-based access.
  • Removals

    • Deleted legacy sortition module contracts and related base implementations.
  • Bug Fixes

    • Improved error handling for failed withdrawals and stake updates.
  • Chores

    • Updated deployment scripts and proxy contract names to reflect the new architecture.
    • Deprecated old staking result enums and updated documentation for clarity.

Copy link

netlify bot commented Jun 9, 2025

Deploy Preview for kleros-v2-university failed. Why did it fail? →

Name Link
🔨 Latest commit 6c28f88
🔍 Latest deploy log https://app.netlify.com/projects/kleros-v2-university/deploys/68475f29db07d50008cb6f3c

Copy link

netlify bot commented Jun 9, 2025

Deploy Preview for kleros-v2-testnet ready!

Name Link
🔨 Latest commit 6c28f88
🔍 Latest deploy log https://app.netlify.com/projects/kleros-v2-testnet/deploys/68475f2920c73f0008ef8313
😎 Deploy Preview https://deploy-preview-2021--kleros-v2-testnet.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Copy link

netlify bot commented Jun 9, 2025

Deploy Preview for kleros-v2-testnet-devtools ready!

Name Link
🔨 Latest commit 6c28f88
🔍 Latest deploy log https://app.netlify.com/projects/kleros-v2-testnet-devtools/deploys/68475f291bdc580008ad3851
😎 Deploy Preview https://deploy-preview-2021--kleros-v2-testnet-devtools.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Copy link
Contributor

coderabbitai bot commented Jun 9, 2025

Walkthrough

This update introduces a major architectural overhaul of the arbitration, staking, and vault system. The legacy SortitionModule and related contracts are removed, replaced by a modular architecture featuring new StakeController, Vault, and SortitionSumTree contracts, each with dedicated interfaces. The deployment scripts, core contracts, dispute kits, and associated proxies are refactored to integrate these new components and their upgraded initialization and interaction patterns.

Changes

File(s) / Group Change Summary
contracts/src/arbitration/SortitionModule*.sol Deleted: Legacy SortitionModule, SortitionModuleNeo, and SortitionModuleBase contracts removed.
contracts/src/arbitration/SortitionSumTree.sol Added: New SortitionSumTree contract for managing weighted sortition trees and juror draws.
contracts/src/arbitration/StakeController.sol Added: New StakeController contract to coordinate staking, delayed stakes, and phase management.
contracts/src/arbitration/VaultBase.sol,
Vault.sol,
VaultNeo.sol
Added: Vault, VaultBase, and VaultNeo contracts for PNK token management, with NFT gating and deposit limits.
contracts/src/arbitration/interfaces/IStakeController.sol,
ISortitionSumTree.sol,
IVault.sol
Added: Interfaces for StakeController, SortitionSumTree, and Vault.
contracts/src/arbitration/KlerosCoreBase.sol,
KlerosCore.sol,
KlerosCoreNeo.sol
Refactored to remove SortitionModule logic, integrate StakeController and Vault, update initialization.
contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol Refactored to use StakeController for juror drawing and balances, update period enum usage.
contracts/src/libraries/Constants.sol Marked OnError and most StakingResult enum values as deprecated; added Delayed to StakingResult.
contracts/src/test/SortitionModuleMock.sol Now inherits from SortitionSumTree; updated types accordingly.
contracts/src/proxy/KlerosProxies.sol Renamed proxy contracts to match new architecture; added VaultNeo proxy.
contracts/deploy/00-home-chain-arbitration.ts Deployment script refactored for new architecture: deploys and wires Vault, StakeController, SortitionSumTree.
contracts/deploy/utils/klerosCoreHelper.ts Minor type annotation changes; no functional impact.

Sequence Diagram(s)

sequenceDiagram
    participant Deployer
    participant Vault
    participant StakeController
    participant SortitionSumTree
    participant KlerosCore
    participant DisputeKit
    participant RNG

    Deployer->>Vault: Deploy (governor, token, stakeController, core)
    Deployer->>SortitionSumTree: Deploy (governor, stakeController)
    Deployer->>StakeController: Deploy (governor, core, vault, sortitionSumTree, timings, RNG)
    Deployer->>KlerosCore: Deploy (governor, guardian, prosecution, disputeKit, stakeController, vault, ...)
    Deployer->>DisputeKit: Deploy (core)
    KlerosCore->>StakeController: setStake / setStakeDelayed / lockStake / unlockStake / penalties
    StakeController->>Vault: deposit / withdraw / lockTokens / unlockTokens / applyPenalty
    StakeController->>SortitionSumTree: setStake / draw
    StakeController->>RNG: Request random number (during phase change)
    StakeController->>Vault: transferReward (for juror rewards)
Loading

Poem

In the warren of code, where carrots abound,
Old roots are replaced, new tunnels are found.
Vaults keep the treasure, controllers the law,
Sortition trees grow, without a flaw.
With paws on the ledger and whiskers alert—
This bunny brings order, where chaos did flirt!
🥕✨


🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need 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)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai or @coderabbitai title anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

netlify bot commented Jun 9, 2025

Deploy Preview for kleros-v2-neo ready!

Name Link
🔨 Latest commit 6c28f88
🔍 Latest deploy log https://app.netlify.com/projects/kleros-v2-neo/deploys/68475f295375b10008b57e27
😎 Deploy Preview https://deploy-preview-2021--kleros-v2-neo.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 20

🔭 Outside diff range comments (1)
contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol (1)

210-236: 🛠️ Refactor suggestion

Unchecked assumptions around _nonce uniqueness and juror re-draws.

stakeController.draw is invoked with an externally supplied _nonce, but no contract-side check guarantees that the same (disputeID, nonce) pair is not reused.
Reusing the nonce would let the caller “re-roll” until a desired juror passes _postDrawCheck, compromising randomness.

Guard recommendation:

+// inside draw(..)
+require(core.isNonceUnused(_coreDisputeID, _nonce), "Nonce already used");
+core.setNonceUsed(_coreDisputeID, _nonce);

(or equivalent tracking inside this DK).
Also consider emitting an event for every successful draw to simplify off-chain auditing.

🧹 Nitpick comments (18)
contracts/src/arbitration/Vault.sol (1)

40-44: Minor: _authorizeUpgrade marked view unnecessarily.

OpenZeppelin’s _authorizeUpgrade is non-view; adding view is allowed but unusual and prevents emitting events or writing state inside the hook in the future.
Consider removing view for consistency with OZ patterns.

contracts/src/arbitration/StakeController.sol (1)

61-65: Same remark as in Vault: avoid declaring _authorizeUpgrade as view.

Leaving it non-view keeps the door open for future state changes (e.g., upgrade counters) without breaking the signature hierarchy.

contracts/src/arbitration/interfaces/ISortitionSumTree.sol (2)

10-18: Missing IERC-165 support for interface discovery

Most consumer contracts rely on ERC-165 to verify that a target supports an interface.
Consider adding type(ISortitionSumTree).interfaceId to a dedicated constants library or extend the base implementation with supportsInterface(bytes4).


47-58: Docstring vs. signature mismatch (stakeOf)

The second overload comment says “stake of a juror in a court by tree key and stake path ID” but the parameter name _courtID has been replaced by _key.
Clarify wording to avoid confusion.

contracts/deploy/00-home-chain-arbitration-v2.ts (3)

48-50: Comment typo and outdated name

The inline comment still refers to “SortitionModule” and contains a duplicate comma. Minor, but can mislead future maintainers.


69-71: Hard-coded timing constants hamper test flexibility

minStakingTime / maxDrawingTime are set once based on isDevnet.
Expose them through environment variables or script parameters so integration tests can tweak timings without editing code.


121-127: RNG access-control variable naming inconsistency

The TODO comment notes that sortitionModule on RNG is misleading.
Consider opening an issue + migration plan now; otherwise this confusion will persist into production deployments.

contracts/src/arbitration/interfaces/IVault.sol (2)

46-55: applyPenalty return values under-specified

Docstring says the first value “including locked PNK”, but callers also need the unlocked remaining balance to decide whether additional penalties can be applied.
Consider returning (uint256 unlockedBalance, uint256 lockedBalance, uint256 actualPenalty) for clarity.


14-20: Event suite lacks PenaltyApplied granularity

Using a generic Penalty event makes it hard to index who triggered the action (e.g. core vs governor).
Add an indexed address initiator field or emit a dedicated PenaltyApplied(address indexed juror, address indexed caller, uint256 amount).

contracts/src/arbitration/KlerosCoreNeo.sol (1)

95-97: Minor gas saving by caching the whitelist flag

The storage read is performed twice (check + implicit return). Cache it once.

-        if (!arbitrableWhitelist[msg.sender]) revert ArbitrableNotWhitelisted();
-        return super._createDispute(_numberOfChoices, _extraData, _feeToken, _feeAmount);
+        bool allowed = arbitrableWhitelist[msg.sender];
+        if (!allowed) revert ArbitrableNotWhitelisted();
+        return super._createDispute(_numberOfChoices, _extraData, _feeToken, _feeAmount);
contracts/src/arbitration/KlerosCore.sol (1)

63-67: Doc-comment noise

An empty block with a verbose comment adds little value—condense it.

-    /// @notice Access Control to perform implementation upgrades (UUPS Proxiable)
-    ///         Only the governor can perform upgrades (`onlyByGovernor`)
+    /// @notice UUPS upgrade authorization – restricted via `onlyByGovernor`
contracts/src/arbitration/VaultNeo.sol (2)

128-132: Move the totalDeposited decrement after the call

Safer (and clearer) to update state only after super._withdraw succeeds.

-        totalDeposited -= _amount;
-        return super._withdraw(_to, _amount);
+        uint256 pnkAmount = super._withdraw(_to, _amount);
+        totalDeposited -= _amount;
+        return pnkAmount;

102-120: Micro-optimisation: unchecked arithmetic & cached balance

Saves ~200 gas and removes a redundant addition overflow check that’s already guarded.

-            uint256 currentUserDeposit = jurorBalances[_from].deposited;
-            if (currentUserDeposit + _amount > maxDepositPerUser) {
+            uint256 currentUserDeposit = jurorBalances[_from].deposited;
+            unchecked {
+                uint256 newTotal = currentUserDeposit + _amount;
+                if (newTotal > maxDepositPerUser) {
                     revert ExceedsMaxDepositPerUser();
-            }
+                }
+            }
contracts/src/arbitration/SortitionSumTreeBase.sol (1)

246-255: Safer default for tree branching factor

DEFAULT_K is silently used when _extraData is shorter than 32 bytes. Consider validating that DEFAULT_K > 1 (or re-using the InvalidTreeK revert) to avoid accidentally deploying 1-ary trees due to mis-configured extraData.

contracts/src/arbitration/KlerosCoreBase.sol (1)

471-483: Docstring & signature drift

The NatSpec comment mentions _depositPreFunded, but the parameter list no longer contains it.
Please update the comment to avoid confusion for integrators.

contracts/src/arbitration/VaultBase.sol (1)

82-99: Governance setters emit no events

Changing governor, stakeController, or core is security-sensitive; on-chain monitoring tools rely on events.
Emit specific events (e.g. GovernorChanged(oldGov, newGov)) in each setter.

contracts/src/arbitration/StakeControllerBase.sol (2)

276-286: Event naming inconsistency may break off-chain parsers

The comment hints that emit StakeLocked is mis-named for backwards compatibility.
If you intentionally keep the old name, drop the comment or add a dedicated event TokensLocked to avoid confusion.


402-425: Micro-optimisation: replace O(n) search with indexed removal

When the number of courts per juror grows, _removeCourt’s linear scan becomes expensive.
Maintaining an additional mapping(courtId ⇒ index) lets you delete in O(1) while preserving constant gas per call.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4adedb5 and 799dad1.

📒 Files selected for processing (22)
  • contracts/deploy/00-home-chain-arbitration-v2-neo.ts (1 hunks)
  • contracts/deploy/00-home-chain-arbitration-v2.ts (1 hunks)
  • contracts/deploy/utils/klerosCoreHelper.ts (1 hunks)
  • contracts/src/arbitration/KlerosCore.sol (1 hunks)
  • contracts/src/arbitration/KlerosCoreBase.sol (19 hunks)
  • contracts/src/arbitration/KlerosCoreNeo.sol (2 hunks)
  • contracts/src/arbitration/SortitionModule.sol (0 hunks)
  • contracts/src/arbitration/SortitionModuleBase.sol (0 hunks)
  • contracts/src/arbitration/SortitionModuleNeo.sol (0 hunks)
  • contracts/src/arbitration/SortitionSumTree.sol (1 hunks)
  • contracts/src/arbitration/SortitionSumTreeBase.sol (1 hunks)
  • contracts/src/arbitration/StakeController.sol (1 hunks)
  • contracts/src/arbitration/StakeControllerBase.sol (1 hunks)
  • contracts/src/arbitration/Vault.sol (1 hunks)
  • contracts/src/arbitration/VaultBase.sol (1 hunks)
  • contracts/src/arbitration/VaultNeo.sol (1 hunks)
  • contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol (7 hunks)
  • contracts/src/arbitration/interfaces/ISortitionSumTree.sol (1 hunks)
  • contracts/src/arbitration/interfaces/IStakeController.sol (1 hunks)
  • contracts/src/arbitration/interfaces/IVault.sol (1 hunks)
  • contracts/src/libraries/Constants.sol (1 hunks)
  • contracts/src/test/SortitionModuleMock.sol (1 hunks)
💤 Files with no reviewable changes (3)
  • contracts/src/arbitration/SortitionModule.sol
  • contracts/src/arbitration/SortitionModuleBase.sol
  • contracts/src/arbitration/SortitionModuleNeo.sol
🧰 Additional context used
🧬 Code Graph Analysis (1)
contracts/deploy/00-home-chain-arbitration-v2-neo.ts (4)
contracts/deployments/utils.ts (1)
  • deployments (3-16)
contracts/deploy/utils/deployTokens.ts (2)
  • deployERC20AndFaucet (6-17)
  • deployERC721 (55-67)
contracts/deploy/utils/getContractOrDeploy.ts (2)
  • getContractOrDeploy (6-20)
  • getContractOrDeployUpgradable (22-36)
contracts/deploy/utils/deployUpgradable.ts (1)
  • deployUpgradable (37-89)
🪛 Biome (1.9.4)
contracts/deploy/utils/klerosCoreHelper.ts

[error] 1-1: Declarations inside of a import declaration may not have duplicates

a second declaration of KlerosCore is not allowed

KlerosCore is first declared here

(parse)

⏰ Context from checks skipped due to timeout of 90000ms (15)
  • GitHub Check: Redirect rules - kleros-v2-testnet-devtools
  • GitHub Check: Redirect rules - kleros-v2-testnet
  • GitHub Check: Redirect rules - kleros-v2-testnet
  • GitHub Check: Redirect rules - kleros-v2-university
  • GitHub Check: SonarCloud
  • GitHub Check: Header rules - kleros-v2-testnet
  • GitHub Check: contracts-testing
  • GitHub Check: Analyze (javascript)
  • GitHub Check: Header rules - kleros-v2-testnet-devtools
  • GitHub Check: Header rules - kleros-v2-testnet
  • GitHub Check: Header rules - kleros-v2-university
  • GitHub Check: Pages changed - kleros-v2-testnet
  • GitHub Check: Pages changed - kleros-v2-testnet-devtools
  • GitHub Check: Pages changed - kleros-v2-university
  • GitHub Check: Pages changed - kleros-v2-testnet
🔇 Additional comments (12)
contracts/src/test/SortitionModuleMock.sol (2)

13-17: LGTM: Proper migration to new SortitionSumTree architecture.

The changes correctly update the mock to inherit from SortitionSumTree instead of the deprecated SortitionModule, maintaining the same testing interface for the refactored architecture.


18-22: LGTM: Function maintains testing interface.

The function correctly accesses the sortitionSumTrees mapping and tree properties, maintaining the same testing interface after the architectural migration.

contracts/src/libraries/Constants.sol (1)

23-27: LGTM: Proper deprecation of OnError enum.

The deprecation of the OnError enum aligns with the architectural refactor to the new StakeController system.

contracts/src/arbitration/SortitionSumTree.sol (5)

5-6: LGTM: Proper imports for modular architecture.

The imports correctly reference the base contract and interface, supporting the new modular design that separates tree operations from stake management.


8-12: LGTM: Clear contract documentation and versioning.

The documentation clearly defines the contract's focused responsibility within the new modular architecture. The version "2.0.0" appropriately reflects the major architectural changes.


18-21: LGTM: Proper upgradeable contract constructor.

The constructor correctly disables initializers following OpenZeppelin best practices for upgradeable contracts.


26-28: LGTM: Proper initialization for upgradeable contract.

The initialize function correctly implements the upgradeable contract pattern, taking necessary parameters and delegating to the base contract initializer.


36-38: LGTM: Secure upgrade authorization.

The upgrade authorization correctly restricts implementation upgrades to the governor only, following secure UUPS proxy pattern best practices.

contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol (1)

623-629:

❌ Incorrect review comment

Possible parameter-order mismatch with getJurorBalance → risk of wrong insolvency checks.

Only totalLocked and totalStaked are used, but the tuple de-structuring assumes they are returned in positions 1 and 3:

(, uint256 totalLocked, , uint256 totalStaked, , ) = ...

Verify the actual signature in IStakeController:

function getJurorBalance(address juror, uint96 courtID)
    external
    view
    returns (uint256 staked, uint256 locked, …)

If the order is (staked, locked, …), totalLocked and totalStaked are being swapped, letting insolvent jurors slip through.
Please cross-check and swap the indices if needed.


🏁 Script executed:

#!/bin/bash
# Locate and print the signature of getJurorBalance for confirmation
rg -n "function getJurorBalance" -A2

Length of output: 83834


🏁 Script executed:

#!/bin/bash
# Find the IStakeController interface to confirm getJurorBalance signature
rg -l "interface IStakeController" -n src/arbitration/interfaces

Length of output: 133


🏁 Script executed:

#!/bin/bash
# Search for IStakeController interface to confirm getJurorBalance signature
rg -n "interface IStakeController" -A2 contracts/src/arbitration

Length of output: 374


🏁 Script executed:

#!/bin/bash
# Retrieve the getJurorBalance signature from IStakeController
rg -n "function getJurorBalance" -A2 contracts/src/arbitration/interfaces/IStakeController.sol

Length of output: 186


🏁 Script executed:

#!/bin/bash
# Print the full signature of getJurorBalance including its returns
sed -n '160,200p' contracts/src/arbitration/interfaces/IStakeController.sol

Length of output: 1543


Ignore the getJurorBalance order mismatch – the tuple indices are correct

IConfirmed that IStakeController.getJurorBalance is declared as:

function getJurorBalance(
    address _juror,
    uint96 _courtID
) external view returns (
    uint256 availablePnk,   // 1
    uint256 lockedPnk,      // 2
    uint256 penaltyPnk,     // 3
    uint256 totalStaked,    // 4
    uint256 stakedInCourt,  // 5
    uint256 nbCourts        // 6
);

The destructuring in DisputeKitClassicBase.sol:

(, uint256 totalLocked, , uint256 totalStaked, , ) = 
    core.stakeController().getJurorBalance(_juror, courtID);

correctly maps lockedPnktotalLocked (2nd slot) and totalStakedtotalStaked (4th slot). No swap is needed.

Likely an incorrect or invalid review comment.

contracts/src/arbitration/KlerosCore.sol (1)

22-44: Confirm initializer versioning

This contract uses initializer, while other modules use reinitializer(n).
If an existing proxy has already consumed its initializer, this call will revert. Ensure the proxy is always fresh or switch to reinitializer(2) (or the next slot).

contracts/src/arbitration/KlerosCoreBase.sol (1)

779-805: Unchecked external call may revert entire penalty loop

vault.withdraw is wrapped in try/catch, good.
vault.transferReward (used both here and in _executeRewards) isn’t – a single vault failure would revert the whole execute() call and block dispute finalisation.
Wrap it similarly or make the vault return a boolean to keep liveness.

contracts/src/arbitration/StakeControllerBase.sol (1)

176-199: Gas/overflow safety in executeDelayedStakes loop

  1. actualIterations arithmetic can overflow for very large _iterations (> 2⁵⁶).
  2. Processing user-supplied _iterations without an upper cap can still make the call run out-of-gas even if _iterations is small but each inner call is expensive.

Consider:

  • Using unchecked with explicit bounds checks to save gas and avoid overflow.
  • Hard-cap actualIterations to e.g. 500 per call.

import { BigNumberish, toBigInt } from "ethers";

export const changeCurrencyRate = async (
core: KlerosCore | KlerosCoreNeo | KlerosCoreRuler | KlerosCoreUniversity,
core: KlerosCore | KlerosCoreNeo | KlerosCoreRuler | KlerosCoreUniversity | KlerosCore,
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

Fix duplicate type in parameter annotation.

The core parameter type annotation contains a duplicate KlerosCore type in the union.

Apply this diff to remove the duplicate:

-  core: KlerosCore | KlerosCoreNeo | KlerosCoreRuler | KlerosCoreUniversity | KlerosCore,
+  core: KlerosCore | KlerosCoreNeo | KlerosCoreRuler | KlerosCoreUniversity,
📝 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
core: KlerosCore | KlerosCoreNeo | KlerosCoreRuler | KlerosCoreUniversity | KlerosCore,
core: KlerosCore | KlerosCoreNeo | KlerosCoreRuler | KlerosCoreUniversity,
🤖 Prompt for AI Agents
In contracts/deploy/utils/klerosCoreHelper.ts at line 5, the type annotation for
the parameter 'core' includes a duplicate 'KlerosCore' in the union type. Remove
the repeated 'KlerosCore' so that each type appears only once in the union.

@@ -2,7 +1,8 @@
import { KlerosCore, KlerosCoreNeo, KlerosCoreRuler, KlerosCoreUniversity, KlerosCore } from "../../typechain-types";
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

Fix duplicate import causing syntax error.

The import statement contains a duplicate KlerosCore import, which will cause a compilation error.

Apply this diff to remove the duplicate:

-import { KlerosCore, KlerosCoreNeo, KlerosCoreRuler, KlerosCoreUniversity, KlerosCore } from "../../typechain-types";
+import { KlerosCore, KlerosCoreNeo, KlerosCoreRuler, KlerosCoreUniversity } from "../../typechain-types";
🧰 Tools
🪛 Biome (1.9.4)

[error] 1-1: Declarations inside of a import declaration may not have duplicates

a second declaration of KlerosCore is not allowed

KlerosCore is first declared here

(parse)

🤖 Prompt for AI Agents
In contracts/deploy/utils/klerosCoreHelper.ts at line 1, the import statement
includes a duplicate import of KlerosCore, causing a syntax error. Remove the
repeated KlerosCore from the import list so that each imported item appears only
once.

Comment on lines 29 to 40
enum StakingResult {
Successful,
StakingTransferFailed,
UnstakingTransferFailed,
CannotStakeInMoreCourts,
CannotStakeInThisCourt,
CannotStakeLessThanMinStake,
CannotStakeMoreThanMaxStakePerJuror,
CannotStakeMoreThanMaxTotalStaked,
CannotStakeZeroWhenNoStake
Delayed,
StakingTransferFailed, // DEPRECATED
UnstakingTransferFailed, // DEPRECATED
CannotStakeInMoreCourts, // DEPRECATED
CannotStakeInThisCourt, // DEPRECATED
CannotStakeLessThanMinStake, // DEPRECATED
CannotStakeMoreThanMaxStakePerJuror, // DEPRECATED
CannotStakeMoreThanMaxTotalStaked, // DEPRECATED
CannotStakeZeroWhenNoStake // DEPRECATED
}
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

LGTM: StakingResult enum updated for new architecture.

The addition of Delayed and deprecation of legacy values aligns with the new StakeController system. The Delayed result likely supports deferred stake execution during certain phases.

Verify that deprecated StakingResult values are not used in the new code:


🏁 Script executed:

#!/bin/bash
# Description: Check for usage of deprecated StakingResult values in the new codebase

# Search for usage of deprecated StakingResult values
echo "Checking for usage of deprecated StakingResult values..."
rg -A 2 "StakingResult\.(StakingTransferFailed|UnstakingTransferFailed|CannotStakeInMoreCourts|CannotStakeInThisCourt|CannotStakeLessThanMinStake|CannotStakeMoreThanMaxStakePerJuror|CannotStakeMoreThanMaxTotalStaked|CannotStakeZeroWhenNoStake)" --type sol

Length of output: 408


🏁 Script executed:

#!/bin/bash
# Description: Re-check usage of deprecated StakingResult values across all Solidity files

echo "Searching for deprecated StakingResult usages in .sol files..."
rg -n "StakingResult\.(StakingTransferFailed|UnstakingTransferFailed|CannotStakeInMoreCourts|CannotStakeInThisCourt|CannotStakeLessThanMinStake|CannotStakeMoreThanMaxStakePerJuror|CannotStakeMoreThanMaxTotalStaked|CannotStakeZeroWhenNoStake)" -g "*.sol" || true

Length of output: 2125


Update deprecated StakingResult usages

The verification shows that deprecated enum values are still referenced in the codebase. Please replace or remove these to align with the new StakeController model:

• contracts/src/arbitration/university/SortitionModuleUniversity.sol:153
• contracts/src/arbitration/university/KlerosCoreUniversity.sol:1056, 1060, 1075, 1081, 1091–1095

Ensure you migrate logic to use the new success/delayed result paths (or other appropriate outcomes) and drop reliance on the deprecated cases.

🤖 Prompt for AI Agents
In contracts/src/libraries/Constants.sol around lines 29 to 40, the
StakingResult enum has deprecated values that are still used in the codebase.
Locate all references to these deprecated enum values in
contracts/src/arbitration/university/SortitionModuleUniversity.sol at line 153
and in contracts/src/arbitration/university/KlerosCoreUniversity.sol between
lines 1056 and 1095. Replace or remove these deprecated enum usages by updating
the logic to use the new StakingResult values such as Successful or Delayed,
ensuring the code aligns with the new StakeController architecture and no longer
relies on deprecated cases.

@@ -2,7 +2,7 @@

pragma solidity 0.8.24;

import {KlerosCore, KlerosCoreBase, IDisputeKit, ISortitionModule} from "../KlerosCore.sol";
import {KlerosCore, KlerosCoreBase, IDisputeKit, IStakeController} from "../KlerosCore.sol";
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

IStakeController very likely sits in its own interface file, not in KlerosCore.sol.

Importing it from ../KlerosCore.sol will fail unless that file also declares the interface – which would be surprising given the new modular split.
Switch the import to the dedicated interface path to avoid a compile-time error.

-import {KlerosCore, KlerosCoreBase, IDisputeKit, IStakeController} from "../KlerosCore.sol";
+import {KlerosCore, KlerosCoreBase, IDisputeKit} from "../KlerosCore.sol";
+import {IStakeController} from "../interfaces/IStakeController.sol";
📝 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
import {KlerosCore, KlerosCoreBase, IDisputeKit, IStakeController} from "../KlerosCore.sol";
import {KlerosCore, KlerosCoreBase, IDisputeKit} from "../KlerosCore.sol";
import {IStakeController} from "../interfaces/IStakeController.sol";
🤖 Prompt for AI Agents
In contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol at line 5,
the import statement incorrectly imports IStakeController from KlerosCore.sol,
but IStakeController likely resides in its own interface file. Update the import
path to point to the dedicated interface file for IStakeController instead of
../KlerosCore.sol to prevent compile-time errors.

Comment on lines +22 to +34
/// @dev Initializer (constructor equivalent for upgradable contracts).
/// @param _governor The governor's address.
/// @param _pnk The address of the PNK token contract.
/// @param _stakeController The address of the stake controller.
/// @param _core The address of the KlerosCore contract.
function initialize(
address _governor,
IERC20 _pnk,
address _stakeController,
address _core
) external reinitializer(1) {
__VaultBase_initialize(_governor, _pnk, _stakeController, _core);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Use initializer for the first init – reinitializer(1) is misleading here.

This is the very first initialisation of the proxy; using reinitializer(1) suggests the contract was already initialised with version 0, which is not the case and can cause confusion when adding future re-initialisers.

-    ) external reinitializer(1) {
+    ) external initializer {

No functional change, but it aligns with OZ’s upgradeable-contract conventions.

📝 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
/// @dev Initializer (constructor equivalent for upgradable contracts).
/// @param _governor The governor's address.
/// @param _pnk The address of the PNK token contract.
/// @param _stakeController The address of the stake controller.
/// @param _core The address of the KlerosCore contract.
function initialize(
address _governor,
IERC20 _pnk,
address _stakeController,
address _core
) external reinitializer(1) {
__VaultBase_initialize(_governor, _pnk, _stakeController, _core);
}
/// @dev Initializer (constructor equivalent for upgradable contracts).
/// @param _governor The governor's address.
/// @param _pnk The address of the PNK token contract.
/// @param _stakeController The address of the stake controller.
/// @param _core The address of the KlerosCore contract.
function initialize(
address _governor,
IERC20 _pnk,
address _stakeController,
address _core
) external initializer {
__VaultBase_initialize(_governor, _pnk, _stakeController, _core);
}
🤖 Prompt for AI Agents
In contracts/src/arbitration/Vault.sol around lines 22 to 34, replace the
`reinitializer(1)` modifier on the initialize function with the `initializer`
modifier. This change correctly marks this function as the first initialization
step for the upgradeable contract, avoiding confusion about prior initialization
versions and aligning with OpenZeppelin's upgradeable contract conventions.

Comment on lines 1081 to 1111
function _setStake(address _account, uint96 _courtID, uint256 _newStake) internal returns (bool success) {
if (_courtID == FORKING_COURT || _courtID >= courts.length) {
_stakingFailed(_onError, StakingResult.CannotStakeInThisCourt); // Staking directly into the forking court is not allowed.
return false;
revert StakingNotPossibleInThisCourt();
}
if (_newStake != 0 && _newStake < courts[_courtID].minStake) {
_stakingFailed(_onError, StakingResult.CannotStakeLessThanMinStake); // Staking less than the minimum stake is not allowed.
return false;
revert StakingLessThanCourtMinStake();
}
(uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) = sortitionModule.setStake(
(uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) = stakeController.validateStake(
_account,
_courtID,
_newStake,
_alreadyTransferred
_newStake
);
if (stakingResult != StakingResult.Successful) {
_stakingFailed(_onError, stakingResult);
return false;
if (stakingResult == StakingResult.Delayed) {
return true;
}
success = true;
if (pnkDeposit > 0) {
if (!pinakion.safeTransferFrom(_account, address(this), pnkDeposit)) {
_stakingFailed(_onError, StakingResult.StakingTransferFailed);
return false;
try vault.deposit(_account, pnkDeposit) {} catch {
success = false;
}
}
if (pnkWithdrawal > 0) {
if (!pinakion.safeTransfer(_account, pnkWithdrawal)) {
_stakingFailed(_onError, StakingResult.UnstakingTransferFailed);
return false;
try vault.withdraw(_account, pnkWithdrawal) {} catch {
success = false;
}
}
return true;
}

/// @dev It may revert depending on the _onError parameter.
function _stakingFailed(OnError _onError, StakingResult _result) internal pure virtual {
if (_onError == OnError.Return) return;
if (_result == StakingResult.StakingTransferFailed) revert StakingTransferFailed();
if (_result == StakingResult.UnstakingTransferFailed) revert UnstakingTransferFailed();
if (_result == StakingResult.CannotStakeInMoreCourts) revert StakingInTooManyCourts();
if (_result == StakingResult.CannotStakeInThisCourt) revert StakingNotPossibleInThisCourt();
if (_result == StakingResult.CannotStakeLessThanMinStake) revert StakingLessThanCourtMinStake();
if (_result == StakingResult.CannotStakeZeroWhenNoStake) revert StakingZeroWhenNoStake();
if (success) {
stakeController.setStake(_account, _courtID, _newStake, pnkDeposit, pnkWithdrawal);
}
return success;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Partial side-effects leave vault & trees out-of-sync when a deposit/withdraw fails

_setStake

  1. executes vault.deposit and vault.withdraw inside try/catch,
  2. flips success to false on either failure, but
  3. still returns after leaving the vault in an intermediate state (e.g., deposit succeeded, withdrawal failed).
  4. Moreover, stakeController.setStake is not wrapped in try/catch; a revert here undoes none of the previous vault operations.

Consider an “all-or-nothing” approach:

-        success = true;
-        if (pnkDeposit > 0) {
-            try vault.deposit(_account, pnkDeposit) {} catch {
-                success = false;
-            }
-        }
-        if (pnkWithdrawal > 0) {
-            try vault.withdraw(_account, pnkWithdrawal) {} catch {
-                success = false;
-            }
-        }
-        if (success) {
-            stakeController.setStake(_account, _courtID, _newStake, pnkDeposit, pnkWithdrawal);
-        }
+        // optimistic – revert everything on first failure
+        if (pnkDeposit > 0) vault.deposit(_account, pnkDeposit);
+        if (pnkWithdrawal > 0) vault.withdraw(_account, pnkWithdrawal);
+
+        // update trees only after vault succeeded
+        stakeController.setStake(_account, _courtID, _newStake, pnkDeposit, pnkWithdrawal);
+        success = true;

This guarantees state consistency between the vault and the sortition trees.

📝 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 _setStake(address _account, uint96 _courtID, uint256 _newStake) internal returns (bool success) {
if (_courtID == FORKING_COURT || _courtID >= courts.length) {
_stakingFailed(_onError, StakingResult.CannotStakeInThisCourt); // Staking directly into the forking court is not allowed.
return false;
revert StakingNotPossibleInThisCourt();
}
if (_newStake != 0 && _newStake < courts[_courtID].minStake) {
_stakingFailed(_onError, StakingResult.CannotStakeLessThanMinStake); // Staking less than the minimum stake is not allowed.
return false;
revert StakingLessThanCourtMinStake();
}
(uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) = sortitionModule.setStake(
(uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) = stakeController.validateStake(
_account,
_courtID,
_newStake,
_alreadyTransferred
_newStake
);
if (stakingResult != StakingResult.Successful) {
_stakingFailed(_onError, stakingResult);
return false;
if (stakingResult == StakingResult.Delayed) {
return true;
}
success = true;
if (pnkDeposit > 0) {
if (!pinakion.safeTransferFrom(_account, address(this), pnkDeposit)) {
_stakingFailed(_onError, StakingResult.StakingTransferFailed);
return false;
try vault.deposit(_account, pnkDeposit) {} catch {
success = false;
}
}
if (pnkWithdrawal > 0) {
if (!pinakion.safeTransfer(_account, pnkWithdrawal)) {
_stakingFailed(_onError, StakingResult.UnstakingTransferFailed);
return false;
try vault.withdraw(_account, pnkWithdrawal) {} catch {
success = false;
}
}
return true;
}
/// @dev It may revert depending on the _onError parameter.
function _stakingFailed(OnError _onError, StakingResult _result) internal pure virtual {
if (_onError == OnError.Return) return;
if (_result == StakingResult.StakingTransferFailed) revert StakingTransferFailed();
if (_result == StakingResult.UnstakingTransferFailed) revert UnstakingTransferFailed();
if (_result == StakingResult.CannotStakeInMoreCourts) revert StakingInTooManyCourts();
if (_result == StakingResult.CannotStakeInThisCourt) revert StakingNotPossibleInThisCourt();
if (_result == StakingResult.CannotStakeLessThanMinStake) revert StakingLessThanCourtMinStake();
if (_result == StakingResult.CannotStakeZeroWhenNoStake) revert StakingZeroWhenNoStake();
if (success) {
stakeController.setStake(_account, _courtID, _newStake, pnkDeposit, pnkWithdrawal);
}
return success;
}
function _setStake(address _account, uint96 _courtID, uint256 _newStake) internal returns (bool success) {
if (_courtID == FORKING_COURT || _courtID >= courts.length) {
revert StakingNotPossibleInThisCourt();
}
if (_newStake != 0 && _newStake < courts[_courtID].minStake) {
revert StakingLessThanCourtMinStake();
}
(uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) = stakeController.validateStake(
_account,
_courtID,
_newStake
);
if (stakingResult == StakingResult.Delayed) {
return true;
}
// optimistic – revert everything on first failure
if (pnkDeposit > 0) vault.deposit(_account, pnkDeposit);
if (pnkWithdrawal > 0) vault.withdraw(_account, pnkWithdrawal);
// update trees only after vault succeeded
stakeController.setStake(_account, _courtID, _newStake, pnkDeposit, pnkWithdrawal);
success = true;
return success;
}
🤖 Prompt for AI Agents
In contracts/src/arbitration/KlerosCoreBase.sol around lines 1081 to 1111, the
_setStake function currently performs vault.deposit and vault.withdraw calls
inside try/catch blocks but allows partial success, leading to inconsistent
vault and stakeController states. To fix this, refactor the function to
implement an all-or-nothing approach by first validating all operations can
succeed, then performing deposit and withdrawal without try/catch, ensuring that
if any operation fails, the entire transaction reverts and no partial state
changes occur. Also, wrap stakeController.setStake in the same transaction flow
so that any revert there also rolls back vault changes, maintaining state
consistency between the vault and sortition trees.

Comment on lines +148 to +158
/// @inheritdoc IVault
function lockTokens(address _account, uint256 _amount) external virtual override onlyStakeController {
jurorBalances[_account].locked += _amount;
emit Lock(_account, _amount);
}

/// @inheritdoc IVault
function unlockTokens(address _account, uint256 _amount) external virtual override onlyStakeController {
jurorBalances[_account].locked -= _amount;
emit Unlock(_account, _amount);
}
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

Lack of invariant checks when locking / unlocking

lockTokens and unlockTokens blindly mutate locked. This allows the stake-controller to:

  • lock more than the juror’s deposited balance, or
  • unlock more than what is currently locked.

Both will revert due to under-/over-flow only at the second operation (when available balance is read) and produce confusing error sources.

Add explicit require-style guards:

require(jurorBalances[_account].deposited >= jurorBalances[_account].locked + _amount, "Lock exceeds deposit");
require(jurorBalances[_account].locked >= _amount, "Unlock exceeds locked");
🤖 Prompt for AI Agents
In contracts/src/arbitration/VaultBase.sol around lines 148 to 158, the
lockTokens and unlockTokens functions modify the locked balance without checks,
allowing locking beyond deposited balance or unlocking more than locked. Add
require statements in lockTokens to ensure jurorBalances[_account].deposited is
at least jurorBalances[_account].locked plus _amount, and in unlockTokens to
ensure jurorBalances[_account].locked is at least _amount, preventing invalid
state changes and providing clear error messages.

Comment on lines +132 to +146
function _withdraw(address _to, uint256 _amount) internal virtual returns (uint256 pnkAmount) {
if (_amount == 0) revert InvalidAmount();

JurorBalance storage balance = jurorBalances[_to];

// Check available balance (deposited - locked - penalties) for the user
uint256 available = getAvailableBalance(_to);
if (_amount > available) revert InsufficientAvailableBalance();

balance.deposited -= _amount;
pnk.safeTransfer(_to, _amount); // Vault sends PNK to user

emit Withdraw(_to, _amount);
return _amount;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Guard against re-entrancy in _withdraw

_withdraw decreases the user balance and immediately performs an external safeTransfer.
Add a simple ReentrancyGuard or switch to the pull mechanism (withdraw returns amount, user calls claim) to eliminate the remaining re-entrancy surface.
Even though PNK is assumed to be well-behaved, the vault should stay asset-agnostic.

🤖 Prompt for AI Agents
In contracts/src/arbitration/VaultBase.sol around lines 132 to 146, the
_withdraw function updates the user's balance and then calls an external
safeTransfer, which exposes a re-entrancy risk. To fix this, apply a
ReentrancyGuard modifier to the _withdraw function to prevent re-entrant calls,
or refactor the withdrawal logic into a two-step pull mechanism where _withdraw
only records the amount to withdraw and a separate claim function performs the
transfer. This ensures the contract remains asset-agnostic and secure against
re-entrancy attacks.

Comment on lines +104 to +121
/// @inheritdoc IVault
function deposit(address _from, uint256 _amount) external virtual override onlyCore {
_deposit(_from, _amount);
}

/// @dev Internal implementation of deposit.
/// @param _from The user address for the deposit.
/// @param _amount The amount of PNK to deposit.
function _deposit(address _from, uint256 _amount) internal virtual {
if (_amount == 0) revert InvalidAmount();

// Transfer PNK from the user to the vault
// The Vault must be approved by _from to transfer PNK to the vault
pnk.safeTransferFrom(_from, address(this), _amount);
jurorBalances[_from].deposited += _amount;

emit Deposit(_from, _amount);
}
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

Move state-mutations before external token transfer to follow CEI pattern

_deposit updates deposited after calling safeTransferFrom.
A malicious/non-standard ERC-20 could re-enter via the callback hooks available in some tokens, leaving state inconsistent.
Swap the order to “checks → effects → interactions” for consistency with the rest of the contract (e.g. _withdraw already follows that order).

-        pnk.safeTransferFrom(_from, address(this), _amount);
-        jurorBalances[_from].deposited += _amount;
+        jurorBalances[_from].deposited += _amount;
+        pnk.safeTransferFrom(_from, address(this), _amount);
📝 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
/// @inheritdoc IVault
function deposit(address _from, uint256 _amount) external virtual override onlyCore {
_deposit(_from, _amount);
}
/// @dev Internal implementation of deposit.
/// @param _from The user address for the deposit.
/// @param _amount The amount of PNK to deposit.
function _deposit(address _from, uint256 _amount) internal virtual {
if (_amount == 0) revert InvalidAmount();
// Transfer PNK from the user to the vault
// The Vault must be approved by _from to transfer PNK to the vault
pnk.safeTransferFrom(_from, address(this), _amount);
jurorBalances[_from].deposited += _amount;
emit Deposit(_from, _amount);
}
/// @dev Internal implementation of deposit.
/// @param _from The user address for the deposit.
/// @param _amount The amount of PNK to deposit.
function _deposit(address _from, uint256 _amount) internal virtual {
if (_amount == 0) revert InvalidAmount();
// Transfer PNK from the user to the vault
// The Vault must be approved by _from to transfer PNK to the vault
jurorBalances[_from].deposited += _amount;
pnk.safeTransferFrom(_from, address(this), _amount);
emit Deposit(_from, _amount);
}
🤖 Prompt for AI Agents
In contracts/src/arbitration/VaultBase.sol around lines 104 to 121, the _deposit
function updates the jurorBalances state after calling the external token
transfer function pnk.safeTransferFrom, which risks reentrancy issues. To fix
this, reorder the code to update the jurorBalances mapping before calling
safeTransferFrom, following the checks-effects-interactions pattern to prevent
state inconsistencies from malicious token contracts.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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

♻️ Duplicate comments (1)
contracts/src/arbitration/StakeControllerBase.sol (1)

269-274: Past security issue has been resolved ✓

The setStakeDelayed function now correctly includes the onlyByCore modifier, addressing the previous DOS attack vulnerability where anyone could push arbitrary entries into delayedStakes.

🧹 Nitpick comments (2)
contracts/src/arbitration/StakeControllerBase.sol (2)

279-279: Fix misleading comment

The comment mentions that the event name might be misleading but then incorrectly states "should be StakeLocked" when the event is already named StakeLocked.

-        emit StakeLocked(_account, _amount); // Event name might be misleading, should be StakeLocked. Preserved for compatibility if so.
+        emit StakeLocked(_account, _amount);

402-406: Minor optimization: Early return is redundant

The early return check for empty array at line 405 is unnecessary since the for loop at line 409 will naturally handle empty arrays by not executing any iterations.

 function _removeCourt(uint96[] storage _stakedCourts, uint96 _courtID) internal {
     uint256 length = _stakedCourts.length;
-    if (length == 0) {
-        return; // Nothing to remove
-    }
 
     uint256 courtIndexToRemove = type(uint256).max; // Sentinel value indicates not found
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 799dad1 and a050ca6.

📒 Files selected for processing (1)
  • contracts/src/arbitration/StakeControllerBase.sol (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (9)
  • 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: Header rules - kleros-v2-university
  • GitHub Check: Pages changed - kleros-v2-university
  • GitHub Check: contracts-testing
  • GitHub Check: SonarCloud
  • GitHub Check: Analyze (javascript)
🔇 Additional comments (3)
contracts/src/arbitration/StakeControllerBase.sol (3)

81-101: Well-structured initialization pattern

The initialization function properly uses the onlyInitializing modifier and correctly sets all storage variables, including setting delayedStakeReadIndex to 1 to skip index 0 as documented.


177-199: Robust error handling in delayed stake execution

The function properly validates phase and index bounds, uses try-catch to handle failures gracefully, and emits events for failed operations. The deletion of processed stakes prevents re-execution.


255-260: Efficient court management logic

The code correctly manages the stakedCourtIDs array by adding courts when staking from zero and removing them when unstaking to zero, maintaining an accurate list of courts where the juror has active stakes.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (1)
contracts/src/arbitration/KlerosCoreBase.sol (1)

1097-1109: Partial vault success can desynchronise state (repeat of earlier review)
Same issue as previously flagged: a failed deposit/withdraw keeps the vault and trees out-of-sync.

🧹 Nitpick comments (3)
contracts/src/arbitration/StakeControllerBase.sol (3)

202-206: Off-by-one makes _iterations > pending items revert

The actualIterations formula is overly tricky and can underflow when
delayedStakeWriteIndex == delayedStakeReadIndex + _iterations - 1.

A clearer, safer pattern:

uint256 remaining = delayedStakeWriteIndex - delayedStakeReadIndex + 1;
uint256 actual = _iterations > remaining ? remaining : _iterations;

290-301: Locked/unlocked stake events mis-named

emit StakeLocked / StakeUnlocked events are emitted inside lockStake/unlockStake but the event names declared are StakeLocked/StakeUnlocked.
The comment “Event name might be misleading” hints at legacy renaming – clean this up to avoid confusion for indexers.


424-447: _removeCourt is O(n) per delete – consider swap-and-pop with index map

Frequent stake changes can make this hot-path.
Storing mapping(uint96 => uint256 index) next to the array allows O(1) removals without scanning.

This saves gas for large multi-court jurors.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 51d7739 and 9abcf64.

📒 Files selected for processing (5)
  • contracts/src/arbitration/KlerosCoreBase.sol (19 hunks)
  • contracts/src/arbitration/SortitionSumTree.sol (1 hunks)
  • contracts/src/arbitration/StakeControllerBase.sol (1 hunks)
  • contracts/src/libraries/Constants.sol (1 hunks)
  • contracts/src/test/SortitionModuleMock.sol (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • contracts/src/test/SortitionModuleMock.sol
  • contracts/src/libraries/Constants.sol
⏰ Context from checks skipped due to timeout of 90000ms (15)
  • GitHub Check: Redirect rules - kleros-v2-testnet-devtools
  • GitHub Check: Redirect rules - kleros-v2-university
  • GitHub Check: Header rules - kleros-v2-testnet-devtools
  • GitHub Check: Header rules - kleros-v2-university
  • GitHub Check: Redirect rules - kleros-v2-testnet
  • GitHub Check: Pages changed - kleros-v2-testnet-devtools
  • GitHub Check: Redirect rules - kleros-v2-testnet
  • GitHub Check: Pages changed - kleros-v2-university
  • 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
  • GitHub Check: contracts-testing
  • GitHub Check: Analyze (javascript)
  • GitHub Check: SonarCloud
🔇 Additional comments (1)
contracts/src/arbitration/SortitionSumTree.sol (1)

248-260: 🛠️ Refactor suggestion

_stakePathIDToAccount assembly mis-aligns the recovered address

The loop copies the 20 address bytes starting at offset 0x0c, then mload(ptr) returns a left-aligned 32-byte word, so the resulting value contains 12 zero bytes at the end and only the high 20 bytes of the address – i.e., address(mload(ptr)) ≠ original address for most inputs.

A cheaper and safer alternative:

-        assembly {
-            // copy address bytes
-            ...
-            account := mload(ptr)
-        }
+        account = address(uint160(uint256(_stakePathID)));

This uses native slicing and avoids manual memory writes.

Likely an incorrect or invalid review comment.

Comment on lines 118 to 138
bytes32 stakePathID = _accountAndCourtIDToStakePathID(_account, _courtID);
bool finished = false;
uint96 currentCourtID = _courtID;
KlerosCoreBase core = stakeController.core();

while (!finished) {
_set(bytes32(uint256(currentCourtID)), _newStake, stakePathID);
if (currentCourtID == GENERAL_COURT) {
finished = true;
} else {
// Fetch parent court ID. Ensure core.courts() is accessible and correct.
(uint96 parentCourtID, , , , , , ) = core.courts(currentCourtID);
if (parentCourtID == currentCourtID) {
// Avoid infinite loop if parent is self (e.g. for general court already handled or misconfiguration)
finished = true;
} else {
currentCourtID = parentCourtID;
}
}
}
}
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

Parent-court stake is overwritten, breaking aggregate stake accounting

setStake propagates _newStake up the court hierarchy but passes the same _newStake each time.
When a juror stakes in two sibling courts, the second update will overwrite – not add to – the existing node in the parent tree, causing the juror’s total stake in the parent (and further ancestors) to be under-counted.

-            _set(bytes32(uint256(currentCourtID)), _newStake, stakePathID);
+            // Aggregate stake in ancestor courts.
+            uint256 aggregated = _newStake + stakeOf(bytes32(uint256(currentCourtID)), stakePathID);
+            _set(bytes32(uint256(currentCourtID)), aggregated, stakePathID);

A related edge-case: if the juror later stakes directly in the parent court, a different stakePathID will be used, duplicating the juror’s leaf and double-counting their weight.

Consider:

  1. Deriving stakePathID from the ancestor court inside the loop; and
  2. Storing a per-juror aggregation map to avoid O(n²) reads.
📝 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
bytes32 stakePathID = _accountAndCourtIDToStakePathID(_account, _courtID);
bool finished = false;
uint96 currentCourtID = _courtID;
KlerosCoreBase core = stakeController.core();
while (!finished) {
_set(bytes32(uint256(currentCourtID)), _newStake, stakePathID);
if (currentCourtID == GENERAL_COURT) {
finished = true;
} else {
// Fetch parent court ID. Ensure core.courts() is accessible and correct.
(uint96 parentCourtID, , , , , , ) = core.courts(currentCourtID);
if (parentCourtID == currentCourtID) {
// Avoid infinite loop if parent is self (e.g. for general court already handled or misconfiguration)
finished = true;
} else {
currentCourtID = parentCourtID;
}
}
}
}
bytes32 stakePathID = _accountAndCourtIDToStakePathID(_account, _courtID);
bool finished = false;
uint96 currentCourtID = _courtID;
KlerosCoreBase core = stakeController.core();
while (!finished) {
// Aggregate stake in ancestor courts.
uint256 aggregated = _newStake + stakeOf(bytes32(uint256(currentCourtID)), stakePathID);
_set(bytes32(uint256(currentCourtID)), aggregated, stakePathID);
if (currentCourtID == GENERAL_COURT) {
finished = true;
} else {
// Fetch parent court ID. Ensure core.courts() is accessible and correct.
(uint96 parentCourtID, , , , , , ) = core.courts(currentCourtID);
if (parentCourtID == currentCourtID) {
// Avoid infinite loop if parent is self (e.g. for general court already handled or misconfiguration)
finished = true;
} else {
currentCourtID = parentCourtID;
}
}
}
}
🤖 Prompt for AI Agents
In contracts/src/arbitration/SortitionSumTree.sol around lines 118 to 138, the
setStake function incorrectly propagates the same _newStake value up the court
hierarchy, overwriting parent court stakes instead of aggregating them, which
causes under-counting when a juror stakes in multiple sibling courts. To fix
this, modify the loop to derive the stakePathID based on the current ancestor
courtID inside the loop, and accumulate the stakes per juror in a mapping to
aggregate their total stake at each ancestor court, preventing overwrites and
double-counting. This approach ensures correct aggregation of stakes up the
hierarchy and avoids redundant reads.

Comment on lines 202 to 221
uint256 actualIterations = (delayedStakeReadIndex + _iterations) - 1 > delayedStakeWriteIndex
? (delayedStakeWriteIndex - delayedStakeReadIndex) + 1
: _iterations;
uint256 newDelayedStakeReadIndex = delayedStakeReadIndex + actualIterations;

for (uint256 i = delayedStakeReadIndex; i < newDelayedStakeReadIndex; i++) {
DelayedStake storage delayedStake = delayedStakes[i];
if (delayedStake.account == address(0)) continue;

// Let KlerosCore coordinate stake update and vault deposit/withdrawal.
try core.setStakeByController(delayedStake.account, delayedStake.courtID, delayedStake.stake) {
// NOP
} catch (bytes memory data) {
emit DelayedStakeSetFailed(data);
}
delete delayedStakes[i];
}
delayedStakeReadIndex = newDelayedStakeReadIndex;
}

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

executeDelayedStakes ignores failure signal – corrupted stakes remain

core.setStakeByController returns bool success, but the loop deletes the entry regardless of the value, making it impossible to retry failed stakes.

-            try core.setStakeByController(...) {
-                // NOP
-            } catch (bytes memory data) {
+            bool ok;
+            try core.setStakeByController(...) returns (bool _ok) {
+                ok = _ok;
+            } catch (bytes memory data) {
                 emit DelayedStakeSetFailed(data);
+                ok = false;
             }
-            delete delayedStakes[i];
+            if (ok) {
+                delete delayedStakes[i];
+            }

This ensures failed items stay queued for manual inspection or re-execution.

📝 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
uint256 actualIterations = (delayedStakeReadIndex + _iterations) - 1 > delayedStakeWriteIndex
? (delayedStakeWriteIndex - delayedStakeReadIndex) + 1
: _iterations;
uint256 newDelayedStakeReadIndex = delayedStakeReadIndex + actualIterations;
for (uint256 i = delayedStakeReadIndex; i < newDelayedStakeReadIndex; i++) {
DelayedStake storage delayedStake = delayedStakes[i];
if (delayedStake.account == address(0)) continue;
// Let KlerosCore coordinate stake update and vault deposit/withdrawal.
try core.setStakeByController(delayedStake.account, delayedStake.courtID, delayedStake.stake) {
// NOP
} catch (bytes memory data) {
emit DelayedStakeSetFailed(data);
}
delete delayedStakes[i];
}
delayedStakeReadIndex = newDelayedStakeReadIndex;
}
uint256 actualIterations = (delayedStakeReadIndex + _iterations) - 1 > delayedStakeWriteIndex
? (delayedStakeWriteIndex - delayedStakeReadIndex) + 1
: _iterations;
uint256 newDelayedStakeReadIndex = delayedStakeReadIndex + actualIterations;
for (uint256 i = delayedStakeReadIndex; i < newDelayedStakeReadIndex; i++) {
DelayedStake storage delayedStake = delayedStakes[i];
if (delayedStake.account == address(0)) continue;
bool ok;
// Let KlerosCore coordinate stake update and vault deposit/withdrawal.
try core.setStakeByController(delayedStake.account, delayedStake.courtID, delayedStake.stake) returns (bool _ok) {
ok = _ok;
} catch (bytes memory data) {
emit DelayedStakeSetFailed(data);
ok = false;
}
if (ok) {
delete delayedStakes[i];
}
}
delayedStakeReadIndex = newDelayedStakeReadIndex;
}
🤖 Prompt for AI Agents
In contracts/src/arbitration/StakeControllerBase.sol around lines 202 to 221,
the executeDelayedStakes function calls core.setStakeByController which returns
a bool indicating success, but the code deletes the delayedStake entry
regardless of success, preventing retries on failure. Modify the loop to check
the returned bool success value; only delete delayedStakes[i] and advance
delayedStakeReadIndex if the call succeeds. If it fails, emit the failure event
but keep the stake entry intact so it can be retried later.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (4)
contracts/deploy/00-home-chain-arbitration.ts (4)

60-66: Variable name mismatches new architecture
sortitionModuleV2 is a leftover from the removed SortitionModule and now wraps a SortitionSumTree. Rename for clarity to avoid mental friction:

-const sortitionModuleV2 = await deployUpgradable(deployments, "SortitionSumTree", {
+const sortitionSumTree = await deployUpgradable(deployments, "SortitionSumTree", {

Then update the downstream references.


92-108: Duplicate name collision in the deployment artefacts
You register the proxy under "KlerosCore" twice (klerosCoreV2 deploy + later getContract<KlerosCore>("KlerosCore")).
If the first call is skipped because an artefact with that name already exists, klerosCoreV2 will be undefined, breaking every subsequent reference.

Safer pattern:

const klerosCoreProxy = await deployUpgradable(/* … */, { deterministicDeployment: false, log: true });
const core = klerosCoreProxy;

…and avoid a second deployments.get call altogether.


129-136: Silent catch hides failed currency-rate setup
Catching any error and only console.error-ing swallows revert reasons and will happily continue with an inconsistent state.

Either:

await changeCurrencyRate(/* … */);   // let it throw

or log and process.exit(1) so CI fails and the deployment script halts.


145-150: Console logs use proxies but omit implementations – useful for verification
Consider logging both proxy and implementation addresses so on-chain verifiers (e.g. Tenderly / Sourcify) can hook automatically:

console.log(`📦 Vault(proxy): ${vault.address} impl: ${(await deployments.get("Vault_Implementation")).address}`);

This tiny addition saves time when debugging main-net deployments.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9abcf64 and 23a5504.

📒 Files selected for processing (1)
  • contracts/deploy/00-home-chain-arbitration.ts (3 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (16)
  • GitHub Check: Redirect rules - kleros-v2-neo
  • GitHub Check: Header rules - kleros-v2-neo
  • GitHub Check: Pages changed - kleros-v2-neo
  • GitHub Check: Redirect rules - kleros-v2-testnet
  • GitHub Check: Redirect rules - kleros-v2-university
  • GitHub Check: Redirect rules - kleros-v2-testnet
  • GitHub Check: Header rules - kleros-v2-testnet
  • GitHub Check: Header rules - kleros-v2-university
  • GitHub Check: Header rules - kleros-v2-testnet
  • GitHub Check: Analyze (javascript)
  • GitHub Check: Pages changed - kleros-v2-testnet
  • GitHub Check: Pages changed - kleros-v2-university
  • GitHub Check: Pages changed - kleros-v2-testnet
  • GitHub Check: contracts-testing
  • GitHub Check: SonarCloud
  • GitHub Check: Mend Security Check
🔇 Additional comments (1)
contracts/deploy/00-home-chain-arbitration.ts (1)

68-86: Passing predicted addresses to the initializer can hard-revert
If Vault, StakeController, or SortitionSumTree call into each other in their initializers (even a simple require(isContract(addr))), the deployment will revert because the predicted contract’s code is still empty at that block.
Please double-check initializers and, if necessary, split the process:

  1. Deploy contracts with temporary zero/placeholder addresses.
  2. After all proxies exist, call explicit setX functions (already present on many Kleros contracts) to wire them together.

That keeps deployment atomic without assuming silent initializers.

Comment on lines +42 to +53
// Calculate future addresses for circular dependencies
const nonce = await ethers.provider.getTransactionCount(deployer);

const vaultAddress = getContractAddress(deployer, nonce + 1); // deployed on the 2nd tx (nonce+1): Vault Impl tx, Vault Proxy tx
console.log("calculated future Vault address for nonce %d: %s", nonce + 1, vaultAddress);

const stakeControllerAddress = getContractAddress(deployer, nonce + 5); // deployed on the 6th tx (nonce+5): Vault Impl tx, Vault Proxy tx, SortitionModule Impl tx, SortitionModule Proxy tx,, StakeController Impl tx, StakeController Proxy tx
console.log("calculated future StakeController address for nonce %d: %s", nonce + 5, stakeControllerAddress);

const klerosCoreAddress = getContractAddress(deployer, nonce + 7); // deployed on the 8th tx (nonce+7): Vault Impl tx, Vault Proxy tx, SortitionModule Impl tx, SortitionModule Proxy tx, StakeController Impl tx, StakeController Proxy tx, KlerosCore Impl tx, KlerosCore Proxy tx
console.log("calculated future KlerosCore address for nonce %d: %s", nonce + 7, klerosCoreAddress);

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Manual nonce arithmetic is fragile and will break on the first extra transaction
Hard-coding nonce + {1‥7} assumes that no additional tx slips in between the calculated point and the actual deployment (e.g. a library deploy, an implicit OZ admin‐upgrade deploy, or even a gas‐price retry).
Once the assumption is violated every pre-computed address is wrong, bricking the whole deployment.

Consider replacing the manual math with:

  1. predictAddress helpers available in OZ Upgrades, or
  2. deploying the dependency first (e.g. Vault impl only), fetching its actual proxy address with deployments.get and passing that concrete value to the next deploy step.

This turns the circular-dependency problem into a deterministic two-phase deploy without relying on fragile nonce bookkeeping.

🤖 Prompt for AI Agents
In contracts/deploy/00-home-chain-arbitration.ts around lines 42 to 53, the
current approach uses manual nonce arithmetic to predict future contract
addresses, which is fragile and can break if any extra transactions occur before
deployment. To fix this, replace the manual nonce calculations with a more
reliable method such as using OpenZeppelin Upgrades' `predictAddress` helpers or
deploy each dependency sequentially, retrieving the actual deployed proxy
address via `deployments.get` before passing it to the next deployment step.
This ensures deterministic address resolution without relying on fragile nonce
assumptions.

Comment on lines +54 to +65
const vault = await deployUpgradable(deployments, "Vault", {
from: deployer,
args: [deployer, pnk.target, stakeControllerAddress, klerosCoreAddress],
log: true,
}); // nonce (implementation), nonce + 1 (proxy)

// Deploy SortitionSumTree
const sortitionModuleV2 = await deployUpgradable(deployments, "SortitionSumTree", {
from: deployer,
args: [deployer, stakeControllerAddress],
log: true,
}); // nonce + 2 (implementation), nonce + 3 (proxy)
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

pnk.target may point to the implementation, not the proxy
In v6 ethers, .target is the address of the contract instance you created, in this case the implementation returned by OZ’s deployer, not the TransparentUpgradeableProxy users will interact with.

The Vault’s constructor is expected to hold a reference to the tradeable PNK token (proxy). Pass the proxy address instead:

-args: [deployer, pnk.target, stakeControllerAddress, klerosCoreAddress],
+const pnkAddress = await pnk.getAddress(); // proxy address
+args: [deployer, pnkAddress, stakeControllerAddress, klerosCoreAddress],

Same remark applies later to rng.target and core.target. Mixing .target, .address, and .getAddress() is error-prone; pick one consistently (ideally .getAddress() which always gives the proxy).

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In contracts/deploy/00-home-chain-arbitration.ts around lines 54 to 65, the code
uses pnk.target which points to the implementation contract address instead of
the proxy address required by the Vault constructor. To fix this, replace
pnk.target with the proxy address of the PNK token, ideally using
pnk.getAddress() for consistency and correctness. Apply the same change for
rng.target and core.target later in the file, ensuring all contract addresses
passed are proxy addresses obtained via .getAddress() to avoid confusion and
errors.

@jaybuidl jaybuidl force-pushed the refactor/staking-contracts branch from cf077e4 to d99def1 Compare June 9, 2025 21:58
Copy link
Contributor

@coderabbitai coderabbitai bot left a 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

♻️ Duplicate comments (5)
contracts/deploy/00-home-chain-arbitration.ts (2)

42-52: 🛠️ Refactor suggestion

Manual nonce arithmetic issue already flagged – still unresolved

Pre-computing addresses with nonce + {1‥7} is as brittle as before. Any extra tx (e.g., library, retry, admin-upgrade) invalidates every predicted address and bricks the deployment. Please switch to deterministic helpers (predictAddress) or a two-phase deployment approach as proposed earlier.


54-58: ⚠️ Potential issue

Using .target gives implementation addresses, not proxies

pnk.target, rng.target, and similar properties still refer to the implementation contract returned by OZ upgrades, not the proxy the rest of the system expects. Use await pnk.getAddress() / await rng.getAddress() consistently.

Also applies to: 79-82

contracts/src/arbitration/SortitionSumTree.sol (1)

121-137: ⚠️ Potential issue

Parent-court stake aggregation bug persists

_set(bytes32(uint256(currentCourtID)), _newStake, …) overwrites the ancestor node instead of aggregating sibling stakes. Staking twice in two sibling courts will under-count in parents and break weighted draws.

contracts/src/arbitration/KlerosCoreBase.sol (1)

1086-1109: ⚠️ Potential issue

Two long-standing staking issues still present

  1. stakingResult == Delayed path silently returns without queuing the stake (setStakeDelayed), so the request is lost.
  2. Deposit / withdraw are performed in isolation; a failure in either leaves Vault & trees out-of-sync. Adopt the all-or-nothing flow previously suggested.
contracts/src/arbitration/StakeController.sol (1)

96-116: ⚠️ Potential issue

Initializer still lacks basic sanity checks

Zero addresses or nonsense timing values can brick the controller. Add simple require guards (_governor != address(0), _maxDrawingTime > _minStakingTime, etc.) to fail fast.

🧹 Nitpick comments (1)
contracts/src/arbitration/SortitionSumTree.sol (1)

248-260: Safe-packing nit – zero the high 12 bytes

_stakePathIDToAccount leaves the high 12 bytes of the 32-byte word uninitialised before mload, relying on previous memory being zero. Zero them explicitly to avoid edge-case garbage.

-            let ptr := mload(0x40)
+            let ptr := mload(0x40)
+            mstore(ptr, 0)         // clear 32 bytes
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 23a5504 and d99def1.

📒 Files selected for processing (5)
  • contracts/deploy/00-home-chain-arbitration.ts (3 hunks)
  • contracts/src/arbitration/KlerosCoreBase.sol (19 hunks)
  • contracts/src/arbitration/SortitionSumTree.sol (1 hunks)
  • contracts/src/arbitration/StakeController.sol (1 hunks)
  • contracts/src/test/SortitionModuleMock.sol (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • contracts/src/test/SortitionModuleMock.sol
⏰ Context from checks skipped due to timeout of 90000ms (4)
  • GitHub Check: SonarCloud
  • GitHub Check: contracts-testing
  • GitHub Check: Analyze (javascript)
  • GitHub Check: Mend Security Check

Comment on lines +320 to +338
function setJurorInactive(address _account) external override onlyByCore returns (uint256 pnkToWithdraw) {
JurorStake storage currentJurorStake = jurorStakes[_account];
uint96[] storage stakedCourtIDs = currentJurorStake.stakedCourtIDs;
while (stakedCourtIDs.length > 0) {
uint96 courtID = stakedCourtIDs[0];
uint256 currentStakeInCourt = currentJurorStake.stakes[courtID];
if (phase == Phase.staking) {
setStake(_account, courtID, 0, 0, currentStakeInCourt);
} else {
setStakeDelayed(_account, courtID, 0);
}
}
if (phase == Phase.staking) {
pnkToWithdraw = vault.getAvailableBalance(_account);
emit JurorSetInactive(_account, false);
} else {
pnkToWithdraw = 0;
emit JurorSetInactive(_account, true);
}
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

Infinite loop when marking juror inactive outside staking phase

setJurorInactive iterates while (stakedCourtIDs.length > 0) but, in non-staking phases, setStakeDelayed does not shrink stakedCourtIDs. The length never decreases → infinite loop & out-of-gas.

Fix idea:

-            } else {
-                setStakeDelayed(_account, courtID, 0);
+            } else {
+                setStakeDelayed(_account, courtID, 0);
+                _removeCourt(stakedCourtIDs, courtID); // ensure progress

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In contracts/src/arbitration/StakeController.sol around lines 320 to 338, the
while loop in setJurorInactive causes an infinite loop outside the staking phase
because setStakeDelayed does not remove court IDs from stakedCourtIDs, so its
length never decreases. To fix this, modify the loop to remove the processed
courtID from stakedCourtIDs after calling setStakeDelayed, ensuring the array
shrinks and the loop terminates properly.

Copy link

codeclimate bot commented Jun 9, 2025

Code Climate has analyzed commit 6c28f88 and detected 0 issues on this pull request.

View more on Code Climate.

Copy link

sonarqubecloud bot commented Jun 9, 2025

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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

♻️ Duplicate comments (8)
contracts/src/arbitration/Vault.sol (1)

31-31: Use initializer for the first init – reinitializer(1) is misleading here.

This is the very first initialisation of the proxy; using reinitializer(1) suggests the contract was already initialised with version 0, which is not the case and can cause confusion when adding future re-initialisers.

-    ) external reinitializer(1) {
+    ) external initializer {
contracts/src/arbitration/VaultNeo.sol (1)

23-23: totalDeposited can get out-of-sync

The counter is only updated in _deposit / _withdraw. If the base contract slashes or force-withdraws funds (e.g., penalties), the cap invariant breaks and future deposits may be blocked.
Ensure every path that changes jurorBalances[..].deposited also updates totalDeposited, or compute the total on-demand.

contracts/src/arbitration/SortitionSumTree.sol (1)

118-138: Parent-court stake is overwritten, breaking aggregate stake accounting

setStake propagates _newStake up the court hierarchy but passes the same _newStake each time.
When a juror stakes in two sibling courts, the second update will overwrite – not add to – the existing node in the parent tree, causing the juror's total stake in the parent (and further ancestors) to be under-counted.

-            _set(bytes32(uint256(currentCourtID)), _newStake, stakePathID);
+            // Aggregate stake in ancestor courts.
+            uint256 aggregated = _newStake + stakeOf(bytes32(uint256(currentCourtID)), stakePathID);
+            _set(bytes32(uint256(currentCourtID)), aggregated, stakePathID);
contracts/src/arbitration/KlerosCoreBase.sol (3)

1091-1094: Delayed stake never enqueued – stake silently discarded

_setStake exits early when stakingResult == StakingResult.Delayed, but it does not call stakeController.setStakeDelayed, so the juror's request is lost.

-        if (stakingResult == StakingResult.Delayed) {
-            return true;
-        }
+        if (stakingResult == StakingResult.Delayed) {
+            stakeController.setStakeDelayed(_account, _courtID, _newStake);
+            return true;
+        }

1095-1109: Partial side-effects leave vault & trees out-of-sync when a deposit/withdraw fails

_setStake executes vault.deposit and vault.withdraw inside try/catch, flips success to false on either failure, but still returns after leaving the vault in an intermediate state. Moreover, stakeController.setStake is not wrapped in try/catch; a revert here undoes none of the previous vault operations.

Consider an "all-or-nothing" approach:

-        success = true;
-        if (pnkDeposit > 0) {
-            try vault.deposit(_account, pnkDeposit) {} catch {
-                success = false;
-            }
-        }
-        if (pnkWithdrawal > 0) {
-            try vault.withdraw(_account, pnkWithdrawal) {} catch {
-                success = false;
-            }
-        }
-        if (success) {
-            stakeController.setStake(_account, _courtID, _newStake, pnkDeposit, pnkWithdrawal);
-        }
+        // optimistic – revert everything on first failure
+        if (pnkDeposit > 0) vault.deposit(_account, pnkDeposit);
+        if (pnkWithdrawal > 0) vault.withdraw(_account, pnkWithdrawal);
+
+        // update trees only after vault succeeded
+        stakeController.setStake(_account, _courtID, _newStake, pnkDeposit, pnkWithdrawal);
+        success = true;

778-803: Penalty flow unlocks then withdraws – may withdraw more than available

setJurorPenalty unlocks _penalty PNK, reducing the juror's locked balance. Immediately after, inactive-juror handling may attempt vault.withdraw(pnkToWithdraw), which can include the just-unlocked amount. If applyPenalty already burned the balance, withdraw can revert or drain zero-balance.

Recommend:

  1. Compute available = vault.getAvailableBalance(account) after the penalty;
  2. Cap pnkToWithdraw = min(pnkToWithdraw, available);
  3. Abort withdrawal when available == 0.
contracts/src/arbitration/StakeController.sol (2)

96-116: Missing parameter validation in initialize function

The initialize function lacks validation for critical parameters, allowing zero addresses and invalid timing values. This matches a previously identified concern.

Add require statements to ensure all address parameters are non-zero and that timing parameters are valid:

 function initialize(
     address _governor,
     KlerosCoreBase _core,
     IVault _vault,
     ISortitionSumTree _sortition,
     uint256 _minStakingTime,
     uint256 _maxDrawingTime,
     RNG _rng,
     uint256 _rngLookahead
 ) external initializer {
+    require(_governor != address(0) && address(_core) != address(0) && address(_vault) != address(0), "Zero addr");
+    require(address(_sortition) != address(0) && address(_rng) != address(0), "Zero addr");
+    require(_rngLookahead > 0 && _maxDrawingTime > _minStakingTime, "Bad timing params");
     governor = _governor;
     core = _core;
     vault = _vault;

320-339: Potential infinite loop in setJurorInactive function

The while loop in setJurorInactive can cause an infinite loop outside the staking phase because setStakeDelayed does not remove court IDs from stakedCourtIDs, preventing the loop from terminating.

The issue occurs because when phase != Phase.staking, setStakeDelayed is called but doesn't modify the stakedCourtIDs array, while setStake (called during staking phase) properly removes courts via _removeCourt. This creates an infinite loop during non-staking phases.

🧹 Nitpick comments (5)
contracts/src/arbitration/SortitionSumTree.sol (1)

59-61: Remove duplicate constructor section comment.

There are two identical "Constructor" section comments (lines 59-61 and 60-61).

-    // ************************************* //
-    // *            Constructor            * //
-    // ************************************* //
-
     // ************************************* //
     // *            Constructor            * //
     // ************************************* //
contracts/src/arbitration/StakeController.sol (4)

202-204: Optimize arithmetic calculation in executeDelayedStakes

The arithmetic for calculating actualIterations can be simplified and made more readable.

-        uint256 actualIterations = (delayedStakeReadIndex + _iterations) - 1 > delayedStakeWriteIndex
-            ? (delayedStakeWriteIndex - delayedStakeReadIndex) + 1
-            : _iterations;
+        uint256 maxPossibleIterations = delayedStakeWriteIndex - delayedStakeReadIndex + 1;
+        uint256 actualIterations = _iterations > maxPossibleIterations ? maxPossibleIterations : _iterations;

212-217: Consider logging more details for failed delayed stakes

The current error handling only emits the raw error data, which may not provide sufficient context for debugging failed stake operations.

             try core.setStakeByController(delayedStake.account, delayedStake.courtID, delayedStake.stake) {
                 // NOP
             } catch (bytes memory data) {
-                emit DelayedStakeSetFailed(data);
+                emit DelayedStakeSetFailed(delayedStake.account, delayedStake.courtID, delayedStake.stake, data);
             }

Note: This would require updating the event definition in the interface as well.


300-300: Remove outdated comment about event name

The comment suggests the event name might be misleading and should be StakeLocked, but the actual event being emitted is already StakeLocked, making this comment incorrect.

         vault.lockTokens(_account, _amount);
-        emit StakeLocked(_account, _amount); // Event name might be misleading, should be StakeLocked. Preserved for compatibility if so.
+        emit StakeLocked(_account, _amount);

164-167: Consider phase validation in changeRandomNumberGenerator

The function only handles the generating phase but doesn't validate or handle other phases that might also need special consideration when changing RNG parameters.

Consider adding validation or handling for other phases:

 function changeRandomNumberGenerator(RNG _rng, uint256 _rngLookahead) external onlyByGovernor {
     rng = _rng;
     rngLookahead = _rngLookahead;
     if (phase == Phase.generating) {
         rng.requestRandomness(block.number + rngLookahead);
         randomNumberRequestBlock = block.number;
+    } else if (phase == Phase.drawing && randomNumber == 0) {
+        // Reset if we're in drawing phase but don't have a random number yet
+        phase = Phase.staking;
+        lastPhaseChange = block.timestamp;
+        emit NewPhase(phase);
     }
 }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cb7d9b4 and 6c28f88.

📒 Files selected for processing (9)
  • contracts/src/arbitration/KlerosCore.sol (1 hunks)
  • contracts/src/arbitration/KlerosCoreBase.sol (19 hunks)
  • contracts/src/arbitration/KlerosCoreNeo.sol (2 hunks)
  • contracts/src/arbitration/SortitionSumTree.sol (1 hunks)
  • contracts/src/arbitration/StakeController.sol (1 hunks)
  • contracts/src/arbitration/Vault.sol (1 hunks)
  • contracts/src/arbitration/VaultBase.sol (1 hunks)
  • contracts/src/arbitration/VaultNeo.sol (1 hunks)
  • contracts/src/arbitration/view/KlerosCoreSnapshotProxy.sol (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • contracts/src/arbitration/KlerosCore.sol
  • contracts/src/arbitration/KlerosCoreNeo.sol
  • contracts/src/arbitration/VaultBase.sol
⏰ Context from checks skipped due to timeout of 90000ms (15)
  • 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: Redirect rules - kleros-v2-university
  • GitHub Check: Redirect rules - kleros-v2-testnet
  • GitHub Check: Header rules - kleros-v2-testnet
  • GitHub Check: Header rules - kleros-v2-testnet
  • GitHub Check: Header rules - kleros-v2-university
  • GitHub Check: Pages changed - kleros-v2-testnet
  • GitHub Check: Pages changed - kleros-v2-testnet
  • GitHub Check: Pages changed - kleros-v2-university
  • GitHub Check: contracts-testing
  • GitHub Check: Analyze (javascript)
  • GitHub Check: SonarCloud
🔇 Additional comments (19)
contracts/src/arbitration/view/KlerosCoreSnapshotProxy.sol (3)

5-5: LGTM - Interface import updated correctly.

The import has been properly updated from ISortitionModule to IStakeController to align with the new architecture.


8-8: LGTM - Function signature updated correctly.

The interface method has been updated from sortitionModule() to stakeController() with the appropriate return type change.


70-70:

✅ Verification successful

Verify tuple destructuring matches new interface.

The tuple destructuring has been updated to extract totalStaked from a different position. Ensure this matches the return structure of IStakeController.getJurorBalance().


🏁 Script executed:

#!/bin/bash
# Description: Verify the return structure of getJurorBalance in IStakeController interface
# Expected: Should show the tuple structure and position of totalStaked

ast-grep --pattern $'function getJurorBalance($_, $_) external view returns ($_);'

Length of output: 83


🏁 Script executed:

#!/bin/bash
# Locate the IStakeController interface file
echo "Searching for IStakeController interface..."
rg -l "^interface IStakeController" -n

# Extract the getJurorBalance function signature across the repo
echo "Extracting getJurorBalance signature..."
rg "function getJurorBalance" -A2 -n

Length of output: 85057


All clear: tuple destructuring is correct
The proxy’s balanceOf now calls core.sortitionModule().getJurorBalance(_account, 0), which returns

(uint256 totalStaked, uint256 totalLocked, uint256 stakedInCourt, uint256 nbCourts)

The current destructuring

(totalStaked, , , )

correctly extracts the totalStaked value. No changes needed.

contracts/src/arbitration/VaultNeo.sol (4)

58-58: LGTM - Appropriate reinitializer version.

Using reinitializer(2) is correct here as this contract extends the base vault functionality with additional features.


104-106: LGTM - NFT gating logic is correct.

The NFT eligibility check correctly validates that the depositor owns at least one NFT when the depositNft is configured.


109-114: LGTM - Per-user deposit limit enforced correctly.

The logic correctly checks current user deposits plus new amount against the maximum limit.


117-119: LGTM - Total deposit cap enforced correctly.

The total deposit cap check is implemented properly before allowing the deposit.

contracts/src/arbitration/SortitionSumTree.sol (3)

71-71: LGTM - Correct use of initializer for first initialization.

Unlike the Vault contracts, this correctly uses initializer for the first initialization.


103-110: LGTM - Tree creation logic is sound.

The validation checks for existing trees and minimum K value are appropriate, and the initial root node setup is correct.


154-156: LGTM - Proper handling of empty trees.

The function correctly returns address(0) when no jurors are staked in the tree.

contracts/src/arbitration/KlerosCoreBase.sol (7)

7-8: LGTM - Interface imports updated correctly.

The imports have been properly updated to use the new IStakeController and IVault interfaces.


96-97: LGTM - Storage variables updated for new architecture.

The storage has been correctly updated to use stakeController and vault instead of the deprecated sortition module.


180-183: LGTM - Access control modifier updated correctly.

The modifier has been appropriately updated from onlySortitionModule to onlyStakeController.


475-481: LGTM - Function renamed and signature updated appropriately.

The function has been correctly renamed from setStakeBySortitionModule to setStakeByController with appropriate access control.


611-611: LGTM - Stake locking updated to use stakeController.

The call has been correctly updated to use stakeController.lockStake instead of the old sortition module.


851-851: LGTM - Stake unlocking updated to use stakeController.

The call has been correctly updated to use stakeController.unlockStake.


858-858: LGTM - Reward transfer updated to use vault.

The reward transfer has been correctly updated to use vault.transferReward.

contracts/src/arbitration/StakeController.sol (2)

423-447: Well-implemented array element removal

The _removeCourt function correctly implements the swap-and-pop pattern for efficient array element removal while maintaining O(1) complexity.


246-247:

❌ Incorrect review comment

Verify MAX_STAKE_PATHS constant is properly imported

The code references MAX_STAKE_PATHS constant but it's not defined in this contract. Ensure this constant is properly imported from the Constants library.


🏁 Script executed:

#!/bin/bash
# Description: Verify MAX_STAKE_PATHS constant is defined in Constants library
# Expected: Find the constant definition in the imported Constants file

rg -A 2 "MAX_STAKE_PATHS" contracts/src/libraries/Constants.sol

Length of output: 229


🏁 Script executed:

#!/bin/bash
# Check if Constants library is imported in StakeController.sol
rg -n "import.*Constants" contracts/src/arbitration/StakeController.sol

# Display the beginning of the file to inspect imports
head -n 40 contracts/src/arbitration/StakeController.sol

Length of output: 1632


No missing import for MAX_STAKE_PATHS

The MAX_STAKE_PATHS constant is defined in contracts/src/libraries/Constants.sol (uint256 constant MAX_STAKE_PATHS = 4;) and is properly imported in StakeController.sol on line 13. No changes are necessary.

Likely an incorrect or invalid review comment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
1 participant