Skip to content

Conversation

jaybuidl
Copy link
Member

@jaybuidl jaybuidl commented Aug 8, 2025

PR-Codex overview

This PR focuses on significant refactoring and updates to the Kleros arbitration system, including changes to governance, contract interfaces, and the introduction of new components for improved functionality and user experience.

Detailed summary

  • Updated governance from governor to owner in multiple contracts.
  • Introduced new IRNG interface for random number generation.
  • Refactored dispute kit contracts for better modularity.
  • Improved UI components with Markdown rendering.
  • Added error handling for external link validation.
  • Enhanced deployment scripts and configuration files.
  • Updated test cases for new dispute kit functionalities.
  • Removed deprecated contracts and files.

The following files were skipped due to too many changes: contracts/scripts/populatePolicyRegistry.ts, contracts/src/proxy/KlerosProxies.sol, web/src/pages/Resolver/NavigationButtons/NextButton.tsx, web/src/components/DisputePreview/DisputeContext.tsx, contracts/deploy/change-sortition-module-rng.ts, contracts/deploy/00-randomizer-rng.ts, contracts/src/proxy/UUPSProxy.sol, contracts/src/arbitration/dispute-kits/DisputeKitClassic.sol, web/src/context/NewDisputeContext.tsx, web/src/pages/Cases/CaseDetails/Voting/Classic/Reveal.tsx, contracts/deploy/00-chainlink-rng.ts, contracts/src/arbitration/devtools/DisputeResolverRuler.sol, contracts/test/integration/getContractsViem.test.ts, contracts/test/arbitration/dispute-kit-gated-shutter.ts, web/src/pages/Cases/CaseDetails/Timeline.tsx, contracts/deployments/contractsViem.ts, contracts/deploy/00-home-chain-arbitration-university.ts, web/src/pages/Resolver/Briefing/Description.tsx, contracts/src/arbitration/dispute-kits/DisputeKitSybilResistant.sol, contracts/test/arbitration/ruler.ts, contracts/package.json, contracts/scripts/changeOwner.ts, contracts/src/gateway/interfaces/IForeignGateway.sol, web/src/pages/Resolver/Parameters/Court/index.tsx, contracts/test/arbitration/draw.ts, contracts/deployments/disputeKitsViem.ts, contracts/deploy/upgrade-all.ts, contracts/src/arbitration/interfaces/IArbitrableV2.sol, contracts/src/arbitration/evidence/EvidenceModule.sol, contracts/deployments/contractsEthers.ts, contracts/scripts/populateCourts.ts, web/src/components/DisputeFeatures/Features/GatedErc20.tsx, web/src/pages/Profile/Stakes/Header.tsx, contracts/src/arbitration/view/KlerosCoreSnapshotProxy.sol, contracts/src/arbitration/DisputeTemplateRegistry.sol, web/src/hooks/useVotingContext.tsx, contracts/test/integration/getContractsEthers.test.ts, contracts/src/proxy/UUPSProxiable.sol, web/src/components/DisputeFeatures/Features/GatedErc1155.tsx, contracts/src/arbitration/PolicyRegistry.sol, web/src/components/MarkdownEditor.tsx, web/src/components/ExternalLinkWarning.tsx, contracts/src/rng/RNGWithFallback.sol, contracts/src/kleros-v1/kleros-liquid/KlerosLiquidToV2Governor.sol, contracts/test/foundry/KlerosCore_RNG.t.sol, contracts/test/proxy/index.ts, contracts/test/evidence/index.ts, contracts/src/gateway/interfaces/IHomeGateway.sol, contracts/README.md, web/src/hooks/useTokenAddressValidation.ts, contracts/scripts/utils/contracts.ts, contracts/src/test/SortitionTreesMock.sol, web/src/pages/Resolver/Parameters/Court/FeatureSelection/index.tsx, contracts/deploy/00-home-chain-arbitration.ts, contracts/src/rng/BlockhashRNG.sol, .github/workflows/contracts-testing.yml, contracts/src/rng/RandomizerRNG.sol, contracts/src/arbitration/dispute-kits/DisputeKitGated.sol, contracts/test/integration/index.ts, contracts/src/arbitration/interfaces/IArbitratorV2.sol, web/src/components/MarkdownRenderer.tsx, contracts/src/arbitration/arbitrables/DisputeResolver.sol, web/src/consts/disputeFeature.ts, web/src/styles/mdxEditorTheme.ts, contracts/src/arbitration/devtools/KlerosCoreRuler.sol, contracts/deploy/00-home-chain-arbitration-neo.ts, contracts/src/gateway/ForeignGateway.sol, contracts/test/foundry/KlerosCore_Disputes.t.sol, contracts/CHANGELOG.md, contracts/src/arbitration/arbitrables/ArbitrableExample.sol, contracts/test/foundry/KlerosCore_Drawing.t.sol, contracts/test/foundry/KlerosCore_TestBase.sol, contracts/test/foundry/KlerosCore_Initialization.t.sol, contracts/src/libraries/SortitionTrees.sol, contracts/src/rng/ChainlinkConsumerBaseV2Plus.sol, contracts/src/arbitration/dispute-kits/DisputeKitShutter.sol, contracts/src/gateway/HomeGateway.sol, contracts/test/rng/index.ts, contracts/src/kleros-v1/kleros-liquid-xdai/xKlerosLiquidV2.sol, contracts/src/rng/ChainlinkRNG.sol, contracts/src/arbitration/interfaces/IDisputeKit.sol, contracts/src/arbitration/interfaces/ISortitionModule.sol, contracts/test/arbitration/dispute-kit-gated.ts, contracts/src/arbitration/dispute-kits/DisputeKitGatedShutter.sol, contracts/test/arbitration/staking.ts, contracts/deployments/arbitrumSepoliaDevnet/DisputeTemplateRegistryUniversity_Proxy.json, contracts/src/arbitration/university/SortitionModuleUniversity.sol, contracts/test/foundry/KlerosCore_Governance.t.sol, contracts/deployments/arbitrumSepoliaDevnet/DisputeTemplateRegistryUniversity.json, contracts/src/arbitration/KlerosGovernor.sol, contracts/src/arbitration/evidence/ModeratedEvidenceModule.sol, contracts/test/arbitration/staking-neo.ts, contracts/test/foundry/KlerosCore_Voting.t.sol, contracts/src/arbitration/SortitionModule.sol, contracts/deployments/arbitrumSepoliaDevnet/SortitionModuleUniversity_Proxy.json, contracts/test/arbitration/helpers/dispute-kit-gated-common.ts, contracts/deployments/arbitrumSepoliaDevnet/DisputeKitClassicUniversity_Proxy.json, contracts/test/sortition/index.ts, contracts/test/foundry/KlerosCore_Staking.t.sol, contracts/deployments/arbitrumSepoliaDevnet/KlerosCoreUniversity_Proxy.json, contracts/deployments/arbitrumSepoliaDevnet/SortitionModuleUniversity.json, contracts/test/arbitration/helpers/dispute-kit-shutter-common.ts, contracts/deployments/arbitrumSepoliaDevnet/DisputeKitClassicUniversity.json, contracts/test/foundry/KlerosCore_Execution.t.sol, contracts/test/foundry/KlerosCore_Appeals.t.sol, contracts/deployments/arbitrumSepoliaDevnet/KlerosCoreUniversity.json, contracts/src/arbitration/university/KlerosCoreUniversity.sol, contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol, contracts/deployments/arbitrumSepoliaDevnet/DisputeTemplateRegistryUniversity_Implementation.json, contracts/src/arbitration/KlerosCore.sol, contracts/deployments/arbitrum.ts, contracts/deployments/arbitrumSepoliaDevnet.ts, contracts/deployments/arbitrumSepoliaDevnet/DisputeResolverUniversity.json, contracts/deployments/mainnet.viem.ts, contracts/deployments/devnet.viem.ts, contracts/audit/METRICS.md, yarn.lock, contracts/deployments/arbitrumSepoliaDevnet/SortitionModuleUniversity_Implementation.json, contracts/deployments/arbitrumSepoliaDevnet/KlerosCoreUniversity_Implementation.json, contracts/deployments/arbitrumSepoliaDevnet/DisputeKitClassicUniversity_Implementation.json, contracts/audit/METRICS.html

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

Summary by CodeRabbit

  • New Features

    • Multiple dispute kits supported, RNG with fallback, token‑gate validation (ERC‑20/721/1155), juror recovery for shutter flows, and new Stakes header (Available/Staked/Locked).
  • Changes

    • Ownership model unified; rewards/penalties updated to stake PNK; juror provenance tracked in draws; timeline and voting UI refined; number formatting improved.
  • Bug Fixes

    • Abort shutter commits if API missing; skip zero‑amount transfers/events; safer commit/reveal and reward flows.
  • Documentation

    • Added llms.txt, X‑Robots‑Tag header, and contract docs build/serve scripts.
  • Chores

    • Tooling and compiler upgrades; CI/test workflow simplified.
  • Tests

    • Expanded RNG, staking, voting, appeals and dispute test coverage.

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 13f3708
🔍 Latest deploy log https://app.netlify.com/projects/kleros-v2-testnet/deploys/68efd30dba3af10008ebd13c
😎 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

Replaces governor→owner across contracts, introduces IRNG and RNGWithFallback (with blockhash fallback), extracts sortition logic into SortitionTrees, rewrites SortitionModule and KlerosCore (owner-led UUPS, juror NFT, multi‑currency fees, dispute‑kit/court jumps, rewards/penalties), expands dispute‑kit APIs, updates deployments, tests, and frontend gating/validation.

Changes

