Skip to content

Conversation

jaybuidl
Copy link
Member

@jaybuidl jaybuidl commented Aug 8, 2025

PR-Codex overview

This PR focuses on refactoring the Kleros arbitration system by updating contract interfaces, modifying governance patterns, and enhancing the dispute handling mechanisms. It also replaces several components with a new MarkdownRenderer for improved text rendering.

Detailed summary

  • Removed several unused contracts and deployment scripts.
  • Changed governor references to owner across multiple contracts for governance consistency.
  • Updated interfaces to use IRNG for random number generation.
  • Implemented MarkdownRenderer in various components for better markdown support.
  • Adjusted deployment scripts to use new contract structures.
  • Enhanced error handling and validation in dispute-related components.
  • Updated chain configurations and deployment artifacts for better clarity and functionality.

The following files were skipped due to too many changes: 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/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, 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/package.json, contracts/scripts/changeOwner.ts, contracts/test/arbitration/ruler.ts, contracts/src/gateway/interfaces/IForeignGateway.sol, contracts/test/arbitration/draw.ts, contracts/deployments/disputeKitsViem.ts, contracts/src/arbitration/interfaces/IArbitrableV2.sol, contracts/test/arbitration/dispute-kit-gated.ts, contracts/deploy/upgrade-all.ts, contracts/src/arbitration/evidence/EvidenceModule.sol, contracts/deployments/contractsEthers.ts, contracts/scripts/populateCourts.ts, 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/evidence/index.ts, contracts/test/integration/getContractsEthers.test.ts, contracts/src/proxy/UUPSProxiable.sol, 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/foundry/KlerosCore_Drawing.t.sol, contracts/src/arbitration/dispute-kits/DisputeKitGated.sol, contracts/README.md, contracts/src/gateway/interfaces/IHomeGateway.sol, web/src/hooks/useTokenAddressValidation.ts, contracts/scripts/utils/contracts.ts, contracts/src/test/SortitionTreesMock.sol, contracts/deploy/00-home-chain-arbitration.ts, contracts/src/rng/BlockhashRNG.sol, contracts/test/foundry/KlerosCore_Disputes.t.sol, .github/workflows/contracts-testing.yml, contracts/src/rng/RandomizerRNG.sol, contracts/src/arbitration/arbitrables/DisputeResolver.sol, contracts/test/integration/index.ts, contracts/src/arbitration/interfaces/IArbitratorV2.sol, web/src/pages/Resolver/Parameters/Court.tsx, web/src/components/MarkdownRenderer.tsx, 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/src/arbitration/arbitrables/ArbitrableExample.sol, contracts/CHANGELOG.md, 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/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/foundry/KlerosCore_Voting.t.sol, contracts/test/foundry/KlerosCore_Appeals.t.sol, contracts/test/arbitration/staking-neo.ts, contracts/test/foundry/KlerosCore_Staking.t.sol, contracts/src/arbitration/SortitionModule.sol, contracts/deployments/arbitrumSepoliaDevnet/SortitionModuleUniversity_Proxy.json, contracts/deployments/arbitrumSepoliaDevnet/DisputeKitClassicUniversity_Proxy.json, contracts/test/sortition/index.ts, contracts/test/arbitration/dispute-kit-shutter.ts, contracts/deployments/arbitrumSepoliaDevnet/KlerosCoreUniversity_Proxy.json, contracts/deployments/arbitrumSepoliaDevnet/SortitionModuleUniversity.json, contracts/test/foundry/KlerosCore_Execution.t.sol, contracts/deployments/arbitrumSepoliaDevnet/DisputeKitClassicUniversity.json, contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol, contracts/deployments/arbitrumSepoliaDevnet/KlerosCoreUniversity.json, contracts/src/arbitration/university/KlerosCoreUniversity.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

    • Multi-dispute‑kit support, RNG with automatic fallback for more reliable draws, and live token‑gate validation (ERC‑20/721/1155).
    • New Stakes profile header showing Available, Staked and Locked PNK.
  • Changes

    • Voting UI: improved commit/reveal flows (Shutter recovery path), flexible rendering, timeline/period display tweaks, and thousands‑separated number formatting.
    • Governance wording consolidated from governor→owner across UI.
  • Bug Fixes

    • Safer Shutter commit/reveal flow and abort when Shutter API env is missing; avoid zero‑amount transfers/emissions.
  • Documentation

    • Updated changelog, added llms.txt, robots header, and contract docs build/start scripts.
  • Chores

    • Dependency/tooling updates, CI focused on Hardhat tests, many type/test improvements.

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

Rework of contracts, deployments, scripts, tests and frontend: governance renamed governor→owner; RNG refactored to IRNG with RNGWithFallback and timestamp/blockhash fallback; sortition extracted into SortitionTrees and SortitionModule rewritten; KlerosCore and dispute-kit APIs redesigned; many deploy/test artifacts and web validations added/updated.

Changes

