Skip to content

Conversation

jaybuidl
Copy link
Member

@jaybuidl jaybuidl commented Aug 8, 2025

PR-Codex overview

This PR focuses on refactoring the governance structure in multiple smart contracts, changing references from governor to owner, and updating the Solidity pragma version. It also introduces a new IRNG interface and modifies various contract initializers and methods accordingly.

Detailed summary

  • Changed governor to owner in multiple contracts.
  • Updated Solidity pragma from ^0.8.24 to >=0.8.0 <0.9.0.
  • Introduced IRNG interface for random number generation.
  • Modified initializers to accept owner instead of governor.
  • Updated various method signatures and access control modifiers.
  • Refactored deployment scripts for RNG and Chainlink RNG.
  • Adjusted test cases for new contract structures and dependencies.

The following files were skipped due to too many changes: contracts/test/arbitration/dispute-kit-gated.ts, contracts/src/arbitration/KlerosCoreNeo.sol, contracts/scripts/changeOwner.ts, contracts/src/arbitration/PolicyRegistry.sol, contracts/test/integration/index.ts, contracts/deployments/disputeKitsViem.ts, web/src/pages/Profile/Stakes/Header.tsx, web/src/hooks/useVotingContext.tsx, contracts/test/evidence/index.ts, contracts/src/arbitration/arbitrables/DisputeResolver.sol, contracts/src/arbitration/SortitionModuleNeo.sol, contracts/src/arbitration/interfaces/IDisputeKit.sol, contracts/src/rng/RNGWithFallback.sol, contracts/test/arbitration/staking-neo.ts, contracts/src/arbitration/arbitrables/ArbitrableExample.sol, contracts/src/kleros-v1/kleros-liquid/KlerosLiquidToV2Governor.sol, contracts/test/foundry/KlerosCore_RNG.t.sol, contracts/test/proxy/index.ts, contracts/deploy/00-home-chain-arbitration-neo.ts, contracts/test/foundry/KlerosCore_Drawing.t.sol, web/src/hooks/useTokenAddressValidation.ts, contracts/src/rng/RandomizerRNG.sol, contracts/deploy/00-home-chain-arbitration.ts, contracts/src/arbitration/devtools/KlerosCoreRuler.sol, contracts/src/test/SortitionTreesMock.sol, contracts/src/rng/BlockhashRNG.sol, contracts/test/foundry/KlerosCore_Disputes.t.sol, .github/workflows/contracts-testing.yml, contracts/CHANGELOG.md, contracts/src/gateway/ForeignGateway.sol, web/src/pages/Resolver/Parameters/Court.tsx, contracts/src/arbitration/dispute-kits/DisputeKitShutter.sol, contracts/src/arbitration/dispute-kits/DisputeKitGatedShutter.sol, contracts/src/gateway/HomeGateway.sol, contracts/test/foundry/KlerosCore_Initialization.t.sol, contracts/src/rng/ChainlinkRNG.sol, contracts/test/foundry/KlerosCore_TestBase.sol, contracts/src/libraries/SortitionTrees.sol, contracts/src/arbitration/evidence/ModeratedEvidenceModule.sol, contracts/src/rng/ChainlinkConsumerBaseV2Plus.sol, contracts/src/arbitration/university/SortitionModuleUniversity.sol, contracts/test/rng/index.ts, contracts/src/kleros-v1/kleros-liquid-xdai/xKlerosLiquidV2.sol, contracts/src/arbitration/KlerosGovernor.sol, contracts/test/foundry/KlerosCore_Governance.t.sol, yarn.lock, contracts/test/foundry/KlerosCore_Voting.t.sol, contracts/test/foundry/KlerosCore_Appeals.t.sol, contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol, contracts/test/foundry/KlerosCore_Staking.t.sol, contracts/src/arbitration/university/KlerosCoreUniversity.sol, contracts/test/sortition/index.ts, contracts/test/arbitration/dispute-kit-shutter.ts, contracts/src/arbitration/KlerosCoreBase.sol, contracts/src/arbitration/SortitionModuleBase.sol, contracts/test/foundry/KlerosCore_Execution.t.sol, contracts/audit/METRICS.md, contracts/audit/METRICS.html

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

Summary by CodeRabbit

  • New Features

    • Multiple dispute‑kit support with on‑chain discovery; RNG now uses VRF with automatic fallback; juror PNK rewards can be staked.
  • Improvements

    • UI: multi‑kit voting, token‑gate validation, new Stakes header, formatted numbers, timeline/labels tweaks; site robots header and updated changelog.
  • Bug Fixes

    • Preserve Shutter auto‑reveal to prevent premature phase transitions.
  • Dependency & Tooling

    • Solidity/Hardhat/Foundry upgrades, viem added as a peer dependency, deployment/test tooling refined.
  • Tests

    • Large expansion of RNG, drawing, staking, voting, appeals, governance and integration tests.

jaybuidl and others added 25 commits July 23, 2025 15:58
Feat: Dispute Kits helper in contracts package
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Enabled Hardhat viaIR compilation with solc v0.8.30
chore: viaIR compilation enabled for Foundry with explicit solc v0.8.30
Copy link

netlify bot commented Aug 8, 2025

Deploy Preview for kleros-v2-testnet ready!

Name Link
🔨 Latest commit 98f7eac
🔍 Latest deploy log https://app.netlify.com/projects/kleros-v2-testnet/deploys/68b9a9a9b6800900085bc5fe
😎 Deploy Preview https://deploy-preview-2076--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
Contributor

coderabbitai bot commented Aug 8, 2025

Walkthrough

Wide-scope refactor: rename governor→owner and replace string requires with typed errors; introduce IRNG and RNGWithFallback with blockhash fallback; extract SortitionTrees and change sortition/staking APIs; split coherence into reward/penalty with ONE_BASIS_POINT; add Viem dispute-kit helper; many deployment, test, and frontend updates.

Changes