Cohort / File(s) Summary
Core rewrite & university
contracts/src/arbitration/KlerosCore.sol, contracts/src/arbitration/KlerosCoreUniversity.sol, removed contracts/src/arbitration/KlerosCoreBase.sol, many deployment artifacts under contracts/deployments/...
Reimplement core as owner-controlled UUPS with new structs/events (JurorRewardPenalty/LeftoverRewardSent), juror NFT support, multi-currency fees, dispute‑kit and court jump support, storage gaps and wide ABI/signature changes; updated deployment artifacts.
Sortition library & module
contracts/src/libraries/SortitionTrees.sol, contracts/src/arbitration/SortitionModule.sol, contracts/src/arbitration/...University.sol, removed SortitionModuleBase.sol, contracts/src/test/SortitionTreesMock.sol
Extract SortitionTrees (uint96 courtID) and rewrite SortitionModule as upgradeable with phases, delayed stakes, penalties/rewards, forcedUnstake, leftover PNK withdrawal; draw returns (address, fromSubcourtID).
RNG infra
contracts/src/rng/IRNG.sol, contracts/src/rng/RNGWithFallback.sol, BlockhashRNG.sol, ChainlinkRNG.sol, RandomizerRNG.sol, IncrementalNG.sol, contracts/src/test/RNGMock.sol
Add IRNG interface and convert RNGs to zero-arg IRNG API; add RNGWithFallback that forwards to primary RNG and falls back to blockhash after timeout; add owner/consumer controls and test mocks.
Dispute kits & voting
contracts/src/arbitration/dispute-kits/*, contracts/src/arbitration/interfaces/IDisputeKit.sol, DisputeKitClassicBase.sol
Expand IDisputeKit API (draw returns fromSubcourtID, getDegreeOfCoherenceReward/Penalty, getNbVotesAfterAppeal, earlyCourtJump, round/vote queries); introduce jumpDisputeKitID and owner-based initializers; Shutter/Gated kits add recoveryCommitments and new commit/vote signatures and behavior.
Governance & ownership rename
contracts/src/** (many), contracts/scripts/changeOwner.ts (added), removed contracts/scripts/changeGovernor.ts
Mass rename governor→owner across storage/modifiers/initializers; replace string require() reverts with custom errors; consolidate initializers and add a Hardhat change-owner task.
Interfaces, constants & removed libs
contracts/src/arbitration/interfaces/*.sol, contracts/src/libraries/Constants.sol, removed contracts/src/libraries/CappedMath.sol
Broaden Solidity pragmas, add ONE_BASIS_POINT, update many interfaces (IArbitrableV2, IArbitratorV2, ISortitionModule, etc.), and remove CappedMath (replace saturated ops with plain arithmetic).
Deployments & tooling
contracts/deploy/*, contracts/deployments/index.ts, contracts/deployments/disputeKitsViem.ts, contracts/scripts/getDisputeKits.ts, contracts/deployments/*
Remove Neo-specific naming (mainnetNeo→mainnet), add University DisputeTemplateRegistry proxies/implementations, wire RNGWithFallback into deploys, add Viem dispute-kit helper and script, update typed getContract usage, add forge doc scripts and viem peerDependency.
Tests (Hardhat & Foundry)
contracts/test/**, contracts/test/foundry/**
Add many Foundry suites; migrate Hardhat tests to typed ethers.getContract<T>, update tests to owner-based errors, remove RNG lookahead mining, and adapt tests to new APIs/events and sortition/IRNG changes.
Frontend & UX
web/src/hooks/*, web/src/pages/*, web/src/utils/extradataToTokenInfo.ts, web/src/components/*, web/netlify.toml, web/src/public/llms.txt
Add token-address validation hooks (ERC20/ERC721/ERC1155), integrate gated dispute-kit validation UI, Shutter env guard, RFA fallback in Reveal, timeline refactor to use currentPeriodIndex, copy/format tweaks, Netlify header and llms.txt.
Build & config
contracts/foundry.toml, foundry.toml, contracts/hardhat.config.ts, remappings.txt, contracts/.solcover.js, .github/workflows/contracts-testing.yml
Bump solc to 0.8.30, enable viaIR, adjust optimizer runs, add Foundry profile and remappings, set solcover irMinimum, and simplify CI to focus on Hardhat tests.
Library & test helpers
contracts/src/test/SortitionModuleMock.sol, contracts/src/test/SortitionTreesMock.sol
New SortitionTrees testing wrappers/mocks to exercise tree operations and draw semantics.
Misc & cleanup
deleted Neo-specific files/proxies, updated proxies contracts/src/proxy/*, scripts (contracts/scripts/*), metrics scripts
Remove deprecated Neo contracts/proxies and deploy scripts, add metrics/generation scripts, shell fixes and lints, and UI component replacements (Profile Stakes/Header added, Courts Header removed).

Sequence Diagram(s)

%%{init: {"theme":"base","themeVariables":{"actorBorder":"#2b2b2b","actorBg":"#f7fafc","noteBg":"#f0f4f8"}}}%%
sequenceDiagram
  autonumber
  participant SM as SortitionModule
  participant RF as RNGWithFallback
  participant PR as PrimaryRNG
  participant BH as BlockhashRNG

  SM->>RF: requestRandomness()
  RF->>PR: requestRandomness() (forward)
  RF->>RF: store requestTimestamp

  Note over SM,RF: At drawing/receive phase
  SM->>RF: receiveRandomness()
  alt Primary RNG returns value
    RF-->>SM: return primary random
  else timed out or primary returns 0
    RF->>BH: compute fallback from blockhash
    RF-->>SM: return fallback random (emit RNGFallback)
  end
  SM->>SM: draw(courtID, disputeID, nonce)
  SM-->>Caller: (drawnAddress, fromSubcourtID)
Loading
sequenceDiagram
  autonumber
  participant User
  participant Core as KlerosCore
  participant DK as IDisputeKit
  participant SM as SortitionModule

  User->>Core: createDispute(...) + fees
  Core->>DK: getNbVotesAfterAppeal(prevDK, currentNbVotes)
  DK-->>Core: nbVotesNext
  Core->>DK: earlyCourtJump(coreDisputeID)?
  alt DisputeKit requests jump
    Core->>Core: select new court & DK (getJumpDisputeKitID)
    Core-->>User: emit CourtJump / DisputeKitJump
  else continue
    Core-->>User: normal flow
  end
  Core->>SM: postDrawHook(...) / schedule draws
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~150+ minutes

Possibly related PRs

Suggested labels

Package: Contracts

Poem

"I nibble code beneath the moonlit log,
I hop through trees of stakes and fog.
Owners tend the carrot chest,
RNG fallback keeps draws at rest.
A rabbit claps — a tidy cog!" 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title Check ❓ Inconclusive The title "Release" is too short and generic to convey the primary changes; this PR performs a large, breaking refactor (governor→owner renames, IRNG/RNGWithFallback, extensive interface/storage changes, Solidity/tooling bumps, deployment and test updates). As written it does not help a reviewer quickly understand the main scope or breaking impact of the changeset. Please replace the title with a concise, specific summary (one sentence) that highlights the release version and most important changes, for example: "chore(release): v0.13.0 — rename governor→owner, add IRNG & RNGWithFallback, bump Solidity to 0.8.30". A clear title like this makes the breaking changes and intent immediately apparent to reviewers.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dev

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

netlify bot commented Aug 8, 2025

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

Name Link
🔨 Latest commit 13f3708
🔍 Latest deploy log https://app.netlify.com/projects/kleros-v2-university/deploys/68efd30d91d6c90008366623

Copy link

netlify bot commented Aug 8, 2025

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

Name Link
🔨 Latest commit 13f3708
🔍 Latest deploy log https://app.netlify.com/projects/kleros-v2-neo-devtools/deploys/68efd30d097551000838d063

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

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/university/KlerosCoreUniversity.sol (1)

203-214: Guard against zero address in initializer.

The initialize function sets owner without validating that _owner is not the zero address (line 214). If the contract is initialized with _owner = address(0), all governance functions will be permanently locked since the onlyByOwner modifier will never pass.

Apply this diff to add validation:

     function initialize(
         address _owner,
         address _instructor,
         IERC20 _pinakion,
         address _jurorProsecutionModule,
         IDisputeKit _disputeKit,
         bool _hiddenVotes,
         uint256[4] memory _courtParameters,
         uint256[4] memory _timesPerPeriod,
         ISortitionModuleUniversity _sortitionModuleAddress
     ) external initializer {
+        if (_owner == address(0)) revert ZeroAddress();
         owner = _owner;

You'll also need to add the error declaration:

error ZeroAddress();
♻️ Duplicate comments (9)
contracts/src/arbitration/arbitrables/DisputeResolver.sol (1)

99-111: Add safe lookup validation for dispute ID.

The function retrieves localDisputeID from the mapping without verifying that _arbitratorDisputeID exists. If the ID is unknown, the mapping returns 0, which could either access the wrong dispute (if index 0 is valid) or cause unexpected behavior.

This issue was previously identified. Add the UnknownDisputeID error (see past review comments) and validate the dispute exists:

     function rule(uint256 _arbitratorDisputeID, uint256 _ruling) external override {
         uint256 localDisputeID = arbitratorDisputeIDToLocalID[_arbitratorDisputeID];
+        if (localDisputeID >= disputes.length || localDisputeID == 0 && arbitratorDisputeIDToLocalID[_arbitratorDisputeID] != 0) {
+            revert UnknownDisputeID();
+        }
         DisputeStruct storage dispute = disputes[localDisputeID];

Note: A more robust approach would be to use a flag or check if arbitratorDisputeIDToLocalID has been explicitly set for the given _arbitratorDisputeID.

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

118-128: Add zero-address guards in initializer.

The initializer accepts _owner, _core, and _rng without validating they are non-zero. Setting any of these to address(0) would break access control (owner), integration (core), or randomness generation (rng).

Based on past review comments.

Apply this diff to add guards:

     ) external initializer {
+        if (_owner == address(0)) revert OwnerZeroAddress();
+        if (address(_core) == address(0)) revert CoreZeroAddress();
+        if (address(_rng) == address(0)) revert RNGZeroAddress();
         owner = _owner;
         core = _core;

Add the corresponding errors to the errors section:

+    error OwnerZeroAddress();
+    error CoreZeroAddress();
+    error RNGZeroAddress();
     error OwnerOnly();

150-151: Remove incorrect view modifier from _authorizeUpgrade.

The view modifier conflicts with the UUPSUpgradeable base contract's expected signature and prevents the upgrade mechanism from functioning correctly.

Based on past review comments.

Apply this diff:

-    function _authorizeUpgrade(address) internal view override onlyByOwner {
+    function _authorizeUpgrade(address) internal override onlyByOwner {
         // NOP

156-158: Guard against zero-address owner and emit OwnerChanged event.

Setting owner to address(0) would permanently break access control. Also, ownership changes should be logged for transparency and monitoring.

Based on past review comments.

Apply this diff:

     function changeOwner(address _owner) external onlyByOwner {
+        if (_owner == address(0)) revert OwnerZeroAddress();
+        emit OwnerChanged(owner, _owner);
         owner = _owner;
     }

Add the event to the events section:

+    /// @notice Emitted when the owner changes.
+    /// @param _previousOwner The previous owner.
+    /// @param _newOwner The new owner.
+    event OwnerChanged(address indexed _previousOwner, address indexed _newOwner);
+
     /// @notice Emitted when a juror stakes in a court.

425-429: Consider clamping unlock amount to prevent revert on underflow.

If _relativeAmount > juror.lockedPnk, the subtraction will revert due to Solidity 0.8+ underflow protection. Depending on the intended behavior, you may want to clamp the unlock to the current locked amount rather than reverting.

Based on past review comments.

If clamping is desired, apply this diff:

     function unlockStake(address _account, uint256 _relativeAmount) external override onlyByCore {
         Juror storage juror = jurors[_account];
+        if (_relativeAmount > juror.lockedPnk) {
+            _relativeAmount = juror.lockedPnk;
+        }
         juror.lockedPnk -= _relativeAmount;
         emit StakeLocked(_account, _relativeAmount, true);

Alternatively, if reverts are intended when trying to unlock more than locked, consider adding a custom error for clarity:

     function unlockStake(address _account, uint256 _relativeAmount) external override onlyByCore {
         Juror storage juror = jurors[_account];
+        if (_relativeAmount > juror.lockedPnk) revert UnlockAmountExceedsLocked();
         juror.lockedPnk -= _relativeAmount;
contracts/src/arbitration/interfaces/ISortitionModule.sol (1)

212-215: Fix @return doc for getJurorLeftoverPNK.

It returns a uint256 amount, not a boolean.

-    /// @return Whether the juror has leftover PNK.
+    /// @return The amount of leftover PNK for the juror.
contracts/src/arbitration/KlerosCore.sol (3)

396-425: Add zero-address guards to governance setters.

Prevent bricking ownership/core wiring by setting zero addresses.

 function changeOwner(address payable _owner) external onlyByOwner {
-    owner = _owner;
+    if (_owner == address(0)) revert ZeroAddressOwner();
+    owner = _owner;
 }
 function changeGuardian(address _guardian) external onlyByOwner {
-    guardian = _guardian;
+    if (_guardian == address(0)) revert ZeroAddress();
+    guardian = _guardian;
 }
 function changePinakion(IERC20 _pinakion) external onlyByOwner {
-    pinakion = _pinakion;
+    if (address(_pinakion) == address(0)) revert ZeroAddress();
+    pinakion = _pinakion;
 }
 function changeJurorProsecutionModule(address _jurorProsecutionModule) external onlyByOwner {
-    jurorProsecutionModule = _jurorProsecutionModule;
+    if (_jurorProsecutionModule == address(0)) revert ZeroAddress();
+    jurorProsecutionModule = _jurorProsecutionModule;
 }
 function changeSortitionModule(ISortitionModule _sortitionModule) external onlyByOwner {
-    sortitionModule = _sortitionModule;
+    if (address(_sortitionModule) == address(0)) revert ZeroAddress();
+    sortitionModule = _sortitionModule;
 }
 function changeJurorNft(IERC721 _jurorNft) external onlyByOwner {
-    jurorNft = _jurorNft;
+    if (address(_jurorNft) == address(0)) revert ZeroAddress();
+    jurorNft = _jurorNft;
 }

Add errors (outside this hunk):

error ZeroAddressOwner();
error ZeroAddress();

Also applies to: 581-585


369-373: _authorizeUpgrade mutability mismatch (remove view).
Parent UUPS function is non-view; overriding as view will not compile.

-    function _authorizeUpgrade(address) internal view override onlyByOwner {
+    function _authorizeUpgrade(address) internal override onlyByOwner {

1203-1205: Add zero-rate guard in convertEthToTokenAmount (avoid div-by-zero).

-    function convertEthToTokenAmount(IERC20 _toToken, uint256 _amountInEth) public view returns (uint256) {
-        return (_amountInEth * 10 ** currencyRates[_toToken].rateDecimals) / currencyRates[_toToken].rateInEth;
-    }
+    function convertEthToTokenAmount(IERC20 _toToken, uint256 _amountInEth) public view returns (uint256) {
+        CurrencyRate memory rate = currencyRates[_toToken];
+        if (rate.rateInEth == 0) revert TokenNotAccepted();
+        return (_amountInEth * 10 ** rate.rateDecimals) / rate.rateInEth;
+    }
🧹 Nitpick comments (3)
contracts/src/arbitration/SortitionModule.sol (1)

162-191: Consider emitting events for governance parameter changes.

While not critical, emitting events for minStakingTime, maxDrawingTime, maxStakePerJuror, and maxTotalStaked changes would improve observability and make governance actions easier to track off-chain.

Example for changeMinStakingTime:

     function changeMinStakingTime(uint256 _minStakingTime) external onlyByOwner {
+        emit MinStakingTimeChanged(minStakingTime, _minStakingTime);
         minStakingTime = _minStakingTime;
     }

Add corresponding events:

+    event MinStakingTimeChanged(uint256 previousValue, uint256 newValue);
+    event MaxDrawingTimeChanged(uint256 previousValue, uint256 newValue);
+    event MaxStakePerJurorChanged(uint256 previousValue, uint256 newValue);
+    event MaxTotalStakedChanged(uint256 previousValue, uint256 newValue);
contracts/src/arbitration/interfaces/ISortitionModule.sol (2)

174-190: Document the second return value of draw().

NatSpec omits fromSubcourtID.

-    /// @return drawnAddress The drawn address.
+    /// @return drawnAddress The drawn address.
+    /// @return fromSubcourtID The subcourt ID from which the juror was drawn.

101-109: Remove duplicated @dev lines in comments.

There are repeated complexity lines; tighten to a single description.

Also applies to: 118-120

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 06db2d0 and 7cb1275.

📒 Files selected for processing (8)
  • contracts/src/arbitration/KlerosCore.sol (4 hunks)
  • contracts/src/arbitration/SortitionModule.sol (2 hunks)
  • contracts/src/arbitration/arbitrables/DisputeResolver.sol (6 hunks)
  • contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol (24 hunks)
  • contracts/src/arbitration/interfaces/IArbitratorV2.sol (3 hunks)
  • contracts/src/arbitration/interfaces/IEvidence.sol (1 hunks)
  • contracts/src/arbitration/interfaces/ISortitionModule.sol (2 hunks)
  • contracts/src/arbitration/university/KlerosCoreUniversity.sol (38 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-30T17:18:12.895Z
Learnt from: jaybuidl
PR: kleros/kleros-v2#2145
File: contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol:277-286
Timestamp: 2025-09-30T17:18:12.895Z
Learning: In DisputeKitClassicBase.sol's castCommit function, jurors are allowed to re-submit commits during the commit period. The implementation uses a commitCount variable to track only first-time commits (where commit == bytes32(0)) so that totalCommitted is not incremented when a juror updates their existing commit.

Applied to files:

  • contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.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). (6)
  • GitHub Check: Analyze (javascript)
  • GitHub Check: SonarCloud
  • GitHub Check: hardhat-tests
  • GitHub Check: hardhat-tests
  • GitHub Check: Analyze (javascript)
  • GitHub Check: SonarCloud
🔇 Additional comments (28)
contracts/src/arbitration/arbitrables/DisputeResolver.sol (3)

33-40: LGTM! Clean ownership modifier implementation.

The onlyByOwner modifier correctly uses custom errors and follows best practices for access control.


77-97: LGTM! Function signature correctly updated.

The createDisputeForTemplate function properly delegates to _createDispute with the updated parameter list, aligning with the template-based dispute creation refactor.


117-138: LGTM! Good validation and event emission.

The function correctly validates minimum ruling options and emits the updated DisputeRequest event with the template ID instead of URI, aligning with the template-based dispute creation refactor.

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

7-9: LGTM! Documentation improvements.

The changes improve the documentation quality:

  • Using @notice instead of @dev is more appropriate for event documentation that external integrators will reference.
  • Typo fixes ("responsibility" and "submitting") enhance clarity.

3-3: Broadened pragma does not affect compilation constraints
IEvidence.sol only declares an event (supported since 0.8.0), and its sole importer (contracts/src/arbitration/evidence/EvidenceModule.sol) still uses ^0.8.24. No further action required.

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

42-43: LGTM! Upgradeability slots added.

The addition of __gap slots to Court, Dispute, and Round structs follows best practices for UUPS proxy upgradeability, allowing future versions to add new storage variables without breaking the storage layout.

Also applies to: 52-53, 68-69


63-63: LGTM! Per-juror court tracking added.

The drawnJurorFromCourtIDs array enables tracking which court each juror was drawn from, allowing penalties to be correctly applied to the juror's stake in the specific subcourt. This is used effectively at lines 609 (population) and 778 (penalty application).


766-768: LGTM! Defensive coherence bounds checking.

The guards preventing coherence values from exceeding ONE_BASIS_POINT (lines 766-768 for penalties, 842-847 for rewards) are good defensive programming. Even though the dispute kit should ensure coherence ∈ [0, ONE_BASIS_POINT], these checks prevent potential overflow or incorrect calculations if a malicious or buggy dispute kit returns invalid values.

Also applies to: 842-847


833-839: LGTM! Separate PNK and fee coherence for rewards.

Using separate pnkCoherence and feeCoherence values (via getDegreeOfCoherenceReward) allows for more flexible reward policies where jurors can receive different proportions of PNK vs. fee rewards based on their degree of coherence. This is a good design improvement over a single coherence value.


11-11: ONE_BASIS_POINT is correctly defined in Constants.sol (uint256 constant at line 24).

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

5-9: LGTM! Import and constant additions align with refactor.

The additions of ISortitionModule to imports and ONE_BASIS_POINT, DISPUTE_KIT_CLASSIC constants properly support the updated draw function signature and jump dispute kit fallback logic.


30-30: LGTM! Upgradeability gaps properly implemented.

The addition of __gap storage slots in structs (10 slots each) and contract storage (50 slots) follows UUPS upgradeability best practices, providing adequate headroom for future upgrades.

Also applies to: 46-46, 54-54, 74-74


45-45: LGTM! Storage additions properly support new functionality.

The new storage variables serve clear purposes:

  • alreadyDrawn mapping prevents duplicate draws when singleDrawPerJuror is enabled
  • voted field prevents double voting with explicit checks
  • owner replaces governor consistently with the refactor
  • jumpDisputeKitID enables configurable dispute kit switching with a fallback to DISPUTE_KIT_CLASSIC

All variables are properly utilized in their respective code paths.

Also applies to: 50-50, 65-65, 72-72


131-144: LGTM! Modifier refactoring is consistent.

The modifiers correctly implement:

  • onlyByOwner with OwnerOnly error (replacing governor-based access)
  • onlyByCore with KlerosCoreOnly error
  • notJumped with DisputeJumpedToParentDK error

Note: While the past review comment about adding an active-dispute guard to notJumped remains valid from a defense-in-depth perspective, the current implementation is functionally safe because all functions using this modifier (castCommit, castVote, fundAppeal) independently verify coreDisputeIDToActive.


150-165: LGTM! Initializer correctly updated.

The initializer signature properly reflects the governance refactor:

  • Accepts _owner instead of governor
  • Initializes new jumpDisputeKitID parameter
  • Maintains onlyInitializing protection

All parameters are correctly assigned to storage.


171-196: LGTM! Governance functions properly refactored.

All governance functions correctly implement owner-based access control:

  • executeOwnerProposal uses custom UnsuccessfulCall error for clarity
  • changeOwner replaces changeGovernor
  • changeCore now requires owner authorization
  • changeJumpDisputeKitID provides configuration for post-jump dispute kit

The consistent use of onlyByOwner and custom errors improves error handling.


202-224: LGTM! Signature updated to ignore nbVotes parameter.

The createDispute function correctly ignores the _nbVotes parameter (per interface update) while maintaining all initialization logic. The dispute setup, round creation, and event emission remain correct.


226-251: LGTM! Draw function correctly updated.

The function properly implements the updated signature:

  • Returns (address drawnAddress, uint96 fromSubcourtID) matching interface changes
  • Correctly unpacks both values from sortitionModule.draw
  • Handles the address(0) case (no staked jurors) with early return
  • Sets alreadyDrawn[drawnAddress] after adding the vote to prevent duplicate draws

The integration with _postDrawCheck (line 739) correctly uses the alreadyDrawn mapping.


253-288: LGTM! Commit casting correctly handles re-submissions.

The implementation properly supports multiple commit submissions:

  • Public wrapper castCommit provides external access
  • Enhanced validation with period, empty commit, and active status checks
  • commitCount logic (lines 277-286) correctly tracks only first-time commits to prevent double-counting when jurors update their commits

This matches the documented learning about allowing re-submissions during the commit period.

Based on learnings.


290-361: LGTM! Vote casting with comprehensive validation.

The implementation correctly handles vote submission:

  • Public wrapper castVote provides external access
  • Enhanced validation covers all edge cases: period checks, empty arrays, active status, choice bounds, hidden vote hash matching, and duplicate votes
  • vote.voted flag (line 340) prevents double voting with explicit check at line 338
  • Vote counting logic properly updates totals and determines winning choice

The comprehensive error handling with custom errors improves debugging and user experience.


363-432: LGTM! Appeal funding with proper period enforcement.

The function correctly implements appeal funding:

  • Enhanced validation for choice bounds, active status, and appeal period
  • Loser appeal period restriction (lines 380-385) properly enforces that losing side must appeal within the first 50% of the appeal period (using LOSER_APPEAL_PERIOD_MULTIPLIER)
  • AppealFeeIsAlreadyPaid check prevents duplicate funding
  • Two-choice funding triggers appeal with correct round management

The appeal period calculations and multi-choice funding logic are correct.


434-479: LGTM! Withdrawal protection includes pause check.

The function properly validates withdrawal conditions:

  • Ensures dispute is resolved before allowing withdrawals
  • Adds safety check for core contract pause state (line 449) to prevent operations during maintenance or emergency situations
  • Verifies active status for the dispute

The paused check is a good safety feature. The fee distribution logic correctly handles winner rewards and loser reimbursements.


485-496: LGTM! View functions with good extensibility.

The view function updates are well-designed:

  • hashVote and _getExpectedVoteHash are virtual to allow derived contracts to customize vote hashing and verification
  • getDegreeOfCoherenceReward and getDegreeOfCoherencePenalty properly implement the IDisputeKit interface
  • getJumpDisputeKitID (lines 626-629) includes a defensive fallback to DISPUTE_KIT_CLASSIC when jumpDisputeKitID is 0, ensuring a valid kit ID is always returned

The extensibility and safety features are well-considered.

Also applies to: 527-548, 626-629, 707-718


720-743: LGTM! Duplicate draw prevention correctly implemented.

The _postDrawCheck function properly prevents duplicate draws:

  • Retrieves the current round
  • Checks the alreadyDrawn mapping when singleDrawPerJuror is enabled
  • Returns false if the juror was already drawn, preventing duplicate selection

This correctly integrates with the draw function where alreadyDrawn is set after adding a vote.


745-767: LGTM! Comprehensive custom error declarations.

The custom errors cover all validation scenarios:

  • Access control (OwnerOnly, KlerosCoreOnly)
  • State validation (DisputeJumpedToParentDK, NotActiveForCoreDisputeID, CoreIsPaused, DisputeNotResolved)
  • Period enforcement (NotCommitPeriod, NotVotePeriod, NotAppealPeriod, NotAppealPeriodForLoser)
  • Input validation (EmptyCommit, EmptyVoteIDs, ChoiceOutOfBounds)
  • Vote integrity (HashDoesNotMatchHiddenVoteCommitment, VoteAlreadyCast, JurorHasToOwnTheVote)
  • Appeal constraints (AppealFeeIsAlreadyPaid)

Custom errors provide gas efficiency and clearer error messages compared to require statements.

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

83-89: currentRuling addition looks good.

Including overridden flag in the return tuple is clear and consistent.


42-51: Ensure implementers use calldata in overrides.

Both createDispute(bytes) and arbitrationCost(bytes) are declared with calldata. Implementations must match data locations; using memory in overrides will not compile. KlerosCore currently uses memory for these two (see its lines 630-639 and 1065-1068). Align to calldata.

Also applies to: 70-75, 76-82

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

22-23: Validate version override target.

If no base declares version(), this override will not compile. Either remove override or ensure a base interface/function matches.

-    string public constant override version = "2.0.0";
+    string public constant version = "2.0.0";

Comment on lines +59 to 63
/// @notice Changes the owner.
/// @param _owner The address of the new owner.
function changeOwner(address _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.

⚠️ Potential issue | 🟠 Major

Add event emission for ownership changes.

Critical governance state changes should emit events for off-chain monitoring and transparency. Consider adding an OwnerChanged event.

Example implementation:

+    event OwnerChanged(address indexed previousOwner, address indexed newOwner);
+
     /// @notice Changes the owner.
     /// @param _owner The address of the new owner.
     function changeOwner(address _owner) external onlyByOwner {
+        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
/// @notice Changes the owner.
/// @param _owner The address of the new owner.
function changeOwner(address _owner) external onlyByOwner {
owner = _owner;
}
event OwnerChanged(address indexed previousOwner, address indexed newOwner);
/// @notice Changes the owner.
/// @param _owner The address of the new owner.
function changeOwner(address _owner) external onlyByOwner {
emit OwnerChanged(owner, _owner);
owner = _owner;
}
🤖 Prompt for AI Agents
In contracts/src/arbitration/arbitrables/DisputeResolver.sol around lines 59 to
63, the changeOwner function mutates critical governance state but does not emit
an event; declare an OwnerChanged (or OwnerUpdated) event with previous and new
owner addresses and modify changeOwner to emit that event after updating owner
(or emit with previous owner captured before assignment) so off-chain services
can track ownership changes.

Comment on lines +65 to 71
function changeArbitrator(IArbitratorV2 _arbitrator) external onlyByOwner {
arbitrator = _arbitrator;
}

function changeTemplateRegistry(IDisputeTemplateRegistry _templateRegistry) external {
require(governor == msg.sender, "Access not allowed: Governor only.");
function changeTemplateRegistry(IDisputeTemplateRegistry _templateRegistry) external onlyByOwner {
templateRegistry = _templateRegistry;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add event emissions for critical configuration changes.

Both changeArbitrator and changeTemplateRegistry modify critical contract parameters but don't emit events. This impacts off-chain monitoring and auditability.

Example implementation:

+    event ArbitratorChanged(IArbitratorV2 indexed previousArbitrator, IArbitratorV2 indexed newArbitrator);
+    event TemplateRegistryChanged(IDisputeTemplateRegistry indexed previousRegistry, IDisputeTemplateRegistry indexed newRegistry);
+
     function changeArbitrator(IArbitratorV2 _arbitrator) external onlyByOwner {
+        emit ArbitratorChanged(arbitrator, _arbitrator);
         arbitrator = _arbitrator;
     }
 
     function changeTemplateRegistry(IDisputeTemplateRegistry _templateRegistry) external onlyByOwner {
+        emit TemplateRegistryChanged(templateRegistry, _templateRegistry);
         templateRegistry = _templateRegistry;
     }
📝 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 changeArbitrator(IArbitratorV2 _arbitrator) external onlyByOwner {
arbitrator = _arbitrator;
}
function changeTemplateRegistry(IDisputeTemplateRegistry _templateRegistry) external {
require(governor == msg.sender, "Access not allowed: Governor only.");
function changeTemplateRegistry(IDisputeTemplateRegistry _templateRegistry) external onlyByOwner {
templateRegistry = _templateRegistry;
}
// Add these event declarations at contract scope, before the functions
event ArbitratorChanged(
IArbitratorV2 indexed previousArbitrator,
IArbitratorV2 indexed newArbitrator
);
event TemplateRegistryChanged(
IDisputeTemplateRegistry indexed previousRegistry,
IDisputeTemplateRegistry indexed newRegistry
);
function changeArbitrator(IArbitratorV2 _arbitrator) external onlyByOwner {
emit ArbitratorChanged(arbitrator, _arbitrator);
arbitrator = _arbitrator;
}
function changeTemplateRegistry(IDisputeTemplateRegistry _templateRegistry) external onlyByOwner {
emit TemplateRegistryChanged(templateRegistry, _templateRegistry);
templateRegistry = _templateRegistry;
}
🤖 Prompt for AI Agents
In contracts/src/arbitration/arbitrables/DisputeResolver.sol around lines 65 to
71, the functions changeArbitrator and changeTemplateRegistry update critical
config but do not emit events; declare two events (e.g.,
ArbitratorChanged(address indexed previous, address indexed current, address
indexed changedBy) and TemplateRegistryChanged(address indexed previous, address
indexed current, address indexed changedBy)) near the top of the contract, and
modify each function to capture the previous value, update the state, then emit
the corresponding event with previous value, new value and msg.sender to ensure
off-chain monitoring and auditability.

Comment on lines +5 to +13
import {IArbitrableV2, IArbitratorV2} from "./interfaces/IArbitratorV2.sol";
import {IDisputeKit} from "./interfaces/IDisputeKit.sol";
import {ISortitionModule} from "./interfaces/ISortitionModule.sol";
import {Initializable} from "../proxy/Initializable.sol";
import {UUPSProxiable} from "../proxy/UUPSProxiable.sol";
import {SafeERC20, IERC20} from "../libraries/SafeERC20.sol";
import {SafeSend} from "../libraries/SafeSend.sol";
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "../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.

⚠️ Potential issue | 🔴 Critical

Fix incorrect import of IArbitrableV2.

IArbitrableV2 is not declared in IArbitratorV2.sol; this import will fail. Import it from its own file.

-import {IArbitrableV2, IArbitratorV2} from "./interfaces/IArbitratorV2.sol";
+import {IArbitratorV2} from "./interfaces/IArbitratorV2.sol";
+import {IArbitrableV2} from "./interfaces/IArbitrableV2.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 {IArbitrableV2, IArbitratorV2} from "./interfaces/IArbitratorV2.sol";
import {IDisputeKit} from "./interfaces/IDisputeKit.sol";
import {ISortitionModule} from "./interfaces/ISortitionModule.sol";
import {Initializable} from "../proxy/Initializable.sol";
import {UUPSProxiable} from "../proxy/UUPSProxiable.sol";
import {SafeERC20, IERC20} from "../libraries/SafeERC20.sol";
import {SafeSend} from "../libraries/SafeSend.sol";
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "../libraries/Constants.sol";
import {IArbitratorV2} from "./interfaces/IArbitratorV2.sol";
import {IArbitrableV2} from "./interfaces/IArbitrableV2.sol";
import {IDisputeKit} from "./interfaces/IDisputeKit.sol";
import {ISortitionModule} from "./interfaces/ISortitionModule.sol";
import {Initializable} from "../proxy/Initializable.sol";
import {UUPSProxiable} from "../proxy/UUPSProxiable.sol";
import {SafeERC20, IERC20} from "../libraries/SafeERC20.sol";
import {SafeSend} from "../libraries/SafeSend.sol";
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "../libraries/Constants.sol";
🤖 Prompt for AI Agents
In contracts/src/arbitration/KlerosCore.sol around lines 5 to 13, the import
statement mistakenly imports IArbitrableV2 from "./interfaces/IArbitratorV2.sol"
which does not declare it; change the import to the correct file (e.g.
"./interfaces/IArbitrableV2.sol") so IArbitrableV2 is imported from its own
interface file, and verify any other references/exports still compile after
updating the path.

Comment on lines +97 to +99
uint256 private constant NON_PAYABLE_AMOUNT = (2 ** 256 - 2) / 2; // An amount higher than the supply of ETH.

address public owner; // The owner of the contract.
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Replace overflowing literal with type(uint256).max.

(2 ** 256 - 2) overflows in constant context. Use type(uint256).max.

-    uint256 private constant NON_PAYABLE_AMOUNT = (2 ** 256 - 2) / 2; // An amount higher than the supply of ETH.
+    // An amount higher than the supply of ETH.
+    uint256 private constant NON_PAYABLE_AMOUNT = (type(uint256).max - 1) / 2;
📝 Committable suggestion

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

Suggested change
uint256 private constant NON_PAYABLE_AMOUNT = (2 ** 256 - 2) / 2; // An amount higher than the supply of ETH.
address public owner; // The owner of the contract.
// An amount higher than the supply of ETH.
uint256 private constant NON_PAYABLE_AMOUNT = (type(uint256).max - 1) / 2;
address public owner; // The owner of the contract.
🤖 Prompt for AI Agents
contracts/src/arbitration/KlerosCore.sol around lines 97 to 99: the constant
expression "(2 ** 256 - 2) / 2" overflows in a constant context; replace the
overflowing literal with the safe builtin constant type(uint256).max by
assigning the intended maximum uint256 value using type(uint256).max and update
the constant definition accordingly so it compiles without overflow.

Comment on lines +630 to +639
/// @inheritdoc IArbitratorV2
function createDispute(
uint256 _numberOfChoices,
bytes memory _extraData
) external payable override returns (uint256 disputeID) {
if (msg.value < arbitrationCost(_extraData)) revert ArbitrationFeesNotEnough();

return _createDispute(_numberOfChoices, _extraData, NATIVE_CURRENCY, msg.value);
}

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

Override signature/data-location mismatch with IArbitratorV2.

Interface uses calldata; these overrides use memory. Align to calldata to compile.

-    function createDispute(
-        uint256 _numberOfChoices,
-        bytes memory _extraData
-    ) external payable override returns (uint256 disputeID) {
+    function createDispute(
+        uint256 _numberOfChoices,
+        bytes calldata _extraData
+    ) external payable override returns (uint256 disputeID) {
-    function arbitrationCost(bytes memory _extraData) public view override returns (uint256 cost) {
+    function arbitrationCost(bytes calldata _extraData) public view override returns (uint256 cost) {

Also applies to: 1065-1068

🤖 Prompt for AI Agents
In contracts/src/arbitration/KlerosCore.sol around lines 630-639 (and similarly
at 1065-1068), the function overrides use `bytes memory _extraData` but the
IArbitratorV2 interface declares `bytes calldata`; change the overrides'
parameter to `bytes calldata _extraData` to match the interface. Also update any
calls/forwarding to _createDispute or arbitrationCost accordingly: either change
the internal _createDispute signature to accept calldata as well or explicitly
convert/pass the calldata to the form the internal function expects (e.g., if
_createDispute expects memory, make a single conversion at the call site).
Ensure both override declarations and any matching overloads are updated so
signatures/data locations match the interface.

Comment on lines +674 to +682
// Obtain the feeForJuror in the same currency as the _feeAmount
uint256 feeForJuror = (_feeToken == NATIVE_CURRENCY)
? court.feeForJuror
: convertEthToTokenAmount(_feeToken, court.feeForJuror);
round.nbVotes = _feeAmount / feeForJuror;
round.disputeKitID = disputeKitID;
round.pnkAtStakePerJuror = _calculatePnkAtStake(court.minStake, court.alpha);
round.totalFeesForJurors = _feeAmount;
round.feeToken = IERC20(_feeToken);
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

Guard against zero feeForJuror and zero nbVotes (division-by-zero).

feeForJuror can be zero (misconfig), making nbVotes = _feeAmount / 0. Also, nbVotes == 0 creates downstream divide-by-zero in execute().

-        uint256 feeForJuror = (_feeToken == NATIVE_CURRENCY)
+        uint256 feeForJuror = (_feeToken == NATIVE_CURRENCY)
             ? court.feeForJuror
             : convertEthToTokenAmount(_feeToken, court.feeForJuror);
-        round.nbVotes = _feeAmount / feeForJuror;
+        if (feeForJuror == 0) revert ZeroFeeForJuror();
+        round.nbVotes = _feeAmount / feeForJuror;
+        if (round.nbVotes == 0) revert InsufficientFeesForOneJuror();

Add errors (outside this hunk):

error ZeroFeeForJuror();
error InsufficientFeesForOneJuror();
🤖 Prompt for AI Agents
In contracts/src/arbitration/KlerosCore.sol around lines 674 to 682, guard
against feeForJuror being zero and nbVotes resolving to zero: first check if
feeForJuror == 0 and revert with the new ZeroFeeForJuror() error, then compute
nbVotes as _feeAmount / feeForJuror and if nbVotes == 0 revert with the new
InsufficientFeesForOneJuror() error; additionally add the two error declarations
(error ZeroFeeForJuror(); error InsufficientFeesForOneJuror();) outside this
hunk (near other error defs) so the reverts compile.

Comment on lines +1369 to +1391
// Note that if the _extraData doesn't contain 32 bytes, default values are used.
if (_extraData.length >= 64) {
assembly {
// solium-disable-line security/no-inline-assembly
courtID := mload(add(_extraData, 0x20))
minJurors := mload(add(_extraData, 0x40))
disputeKitID := mload(add(_extraData, 0x60))
}
if (courtID == FORKING_COURT || courtID >= courts.length) {
courtID = GENERAL_COURT;
}
if (minJurors == 0) {
minJurors = DEFAULT_NB_OF_JURORS;
}
if (disputeKitID == NULL_DISPUTE_KIT || disputeKitID >= disputeKits.length) {
disputeKitID = DISPUTE_KIT_CLASSIC;
}
} else {
courtID = GENERAL_COURT;
minJurors = DEFAULT_NB_OF_JURORS;
disputeKitID = DISPUTE_KIT_CLASSIC;
}
}
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

Fix out-of-bounds reads in _extraDataToCourtIDMinJurorsDisputeKit.

You read 96 bytes with only a length >= 64 check; when 64 ≤ len < 96, mload at 0x60 reads garbage. Parse each field guarded by its own length.

-        if (_extraData.length >= 64) {
-            assembly {
-                courtID := mload(add(_extraData, 0x20))
-                minJurors := mload(add(_extraData, 0x40))
-                disputeKitID := mload(add(_extraData, 0x60))
-            }
+        if (_extraData.length >= 32) {
+            assembly { courtID := mload(add(_extraData, 0x20)) }
+        } else {
+            courtID = GENERAL_COURT;
+        }
+        if (_extraData.length >= 64) {
+            assembly { minJurors := mload(add(_extraData, 0x40)) }
+        } else {
+            minJurors = DEFAULT_NB_OF_JURORS;
+        }
+        if (_extraData.length >= 96) {
+            assembly { disputeKitID := mload(add(_extraData, 0x60)) }
         }
-        else {
-            courtID = GENERAL_COURT;
-            minJurors = DEFAULT_NB_OF_JURORS;
-            disputeKitID = DISPUTE_KIT_CLASSIC;
-        }
+        if (courtID == FORKING_COURT || courtID >= courts.length) courtID = GENERAL_COURT;
+        if (minJurors == 0) minJurors = DEFAULT_NB_OF_JURORS;
+        if (disputeKitID == NULL_DISPUTE_KIT || disputeKitID >= disputeKits.length) disputeKitID = DISPUTE_KIT_CLASSIC;
📝 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
// Note that if the _extraData doesn't contain 32 bytes, default values are used.
if (_extraData.length >= 64) {
assembly {
// solium-disable-line security/no-inline-assembly
courtID := mload(add(_extraData, 0x20))
minJurors := mload(add(_extraData, 0x40))
disputeKitID := mload(add(_extraData, 0x60))
}
if (courtID == FORKING_COURT || courtID >= courts.length) {
courtID = GENERAL_COURT;
}
if (minJurors == 0) {
minJurors = DEFAULT_NB_OF_JURORS;
}
if (disputeKitID == NULL_DISPUTE_KIT || disputeKitID >= disputeKits.length) {
disputeKitID = DISPUTE_KIT_CLASSIC;
}
} else {
courtID = GENERAL_COURT;
minJurors = DEFAULT_NB_OF_JURORS;
disputeKitID = DISPUTE_KIT_CLASSIC;
}
}
// Note that if the _extraData doesn't contain 32 bytes, default values are used.
if (_extraData.length >= 32) {
assembly { // solium-disable-line security/no-inline-assembly
courtID := mload(add(_extraData, 0x20))
}
} else {
courtID = GENERAL_COURT;
}
if (_extraData.length >= 64) {
assembly { // solium-disable-line security/no-inline-assembly
minJurors := mload(add(_extraData, 0x40))
}
} else {
minJurors = DEFAULT_NB_OF_JURORS;
}
if (_extraData.length >= 96) {
assembly { // solium-disable-line security/no-inline-assembly
disputeKitID := mload(add(_extraData, 0x60))
}
}
if (courtID == FORKING_COURT || courtID >= courts.length) courtID = GENERAL_COURT;
if (minJurors == 0) minJurors = DEFAULT_NB_OF_JURORS;
if (disputeKitID == NULL_DISPUTE_KIT || disputeKitID >= disputeKits.length) disputeKitID = DISPUTE_KIT_CLASSIC;
}
🤖 Prompt for AI Agents
In contracts/src/arbitration/KlerosCore.sol around lines 1369-1391, the function
_extraDataToCourtIDMinJurorsDisputeKit currently reads three 32-byte words
unconditionally when _extraData.length >= 64, causing an out-of-bounds read if
length is between 64 and 95; change the logic to load each field only if the
data contains that field: read courtID when _extraData.length >= 32, read
minJurors when _extraData.length >= 64, and read disputeKitID when
_extraData.length >= 96; for any missing field, set the existing defaults
(GENERAL_COURT, DEFAULT_NB_OF_JURORS, DISPUTE_KIT_CLASSIC) and keep the existing
bounds checks (e.g., fork/general court check, minJurors zero check,
disputeKitID validity) after the conditional loads.

…origin

Autostake the token rewards in the court where the stake originates from
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (11)
contracts/src/arbitration/KlerosCore.sol (11)

398-425: Guard governance setters against zero address to prevent misconfiguration.

Owner/pinakion/sortition module must not be zero.

     function changeOwner(address payable _owner) external onlyByOwner {
-        owner = _owner;
+        if (_owner == address(0)) revert ZeroAddressOwner();
+        owner = _owner;
     }
@@
     function changePinakion(IERC20 _pinakion) external onlyByOwner {
-        pinakion = _pinakion;
+        if (address(_pinakion) == address(0)) revert ZeroAddress();
+        pinakion = _pinakion;
     }
@@
     function changeSortitionModule(ISortitionModule _sortitionModule) external onlyByOwner {
-        sortitionModule = _sortitionModule;
+        if (address(_sortitionModule) == address(0)) revert ZeroAddress();
+        sortitionModule = _sortitionModule;
     }

Optional: apply same guard to changeGuardian/changeJurorProsecutionModule/changeJurorNft if zero is not a valid sentinel.


5-5: Fix incorrect import of IArbitrableV2 (compilation blocker).

IArbitrableV2 is not declared in IArbitratorV2.sol. Import it from its own file.

-import {IArbitrableV2, IArbitratorV2} from "./interfaces/IArbitratorV2.sol";
+import {IArbitratorV2} from "./interfaces/IArbitratorV2.sol";
+import {IArbitrableV2} from "./interfaces/IArbitrableV2.sol";

97-99: Replace overflowing literal with type(uint256).max.

(2 ** 256 - 2) overflows in constant context in 0.8.x.

-    uint256 private constant NON_PAYABLE_AMOUNT = (2 ** 256 - 2) / 2; // An amount higher than the supply of ETH.
+    // An amount higher than the supply of ETH.
+    uint256 private constant NON_PAYABLE_AMOUNT = (type(uint256).max - 1) / 2;

369-372: _authorizeUpgrade override must not be view (signature mismatch).

UUPS parent is non-view; this override won’t compile.

-    function _authorizeUpgrade(address) internal view override onlyByOwner {
+    function _authorizeUpgrade(address) internal override onlyByOwner {

630-639: Align override to interface: use calldata for _extraData.

Interface expects calldata; memory mismatch won’t compile.

     function createDispute(
         uint256 _numberOfChoices,
-        bytes memory _extraData
+        bytes calldata _extraData
     ) external payable override returns (uint256 disputeID) {

674-682: Guard zero feeForJuror and enforce at least one juror.

Avoid div-by-zero and zero nbVotes.

         uint256 feeForJuror = (_feeToken == NATIVE_CURRENCY)
             ? court.feeForJuror
             : convertEthToTokenAmount(_feeToken, court.feeForJuror);
-        round.nbVotes = _feeAmount / feeForJuror;
+        if (feeForJuror == 0) revert ZeroFeeForJuror();
+        round.nbVotes = _feeAmount / feeForJuror;
+        if (round.nbVotes == 0) revert InsufficientFeesForOneJuror();

Add errors:

@@
     error TransferFailed();
     error WhenNotPausedOnly();
     error WhenPausedOnly();
     error StakingZeroWhenNoStake();
+    error ZeroFeeForJuror();
+    error InsufficientFeesForOneJuror();

828-891: execute() reentrancy window around external transfers; add guard.

External sends (ETH/ERC20) happen during iteration while round state is still mutable.

-    function execute(uint256 _disputeID, uint256 _round, uint256 _iterations) external whenNotPaused {
+    function execute(uint256 _disputeID, uint256 _round, uint256 _iterations) external whenNotPaused nonReentrant {

Also add the guard import and inheritance:

+import {ReentrancyGuardTransient} from "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol";
- contract KlerosCore is IArbitratorV2, Initializable, UUPSProxiable {
+ contract KlerosCore is IArbitratorV2, Initializable, UUPSProxiable, ReentrancyGuardTransient {

1067-1071: Align arbitrationCost override to calldata.

Match interface to compile.

-    function arbitrationCost(bytes memory _extraData) public view override returns (uint256 cost) {
+    function arbitrationCost(bytes calldata _extraData) public view override returns (uint256 cost) {

1206-1208: Guard zero currency rate to avoid division by zero.

Fail fast when rate not configured.

-    function convertEthToTokenAmount(IERC20 _toToken, uint256 _amountInEth) public view returns (uint256) {
-        return (_amountInEth * 10 ** currencyRates[_toToken].rateDecimals) / currencyRates[_toToken].rateInEth;
-    }
+    function convertEthToTokenAmount(IERC20 _toToken, uint256 _amountInEth) public view returns (uint256) {
+        CurrencyRate memory rate = currencyRates[_toToken];
+        if (rate.rateInEth == 0) revert TokenRateNotConfigured();
+        return (_amountInEth * 10 ** rate.rateDecimals) / rate.rateInEth;
+    }

Add error:

@@
     error StakingZeroWhenNoStake();
+    error TokenRateNotConfigured();

1370-1394: Fix out-of-bounds reads when parsing _extraData.

Currently reading 0x60 when len is 64–95 reads garbage.

-    if (_extraData.length >= 64) {
-        assembly {
-            courtID := mload(add(_extraData, 0x20))
-            minJurors := mload(add(_extraData, 0x40))
-            disputeKitID := mload(add(_extraData, 0x60))
-        }
+    if (_extraData.length >= 32) {
+        assembly { courtID := mload(add(_extraData, 0x20)) }
+    } else {
+        courtID = GENERAL_COURT;
+    }
+    if (_extraData.length >= 64) {
+        assembly { minJurors := mload(add(_extraData, 0x40)) }
+    } else {
+        minJurors = DEFAULT_NB_OF_JURORS;
+    }
+    if (_extraData.length >= 96) {
+        assembly { disputeKitID := mload(add(_extraData, 0x60)) }
     }
-    else {
-        courtID = GENERAL_COURT;
-        minJurors = DEFAULT_NB_OF_JURORS;
-        disputeKitID = DISPUTE_KIT_CLASSIC;
-    }
+    if (courtID == FORKING_COURT || courtID >= courts.length) courtID = GENERAL_COURT;
+    if (minJurors == 0) minJurors = DEFAULT_NB_OF_JURORS;
+    if (disputeKitID == NULL_DISPUTE_KIT || disputeKitID >= disputeKits.length) disputeKitID = DISPUTE_KIT_CLASSIC;

800-805: Appeal: guard zero feeForJuror.

Division by zero if court misconfigured.

         Court storage court = courts[newCourtID];
-        extraRound.nbVotes = msg.value / court.feeForJuror; // As many votes that can be afforded by the provided funds.
+        if (court.feeForJuror == 0) revert ZeroFeeForJuror();
+        extraRound.nbVotes = msg.value / court.feeForJuror; // As many votes that can be afforded by the provided funds.
🧹 Nitpick comments (1)
contracts/src/arbitration/KlerosCore.sol (1)

563-579: Consider enforcing rate presence when accepting tokens (optional).

To prevent accepted tokens with zero rate silently reverting later, validate rate on acceptance.

     function changeAcceptedFeeTokens(IERC20 _feeToken, bool _accepted) external onlyByOwner {
+        if (_accepted) {
+            CurrencyRate memory rate = currencyRates[_feeToken];
+            if (rate.rateInEth == 0) revert TokenRateNotConfigured();
+        }
         currencyRates[_feeToken].feePaymentAccepted = _accepted;
         emit AcceptedFeeToken(_feeToken, _accepted);
     }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7cb1275 and 4d14f47.

📒 Files selected for processing (1)
  • contracts/src/arbitration/KlerosCore.sol (4 hunks)
⏰ 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). (11)
  • 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
  • GitHub Check: SonarCloud
  • GitHub Check: SonarCloud

Comment on lines +315 to +323
owner = _owner;
guardian = _guardian;
pinakion = _pinakion;
jurorProsecutionModule = _jurorProsecutionModule;
sortitionModule = _sortitionModuleAddress;
wNative = _wNative;
jurorNft = _jurorNft;

// NULL_DISPUTE_KIT: an empty element at index 0 to indicate when a dispute kit is not supported.
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add zero-address validation in initialize for critical dependencies.

Avoid bricking ownership and external call sites (pinakion, sortition, wNative, DK).

     ) external initializer {
-        owner = _owner;
+        if (_owner == address(0)) revert ZeroAddressOwner();
+        if (address(_pinakion) == address(0)) revert ZeroAddress();
+        if (address(_disputeKit) == address(0)) revert ZeroAddress();
+        if (address(_sortitionModuleAddress) == address(0)) revert ZeroAddress();
+        if (_wNative == address(0)) revert ZeroAddress();
+        owner = _owner;
         guardian = _guardian;
         pinakion = _pinakion;
         jurorProsecutionModule = _jurorProsecutionModule;
         sortitionModule = _sortitionModuleAddress;
         wNative = _wNative;
         jurorNft = _jurorNft;

Add errors:

@@
     error OwnerOnly();
+    error ZeroAddressOwner();
+    error ZeroAddress();

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

🤖 Prompt for AI Agents
In contracts/src/arbitration/KlerosCore.sol around lines 315 to 323, the
initialize function sets critical dependency addresses without validating they
are non-zero; add require checks to prevent setting owner, guardian, pinakion,
jurorProsecutionModule, sortitionModule, wNative, jurorNft and the dispute kit
(NULL_DISPUTE_KIT handling) to the zero address. For each variable add a
require(address != address(0), "ZERO_<NAME>") with a clear revert string (e.g.,
ZERO_OWNER, ZERO_GUARDIAN, ZERO_PINAKION, ZERO_JUROR_PROSECUTION_MODULE,
ZERO_SORTITION_MODULE, ZERO_WNATIVE, ZERO_JUROR_NFT, ZERO_DISPUTE_KIT) before
assigning state to ensure initialization cannot be bricked by empty addresses.

Comment on lines +430 to +434
function addNewDisputeKit(IDisputeKit _disputeKitAddress) external onlyByOwner {
uint256 disputeKitID = disputeKits.length;
disputeKits.push(_disputeKitAddress);
emit DisputeKitCreated(disputeKitID, _disputeKitAddress);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validate dispute kit address when adding.

Prevent pushing address(0) DK.

     function addNewDisputeKit(IDisputeKit _disputeKitAddress) external onlyByOwner {
+        if (address(_disputeKitAddress) == address(0)) revert ZeroAddress();
         uint256 disputeKitID = disputeKits.length;
         disputeKits.push(_disputeKitAddress);
         emit DisputeKitCreated(disputeKitID, _disputeKitAddress);
     }
📝 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 addNewDisputeKit(IDisputeKit _disputeKitAddress) external onlyByOwner {
uint256 disputeKitID = disputeKits.length;
disputeKits.push(_disputeKitAddress);
emit DisputeKitCreated(disputeKitID, _disputeKitAddress);
}
function addNewDisputeKit(IDisputeKit _disputeKitAddress) external onlyByOwner {
if (address(_disputeKitAddress) == address(0)) revert ZeroAddress();
uint256 disputeKitID = disputeKits.length;
disputeKits.push(_disputeKitAddress);
emit DisputeKitCreated(disputeKitID, _disputeKitAddress);
}

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (3)
contracts/src/rng/ChainlinkRNG.sol (3)

66-80: Add ZeroAddress checks for owner/consumer in constructor.

As previously noted, the constructor lacks validation that _owner and _consumer are not the zero address, which could brick the contract or make RNG requests impossible.

Apply this diff to add the checks:

     constructor(
         address _owner,
         address _consumer,
         address _vrfCoordinator,
         bytes32 _keyHash,
         uint256 _subscriptionId,
         uint16 _requestConfirmations,
         uint32 _callbackGasLimit
     ) VRFConsumerBaseV2Plus(_owner, _vrfCoordinator) {
+        if (_owner == address(0) || _consumer == address(0)) revert ZeroAddress();
         consumer = _consumer;

92-96: Prevent setting consumer to zero (would make requests impossible).

As previously noted, setting consumer to the zero address would break RNG requests since only the consumer can call requestRandomness.

Apply this diff to add validation:

     function changeConsumer(address _consumer) external onlyByOwner {
+        if (_consumer == address(0)) revert ZeroAddress();
         consumer = _consumer;
     }

98-103: Fix: changeVrfCoordinator can brick RNG by setting zero address.

As previously noted, setting the VRF coordinator to the zero address would break RNG functionality.

Apply this diff to add validation:

     function changeVrfCoordinator(address _vrfCoordinator) external onlyByOwner {
+        if (_vrfCoordinator == address(0)) revert ZeroAddress();
         s_vrfCoordinator = IVRFCoordinatorV2Plus(_vrfCoordinator);
         emit CoordinatorSet(_vrfCoordinator);
     }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4d14f47 and e21d261.

📒 Files selected for processing (1)
  • contracts/src/rng/ChainlinkRNG.sol (5 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 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/src/rng/ChainlinkRNG.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). (6)
  • GitHub Check: hardhat-tests
  • GitHub Check: SonarCloud
  • GitHub Check: Analyze (javascript)
  • GitHub Check: SonarCloud
  • GitHub Check: hardhat-tests
  • GitHub Check: Analyze (javascript)
🔇 Additional comments (7)
contracts/src/rng/ChainlinkRNG.sol (7)

1-12: LGTM: Import and inheritance structure aligns with IRNG refactor.

The contract correctly implements the IRNG interface and inherits from the local VRFConsumerBaseV2Plus, consistent with the PR's goal to refactor RNG contracts.


17-24: LGTM: Consumer-based state variable.

The new consumer address variable appropriately replaces the old governor/sortitionModule pattern as part of the RNG refactor.


30-37: LGTM: Event parameter naming convention.

The underscore-prefixed parameter names (_requestId, _randomWord) follow common Solidity style conventions.


43-51: LGTM: Access control modifiers.

The onlyByOwner and onlyByConsumer modifiers correctly enforce access restrictions using custom errors.


105-127: LGTM: VRF configuration governance functions.

The remaining governance functions (changeKeyHash, changeSubscriptionId, changeRequestConfirmations, changeCallbackGasLimit) allow necessary VRF parameter updates. While additional validation could be added, invalid values would likely be rejected by the Chainlink VRF coordinator.


133-152: LGTM: requestRandomness aligns with IRNG interface.

The function correctly implements the IRNG interface with no arguments and consumer-only access control. The VRF request construction is appropriate. Based on learnings, this is called only by the SortitionModule without concurrent requests, so the single-request pattern is acceptable.


154-171: LGTM: Callback and view functions.

Both fulfillRandomWords (internal VRF callback) and receiveRandomness (IRNG view function) are correctly implemented. Access control is handled appropriately by the parent VRF coordinator.

Comment on lines +86 to 90
/// @notice Changes the owner of the contract.
/// @param _owner The new owner.
function changeOwner(address _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.

⚠️ Potential issue | 🔴 Critical

Critical: changeOwner can brick governance by setting owner to zero.

Setting owner to the zero address would permanently lock all onlyByOwner functions, bricking contract governance.

Apply this diff to add validation:

     function changeOwner(address _owner) external onlyByOwner {
+        if (_owner == address(0)) revert ZeroAddress();
         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
/// @notice Changes the owner of the contract.
/// @param _owner The new owner.
function changeOwner(address _owner) external onlyByOwner {
owner = _owner;
}
/// @notice Changes the owner of the contract.
/// @param _owner The new owner.
function changeOwner(address _owner) external onlyByOwner {
if (_owner == address(0)) revert ZeroAddress();
owner = _owner;
}
🤖 Prompt for AI Agents
In contracts/src/rng/ChainlinkRNG.sol around lines 86 to 90, the changeOwner
function allows setting owner to the zero address which would permanently lock
all onlyByOwner functionality; add a validation check that reverts if _owner ==
address(0) (e.g. require(_owner != address(0), "Owner cannot be zero address"))
before assigning owner, and optionally emit an ownership-changed event if the
contract uses one.

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 (1)
contracts/src/arbitration/KlerosGovernor.sol (1)

119-143: Set owner and templateRegistry in constructor; otherwise deploy or usage will fail.

  • templateRegistry is used but never initialized (constructor will revert).
  • Add constructor params for _templateRegistry and _owner, and assign both.
-    /// @param _wNative The wrapped native token address, typically wETH.
+    /// @param _wNative The wrapped native token address, typically wETH.
+    /// @param _templateRegistry The dispute template registry.
+    /// @param _owner The owner address.
     constructor(
         IArbitratorV2 _arbitrator,
         bytes memory _arbitratorExtraData,
         string memory _templateData,
         string memory _templateDataMappings,
         uint256 _submissionBaseDeposit,
         uint256 _submissionTimeout,
         uint256 _executionTimeout,
         uint256 _withdrawTimeout,
-        address _wNative
+        address _wNative,
+        IDisputeTemplateRegistry _templateRegistry,
+        address _owner
     ) {
         arbitrator = _arbitrator;
         arbitratorExtraData = _arbitratorExtraData;
         wNative = _wNative;
+        templateRegistry = _templateRegistry;
+        owner = _owner;
 
         lastApprovalTime = block.timestamp;
         submissionBaseDeposit = _submissionBaseDeposit;
         submissionTimeout = _submissionTimeout;
         executionTimeout = _executionTimeout;
         withdrawTimeout = _withdrawTimeout;
         sessions.push();
 
         templateId = templateRegistry.setDisputeTemplate("", _templateData, _templateDataMappings);
     }
♻️ Duplicate comments (17)
contracts/src/gateway/HomeGateway.sol (4)

61-63: Docstring still references PolicyRegistry instead of HomeGateway.

This issue was previously flagged but remains unfixed in the current code. The constructor docstring incorrectly references PolicyRegistry instead of HomeGateway.


146-152: Reentrancy window still present in native currency path.

The relayedData.relayer assignment occurs after the external call to arbitrator.createDispute, leaving a reentrancy window where a malicious arbitrator could re-enter and bypass the "already relayed" check. This issue was previously flagged and marked as addressed, but the vulnerable pattern remains in the current code.


167-167: Missing override keyword on ERC20 overload.

This overload of relayCreateDispute is missing the override specifier required to match the interface declaration in IHomeGateway. This issue was previously flagged but remains unfixed.


182-191: Reentrancy window still present in ERC20 path despite marked as addressed.

The relayedData.relayer assignment occurs after safeTransferFrom, increaseAllowance, and arbitrator.createDispute calls, leaving a reentrancy window. This issue was previously marked as "✅ Addressed in commits 59ca8e1 to 7c30463", but the vulnerable pattern remains in the current code version under review.

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

51-56: Missing owner storage; add owner state variable.

     IDisputeTemplateRegistry public templateRegistry; // The dispute template registry.
     uint256 public templateId; // The current dispute template identifier.
+    address public owner; // Contract owner for governance actions.

93-105: Add changeOwner event and function to rotate ownership.

     event ListSubmitted(
         uint256 indexed _listID,
         address indexed _submitter,
         uint256 indexed _session,
         string _description
     );
+
+    /// @notice Emitted when the owner changes.
+    event OwnerChanged(address indexed previousOwner, address indexed newOwner);
     function changeDisputeTemplate(
         string memory _templateData,
         string memory _templateDataMappings
     ) external onlyByOwner {
         templateId = templateRegistry.setDisputeTemplate("", _templateData, _templateDataMappings);
     }
+
+    /// @notice Changes the contract owner.
+    /// @param _owner New owner address.
+    function changeOwner(address _owner) external onlyByOwner {
+        if (_owner == address(0)) revert OwnerOnly();
+        emit OwnerChanged(owner, _owner);
+        owner = _owner;
+    }

Also applies to: 149-196


226-235: Bounds-check _data slicing to avoid OOB reads; fail fast with a typed error.

-        for (uint256 i = 0; i < _target.length; i++) {
-            bytes memory readData = new bytes(_dataSize[i]);
+        for (uint256 i = 0; i < _target.length; i++) {
+            if (readingPosition + _dataSize[i] > _data.length) revert WrongInputTargetAndDatasize();
+            bytes memory readData = new bytes(_dataSize[i]);
             Transaction storage transaction = submission.txs.push();
             transaction.target = _target[i];
             transaction.value = _value[i];
             for (uint256 j = 0; j < _dataSize[i]; j++) {
                 readData[j] = _data[readingPosition + j];
             }
             transaction.data = readData;
             readingPosition += _dataSize[i];
             currentTxHash = keccak256(abi.encodePacked(transaction.target, transaction.value, transaction.data));
             listHash = keccak256(abi.encodePacked(currentTxHash, listHash));
         }
+        if (readingPosition != _data.length) revert WrongInputTargetAndDatasize();

272-273: Avoid transfer; use SafeSend consistently.

-        payable(msg.sender).transfer(submission.deposit);
+        payable(msg.sender).safeSend(submission.deposit, wNative);

84-86: Critical: onlyByOwner blocks all governance; introduce owner storage and compare against it.

The modifier compares msg.sender to address(this). Add an owner variable and check msg.sender == owner.

-    modifier onlyByOwner() {
-        if (address(this) != msg.sender) revert OwnerOnly();
+    modifier onlyByOwner() {
+        if (owner != msg.sender) revert OwnerOnly();
         _;
     }

351-356: Close reentrancy window: set executed before external call (CEI).

-            if (!transaction.executed && transaction.value <= expendableFunds) {
-                (bool callResult, ) = transaction.target.call{value: transaction.value}(transaction.data);
-                // An extra check to prevent re-entrancy through target call.
-                if (callResult == true) {
-                    if (transaction.executed) revert AlreadyExecuted();
-                    transaction.executed = true;
-                }
-            }
+            if (!transaction.executed && transaction.value <= expendableFunds) {
+                // Effects
+                transaction.executed = true;
+                // Interaction
+                (bool callResult, ) = transaction.target.call{value: transaction.value}(transaction.data);
+                if (!callResult) {
+                    // Restore effect to preserve current non-reverting behavior on target failure.
+                    transaction.executed = false;
+                }
+            }
contracts/src/arbitration/evidence/EvidenceModule.sol (1)

38-42: Guard against zero owner during initialize().

-    function initialize(address _owner) external initializer {
-        owner = _owner;
+    function initialize(address _owner) external initializer {
+        if (_owner == address(0)) revert ZeroAddress();
+        owner = _owner;
     }
     error OwnerOnly();
+    error ZeroAddress();

Also applies to: 71-72

contracts/src/arbitration/arbitrables/DisputeResolver.sol (3)

100-111: Critical: Validate arbitrator dispute ID mapping before access.

The mapping lookup at Line 101 does not validate that _arbitratorDisputeID exists. Unknown IDs will default to 0, incorrectly targeting disputes[0].

Apply this diff to add validation using a sentinel pattern:

+    error UnknownDisputeID();
+
     function rule(uint256 _arbitratorDisputeID, uint256 _ruling) external override {
-        uint256 localDisputeID = arbitratorDisputeIDToLocalID[_arbitratorDisputeID];
+        uint256 localDisputeIDPlusOne = arbitratorDisputeIDToLocalID[_arbitratorDisputeID];
+        if (localDisputeIDPlusOne == 0) revert UnknownDisputeID();
+        uint256 localDisputeID = localDisputeIDPlusOne - 1;
         DisputeStruct storage dispute = disputes[localDisputeID];

And update the mapping write at Line 135:

-        arbitratorDisputeIDToLocalID[arbitratorDisputeID] = localDisputeID;
+        arbitratorDisputeIDToLocalID[arbitratorDisputeID] = localDisputeID + 1;

59-63: Add event emission for ownership changes.

Critical governance state changes should emit events for off-chain monitoring and transparency.

Apply this diff:

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

65-71: Add event emissions for critical configuration changes.

Both changeArbitrator and changeTemplateRegistry modify critical contract parameters but don't emit events, impacting off-chain monitoring and auditability.

Apply this diff:

+    event ArbitratorChanged(IArbitratorV2 indexed previousArbitrator, IArbitratorV2 indexed newArbitrator);
+    event TemplateRegistryChanged(IDisputeTemplateRegistry indexed previousRegistry, IDisputeTemplateRegistry indexed newRegistry);
+
     function changeArbitrator(IArbitratorV2 _arbitrator) external onlyByOwner {
+        emit ArbitratorChanged(arbitrator, _arbitrator);
         arbitrator = _arbitrator;
     }

     function changeTemplateRegistry(IDisputeTemplateRegistry _templateRegistry) external onlyByOwner {
+        emit TemplateRegistryChanged(templateRegistry, _templateRegistry);
         templateRegistry = _templateRegistry;
     }
contracts/src/arbitration/evidence/ModeratedEvidenceModule.sol (2)

143-158: Add validation bounds for governance setters.

The governance setters lack bounds checking. Zero or nonsensical values can cause arithmetic underflows or logic failures downstream (e.g., Line 260's feeRewards - arbitrationCost or zero timeout).

Apply this diff to add guardrails:

     function changeInitialDepositMultiplier(uint256 _initialDepositMultiplier) external onlyOwner {
+        require(_initialDepositMultiplier <= MULTIPLIER_DIVISOR, "Initial deposit > 100%");
         initialDepositMultiplier = _initialDepositMultiplier;
     }

     function changeTotalCostMultiplier(uint256 _totalCostMultiplier) external onlyOwner {
+        require(_totalCostMultiplier >= MULTIPLIER_DIVISOR, "Total cost must cover arbitration");
         totalCostMultiplier = _totalCostMultiplier;
     }

     function changeBondTimeout(uint256 _bondTimeout) external onlyOwner {
+        require(_bondTimeout > 0, "Timeout cannot be zero");
         bondTimeout = _bondTimeout;
     }

375-375: Replace send with call to prevent fund loss.

Line 375 uses send() which can fail silently (e.g., gas limit), permanently stranding user funds since contributions are zeroed before the transfer.

Apply this diff:

-        _beneficiary.send(reward); // It is the user responsibility to accept ETH.
+        (bool success, ) = _beneficiary.call{value: reward}("");
+        require(success, "ETH transfer failed");

State updates occur before the external call, so on failure the revert restores state, allowing retry.

contracts/src/arbitration/arbitrables/ArbitrableExample.sol (1)

150-161: Critical: Validate arbitrator dispute ID mapping before access.

Line 151 does not validate that _arbitratorDisputeID exists in the mapping. Unknown IDs will default to 0, incorrectly targeting disputes[0].

Apply this diff to add validation using a sentinel pattern:

+    error UnknownDisputeID();
+
     function rule(uint256 _arbitratorDisputeID, uint256 _ruling) external override {
-        uint256 localDisputeID = externalIDtoLocalID[_arbitratorDisputeID];
+        uint256 localDisputeIDPlusOne = externalIDtoLocalID[_arbitratorDisputeID];
+        if (localDisputeIDPlusOne == 0) revert UnknownDisputeID();
+        uint256 localDisputeID = localDisputeIDPlusOne - 1;
         DisputeStruct storage dispute = disputes[localDisputeID];

And update mapping writes at Lines 124 and 144:

-        externalIDtoLocalID[disputeID] = localDisputeID;
+        externalIDtoLocalID[disputeID] = localDisputeID + 1;
🧹 Nitpick comments (1)
contracts/src/arbitration/KlerosGovernor.sol (1)

367-368: Clamp expendable funds to avoid underflow on view.

-        return address(this).balance - reservedETH;
+        uint256 bal = address(this).balance;
+        return bal > reservedETH ? bal - reservedETH : 0;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e21d261 and f70050e.

📒 Files selected for processing (10)
  • contracts/src/arbitration/KlerosGovernor.sol (11 hunks)
  • contracts/src/arbitration/arbitrables/ArbitrableExample.sol (5 hunks)
  • contracts/src/arbitration/arbitrables/DisputeResolver.sol (6 hunks)
  • contracts/src/arbitration/devtools/DisputeResolverRuler.sol (3 hunks)
  • contracts/src/arbitration/evidence/EvidenceModule.sol (3 hunks)
  • contracts/src/arbitration/evidence/ModeratedEvidenceModule.sol (21 hunks)
  • contracts/src/arbitration/interfaces/IArbitrableV2.sol (1 hunks)
  • contracts/src/arbitration/interfaces/IEvidence.sol (1 hunks)
  • contracts/src/gateway/HomeGateway.sol (9 hunks)
  • contracts/src/gateway/interfaces/IHomeGateway.sol (1 hunks)
⏰ 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: hardhat-tests
  • GitHub Check: Analyze (javascript)
  • GitHub Check: SonarCloud
  • GitHub Check: SonarCloud
  • GitHub Check: hardhat-tests
  • GitHub Check: Analyze (javascript)
🔇 Additional comments (12)
contracts/src/gateway/interfaces/IHomeGateway.sol (2)

69-84: LGTM!

The new view functions provide useful accessors for dispute routing (disputeHashToHomeID, foreignChainID, foreignGateway) and fee management (feeToken). The functions are well-documented and follow consistent naming conventions.


3-3: Pragma version constraint consistent with other interfaces
All interface files under contracts/src (e.g., IForeignGateway.sol, IRNG.sol) already use >=0.8.0 <0.9.0, so this change aligns with the established pattern.

contracts/src/gateway/HomeGateway.sol (2)

185-186: SafeERC20 return-value checks are valid
In this codebase, safeTransferFrom(...) and increaseAllowance(...) both return bool, so the existing if (!… ) revert checks are correct. No changes required.

Likely an incorrect or invalid review comment.


75-75: Ensure upgrade-safe initialization
Changing reinitializer(1) to initializer will lock out any reinitialization on already-deployed proxies. If HomeGateway is already live, this breaks its upgrade path. Either revert to reinitializer(2) for the new step or confirm there are no existing HomeGateway deployments before merging.

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

7-12: Looks good — interface and docs aligned with broader changes.

contracts/src/arbitration/devtools/DisputeResolverRuler.sol (1)

19-26: LGTM — constructor ownership + DisputeRequest emission match new interface.

Also applies to: 48-50

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

17-22: Event surface simplification is consistent; no issues found.

contracts/src/arbitration/evidence/ModeratedEvidenceModule.sol (3)

205-206: LGTM: Arithmetic operations are safe in Solidity 0.8.24.

The removal of CappedMath is safe as Solidity 0.8.24 has built-in overflow/underflow checks that will revert on arithmetic errors.


245-250: LGTM: Arithmetic operations are safe in Solidity 0.8.24.

The direct arithmetic operations (including Line 248's * 2 multiplication) are protected by Solidity 0.8.24's built-in overflow checks.


311-313: LGTM: Simplified logic maintains correctness.

The replacement of CappedMath.subCap with a conditional computing the remaining required amount is correct and leverages Solidity 0.8.24's built-in underflow protection.

contracts/src/arbitration/arbitrables/ArbitrableExample.sol (2)

34-34: Consider adding a changeOwner function.

The contract declares owner as immutable, preventing ownership transfer. While this may be intentional, it differs from DisputeResolver and ModeratedEvidenceModule which support ownership changes.

Verify whether immutable ownership is intentional for this example contract. If ownership transfer is needed, apply this diff:

-    address public immutable owner;
+    address public owner;

Then add a governance function:

    function changeOwner(address _owner) external onlyByOwner {
        emit OwnerChanged(owner, _owner);
        owner = _owner;
    }

And declare the event:

    event OwnerChanged(address indexed previousOwner, address indexed newOwner);

140-141: LGTM: Custom errors improve WETH transfer error handling.

The custom errors TransferFailed and AllowanceIncreaseFailed provide clear, gas-efficient revert reasons for WETH operations.

Comment on lines +86 to +107
function changeArbitrator(IArbitratorV2 _arbitrator) external onlyByOwner {
arbitrator = _arbitrator;
}

function changeArbitratorExtraData(bytes calldata _arbitratorExtraData) external onlyByGovernor {
function changeArbitratorExtraData(bytes calldata _arbitratorExtraData) external onlyByOwner {
arbitratorExtraData = _arbitratorExtraData;
}

function changeTemplateRegistry(IDisputeTemplateRegistry _templateRegistry) external onlyByGovernor {
function changeTemplateRegistry(IDisputeTemplateRegistry _templateRegistry) external onlyByOwner {
templateRegistry = _templateRegistry;
}

function changeDisputeTemplate(
string memory _templateData,
string memory _templateDataMappings
) external onlyByGovernor {
) external onlyByOwner {
templateId = templateRegistry.setDisputeTemplate("", _templateData, _templateDataMappings);
}

function changeNumberOfRulingOptions(uint256 _numberOfRulingOptions) external onlyByOwner {
numberOfRulingOptions = _numberOfRulingOptions;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add event emissions for governance changes.

The governance functions lack event emissions, impacting off-chain monitoring and auditability.

Apply this diff to add events:

+    event ArbitratorChanged(IArbitratorV2 indexed previousArbitrator, IArbitratorV2 indexed newArbitrator);
+    event ArbitratorExtraDataChanged(bytes previousExtraData, bytes newExtraData);
+    event TemplateRegistryChanged(IDisputeTemplateRegistry indexed previousRegistry, IDisputeTemplateRegistry indexed newRegistry);
+    event DisputeTemplateChanged(uint256 indexed previousTemplateId, uint256 indexed newTemplateId);
+    event NumberOfRulingOptionsChanged(uint256 previousNumber, uint256 newNumber);
+
     function changeArbitrator(IArbitratorV2 _arbitrator) external onlyByOwner {
+        emit ArbitratorChanged(arbitrator, _arbitrator);
         arbitrator = _arbitrator;
     }

     function changeArbitratorExtraData(bytes calldata _arbitratorExtraData) external onlyByOwner {
+        emit ArbitratorExtraDataChanged(arbitratorExtraData, _arbitratorExtraData);
         arbitratorExtraData = _arbitratorExtraData;
     }

     function changeTemplateRegistry(IDisputeTemplateRegistry _templateRegistry) external onlyByOwner {
+        emit TemplateRegistryChanged(templateRegistry, _templateRegistry);
         templateRegistry = _templateRegistry;
     }

     function changeDisputeTemplate(
         string memory _templateData,
         string memory _templateDataMappings
     ) external onlyByOwner {
+        uint256 previousTemplateId = templateId;
         templateId = templateRegistry.setDisputeTemplate("", _templateData, _templateDataMappings);
+        emit DisputeTemplateChanged(previousTemplateId, templateId);
     }

     function changeNumberOfRulingOptions(uint256 _numberOfRulingOptions) external onlyByOwner {
+        emit NumberOfRulingOptionsChanged(numberOfRulingOptions, _numberOfRulingOptions);
         numberOfRulingOptions = _numberOfRulingOptions;
     }
📝 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 changeArbitrator(IArbitratorV2 _arbitrator) external onlyByOwner {
arbitrator = _arbitrator;
}
function changeArbitratorExtraData(bytes calldata _arbitratorExtraData) external onlyByGovernor {
function changeArbitratorExtraData(bytes calldata _arbitratorExtraData) external onlyByOwner {
arbitratorExtraData = _arbitratorExtraData;
}
function changeTemplateRegistry(IDisputeTemplateRegistry _templateRegistry) external onlyByGovernor {
function changeTemplateRegistry(IDisputeTemplateRegistry _templateRegistry) external onlyByOwner {
templateRegistry = _templateRegistry;
}
function changeDisputeTemplate(
string memory _templateData,
string memory _templateDataMappings
) external onlyByGovernor {
) external onlyByOwner {
templateId = templateRegistry.setDisputeTemplate("", _templateData, _templateDataMappings);
}
function changeNumberOfRulingOptions(uint256 _numberOfRulingOptions) external onlyByOwner {
numberOfRulingOptions = _numberOfRulingOptions;
}
// ───────────────────────────────────────────────────────────
// Events for governance changes
// ───────────────────────────────────────────────────────────
event ArbitratorChanged(IArbitratorV2 indexed previousArbitrator, IArbitratorV2 indexed newArbitrator);
event ArbitratorExtraDataChanged(bytes previousExtraData, bytes newExtraData);
event TemplateRegistryChanged(IDisputeTemplateRegistry indexed previousRegistry, IDisputeTemplateRegistry indexed newRegistry);
event DisputeTemplateChanged(uint256 indexed previousTemplateId, uint256 indexed newTemplateId);
event NumberOfRulingOptionsChanged(uint256 previousNumber, uint256 newNumber);
function changeArbitrator(IArbitratorV2 _arbitrator) external onlyByOwner {
emit ArbitratorChanged(arbitrator, _arbitrator);
arbitrator = _arbitrator;
}
function changeArbitratorExtraData(bytes calldata _arbitratorExtraData) external onlyByOwner {
emit ArbitratorExtraDataChanged(arbitratorExtraData, _arbitratorExtraData);
arbitratorExtraData = _arbitratorExtraData;
}
function changeTemplateRegistry(IDisputeTemplateRegistry _templateRegistry) external onlyByOwner {
emit TemplateRegistryChanged(templateRegistry, _templateRegistry);
templateRegistry = _templateRegistry;
}
function changeDisputeTemplate(
string memory _templateData,
string memory _templateDataMappings
) external onlyByOwner {
uint256 previousTemplateId = templateId;
templateId = templateRegistry.setDisputeTemplate("", _templateData, _templateDataMappings);
emit DisputeTemplateChanged(previousTemplateId, templateId);
}
function changeNumberOfRulingOptions(uint256 _numberOfRulingOptions) external onlyByOwner {
emit NumberOfRulingOptionsChanged(numberOfRulingOptions, _numberOfRulingOptions);
numberOfRulingOptions = _numberOfRulingOptions;
}

Comment on lines +135 to 139
/// @notice Change the owner of the contract.
/// @param _owner The address of the new owner.
function changeOwner(address _owner) external onlyOwner {
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.

⚠️ Potential issue | 🟠 Major

Add event emission for ownership changes.

The changeOwner function modifies critical governance state but does not emit an event, impacting off-chain monitoring and transparency.

Apply this diff:

+    event OwnerChanged(address indexed previousOwner, address indexed newOwner);
+
     function changeOwner(address _owner) external onlyOwner {
+        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
/// @notice Change the owner of the contract.
/// @param _owner The address of the new owner.
function changeOwner(address _owner) external onlyOwner {
owner = _owner;
}
event OwnerChanged(address indexed previousOwner, address indexed newOwner);
/// @notice Change the owner of the contract.
/// @param _owner The address of the new owner.
function changeOwner(address _owner) external onlyOwner {
emit OwnerChanged(owner, _owner);
owner = _owner;
}
🤖 Prompt for AI Agents
In contracts/src/arbitration/evidence/ModeratedEvidenceModule.sol around lines
135-139, the changeOwner function updates owner without emitting an event; add a
dedicated event declaration (e.g. event OwnerChanged(address indexed
previousOwner, address indexed newOwner);) near the top of the contract and
modify changeOwner to emit that event with the old owner and _owner before
assigning owner = _owner, preserving onlyOwner security. Ensure event names and
indexing follow project conventions.

Comment on lines 21 to 28
event CrossChainDisputeIncoming(
IArbitratorV2 _arbitrator,
uint256 _arbitrableChainId,
address indexed _arbitrable,
uint256 indexed _arbitrableDisputeID,
uint256 indexed _arbitratorDisputeID,
uint256 _externalDisputeID,
uint256 _templateId,
string _templateUri
uint256 _templateId
);
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

🧩 Analysis chain

Verify all event listeners have been updated.

The CrossChainDisputeIncoming event signature has been modified, removing the _externalDisputeID and _templateUri parameters. This is a breaking change that will affect any off-chain indexers, front-end listeners, or other contracts that consume this event.

Ensure all event listeners, including subgraphs, front-end code, and any dependent contracts, have been updated to match the new signature.

Run the following script to identify potential event listeners in the codebase:


🏁 Script executed:

#!/bin/bash
# Search for references to CrossChainDisputeIncoming event

rg -n 'CrossChainDisputeIncoming' --type=ts --type=js --type=sol -C 3

Length of output: 27


🏁 Script executed:

#!/bin/bash
# Search for CrossChainDisputeIncoming references across all files
rg -n 'CrossChainDisputeIncoming' -C 3

Length of output: 5922


Update CrossChainDisputeIncoming signature in listeners and ABI

  • In subgraph/core/src/entities/Dispute.ts, remove _externalDisputeId and _templateUri from the CrossChainDisputeIncoming definition and its Typestring.
  • Regenerate or update the CrossChainDisputeIncoming ABI entries in contracts/deployments/*viem.ts (mainnet, testnet, devnet) to match the new six-parameter signature.
🤖 Prompt for AI Agents
In contracts/src/gateway/interfaces/IHomeGateway.sol around lines 21–28, the
CrossChainDisputeIncoming event signature was changed to six parameters
(IArbitratorV2, uint256 _arbitrableChainId, address indexed _arbitrable, uint256
indexed _arbitrableDisputeID, uint256 indexed _arbitratorDisputeID, uint256
_templateId); update subgraph/core/src/entities/Dispute.ts to remove
`_externalDisputeId` and `_templateUri` from the CrossChainDisputeIncoming
definition and its Typestring, and then regenerate or manually update the
CrossChainDisputeIncoming ABI entries in contracts/deployments/*viem.ts
(mainnet, testnet, devnet) so the event ABI matches the new six-parameter
signature (ensure parameter order, names, types, and indexed flags match
exactly).

Comment on lines 35 to 43
struct RelayCreateDisputeParams {
bytes32 foreignBlockHash;
uint256 foreignChainID;
address foreignArbitrable;
uint256 foreignDisputeID;
uint256 externalDisputeID;
uint256 templateId;
string templateUri;
uint256 choices;
bytes extraData;
}
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

🧩 Analysis chain

Verify all usages of RelayCreateDisputeParams have been updated.

The RelayCreateDisputeParams struct has been modified with new fields (foreignChainID, foreignArbitrable) and removed fields (per the AI summary). This is a breaking change affecting any code that constructs or destructures this struct.

Ensure all contracts, tests, and scripts that use this struct have been updated to match the new structure.

Run the following script to locate all usages of RelayCreateDisputeParams:


🏁 Script executed:

#!/bin/bash
# Find all references to RelayCreateDisputeParams in the codebase

rg -n 'RelayCreateDisputeParams' --type=sol --type=ts --type=js -C 5

Length of output: 27


🏁 Script executed:

#!/bin/bash
# Search for all references to RelayCreateDisputeParams in .sol, .ts, and .js files
rg -n 'RelayCreateDisputeParams' -g '*.sol' -g '*.ts' -g '*.js' -C5

Length of output: 9772


🏁 Script executed:

#!/bin/bash
# List component names for RelayCreateDisputeParams in each .viem.ts file
for file in contracts/deployments/*.viem.ts; do
  echo "File: $file"
  awk '
    /internalType: "struct IHomeGateway.RelayCreateDisputeParams"/ {inStruct=1}
    inStruct && /components: \[/ {inComponents=1}
    inComponents && /name:/ {
      match($0, /name: "([^"]+)"/, arr)
      print "  - " arr[1]
    }
    inComponents && /\],/ {inComponents=0; inStruct=0}
  ' "$file"
done

Length of output: 1070


Update deployment artifacts for RelayCreateDisputeParams

contracts/deployments/*.viem.ts still include the removed fields externalDisputeID (and in mainnet/testnet also templateUri) in the RelayCreateDisputeParams tuple. Regenerate these deployment files from the updated ABI so their components match the new struct.

🤖 Prompt for AI Agents
In contracts/src/gateway/interfaces/IHomeGateway.sol around lines 35 to 43, the
RelayCreateDisputeParams struct was changed (removed fields externalDisputeID
and templateUri) but the generated deployment artifacts in
contracts/deployments/*.viem.ts still include those removed tuple components;
regenerate the deployment files from the updated ABI (or re-run the ABI-to-viem
generation script) so the RelayCreateDisputeParams tuple in each
contracts/deployments/*.viem.ts matches the new struct order and fields (bytes32
foreignBlockHash, uint256 foreignChainID, address foreignArbitrable, uint256
foreignDisputeID, uint256 templateId, uint256 choices, bytes extraData),
removing any references to externalDisputeID and templateUri and updating types
and tuple indices accordingly.

Comment on lines +57 to 63
/// @notice Relays a dispute creation from the ForeignGateway to the home arbitrator using the same parameters as the ones on the foreign chain.
///
/// @dev Providing incorrect parameters will create a different hash than on the foreignChain and will not affect the actual dispute/arbitrable's ruling.
/// This function accepts the fees payment in the ERC20 `acceptedFeeToken()`.
///
/// @param _params The parameters of the dispute, see `RelayCreateDisputeParams`.
function relayCreateDispute(RelayCreateDisputeParams memory _params, uint256 _feeAmount) external;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix documentation reference to match actual function name.

The documentation at line 60 refers to acceptedFeeToken(), but the actual function defined in this interface is feeToken() (line 84). Update the documentation to reference the correct function name for consistency.

Apply this diff to fix the documentation:

-    /// This function accepts the fees payment in the ERC20 `acceptedFeeToken()`.
+    /// This function accepts the fees payment in the ERC20 `feeToken()`.
📝 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
/// @notice Relays a dispute creation from the ForeignGateway to the home arbitrator using the same parameters as the ones on the foreign chain.
///
/// @dev Providing incorrect parameters will create a different hash than on the foreignChain and will not affect the actual dispute/arbitrable's ruling.
/// This function accepts the fees payment in the ERC20 `acceptedFeeToken()`.
///
/// @param _params The parameters of the dispute, see `RelayCreateDisputeParams`.
function relayCreateDispute(RelayCreateDisputeParams memory _params, uint256 _feeAmount) external;
/// @notice Relays a dispute creation from the ForeignGateway to the home arbitrator using the same parameters as the ones on the foreign chain.
///
/// @dev Providing incorrect parameters will create a different hash than on the foreignChain and will not affect the actual dispute/arbitrable's ruling.
/// This function accepts the fees payment in the ERC20 `feeToken()`.
///
/// @param _params The parameters of the dispute, see `RelayCreateDisputeParams`.
function relayCreateDispute(RelayCreateDisputeParams memory _params, uint256 _feeAmount) external;
🤖 Prompt for AI Agents
In contracts/src/gateway/interfaces/IHomeGateway.sol around lines 57 to 63, the
NatSpec comment incorrectly references acceptedFeeToken() whereas the actual
interface exposes feeToken(); update the documentation line to reference
feeToken() so the comment matches the real function name, keeping the rest of
the doc 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: 1

♻️ Duplicate comments (2)
contracts/scripts/generateMetrics.sh (2)

3-5: Anchor paths to the script directory and enable strict mode.

SOURCE_DIR="src" only works when the caller’s CWD is contracts/; run this script from elsewhere (e.g., repo root) and every glob fails. Use SCRIPT_DIR to make paths robust and enable strict mode so failures surface immediately.

-#!/usr/bin/env bash
-
-SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
-
-SOURCE_DIR="src"
+#!/usr/bin/env bash
+set -Eeuo pipefail
+shopt -s failglob
+
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
+SOURCE_DIR="${SCRIPT_DIR}/../src"

7-24: Write metrics atomically to avoid clobbering METRICS.html on failure.

The direct >METRICS.html truncates the report before yarn dlx finishes; any failure leaves an empty file. Buffer into a temp file and move it into place only on success.

-yarn dlx solidity-code-metrics \
+TMP_OUT="$(mktemp)"
+trap 'rm -f "$TMP_OUT"' EXIT
+
+yarn dlx solidity-code-metrics \
     "$SOURCE_DIR"/arbitration/KlerosCore* \
     "$SOURCE_DIR"/arbitration/PolicyRegistry.sol \
     "$SOURCE_DIR"/arbitration/SortitionModule* \
     "$SOURCE_DIR"/arbitration/arbitrables/DisputeResolver.sol \
     "$SOURCE_DIR"/arbitration/DisputeTemplateRegistry.sol \
     "$SOURCE_DIR"/arbitration/dispute-kits/* \
     "$SOURCE_DIR"/arbitration/evidence/EvidenceModule.sol \
     "$SOURCE_DIR"/arbitration/interfaces/* \
     "$SOURCE_DIR"/libraries/Constants.sol \
     "$SOURCE_DIR"/libraries/SortitionTrees.sol \
     "$SOURCE_DIR"/libraries/Safe* \
     "$SOURCE_DIR"/rng/RNGWithFallback.sol \
     "$SOURCE_DIR"/rng/ChainlinkRNG.sol \
     "$SOURCE_DIR"/rng/IRNG.sol \
     "$SOURCE_DIR"/proxy/UUPSProx* \
     "$SOURCE_DIR"/proxy/Initializable.sol \
---html >METRICS.html
+--html >"$TMP_OUT"
+
+mv "$TMP_OUT" "$OUTPUT_FILE"
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f70050e and decd3e7.

📒 Files selected for processing (1)
  • contracts/scripts/generateMetrics.sh (1 hunks)
🧰 Additional context used
🪛 Shellcheck (0.11.0)
contracts/scripts/generateMetrics.sh

[warning] 3-3: SCRIPT_DIR appears unused. Verify use (or export if used externally).

(SC2034)

⏰ 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: hardhat-tests
  • GitHub Check: SonarCloud
  • GitHub Check: Analyze (javascript)
  • GitHub Check: hardhat-tests
  • GitHub Check: SonarCloud


SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"

SOURCE_DIR="src"
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Define a default output path before strict mode makes it mandatory.

Once set -u is on, referencing OUTPUT_FILE without defining it will abort the script. Provide a sensible default while still allowing overrides.

-SOURCE_DIR="${SCRIPT_DIR}/../src"
+SOURCE_DIR="${SCRIPT_DIR}/../src"
+OUTPUT_FILE="${OUTPUT_FILE:-${SCRIPT_DIR}/../METRICS.html}"

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

🤖 Prompt for AI Agents
In contracts/scripts/generateMetrics.sh around line 5, the script references
OUTPUT_FILE after set -u which will fail if OUTPUT_FILE is unset; add a default
assignment before enabling strict unset checks, e.g. set OUTPUT_FILE to a
sensible default using parameter expansion
(OUTPUT_FILE="${OUTPUT_FILE:-dist/metrics.json}") so callers can still override
via environment or args, and ensure this line is placed before the set -u line.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (2)
contracts/src/arbitration/dispute-kits/DisputeKitGatedShutter.sol (1)

44-44: Duplicate: Transient flag usage across external calls remains problematic.

The past review comment on this line correctly identifies that callerIsJuror is set before making calls that may interact with external contracts (via _castVotecore.disputes/core.courts). The current implementation in castVoteShutter (lines 153-158) still follows this problematic pattern without the suggested save/restore or explicit context passing.

Refer to the detailed refactor suggestion in past review comments.

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

85-99: Fix NatSpec: "reward" should be "penalty" at line 92.

The function calculates penalties, but line 92's @return still mentions "reward". While a past comment indicated this was addressed, the current code still has the inconsistency.

Apply this diff:

-    /// @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.
🧹 Nitpick comments (3)
contracts/src/arbitration/dispute-kits/DisputeKitGatedShutter.sol (1)

80-87: Validate non-zero owner address.

The initializer accepts _owner but doesn't verify it's non-zero. This could lock the contract with an unusable owner.

Apply this diff:

 function initialize(
     address _owner,
     KlerosCore _core,
     address _wNative,
     uint256 _jumpDisputeKitID
 ) external initializer {
+    if (_owner == address(0)) revert ZeroOwner();
     __DisputeKitClassicBase_initialize(_owner, _core, _wNative, _jumpDisputeKitID);
 }

Add the error declaration:

+    error ZeroOwner();
     error EmptyRecoveryCommit();
contracts/src/arbitration/dispute-kits/DisputeKitShutter.sol (1)

64-71: Validate non-zero owner address.

Like DisputeKitGatedShutter, this initializer should validate _owner != address(0) to prevent locking the contract.

Apply this diff:

 function initialize(
     address _owner,
     KlerosCore _core,
     address _wNative,
     uint256 _jumpDisputeKitID
 ) external initializer {
+    if (_owner == address(0)) revert ZeroOwner();
     __DisputeKitClassicBase_initialize(_owner, _core, _wNative, _jumpDisputeKitID);
 }

Add the error declaration:

+    error ZeroOwner();
     error EmptyRecoveryCommit();
contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol (1)

153-163: Validate non-zero owner address in base initializer.

The base initializer should validate _owner != address(0) to prevent derived contracts from being locked with an unusable owner.

Apply this diff:

 function __DisputeKitClassicBase_initialize(
     address _owner,
     KlerosCore _core,
     address _wNative,
     uint256 _jumpDisputeKitID
 ) internal onlyInitializing {
+    if (_owner == address(0)) revert ZeroOwner();
     owner = _owner;
     core = _core;
     wNative = _wNative;
     jumpDisputeKitID = _jumpDisputeKitID;
 }

Add to the errors section:

+    error ZeroOwner();
     error OwnerOnly();
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between decd3e7 and a934ba5.

📒 Files selected for processing (4)
  • contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol (21 hunks)
  • contracts/src/arbitration/dispute-kits/DisputeKitGatedShutter.sol (6 hunks)
  • contracts/src/arbitration/dispute-kits/DisputeKitShutter.sol (3 hunks)
  • contracts/src/arbitration/interfaces/IDisputeKit.sol (3 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-30T17:18:12.895Z
Learnt from: jaybuidl
PR: kleros/kleros-v2#2145
File: contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol:277-286
Timestamp: 2025-09-30T17:18:12.895Z
Learning: In DisputeKitClassicBase.sol's castCommit function, jurors are allowed to re-submit commits during the commit period. The implementation uses a commitCount variable to track only first-time commits (where commit == bytes32(0)) so that totalCommitted is not incremented when a juror updates their existing commit.

Applied to files:

  • contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.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). (6)
  • GitHub Check: hardhat-tests
  • GitHub Check: SonarCloud
  • GitHub Check: Analyze (javascript)
  • GitHub Check: hardhat-tests
  • GitHub Check: Analyze (javascript)
  • GitHub Check: SonarCloud
🔇 Additional comments (19)
contracts/src/arbitration/dispute-kits/DisputeKitGatedShutter.sol (4)

114-134: LGTM!

The recovery commitment storage and validation logic is correct. The empty recovery commit check at line 122 properly prevents invalid data, and the storage loop at lines 127-129 correctly associates recovery commitments with each vote.


170-183: Hashing logic is correct given the transient pattern.

The conditional hashing (lines 175-182) correctly differentiates between juror and non-juror contexts: jurors hash without justification for recovery, while non-jurors include it. This logic is sound, though it depends on the fragile callerIsJuror pattern already flagged.


190-200: LGTM!

The recovery path selection logic correctly uses recoveryCommitments for jurors and the stored commit for non-jurors, enabling the recovery mechanism.


212-227: LGTM!

The assembly logic for unpacking token gate parameters is correct. The length check at line 216 prevents out-of-bounds reads, and the bit manipulation properly extracts the address and boolean flag.

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

33-45: LGTM!

The documentation clarifies that KlerosCore.Round must be created before calling this function, which is helpful for implementers.


124-141: LGTM!

The new getter functions (earlyCourtJump, getNbVotesAfterAppeal, getJumpDisputeKitID) provide useful introspection capabilities for dispute kit behavior after jumps and appeals.


149-188: LGTM!

The getRoundInfo and getVoteInfo functions provide comprehensive access to round and vote state, which is valuable for off-chain queries and UI integration.


52-55: All IDisputeKit.draw implementations match updated signature
DisputeKitClassicBase.sol’s draw returns (address drawnAddress, uint96 fromSubcourtID) as per IDisputeKit; no further changes needed.

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

98-118: LGTM!

The recovery commitment storage (lines 108-113) and validation (line 106) are correctly implemented, matching the pattern in DisputeKitGatedShutter.


154-188: LGTM!

The hashVote conditional logic (lines 159-166) and _getExpectedVoteHash recovery path selection (lines 183-187) correctly implement the shutter recovery mechanism, matching the pattern in DisputeKitGatedShutter.

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

56-79: LGTM!

The Active struct (lines 56-59) provides clear tracking of dispute lifecycle across dispute kits. The storage gaps in structs (lines 29, 45, 53) properly reserve slots for future upgrades following the UUPS pattern.


128-142: LGTM!

The isActive modifier (lines 138-142) correctly enforces both dispute existence and current round activity, preventing operations on jumped or unknown disputes.


169-194: LGTM!

The governance functions are appropriately restricted to the owner. The executeOwnerProposal function (lines 173-176) provides necessary flexibility for upgrades and emergency actions while being properly gated.


200-231: LGTM!

The dispute creation logic correctly handles both new disputes (lines 215-219) and disputes returning from another DK (lines 210-213). The Active tracking at lines 221-222 properly marks both the dispute and current round as active.


234-258: LGTM!

The draw function correctly returns both drawnAddress and fromSubcourtID (line 237, 245, 248), matching the updated IDisputeKit interface. The early return at line 248 when no one has staked is appropriate.


272-294: LGTM!

The commitCount logic (lines 284-289) correctly tracks only first-time commits, preventing double-counting when jurors update their commitments during the commit period. Based on learnings.


372-436: LGTM!

The appeal funding logic correctly marks the current round as inactive (line 424) when a court jump occurs, ensuring the isActive modifier properly rejects operations on jumped disputes.


534-571: LGTM!

The split between getDegreeOfCoherenceReward and getDegreeOfCoherencePenalty (lines 534-554) is clean, with both sharing the internal _getDegreeOfCoherence logic (lines 556-571). This matches the updated IDisputeKit interface.


618-635: LGTM!

The new getters are correctly implemented:

  • earlyCourtJump returns false (appropriate for classic DK)
  • getNbVotesAfterAppeal uses the standard formula (n * 2) + 1
  • getJumpDisputeKitID safely falls back to DISPUTE_KIT_CLASSIC when jumpDisputeKitID is unset

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

♻️ Duplicate comments (16)
contracts/src/arbitration/university/KlerosCoreUniversity.sol (3)

279-281: [Duplicate] Zero-address guard still missing for changeOwner.

This issue was previously flagged in past reviews but remains unaddressed. The changeOwner function allows setting owner to the zero address, which would lock the contract's ownership.

Based on past reviews, apply this fix:

 function changeOwner(address payable _owner) external onlyByOwner {
+    if (_owner == address(0)) revert ZeroAddress();
     owner = _owner;
 }

Add the error definition:

+error ZeroAddress();

291-306: [Duplicate] Zero-address guards still missing for parameter setters.

This issue was previously flagged in past reviews but remains unaddressed. The setters for pinakion, jurorProsecutionModule, and sortitionModule lack zero-address validation, which could lead to unusable contract state.

Based on past reviews, add guards to each setter:

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

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

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

810-815: [Duplicate] Critical: Replace .send with .call for ETH transfers.

This issue was previously flagged in past reviews but remains unaddressed. Using .send for ETH transfers is unsafe because it caps gas at 2300 and can silently fail. This affects multiple locations where fees and penalties are sent to the owner or jurors.

Based on past reviews, replace all .send calls with .call:

 if (round.feeToken == NATIVE_CURRENCY) {
-    payable(owner).send(round.totalFeesForJurors);
+    (bool okFee, ) = payable(owner).call{value: round.totalFeesForJurors}("");
+    if (!okFee) revert NativeTransferFailed();
 } else {
     round.feeToken.safeTransfer(owner, round.totalFeesForJurors);
 }
-pinakion.safeTransfer(owner, _params.pnkPenaltiesInRound);
+pinakion.safeTransfer(owner, _params.pnkPenaltiesInRound);

Apply similar changes to lines 866, 899 (juror rewards) and add the error:

+error NativeTransferFailed();

Also applies to: 866-869, 894-894, 899-902

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

396-412: Governance setters: zero-address guards

Add basic validation for owner/pinakion/sortitionModule to avoid accidental zeroing. Juror NFT may be intentionally set to zero to disable gating, so skip that one if desired.

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

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

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

Also applies to: 420-425, 581-585


427-434: Validate dispute kit address when adding

Prevent adding address(0).

 function addNewDisputeKit(IDisputeKit _disputeKitAddress) external onlyByOwner {
+    if (address(_disputeKitAddress) == address(0)) revert ZeroAddress();
     uint256 disputeKitID = disputeKits.length;
     disputeKits.push(_disputeKitAddress);
     emit DisputeKitCreated(disputeKitID, _disputeKitAddress);
 }

315-363: Initialize: add zero-address validation for critical dependencies

Prevent bricking ownership/core wiring.

-        owner = _owner;
+        if (_owner == address(0)) revert ZeroAddressOwner();
+        if (address(_pinakion) == address(0)) revert ZeroAddress();
+        if (address(_disputeKit) == address(0)) revert ZeroAddress();
+        if (address(_sortitionModuleAddress) == address(0)) revert ZeroAddress();
+        if (_wNative == address(0)) revert ZeroAddress();
+        owner = _owner;
         guardian = _guardian;
         pinakion = _pinakion;
         jurorProsecutionModule = _jurorProsecutionModule;
         sortitionModule = _sortitionModuleAddress;
         wNative = _wNative;
         jurorNft = _jurorNft;

Add errors:

+error ZeroAddressOwner();
+error ZeroAddress();

825-894: Reentrancy window in execute() around external transfers

External sends occur while state like penalties and paid sums are still settling. Add a guard or move all state writes before transfers.

Options:

  • Add OZ ReentrancyGuardTransient and mark execute nonReentrant; or
  • Persist all state updates first, then perform transfers after. Prefer state-first if feasible.

Example:

- contract KlerosCore is IArbitratorV2, Initializable, UUPSProxiable {
+ import {ReentrancyGuardTransient} from "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol";
+ contract KlerosCore is IArbitratorV2, Initializable, UUPSProxiable, ReentrancyGuardTransient {
 ...
- function execute(uint256 _disputeID, uint256 _round, uint256 _iterations) external whenNotPaused {
+ function execute(uint256 _disputeID, uint256 _round, uint256 _iterations) external whenNotPaused nonReentrant {

371-373: UUPS override mutability mismatch (“view”)

parent _authorizeUpgrade(address) is non-view. Remove view or it won’t compile.

-function _authorizeUpgrade(address) internal view override onlyByOwner {
+function _authorizeUpgrade(address) internal override onlyByOwner {

5-5: Incorrect import: IArbitrableV2 is not in IArbitratorV2.sol

Import IArbitrableV2 from its own file to compile.

-import {IArbitrableV2, IArbitratorV2} from "./interfaces/IArbitratorV2.sol";
+import {IArbitratorV2} from "./interfaces/IArbitratorV2.sol";
+import {IArbitrableV2} from "./interfaces/IArbitrableV2.sol";

630-639: Interface data-location mismatch (bytes memory → calldata)

IArbitratorV2 typically uses bytes calldata. Align override to avoid signature mismatch.

-function createDispute(
+function createDispute(
     uint256 _numberOfChoices,
-    bytes memory _extraData
+    bytes calldata _extraData
 ) external payable override returns (uint256 disputeID) {

Also align the internal calls accordingly (no behavior change).


1069-1074: Interface data-location mismatch in arbitrationCost

Align to bytes calldata to match interface.

-function arbitrationCost(bytes memory _extraData) public view override returns (uint256 cost) {
+function arbitrationCost(bytes calldata _extraData) public view override returns (uint256 cost) {

1208-1211: Division-by-zero risk in convertEthToTokenAmount

rateInEth can be zero for unconfigured tokens. Fail fast with a clear error.

-function convertEthToTokenAmount(IERC20 _toToken, uint256 _amountInEth) public view returns (uint256) {
-    return (_amountInEth * 10 ** currencyRates[_toToken].rateDecimals) / currencyRates[_toToken].rateInEth;
-}
+function convertEthToTokenAmount(IERC20 _toToken, uint256 _amountInEth) public view returns (uint256) {
+    CurrencyRate memory rate = currencyRates[_toToken];
+    if (rate.rateInEth == 0) revert TokenNotAccepted();
+    return (_amountInEth * 10 ** rate.rateDecimals) / rate.rateInEth;
+}

97-99: Constant overflows in 0.8.x

(2 ** 256 - 2) overflows in a constant expression. Use type(uint256).max.

-uint256 private constant NON_PAYABLE_AMOUNT = (2 ** 256 - 2) / 2; // An amount higher than the supply of ETH.
+// An amount higher than the supply of ETH.
+uint256 private constant NON_PAYABLE_AMOUNT = (type(uint256).max - 1) / 2;

674-683: Guard zero feeForJuror and zero nbVotes (div-by-zero and dead rounds)

feeForJuror can be zero; nbVotes can compute to zero, later causing division by zero in execute().

 uint256 feeForJuror = (_feeToken == NATIVE_CURRENCY)
     ? court.feeForJuror
     : convertEthToTokenAmount(_feeToken, court.feeForJuror);
-round.nbVotes = _feeAmount / feeForJuror;
+if (feeForJuror == 0) revert ZeroFeeForJuror();
+round.nbVotes = _feeAmount / feeForJuror;
+if (round.nbVotes == 0) revert InsufficientFeesForOneJuror();

Add errors (near other errors):

+error ZeroFeeForJuror();
+error InsufficientFeesForOneJuror();

1372-1396: Out-of-bounds mload when _extraData length is 64–95 bytes

You mload 96 bytes while only checking length >= 64. Load each slot conditionally.

-        if (_extraData.length >= 64) {
-            assembly {
-                courtID := mload(add(_extraData, 0x20))
-                minJurors := mload(add(_extraData, 0x40))
-                disputeKitID := mload(add(_extraData, 0x60))
-            }
-            if (courtID == FORKING_COURT || courtID >= courts.length) {
-                courtID = GENERAL_COURT;
-            }
-            if (minJurors == 0) {
-                minJurors = DEFAULT_NB_OF_JURORS;
-            }
-            if (disputeKitID == NULL_DISPUTE_KIT || disputeKitID >= disputeKits.length) {
-                disputeKitID = DISPUTE_KIT_CLASSIC;
-            }
-        } else {
-            courtID = GENERAL_COURT;
-            minJurors = DEFAULT_NB_OF_JURORS;
-            disputeKitID = DISPUTE_KIT_CLASSIC;
-        }
+        // Defaults
+        courtID = GENERAL_COURT;
+        minJurors = DEFAULT_NB_OF_JURORS;
+        disputeKitID = DISPUTE_KIT_CLASSIC;
+        if (_extraData.length >= 32) {
+            assembly { courtID := mload(add(_extraData, 0x20)) }
+        }
+        if (_extraData.length >= 64) {
+            assembly { minJurors := mload(add(_extraData, 0x40)) }
+        }
+        if (_extraData.length >= 96) {
+            assembly { disputeKitID := mload(add(_extraData, 0x60)) }
+        }
+        if (courtID == FORKING_COURT || courtID >= courts.length) courtID = GENERAL_COURT;
+        if (minJurors == 0) minJurors = DEFAULT_NB_OF_JURORS;
+        if (disputeKitID == NULL_DISPUTE_KIT || disputeKitID >= disputeKits.length) disputeKitID = DISPUTE_KIT_CLASSIC;
contracts/src/arbitration/interfaces/IDisputeKit.sol (1)

94-101: NatSpec: “reward” → “penalty” for penalty accessor return

The @return description still says “PNK reward” for a penalty getter. Change to “PNK penalty”.

Apply:

-/// @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.
🧹 Nitpick comments (2)
contracts/src/arbitration/interfaces/IDisputeKit.sol (1)

151-161: Fix “Commited” typo in getRoundInfo NatSpec

Spelling: “Committed”.

-/// @return totalCommited Number of jurors who cast the commit already (only relevant for hidden votes).
+/// @return totalCommitted Number of jurors who cast the commit already (only relevant for hidden votes).

Optional: align the return variable name in the signature to totalCommitted across implementations for consistency.

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

178-182: Optional: zero-address guard on changeOwner

Prevent accidental loss of ownership.

 function changeOwner(address payable _owner) external onlyByOwner {
-    owner = _owner;
+    if (_owner == address(0)) revert OwnerOnly(); // Or a dedicated ZeroAddress error
+    owner = _owner;
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a934ba5 and 6b7e365.

📒 Files selected for processing (4)
  • contracts/src/arbitration/KlerosCore.sol (4 hunks)
  • contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol (21 hunks)
  • contracts/src/arbitration/interfaces/IDisputeKit.sol (3 hunks)
  • contracts/src/arbitration/university/KlerosCoreUniversity.sol (39 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-30T17:18:12.895Z
Learnt from: jaybuidl
PR: kleros/kleros-v2#2145
File: contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol:277-286
Timestamp: 2025-09-30T17:18:12.895Z
Learning: In DisputeKitClassicBase.sol's castCommit function, jurors are allowed to re-submit commits during the commit period. The implementation uses a commitCount variable to track only first-time commits (where commit == bytes32(0)) so that totalCommitted is not incremented when a juror updates their existing commit.

Applied to files:

  • contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.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). (6)
  • GitHub Check: Analyze (javascript)
  • GitHub Check: hardhat-tests
  • GitHub Check: SonarCloud
  • GitHub Check: Analyze (javascript)
  • GitHub Check: hardhat-tests
  • GitHub Check: SonarCloud
🔇 Additional comments (6)
contracts/src/arbitration/university/KlerosCoreUniversity.sol (5)

11-11: LGTM: Import and version update are consistent with the refactor.

The Constants.sol import provides ONE_BASIS_POINT used throughout the coherence calculations, and the version bump to "2.0.0" appropriately reflects the governor→owner migration and other significant changes.

Also applies to: 18-18


42-42: LGTM: Storage gaps added for upgradeability.

The __gap slots in Court, Dispute, and Round structs are essential for UUPS proxies, reserving storage space for future upgrades without causing storage collisions.

Also applies to: 52-52, 68-68


759-770: LGTM: Defensive coherence capping prevents arithmetic issues.

The coherence value is capped at ONE_BASIS_POINT (lines 768-770) before use in penalty calculations (line 773). While the comment notes this should be ensured by the dispute kit, the guard is good defensive programming that prevents potential underflow in the penalty calculation: penalty = stake * (ONE_BASIS_POINT - coherence) / ONE_BASIS_POINT.


835-849: LGTM: Separate coherence values for PNK and fee rewards enhance flexibility.

The new getDegreeOfCoherenceReward returns separate pnkCoherence and feeCoherence values (line 835), each capped at ONE_BASIS_POINT (lines 844-849). This allows dispute kits to implement different reward mechanisms for PNK staking rewards versus arbitration fee rewards, providing more granular control over incentive structures.

The calculations at lines 858 and 860 correctly apply each coherence value to its respective reward pool.

Also applies to: 858-860


602-602: Explicitly validate fromSubcourtID before use
Add a bounds check after the draw call to prevent out-of-bounds access on courts[fromSubcourtID], or document/assert that all dispute kits return valid court IDs:

 (address drawnAddress, uint96 fromSubcourtID) = disputeKit.draw(_disputeID, iteration);
+ if (fromSubcourtID != 0 && fromSubcourtID >= courts.length) revert InvalidCourtID();
 round.drawnJurorFromCourtIDs.push(fromSubcourtID != 0 ? fromSubcourtID : dispute.courtID);
contracts/src/arbitration/KlerosCore.sol (1)

563-579: Emitting undeclared events (AcceptedFeeToken, NewCurrencyRate)

Both events are emitted but not declared here. Declare them (or import an interface that declares them) to compile.

Add near other event declarations:

+    /// @notice Emitted when a fee token acceptance changes.
+    event AcceptedFeeToken(IERC20 indexed _feeToken, bool indexed _accepted);
+    /// @notice Emitted when a currency rate is updated.
+    event NewCurrencyRate(IERC20 indexed _feeToken, uint64 _rateInEth, uint8 _rateDecimals);
⛔ Skipped due to learnings
Learnt from: jaybuidl
PR: kleros/kleros-v2#2126
File: contracts/src/arbitration/KlerosCore.sol:472-489
Timestamp: 2025-09-04T23:36:16.415Z
Learning: In this repo, KlerosCore emits AcceptedFeeToken and NewCurrencyRate events that are declared in contracts/src/arbitration/interfaces/IArbitratorV2.sol; implementations don’t need to redeclare these events.

pragma solidity ^0.8.24;

import {KlerosCore, KlerosCoreBase, IDisputeKit, ISortitionModule} from "../KlerosCore.sol";
import {KlerosCore, IDisputeKit, ISortitionModule} from "../KlerosCore.sol";
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Broken imports: IDisputeKit/ISortitionModule are not in KlerosCore.sol

Import interfaces from their files; only import KlerosCore from its file.

-import {KlerosCore, IDisputeKit, ISortitionModule} from "../KlerosCore.sol";
+import {KlerosCore} from "../KlerosCore.sol";
+import {IDisputeKit} from "../interfaces/IDisputeKit.sol";
+import {ISortitionModule} from "../interfaces/ISortitionModule.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, IDisputeKit, ISortitionModule} from "../KlerosCore.sol";
import {KlerosCore} from "../KlerosCore.sol";
import {IDisputeKit} from "../interfaces/IDisputeKit.sol";
import {ISortitionModule} from "../interfaces/ISortitionModule.sol";
🤖 Prompt for AI Agents
In contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol around line
5, the import statement wrongly tries to import IDisputeKit and ISortitionModule
from KlerosCore.sol; those interfaces live in their own files. Change the import
so only KlerosCore is imported from "../KlerosCore.sol" and add separate import
lines pulling IDisputeKit and ISortitionModule from their respective files
(e.g., "../interfaces/IDisputeKit.sol" and "../interfaces/ISortitionModule.sol"
or the correct relative paths in this repo) so each symbol is imported from the
file that defines it.

Comment on lines +49 to 58
/// @notice Draws the juror from the sortition tree. The drawn address is picked up by Kleros Core.
/// @dev Access restricted to Kleros Core only.
/// @param _coreDisputeID The ID of the dispute in Kleros Core, not in the Dispute Kit.
/// @param _nonce Nonce.
/// @return drawnAddress The drawn address.
function draw(uint256 _coreDisputeID, uint256 _nonce) external returns (address drawnAddress);
function draw(
uint256 _coreDisputeID,
uint256 _nonce
) external returns (address drawnAddress, uint96 fromSubcourtID);

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add missing @return doc for fromSubcourtID in draw()

The function returns (address, uint96) but only documents the address.

 /// @param _nonce Nonce.
 /// @return drawnAddress The drawn address.
+/// @return fromSubcourtID The subcourt ID the juror was drawn from.
 function draw(
📝 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
/// @notice Draws the juror from the sortition tree. The drawn address is picked up by Kleros Core.
/// @dev Access restricted to Kleros Core only.
/// @param _coreDisputeID The ID of the dispute in Kleros Core, not in the Dispute Kit.
/// @param _nonce Nonce.
/// @return drawnAddress The drawn address.
function draw(uint256 _coreDisputeID, uint256 _nonce) external returns (address drawnAddress);
function draw(
uint256 _coreDisputeID,
uint256 _nonce
) external returns (address drawnAddress, uint96 fromSubcourtID);
/// @notice Draws the juror from the sortition tree. The drawn address is picked up by Kleros Core.
/// @dev Access restricted to Kleros Core only.
/// @param _coreDisputeID The ID of the dispute in Kleros Core, not in the Dispute Kit.
/// @param _nonce Nonce.
/// @return drawnAddress The drawn address.
/// @return fromSubcourtID The subcourt ID the juror was drawn from.
function draw(
uint256 _coreDisputeID,
uint256 _nonce
) external returns (address drawnAddress, uint96 fromSubcourtID);
🤖 Prompt for AI Agents
In contracts/src/arbitration/interfaces/IDisputeKit.sol around lines 49 to 58,
the draw() function's NatSpec only documents the returned drawnAddress but omits
the returned fromSubcourtID; add a @return tag for fromSubcourtID describing
that it is the subcourt ID (uint96) the drawn juror comes from (or similar
concise description) so both return values are documented in the function's
NatSpec.

Comment on lines +801 to +806
Court storage court = courts[newCourtID];
extraRound.nbVotes = msg.value / court.feeForJuror; // As many votes that can be afforded by the provided funds.
extraRound.pnkAtStakePerJuror = _calculatePnkAtStake(court.minStake, court.alpha);
extraRound.totalFeesForJurors = msg.value;
extraRound.disputeKitID = newDisputeKitID;

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

Appeal path: same zero-fee/zero-votes guard

Mirror the checks here to avoid nbVotes==0 and downstream divide-by-zero in execute().

- extraRound.nbVotes = msg.value / court.feeForJuror; // As many votes that can be afforded by the provided funds.
+ if (court.feeForJuror == 0) revert ZeroFeeForJuror();
+ extraRound.nbVotes = msg.value / court.feeForJuror; // As many votes that can be afforded by the provided funds.
+ if (extraRound.nbVotes == 0) revert InsufficientFeesForOneJuror();
📝 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
Court storage court = courts[newCourtID];
extraRound.nbVotes = msg.value / court.feeForJuror; // As many votes that can be afforded by the provided funds.
extraRound.pnkAtStakePerJuror = _calculatePnkAtStake(court.minStake, court.alpha);
extraRound.totalFeesForJurors = msg.value;
extraRound.disputeKitID = newDisputeKitID;
Court storage court = courts[newCourtID];
if (court.feeForJuror == 0) revert ZeroFeeForJuror();
extraRound.nbVotes = msg.value / court.feeForJuror; // As many votes that can be afforded by the provided funds.
if (extraRound.nbVotes == 0) revert InsufficientFeesForOneJuror();
extraRound.pnkAtStakePerJuror = _calculatePnkAtStake(court.minStake, court.alpha);
extraRound.totalFeesForJurors = msg.value;
extraRound.disputeKitID = newDisputeKitID;
🤖 Prompt for AI Agents
In contracts/src/arbitration/KlerosCore.sol around lines 801 to 806, the code
computes extraRound.nbVotes = msg.value / court.feeForJuror but does not guard
against nbVotes == 0 which can cause a downstream divide-by-zero in execute();
add the same zero-fee/zero-votes guard used elsewhere: require(msg.value >=
court.feeForJuror, "Not enough funds to buy at least one vote") or compute
nbVotes and require(nbVotes > 0, "No votes purchased"); if you prefer
non-reverting behavior, revert or refund when msg.value < court.feeForJuror so
extraRound.nbVotes cannot be zero and extraRound.totalFeesForJurors remains
consistent.

Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
1 Security Hotspot

See analysis details on SonarQube Cloud

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (2)
contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol (1)

5-5: Fix broken imports for interfaces (critical).

IDisputeKit and ISortitionModule shouldn’t be imported from KlerosCore.sol. Import them from their interface files.

-import {KlerosCore, IDisputeKit, ISortitionModule} from "../KlerosCore.sol";
+import {KlerosCore} from "../KlerosCore.sol";
+import {IDisputeKit} from "../interfaces/IDisputeKit.sol";
+import {ISortitionModule} from "../interfaces/ISortitionModule.sol";
contracts/src/arbitration/dispute-kits/DisputeKitGatedShutter.sol (1)

181-209: Remove transient callerIsJuror; the context still leaks across external calls.

castVoteShutter flips the transient storage flag before delegating to _castVote, but _castVote goes on to touch core (and other untrusted externals) before returning. Any re-entrant read of hashVote/_getExpectedVoteHash during that window now observes the “juror” branch even though the caller is not the juror, which defeats the whole recovery/commit separation. Please get rid of the cross-call flag and plumb an explicit isJuror context into the hashing/expected-hash helpers so the decision is scoped to the current stack frame only, as already discussed in the earlier review.

Also applies to: 223-228

🧹 Nitpick comments (7)
contracts/test/arbitration/dispute-kit-shutter.ts (2)

11-12: Consider configuring ESLint rules at project level for test directories.

While these ESLint disables are functional, you might want to configure these rules in your ESLint config for all test files rather than repeating them in each file. This improves maintainability.


25-25: Remove unnecessary async keyword from describe callback.

The describe callback doesn't need to be async since the asynchronous setup is handled by the beforeEach hook. Having async on describe is non-standard and can cause confusion.

Apply this diff:

-describe("DisputeKitShutter", async () => {
+describe("DisputeKitShutter", () => {
contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol (2)

29-30: Avoid __gap arrays inside structs (gas bloat; not standard upgrade pattern).

Placing __gap inside Dispute/Round/Vote inflates storage/gas per dispute/round/vote and doesn’t follow common OZ upgradeability guidance (reserve at contract level). Prefer removing these struct-level gaps and rely on top-level __gap.

Do you want a patch to remove these and keep only the contract-level uint256[50] private __gap;?

Also applies to: 45-46, 53-54


169-176: Emit events for governance changes (owner/core/jump DK).

Add events for executeOwnerProposal (destination, amount), changeOwner, changeCore, and changeJumpDisputeKitID to improve observability and audits.

Also applies to: 178-195

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

23-25: Nit: avoid async arrow on describe blocks.

Mocha doesn’t await async describe callbacks. Prefer non‑async describe and keep async in hooks/tests.

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

74-81: Add event for token whitelist changes.

Emitting an event with (token, supported) per change improves auditability.

Example:

  • event SupportedTokenChanged(address token, bool supported);

87-101: Consider adding onlyByCore on override for early fail and clarity.

Relying on super.createDispute to enforce onlyByCore works but late‑fails after your whitelist check. Add onlyByCore here to short‑circuit unauthorized calls.

-    ) public override {
+    ) public override onlyByCore {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6b7e365 and 13f3708.

📒 Files selected for processing (8)
  • contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol (21 hunks)
  • contracts/src/arbitration/dispute-kits/DisputeKitGated.sol (4 hunks)
  • contracts/src/arbitration/dispute-kits/DisputeKitGatedShutter.sol (6 hunks)
  • contracts/src/test/DisputeKitGatedMock.sol (1 hunks)
  • contracts/src/test/DisputeKitGatedShutterMock.sol (1 hunks)
  • contracts/test/arbitration/dispute-kit-gated-shutter.ts (1 hunks)
  • contracts/test/arbitration/dispute-kit-gated.ts (1 hunks)
  • contracts/test/arbitration/dispute-kit-shutter.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • contracts/test/arbitration/dispute-kit-gated.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-30T17:18:12.895Z
Learnt from: jaybuidl
PR: kleros/kleros-v2#2145
File: contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol:277-286
Timestamp: 2025-09-30T17:18:12.895Z
Learning: In DisputeKitClassicBase.sol's castCommit function, jurors are allowed to re-submit commits during the commit period. The implementation uses a commitCount variable to track only first-time commits (where commit == bytes32(0)) so that totalCommitted is not incremented when a juror updates their existing commit.

Applied to files:

  • contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol
🧬 Code graph analysis (2)
contracts/test/arbitration/dispute-kit-gated-shutter.ts (2)
contracts/test/arbitration/helpers/dispute-kit-gated-common.ts (10)
  • TokenGatedTestContext (27-46)
  • setupTokenGatedTest (165-236)
  • testTokenWhitelistManagement (240-305)
  • testAccessControl (307-323)
  • testUnsupportedTokenErrors (325-368)
  • testERC20Gating (370-431)
  • testERC721Gating (433-494)
  • testERC1155Gating (496-557)
  • testWhitelistIntegration (559-625)
  • testNoTokenGateAddress (627-704)
contracts/test/arbitration/helpers/dispute-kit-shutter-common.ts (7)
  • ShutterTestContext (25-50)
  • setupShutterTest (206-325)
  • testCommitPhase (329-443)
  • testNormalFlowBotReveals (445-582)
  • testRecoveryFlowJurorReveals (584-730)
  • testHashFunctionBehavior (732-777)
  • testEdgeCasesAndSecurity (779-880)
contracts/test/arbitration/dispute-kit-shutter.ts (1)
contracts/test/arbitration/helpers/dispute-kit-shutter-common.ts (7)
  • ShutterTestContext (25-50)
  • setupShutterTest (206-325)
  • testCommitPhase (329-443)
  • testNormalFlowBotReveals (445-582)
  • testRecoveryFlowJurorReveals (584-730)
  • testHashFunctionBehavior (732-777)
  • testEdgeCasesAndSecurity (779-880)
⏰ 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: hardhat-tests
  • GitHub Check: SonarCloud
  • GitHub Check: Mend Security Check
  • GitHub Check: hardhat-tests
  • GitHub Check: Analyze (javascript)
  • GitHub Check: SonarCloud
🔇 Additional comments (9)
contracts/test/arbitration/dispute-kit-shutter.ts (3)

1-9: LGTM!

The imports are well-organized and appropriate for consolidating shared Shutter test scenarios.


14-24: LGTM!

The documentation clearly describes the test suite's purpose and coverage areas.


26-38: LGTM!

The test structure correctly uses the callback pattern to share context with the test helpers, ensuring each test gets a fresh context from beforeEach.

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

284-294: Commit re-submission accounted correctly.

Using commitCount only for first-time commits prevents double-counting when jurors update their commit during the commit period. Matches intended behavior.

Based on learnings

contracts/src/test/DisputeKitGatedMock.sol (1)

10-14: LGTM: minimal test wrapper.

Public wrapper cleanly exposes internal decoder for tests without affecting production code.

contracts/src/test/DisputeKitGatedShutterMock.sol (1)

10-14: LGTM: mirrors gated mock for shutter path.

Simple, pure wrapper is appropriate for test introspection.

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

55-62: Initialize allows token‑free disputes; good default.

Setting supportedTokens[address(0)] = true enables non‑gated usage by default. Sensible choice.


116-131: Bounds check is correct; assembly offsets look right.

160‑byte minimum, then load packed address/flag at +0x80 and tokenId at +0xA0; unpacking mask/shift is OK.


141-155: Gate draw logic looks correct; supportedTokens checked at creation.

NO_TOKEN_GATE short‑circuits; ERC1155 vs ERC20/721 balance checks are appropriate.

Comment on lines 73 to +74
bool public singleDrawPerJuror; // Whether each juror can only draw once per dispute, false by default.
mapping(uint256 localDisputeID => mapping(uint256 localRoundID => mapping(address drawnAddress => bool)))
public alreadyDrawn; // True if the address has already been drawn, false by default. To be added to the Round struct when fully redeploying rather than upgrading.
mapping(uint256 coreDisputeID => bool) public coreDisputeIDToActive; // True if this dispute kit is active for this core dispute ID.
mapping(uint256 coreDisputeID => Active) public coreDisputeIDToActive; // Active status of the dispute and the current round.
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

“singleDrawPerJuror” is enforced per round, not per dispute.

Comment says “only draw once per dispute”, but alreadyDrawn is stored on Round and checked against the current (last) round. Either:

  • enforce at dispute level (track across all rounds), or
  • update the comment to “once per round”.

Would you like a diff to enforce “once per dispute” via a Dispute-level mapping?

Also applies to: 742-747

🤖 Prompt for AI Agents
In contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol around lines
73-74 (and also apply the same change to the comment at 742-747): the comment
"Whether each juror can only draw once per dispute" is incorrect because the
implementation tracks alreadyDrawn on the Round and enforces it per round.
Either (A) update the comment to "once per round" to reflect current behavior,
or (B) change the implementation to enforce once-per-dispute by adding a storage
mapping mapping(uint256 => mapping(address => bool)) that records whether a
juror has been drawn for that dispute, then replace the current round-level
alreadyDrawn checks with lookups/sets on the new dispute-level mapping when
drawing jurors; ensure to initialize/clear state only per dispute lifecycle and
update any related logic and comments accordingly.

Comment on lines +459 to +477
for (uint256 i = 0; i < dispute.rounds.length; i++) {
Round storage round = dispute.rounds[i];

if (!round.hasPaid[_choice]) {
// Allow to reimburse if funding was unsuccessful for this ruling option.
amount += round.contributions[_beneficiary][_choice];
} else {
// Funding was successful for this ruling option.
if (_choice == finalRuling) {
// This ruling option is the ultimate winner.
amount += round.paidFees[_choice] > 0
? (round.contributions[_beneficiary][_choice] * round.feeRewards) / round.paidFees[_choice]
: 0;
} else if (!round.hasPaid[finalRuling]) {
// The ultimate winner was not funded in this round. In this case funded ruling option(s) are reimbursed.
amount +=
(round.contributions[_beneficiary][_choice] * round.feeRewards) /
(round.paidFees[round.fundedChoices[0]] + round.paidFees[round.fundedChoices[1]]);
}
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

Fix potential revert when only one choice was funded.

Division uses fundedChoices[0] + fundedChoices[1]. If only one choice got funded in a round, this indexing reverts, blocking withdrawals.

Apply this safer aggregation:

-                } else if (!round.hasPaid[finalRuling]) {
-                    // The ultimate winner was not funded in this round. In this case funded ruling option(s) are reimbursed.
-                    amount +=
-                        (round.contributions[_beneficiary][_choice] * round.feeRewards) /
-                        (round.paidFees[round.fundedChoices[0]] + round.paidFees[round.fundedChoices[1]]);
-                }
+                } else if (!round.hasPaid[finalRuling]) {
+                    // The ultimate winner was not funded in this round. Reimburse funded ruling option(s) pro‑rata.
+                    uint256 denom;
+                    for (uint256 j = 0; j < round.fundedChoices.length; j++) {
+                        denom += round.paidFees[round.fundedChoices[j]];
+                    }
+                    if (denom > 0) {
+                        amount += (round.contributions[_beneficiary][_choice] * round.feeRewards) / denom;
+                    }
+                }
📝 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
for (uint256 i = 0; i < dispute.rounds.length; i++) {
Round storage round = dispute.rounds[i];
if (!round.hasPaid[_choice]) {
// Allow to reimburse if funding was unsuccessful for this ruling option.
amount += round.contributions[_beneficiary][_choice];
} else {
// Funding was successful for this ruling option.
if (_choice == finalRuling) {
// This ruling option is the ultimate winner.
amount += round.paidFees[_choice] > 0
? (round.contributions[_beneficiary][_choice] * round.feeRewards) / round.paidFees[_choice]
: 0;
} else if (!round.hasPaid[finalRuling]) {
// The ultimate winner was not funded in this round. In this case funded ruling option(s) are reimbursed.
amount +=
(round.contributions[_beneficiary][_choice] * round.feeRewards) /
(round.paidFees[round.fundedChoices[0]] + round.paidFees[round.fundedChoices[1]]);
}
for (uint256 i = 0; i < dispute.rounds.length; i++) {
Round storage round = dispute.rounds[i];
if (!round.hasPaid[_choice]) {
// Allow to reimburse if funding was unsuccessful for this ruling option.
amount += round.contributions[_beneficiary][_choice];
} else {
// Funding was successful for this ruling option.
if (_choice == finalRuling) {
// This ruling option is the ultimate winner.
amount += round.paidFees[_choice] > 0
? (round.contributions[_beneficiary][_choice] * round.feeRewards) / round.paidFees[_choice]
: 0;
} else if (!round.hasPaid[finalRuling]) {
// The ultimate winner was not funded in this round. Reimburse funded ruling option(s) pro-rata.
uint256 denom;
for (uint256 j = 0; j < round.fundedChoices.length; j++) {
denom += round.paidFees[round.fundedChoices[j]];
}
if (denom > 0) {
amount += (round.contributions[_beneficiary][_choice] * round.feeRewards) / denom;
}
}
}
}
🤖 Prompt for AI Agents
In contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol around lines
459 to 477, the code assumes fundedChoices has two entries and accesses
fundedChoices[0] + fundedChoices[1], which can revert if only one choice was
funded; change the logic to safely compute the denominator by summing paidFees
for all entries in round.fundedChoices (e.g., loop over fundedChoices and
accumulate round.paidFees[index]) and use that accumulated sum in the division,
and ensure you handle the zero-denominator case (skip or return 0) to avoid
division-by-zero reverts.

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.

5 participants