Cohort / File(s) Summary
Governance rename
contracts/src/**, contracts/scripts/*, contracts/deploy/*, contracts/test/**, contracts/src/proxy/**, contracts/src/token/Faucet.sol
Replaced governorowner across storage, modifiers, initializers and tasks; renamed changeGovernor→changeOwner, updated upgrade guards and added OwnerOnly errors; replaced change-governor task with change-owner.
RNG & randomness
contracts/src/rng/*, contracts/test/*, contracts/deploy/*
Introduced IRNG interface; added RNGWithFallback (primary RNG + timeout → blockhash fallback), updated Blockhash/Chainlink/Randomizer/Incremental to IRNG (no block params), added RNGMock for tests and consumer/owner access controls.
Sortition & trees
contracts/src/libraries/SortitionTrees.sol, contracts/src/arbitration/SortitionModule*.sol, contracts/src/test/SortitionTreesMock.sol
Extracted SortitionTrees library, replaced bytes32 tree keys with uint96 courtIDs, rewrote SortitionModule (upgradeable) with delayed-stake, penalties/rewards, forcedUnstake, withdrawLeftoverPNK and draw returning (drawnAddress, fromSubcourtID).
Core rewrite & university
contracts/src/arbitration/KlerosCore*.sol, removed KlerosCoreBase.sol/KlerosCoreNeo.sol
Reimplemented KlerosCore as upgradeable, owner-controlled contract(s) (University variant included): juror NFT support, richer dispute-kit/court jump model, storage gaps, new events (JurorRewardPenalty), many public views/mutations and currency/fee handling.
Dispute kits & voting flows
contracts/src/arbitration/dispute-kits/*, contracts/src/arbitration/interfaces/IDisputeKit.sol
Dispute-kit initializers accept _jumpDisputeKitID; IDisputeKit expanded (getDegreeOfCoherenceReward/Penalty, earlyCourtJump, getNbVotesAfterAppeal, getRoundInfo/getVoteInfo); Shutter/Gated kits add recoveryCommitments and juror-aware commit/reveal APIs.
Interfaces, constants & libraries
contracts/src/arbitration/interfaces/*.sol, contracts/src/libraries/Constants.sol
Broadened Solidity pragmas (>=0.8.0 <0.9.0), added ONE_BASIS_POINT, removed CappedMath, updated many interface signatures (IRNG, ISortitionModule, IArbitratorV2, IArbitrableV2, gateway interfaces).
Deployments & tooling
contracts/deploy/*, contracts/deployments/*, contracts/deployments/*.json, contracts/package.json
Removed Neo-specific paths, added RNGWithFallback wiring and new dispute-kit deployments, added DisputeTemplateRegistryUniversity artifacts, added Viem helper getDisputeKits, converted getContract to generics, added viem peerDependency and package resolutions.
Tests (Foundry & Hardhat)
contracts/test/**, contracts/test/foundry/**
Added many Foundry suites (initialization, governance, RNG fallback, drawing, disputes, voting, appeals, execution, staking); updated Hardhat tests to new signatures, owner errors, removed lookahead mining and improved typing.
Frontend & UX
web/src/hooks/*, web/src/pages/*, web/src/context/*, web/src/utils/*, web/netlify.toml, web/src/public/llms.txt
Added token-address validation hooks (ERC20/ERC721/ERC1155) and UI validation (Resolver/Court), Next-button gating for gated kits, Shutter API guard, Reveal RFA fallback when answers empty, timeline/labels/number-format tweaks, llms.txt and Netlify header.
Config & CI
contracts/hardhat.config.ts, contracts/foundry.toml, foundry.toml, contracts/.solcover.js, .github/workflows/contracts-testing.yml
Bumped Solidity to 0.8.30, enabled viaIR (env-driven), added Foundry profile, solcover irMinimum flag, increased optimizer runs, simplified CI to Hardhat tests.
Removals & cleanup
multiple deleted files (e.g., contracts/src/libraries/CappedMath.sol, Neo proxies, several deploy scripts)
Removed deprecated/Neo-specific files and proxies; consolidated mainnet naming (mainnetNeo → mainnet); updated deployment helpers and artifacts.

Sequence Diagram(s)

%%{init: {"themeVariables":{"actorBorder":"#2b2b2b","actorBg":"#f7fafc","noteBg":"#f0f4f8"}}}%%
sequenceDiagram
  autonumber
  participant Sortition as SortitionModule
  participant RNGF as RNGWithFallback
  participant Primary as IRNG
  participant BH as BlockhashRNG

  Sortition->>RNGF: requestRandomness()
  RNGF->>Primary: forward requestRandomness()
  RNGF->>RNGF: record requestTimestamp

  Note over RNGF,Sortition: at drawing time
  Sortition->>RNGF: receiveRandomness()
  alt Primary returns non-zero
    RNGF-->>Sortition: return random from Primary
  else Primary timed out or zero
    RNGF-->>Sortition: compute fallback (blockhash) and emit RNGFallback
  end
  Sortition->>Sortition: perform draw(courtID, disputeID, nonce)
  Sortition-->>Caller: (drawnAddress, fromSubcourtID)
Loading
sequenceDiagram
  autonumber
  participant User
  participant Core as KlerosCore
  participant DK as IDisputeKit
  participant SM as SortitionModule

  User->>Core: request appeal funding / appeal action
  Core->>DK: getNbVotesAfterAppeal(previousDK, currentNbVotes)
  DK-->>Core: nbVotesNext
  Core->>DK: earlyCourtJump(coreDisputeID)?
  alt DK requests court/ DK jump
    Core->>Core: compute new court & DK (getJumpDisputeKitID)
    Core-->>User: emit CourtJump / DisputeKitJump
  else
    Core-->>User: continue in same DK/court
  end
  Core->>SM: postDrawHook / draw scheduling
Loading

Estimated code review effort

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

Possibly related PRs

Poem

"I nibble ledger rows at dawn,
I hop where fallback seeds are sown.
Owners now wake where governors slept,
Trees draw jurors as carrots leapt.
A rabbit cheers the merge — tiny hops, big code!" 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title Check ❓ Inconclusive The title "Release" is overly generic and does not communicate the primary scope of this large, breaking refactor (governor→owner migration, IRNG/RNGWithFallback introduction, broad interface/storage changes, version bumps, and test updates), so a reviewer scanning PRs cannot tell the main technical impact from the title alone. Please replace the title with a concise, descriptive one that highlights the release/version and top-level changes. Suggested title: "v0.13.0 — Migrate governor→owner; introduce IRNG & RNGWithFallback; update interfaces/tests (breaking)".
✅ Passed checks (2 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • 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 0e96a87
🔍 Latest deploy log https://app.netlify.com/projects/kleros-v2-university/deploys/68d27f3b66cff100089bafb4

Copy link

netlify bot commented Aug 8, 2025

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

Name Link
🔨 Latest commit 0e96a87
🔍 Latest deploy log https://app.netlify.com/projects/kleros-v2-neo-devtools/deploys/68d27f3ba382070008fcc30f

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

Caution

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

⚠️ Outside diff range comments (17)
contracts/src/arbitration/evidence/ModeratedEvidenceModule.sol (1)

94-101: Constructor: validate governance params.

Add basic sanity checks to avoid misconfiguration at deploy-time.

@@
-    constructor(
+    constructor(
         IArbitratorV2 _arbitrator,
         address _owner,
         IDisputeTemplateRegistry _templateRegistry,
         uint256 _totalCostMultiplier,
         uint256 _initialDepositMultiplier,
         uint256 _bondTimeout,
@@
-    ) {
+    ) {
+        require(_owner != address(0), "owner=0");
+        require(_initialDepositMultiplier <= MULTIPLIER_DIVISOR, "initMult>100%");
+        // Need 2 * totalCost >= arbitrationCost to fund a dispute; i.e., totalCostMultiplier >= MULTIPLIER_DIVISOR/2.
+        require(_totalCostMultiplier >= MULTIPLIER_DIVISOR / 2, "totalCostMult<50%");
+        require(_bondTimeout > 0, "timeout=0");

Also applies to: 105-113, 115-121

contracts/src/libraries/SafeERC20.sol (2)

18-25: increaseAllowance isn’t “safe”: ignores approve return value.

Use the same low-level pattern you use in safeTransfer/From so non‑standard ERC20s don’t silently fail.

-    function increaseAllowance(IERC20 _token, address _spender, uint256 _addedValue) internal returns (bool) {
-        _token.approve(_spender, _token.allowance(address(this), _spender) + _addedValue);
-        return true;
-    }
+    function increaseAllowance(IERC20 _token, address _spender, uint256 _addedValue) internal returns (bool) {
+        uint256 newAllowance = _token.allowance(address(this), _spender) + _addedValue;
+        (bool success, bytes memory data) =
+            address(_token).call(abi.encodeCall(IERC20.approve, (_spender, newAllowance)));
+        return (success && (data.length == 0 || abi.decode(data, (bool))));
+    }

27-35: Fix ignored SafeERC20 return values

Several call sites ignore the bool returned by SafeERC20.safeTransfer / safeTransferFrom (silent-failure risk). Either check the result (require(...) or if (!...) handle) or change the helper to revert. Examples that need fixes:

  • contracts/src/arbitration/KlerosCore.sol:626 — pinakion.safeTransfer(_account, _amount);
  • contracts/src/arbitration/KlerosCore.sol:951 — pinakion.safeTransfer(owner, _params.pnkPenaltiesInRound);
  • contracts/src/arbitration/KlerosCore.sol:1004 — pinakion.safeTransfer(account, pnkReward);
  • contracts/src/arbitration/KlerosCore.sol:1024 — pinakion.safeTransfer(owner, leftoverPnkReward);
  • contracts/src/arbitration/KlerosCore.sol:1260 — _feeToken.safeTransfer(_recipient, _amount) (internal _transferFeeToken; currently unchecked)
  • contracts/src/arbitration/university/KlerosCoreUniversity.sol:476 — pinakion.safeTransfer(_account, _amount);
  • contracts/src/arbitration/university/KlerosCoreUniversity.sol:811 — round.feeToken.safeTransfer(owner, round.totalFeesForJurors);
  • contracts/src/arbitration/university/KlerosCoreUniversity.sol:813 — pinakion.safeTransfer(owner, _params.pnkPenaltiesInRound);
  • contracts/src/arbitration/university/KlerosCoreUniversity.sol:867 — round.feeToken.safeTransfer(account, feeReward);
  • contracts/src/arbitration/university/KlerosCoreUniversity.sol:872 — pinakion.safeTransfer(account, pnkReward);
  • contracts/src/arbitration/university/KlerosCoreUniversity.sol:892 — pinakion.safeTransfer(owner, leftoverPnkReward);
  • contracts/src/arbitration/university/KlerosCoreUniversity.sol:900 — round.feeToken.safeTransfer(owner, leftoverFeeReward);
  • contracts/src/arbitration/devtools/KlerosCoreRuler.sol:520 — round.feeToken.safeTransfer(account, feeReward);
contracts/src/arbitration/KlerosGovernor.sol (3)

120-143: Constructor issues: owner unset and templateRegistry is zero address

  • Set owner via constructor arg.
  • Inject IDisputeTemplateRegistry; current call on zero address will revert on decode.
-    constructor(
+    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;
+        if (address(templateRegistry) == address(0)) revert WrongInputTargetAndDatasize(); // or introduce a dedicated InvalidAddress error.
+        if (owner == address(0)) revert OwnerOnly(); // or introduce InvalidOwner().
@@
-        templateId = templateRegistry.setDisputeTemplate("", _templateData, _templateDataMappings);
+        templateId = templateRegistry.setDisputeTemplate("", _templateData, _templateDataMappings);
     }

315-337: Validate dispute ID in rule()

Guard against incorrect _disputeID; prevents misattribution if arbitrator calls with a different dispute.

     function rule(uint256 _disputeID, uint256 _ruling) external override {
         Session storage session = sessions[sessions.length - 1];
         if (msg.sender != address(arbitrator)) revert OnlyArbitratorAllowed();
         if (session.status != Status.DisputeCreated) revert NotDisputed();
+        if (_disputeID != session.disputeID) revert NotDisputed(); // or a dedicated WrongDisputeID error.
         if (_ruling > session.submittedLists.length) revert RulingOutOfBounds();

110-120: Critical: constructor calls templateRegistry before it's initialized — deployment will revert

contracts/src/arbitration/KlerosGovernor.sol — constructor invokes templateRegistry.setDisputeTemplate(...) but templateRegistry is never assigned; initialize templateRegistry (e.g., add a constructor parameter and set it) and update all deployment/tests to match the new constructor signature (wNative was also added).

contracts/src/arbitration/devtools/KlerosCoreRuler.sol (5)

326-330: Prevent division by zero and constrain decimals in changeCurrencyRates()

arbitrationCost(..., _feeToken) calls convertEthToTokenAmount, which divides by rateInEth. Zero rate bricks fee-path calls.

-    function changeCurrencyRates(IERC20 _feeToken, uint64 _rateInEth, uint8 _rateDecimals) external onlyByOwner {
-        currencyRates[_feeToken].rateInEth = _rateInEth;
-        currencyRates[_feeToken].rateDecimals = _rateDecimals;
+    function changeCurrencyRates(IERC20 _feeToken, uint64 _rateInEth, uint8 _rateDecimals) external onlyByOwner {
+        if (_rateInEth == 0) revert UnsuccessfulCall(); // consider dedicated InvalidRate()
+        if (_rateDecimals > 36) revert UnsuccessfulCall(); // sanity cap to avoid overflow
+        currencyRates[_feeToken].rateInEth = _rateInEth;
+        currencyRates[_feeToken].rateDecimals = _rateDecimals;
         emit NewCurrencyRate(_feeToken, _rateInEth, _rateDecimals);
     }

380-389: Fix signature: external functions must use calldata for bytes

This won’t compile as-is.

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

575-591: ERC20 disputes: appealCost unit mismatch

nbVotes is computed using ERC20-denominated totalFeesForJurors divided by ETH feeForJuror. Convert ERC20 fees to ETH first.

-        Round storage round = dispute.rounds[dispute.rounds.length - 1];
-        Court storage court = courts[dispute.courtID];
-        uint256 nbVotes = round.totalFeesForJurors / court.feeForJuror;
+        Round storage round = dispute.rounds[dispute.rounds.length - 1];
+        Court storage court = courts[dispute.courtID];
+        uint256 feePaidInEth = round.feeToken == NATIVE_CURRENCY
+            ? round.totalFeesForJurors
+            : _convertTokenToEthAmount(round.feeToken, round.totalFeesForJurors);
+        uint256 nbVotes = feePaidInEth / court.feeForJuror;

Add helper (outside this hunk):

function _convertTokenToEthAmount(IERC20 _fromToken, uint256 _amount) internal view returns (uint256) {
    CurrencyRate storage r = currencyRates[_fromToken];
    if (r.rateInEth == 0) revert UnsuccessfulCall(); // or dedicated InvalidRate()
    return (_amount * r.rateInEth) / (10 ** r.rateDecimals);
}

598-601: Add override to currentRuling()

Missing override causes a compile error.

-    function currentRuling(uint256 _disputeID) public view returns (uint256 ruling, bool tied, bool overridden) {
+    function currentRuling(uint256 _disputeID) public view override returns (uint256 ruling, bool tied, bool overridden) {

512-532: Make execute() idempotent and revert on failed transfers

Currently transfers the full amount every call, ignores ETH send result, and can desync accounting. Send only the remaining, and revert on failure.

-        uint256 feeReward = round.totalFeesForJurors;
-        round.sumFeeRewardPaid += feeReward;
-        if (round.feeToken == NATIVE_CURRENCY) {
-            // The dispute fees were paid in ETH
-            payable(account).send(feeReward);
-        } else {
-            // The dispute fees were paid in ERC20
-            round.feeToken.safeTransfer(account, feeReward);
-        }
+        uint256 remaining = round.totalFeesForJurors - round.sumFeeRewardPaid;
+        if (remaining == 0) return;
+        round.sumFeeRewardPaid += remaining;
+        if (round.feeToken == NATIVE_CURRENCY) {
+            (bool ok, ) = payable(account).call{value: remaining}("");
+            if (!ok) revert UnsuccessfulCall();
+        } else {
+            round.feeToken.safeTransfer(account, remaining);
+        }
         emit TokenAndETHShift(
             account,
             _disputeID,
             _round,
             ONE_BASIS_POINT,
             ONE_BASIS_POINT,
             int256(0),
-            int256(feeReward),
+            int256(remaining),
             round.feeToken
         );
contracts/test/arbitration/staking.ts (1)

370-371: Use BigInt literals in expectations

Ethers v6 returns bigint for uints.

-      expect(await sortition.getJurorCourtIDs(deployer)).to.be.deep.equal([1, 2]);
+      expect(await sortition.getJurorCourtIDs(deployer)).to.be.deep.equal([1n, 2n]);
contracts/src/gateway/HomeGateway.sol (1)

208-220: Guard rule() against unknown dispute IDs

     function rule(uint256 _disputeID, uint256 _ruling) external override {
         if (msg.sender != address(arbitrator)) revert ArbitratorOnly();
 
         bytes32 disputeHash = disputeIDtoHash[_disputeID];
+        if (disputeHash == bytes32(0)) revert UnknownDisputeID();
         RelayedData memory relayedData = disputeHashtoRelayedData[disputeHash];
@@
-    error AllowanceIncreaseFailed();
+    error AllowanceIncreaseFailed();
+    error UnknownDisputeID();

Also applies to: 236-248

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

144-159: Validate non-empty _voteIDs before indexing _voteIDs[0].

Avoid out-of-bounds access when the caller passes an empty array.

 function castVoteShutter(
@@
-    ) external {
+    ) external {
+        if (_voteIDs.length == 0) revert(); // replace with a descriptive custom error
         Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]];
         address juror = dispute.rounds[dispute.rounds.length - 1].votes[_voteIDs[0]].account;

If you prefer not to add a new error, reuse an existing one used for invalid inputs.

contracts/src/gateway/ForeignGateway.sol (1)

224-227: Avoid .transfer; use call to prevent gas stipend issues.

Paying untrusted relayers via transfer can fail due to 2300-gas limit. Switch to call.

-        payable(dispute.relayer).transfer(amount);
+        (bool ok, ) = payable(dispute.relayer).call{value: amount}("");
+        require(ok, "ETH transfer failed");
contracts/src/arbitration/university/KlerosCoreUniversity.sol (2)

203-219: Initializer: add zero-address guards (owner, token, sortition).

Prevent bricking on deploy.

-    function initialize(
+    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 {
-        owner = _owner;
+        if (
+            _owner == address(0) ||
+            address(_pinakion) == address(0) ||
+            address(_sortitionModuleAddress) == address(0)
+        ) revert ZeroAddress();
+        owner = _owner;
         instructor = _instructor;
         pinakion = _pinakion;
         jurorProsecutionModule = _jurorProsecutionModule;
         sortitionModule = _sortitionModuleAddress;

Also add:

+error ZeroAddress();

in the Errors block.


1056-1059: Division-by-zero risk in convertEthToTokenAmount.

Guard missing when rate is unset (0).

-    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.feePaymentAccepted || rate.rateInEth == 0) revert TokenNotAccepted();
+        return (_amountInEth * 10 ** rate.rateDecimals) / rate.rateInEth;
+    }
♻️ Duplicate comments (44)
contracts/src/rng/RandomizerRNG.sol (1)

68-78: Emit events and guard zero on owner/consumer changes.
Improves observability and safety. (Previously suggested.)

-    function changeOwner(address _owner) external onlyByOwner {
-        owner = _owner;
-    }
+    event OwnerChanged(address indexed previousOwner, address indexed newOwner);
+    function changeOwner(address _owner) external onlyByOwner {
+        require(_owner != address(0), "OwnerZeroAddress");
+        address previousOwner = owner;
+        owner = _owner;
+        emit OwnerChanged(previousOwner, _owner);
+    }
@@
-    function changeConsumer(address _consumer) external onlyByOwner {
-        consumer = _consumer;
-    }
+    event ConsumerChanged(address indexed previousConsumer, address indexed newConsumer);
+    function changeConsumer(address _consumer) external onlyByOwner {
+        require(_consumer != address(0), "ConsumerZeroAddress");
+        address previousConsumer = consumer;
+        consumer = _consumer;
+        emit ConsumerChanged(previousConsumer, _consumer);
+    }
contracts/package.json (1)

160-167: Peer viem ^2.24.1 vs root resolution mismatch (repeat).

Align root resolution with this peer or relax the peer; otherwise expect workspace warnings.

#!/bin/bash
# List all viem versions across the monorepo (deps/dev/peer/resolutions/overrides).
fd -a package.json | while read -r f; do
  echo "== $f"
  jq -r '{
    deps: (.dependencies.viem // empty),
    dev: (.devDependencies.viem // empty),
    peer: (.peerDependencies.viem // empty),
    resolutions: (.resolutions.viem // empty),
    overrides: (.overrides.viem // empty),
    pnpm_overrides: (.pnpm.overrides.viem // empty)
  }' "$f"
done
contracts/src/arbitration/PolicyRegistry.sol (2)

48-52: Disallow zero owner during initialize() (repeat).

Prevent bricking governance on deployment.

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

Also add:

+    error ZeroAddress();

66-70: changeOwner should forbid zero and emit an event (repeat).

Avoid accidental renounce; add explicit event for transparency.

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

Optionally add a separate renounceOwner() if you want to support intentional renounce.

contracts/src/rng/BlockhashRNG.sol (5)

21-23: Unbounded storage growth in randomNumbers mapping

Every fulfilled request writes a new mapping slot keyed by expectedTimestamp; no cleanup path. If historical randomness isn’t required, switch to a single-slot cache.

-    mapping(uint256 timestamp => uint256 number) public randomNumbers;
+    uint256 public lastReadyTimestamp;
+    uint256 public lastRandomNumber;
+    uint256 public lastRandomBlock;

Follow-on (inside receiveRandomness):

-        randomNumber = randomNumbers[expectedTimestamp];
+        if (lastReadyTimestamp == expectedTimestamp && lastRandomNumber != 0) {
+            return lastRandomNumber;
+        }
@@
-        randomNumbers[expectedTimestamp] = randomNumber;
+        lastReadyTimestamp = expectedTimestamp;
+        lastRandomNumber = randomNumber;
+        lastRandomBlock = targetBlock;

13-13: Fix filename casing mismatch (BlockHashRNG vs BlockhashRNG.sol)

On case-sensitive systems, mismatched casing breaks imports/tooling. Rename file to BlockHashRNG.sol and update all references.

#!/bin/bash
# Verify and list references to both casings.
rg -nP -C1 '(BlockhashRNG\.sol|BlockHashRNG\.sol|\bBlockhashRNG\b|\bBlockHashRNG\b)' contracts scripts web

28-36: Compilation blocker: custom errors used but not declared

Modifiers revert with OwnerOnly()/ConsumerOnly() but those errors aren’t defined. Declare them (and while at it, predeclare InvalidOwner/InvalidConsumer for governance hardening).

 contract BlockHashRNG is IRNG {
+    // Errors
+    error OwnerOnly();
+    error ConsumerOnly();
+    error InvalidOwner();
+    error InvalidConsumer();

72-75: Fix caller-timing bias: commit the target block at request time

receiveRandomness() derives from blockhash(block.number - 1) at call time, letting the first post-ready caller influence the block used. Pre-commit the target block at request and always derive from it; cache the block number used. Return 0 if the hash is unavailable (>256 blocks).

-    uint256 public requestTimestamp; // Timestamp of the current request
-    mapping(uint256 timestamp => uint256 number) public randomNumbers; // randomNumbers[timestamp] is the random number for this timestamp, 0 otherwise.
+    uint256 public requestTimestamp; // Timestamp of the current request.
+    uint256 public requestBlock; // Block number at request time to prevent timing manipulation.
+    mapping(uint256 timestamp => uint256 number) public randomNumbers; // randomNumbers[timestamp] is the random number for this window, 0 otherwise.
+    mapping(uint256 timestamp => uint256 blockNumber) public randomBlocks; // Block used to derive randomness for the window.

@@
-    function requestRandomness() external override onlyByConsumer {
-        requestTimestamp = block.timestamp;
-    }
+    function requestRandomness() external override onlyByConsumer {
+        requestTimestamp = block.timestamp;
+        requestBlock = block.number;
+    }

@@
-        // Use last block hash for randomness
-        randomNumber = uint256(blockhash(block.number - 1));
-        if (randomNumber != 0) {
-            randomNumbers[expectedTimestamp] = randomNumber;
-        }
-        return randomNumber;
+        // Derive from the pre-committed block to remove timing bias.
+        uint256 targetBlock = requestBlock + 1;
+        randomNumber = uint256(blockhash(targetBlock));
+        if (randomNumber == 0) {
+            // Not yet mined or hash aged out (>256 blocks). Let caller retry or re-request.
+            return 0;
+        }
+        randomNumbers[expectedTimestamp] = randomNumber;
+        randomBlocks[expectedTimestamp] = targetBlock;
+        return randomNumber;

Also applies to: 96-103, 21-23


42-50: Harden governance: zero-address checks + role-change events

Protect against bricking the contract by setting roles to address(0), and emit events on changes.

+    // Events
+    event OwnerChanged(address indexed previousOwner, address indexed newOwner);
+    event ConsumerChanged(address indexed previousConsumer, address indexed newConsumer);

@@
-    constructor(address _owner, address _consumer, uint256 _lookaheadTime) {
-        owner = _owner;
-        consumer = _consumer;
+    constructor(address _owner, address _consumer, uint256 _lookaheadTime) {
+        if (_owner == address(0)) revert InvalidOwner();
+        if (_consumer == address(0)) revert InvalidConsumer();
+        owner = _owner;
+        consumer = _consumer;
         lookaheadTime = _lookaheadTime;
     }

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

@@
-    function changeConsumer(address _consumer) external onlyByOwner {
-        consumer = _consumer;
-    }
+    function changeConsumer(address _consumer) external onlyByOwner {
+        if (_consumer == address(0)) revert InvalidConsumer();
+        emit ConsumerChanged(consumer, _consumer);
+        consumer = _consumer;
+    }

Also applies to: 56-66

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

84-99: Fix NatSpec: “reward” → “penalty” for penalty accessor

The @return line still says “reward”.

-    /// @return pnkCoherence The degree of coherence in basis points for the dispute PNK reward.
+    /// @return pnkCoherence The degree of coherence in basis points for the dispute PNK penalty.
contracts/src/arbitration/KlerosGovernor.sol (3)

223-236: Bounds-check while splitting _data

readingPosition + _dataSize[i] can exceed _data.length; revert with typed error before copying; also assert full consumption after the loop.

         uint256 readingPosition;
         for (uint256 i = 0; i < _target.length; i++) {
-            bytes memory readData = new bytes(_dataSize[i]);
+            if (readingPosition + _dataSize[i] > _data.length) revert WrongInputTargetAndDatasize();
+            bytes memory readData = new bytes(_dataSize[i]);
@@
             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();

84-86: onlyByOwner currently unusable (checks address(this))

Guard blocks all external governance calls. Add owner storage and compare against it.

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

348-356: Eliminate reentrancy window around external call

Set executed before calling target; roll back if call fails to preserve current semantics.

-            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 first
+                transaction.executed = true;
+                // Interaction
+                (bool callResult, ) = transaction.target.call{value: transaction.value}(transaction.data);
+                if (!callResult) {
+                    // Restore effect on failure to keep non-reverting behavior.
+                    transaction.executed = false;
+                }
+            }
contracts/src/arbitration/devtools/KlerosCoreRuler.sol (2)

175-181: Guard zero owner and zero token in initialize()

Prevent bricking and division-by-zero later via unset rate data.

-    function initialize(address _owner, IERC20 _pinakion, uint256[4] memory _courtParameters) external initializer {
-        owner = _owner;
-        pinakion = _pinakion;
+    function initialize(address _owner, IERC20 _pinakion, uint256[4] memory _courtParameters) external initializer {
+        if (_owner == address(0)) revert OwnerOnly(); // consider a dedicated ZeroAddress()
+        if (address(_pinakion) == address(0)) revert OwnerOnly(); // or a dedicated ZeroAddress()
+        owner = _owner;
+        pinakion = _pinakion;

230-234: Emit OwnerChanged and reject zero address in changeOwner()

Improves observability and avoids accidental lockout.

-    function changeOwner(address payable _owner) external onlyByOwner {
-        owner = _owner;
-    }
+    event OwnerChanged(address indexed _oldOwner, address indexed _newOwner);
+    function changeOwner(address payable _owner) external onlyByOwner {
+        if (_owner == address(0)) revert OwnerOnly(); // or ZeroAddress()
+        address old = owner;
+        owner = _owner;
+        emit OwnerChanged(old, _owner);
+    }
contracts/CHANGELOG.md (3)

27-27: Fix constant name and wording

Use the correct constant and clearer phrasing.

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

30-30: Clarify pragma widening scope and exact range

“interfaces only” is misleading; specify the actual range and scope.

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

171-172: Avoid broken link for unreleased tag

Comment out until published.

-[0.13.0]: https://github.com/kleros/kleros-v2/releases/tag/@kleros%2Fkleros-v2-contracts@0.13.0
+<!-- [0.13.0]: (add after publishing) -->
contracts/src/arbitration/DisputeTemplateRegistry.sol (1)

41-45: Reject zero owner in initialize()

Prevents accidental lockout.

-    function initialize(address _owner) external initializer {
-        owner = _owner;
-    }
+    function initialize(address _owner) external initializer {
+        if (_owner == address(0)) revert OwnerOnly(); // or ZeroAddress()
+        owner = _owner;
+    }
contracts/test/foundry/KlerosCore_Execution.t.sol (1)

484-491: Leftover PNK withdrawal authorization: fix confirmed

Negative test on Core-only function plus successful withdrawal via sortitionModule addresses the earlier concern.

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

147-159: Critical: unknown arbitrator dispute IDs currently target disputes[0]

Guard missing + zero-sentinel mapping allows unknown IDs to resolve to index 0. Use 1-based sentinel mapping and revert on unknown.

Apply:

@@
-        disputeID = arbitrator.createDispute{value: msg.value}(numberOfRulingOptions, arbitratorExtraData);
-        externalIDtoLocalID[disputeID] = localDisputeID;
+        disputeID = arbitrator.createDispute{value: msg.value}(numberOfRulingOptions, arbitratorExtraData);
+        externalIDtoLocalID[disputeID] = localDisputeID + 1;
@@
-        disputeID = arbitrator.createDispute(numberOfRulingOptions, arbitratorExtraData, weth, _feeInWeth);
-        externalIDtoLocalID[disputeID] = localDisputeID;
+        disputeID = arbitrator.createDispute(numberOfRulingOptions, arbitratorExtraData, weth, _feeInWeth);
+        externalIDtoLocalID[disputeID] = localDisputeID + 1;
@@
-    function rule(uint256 _arbitratorDisputeID, uint256 _ruling) external override {
-        uint256 localDisputeID = externalIDtoLocalID[_arbitratorDisputeID];
-        DisputeStruct storage dispute = disputes[localDisputeID];
+    function rule(uint256 _arbitratorDisputeID, uint256 _ruling) external override {
+        uint256 localDisputeIDPlusOne = externalIDtoLocalID[_arbitratorDisputeID];
+        if (localDisputeIDPlusOne == 0) revert UnknownDisputeID();
+        uint256 localDisputeID = localDisputeIDPlusOne - 1;
+        DisputeStruct storage dispute = disputes[localDisputeID];
@@
-    error DisputeAlreadyRuled();
+    error DisputeAlreadyRuled();
+    error UnknownDisputeID();

Also applies to: 118-123, 140-145, 165-171

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

61-69: Fix constructor NatSpec (copy/paste from PolicyRegistry)

-    /// @notice Constructs the `PolicyRegistry` contract.
+    /// @notice Initializes the HomeGateway contract.
     /// @param _owner The owner's address.
     /// @param _arbitrator The address of the arbitrator.
     /// @param _veaInbox The address of the vea inbox.
     /// @param _foreignChainID The ID of the foreign chain.
     /// @param _foreignGateway The address of the foreign gateway.
     /// @param _feeToken The address of the fee token.

168-169: Add missing override on ERC20 overload

-function relayCreateDispute(RelayCreateDisputeParams memory _params, uint256 _feeAmount) external {
+function relayCreateDispute(RelayCreateDisputeParams memory _params, uint256 _feeAmount) external override {

146-155: Close reentrancy window (native-fee path): pre-mark before external calls

Set relayer (and record cost) before calling arbitrator to prevent duplicate-relay on reentrancy.

         RelayedData storage relayedData = disputeHashtoRelayedData[disputeHash];
         if (relayedData.relayer != address(0)) revert DisputeAlreadyRelayed();
+        relayedData.relayer = msg.sender;
+        relayedData.arbitrationCost = msg.value;
 
-        uint256 disputeID = arbitrator.createDispute{value: msg.value}(_params.choices, _params.extraData);
+        uint256 disputeID = arbitrator.createDispute{value: msg.value}(_params.choices, _params.extraData);
         disputeIDtoHash[disputeID] = disputeHash;
         disputeHashtoID[disputeHash] = disputeID;
-        relayedData.relayer = msg.sender;

184-196: Close reentrancy window (ERC20-fee path) and persist cost

         RelayedData storage relayedData = disputeHashtoRelayedData[disputeHash];
         if (relayedData.relayer != address(0)) revert DisputeAlreadyRelayed();
+        relayedData.relayer = msg.sender;
+        relayedData.arbitrationCost = _feeAmount;
 
         if (!feeToken.safeTransferFrom(msg.sender, address(this), _feeAmount)) revert TransferFailed();
         if (!feeToken.increaseAllowance(address(arbitrator), _feeAmount)) revert AllowanceIncreaseFailed();
@@
-        relayedData.relayer = msg.sender;
contracts/src/libraries/SortitionTrees.sol (1)

68-85: draw(): bounds-check children and guarantee progress to avoid OOB read/infinite loop.

Current code indexes children unconditionally; if a child index is past nodes.length or all children have zero weight, it can read OOB or spin forever.

Apply:

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

And declare the error:

 error TreeAlreadyExists();
 error KMustBeGreaterThanOne();
+error InvalidTreeState();
contracts/src/arbitration/dispute-kits/DisputeKitGatedShutter.sol (2)

153-159: Do not rely on transient state across external calls; pass explicit context.

callerIsJuror spans the _castVote external call, creating reentrancy/leakage risk and brittle coupling. Compute juror context and pass it to hashing/expected-hash helpers.

Minimal refactor sketch:

-        callerIsJuror = juror == msg.sender;
-        // `_castVote()` ensures that all the `_voteIDs` do belong to `juror`
-        _castVote(_coreDisputeID, _voteIDs, _choice, _salt, _justification, juror);
-        callerIsJuror = false;
+        bool isJuror = (juror == msg.sender);
+        _castVoteWithContext(_coreDisputeID, _voteIDs, _choice, _salt, _justification, juror, isJuror);

Introduce context-aware helpers and make hashVote/_getExpectedVoteHash use the passed flag instead of transient state:

-    function hashVote(...) public view override returns (bytes32) { ... callerIsJuror ... }
+    function _hashVoteWithContext(uint256 _choice, uint256 _salt, string memory _justification, bool isJuror)
+        internal pure returns (bytes32)
+    {
+        if (isJuror) return keccak256(abi.encodePacked(_choice, _salt));
+        bytes32 justificationHash = keccak256(bytes(_justification));
+        return keccak256(abi.encode(_choice, _salt, justificationHash));
+    }

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

Then in _castVoteWithContext replace calls to hashVote/_getExpectedVoteHash with the context-aware variants.


171-183: Make juror/non-juror hashing explicit; avoid dependence on transient state.

If you keep hashVote public, default to non-juror semantics and expose a context-aware internal as suggested above. Current approach ties behavior to mutable state.

contracts/test/arbitration/staking-neo.ts (1)

301-304: Fix Chai chain: use to.be.revertedWithCustomError.

Hardhat/Waffle matchers require .to.be.revertedWithCustomError(...).

-          await expect(sortition.executeDelayedStakes(10)).to.revertedWithCustomError(
+          await expect(sortition.executeDelayedStakes(10)).to.be.revertedWithCustomError(
             sortition,
             "NoDelayedStakeToExecute"
           );

Apply the same change at both locations.

Also applies to: 368-371

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

92-104: Guard against unknown dispute IDs (safe lookup) and avoid accidental index 0 usage.

Unknown _arbitratorDisputeID currently maps to 0 and will index disputes[0]. Add an explicit existence check and store localID+1 in the mapping to disambiguate “unset” from “index 0”.

Apply:

 function rule(uint256 _arbitratorDisputeID, uint256 _ruling) external override {
-        uint256 localDisputeID = arbitratorDisputeIDToLocalID[_arbitratorDisputeID];
-        DisputeStruct storage dispute = disputes[localDisputeID];
-        if (msg.sender != address(arbitrator)) revert ArbitratorOnly();
+        if (msg.sender != address(arbitrator)) revert ArbitratorOnly();
+        uint256 localDisputeIDPlusOne = arbitratorDisputeIDToLocalID[_arbitratorDisputeID];
+        if (localDisputeIDPlusOne == 0) revert UnknownDisputeID();
+        uint256 localDisputeID = localDisputeIDPlusOne - 1;
+        DisputeStruct storage dispute = disputes[localDisputeID];
         if (_ruling > dispute.numberOfRulingOptions) revert RulingOutOfBounds();
         if (dispute.isRuled) revert DisputeAlreadyRuled();
-        arbitratorDisputeIDToLocalID[arbitratorDisputeID] = localDisputeID;
+        arbitratorDisputeIDToLocalID[arbitratorDisputeID] = localDisputeID + 1;
 error OwnerOnly();
 error ArbitratorOnly();
+error UnknownDisputeID();
 error RulingOutOfBounds();
 error DisputeAlreadyRuled();
 error ShouldBeAtLeastTwoRulingOptions();

Run to find any external reads of arbitratorDisputeIDToLocalID that assume raw index semantics:

#!/bin/bash
rg -n -C2 '\barbitratorDisputeIDToLocalID\b'

Also applies to: 128-131, 137-142

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

190-212: Fix penalty application to preserve invariants and return remainder.

Clamp applied penalty to min(_penalty, total staked, stake in this court) and return leftover for upstream cascading.

-    ) external override onlyByCore returns (uint256 pnkBalance, uint256 newCourtStake, uint256 availablePenalty) {
-        Juror storage juror = jurors[_account];
-        availablePenalty = _penalty;
-        newCourtStake = _stakeOf(_account, _courtID);
-        if (juror.stakedPnk < _penalty) {
-            availablePenalty = juror.stakedPnk;
-        }
-
-        if (availablePenalty == 0) return (juror.stakedPnk, newCourtStake, 0); // No penalty to apply.
-
-        uint256 currentStake = _stakeOf(_account, _courtID);
-        uint256 newStake = 0;
-        if (currentStake >= availablePenalty) {
-            newStake = currentStake - availablePenalty;
-        }
-        _setStake(_account, _courtID, 0, availablePenalty, newStake);
-        pnkBalance = juror.stakedPnk; // updated by _setStake()
-        newCourtStake = _stakeOf(_account, _courtID); // updated by _setStake()
-    }
+    ) external override onlyByCore returns (uint256 pnkBalance, uint256 newCourtStake, uint256 availablePenalty) {
+        Juror storage juror = jurors[_account];
+        uint256 currentStake = _stakeOf(_account, _courtID);
+        uint256 maxApplicable = _penalty <= juror.stakedPnk ? _penalty : juror.stakedPnk;
+        uint256 applied = currentStake <= maxApplicable ? currentStake : maxApplicable;
+        if (applied == 0) return (juror.stakedPnk, currentStake, maxApplicable);
+        uint256 newStake = currentStake - applied;
+        _setStake(_account, _courtID, 0, applied, newStake);
+        pnkBalance = juror.stakedPnk;
+        newCourtStake = _stakeOf(_account, _courtID);
+        availablePenalty = maxApplicable - applied;
+    }
contracts/src/arbitration/dispute-kits/DisputeKitShutter.sol (2)

64-71: Initialize: validate nonzero owner.

Mirror base kits; avoid zero-owner.

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

Add error at the end:

 error EmptyRecoveryCommit();
+error ZeroAddress();

128-143: Check non-empty vote set and scope callerIsJuror via save/restore.

Prevents OOB on _voteIDs[0] and avoids leaking juror-context across reentrancy/nested calls.

 function castVoteShutter(
@@
 ) external {
-    Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]];
+    require(_voteIDs.length > 0, "Empty voteIDs");
+    Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]];
     address juror = dispute.rounds[dispute.rounds.length - 1].votes[_voteIDs[0]].account;
-
-    callerIsJuror = juror == msg.sender;
+    bool _prev = callerIsJuror;
+    callerIsJuror = (juror == msg.sender);
@@
-    callerIsJuror = false;
+    callerIsJuror = _prev;
 }
contracts/src/arbitration/university/KlerosCoreUniversity.sol (4)

291-293: Setters: add zero-address guards (pinakion, prosecution module, sortition).

Setting these to zero can deadlock operations.

-    function changePinakion(IERC20 _pinakion) external onlyByOwner {
-        pinakion = _pinakion;
+    function changePinakion(IERC20 _pinakion) external onlyByOwner {
+        if (address(_pinakion) == address(0)) revert ZeroAddress();
+        pinakion = _pinakion;
     }
-    function changeJurorProsecutionModule(address _jurorProsecutionModule) external onlyByOwner {
-        jurorProsecutionModule = _jurorProsecutionModule;
+    function changeJurorProsecutionModule(address _jurorProsecutionModule) external onlyByOwner {
+        if (_jurorProsecutionModule == address(0)) revert ZeroAddress();
+        jurorProsecutionModule = _jurorProsecutionModule;
     }
-    function changeSortitionModule(ISortitionModuleUniversity _sortitionModule) external onlyByOwner {
-        sortitionModule = _sortitionModule;
+    function changeSortitionModule(ISortitionModuleUniversity _sortitionModule) external onlyByOwner {
+        if (address(_sortitionModule) == address(0)) revert ZeroAddress();
+        sortitionModule = _sortitionModule;
     }

Also applies to: 297-299, 304-306


804-814: ETH transfer uses .send without checking success. Replace with call and revert on failure.

-            if (round.feeToken == NATIVE_CURRENCY) {
-                // The dispute fees were paid in ETH
-                payable(owner).send(round.totalFeesForJurors);
-            } else {
+            if (round.feeToken == NATIVE_CURRENCY) {
+                (bool okFee, ) = payable(owner).call{value: round.totalFeesForJurors}("");
+                if (!okFee) revert NativeTransferFailed();
+            } else {
                 // The dispute fees were paid in ERC20
                 round.feeToken.safeTransfer(owner, round.totalFeesForJurors);
             }
             pinakion.safeTransfer(owner, _params.pnkPenaltiesInRound);

Add in Errors:

+error NativeTransferFailed();

862-868: ETH reward to juror: .send can silently fail. Switch to call.

-        if (round.feeToken == NATIVE_CURRENCY) {
-            // The dispute fees were paid in ETH
-            payable(account).send(feeReward);
-        } else {
+        if (round.feeToken == NATIVE_CURRENCY) {
+            (bool ok, ) = payable(account).call{value: feeReward}("");
+            if (!ok) revert NativeTransferFailed();
+        } else {
             // The dispute fees were paid in ERC20
             round.feeToken.safeTransfer(account, feeReward);
         }

895-901: Leftover ETH to owner: replace .send with checked call.

-                    if (round.feeToken == NATIVE_CURRENCY) {
-                        // The dispute fees were paid in ETH
-                        payable(owner).send(leftoverFeeReward);
-                    } else {
+                    if (round.feeToken == NATIVE_CURRENCY) {
+                        (bool ok2, ) = payable(owner).call{value: leftoverFeeReward}("");
+                        if (!ok2) revert NativeTransferFailed();
+                    } else {
                         // The dispute fees were paid in ERC20
                         round.feeToken.safeTransfer(owner, leftoverFeeReward);
                     }
contracts/src/arbitration/SortitionModule.sol (3)

154-158: changeOwner: guard zero and emit OwnerChanged.

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

Add event:

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

(Place in the Events section.)


425-429: Clamp unlock to avoid underflow.

-    function unlockStake(address _account, uint256 _relativeAmount) external override onlyByCore {
-        Juror storage juror = jurors[_account];
-        juror.lockedPnk -= _relativeAmount;
+    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);

173-179: changeRandomNumberGenerator: guard zero address.

-    function changeRandomNumberGenerator(IRNG _rng) external onlyByOwner {
-        rng = _rng;
+    function changeRandomNumberGenerator(IRNG _rng) external onlyByOwner {
+        if (address(_rng) == address(0)) revert ZeroAddressOwner();
+        rng = _rng;
         if (phase == Phase.generating) {
             rng.requestRandomness();
         }
     }
contracts/src/arbitration/KlerosCore.sol (2)

371-372: Fix override signature: remove view from _authorizeUpgrade.

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

1196-1198: Division-by-zero risk in convertEthToTokenAmount.

Guard when rate is unset or token not accepted.

-    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.feePaymentAccepted || rate.rateInEth == 0) revert TokenNotAccepted();
+        return (_amountInEth * 10 ** rate.rateDecimals) / rate.rateInEth;
+    }
contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol (2)

141-144: Guard against non-active disputes in notJumped modifier.

Accessing disputes[coreDisputeIDToLocal[_coreDisputeID]] without verifying active status relies on later checks to catch inactive IDs. Add an active-dispute guard here to prevent accidental reads of an uninitialized local dispute (id 0).


523-543: Fix NatSpec: "reward" → "penalty" in penalty accessor.

The NatSpec documentation for getDegreeOfCoherencePenalty incorrectly refers to "reward" instead of "penalty".

Apply this diff to fix the documentation:

-/// @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.

Comment on lines +25 to 37
/// @notice Initializer.
/// @param _owner The owner's address.
/// @param _core The KlerosCore arbitrator.
/// @param _wNative The wrapped native token address, typically wETH.
function initialize(address _governor, KlerosCore _core, address _wNative) external reinitializer(1) {
__DisputeKitClassicBase_initialize(_governor, _core, _wNative);
}

function reinitialize(address _wNative) external reinitializer(9) {
wNative = _wNative;
/// @param _jumpDisputeKitID The ID of the dispute kit to switch to after the court jump.
function initialize(
address _owner,
KlerosCore _core,
address _wNative,
uint256 _jumpDisputeKitID
) external initializer {
__DisputeKitClassicBase_initialize(_owner, _core, _wNative, _jumpDisputeKitID);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Validate inputs or confirm base initializer does

Ensure __DisputeKitClassicBase_initialize() rejects zero owner and invalid _wNative. If already enforced in base, ignore.


🏁 Script executed:

#!/bin/bash
rg -nP '__DisputeKitClassicBase_initialize\(' -n -C2
rg -nP 'function\s+__DisputeKitClassicBase_initialize' contracts -n -C5

Length of output: 280366


Reject zero/invalid inputs in DisputeKitClassicBase initializer

The base initializer (contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol :: __DisputeKitClassicBase_initialize) assigns governor/core/wNative without checks — add validation and revert if _governor == address(0) or address(_core) == address(0) or _wNative == address(0).
Location: contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol (function __DisputeKitClassicBase_initialize).

🤖 Prompt for AI Agents
In contracts/src/arbitration/dispute-kits/DisputeKitClassic.sol around lines 25
to 37, the call to __DisputeKitClassicBase_initialize forwards parameters that
the base initializer currently accepts without validation; update the base
initializer in contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol
(function __DisputeKitClassicBase_initialize) to validate inputs and revert on
invalid values: add require checks to ensure the governor address is not
address(0), the core argument (address(_core)) is not address(0), and _wNative
is not address(0), and use clear revert messages (e.g., "Governor zero", "Core
zero", "wNative zero") so initialization fails early on bad inputs.

Comment on lines +141 to 151
/// @notice Change the proportion of arbitration fees that must be paid as fee stake by parties when there is no winner or loser (e.g. when the arbitrator refused to rule).
/// @param _initialDepositMultiplier Multiplier of arbitration fees that must be paid as fee stake. In basis points.
function changeInitialDepositMultiplier(uint256 _initialDepositMultiplier) external onlyGovernor {
function changeInitialDepositMultiplier(uint256 _initialDepositMultiplier) external onlyOwner {
initialDepositMultiplier = _initialDepositMultiplier;
}

/// @dev Change the proportion of arbitration fees that must be paid as fee stake by the winner of the previous round.
/// @notice Change the proportion of arbitration fees that must be paid as fee stake by the winner of the previous round.
/// @param _totalCostMultiplier Multiplier of arbitration fees that must be paid as fee stake. In basis points.
function changeTotalCostMultiplier(uint256 _totalCostMultiplier) external onlyGovernor {
function changeTotalCostMultiplier(uint256 _totalCostMultiplier) external onlyOwner {
totalCostMultiplier = _totalCostMultiplier;
}
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

Guard rails for governance setters to prevent funding deadlocks and nonsensical configs.

Without bounds, settings can cause reverts later (see Line 260 underflow scenario) or create zero-timeout markets.

@@
-    function changeInitialDepositMultiplier(uint256 _initialDepositMultiplier) external onlyOwner {
-        initialDepositMultiplier = _initialDepositMultiplier;
+    function changeInitialDepositMultiplier(uint256 _initialDepositMultiplier) external onlyOwner {
+        require(_initialDepositMultiplier <= MULTIPLIER_DIVISOR, "initMult>100%");
+        initialDepositMultiplier = _initialDepositMultiplier;
     }
@@
-    function changeTotalCostMultiplier(uint256 _totalCostMultiplier) external onlyOwner {
-        totalCostMultiplier = _totalCostMultiplier;
+    function changeTotalCostMultiplier(uint256 _totalCostMultiplier) external onlyOwner {
+        // Ensure both sides reaching 'totalCost' covers at least one arbitrationCost.
+        require(_totalCostMultiplier >= MULTIPLIER_DIVISOR / 2, "totalCostMult<50%");
+        totalCostMultiplier = _totalCostMultiplier;
     }
@@
-    function changeBondTimeout(uint256 _bondTimeout) external onlyOwner {
-        bondTimeout = _bondTimeout;
+    function changeBondTimeout(uint256 _bondTimeout) external onlyOwner {
+        require(_bondTimeout > 0, "timeout=0");
+        bondTimeout = _bondTimeout;
     }

Also applies to: 153-158

Comment on lines +344 to 351
/// @notice Withdraws contributions of moderations. Reimburses contributions if the appeal was not fully funded.
///
/// @dev If the appeal was fully funded, sends the fee stake rewards and reimbursements proportional to the contributions made to the winner of a dispute.
/// Optimization opportunity: use `bytes calldata args` and compress _evidenceID and _moderationID (only for optimistic rollups).
///
/// @param _beneficiary The address that made contributions.
/// @param _evidenceID The ID of the associated evidence submission.
/// @param _moderationID The ID of the moderatino occurence.
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

Withdrawal can permanently strand user funds on send failure. Use call + require.

Currently contributions are zeroed and then send is attempted; on failure, the user cannot re-attempt withdrawal and funds remain stuck.

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

State updates occur before the external call; if the transfer fails, the revert restores state, allowing retry with an EOA or different beneficiary.

Also applies to: 380-381

Comment on lines +212 to 216
/// @notice Checks if the juror has any leftover PNK in the contract.
/// @param _juror The address of the juror.
/// @return Whether the juror has leftover PNK.
function getJurorLeftoverPNK(address _juror) external view returns (uint256);

function createDisputeHook(uint256 _disputeID, uint256 _roundID) external;

function postDrawHook(uint256 _disputeID, uint256 _roundID) external;

function withdrawLeftoverPNK(address _account) 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

Fix return description for getJurorLeftoverPNK()

It returns an amount (uint256), not a boolean.

-    /// @return Whether the juror has leftover PNK.
+    /// @return The amount of leftover PNK for the juror.
📝 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 Checks if the juror has any leftover PNK in the contract.
/// @param _juror The address of the juror.
/// @return Whether the juror has leftover PNK.
function getJurorLeftoverPNK(address _juror) external view returns (uint256);
function createDisputeHook(uint256 _disputeID, uint256 _roundID) external;
function postDrawHook(uint256 _disputeID, uint256 _roundID) external;
function withdrawLeftoverPNK(address _account) external;
}
/// @notice Checks if the juror has any leftover PNK in the contract.
/// @param _juror The address of the juror.
/// @return The amount of leftover PNK for the juror.
function getJurorLeftoverPNK(address _juror) external view returns (uint256);
}
🤖 Prompt for AI Agents
In contracts/src/arbitration/interfaces/ISortitionModule.sol around lines 212 to
216, the @return NatSpec for getJurorLeftoverPNK() incorrectly describes a
boolean; update the @return description to indicate it returns the leftover PNK
amount (uint256) for the juror, e.g., "The amount of leftover PNK for the juror
(in wei)."

Comment on lines +398 to +401
function changeOwner(address payable _owner) external onlyByOwner {
owner = _owner;
}

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Zero-address guards for governance setters.

Avoid breaking access-control and core wiring.

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

Add errors if missing:

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

Also applies to: 410-412, 423-425

🤖 Prompt for AI Agents
In contracts/src/arbitration/KlerosCore.sol around lines 398-401 (and similarly
for 410-412 and 423-425), the governance setter functions lack guards against
the zero address; add a check that the provided address is not address(0) and
revert with a clear error (either a custom error like ZeroAddress() or a require
with message) before assigning the new value, keeping the existing onlyByOwner
modifier intact so the function both enforces caller authority and protects
against accidentally wiring governance or owner to the zero address.

Comment on lines +121 to 126
/// @notice Changes the home gateway.
/// @param _homeGateway The address of the new home gateway.
function changeHomeGateway(address _homeGateway) external {
require(governor == msg.sender, "Access not allowed: Governor only.");
if (owner != msg.sender) revert OwnerOnly();
homeGateway = _homeGateway;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Enforce owner access for changeHomeGateway (consistency).

Also validate nonzero address.

-function changeHomeGateway(address _homeGateway) external {
-    if (owner != msg.sender) revert OwnerOnly();
-    homeGateway = _homeGateway;
-}
+function changeHomeGateway(address _homeGateway) external onlyByOwner {
+    if (_homeGateway == address(0)) revert OwnerOnly();
+    homeGateway = _homeGateway;
+}

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

🤖 Prompt for AI Agents
In contracts/src/gateway/ForeignGateway.sol around lines 121–126, the
changeHomeGateway function should enforce owner-only access consistently and
reject the zero address; replace the inline owner check with the contract's
standard onlyOwner modifier (or call the same OwnerOnly check path) and add a
validation that _homeGateway != address(0) that reverts with a suitable error
(e.g., InvalidAddress or ZeroAddress) before assigning homeGateway.

Comment on lines +53 to 62
/// @notice Constructor
/// @param _owner The Owner of the contract.
/// @param _consumer The address that can request random numbers.
/// @param _randomizer The Randomizer.ai oracle contract.
constructor(address _owner, address _consumer, IRandomizer _randomizer) {
owner = _owner;
consumer = _consumer;
randomizer = _randomizer;
callbackGasLimit = 50000;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Validate constructor addresses to prevent bricking.
Zero owner disables admin; zero consumer blocks requests.

 constructor(address _owner, address _consumer, IRandomizer _randomizer) {
-        owner = _owner;
-        consumer = _consumer;
+        require(_owner != address(0), "OwnerZeroAddress");
+        require(_consumer != address(0), "ConsumerZeroAddress");
+        owner = _owner;
+        consumer = _consumer;
         randomizer = _randomizer;
         callbackGasLimit = 50000;
 }
📝 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 Constructor
/// @param _owner The Owner of the contract.
/// @param _consumer The address that can request random numbers.
/// @param _randomizer The Randomizer.ai oracle contract.
constructor(address _owner, address _consumer, IRandomizer _randomizer) {
owner = _owner;
consumer = _consumer;
randomizer = _randomizer;
callbackGasLimit = 50000;
}
/// @notice Constructor
/// @param _owner The Owner of the contract.
/// @param _consumer The address that can request random numbers.
/// @param _randomizer The Randomizer.ai oracle contract.
constructor(address _owner, address _consumer, IRandomizer _randomizer) {
require(_owner != address(0), "OwnerZeroAddress");
require(_consumer != address(0), "ConsumerZeroAddress");
owner = _owner;
consumer = _consumer;
randomizer = _randomizer;
callbackGasLimit = 50000;
}
🤖 Prompt for AI Agents
In contracts/src/rng/RandomizerRNG.sol around lines 53 to 62, the constructor
doesn't validate input addresses which can brick the contract if owner or
consumer is zero (and possibly randomizer); add require checks at the start of
the constructor to ensure _owner != address(0), _consumer != address(0) and
_randomizer != IRandomizer(address(0)) with clear revert messages, then assign
owner, consumer and randomizer and set callbackGasLimit as before.

Comment on lines +86 to 91
/// @notice Change the Randomizer address.
/// @param _randomizer the new Randomizer address.
function setRandomizer(address _randomizer) external onlyByGovernor {
function setRandomizer(address _randomizer) external onlyByOwner {
randomizer = IRandomizer(_randomizer);
}

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Validate and log Randomizer address changes.
Avoid setting to EOA/zero; emit event.

+    event RandomizerChanged(address indexed previousRandomizer, address indexed newRandomizer);
-    function setRandomizer(address _randomizer) external onlyByOwner {
-        randomizer = IRandomizer(_randomizer);
-    }
+    function setRandomizer(address _randomizer) external onlyByOwner {
+        require(_randomizer != address(0), "RandomizerZeroAddress");
+        require(_randomizer.code.length > 0, "RandomizerNotContract");
+        address prev = address(randomizer);
+        randomizer = IRandomizer(_randomizer);
+        emit RandomizerChanged(prev, _randomizer);
+    }
📝 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 Randomizer address.
/// @param _randomizer the new Randomizer address.
function setRandomizer(address _randomizer) external onlyByGovernor {
function setRandomizer(address _randomizer) external onlyByOwner {
randomizer = IRandomizer(_randomizer);
}
/// @notice Change the Randomizer address.
/// @param _randomizer the new Randomizer address.
event RandomizerChanged(address indexed previousRandomizer, address indexed newRandomizer);
function setRandomizer(address _randomizer) external onlyByOwner {
require(_randomizer != address(0), "RandomizerZeroAddress");
require(_randomizer.code.length > 0, "RandomizerNotContract");
address prev = address(randomizer);
randomizer = IRandomizer(_randomizer);
emit RandomizerChanged(prev, _randomizer);
}
🤖 Prompt for AI Agents
In contracts/src/rng/RandomizerRNG.sol around lines 86 to 91, the setRandomizer
function should validate the new address and emit an event: add checks
require(_randomizer != address(0)), require(_randomizer != address(randomizer))
to prevent zero and redundant updates, and require(_randomizer.code.length > 0)
(or use extcodesize) to ensure it's a contract not an EOA; capture the old
address in a local variable, set randomizer = IRandomizer(_randomizer), and emit
a new event RandomizerChanged(address indexed oldRandomizer, address indexed
newRandomizer) with the old and new addresses.

Comment on lines +8 to 17
/// @notice Batch send transactions, all the calls must succeed.
/// @param targets The targets of the calls.
/// @param values The values of the calls.
/// @param datas The datas of the calls.
function batchSend(address[] memory targets, uint256[] memory values, bytes[] memory datas) public payable {
for (uint256 i = 0; i < targets.length; i++) {
(bool success, ) = targets[i].call{value: values[i]}(datas[i]);
if (!success) revert("transaction failed"); // All the calls must succeed.
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Propagate revert reasons, prevent stuck ETH, and cut copy costs.

  • Bubble up callee revert data for debuggability.
  • Enforce length match and msg.value == sum(values) to avoid permanently trapped ETH.
  • Use external + calldata to avoid memory copies.
-function batchSend(address[] memory targets, uint256[] memory values, bytes[] memory datas) public payable {
-    for (uint256 i = 0; i < targets.length; i++) {
-        (bool success, ) = targets[i].call{value: values[i]}(datas[i]);
-        if (!success) revert("transaction failed"); // All the calls must succeed.
-    }
-}
+function batchSend(address[] calldata targets, uint256[] calldata values, bytes[] calldata data) external payable {
+    require(targets.length == values.length && targets.length == data.length, "length mismatch");
+    uint256 total;
+    for (uint256 i = 0; i < values.length; i++) {
+        total += values[i];
+    }
+    require(total == msg.value, "msg.value != sum(values)");
+    for (uint256 i = 0; i < targets.length; i++) {
+        (bool success, bytes memory returndata) = targets[i].call{value: values[i]}(data[i]);
+        if (!success) {
+            assembly {
+                returndatacopy(0, 0, returndatasize())
+                revert(0, returndatasize())
+            }
+        }
+    }
+}
📝 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 Batch send transactions, all the calls must succeed.
/// @param targets The targets of the calls.
/// @param values The values of the calls.
/// @param datas The datas of the calls.
function batchSend(address[] memory targets, uint256[] memory values, bytes[] memory datas) public payable {
for (uint256 i = 0; i < targets.length; i++) {
(bool success, ) = targets[i].call{value: values[i]}(datas[i]);
if (!success) revert("transaction failed"); // All the calls must succeed.
}
}
/// @notice Batch send transactions, all the calls must succeed.
/// @param targets The targets of the calls.
/// @param values The values of the calls.
/// @param datas The datas of the calls.
function batchSend(address[] calldata targets, uint256[] calldata values, bytes[] calldata data) external payable {
require(targets.length == values.length && targets.length == data.length, "length mismatch");
uint256 total;
for (uint256 i = 0; i < values.length; i++) {
total += values[i];
}
require(total == msg.value, "msg.value != sum(values)");
for (uint256 i = 0; i < targets.length; i++) {
(bool success, bytes memory returndata) = targets[i].call{value: values[i]}(data[i]);
if (!success) {
assembly {
returndatacopy(0, 0, returndatasize())
revert(0, returndatasize())
}
}
}
}
🤖 Prompt for AI Agents
In contracts/src/utils/TransactionBatcher.sol around lines 8 to 17, the
batchSend implementation should be changed to an external function using
calldata for the three arrays, validate that targets.length == values.length ==
datas.length, assert that msg.value == sum(values) to prevent ETH from becoming
stuck, and propagate callee revert reasons instead of masking them; iterate over
calls and after each call, if it failed copy and revert with the returned
returndata (using assembly to revert(returnedData, returnedSize)) so the
original error bubbles up.

Comment on lines 23 to 32
function batchSendUnchecked(
address[] memory targets,
uint256[] memory values,
bytes[] memory datas
) public payable {
for (uint256 i = 0; i < targets.length; i++) {
/// forge-lint: disable-next-line(unchecked-call)
targets[i].call{value: values[i]}(datas[i]); // Intentionally ignoring return 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: unchecked batch is reentrancy-drainable and can trap ETH; add guard + length/value checks.

A malicious callee can reenter during one call and siphon the contract’s remaining balance, causing later sends to silently fail. Also, surplus msg.value (or prefunded balance) can get stuck forever. Add a reentrancy guard and enforce array length equality and msg.value == sum(values).

Apply this diff within the function:

-function batchSendUnchecked(
-        address[] memory targets,
-        uint256[] memory values,
-        bytes[] memory datas
-    ) public payable {
-        for (uint256 i = 0; i < targets.length; i++) {
-            /// forge-lint: disable-next-line(unchecked-call)
-            targets[i].call{value: values[i]}(datas[i]); // Intentionally ignoring return value.
-        }
-    }
+function batchSendUnchecked(
+    address[] calldata targets,
+    uint256[] calldata values,
+    bytes[] calldata data
+) external payable nonReentrant {
+    require(targets.length == values.length && targets.length == data.length, "length mismatch");
+    uint256 total;
+    for (uint256 i = 0; i < values.length; i++) {
+        total += values[i];
+    }
+    require(total == msg.value, "msg.value != sum(values)");
+    for (uint256 i = 0; i < targets.length; i++) {
+        /// forge-lint: disable-next-line(unchecked-call)
+        targets[i].call{value: values[i]}(data[i]); // Intentionally ignoring return value.
+    }
+}

Add a minimal guard in the contract (outside the shown hunk):

bool private _locked;
modifier nonReentrant() {
    require(!_locked, "ReentrancyGuard: reentrant call");
    _locked = true;
    _;
    _locked = false;
}
🤖 Prompt for AI Agents
In contracts/src/utils/TransactionBatcher.sol around lines 23-32, the
batchSendUnchecked function is vulnerable to reentrancy and can trap ETH because
it performs unchecked external calls and doesn't validate input lengths or that
msg.value equals the total send amount; to fix: add a minimal reentrancy guard
to the contract (bool private _locked; modifier nonReentrant() {
require(!_locked, "ReentrancyGuard: reentrant call"); _locked = true; _; _locked
= false; }), apply the nonReentrant modifier to batchSendUnchecked, add require
checks at the start of the function to ensure targets.length == values.length &&
targets.length == datas.length, compute the sum of values in a pre-loop and
require(sum == msg.value), then proceed with the loop (still using call) knowing
the total was validated and reentrancy is blocked.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
contracts/test/integration/index.ts (1)

162-164: Don’t hardcode VRF requestId; use the emitted request or RNG state

Hardcoding 1 is brittle and can break if additional requests occur. Also prefer the 2-arg fulfill unless you intend to override words.

-    await vrfCoordinator.fulfillRandomWords(1, rng.target, []);
+    // Derive requestId (option A: from VRF mock events; option B: from RNG contract state/event).
+    const from = (await ethers.provider.getBlockNumber()) - 5; // small backoff window
+    const to = 'latest';
+    const logs = await vrfCoordinator.queryFilter(
+      vrfCoordinator.filters.RandomWordsRequested(),
+      from,
+      to
+    );
+    const requestId = logs[logs.length - 1].args.requestId;
+    await vrfCoordinator.fulfillRandomWords(requestId, rng.target);

If ChainlinkRNG exposes last request id (e.g., rng.lastRequestId()), use that instead; I can wire it up if you confirm the ABI.

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

369-373: _authorizeUpgrade signature mismatch with UUPS base (remove view).

Parent declares non-view; this override adds view and won’t compile.

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

827-891: Reentrancy window in execute()/reward transfers — guard or persist-before-interact.

execute() and _executeRewards() perform external calls (ETH/ERC20 transfers, sortitionModule) while round state is only partially persisted.

Option A (minimal): add a transient reentrancy guard.

+import {ReentrancyGuardTransient} from "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol";
- contract KlerosCore is IArbitratorV2, Initializable, UUPSProxiable {
+ 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 {

Option B (preferred but larger): write all round/dispute state (e.g., round.pnkPenalties, counters) before any external transfer, then pay out. I can draft this if you want it now.

Also applies to: 1001-1010, 1258-1267


887-890: Persist penalties before transfers to owner/jurors.

round.pnkPenalties is written after external transfers; reentrancy could observe stale values.

Persist updated pnkPenalties and any sums before calling _transferFeeToken/pinakion.safeTransfer.

Also applies to: 950-961, 1024-1043


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

Avoid bricking ownership, token, or module wiring.

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

Add errors:

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

Also applies to: 580-585


1202-1204: Guard against division-by-zero in convertEthToTokenAmount.

rateInEth can be zero if not configured; current code would revert with raw division.

-    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;
+    }
contracts/CHANGELOG.md (3)

27-27: Fix constant name and wording.

ONE_BASIS_POINT (singular), and “Replace … with …” is clearer.

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

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

Not just “interfaces only”; RNG contracts also widened; include exact constraint.

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

173-174: Avoid 404: defer 0.13.0 release link until published.

Comment out until the tag exists.

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

Optionally confirm availability before merge:

#!/bin/bash
curl -s -o /dev/null -w "%{http_code}\n" \
"https://github.com/kleros/kleros-v2/releases/tag/@kleros%2Fkleros-v2-contracts@0.13.0"
🧹 Nitpick comments (6)
contracts/test/integration/index.ts (3)

59-68: Typed getContract calls: verify deployment keys match artifact names

Generics usage is good. Please confirm the deployment key for the VRF mock. If the deployment is saved as "ChainlinkVRFCoordinatorV2Mock" (common for the mock), prefer matching it here to avoid type/runtime drift.

-  vrfCoordinator = await ethers.getContract<ChainlinkVRFCoordinatorV2Mock>("ChainlinkVRFCoordinator");
+  // If the deployment key is actually "ChainlinkVRFCoordinatorV2Mock", align the name:
+  vrfCoordinator = await ethers.getContract<ChainlinkVRFCoordinatorV2Mock>("ChainlinkVRFCoordinatorV2Mock");

134-151: Avoid re-normalizing a bytes32 block hash with toBeHex

lastBlock.hash is already a 32-byte hex string. toBeHex may alter width. Pass the hash as-is (or zero-pad explicitly) to ensure strict bytes32 fidelity.

-            foreignBlockHash: ethers.toBeHex(lastBlock.hash),
+            // Hash is already bytes32; pass as-is (or zeroPadValue(..., 32) if needed).
+            foreignBlockHash: lastBlock.hash,

208-213: Remove unnecessary async from log helper

The function is sync; dropping async avoids returning a dangling Promise and clarifies intent.

-const logJurorBalance = async (result: { totalStakedPnk: bigint; totalLocked: bigint }) => {
+const logJurorBalance = (result: { totalStakedPnk: bigint; totalLocked: bigint }) => {
contracts/src/arbitration/KlerosCore.sol (3)

315-323: Initializer: validate critical addresses (owner, dispute kit, sortition, wNative, PNK).

Prevents foot-guns at deployment time.

-        owner = _owner;
+        if (_owner == address(0)) revert ZeroAddressOwner();
+        owner = _owner;
@@
-        pinakion = _pinakion;
+        if (address(_pinakion) == address(0)) revert ZeroAddress();
+        pinakion = _pinakion;
@@
-        sortitionModule = _sortitionModuleAddress;
+        if (address(_sortitionModuleAddress) == address(0)) revert ZeroAddress();
+        sortitionModule = _sortitionModuleAddress;
@@
-        wNative = _wNative;
+        if (_wNative == address(0)) revert ZeroAddress();
+        wNative = _wNative;
@@
-        jurorNft = _jurorNft;
+        // Optional: allow zero to mean "no gating", otherwise guard
+        jurorNft = _jurorNft;

429-433: addNewDisputeKit: validate input and emit after state is consistent.

Check for zero address to avoid phantom DK entries.

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

331-336: TODO left for FORKING_COURT initialization.

If this must ship in v0.13.0, either fill properties and emit, or leave a clear “not used on mainnet” note.

I can draft sane defaults and the emit CourtCreated() for Forking court—want me to push a patch?

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7c30463 and c97270a.

📒 Files selected for processing (4)
  • contracts/CHANGELOG.md (2 hunks)
  • contracts/src/arbitration/KlerosCore.sol (4 hunks)
  • contracts/test/foundry/KlerosCore_Execution.t.sol (1 hunks)
  • contracts/test/integration/index.ts (6 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-09-04T23:36:16.415Z
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.

Applied to files:

  • contracts/src/arbitration/KlerosCore.sol
📚 Learning: 2025-09-03T22:48:32.972Z
Learnt from: jaybuidl
PR: kleros/kleros-v2#0
File: :0-0
Timestamp: 2025-09-03T22:48:32.972Z
Learning: In the Kleros v2 codebase, the team prioritizes gas optimization over strict CEI pattern compliance when dealing with trusted contracts. For penalty execution logic, they prefer batching storage writes (`round.pnkPenalties`) rather than updating incrementally after each penalty calculation to save gas costs, as the risk is extremely low between trusted contracts.

Applied to files:

  • contracts/src/arbitration/KlerosCore.sol
🧬 Code graph analysis (1)
contracts/test/integration/index.ts (1)
contracts/deploy/utils/index.ts (1)
  • PNK (40-40)
🔇 Additional comments (5)
contracts/test/integration/index.ts (3)

79-79: Switch to totalStakedPnk looks correct

Assertions against the renamed field align with the updated ABI/struct.

Also applies to: 86-86, 93-93, 100-100


120-121: Event shape update acknowledged

Asserting the final argument as 0 matches the removal of templateUri and the separate choices arg.


189-197: Execute flow and event assertions look consistent

The TokenAndETHShift and Ruling assertions line up with the no-appeal path.

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

646-651: ```shell
#!/bin/bash
set -euo pipefail

echo "PWD: $(pwd)"
echo "Root listing:"
ls -la

echo
echo "Searching repository for relevant symbols..."
rg -n --hidden -S "KlerosCore.sol" -C3 || true
rg -n --hidden -S "contracts/src/arbitration/KlerosCore.sol" -C3 || true
rg -n --hidden -S "using\s+SafeERC20\s+for" -n -C3 || true
rg -n --hidden -S "safeTransferFrom(" -n -C3 || true
rg -n --hidden -S "safeTransfer(" -n -C3 || true
rg -n --hidden -S "library\s+SafeERC20" -n -C3 || true
rg -n --hidden -S "interface\s+IERC20" -n -C3 || true
rg -n --hidden -S "TransferFailed" -n -C3 || true

if [ -f contracts/src/arbitration/KlerosCore.sol ]; then
echo
echo "----- contracts/src/arbitration/KlerosCore.sol (head) -----"
sed -n '1,220p' contracts/src/arbitration/KlerosCore.sol || true
echo
echo "----- contracts/src/arbitration/KlerosCore.sol (lines 630-670) -----"
sed -n '630,670p' contracts/src/arbitration/KlerosCore.sol || true
else
echo
echo "contracts/src/arbitration/KlerosCore.sol not found at that path."
echo "Listing top-level directories:"
ls -la | sed -n '1,200p'
fi


---

`1401-1401`: **Rename error: InvalidDisputKitParent → InvalidDisputeKitParent**

Typo confirmed in multiple files — update the error declaration and all usages, then recompile to regenerate deployment/ABI artifacts.

Locations found:
- contracts/src/arbitration/KlerosCoreBase.sol
- contracts/src/arbitration/KlerosCore.sol
- contracts/src/arbitration/university/KlerosCoreUniversity.sol:1176
- contracts/deployments/arbitrumSepoliaDevnet/KlerosCore_Implementation.json
- contracts/deployments/arbitrumSepoliaDevnet/SortitionModule_Implementation.json

Apply this change:

```diff
-error InvalidDisputKitParent();
+error InvalidDisputeKitParent();

Verify no remaining occurrences: rg -n 'InvalidDisputKitParent|InvalidDisputeKitParent'.

import {KlerosCore} from "../../src/arbitration/KlerosCore.sol";
import {SortitionModule} from "../../src/arbitration/SortitionModule.sol";
import {DisputeKitClassicBase} from "../../src/arbitration/dispute-kits/DisputeKitClassicBase.sol";
import {IArbitratorV2, IArbitrableV2} from "../../src/arbitration/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

Wrong import path for interfaces (won’t compile).

Import IArbitratorV2/IArbitrableV2 from interfaces, not from KlerosCore.sol.

-import {IArbitratorV2, IArbitrableV2} from "../../src/arbitration/KlerosCore.sol";
+import {IArbitratorV2, IArbitrableV2} from "../../src/arbitration/interfaces/IArbitratorV2.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 {IArbitratorV2, IArbitrableV2} from "../../src/arbitration/KlerosCore.sol";
import {IArbitratorV2, IArbitrableV2} from "../../src/arbitration/interfaces/IArbitratorV2.sol";
🤖 Prompt for AI Agents
In contracts/test/foundry/KlerosCore_Execution.t.sol around line 8, the test
imports IArbitratorV2 and IArbitrableV2 from the KlerosCore.sol implementation
which causes compile errors; update the import to pull these interfaces from
their proper interfaces file (e.g., the interfaces directory or file where
IArbitratorV2 and IArbitrableV2 are defined), adjust the import path to the
correct relative path, and ensure the import statement references the interface
file name instead of KlerosCore.sol.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

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

69-94: Length check is off by 32 bytes for tokenId read.

You mload at offset 0xA0 (fifth param); require length >= 192, not 160, to avoid reading zeroed memory and mis-gating ERC-1155.

Apply:

-        // Need at least 160 bytes to safely read the parameters
-        if (_extraData.length < 160) return (address(0), false, 0);
+        // Need at least 192 bytes to safely read up to the 5th parameter
+        if (_extraData.length < 192) return (address(0), false, 0);
contracts/deployments/arbitrumSepoliaDevnet/DisputeResolverUniversity.json (1)

147-158: Guard against setting zero owner.

The underlying implementation (per embedded metadata) doesn’t reject address(0) in changeOwner(). Add a zero-address check to avoid bricking ownership.

Apply this change in DisputeResolver.sol:

 function changeOwner(address _owner) external {
-    if (owner != msg.sender) revert OwnerOnly();
+    if (owner != msg.sender) revert OwnerOnly();
+    if (_owner == address(0)) revert OwnerOnly(); // or a dedicated ZeroAddress()
     owner = _owner;
 }
contracts/src/arbitration/devtools/KlerosCoreRuler.sol (4)

5-6: Fix incorrect import of IArbitrableV2.

You’re importing IArbitrableV2 from IArbitratorV2.sol. Import it from its own file to compile.

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

391-402: Incorrect use of SafeERC20.safeTransferFrom.

safeTransferFrom returns no bool; current check won’t compile. Just call it; it reverts on failure.

- if (!_feeToken.safeTransferFrom(msg.sender, address(this), _feeAmount)) revert TransferFailed();
- return _createDispute(_numberOfChoices, _extraData, _feeToken, _feeAmount);
+ _feeToken.safeTransferFrom(msg.sender, address(this), _feeAmount);
+ return _createDispute(_numberOfChoices, _extraData, _feeToken, _feeAmount);

516-521: Don’t ignore send() return; prefer call and check.

Use low-level call with success check to avoid silent ETH loss.

- payable(account).send(feeReward);
+ (bool ok, ) = payable(account).call{value: feeReward}("");
+ if (!ok) revert TransferFailed();

631-633: Protect against division by zero in currency conversion.

Guard rateInEth > 0 to avoid division by zero.

function convertEthToTokenAmount(IERC20 _toToken, uint256 _amountInEth) public view returns (uint256) {
-    return (_amountInEth * 10 ** currencyRates[_toToken].rateDecimals) / currencyRates[_toToken].rateInEth;
+    uint64 rate = currencyRates[_toToken].rateInEth;
+    if (rate == 0) revert TokenNotAccepted(); // or a dedicated RateNotSet()
+    return (_amountInEth * 10 ** currencyRates[_toToken].rateDecimals) / rate;
}
contracts/src/arbitration/university/KlerosCoreUniversity.sol (3)

203-219: Initializer: validate critical addresses.

Reject zero for _owner, _pinakion, and _sortitionModuleAddress (and optionally _disputeKit) to prevent dead config.

 function initialize(
-        address _owner,
+        address _owner,
         address _instructor,
         IERC20 _pinakion,
         address _jurorProsecutionModule,
         IDisputeKit _disputeKit,
@@
-    ) external initializer {
-        owner = _owner;
+    ) external initializer {
+        if (_owner == address(0)) revert ZeroAddress();
+        if (address(_pinakion) == address(0)) revert ZeroAddress();
+        if (address(_disputeKit) == address(0)) revert ZeroAddress();
+        if (address(_sortitionModuleAddress) == address(0)) revert ZeroAddress();
+        owner = _owner;

Add once:

+error ZeroAddress();

684-746: Reentrancy window in execute() around external transfers.

State (e.g., round.pnkPenalties) is updated after transfers. Add a reentrancy guard or persist state before interactions.

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

1056-1058: Division-by-zero risk in convertEthToTokenAmount.

Guard rateInEth to avoid raw division revert and to signal unsupported tokens clearly.

-    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;
+    }
contracts/scripts/utils/contracts.ts (1)

61-71: Rename keys to transactionBatcher / blockHashRng / klerosCoreSnapshotProxy for cross‑package consistency

Tests/deployments already use transactionBatcher, blockHashRng and klerosCoreSnapshotProxy — contracts/scripts/utils/contracts.ts still returns batcher / blockHashRNG / snapshotProxy. Rename the keys and local variables and update consumers (e.g. contracts/scripts/changeOwner.ts uses snapshotProxy at ~lines 43 and 70).

@@
   return {
     ...coreSpecificNames[coreType],
     evidence: "EvidenceModule",
     policyRegistry: "PolicyRegistry",
-    batcher: "TransactionBatcher",
+    transactionBatcher: "TransactionBatcher",
     chainlinkRng: "ChainlinkRNG",
     randomizerRng: "RandomizerRNG",
-    blockHashRNG: "BlockHashRNG",
+    blockHashRng: "BlockHashRNG",
     pnk: "PNK",
-    snapshotProxy: "KlerosCoreSnapshotProxy",
+    klerosCoreSnapshotProxy: "KlerosCoreSnapshotProxy",
   };
@@
-  const policyRegistry = await ethers.getContract<PolicyRegistry>(getContractNames(coreType).policyRegistry);
-  const batcher = await ethers.getContract<TransactionBatcher>(getContractNames(coreType).batcher);
+  const policyRegistry = await ethers.getContract<PolicyRegistry>(getContractNames(coreType).policyRegistry);
+  const transactionBatcher = await ethers.getContract<TransactionBatcher>(
+    getContractNames(coreType).transactionBatcher
+  );
   const chainlinkRng = await ethers.getContractOrNull<ChainlinkRNG>(getContractNames(coreType).chainlinkRng);
   const randomizerRng = await ethers.getContractOrNull<RandomizerRNG>(getContractNames(coreType).randomizerRng);
-  const blockHashRNG = await ethers.getContractOrNull<BlockHashRNG>(getContractNames(coreType).blockHashRNG);
+  const blockHashRng = await ethers.getContractOrNull<BlockHashRNG>(getContractNames(coreType).blockHashRng);
   const pnk = await ethers.getContract<PNK>(getContractNames(coreType).pnk);
-  const snapshotProxy = await ethers.getContractOrNull<KlerosCoreSnapshotProxy>(
-    getContractNames(coreType).snapshotProxy
+  const klerosCoreSnapshotProxy = await ethers.getContractOrNull<KlerosCoreSnapshotProxy>(
+    getContractNames(coreType).klerosCoreSnapshotProxy
   );
@@
     chainlinkRng,
     randomizerRng,
-    blockHashRNG,
+    blockHashRng,
     pnk,
-    batcher,
-    snapshotProxy,
+    transactionBatcher,
+    klerosCoreSnapshotProxy,
   };

Files to update: contracts/scripts/utils/contracts.ts (occurrences around lines 63–71, 108–117, 131–135) and consumers that destructure these keys (notably contracts/scripts/changeOwner.ts — replace snapshotProxy with klerosCoreSnapshotProxy).

♻️ Duplicate comments (10)
contracts/hardhat.config.ts (1)

29-36: Parameterize optimizer runs to avoid toolchain drift.

Read from SOLC_OPTIMIZER_RUNS to keep Hardhat/Foundry in sync. This was suggested earlier; reiterating for visibility.

Apply:

           evmVersion: "cancun",
           viaIR: process.env.VIA_IR !== "false", // Defaults to true
           optimizer: {
             enabled: true,
-            runs: 1000,
+            runs: Number(process.env.SOLC_OPTIMIZER_RUNS ?? 1000),
           },
contracts/CHANGELOG.md (3)

28-28: Fix constant name.

Use ONE_BASIS_POINT (singular).

Apply:

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

31-31: Clarify pragma widening scope.

State exact range and affected modules.

Apply:

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

174-175: Defer 0.13.0 link until live.

Remove/Comment the 0.13.0 release link to avoid 404.

Apply:

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

Optionally verify locally:

curl -s -o /dev/null -w "%{http_code}\n" \
"https://github.com/kleros/kleros-v2/releases/tag/@kleros%2Fkleros-v2-contracts@0.13.0"
contracts/src/arbitration/devtools/KlerosCoreRuler.sol (2)

230-234: Emit OwnerChanged and guard zero address in changeOwner().

Add event and zero-address check for observability/safety.

- function changeOwner(address payable _owner) external onlyByOwner {
-     owner = _owner;
- }
+ event OwnerChanged(address indexed _oldOwner, address indexed _newOwner);
+ function changeOwner(address payable _owner) external onlyByOwner {
+     if (_owner == address(0)) revert OwnerOnly();
+     address old = owner;
+     owner = _owner;
+     emit OwnerChanged(old, _owner);
+ }

175-183: Reject zero owner/token in initialize().

Prevent permanent lockout or null token by rejecting zero addresses.

 function initialize(address _owner, IERC20 _pinakion, uint256[4] memory _courtParameters) external initializer {
-    owner = _owner;
-    pinakion = _pinakion;
+    if (_owner == address(0)) revert OwnerOnly(); // or ZeroAddress()
+    if (address(_pinakion) == address(0)) revert TokenNotAccepted(); // or ZeroAddress()
+    owner = _owner;
+    pinakion = _pinakion;
contracts/src/arbitration/KlerosCore.sol (3)

822-890: execute(): reentrancy hardening.

Consider nonReentrant (or persist state before external transfers). Token callbacks can reenter.


396-425: Zero-address guards for governance setters.

Protect changeOwner, changeGuardian, changePinakion, changeJurorProsecutionModule, changeSortitionModule.


1202-1204: Division-by-zero risk in convertEthToTokenAmount.

Add a guard for rateInEth == 0 and reuse TokenNotAccepted().

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

279-306: Zero-address guards for governance setters.

Add checks to changeOwner, changePinakion, changeJurorProsecutionModule, and changeSortitionModule to avoid bricking state.

🧹 Nitpick comments (27)
contracts/deployments/contractsViem.ts (1)

61-64: Tighten ABI typing for viem contracts

Use viem’s Abi type instead of any[] to regain type safety on getContract calls.

Apply:

-import { type PublicClient, type WalletClient, getContract } from "viem";
+import { type PublicClient, type WalletClient, type Abi, getContract } from "viem";
@@
 type ContractInstance = {
   address: `0x${string}`;
-  abi: readonly any[];
+  abi: Abi;
 };
contracts/deployments/contractsEthers.ts (1)

206-218: Inconsistency vs viem: chainlinkRng included for university here but omitted in viem

Viem university path omits chainlinkRng (“Not used in university”), but ethers path includes it. Align both to avoid surprises.

Apply:

         {
           dkClassicConfig: devnetDkClassicUniversityConfig,
           drConfig: devnetDrUniversityConfig,
           dtrConfig: devnetDtrUniversityConfig,
           evidenceConfig: devnetEvidenceConfig,
           policyRegistryConfig: devnetPolicyRegistryConfig,
           batcherConfig: devnetBatcherConfig,
-          chainlinkRngConfig: devnetChainlinkRngConfig,
           blockHashRngConfig: devnetBlockHashRngConfig,
           pnkConfig: devnetPnkConfig,
           snapshotProxyConfig: devnetSnapshotProxyConfig,
         },

Optional cross-check:

#!/bin/bash
echo "== viem university RNG keys =="
rg -nC2 'case "university":' contracts/deployments/contractsViem.ts

echo "== ethers university RNG keys =="
rg -nC2 'case "university":' contracts/deployments/contractsEthers.ts
contracts/hardhat.config.ts (1)

296-300: Unify Etherscan API key env var and avoid duplicate root verify blocks.

Use one env var (ETHERSCAN_API_KEY) to prevent confusion with network-level verify settings.

Apply:

   verify: {
     etherscan: {
-      apiKey: process.env.ETHERSCAN_API_KEY_FIX,
+      apiKey: process.env.ETHERSCAN_API_KEY,
     },
   },
contracts/CHANGELOG.md (1)

7-7: Label as Unreleased until published.

Avoid a concrete date if not released; prevents confusion.

Apply:

-## [0.13.0] - 2025-08-07 (Not published yet)
+## [0.13.0] - Unreleased
contracts/deploy/00-home-chain-arbitration-university.ts (2)

41-46: Nonce-based address prediction: add a safety check.

Race-sensitive; verify predicted equals actual post-deploy to avoid wiring SortitionModule to a wrong core if tx ordering changes.

Apply:

   if (!klerosCoreAddress) {
     const nonce = await ethers.provider.getTransactionCount(deployer);
     klerosCoreAddress = getContractAddress(deployer, nonce + 3);
     console.log("calculated future KlerosCoreUniversity address for nonce %d: %s", nonce + 3, klerosCoreAddress);
   }
+  // After deploying KlerosCoreUniversity below, assert addresses match.

And after deploying KlerosCoreUniversity:

   const klerosCore = await deployUpgradable(deployments, "KlerosCoreUniversity", {
     ...
   });
+  if (klerosCoreAddress && klerosCore.address !== klerosCoreAddress) {
+    console.warn("Predicted core address %s differs from deployed %s", klerosCoreAddress, klerosCore.address);
+  }

90-95: Arg sources: mix of .target/.address.

Both valid (ethers v6), but consider standardizing to one for consistency.

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

79-82: Visibility: consider internal.

Function is only used internally; make it internal to reduce surface.

Apply:

-    ) public pure returns (address tokenGate, bool isERC1155, uint256 tokenId) {
+    ) internal pure returns (address tokenGate, bool isERC1155, uint256 tokenId) {
contracts/deployments/arbitrumSepoliaDevnet/DisputeResolverUniversity.json (1)

254-270: Add bounds check on mapped dispute ID before indexing.

rule() uses arbitratorDisputeIDToLocalID to index disputes without checking existence. A bad _arbitratorDisputeID would target dispute 0. Add if (localDisputeID >= disputes.length) revert;.

 function rule(uint256 _arbitratorDisputeID, uint256 _ruling) external override {
-    uint256 localDisputeID = arbitratorDisputeIDToLocalID[_arbitratorDisputeID];
+    uint256 localDisputeID = arbitratorDisputeIDToLocalID[_arbitratorDisputeID];
+    if (localDisputeID >= disputes.length) revert DisputeAlreadyRuled(); // or a dedicated NotFound()
contracts/src/arbitration/dispute-kits/DisputeKitGatedShutter.sol (2)

114-134: Write to recoveryCommitments after commit succeeds.

Currently you store recovery commitments before _castCommit() validates ownership; although a revert rolls back, you pay gas for a doomed write. Move the loop below _castCommit() for efficiency.

- for (uint256 i = 0; i < _voteIDs.length; i++) {
-   recoveryCommitments[localDisputeID][localRoundID][_voteIDs[i]] = _recoveryCommit;
- }
- _castCommit(...);
+ _castCommit(...);
+ for (uint256 i = 0; i < _voteIDs.length; i++) {
+   recoveryCommitments[localDisputeID][localRoundID][_voteIDs[i]] = _recoveryCommit;
+ }

144-151: Validate non-empty _voteIDs.

Indexing _voteIDs[0] without checking length will revert on empty arrays. Add require(_voteIDs.length > 0).

 function castVoteShutter(
 ...
 ) external {
+    if (_voteIDs.length == 0) revert(); // use a dedicated EmptyVotes() error
contracts/deployments/arbitrumSepoliaDevnet/SortitionModuleUniversity.json (1)

298-310: executeDelayedStakes(): ensure gas-capping and iteration safety.

Make sure implementation caps work per-iteration and uses defensive gas checks to avoid griefing.

contracts/deployments/arbitrumSepoliaDevnet/KlerosCoreUniversity.json (2)

1457-1479: Expose pnk-at-stake per juror: good introspection.

New getter helps DKs/UI. Confirm units are PNK (not wei) and documented accordingly.


1387-1391: Owner arbitrary-call foot‑gun.

executeOwnerProposal is powerful; ensure operational policy (multisig/EO timelock) controls owner in non-dev environments.

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

221-229: Arbitrary-call helper is fine for dev; document non-dev constraints.

Keep this only in dev tooling; otherwise guard via multisig/timelock owner.

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

779-802: Index bounds check for penalizedInCourtID.

Accessing courts[penalizedInCourtID] without validating it is a valid court can revert.

-        } else if (newCourtStake < courts[penalizedInCourtID].minStake) {
+        } else if (penalizedInCourtID < courts.length && newCourtStake < courts[penalizedInCourtID].minStake) {
contracts/src/arbitration/KlerosCore.sol (1)

301-323: Initializer: validate critical addresses (owner, wNative, modules).

Set guards to prevent misconfiguration, especially wNative required by SafeSend.

-    ) external initializer {
-        owner = _owner;
-        guardian = _guardian;
+    ) external initializer {
+        if (_owner == address(0)) revert ZeroAddress();
+        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;

Add once:

+error ZeroAddress();
contracts/README.md (5)

77-77: Clarify environment naming and stability window.

“V2 Devnet (unstable)” is clear, but readers won’t know how it differs from “V2 Neo (prelaunch)” and “Official Testnet.” Add a one‑liner under this heading stating intended usage, reset cadence, and breakage expectations, ideally with a “last refreshed on YYYY‑MM‑DD” note.


87-87: Document the University variants on first mention.

Add a short parenthetical after the first “University” contract (scope, governance/owner, and how it differs from the non‑University counterpart). This avoids repeating explanations across the list.


95-95: Include implementation verification or drop it for consistency.

You list both proxy and implementation here (good), but not all entries consistently include the implementation link across sections. Either standardize on “proxy + implementation” for all proxied contracts or stick to proxy only with a note on how to find the logic address in artifacts.


100-100: Disambiguate KlerosCore vs KlerosCoreUniversity.

A brief note on whether both cores run concurrently and how integrators should choose which ABI/address to target would reduce confusion for readers skimming deployments.


106-106: Cross‑check SortitionModuleUniversity addresses and add linkage.

  • Verify proxy address against deployments (script above).
  • Consider adding a short line linking SortitionModuleUniversity to its corresponding Core (e.g., “used by KlerosCoreUniversity on Arbitrum Sepolia”).
contracts/scripts/utils/contracts.ts (6)

59-59: Tighten error message grammar.

Drop the comma and simplify phrasing.

-  if (!(coreType in coreSpecificNames)) throw new Error("Invalid core type, must be one of BASE, or UNIVERSITY");
+  if (!(coreType in coreSpecificNames)) throw new Error("Invalid core type: must be BASE or UNIVERSITY");

82-83: Type unions for core/sortition — LGTM; minor readability win available.

Optional: cache getContractNames(coreType) once to avoid repeated calls.

 export const getContracts = async (hre: HardhatRuntimeEnvironment, coreType: Core) => {
   const { ethers } = hre;
+  const names = getContractNames(coreType);
   let core: KlerosCore | KlerosCoreUniversity;
   let sortition: SortitionModule | SortitionModuleUniversity;
   switch (coreType) {
     case Cores.BASE:
-      core = await ethers.getContract<KlerosCore>(getContractNames(coreType).core);
-      sortition = await ethers.getContract<SortitionModule>(getContractNames(coreType).sortition);
+      core = await ethers.getContract<KlerosCore>(names.core);
+      sortition = await ethers.getContract<SortitionModule>(names.sortition);
       break;
     case Cores.UNIVERSITY:
-      core = await ethers.getContract<KlerosCoreUniversity>(getContractNames(coreType).core);
-      sortition = await ethers.getContract<SortitionModuleUniversity>(getContractNames(coreType).sortition);
+      core = await ethers.getContract<KlerosCoreUniversity>(names.core);
+      sortition = await ethers.getContract<SortitionModuleUniversity>(names.sortition);
       break;

111-113: BlockHash RNG optionality — align with deployments or confirm optional nature.

Deployments/Viem expose blockHashRng as non‑optional. If this contract is always present on supported networks, switch to getContract to avoid undefined at call sites; otherwise, keep OrNull but document it.

-const blockHashRng = await ethers.getContractOrNull<BlockHashRNG>(getContractNames(coreType).blockHashRng);
+const blockHashRng = await ethers.getContract<BlockHashRNG>(getContractNames(coreType).blockHashRng);

94-95: Mirror the earlier error‑message fix.

-      throw new Error("Invalid core type, must be one of BASE, or UNIVERSITY");
+      throw new Error("Invalid core type: must be BASE or UNIVERSITY");

145-150: Network inference: centralize list and improve error detail; consider university networks.

  • Deduplicate the network list across both functions.
  • Include the actual network.name in the error for faster troubleshooting.
  • If University deployments will be used via network name, add a branch or env‑flag to select UNIVERSITY.
 export const getContractsFromNetwork = async (hre: HardhatRuntimeEnvironment) => {
   const { network } = hre;
-  if (["arbitrumSepoliaDevnet", "arbitrumSepolia", "arbitrum"].includes(network.name)) {
+  const BASE_NETWORKS = ["arbitrumSepoliaDevnet", "arbitrumSepolia", "arbitrum"] as const;
+  if (BASE_NETWORKS.includes(network.name as any)) {
     return getContracts(hre, Cores.BASE);
   } else {
-    throw new Error("Invalid network");
+    throw new Error(`Invalid network: ${network.name}. Expected one of: ${BASE_NETWORKS.join(", ")}.`);
   }
 }

159-164: Apply the same BASE_NETWORKS constant and detailed error here.

-  if (["arbitrumSepoliaDevnet", "arbitrumSepolia", "arbitrum"].includes(network.name)) {
+  const BASE_NETWORKS = ["arbitrumSepoliaDevnet", "arbitrumSepolia", "arbitrum"] as const;
+  if (BASE_NETWORKS.includes(network.name as any)) {
     return getContractNames(Cores.BASE);
   } else {
-    throw new Error("Invalid network");
+    throw new Error(`Invalid network: ${network.name}. Expected one of: ${BASE_NETWORKS.join(", ")}.`);
   }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c97270a and 55589e5.

📒 Files selected for processing (25)
  • contracts/CHANGELOG.md (2 hunks)
  • contracts/README.md (2 hunks)
  • contracts/deploy/00-home-chain-arbitration-university.ts (4 hunks)
  • contracts/deployments/arbitrumSepoliaDevnet/DisputeKitClassicUniversity.json (19 hunks)
  • contracts/deployments/arbitrumSepoliaDevnet/DisputeKitClassicUniversity_Proxy.json (3 hunks)
  • contracts/deployments/arbitrumSepoliaDevnet/DisputeResolverUniversity.json (10 hunks)
  • contracts/deployments/arbitrumSepoliaDevnet/DisputeTemplateRegistryUniversity.json (1 hunks)
  • contracts/deployments/arbitrumSepoliaDevnet/DisputeTemplateRegistryUniversity_Implementation.json (1 hunks)
  • contracts/deployments/arbitrumSepoliaDevnet/DisputeTemplateRegistryUniversity_Proxy.json (1 hunks)
  • contracts/deployments/arbitrumSepoliaDevnet/KlerosCoreUniversity.json (18 hunks)
  • contracts/deployments/arbitrumSepoliaDevnet/KlerosCoreUniversity_Proxy.json (2 hunks)
  • contracts/deployments/arbitrumSepoliaDevnet/SortitionModuleUniversity.json (13 hunks)
  • contracts/deployments/arbitrumSepoliaDevnet/SortitionModuleUniversity_Proxy.json (2 hunks)
  • contracts/deployments/contractsEthers.ts (5 hunks)
  • contracts/deployments/contractsViem.ts (4 hunks)
  • contracts/hardhat.config.ts (2 hunks)
  • contracts/scripts/utils/contracts.ts (5 hunks)
  • contracts/scripts/utils/execution.ts (1 hunks)
  • contracts/src/arbitration/KlerosCore.sol (4 hunks)
  • contracts/src/arbitration/devtools/KlerosCoreRuler.sol (12 hunks)
  • contracts/src/arbitration/dispute-kits/DisputeKitGated.sol (3 hunks)
  • contracts/src/arbitration/dispute-kits/DisputeKitGatedShutter.sol (6 hunks)
  • contracts/src/arbitration/university/KlerosCoreUniversity.sol (38 hunks)
  • contracts/src/proxy/KlerosProxies.sol (1 hunks)
  • contracts/test/arbitration/dispute-kit-gated.ts (3 hunks)
✅ Files skipped from review due to trivial changes (3)
  • contracts/deployments/arbitrumSepoliaDevnet/DisputeTemplateRegistryUniversity_Proxy.json
  • contracts/scripts/utils/execution.ts
  • contracts/deployments/arbitrumSepoliaDevnet/DisputeTemplateRegistryUniversity.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • contracts/test/arbitration/dispute-kit-gated.ts
🧰 Additional context used
🧠 Learnings (5)
📚 Learning: 2025-09-03T22:48:32.972Z
Learnt from: jaybuidl
PR: kleros/kleros-v2#0
File: :0-0
Timestamp: 2025-09-03T22:48:32.972Z
Learning: In the Kleros v2 codebase, the team prioritizes gas optimization over strict CEI pattern compliance when dealing with trusted contracts. For penalty execution logic, they prefer batching storage writes (`round.pnkPenalties`) rather than updating incrementally after each penalty calculation to save gas costs, as the risk is extremely low between trusted contracts.

Applied to files:

  • contracts/src/arbitration/university/KlerosCoreUniversity.sol
  • contracts/src/arbitration/KlerosCore.sol
📚 Learning: 2025-09-03T19:34:58.056Z
Learnt from: jaybuidl
PR: kleros/kleros-v2#2107
File: contracts/src/arbitration/university/KlerosCoreUniversity.sol:1083-1092
Timestamp: 2025-09-03T19:34:58.056Z
Learning: KlerosCoreUniversity and SortitionModuleUniversity do not have phases, unlike KlerosCoreBase and SortitionModuleBase. Therefore, validateStake in the University contracts will never return StakingResult.Delayed, only Successful or other failure states.

Applied to files:

  • contracts/src/arbitration/university/KlerosCoreUniversity.sol
📚 Learning: 2025-09-04T23:36:16.415Z
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.

Applied to files:

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

Applied to files:

  • contracts/src/arbitration/university/KlerosCoreUniversity.sol
  • contracts/deployments/arbitrumSepoliaDevnet/SortitionModuleUniversity.json
📚 Learning: 2025-01-22T00:55:35.842Z
Learnt from: jaybuidl
PR: kleros/kleros-v2#1805
File: contracts/src/proxy/UUPSProxiable.sol:127-129
Timestamp: 2025-01-22T00:55:35.842Z
Learning: In Solidity, a public state variable automatically implements a corresponding external view function with the same name. For example, `string public constant override version = "0.8.0"` implements the abstract `function version() external view returns (string memory)`.

Applied to files:

  • contracts/deployments/arbitrumSepoliaDevnet/DisputeKitClassicUniversity.json
  • contracts/deployments/arbitrumSepoliaDevnet/KlerosCoreUniversity.json
🧬 Code graph analysis (3)
contracts/scripts/utils/contracts.ts (3)
contracts/deployments/contractsEthers.ts (1)
  • getContracts (169-275)
contracts/deployments/contractsViem.ts (1)
  • getContracts (235-343)
contracts/deployments/index.ts (2)
  • getContracts (18-18)
  • getContracts (19-19)
contracts/deploy/00-home-chain-arbitration-university.ts (1)
contracts/deploy/utils/deployUpgradable.ts (1)
  • deployUpgradable (37-89)
contracts/deployments/contractsEthers.ts (1)
contracts/deployments/utils.ts (1)
  • getAddress (25-29)
⏰ 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: SonarCloud
  • GitHub Check: hardhat-tests
🔇 Additional comments (43)
contracts/deployments/contractsViem.ts (4)

170-177: LGTM: University wiring now binds DTR university config

Alias usage matches the import; no issues spotted.


207-228: Add temporary back-compat alias for "mainnetNeo"

Add a fallthrough case "mainnetNeo" before case "mainnet" to preserve backward compatibility; sandbox rg returned "No files were searched" — verify callers locally with rg -n '\bmainnetNeo\b' -g '!**/node_modules/**'.

   switch (deployment) {
@@
-    case "mainnet":
+    case "mainnetNeo": // temporary back-compat
+      /* fallthrough */
+    case "mainnet":
       return getCommonConfigs({

43-49: Manual verification required — purge Neo-specific aliases (mainnetNeo / Neo*Config)

Good rename to mainnetConfig; automated sandbox search couldn't confirm absence of Neo-specific names. Run at repo root:
rg -n --hidden -uu -S '(\bmainnetNeo\b|\bNeoConfig\b|\bklerosCoreNeoConfig\b|\bsortitionModuleNeoConfig\b|\bdisputeKit\w
NeoConfig\b|\bdisputeResolverNeoConfig\b)' -g '!/node_modules/'
Remove or update any matches.

Location: contracts/deployments/contractsViem.ts lines 43–49 (already updated).


22-24: ```shell
#!/bin/bash
set -euo pipefail

echo "PWD: $(pwd)"
echo
echo "== list contracts/deployments files =="
ls -la contracts/deployments || true
echo
echo "== all occurrences of the word 'deployments' under contracts/ =="
rg -n --hidden -S -g 'contracts/' '\bdeployments\b' || true
echo
echo "== literal 'export const deployments' =="
rg -n --hidden -F -g 'contracts/
' 'export const deployments' || true
echo
echo "== literal 'const deployments =' =="
rg -n --hidden -F -g 'contracts/' 'const deployments =' || true
echo
echo "== literal 'deployments = {' =="
rg -n -F --hidden -g 'contracts/
' 'deployments = {' || true
echo
echo "== occurrences of the string '"university"' under contracts/ =="
rg -n --hidden -S -g 'contracts/' '"university"' || true
echo
echo "== occurrences of disputeTemplateRegistryUniversityAddress =="
rg -n --hidden -S -g 'contracts/
' 'disputeTemplateRegistryUniversityAddress' || true
echo
echo "== show devnet.viem.ts snippet around disputeTemplateRegistryUniversityConfig =="
sed -n '9888,9906p' contracts/deployments/devnet.viem.ts || true
echo
echo "== show first 140 lines of contracts/deployments/contractsViem.ts =="
sed -n '1,140p' contracts/deployments/contractsViem.ts || true
echo
echo "== try to find any 'deployments["university"]' or "deployments['university']" usages =="
rg -n -F --hidden -g 'contracts/' 'deployments["university"]' || true
rg -n -F --hidden -g 'contracts/
' "deployments['university']" || true


</blockquote></details>
<details>
<summary>contracts/deployments/contractsEthers.ts (4)</summary><blockquote>

`21-23`: **LGTM: University DTR alias import added**

Import aligns with the new university deployment path.

---

`171-173`: **LGTM: Narrowed unions to non‑Neo variants**

Union types now reflect University vs standard core/sortition only.

---

`248-250`: **LGTM: Mainnet connects via standard factories**

Correct switch to KlerosCore__factory and SortitionModule__factory for mainnet.

---

`42-48`: **Mainnet consolidation — confirm no remaining Neo factories/configs referenced**

rg in the sandbox returned "No files were searched"; cannot verify. Run from the repo root and paste the output:

rg -nP '(KlerosCoreNeo|SortitionModuleNeo|__factoryNeo|mainnetNeo|NeoConfig)' --hidden --no-ignore-vcs -S --glob '!node_modules/**' .

If there are no matches, resolve.

</blockquote></details>
<details>
<summary>contracts/hardhat.config.ts (1)</summary><blockquote>

`18-18`: **Script rename: good.**

Switch to changeOwner script aligns with owner-based governance.

</blockquote></details>
<details>
<summary>contracts/deploy/00-home-chain-arbitration-university.ts (3)</summary><blockquote>

`27-33`: **Good: deploy registry via upgradable path.**

Owner-initialized, consistent with owner model.

---

`37-39`: **Initializer args look correct.**

[owner, core=ZeroAddress, wNative, jumpID=1] matches new signature.


Please confirm DisputeKitClassicUniversity.initialize signature matches these four params.

---

`73-79`: **changeCore sequencing: fine.**

Only call when needed; good.

</blockquote></details>
<details>
<summary>contracts/src/proxy/KlerosProxies.sol (1)</summary><blockquote>

`34-36`: **Add University registry proxy: LGTM.**

Keeps explorer names meaningful; mirrors existing pattern.

</blockquote></details>
<details>
<summary>contracts/src/arbitration/dispute-kits/DisputeKitGated.sol (1)</summary><blockquote>

`41-53`: **Initializer shift to owner: good.**

Signature and _authorizeUpgrade guard align with the owner model.

</blockquote></details>
<details>
<summary>contracts/deployments/arbitrumSepoliaDevnet/SortitionModuleUniversity_Proxy.json (1)</summary><blockquote>

`58-61`: **Confirm initializer maps to (owner, core).**

'cast' was not available in the execution environment; run locally:
cast calldata decode 'initialize(address,address)' 0x485cc955000000000000000000000000f1c7c037891525e360c59f708739ac09a7670c59000000000000000000000000a34dbbd0e5e1d09bd683455f9dbc393797bc558f
Confirm it decodes to (owner, core) and that the second address equals the KlerosCoreUniversity proxy you computed. File: contracts/deployments/arbitrumSepoliaDevnet/SortitionModuleUniversity_Proxy.json (args at lines 58-61).

</blockquote></details>
<details>
<summary>contracts/deployments/arbitrumSepoliaDevnet/KlerosCoreUniversity_Proxy.json (1)</summary><blockquote>

`101-104`: **Sanity-check initializer calldata and argument order**

Confirm selector 0xe399d29b decodes to
initialize(address,address,address,address,address,bool,uint256[4],uint256[4],address)
and that the calldata in contracts/deployments/arbitrumSepoliaDevnet/KlerosCoreUniversity_Proxy.json (lines 101–104) maps, in order, to: owner, instructor, PNK, wNative, DisputeKit, bool flag, uint256[4] configA, uint256[4] configB, sortitionModule. Run locally and paste the decoded output:

cast calldata decode 'initialize(address,address,address,address,address,bool,uint256[4],uint256[4],address)' 0xe399d29b000000000000000000000000f1c7c037891525e360c59f708739ac09a7670c59000000000000000000000000f1c7c037891525e360c59f708739ac09a7670c5900000000000000000000000034b944d42cacfc8266955d07a80181d2054aa225000000000000000000000000000000000000000000000000000000000000000000000000000000000000000082f2089442979a6b56c80274d144575980092f91000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ad78ebc5ac62000000000000000000000000000000000000000000000000000000000000000002710000000000000000000000000000000000000000000000000016345785d8a00000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000009f55804177e7e44e558616cd7d06b865788214ca

or

node -e "const {utils}=require('ethers'); const iface=new utils.Interface(['function initialize(address,address,address,address,address,bool,uint256[4],uint256[4],address)']); console.log(JSON.stringify(iface.decodeFunctionData('initialize','0xe399d29b000000000000000000000000f1c7c037891525e360c59f708739ac09a7670c59000000000000000000000000f1c7c037891525e360c59f708739ac09a7670c5900000000000000000000000034b944d42cacfc8266955d07a80181d2054aa225000000000000000000000000000000000000000000000000000000000000000000000000000000000000000082f2089442979a6b56c80274d144575980092f91000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ad78ebc5ac62000000000000000000000000000000000000000000000000000000000000000002710000000000000000000000000000000000000000000000000016345785d8a00000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000009f55804177e7e44e558616cd7d06b865788214ca')));" 

Paste decoded output in PR comment.

</blockquote></details>
<details>
<summary>contracts/deployments/arbitrumSepoliaDevnet/DisputeKitClassicUniversity_Proxy.json (1)</summary><blockquote>

`58-61`: **Verify DisputeKit initializer encoding.**

Confirm selector 0xcf756fdf == initialize(address owner, KlerosCore core, address wNative, uint256 jumpDisputeKitID) and that the args decode to [deployer, 0x0000000000000000000000000000000000000000, WETH, 1] (contracts/deployments/arbitrumSepoliaDevnet/DisputeKitClassicUniversity_Proxy.json — args at lines 58–61).

Re-run locally:
```shell
cast calldata decode 'initialize(address,address,address,uint256)' \
0xcf756fdf000000000000000000000000f1c7c037891525e360c59f708739ac09a7670c5900000000000000000000000000000000000000000000000000000000000000000000000000000000000000003829a2486d53ee984a0ca2d76552715726b771380000000000000000000000000000000000000000000000000000000000000001

Fallback (if cast missing):

python3 - <<'PY'
import sys,subprocess
try:
  from eth_utils import keccak, to_checksum_address
  from eth_abi import decode_abi
except:
  subprocess.check_call([sys.executable,'-m','pip','install','eth-abi','eth-utils'])
  from eth_utils import keccak, to_checksum_address
  from eth_abi import decode_abi
calldata="0xcf756fdf000000000000000000000000f1c7c037891525e360c59f708739ac09a7670c5900000000000000000000000000000000000000000000000000000000000000000000000000000000000000003829a2486d53ee984a0ca2d76552715726b771380000000000000000000000000000000000000000000000000000000000000001"
data=bytes.fromhex(calldata[2:])
print("selector: 0x"+data[:4].hex())
print("expected: 0x"+keccak(text="initialize(address,address,address,uint256)")[:4].hex())
owner,core,wNative,jumpID = decode_abi(['address','address','address','uint256'], data[4:])
print("owner:", to_checksum_address('0x'+owner.hex()))
print("core:", to_checksum_address('0x'+core.hex()))
print("wNative:", to_checksum_address('0x'+wNative.hex()))
print("jumpDisputeKitID:", jumpID)
PY
contracts/deployments/arbitrumSepoliaDevnet/DisputeResolverUniversity.json (3)

20-44: Owner-based access surface looks consistent.

New OwnerOnly error and owner-centric ABI changes align with the repo-wide governor→owner refactor. No issues spotted here.


241-252: owner() getter presence confirmed.

ABI includes owner() as view; consistent with the refactor.


285-300: Deployment metadata sanity-check — templateRegistry mismatch

  • contracts/deployments/arbitrumSepoliaDevnet/DisputeResolverUniversity.json — args[0]=0xA34dBBD0E5e1d09bd683455f9dbC393797BC558f, args[1]=0xc852F94f90E3B06Da6eCfB61d76561ECfb94613f
  • contracts/deployments/**/KlerosCoreUniversity.json — address=0xA34dBBD0E5e1d09bd683455f9dbC393797BC558f (matches args[0])
  • contracts/deployments/**/DisputeTemplateRegistryUniversity.json — address=0x75A5D16e9A699162506E4d79D68CF646e6600ba1 (does NOT match args[1])

Confirm which templateRegistry address is intended and update either args[1] in DisputeResolverUniversity.json or the DisputeTemplateRegistryUniversity artifact.

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

31-32: Version constant OK.

Version string override present and public; matches owner-based V2 surface.


202-227: Bounds/format checks in __extraDataToTokenInfo look fine.

160-byte length guard aligns with the offsets you load (0x80 and 0xA0).


44-45: ```shell
#!/bin/bash
set -euo pipefail

echo "=== List some .sol files (top 200) ==="
if command -v fd >/dev/null 2>&1; then
fd -e sol --hidden --no-ignore | sed -n '1,200p' || true
else
find . -name '*.sol' | sed -n '1,200p' || true
fi

echo
echo "=== Locate DisputeKitGatedShutter.sol ==="
SOL_PATHS="$(fd -e sol DisputeKitGatedShutter.sol || find . -name 'DisputeKitGatedShutter.sol' -print || true)"
if [ -z "$SOL_PATHS" ]; then
echo "Not found by name search; listing any files under contracts/src/arbitration/dispute-kits/"
find contracts/src/arbitration/dispute-kits -maxdepth 2 -type f -name '*.sol' -print || true
else
echo "$SOL_PATHS"
fi

echo
echo "=== Print DisputeKitGatedShutter.sol (if found) ==="
if [ -n "$SOL_PATHS" ]; then
echo "$SOL_PATHS" | while IFS= read -r f; do
echo "---- $f ----"
sed -n '1,400p' "$f" || true
done
fi

echo
echo "=== Search for 'callerIsJuror' ==="
rg -n --hidden --no-ignore "callerIsJuror" -S || true

echo
echo "=== Search for 'castVoteShutter' and 'castVote' occurrences ==="
rg -n --hidden --no-ignore "castVoteShutter" -S -C3 || rg -n --hidden --no-ignore "castVote" -S -C2 || true

echo
echo "=== Search for '_castVote' definitions and calls (show context) ==="
rg -n --hidden --no-ignore "function _castVote" -S -C8 || rg -n --hidden --no-ignore "_castVote(" -S -C6 || true

echo
echo "=== Search for 'hashVote' and '_getExpectedVoteHash' occurrences ==="
rg -n --hidden --no-ignore "hashVote" -S -C3 || true
rg -n --hidden --no-ignore "_getExpectedVoteHash" -S -C3 || true

echo
echo "=== Search for 'recoveryCommitments' usage ==="
rg -n --hidden --no-ignore "recoveryCommitments" -S -C3 || true

echo
echo "=== Done ==="


</blockquote></details>
<details>
<summary>contracts/deployments/arbitrumSepoliaDevnet/SortitionModuleUniversity.json (7)</summary><blockquote>

`33-52`: **Access-control errors added (KlerosCoreOnly/OwnerOnly) are consistent.**

Matches the owner-centric governance pattern. No issues.

---

`499-510`: **owner() visibility via proxy ABI—OK.**

Owner getter exposed; aligns with initialize(owner, core).

---

`744-755`: **version() getter present.**

Expose semantic version to aid client compatibility checks.

---

`825-832`: **Resolved — proxy initialize wiring matches KlerosCoreUniversity address**  
SortitionModuleUniversity.execute.args[1] equals KlerosCoreUniversity.address: 0xA34dBBD0E5e1d09bd683455f9dbC393797BC558f.

---

`234-250`: ```shell
#!/bin/bash
set -euo pipefail
echo "1) repo-wide search for createTree occurrences (calls, ABI names, artifacts)..."
rg -n --hidden -S -C3 '\bcreateTree\s*\(' || true
rg -n --hidden -S -C3 '"name"\s*:\s*"createTree"' || true

echo "2) search for courtID variants and uint96/uint256 usages..."
rg -n --hidden -S -C3 '\b(courtID|courtId|_courtID|_courtId)\b' || true
rg -n --hidden -S -C3 '\buint(96|256)\b' || true

echo "3) search common script/deploy folders for callers (ts/js/py)..."
rg -n --hidden -S -g 'scripts/**' -g 'deploy/**' -g 'deployments/**' -C3 '\bcreateTree\b|\b(courtID|courtId|_courtID|_courtId)\b' || true

echo "4) look under contracts/deployments for the SortitionModuleUniversity artifact and show up to first 260 lines if present..."
if rg -n --hidden -S -g 'contracts/deployments/**/SortitionModuleUniversity.json' -l >/dev/null 2>&1; then
  rg -n --hidden -S -g 'contracts/deployments/**/SortitionModuleUniversity.json' -n || true
  matches=$(rg -n --hidden -S -g 'contracts/deployments/**/SortitionModuleUniversity.json' -l 'createTree|_courtID|uint96|uint256' || true)
  if [ -n "$matches" ]; then
    echo "Artifact files with relevant tokens:"
    printf '%s\n' "$matches"
    while IFS= read -r f; do
      echo "----- $f -----"
      sed -n '1,260p' "$f" || true
    done <<< "$matches"
  else
    echo "No direct matches for createTree/_courtID/uint96/uint256 inside SortitionModuleUniversity artifact(s). Showing file head(s) if present:"
    for f in $(rg -n --hidden -S -g 'contracts/deployments/**/SortitionModuleUniversity.json' -l || true); do
      echo "----- $f -----"
      sed -n '1,260p' "$f" || true
    done
  fi
else
  echo "No SortitionModuleUniversity.json artifact found under contracts/deployments."
fi

echo "Done."

282-297: ```shell
#!/bin/bash
set -euo pipefail

echo "=== Show DrawButton.tsx (1-240) ==="
sed -n '1,240p' web/src/pages/Cases/CaseDetails/MaintenanceButtons/DrawButton.tsx || true

echo
echo "=== All occurrences of 'draw(' in web/ (with context) ==="
rg -n --hidden -S 'draw(' web -g 'web/**' -C3 || true

echo
echo "=== Search for 'drawnAddress' or 'fromSubcourtID' anywhere ==="
rg -n --hidden -S 'drawnAddress|fromSubcourtID' || true

echo
echo "=== Search for definitions/imports named 'draw' in web/ ==="
rg -n --hidden -S '(^|\W)(function|const|let|export)\s+draw\b|import\s+\{[^}]*\bdraw\b' web -g 'web/**' -C2 || true


---

`82-119`: ```shell
#!/bin/bash
set -euo pipefail
echo "rg version:" $(rg --version | head -n1 2>/dev/null || true)

echo "Searching repository for LeftoverPNK / withdrawLeftoverPNK symbols..."
rg -n -S --hidden --no-ignore 'withdrawLeftoverPNK|LeftoverPNKWithdrawn|LeftoverPNK' || true

echo
echo "Showing matches with context (6 lines):"
rg -n -C6 -S --hidden --no-ignore 'withdrawLeftoverPNK|LeftoverPNKWithdrawn|LeftoverPNK' || true

JSON_PATH='contracts/deployments/arbitrumSepoliaDevnet/SortitionModuleUniversity.json'
if [ -f "$JSON_PATH" ]; then
  echo
  echo "Printing $JSON_PATH (lines 1-240):"
  sed -n '1,240p' "$JSON_PATH" || true
else
  echo
  echo "$JSON_PATH not found"
fi
contracts/deployments/arbitrumSepoliaDevnet/KlerosCoreUniversity.json (5)

138-147: OwnerOnly/OwnerOrInstructorOnly introduced—matches refactor.

Error surface aligns with instructor/owner roles.


1713-1724: owner() getter presence confirmed.

ABI aligns with owner model.


292-301: ```shell
#!/bin/bash
set -euo pipefail
echo "=== Search: 'CourtCreated' ==="
rg -n --hidden -S '\bCourtCreated\b' -C3 || true

echo
echo "=== Search: 'emit CourtCreated(' ==="
rg -n --hidden -S 'emit\s+CourtCreated(' -C3 || true

echo
echo "=== Search: bare 'CourtCreated(' occurrences ==="
rg -n --hidden -S 'CourtCreated(' -C3 || true

echo
echo "=== Search: 'uint96' usages ==="
rg -n --hidden -S '\buint96\b' -C3 || true

echo
echo "=== Search: common court id identifiers (courtId, courtID, court_id) ==="
rg -n --hidden -S '\bcourtId\b' -C3 || true
rg -n --hidden -S '\bcourtID\b' -C3 || true
rg -n --hidden -S '\bcourt_id\b' -C3 || true

echo
echo "=== Search: explicit casts to uint96 ==="
rg -n --hidden -S 'uint96(' -C3 || true

echo
echo "=== Search: ABI/JSON files mentioning CourtCreated ==="
rg -n --hidden -S '"CourtCreated"' -g 'contracts/**' -C2 || true

echo
echo "=== Completed searches ==="


---

`1966-1987`: **Cross-artifact wiring check (sortition module) — verified match.**

initialize's last arg equals SortitionModuleUniversity proxy address 0x9f55804177e7E44E558616cD7d06B865788214cA; no change required.

---

`1532-1561`: ```shell
#!/bin/bash
set -euo pipefail

echo "1) Searching for 'struct Round' occurrences..."
rg -n --hidden --no-ignore "struct Round" || true

echo
echo "2) Searching for KlerosCoreUniversity references and Round-related fields..."
rg -n --hidden --no-ignore "KlerosCoreUniversity|drawnJurors|drawnJurorFromCourtIDs|sumFeeRewardPaid|sumPnkRewardPaid|feeToken|drawIterations|Round\\[\\]" -S || true

echo
echo "3) Looking for deployments JSON named KlerosCoreUniversity*.json under contracts/ ..."
find contracts -type f -name 'KlerosCoreUniversity*.json' -print || true

echo
echo "4) If deployment JSON(s) found, print lines ~1520-1568 to inspect stored snippet(s)..."
for f in $(find contracts -type f -name 'KlerosCoreUniversity*.json' 2>/dev/null || true); do
  echo "---- $f ----"
  sed -n '1500,1570p' "$f" || true
done

echo
echo "5) Print nearby context for each file that defines 'struct Round' (first 200 lines)."
for file in $(rg -n --hidden --no-ignore "struct Round" -S --files || true); do
  echo "----- $file -----"
  sed -n '1,220p' "$file" || true
done

echo
echo "Done. If you want, run your storage layout tool (hardhat/forge) to diff the Round struct across compiled versions/artifacts (e.g. hardhat-storage-layout plugin / foundry's forge inspect)."
contracts/src/arbitration/devtools/KlerosCoreRuler.sol (2)

16-16: Version constant OK.

Public constant override fulfills the abstract version() requirement.


317-330: Event emits AcceptedFeeToken/NewCurrencyRate are valid via interface.

No need to redeclare; emitting events defined in IArbitratorV2 is fine.

contracts/deployments/arbitrumSepoliaDevnet/DisputeKitClassicUniversity.json (1)

1362-1366: Initialization wired with core = 0x0 — confirm post-deploy changeCore call.

File: contracts/deployments/arbitrumSepoliaDevnet/DisputeKitClassicUniversity.json
Lines: 1362-1366

  "0xf1C7c037891525E360C59f708739Ac09A7670c59",
  "0x0000000000000000000000000000000000000000",
  "0x3829A2486d53ee984a0ca2D76552715726b77138",
  1
]