Cohort / File(s) Change Summary
Changelog & release
contracts/CHANGELOG.md
Add 0.13.0 (top) and 0.12.0 entries; update anchors.
Viem dispute-kit helper & scripts
contracts/deployments/disputeKitsViem.ts, contracts/deployments/index.ts, contracts/scripts/getDisputeKits.ts
New viem-based helper and CLI script to fetch DisputeKitCreated logs, map IDs→addresses and export types.
RNG interface, fallback & implementations
contracts/src/rng/IRNG.sol, contracts/src/rng/RNGWithFallback.sol, contracts/src/rng/*, contracts/src/test/RNGMock.sol, contracts/test/**
Add IRNG (0-arg request/receive); add RNGWithFallback (primary RNG + timeout fallback to blockhash), migrate Chainlink/Randomizer/Blockhash/Incremental to IRNG, add RNGMock and tests; change deployments/tests to consumer/owner model and time-based readiness.
Sortition library & API surface
contracts/src/libraries/SortitionTrees.sol, contracts/src/test/SortitionTreesMock.sol, contracts/src/test/SortitionModuleMock.sol, contracts/src/arbitration/SortitionModule*.sol, contracts/src/arbitration/SortitionModuleBase.sol
Extract SortitionTrees library, introduce TreeKey/CourtID types; change createTree/draw signatures to use uint96 courtID and return fromSubcourtID; add validateStake(..., bool), setStakePenalty, setStakeReward, forcedUnstake*, getJurorBalance.
Core, dispute-kit & coherence changes
contracts/src/arbitration/KlerosCore*.sol, contracts/src/arbitration/dispute-kits/*, contracts/src/arbitration/interfaces/IDisputeKit.sol, contracts/src/libraries/Constants.sol
Replace ALPHA_DIVISOR with ONE_BASIS_POINT; split coherence API into getDegreeOfCoherenceReward/getDegreeOfCoherencePenalty; record drawnJurorFromCourtIDs; add jumpDisputeKitID/jump APIs; route PNK rewards to staking via setStakeReward/penalty paths.
Ownership & typed errors (widespread)
contracts/src/**, contracts/scripts/*
Mass rename governorowner, onlyByGovernoronlyByOwner, changeGovernorchangeOwner; replace string require messages with custom errors; remove changeGovernor task and add change-owner task.
Deployments & scripts
contracts/deploy/*, contracts/scripts/*, contracts/deploy/00-*.ts, contracts/deploy/00-rng*.ts
Introduce/use RNGWithFallback, change consumer wiring (changeConsumer), migrate many deploy scripts to typed ethers.getContract<T>, remove obsolete RNG scripts, adjust DK constructor args (jump DK id) and enablement flows.
Tooling & compiler config
contracts/hardhat.config.ts, contracts/foundry.toml, contracts/.solcover.js, contracts/scripts/coverage.sh, contracts/package.json, package.json, cspell.json
Pin solc to 0.8.30, enable viaIR (env-driven), increase optimizer runs, add foundry profile, set irMinimum in solcover, add viem peerDependency & resolution, add IRNG to spell dictionary.
Tests: JS/TS & Foundry
contracts/test/**, contracts/test/foundry/**
Convert test contract retrievals to typed ethers.getContract<T>, update RNG flows to zero-arg API and time-based advances (remove lookahead mining), assert custom errors, and add extensive Foundry suites (RNG, Drawing, Voting, Staking, Appeals, Execution, Governance, Initialization).
Frontend: token-gate validation & UI
web/src/hooks/useTokenAddressValidation.ts, web/src/pages/Resolver/Parameters/Court.tsx, web/src/pages/Resolver/NavigationButtons/NextButton.tsx, web/src/context/NewDisputeContext.tsx, web/src/utils/extradataToTokenInfo.ts
Add debounced viem-backed ERC20/ERC721/ERC1155 validation hooks, UI validation feedback components, persist isTokenGateValid in dispute context, guard Next button for gated kits, and make extraDataToTokenInfo return optional.
Frontend: voting & Shutter
web/src/hooks/useVotingContext.tsx, web/src/pages/Cases/CaseDetails/Voting/Shutter/*
Support multiple dispute kits in voting context, refactor Shutter rendering to render commit/reveal independently, add REACT_APP_SHUTTER_API guard in commit flow.
UI, timeline, labels & docs
web/src/pages/Cases/CaseDetails/Timeline.tsx, web/src/components/DisputeView/CardLabels/index.tsx, web/src/public/llms.txt, web/netlify.toml
Switch timeline to period-index model, adjust commit visibility logic, update label copy, add llms.txt, and set Netlify header.
Profile UI: Stakes
web/src/pages/Profile/Stakes/*, web/src/pages/Profile/index.tsx, removed web/src/pages/Profile/Courts/Header.tsx
Add new Stakes header component showing available/effective/locked stakes, wire into Profile, and remove old Courts Header.
Misc & cleanup
various contracts/*.ts, tests, small UI files
Replace runtime casts with generic ethers.getContract<T>, update imports, add generateMetrics.sh, add small UI/formatting fixes, and remove unused deployment scripts.

Sequence Diagram(s)

sequenceDiagram
    actor DevScript as getDisputeKits.ts
    participant Client as Viem PublicClient
    participant Helper as disputeKitsViem.getDisputeKits
    participant Chain as KlerosCore (on-chain)

    note over DevScript,Client: init client via ARBITRUM_SEPOLIA_RPC
    DevScript->>Helper: getDisputeKits(client, "devnet")
    Helper->>Chain: query DisputeKitCreated events (filter)
    Chain-->>Helper: event logs
    Helper->>Helper: decode events → id→address map
    Helper->>Helper: correlate with local artifacts (isGated/isShutter)
    Helper-->>DevScript: return DisputeKitByIds
Loading
sequenceDiagram
    participant Sort as SortitionModule
    participant RF as RNGWithFallback
    participant P as PrimaryRNG (IRNG)
    participant BH as BlockhashRNG

    Sort->>RF: requestRandomness()
    RF->>P: requestRandomness()
    Note over P: primary RNG fulfills asynchronously
    Sort->>RF: receiveRandomness()
    alt primary ready (non-zero)
        RF->>P: receiveRandomness()
        P-->>RF: random
        RF-->>Sort: return primary random
    else primary timed out or returned 0
        RF->>BH: compute fallback from recent blockhash
        RF-->>Sort: return fallback random (emit RNGFallback)
    end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • RNG Fallback #2054 — IRNG & RNGWithFallback: introduces IRNG, RNGWithFallback, and consumer/owner RNG wiring; directly overlaps RNG interface, implementations, and tests.
  • Sortition trees extracted to a library #2113 — SortitionTrees extraction: adds SortitionTrees library and updates sortition APIs (createTree/draw signatures and TreeKey/CourtID types); directly overlaps sortition refactor.
  • Contracts: multi-dimensional coherence #2090 — Coherence API & basis-point refactor: splits coherence into reward/penalty APIs and introduces ONE_BASIS_POINT; directly overlaps core/dispute-kit coherence changes.

Poem

I hopped through logs at break of day,
fetched kits and IDs along the way.
RNGs that fallback when answers delay,
owners and errors now lead the way.
A rabbit cheers — code stitched and gay 🐇

✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dev

🪧 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.
    • 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.
  • 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 the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit 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

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • 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 Aug 8, 2025

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

Name Link
🔨 Latest commit 98f7eac
🔍 Latest deploy log https://app.netlify.com/projects/kleros-v2-university/deploys/68b9a9a973b2570008dd22d9

Copy link

netlify bot commented Aug 8, 2025

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

Name Link
🔨 Latest commit 98f7eac
🔍 Latest deploy log https://app.netlify.com/projects/kleros-v2-neo-devtools/deploys/68b9a9a97216be000829d466
😎 Deploy Preview https://deploy-preview-2076--kleros-v2-neo-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 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: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
contracts/test/arbitration/dispute-kit-gated.ts (1)

3-12: Resolve PNK name collision (type vs value)

Importing PNK (contract type) and declaring const PNK = (...) creates a value-namespace conflict and can break type elision. Use a type-only import with an alias and rename the helper.

-import {
-  PNK,
-  KlerosCore,
-  SortitionModule,
-  IncrementalNG,
-  DisputeKitGated,
-  TestERC20,
-  TestERC721,
-  TestERC1155,
-} from "../../typechain-types";
+import type {
+  PNK as PNKToken,
+  KlerosCore,
+  SortitionModule,
+  IncrementalNG,
+  DisputeKitGated,
+  TestERC20,
+  TestERC721,
+  TestERC1155,
+} from "../../typechain-types";
-  const PNK = (amount: BigNumberish) => toBigInt(amount) * 10n ** 18n;
+  const toPNK = (amount: BigNumberish) => toBigInt(amount) * 10n ** 18n;
-  let pnk: PNK;
+  let pnk: PNKToken;
-  const minStake = PNK(200);
+  const minStake = toPNK(200);
-    pnk = await ethers.getContract<PNK>("PNK");
+    pnk = await ethers.getContract<PNKToken>("PNK");

Also applies to: 24-25, 30-31, 40-41, 51-51

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

73-75: Missing access control on reinitializer allows anyone to set wNative once.

Any account can call reinitialize() (version 6) and change wNative the first time after upgrade. Restrict to owner.

-    function reinitialize(address _wNative) external reinitializer(6) {
+    function reinitialize(address _wNative) external onlyByOwner reinitializer(6) {
         wNative = _wNative;
     }
contracts/src/arbitration/SortitionModuleNeo.sol (1)

88-90: Per-court cap check is incorrect; uses total staked instead of court stake.

juror.stakedPnk + stakeChange enforces a global cap, but the variable name/comment say “in a court.” Use _newStake (or stakeOf(_account, _courtID)), not total.

-            if (juror.stakedPnk + stakeChange > maxStakePerJuror) {
+            // Enforce per-court cap
+            if (_newStake > maxStakePerJuror) {
                 return (0, 0, StakingResult.CannotStakeMoreThanMaxStakePerJuror);
             }
♻️ Duplicate comments (4)
contracts/CHANGELOG.md (3)

27-27: Clarify exact pragma range and actual scope (not “interfaces only”).

The note should specify the range and include all modules that adopted it (interfaces and RNG contracts).

- - Widen the allowed solc version to any v0.8.x for the interfaces only ([#2083](https://github.com/kleros/kleros-v2/issues/2083))
+ - Widen the allowed pragma to `>=0.8.0 <0.9.0` for interfaces and RNG contracts ([#2083](https://github.com/kleros/kleros-v2/issues/2083))

29-29: Fix constant name: ONE_BASIS_POINT (singular).

- - Consolidate the constant `ALPHA_DIVISOR` with `ONE_BASIS_POINTS` ([#2090](https://github.com/kleros/kleros-v2/issues/2090))
+ - Replace `ALPHA_DIVISOR` with `ONE_BASIS_POINT` ([#2090](https://github.com/kleros/kleros-v2/issues/2090))

7-7: Avoid broken release anchors for an unpublished tag (0.13.0).

Keep the note “Not published yet” but comment out the anchor link until the tag exists.

-## [0.13.0] - 2025-08-07 (Not published yet)
+## [0.13.0] - 2025-08-07 (Not published yet)
@@
-[0.13.0]: https://github.com/kleros/kleros-v2/releases/tag/@kleros%[email protected]
+<!-- [0.13.0]: (add after publishing) -->

Run to confirm before merging:

#!/bin/bash
set -euo pipefail
for tag in \
  "https://github.com/kleros/kleros-v2/releases/tag/@kleros%[email protected]" \
  "https://github.com/kleros/kleros-v2/releases/tag/@kleros%[email protected]"
do
  code=$(curl -s -o /dev/null -w "%{http_code}" "$tag")
  printf "%-120s %s\n" "$tag" "$code"
done

Also applies to: 154-155

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

137-140: Address previous review comment on notJumped modifier

The notJumped modifier still accesses disputes without verifying active status. Consider adding the active-dispute guard as suggested in the previous review to prevent accidental reads of uninitialized local disputes.

🧹 Nitpick comments (15)
contracts/test/arbitration/dispute-kit-gated.ts (3)

112-112: Avoid hardcoding gasLimit in tests

Let ethers estimate gas to catch regressions; add a small buffer only if needed.

-        .setStake(Courts.GENERAL, thousandPNK(10), { gasLimit: 500000 })
+        // Prefer estimation; fallback to a small safety buffer if CI flakiness appears.
+        // const gas = await core.connect(juror).setStake.estimateGas(Courts.GENERAL, thousandPNK(10));
+        // await core.connect(juror).setStake(Courts.GENERAL, thousandPNK(10), { gasLimit: gas + 50_000n });
+        .setStake(Courts.GENERAL, thousandPNK(10))

115-115: Avoid magic numbers for courtId

Use Courts.GENERAL instead of 1 for consistency and resilience.

-      expect(await sortitionModule.getJurorBalance(juror.address, 1)).to.deep.equal([
+      expect(await sortitionModule.getJurorBalance(juror.address, Courts.GENERAL)).to.deep.equal([

64-64: Ensure owner context for RNG change

changeRandomNumberGenerator is owner-gated; call it explicitly as the deployer signer to avoid flaky defaults. Local rg search returned only ABI metadata (no other two-arg call sites found).

-    await sortitionModule.changeRandomNumberGenerator(rng.target).then((tx) => tx.wait());
+    await sortitionModule
+      .connect(await ethers.getSigner(deployer))
+      .changeRandomNumberGenerator(rng.target)
+      .then((tx) => tx.wait());
contracts/CHANGELOG.md (3)

15-15: Tighten wording and use the canonical EIP link.

Replace “in order to comply” with “to comply” and link to the official EIP page.

- - **Breaking:** Rename `governor` to `owner` in order to comply with the lightweight ownership standard [ERC-5313](https://eipsinsight.com/ercs/erc-5313) ([#2112](https://github.com/kleros/kleros-v2/issues/2112))
+ - **Breaking:** Rename `governor` to `owner` to comply with the lightweight ownership standard [ERC‑5313](https://eips.ethereum.org/EIPS/eip-5313) ([#2112](https://github.com/kleros/kleros-v2/issues/2112))

21-21: Typo: “bypass” instead of “by-pass”.

- - Allow stake changes to by-pass delayed stakes when initiated by the SortitionModule by setting the `_noDelay` parameter to `true` in `SortitionModule.validateStake()` ([#2107](https://github.com/kleros/kleros-v2/issues/2107))
+ - Allow stake changes to bypass delayed stakes when initiated by the SortitionModule by setting `_noDelay = true` in `SortitionModule.validateStake()` ([#2107](https://github.com/kleros/kleros-v2/issues/2107))

39-39: Improve phrasing for the period transition.

- - Do not pass to Voting period if all the commits are cast because it breaks the current Shutter auto-reveal process. ([#2085](https://github.com/kleros/kleros-v2/issues/2085))
+ - Do not advance to the Voting period when all commits are cast to preserve the current Shutter auto‑reveal process. ([#2085](https://github.com/kleros/kleros-v2/issues/2085))
contracts/src/arbitration/KlerosCoreNeo.sol (2)

96-98: Emit an event when (un)whitelisting arbitrables.

A small event improves auditability and tooling integrations.

-    function changeArbitrableWhitelist(address _arbitrable, bool _allowed) external onlyByOwner {
-        arbitrableWhitelist[_arbitrable] = _allowed;
-    }
+    function changeArbitrableWhitelist(address _arbitrable, bool _allowed) external onlyByOwner {
+        arbitrableWhitelist[_arbitrable] = _allowed;
+        emit ArbitrableWhitelistChanged(_arbitrable, _allowed);
+    }

Add outside this hunk:

event ArbitrableWhitelistChanged(address indexed arbitrable, bool allowed);

12-12: Version string may be stale.

version = "0.10.0" looks outdated relative to 0.13.0 release notes. If this constant is surfaced to clients, consider bumping.

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

65-71: Emit events for governance parameter changes.

Helps monitoring and reproducibility of off-chain configs.

-    function changeMaxStakePerJuror(uint256 _maxStakePerJuror) external onlyByOwner {
-        maxStakePerJuror = _maxStakePerJuror;
-    }
+    function changeMaxStakePerJuror(uint256 _maxStakePerJuror) external onlyByOwner {
+        maxStakePerJuror = _maxStakePerJuror;
+        emit MaxStakePerJurorChanged(_maxStakePerJuror);
+    }
@@
-    function changeMaxTotalStaked(uint256 _maxTotalStaked) external onlyByOwner {
-        maxTotalStaked = _maxTotalStaked;
-    }
+    function changeMaxTotalStaked(uint256 _maxTotalStaked) external onlyByOwner {
+        maxTotalStaked = _maxTotalStaked;
+        emit MaxTotalStakedChanged(_maxTotalStaked);
+    }

Add outside this hunk:

event MaxStakePerJurorChanged(uint256 maxStakePerJuror);
event MaxTotalStakedChanged(uint256 maxTotalStaked);

77-82: Missing doc for _noDelay in _validateStake signature.

Add a brief natspec note explaining how it bypasses delayed stakes when set by SortitionModule.


10-10: Version string may be stale.

version = "0.9.0" appears out of sync with package release notes. Consider updating if this is user-facing.

contracts/src/arbitration/university/KlerosCoreUniversity.sol (1)

775-798: Good improvement: Enhanced penalty application flow.

The penalty application now:

  1. Uses the juror's origin court ID from drawnJurorFromCourtIDs
  2. Returns the updated PNK balance and court stake from setStakePenalty
  3. Properly handles inactive jurors or those below minimum stake

This provides better granularity in tracking where penalties are applied.

Consider adding a comment explaining why penalties are applied to the origin court rather than the dispute's court, as this might not be immediately obvious to future maintainers.

contracts/src/arbitration/interfaces/IDisputeKit.sol (3)

119-123: Clarify the timing semantics of earlyCourtJump.

Specify whether this indicates a jump for the next round vs. current, to avoid Core-side ambiguity.

-/// @return Whether the dispute is jumping to a parent court or not.
+/// @return Whether the next round will jump to the parent court (early jump) as determined by this dispute kit.

3-3: Tighten IDisputeKit.sol pragma to project‐standard 0.8.24
At contracts/src/arbitration/interfaces/IDisputeKit.sol line 3:

-pragma solidity >=0.8.0 <0.9.0;
+pragma solidity >=0.8.24 <0.9.0;

(Optional: other arbitration interfaces—IEvidence.sol, ISortitionModule.sol, IDisputeTemplateRegistry.sol, IArbitratorV2.sol, IArbitrableV2.sol—also use >=0.8.0 <0.9.0; consider aligning them too.)


124-131: Remove inline TODO and document invariants for getNbVotesAfterAppeal

Remove the inline // TODO and clarify behavior in NatSpec:

-    ) external view returns (uint256); // TODO: remove previousDisputeKit
+    ) external view returns (uint256);
 /// @param _previousDisputeKit The previous Dispute Kit.
 /// @param _currentNbVotes The number of votes before the appeal.
+/// @notice Must return a value ≥ `_currentNbVotes`.
+/// @notice `_previousDisputeKit` may equal the current kit; handle both same-kit and migration cases.
 /// @return The number of votes after the appeal.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 361fa55 and 953bd95.

📒 Files selected for processing (12)
  • contracts/CHANGELOG.md (2 hunks)
  • contracts/src/arbitration/KlerosCoreBase.sol (24 hunks)
  • contracts/src/arbitration/KlerosCoreNeo.sol (5 hunks)
  • contracts/src/arbitration/SortitionModuleBase.sol (21 hunks)
  • contracts/src/arbitration/SortitionModuleNeo.sol (5 hunks)
  • contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol (15 hunks)
  • contracts/src/arbitration/interfaces/IDisputeKit.sol (4 hunks)
  • contracts/src/arbitration/interfaces/ISortitionModule.sol (3 hunks)
  • contracts/src/arbitration/university/KlerosCoreUniversity.sol (25 hunks)
  • contracts/src/arbitration/university/SortitionModuleUniversity.sol (10 hunks)
  • contracts/test/arbitration/dispute-kit-gated.ts (2 hunks)
  • contracts/test/foundry/KlerosCore_Execution.t.sol (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • contracts/test/foundry/KlerosCore_Execution.t.sol
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-09-03T19:34:58.018Z
Learnt from: jaybuidl
PR: kleros/kleros-v2#2107
File: contracts/src/arbitration/university/KlerosCoreUniversity.sol:1083-1092
Timestamp: 2025-09-03T19:34:58.018Z
Learning: KlerosCoreUniversity and SortitionModuleUniversity do not have phases, unlike KlerosCoreBase and SortitionModuleBase. Therefore, validateStake in the University contracts will never return StakingResult.Delayed, only Successful or other failure states.

Applied to files:

  • contracts/src/arbitration/university/SortitionModuleUniversity.sol
  • contracts/src/arbitration/university/KlerosCoreUniversity.sol
📚 Learning: 2024-11-19T05:31:48.701Z
Learnt from: Harman-singh-waraich
PR: kleros/kleros-v2#1744
File: web/src/hooks/useGenesisBlock.ts:9-31
Timestamp: 2024-11-19T05:31:48.701Z
Learning: In `useGenesisBlock.ts`, within the `useEffect` hook, the conditions (`isKlerosUniversity`, `isKlerosNeo`, `isTestnetDeployment`) are mutually exclusive, so multiple imports won't execute simultaneously, and race conditions are not a concern.

Applied to files:

  • contracts/test/arbitration/dispute-kit-gated.ts
📚 Learning: 2024-11-19T16:31:08.965Z
Learnt from: jaybuidl
PR: kleros/kleros-v2#1746
File: contracts/config/courts.v2.mainnet-neo.json:167-170
Timestamp: 2024-11-19T16:31:08.965Z
Learning: In the court hierarchy, child courts' `minStake` must be greater than or equal to their parent court's `minStake`.

Applied to files:

  • contracts/src/arbitration/SortitionModuleBase.sol
📚 Learning: 2024-11-19T16:31:08.965Z
Learnt from: jaybuidl
PR: kleros/kleros-v2#1746
File: contracts/config/courts.v2.mainnet-neo.json:167-170
Timestamp: 2024-11-19T16:31:08.965Z
Learning: In `contracts/config/courts.v2.mainnet-neo.json`, the `minStake` parameter is denominated in PNK, not ETH.

Applied to files:

  • contracts/src/arbitration/university/KlerosCoreUniversity.sol
🧬 Code graph analysis (1)
contracts/test/arbitration/dispute-kit-gated.ts (2)
contracts/deploy/utils/index.ts (1)
  • PNK (40-40)
contracts/deploy/utils/deployTokens.ts (2)
  • deployERC721 (55-67)
  • deployERC1155 (69-81)
🪛 LanguageTool
contracts/CHANGELOG.md

[style] ~15-~15: Consider a more concise word here.
Context: ...reaking:** Rename governor to owner in order to comply with the lightweight ownership s...

(IN_ORDER_TO_PREMIUM)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: contracts-testing
  • GitHub Check: SonarCloud
  • GitHub Check: Analyze (javascript)
  • GitHub Check: Mend Security Check
  • GitHub Check: Analyze (javascript)
  • GitHub Check: SonarCloud
  • GitHub Check: contracts-testing
🔇 Additional comments (72)
contracts/test/arbitration/dispute-kit-gated.ts (3)

50-54: Typed getContract usages look good

Strong typing for DisputeKitGated/PNK/DAI/Core/SortitionModule improves safety. LGTM.


62-62: Typed RNG retrieval LGTM

Correctly fetching IncrementalNG with generics.


68-68: Typed NFT contract retrievals LGTM

Safer than untyped fetches. Nice.

Also applies to: 71-71

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

31-45: Initialize with owner: ABI-safe rename looks good.


81-84: Upgrade authorization guarded by owner: OK.


110-112: External call in eligibility check: confirm trusted NFT.

jurorNft.balanceOf(msg.sender) is an external call before state changes. If jurorNft were set to a malicious contract, it could reenter. If not already guaranteed elsewhere, consider restricting jurorNft to known-good implementations and/or adding a reentrancy guard to setters that could change it.

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

29-47: Initializer rename and IRNG wiring look consistent.


95-103: Apply totalStaked update only after successful validation
Move the mutation of totalStaked to after invoking super._validateStake and guard it on stakingResult == StakingResult.Successful && phase == Phase.staking to prevent inconsistent state.

 (pnkDeposit, pnkWithdrawal, stakingResult) = super._validateStake(_account, _courtID, _newStake, _noDelay);
-if (phase == Phase.staking) {
-    totalStaked = stakeIncrease ? totalStaked + stakeChange : totalStaked - stakeChange;
-}
+if (stakingResult == StakingResult.Successful && phase == Phase.staking) {
+    totalStaked = stakeIncrease ? totalStaked + stakeChange : totalStaked - stakeChange;
+}
contracts/src/arbitration/university/SortitionModuleUniversity.sol (9)

32-32: Owner-based governance transition completed

The governance model has been successfully migrated from governor to owner, consistent with the broader refactoring across the codebase.


69-77: Improved error handling with custom errors

The access control modifiers now use custom errors (OwnerOnly, KlerosCoreOnly) instead of require statements, which is more gas-efficient and provides better error information.


90-93: Owner parameter in initialization

The initializer correctly accepts _owner parameter instead of _governor, maintaining consistency with the governance migration.


142-144: Enhanced validateStake with delay control

The addition of _noDelay parameter allows bypassing delayed stake logic when needed. Based on the retrieved learning, the University contracts don't have phases, so this parameter enables fine-grained control over stake validation.


187-243: Comprehensive stake management API

The new stake management functions provide excellent granular control:

  • setStake delegates to internal _setStake
  • setStakePenalty handles penalty application with proper bounds checking
  • setStakeReward deposits rewards with validation
  • Internal _setStake centralizes stake updates and event emission

The implementation properly handles edge cases like zero penalties and zero rewards.


306-323: Enhanced unstaking capabilities

The renamed forcedUnstakeAllCourts (from setJurorInactive) and new forcedUnstake function provide both comprehensive and targeted unstaking capabilities, improving juror management flexibility.


335-335: Improved withdrawal validation

Using custom error NotEligibleForWithdrawal() instead of require statement is more gas-efficient and provides better error information.


348-354: Enhanced draw function with subcourt tracking

The draw function now returns fromSubcourtID in addition to drawnAddress, enabling provenance tracking consistent with the interface changes.


428-430: Proper error declarations

All custom errors are properly declared at the end of the contract, following Solidity conventions.

contracts/src/arbitration/interfaces/ISortitionModule.sol (4)

3-3: Consistent pragma version update

The pragma version has been updated to >=0.8.0 <0.9.0, maintaining consistency with other interface files.


18-23: Enhanced validateStake with delay control

The addition of _noDelay parameter provides necessary control over stake timing behavior, allowing bypassing of delayed stake logic when appropriate.


33-43: Comprehensive stake management interface

The new stake management functions replace and expand upon the previous limited interface:

  • setStakePenalty replaces penalizeStake with court-specific functionality
  • setStakeReward enables reward staking
  • forcedUnstakeAllCourts replaces setJurorInactive with clearer naming
  • forcedUnstake provides court-specific unstaking

These changes provide much more granular control over stake management.


51-55: Enhanced draw function signature

The draw function now returns both drawnAddress and fromSubcourtID, enabling better tracking of juror origins across the sortition system.

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

63-63: Important: Track drawn juror subcourt origins

The addition of drawnJurorFromCourtIDs array to the Round struct enables tracking which subcourt each juror was drawn from. This is crucial for proper penalty application and stake management.


94-94: Governance migration to owner-based model

The transition from governor to owner is consistent with the broader governance refactoring across the codebase.


171-179: Improved access control with custom errors

The modifiers now use custom errors (OwnerOnly, GuardianOrOwnerOnly) instead of require statements, providing better gas efficiency and error reporting.


196-208: Updated initialization for owner-based governance

The initialization function correctly accepts _owner parameter and assigns it to the owner storage variable, maintaining consistency with the governance migration.


277-286: Owner governance functions updated

The governance functions have been properly updated:

  • executeOwnerProposal replaces executeGovernorProposal with proper parameter naming
  • changeOwner replaces changeGovernor with consistent naming

All functions use onlyByOwner modifier appropriately.


467-467: Enhanced staking with delay control

The _setStake calls now include a _noDelay parameter, providing fine-grained control over stake timing. The setStakeBySortitionModule properly uses _noDelay: true to bypass delays for module-initiated stakes.

Also applies to: 475-476


610-617: Enhanced draw with subcourt tracking

The draw function now captures and stores the fromSubcourtID from the dispute kit, enabling proper tracking of juror origins. The fallback to dispute.courtID when fromSubcourtID is 0 is a sensible default.


643-651: Centralized court jump logic

The appeal function now uses centralized _getCourtAndDisputeKitJumps helper function, improving code organization and reducing duplication.


565-569: Improved commit period handling

The removal of areCommitsAllCast check simplifies the period transition logic, relying on time-based conditions instead. This reduces complexity while maintaining security.


759-795: Enhanced penalty execution with subcourt tracking

The penalty execution now:

  • Uses getDegreeOfCoherencePenalty for penalty-specific coherence calculation
  • Properly tracks the subcourt where penalties are applied via round.drawnJurorFromCourtIDs
  • Uses setStakePenalty for court-specific penalty application
  • Implements proper unstaking logic based on balance and minimum stake requirements

This provides much more precise penalty management.


828-872: Enhanced reward execution with coherence separation

The reward execution now:

  • Uses getDegreeOfCoherenceReward to get separate PNK and fee coherence values
  • Properly bounds checks both coherence values
  • Attempts to stake PNK rewards via setStakeReward with fallback to direct transfer
  • Uses separate coherence values for PNK and fee calculations

This provides more granular and flexible reward distribution.


807-808: Leftover rewards redirected to owner

Leftover rewards are now properly redirected to the owner instead of governor, maintaining consistency with the governance migration.

Also applies to: 880-883


939-957: Enhanced appeal cost calculation

The appeal cost calculation now uses the centralized _getCourtAndDisputeKitJumps function and properly accounts for vote count changes after appeals via getNbVotesAfterAppeal.


1065-1098: Excellent centralized jump logic helpers

The new helper functions provide clean abstraction for court and dispute kit jumping logic:

  • _isCourtJumping checks both early jumps and vote thresholds
  • _getCourtAndDisputeKitJumps determines all jump parameters in one place

This reduces code duplication and improves maintainability.


1114-1126: ONE_BASIS_POINT migration completed

All coherence calculations now use ONE_BASIS_POINT instead of ALPHA_DIVISOR, providing consistency across the codebase and aligning with the new basis point standard.


1144-1163: Enhanced _setStake with delay control

The internal _setStake function now accepts a _noDelay parameter that's properly passed to validateStake, enabling fine-grained control over stake timing behavior.


1236-1237: Proper error declarations for owner-based governance

The custom errors OwnerOnly and GuardianOrOwnerOnly are properly declared, replacing the old governor-based error names.

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

9-9: ONE_BASIS_POINT import for consistency

The import of ONE_BASIS_POINT from Constants.sol aligns with the codebase-wide migration to standardized basis point calculations.


62-62: Owner-based governance migration

The storage variable has been updated from governor to owner, consistent with the governance refactoring across the codebase.


127-134: Improved access control with custom errors

The access control modifiers now use custom errors (OwnerOnly, KlerosCoreOnly) instead of require statements, providing better gas efficiency and error information.


147-158: Owner parameter in initialization

The initializer has been properly updated to accept _owner parameter instead of _governor and assigns it correctly.


164-177: Owner governance functions updated

The governance functions have been properly migrated:

  • executeOwnerProposal with custom error handling
  • changeOwner with proper parameter naming
  • changeCore now uses onlyByOwner modifier

227-240: Enhanced draw with subcourt tracking

The draw function now returns both drawnAddress and fromSubcourtID from the sortition module, enabling proper tracking of juror origins. The early return maintains the same address when no juror is available.


268-270: Comprehensive error handling in commit casting

The commit casting validation now uses custom errors for all failure cases:

  • NotCommitPeriod for period validation
  • EmptyCommit for empty commit validation
  • NotActiveForCoreDisputeID for dispute activity validation

This provides better error reporting and gas efficiency.


275-275: Consistent juror ownership validation

The vote ownership validation uses the custom error JurorHasToOwnTheVote, maintaining consistency with the error handling refactoring.


309-327: Comprehensive vote casting validation

The vote casting function now uses custom errors for all validation:

  • Period, empty vote IDs, dispute activity, and choice bounds checking
  • Proper juror ownership, hidden vote commitment, and duplicate vote validation

This provides complete error coverage with efficient custom errors.


358-382: Enhanced appeal funding validation

The appeal funding validation uses custom errors appropriately:

  • ChoiceOutOfBounds for choice validation
  • NotActiveForCoreDisputeID for dispute activity
  • AppealPeriodIsOver and AppealPeriodIsOverForLoser for timing validation
  • AppealFeeIsAlreadyPaid for duplicate payment prevention

The loser appeal period calculation correctly uses ONE_BASIS_POINT.


438-441: Proper withdrawal validation

The withdrawal function uses custom errors (DisputeNotResolved, CoreIsPaused, NotActiveForCoreDisputeID) for all validation checks, maintaining consistency.


527-551: Well-designed coherence API implementation

The coherence API has been properly implemented:

  • getDegreeOfCoherenceReward returns the same coherence for both PNK and fee rewards
  • getDegreeOfCoherencePenalty returns coherence for penalties
  • Both delegate to internal _getDegreeOfCoherence for consistent logic
  • Uses ONE_BASIS_POINT for full coherence value

This provides the flexibility for different coherence models while maintaining consistency in the classic implementation.


628-640: New dispute kit control APIs

The implementation of earlyCourtJump (returning false) and getNbVotesAfterAppeal (implementing 2x+1 logic) provides the necessary interface compliance while maintaining classic dispute kit behavior.


754-772: Comprehensive error declarations

All custom errors are properly declared at the end of the contract, covering all validation scenarios with descriptive names. This provides excellent error reporting and gas efficiency.

contracts/src/arbitration/university/KlerosCoreUniversity.sol (8)

93-93: LGTM! Consistent ownership terminology refactoring.

The renaming from governor to owner and the corresponding modifier updates (onlyByGovernoronlyByOwner, onlyByGovernorOrInstructoronlyByOwnerOrInstructor) are correctly implemented across all access control checks.

Also applies to: 166-169, 176-179


525-525: LGTM! Coherence calculation correctly uses ONE_BASIS_POINT.

The replacement of ALPHA_DIVISOR with ONE_BASIS_POINT for calculating PNK at stake per juror maintains the same logic while aligning with the standardized constant naming across the codebase.

Also applies to: 655-655


599-599: Good addition: Tracking juror origin courts.

The new drawnJurorFromCourtIDs array and the logic to populate it with fromSubcourtID enables better tracking of where jurors were drawn from. This is particularly useful when jurors stake in subcourts and are drawn for cases in parent courts.

Also applies to: 606-606


866-869: Good addition: Automatic stake reward handling.

The new logic attempts to stake PNK rewards directly through setStakeReward. If staking fails (e.g., juror was unstaked), it falls back to transferring the PNK directly to the juror. This provides a good user experience while maintaining safety.


1086-1092: LGTM! Enhanced stake validation with no-delay parameter.

The addition of the _noDelay parameter to _setStake allows bypassing the delay mechanism when needed, which is consistent with the learnings about KlerosCoreUniversity not having phases.


801-809: No changes needed: owner can be cast to payable. The payable(owner) conversions on lines 804 and 892 correctly cast the address public owner into an address payable, so ETH transfers will compile and execute as intended.

Likely an incorrect or invalid review comment.


176-179: Error naming consistency confirmed. All Only-suffix errors (OwnerOnly, InstructorOnly, OwnerOrInstructorOnly) in KlerosCoreUniversity.sol follow the established naming convention.


754-765: Approve coherence penalty calculation. The getDegreeOfCoherencePenalty signature in IDisputeKit matches the five arguments passed in KlerosCoreUniversity, and capping coherence at ONE_BASIS_POINT followed by inverting via (ONE_BASIS_POINT – coherence) correctly computes the penalty proportion.

contracts/src/arbitration/SortitionModuleBase.sol (10)

10-10: LGTM! Correct RNG interface migration.

The migration from RNG to IRNG interface is properly implemented throughout the file. The simplified RNG interface no longer requires block parameters for requestRandomness() and receiveRandomness().

Also applies to: 54-54, 107-107, 155-159


52-52: Note: Deprecated fields marked for removal.

The comments indicate that randomNumberRequestBlock (line 52) and rngLookahead (line 56) are deprecated and will be removed in the next redeploy. These fields are no longer used in the active code paths.

Also applies to: 56-56


168-179: LGTM! Improved error handling with typed errors.

The phase transition logic now uses descriptive typed errors (MinStakingTimeNotPassed, NoDisputesThatNeedJurors, RandomNumberNotReady, DisputesWithoutJurorsAndMaxDrawingTimeNotPassed) instead of generic reverts, improving debugging and error handling.


193-194: Good validation: Tree creation checks.

The function now properly validates that:

  1. A tree doesn't already exist at the key
  2. K value is greater than 1

These checks prevent invalid tree configurations.


401-401: Excellent refactor: Stake path ID abstraction.

The introduction of stakePathID and the helper functions _accountAndCourtIDToStakePathID and _stakePathIDToAccountAndCourtID provides better encapsulation of the account-court relationship. The bit packing (20 bytes address + 12 bytes courtID) is efficient and well-documented.

Also applies to: 530-531, 537-538, 625-631, 703-734, 736-767


323-345: Important addition: Penalty application with balance tracking.

The new setStakePenalty function properly:

  1. Checks available stake before applying penalties
  2. Returns the updated balances for the caller to use
  3. Handles edge cases where penalty exceeds available stake

This provides essential functionality for the penalty system.


347-369: Good addition: Reward staking with safety checks.

The setStakeReward function:

  1. Returns early if no reward to add
  2. Validates juror has stake before adding rewards
  3. Returns success status for proper handling

This prevents rewards from being lost or misapplied.


439-456: Important additions: Forced unstaking capabilities.

The new forcedUnstakeAllCourts and forcedUnstake functions allow the core to remove inactive jurors from courts. This is essential for maintaining system health when jurors become inactive or fall below minimum stakes.


491-492: Good enhancement: Draw function returns origin court.

The draw function now returns both the drawn address and the fromSubcourtID, providing information about where the juror was originally staked. This aligns with the changes in KlerosCoreUniversity that track juror origins.

Also applies to: 521-522


403-403: Fix typo in variable name.

There's a typo in the variable name.

-        uint96 currentCourtID = _courtID;
+        uint96 currentCourtID = _courtID;

Actually, looking at the code again, the variable name is correct. The comment about CurrenCourtID in the summary appears to be incorrect.

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

73-82: Coherence (reward): document units/range explicitly.

-/// @return pnkCoherence The degree of coherence in basis points for the dispute PNK reward.
-/// @return feeCoherence The degree of coherence in basis points for the dispute fee reward.
+/// @return pnkCoherence The degree of coherence in basis points (0..10000) for the dispute PNK reward.
+/// @return feeCoherence The degree of coherence in basis points (0..10000) for the dispute fee reward.

No occurrences of the legacy getDegreeOfCoherence(...) remain in the contract code (only in specs and deployment metadata).


51-54: draw: add missing @return fromSubcourtID and confirm uint96 matches Core’s courtID type.

  • Prepend above the draw signature:
    /// @return drawnAddress The drawn address.
    +/// @return fromSubcourtID The subcourt ID from which the juror was drawn.
  • Ensure fromSubcourtID is typed uint96, consistent with courtID in KlerosCoreBase ([verify type] uint96).

Comment on lines +83 to +96
/// @dev Gets the degree of coherence of a particular voter. This function is called by Kleros Core in order to determine the amount of the penalty.
/// @param _coreDisputeID The ID of the dispute in Kleros Core, not in the Dispute Kit.
/// @param _coreRoundID The ID of the round in Kleros Core, not in the Dispute Kit.
/// @param _voteID The ID of the vote.
/// @param _feePerJuror The fee per juror.
/// @param _pnkAtStakePerJuror The PNK at stake per juror.
/// @return pnkCoherence The degree of coherence in basis points for the dispute PNK reward.
function getDegreeOfCoherencePenalty(
uint256 _coreDisputeID,
uint256 _coreRoundID,
uint256 _voteID,
uint256 _feePerJuror,
uint256 _pnkAtStakePerJuror
) external view returns (uint256);
) external view returns (uint256 pnkCoherence);
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 NatSpec: “reward” → “penalty”.

The return doc for the penalty accessor mentions “reward”, which is misleading.

-/// @return pnkCoherence The degree of coherence in basis points for the dispute PNK reward.
+/// @return pnkCoherence The degree of coherence in basis points for the dispute PNK penalty.
📝 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 Gets the degree of coherence of a particular voter. This function is called by Kleros Core in order to determine the amount of the penalty.
/// @param _coreDisputeID The ID of the dispute in Kleros Core, not in the Dispute Kit.
/// @param _coreRoundID The ID of the round in Kleros Core, not in the Dispute Kit.
/// @param _voteID The ID of the vote.
/// @param _feePerJuror The fee per juror.
/// @param _pnkAtStakePerJuror The PNK at stake per juror.
/// @return pnkCoherence The degree of coherence in basis points for the dispute PNK reward.
function getDegreeOfCoherencePenalty(
uint256 _coreDisputeID,
uint256 _coreRoundID,
uint256 _voteID,
uint256 _feePerJuror,
uint256 _pnkAtStakePerJuror
) external view returns (uint256);
) external view returns (uint256 pnkCoherence);
/// @dev Gets the degree of coherence of a particular voter. This function is called by Kleros Core in order to determine the amount of the penalty.
/// @param _coreDisputeID The ID of the dispute in Kleros Core, not in the Dispute Kit.
/// @param _coreRoundID The ID of the round in Kleros Core, not in the Dispute Kit.
/// @param _voteID The ID of the vote.
/// @param _feePerJuror The fee per juror.
/// @param _pnkAtStakePerJuror The PNK at stake per juror.
/// @return pnkCoherence The degree of coherence in basis points for the dispute PNK penalty.
function getDegreeOfCoherencePenalty(
uint256 _coreDisputeID,
uint256 _coreRoundID,
uint256 _voteID,
uint256 _feePerJuror,
uint256 _pnkAtStakePerJuror
) external view returns (uint256 pnkCoherence);
🤖 Prompt for AI Agents
In contracts/src/arbitration/interfaces/IDisputeKit.sol around lines 83 to 96,
the NatSpec @return description incorrectly refers to a "reward" but this
function returns the degree of coherence used for a PNK penalty; update the
@return text to say "penalty" (e.g., "pnkCoherence The degree of coherence in
basis points for the dispute PNK penalty.") keeping signature and return name
unchanged.

Comment on lines +89 to 91
function changeJurorNft(IERC721 _jurorNft) external onlyByOwner {
jurorNft = _jurorNft;
}
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 zero juror NFT and emit an event.

Prevent bricking setStake() via zero address and emit for off-chain tracking.

-    function changeJurorNft(IERC721 _jurorNft) external onlyByOwner {
-        jurorNft = _jurorNft;
-    }
+    function changeJurorNft(IERC721 _jurorNft) external onlyByOwner {
+        if (address(_jurorNft) == address(0)) revert ZeroAddress();
+        jurorNft = _jurorNft;
+        emit JurorNftChanged(_jurorNft);
+    }

Additions outside this hunk:

event JurorNftChanged(IERC721 jurorNft);
error ZeroAddress();
🤖 Prompt for AI Agents
In contracts/src/arbitration/KlerosCoreNeo.sol around lines 89 to 91, the
changeJurorNft function should validate the input and emit an event: add a guard
that reverts with ZeroAddress() if _jurorNft equals IERC721(address(0)), then
set jurorNft = _jurorNft and emit JurorNftChanged(_jurorNft); ensure the event
JurorNftChanged(IERC721 jurorNft) and error ZeroAddress() are declared in the
contract (as noted in the review).


await deployERC1155(hre, deployer, "TestERC1155", "Nft1155");
nft1155 = (await ethers.getContract("Nft1155")) as TestERC1155;
nft1155 = await ethers.getContract<TestERC1155>("Nft1155");
await nft1155.mint(deployer, TOKEN_ID, 1, "0x00");
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

Await mining for state-changing txs (.wait())

These txs aren’t mined before assertions, which can cause flaky tests.

-    await nft1155.mint(deployer, TOKEN_ID, 1, "0x00");
+    await nft1155.mint(deployer, TOKEN_ID, 1, "0x00").then((tx) => tx.wait());
-      dai.transfer(juror1.address, 1);
+      await dai.transfer(juror1.address, 1).then((tx) => tx.wait());
-      await nft721.safeMint(juror2.address);
+      await nft721.safeMint(juror2.address).then((tx) => tx.wait());
-      await nft1155.mint(juror2.address, TOKEN_ID, 1, "0x00");
+      await nft1155.mint(juror2.address, TOKEN_ID, 1, "0x00").then((tx) => tx.wait());

Also applies to: 162-162, 209-209, 256-256

🤖 Prompt for AI Agents
In contracts/test/arbitration/dispute-kit-gated.ts around lines 72, 162, 209 and
256, the state-changing mint/send transactions are awaited only for the promise
but not for on-chain mining, which can lead to flaky tests; change each call to
capture the transaction then await its .wait() (e.g., const tx = await
nft1155.mint(...); await tx.wait();) before running assertions so the
transactions are mined and state is settled.

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: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
contracts/src/arbitration/KlerosCoreBase.sol (3)

94-104: Owner storage OK; add EIP-173-style event and zero-address guard.

Track ownership changes and prevent accidental burn on init/change.

   address public owner; // The owner of the contract.
+  event OwnerChanged(address indexed previousOwner, address indexed newOwner);

And emit in initializer/changeOwner (see dedicated diffs below). Also consider whether guardian may be zero by design; if not, guard similarly.


196-209: Initializer: validate inputs and emit ownership event.

Prevent zero owner; record initial transfer for indexers.

 function __KlerosCoreBase_initialize(
-        address _owner,
+        address _owner,
         address _guardian,
@@
 ) internal onlyInitializing {
-        owner = _owner;
+        if (_owner == address(0)) revert OwnerZeroAddress();
+        owner = _owner;
+        emit OwnerChanged(address(0), _owner);
         guardian = _guardian;

Add error at the bottom:

+    error OwnerZeroAddress();

452-456: Remove redundant event declaration and add zero‐rate validation
The NewCurrencyRate event is declared in IArbitratorV2 and inherited—no need to re‐declare it in KlerosCoreBase.sol. You must, however, prevent a zero rate (which would later cause a division‐by‐zero in convertEthToTokenAmount). In contracts/src/arbitration/KlerosCoreBase.sol, update changeCurrencyRates to:

 function changeCurrencyRates(IERC20 _feeToken, uint64 _rateInEth, uint8 _rateDecimals) external onlyByGovernor {
+    if (_rateInEth == 0) revert InvalidRate();
     currencyRates[_feeToken].rateInEth = _rateInEth;
     currencyRates[_feeToken].rateDecimals = _rateDecimals;
     emit NewCurrencyRate(_feeToken, _rateInEth, _rateDecimals);
 }

And add at the bottom of the file:

+    error InvalidRate();
♻️ Duplicate comments (4)
contracts/CHANGELOG.md (3)

29-29: Clarify exact pragma range and scope (“interfaces only” likely inaccurate).

Recommend stating the exact range and reflecting actual scope (interfaces and RNG). Please verify across the repo before applying.

- - Widen the allowed solc version to any v0.8.x for the interfaces only ([#2083](https://github.com/kleros/kleros-v2/issues/2083))
+ - Widen the allowed pragma to `>=0.8.0 <0.9.0` for interfaces (and RNG contracts if applicable) ([#2083](https://github.com/kleros/kleros-v2/issues/2083))

To verify scope:

#!/bin/bash
rg -nP '^\s*pragma\s+solidity\s+[^;]+;' -g 'contracts/**' -C0

7-7: Avoid broken release links for an unpublished tag.

If 0.13.0 isn’t live yet, comment out its link and optionally mark the section as “Unreleased.” Update when published.

-## [0.13.0] - 2025-08-07 (Not published yet)
+## [0.13.0] - Unreleased
-[0.13.0]: https://github.com/kleros/kleros-v2/releases/tag/@kleros%[email protected]
+<!-- [0.13.0]: add after publishing -->

To check link availability:

#!/bin/bash
curl -s -o /dev/null -w "%{http_code}\n" \
"https://github.com/kleros/kleros-v2/releases/tag/@kleros%[email protected]"

Also applies to: 156-157


31-31: Fix constant name: ONE_BASIS_POINT (singular).

Public-facing docs must match the code. Use the correct identifier.

- - Consolidate the constant `ALPHA_DIVISOR` with `ONE_BASIS_POINTS` ([#2090](https://github.com/kleros/kleros-v2/issues/2090))
+ - Replace `ALPHA_DIVISOR` with `ONE_BASIS_POINT` ([#2090](https://github.com/kleros/kleros-v2/issues/2090))
contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol (1)

137-140: Add active-dispute guard in notJumped (prevents accidental reads of dispute 0).

Accessing disputes[coreDisputeIDToLocal[_coreDisputeID]] without validating activity can read the zero-initialized dispute. Guard with coreDisputeIDToActive first.

 modifier notJumped(uint256 _coreDisputeID) {
-        if (disputes[coreDisputeIDToLocal[_coreDisputeID]].jumped) revert DisputeJumpedToParentDK();
+        if (!coreDisputeIDToActive[_coreDisputeID]) revert NotActiveForCoreDisputeID();
+        if (disputes[coreDisputeIDToLocal[_coreDisputeID]].jumped) revert DisputeJumpedToParentDK();
         _;
 }
🧹 Nitpick comments (28)
contracts/src/arbitration/KlerosCoreBase.sol (3)

63-68: Keep arrays aligned; consider sanity checks on lengths.

Adding drawnJurorFromCourtIDs parallel to drawnJurors is fine. Ensure all push/pop sites keep lengths equal; add an internal assert in execute paths to detect divergence early.

+        // Sanity: keep arrays aligned (dev-only; remove if gas-sensitive)
+        assert(round.drawnJurorFromCourtIDs.length == round.drawnJurors.length);

315-320: Adding new DK: validate address and uniqueness.

Check non-zero and that the address has code; optional: prevent duplicate insertion.

-    function addNewDisputeKit(IDisputeKit _disputeKitAddress) external onlyByOwner {
+    function addNewDisputeKit(IDisputeKit _disputeKitAddress) external onlyByOwner {
+        if (address(_disputeKitAddress) == address(0)) revert InvalidAddress();
         uint256 disputeKitID = disputeKits.length;

342-382: Court creation flow looks good; tiny nit on casting.

  • Flow: validate, push, enable DKs, ensure Classic enabled, set fields, create tree, link parent — solid.
  • Nit: uint96 courtID = uint96(courts.length); is fine given practical limits; add an assert for future-proofing.
-        uint96 courtID = uint96(courts.length);
+        uint256 _newId = courts.length;
+        assert(_newId < type(uint96).max);
+        uint96 courtID = uint96(_newId);

Also applies to: 346-354, 367-368

contracts/CHANGELOG.md (3)

15-15: Tighten phrasing and use canonical ERC link.

Replace “in order to comply” with “to comply” and point to eips.ethereum.org.

- - **Breaking:** Rename `governor` to `owner` in order to comply with the lightweight ownership standard [ERC-5313](https://eipsinsight.com/ercs/erc-5313) ([#2112](https://github.com/kleros/kleros-v2/issues/2112))
+ - **Breaking:** Rename `governor` to `owner` to comply with the lightweight ownership standard [ERC-5313](https://eips.ethereum.org/EIPS/eip-5313) ([#2112](https://github.com/kleros/kleros-v2/issues/2112))

21-21: Spell and style: “bypass” (one word).

Minor wording cleanup.

- - Allow stake changes to by-pass delayed stakes when initiated by the SortitionModule by setting the `_noDelay` parameter to `true` in `SortitionModule.validateStake()` ([#2107](https://github.com/kleros/kleros-v2/issues/2107))
+ - Allow stake changes to bypass delayed stakes when initiated by the SortitionModule by setting `_noDelay` to `true` in `SortitionModule.validateStake()` ([#2107](https://github.com/kleros/kleros-v2/issues/2107))

41-41: Grammar: “advance to the Voting period.”

Improves readability.

- - Do not pass to Voting period if all the commits are cast because it breaks the current Shutter auto-reveal process. ([#2085](https://github.com/kleros/kleros-v2/issues/2085))
+ - Do not advance to the Voting period if all commits are cast, as it breaks the current Shutter auto‑reveal process. ([#2085](https://github.com/kleros/kleros-v2/issues/2085))
contracts/src/libraries/SortitionTrees.sol (4)

170-171: Typo in NatSpec: “subtract,” not “substract.”

Public docs should be clean.

-    /// @param _plusOrMinus Whether to add (true) or substract (false).
-    /// @param _value The value to add or substract.
+    /// @param _plusOrMinus Whether to add (true) or subtract (false).
+    /// @param _value The value to add or subtract.

3-3: Align pragma with toolchain or widen consistently.

Hardhat/Foundry target 0.8.30 per CHANGELOG. Consider ^0.8.30 or >=0.8.0 <0.9.0 for consistency with interfaces.

-pragma solidity ^0.8.24;
+pragma solidity ^0.8.30;
// or: pragma solidity >=0.8.0 <0.9.0;

206-211: Update linter directive (“solium” → “solhint”).

Solium is deprecated; use solhint directive to suppress the inline assembly warning.

-            // solium-disable-line security/no-inline-assembly
+            // solhint-disable-next-line no-inline-assembly

217-224: Mirror linter directive for unpack as well.

-            // solium-disable-line security/no-inline-assembly
+            // solhint-disable-next-line no-inline-assembly
contracts/test/sortition/index.ts (4)

315-334: Reduce flakiness of weighted draw test.

With only 100 samples, randomness modulo arithmetic could underperform the 80% threshold sporadically. Increase draws (e.g., 1_000) or relax the threshold a bit (e.g., > 70%) to stabilize CI.

- const numDraws = 100;
+ const numDraws = 1000;

- expect(draws[getTestAddress(1).toLowerCase()]).to.be.greaterThan(numDraws * 0.8);
+ expect(draws[getTestAddress(1).toLowerCase()]).to.be.greaterThan(numDraws * 0.7);

631-647: Make the MaxUint256 “gas limit” test deterministic.

Catching an error by matching /gas/i is brittle across EVMs/providers. Either assert the call does not revert or skip if environment-dependent.

- try {
-   await sortitionTree.set(0, juror, maxStake);
-   expect(await sortitionTree.stakeOf(0, juror)).to.equal(maxStake);
- } catch (error) {
-   // Expected to fail due to gas limits, not due to overflow
-   expect(error).to.match(/gas/i);
- }
+ await expect(sortitionTree.set(0, juror, maxStake)).to.not.be.reverted;
+ expect(await sortitionTree.stakeOf(0, juror)).to.equal(maxStake);

509-519: Tighten the expectation on returned court IDs or document rationale.

Allowing ranges like [0,1] and [1,2] hides regressions. If the algorithm should return the exact source court, assert equality; otherwise, add a brief comment explaining why multiple values are valid.


15-17: Add a tiny helper to standardize casting in assertions.

Optional convenience to reduce repetition.

   // Helper function to create a test juror address
   const getTestAddress = (index: number): string => accounts[index % accounts.length].address;
+  // Helper for BigInt casting in ethers v6 assertions
+  const bn = (v: number | bigint) => BigInt(v);
contracts/src/arbitration/interfaces/ISortitionModule.sol (1)

33-40: Clarify setStakePenalty return semantics.

The name availablePenalty is ambiguous. If it represents “remaining penalty to apply in other courts,” document it here, or consider renaming in a future major bump. Ensure implementations return the remainder, not the applied amount.

contracts/src/arbitration/university/SortitionModuleUniversity.sol (4)

115-118: Mark createTree as overriding (and consider restricting caller).

If ISortitionModuleUniversity declares createTree, add override. Also consider onlyByCore if creation should be core-driven.

-    function createTree(uint96 _courtID, bytes memory _extraData) external {
+    function createTree(uint96 _courtID, bytes memory _extraData) external override /* onlyByCore */ {

246-287: _setStake: minor safety and clarity improvements.

  • Consider guarding juror.stakedPnk -= _pnkWithdrawal; with an assert (or rely on upstream invariant) to avoid underflow if callers change.
  • Event: consider making _courtID a uint96 in StakeSet for consistency.
- event StakeSet(address indexed _address, uint256 _courtID, uint256 _amount, uint256 _amountAllCourts);
+ event StakeSet(address indexed _address, uint96 _courtID, uint256 _amount, uint256 _amountAllCourts);

69-76: Initialize: add zero-address checks (optional).

Add sanity checks for _owner and _core to fail fast on bad deployments.

 function initialize(address _owner, KlerosCoreUniversity _core) external reinitializer(1) {
-    owner = _owner;
-    core = _core;
+    if (_owner == address(0)) revert OwnerOnly(); // or a dedicated ZeroAddress error
+    owner = _owner;
+    if (address(_core) == address(0)) revert KlerosCoreOnly(); // or a dedicated ZeroAddress error
+    core = _core;
 }

Also applies to: 88-93


348-351: draw: return fromSubcourtID explicitly (even if zero).

Tiny readability improvement; no behavior change.

-function draw(uint96, uint256, uint256) public view override returns (address drawnAddress, uint96 fromSubcourtID) {
-    drawnAddress = transientJuror;
-}
+function draw(uint96 _courtID, uint256, uint256) public view override returns (address drawnAddress, uint96 fromSubcourtID) {
+    drawnAddress = transientJuror;
+    fromSubcourtID = _courtID; // educational mock: echo back
+}
contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol (5)

173-177: Normalize changeOwner signature to match storage type.

owner is address; changeOwner takes address payable. Make types consistent.

-    function changeOwner(address payable _owner) external onlyByOwner {
+    function changeOwner(address _owner) external onlyByOwner {
         owner = _owner;
     }

260-278: Explicitly bound-check vote IDs to avoid Panic reverts.

Accessing round.votes[_voteIDs[i]] without bounds checks can trigger a low-level Panic. Emit a clear error instead.

     for (uint256 i = 0; i < _voteIDs.length; i++) {
+        if (_voteIDs[i] >= round.votes.length) revert VoteIDOutOfBounds();
         if (round.votes[_voteIDs[i]].account != msg.sender) revert JurorHasToOwnTheVote();
         round.votes[_voteIDs[i]].commit = _commit;
     }

Also add at the bottom error list:

+    error VoteIDOutOfBounds();

320-328: Same bound-check for castVote loop.

 for (uint256 i = 0; i < _voteIDs.length; i++) {
+    if (_voteIDs[i] >= round.votes.length) revert VoteIDOutOfBounds();
     if (round.votes[_voteIDs[i]].account != _juror) revert JurorHasToOwnTheVote();
     if (hiddenVotes && round.votes[_voteIDs[i]].commit != voteHash)
         revert HashDoesNotMatchHiddenVoteCommitment();
     if (round.votes[_voteIDs[i]].voted) revert VoteAlreadyCast();
     ...
 }

164-171: Bubble revert reasons from executeOwnerProposal for easier debugging.

Optional: on failure, bubble returndata instead of a generic UnsuccessfulCall. Keeps revert diagnostics.

 function executeOwnerProposal(address _destination, uint256 _amount, bytes memory _data) external onlyByOwner {
-    (bool success, ) = _destination.call{value: _amount}(_data);
-    if (!success) revert UnsuccessfulCall();
+    (bool success, bytes memory returndata) = _destination.call{value: _amount}(_data);
+    if (!success) {
+        assembly {
+            revert(add(returndata, 0x20), mload(returndata))
+        }
+    }
 }

663-681: Fix returned name typo (totalCommited → totalCommitted).

ABI names matter for tooling/UX; align spelling.

-            uint256 totalCommited,
+            uint256 totalCommitted,
contracts/src/test/SortitionTreesMock.sol (2)

58-62: Avoid external self-calls (this.set) inside loops to save gas and reentrancy surface.

Refactor to an internal helper and call it directly.

-    function set(uint96 _courtID, address _account, uint256 _value) external {
+    function set(uint96 _courtID, address _account, uint256 _value) external {
+        _set(_courtID, _account, _value);
+    }
+
+    function _set(uint96 _courtID, address _account, uint256 _value) internal {
         TreeKey key = CourtID.wrap(_courtID).toTreeKey();
         bytes32 stakePathID = SortitionTrees.toStakePathID(_account, _courtID);
         SortitionTrees.set(trees[key], _value, stakePathID);
     }
 
     /// @dev Set stake for a juror across multiple courts (for testing hierarchy)
     function setStakeInHierarchy(uint96[] calldata _courtIDs, address _account, uint256 _value) external {
         for (uint256 i = 0; i < _courtIDs.length; i++) {
-            this.set(_courtIDs[i], _account, _value);
+            _set(_courtIDs[i], _account, _value);
         }
     }

69-73: Avoid external self-calls (this.stakeOf) in getters.

Use an internal helper to read directly.

-    function getStakesAcrossCourts(
+    function getStakesAcrossCourts(
         address _account,
         uint96[] calldata _courtIDs
     ) external view returns (uint256[] memory stakes) {
         stakes = new uint256[](_courtIDs.length);
         for (uint256 i = 0; i < _courtIDs.length; i++) {
-            stakes[i] = this.stakeOf(_courtIDs[i], _account);
+            stakes[i] = _stakeOf(_courtIDs[i], _account);
         }
     }
+
+    function _stakeOf(uint96 _courtID, address _account) internal view returns (uint256) {
+        TreeKey key = CourtID.wrap(_courtID).toTreeKey();
+        bytes32 stakePathID = SortitionTrees.toStakePathID(_account, _courtID);
+        return SortitionTrees.stakeOf(trees[key], stakePathID);
+    }
contracts/src/arbitration/SortitionModuleBase.sol (2)

303-319: Docstring mismatch: setStakePenalty is a penalty, not a reward.

Fix comment to avoid confusion.

-/// @dev Update the state of the stakes with a PNK reward deposit, called by KC during rewards execution.
+/// @dev Apply a PNK penalty (deduction) to the juror’s stake, called by KC during rewards execution.

202-208: executeDelayedStakes: no-op handling when nothing pending.

Current revert uses delayedStakeWriteIndex < delayedStakeReadIndex. When equal (common “empty” case after full drain), revert would not trigger, but loop becomes 0-iter, which is fine. If you prefer explicit UX, consider reverting also on equality.

-    if (delayedStakeWriteIndex < delayedStakeReadIndex) revert NoDelayedStakeToExecute();
+    if (delayedStakeWriteIndex + 1 <= delayedStakeReadIndex) revert NoDelayedStakeToExecute();
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 953bd95 and a7cfba0.

📒 Files selected for processing (10)
  • contracts/CHANGELOG.md (2 hunks)
  • contracts/src/arbitration/KlerosCoreBase.sol (27 hunks)
  • contracts/src/arbitration/SortitionModuleBase.sol (13 hunks)
  • contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol (14 hunks)
  • contracts/src/arbitration/interfaces/ISortitionModule.sol (3 hunks)
  • contracts/src/arbitration/university/SortitionModuleUniversity.sol (11 hunks)
  • contracts/src/libraries/SortitionTrees.sol (1 hunks)
  • contracts/src/test/SortitionModuleMock.sol (1 hunks)
  • contracts/src/test/SortitionTreesMock.sol (1 hunks)
  • contracts/test/sortition/index.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2024-11-19T16:31:08.965Z
Learnt from: jaybuidl
PR: kleros/kleros-v2#1746
File: contracts/config/courts.v2.mainnet-neo.json:167-170
Timestamp: 2024-11-19T16:31:08.965Z
Learning: In the court hierarchy, child courts' `minStake` must be greater than or equal to their parent court's `minStake`.

Applied to files:

  • contracts/src/arbitration/SortitionModuleBase.sol
  • contracts/src/arbitration/KlerosCoreBase.sol
📚 Learning: 2024-11-19T16:09:41.467Z
Learnt from: jaybuidl
PR: kleros/kleros-v2#1746
File: contracts/config/courts.v2.mainnet-neo.json:3-17
Timestamp: 2024-11-19T16:09:41.467Z
Learning: In `contracts/config/courts.v2.mainnet-neo.json`, the General Court (ID: 1) can have its `parent` ID set to itself (`"parent": 1`), as there is no parent court with ID 0 currently.

Applied to files:

  • contracts/src/arbitration/KlerosCoreBase.sol
🪛 LanguageTool
contracts/CHANGELOG.md

[style] ~15-~15: Consider a more concise word here.
Context: ...reaking:** Rename governor to owner in order to comply with the lightweight ownership s...

(IN_ORDER_TO_PREMIUM)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Analyze (javascript)
  • GitHub Check: SonarCloud
  • GitHub Check: contracts-testing
  • GitHub Check: contracts-testing
  • GitHub Check: SonarCloud
  • GitHub Check: Analyze (javascript)
🔇 Additional comments (23)
contracts/src/arbitration/KlerosCoreBase.sol (17)

171-179: Access-control rename to Owner LGTM.


226-227: Tree creation ordering OK.

Creating FORKING and GENERAL trees early matches usage later.

Also applies to: 239-240


261-272: Pause/unpause semantics consistent.

Guardian-or-owner for pause and owner-only for unpause is coherent.


392-417: Parameter update validations LGTM.

Parent/children minStake constraints enforced correctly.


423-438: enableDisputeKits: good guardrails on Classic DK.


467-468: OnError.Revert choice is correct for user-facing staking.


470-477: By-sortition staking path: good separation and no-delay.

Using OnError.Return avoids bricking the module loop.


610-618: Track subcourt for penalty: good; add bounds fallback already handled.

Using fromSubcourtID != 0 ? fromSubcourtID : dispute.courtID is safe if DK returns 0 for “none”.


643-651: Jump helpers integration LGTM.

Emitting CourtJump on actual jump is correct.


939-957: appealCost: jump logic OK; TODO on General Court fork should be actionable.

Returning NON_PAYABLE_AMOUNT is a sentinel; either implement or open a tracked issue so integrators can handle it predictably.


1045-1051: isDisputeKitJumping: early exit is fine.


1065-1076: New jump helpers read cleanly; solid separation of concerns.

Also applies to: 1078-1098


1114-1118: ONE_BASIS_POINT migration LGTM.

Replacing legacy divisor with basis-point constant is correct and gas-neutral.

Also applies to: 1125-1126


1141-1150: _setStake signature/usage: good extension for no-delay and error policy.

Also applies to: 1162-1164


1236-1238: Error renames align with owner/guardian migration.


443-446: AcceptedFeeToken already declared in IArbitratorV2.sol
The event is defined in the IArbitratorV2 interface (contracts/src/arbitration/interfaces/IArbitratorV2.sol:30) and imported by KlerosCoreBase.sol, so no additional declaration is needed.

Likely an incorrect or invalid review comment.


565-567: Confirm commit-period gating in Shutter and add edge-case tests
Ensure removing the “all commits cast” shortcut doesn’t break Shutter auto-reveal. Add tests covering:

  • Early full-commit pass in KlerosCoreBase (commit→vote transition when all commits are in before window end)
  • Commit gating in KlerosCoreUniversity (University flow)
  • Short commit windows and timing edges (warp scenarios)
contracts/src/test/SortitionModuleMock.sol (2)

6-6: Import looks correct and scoped.


12-12: LGTM: mock reads SortitionTrees state via typed key.

Access pattern aligns with the new library; return values preserved.

contracts/src/arbitration/interfaces/ISortitionModule.sol (1)

51-56: Resolved — draw 2-tuple is propagated and consumed.

  • Interfaces updated: ISortitionModule.draw and IDisputeKit.draw now return (address, uint96). (contracts/src/arbitration/interfaces/ISortitionModule.sol, contracts/src/arbitration/interfaces/IDisputeKit.sol)
  • Sortition implements the 2-tuple: SortitionModuleBase.draw returns (address, uint96) and sources the second value from the trees. (contracts/src/arbitration/SortitionModuleBase.sol)
  • Dispute kits forward it: DisputeKitClassicBase.draw forwards (drawnAddress, fromSubcourtID) from sortitionModule.draw. (contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol)
  • Core consumes it: KlerosCoreBase.draw receives (drawnAddress, fromSubcourtID) and pushes fromSubcourtID into round.drawnJurorFromCourtIDs. (contracts/src/arbitration/KlerosCoreBase.sol)

No further changes required.

contracts/src/arbitration/university/SortitionModuleUniversity.sol (1)

331-339: Leftover PNK withdrawal: good state-change-before-external-call pattern.

Setting stakedPnk = 0 before transferBySortitionModule prevents reentrancy double-withdrawals. LGTM.

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

615-622: Confirm isAppealFunded semantics when exactly one choice is funded.

Function name/docs suggest “funding finished prematurely when losing side didn’t fund.” Current condition returns true only if fundedChoices.length == 0. Should it also return true when fundedChoices.length == 1 and loser window elapsed?

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

481-489: draw should not depend on stale randomNumber across Drawing phases.

If Drawing spans multiple disputes, ensure randomNumber is updated every Generating phase and cannot persist across cycles unexpectedly. Consider zeroing randomNumber when entering staking or after Drawing completes.

Comment on lines +16 to 23
function createTree(uint96 _courtID, bytes memory _extraData) external;

function validateStake(
address _account,
uint96 _courtID,
uint256 _newStake
uint256 _newStake,
bool _noDelay
) external returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult);
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

createTree/validateStake signature changes look consistent; ensure downstream updates.

All implementers/call sites must adopt (uint96 _courtID, bytes _extraData) and extra _noDelay param. Recommend verifying overrides and mocks compile against this interface.

Run:


🏁 Script executed:

#!/bin/bash
# Validate overrides across the repo
rg -nP 'interface\s+ISortitionModule\b' -C1
rg -nP 'function\s+createTree\s*\(\s*uint96\s*_courtID,\s*bytes' -C2
rg -nP 'validateStake\s*\(\s*address\s*_account,\s*uint96\s*_courtID,\s*uint256\s*_newStake,\s*bool\s*_noDelay' -C2
rg -nP '(contract|abstract).*SortitionModule.*\b(createTree|validateStake)\b' -C3

Length of output: 317853


Fix ISortitionModule signature mismatch — update implementations & callers to the new signatures (createTree(uint96, bytes) and validateStake(..., bool _noDelay))

rg shows no uses of the new signatures; the repo still uses createTree(bytes32, bytes) and validateStake(address,uint96,uint256). Update these locations at minimum:

  • contracts/src/arbitration/SortitionModuleBase.sol — change createTree/validateStake signatures and internals.
  • contracts/src/arbitration/SortitionModule.sol — update overrides to match interface.
  • contracts/src/arbitration/KlerosCoreBase.sol — update calls to sortitionModule.createTree(...) and sortitionModule.validateStake(..., _noDelay).
  • contracts/deployments/** and any tests/mocks that rely on the old ABI — regenerate/update.
🤖 Prompt for AI Agents
In contracts/src/arbitration/interfaces/ISortitionModule.sol around lines 16–23
the interface signatures changed to createTree(uint96 _courtID, bytes
_extraData) and validateStake(address _account, uint96 _courtID, uint256
_newStake, bool _noDelay) returns (uint256 pnkDeposit, uint256 pnkWithdrawal,
StakingResult stakingResult); update the implementations and callers
accordingly: in contracts/src/arbitration/SortitionModuleBase.sol and
contracts/src/arbitration/SortitionModule.sol change the function declarations
to match the new signatures and adjust internal logic to accept a uint96 courtID
for createTree and handle the added _noDelay boolean and the three-value return
from validateStake; in contracts/src/arbitration/KlerosCoreBase.sol update all
calls to sortitionModule.createTree(...) and sortitionModule.validateStake(...,
_noDelay) to pass a uint96 courtID and the new _noDelay flag and to capture the
pnkDeposit/pnkWithdrawal/stakingResult return values; finally update any
deployment scripts, generated ABIs, tests and mocks under
contracts/deployments/** and test files to the new ABI signatures and regenerate
artifacts so builds/tests use the updated interface.

Comment on lines +273 to 281
/// @dev Allows the owner to call anything on behalf of the contract.
/// @param _destination The destination of the call.
/// @param _amount The value sent with the call.
/// @param _data The data sent with the call.
function executeGovernorProposal(
address _destination,
uint256 _amount,
bytes memory _data
) external onlyByGovernor {
function executeOwnerProposal(address _destination, uint256 _amount, bytes memory _data) external onlyByOwner {
(bool success, ) = _destination.call{value: _amount}(_data);
if (!success) revert UnsuccessfulCall();
}

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

Owner arbitrary-call: add event and reentrancy guard.

This function can send ETH and call arbitrary code; emit an audit trail and block reentrancy.

-    function executeOwnerProposal(address _destination, uint256 _amount, bytes memory _data) external onlyByOwner {
+    event OwnerCallExecuted(address indexed destination, uint256 value, bytes data, bool success);
+    function executeOwnerProposal(address _destination, uint256 _amount, bytes memory _data) external onlyByOwner nonReentrant {
         (bool success, ) = _destination.call{value: _amount}(_data);
         if (!success) revert UnsuccessfulCall();
+        emit OwnerCallExecuted(_destination, _amount, _data, success);
     }

See guard scaffold at the end of this review for a minimal nonReentrant modifier.

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

🤖 Prompt for AI Agents
In contracts/src/arbitration/KlerosCoreBase.sol around lines 273 to 281, the
executeOwnerProposal allows sending ETH and calling arbitrary code but lacks an
event and reentrancy protection; add a nonReentrant modifier to the contract
(e.g., using a private uint256 _reentrancyLock with checks and updates) and
apply it to executeOwnerProposal, declare a descriptive event (e.g.,
OwnerExecutedProposal(address indexed destination, uint256 amount, bytes data,
bool success)) and emit it after the call (including success/failure info), and
ensure the modifier prevents reentrancy by setting and clearing the lock and
reverting on reentrancy attempts.

Comment on lines +282 to 286
/// @dev Changes the `owner` storage variable.
/// @param _owner The new value for the `owner` storage variable.
function changeOwner(address payable _owner) external onlyByOwner {
owner = _owner;
}
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

Ownership change: emit event and reject zero-address unless “renounce” is intended.

If renouncing is wanted, expose a dedicated renounceOwner(); otherwise prevent zero.

-    function changeOwner(address payable _owner) external onlyByOwner {
-        owner = _owner;
+    function changeOwner(address payable _owner) external onlyByOwner {
+        if (_owner == address(0)) revert OwnerZeroAddress();
+        emit OwnerChanged(owner, _owner);
+        owner = _owner;
     }
📝 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 Changes the `owner` storage variable.
/// @param _owner The new value for the `owner` storage variable.
function changeOwner(address payable _owner) external onlyByOwner {
owner = _owner;
}
/// @dev Changes the `owner` storage variable.
/// @param _owner The new value for the `owner` storage variable.
function changeOwner(address payable _owner) external onlyByOwner {
if (_owner == address(0)) revert OwnerZeroAddress();
emit OwnerChanged(owner, _owner);
owner = _owner;
}
🤖 Prompt for AI Agents
In contracts/src/arbitration/KlerosCoreBase.sol around lines 282 to 286, the
changeOwner function currently assigns owner directly; update it to (1) reject
the zero address with a require(check) to prevent accidental renouncement unless
explicitly intended, (2) emit an OwnershipTransferred (or OwnerChanged) event
with previous and new owner values, and (3) if renouncement should be supported,
implement a separate renounceOwner() function that sets owner to address(0) and
emits the same event; ensure onlyByOwner remains enforced.

Comment on lines 288 to 312
/// @dev Changes the `guardian` storage variable.
/// @param _guardian The new value for the `guardian` storage variable.
function changeGuardian(address _guardian) external onlyByGovernor {
function changeGuardian(address _guardian) external onlyByOwner {
guardian = _guardian;
}

/// @dev Changes the `pinakion` storage variable.
/// @param _pinakion The new value for the `pinakion` storage variable.
function changePinakion(IERC20 _pinakion) external onlyByGovernor {
function changePinakion(IERC20 _pinakion) external onlyByOwner {
pinakion = _pinakion;
}

/// @dev Changes the `jurorProsecutionModule` storage variable.
/// @param _jurorProsecutionModule The new value for the `jurorProsecutionModule` storage variable.
function changeJurorProsecutionModule(address _jurorProsecutionModule) external onlyByGovernor {
function changeJurorProsecutionModule(address _jurorProsecutionModule) external onlyByOwner {
jurorProsecutionModule = _jurorProsecutionModule;
}

/// @dev Changes the `_sortitionModule` storage variable.
/// Note that the new module should be initialized for all courts.
/// @param _sortitionModule The new value for the `sortitionModule` storage variable.
function changeSortitionModule(ISortitionModule _sortitionModule) external onlyByGovernor {
function changeSortitionModule(ISortitionModule _sortitionModule) external onlyByOwner {
sortitionModule = _sortitionModule;
}

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

Change-XXX setters: add basic validation.

  • changeGuardian: prevent setting owner as guardian (optional) or zero if not allowed.
  • changePinakion: disallow zero; consider preventing NATIVE_CURRENCY.
  • changeJurorProsecutionModule/changeSortitionModule: require contract code present; for sortition, verify the module is initialized for all courts or document the exact migration procedure.
-    function changePinakion(IERC20 _pinakion) external onlyByOwner {
+    function changePinakion(IERC20 _pinakion) external onlyByOwner {
+        if (address(_pinakion) == address(0)) revert InvalidAddress();
         pinakion = _pinakion;
     }
@@
-    function changeSortitionModule(ISortitionModule _sortitionModule) external onlyByOwner {
-        sortitionModule = _sortitionModule;
+    function changeSortitionModule(ISortitionModule _sortitionModule) external onlyByOwner {
+        if (address(_sortitionModule) == address(0)) revert InvalidAddress();
+        sortitionModule = _sortitionModule;
     }

Add:

+    error InvalidAddress();

Please confirm where the “initialize for all courts” step is executed during upgrade.


🏁 Script executed:

#!/bin/bash
set -e

# Verify existing validation in changeXXX functions
rg -n -C3 "function changeGuardian" -g "contracts/src/arbitration/KlerosCoreBase.sol"
rg -n -C3 "function changePinakion" -g "contracts/src/arbitration/KlerosCoreBase.sol"
rg -n -C3 "function changeJurorProsecutionModule" -g "contracts/src/arbitration/KlerosCoreBase.sol"
rg -n -C3 "function changeSortitionModule" -g "contracts/src/arbitration/KlerosCoreBase.sol"

# Check court creation and sortition module initialization
rg -n -C3 "function createCourt" -g "contracts/src/arbitration/KlerosCoreBase.sol"
rg -n -C3 "initialize" -g "contracts/src/arbitration/KlerosCoreBase.sol"

Length of output: 5036


🏁 Script executed:

#!/bin/bash
set -e

echo "--- sortitionModule.initialize callers ---"
rg -nP "sortitionModule\\.initialize" -g contracts/src/arbitration/KlerosCoreBase.sol || echo "none found"

echo "--- createCourt definition and body ---"
rg -nP -C10 "function createCourt" -g contracts/src/arbitration/KlerosCoreBase.sol

echo "--- __KlerosCoreBase_initialize implementation ---"
rg -nP -C5 "__KlerosCoreBase_initialize" -g contracts/src/arbitration/KlerosCoreBase.sol

Length of output: 3595


🏁 Script executed:

#!/bin/bash
set -e

echo "----- createCourt implementation (lines 310–380) -----"
sed -n '310,380p' contracts/src/arbitration/KlerosCoreBase.sol

echo
echo "----- All calls to sortitionModule. within file -----"
rg -n "sortitionModule\." -g contracts/src/arbitration/KlerosCoreBase.sol || echo "none found"

Length of output: 5392


Add non-zero address checks to all setters and document sortition module migration

  • In changeGuardian, changePinakion, changeJurorProsecutionModule and changeSortitionModule, add
    if (address(_…Parameter…) == address(0)) revert InvalidAddress();
    and declare
    error InvalidAddress();
    alongside the other custom errors.
  • After swapping in a new sortitionModule, existing courts must each call
    sortitionModule.createTree(courtID, _sortitionExtraData);
    as part of your upgrade or migration script—no such initialization currently exists in changeSortitionModule. Document or implement where and how you’ll initialize the new module for all pre-existing courts.

Comment on lines 758 to 771
// [0, 1] value that determines how coherent the juror was in this round, in basis points.
uint256 degreeOfCoherence = disputeKit.getDegreeOfCoherence(
uint256 coherence = disputeKit.getDegreeOfCoherencePenalty(
_params.disputeID,
_params.round,
_params.repartition,
_params.feePerJurorInRound,
_params.pnkAtStakePerJurorInRound
);
if (degreeOfCoherence > ALPHA_DIVISOR) {
// Make sure the degree doesn't exceed 1, though it should be ensured by the dispute kit.
degreeOfCoherence = ALPHA_DIVISOR;

// Guard against degree exceeding 1, though it should be ensured by the dispute kit.
if (coherence > ONE_BASIS_POINT) {
coherence = ONE_BASIS_POINT;
}

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

Penalty path: coherence clamp good; consider reentrancy posture.

External calls occur before state sync and token sends (unlock, setStakePenalty, potential ETH send to owner). Add nonReentrant on execute() or move all state writes before external interactions where feasible.

Additionally, message comment says “breaks CEI” near round.pnkPenalties update. If keeping the cached write, guard the whole execute() with nonReentrant (see scaffold below).

Also applies to: 773-786, 797-803, 805-816

Comment on lines 827 to 842
// [0, 1] value that determines how coherent the juror was in this round, in basis points.
uint256 degreeOfCoherence = disputeKit.getDegreeOfCoherence(
(uint256 pnkCoherence, uint256 feeCoherence) = disputeKit.getDegreeOfCoherenceReward(
_params.disputeID,
_params.round,
_params.repartition % _params.numberOfVotesInRound,
_params.feePerJurorInRound,
_params.pnkAtStakePerJurorInRound
);

// Make sure the degree doesn't exceed 1, though it should be ensured by the dispute kit.
if (degreeOfCoherence > ALPHA_DIVISOR) {
degreeOfCoherence = ALPHA_DIVISOR;
// Guard against degree exceeding 1, though it should be ensured by the dispute kit.
if (pnkCoherence > ONE_BASIS_POINT) {
pnkCoherence = ONE_BASIS_POINT;
}
if (feeCoherence > ONE_BASIS_POINT) {
feeCoherence = ONE_BASIS_POINT;
}
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

Rewards path: coherent split by PNK/fees is correct; minor hardening.

  • Clamp to ONE_BASIS_POINT is good.
  • Consider reentrancy guard on execute() due to ETH/WERC20 sends to jurors/owner.
  • Optional: emit a single “RoundSettled” summary event at end to ease analytics.

Also applies to: 845-855, 857-863, 869-872, 874-893

🤖 Prompt for AI Agents
In contracts/src/arbitration/KlerosCoreBase.sol around lines 827 to 842, the
coherence clamp is fine but the execute() path performs ETH/WERC20 transfers to
jurors/owner and needs reentrancy hardening: add a reentrancy guard (e.g.,
inherit OpenZeppelin ReentrancyGuard and mark execute() nonReentrant or
implement an internal mutex), ensure checks-effects-interactions by performing
all state updates (marking round settled, updating balances/flags) before any
external token/ETH transfers, and emit a single RoundSettled summary event at
the end of the function summarizing amounts and recipients for easier analytics.

Comment on lines +185 to 189
function createTree(uint96 _courtID, bytes memory _extraData) external override onlyByCore {
TreeKey key = CourtID.wrap(_courtID).toTreeKey();
uint256 K = _extraDataToTreeK(_extraData);
require(tree.K == 0, "Tree already exists.");
require(K > 1, "K must be greater than one.");
tree.K = K;
tree.nodes.push(0);
sortitionSumTrees.createTree(key, K);
}
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

Validate K ≥ 2 when creating trees (defensive invariant).

Creating a K=0/1 tree breaks sum-tree invariants. Guard before createTree.

 function createTree(uint96 _courtID, bytes memory _extraData) external override onlyByCore {
     TreeKey key = CourtID.wrap(_courtID).toTreeKey();
     uint256 K = _extraDataToTreeK(_extraData);
+    if (K < 2) revert KMustBeGreaterThanOne();
     sortitionSumTrees.createTree(key, K);
 }

Add error:

 error NotDrawingPhase();
+error KMustBeGreaterThanOne();
📝 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 createTree(uint96 _courtID, bytes memory _extraData) external override onlyByCore {
TreeKey key = CourtID.wrap(_courtID).toTreeKey();
uint256 K = _extraDataToTreeK(_extraData);
require(tree.K == 0, "Tree already exists.");
require(K > 1, "K must be greater than one.");
tree.K = K;
tree.nodes.push(0);
sortitionSumTrees.createTree(key, K);
}
// … elsewhere in the file, alongside other error declarations
error NotDrawingPhase();
error KMustBeGreaterThanOne();
function createTree(uint96 _courtID, bytes memory _extraData) external override onlyByCore {
TreeKey key = CourtID.wrap(_courtID).toTreeKey();
uint256 K = _extraDataToTreeK(_extraData);
if (K < 2) revert KMustBeGreaterThanOne();
sortitionSumTrees.createTree(key, K);
}
🤖 Prompt for AI Agents
In contracts/src/arbitration/SortitionModuleBase.sol around lines 185 to 189,
the computed tree arity K from _extraDataToTreeK(_extraData) is not validated,
allowing K=0 or K=1 which breaks sum-tree invariants; add a guard immediately
after computing K to require K >= 2 and revert with a clear error (either
require(K >= 2, "K must be >= 2") or a custom error) before calling
sortitionSumTrees.createTree(key, K) so creation never proceeds with invalid K.

Comment on lines +197 to +219
function setStakePenalty(
address _account,
uint96 _courtID,
uint256 _penalty
) external override onlyByCore returns (uint256 pnkBalance, uint256 newCourtStake, uint256 availablePenalty) {
Juror storage juror = jurors[_account];
availablePenalty = _penalty;
newCourtStake = _stakeOf(_account, _courtID);
if (juror.stakedPnk < _penalty) {
availablePenalty = juror.stakedPnk;
}

if (availablePenalty == 0) return (juror.stakedPnk, newCourtStake, 0); // No penalty to apply.

uint256 currentStake = _stakeOf(_account, _courtID);
uint256 newStake = 0;
if (currentStake >= availablePenalty) {
newStake = currentStake - availablePenalty;
}
_setStake(_account, _courtID, 0, availablePenalty, newStake);
pnkBalance = juror.stakedPnk; // updated by _setStake()
newCourtStake = _stakeOf(_account, _courtID); // updated by _setStake()
}
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

Fix penalty application to preserve accounting invariants and return remainder.

Current code withdraws _penalty from stakedPnk even when the court stake is smaller, leaving stakedPnk < sum of per-court stakes and returning an availablePenalty that equals the applied amount, not the remainder. Clamp applied penalty to the lesser of (a) total balance available, (b) the court stake; then return the leftover for upstream cascading.

-    ) external override onlyByCore returns (uint256 pnkBalance, uint256 newCourtStake, uint256 availablePenalty) {
-        Juror storage juror = jurors[_account];
-        availablePenalty = _penalty;
-        newCourtStake = _stakeOf(_account, _courtID);
-        if (juror.stakedPnk < _penalty) {
-            availablePenalty = juror.stakedPnk;
-        }
-
-        if (availablePenalty == 0) return (juror.stakedPnk, newCourtStake, 0); // No penalty to apply.
-
-        uint256 currentStake = _stakeOf(_account, _courtID);
-        uint256 newStake = 0;
-        if (currentStake >= availablePenalty) {
-            newStake = currentStake - availablePenalty;
-        }
-        _setStake(_account, _courtID, 0, availablePenalty, newStake);
-        pnkBalance = juror.stakedPnk; // updated by _setStake()
-        newCourtStake = _stakeOf(_account, _courtID); // updated by _setStake()
-    }
+    ) external override onlyByCore returns (uint256 pnkBalance, uint256 newCourtStake, uint256 availablePenalty) {
+        Juror storage juror = jurors[_account];
+        // Max we can take overall in this tx
+        uint256 maxApplicable = _penalty <= juror.stakedPnk ? _penalty : juror.stakedPnk;
+        uint256 currentStake = _stakeOf(_account, _courtID);
+        // Apply only what this court can cover
+        uint256 applied = currentStake <= maxApplicable ? currentStake : maxApplicable;
+        if (applied == 0) {
+            return (juror.stakedPnk, currentStake, maxApplicable); // nothing applied here; leftover equals maxApplicable
+        }
+        uint256 newStake = currentStake - applied;
+        _setStake(_account, _courtID, 0, applied, newStake);
+        pnkBalance = juror.stakedPnk; // updated by _setStake()
+        newCourtStake = _stakeOf(_account, _courtID); // updated by _setStake()
+        availablePenalty = maxApplicable - applied; // remainder to be applied to other courts upstream
+    }
📝 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 setStakePenalty(
address _account,
uint96 _courtID,
uint256 _penalty
) external override onlyByCore returns (uint256 pnkBalance, uint256 newCourtStake, uint256 availablePenalty) {
Juror storage juror = jurors[_account];
availablePenalty = _penalty;
newCourtStake = _stakeOf(_account, _courtID);
if (juror.stakedPnk < _penalty) {
availablePenalty = juror.stakedPnk;
}
if (availablePenalty == 0) return (juror.stakedPnk, newCourtStake, 0); // No penalty to apply.
uint256 currentStake = _stakeOf(_account, _courtID);
uint256 newStake = 0;
if (currentStake >= availablePenalty) {
newStake = currentStake - availablePenalty;
}
_setStake(_account, _courtID, 0, availablePenalty, newStake);
pnkBalance = juror.stakedPnk; // updated by _setStake()
newCourtStake = _stakeOf(_account, _courtID); // updated by _setStake()
}
function setStakePenalty(
address _account,
uint96 _courtID,
uint256 _penalty
) external override onlyByCore returns (uint256 pnkBalance, uint256 newCourtStake, uint256 availablePenalty) {
Juror storage juror = jurors[_account];
// Max we can take overall in this tx
uint256 maxApplicable = _penalty <= juror.stakedPnk ? _penalty : juror.stakedPnk;
uint256 currentStake = _stakeOf(_account, _courtID);
// Apply only what this court can cover
uint256 applied = currentStake <= maxApplicable ? currentStake : maxApplicable;
if (applied == 0) {
return (juror.stakedPnk, currentStake, maxApplicable); // nothing applied here; leftover equals maxApplicable
}
uint256 newStake = currentStake - applied;
_setStake(_account, _courtID, 0, applied, newStake);
pnkBalance = juror.stakedPnk; // updated by _setStake()
newCourtStake = _stakeOf(_account, _courtID); // updated by _setStake()
availablePenalty = maxApplicable - applied; // remainder to be applied to other courts upstream
}
🤖 Prompt for AI Agents
In contracts/src/arbitration/university/SortitionModuleUniversity.sol around
lines 197-219, the function currently withdraws the full requested _penalty from
stakedPnk regardless of the court-specific stake, breaking accounting and
returning the wrong availablePenalty; fix by computing an appliedPenalty =
min(_penalty, juror.stakedPnk, currentStake) (clamp to available PNK and the
court stake), call _setStake to withdraw only appliedPenalty from the
juror/court, then set pnkBalance = juror.stakedPnk and newCourtStake =
_stakeOf(_account, _courtID) after the update, and return availablePenalty =
_penalty - appliedPenalty so the remainder can cascade upstream.

Comment on lines +66 to +83
// While it still has children
uint256 treeIndex = 0;
while ((_tree.K * treeIndex) + 1 < _tree.nodes.length) {
for (uint256 i = 1; i <= _tree.K; i++) {
// Loop over children.
uint256 nodeIndex = (_tree.K * treeIndex) + i;
uint256 nodeValue = _tree.nodes[nodeIndex];

if (currentDrawnNumber >= nodeValue) {
// Go to the next child.
currentDrawnNumber -= nodeValue;
} else {
// Pick this child.
treeIndex = nodeIndex;
break;
}
}
}
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

draw(): out-of-bounds read risk and potential infinite loop.

Iterating i=1..K unconditionally reads children that may not exist yet (nodeIndex >= nodes.length), which reverts. If no child consumes the draw, the while loop can spin. Fix by bounds-checking each child and failing safely.

-        while ((_tree.K * treeIndex) + 1 < _tree.nodes.length) {
-            for (uint256 i = 1; i <= _tree.K; i++) {
-                // Loop over children.
-                uint256 nodeIndex = (_tree.K * treeIndex) + i;
-                uint256 nodeValue = _tree.nodes[nodeIndex];
-
-                if (currentDrawnNumber >= nodeValue) {
-                    // Go to the next child.
-                    currentDrawnNumber -= nodeValue;
-                } else {
-                    // Pick this child.
-                    treeIndex = nodeIndex;
-                    break;
-                }
-            }
-        }
+        while ((_tree.K * treeIndex) + 1 < _tree.nodes.length) {
+            bool picked = false;
+            for (uint256 i = 1; i <= _tree.K; i++) {
+                uint256 nodeIndex = (_tree.K * treeIndex) + i;
+                if (nodeIndex >= _tree.nodes.length) continue; // missing child slot => value 0
+                uint256 nodeValue = _tree.nodes[nodeIndex];
+                if (currentDrawnNumber < nodeValue) {
+                    treeIndex = nodeIndex;
+                    picked = true;
+                    break;
+                }
+                currentDrawnNumber -= nodeValue;
+            }
+            if (!picked) revert InvalidTreeState(); // defensive: sums must match
+        }

Add the new error:

     error TreeAlreadyExists();
     error KMustBeGreaterThanOne();
+    error InvalidTreeState();
📝 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
// While it still has children
uint256 treeIndex = 0;
while ((_tree.K * treeIndex) + 1 < _tree.nodes.length) {
for (uint256 i = 1; i <= _tree.K; i++) {
// Loop over children.
uint256 nodeIndex = (_tree.K * treeIndex) + i;
uint256 nodeValue = _tree.nodes[nodeIndex];
if (currentDrawnNumber >= nodeValue) {
// Go to the next child.
currentDrawnNumber -= nodeValue;
} else {
// Pick this child.
treeIndex = nodeIndex;
break;
}
}
}
// While it still has children
uint256 treeIndex = 0;
while ((_tree.K * treeIndex) + 1 < _tree.nodes.length) {
bool picked = false;
for (uint256 i = 1; i <= _tree.K; i++) {
uint256 nodeIndex = (_tree.K * treeIndex) + i;
if (nodeIndex >= _tree.nodes.length) continue; // missing child slot ⇒ value 0
uint256 nodeValue = _tree.nodes[nodeIndex];
if (currentDrawnNumber < nodeValue) {
// Pick this child.
treeIndex = nodeIndex;
picked = true;
break;
}
// Otherwise subtract and continue.
currentDrawnNumber -= nodeValue;
}
if (!picked) revert InvalidTreeState(); // defensive: sums must match
}
Suggested change
// While it still has children
uint256 treeIndex = 0;
while ((_tree.K * treeIndex) + 1 < _tree.nodes.length) {
for (uint256 i = 1; i <= _tree.K; i++) {
// Loop over children.
uint256 nodeIndex = (_tree.K * treeIndex) + i;
uint256 nodeValue = _tree.nodes[nodeIndex];
if (currentDrawnNumber >= nodeValue) {
// Go to the next child.
currentDrawnNumber -= nodeValue;
} else {
// Pick this child.
treeIndex = nodeIndex;
break;
}
}
}
error TreeAlreadyExists();
error KMustBeGreaterThanOne();
error InvalidTreeState();
🤖 Prompt for AI Agents
In contracts/src/libraries/SortitionTrees.sol around lines 66 to 83, the loop
unconditionally indexes children and can read past nodes.length or loop forever
if no child consumes the draw; add a bounds check before computing/reading
nodeValue (skip or treat missing child as zero), and if you detect nodeIndex >=
_tree.nodes.length then revert with a new descriptive error (e.g.,
NoChildAtIndex) or handle by breaking out safely; ensure the while loop can
always exit by either selecting a valid child or reverting when no children
exist; also add the new error declaration near the top of the contract/library.

Comment on lines +99 to +105
const nodes = await sortitionTree.getTreeNodes(testCase.courtID);
expect(nodes.length).to.equal(1);
expect(nodes[0]).to.equal(0); // Root starts at 0

const stack = await sortitionTree.getTreeStack(testCase.courtID);
expect(stack.length).to.equal(0); // Empty stack initially
}
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

Fix BigInt vs number mismatches in assertions (ethers v6 returns BigInt).

Many contract reads return BigInt; several expects compare to JS numbers, causing fragile or failing tests. Prefer 0n/100n or cast with BigInt()/Number() consistently.

Examples below; please apply the same pattern across the file.

@@
- expect(nodes[0]).to.equal(0); // Root starts at 0
+ expect(nodes[0]).to.equal(0n); // Root starts at 0

- expect(await sortitionTree.stakeOf(0, juror)).to.equal(stake);
- expect(await sortitionTree.getRootSum(0)).to.equal(stake);
+ expect(await sortitionTree.stakeOf(0, juror)).to.equal(BigInt(stake));
+ expect(await sortitionTree.getRootSum(0)).to.equal(BigInt(stake));

- const expectedTotal = stakes.reduce((sum, stake) => sum + stake, 0);
+ const expectedTotal = stakes.reduce((sum, stake) => sum + BigInt(stake), 0n);
  expect(await sortitionTree.getRootSum(0)).to.equal(expectedTotal);

- expect(await sortitionTree.getRootSum(0)).to.equal(200 + newStake);
+ expect(await sortitionTree.getRootSum(0)).to.equal(200n + BigInt(newStake));

- expect(await sortitionTree.getNodeIndex(0, juror)).to.equal(0); // Should be cleared
+ expect(await sortitionTree.getNodeIndex(0, juror)).to.equal(0n); // Should be cleared

- expect(await sortitionTree.stakeOf(0, juror4)).to.equal(150);
+ expect(await sortitionTree.stakeOf(0, juror4)).to.equal(150n);

- expect(courtID).to.equal(0);
+ expect(Number(courtID)).to.equal(0);

- const stakes = await sortitionTree.getStakesAcrossCourts(juror, courtIDs);
- for (const retrievedStake of stakes) {
-   expect(retrievedStake).to.equal(stake);
- }
+ const stakes = await sortitionTree.getStakesAcrossCourts(juror, courtIDs);
+ for (const retrievedStake of stakes) {
+   expect(retrievedStake).to.equal(BigInt(stake));
+ }

- expect(await sortitionTree.getRootSum(0)).to.equal(700);
+ expect(await sortitionTree.getRootSum(0)).to.equal(700n);

Also applies to: 144-151, 162-165, 179-181, 191-204, 205-213, 221-224, 241-244, 268-270, 280-283, 289-295, 309-314, 331-334, 341-353, 375-378, 390-396, 411-415, 445-446, 475-479, 556-559, 569-587, 609-614, 677-681, 692-695

🤖 Prompt for AI Agents
In contracts/test/sortition/index.ts around lines 99-105 (and also update the
ranges 144-151, 162-165, 179-181, 191-204, 205-213, 221-224, 241-244, 268-270,
280-283, 289-295, 309-314, 331-334, 341-353, 375-378, 390-396, 411-415, 445-446,
475-479, 556-559, 569-587, 609-614, 677-681, 692-695) assertions compare BigInt
results from ethers v6 to JS numbers; change those expectations to use BigInt
literals (e.g., 0n, 100n) or cast the contract return explicitly with
BigInt()/Number() consistently so types match. For each expect(...)
.to.equal(...) replace numeric RHS with BigInt literals or wrap the LHS in
Number(...) if you intend JS numbers, ensuring consistency across all listed
lines, and update any length/element comparisons likewise to compare BigInt to
BigInt or numbers to numbers.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
contracts/src/arbitration/dispute-kits/DisputeKitSybilResistant.sol (1)

39-55: Add zero-address validation for _poh and _wNative.
Prevent silent misconfiguration by validating inputs before calling the base initializer.

 function initialize(
     address _owner,
     KlerosCore _core,
     IProofOfHumanity _poh,
     address _wNative,
     uint256 _jumpDisputeKitID
 ) external reinitializer(1) {
+    require(address(_poh) != address(0), "DKSR: POH zero");
+    require(_wNative != address(0), "DKSR: wNative zero");
     __DisputeKitClassicBase_initialize(_owner, _core, _wNative, _jumpDisputeKitID);
     poh = _poh;
     singleDrawPerJuror = true;
 }

Confirmed reinitializer(1) does not conflict with any base or sibling initializers.

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

142-146: Fix dependency tag: script depends on RNGWithFallback, not ChainlinkRNG

This deploy retrieves RNGWithFallback (Line 53) and will fail if it hasn’t been deployed yet. Update the dependency tag to ensure ordering.

-deployArbitration.dependencies = ["ChainlinkRNG"];
+deployArbitration.dependencies = ["RNGWithFallback"];
♻️ Duplicate comments (9)
contracts/CHANGELOG.md (3)

30-30: Clarify pragma widening scope and exact range.

Reflect actual range and include RNG contracts if applicable.

- Widen the allowed solc version to any v0.8.x for the interfaces only ([#2083](https://github.com/kleros/kleros-v2/issues/2083))
+ Widen pragma to `>=0.8.0 <0.9.0` for interfaces and RNG contracts ([#2083](https://github.com/kleros/kleros-v2/issues/2083))

32-32: Fix constant name and wording.

Use singular and a clearer action verb.

- Consolidate the constant `ALPHA_DIVISOR` with `ONE_BASIS_POINTS` ([#2090](https://github.com/kleros/kleros-v2/issues/2090))
+ Replace `ALPHA_DIVISOR` with `ONE_BASIS_POINT` ([#2090](https://github.com/kleros/kleros-v2/issues/2090))

158-159: Remove unpublished 0.13.0 release link to avoid 404s.

Keep the anchor commented until the tag is live.

-[0.13.0]: https://github.com/kleros/kleros-v2/releases/tag/@kleros%[email protected]
+<!-- [0.13.0]: (add after publishing) -->

Run to confirm availability before uncommenting:

#!/usr/bin/env bash
curl -s -o /dev/null -w "%{http_code}\n" \
"https://github.com/kleros/kleros-v2/releases/tag/@kleros%[email protected]"
contracts/src/arbitration/interfaces/IDisputeKit.sol (1)

83-96: Fix NatSpec: “reward” → “penalty” (still unresolved).

The @return text for the penalty accessor incorrectly says “reward”.

-/// @return pnkCoherence The degree of coherence in basis points for the dispute PNK reward.
+/// @return pnkCoherence The degree of coherence in basis points for the dispute PNK penalty.
 function getDegreeOfCoherencePenalty(
contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol (1)

138-141: Guard against inactive/unknown disputes in notJumped.

Accessing disputes[coreDisputeIDToLocal[_coreDisputeID]] without an active check can read an uninitialized struct and underflow on rounds.length - 1. Add the active guard.

 modifier notJumped(uint256 _coreDisputeID) {
+    if (!coreDisputeIDToActive[_coreDisputeID]) revert NotActiveForCoreDisputeID();
     if (disputes[coreDisputeIDToLocal[_coreDisputeID]].jumped) revert DisputeJumpedToParentDK();
     _;
 }
contracts/src/arbitration/KlerosCoreBase.sol (4)

758-773: Coherence clamping is good; consider adding reentrancy protection.

The penalty execution correctly clamps coherence to ONE_BASIS_POINT and properly calculates penalties. However, the function performs external calls before completing all state updates.

Based on the team's preference for gas optimization (as noted in learnings), the current implementation is acceptable. The comment at line 746 acknowledges the CEI pattern break. Consider adding a nonReentrant modifier to the main execute() function if reentrancy becomes a concern.


273-280: Add event emission and consider reentrancy protection for owner's arbitrary calls.

This function allows arbitrary external calls with ETH value. While the owner is trusted, it's good practice to emit an event for auditability and consider reentrancy protection.

+    event OwnerCallExecuted(address indexed destination, uint256 value, bytes data, bool success);
     function executeOwnerProposal(address _destination, uint256 _amount, bytes memory _data) external onlyByOwner {
         (bool success, ) = _destination.call{value: _amount}(_data);
         if (!success) revert UnsuccessfulCall();
+        emit OwnerCallExecuted(_destination, _amount, _data, success);
     }

282-286: Validate owner address and emit ownership change event.

The function should prevent setting a zero address (unless renouncement is intended) and emit an event for transparency.

+    event OwnerChanged(address indexed previousOwner, address indexed newOwner);
     function changeOwner(address payable _owner) external onlyByOwner {
+        if (_owner == address(0)) revert InvalidAddress();
+        address previousOwner = owner;
         owner = _owner;
+        emit OwnerChanged(previousOwner, _owner);
     }

Additionally, define the error:

error InvalidAddress();

288-311: Add zero address validation for critical setter functions.

These setter functions should validate against zero addresses to prevent accidental misconfigurations.

     function changeGuardian(address _guardian) external onlyByOwner {
+        if (_guardian == address(0)) revert InvalidAddress();
         guardian = _guardian;
     }

     function changePinakion(IERC20 _pinakion) external onlyByOwner {
+        if (address(_pinakion) == address(0)) revert InvalidAddress();
         pinakion = _pinakion;
     }

     function changeJurorProsecutionModule(address _jurorProsecutionModule) external onlyByOwner {
+        if (_jurorProsecutionModule == address(0)) revert InvalidAddress();
         jurorProsecutionModule = _jurorProsecutionModule;
     }

     function changeSortitionModule(ISortitionModule _sortitionModule) external onlyByOwner {
+        if (address(_sortitionModule) == address(0)) revert InvalidAddress();
         sortitionModule = _sortitionModule;
     }

Note: When changing the sortition module, ensure all existing courts are properly initialized with the new module through migration scripts.

🧹 Nitpick comments (19)
contracts/CHANGELOG.md (5)

22-22: Spelling: “bypass” (no hyphen).

- Allow stake changes to by-pass delayed stakes when initiated by the SortitionModule by setting the `_noDelay` parameter to `true` in `SortitionModule.validateStake()` ([#2107](https://github.com/kleros/kleros-v2/issues/2107))
+ Allow stake changes to bypass delayed stakes when initiated by the SortitionModule by setting `_noDelay` to `true` in `SortitionModule.validateStake()` ([#2107](https://github.com/kleros/kleros-v2/issues/2107))

25-25: Clarify what switches to timestamps.

Blockhash requires block numbers; if the timeout calculation (not the hash lookup) is what moved to timestamps, say so to avoid confusion.

- Use `block.timestamp` rather than `block.number` for `BlockhashRNG` for better reliability on Arbitrum as block production is sporadic depending on network conditions. ([#2054](https://github.com/kleros/kleros-v2/issues/2054))
+ Use timestamp-based timeouts instead of block-number-based ones in `BlockhashRNG` to improve reliability on Arbitrum, where block production can be sporadic. ([#2054](https://github.com/kleros/kleros-v2/issues/2054))

43-43: Grammar/clarity: “advance to the Voting period.”

- Do not pass to Voting period if all the commits are cast because it breaks the current Shutter auto-reveal process. ([#2085](https://github.com/kleros/kleros-v2/issues/2085))
+ Do not advance to the Voting period when all commits are already cast; this preserves the current Shutter auto‑reveal process. ([#2085](https://github.com/kleros/kleros-v2/issues/2085))

15-15: Style + canonical ERC link.

Concise phrasing and link to the official EIP page.

- **Breaking:** Rename `governor` to `owner` in order to comply with the lightweight ownership standard [ERC-5313](https://eipsinsight.com/ercs/erc-5313) ([#2112](https://github.com/kleros/kleros-v2/issues/2112))
+ **Breaking:** Rename `governor` to `owner` to comply with [ERC‑5313](https://eips.ethereum.org/EIPS/eip-5313) ([#2112](https://github.com/kleros/kleros-v2/issues/2112))

31-31: Confirm whether a separate penalty function/API was added.

AI notes suggest splitting coherence into reward/penalty functions. If true, document it explicitly here.

Proposed addendum:

  • Add getDegreeOfCoherencePenalty() (separate penalty path) and align docs accordingly (#2090)
contracts/src/arbitration/interfaces/IDisputeKit.sol (2)

46-55: Add missing NatSpec for new return value in draw.

Document fromSubcourtID to match the expanded signature.

 /// @param _nonce Nonce.
-/// @return drawnAddress The drawn address.
+/// @return drawnAddress The drawn address.
+/// @return fromSubcourtID The originating subcourt ID of the drawn juror.
 function draw(

124-132: Confirm TODO or remove param.

The _previousDisputeKit parameter is marked TODO for removal. Either remove it now (breaking change) or track a follow-up to deprecate it in the next major.

contracts/deploy/00-home-chain-arbitration-neo.ts (1)

39-44: Nonce-based precompute: add a sanity check.

Given the reliance on nonce ordering, assert the deployed KlerosCoreNeo address equals the precomputed klerosCoreAddress after deployment to fail fast if nonces shift.

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

168-175: Emit event on successful owner call (optional).

Add an event to audit privileged calls.

 function executeOwnerProposal(address _destination, uint256 _amount, bytes memory _data) external onlyByOwner {
     (bool success, ) = _destination.call{value: _amount}(_data);
     if (!success) revert UnsuccessfulCall();
+    emit OwnerCallExecuted(_destination, _amount, _data);
 }

Add this event in the events section:

event OwnerCallExecuted(address indexed destination, uint256 value, bytes data);

177-194: Ownership/admin setters: add events and align types.

  • changeOwner takes address payable but stores address; make the param address for consistency.
  • Emit events for changeOwner/changeCore/changeJumpDisputeKitID.
-/// @param _owner The new value for the `owner` storage variable.
-function changeOwner(address payable _owner) external onlyByOwner {
-    owner = _owner;
-}
+/// @param _owner The new value for the `owner` storage variable.
+function changeOwner(address _owner) external onlyByOwner {
+    owner = _owner;
+    emit OwnerChanged(_owner);
+}

- function changeCore(address _core) external onlyByOwner {
-    core = KlerosCore(_core);
-}
+ function changeCore(address _core) external onlyByOwner {
+    core = KlerosCore(_core);
+    emit CoreChanged(_core);
+}

- function changeJumpDisputeKitID(uint256 _jumpDisputeKitID) external onlyByOwner {
-    jumpDisputeKitID = _jumpDisputeKitID;
-}
+ function changeJumpDisputeKitID(uint256 _jumpDisputeKitID) external onlyByOwner {
+    jumpDisputeKitID = _jumpDisputeKitID;
+    emit JumpDisputeKitIDChanged(_jumpDisputeKitID);
+}

Add events:

event OwnerChanged(address indexed newOwner);
event CoreChanged(address indexed newCore);
event JumpDisputeKitIDChanged(uint256 indexed newJumpDisputeKitID);

650-656: Fix typo and phrasing in NatSpec (“kid” → “kit”).

-/// @dev Returns the dispute kid ID be used after court jump by Kleros Core.
+/// @dev Returns the dispute kit ID to be used after a court jump by Kleros Core.
contracts/src/arbitration/dispute-kits/DisputeKitShutter.sol (1)

45-56: Initializer wiring: OK; consider adding zero-address checks.
Owner/core/wNative/jumpDisputeKitID are forwarded as expected. Add validation to hard-fail on bad params.

Apply this diff:

 function initialize(
     address _owner,
     KlerosCore _core,
     address _wNative,
     uint256 _jumpDisputeKitID
 ) external reinitializer(1) {
-    __DisputeKitClassicBase_initialize(_owner, _core, _wNative, _jumpDisputeKitID);
+    require(_owner != address(0), "DKShutter: owner zero");
+    require(address(_core) != address(0), "DKShutter: core zero");
+    require(_wNative != address(0), "DKShutter: wNative zero");
+    __DisputeKitClassicBase_initialize(_owner, _core, _wNative, _jumpDisputeKitID);
 }
contracts/test/foundry/KlerosCore_Voting.t.sol (3)

14-123: Commit/reveal happy-path and guards look solid.
Good coverage of period gating, ownership checks, and hash mismatches.

  • Consider asserting nbVoters/choiceCount when fetched to avoid unused locals.

161-270: Direct voting flow: OK; stabilize magic numbers.
Assertions/readability would benefit from named constants for choices (e.g., YES=1, NO=2).

Apply this diff snippet at top of the test or within the function:

-        vm.prank(staker1);
+        uint256 constant NO = 2;
+        vm.prank(staker1);
...
-        disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ");
+        disputeKit.castVote(disputeID, voteIDs, NO, 0, "XYZ");

387-486: Derive newDkID dynamically instead of hard-coding
Replace the hard-coded 2 with the length getter, e.g.:

-        uint256 newDkID = 2;
+        // Use Core’s getter to avoid relying on fixed ordering
+        uint256 newDkID = core.getDisputeKitsLength() - 1;

Drop the TODO once this is in place. Optionally, assert the proxy’s implementation matches address(dkLogic) after initialization to enforce UUPS invariants.

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

44-49: Precomputed KlerosCore address is brittle—add a sanity check after deploy

If any extra tx slips in, SortitionModule would be initialized with a wrong core address. Add a post-deploy assert to fail fast.

   const core = await hre.ethers.getContract<KlerosCore>("KlerosCore");
+  // Sanity check: precomputed vs deployed proxy address must match
+  if ((klerosCoreAddress ?? "").toLowerCase() !== core.target.toLowerCase()) {
+    throw new Error(
+      `KlerosCore address mismatch: precomputed ${klerosCoreAddress} vs deployed ${core.target}`
+    );
+  }
contracts/test/foundry/KlerosCore_TestBase.sol (3)

117-124: Prefer abi.encodeCall over abi.encodeWithSignature for safety

Stronger type safety and refactor resilience.

-        bytes memory initDataDk = abi.encodeWithSignature(
-            "initialize(address,address,address,uint256)",
-            owner,
-            address(proxyCore),
-            address(wNative),
-            DISPUTE_KIT_CLASSIC
-        );
+        bytes memory initDataDk = abi.encodeCall(
+            DisputeKitClassic.initialize,
+            (owner, address(proxyCore), address(wNative), DISPUTE_KIT_CLASSIC)
+        );

-        bytes memory initDataSm = abi.encodeWithSignature(
-            "initialize(address,address,uint256,uint256,address)",
-            owner,
-            address(proxyCore),
-            minStakingTime,
-            maxDrawingTime,
-            rng
-        );
+        bytes memory initDataSm = abi.encodeCall(
+            SortitionModuleMock.initialize,
+            (owner, address(proxyCore), minStakingTime, maxDrawingTime, address(rng))
+        );

-        bytes memory initDataRegistry = abi.encodeWithSignature("initialize(address)", owner);
+        bytes memory initDataRegistry = abi.encodeCall(DisputeTemplateRegistry.initialize, (owner));

Also applies to: 128-136, 165-167


112-114: Initialize BlockHashRNG with a clear zero consumer, then set it

Make the intent explicit and avoid passing an uninitialized variable cast to address.

-        rngLookahead = 30;
-        rng = new BlockHashRNG(msg.sender, address(sortitionModule), rngLookahead);
+        rngLookahead = 30;
+        rng = new BlockHashRNG(msg.sender, address(0), rngLookahead);
...
         vm.prank(owner);
         rng.changeConsumer(address(sortitionModule));

Also applies to: 137-141


188-213: Avoid double external call in _createStandardCourt

Micro-opt to store children locally before indexing.

-        return uint96(core.getCourtChildren(parent)[core.getCourtChildren(parent).length - 1]);
+        uint256[] memory children = core.getCourtChildren(parent);
+        return uint96(children[children.length - 1]);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a7cfba0 and 068df77.

📒 Files selected for processing (15)
  • contracts/CHANGELOG.md (2 hunks)
  • contracts/deploy/00-home-chain-arbitration-neo.ts (6 hunks)
  • contracts/deploy/00-home-chain-arbitration.ts (5 hunks)
  • contracts/src/arbitration/KlerosCoreBase.sol (27 hunks)
  • contracts/src/arbitration/dispute-kits/DisputeKitClassic.sol (2 hunks)
  • contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol (14 hunks)
  • contracts/src/arbitration/dispute-kits/DisputeKitGated.sol (2 hunks)
  • contracts/src/arbitration/dispute-kits/DisputeKitGatedShutter.sol (2 hunks)
  • contracts/src/arbitration/dispute-kits/DisputeKitShutter.sol (2 hunks)
  • contracts/src/arbitration/dispute-kits/DisputeKitSybilResistant.sol (3 hunks)
  • contracts/src/arbitration/interfaces/IDisputeKit.sol (4 hunks)
  • contracts/test/foundry/KlerosCore_Appeals.t.sol (1 hunks)
  • contracts/test/foundry/KlerosCore_Initialization.t.sol (1 hunks)
  • contracts/test/foundry/KlerosCore_TestBase.sol (1 hunks)
  • contracts/test/foundry/KlerosCore_Voting.t.sol (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • contracts/test/foundry/KlerosCore_Appeals.t.sol
  • contracts/test/foundry/KlerosCore_Initialization.t.sol
🧰 Additional context used
🧠 Learnings (5)
📚 Learning: 2025-09-03T22:48:32.951Z
Learnt from: jaybuidl
PR: kleros/kleros-v2#0
File: :0-0
Timestamp: 2025-09-03T22:48:32.951Z
Learning: In the Kleros v2 codebase, the team prioritizes gas optimization over strict CEI pattern compliance when dealing with trusted contracts. For penalty execution logic, they prefer batching storage writes (`round.pnkPenalties`) rather than updating incrementally after each penalty calculation to save gas costs, as the risk is extremely low between trusted contracts.

Applied to files:

  • contracts/src/arbitration/KlerosCoreBase.sol
📚 Learning: 2024-11-19T16:09:41.467Z
Learnt from: jaybuidl
PR: kleros/kleros-v2#1746
File: contracts/config/courts.v2.mainnet-neo.json:3-17
Timestamp: 2024-11-19T16:09:41.467Z
Learning: In `contracts/config/courts.v2.mainnet-neo.json`, the General Court (ID: 1) can have its `parent` ID set to itself (`"parent": 1`), as there is no parent court with ID 0 currently.

Applied to files:

  • contracts/src/arbitration/KlerosCoreBase.sol
📚 Learning: 2024-11-19T16:31:08.965Z
Learnt from: jaybuidl
PR: kleros/kleros-v2#1746
File: contracts/config/courts.v2.mainnet-neo.json:167-170
Timestamp: 2024-11-19T16:31:08.965Z
Learning: In the court hierarchy, child courts' `minStake` must be greater than or equal to their parent court's `minStake`.

Applied to files:

  • contracts/src/arbitration/KlerosCoreBase.sol
📚 Learning: 2024-11-19T16:31:08.965Z
Learnt from: jaybuidl
PR: kleros/kleros-v2#1746
File: contracts/config/courts.v2.mainnet-neo.json:167-170
Timestamp: 2024-11-19T16:31:08.965Z
Learning: In `contracts/config/courts.v2.mainnet-neo.json`, the `minStake` parameter is denominated in PNK, not ETH.

Applied to files:

  • contracts/deploy/00-home-chain-arbitration-neo.ts
📚 Learning: 2024-12-07T11:39:10.927Z
Learnt from: jaybuidl
PR: kleros/kleros-v2#1778
File: contracts/src/rng/ChainlinkRNG.sol:135-153
Timestamp: 2024-12-07T11:39:10.927Z
Learning: In the `contracts/src/rng/ChainlinkRNG.sol` contract, the `requestRandomness` function is restricted to being called only by the `SortitionModule`, which does not send concurrent requests. Therefore, it's acceptable not to handle multiple random number requests concurrently in this context.

Applied to files:

  • contracts/deploy/00-home-chain-arbitration-neo.ts
  • contracts/deploy/00-home-chain-arbitration.ts
🧬 Code graph analysis (2)
contracts/deploy/00-home-chain-arbitration-neo.ts (1)
contracts/deploy/utils/deployUpgradable.ts (1)
  • deployUpgradable (37-89)
contracts/deploy/00-home-chain-arbitration.ts (3)
contracts/deploy/utils/deployUpgradable.ts (1)
  • deployUpgradable (37-89)
contracts/deployments/devnet.viem.ts (1)
  • klerosCoreAddress (12320-12322)
contracts/deployments/testnet.viem.ts (1)
  • klerosCoreAddress (10190-10192)
🪛 LanguageTool
contracts/CHANGELOG.md

[style] ~15-~15: Consider a more concise word here.
Context: ...reaking:** Rename governor to owner in order to comply with the lightweight ownership s...

(IN_ORDER_TO_PREMIUM)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: Redirect rules - kleros-v2-university
  • GitHub Check: Header rules - kleros-v2-university
  • GitHub Check: Pages changed - kleros-v2-university
  • GitHub Check: Redirect rules - kleros-v2-testnet
  • GitHub Check: Redirect rules - kleros-v2-testnet
  • GitHub Check: Header rules - kleros-v2-testnet
  • GitHub Check: Header rules - kleros-v2-testnet
  • GitHub Check: Pages changed - kleros-v2-testnet
  • GitHub Check: Pages changed - kleros-v2-testnet
🔇 Additional comments (39)
contracts/src/arbitration/interfaces/IDisputeKit.sol (3)

3-3: Pragma range LGTM.

Compatible with implementing contracts using ^0.8.24.


73-82: Reward coherence API LGTM.

Signature and dual return (PNK/fee) are consistent with implementations.


119-123: earlyCourtJump view added — LGTM.

Interface matches implementations returning false by default.

contracts/deploy/00-home-chain-arbitration-neo.ts (4)

48-59: RNGWithFallback wiring LGTM.

Passing rngWithFallback.target into SortitionModuleNeo matches the new consumer model.


96-101: Consumer update flow LGTM.

Idempotent changeConsumer guarded by an equality check.


128-149: DK wiring and ID derivation LGTM.

Constructor args and post-add ID computation align with the new jump semantics.


158-160: Keep ChainlinkRNG as the dependency. The 00-rng-chainlink.ts script, tagged "ChainlinkRNG", deploys both the ChainlinkRNG and RNGWithFallback contracts; there is no script tagged "RNGWithFallback".

Likely an incorrect or invalid review comment.

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

237-257: draw path LGTM.

Tuple return and 0-address handling align with ISortitionModule and interface.


765-787: Typed errors LGTM.

Clear, specific, and used consistently across code paths.

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

171-174: LGTM! Consistent governance terminology update.

The modifier rename from onlyByGovernor to onlyByOwner is correctly implemented with the appropriate error check.


176-179: LGTM! Guardian/owner access modifier properly updated.

The combined guardian/owner modifier correctly checks both roles and throws the appropriate renamed error.


827-842: Coherence split for PNK and fees is correctly implemented.

The rewards calculation properly uses separate coherence values for PNK and fees, both clamped to ONE_BASIS_POINT. This provides more granular control over reward distribution.


856-863: Good implementation of reward staking with fallback mechanism.

The code attempts to stake PNK rewards directly through sortitionModule.setStakeReward() and falls back to a direct transfer if staking fails. This is a thoughtful design that ensures jurors always receive their rewards.


643-651: Court jump logic refactoring looks good.

The extraction of court and dispute kit jump logic into _getCourtAndDisputeKitJumps() improves code organization and maintainability.


1065-1102: Well-structured helper functions for court jumps.

The new _isCourtJumping() and _getCourtAndDisputeKitJumps() functions properly encapsulate the jump logic, making it more testable and maintainable. The fallback to DISPUTE_KIT_CLASSIC when a compatible kit isn't found is a good safety measure.

contracts/src/arbitration/dispute-kits/DisputeKitGatedShutter.sol (4)

31-31: Version bump is consistent with release.

Version correctly updated from 0.12.0 to 0.13.0.


65-72: Initializer properly updated for owner-based governance.

The initializer correctly accepts _owner instead of _governor and includes the new _jumpDisputeKitID parameter for court jump functionality.


74-76: Reinitializer correctly implements jump kit ID update.

The reinitializer(10) properly updates the jumpDisputeKitID storage variable, enabling the dispute kit jump functionality.


83-86: Upgrade authorization correctly updated to owner-based control.

The _authorizeUpgrade function now uses onlyByOwner modifier, consistent with the governance terminology refactor.

contracts/src/arbitration/dispute-kits/DisputeKitGated.sol (4)

30-30: Version bump is consistent across dispute kits.

Version correctly updated to 0.13.0, matching other dispute kit implementations.


46-53: Initializer properly implements owner governance and jump kit functionality.

The function signature and implementation correctly use _owner and include _jumpDisputeKitID for court jump support.


55-57: Reinitializer correctly updates jump dispute kit ID.

The reinitializer(10) properly sets the jumpDisputeKitID variable.


64-67: Access control properly updated for owner-based upgrades.

The _authorizeUpgrade function correctly uses the onlyByOwner modifier.

contracts/src/arbitration/dispute-kits/DisputeKitClassic.sol (4)

14-14: Version consistently updated across all dispute kits.

Version 0.13.0 aligns with the other dispute kit implementations.


30-37: Classic dispute kit initializer properly updated.

The initializer correctly implements the owner-based governance model and includes the jump dispute kit functionality.


39-41: Reinitializer implementation is consistent.

The reinitializer(10) properly updates the jumpDisputeKitID.


48-51: Upgrade control properly restricted to owner.

The _authorizeUpgrade function correctly enforces owner-only access.

contracts/src/arbitration/dispute-kits/DisputeKitSybilResistant.sol (2)

21-21: Version bump looks good.
Matches the broader SDK/contract versioning in the PR.


61-63: Owner-only upgrade gate: OK.
UUPS authorization correctly updated to owner.

contracts/src/arbitration/dispute-kits/DisputeKitShutter.sol (2)

15-15: Version bump acknowledged.
Consistent with related DKs.


67-69: Owner-only upgrade gate: OK.
UUPS authorization correctly restricted to owner.

contracts/test/foundry/KlerosCore_Voting.t.sol (4)

125-159: Timeout checks for commit period: OK.
Reverts and NewPeriod emission expectations align with timing.


272-298: Vote-period timeout path: OK.
Covers VotePeriodNotPassed and transition to appeal with proper events.


299-327: Ruling check: OK.
Covers (ruling, tied, overridden) tuple after a batch vote.


329-386: Quick pass period on single committed vote: OK.
Good assertion that period advances based on committed/cast completeness.

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

90-95: LGTM: consumer wiring uses idempotent changeConsumer

Checking rngWithFallback.consumer() before change prevents unnecessary txs and aligns with the new RNGWithFallback API.


53-57: LGTM: SortitionModule now takes RNGWithFallback target

Constructor args reflect the new RNG integration; min/max timing args remain intact.

contracts/test/foundry/KlerosCore_TestBase.sol (2)

103-106: Verify PNK initial supply assumptions

Transferring totalSupply - 2 ether to msg.sender presumes where the initial supply lives. If PNK already mints to msg.sender, this is a no-op; if not, it may revert. Confirm PNK’s constructor behavior and adjust (mint/deal) accordingly.


142-156: LGTM: Core initialization wiring is consistent with new ownership/RNG model

Parameters, DK, sortition module, and WNative wiring look coherent for the updated interfaces.

Comment on lines 546 to 560
/// @dev Gets the degree of coherence of a particular voter. This function is called by Kleros Core in order to determine the amount of the penalty.
/// @param _coreDisputeID The ID of the dispute in Kleros Core, not in the Dispute Kit.
/// @param _coreRoundID The ID of the round in Kleros Core, not in the Dispute Kit.
/// @param _voteID The ID of the vote.
/// @return pnkCoherence The degree of coherence in basis points for the dispute PNK reward.
function getDegreeOfCoherencePenalty(
uint256 _coreDisputeID,
uint256 _coreRoundID,
uint256 _voteID,
uint256 /* _feePerJuror */,
uint256 /* _pnkAtStakePerJuror */
) external view override returns (uint256 pnkCoherence) {
return _getDegreeOfCoherence(_coreDisputeID, _coreRoundID, _voteID);
}

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 NatSpec: “reward” → “penalty” in penalty accessor.

-/// @return pnkCoherence The degree of coherence in basis points for the dispute PNK reward.
+/// @return pnkCoherence The degree of coherence in basis points for the dispute PNK penalty.
 function getDegreeOfCoherencePenalty(
📝 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 Gets the degree of coherence of a particular voter. This function is called by Kleros Core in order to determine the amount of the penalty.
/// @param _coreDisputeID The ID of the dispute in Kleros Core, not in the Dispute Kit.
/// @param _coreRoundID The ID of the round in Kleros Core, not in the Dispute Kit.
/// @param _voteID The ID of the vote.
/// @return pnkCoherence The degree of coherence in basis points for the dispute PNK reward.
function getDegreeOfCoherencePenalty(
uint256 _coreDisputeID,
uint256 _coreRoundID,
uint256 _voteID,
uint256 /* _feePerJuror */,
uint256 /* _pnkAtStakePerJuror */
) external view override returns (uint256 pnkCoherence) {
return _getDegreeOfCoherence(_coreDisputeID, _coreRoundID, _voteID);
}
/// @dev Gets the degree of coherence of a particular voter. This function is called by Kleros Core in order to determine the amount of the penalty.
/// @param _coreDisputeID The ID of the dispute in Kleros Core, not in the Dispute Kit.
/// @param _coreRoundID The ID of the round in Kleros Core, not in the Dispute Kit.
/// @param _voteID The ID of the vote.
/// @return pnkCoherence The degree of coherence in basis points for the dispute PNK penalty.
function getDegreeOfCoherencePenalty(
uint256 _coreDisputeID,
uint256 _coreRoundID,
uint256 _voteID,
uint256 /* _feePerJuror */,
uint256 /* _pnkAtStakePerJuror */
) external view override returns (uint256 pnkCoherence) {
return _getDegreeOfCoherence(_coreDisputeID, _coreRoundID, _voteID);
}
🤖 Prompt for AI Agents
In contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol around lines
546 to 560, the NatSpec for getDegreeOfCoherencePenalty incorrectly says "PNK
reward"; update the function comment and @return description to say "PNK
penalty" (or "penalty" instead of "reward") so the documentation matches the
function purpose and name; make no other code changes.

Comment on lines +58 to 60
function reinitialize(uint256 _jumpDisputeKitID) external reinitializer(10) {
jumpDisputeKitID = _jumpDisputeKitID;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Critical: reinitialize is publicly callable; lets anyone set jumpDisputeKitID once.
This is an owner-governed parameter; the first arbitrary caller can lock in an unintended DK ID.

Apply this diff to restrict and reuse base logic (emits/validation if present):

-function reinitialize(uint256 _jumpDisputeKitID) external reinitializer(10) {
-    jumpDisputeKitID = _jumpDisputeKitID;
-}
+function reinitialize(uint256 _jumpDisputeKitID)
+    external
+    reinitializer(10)
+    onlyByOwner
+{
+    // Prefer the base mutator if available to keep events/validation consistent
+    // e.g., DisputeKitClassicBase.changeJumpDisputeKitID(uint256)
+    changeJumpDisputeKitID(_jumpDisputeKitID);
+}

If changeJumpDisputeKitID is not external, set via an internal helper that mirrors its checks and emits an event.

📝 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 reinitialize(uint256 _jumpDisputeKitID) external reinitializer(10) {
jumpDisputeKitID = _jumpDisputeKitID;
}
function reinitialize(uint256 _jumpDisputeKitID)
external
reinitializer(10)
onlyByOwner
{
// Prefer the base mutator if available to keep events/validation consistent
// e.g., DisputeKitClassicBase.changeJumpDisputeKitID(uint256)
changeJumpDisputeKitID(_jumpDisputeKitID);
}

Comment on lines +133 to +136
/// @dev Returns the dispute kid ID be used after court jump by Kleros Core.
/// @return The ID of the dispute kit in Kleros Core disputeKits array.
function getJumpDisputeKitID() external view returns (uint256);

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 typo and phrasing in NatSpec (“kid” → “kit”).

-/// @dev Returns the dispute kid ID be used after court jump by Kleros Core.
+/// @dev Returns the dispute kit ID to be used after a court jump by Kleros Core.
📝 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 Returns the dispute kid ID be used after court jump by Kleros Core.
/// @return The ID of the dispute kit in Kleros Core disputeKits array.
function getJumpDisputeKitID() external view returns (uint256);
/// @dev Returns the dispute kit ID to be used after a court jump by Kleros Core.
/// @return The ID of the dispute kit in Kleros Core disputeKits array.
function getJumpDisputeKitID() external view returns (uint256);
🤖 Prompt for AI Agents
In contracts/src/arbitration/interfaces/IDisputeKit.sol around lines 133 to 136,
the NatSpec comment contains a typo and awkward phrasing ("kid" → "kit" and
unclear wording); update the doc comment to read something like: "/// @dev
Returns the dispute kit ID to be used after a court jump by Kleros Core." and
ensure the @return description remains accurate: "/// @return The ID of the
dispute kit in Kleros Core disputeKits array."

Comment on lines +4 to +10
import {KlerosCore_TestBase} from "./KlerosCore_TestBase.sol";
import {KlerosCoreBase} from "../../src/arbitration/KlerosCoreBase.sol";
import {DisputeKitClassic, DisputeKitClassicBase} from "../../src/arbitration/dispute-kits/DisputeKitClassic.sol";
import {IDisputeKit} from "../../src/arbitration/interfaces/IDisputeKit.sol";
import {UUPSProxy} from "../../src/proxy/UUPSProxy.sol";
import "../../src/libraries/Constants.sol";

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

Verify DisputeKitClassicBase import path.
Base type appears imported from DisputeKitClassic.sol; in other files it lives in DisputeKitClassicBase.sol. Ensure this resolves to the intended symbol.


🏁 Script executed:

#!/bin/bash
# Show where DisputeKitClassicBase is defined and how it's imported across the repo.
rg -nP 'contract\s+DisputeKitClassicBase\b|import\s+\{[^}]*DisputeKitClassicBase' -C2

Length of output: 279793


Fix DisputeKitClassicBase import path
In contracts/test/foundry/KlerosCore_Voting.t.sol, replace the combined import from DisputeKitClassic.sol with separate imports—DisputeKitClassic remains in DisputeKitClassic.sol, but DisputeKitClassicBase must come from DisputeKitClassicBase.sol.

-import {DisputeKitClassic, DisputeKitClassicBase} from "../../src/arbitration/dispute-kits/DisputeKitClassic.sol";
+import {DisputeKitClassic} from "../../src/arbitration/dispute-kits/DisputeKitClassic.sol";
+import {DisputeKitClassicBase} from "../../src/arbitration/dispute-kits/DisputeKitClassicBase.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_TestBase} from "./KlerosCore_TestBase.sol";
import {KlerosCoreBase} from "../../src/arbitration/KlerosCoreBase.sol";
import {DisputeKitClassic, DisputeKitClassicBase} from "../../src/arbitration/dispute-kits/DisputeKitClassic.sol";
import {IDisputeKit} from "../../src/arbitration/interfaces/IDisputeKit.sol";
import {UUPSProxy} from "../../src/proxy/UUPSProxy.sol";
import "../../src/libraries/Constants.sol";
import {KlerosCore_TestBase} from "./KlerosCore_TestBase.sol";
import {KlerosCoreBase} from "../../src/arbitration/KlerosCoreBase.sol";
import {DisputeKitClassic} from "../../src/arbitration/dispute-kits/DisputeKitClassic.sol";
import {DisputeKitClassicBase} from "../../src/arbitration/dispute-kits/DisputeKitClassicBase.sol";
import {IDisputeKit} from "../../src/arbitration/interfaces/IDisputeKit.sol";
import {UUPSProxy} from "../../src/proxy/UUPSProxy.sol";
import "../../src/libraries/Constants.sol";
🤖 Prompt for AI Agents
In contracts/test/foundry/KlerosCore_Voting.t.sol around lines 4 to 10, the
current combined import pulls DisputeKitClassic and DisputeKitClassicBase from
DisputeKitClassic.sol but DisputeKitClassicBase actually lives in
DisputeKitClassicBase.sol; update the imports so DisputeKitClassic is imported
from ../../src/arbitration/dispute-kits/DisputeKitClassic.sol and
DisputeKitClassicBase is imported from
../../src/arbitration/dispute-kits/DisputeKitClassicBase.sol (keeping other
imports unchanged).

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: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
contracts/hardhat.config.ts (2)

16-24: Load dotenv before requiring task scripts

Require’d task files may read env at import time. Move dotenv.config() above the require("./scripts/...") lines to ensure env is available.

-import * as dotenv from "dotenv";
+import * as dotenv from "dotenv";
+dotenv.config();
@@
-require("./scripts/populatePolicyRegistry");
-require("./scripts/populateCourts");
+require("./scripts/populatePolicyRegistry");
+require("./scripts/populateCourts");
 require("./scripts/changeOwner");
@@
-dotenv.config();

302-306: Watcher path likely wrong (sources are in ./src)

paths.sources is "./src" but watcher watches "./contracts". It may miss changes.

-      files: ["./contracts"],
+      files: ["./src"],
contracts/src/arbitration/dispute-kits/DisputeKitGatedShutter.sol (1)

139-146: Bounds-check _voteIDs before indexing

castVoteShutter reads _voteIDs[0] without verifying length, causing an index-out-of-bounds revert instead of the intended EmptyVoteIDs error.

Apply:

 function castVoteShutter(
   uint256 _coreDisputeID,
   uint256[] calldata _voteIDs,
   uint256 _choice,
   uint256 _salt,
   string memory _justification
 ) external {
+  if (_voteIDs.length == 0) revert EmptyVoteIDs();
   Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]];
   address juror = dispute.rounds[dispute.rounds.length - 1].votes[_voteIDs[0]].account;
   ...
 }
contracts/src/arbitration/dispute-kits/DisputeKitShutter.sol (1)

123-139: Guard against empty vote set in castVoteShutter.

Indexing _voteIDs[0] will revert with out-of-bounds; add an explicit check and a dedicated error for clarity.

Apply:

 function castVoteShutter(
@@
-    ) external {
+    ) external {
+        if (_voteIDs.length == 0) revert EmptyVoteSet();
         Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]];
         address juror = dispute.rounds[dispute.rounds.length - 1].votes[_voteIDs[0]].account;
@@
         callerIsJuror = false;
     }