Proxy was initialized with _core = 0x0. Repo search shows changeCore exists (contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol and contracts/src/arbitration/view/KlerosCoreSnapshotProxy.sol) and is exercised in tests (contracts/test/foundry/KlerosCore_Staking.t.sol at the changeCore calls), but I found no deployment script or post-deploy step in this repo that calls changeCore for this deployment. Ensure your deployment flow calls changeCore(...) immediately after initialize, otherwise Core-only checks will fail.

contracts/README.md (1)

93-93: Verify DisputeResolverUniversity address in README matches deployment artifact

contracts/README.md (line 93) lists DisputeResolverUniversity = 0x8a7902Ef9a5308C7DF0A68A28EEDd6D83436993D — confirm it equals the "address" field in deployments/arbitrumSepolia/DisputeResolverUniversity.json (or equivalent under deployments/*/arbitrumSepolia). Command to check: jq -r '.address' deployments/arbitrumSepolia/DisputeResolverUniversity.json. If different, update README or the artifact. Automated verification failed (script errors); verify manually or provide artifact path to re-run.

contracts/scripts/utils/contracts.ts (4)

31-34: Docs: coreType description reads clearly.

No issues.


75-79: Docs: network‑inferred contracts description — LGTM.


37-46: Verify DisputeTemplateRegistry artifact & TypeChain export (exact casing)

Hardhat artifacts/typechain-types were not found in the sandbox — confirm your build output contains artifacts/.../DisputeTemplateRegistry.json and that TypeChain exports a type named DisputeTemplateRegistry (exact case) so the BASE mapping in contracts/scripts/utils/contracts.ts resolves at runtime. Regenerate artifacts/typechain or adjust the mapping if not present.


48-56: ```shell
#!/bin/bash
set -euo pipefail
echo "PWD: $(pwd)"
echo "Git branch: $(git rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
echo

Locate directories under contracts/deployments that contain both JSONs

dirs=()
for base in contracts/deployments/*/DisputeTemplateRegistry.json; do
[ -e "$base" ] || continue
dir=$(dirname "$base")
uni="$dir/DisputeTemplateRegistryUniversity.json"
if [ -f "$uni" ]; then
dirs+=("$dir")
fi
done

if [ ${#dirs[@]} -eq 0 ]; then
echo "No directories found with both DisputeTemplateRegistry.json and DisputeTemplateRegistryUniversity.json under contracts/deployments."
else
for dir in "${dirs[@]}"; do
echo "PAIR: $dir"
base="$dir/DisputeTemplateRegistry.json"
uni="$dir/DisputeTemplateRegistryUniversity.json"
echo "Files:"
ls -l "$base" "$uni" || true
echo "Extracting .abi from each (jq -cS) and comparing..."
jq -cS '.abi' "$base" > /tmp/dtr.base.abi || { echo "jq failed on $base"; continue; }
jq -cS '.abi' "$uni" > /tmp/dtr.uni.abi || { echo "jq failed on $uni"; continue; }
if cmp -s /tmp/dtr.base.abi /tmp/dtr.uni.abi; then
echo "RESULT: ABIs IDENTICAL"
else
echo "RESULT: ABIs DIFFER"
echo "DIFF (unified):"
diff -u /tmp/dtr.base.abi /tmp/dtr.uni.abi || true
fi
echo "----"
done
fi

echo
echo "All DisputeTemplateRegistry JSON files under contracts/deployments:"
ls -1 contracts/deployments//DisputeTemplateRegistry.json || true

echo
echo "Searching repository (grep) for DisputeTemplateRegistry mentions (unrestricted, excluding node_modules/.git):"
grep -RIn --exclude-dir=node_modules --exclude-dir=.git -E "DisputeTemplateRegistryUniversity|DisputeTemplateRegistry" . || true

echo
echo "Attempting to locate contracts/scripts/utils/contracts.ts and print first 240 lines if present:"
find . -type f -path "*/contracts/scripts/utils/contracts.ts" -print -exec sed -n '1,240p' {} ; || true


</blockquote></details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Comment on lines +114 to +125
"inputs": [
{
"internalType": "address",
"name": "_owner",
"type": "address"
}
],
"name": "changeOwner",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Guard owner setter against zero address.

changeOwner(address _owner) in the underlying implementation (see embedded source) allows setting owner to address(0), which can brick upgrades/governance.

Apply in src/arbitration/DisputeTemplateRegistry.sol:

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

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

🤖 Prompt for AI Agents
In
contracts/deployments/arbitrumSepoliaDevnet/DisputeTemplateRegistryUniversity_Implementation.json
around lines 114-125, the changeOwner(address _owner) ABI maps to an
implementation that currently allows setting owner to address(0); update the
implementation in src/arbitration/DisputeTemplateRegistry.sol so changeOwner
validates the new owner by adding a require(_owner != address(0),
"DisputeTemplateRegistry: new owner is the zero address"); then assign the owner
and emit any existing OwnershipTransferred/event as before to prevent bricking
upgrades/governance.

Comment on lines +126 to +138
{
"inputs": [
{
"internalType": "address",
"name": "_owner",
"type": "address"
}
],
"name": "initialize",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
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

Add zero-address check in initializer.

initialize(address _owner) should reject zero to avoid deploying an unusable proxy.

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

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

🤖 Prompt for AI Agents
In
contracts/deployments/arbitrumSepoliaDevnet/DisputeTemplateRegistryUniversity_Implementation.json
around lines 126 to 138, the initialize(address _owner) ABI entry lacks a
zero-address validation; update the implementation of initialize to revert if
_owner == address(0) (e.g., require(_owner != address(0), "Owner cannot be zero
address")) so proxies cannot be initialized with the zero address and become
unusable.

Comment on lines 804 to 814
if (_params.repartition == _params.numberOfVotesInRound - 1 && _params.coherentCount == 0) {
// No one was coherent, send the rewards to the governor.
// No one was coherent, send the rewards to the owner.
if (round.feeToken == NATIVE_CURRENCY) {
// The dispute fees were paid in ETH
payable(governor).send(round.totalFeesForJurors);
payable(owner).send(round.totalFeesForJurors);
} else {
// The dispute fees were paid in ERC20
round.feeToken.safeTransfer(governor, round.totalFeesForJurors);
round.feeToken.safeTransfer(owner, round.totalFeesForJurors);
}
pinakion.safeTransfer(governor, _params.pnkPenaltiesInRound);
pinakion.safeTransfer(owner, _params.pnkPenaltiesInRound);
emit LeftoverRewardSent(
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

Do not use .send for ETH transfers; check success.

.send caps gas and ignores failures. Use low-level call (or a SafeSend helper) and revert on failure.

-                payable(owner).send(round.totalFeesForJurors);
+                (bool okFee, ) = payable(owner).call{value: round.totalFeesForJurors}("");
+                if (!okFee) revert UnsuccessfulCall();
@@
-            payable(account).send(feeReward);
+            (bool ok, ) = payable(account).call{value: feeReward}("");
+            if (!ok) revert UnsuccessfulCall();
@@
-                        payable(owner).send(leftoverFeeReward);
+                        (bool ok2, ) = payable(owner).call{value: leftoverFeeReward}("");
+                        if (!ok2) revert UnsuccessfulCall();

Also applies to: 861-869, 895-901

Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
1 Security Hotspot
C Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

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

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

Successfully merging this pull request may close these issues.

4 participants