And add the error (see bottom of file diff below).

♻️ Duplicate comments (6)
contracts/CHANGELOG.md (2)

161-161: Remove unpublished 0.13.0 release link (still 404).

Tag @kleros/[email protected] is not present as of 2025-09-04; keep this commented until published. (github.com)

Apply:

-[0.13.0]: https://github.com/kleros/kleros-v2/releases/tag/@kleros%[email protected]
+<!-- [0.13.0]: (add after publishing) -->

26-26: Fix constant name and wording.

The constant is ONE_BASIS_POINT (singular); “replace” is clearer than “consolidate.”

-- Consolidate the constant `ALPHA_DIVISOR` with `ONE_BASIS_POINTS` ([#2090](https://github.com/kleros/kleros-v2/issues/2090))
+- Replace `ALPHA_DIVISOR` with `ONE_BASIS_POINT` ([#2090](https://github.com/kleros/kleros-v2/issues/2090))
contracts/hardhat.config.ts (1)

35-35: Expose optimizer runs via env; keep default 2000

Improves Hardhat/Foundry parity and CI flexibility. This echoes a prior suggestion.

-            runs: 2000,
+            runs: Number(process.env.SOLC_OPTIMIZER_RUNS ?? 2000),
contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol (2)

547-553: Fix NatSpec: penalty docstring says “reward” (duplicate of prior review)

Update @return description to “PNK penalty”.

-/// @return pnkCoherence The degree of coherence in basis points for the dispute PNK reward.
+/// @return pnkCoherence The degree of coherence in basis points for the dispute PNK penalty.

138-141: Guard notJumped with “active” check (duplicate of prior review)

Add NotActiveForCoreDisputeID before checking jumped to fail fast on inactive disputes.

 modifier notJumped(uint256 _coreDisputeID) {
-    if (disputes[coreDisputeIDToLocal[_coreDisputeID]].jumped) revert DisputeJumpedToParentDK();
+    if (!coreDisputeIDToActive[_coreDisputeID]) revert NotActiveForCoreDisputeID();
+    if (disputes[coreDisputeIDToLocal[_coreDisputeID]].jumped) revert DisputeJumpedToParentDK();
     _;
 }
contracts/src/arbitration/dispute-kits/DisputeKitShutter.sol (1)

73-75: Critical: reinitialize lacks access control; first caller can set jumpDisputeKitID.

Restrict to owner and reuse the base mutator to keep events/validation consistent. This was flagged previously and remains unresolved.

Apply:

-function reinitialize(uint256 _jumpDisputeKitID) external reinitializer(10) {
-    jumpDisputeKitID = _jumpDisputeKitID;
-}
+function reinitialize(uint256 _jumpDisputeKitID)
+    external
+    reinitializer(10)
+    onlyByOwner
+{
+    changeJumpDisputeKitID(_jumpDisputeKitID);
+}
🧹 Nitpick comments (23)
contracts/CHANGELOG.md (6)

7-7: Use “Unreleased” header format for not-yet-published entries.

Align with Common/Keep a Changelog style.

-## [0.13.0] - 2025-08-07 (Not published yet)
+## [0.13.0] - Unreleased

15-15: Tighten phrasing; prefer canonical EIP link.

-- **Breaking:** Rename `governor` to `owner` in order to comply with the lightweight ownership standard [ERC-5313](https://eipsinsight.com/ercs/erc-5313) ([#2112](https://github.com/kleros/kleros-v2/issues/2112))
+- **Breaking:** Rename `governor` to `owner` to comply with lightweight ownership standard [ERC‑5313](https://eips.ethereum.org/EIPS/eip-5313) ([#2112](https://github.com/kleros/kleros-v2/issues/2112))

29-29: Clarify pragma widening scope and exact range.

Current text says “interfaces only,” but PR scope appears broader. Please confirm and update.

Option A (interfaces only):

- Widen the allowed solc version to any v0.8.x for the interfaces only ([#2083](https://github.com/kleros/kleros-v2/issues/2083))
+ Widen interface pragmas to `>=0.8.0 <0.9.0` ([#2083](https://github.com/kleros/kleros-v2/issues/2083))

Option B (interfaces + RNG and others):

- Widen the allowed solc version to any v0.8.x for the interfaces only ([#2083](https://github.com/kleros/kleros-v2/issues/2083))
+ Widen pragmas to `>=0.8.0 <0.9.0` across interfaces (and RNG contracts) ([#2083](https://github.com/kleros/kleros-v2/issues/2083))

20-21: Minor style: spell out “third parties.”

- Authenticate the calls to the RNGs to prevent 3rd parties from depleting the Chainlink VRF subscription funds ([#2054](https://github.com/kleros/kleros-v2/issues/2054))
+ Authenticate the calls to the RNGs to prevent third parties from depleting the Chainlink VRF subscription funds ([#2054](https://github.com/kleros/kleros-v2/issues/2054))

42-42: Spelling: “bypass” (one word).

- Allow stake changes to by-pass delayed stakes when initiated by the SortitionModule by setting the `_noDelay` parameter to `true` in `SortitionModule.validateStake()` ([#2107](https://github.com/kleros/kleros-v2/issues/2107))
+ Allow stake changes to bypass delayed stakes when initiated by the SortitionModule by setting the `_noDelay` parameter to `true` in `SortitionModule.validateStake()` ([#2107](https://github.com/kleros/kleros-v2/issues/2107))

46-46: Grammar: “advance to the Voting period.”

- Do not pass to Voting period if all the commits are cast because it breaks the current Shutter auto-reveal process. ([#2085](https://github.com/kleros/kleros-v2/issues/2085))
+ Do not advance to the Voting period if all commits are cast, as it breaks the current Shutter auto‑reveal process. ([#2085](https://github.com/kleros/kleros-v2/issues/2085))
contracts/foundry.toml (2)

5-5: via_ir = true: good default; keep an escape hatch for comparisons.

IR pipeline is generally beneficial, but debugging certain optimizer issues is easier with a no-IR profile.

Proposed profiles:

 [profile.default]
 via_ir = true
 optimizer = true
 optimizer_runs = 10000
+
+[profile.noir]
+via_ir = false
+optimizer = true
+optimizer_runs = 200

Build without IR when needed: FOUNDRY_PROFILE=noir forge build.


6-7: 10k optimizer runs: split profiles to reduce CI time and keep release builds hot.

10k can materially slow compiles. Use a fast CI profile and keep 10k for release/gas benchmarking.

 [profile.default]
 solc = "0.8.30"
 evm_version = "cancun"
 via_ir = true
 optimizer = true
 optimizer_runs = 10000
+
+[profile.ci]
+optimizer = true
+optimizer_runs = 200
+via_ir = true
+# Fail on warnings in CI to catch issues early
+deny_warnings = true
+# Deterministic bytecode for reproducible diffs
+bytecode_hash = "none"

Use: FOUNDRY_PROFILE=ci forge build.

.github/workflows/contracts-testing.yml (5)

49-54: Drop the redundant yarn set version step.

corepack prepare [email protected] --activate is sufficient; yarn set version modifies repo files unnecessarily in CI.

       - name: Set up corepack (for yarn)
         run: |
           corepack enable
           corepack prepare [email protected] --activate
-          yarn set version 4.9.2

29-44: Validate allowed egress; avoid hard-coded IPs and add common endpoints.

54.185.253.63:443 is brittle. Consider removing it and, if needed, add api.github.com:443 and storage.googleapis.com:443 (node-gyp/prebuilds, solc mirrors).

           allowed-endpoints: >
             binaries.soliditylang.org:443
+            api.github.com:443
             classic.yarnpkg.com:443
             github.com:443
             nightly.yarnpkg.com:443
             nodejs.org:443
             objects.githubusercontent.com:443
             registry.yarnpkg.com:443
             registry.npmjs.org:443
-            54.185.253.63:443
+            storage.googleapis.com:443

76-79: Double-check test invocation context.

If you keep PnP, running from the repo root with a workspace target is often cleaner.

-      - name: Run Hardhat tests
-        run: yarn test
-        working-directory: contracts
+      - name: Run Hardhat tests
+        run: yarn workspace @kleros/kleros-v2-contracts test

26-28: Add job-level concurrency to cancel superseded runs.

Saves CI minutes on rapid pushes to the same ref.

   hardhat-tests:
     runs-on: ubuntu-latest
+    concurrency:
+      group: hardhat-tests-${{ github.workflow }}-${{ github.ref }}
+      cancel-in-progress: true

80-144: Track Foundry enablement without large commented blocks.

Consider a stub job guarded by if: false or a separate workflow to keep YAML tidy and make re-enablement a one-line change.

Example:

foundry-tests:
  if: false
  runs-on: ubuntu-latest
  steps:
    - run: echo "Foundry temporarily disabled"
contracts/hardhat.config.ts (1)

31-31: Parameterize EVM version to ease cross-network builds

Keep default “cancun” but allow override via env to unblock networks/tooling that lag.

-          evmVersion: "cancun",
+          evmVersion: process.env.SOLC_EVM_VERSION ?? "cancun",
contracts/src/arbitration/dispute-kits/DisputeKitGatedShutter.sol (2)

168-181: Consistent encoding for hashes (nit)

You mix abi.encode (full commit) and abi.encodePacked (recovery). For fixed-size (uint256,uint256) both are 64 bytes today, but using a single scheme improves auditability and reduces ambiguity.

Consider:

- return keccak256(abi.encodePacked(_choice, _salt));
+ return keccak256(abi.encode(_choice, _salt));

215-229: Assembly offsets look correct; add brief invariant comment

The 160-byte length guard matches loads at offsets 128 and 160. Add a short note that layout comes from encode(courtID,minJurors,disputeKitID,packedTokenGateAndFlag,tokenId).

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

177-181: Validate non-zero owner on transfer (optional)

Block accidental renouncement-by-zero here; if zero-owner is desired, make it explicit via a separate function.

 function changeOwner(address payable _owner) external onlyByOwner {
-    owner = _owner;
+    if (_owner == address(0)) revert UnsuccessfulCall(); // or a new ZeroAddress() error
+    owner = _owner;
 }
contracts/test/arbitration/dispute-kit-shutter.ts (3)

160-178: Read commit period from core instead of hard-coding 300s

Avoid drift with deployments that tweak timesPerPeriod; fetch from the contract.

-  const timesPerPeriod = [300, 300, 300, 300];
-  const commitPeriod = timesPerPeriod[Period.commit];
+  const court = await core.courts(courtId);
+  const commitPeriod = Number(court.timesPerPeriod[Period.commit] ?? 300);

138-149: Avoid assuming disputeId = 0

Derive from events or a counter to be robust when tests run with other suites.

- await core["createDispute(uint256,bytes)"](2, extraData, { value: arbitrationCost }).then((tx) => tx.wait());
- const disputeId = 0;
+ const tx = await core["createDispute(uint256,bytes)"](2, extraData, { value: arbitrationCost });
+ const rc = await tx.wait();
+ const disputeId = Number(rc!.logs.find((l) => l.fragment?.name === "DisputeCreation")!.args![0]);

180-203: Iterating votes via getVoteInfo: consider bounded iteration

Using try/catch sentinel is fine; optionally read votes.length via getRoundInfo to bound the loop and speed up failures.

- const maxVotes = 10; // Reasonable limit for testing
+ const [, , , , nbVoters] = await disputeKitShutter.getRoundInfo(disputeId, roundIndex, 0);
+ const maxVotes = Number(nbVoters);
contracts/src/arbitration/dispute-kits/DisputeKitShutter.sol (3)

101-121: Minor API hygiene and CEI ordering in castCommitShutter.

  • Add an explicit non-empty _voteIDs guard for clearer errors.
  • Consider moving recoveryCommitments writes after _castCommit to minimize wasted writes on revert and follow checks-effects-interactions.

Apply:

 function castCommitShutter(
@@
-    ) external notJumped(_coreDisputeID) {
-        if (_recoveryCommit == bytes32(0)) revert EmptyRecoveryCommit();
-
-        uint256 localDisputeID = coreDisputeIDToLocal[_coreDisputeID];
-        Dispute storage dispute = disputes[localDisputeID];
-        uint256 localRoundID = dispute.rounds.length - 1;
-        for (uint256 i = 0; i < _voteIDs.length; i++) {
-            recoveryCommitments[localDisputeID][localRoundID][_voteIDs[i]] = _recoveryCommit;
-        }
-
-        // `_castCommit()` ensures that the caller owns the vote
-        _castCommit(_coreDisputeID, _voteIDs, _commit);
+    ) external notJumped(_coreDisputeID) {
+        if (_voteIDs.length == 0) revert EmptyVoteSet();
+        if (_recoveryCommit == bytes32(0)) revert EmptyRecoveryCommit();
+
+        // `_castCommit()` ensures that the caller owns the vote and period is valid
+        _castCommit(_coreDisputeID, _voteIDs, _commit);
+
+        uint256 localDisputeID = coreDisputeIDToLocal[_coreDisputeID];
+        Dispute storage dispute = disputes[localDisputeID];
+        uint256 localRoundID = dispute.rounds.length - 1;
+        for (uint256 i = 0; i < _voteIDs.length; i++) {
+            recoveryCommitments[localDisputeID][localRoundID][_voteIDs[i]] = _recoveryCommit;
+        }

192-193: Complementary error for empty vote sets.

Define an explicit error for the new guards.

Apply:

-    error EmptyRecoveryCommit();
+    error EmptyRecoveryCommit();
+    error EmptyVoteSet();

28-28: Decouple callerIsJuror flag from external calls
In castVoteShutter (DisputeKitShutter.sol 133–138) you set a contract-wide callerIsJuror for the entire _castVote call, yet _castVote invokes external functions (core.disputes and core.courts in DisputeKitClassicBase.sol 309–316). If the core contract ever reenters, it could observe unintended juror-path branching in hashVote.
• Compute juror context on the fly instead: pass msg.sender into hashVote/_getExpectedVoteHash rather than relying on a transient storage flag.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 068df77 and 5e987c4.

📒 Files selected for processing (8)
  • .github/workflows/contracts-testing.yml (1 hunks)
  • contracts/CHANGELOG.md (2 hunks)
  • contracts/foundry.toml (1 hunks)
  • contracts/hardhat.config.ts (2 hunks)
  • contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol (15 hunks)
  • contracts/src/arbitration/dispute-kits/DisputeKitGatedShutter.sol (7 hunks)
  • contracts/src/arbitration/dispute-kits/DisputeKitShutter.sol (7 hunks)
  • contracts/test/arbitration/dispute-kit-shutter.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
contracts/test/arbitration/dispute-kit-shutter.ts (1)
contracts/deploy/utils/index.ts (1)
  • PNK (40-40)
🪛 LanguageTool
contracts/CHANGELOG.md

[style] ~15-~15: Consider a more concise word here.
Context: ...reaking:** Rename governor to owner in order to comply with the lightweight ownership s...

(IN_ORDER_TO_PREMIUM)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: Dependabot
  • GitHub Check: hardhat-tests
  • GitHub Check: SonarCloud
  • GitHub Check: Analyze (javascript)
  • GitHub Check: Mend Security Check
  • GitHub Check: SonarCloud
  • GitHub Check: Analyze (javascript)
  • GitHub Check: hardhat-tests
🔇 Additional comments (22)
contracts/CHANGELOG.md (1)

162-162: Confirm 0.12.0 link is live (it is).

No change needed; just confirming this anchor works. (github.com)

contracts/foundry.toml (1)

3-4: Pragmas allow 0.8.30; manual Cancun build verification required

  • All detected pragma solidity constraints (^0.8.24, ^0.8.28, >=0.8.0 <0.9.0, ^0.8.0) accept compiler version 0.8.30.
  • CI lacks a functioning forge; please run locally
    forge build -vv --compiler-version 0.8.30 --evm-version cancun
    and validate deployments on mainnet, Gnosis, Arbitrum, Sepolia.
.github/workflows/contracts-testing.yml (1)

45-48: Good pinning and submodules.

Pinned SHAs for actions and recursive submodules look solid.

contracts/hardhat.config.ts (1)

32-32: viaIR defaulting via env looks good

Sane default with easy opt-out for coverage/perf.

contracts/src/arbitration/dispute-kits/DisputeKitGatedShutter.sol (3)

37-39: Good: Recovery-commit storage added with clear indexing

Per-dispute/round/vote mapping is appropriate and the public getter simplifies tests and tooling.


75-87: Initializer as reinitializer(1): confirm upgrade path

Switching to reinitializer(1) is fine for upgrades but will brick fresh proxies if an initializer(1) was already used differently. Confirm no proxy instances have consumed version 1 for this DK in production.


231-253: Good: gating check is minimal and gas-efficient

ERC-1155 vs ERC-20/721 balance checks are clean and read-only; default “no gate” path returns early.

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

235-249: LGTM: draw() return surface updated

Returning (drawnAddress, fromSubcourtID) aligns with ISortitionModule.draw; early-return on zero address avoids extra work.


275-287: LGTM: commit flow hardens period/active/ownership checks

Clear revert reasons and batched updates per vote. Event emission preserved.


321-337: LGTM: vote flow with explicit hidden-votes validation

The HashDoesNotMatchHiddenVoteCommitment and JurorHasToOwnTheVote checks are ordered sensibly; stack-too-deep workaround is clean.


651-657: LGTM: jump DK fallback

Graceful fallback to DISPUTE_KIT_CLASSIC when unset is sensible.


742-754: LGTM: default expected-hash accessor

Virtual helper cleanly isolates commit source; enables Shutter overrides.

contracts/test/arbitration/dispute-kit-shutter.ts (3)

36-74: Deterministic RNG + dedicated hidden-vote court: solid setup

Fixture plus IncrementalNG and explicit court params create a stable testbed.


324-349: Nice: event assertions and state checks cover the happy path

Covers CommitCastShutter and VoteCast plus getVoteInfo state.


574-598: Attack-path test is valuable

Validates that non-jurors cannot exploit recovery path.

contracts/src/arbitration/dispute-kits/DisputeKitShutter.sol (7)

15-16: Version constant update looks good.


21-23: Public recoveryCommitments mapping: OK.

Clear separation by (localDisputeID, localRoundID, voteID) makes reads straightforward.


81-85: UUPS upgrade gate to owner: OK.


176-186: Expected-hash selection by context looks correct.


3-3: All target networks support EIP-1153 (transient storage)
Ethereum mainnet (Dencun/Cancun), Arbitrum One (ArbOS v20 “Atlas”), Optimism (Ecotone), Base (Ecotone), and Gnosis Chain (Pectra) all include EIP-1153 as of September 4, 2025.


60-71: reinitializer(1) safe to use: DisputeKitClassicBase does not declare any external initializer or reinitializer, so version 1 remains unconsumed and this call will succeed.


156-165: Hashing scheme alignment: verify off-chain commit generators match encodings.

Juror-path uses abi.encodePacked(_choice, _salt) while non-juror uses abi.encode(_choice, _salt, keccak256(bytes(_justification))). Confirm Shutter/off-chain tooling generates _commit and _recoveryCommit with these exact encodings to avoid silent mismatches.

Comment on lines +19 to 21
permissions: # added using https://github.com/step-security/secure-workflows
contents: read

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

Grant actions: read so the cache can restore.

When you set a restrictive permissions block, unspecified scopes default to none. actions/cache requires actions: read for restore.

-permissions: # added using https://github.com/step-security/secure-workflows
-  contents: read
+permissions: # added using https://github.com/step-security/secure-workflows
+  contents: read
+  actions: read
📝 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
permissions: # added using https://github.com/step-security/secure-workflows
contents: read
permissions: # added using https://github.com/step-security/secure-workflows
contents: read
actions: read
🤖 Prompt for AI Agents
.github/workflows/contracts-testing.yml around lines 19 to 21: the workflow’s
permissions block only sets contents: read but omits actions: read which
prevents actions/cache from restoring; update the permissions block to
explicitly include actions: read (e.g., add "actions: read" alongside contents:
read) so the cache restore step can access the actions permission.

Comment on lines +61 to +72
- name: Cache node modules
uses: actions/cache@0c907a75c2c80ebcb7f088228285e798b750cf8f # v4.2.1
env:
cache-name: cache-node-modules
with:
path: |
~/.npm
**/node_modules
key: ${{ runner.os }}-build-${{ secrets.CACHE_VERSION }}-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-build-${{ secrets.CACHE_VERSION }}-${{ env.cache-name }}-

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

Cache the Yarn Berry artifacts instead of npm/node_modules.

With Yarn 4 (PnP by default), ~/.npm and **/node_modules are often unused. Cache .yarn/cache and install state for real wins.

-      - name: Cache node modules
+      - name: Cache Yarn artifacts
         uses: actions/cache@0c907a75c2c80ebcb7f088228285e798b750cf8f # v4.2.1
-        env:
-          cache-name: cache-node-modules
         with:
-          path: |
-            ~/.npm
-            **/node_modules
-          key: ${{ runner.os }}-build-${{ secrets.CACHE_VERSION }}-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}
-          restore-keys: |
-            ${{ runner.os }}-build-${{ secrets.CACHE_VERSION }}-${{ env.cache-name }}-
+          path: |
+            .yarn/cache
+            .yarn/install-state.gz
+          key: ${{ runner.os }}-yarn-${{ secrets.CACHE_VERSION }}-${{ hashFiles('**/yarn.lock', '.yarnrc.yml') }}
+          restore-keys: |
+            ${{ runner.os }}-yarn-${{ secrets.CACHE_VERSION }}-
📝 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
- name: Cache node modules
uses: actions/cache@0c907a75c2c80ebcb7f088228285e798b750cf8f # v4.2.1
env:
cache-name: cache-node-modules
with:
path: |
~/.npm
**/node_modules
key: ${{ runner.os }}-build-${{ secrets.CACHE_VERSION }}-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-build-${{ secrets.CACHE_VERSION }}-${{ env.cache-name }}-
- name: Cache Yarn artifacts
uses: actions/cache@0c907a75c2c80ebcb7f088228285e798b750cf8f # v4.2.1
with:
path: |
.yarn/cache
.yarn/install-state.gz
key: ${{ runner.os }}-yarn-${{ secrets.CACHE_VERSION }}-${{ hashFiles('**/yarn.lock', '.yarnrc.yml') }}
restore-keys: |
${{ runner.os }}-yarn-${{ secrets.CACHE_VERSION }}-

Comment on lines +73 to +75
- name: Install contracts dependencies
run: yarn workspace @kleros/kleros-v2-contracts install

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 install step: Yarn v4 doesn’t support yarn workspace <name> install.

This will either no-op or fail unless the workspace defines an install script. Do a root install (Berry) and optionally focus the contracts workspace.

-      - name: Install contracts dependencies
-        run: yarn workspace @kleros/kleros-v2-contracts install
+      - name: Install dependencies
+        run: |
+          yarn install --immutable
+          yarn workspaces focus @kleros/kleros-v2-contracts
📝 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
- name: Install contracts dependencies
run: yarn workspace @kleros/kleros-v2-contracts install
- name: Install dependencies
run: |
yarn install --immutable
yarn workspaces focus @kleros/kleros-v2-contracts
🤖 Prompt for AI Agents
In .github/workflows/contracts-testing.yml around lines 73-75, the workflow uses
"yarn workspace @kleros/kleros-v2-contracts install", which fails under Yarn v4;
replace that step with a root install (e.g., "yarn install" or "yarn install
--immutable" for CI) and, if you want to limit deps to the contracts workspace,
follow with a workspace focus command such as "yarn workspaces focus
@kleros/kleros-v2-contracts --all" (or similar focus flags) so dependencies are
prepared for that specific workspace.

@@ -26,28 +26,13 @@ const config: HardhatUserConfig = {
solidity: {
compilers: [
{
version: "0.8.28",
version: "0.8.30",
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

Solc 0.8.30: align across toolchains

Ensure Foundry uses the same solc_version to avoid bytecode/gas drift.


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "== Hardhat =="
rg -n 'version:\s*"0\.8\.\d+"' contracts/hardhat.config.ts -n
echo
echo "== Foundry configs =="
fd -a "foundry.toml" | while read -r f; do
  echo ">>> $f"
  rg -n 'solc_version|evm_version|optimizer_runs|via_ir' "$f" || true
done

Length of output: 522


Specify solc_version in Foundry config
Add solc_version = "0.8.30" to contracts/foundry.toml to match Hardhat’s compiler version and prevent bytecode/gas drift.

🤖 Prompt for AI Agents
In contracts/foundry.toml (add a new or update existing [profile] section), set
solc_version = "0.8.30" to match the Hardhat compiler version declared in
contracts/hardhat.config.ts at line 29; update or insert the solc_version key
under the appropriate profile or the global config so Foundry uses 0.8.30 and
prevents bytecode/gas drift between toolchains.

// * Transient Storage * //
// ************************************* //

bool transient callerIsJuror;
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

Avoid transient “callerIsJuror” across external calls; pass explicit context instead

Using a transient flag that influences hashVote/_getExpectedVoteHash across external calls (core.disputes/courts) is fragile and reentrancy-prone. Prefer plumbing explicit context to hashing/expected-hash logic to avoid cross-call leakage.

Apply this refactor:

- bool transient callerIsJuror;
+// Remove transient state; compute juror context per-call.

- function castVoteShutter(...) external {
+ function castVoteShutter(...) external {
     Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]];
     address juror = dispute.rounds[dispute.rounds.length - 1].votes[_voteIDs[0]].account;
-
-    callerIsJuror = juror == msg.sender;
-    _castVote(_coreDisputeID, _voteIDs, _choice, _salt, _justification, juror);
-    callerIsJuror = false;
+    bool isJuror = (juror == msg.sender);
+    _castVoteWithContext(_coreDisputeID, _voteIDs, _choice, _salt, _justification, juror, isJuror);
 }

- function hashVote(uint256 _choice, uint256 _salt, string memory _justification)
-   public view override returns (bytes32) {
-   if (callerIsJuror) { return keccak256(abi.encodePacked(_choice, _salt)); }
-   ...
- }
+function _hashVoteWithContext(
+  uint256 _choice, uint256 _salt, string memory _justification, bool isJuror
+) internal pure returns (bytes32) {
+  if (isJuror) return keccak256(abi.encodePacked(_choice, _salt));
+  bytes32 justificationHash = keccak256(bytes(_justification));
+  return keccak256(abi.encode(_choice, _salt, justificationHash));
+}
+
+function hashVote(uint256 _choice, uint256 _salt, string memory _justification)
+  public view override returns (bytes32) {
+  // Public helper (no juror context): default to non-juror hashing.
+  bytes32 justificationHash = keccak256(bytes(_justification));
+  return keccak256(abi.encode(_choice, _salt, justificationHash));
+}

- function _getExpectedVoteHash(...) internal view override returns (bytes32) {
-   if (callerIsJuror) { return recoveryCommitments[_localDisputeID][_localRoundID][_voteID]; }
-   return disputes[_localDisputeID].rounds[_localRoundID].votes[_voteID].commit;
- }
+function _getExpectedVoteHashWithContext(
+  uint256 _localDisputeID, uint256 _localRoundID, uint256 _voteID, bool isJuror
+) internal view returns (bytes32) {
+  return isJuror
+    ? recoveryCommitments[_localDisputeID][_localRoundID][_voteID]
+    : disputes[_localDisputeID].rounds[_localRoundID].votes[_voteID].commit;
+}

+function _castVoteWithContext(
+  uint256 _coreDisputeID,
+  uint256[] calldata _voteIDs,
+  uint256 _choice,
+  uint256 _salt,
+  string memory _justification,
+  address _juror,
+  bool isJuror
+) internal {
+  // copy body of _castVote but replace:
+  // bytes32 actualVoteHash = hashVote(...);
+  // with:
+  // bytes32 actualVoteHash = _hashVoteWithContext(_choice, _salt, _justification, isJuror);
+  // and _getExpectedVoteHash(...) with _getExpectedVoteHashWithContext(..., isJuror)
+}

If you prefer a smaller diff, at minimum avoid setting the transient flag before external calls and compute the juror context only around hashing/expected-hash checks.

Also applies to: 149-155, 168-181, 193-202

🤖 Prompt for AI Agents
In contracts/src/arbitration/dispute-kits/DisputeKitGatedShutter.sol around line
44 (and also review usages at lines 149-155, 168-181, 193-202), the transient
bool callerIsJuror is set before making external calls which can leak or be
observed across reentrant/external boundaries; instead remove the transient flag
usage across external calls and pass an explicit juror-context parameter into
hashVote and _getExpectedVoteHash (or compute the juror-context locally
immediately before calling those functions). Concretely: stop setting
callerIsJuror prior to any external call, add a jurorContext parameter (or local
variable) that you compute from msg.sender/role right where
hashing/expected-hash is needed, and update hashVote/_getExpectedVoteHash
signatures and all internal call sites to accept the explicit context; if you
opt for a minimal change, ensure callerIsJuror is only assigned and used within
the same non-external-call scope (compute and consume it immediately around the
hashing checks) so it never persists across external calls.

Copy link

sonarqubecloud bot commented Sep 4, 2025

Quality Gate Failed Quality Gate failed

Failed conditions
60 Security Hotspots
E Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
contracts/src/arbitration/SortitionModuleNeo.sol (1)

95-103: State drift risk: totalStaked updates occur before super; move after and gate on actual change

If super._validateStake later returns a non-success result or defers the change (e.g., when _noDelay is false outside staking), totalStaked is still mutated, desynchronizing module state. Update totalStaked only after super and only if the stake actually changed now.

Apply:

-        if (phase == Phase.staking) {
-            if (stakeIncrease) {
-                totalStaked += stakeChange;
-            } else {
-                totalStaked -= stakeChange;
-            }
-        }
-        (pnkDeposit, pnkWithdrawal, stakingResult) = super._validateStake(_account, _courtID, _newStake, _noDelay);
+        (pnkDeposit, pnkWithdrawal, stakingResult) = super._validateStake(_account, _courtID, _newStake, _noDelay);
+        // Reflect only immediate, realized stake changes.
+        uint256 newStakeAfter = stakeOf(_account, _courtID);
+        if (newStakeAfter != currentStake && (phase == Phase.staking || _noDelay)) {
+            if (newStakeAfter > currentStake) {
+                totalStaked += (newStakeAfter - currentStake);
+            } else {
+                totalStaked -= (currentStake - newStakeAfter);
+            }
+        }
🧹 Nitpick comments (4)
contracts/src/arbitration/SortitionModuleNeo.sol (4)

5-5: Avoid transitive import of IRNG to reduce coupling

Prefer importing IRNG directly from its own interface file instead of re-exporting through SortitionModuleBase to prevent fragile compile-order or circular-dependency issues.


38-47: Reinitializer bump and IRNG migration—double-check upgrade path and null checks

  • Confirm the reinitializer(2) version is compatible with existing proxy history for this contract. Ensure no previously-used version equals 2 (otherwise initialize would be blocked). The presence of initialize4() suggests version bumps elsewhere—please verify the intended call graph.
  • Ensure _rng cannot be the zero address (if the base initializer doesn’t already validate it).

65-71: Emit events for governance changes (+ optional sanity checks)

  • Emit events when maxStakePerJuror or maxTotalStaked change to aid observability and indexing.
  • Optionally guard against setting values that would make staking impossible (e.g., zero) unless that’s intentional.

Apply this diff and add event declarations:

 function changeMaxStakePerJuror(uint256 _maxStakePerJuror) external onlyByOwner {
-    maxStakePerJuror = _maxStakePerJuror;
+    uint256 old = maxStakePerJuror;
+    maxStakePerJuror = _maxStakePerJuror;
+    emit MaxStakePerJurorChanged(old, _maxStakePerJuror);
 }
 
 function changeMaxTotalStaked(uint256 _maxTotalStaked) external onlyByOwner {
-    maxTotalStaked = _maxTotalStaked;
+    uint256 old = maxTotalStaked;
+    maxTotalStaked = _maxTotalStaked;
+    emit MaxTotalStakedChanged(old, _maxTotalStaked);
 }

Add inside the contract (near storage):

event MaxStakePerJurorChanged(uint256 previous, uint256 current);
event MaxTotalStakedChanged(uint256 previous, uint256 current);

3-3: Pragma range differs from PR objective

PR notes broadening to >=0.8.0 <0.9.0. This file still pins ^0.8.24. If intentional (e.g., due to base/library constraints), ignore; otherwise, align with the repo-wide pragma or rely on tooling config.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 5e987c4 and 98f7eac.

📒 Files selected for processing (2)
  • contracts/src/arbitration/SortitionModuleNeo.sol (5 hunks)
  • contracts/src/libraries/Constants.sol (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • contracts/src/libraries/Constants.sol
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: Analyze (javascript)
  • GitHub Check: SonarCloud
  • GitHub Check: hardhat-tests
  • GitHub Check: Scorecards analysis
  • GitHub Check: Analyze (javascript)
  • GitHub Check: SonarCloud
  • GitHub Check: hardhat-tests
🔇 Additional comments (3)
contracts/src/arbitration/SortitionModuleNeo.sol (3)

30-30: Docs: owner rename is correct

Param rename in the initializer docs matches the ownership refactor.


60-62: UUPS authorization guarded by onlyByOwner—LGTM


80-82: All _validateStake call sites updated with the new _noDelay parameter — Verified that SortitionModuleBase.sol (line 237) and SortitionModuleNeo.sol (line 102) both pass the boolean flag, matching the updated signature.

Comment on lines +88 to 90
if (juror.stakedPnk + stakeChange > maxStakePerJuror || currentStake + stakeChange > maxStakePerJuror) {
return (0, 0, StakingResult.CannotStakeMoreThanMaxStakePerJuror);
}
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

Bug: per-court limit mistakenly uses total juror stake

The check uses juror.stakedPnk (total across courts) against a per-court cap, which can incorrectly reject valid stakes in multi-court scenarios. Compare the desired per-court stake (_newStake) to maxStakePerJuror instead.

Apply:

-            if (juror.stakedPnk + stakeChange > maxStakePerJuror || currentStake + stakeChange > maxStakePerJuror) {
+            if (_newStake > maxStakePerJuror) {
                 return (0, 0, StakingResult.CannotStakeMoreThanMaxStakePerJuror);
             }

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

Successfully merging this pull request may close these issues.

4 participants