From 4594536c5f9ed5fbfe489dfb9dbef6d23766d8e2 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Tue, 5 Aug 2025 13:23:03 +0100 Subject: [PATCH 01/46] chore: changed contracts viem dependency as peer dependency --- contracts/package.json | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/contracts/package.json b/contracts/package.json index fc719eb7f..239c1665c 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -121,7 +121,7 @@ "@types/mocha": "^10.0.10", "@types/node": "^20.17.6", "@types/sinon": "^17.0.4", - "@wagmi/cli": "^2.2.0", + "@wagmi/cli": "^2.3.2", "abitype": "^0.10.3", "chai": "^4.5.0", "dotenv": "^16.6.1", @@ -157,7 +157,14 @@ "@kleros/vea-contracts": "^0.6.0", "@openzeppelin/contracts": "^5.4.0", "@shutter-network/shutter-sdk": "0.0.2", - "isomorphic-fetch": "^3.0.0", + "isomorphic-fetch": "^3.0.0" + }, + "peerDependencies": { "viem": "^2.24.1" + }, + "peerDependenciesMeta": { + "viem": { + "optional": false + } } } From 5a81f9ecf9e4d182284fcf1a99f5aaadda81fbef Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Tue, 5 Aug 2025 13:24:53 +0100 Subject: [PATCH 02/46] feat: dispute kit helper --- contracts/deployments/disputeKitsViem.ts | 85 ++++++++++++++++++++++++ contracts/deployments/index.ts | 3 + contracts/scripts/getDisputeKits.ts | 33 +++++++++ 3 files changed, 121 insertions(+) create mode 100644 contracts/deployments/disputeKitsViem.ts create mode 100644 contracts/scripts/getDisputeKits.ts diff --git a/contracts/deployments/disputeKitsViem.ts b/contracts/deployments/disputeKitsViem.ts new file mode 100644 index 000000000..45ae23998 --- /dev/null +++ b/contracts/deployments/disputeKitsViem.ts @@ -0,0 +1,85 @@ +import { getContracts } from "./contractsViem"; +import { Abi, AbiEvent, getAbiItem, PublicClient } from "viem"; +import { DeploymentName } from "./utils"; + +export type DisputeKitContracts = ReturnType; +export type DisputeKit = + | NonNullable + | NonNullable + | NonNullable + | NonNullable + | null; +export type DisputeKitInfos = { + address: `0x${string}`; + contract: DisputeKit; + isGated: boolean; + isShutter: boolean; +}; +export type DisputeKitByIds = Record; + +const fetchDisputeKits = async (client: PublicClient, klerosCoreAddress: `0x${string}`, klerosCoreAbi: Abi) => { + const DisputeKitCreated = getAbiItem({ + abi: klerosCoreAbi, + name: "DisputeKitCreated", + }) as AbiEvent; + const logs = await client.getLogs({ + address: klerosCoreAddress, + event: DisputeKitCreated, + fromBlock: 0n, + toBlock: "latest", + }); + return Object.fromEntries( + logs + .filter((log) => { + const args = log.args as Record; + return "_disputeKitID" in args && "_disputeKitAddress" in args; + }) + .map((log) => { + const { _disputeKitID, _disputeKitAddress } = log.args as { + _disputeKitID: bigint; + _disputeKitAddress: string; + }; + return { + disputeKitID: _disputeKitID, + disputeKitAddress: _disputeKitAddress, + }; + }) + .map(({ disputeKitID, disputeKitAddress }) => [disputeKitID!.toString(), disputeKitAddress as `0x${string}`]) + ); +}; + +export const getDisputeKits = async (client: PublicClient, deployment: DeploymentName): Promise => { + const { klerosCore, disputeKitClassic, disputeKitShutter, disputeKitGated, disputeKitGatedShutter } = getContracts({ + publicClient: client, + deployment: deployment, + }); + + const isDefined = (kit: T): kit is NonNullable => kit != null; + const disputeKitContracts = [disputeKitClassic, disputeKitShutter, disputeKitGated, disputeKitGatedShutter].filter( + isDefined + ); + const shutterEnabled = [disputeKitShutter, disputeKitGatedShutter].filter(isDefined); + const gatedEnabled = [disputeKitGated, disputeKitGatedShutter].filter(isDefined); + + const disputeKitMap = await fetchDisputeKits(client, klerosCore.address, klerosCore.abi); + + return Object.fromEntries( + Object.entries(disputeKitMap).map(([disputeKitID, address]) => { + const contract = + disputeKitContracts.find((contract) => contract.address.toLowerCase() === address.toLowerCase()) ?? null; + return [ + disputeKitID, + { + address, + contract: contract satisfies DisputeKit, + isGated: contract + ? gatedEnabled.some((gated) => contract.address.toLowerCase() === gated.address.toLowerCase()) + : false, + isShutter: contract + ? shutterEnabled.some((shutter) => contract.address.toLowerCase() === shutter.address.toLowerCase()) + : false, + }, + ]; + }) + ); +}; diff --git a/contracts/deployments/index.ts b/contracts/deployments/index.ts index 3479c5edf..c94968751 100644 --- a/contracts/deployments/index.ts +++ b/contracts/deployments/index.ts @@ -17,3 +17,6 @@ export * from "./utils"; // Contracts getters export { getContracts as getContractsEthers } from "./contractsEthers"; export { getContracts as getContractsViem } from "./contractsViem"; + +// Dispute kits getters +export { getDisputeKits as getDisputeKitsViem, type DisputeKitByIds, type DisputeKitInfos } from "./disputeKitsViem"; diff --git a/contracts/scripts/getDisputeKits.ts b/contracts/scripts/getDisputeKits.ts new file mode 100644 index 000000000..32f2b18eb --- /dev/null +++ b/contracts/scripts/getDisputeKits.ts @@ -0,0 +1,33 @@ +import { getDisputeKits } from "../deployments/disputeKitsViem"; +import { createPublicClient, http } from "viem"; +import { arbitrumSepolia } from "viem/chains"; + +const rpc = process.env.ARBITRUM_SEPOLIA_RPC; +if (!rpc) { + throw new Error("ARBITRUM_SEPOLIA_RPC is not set"); +} + +const client = createPublicClient({ + chain: arbitrumSepolia, + transport: http(rpc), +}); + +async function main() { + try { + console.log("Fetching DisputeKitCreated events..."); + const disputeKitResult = await getDisputeKits(client, "devnet"); + console.log(disputeKitResult); + } catch (error) { + console.error("Error fetching events:", error); + throw error; + } +} + +if (require.main === module) { + main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); +} From 4c2277baabaa898fbf8af75b91448472241f069c Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Tue, 5 Aug 2025 13:29:23 +0100 Subject: [PATCH 03/46] chore: override viem resolution from viem@npm:2.x to npm:^2.23.2 because of @wagmi/cli --- package.json | 3 +- yarn.lock | 367 +++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 345 insertions(+), 25 deletions(-) diff --git a/package.json b/package.json index 6c9deb2d4..01e9468a1 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,8 @@ "elliptic@npm:6.5.4": "npm:6.6.1", "word-wrap@npm:~1.2.3": "npm:1.2.5", "@codemirror/state": "npm:6.5.2", - "undici@npm:7.3.0": "npm:7.5.0" + "undici@npm:7.3.0": "npm:7.5.0", + "viem@npm:2.x": "npm:^2.23.2" }, "scripts": { "check-prerequisites": "scripts/check-prerequisites.sh", diff --git a/yarn.lock b/yarn.lock index 166200247..0b2eacdf0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3065,6 +3065,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/aix-ppc64@npm:0.25.8": + version: 0.25.8 + resolution: "@esbuild/aix-ppc64@npm:0.25.8" + conditions: os=aix & cpu=ppc64 + languageName: node + linkType: hard + "@esbuild/android-arm64@npm:0.19.12": version: 0.19.12 resolution: "@esbuild/android-arm64@npm:0.19.12" @@ -3079,6 +3086,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-arm64@npm:0.25.8": + version: 0.25.8 + resolution: "@esbuild/android-arm64@npm:0.25.8" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/android-arm@npm:0.19.12": version: 0.19.12 resolution: "@esbuild/android-arm@npm:0.19.12" @@ -3093,6 +3107,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-arm@npm:0.25.8": + version: 0.25.8 + resolution: "@esbuild/android-arm@npm:0.25.8" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + "@esbuild/android-x64@npm:0.19.12": version: 0.19.12 resolution: "@esbuild/android-x64@npm:0.19.12" @@ -3107,6 +3128,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-x64@npm:0.25.8": + version: 0.25.8 + resolution: "@esbuild/android-x64@npm:0.25.8" + conditions: os=android & cpu=x64 + languageName: node + linkType: hard + "@esbuild/darwin-arm64@npm:0.19.12": version: 0.19.12 resolution: "@esbuild/darwin-arm64@npm:0.19.12" @@ -3121,6 +3149,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/darwin-arm64@npm:0.25.8": + version: 0.25.8 + resolution: "@esbuild/darwin-arm64@npm:0.25.8" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/darwin-x64@npm:0.19.12": version: 0.19.12 resolution: "@esbuild/darwin-x64@npm:0.19.12" @@ -3135,6 +3170,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/darwin-x64@npm:0.25.8": + version: 0.25.8 + resolution: "@esbuild/darwin-x64@npm:0.25.8" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@esbuild/freebsd-arm64@npm:0.19.12": version: 0.19.12 resolution: "@esbuild/freebsd-arm64@npm:0.19.12" @@ -3149,6 +3191,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/freebsd-arm64@npm:0.25.8": + version: 0.25.8 + resolution: "@esbuild/freebsd-arm64@npm:0.25.8" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/freebsd-x64@npm:0.19.12": version: 0.19.12 resolution: "@esbuild/freebsd-x64@npm:0.19.12" @@ -3163,6 +3212,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/freebsd-x64@npm:0.25.8": + version: 0.25.8 + resolution: "@esbuild/freebsd-x64@npm:0.25.8" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/linux-arm64@npm:0.19.12": version: 0.19.12 resolution: "@esbuild/linux-arm64@npm:0.19.12" @@ -3177,6 +3233,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-arm64@npm:0.25.8": + version: 0.25.8 + resolution: "@esbuild/linux-arm64@npm:0.25.8" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/linux-arm@npm:0.19.12": version: 0.19.12 resolution: "@esbuild/linux-arm@npm:0.19.12" @@ -3191,6 +3254,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-arm@npm:0.25.8": + version: 0.25.8 + resolution: "@esbuild/linux-arm@npm:0.25.8" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + "@esbuild/linux-ia32@npm:0.19.12": version: 0.19.12 resolution: "@esbuild/linux-ia32@npm:0.19.12" @@ -3205,6 +3275,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-ia32@npm:0.25.8": + version: 0.25.8 + resolution: "@esbuild/linux-ia32@npm:0.25.8" + conditions: os=linux & cpu=ia32 + languageName: node + linkType: hard + "@esbuild/linux-loong64@npm:0.19.12": version: 0.19.12 resolution: "@esbuild/linux-loong64@npm:0.19.12" @@ -3219,6 +3296,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-loong64@npm:0.25.8": + version: 0.25.8 + resolution: "@esbuild/linux-loong64@npm:0.25.8" + conditions: os=linux & cpu=loong64 + languageName: node + linkType: hard + "@esbuild/linux-mips64el@npm:0.19.12": version: 0.19.12 resolution: "@esbuild/linux-mips64el@npm:0.19.12" @@ -3233,6 +3317,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-mips64el@npm:0.25.8": + version: 0.25.8 + resolution: "@esbuild/linux-mips64el@npm:0.25.8" + conditions: os=linux & cpu=mips64el + languageName: node + linkType: hard + "@esbuild/linux-ppc64@npm:0.19.12": version: 0.19.12 resolution: "@esbuild/linux-ppc64@npm:0.19.12" @@ -3247,6 +3338,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-ppc64@npm:0.25.8": + version: 0.25.8 + resolution: "@esbuild/linux-ppc64@npm:0.25.8" + conditions: os=linux & cpu=ppc64 + languageName: node + linkType: hard + "@esbuild/linux-riscv64@npm:0.19.12": version: 0.19.12 resolution: "@esbuild/linux-riscv64@npm:0.19.12" @@ -3261,6 +3359,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-riscv64@npm:0.25.8": + version: 0.25.8 + resolution: "@esbuild/linux-riscv64@npm:0.25.8" + conditions: os=linux & cpu=riscv64 + languageName: node + linkType: hard + "@esbuild/linux-s390x@npm:0.19.12": version: 0.19.12 resolution: "@esbuild/linux-s390x@npm:0.19.12" @@ -3275,6 +3380,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-s390x@npm:0.25.8": + version: 0.25.8 + resolution: "@esbuild/linux-s390x@npm:0.25.8" + conditions: os=linux & cpu=s390x + languageName: node + linkType: hard + "@esbuild/linux-x64@npm:0.19.12": version: 0.19.12 resolution: "@esbuild/linux-x64@npm:0.19.12" @@ -3289,6 +3401,20 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-x64@npm:0.25.8": + version: 0.25.8 + resolution: "@esbuild/linux-x64@npm:0.25.8" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/netbsd-arm64@npm:0.25.8": + version: 0.25.8 + resolution: "@esbuild/netbsd-arm64@npm:0.25.8" + conditions: os=netbsd & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/netbsd-x64@npm:0.19.12": version: 0.19.12 resolution: "@esbuild/netbsd-x64@npm:0.19.12" @@ -3303,6 +3429,20 @@ __metadata: languageName: node linkType: hard +"@esbuild/netbsd-x64@npm:0.25.8": + version: 0.25.8 + resolution: "@esbuild/netbsd-x64@npm:0.25.8" + conditions: os=netbsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/openbsd-arm64@npm:0.25.8": + version: 0.25.8 + resolution: "@esbuild/openbsd-arm64@npm:0.25.8" + conditions: os=openbsd & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/openbsd-x64@npm:0.19.12": version: 0.19.12 resolution: "@esbuild/openbsd-x64@npm:0.19.12" @@ -3317,6 +3457,20 @@ __metadata: languageName: node linkType: hard +"@esbuild/openbsd-x64@npm:0.25.8": + version: 0.25.8 + resolution: "@esbuild/openbsd-x64@npm:0.25.8" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/openharmony-arm64@npm:0.25.8": + version: 0.25.8 + resolution: "@esbuild/openharmony-arm64@npm:0.25.8" + conditions: os=openharmony & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/sunos-x64@npm:0.19.12": version: 0.19.12 resolution: "@esbuild/sunos-x64@npm:0.19.12" @@ -3331,6 +3485,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/sunos-x64@npm:0.25.8": + version: 0.25.8 + resolution: "@esbuild/sunos-x64@npm:0.25.8" + conditions: os=sunos & cpu=x64 + languageName: node + linkType: hard + "@esbuild/win32-arm64@npm:0.19.12": version: 0.19.12 resolution: "@esbuild/win32-arm64@npm:0.19.12" @@ -3345,6 +3506,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-arm64@npm:0.25.8": + version: 0.25.8 + resolution: "@esbuild/win32-arm64@npm:0.25.8" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/win32-ia32@npm:0.19.12": version: 0.19.12 resolution: "@esbuild/win32-ia32@npm:0.19.12" @@ -3359,6 +3527,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-ia32@npm:0.25.8": + version: 0.25.8 + resolution: "@esbuild/win32-ia32@npm:0.25.8" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + "@esbuild/win32-x64@npm:0.19.12": version: 0.19.12 resolution: "@esbuild/win32-x64@npm:0.19.12" @@ -3373,6 +3548,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-x64@npm:0.25.8": + version: 0.25.8 + resolution: "@esbuild/win32-x64@npm:0.25.8" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@eslint-community/eslint-utils@npm:^4.2.0, @eslint-community/eslint-utils@npm:^4.4.0": version: 4.4.0 resolution: "@eslint-community/eslint-utils@npm:4.4.0" @@ -5955,7 +6137,7 @@ __metadata: "@types/mocha": "npm:^10.0.10" "@types/node": "npm:^20.17.6" "@types/sinon": "npm:^17.0.4" - "@wagmi/cli": "npm:^2.2.0" + "@wagmi/cli": "npm:^2.3.2" abitype: "npm:^0.10.3" chai: "npm:^4.5.0" dotenv: "npm:^16.6.1" @@ -5986,7 +6168,11 @@ __metadata: ts-node: "npm:^10.9.2" typechain: "npm:^8.3.2" typescript: "npm:^5.6.3" - viem: "npm:^2.24.1" + peerDependencies: + viem: ^2.24.1 + peerDependenciesMeta: + viem: + optional: false languageName: unknown linkType: soft @@ -10999,6 +11185,39 @@ __metadata: languageName: node linkType: hard +"@wagmi/cli@npm:^2.3.2": + version: 2.3.2 + resolution: "@wagmi/cli@npm:2.3.2" + dependencies: + abitype: "npm:^1.0.4" + bundle-require: "npm:^5.1.0" + cac: "npm:^6.7.14" + change-case: "npm:^5.4.4" + chokidar: "npm:4.0.1" + dedent: "npm:^0.7.0" + dotenv: "npm:^16.3.1" + dotenv-expand: "npm:^10.0.0" + esbuild: "npm:~0.25.4" + escalade: "npm:3.2.0" + fdir: "npm:^6.1.1" + nanospinner: "npm:1.2.2" + pathe: "npm:^1.1.2" + picocolors: "npm:^1.0.0" + picomatch: "npm:^3.0.0" + prettier: "npm:^3.0.3" + viem: "npm:2.x" + zod: "npm:^3.22.2" + peerDependencies: + typescript: ">=5.0.4" + peerDependenciesMeta: + typescript: + optional: true + bin: + wagmi: dist/esm/cli.js + checksum: 10/85c6b1d4960c6d080d067f7dbac34e6a27d822690d33ef6c131b7714f6be13c5e04b2dd506fce38992ee6183ebe0fb48160e30f4cf901a27ccf34fd1b2dae529 + languageName: node + linkType: hard + "@wagmi/connectors@npm:5.7.11, @wagmi/connectors@npm:>=5.7.11, @wagmi/connectors@npm:^5.7.11": version: 5.7.11 resolution: "@wagmi/connectors@npm:5.7.11" @@ -13621,6 +13840,17 @@ __metadata: languageName: node linkType: hard +"bundle-require@npm:^5.1.0": + version: 5.1.0 + resolution: "bundle-require@npm:5.1.0" + dependencies: + load-tsconfig: "npm:^0.2.3" + peerDependencies: + esbuild: ">=0.18" + checksum: 10/735e0220055b9bdac20bea48ec1e10dc3a205232c889ef54767900bebdc721959c4ccb221e4ea434d7ddcd693a8a4445c3d0598e4040ee313ce0ac3aae3e6178 + languageName: node + linkType: hard + "busboy@npm:1.6.0, busboy@npm:^1.6.0": version: 1.6.0 resolution: "busboy@npm:1.6.0" @@ -17150,6 +17380,95 @@ __metadata: languageName: node linkType: hard +"esbuild@npm:~0.25.4": + version: 0.25.8 + resolution: "esbuild@npm:0.25.8" + dependencies: + "@esbuild/aix-ppc64": "npm:0.25.8" + "@esbuild/android-arm": "npm:0.25.8" + "@esbuild/android-arm64": "npm:0.25.8" + "@esbuild/android-x64": "npm:0.25.8" + "@esbuild/darwin-arm64": "npm:0.25.8" + "@esbuild/darwin-x64": "npm:0.25.8" + "@esbuild/freebsd-arm64": "npm:0.25.8" + "@esbuild/freebsd-x64": "npm:0.25.8" + "@esbuild/linux-arm": "npm:0.25.8" + "@esbuild/linux-arm64": "npm:0.25.8" + "@esbuild/linux-ia32": "npm:0.25.8" + "@esbuild/linux-loong64": "npm:0.25.8" + "@esbuild/linux-mips64el": "npm:0.25.8" + "@esbuild/linux-ppc64": "npm:0.25.8" + "@esbuild/linux-riscv64": "npm:0.25.8" + "@esbuild/linux-s390x": "npm:0.25.8" + "@esbuild/linux-x64": "npm:0.25.8" + "@esbuild/netbsd-arm64": "npm:0.25.8" + "@esbuild/netbsd-x64": "npm:0.25.8" + "@esbuild/openbsd-arm64": "npm:0.25.8" + "@esbuild/openbsd-x64": "npm:0.25.8" + "@esbuild/openharmony-arm64": "npm:0.25.8" + "@esbuild/sunos-x64": "npm:0.25.8" + "@esbuild/win32-arm64": "npm:0.25.8" + "@esbuild/win32-ia32": "npm:0.25.8" + "@esbuild/win32-x64": "npm:0.25.8" + dependenciesMeta: + "@esbuild/aix-ppc64": + optional: true + "@esbuild/android-arm": + optional: true + "@esbuild/android-arm64": + optional: true + "@esbuild/android-x64": + optional: true + "@esbuild/darwin-arm64": + optional: true + "@esbuild/darwin-x64": + optional: true + "@esbuild/freebsd-arm64": + optional: true + "@esbuild/freebsd-x64": + optional: true + "@esbuild/linux-arm": + optional: true + "@esbuild/linux-arm64": + optional: true + "@esbuild/linux-ia32": + optional: true + "@esbuild/linux-loong64": + optional: true + "@esbuild/linux-mips64el": + optional: true + "@esbuild/linux-ppc64": + optional: true + "@esbuild/linux-riscv64": + optional: true + "@esbuild/linux-s390x": + optional: true + "@esbuild/linux-x64": + optional: true + "@esbuild/netbsd-arm64": + optional: true + "@esbuild/netbsd-x64": + optional: true + "@esbuild/openbsd-arm64": + optional: true + "@esbuild/openbsd-x64": + optional: true + "@esbuild/openharmony-arm64": + optional: true + "@esbuild/sunos-x64": + optional: true + "@esbuild/win32-arm64": + optional: true + "@esbuild/win32-ia32": + optional: true + "@esbuild/win32-x64": + optional: true + bin: + esbuild: bin/esbuild + checksum: 10/9897411732768e652d90fa5dfadae965e8f420d24e5f23fa0604331a1441769e2c7ee4e41ca53e926f1fb51a53af52e01fc9070fdc1a4edf3e9ec9208ee41273 + languageName: node + linkType: hard + "escalade@npm:3.2.0": version: 3.2.0 resolution: "escalade@npm:3.2.0" @@ -33829,28 +34148,6 @@ __metadata: languageName: node linkType: hard -"viem@npm:2.x, viem@npm:^2.1.1": - version: 2.21.50 - resolution: "viem@npm:2.21.50" - dependencies: - "@noble/curves": "npm:1.6.0" - "@noble/hashes": "npm:1.5.0" - "@scure/bip32": "npm:1.5.0" - "@scure/bip39": "npm:1.4.0" - abitype: "npm:1.0.6" - isows: "npm:1.0.6" - ox: "npm:0.1.2" - webauthn-p256: "npm:0.0.10" - ws: "npm:8.18.0" - peerDependencies: - typescript: ">=5.0.4" - peerDependenciesMeta: - typescript: - optional: true - checksum: 10/6525c7dfa679d48759d50a31751b1d608f055e4396506c4f48550b81655b75b53978bd2dbe39099ac200f549c7429261d3478810dbd63b36df6a0afd77f69931 - languageName: node - linkType: hard - "viem@npm:>=2.23.11, viem@npm:^2.22.21, viem@npm:^2.23.10, viem@npm:^2.24.1": version: 2.24.1 resolution: "viem@npm:2.24.1" @@ -33872,6 +34169,28 @@ __metadata: languageName: node linkType: hard +"viem@npm:^2.1.1": + version: 2.21.50 + resolution: "viem@npm:2.21.50" + dependencies: + "@noble/curves": "npm:1.6.0" + "@noble/hashes": "npm:1.5.0" + "@scure/bip32": "npm:1.5.0" + "@scure/bip39": "npm:1.4.0" + abitype: "npm:1.0.6" + isows: "npm:1.0.6" + ox: "npm:0.1.2" + webauthn-p256: "npm:0.0.10" + ws: "npm:8.18.0" + peerDependencies: + typescript: ">=5.0.4" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/6525c7dfa679d48759d50a31751b1d608f055e4396506c4f48550b81655b75b53978bd2dbe39099ac200f549c7429261d3478810dbd63b36df6a0afd77f69931 + languageName: node + linkType: hard + "viem@npm:^2.21.59": version: 2.22.17 resolution: "viem@npm:2.22.17" From efb2aadc10d903026ce8bef2ae351c1d3c16c8be Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Tue, 5 Aug 2025 13:35:42 +0100 Subject: [PATCH 04/46] chore: changelog --- contracts/CHANGELOG.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/contracts/CHANGELOG.md b/contracts/CHANGELOG.md index 2e571aa14..bad94a295 100644 --- a/contracts/CHANGELOG.md +++ b/contracts/CHANGELOG.md @@ -4,7 +4,17 @@ All notable changes to this package will be documented in this file. The format is based on [Common Changelog](https://common-changelog.org/). -## [0.11.0] - 2025-08-01 +## [0.12.0] - 2025-08-05 + +### Changed + +- **Breaking:** Make `viem` a peer dependency, it should be provided by the consuming package ([`4594536`](https://github.com/kleros/kleros-v2/commit/4594536c)) + +### Added + +- Add helper function `getDisputeKitsViem` to retrieve a deployment's available dispute kit infos including their capabilities (`isShutter`, `isGated`) ([`5a81f9e`](https://github.com/kleros/kleros-v2/commit/5a81f9ec)) + +## [0.11.0] - 2025-08-02 ### Changed @@ -107,6 +117,7 @@ The format is based on [Common Changelog](https://common-changelog.org/). ## [0.8.1] - 2025-04-10 +[0.12.0]: https://github.com/kleros/kleros-v2/releases/tag/@kleros%2Fkleros-v2-contracts@0.12.0 [0.11.0]: https://github.com/kleros/kleros-v2/releases/tag/@kleros%2Fkleros-v2-contracts@0.11.0 [0.10.0]: https://github.com/kleros/kleros-v2/releases/tag/@kleros%2Fkleros-v2-contracts@0.10.0 [0.9.4]: https://github.com/kleros/kleros-v2/releases/tag/@kleros%2Fkleros-v2-contracts@0.9.4 From e3ed3c95030d2f23e6307942020f11521fd60767 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Tue, 5 Aug 2025 14:02:14 +0100 Subject: [PATCH 05/46] chore: published @kleros/kleros-v2-contracts@0.12.0 --- contracts/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/package.json b/contracts/package.json index 239c1665c..0015dd220 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -1,6 +1,6 @@ { "name": "@kleros/kleros-v2-contracts", - "version": "0.11.0", + "version": "0.12.0", "description": "Smart contracts for Kleros version 2", "main": "./cjs/deployments/index.js", "module": "./esm/deployments/index.js", From 77c85494c78ff3522fb3ebc535c3a562a18a5043 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Thu, 7 Aug 2025 13:21:58 +0100 Subject: [PATCH 06/46] chore: enabled Hardhat viaIR compilation with solc v0.8.30, bumped hardhat --- contracts/hardhat.config.ts | 6 +++--- contracts/package.json | 2 +- yarn.lock | 23 +++++++---------------- 3 files changed, 11 insertions(+), 20 deletions(-) diff --git a/contracts/hardhat.config.ts b/contracts/hardhat.config.ts index 75379d0b9..12dcc321f 100644 --- a/contracts/hardhat.config.ts +++ b/contracts/hardhat.config.ts @@ -26,9 +26,9 @@ const config: HardhatUserConfig = { solidity: { compilers: [ { - version: "0.8.28", + version: "0.8.30", settings: { - // viaIR: true, + viaIR: true, optimizer: { enabled: true, runs: 100, @@ -44,7 +44,7 @@ const config: HardhatUserConfig = { // For Vea version: "0.8.24", settings: { - // viaIR: true, + viaIR: true, optimizer: { enabled: true, runs: 100, diff --git a/contracts/package.json b/contracts/package.json index 0015dd220..c255d5639 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -131,7 +131,7 @@ "gluegun": "^5.2.0", "graphql": "^16.9.0", "graphql-request": "^7.1.2", - "hardhat": "2.25.0", + "hardhat": "2.26.2", "hardhat-contract-sizer": "^2.10.0", "hardhat-deploy": "^1.0.4", "hardhat-deploy-ethers": "^0.4.2", diff --git a/yarn.lock b/yarn.lock index 0b2eacdf0..e6a9c8cf1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6147,7 +6147,7 @@ __metadata: gluegun: "npm:^5.2.0" graphql: "npm:^16.9.0" graphql-request: "npm:^7.1.2" - hardhat: "npm:2.25.0" + hardhat: "npm:2.26.2" hardhat-contract-sizer: "npm:^2.10.0" hardhat-deploy: "npm:^1.0.4" hardhat-deploy-ethers: "npm:^0.4.2" @@ -7655,7 +7655,7 @@ __metadata: languageName: node linkType: hard -"@nomicfoundation/edr@npm:^0.11.1": +"@nomicfoundation/edr@npm:^0.11.3": version: 0.11.3 resolution: "@nomicfoundation/edr@npm:0.11.3" dependencies: @@ -10277,13 +10277,6 @@ __metadata: languageName: node linkType: hard -"@types/lru-cache@npm:^5.1.0": - version: 5.1.1 - resolution: "@types/lru-cache@npm:5.1.1" - checksum: 10/0afadefc983306684a8ef95b6337a0d9e3f687e7e89e1f1f3f2e1ce3fbab5b018bb84cf277d781f871175a2c8f0176762b69e58b6f4296ee1b816cea94d5ef06 - languageName: node - linkType: hard - "@types/mdast@npm:^3.0.0": version: 3.0.15 resolution: "@types/mdast@npm:3.0.15" @@ -20259,17 +20252,15 @@ __metadata: languageName: node linkType: hard -"hardhat@npm:2.25.0": - version: 2.25.0 - resolution: "hardhat@npm:2.25.0" +"hardhat@npm:2.26.2": + version: 2.26.2 + resolution: "hardhat@npm:2.26.2" dependencies: "@ethereumjs/util": "npm:^9.1.0" "@ethersproject/abi": "npm:^5.1.2" - "@nomicfoundation/edr": "npm:^0.11.1" + "@nomicfoundation/edr": "npm:^0.11.3" "@nomicfoundation/solidity-analyzer": "npm:^0.1.0" "@sentry/node": "npm:^5.18.1" - "@types/bn.js": "npm:^5.1.0" - "@types/lru-cache": "npm:^5.1.0" adm-zip: "npm:^0.4.16" aggregate-error: "npm:^3.0.0" ansi-escapes: "npm:^4.3.0" @@ -20314,7 +20305,7 @@ __metadata: optional: true bin: hardhat: internal/cli/bootstrap.js - checksum: 10/b74e83cf8b48e782dd9b7db0d640bcd68fe303c9e269686f9aa4ddcdd7b80e1ca932907003fd42fda005f38d486e4e59726b0f38fd8bf0b981e5810abcc907db + checksum: 10/ef9f5f232264ed45a406a7053ccce71e67b0ce084de2de6fa2c24ff0bb1ec0d7b69f61769b3ac94a50445709b25bcf0d8ee135e1509e53331a3fea44d01cbb63 languageName: node linkType: hard From e41ee562f40732ede9ab0ff1595544499f777924 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Thu, 7 Aug 2025 14:47:07 +0100 Subject: [PATCH 07/46] chore: viaIR compilation enabled for Foundry with explicit solc v0.8.30 bumped @kleros/vea-contracts to v0.7.0 --- contracts/foundry.toml | 13 +++++++++++-- contracts/hardhat.config.ts | 18 +----------------- contracts/package.json | 2 +- yarn.lock | 10 +++++----- 4 files changed, 18 insertions(+), 25 deletions(-) diff --git a/contracts/foundry.toml b/contracts/foundry.toml index a8c6351ec..00b0c68b8 100644 --- a/contracts/foundry.toml +++ b/contracts/foundry.toml @@ -1,15 +1,24 @@ [profile.default] +solc = "0.8.30" +via_ir = true +optimizer = true +optimizer_runs = 500 +optimizer_details = { yulDetails = { stackAllocation = true } } +additional_compiler_profiles = [ + { name = "tests", via_ir = false } +] +compilation_restrictions = [ + { paths = "test/foundry/KlerosCore.t.sol", via_ir = false }, +] src = 'src' out = 'out' libs = ['../node_modules', 'lib'] [rpc_endpoints] arbitrumSepolia = "https://sepolia-rollup.arbitrum.io/rpc" -arbitrumGoerli = "https://goerli-rollup.arbitrum.io/rpc" arbitrum = "https://arb1.arbitrum.io/rpc" sepolia = "https://sepolia.infura.io/v3/${INFURA_API_KEY}" -goerli = "https://goerli.infura.io/v3/${INFURA_API_KEY}" mainnet = "https://mainnet.infura.io/v3/${INFURA_API_KEY}" chiado = "https://rpc.chiado.gnosis.gateway.fm" gnosischain = "https://rpc.gnosis.gateway.fm" diff --git a/contracts/hardhat.config.ts b/contracts/hardhat.config.ts index 12dcc321f..be54c9fcb 100644 --- a/contracts/hardhat.config.ts +++ b/contracts/hardhat.config.ts @@ -31,23 +31,7 @@ const config: HardhatUserConfig = { viaIR: true, optimizer: { enabled: true, - runs: 100, - }, - outputSelection: { - "*": { - "*": ["storageLayout"], - }, - }, - }, - }, - { - // For Vea - version: "0.8.24", - settings: { - viaIR: true, - optimizer: { - enabled: true, - runs: 100, + runs: 10000, }, outputSelection: { "*": { diff --git a/contracts/package.json b/contracts/package.json index c255d5639..0bca4e700 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -154,7 +154,7 @@ }, "dependencies": { "@chainlink/contracts": "^1.4.0", - "@kleros/vea-contracts": "^0.6.0", + "@kleros/vea-contracts": "^0.7.0", "@openzeppelin/contracts": "^5.4.0", "@shutter-network/shutter-sdk": "0.0.2", "isomorphic-fetch": "^3.0.0" diff --git a/yarn.lock b/yarn.lock index e6a9c8cf1..c2cb4f2d5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6123,7 +6123,7 @@ __metadata: "@kleros/kleros-v2-eslint-config": "workspace:^" "@kleros/kleros-v2-prettier-config": "workspace:^" "@kleros/kleros-v2-tsconfig": "workspace:^" - "@kleros/vea-contracts": "npm:^0.6.0" + "@kleros/vea-contracts": "npm:^0.7.0" "@logtail/pino": "npm:^0.5.0" "@nomicfoundation/hardhat-chai-matchers": "npm:^2.1.0" "@nomicfoundation/hardhat-ethers": "npm:^3.1.0" @@ -6402,10 +6402,10 @@ __metadata: languageName: node linkType: hard -"@kleros/vea-contracts@npm:^0.6.0": - version: 0.6.0 - resolution: "@kleros/vea-contracts@npm:0.6.0" - checksum: 10/1dafd94620d3392c2e00e09e7d1ca923007143f8625b4b584411a7b49404ae5630e870d3e260685964d37ccb9c4c4ab406523b8ec4dd9f89bcf6099a4f5976ec +"@kleros/vea-contracts@npm:^0.7.0": + version: 0.7.0 + resolution: "@kleros/vea-contracts@npm:0.7.0" + checksum: 10/bba12886020cd4bfce39938de56edf2b56472627871ef91b10b721de655e5c20f632a8cb57679927d868375218007898b12033d769b7d33cd3f18447ca093896 languageName: node linkType: hard From 2d94bfdd1710638354e1aa31e2b1c13b822dda0e Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Thu, 7 Aug 2025 15:00:09 +0100 Subject: [PATCH 08/46] chore: changelog --- contracts/CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/contracts/CHANGELOG.md b/contracts/CHANGELOG.md index bad94a295..bfdfafd76 100644 --- a/contracts/CHANGELOG.md +++ b/contracts/CHANGELOG.md @@ -4,6 +4,15 @@ All notable changes to this package will be documented in this file. The format is based on [Common Changelog](https://common-changelog.org/). +## [0.13.0] - 2025-08-07 (Not published yet) + +### Changed + +- Set the Hardhat Solidity version to v0.8.30 and enable the IR pipeline ([#2069](https://github.com/kleros/kleros-v2/issues/2069)) +- Set the Foundry Solidity version to v0.8.30 and enable the IR pipeline ([#2073](https://github.com/kleros/kleros-v2/issues/2073)) +- Bump `hardhat` to v2.26.2 ([#2069](https://github.com/kleros/kleros-v2/issues/2069)) +- Bump `@kleros/vea-contracts` to v0.7.0 ([#2073](https://github.com/kleros/kleros-v2/issues/2073)) + ## [0.12.0] - 2025-08-05 ### Changed @@ -117,6 +126,7 @@ The format is based on [Common Changelog](https://common-changelog.org/). ## [0.8.1] - 2025-04-10 +[0.13.0]: https://github.com/kleros/kleros-v2/releases/tag/@kleros%2Fkleros-v2-contracts@0.13.0 [0.12.0]: https://github.com/kleros/kleros-v2/releases/tag/@kleros%2Fkleros-v2-contracts@0.12.0 [0.11.0]: https://github.com/kleros/kleros-v2/releases/tag/@kleros%2Fkleros-v2-contracts@0.11.0 [0.10.0]: https://github.com/kleros/kleros-v2/releases/tag/@kleros%2Fkleros-v2-contracts@0.10.0 From bfe11a728bf8791fa3cde72d3b1c7970482c4af3 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Thu, 24 Jul 2025 20:54:58 +0200 Subject: [PATCH 09/46] feat: rng fallback --- contracts/deploy/00-chainlink-rng.ts | 2 + .../deploy/00-home-chain-arbitration-neo.ts | 22 +-- contracts/deploy/00-home-chain-arbitration.ts | 13 +- contracts/deploy/00-home-chain-resolver.ts | 1 - contracts/deploy/00-rng.ts | 11 +- .../deploy/change-sortition-module-rng.ts | 8 +- contracts/src/arbitration/SortitionModule.sol | 8 +- .../src/arbitration/SortitionModuleBase.sol | 26 +-- .../src/arbitration/SortitionModuleNeo.sol | 8 +- contracts/src/rng/BlockhashRNG.sol | 122 +++++++++--- contracts/src/rng/ChainlinkRNG.sol | 40 ++-- contracts/src/rng/IRNG.sol | 13 ++ contracts/src/rng/IncrementalNG.sol | 17 +- contracts/src/rng/RNG.sol | 14 -- contracts/src/rng/RNGWithFallback.sol | 184 ++++++++++++++++++ contracts/src/rng/RandomizerRNG.sol | 33 ++-- .../test/arbitration/dispute-kit-gated.ts | 7 +- contracts/test/arbitration/draw.ts | 7 +- contracts/test/arbitration/staking-neo.ts | 19 +- contracts/test/arbitration/staking.ts | 21 +- contracts/test/foundry/KlerosCore.t.sol | 86 ++++---- contracts/test/integration/index.ts | 5 +- contracts/test/proxy/index.ts | 2 +- contracts/test/rng/index.ts | 100 ++++++---- cspell.json | 1 + 25 files changed, 503 insertions(+), 267 deletions(-) create mode 100644 contracts/src/rng/IRNG.sol delete mode 100644 contracts/src/rng/RNG.sol create mode 100644 contracts/src/rng/RNGWithFallback.sol diff --git a/contracts/deploy/00-chainlink-rng.ts b/contracts/deploy/00-chainlink-rng.ts index 1062fe936..a811f642b 100644 --- a/contracts/deploy/00-chainlink-rng.ts +++ b/contracts/deploy/00-chainlink-rng.ts @@ -70,6 +70,8 @@ const deployRng: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { ], log: true, }); + + console.log("Register this Chainlink consumer here: http://vrf.chain.link/"); }; deployRng.tags = ["ChainlinkRNG"]; diff --git a/contracts/deploy/00-home-chain-arbitration-neo.ts b/contracts/deploy/00-home-chain-arbitration-neo.ts index 45a6a7d15..8d291d570 100644 --- a/contracts/deploy/00-home-chain-arbitration-neo.ts +++ b/contracts/deploy/00-home-chain-arbitration-neo.ts @@ -12,7 +12,6 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) const { ethers, deployments, getNamedAccounts, getChainId } = hre; const { deploy } = deployments; const { ZeroAddress } = hre.ethers; - const RNG_LOOKAHEAD = 20; // fallback to hardhat node signers on local network const deployer = (await getNamedAccounts()).deployer ?? (await hre.ethers.getSigners())[0].address; @@ -50,16 +49,7 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) const maxTotalStaked = PNK(2_000_000); const sortitionModule = await deployUpgradable(deployments, "SortitionModuleNeo", { from: deployer, - args: [ - deployer, - klerosCoreAddress, - minStakingTime, - maxFreezingTime, - rng.target, - RNG_LOOKAHEAD, - maxStakePerJuror, - maxTotalStaked, - ], + args: [deployer, klerosCoreAddress, minStakingTime, maxFreezingTime, rng.target, maxStakePerJuror, maxTotalStaked], log: true, }); // nonce (implementation), nonce+1 (proxy) @@ -94,11 +84,11 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) await disputeKitContract.changeCore(klerosCore.address); } - // rng.changeSortitionModule() only if necessary - const rngSortitionModule = await rng.sortitionModule(); - if (rngSortitionModule !== sortitionModule.address) { - console.log(`rng.changeSortitionModule(${sortitionModule.address})`); - await rng.changeSortitionModule(sortitionModule.address); + // rng.changeConsumer() only if necessary + const rngConsumer = await rng.consumer(); + if (rngConsumer !== sortitionModule.address) { + console.log(`rng.changeConsumer(${sortitionModule.address})`); + await rng.changeConsumer(sortitionModule.address); } const core = (await hre.ethers.getContract("KlerosCoreNeo")) as KlerosCoreNeo; diff --git a/contracts/deploy/00-home-chain-arbitration.ts b/contracts/deploy/00-home-chain-arbitration.ts index c22a1b960..80e1bd506 100644 --- a/contracts/deploy/00-home-chain-arbitration.ts +++ b/contracts/deploy/00-home-chain-arbitration.ts @@ -12,7 +12,6 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) const { ethers, deployments, getNamedAccounts, getChainId } = hre; const { deploy } = deployments; const { ZeroAddress } = hre.ethers; - const RNG_LOOKAHEAD = 20; // fallback to hardhat node signers on local network const deployer = (await getNamedAccounts()).deployer ?? (await hre.ethers.getSigners())[0].address; @@ -53,7 +52,7 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) const rng = (await ethers.getContract("ChainlinkRNG")) as ChainlinkRNG; const sortitionModule = await deployUpgradable(deployments, "SortitionModule", { from: deployer, - args: [deployer, klerosCoreAddress, minStakingTime, maxFreezingTime, rng.target, RNG_LOOKAHEAD], + args: [deployer, klerosCoreAddress, minStakingTime, maxFreezingTime, rng.target], log: true, }); // nonce (implementation), nonce+1 (proxy) @@ -87,11 +86,11 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) await disputeKitContract.changeCore(klerosCore.address); } - // rng.changeSortitionModule() only if necessary - const rngSortitionModule = await rng.sortitionModule(); - if (rngSortitionModule !== sortitionModule.address) { - console.log(`rng.changeSortitionModule(${sortitionModule.address})`); - await rng.changeSortitionModule(sortitionModule.address); + // rng.changeConsumer() only if necessary + const rngConsumer = await rng.consumer(); + if (rngConsumer !== sortitionModule.address) { + console.log(`rng.changeConsumer(${sortitionModule.address})`); + await rng.changeConsumer(sortitionModule.address); } const core = (await hre.ethers.getContract("KlerosCore")) as KlerosCore; diff --git a/contracts/deploy/00-home-chain-resolver.ts b/contracts/deploy/00-home-chain-resolver.ts index d7d2186ef..5aa5e7b20 100644 --- a/contracts/deploy/00-home-chain-resolver.ts +++ b/contracts/deploy/00-home-chain-resolver.ts @@ -1,7 +1,6 @@ import { HardhatRuntimeEnvironment } from "hardhat/types"; import { DeployFunction } from "hardhat-deploy/types"; import { HomeChains, isSkipped } from "./utils"; -import { deployUpgradable } from "./utils/deployUpgradable"; const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const { deployments, getNamedAccounts, getChainId } = hre; diff --git a/contracts/deploy/00-rng.ts b/contracts/deploy/00-rng.ts index 2489406c1..5eedf19b2 100644 --- a/contracts/deploy/00-rng.ts +++ b/contracts/deploy/00-rng.ts @@ -2,13 +2,12 @@ import { HardhatRuntimeEnvironment } from "hardhat/types"; import { DeployFunction } from "hardhat-deploy/types"; import { SortitionModule } from "../typechain-types"; import { HomeChains, isMainnet, isSkipped } from "./utils"; -import { deployUpgradable } from "./utils/deployUpgradable"; import { getContractOrDeploy } from "./utils/getContractOrDeploy"; const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const { deployments, getNamedAccounts, getChainId, ethers } = hre; const { deploy } = deployments; - const RNG_LOOKAHEAD = 20; + const RNG_LOOKAHEAD_TIME = 30 * 60; // 30 minutes in seconds // fallback to hardhat node signers on local network const deployer = (await getNamedAccounts()).deployer ?? (await hre.ethers.getSigners())[0].address; @@ -32,11 +31,15 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) const rng2 = await deploy("BlockHashRNG", { from: deployer, - args: [], + args: [ + deployer, // governor + sortitionModule.target, // consumer + RNG_LOOKAHEAD_TIME, + ], log: true, }); - await sortitionModule.changeRandomNumberGenerator(rng2.address, RNG_LOOKAHEAD); + await sortitionModule.changeRandomNumberGenerator(rng2.address); }; deployArbitration.tags = ["RNG"]; diff --git a/contracts/deploy/change-sortition-module-rng.ts b/contracts/deploy/change-sortition-module-rng.ts index a9573e6be..2b5e72435 100644 --- a/contracts/deploy/change-sortition-module-rng.ts +++ b/contracts/deploy/change-sortition-module-rng.ts @@ -23,11 +23,11 @@ const task: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { sortitionModule = await ethers.getContract("SortitionModule"); } - console.log(`chainlinkRng.changeSortitionModule(${sortitionModule.target})`); - await chainlinkRng.changeSortitionModule(sortitionModule.target); + console.log(`chainlinkRng.changeConsumer(${sortitionModule.target})`); + await chainlinkRng.changeConsumer(sortitionModule.target); - console.log(`sortitionModule.changeRandomNumberGenerator(${chainlinkRng.target}, 0)`); - await sortitionModule.changeRandomNumberGenerator(chainlinkRng.target, 0); + console.log(`sortitionModule.changeRandomNumberGenerator(${chainlinkRng.target})`); + await sortitionModule.changeRandomNumberGenerator(chainlinkRng.target); }; task.tags = ["ChangeSortitionModuleRNG"]; diff --git a/contracts/src/arbitration/SortitionModule.sol b/contracts/src/arbitration/SortitionModule.sol index cb4f14c58..7e881264b 100644 --- a/contracts/src/arbitration/SortitionModule.sol +++ b/contracts/src/arbitration/SortitionModule.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.24; -import {SortitionModuleBase, KlerosCore, RNG} from "./SortitionModuleBase.sol"; +import {SortitionModuleBase, KlerosCore, IRNG} from "./SortitionModuleBase.sol"; /// @title SortitionModule /// @dev A factory of trees that keeps track of staked values for sortition. @@ -24,16 +24,14 @@ contract SortitionModule is SortitionModuleBase { /// @param _minStakingTime Minimal time to stake /// @param _maxDrawingTime Time after which the drawing phase can be switched /// @param _rng The random number generator. - /// @param _rngLookahead Lookahead value for rng. function initialize( address _governor, KlerosCore _core, uint256 _minStakingTime, uint256 _maxDrawingTime, - RNG _rng, - uint256 _rngLookahead + IRNG _rng ) external reinitializer(1) { - __SortitionModuleBase_initialize(_governor, _core, _minStakingTime, _maxDrawingTime, _rng, _rngLookahead); + __SortitionModuleBase_initialize(_governor, _core, _minStakingTime, _maxDrawingTime, _rng); } function initialize4() external reinitializer(4) { diff --git a/contracts/src/arbitration/SortitionModuleBase.sol b/contracts/src/arbitration/SortitionModuleBase.sol index 577d9fd22..c554c9c9c 100644 --- a/contracts/src/arbitration/SortitionModuleBase.sol +++ b/contracts/src/arbitration/SortitionModuleBase.sol @@ -7,7 +7,7 @@ import {ISortitionModule} from "./interfaces/ISortitionModule.sol"; import {IDisputeKit} from "./interfaces/IDisputeKit.sol"; import {Initializable} from "../proxy/Initializable.sol"; import {UUPSProxiable} from "../proxy/UUPSProxiable.sol"; -import {RNG} from "../rng/RNG.sol"; +import {IRNG} from "../rng/IRNG.sol"; import "../libraries/Constants.sol"; /// @title SortitionModuleBase @@ -50,11 +50,9 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr uint256 public minStakingTime; // The time after which the phase can be switched to Drawing if there are open disputes. uint256 public maxDrawingTime; // The time after which the phase can be switched back to Staking. uint256 public lastPhaseChange; // The last time the phase was changed. - uint256 public randomNumberRequestBlock; // Number of the block when RNG request was made. uint256 public disputesWithoutJurors; // The number of disputes that have not finished drawing jurors. - RNG public rng; // The random number generator. + IRNG public rng; // The random number generator. uint256 public randomNumber; // Random number returned by RNG. - uint256 public rngLookahead; // Minimal block distance between requesting and obtaining a random number. uint256 public delayedStakeWriteIndex; // The index of the last `delayedStake` item that was written to the array. 0 index is skipped. uint256 public delayedStakeReadIndex; // The index of the next `delayedStake` item that should be processed. Starts at 1 because 0 index is skipped. mapping(bytes32 treeHash => SortitionSumTree) sortitionSumTrees; // The mapping trees by keys. @@ -104,8 +102,7 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr KlerosCore _core, uint256 _minStakingTime, uint256 _maxDrawingTime, - RNG _rng, - uint256 _rngLookahead + IRNG _rng ) internal onlyInitializing { governor = _governor; core = _core; @@ -113,7 +110,6 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr maxDrawingTime = _maxDrawingTime; lastPhaseChange = block.timestamp; rng = _rng; - rngLookahead = _rngLookahead; delayedStakeReadIndex = 1; } @@ -153,15 +149,12 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr maxDrawingTime = _maxDrawingTime; } - /// @dev Changes the `_rng` and `_rngLookahead` storage variables. - /// @param _rng The new value for the `RNGenerator` storage variable. - /// @param _rngLookahead The new value for the `rngLookahead` storage variable. - function changeRandomNumberGenerator(RNG _rng, uint256 _rngLookahead) external onlyByGovernor { + /// @dev Changes the `rng` storage variable. + /// @param _rng The new random number generator. + function changeRandomNumberGenerator(IRNG _rng) external onlyByGovernor { rng = _rng; - rngLookahead = _rngLookahead; if (phase == Phase.generating) { - rng.requestRandomness(block.number + rngLookahead); - randomNumberRequestBlock = block.number; + rng.requestRandomness(); } } @@ -176,11 +169,10 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr "The minimum staking time has not passed yet." ); require(disputesWithoutJurors > 0, "There are no disputes that need jurors."); - rng.requestRandomness(block.number + rngLookahead); - randomNumberRequestBlock = block.number; + rng.requestRandomness(); phase = Phase.generating; } else if (phase == Phase.generating) { - randomNumber = rng.receiveRandomness(randomNumberRequestBlock + rngLookahead); + randomNumber = rng.receiveRandomness(); require(randomNumber != 0, "Random number is not ready yet"); phase = Phase.drawing; } else if (phase == Phase.drawing) { diff --git a/contracts/src/arbitration/SortitionModuleNeo.sol b/contracts/src/arbitration/SortitionModuleNeo.sol index b966c9379..9758882fe 100644 --- a/contracts/src/arbitration/SortitionModuleNeo.sol +++ b/contracts/src/arbitration/SortitionModuleNeo.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.24; -import {SortitionModuleBase, KlerosCore, RNG, StakingResult} from "./SortitionModuleBase.sol"; +import {SortitionModuleBase, KlerosCore, IRNG, StakingResult} from "./SortitionModuleBase.sol"; /// @title SortitionModuleNeo /// @dev A factory of trees that keeps track of staked values for sortition. @@ -32,7 +32,6 @@ contract SortitionModuleNeo is SortitionModuleBase { /// @param _minStakingTime Minimal time to stake /// @param _maxDrawingTime Time after which the drawing phase can be switched /// @param _rng The random number generator. - /// @param _rngLookahead Lookahead value for rng. /// @param _maxStakePerJuror The maximum amount of PNK a juror can stake in a court. /// @param _maxTotalStaked The maximum amount of PNK that can be staked in all courts. function initialize( @@ -40,12 +39,11 @@ contract SortitionModuleNeo is SortitionModuleBase { KlerosCore _core, uint256 _minStakingTime, uint256 _maxDrawingTime, - RNG _rng, - uint256 _rngLookahead, + IRNG _rng, uint256 _maxStakePerJuror, uint256 _maxTotalStaked ) external reinitializer(2) { - __SortitionModuleBase_initialize(_governor, _core, _minStakingTime, _maxDrawingTime, _rng, _rngLookahead); + __SortitionModuleBase_initialize(_governor, _core, _minStakingTime, _maxDrawingTime, _rng); maxStakePerJuror = _maxStakePerJuror; maxTotalStaked = _maxTotalStaked; } diff --git a/contracts/src/rng/BlockhashRNG.sol b/contracts/src/rng/BlockhashRNG.sol index 4421b4301..8568de34e 100644 --- a/contracts/src/rng/BlockhashRNG.sol +++ b/contracts/src/rng/BlockhashRNG.sol @@ -2,43 +2,119 @@ pragma solidity ^0.8.24; -import "./RNG.sol"; +import "./IRNG.sol"; /// @title Random Number Generator using blockhash with fallback. -/// @author Clément Lesaege - /// @dev /// Random Number Generator returning the blockhash with a fallback behaviour. -/// In case no one called it within the 256 blocks, it returns the previous blockhash. -/// This contract must be used when returning 0 is a worse failure mode than returning another blockhash. -/// Allows saving the random number for use in the future. It allows the contract to still access the blockhash even after 256 blocks. -contract BlockHashRNG is RNG { - mapping(uint256 block => uint256 number) public randomNumbers; // randomNumbers[block] is the random number for this block, 0 otherwise. +/// On L2 like Arbitrum block production is sporadic so block timestamp is more reliable than block number. +/// Returns 0 when no random number is available. +/// Allows saving the random number for use in the future. It allows the contract to retrieve the blockhash even after the time window. +contract BlockHashRNG is IRNG { + // ************************************* // + // * Storage * // + // ************************************* // + + address public governor; // The address that can withdraw funds. + address public consumer; // The address that can request random numbers. + uint256 public immutable lookaheadTime; // Minimal time in seconds between requesting and obtaining a random number. + 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. + + // ************************************* // + // * Function Modifiers * // + // ************************************* // + + modifier onlyByGovernor() { + require(governor == msg.sender, "Governor only"); + _; + } + + modifier onlyByConsumer() { + require(consumer == msg.sender, "Consumer only"); + _; + } + + // ************************************* // + // * Constructor * // + // ************************************* // + + /// @dev Constructor. + /// @param _governor The Governor of the contract. + /// @param _consumer The address that can request random numbers. + /// @param _lookaheadTime The time lookahead in seconds for the random number. + constructor(address _governor, address _consumer, uint256 _lookaheadTime) { + governor = _governor; + consumer = _consumer; + lookaheadTime = _lookaheadTime; + } + + // ************************************* // + // * Governance * // + // ************************************* // + + /// @dev Changes the governor of the contract. + /// @param _governor The new governor. + function changeGovernor(address _governor) external onlyByGovernor { + governor = _governor; + } + + /// @dev Changes the consumer of the RNG. + /// @param _consumer The new consumer. + function changeConsumer(address _consumer) external onlyByGovernor { + consumer = _consumer; + } + + // ************************************* // + // * State Modifiers * // + // ************************************* // /// @dev Request a random number. - /// @param _block Block the random number is linked to. - function requestRandomness(uint256 _block) external override { - // nop + function requestRandomness() external override onlyByConsumer { + requestTimestamp = block.timestamp; } /// @dev Return the random number. If it has not been saved and is still computable compute it. - /// @param _block Block the random number is linked to. /// @return randomNumber The random number or 0 if it is not ready or has not been requested. - function receiveRandomness(uint256 _block) external override returns (uint256 randomNumber) { - randomNumber = randomNumbers[_block]; + function receiveRandomness() external override onlyByConsumer returns (uint256 randomNumber) { + if (requestTimestamp == 0) return 0; // No request made + + uint256 expectedTimestamp = requestTimestamp + lookaheadTime; + + // Check if enough time has passed + if (block.timestamp < expectedTimestamp) { + return 0; // Not ready yet + } + + // Check if we already have a saved random number for this timestamp window + randomNumber = randomNumbers[expectedTimestamp]; if (randomNumber != 0) { return randomNumber; } - if (_block < block.number) { - // The random number is not already set and can be. - if (blockhash(_block) != 0x0) { - // Normal case. - randomNumber = uint256(blockhash(_block)); - } else { - // The contract was not called in time. Fallback to returning previous blockhash. - randomNumber = uint256(blockhash(block.number - 1)); - } + // Use last block hash for randomness + randomNumber = uint256(blockhash(block.number - 1)); + if (randomNumber != 0) { + randomNumbers[expectedTimestamp] = randomNumber; } - randomNumbers[_block] = randomNumber; + return randomNumber; + } + + // ************************************* // + // * View Functions * // + // ************************************* // + + /// @dev Check if randomness is ready to be received. + /// @return ready True if randomness can be received. + function isRandomnessReady() external view returns (bool ready) { + if (requestTimestamp == 0) return false; + return block.timestamp >= requestTimestamp + lookaheadTime; + } + + /// @dev Get the timestamp when randomness will be ready. + /// @return readyTimestamp The timestamp when randomness will be available. + function getRandomnessReadyTimestamp() external view returns (uint256 readyTimestamp) { + if (requestTimestamp == 0) return 0; + return requestTimestamp + lookaheadTime; } } diff --git a/contracts/src/rng/ChainlinkRNG.sol b/contracts/src/rng/ChainlinkRNG.sol index b829177c3..fe5a9ff19 100644 --- a/contracts/src/rng/ChainlinkRNG.sol +++ b/contracts/src/rng/ChainlinkRNG.sol @@ -5,17 +5,17 @@ pragma solidity ^0.8.24; import {VRFConsumerBaseV2Plus, IVRFCoordinatorV2Plus} from "@chainlink/contracts/src/v0.8/vrf/dev/VRFConsumerBaseV2Plus.sol"; import {VRFV2PlusClient} from "@chainlink/contracts/src/v0.8/vrf/dev/libraries/VRFV2PlusClient.sol"; -import "./RNG.sol"; +import "./IRNG.sol"; /// @title Random Number Generator that uses Chainlink VRF v2.5 /// https://blog.chain.link/introducing-vrf-v2-5/ -contract ChainlinkRNG is RNG, VRFConsumerBaseV2Plus { +contract ChainlinkRNG is IRNG, VRFConsumerBaseV2Plus { // ************************************* // // * Storage * // // ************************************* // address public governor; // The address that can withdraw funds. - address public sortitionModule; // The address of the SortitionModule. + address public consumer; // The address that can request random numbers. bytes32 public keyHash; // The gas lane key hash value - Defines the maximum gas price you are willing to pay for a request in wei (ID of the off-chain VRF job). uint256 public subscriptionId; // The unique identifier of the subscription used for funding requests. uint16 public requestConfirmations; // How many confirmations the Chainlink node should wait before responding. @@ -29,13 +29,13 @@ contract ChainlinkRNG is RNG, VRFConsumerBaseV2Plus { // ************************************* // /// @dev Emitted when a request is sent to the VRF Coordinator - /// @param requestId The ID of the request - event RequestSent(uint256 indexed requestId); + /// @param _requestId The ID of the request + event RequestSent(uint256 indexed _requestId); /// Emitted when a request has been fulfilled. - /// @param requestId The ID of the request - /// @param randomWord The random value answering the request. - event RequestFulfilled(uint256 indexed requestId, uint256 randomWord); + /// @param _requestId The ID of the request + /// @param _randomWord The random value answering the request. + event RequestFulfilled(uint256 indexed _requestId, uint256 _randomWord); // ************************************* // // * Function Modifiers * // @@ -46,8 +46,8 @@ contract ChainlinkRNG is RNG, VRFConsumerBaseV2Plus { _; } - modifier onlyBySortitionModule() { - require(sortitionModule == msg.sender, "SortitionModule only"); + modifier onlyByConsumer() { + require(consumer == msg.sender, "Consumer only"); _; } @@ -57,7 +57,7 @@ contract ChainlinkRNG is RNG, VRFConsumerBaseV2Plus { /// @dev Constructor, initializing the implementation to reduce attack surface. /// @param _governor The Governor of the contract. - /// @param _sortitionModule The address of the SortitionModule contract. + /// @param _consumer The address that can request random numbers. /// @param _vrfCoordinator The address of the VRFCoordinator contract. /// @param _keyHash The gas lane key hash value - Defines the maximum gas price you are willing to pay for a request in wei (ID of the off-chain VRF job). /// @param _subscriptionId The unique identifier of the subscription used for funding requests. @@ -66,7 +66,7 @@ contract ChainlinkRNG is RNG, VRFConsumerBaseV2Plus { /// @dev https://docs.chain.link/vrf/v2-5/subscription/get-a-random-number constructor( address _governor, - address _sortitionModule, + address _consumer, address _vrfCoordinator, bytes32 _keyHash, uint256 _subscriptionId, @@ -74,7 +74,7 @@ contract ChainlinkRNG is RNG, VRFConsumerBaseV2Plus { uint32 _callbackGasLimit ) VRFConsumerBaseV2Plus(_vrfCoordinator) { governor = _governor; - sortitionModule = _sortitionModule; + consumer = _consumer; keyHash = _keyHash; subscriptionId = _subscriptionId; requestConfirmations = _requestConfirmations; @@ -91,10 +91,10 @@ contract ChainlinkRNG is RNG, VRFConsumerBaseV2Plus { governor = _governor; } - /// @dev Changes the sortition module of the contract. - /// @param _sortitionModule The new sortition module. - function changeSortitionModule(address _sortitionModule) external onlyByGovernor { - sortitionModule = _sortitionModule; + /// @dev Changes the consumer of the RNG. + /// @param _consumer The new consumer. + function changeConsumer(address _consumer) external onlyByGovernor { + consumer = _consumer; } /// @dev Changes the VRF Coordinator of the contract. @@ -132,8 +132,8 @@ contract ChainlinkRNG is RNG, VRFConsumerBaseV2Plus { // * State Modifiers * // // ************************************* // - /// @dev Request a random number. SortitionModule only. - function requestRandomness(uint256 /*_block*/) external override onlyBySortitionModule { + /// @dev Request a random number. Consumer only. + function requestRandomness() external override onlyByConsumer { // Will revert if subscription is not set and funded. uint256 requestId = s_vrfCoordinator.requestRandomWords( VRFV2PlusClient.RandomWordsRequest({ @@ -167,7 +167,7 @@ contract ChainlinkRNG is RNG, VRFConsumerBaseV2Plus { /// @dev Return the random number. /// @return randomNumber The random number or 0 if it is not ready or has not been requested. - function receiveRandomness(uint256 /*_block*/) external view override returns (uint256 randomNumber) { + function receiveRandomness() external view override returns (uint256 randomNumber) { randomNumber = randomNumbers[lastRequestId]; } } diff --git a/contracts/src/rng/IRNG.sol b/contracts/src/rng/IRNG.sol new file mode 100644 index 000000000..a561029a2 --- /dev/null +++ b/contracts/src/rng/IRNG.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.24; + +/// @title Random Number Generator interface +interface IRNG { + /// @dev Request a random number. + function requestRandomness() external; + + /// @dev Receive the random number. + /// @return randomNumber Random Number. If the number is not ready or has not been required 0 instead. + function receiveRandomness() external returns (uint256 randomNumber); +} diff --git a/contracts/src/rng/IncrementalNG.sol b/contracts/src/rng/IncrementalNG.sol index aa4b7d840..542090e71 100644 --- a/contracts/src/rng/IncrementalNG.sol +++ b/contracts/src/rng/IncrementalNG.sol @@ -1,13 +1,12 @@ // SPDX-License-Identifier: MIT -/// @title Incremental Number Generator -/// @author JayBuidl -/// @dev A Random Number Generator which returns a number incremented by 1 each time. Useful as a fallback method. - pragma solidity ^0.8.24; -import "./RNG.sol"; +import "./IRNG.sol"; -contract IncrementalNG is RNG { +/// @title Incremental Number Generator +/// @dev A Random Number Generator which returns a number incremented by 1 each time. +/// For testing purposes. +contract IncrementalNG is IRNG { uint256 public number; constructor(uint256 _start) { @@ -15,15 +14,13 @@ contract IncrementalNG is RNG { } /// @dev Request a random number. - /// @param _block Block the random number is linked to. - function requestRandomness(uint256 _block) external override { + function requestRandomness() external override { // nop } /// @dev Get the "random number" (which is always the same). - /// @param _block Block the random number is linked to. /// @return randomNumber The random number or 0 if it is not ready or has not been requested. - function receiveRandomness(uint256 _block) external override returns (uint256 randomNumber) { + function receiveRandomness() external override returns (uint256 randomNumber) { unchecked { return number++; } diff --git a/contracts/src/rng/RNG.sol b/contracts/src/rng/RNG.sol deleted file mode 100644 index 5142aa0e5..000000000 --- a/contracts/src/rng/RNG.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.24; - -interface RNG { - /// @dev Request a random number. - /// @param _block Block linked to the request. - function requestRandomness(uint256 _block) external; - - /// @dev Receive the random number. - /// @param _block Block the random number is linked to. - /// @return randomNumber Random Number. If the number is not ready or has not been required 0 instead. - function receiveRandomness(uint256 _block) external returns (uint256 randomNumber); -} diff --git a/contracts/src/rng/RNGWithFallback.sol b/contracts/src/rng/RNGWithFallback.sol new file mode 100644 index 000000000..94c2b0e03 --- /dev/null +++ b/contracts/src/rng/RNGWithFallback.sol @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "./IRNG.sol"; + +/// @title RNG with fallback mechanism +/// @notice Uses multiple RNG implementations with automatic fallback if default RNG does not respond passed a timeout. +contract RNGWithFallback is IRNG { + uint256 public constant DEFAULT_RNG = 0; + + // ************************************* // + // * Storage * // + // ************************************* // + + address public governor; // Governor address + address public consumer; // Consumer address + IRNG[] public rngs; // List of RNG implementations + uint256 public fallbackTimeoutSeconds; // Time in seconds to wait before falling back to next RNG + uint256 public requestTimestamp; // Timestamp of the current request + uint256 public currentRngIndex; // Index of the current RNG + bool public isRequesting; // Whether a request is in progress + + // ************************************* // + // * Events * // + // ************************************* // + + event RNGDefaultChanged(address indexed _newDefaultRng); + event RNGFallback(uint256 _fromIndex, uint256 _toIndex); + event RNGFailure(); + event RNGFallbackAdded(address indexed _rng); + event RNGFallbackRemoved(address indexed _rng); + event FallbackTimeoutChanged(uint256 _newTimeout); + + // ************************************* // + // * Constructor * // + // ************************************* // + + /// @param _governor Governor address + /// @param _consumer Consumer address + /// @param _fallbackTimeoutSeconds Time in seconds to wait before falling back to next RNG + /// @param _defaultRng The default RNG + constructor(address _governor, address _consumer, uint256 _fallbackTimeoutSeconds, IRNG _defaultRng) { + require(address(_defaultRng) != address(0), "Invalid default RNG"); + + governor = _governor; + consumer = _consumer; + fallbackTimeoutSeconds = _fallbackTimeoutSeconds; + rngs.push(_defaultRng); + } + + // ************************************* // + // * Function Modifiers * // + // ************************************* // + + modifier onlyByGovernor() { + require(msg.sender == governor, "Governor only"); + _; + } + + modifier onlyByConsumer() { + require(msg.sender == consumer, "Consumer only"); + _; + } + + // ************************************* // + // * State Modifiers * // + // ************************************* // + + /// @dev Request a random number from the default RNG + function requestRandomness() external override onlyByConsumer { + require(!isRequesting, "Request already in progress"); + _requestRandomness(DEFAULT_RNG); + } + + function _requestRandomness(uint256 _rngIndex) internal { + isRequesting = true; + requestTimestamp = block.timestamp; + currentRngIndex = _rngIndex; + rngs[_rngIndex].requestRandomness(); + } + + /// @dev Receive the random number with fallback logic + /// @return randomNumber Random Number + function receiveRandomness() external override onlyByConsumer returns (uint256 randomNumber) { + // Try to get random number from current RNG + randomNumber = rngs[currentRngIndex].receiveRandomness(); + + // If we got a valid number, clear the request + if (randomNumber != 0) { + isRequesting = false; + return randomNumber; + } + + // If the timeout is exceeded, try next RNG + if (block.timestamp > requestTimestamp + fallbackTimeoutSeconds) { + uint256 nextIndex = currentRngIndex + 1; + + // If we have another RNG to try, switch to it and request again + if (nextIndex < rngs.length) { + emit RNGFallback(currentRngIndex, nextIndex); + currentRngIndex = nextIndex; + _requestRandomness(nextIndex); + } else { + // No more RNGs to try + emit RNGFailure(); + } + } + return randomNumber; + } + + // ************************************* // + // * Governance Functions * // + // ************************************* // + + /// @dev Change the governor + /// @param _newGovernor Address of the new governor + function changeGovernor(address _newGovernor) external onlyByGovernor { + governor = _newGovernor; + } + + /// @dev Change the consumer + /// @param _consumer Address of the new consumer + function changeConsumer(address _consumer) external onlyByGovernor { + consumer = _consumer; + } + + /// @dev Change the default RNG + /// @param _newDefaultRng Address of the new default RNG + function changeDefaultRng(IRNG _newDefaultRng) external onlyByGovernor { + require(address(_newDefaultRng) != address(0), "Invalid RNG"); + rngs[DEFAULT_RNG] = _newDefaultRng; + emit RNGDefaultChanged(address(_newDefaultRng)); + + // Take over any pending request + _requestRandomness(DEFAULT_RNG); + } + + /// @dev Add a new RNG fallback + /// @param _newFallbackRng Address of the new RNG fallback + function addRngFallback(IRNG _newFallbackRng) external onlyByGovernor { + require(address(_newFallbackRng) != address(0), "Invalid RNG"); + rngs.push(_newFallbackRng); + emit RNGFallbackAdded(address(_newFallbackRng)); + } + + /// @dev Remove an RNG fallback + function removeLastRngFallback() external onlyByGovernor { + require(rngs.length > 1, "No fallback RNG"); + + // If the removed RNG is the current one, reset the fallback index + if (currentRngIndex > rngs.length - 2) { + currentRngIndex = DEFAULT_RNG; + } + + IRNG removedRng = rngs[rngs.length - 1]; + rngs.pop(); + emit RNGFallbackRemoved(address(removedRng)); + } + + /// @dev Change the fallback timeout + /// @param _fallbackTimeoutSeconds New timeout in seconds + function changeFallbackTimeout(uint256 _fallbackTimeoutSeconds) external onlyByGovernor { + fallbackTimeoutSeconds = _fallbackTimeoutSeconds; + emit FallbackTimeoutChanged(_fallbackTimeoutSeconds); + } + + /// @dev Emergency reset the RNG. + /// Useful for the governor to ensure that re-requesting a random number will not be blocked by a previous request. + function emergencyReset() external onlyByGovernor { + isRequesting = false; + requestTimestamp = 0; + currentRngIndex = DEFAULT_RNG; + } + + // ************************************* // + // * View Functions * // + // ************************************* // + + /// @dev Get the number of RNGs + /// @return Number of RNGs + function getRNGsCount() external view returns (uint256) { + return rngs.length; + } +} diff --git a/contracts/src/rng/RandomizerRNG.sol b/contracts/src/rng/RandomizerRNG.sol index 940c2cf5d..6db56fa3b 100644 --- a/contracts/src/rng/RandomizerRNG.sol +++ b/contracts/src/rng/RandomizerRNG.sol @@ -2,18 +2,18 @@ pragma solidity ^0.8.24; -import "./RNG.sol"; +import "./IRNG.sol"; import "./IRandomizer.sol"; /// @title Random Number Generator that uses Randomizer.ai /// https://randomizer.ai/ -contract RandomizerRNG is RNG { +contract RandomizerRNG is IRNG { // ************************************* // // * Storage * // // ************************************* // address public governor; // The address that can withdraw funds. - address public sortitionModule; // The address of the SortitionModule. + address public consumer; // The address that can request random numbers. IRandomizer public randomizer; // Randomizer address. uint256 public callbackGasLimit; // Gas limit for the Randomizer.ai callback. uint256 public lastRequestId; // The last request ID. @@ -41,8 +41,8 @@ contract RandomizerRNG is RNG { _; } - modifier onlyBySortitionModule() { - require(sortitionModule == msg.sender, "SortitionModule only"); + modifier onlyByConsumer() { + require(consumer == msg.sender, "Consumer only"); _; } @@ -51,11 +51,12 @@ contract RandomizerRNG is RNG { // ************************************* // /// @dev Constructor - /// @param _randomizer Randomizer contract. - /// @param _governor Governor of the contract. - constructor(address _governor, address _sortitionModule, IRandomizer _randomizer) { + /// @param _governor The Governor of the contract. + /// @param _consumer The address that can request random numbers. + /// @param _randomizer The Randomizer.ai oracle contract. + constructor(address _governor, address _consumer, IRandomizer _randomizer) { governor = _governor; - sortitionModule = _sortitionModule; + consumer = _consumer; randomizer = _randomizer; callbackGasLimit = 50000; } @@ -70,10 +71,10 @@ contract RandomizerRNG is RNG { governor = _governor; } - /// @dev Changes the sortition module of the contract. - /// @param _sortitionModule The new sortition module. - function changeSortitionModule(address _sortitionModule) external onlyByGovernor { - sortitionModule = _sortitionModule; + /// @dev Changes the consumer of the RNG. + /// @param _consumer The new consumer. + function changeConsumer(address _consumer) external onlyByGovernor { + consumer = _consumer; } /// @dev Change the Randomizer callback gas limit. @@ -98,8 +99,8 @@ contract RandomizerRNG is RNG { // * State Modifiers * // // ************************************* // - /// @dev Request a random number. SortitionModule only. - function requestRandomness(uint256 /*_block*/) external override onlyBySortitionModule { + /// @dev Request a random number. Consumer only. + function requestRandomness() external override onlyByConsumer { uint256 requestId = randomizer.request(callbackGasLimit); lastRequestId = requestId; emit RequestSent(requestId); @@ -120,7 +121,7 @@ contract RandomizerRNG is RNG { /// @dev Return the random number. /// @return randomNumber The random number or 0 if it is not ready or has not been requested. - function receiveRandomness(uint256 /*_block*/) external view override returns (uint256 randomNumber) { + function receiveRandomness() external view override returns (uint256 randomNumber) { randomNumber = randomNumbers[lastRequestId]; } } diff --git a/contracts/test/arbitration/dispute-kit-gated.ts b/contracts/test/arbitration/dispute-kit-gated.ts index daa78ce0c..aa8c504db 100644 --- a/contracts/test/arbitration/dispute-kit-gated.ts +++ b/contracts/test/arbitration/dispute-kit-gated.ts @@ -61,7 +61,7 @@ describe("DisputeKitGated", async () => { }); rng = (await ethers.getContract("IncrementalNG")) as IncrementalNG; - await sortitionModule.changeRandomNumberGenerator(rng.target, 20).then((tx) => tx.wait()); + await sortitionModule.changeRandomNumberGenerator(rng.target).then((tx) => tx.wait()); const hre = require("hardhat"); await deployERC721(hre, deployer, "TestERC721", "Nft721"); @@ -141,11 +141,6 @@ describe("DisputeKitGated", async () => { await network.provider.send("evm_mine"); await sortitionModule.passPhase().then((tx) => tx.wait()); // Staking -> Generating - const lookahead = await sortitionModule.rngLookahead(); - for (let index = 0; index < lookahead; index++) { - await network.provider.send("evm_mine"); - } - await sortitionModule.passPhase().then((tx) => tx.wait()); // Generating -> Drawing return core.draw(disputeId, 70, { gasLimit: 10000000 }); }; diff --git a/contracts/test/arbitration/draw.ts b/contracts/test/arbitration/draw.ts index 12790fdb5..4d2882a57 100644 --- a/contracts/test/arbitration/draw.ts +++ b/contracts/test/arbitration/draw.ts @@ -81,7 +81,7 @@ describe("Draw Benchmark", async () => { }); rng = (await ethers.getContract("IncrementalNG")) as IncrementalNG; - await sortitionModule.changeRandomNumberGenerator(rng.target, 20).then((tx) => tx.wait()); + await sortitionModule.changeRandomNumberGenerator(rng.target).then((tx) => tx.wait()); // CourtId 2 = CHILD_COURT const minStake = 3n * 10n ** 20n; // 300 PNK @@ -174,11 +174,6 @@ describe("Draw Benchmark", async () => { await network.provider.send("evm_mine"); await sortitionModule.passPhase().then((tx) => tx.wait()); // Staking -> Generating - const lookahead = await sortitionModule.rngLookahead(); - for (let index = 0; index < lookahead; index++) { - await network.provider.send("evm_mine"); - } - await sortitionModule.passPhase().then((tx) => tx.wait()); // Generating -> Drawing await expectFromDraw(core.draw(0, 20, { gasLimit: 1000000 })); diff --git a/contracts/test/arbitration/staking-neo.ts b/contracts/test/arbitration/staking-neo.ts index 85b0dc884..ffbdc55fa 100644 --- a/contracts/test/arbitration/staking-neo.ts +++ b/contracts/test/arbitration/staking-neo.ts @@ -56,13 +56,13 @@ describe("Staking", async () => { fallbackToGlobal: true, keepExistingDeployments: false, }); - pnk = (await ethers.getContract("PNK")) as PNK; - core = (await ethers.getContract("KlerosCoreNeo")) as KlerosCoreNeo; - sortition = (await ethers.getContract("SortitionModuleNeo")) as SortitionModuleNeo; - rng = (await ethers.getContract("ChainlinkRNG")) as ChainlinkRNG; - vrfCoordinator = (await ethers.getContract("ChainlinkVRFCoordinator")) as ChainlinkVRFCoordinatorV2Mock; - resolver = (await ethers.getContract("DisputeResolverNeo")) as DisputeResolver; - nft = (await ethers.getContract("KlerosV2NeoEarlyUser")) as TestERC721; + pnk = await ethers.getContract("PNK"); + core = await ethers.getContract("KlerosCoreNeo"); + sortition = await ethers.getContract("SortitionModuleNeo"); + rng = await ethers.getContract("ChainlinkRNG"); + vrfCoordinator = await ethers.getContract("ChainlinkVRFCoordinator"); + resolver = await ethers.getContract("DisputeResolverNeo"); + nft = await ethers.getContract("KlerosV2NeoEarlyUser"); // Juror signer setup and funding const { firstWallet } = await getNamedAccounts(); @@ -105,10 +105,7 @@ describe("Staking", async () => { const drawFromGeneratingPhase = async () => { expect(await sortition.phase()).to.be.equal(1); // Generating - const lookahead = await sortition.rngLookahead(); - for (let index = 0; index < lookahead; index++) { - await network.provider.send("evm_mine"); - } + await network.provider.send("evm_mine"); await vrfCoordinator.fulfillRandomWords(1, rng.target, []); await sortition.passPhase(); // Generating -> Drawing diff --git a/contracts/test/arbitration/staking.ts b/contracts/test/arbitration/staking.ts index 4d0262c22..d27a5f10e 100644 --- a/contracts/test/arbitration/staking.ts +++ b/contracts/test/arbitration/staking.ts @@ -27,11 +27,11 @@ describe("Staking", async () => { fallbackToGlobal: true, keepExistingDeployments: false, }); - pnk = (await ethers.getContract("PNK")) as PNK; - core = (await ethers.getContract("KlerosCore")) as KlerosCore; - sortition = (await ethers.getContract("SortitionModule")) as SortitionModule; - rng = (await ethers.getContract("ChainlinkRNG")) as ChainlinkRNG; - vrfCoordinator = (await ethers.getContract("ChainlinkVRFCoordinator")) as ChainlinkVRFCoordinatorV2Mock; + pnk = await ethers.getContract("PNK"); + core = await ethers.getContract("KlerosCore"); + sortition = await ethers.getContract("SortitionModule"); + rng = await ethers.getContract("ChainlinkRNG"); + vrfCoordinator = await ethers.getContract("ChainlinkVRFCoordinator"); }; describe("When outside the Staking phase", async () => { @@ -53,11 +53,8 @@ describe("Staking", async () => { await network.provider.send("evm_increaseTime", [2000]); // Wait for minStakingTime await network.provider.send("evm_mine"); - const lookahead = await sortition.rngLookahead(); await sortition.passPhase(); // Staking -> Generating - for (let index = 0; index < lookahead; index++) { - await network.provider.send("evm_mine"); - } + await network.provider.send("evm_mine"); balanceBefore = await pnk.balanceOf(deployer); }; @@ -393,11 +390,9 @@ describe("Staking", async () => { await network.provider.send("evm_increaseTime", [2000]); // Wait for minStakingTime await network.provider.send("evm_mine"); - const lookahead = await sortition.rngLookahead(); await sortition.passPhase(); // Staking -> Generating - for (let index = 0; index < lookahead; index++) { - await network.provider.send("evm_mine"); - } + await network.provider.send("evm_mine"); + await vrfCoordinator.fulfillRandomWords(1, rng.target, []); await sortition.passPhase(); // Generating -> Drawing diff --git a/contracts/test/foundry/KlerosCore.t.sol b/contracts/test/foundry/KlerosCore.t.sol index 20f8555a7..b7bb3d73d 100644 --- a/contracts/test/foundry/KlerosCore.t.sol +++ b/contracts/test/foundry/KlerosCore.t.sol @@ -53,7 +53,7 @@ contract KlerosCoreTest is Test { uint256 minStakingTime; uint256 maxDrawingTime; - uint256 rngLookahead; + uint256 rngLookahead; // Time in seconds string templateData; string templateDataMappings; @@ -63,7 +63,6 @@ contract KlerosCoreTest is Test { SortitionModuleMock smLogic = new SortitionModuleMock(); DisputeKitClassic dkLogic = new DisputeKitClassic(); DisputeTemplateRegistry registryLogic = new DisputeTemplateRegistry(); - rng = new BlockHashRNG(); pinakion = new PNK(); feeToken = new TestERC20("Test", "TST"); wNative = new TestERC20("wrapped ETH", "wETH"); @@ -93,9 +92,11 @@ contract KlerosCoreTest is Test { sortitionExtraData = abi.encode(uint256(5)); minStakingTime = 18; maxDrawingTime = 24; - rngLookahead = 20; hiddenVotes = false; + rngLookahead = 600; + rng = new BlockHashRNG(msg.sender, address(sortitionModule), rngLookahead); + UUPSProxy proxyCore = new UUPSProxy(address(coreLogic), ""); bytes memory initDataDk = abi.encodeWithSignature( @@ -109,17 +110,18 @@ contract KlerosCoreTest is Test { disputeKit = DisputeKitClassic(address(proxyDk)); bytes memory initDataSm = abi.encodeWithSignature( - "initialize(address,address,uint256,uint256,address,uint256)", + "initialize(address,address,uint256,uint256,address)", governor, address(proxyCore), minStakingTime, maxDrawingTime, - rng, - rngLookahead + rng ); UUPSProxy proxySm = new UUPSProxy(address(smLogic), initDataSm); sortitionModule = SortitionModuleMock(address(proxySm)); + vm.prank(governor); + rng.changeConsumer(address(sortitionModule)); core = KlerosCoreMock(address(proxyCore)); core.initialize( @@ -239,11 +241,9 @@ contract KlerosCoreTest is Test { assertEq(sortitionModule.minStakingTime(), 18, "Wrong minStakingTime"); assertEq(sortitionModule.maxDrawingTime(), 24, "Wrong maxDrawingTime"); assertEq(sortitionModule.lastPhaseChange(), block.timestamp, "Wrong lastPhaseChange"); - assertEq(sortitionModule.randomNumberRequestBlock(), 0, "randomNumberRequestBlock should be 0"); assertEq(sortitionModule.disputesWithoutJurors(), 0, "disputesWithoutJurors should be 0"); assertEq(address(sortitionModule.rng()), address(rng), "Wrong RNG address"); assertEq(sortitionModule.randomNumber(), 0, "randomNumber should be 0"); - assertEq(sortitionModule.rngLookahead(), 20, "Wrong rngLookahead"); assertEq(sortitionModule.delayedStakeWriteIndex(), 0, "delayedStakeWriteIndex should be 0"); assertEq(sortitionModule.delayedStakeReadIndex(), 1, "Wrong delayedStakeReadIndex"); @@ -260,7 +260,6 @@ contract KlerosCoreTest is Test { KlerosCoreMock coreLogic = new KlerosCoreMock(); SortitionModuleMock smLogic = new SortitionModuleMock(); DisputeKitClassic dkLogic = new DisputeKitClassic(); - rng = new BlockHashRNG(); pinakion = new PNK(); governor = msg.sender; @@ -280,9 +279,11 @@ contract KlerosCoreTest is Test { sortitionExtraData = abi.encode(uint256(5)); minStakingTime = 18; maxDrawingTime = 24; - rngLookahead = 20; hiddenVotes = false; + rngLookahead = 20; + rng = new BlockHashRNG(msg.sender, address(sortitionModule), rngLookahead); + UUPSProxy proxyCore = new UUPSProxy(address(coreLogic), ""); bytes memory initDataDk = abi.encodeWithSignature( @@ -296,17 +297,18 @@ contract KlerosCoreTest is Test { disputeKit = DisputeKitClassic(address(proxyDk)); bytes memory initDataSm = abi.encodeWithSignature( - "initialize(address,address,uint256,uint256,address,uint256)", + "initialize(address,address,uint256,uint256,address)", governor, address(proxyCore), minStakingTime, maxDrawingTime, - rng, - rngLookahead + rng ); UUPSProxy proxySm = new UUPSProxy(address(smLogic), initDataSm); sortitionModule = SortitionModuleMock(address(proxySm)); + vm.prank(governor); + rng.changeConsumer(address(sortitionModule)); core = KlerosCoreMock(address(proxyCore)); vm.expectEmit(true, true, true, true); @@ -1015,7 +1017,7 @@ contract KlerosCoreTest is Test { assertEq(sortitionModule.disputesWithoutJurors(), 1, "Wrong disputesWithoutJurors count"); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase assertEq(pinakion.balanceOf(address(core)), 1000, "Wrong token balance of the core"); @@ -1062,7 +1064,7 @@ contract KlerosCoreTest is Test { arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase assertEq(pinakion.balanceOf(address(core)), 2000, "Wrong token balance of the core"); @@ -1089,7 +1091,7 @@ contract KlerosCoreTest is Test { arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase uint256 disputeID = 0; @@ -1145,7 +1147,7 @@ contract KlerosCoreTest is Test { arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase uint256 disputeID = 0; core.draw(disputeID, DEFAULT_NB_OF_JURORS); @@ -1433,7 +1435,7 @@ contract KlerosCoreTest is Test { arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase vm.expectEmit(true, true, true, true); @@ -1512,7 +1514,7 @@ contract KlerosCoreTest is Test { arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); // Dispute uses general court by default vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase (uint96 courtID, , , , ) = core.disputes(disputeID); @@ -1555,7 +1557,7 @@ contract KlerosCoreTest is Test { arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase core.draw(disputeID, DEFAULT_NB_OF_JURORS); @@ -1665,7 +1667,7 @@ contract KlerosCoreTest is Test { arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase core.draw(disputeID, DEFAULT_NB_OF_JURORS); @@ -1690,7 +1692,7 @@ contract KlerosCoreTest is Test { arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase core.draw(disputeID, DEFAULT_NB_OF_JURORS - 1); // Draw less to check the require later @@ -1801,7 +1803,7 @@ contract KlerosCoreTest is Test { arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase core.draw(disputeID, DEFAULT_NB_OF_JURORS); @@ -1828,7 +1830,7 @@ contract KlerosCoreTest is Test { arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase core.draw(disputeID, DEFAULT_NB_OF_JURORS); @@ -1869,7 +1871,7 @@ contract KlerosCoreTest is Test { arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase core.draw(disputeID, DEFAULT_NB_OF_JURORS); @@ -1918,7 +1920,7 @@ contract KlerosCoreTest is Test { arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase core.draw(disputeID, DEFAULT_NB_OF_JURORS); @@ -2008,7 +2010,7 @@ contract KlerosCoreTest is Test { arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase core.draw(disputeID, DEFAULT_NB_OF_JURORS); @@ -2052,7 +2054,7 @@ contract KlerosCoreTest is Test { arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase core.draw(disputeID, DEFAULT_NB_OF_JURORS); @@ -2153,7 +2155,7 @@ contract KlerosCoreTest is Test { arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 0); @@ -2231,7 +2233,7 @@ contract KlerosCoreTest is Test { arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase core.draw(disputeID, DEFAULT_NB_OF_JURORS); @@ -2265,7 +2267,7 @@ contract KlerosCoreTest is Test { arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase // Split the stakers' votes. The first staker will get VoteID 0 and the second will take the rest. @@ -2277,7 +2279,7 @@ contract KlerosCoreTest is Test { core.setStake(GENERAL_COURT, 20000); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase core.draw(disputeID, 2); // Assign leftover votes to staker2 @@ -2383,7 +2385,7 @@ contract KlerosCoreTest is Test { arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase core.draw(disputeID, DEFAULT_NB_OF_JURORS); @@ -2458,7 +2460,7 @@ contract KlerosCoreTest is Test { arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase core.draw(disputeID, DEFAULT_NB_OF_JURORS); @@ -2504,7 +2506,7 @@ contract KlerosCoreTest is Test { arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase core.draw(disputeID, DEFAULT_NB_OF_JURORS); @@ -2559,7 +2561,7 @@ contract KlerosCoreTest is Test { arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase core.draw(disputeID, DEFAULT_NB_OF_JURORS); @@ -2649,7 +2651,7 @@ contract KlerosCoreTest is Test { core.setStake(GENERAL_COURT, 20000); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase core.draw(disputeID, DEFAULT_NB_OF_JURORS); @@ -2703,7 +2705,7 @@ contract KlerosCoreTest is Test { core.setStake(GENERAL_COURT, 20000); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase core.draw(disputeID, DEFAULT_NB_OF_JURORS); @@ -2742,7 +2744,7 @@ contract KlerosCoreTest is Test { arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase core.draw(disputeID, DEFAULT_NB_OF_JURORS); @@ -2796,7 +2798,7 @@ contract KlerosCoreTest is Test { arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase core.draw(disputeID, DEFAULT_NB_OF_JURORS); @@ -2839,7 +2841,7 @@ contract KlerosCoreTest is Test { arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase core.draw(disputeID, DEFAULT_NB_OF_JURORS); @@ -2938,7 +2940,7 @@ contract KlerosCoreTest is Test { vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 0); diff --git a/contracts/test/integration/index.ts b/contracts/test/integration/index.ts index 395b9ed8e..5b4b7ea9a 100644 --- a/contracts/test/integration/index.ts +++ b/contracts/test/integration/index.ts @@ -9,8 +9,6 @@ import { HomeGateway, VeaMock, DisputeKitClassic, - RandomizerRNG, - RandomizerMock, SortitionModule, ChainlinkRNG, ChainlinkVRFCoordinatorV2Mock, @@ -161,7 +159,6 @@ describe("Integration tests", async () => { console.log("KC phase: %d", await sortitionModule.phase()); await sortitionModule.passPhase(); // Staking -> Generating - await mineBlocks(ethers.getNumber(await sortitionModule.rngLookahead())); // Wait for finality expect(await sortitionModule.phase()).to.equal(Phase.generating); console.log("KC phase: %d", await sortitionModule.phase()); await vrfCoordinator.fulfillRandomWords(1, rng.target, []); @@ -206,6 +203,6 @@ describe("Integration tests", async () => { }; }); -const logJurorBalance = async (result) => { +const logJurorBalance = async (result: { totalStaked: bigint; totalLocked: bigint }) => { console.log("staked=%s, locked=%s", ethers.formatUnits(result.totalStaked), ethers.formatUnits(result.totalLocked)); }; diff --git a/contracts/test/proxy/index.ts b/contracts/test/proxy/index.ts index 6b66a27fb..410753f97 100644 --- a/contracts/test/proxy/index.ts +++ b/contracts/test/proxy/index.ts @@ -4,7 +4,7 @@ import { DeployResult } from "hardhat-deploy/types"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import { deployUpgradable } from "../../deploy/utils/deployUpgradable"; import { UpgradedByInheritanceV1, UpgradedByInheritanceV2 } from "../../typechain-types"; -import { UpgradedByRewrite as UpgradedByRewriteV1 } from "../../typechain-types/src/proxy/mock/by-rewrite"; +import { UpgradedByRewrite as UpgradedByRewriteV1 } from "../../typechain-types/src/proxy/mock/by-rewrite/UpgradedByRewrite.sol"; import { UpgradedByRewrite as UpgradedByRewriteV2 } from "../../typechain-types/src/proxy/mock/by-rewrite/UpgradedByRewriteV2.sol"; let deployer: HardhatEthersSigner; diff --git a/contracts/test/rng/index.ts b/contracts/test/rng/index.ts index 3d4906721..5e74b4a3a 100644 --- a/contracts/test/rng/index.ts +++ b/contracts/test/rng/index.ts @@ -21,13 +21,13 @@ describe("IncrementalNG", async () => { }); it("Should return a number incrementing each time", async () => { - expect(await rng.receiveRandomness.staticCall(689376)).to.equal(initialNg); - await rng.receiveRandomness(29543).then((tx) => tx.wait()); - expect(await rng.receiveRandomness.staticCall(5894382)).to.equal(initialNg + 1); - await rng.receiveRandomness(0).then((tx) => tx.wait()); - expect(await rng.receiveRandomness.staticCall(3465)).to.equal(initialNg + 2); - await rng.receiveRandomness(2n ** 255n).then((tx) => tx.wait()); - expect(await rng.receiveRandomness.staticCall(0)).to.equal(initialNg + 3); + expect(await rng.receiveRandomness.staticCall()).to.equal(initialNg); + await rng.receiveRandomness().then((tx) => tx.wait()); + expect(await rng.receiveRandomness.staticCall()).to.equal(initialNg + 1); + await rng.receiveRandomness().then((tx) => tx.wait()); + expect(await rng.receiveRandomness.staticCall()).to.equal(initialNg + 2); + await rng.receiveRandomness().then((tx) => tx.wait()); + expect(await rng.receiveRandomness.staticCall()).to.equal(initialNg + 3); }); }); @@ -35,26 +35,48 @@ describe("BlockHashRNG", async () => { let rng: BlockHashRNG; beforeEach("Setup", async () => { - const rngFactory = await ethers.getContractFactory("BlockHashRNG"); - rng = (await rngFactory.deploy()) as BlockHashRNG; + const [deployer] = await ethers.getSigners(); + await deployments.delete("BlockHashRNG"); + await deployments.deploy("BlockHashRNG", { + from: deployer.address, + args: [deployer.address, deployer.address, 10], // governor, consumer, lookaheadTime (seconds) + }); + rng = await ethers.getContract("BlockHashRNG"); }); - it("Should return a non-zero number for a block number in the past", async () => { - const tx = await rng.receiveRandomness(5); - const trace = await network.provider.send("debug_traceTransaction", [tx.hash]); - await tx.wait(); - const [rn] = abiCoder.decode(["uint"], ethers.getBytes(`${trace.returnValue}`)); - expect(rn).to.not.equal(0); - await tx.wait(); + it("Should return a non-zero number after requesting and waiting", async () => { + // First request randomness + await rng.requestRandomness(); + + // Check that it's not ready yet + expect(await rng.isRandomnessReady()).to.be.false; + + // Advance time by 10 seconds (the lookahead time) + await network.provider.send("evm_increaseTime", [10]); + await network.provider.send("evm_mine"); + + // Now it should be ready + expect(await rng.isRandomnessReady()).to.be.true; + + // Get the random number + const randomNumber = await rng.receiveRandomness.staticCall(); + expect(randomNumber).to.not.equal(0); }); - it("Should return zero for a block number in the future", async () => { - const tx = await rng.receiveRandomness(9876543210); - const trace = await network.provider.send("debug_traceTransaction", [tx.hash]); - await tx.wait(); - const [rn] = abiCoder.decode(["uint"], ethers.getBytes(`${trace.returnValue}`)); - expect(rn).to.equal(0); - await tx.wait(); + it("Should return 0 if randomness not requested", async () => { + const randomNumber = await rng.receiveRandomness.staticCall(); + expect(randomNumber).to.equal(0); + }); + + it("Should return 0 if not enough time has passed", async () => { + await rng.requestRandomness(); + + // Don't advance time enough + await network.provider.send("evm_increaseTime", [5]); // Only 5 seconds + await network.provider.send("evm_mine"); + + const randomNumber = await rng.receiveRandomness.staticCall(); + expect(randomNumber).to.equal(0); }); }); @@ -73,39 +95,33 @@ describe("ChainlinkRNG", async () => { it("Should return a non-zero random number", async () => { const requestId = 1; - const expectedRn = BigInt( - ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(["uint256", "uint256"], [requestId, 0])) - ); + const expectedRn = BigInt(ethers.keccak256(abiCoder.encode(["uint256", "uint256"], [requestId, 0]))); - let tx = await rng.requestRandomness(0); + let tx = await rng.requestRandomness(); await expect(tx).to.emit(rng, "RequestSent").withArgs(requestId); tx = await vrfCoordinator.fulfillRandomWords(requestId, rng.target, []); await expect(tx).to.emit(rng, "RequestFulfilled").withArgs(requestId, expectedRn); - const rn = await rng.receiveRandomness(0); + const rn = await rng.receiveRandomness(); expect(rn).to.equal(expectedRn); await tx.wait(); }); it("Should return only the last random number when multiple requests are made", async () => { // First request - let tx = await rng.requestRandomness(0); + let tx = await rng.requestRandomness(); const requestId1 = 1; await expect(tx).to.emit(rng, "RequestSent").withArgs(requestId1); // Second request - tx = await rng.requestRandomness(0); + tx = await rng.requestRandomness(); const requestId2 = 2; await expect(tx).to.emit(rng, "RequestSent").withArgs(requestId2); // Generate expected random numbers - const expectedRn1 = BigInt( - ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(["uint256", "uint256"], [requestId1, 0])) - ); - const expectedRn2 = BigInt( - ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(["uint256", "uint256"], [requestId2, 0])) - ); + const expectedRn1 = BigInt(ethers.keccak256(abiCoder.encode(["uint256", "uint256"], [requestId1, 0]))); + const expectedRn2 = BigInt(ethers.keccak256(abiCoder.encode(["uint256", "uint256"], [requestId2, 0]))); expect(expectedRn1).to.not.equal(expectedRn2, "Random numbers should be different"); // Fulfill first request @@ -117,7 +133,7 @@ describe("ChainlinkRNG", async () => { await expect(tx).to.emit(rng, "RequestFulfilled").withArgs(requestId2, expectedRn2); // Should return only the last random number - const rn = await rng.receiveRandomness(0); + const rn = await rng.receiveRandomness(); expect(rn).to.equal(expectedRn2); await tx.wait(); }); @@ -141,25 +157,25 @@ describe("RandomizerRNG", async () => { const expectedRn = BigInt(ethers.hexlify(randomBytes)); const requestId = 1; - let tx = await rng.requestRandomness(0); + let tx = await rng.requestRandomness(); await expect(tx).to.emit(rng, "RequestSent").withArgs(requestId); tx = await randomizer.relay(rng.target, requestId, randomBytes); await expect(tx).to.emit(rng, "RequestFulfilled").withArgs(requestId, expectedRn); - const rn = await rng.receiveRandomness(0); + const rn = await rng.receiveRandomness(); expect(rn).to.equal(expectedRn); await tx.wait(); }); it("Should return only the last random number when multiple requests are made", async () => { // First request - let tx = await rng.requestRandomness(0); + let tx = await rng.requestRandomness(); const requestId1 = 1; await expect(tx).to.emit(rng, "RequestSent").withArgs(requestId1); // Second request - tx = await rng.requestRandomness(0); + tx = await rng.requestRandomness(); const requestId2 = 2; await expect(tx).to.emit(rng, "RequestSent").withArgs(requestId2); @@ -180,7 +196,7 @@ describe("RandomizerRNG", async () => { await expect(tx).to.emit(rng, "RequestFulfilled").withArgs(requestId2, expectedRn2); // Should return only the last random number - const rn = await rng.receiveRandomness(0); + const rn = await rng.receiveRandomness(); expect(rn).to.equal(expectedRn2); await tx.wait(); }); diff --git a/cspell.json b/cspell.json index 0ab3e541c..0fa6dc52c 100644 --- a/cspell.json +++ b/cspell.json @@ -35,6 +35,7 @@ "IERC", "Initializable", "ipfs", + "IRNG", "kleros", "linguo", "Numberish", From 63ecf4a2f421549c6e6003c8c87ccfb906aab891 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Thu, 24 Jul 2025 21:02:25 +0200 Subject: [PATCH 10/46] fix: re-added removed variables to preserve storage layout, marked as deprecated for later removal --- contracts/src/arbitration/SortitionModuleBase.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/src/arbitration/SortitionModuleBase.sol b/contracts/src/arbitration/SortitionModuleBase.sol index c554c9c9c..7692740cc 100644 --- a/contracts/src/arbitration/SortitionModuleBase.sol +++ b/contracts/src/arbitration/SortitionModuleBase.sol @@ -50,9 +50,11 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr uint256 public minStakingTime; // The time after which the phase can be switched to Drawing if there are open disputes. uint256 public maxDrawingTime; // The time after which the phase can be switched back to Staking. uint256 public lastPhaseChange; // The last time the phase was changed. + uint256 public randomNumberRequestBlock; // DEPRECATED: to be removed in the next redeploy uint256 public disputesWithoutJurors; // The number of disputes that have not finished drawing jurors. IRNG public rng; // The random number generator. uint256 public randomNumber; // Random number returned by RNG. + uint256 public rngLookahead; // DEPRECATED: to be removed in the next redeploy uint256 public delayedStakeWriteIndex; // The index of the last `delayedStake` item that was written to the array. 0 index is skipped. uint256 public delayedStakeReadIndex; // The index of the next `delayedStake` item that should be processed. Starts at 1 because 0 index is skipped. mapping(bytes32 treeHash => SortitionSumTree) sortitionSumTrees; // The mapping trees by keys. From 937fb976be4f285e0ac5c6d8044165f83940e2bf Mon Sep 17 00:00:00 2001 From: unknownunknown1 Date: Wed, 6 Aug 2025 16:18:27 +1000 Subject: [PATCH 11/46] feat(RNG): fallback contract update --- contracts/src/rng/BlockhashRNG.sol | 6 +- contracts/src/rng/RNGWithFallback.sol | 106 ++++-------------------- contracts/test/foundry/KlerosCore.t.sol | 4 +- 3 files changed, 19 insertions(+), 97 deletions(-) diff --git a/contracts/src/rng/BlockhashRNG.sol b/contracts/src/rng/BlockhashRNG.sol index 8568de34e..9070d0751 100644 --- a/contracts/src/rng/BlockhashRNG.sol +++ b/contracts/src/rng/BlockhashRNG.sol @@ -77,7 +77,7 @@ contract BlockHashRNG is IRNG { /// @dev Return the random number. If it has not been saved and is still computable compute it. /// @return randomNumber The random number or 0 if it is not ready or has not been requested. function receiveRandomness() external override onlyByConsumer returns (uint256 randomNumber) { - if (requestTimestamp == 0) return 0; // No request made + if (requestTimestamp == 0) return 0; // No requests were made yet. uint256 expectedTimestamp = requestTimestamp + lookaheadTime; @@ -107,14 +107,14 @@ contract BlockHashRNG is IRNG { /// @dev Check if randomness is ready to be received. /// @return ready True if randomness can be received. function isRandomnessReady() external view returns (bool ready) { - if (requestTimestamp == 0) return false; + if (requestTimestamp == 0) return false; // No requests were made yet. return block.timestamp >= requestTimestamp + lookaheadTime; } /// @dev Get the timestamp when randomness will be ready. /// @return readyTimestamp The timestamp when randomness will be available. function getRandomnessReadyTimestamp() external view returns (uint256 readyTimestamp) { - if (requestTimestamp == 0) return 0; + if (requestTimestamp == 0) return 0; // No requests were made yet. return requestTimestamp + lookaheadTime; } } diff --git a/contracts/src/rng/RNGWithFallback.sol b/contracts/src/rng/RNGWithFallback.sol index 94c2b0e03..95b635cef 100644 --- a/contracts/src/rng/RNGWithFallback.sol +++ b/contracts/src/rng/RNGWithFallback.sol @@ -6,29 +6,21 @@ import "./IRNG.sol"; /// @title RNG with fallback mechanism /// @notice Uses multiple RNG implementations with automatic fallback if default RNG does not respond passed a timeout. contract RNGWithFallback is IRNG { - uint256 public constant DEFAULT_RNG = 0; - // ************************************* // // * Storage * // // ************************************* // + IRNG public immutable rng; // RNG address. address public governor; // Governor address address public consumer; // Consumer address - IRNG[] public rngs; // List of RNG implementations uint256 public fallbackTimeoutSeconds; // Time in seconds to wait before falling back to next RNG uint256 public requestTimestamp; // Timestamp of the current request - uint256 public currentRngIndex; // Index of the current RNG - bool public isRequesting; // Whether a request is in progress // ************************************* // // * Events * // // ************************************* // - event RNGDefaultChanged(address indexed _newDefaultRng); - event RNGFallback(uint256 _fromIndex, uint256 _toIndex); - event RNGFailure(); - event RNGFallbackAdded(address indexed _rng); - event RNGFallbackRemoved(address indexed _rng); + event RNGFallback(); event FallbackTimeoutChanged(uint256 _newTimeout); // ************************************* // @@ -38,14 +30,14 @@ contract RNGWithFallback is IRNG { /// @param _governor Governor address /// @param _consumer Consumer address /// @param _fallbackTimeoutSeconds Time in seconds to wait before falling back to next RNG - /// @param _defaultRng The default RNG - constructor(address _governor, address _consumer, uint256 _fallbackTimeoutSeconds, IRNG _defaultRng) { - require(address(_defaultRng) != address(0), "Invalid default RNG"); + /// @param _rng The RNG address (e.g. Chainlink) + constructor(address _governor, address _consumer, uint256 _fallbackTimeoutSeconds, IRNG _rng) { + require(address(_rng) != address(0), "Invalid default RNG"); governor = _governor; consumer = _consumer; fallbackTimeoutSeconds = _fallbackTimeoutSeconds; - rngs.push(_defaultRng); + rng = _rng; } // ************************************* // @@ -66,44 +58,25 @@ contract RNGWithFallback is IRNG { // * State Modifiers * // // ************************************* // - /// @dev Request a random number from the default RNG + /// @dev Request a random number from the RNG function requestRandomness() external override onlyByConsumer { - require(!isRequesting, "Request already in progress"); - _requestRandomness(DEFAULT_RNG); - } - - function _requestRandomness(uint256 _rngIndex) internal { - isRequesting = true; requestTimestamp = block.timestamp; - currentRngIndex = _rngIndex; - rngs[_rngIndex].requestRandomness(); + rng.requestRandomness(); } /// @dev Receive the random number with fallback logic /// @return randomNumber Random Number function receiveRandomness() external override onlyByConsumer returns (uint256 randomNumber) { - // Try to get random number from current RNG - randomNumber = rngs[currentRngIndex].receiveRandomness(); + // Try to get random number from the RNG contract + randomNumber = rng.receiveRandomness(); // If we got a valid number, clear the request if (randomNumber != 0) { - isRequesting = false; return randomNumber; - } - - // If the timeout is exceeded, try next RNG - if (block.timestamp > requestTimestamp + fallbackTimeoutSeconds) { - uint256 nextIndex = currentRngIndex + 1; - - // If we have another RNG to try, switch to it and request again - if (nextIndex < rngs.length) { - emit RNGFallback(currentRngIndex, nextIndex); - currentRngIndex = nextIndex; - _requestRandomness(nextIndex); - } else { - // No more RNGs to try - emit RNGFailure(); - } + } else if (block.timestamp > requestTimestamp + fallbackTimeoutSeconds) { + // If the timeout is exceeded, try the fallback + randomNumber = uint256(blockhash(block.number - 1)); + emit RNGFallback(); } return randomNumber; } @@ -124,61 +97,10 @@ contract RNGWithFallback is IRNG { consumer = _consumer; } - /// @dev Change the default RNG - /// @param _newDefaultRng Address of the new default RNG - function changeDefaultRng(IRNG _newDefaultRng) external onlyByGovernor { - require(address(_newDefaultRng) != address(0), "Invalid RNG"); - rngs[DEFAULT_RNG] = _newDefaultRng; - emit RNGDefaultChanged(address(_newDefaultRng)); - - // Take over any pending request - _requestRandomness(DEFAULT_RNG); - } - - /// @dev Add a new RNG fallback - /// @param _newFallbackRng Address of the new RNG fallback - function addRngFallback(IRNG _newFallbackRng) external onlyByGovernor { - require(address(_newFallbackRng) != address(0), "Invalid RNG"); - rngs.push(_newFallbackRng); - emit RNGFallbackAdded(address(_newFallbackRng)); - } - - /// @dev Remove an RNG fallback - function removeLastRngFallback() external onlyByGovernor { - require(rngs.length > 1, "No fallback RNG"); - - // If the removed RNG is the current one, reset the fallback index - if (currentRngIndex > rngs.length - 2) { - currentRngIndex = DEFAULT_RNG; - } - - IRNG removedRng = rngs[rngs.length - 1]; - rngs.pop(); - emit RNGFallbackRemoved(address(removedRng)); - } - /// @dev Change the fallback timeout /// @param _fallbackTimeoutSeconds New timeout in seconds function changeFallbackTimeout(uint256 _fallbackTimeoutSeconds) external onlyByGovernor { fallbackTimeoutSeconds = _fallbackTimeoutSeconds; emit FallbackTimeoutChanged(_fallbackTimeoutSeconds); } - - /// @dev Emergency reset the RNG. - /// Useful for the governor to ensure that re-requesting a random number will not be blocked by a previous request. - function emergencyReset() external onlyByGovernor { - isRequesting = false; - requestTimestamp = 0; - currentRngIndex = DEFAULT_RNG; - } - - // ************************************* // - // * View Functions * // - // ************************************* // - - /// @dev Get the number of RNGs - /// @return Number of RNGs - function getRNGsCount() external view returns (uint256) { - return rngs.length; - } } diff --git a/contracts/test/foundry/KlerosCore.t.sol b/contracts/test/foundry/KlerosCore.t.sol index b7bb3d73d..692beb009 100644 --- a/contracts/test/foundry/KlerosCore.t.sol +++ b/contracts/test/foundry/KlerosCore.t.sol @@ -94,7 +94,7 @@ contract KlerosCoreTest is Test { maxDrawingTime = 24; hiddenVotes = false; - rngLookahead = 600; + rngLookahead = 30; rng = new BlockHashRNG(msg.sender, address(sortitionModule), rngLookahead); UUPSProxy proxyCore = new UUPSProxy(address(coreLogic), ""); @@ -1471,7 +1471,7 @@ contract KlerosCoreTest is Test { arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase core.draw(disputeID, DEFAULT_NB_OF_JURORS); // No one is staked so check that the empty addresses are not drawn. From 752b64a2655b08cdaa376e04ca5d64e9d6df4e6e Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Wed, 6 Aug 2025 03:06:07 +0100 Subject: [PATCH 12/46] feat: court llms.txt, adding header `X-Robots-Tag: llms-txt` --- web/netlify.toml | 5 +++++ web/src/public/llms.txt | 9 +++++++++ 2 files changed, 14 insertions(+) create mode 100644 web/src/public/llms.txt diff --git a/web/netlify.toml b/web/netlify.toml index 8cf55aefd..3f6682c22 100644 --- a/web/netlify.toml +++ b/web/netlify.toml @@ -9,3 +9,8 @@ YARN_ENABLE_GLOBAL_CACHE = "true" [functions] directory = "web/netlify/functions/" + +[[headers]] + for = "/*" + [headers.values] + X-Robots-Tag = "llms-txt" \ No newline at end of file diff --git a/web/src/public/llms.txt b/web/src/public/llms.txt new file mode 100644 index 000000000..00c5825f0 --- /dev/null +++ b/web/src/public/llms.txt @@ -0,0 +1,9 @@ +# v2.kleros.builders llms.txt + +> Facilitates decentralized arbitration by allowing users to create, manage, and resolve dispute cases through crowdsourced juror consensus, rewarding jurors with cryptocurrency for coherent votes on disputes on the blockchain-based Kleros platform. + +- [Kleros Dispute Dashboard](https://v2.kleros.builders): Dashboard for managing and viewing decentralized dispute cases, jurors, and court statistics on Kleros platform. +- [Kleros Dispute Cases](https://v2.kleros.builders/#/cases/display/1/desc/all): Provide a platform for viewing and managing decentralized dispute resolution cases. +- [Kleros Decentralized Courts](https://v2.kleros.builders/#/courts): Facilitate decentralized dispute resolution by allowing users to stake tokens, participate as jurors, and view court cases. +- [Kleros Jurors Leaderboard](https://v2.kleros.builders/#/jurors/1/desc/all): Display ranking and statistics of jurors based on coherent voting and rewards in the Kleros decentralized arbitration system. +- [Get PNK Token](https://v2.kleros.builders/#/get-pnk): Facilitates cross-chain swaps of PNK tokens typically between the Ethereum and Arbitrum networks. From 4a72da5ee666bf63b3987c04e7bac02c674aa463 Mon Sep 17 00:00:00 2001 From: kemuru <102478601+kemuru@users.noreply.github.com> Date: Thu, 7 Aug 2025 21:17:33 +0200 Subject: [PATCH 13/46] fix: shutter flash --- web/src/hooks/useVotingContext.tsx | 12 ++++++++++-- .../Cases/CaseDetails/Voting/Shutter/index.tsx | 17 ++++++++++++----- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/web/src/hooks/useVotingContext.tsx b/web/src/hooks/useVotingContext.tsx index fa92bf09c..456520cd0 100644 --- a/web/src/hooks/useVotingContext.tsx +++ b/web/src/hooks/useVotingContext.tsx @@ -35,7 +35,7 @@ export const VotingContextProvider: React.FC<{ children: React.ReactNode }> = ({ const { data: drawData, isLoading } = useDrawQuery(address?.toLowerCase(), id, disputeData?.dispute?.currentRound.id); const roundId = disputeData?.dispute?.currentRoundIndex; const voteId = drawData?.draws?.[0]?.voteIDNum; - const { data: hasVoted } = useReadDisputeKitClassicIsVoteActive({ + const { data: hasVotedClassic } = useReadDisputeKitClassicIsVoteActive({ query: { enabled: !isUndefined(roundId) && !isUndefined(voteId), refetchInterval: REFETCH_INTERVAL, @@ -44,12 +44,20 @@ export const VotingContextProvider: React.FC<{ children: React.ReactNode }> = ({ }); const wasDrawn = useMemo(() => !isUndefined(drawData) && drawData.draws.length > 0, [drawData]); - const isHiddenVotes = useMemo(() => disputeData?.dispute?.court.hiddenVotes, [disputeData]); + const isHiddenVotes = useMemo(() => disputeData?.dispute?.court.hiddenVotes ?? false, [disputeData]); const isCommitPeriod = useMemo(() => disputeData?.dispute?.period === "commit", [disputeData]); const isVotingPeriod = useMemo(() => disputeData?.dispute?.period === "vote", [disputeData]); const commited = useMemo(() => !isUndefined(drawData) && drawData?.draws?.[0]?.vote?.commited, [drawData]); const commit = useMemo(() => drawData?.draws?.[0]?.vote?.commit, [drawData]); + + const hasVoted = useMemo(() => { + if (isHiddenVotes && isCommitPeriod) { + return commited; + } + return hasVotedClassic; + }, [isHiddenVotes, isCommitPeriod, commited, hasVotedClassic]); + return ( = ({ arbitrable, setIsOpen, dispute, currentPe const { isCommitPeriod, isVotingPeriod, commited } = useVotingContext(); const voteIDs = useMemo(() => drawData?.draws?.map((draw) => draw.voteIDNum) as string[], [drawData]); - return id && isCommitPeriod && !commited ? ( - - ) : id && isVotingPeriod ? ( - - ) : null; + const shouldShowCommit = id && isCommitPeriod && !commited; + const shouldShowReveal = id && isVotingPeriod; + + if (shouldShowCommit) { + return ; + } + + if (shouldShowReveal) { + return ; + } + + return null; }; export default Shutter; From d3a1293d940187f126dc387edca06c31ce234498 Mon Sep 17 00:00:00 2001 From: kemuru <102478601+kemuru@users.noreply.github.com> Date: Fri, 8 Aug 2025 01:22:33 +0200 Subject: [PATCH 14/46] chore: better return structure --- .../Cases/CaseDetails/Voting/Shutter/index.tsx | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/web/src/pages/Cases/CaseDetails/Voting/Shutter/index.tsx b/web/src/pages/Cases/CaseDetails/Voting/Shutter/index.tsx index 7291efb3d..3000b2872 100644 --- a/web/src/pages/Cases/CaseDetails/Voting/Shutter/index.tsx +++ b/web/src/pages/Cases/CaseDetails/Voting/Shutter/index.tsx @@ -29,15 +29,14 @@ const Shutter: React.FC = ({ arbitrable, setIsOpen, dispute, currentPe const shouldShowCommit = id && isCommitPeriod && !commited; const shouldShowReveal = id && isVotingPeriod; - if (shouldShowCommit) { - return ; - } - - if (shouldShowReveal) { - return ; - } - - return null; + return ( + <> + {shouldShowCommit && ( + + )} + {shouldShowReveal && } + + ); }; export default Shutter; From 9d3ba90b245a84ab917b29ef273a19f25eaf39de Mon Sep 17 00:00:00 2001 From: kemuru <102478601+kemuru@users.noreply.github.com> Date: Fri, 8 Aug 2025 02:05:12 +0200 Subject: [PATCH 15/46] fix: usevotingcontext dynamic disputekit hook calling --- web/src/hooks/useVotingContext.tsx | 77 +++++++++++++++++++++++++----- 1 file changed, 64 insertions(+), 13 deletions(-) diff --git a/web/src/hooks/useVotingContext.tsx b/web/src/hooks/useVotingContext.tsx index 456520cd0..4b216807a 100644 --- a/web/src/hooks/useVotingContext.tsx +++ b/web/src/hooks/useVotingContext.tsx @@ -3,10 +3,16 @@ import React, { useContext, createContext, useMemo } from "react"; import { useParams } from "react-router-dom"; import { useAccount } from "wagmi"; -import { REFETCH_INTERVAL } from "consts/index"; -import { useReadDisputeKitClassicIsVoteActive } from "hooks/contracts/generated"; +import { REFETCH_INTERVAL, DisputeKits } from "consts/index"; +import { + useReadDisputeKitClassicIsVoteActive, + useReadDisputeKitShutterIsVoteActive, + useReadDisputeKitGatedIsVoteActive, + useReadDisputeKitGatedShutterIsVoteActive, +} from "hooks/contracts/generated"; import { useDisputeDetailsQuery } from "hooks/queries/useDisputeDetailsQuery"; import { useDrawQuery } from "hooks/queries/useDrawQuery"; +import { useDisputeKitAddresses } from "hooks/useDisputeKitAddresses"; import { isUndefined } from "utils/index"; interface IVotingContext { @@ -35,14 +41,67 @@ export const VotingContextProvider: React.FC<{ children: React.ReactNode }> = ({ const { data: drawData, isLoading } = useDrawQuery(address?.toLowerCase(), id, disputeData?.dispute?.currentRound.id); const roundId = disputeData?.dispute?.currentRoundIndex; const voteId = drawData?.draws?.[0]?.voteIDNum; - const { data: hasVotedClassic } = useReadDisputeKitClassicIsVoteActive({ + + const disputeKitAddress = disputeData?.dispute?.currentRound?.disputeKit?.address; + const { disputeKitName } = useDisputeKitAddresses({ disputeKitAddress }); + + const hookArgs = [BigInt(id ?? 0), roundId, voteId] as const; + const isEnabled = !isUndefined(roundId) && !isUndefined(voteId); + + // Only call the hook for the specific dispute kit type + const classicVoteResult = useReadDisputeKitClassicIsVoteActive({ + query: { + enabled: isEnabled && disputeKitName === DisputeKits.Classic, + refetchInterval: REFETCH_INTERVAL, + }, + args: hookArgs, + }); + + const shutterVoteResult = useReadDisputeKitShutterIsVoteActive({ + query: { + enabled: isEnabled && disputeKitName === DisputeKits.Shutter, + refetchInterval: REFETCH_INTERVAL, + }, + args: hookArgs, + }); + + const gatedVoteResult = useReadDisputeKitGatedIsVoteActive({ query: { - enabled: !isUndefined(roundId) && !isUndefined(voteId), + enabled: isEnabled && disputeKitName === DisputeKits.Gated, refetchInterval: REFETCH_INTERVAL, }, - args: [BigInt(id ?? 0), roundId, voteId], + args: hookArgs, }); + const gatedShutterVoteResult = useReadDisputeKitGatedShutterIsVoteActive({ + query: { + enabled: isEnabled && disputeKitName === DisputeKits.GatedShutter, + refetchInterval: REFETCH_INTERVAL, + }, + args: hookArgs, + }); + + const hasVoted = useMemo(() => { + switch (disputeKitName) { + case DisputeKits.Classic: + return classicVoteResult.data; + case DisputeKits.Shutter: + return shutterVoteResult.data; + case DisputeKits.Gated: + return gatedVoteResult.data; + case DisputeKits.GatedShutter: + return gatedShutterVoteResult.data; + default: + return undefined; + } + }, [ + disputeKitName, + classicVoteResult.data, + shutterVoteResult.data, + gatedVoteResult.data, + gatedShutterVoteResult.data, + ]); + const wasDrawn = useMemo(() => !isUndefined(drawData) && drawData.draws.length > 0, [drawData]); const isHiddenVotes = useMemo(() => disputeData?.dispute?.court.hiddenVotes ?? false, [disputeData]); const isCommitPeriod = useMemo(() => disputeData?.dispute?.period === "commit", [disputeData]); @@ -50,14 +109,6 @@ export const VotingContextProvider: React.FC<{ children: React.ReactNode }> = ({ const commited = useMemo(() => !isUndefined(drawData) && drawData?.draws?.[0]?.vote?.commited, [drawData]); const commit = useMemo(() => drawData?.draws?.[0]?.vote?.commit, [drawData]); - - const hasVoted = useMemo(() => { - if (isHiddenVotes && isCommitPeriod) { - return commited; - } - return hasVotedClassic; - }, [isHiddenVotes, isCommitPeriod, commited, hasVotedClassic]); - return ( Date: Fri, 8 Aug 2025 02:09:36 +0200 Subject: [PATCH 16/46] chore: comment tweaking --- web/src/hooks/useVotingContext.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/src/hooks/useVotingContext.tsx b/web/src/hooks/useVotingContext.tsx index 4b216807a..59abeb2f7 100644 --- a/web/src/hooks/useVotingContext.tsx +++ b/web/src/hooks/useVotingContext.tsx @@ -48,7 +48,7 @@ export const VotingContextProvider: React.FC<{ children: React.ReactNode }> = ({ const hookArgs = [BigInt(id ?? 0), roundId, voteId] as const; const isEnabled = !isUndefined(roundId) && !isUndefined(voteId); - // Only call the hook for the specific dispute kit type + // Add a hook call for each DisputeKit const classicVoteResult = useReadDisputeKitClassicIsVoteActive({ query: { enabled: isEnabled && disputeKitName === DisputeKits.Classic, @@ -81,6 +81,7 @@ export const VotingContextProvider: React.FC<{ children: React.ReactNode }> = ({ args: hookArgs, }); + // Add a return for each DisputeKit const hasVoted = useMemo(() => { switch (disputeKitName) { case DisputeKits.Classic: From a8942cd8d135194388cb9aadb8fbaf4bee073cc2 Mon Sep 17 00:00:00 2001 From: kemuru <102478601+kemuru@users.noreply.github.com> Date: Sat, 9 Aug 2025 22:10:47 +0200 Subject: [PATCH 17/46] fix: add shutter api check --- web/src/pages/Cases/CaseDetails/Voting/Shutter/Commit.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/web/src/pages/Cases/CaseDetails/Voting/Shutter/Commit.tsx b/web/src/pages/Cases/CaseDetails/Voting/Shutter/Commit.tsx index db7172f74..437bfb3c2 100644 --- a/web/src/pages/Cases/CaseDetails/Voting/Shutter/Commit.tsx +++ b/web/src/pages/Cases/CaseDetails/Voting/Shutter/Commit.tsx @@ -85,6 +85,10 @@ const Commit: React.FC = ({ const handleCommit = useCallback( async (choice: bigint) => { + if (!import.meta.env.REACT_APP_SHUTTER_API) { + console.error("REACT_APP_SHUTTER_API environment variable is not set"); + throw new Error("Cannot commit vote: REACT_APP_SHUTTER_API environment variable is required but not set"); + } const message = { message: saltKey }; const rawSalt = !isUndefined(signingAccount) ? await signingAccount.signMessage(message) From 89848eaddcd22b8d3a9ac2d66f95ac2613ba9b1b Mon Sep 17 00:00:00 2001 From: kemuru <102478601+kemuru@users.noreply.github.com> Date: Sat, 9 Aug 2025 22:14:15 +0200 Subject: [PATCH 18/46] chore: check that its not empty too just in case --- web/src/pages/Cases/CaseDetails/Voting/Shutter/Commit.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/pages/Cases/CaseDetails/Voting/Shutter/Commit.tsx b/web/src/pages/Cases/CaseDetails/Voting/Shutter/Commit.tsx index 437bfb3c2..28363514f 100644 --- a/web/src/pages/Cases/CaseDetails/Voting/Shutter/Commit.tsx +++ b/web/src/pages/Cases/CaseDetails/Voting/Shutter/Commit.tsx @@ -85,8 +85,8 @@ const Commit: React.FC = ({ const handleCommit = useCallback( async (choice: bigint) => { - if (!import.meta.env.REACT_APP_SHUTTER_API) { - console.error("REACT_APP_SHUTTER_API environment variable is not set"); + if (!import.meta.env.REACT_APP_SHUTTER_API || import.meta.env.REACT_APP_SHUTTER_API.trim() === "") { + console.error("REACT_APP_SHUTTER_API environment variable is not set or is empty"); throw new Error("Cannot commit vote: REACT_APP_SHUTTER_API environment variable is required but not set"); } const message = { message: saltKey }; From b8628bb6df64053f419a14ce1f2325ed644b82e0 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Mon, 11 Aug 2025 22:58:49 +0100 Subject: [PATCH 19/46] fix: coverage script breaking after enabling viaIR --- contracts/.solcover.js | 1 + contracts/hardhat.config.ts | 2 +- contracts/scripts/coverage.sh | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/contracts/.solcover.js b/contracts/.solcover.js index d9911ce98..46bc39e3c 100644 --- a/contracts/.solcover.js +++ b/contracts/.solcover.js @@ -7,6 +7,7 @@ const shell = require("shelljs"); module.exports = { istanbulReporter: ["lcov"], configureYulOptimizer: true, + irMinimum: true, onCompileComplete: async function (_config) { await run("typechain"); }, diff --git a/contracts/hardhat.config.ts b/contracts/hardhat.config.ts index be54c9fcb..dc1cce1c6 100644 --- a/contracts/hardhat.config.ts +++ b/contracts/hardhat.config.ts @@ -28,7 +28,7 @@ const config: HardhatUserConfig = { { version: "0.8.30", settings: { - viaIR: true, + viaIR: process.env.VIA_IR !== "false", // Defaults to true optimizer: { enabled: true, runs: 10000, diff --git a/contracts/scripts/coverage.sh b/contracts/scripts/coverage.sh index c228fbae5..9a1fc24eb 100755 --- a/contracts/scripts/coverage.sh +++ b/contracts/scripts/coverage.sh @@ -21,6 +21,7 @@ fi # Generate the Hardhat coverage report yarn clean echo "Building contracts with Hardhat..." +export VIA_IR=false yarn build echo "Running Hardhat coverage..." yarn hardhat coverage --solcoverjs ./.solcover.js --temp artifacts --show-stack-traces --testfiles "test/**/*.ts" From d10251b3088479c456255044a13e9593566810bb Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Tue, 12 Aug 2025 01:36:13 +0100 Subject: [PATCH 20/46] chore: interfaces pragma set to any 0.8 solc version --- contracts/src/arbitration/interfaces/IArbitrableV2.sol | 2 +- contracts/src/arbitration/interfaces/IArbitratorV2.sol | 2 +- contracts/src/arbitration/interfaces/IDisputeKit.sol | 2 +- .../src/arbitration/interfaces/IDisputeTemplateRegistry.sol | 2 +- contracts/src/arbitration/interfaces/IEvidence.sol | 2 +- contracts/src/arbitration/interfaces/ISortitionModule.sol | 3 ++- contracts/src/gateway/interfaces/IForeignGateway.sol | 2 +- contracts/src/gateway/interfaces/IHomeGateway.sol | 2 +- contracts/src/rng/IRandomizer.sol | 2 +- contracts/src/rng/RNG.sol | 2 +- 10 files changed, 11 insertions(+), 10 deletions(-) diff --git a/contracts/src/arbitration/interfaces/IArbitrableV2.sol b/contracts/src/arbitration/interfaces/IArbitrableV2.sol index cba10115e..22dac6e4a 100644 --- a/contracts/src/arbitration/interfaces/IArbitrableV2.sol +++ b/contracts/src/arbitration/interfaces/IArbitrableV2.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; +pragma solidity >=0.8.0 <0.9.0; import "./IArbitratorV2.sol"; diff --git a/contracts/src/arbitration/interfaces/IArbitratorV2.sol b/contracts/src/arbitration/interfaces/IArbitratorV2.sol index e2e76badb..9559c81b3 100644 --- a/contracts/src/arbitration/interfaces/IArbitratorV2.sol +++ b/contracts/src/arbitration/interfaces/IArbitratorV2.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; +pragma solidity >=0.8.0 <0.9.0; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "./IArbitrableV2.sol"; diff --git a/contracts/src/arbitration/interfaces/IDisputeKit.sol b/contracts/src/arbitration/interfaces/IDisputeKit.sol index 4c8f458cc..423f38e3e 100644 --- a/contracts/src/arbitration/interfaces/IDisputeKit.sol +++ b/contracts/src/arbitration/interfaces/IDisputeKit.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; +pragma solidity >=0.8.0 <0.9.0; import "./IArbitratorV2.sol"; diff --git a/contracts/src/arbitration/interfaces/IDisputeTemplateRegistry.sol b/contracts/src/arbitration/interfaces/IDisputeTemplateRegistry.sol index 3ce9d522d..23e09f8d5 100644 --- a/contracts/src/arbitration/interfaces/IDisputeTemplateRegistry.sol +++ b/contracts/src/arbitration/interfaces/IDisputeTemplateRegistry.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; +pragma solidity >=0.8.0 <0.9.0; /// @title IDisputeTemplate /// @notice Dispute Template interface. diff --git a/contracts/src/arbitration/interfaces/IEvidence.sol b/contracts/src/arbitration/interfaces/IEvidence.sol index 0be6082b5..a7683186a 100644 --- a/contracts/src/arbitration/interfaces/IEvidence.sol +++ b/contracts/src/arbitration/interfaces/IEvidence.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; +pragma solidity >=0.8.0 <0.9.0; /// @title IEvidence interface IEvidence { diff --git a/contracts/src/arbitration/interfaces/ISortitionModule.sol b/contracts/src/arbitration/interfaces/ISortitionModule.sol index 183fc331a..5cf10e6ae 100644 --- a/contracts/src/arbitration/interfaces/ISortitionModule.sol +++ b/contracts/src/arbitration/interfaces/ISortitionModule.sol @@ -1,5 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; + +pragma solidity >=0.8.0 <0.9.0; import "../../libraries/Constants.sol"; diff --git a/contracts/src/gateway/interfaces/IForeignGateway.sol b/contracts/src/gateway/interfaces/IForeignGateway.sol index 7bdbe0f13..49f51e5de 100644 --- a/contracts/src/gateway/interfaces/IForeignGateway.sol +++ b/contracts/src/gateway/interfaces/IForeignGateway.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; +pragma solidity >=0.8.0 <0.9.0; import "../../arbitration/interfaces/IArbitratorV2.sol"; import "@kleros/vea-contracts/src/interfaces/gateways/IReceiverGateway.sol"; diff --git a/contracts/src/gateway/interfaces/IHomeGateway.sol b/contracts/src/gateway/interfaces/IHomeGateway.sol index 2ccc00ac7..b80f194d6 100644 --- a/contracts/src/gateway/interfaces/IHomeGateway.sol +++ b/contracts/src/gateway/interfaces/IHomeGateway.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; +pragma solidity >=0.8.0 <0.9.0; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@kleros/vea-contracts/src/interfaces/gateways/ISenderGateway.sol"; diff --git a/contracts/src/rng/IRandomizer.sol b/contracts/src/rng/IRandomizer.sol index ea549ffa4..6a6296f82 100644 --- a/contracts/src/rng/IRandomizer.sol +++ b/contracts/src/rng/IRandomizer.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; +pragma solidity >=0.8.0 <0.9.0; // Randomizer protocol interface interface IRandomizer { diff --git a/contracts/src/rng/RNG.sol b/contracts/src/rng/RNG.sol index 5142aa0e5..391da0075 100644 --- a/contracts/src/rng/RNG.sol +++ b/contracts/src/rng/RNG.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; +pragma solidity >=0.8.0 <0.9.0; interface RNG { /// @dev Request a random number. From 41e899801b26d32f61a103d245a413962f31b0c7 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Tue, 12 Aug 2025 02:08:36 +0100 Subject: [PATCH 21/46] chore: changelog --- contracts/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/CHANGELOG.md b/contracts/CHANGELOG.md index bfdfafd76..800208b3e 100644 --- a/contracts/CHANGELOG.md +++ b/contracts/CHANGELOG.md @@ -10,6 +10,7 @@ The format is based on [Common Changelog](https://common-changelog.org/). - Set the Hardhat Solidity version to v0.8.30 and enable the IR pipeline ([#2069](https://github.com/kleros/kleros-v2/issues/2069)) - Set the Foundry Solidity version to v0.8.30 and enable the IR pipeline ([#2073](https://github.com/kleros/kleros-v2/issues/2073)) +- Widen the allowed solc version to any v0.8.x for the interfaces only ([#2083](https://github.com/kleros/kleros-v2/issues/2083)) - Bump `hardhat` to v2.26.2 ([#2069](https://github.com/kleros/kleros-v2/issues/2069)) - Bump `@kleros/vea-contracts` to v0.7.0 ([#2073](https://github.com/kleros/kleros-v2/issues/2073)) From da7eea1215abaff9460b0b616412bae27e60c360 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Tue, 12 Aug 2025 02:56:50 +0100 Subject: [PATCH 22/46] refactor: minor improvement --- contracts/src/rng/IRNG.sol | 2 +- contracts/src/rng/RNGWithFallback.sol | 53 +++++++++++++-------------- 2 files changed, 26 insertions(+), 29 deletions(-) diff --git a/contracts/src/rng/IRNG.sol b/contracts/src/rng/IRNG.sol index 0a854484b..152c1f15b 100644 --- a/contracts/src/rng/IRNG.sol +++ b/contracts/src/rng/IRNG.sol @@ -8,6 +8,6 @@ interface IRNG { function requestRandomness() external; /// @dev Receive the random number. - /// @return randomNumber Random Number. If the number is not ready or has not been required 0 instead. + /// @return randomNumber Random number or 0 if not available function receiveRandomness() external returns (uint256 randomNumber); } diff --git a/contracts/src/rng/RNGWithFallback.sol b/contracts/src/rng/RNGWithFallback.sol index 95b635cef..3bd8daed7 100644 --- a/contracts/src/rng/RNGWithFallback.sol +++ b/contracts/src/rng/RNGWithFallback.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.24; import "./IRNG.sol"; /// @title RNG with fallback mechanism -/// @notice Uses multiple RNG implementations with automatic fallback if default RNG does not respond passed a timeout. +/// @notice Uses a primary RNG implementation with automatic fallback to a Blockhash RNG if the primary RNG does not respond passed a timeout. contract RNGWithFallback is IRNG { // ************************************* // // * Storage * // @@ -54,33 +54,6 @@ contract RNGWithFallback is IRNG { _; } - // ************************************* // - // * State Modifiers * // - // ************************************* // - - /// @dev Request a random number from the RNG - function requestRandomness() external override onlyByConsumer { - requestTimestamp = block.timestamp; - rng.requestRandomness(); - } - - /// @dev Receive the random number with fallback logic - /// @return randomNumber Random Number - function receiveRandomness() external override onlyByConsumer returns (uint256 randomNumber) { - // Try to get random number from the RNG contract - randomNumber = rng.receiveRandomness(); - - // If we got a valid number, clear the request - if (randomNumber != 0) { - return randomNumber; - } else if (block.timestamp > requestTimestamp + fallbackTimeoutSeconds) { - // If the timeout is exceeded, try the fallback - randomNumber = uint256(blockhash(block.number - 1)); - emit RNGFallback(); - } - return randomNumber; - } - // ************************************* // // * Governance Functions * // // ************************************* // @@ -103,4 +76,28 @@ contract RNGWithFallback is IRNG { fallbackTimeoutSeconds = _fallbackTimeoutSeconds; emit FallbackTimeoutChanged(_fallbackTimeoutSeconds); } + + // ************************************* // + // * State Modifiers * // + // ************************************* // + + /// @dev Request a random number from the primary RNG + /// @dev The consumer is trusted not to make concurrent requests. + function requestRandomness() external override onlyByConsumer { + requestTimestamp = block.timestamp; + rng.requestRandomness(); + } + + /// @dev Receive the random number from the primary RNG with fallback to the blockhash RNG if the primary RNG does not respond passed a timeout. + /// @return randomNumber Random number or 0 if not available + function receiveRandomness() external override onlyByConsumer returns (uint256 randomNumber) { + randomNumber = rng.receiveRandomness(); + + // If we didn't get a random number and the timeout is exceeded, try the fallback + if (randomNumber == 0 && block.timestamp > requestTimestamp + fallbackTimeoutSeconds) { + randomNumber = uint256(blockhash(block.number - 1)); + emit RNGFallback(); + } + return randomNumber; + } } From a8f34152e4c329c054c2efc6cff6353006134589 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Tue, 12 Aug 2025 03:48:50 +0100 Subject: [PATCH 23/46] chore: deployment script support for RNGWithFallback --- contracts/deploy/00-ethereum-pnk.ts | 31 ------------ .../deploy/00-home-chain-arbitration-neo.ts | 22 +++++--- contracts/deploy/00-home-chain-arbitration.ts | 16 +++--- contracts/deploy/00-home-chain-pnk-faucet.ts | 36 ------------- contracts/deploy/00-home-chain-resolver.ts | 3 +- ...0-chainlink-rng.ts => 00-rng-chainlink.ts} | 33 ++++++++++-- ...randomizer-rng.ts => 00-rng-randomizer.ts} | 32 ++++++++++-- contracts/deploy/00-rng.ts | 50 ------------------- ... => change-arbitrable-dispute-template.ts} | 0 contracts/test/rng/index.ts | 11 +++- 10 files changed, 91 insertions(+), 143 deletions(-) delete mode 100644 contracts/deploy/00-ethereum-pnk.ts delete mode 100644 contracts/deploy/00-home-chain-pnk-faucet.ts rename contracts/deploy/{00-chainlink-rng.ts => 00-rng-chainlink.ts} (76%) rename contracts/deploy/{00-randomizer-rng.ts => 00-rng-randomizer.ts} (54%) delete mode 100644 contracts/deploy/00-rng.ts rename contracts/deploy/{05-arbitrable-dispute-template.ts => change-arbitrable-dispute-template.ts} (100%) diff --git a/contracts/deploy/00-ethereum-pnk.ts b/contracts/deploy/00-ethereum-pnk.ts deleted file mode 100644 index 85c674fb7..000000000 --- a/contracts/deploy/00-ethereum-pnk.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { HardhatRuntimeEnvironment } from "hardhat/types"; -import { DeployFunction } from "hardhat-deploy/types"; -import { ForeignChains, HardhatChain, isSkipped } from "./utils"; - -enum Chains { - SEPOLIA = ForeignChains.ETHEREUM_SEPOLIA, - HARDHAT = HardhatChain.HARDHAT, -} - -const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { - const { deployments, getNamedAccounts, getChainId } = hre; - const { deploy } = deployments; - - // fallback to hardhat node signers on local network - const deployer = (await getNamedAccounts()).deployer ?? (await hre.ethers.getSigners())[0].address; - const chainId = Number(await getChainId()); - console.log("deploying to %s with deployer %s", Chains[chainId], deployer); - - await deploy("PinakionV2", { - from: deployer, - args: [], - log: true, - }); -}; - -deployArbitration.tags = ["Pinakion"]; -deployArbitration.skip = async ({ network }) => { - return isSkipped(network, !Chains[network.config.chainId ?? 0]); -}; - -export default deployArbitration; diff --git a/contracts/deploy/00-home-chain-arbitration-neo.ts b/contracts/deploy/00-home-chain-arbitration-neo.ts index 8d291d570..68672b841 100644 --- a/contracts/deploy/00-home-chain-arbitration-neo.ts +++ b/contracts/deploy/00-home-chain-arbitration-neo.ts @@ -6,7 +6,7 @@ import { changeCurrencyRate } from "./utils/klerosCoreHelper"; import { HomeChains, isSkipped, isDevnet, PNK, ETH } from "./utils"; import { getContractOrDeploy, getContractOrDeployUpgradable } from "./utils/getContractOrDeploy"; import { deployERC20AndFaucet, deployERC721 } from "./utils/deployTokens"; -import { ChainlinkRNG, DisputeKitClassic, KlerosCoreNeo } from "../typechain-types"; +import { ChainlinkRNG, DisputeKitClassic, KlerosCoreNeo, RNGWithFallback } from "../typechain-types"; const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const { ethers, deployments, getNamedAccounts, getChainId } = hre; @@ -44,12 +44,20 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) const devnet = isDevnet(hre.network); const minStakingTime = devnet ? 180 : 1800; const maxFreezingTime = devnet ? 600 : 1800; - const rng = (await ethers.getContract("ChainlinkRNG")) as ChainlinkRNG; + const rngWithFallback = await ethers.getContract("RNGWithFallback"); const maxStakePerJuror = PNK(2_000); const maxTotalStaked = PNK(2_000_000); const sortitionModule = await deployUpgradable(deployments, "SortitionModuleNeo", { from: deployer, - args: [deployer, klerosCoreAddress, minStakingTime, maxFreezingTime, rng.target, maxStakePerJuror, maxTotalStaked], + args: [ + deployer, + klerosCoreAddress, + minStakingTime, + maxFreezingTime, + rngWithFallback.target, + maxStakePerJuror, + maxTotalStaked, + ], log: true, }); // nonce (implementation), nonce+1 (proxy) @@ -84,11 +92,11 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) await disputeKitContract.changeCore(klerosCore.address); } - // rng.changeConsumer() only if necessary - const rngConsumer = await rng.consumer(); + // rngWithFallback.changeConsumer() only if necessary + const rngConsumer = await rngWithFallback.consumer(); if (rngConsumer !== sortitionModule.address) { - console.log(`rng.changeConsumer(${sortitionModule.address})`); - await rng.changeConsumer(sortitionModule.address); + console.log(`rngWithFallback.changeConsumer(${sortitionModule.address})`); + await rngWithFallback.changeConsumer(sortitionModule.address); } const core = (await hre.ethers.getContract("KlerosCoreNeo")) as KlerosCoreNeo; diff --git a/contracts/deploy/00-home-chain-arbitration.ts b/contracts/deploy/00-home-chain-arbitration.ts index 80e1bd506..6bd763a88 100644 --- a/contracts/deploy/00-home-chain-arbitration.ts +++ b/contracts/deploy/00-home-chain-arbitration.ts @@ -6,7 +6,7 @@ import { changeCurrencyRate } from "./utils/klerosCoreHelper"; import { HomeChains, isSkipped, isDevnet, PNK, ETH, Courts } from "./utils"; import { getContractOrDeploy, getContractOrDeployUpgradable } from "./utils/getContractOrDeploy"; import { deployERC20AndFaucet } from "./utils/deployTokens"; -import { ChainlinkRNG, DisputeKitClassic, KlerosCore } from "../typechain-types"; +import { ChainlinkRNG, DisputeKitClassic, KlerosCore, RNGWithFallback } from "../typechain-types"; const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const { ethers, deployments, getNamedAccounts, getChainId } = hre; @@ -49,10 +49,10 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) const devnet = isDevnet(hre.network); const minStakingTime = devnet ? 180 : 1800; const maxFreezingTime = devnet ? 600 : 1800; - const rng = (await ethers.getContract("ChainlinkRNG")) as ChainlinkRNG; + const rngWithFallback = await ethers.getContract("RNGWithFallback"); const sortitionModule = await deployUpgradable(deployments, "SortitionModule", { from: deployer, - args: [deployer, klerosCoreAddress, minStakingTime, maxFreezingTime, rng.target], + args: [deployer, klerosCoreAddress, minStakingTime, maxFreezingTime, rngWithFallback.target], log: true, }); // nonce (implementation), nonce+1 (proxy) @@ -79,18 +79,18 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) }); // nonce+2 (implementation), nonce+3 (proxy) // disputeKit.changeCore() only if necessary - const disputeKitContract = (await ethers.getContract("DisputeKitClassic")) as DisputeKitClassic; + const disputeKitContract = await ethers.getContract("DisputeKitClassic"); const currentCore = await disputeKitContract.core(); if (currentCore !== klerosCore.address) { console.log(`disputeKit.changeCore(${klerosCore.address})`); await disputeKitContract.changeCore(klerosCore.address); } - // rng.changeConsumer() only if necessary - const rngConsumer = await rng.consumer(); + // rngWithFallback.changeConsumer() only if necessary + const rngConsumer = await rngWithFallback.consumer(); if (rngConsumer !== sortitionModule.address) { - console.log(`rng.changeConsumer(${sortitionModule.address})`); - await rng.changeConsumer(sortitionModule.address); + console.log(`rngWithFallback.changeConsumer(${sortitionModule.address})`); + await rngWithFallback.changeConsumer(sortitionModule.address); } const core = (await hre.ethers.getContract("KlerosCore")) as KlerosCore; diff --git a/contracts/deploy/00-home-chain-pnk-faucet.ts b/contracts/deploy/00-home-chain-pnk-faucet.ts deleted file mode 100644 index e10eb93bf..000000000 --- a/contracts/deploy/00-home-chain-pnk-faucet.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { HardhatRuntimeEnvironment } from "hardhat/types"; -import { DeployFunction } from "hardhat-deploy/types"; -import { HomeChains, isSkipped } from "./utils"; - -const pnkByChain = new Map([ - [HomeChains.ARBITRUM_ONE, "0x330bD769382cFc6d50175903434CCC8D206DCAE5"], - [HomeChains.ARBITRUM_SEPOLIA, "INSERT ARBITRUM SEPOLIA PNK TOKEN ADDRESS HERE"], -]); - -const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { - const { deployments, getNamedAccounts, getChainId } = hre; - const { deploy, execute } = deployments; - - // fallback to hardhat node signers on local network - const deployer = (await getNamedAccounts()).deployer ?? (await hre.ethers.getSigners())[0].address; - const chainId = Number(await getChainId()); - console.log("deploying to %s with deployer %s", HomeChains[chainId], deployer); - - const pnkAddress = pnkByChain.get(chainId); - if (pnkAddress) { - await deploy("PNKFaucet", { - from: deployer, - contract: "Faucet", - args: [pnkAddress], - log: true, - }); - await execute("PNKFaucet", { from: deployer, log: true }, "changeAmount", hre.ethers.parseUnits("10000", "ether")); - } -}; - -deployArbitration.tags = ["PnkFaucet"]; -deployArbitration.skip = async ({ network }) => { - return isSkipped(network, !HomeChains[network.config.chainId ?? 0]); -}; - -export default deployArbitration; diff --git a/contracts/deploy/00-home-chain-resolver.ts b/contracts/deploy/00-home-chain-resolver.ts index 5aa5e7b20..64d3431f6 100644 --- a/contracts/deploy/00-home-chain-resolver.ts +++ b/contracts/deploy/00-home-chain-resolver.ts @@ -1,6 +1,7 @@ import { HardhatRuntimeEnvironment } from "hardhat/types"; import { DeployFunction } from "hardhat-deploy/types"; import { HomeChains, isSkipped } from "./utils"; +import { getContractOrDeploy } from "./utils/getContractOrDeploy"; const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const { deployments, getNamedAccounts, getChainId } = hre; @@ -14,7 +15,7 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) const klerosCore = await deployments.get("KlerosCore"); const disputeTemplateRegistry = await deployments.get("DisputeTemplateRegistry"); - await deploy("DisputeResolver", { + await getContractOrDeploy(hre, "DisputeResolver", { from: deployer, args: [klerosCore.address, disputeTemplateRegistry.address], log: true, diff --git a/contracts/deploy/00-chainlink-rng.ts b/contracts/deploy/00-rng-chainlink.ts similarity index 76% rename from contracts/deploy/00-chainlink-rng.ts rename to contracts/deploy/00-rng-chainlink.ts index a811f642b..78a1c5e87 100644 --- a/contracts/deploy/00-chainlink-rng.ts +++ b/contracts/deploy/00-rng-chainlink.ts @@ -2,10 +2,10 @@ import { HardhatRuntimeEnvironment } from "hardhat/types"; import { DeployFunction } from "hardhat-deploy/types"; import { HomeChains, isSkipped } from "./utils"; import { getContractOrDeploy } from "./utils/getContractOrDeploy"; +import { RNGWithFallback } from "../typechain-types"; const deployRng: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { - const { deployments, getNamedAccounts, getChainId } = hre; - const { deploy } = deployments; + const { getNamedAccounts, getChainId, ethers } = hre; // fallback to hardhat node signers on local network const deployer = (await getNamedAccounts()).deployer ?? (await hre.ethers.getSigners())[0].address; @@ -57,11 +57,16 @@ const deployRng: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const requestConfirmations = 200; // between 1 and 200 L2 blocks const callbackGasLimit = 100000; - await deploy("ChainlinkRNG", { + const oldRng = await ethers.getContractOrNull("ChainlinkRNG"); + if (!oldRng) { + console.log("Register this Chainlink consumer here: http://vrf.chain.link/"); + } + + const rng = await getContractOrDeploy(hre, "ChainlinkRNG", { from: deployer, args: [ deployer, - deployer, // The consumer is configured as the SortitionModule later + deployer, // The consumer is configured as the RNGWithFallback later ChainlinkVRFCoordinator.target, keyHash, subscriptionId, @@ -71,7 +76,25 @@ const deployRng: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { log: true, }); - console.log("Register this Chainlink consumer here: http://vrf.chain.link/"); + const fallbackTimeoutSeconds = 30 * 60; // 30 minutes + await getContractOrDeploy(hre, "RNGWithFallback", { + from: deployer, + args: [ + deployer, + deployer, // The consumer is configured as the SortitionModule later + fallbackTimeoutSeconds, + rng.target, + ], + log: true, + }); + + // rng.changeConsumer() only if necessary + const rngWithFallback = await ethers.getContract("RNGWithFallback"); + const rngConsumer = await rng.consumer(); + if (rngConsumer !== rngWithFallback.target) { + console.log(`rng.changeConsumer(${rngWithFallback.target})`); + await rng.changeConsumer(rngWithFallback.target); + } }; deployRng.tags = ["ChainlinkRNG"]; diff --git a/contracts/deploy/00-randomizer-rng.ts b/contracts/deploy/00-rng-randomizer.ts similarity index 54% rename from contracts/deploy/00-randomizer-rng.ts rename to contracts/deploy/00-rng-randomizer.ts index c28dc3cda..8413b39f6 100644 --- a/contracts/deploy/00-randomizer-rng.ts +++ b/contracts/deploy/00-rng-randomizer.ts @@ -2,10 +2,10 @@ import { HardhatRuntimeEnvironment } from "hardhat/types"; import { DeployFunction } from "hardhat-deploy/types"; import { HomeChains, isSkipped } from "./utils"; import { getContractOrDeploy } from "./utils/getContractOrDeploy"; +import { RNGWithFallback } from "../typechain-types"; const deployRng: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { - const { deployments, getNamedAccounts, getChainId } = hre; - const { deploy } = deployments; + const { getNamedAccounts, getChainId, ethers } = hre; // fallback to hardhat node signers on local network const deployer = (await getNamedAccounts()).deployer ?? (await hre.ethers.getSigners())[0].address; @@ -20,11 +20,35 @@ const deployRng: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { log: true, }); - await getContractOrDeploy(hre, "RandomizerRNG", { + const rng = await getContractOrDeploy(hre, "RandomizerRNG", { from: deployer, - args: [deployer, deployer, randomizerOracle.target], // The consumer is configured as the SortitionModule later + args: [ + deployer, + deployer, // The consumer is configured as the RNGWithFallback later + randomizerOracle.target, + ], log: true, }); + + const fallbackTimeoutSeconds = 30 * 60; // 30 minutes + await getContractOrDeploy(hre, "RNGWithFallback", { + from: deployer, + args: [ + deployer, + deployer, // The consumer is configured as the SortitionModule later + fallbackTimeoutSeconds, + rng.target, + ], + log: true, + }); + + // rng.changeConsumer() only if necessary + const rngWithFallback = await ethers.getContract("RNGWithFallback"); + const rngConsumer = await rng.consumer(); + if (rngConsumer !== rngWithFallback.target) { + console.log(`rng.changeConsumer(${rngWithFallback.target})`); + await rng.changeConsumer(rngWithFallback.target); + } }; deployRng.tags = ["RandomizerRNG"]; diff --git a/contracts/deploy/00-rng.ts b/contracts/deploy/00-rng.ts deleted file mode 100644 index 5eedf19b2..000000000 --- a/contracts/deploy/00-rng.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { HardhatRuntimeEnvironment } from "hardhat/types"; -import { DeployFunction } from "hardhat-deploy/types"; -import { SortitionModule } from "../typechain-types"; -import { HomeChains, isMainnet, isSkipped } from "./utils"; -import { getContractOrDeploy } from "./utils/getContractOrDeploy"; - -const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { - const { deployments, getNamedAccounts, getChainId, ethers } = hre; - const { deploy } = deployments; - const RNG_LOOKAHEAD_TIME = 30 * 60; // 30 minutes in seconds - - // fallback to hardhat node signers on local network - const deployer = (await getNamedAccounts()).deployer ?? (await hre.ethers.getSigners())[0].address; - const chainId = Number(await getChainId()); - console.log("deploying to %s with deployer %s", HomeChains[chainId], deployer); - - const sortitionModule = (await ethers.getContract("SortitionModuleNeo")) as SortitionModule; - - const randomizerOracle = await getContractOrDeploy(hre, "RandomizerOracle", { - from: deployer, - contract: "RandomizerMock", - args: [], - log: true, - }); - - const rng1 = await deploy("RandomizerRNG", { - from: deployer, - args: [deployer, sortitionModule.target, randomizerOracle.address], - log: true, - }); - - const rng2 = await deploy("BlockHashRNG", { - from: deployer, - args: [ - deployer, // governor - sortitionModule.target, // consumer - RNG_LOOKAHEAD_TIME, - ], - log: true, - }); - - await sortitionModule.changeRandomNumberGenerator(rng2.address); -}; - -deployArbitration.tags = ["RNG"]; -deployArbitration.skip = async ({ network }) => { - return isSkipped(network, isMainnet(network)); -}; - -export default deployArbitration; diff --git a/contracts/deploy/05-arbitrable-dispute-template.ts b/contracts/deploy/change-arbitrable-dispute-template.ts similarity index 100% rename from contracts/deploy/05-arbitrable-dispute-template.ts rename to contracts/deploy/change-arbitrable-dispute-template.ts diff --git a/contracts/test/rng/index.ts b/contracts/test/rng/index.ts index 5e74b4a3a..3bb50fd7c 100644 --- a/contracts/test/rng/index.ts +++ b/contracts/test/rng/index.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import { deployments, ethers, network } from "hardhat"; +import { deployments, ethers, getNamedAccounts, network } from "hardhat"; import { IncrementalNG, BlockHashRNG, @@ -11,6 +11,7 @@ import { const initialNg = 424242; const abiCoder = ethers.AbiCoder.defaultAbiCoder(); +let deployer: string; describe("IncrementalNG", async () => { let rng: IncrementalNG; @@ -85,12 +86,16 @@ describe("ChainlinkRNG", async () => { let vrfCoordinator: ChainlinkVRFCoordinatorV2Mock; beforeEach("Setup", async () => { + ({ deployer } = await getNamedAccounts()); + await deployments.fixture(["ChainlinkRNG"], { fallbackToGlobal: true, keepExistingDeployments: false, }); rng = (await ethers.getContract("ChainlinkRNG")) as ChainlinkRNG; vrfCoordinator = (await ethers.getContract("ChainlinkVRFCoordinator")) as ChainlinkVRFCoordinatorV2Mock; + + await rng.changeConsumer(deployer); }); it("Should return a non-zero random number", async () => { @@ -144,12 +149,16 @@ describe("RandomizerRNG", async () => { let randomizer: RandomizerMock; beforeEach("Setup", async () => { + ({ deployer } = await getNamedAccounts()); + await deployments.fixture(["RandomizerRNG"], { fallbackToGlobal: true, keepExistingDeployments: false, }); rng = (await ethers.getContract("RandomizerRNG")) as RandomizerRNG; randomizer = (await ethers.getContract("RandomizerOracle")) as RandomizerMock; + + await rng.changeConsumer(deployer); }); it("Should return a non-zero random number", async () => { From 78b31699e707aa7f9c6c4be7aa542b78b5220cb0 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Tue, 12 Aug 2025 17:56:24 +0100 Subject: [PATCH 24/46] docs: metrics for upcoming audit --- contracts/audit/METRICS.md | 681 +++++++++++++++++++++++++++ contracts/scripts/generateMetrics.sh | 23 + 2 files changed, 704 insertions(+) create mode 100644 contracts/audit/METRICS.md create mode 100755 contracts/scripts/generateMetrics.sh diff --git a/contracts/audit/METRICS.md b/contracts/audit/METRICS.md new file mode 100644 index 000000000..1a1a40ea3 --- /dev/null +++ b/contracts/audit/METRICS.md @@ -0,0 +1,681 @@ +[get in touch with Consensys Diligence](https://consensys.io/diligence)
+ +[[ 🌐 ](https://consensys.io/diligence) [ 📩 ](mailto:diligence@consensys.net) [ 🔥 ](https://consensys.io/diligence/tools/)] +

+ +# Solidity Metrics for 'CLI' + +## Table of contents + +- [Scope](#t-scope) + - [Source Units in Scope](#t-source-Units-in-Scope) + - [Deployable Logic Contracts](#t-deployable-contracts) + - [Out of Scope](#t-out-of-scope) + - [Excluded Source Units](#t-out-of-scope-excluded-source-units) + - [Duplicate Source Units](#t-out-of-scope-duplicate-source-units) + - [Doppelganger Contracts](#t-out-of-scope-doppelganger-contracts) +- [Report Overview](#t-report) + - [Risk Summary](#t-risk) + - [Source Lines](#t-source-lines) + - [Inline Documentation](#t-inline-documentation) + - [Components](#t-components) + - [Exposed Functions](#t-exposed-functions) + - [StateVariables](#t-statevariables) + - [Capabilities](#t-capabilities) + - [Dependencies](#t-package-imports) + - [Totals](#t-totals) + +## Scope + +This section lists files that are in scope for the metrics report. + +- **Project:** `'CLI'` +- **Included Files:** + - `` +- **Excluded Paths:** + - `` +- **File Limit:** `undefined` + + - **Exclude File list Limit:** `undefined` + +- **Workspace Repository:** `unknown` (`undefined`@`undefined`) + +### Source Units in Scope + +Source Units Analyzed: **`31`**
+Source Units in Scope: **`31`** (**100%**) + +| Type | File | Logic Contracts | Interfaces | Lines | nLines | nSLOC | Comment Lines | Complex. Score | Capabilities | +| -------- | --------------------------------------------------------- | --------------- | ---------- | -------- | -------- | -------- | ------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| 📝 | src/arbitration/KlerosCore.sol | 1 | \*\*\*\* | 75 | 63 | 28 | 26 | 13 | \*\*\*\* | +| 🎨 | src/arbitration/KlerosCoreBase.sol | 1 | \*\*\*\* | 1211 | 1162 | 809 | 286 | 449 | **🖥💰** | +| 📝 | src/arbitration/KlerosCoreNeo.sol | 1 | \*\*\*\* | 142 | 124 | 55 | 51 | 36 | \*\*\*\* | +| 📝 | src/arbitration/PolicyRegistry.sol | 1 | \*\*\*\* | 88 | 88 | 30 | 41 | 24 | \*\*\*\* | +| 📝 | src/arbitration/SortitionModule.sol | 1 | \*\*\*\* | 50 | 44 | 15 | 20 | 13 | \*\*\*\* | +| 🎨 | src/arbitration/SortitionModuleBase.sol | 1 | \*\*\*\* | 689 | 645 | 393 | 205 | 300 | **🖥🧮** | +| 📝 | src/arbitration/SortitionModuleNeo.sol | 1 | \*\*\*\* | 103 | 91 | 48 | 28 | 31 | \*\*\*\* | +| 📝 | src/arbitration/arbitrables/DisputeResolver.sol | 1 | \*\*\*\* | 149 | 134 | 72 | 50 | 43 | **💰** | +| 📝 | src/arbitration/DisputeTemplateRegistry.sol | 1 | \*\*\*\* | 83 | 79 | 30 | 33 | 25 | \*\*\*\* | +| 📝 | src/arbitration/dispute-kits/DisputeKitClassic.sol | 1 | \*\*\*\* | 46 | 46 | 16 | 21 | 13 | \*\*\*\* | +| 🎨 | src/arbitration/dispute-kits/DisputeKitClassicBase.sol | 1 | \*\*\*\* | 713 | 632 | 365 | 225 | 192 | **💰🧮** | +| 📝🔍 | src/arbitration/dispute-kits/DisputeKitGated.sol | 1 | 2 | 117 | 99 | 39 | 51 | 57 | **🖥🔆** | +| 📝🔍 | src/arbitration/dispute-kits/DisputeKitGatedShutter.sol | 1 | 2 | 194 | 159 | 59 | 84 | 69 | **🖥🧮🔆** | +| 📝 | src/arbitration/dispute-kits/DisputeKitShutter.sol | 1 | \*\*\*\* | 123 | 107 | 36 | 54 | 27 | **🧮** | +| 📝🔍 | src/arbitration/dispute-kits/DisputeKitSybilResistant.sol | 1 | 1 | 76 | 58 | 20 | 33 | 16 | **🔆** | +| 📝 | src/arbitration/evidence/EvidenceModule.sol | 1 | \*\*\*\* | 70 | 70 | 26 | 30 | 21 | \*\*\*\* | +| 🔍 | src/arbitration/interfaces/IArbitrableV2.sol | \*\*\*\* | 1 | 40 | 39 | 12 | 22 | 3 | \*\*\*\* | +| 🔍 | src/arbitration/interfaces/IArbitratorV2.sol | \*\*\*\* | 1 | 83 | 44 | 9 | 50 | 14 | **💰** | +| 🔍 | src/arbitration/interfaces/IDisputeKit.sol | \*\*\*\* | 1 | 128 | 39 | 11 | 60 | 23 | \*\*\*\* | +| 🔍 | src/arbitration/interfaces/IDisputeTemplateRegistry.sol | \*\*\*\* | 1 | 25 | 20 | 9 | 8 | 3 | \*\*\*\* | +| 🔍 | src/arbitration/interfaces/IEvidence.sol | \*\*\*\* | 1 | 12 | 12 | 4 | 6 | 1 | \*\*\*\* | +| 🔍 | src/arbitration/interfaces/ISortitionModule.sol | \*\*\*\* | 1 | 63 | 16 | 10 | 4 | 33 | \*\*\*\* | +| | src/libraries/Constants.sol | \*\*\*\* | \*\*\*\* | 39 | 39 | 26 | 13 | 2 | \*\*\*\* | +| 📚 | src/libraries/SafeERC20.sol | 1 | \*\*\*\* | 47 | 47 | 18 | 24 | 12 | \*\*\*\* | +| 📚🔍 | src/libraries/SafeSend.sol | 1 | 1 | 24 | 19 | 9 | 7 | 15 | **💰📤** | +| 📝 | src/rng/RNGWithFallback.sol | 1 | \*\*\*\* | 103 | 103 | 48 | 41 | 35 | \*\*\*\* | +| 📝 | src/rng/ChainlinkRNG.sol | 1 | \*\*\*\* | 173 | 173 | 85 | 70 | 50 | \*\*\*\* | +| 🔍 | src/rng/IRNG.sol | \*\*\*\* | 1 | 13 | 8 | 3 | 5 | 5 | \*\*\*\* | +| 🎨 | src/proxy/UUPSProxiable.sol | 1 | \*\*\*\* | 140 | 122 | 44 | 71 | 46 | **🖥💰👥♻️** | +| 📝 | src/proxy/UUPSProxy.sol | 1 | \*\*\*\* | 90 | 90 | 38 | 37 | 65 | **🖥💰👥** | +| 🎨 | src/proxy/Initializable.sol | 1 | \*\*\*\* | 215 | 215 | 70 | 128 | 31 | **🖥** | +| 📝📚🔍🎨 | **Totals** | **23** | **13** | **5124** | **4587** | **2437** | **1784** | **1667** | **🖥💰📤👥🧮🔆♻️** | + + +Legend: [➕] + + + +##### Deployable Logic Contracts + +Total: 16 + +- 📝 `KlerosCore` +- 📝 `KlerosCoreNeo` +- 📝 `PolicyRegistry` +- 📝 `SortitionModule` +- 📝 `SortitionModuleNeo` +- [➕] + + +#### Out of Scope + +##### Excluded Source Units + +Source Units Excluded: **`0`** + +[➕] + + + +##### Duplicate Source Units + +Duplicate Source Units Excluded: **`0`** + +[➕] + + + +##### Doppelganger Contracts + +Doppelganger Contracts: **`3`** + +[➕] + + + +## Report + +### Overview + +The analysis finished with **`0`** errors and **`0`** duplicate files. + +#### Risk + +
+ +
+ +#### Source Lines (sloc vs. nsloc) + +
+ +
+ +#### Inline Documentation + +- **Comment-to-Source Ratio:** On average there are`1.59` code lines per comment (lower=better). +- **ToDo's:** `2` + +#### Components + +| 📝Contracts | 📚Libraries | 🔍Interfaces | 🎨Abstract | +| ----------- | ----------- | ------------ | ---------- | +| 16 | 2 | 13 | 5 | + +#### Exposed Functions + +This section lists functions that are explicitly declared public or payable. Please note that getter methods for public stateVars are not included. + +| 🌐Public | 💰Payable | +| -------- | --------- | +| 192 | 10 | + +| External | Internal | Private | Pure | View | +| -------- | -------- | ------- | ---- | ---- | +| 175 | 217 | 2 | 13 | 80 | + +#### StateVariables + +| Total | 🌐Public | +| ----- | -------- | +| 87 | 80 | + +#### Capabilities + +| Solidity Versions observed | 🧪 Experimental Features | 💰 Can Receive Funds | 🖥 Uses Assembly | 💣 Has Destroyable Contracts | +| ------------------------------ | ------------------------ | -------------------- | -------------------------- | ---------------------------- | +| `^0.8.24`
`>=0.8.0 <0.9.0` | | `yes` | `yes`
(12 asm blocks) | \*\*\*\* | + +| 📤 Transfers ETH | ⚡ Low-Level Calls | 👥 DelegateCall | 🧮 Uses Hash Functions | 🔖 ECRecover | 🌀 New/Create/Create2 | +| ---------------- | ------------------ | --------------- | ---------------------- | ------------ | --------------------- | +| `yes` | \*\*\*\* | `yes` | `yes` | \*\*\*\* | \*\*\*\* | + +| ♻️ TryCatch | Σ Unchecked | +| ----------- | ----------- | +| `yes` | \*\*\*\* | + +#### Dependencies / External Imports + +| Dependency / Import Path | Count | +| ------------------------------------------------------------------- | ----- | +| @chainlink/contracts/src/v0.8/vrf/dev/VRFConsumerBaseV2Plus.sol | 1 | +| @chainlink/contracts/src/v0.8/vrf/dev/libraries/VRFV2PlusClient.sol | 1 | +| @openzeppelin/contracts/token/ERC20/IERC20.sol | 3 | +| @openzeppelin/contracts/token/ERC721/IERC721.sol | 1 | + +#### Totals + +##### Summary + +
+ +
+ +##### AST Node Statistics + +###### Function Calls + +
+ +
+ +###### Assembly Calls + +
+ +
+ +###### AST Total + +
+ +
+ +##### Inheritance Graph + +[➕] + + + +##### CallGraph + +[➕] + + + +###### Contract Summary + +[➕] + + +____ + +Thinking about smart contract security? We can provide training, ongoing advice, and smart contract auditing. [Contact us](https://consensys.io/diligence/contact/). + diff --git a/contracts/scripts/generateMetrics.sh b/contracts/scripts/generateMetrics.sh new file mode 100755 index 000000000..4e7bea4be --- /dev/null +++ b/contracts/scripts/generateMetrics.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" + +SOURCE_DIR="src" + +yarn dlx solidity-code-metrics \ + "$SOURCE_DIR"/arbitration/KlerosCore* \ + "$SOURCE_DIR"/arbitration/PolicyRegistry.sol \ + "$SOURCE_DIR"/arbitration/SortitionModule* \ + "$SOURCE_DIR"/arbitration/arbitrables/DisputeResolver.sol \ + "$SOURCE_DIR"/arbitration/DisputeTemplateRegistry.sol \ + "$SOURCE_DIR"/arbitration/dispute-kits/* \ + "$SOURCE_DIR"/arbitration/evidence/EvidenceModule.sol \ + "$SOURCE_DIR"/arbitration/interfaces/* \ + "$SOURCE_DIR"/libraries/Constants.sol \ + "$SOURCE_DIR"/libraries/Safe* \ + "$SOURCE_DIR"/rng/RNGWithFallback.sol \ + "$SOURCE_DIR"/rng/ChainlinkRNG.sol \ + "$SOURCE_DIR"/rng/IRNG.sol \ + "$SOURCE_DIR"/proxy/UUPSProx* \ + "$SOURCE_DIR"/proxy/Initializable.sol \ +>METRICS.md From 738ef2c60ae34a5f192c129721ea29db3b9d352d Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Tue, 12 Aug 2025 17:58:05 +0100 Subject: [PATCH 25/46] docs: metrics for upcoming audit --- contracts/audit/METRICS.html | 209524 ++++++++++++++++++++++++++++++++ 1 file changed, 209524 insertions(+) create mode 100644 contracts/audit/METRICS.html diff --git a/contracts/audit/METRICS.html b/contracts/audit/METRICS.html new file mode 100644 index 000000000..df48b7b11 --- /dev/null +++ b/contracts/audit/METRICS.html @@ -0,0 +1,209524 @@ + + + + + + + + + Solidity Metrics + + + + + + + + + + + + + +
+ Rendering Report...

Note: This window will update automatically. In case it is not, close the window and try again (vscode + bug) :/ +
+
+ + From 82f8b1cd345df493de4b2b368bb2f6bf392a26af Mon Sep 17 00:00:00 2001 From: unknownunknown1 Date: Wed, 13 Aug 2025 16:39:20 +1000 Subject: [PATCH 26/46] feat: replace requires with custom errors --- .../arbitration/DisputeTemplateRegistry.sol | 8 +- contracts/src/arbitration/KlerosGovernor.sol | 61 +++++++++----- contracts/src/arbitration/PolicyRegistry.sol | 8 +- .../src/arbitration/SortitionModuleBase.sol | 49 +++++++---- .../arbitrables/ArbitrableExample.sol | 23 ++++-- .../arbitrables/DisputeResolver.sol | 24 ++++-- .../devtools/DisputeResolverRuler.sol | 2 +- .../dispute-kits/DisputeKitClassicBase.sol | 82 ++++++++++++------- .../arbitration/evidence/EvidenceModule.sol | 8 +- .../university/SortitionModuleUniversity.sol | 14 +++- .../view/KlerosCoreSnapshotProxy.sol | 8 +- contracts/test/arbitration/index.ts | 6 +- contracts/test/arbitration/staking-neo.ts | 10 ++- contracts/test/foundry/KlerosCore.t.sol | 48 +++++------ 14 files changed, 234 insertions(+), 117 deletions(-) diff --git a/contracts/src/arbitration/DisputeTemplateRegistry.sol b/contracts/src/arbitration/DisputeTemplateRegistry.sol index 2ea0a71f6..e63d7c297 100644 --- a/contracts/src/arbitration/DisputeTemplateRegistry.sol +++ b/contracts/src/arbitration/DisputeTemplateRegistry.sol @@ -25,7 +25,7 @@ contract DisputeTemplateRegistry is IDisputeTemplateRegistry, UUPSProxiable, Ini // ************************************* // modifier onlyByGovernor() { - require(governor == msg.sender, "Governor only"); + if (governor != msg.sender) revert GovernorOnly(); _; } @@ -80,4 +80,10 @@ contract DisputeTemplateRegistry is IDisputeTemplateRegistry, UUPSProxiable, Ini templateId = templates++; emit DisputeTemplate(templateId, _templateTag, _templateData, _templateDataMappings); } + + // ************************************* // + // * Errors * // + // ************************************* // + + error GovernorOnly(); } diff --git a/contracts/src/arbitration/KlerosGovernor.sol b/contracts/src/arbitration/KlerosGovernor.sol index 7e9415a7b..a0a23061e 100644 --- a/contracts/src/arbitration/KlerosGovernor.sol +++ b/contracts/src/arbitration/KlerosGovernor.sol @@ -70,18 +70,18 @@ contract KlerosGovernor is IArbitrableV2 { modifier duringSubmissionPeriod() { uint256 offset = sessions[sessions.length - 1].durationOffset; - require(block.timestamp - lastApprovalTime <= submissionTimeout + offset, "Submission time has ended."); + if (block.timestamp - lastApprovalTime > submissionTimeout + offset) revert SubmissionTimeHasEnded(); _; } modifier duringApprovalPeriod() { uint256 offset = sessions[sessions.length - 1].durationOffset; - require(block.timestamp - lastApprovalTime > submissionTimeout + offset, "Approval time not started yet."); + if (block.timestamp - lastApprovalTime <= submissionTimeout + offset) revert ApprovalTimeNotStarted(); _; } modifier onlyByGovernor() { - require(address(this) == msg.sender, "Only the governor allowed."); + if (address(this) != msg.sender) revert GovernorOnly(); _; } @@ -208,14 +208,14 @@ contract KlerosGovernor is IArbitrableV2 { uint256[] memory _dataSize, string memory _description ) external payable duringSubmissionPeriod { - require(_target.length == _value.length, "Wrong input: target and value"); - require(_target.length == _dataSize.length, "Wrong input: target and datasize"); + if (_target.length != _value.length) revert WrongInputTargetAndValue(); + if (_target.length != _dataSize.length) revert WrongInputTargetAndDatasize(); Session storage session = sessions[sessions.length - 1]; Submission storage submission = submissions.push(); submission.submitter = payable(msg.sender); // Do the assignment first to avoid creating a new variable and bypass a 'stack too deep' error. submission.deposit = submissionBaseDeposit + arbitrator.arbitrationCost(arbitratorExtraData); - require(msg.value >= submission.deposit, "Not enough ETH to cover deposit"); + if (msg.value < submission.deposit) revert InsufficientDeposit(); bytes32 listHash; bytes32 currentTxHash; @@ -233,7 +233,7 @@ contract KlerosGovernor is IArbitrableV2 { currentTxHash = keccak256(abi.encodePacked(transaction.target, transaction.value, transaction.data)); listHash = keccak256(abi.encodePacked(currentTxHash, listHash)); } - require(!session.alreadySubmitted[listHash], "List already submitted"); + if (session.alreadySubmitted[listHash]) revert ListAlreadySubmitted(); session.alreadySubmitted[listHash] = true; submission.listHash = listHash; submission.submissionTime = block.timestamp; @@ -256,11 +256,11 @@ contract KlerosGovernor is IArbitrableV2 { function withdrawTransactionList(uint256 _submissionID, bytes32 _listHash) external { Session storage session = sessions[sessions.length - 1]; Submission storage submission = submissions[session.submittedLists[_submissionID]]; - require(block.timestamp - lastApprovalTime <= submissionTimeout / 2, "Should be in first half"); - // This require statement is an extra check to prevent _submissionID linking to the wrong list because of index swap during withdrawal. - require(submission.listHash == _listHash, "Wrong list hash"); - require(submission.submitter == msg.sender, "Only submitter can withdraw"); - require(block.timestamp - submission.submissionTime <= withdrawTimeout, "Withdrawing time has passed."); + if (block.timestamp - lastApprovalTime > submissionTimeout / 2) revert ShouldOnlyWithdrawInFirstHalf(); + // This is an extra check to prevent _submissionID linking to the wrong list because of index swap during withdrawal. + if (submission.listHash != _listHash) revert WrongListHash(); + if (submission.submitter != msg.sender) revert OnlySubmitterCanWithdraw(); + if (block.timestamp - submission.submissionTime > withdrawTimeout) revert WithdrawingTimeHasPassed(); session.submittedLists[_submissionID] = session.submittedLists[session.submittedLists.length - 1]; session.alreadySubmitted[_listHash] = false; session.submittedLists.pop(); @@ -273,7 +273,7 @@ contract KlerosGovernor is IArbitrableV2 { /// If nothing was submitted changes session. function executeSubmissions() external duringApprovalPeriod { Session storage session = sessions[sessions.length - 1]; - require(session.status == Status.NoDispute, "Already disputed"); + if (session.status != Status.NoDispute) revert AlreadyDisputed(); if (session.submittedLists.length == 0) { lastApprovalTime = block.timestamp; session.status = Status.Resolved; @@ -310,9 +310,9 @@ contract KlerosGovernor is IArbitrableV2 { /// Note If the final ruling is "0" nothing is approved and deposits will stay locked in the contract. function rule(uint256 _disputeID, uint256 _ruling) external override { Session storage session = sessions[sessions.length - 1]; - require(msg.sender == address(arbitrator), "Only arbitrator allowed"); - require(session.status == Status.DisputeCreated, "Wrong status"); - require(_ruling <= session.submittedLists.length, "Ruling is out of bounds."); + if (msg.sender != address(arbitrator)) revert OnlyArbitratorAllowed(); + if (session.status != Status.DisputeCreated) revert NotDisputed(); + if (_ruling > session.submittedLists.length) revert RulingOutOfBounds(); if (_ruling != 0) { Submission storage submission = submissions[session.submittedLists[_ruling - 1]]; @@ -338,8 +338,8 @@ contract KlerosGovernor is IArbitrableV2 { /// @param _count Number of transactions to execute. Executes until the end if set to "0" or number higher than number of transactions in the list. function executeTransactionList(uint256 _listID, uint256 _cursor, uint256 _count) external { Submission storage submission = submissions[_listID]; - require(submission.approved, "Should be approved"); - require(block.timestamp - submission.approvalTime <= executionTimeout, "Time to execute has passed"); + if (!submission.approved) revert SubmissionNotApproved(); + if (block.timestamp - submission.approvalTime > executionTimeout) revert TimeToExecuteHasPassed(); for (uint256 i = _cursor; i < submission.txs.length && (_count == 0 || i < _cursor + _count); i++) { Transaction storage transaction = submission.txs[i]; uint256 expendableFunds = getExpendableFunds(); @@ -347,7 +347,7 @@ contract KlerosGovernor is IArbitrableV2 { (bool callResult, ) = transaction.target.call{value: transaction.value}(transaction.data); // An extra check to prevent re-entrancy through target call. if (callResult == true) { - require(!transaction.executed, "Already executed"); + if (transaction.executed) revert AlreadyExecuted(); transaction.executed = true; } } @@ -407,4 +407,27 @@ contract KlerosGovernor is IArbitrableV2 { function getCurrentSessionNumber() external view returns (uint256) { return sessions.length - 1; } + + // ************************************* // + // * Errors * // + // ************************************* // + + error SubmissionTimeHasEnded(); + error ApprovalTimeNotStarted(); + error GovernorOnly(); + error WrongInputTargetAndValue(); + error WrongInputTargetAndDatasize(); + error InsufficientDeposit(); + error ListAlreadySubmitted(); + error ShouldOnlyWithdrawInFirstHalf(); + error WrongListHash(); + error OnlySubmitterCanWithdraw(); + error WithdrawingTimeHasPassed(); + error AlreadyDisputed(); + error OnlyArbitratorAllowed(); + error NotDisputed(); + error RulingOutOfBounds(); + error SubmissionNotApproved(); + error TimeToExecuteHasPassed(); + error AlreadyExecuted(); } diff --git a/contracts/src/arbitration/PolicyRegistry.sol b/contracts/src/arbitration/PolicyRegistry.sol index eb32476d6..f4ed36322 100644 --- a/contracts/src/arbitration/PolicyRegistry.sol +++ b/contracts/src/arbitration/PolicyRegistry.sol @@ -32,7 +32,7 @@ contract PolicyRegistry is UUPSProxiable, Initializable { /// @dev Requires that the sender is the governor. modifier onlyByGovernor() { - require(governor == msg.sender, "No allowed: governor only"); + if (governor != msg.sender) revert GovernorOnly(); _; } @@ -85,4 +85,10 @@ contract PolicyRegistry is UUPSProxiable, Initializable { policies[_courtID] = _policy; emit PolicyUpdate(_courtID, _courtName, policies[_courtID]); } + + // ************************************* // + // * Errors * // + // ************************************* // + + error GovernorOnly(); } diff --git a/contracts/src/arbitration/SortitionModuleBase.sol b/contracts/src/arbitration/SortitionModuleBase.sol index 577d9fd22..e49fd2c6c 100644 --- a/contracts/src/arbitration/SortitionModuleBase.sol +++ b/contracts/src/arbitration/SortitionModuleBase.sol @@ -122,12 +122,12 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr // ************************************* // modifier onlyByGovernor() { - require(address(governor) == msg.sender, "Access not allowed: Governor only."); + if (governor != msg.sender) revert GovernorOnly(); _; } modifier onlyByCore() { - require(address(core) == msg.sender, "Access not allowed: KlerosCore only."); + if (address(core) != msg.sender) revert KlerosCoreOnly(); _; } @@ -171,23 +171,19 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr function passPhase() external { if (phase == Phase.staking) { - require( - block.timestamp - lastPhaseChange >= minStakingTime, - "The minimum staking time has not passed yet." - ); - require(disputesWithoutJurors > 0, "There are no disputes that need jurors."); + if (block.timestamp - lastPhaseChange < minStakingTime) revert MinStakingTimeNotPassed(); + if (disputesWithoutJurors == 0) revert NoDisputesThatNeedJurors(); rng.requestRandomness(block.number + rngLookahead); randomNumberRequestBlock = block.number; phase = Phase.generating; } else if (phase == Phase.generating) { randomNumber = rng.receiveRandomness(randomNumberRequestBlock + rngLookahead); - require(randomNumber != 0, "Random number is not ready yet"); + if (randomNumber == 0) revert RandomNumberNotReady(); phase = Phase.drawing; } else if (phase == Phase.drawing) { - require( - disputesWithoutJurors == 0 || block.timestamp - lastPhaseChange >= maxDrawingTime, - "There are still disputes without jurors and the maximum drawing time has not passed yet." - ); + if (disputesWithoutJurors > 0 && block.timestamp - lastPhaseChange < maxDrawingTime) { + revert DisputesWithoutJurorsAndMaxDrawingTimeNotPassed(); + } phase = Phase.staking; } @@ -201,8 +197,8 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr function createTree(bytes32 _key, bytes memory _extraData) external override onlyByCore { SortitionSumTree storage tree = sortitionSumTrees[_key]; uint256 K = _extraDataToTreeK(_extraData); - require(tree.K == 0, "Tree already exists."); - require(K > 1, "K must be greater than one."); + if (tree.K != 0) revert TreeAlreadyExists(); + if (K <= 1) revert KMustBeGreaterThanOne(); tree.K = K; tree.nodes.push(0); } @@ -210,8 +206,8 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr /// @dev Executes the next delayed stakes. /// @param _iterations The number of delayed stakes to execute. function executeDelayedStakes(uint256 _iterations) external { - require(phase == Phase.staking, "Should be in Staking phase."); - require(delayedStakeWriteIndex >= delayedStakeReadIndex, "No delayed stake to execute."); + if (phase != Phase.staking) revert NotStakingPhase(); + if (delayedStakeWriteIndex < delayedStakeReadIndex) revert NoDelayedStakeToExecute(); uint256 actualIterations = (delayedStakeReadIndex + _iterations) - 1 > delayedStakeWriteIndex ? (delayedStakeWriteIndex - delayedStakeReadIndex) + 1 @@ -420,7 +416,7 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr // Can withdraw the leftover PNK if fully unstaked, has no tokens locked and has positive balance. // This withdrawal can't be triggered by calling setStake() in KlerosCore because current stake is technically 0, thus it is done via separate function. uint256 amount = getJurorLeftoverPNK(_account); - require(amount > 0, "Not eligible for withdrawal."); + if (amount == 0) revert NotEligibleForWithdrawal(); jurors[_account].stakedPnk = 0; core.transferBySortitionModule(_account, amount); emit LeftoverPNKWithdrawn(_account, amount); @@ -444,7 +440,7 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr uint256 _coreDisputeID, uint256 _nonce ) public view override returns (address drawnAddress) { - require(phase == Phase.drawing, "Wrong phase."); + if (phase != Phase.drawing) revert NotDrawingPhase(); SortitionSumTree storage tree = sortitionSumTrees[_key]; if (tree.nodes[0] == 0) { @@ -692,4 +688,21 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr stakePathID := mload(ptr) } } + + // ************************************* // + // * Errors * // + // ************************************* // + + error GovernorOnly(); + error KlerosCoreOnly(); + error MinStakingTimeNotPassed(); + error NoDisputesThatNeedJurors(); + error RandomNumberNotReady(); + error DisputesWithoutJurorsAndMaxDrawingTimeNotPassed(); + error TreeAlreadyExists(); + error KMustBeGreaterThanOne(); + error NotStakingPhase(); + error NoDelayedStakeToExecute(); + error NotEligibleForWithdrawal(); + error NotDrawingPhase(); } diff --git a/contracts/src/arbitration/arbitrables/ArbitrableExample.sol b/contracts/src/arbitration/arbitrables/ArbitrableExample.sol index 810688788..b7596566e 100644 --- a/contracts/src/arbitration/arbitrables/ArbitrableExample.sol +++ b/contracts/src/arbitration/arbitrables/ArbitrableExample.sol @@ -37,7 +37,7 @@ contract ArbitrableExample is IArbitrableV2 { // ************************************* // modifier onlyByGovernor() { - require(msg.sender == governor, "Only the governor allowed."); + if (governor != msg.sender) revert GovernorOnly(); _; } @@ -126,8 +126,8 @@ contract ArbitrableExample is IArbitrableV2 { uint256 localDisputeID = disputes.length; disputes.push(DisputeStruct({isRuled: false, ruling: 0, numberOfRulingOptions: numberOfRulingOptions})); - require(weth.safeTransferFrom(msg.sender, address(this), _feeInWeth), "Transfer failed"); - require(weth.increaseAllowance(address(arbitrator), _feeInWeth), "Allowance increase failed"); + if (!weth.safeTransferFrom(msg.sender, address(this), _feeInWeth)) revert TransferFailed(); + if (!weth.increaseAllowance(address(arbitrator), _feeInWeth)) revert AllowanceIncreaseFailed(); disputeID = arbitrator.createDispute(numberOfRulingOptions, arbitratorExtraData, weth, _feeInWeth); externalIDtoLocalID[disputeID] = localDisputeID; @@ -142,13 +142,24 @@ contract ArbitrableExample is IArbitrableV2 { function rule(uint256 _arbitratorDisputeID, uint256 _ruling) external override { uint256 localDisputeID = externalIDtoLocalID[_arbitratorDisputeID]; DisputeStruct storage dispute = disputes[localDisputeID]; - require(msg.sender == address(arbitrator), "Only the arbitrator can execute this."); - require(_ruling <= dispute.numberOfRulingOptions, "Invalid ruling."); - require(dispute.isRuled == false, "This dispute has been ruled already."); + if (msg.sender != address(arbitrator)) revert ArbitratorOnly(); + if (_ruling > dispute.numberOfRulingOptions) revert RulingOutOfBounds(); + if (dispute.isRuled) revert DisputeAlreadyRuled(); dispute.isRuled = true; dispute.ruling = _ruling; emit Ruling(IArbitratorV2(msg.sender), _arbitratorDisputeID, dispute.ruling); } + + // ************************************* // + // * Errors * // + // ************************************* // + + error GovernorOnly(); + error TransferFailed(); + error AllowanceIncreaseFailed(); + error ArbitratorOnly(); + error RulingOutOfBounds(); + error DisputeAlreadyRuled(); } diff --git a/contracts/src/arbitration/arbitrables/DisputeResolver.sol b/contracts/src/arbitration/arbitrables/DisputeResolver.sol index 8fa1da02f..7248356cd 100644 --- a/contracts/src/arbitration/arbitrables/DisputeResolver.sol +++ b/contracts/src/arbitration/arbitrables/DisputeResolver.sol @@ -48,17 +48,17 @@ contract DisputeResolver is IArbitrableV2 { /// @dev Changes the governor. /// @param _governor The address of the new governor. function changeGovernor(address _governor) external { - require(governor == msg.sender, "Access not allowed: Governor only."); + if (governor != msg.sender) revert GovernorOnly(); governor = _governor; } function changeArbitrator(IArbitratorV2 _arbitrator) external { - require(governor == msg.sender, "Access not allowed: Governor only."); + if (governor != msg.sender) revert GovernorOnly(); arbitrator = _arbitrator; } function changeTemplateRegistry(IDisputeTemplateRegistry _templateRegistry) external { - require(governor == msg.sender, "Access not allowed: Governor only."); + if (governor != msg.sender) revert GovernorOnly(); templateRegistry = _templateRegistry; } @@ -109,9 +109,9 @@ contract DisputeResolver is IArbitrableV2 { function rule(uint256 _arbitratorDisputeID, uint256 _ruling) external override { uint256 localDisputeID = arbitratorDisputeIDToLocalID[_arbitratorDisputeID]; DisputeStruct storage dispute = disputes[localDisputeID]; - require(msg.sender == address(arbitrator), "Only the arbitrator can execute this."); - require(_ruling <= dispute.numberOfRulingOptions, "Invalid ruling."); - require(!dispute.isRuled, "This dispute has been ruled already."); + if (msg.sender != address(arbitrator)) revert ArbitratorOnly(); + if (_ruling > dispute.numberOfRulingOptions) revert RulingOutOfBounds(); + if (dispute.isRuled) revert DisputeAlreadyRuled(); dispute.isRuled = true; dispute.ruling = _ruling; @@ -130,7 +130,7 @@ contract DisputeResolver is IArbitrableV2 { string memory _disputeTemplateUri, uint256 _numberOfRulingOptions ) internal virtual returns (uint256 arbitratorDisputeID) { - require(_numberOfRulingOptions > 1, "Should be at least 2 ruling options."); + if (_numberOfRulingOptions <= 1) revert ShouldBeAtLeastTwoRulingOptions(); arbitratorDisputeID = arbitrator.createDispute{value: msg.value}(_numberOfRulingOptions, _arbitratorExtraData); uint256 localDisputeID = disputes.length; @@ -146,4 +146,14 @@ contract DisputeResolver is IArbitrableV2 { uint256 templateId = templateRegistry.setDisputeTemplate("", _disputeTemplate, _disputeTemplateDataMappings); emit DisputeRequest(arbitrator, arbitratorDisputeID, localDisputeID, templateId, _disputeTemplateUri); } + + // ************************************* // + // * Errors * // + // ************************************* // + + error GovernorOnly(); + error ArbitratorOnly(); + error RulingOutOfBounds(); + error DisputeAlreadyRuled(); + error ShouldBeAtLeastTwoRulingOptions(); } diff --git a/contracts/src/arbitration/devtools/DisputeResolverRuler.sol b/contracts/src/arbitration/devtools/DisputeResolverRuler.sol index 85bede8bf..05c033c38 100644 --- a/contracts/src/arbitration/devtools/DisputeResolverRuler.sol +++ b/contracts/src/arbitration/devtools/DisputeResolverRuler.sol @@ -36,7 +36,7 @@ contract DisputeResolverRuler is DisputeResolver { string memory _disputeTemplateUri, uint256 _numberOfRulingOptions ) internal override returns (uint256 arbitratorDisputeID) { - require(_numberOfRulingOptions > 1, "Should be at least 2 ruling options."); + if (_numberOfRulingOptions <= 1) revert ShouldBeAtLeastTwoRulingOptions(); uint256 localDisputeID = disputes.length; DisputeStruct storage dispute = disputes.push(); diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol index 9b1968400..2d804891a 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol @@ -125,17 +125,17 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi // ************************************* // modifier onlyByGovernor() { - require(governor == msg.sender, "Access not allowed: Governor only."); + if (governor != msg.sender) revert GovernorOnly(); _; } modifier onlyByCore() { - require(address(core) == msg.sender, "Access not allowed: KlerosCore only."); + if (address(core) != msg.sender) revert KlerosCoreOnly(); _; } modifier notJumped(uint256 _coreDisputeID) { - require(!disputes[coreDisputeIDToLocal[_coreDisputeID]].jumped, "Dispute jumped to a parent DK!"); + if (disputes[coreDisputeIDToLocal[_coreDisputeID]].jumped) revert DisputeJumpedToParentDK(); _; } @@ -171,7 +171,7 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi bytes memory _data ) external onlyByGovernor { (bool success, ) = _destination.call{value: _amount}(_data); - require(success, "Unsuccessful call"); + if (!success) revert UnsuccessfulCall(); } /// @dev Changes the `governor` storage variable. @@ -269,14 +269,14 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi bytes32 _commit ) internal notJumped(_coreDisputeID) { (, , KlerosCore.Period period, , ) = core.disputes(_coreDisputeID); - require(period == KlerosCoreBase.Period.commit, "The dispute should be in Commit period."); - require(_commit != bytes32(0), "Empty commit."); - require(coreDisputeIDToActive[_coreDisputeID], "Not active for core dispute ID"); + if (period != KlerosCoreBase.Period.commit) revert NotCommitPeriod(); + if (_commit == bytes32(0)) revert EmptyCommit(); + if (!coreDisputeIDToActive[_coreDisputeID]) revert NotActiveForCoreDisputeID(); Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; Round storage round = dispute.rounds[dispute.rounds.length - 1]; for (uint256 i = 0; i < _voteIDs.length; i++) { - require(round.votes[_voteIDs[i]].account == msg.sender, "The caller has to own the vote."); + if (round.votes[_voteIDs[i]].account != msg.sender) revert JurorHasToOwnTheVote(); round.votes[_voteIDs[i]].commit = _commit; } round.totalCommitted += _voteIDs.length; @@ -310,12 +310,12 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi address _juror ) internal notJumped(_coreDisputeID) { (, , KlerosCore.Period period, , ) = core.disputes(_coreDisputeID); - require(period == KlerosCoreBase.Period.vote, "The dispute should be in Vote period."); - require(_voteIDs.length > 0, "No voteID provided"); - require(coreDisputeIDToActive[_coreDisputeID], "Not active for core dispute ID"); + if (period != KlerosCoreBase.Period.vote) revert NotVotePeriod(); + if (_voteIDs.length == 0) revert EmptyVoteIDs(); + if (!coreDisputeIDToActive[_coreDisputeID]) revert NotActiveForCoreDisputeID(); Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; - require(_choice <= dispute.numberOfChoices, "Choice out of bounds"); + if (_choice > dispute.numberOfChoices) revert ChoiceOutOfBounds(); Round storage round = dispute.rounds[dispute.rounds.length - 1]; { @@ -325,12 +325,10 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi // Save the votes. for (uint256 i = 0; i < _voteIDs.length; i++) { - require(round.votes[_voteIDs[i]].account == _juror, "The juror has to own the vote."); - require( - !hiddenVotes || round.votes[_voteIDs[i]].commit == voteHash, - "The vote hash must match the commitment in courts with hidden votes." - ); - require(!round.votes[_voteIDs[i]].voted, "Vote already cast."); + if (round.votes[_voteIDs[i]].account != _juror) revert JurorHasToOwnTheVote(); + if (hiddenVotes && round.votes[_voteIDs[i]].commit != voteHash) + revert HashDoesNotMatchHiddenVoteCommitment(); + if (round.votes[_voteIDs[i]].voted) revert VoteAlreadyCast(); round.votes[_voteIDs[i]].choice = _choice; round.votes[_voteIDs[i]].voted = true; } @@ -361,29 +359,30 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi /// @param _choice A choice that receives funding. function fundAppeal(uint256 _coreDisputeID, uint256 _choice) external payable notJumped(_coreDisputeID) { Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; - require(_choice <= dispute.numberOfChoices, "There is no such ruling to fund."); - require(coreDisputeIDToActive[_coreDisputeID], "Not active for core dispute ID"); + if (_choice > dispute.numberOfChoices) revert ChoiceOutOfBounds(); + if (!coreDisputeIDToActive[_coreDisputeID]) revert NotActiveForCoreDisputeID(); (uint256 appealPeriodStart, uint256 appealPeriodEnd) = core.appealPeriod(_coreDisputeID); - require(block.timestamp >= appealPeriodStart && block.timestamp < appealPeriodEnd, "Appeal period is over."); + if (block.timestamp < appealPeriodStart || block.timestamp >= appealPeriodEnd) revert AppealPeriodIsOver(); uint256 multiplier; (uint256 ruling, , ) = this.currentRuling(_coreDisputeID); if (ruling == _choice) { multiplier = WINNER_STAKE_MULTIPLIER; } else { - require( - block.timestamp - appealPeriodStart < - ((appealPeriodEnd - appealPeriodStart) * LOSER_APPEAL_PERIOD_MULTIPLIER) / ONE_BASIS_POINT, - "Appeal period is over for loser" - ); + if ( + block.timestamp - appealPeriodStart >= + ((appealPeriodEnd - appealPeriodStart) * LOSER_APPEAL_PERIOD_MULTIPLIER) / ONE_BASIS_POINT + ) { + revert AppealPeriodIsOverForLoser(); + } multiplier = LOSER_STAKE_MULTIPLIER; } Round storage round = dispute.rounds[dispute.rounds.length - 1]; uint256 coreRoundID = core.getNumberOfRounds(_coreDisputeID) - 1; - require(!round.hasPaid[_choice], "Appeal fee is already paid."); + if (round.hasPaid[_choice]) revert AppealFeeIsAlreadyPaid(); uint256 appealCost = core.appealCost(_coreDisputeID); uint256 totalCost = appealCost + (appealCost * multiplier) / ONE_BASIS_POINT; @@ -440,9 +439,9 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi uint256 _choice ) external returns (uint256 amount) { (, , , bool isRuled, ) = core.disputes(_coreDisputeID); - require(isRuled, "Dispute should be resolved."); - require(!core.paused(), "Core is paused"); - require(coreDisputeIDToActive[_coreDisputeID], "Not active for core dispute ID"); + if (!isRuled) revert DisputeNotResolved(); + if (core.paused()) revert CoreIsPaused(); + if (!coreDisputeIDToActive[_coreDisputeID]) revert NotActiveForCoreDisputeID(); Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; Round storage round = dispute.rounds[dispute.coreRoundIDToLocal[_coreRoundID]]; @@ -710,4 +709,27 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi result = true; } } + + // ************************************* // + // * Errors * // + // ************************************* // + + error GovernorOnly(); + error KlerosCoreOnly(); + error DisputeJumpedToParentDK(); + error UnsuccessfulCall(); + error NotCommitPeriod(); + error EmptyCommit(); + error NotActiveForCoreDisputeID(); + error JurorHasToOwnTheVote(); + error NotVotePeriod(); + error EmptyVoteIDs(); + error ChoiceOutOfBounds(); + error HashDoesNotMatchHiddenVoteCommitment(); + error VoteAlreadyCast(); + error AppealPeriodIsOver(); + error AppealPeriodIsOverForLoser(); + error AppealFeeIsAlreadyPaid(); + error DisputeNotResolved(); + error CoreIsPaused(); } diff --git a/contracts/src/arbitration/evidence/EvidenceModule.sol b/contracts/src/arbitration/evidence/EvidenceModule.sol index fe55122b9..4967597ab 100644 --- a/contracts/src/arbitration/evidence/EvidenceModule.sol +++ b/contracts/src/arbitration/evidence/EvidenceModule.sol @@ -22,7 +22,7 @@ contract EvidenceModule is IEvidence, Initializable, UUPSProxiable { // ************************************* // modifier onlyByGovernor() { - require(governor == msg.sender, "Access not allowed: Governor only."); + if (governor != msg.sender) revert GovernorOnly(); _; } @@ -67,4 +67,10 @@ contract EvidenceModule is IEvidence, Initializable, UUPSProxiable { function submitEvidence(uint256 _externalDisputeID, string calldata _evidence) external { emit Evidence(_externalDisputeID, msg.sender, _evidence); } + + // ************************************* // + // * Errors * // + // ************************************* // + + error GovernorOnly(); } diff --git a/contracts/src/arbitration/university/SortitionModuleUniversity.sol b/contracts/src/arbitration/university/SortitionModuleUniversity.sol index 619a0b934..db61958fd 100644 --- a/contracts/src/arbitration/university/SortitionModuleUniversity.sol +++ b/contracts/src/arbitration/university/SortitionModuleUniversity.sol @@ -67,12 +67,12 @@ contract SortitionModuleUniversity is ISortitionModuleUniversity, UUPSProxiable, // ************************************* // modifier onlyByGovernor() { - require(address(governor) == msg.sender, "Access not allowed: Governor only."); + if (governor != msg.sender) revert GovernorOnly(); _; } modifier onlyByCore() { - require(address(core) == msg.sender, "Access not allowed: KlerosCore only."); + if (address(core) != msg.sender) revert KlerosCoreOnly(); _; } @@ -280,7 +280,7 @@ contract SortitionModuleUniversity is ISortitionModuleUniversity, UUPSProxiable, // Can withdraw the leftover PNK if fully unstaked, has no tokens locked and has positive balance. // This withdrawal can't be triggered by calling setStake() in KlerosCore because current stake is technically 0, thus it is done via separate function. uint256 amount = getJurorLeftoverPNK(_account); - require(amount > 0, "Not eligible for withdrawal."); + if (amount == 0) revert NotEligibleForWithdrawal(); jurors[_account].stakedPnk = 0; core.transferBySortitionModule(_account, amount); emit LeftoverPNKWithdrawn(_account, amount); @@ -364,4 +364,12 @@ contract SortitionModuleUniversity is ISortitionModuleUniversity, UUPSProxiable, } } } + + // ************************************* // + // * Errors * // + // ************************************* // + + error GovernorOnly(); + error KlerosCoreOnly(); + error NotEligibleForWithdrawal(); } diff --git a/contracts/src/arbitration/view/KlerosCoreSnapshotProxy.sol b/contracts/src/arbitration/view/KlerosCoreSnapshotProxy.sol index e94eccbd9..74cdb84ce 100644 --- a/contracts/src/arbitration/view/KlerosCoreSnapshotProxy.sol +++ b/contracts/src/arbitration/view/KlerosCoreSnapshotProxy.sol @@ -26,7 +26,7 @@ contract KlerosCoreSnapshotProxy { // ************************************* // modifier onlyByGovernor() { - require(governor == msg.sender, "Access not allowed: Governor only."); + if (governor != msg.sender) revert GovernorOnly(); _; } @@ -69,4 +69,10 @@ contract KlerosCoreSnapshotProxy { function balanceOf(address _account) external view returns (uint256 totalStaked) { (totalStaked, , , ) = core.sortitionModule().getJurorBalance(_account, 0); } + + // ************************************* // + // * Errors * // + // ************************************* // + + error GovernorOnly(); } diff --git a/contracts/test/arbitration/index.ts b/contracts/test/arbitration/index.ts index d8e7ef089..4af7dfd63 100644 --- a/contracts/test/arbitration/index.ts +++ b/contracts/test/arbitration/index.ts @@ -73,9 +73,9 @@ describe("DisputeKitClassic", async () => { }); it("Should create a dispute", async () => { - await expect(disputeKit.connect(deployer).createDispute(0, 0, ethers.toBeHex(3), "0x00")).to.be.revertedWith( - "Access not allowed: KlerosCore only." - ); + await expect( + disputeKit.connect(deployer).createDispute(0, 0, ethers.toBeHex(3), "0x00") + ).to.be.revertedWithCustomError(disputeKit, "KlerosCoreOnly"); const tx = await core .connect(deployer) diff --git a/contracts/test/arbitration/staking-neo.ts b/contracts/test/arbitration/staking-neo.ts index 85b0dc884..2f1523519 100644 --- a/contracts/test/arbitration/staking-neo.ts +++ b/contracts/test/arbitration/staking-neo.ts @@ -263,7 +263,10 @@ describe("Staking", async () => { ); expect(await sortition.totalStaked()).to.be.equal(PNK(0)); await drawAndReachStakingPhaseFromGenerating(); - await expect(sortition.executeDelayedStakes(10)).to.revertedWith("No delayed stake to execute."); + await expect(sortition.executeDelayedStakes(10)).to.revertedWithCustomError( + sortition, + "NoDelayedStakeToExecute" + ); expect(await sortition.totalStaked()).to.be.equal(PNK(0)); }); @@ -327,7 +330,10 @@ describe("Staking", async () => { ); expect(await sortition.totalStaked()).to.be.equal(PNK(2000)); await drawAndReachStakingPhaseFromGenerating(); - await expect(sortition.executeDelayedStakes(10)).to.revertedWith("No delayed stake to execute."); + await expect(sortition.executeDelayedStakes(10)).to.revertedWithCustomError( + sortition, + "NoDelayedStakeToExecute" + ); expect(await sortition.totalStaked()).to.be.equal(PNK(2000)); }); diff --git a/contracts/test/foundry/KlerosCore.t.sol b/contracts/test/foundry/KlerosCore.t.sol index 20f8555a7..72a6a565c 100644 --- a/contracts/test/foundry/KlerosCore.t.sol +++ b/contracts/test/foundry/KlerosCore.t.sol @@ -1137,7 +1137,7 @@ contract KlerosCoreTest is Test { vm.prank(staker2); core.setStake(GENERAL_COURT, 10000); - vm.expectRevert(bytes("No delayed stake to execute.")); + vm.expectRevert(SortitionModuleBase.NoDelayedStakeToExecute.selector); sortitionModule.executeDelayedStakes(5); // Set the stake and create a dispute to advance the phase @@ -1150,7 +1150,7 @@ contract KlerosCoreTest is Test { uint256 disputeID = 0; core.draw(disputeID, DEFAULT_NB_OF_JURORS); - vm.expectRevert(bytes("Should be in Staking phase.")); + vm.expectRevert(SortitionModuleBase.NotStakingPhase.selector); sortitionModule.executeDelayedStakes(5); // Create delayed stake @@ -1270,14 +1270,14 @@ contract KlerosCoreTest is Test { assertEq(snapshotProxy.balanceOf(staker1), 12346, "Wrong stPNK balance"); vm.prank(other); - vm.expectRevert(bytes("Access not allowed: Governor only.")); + vm.expectRevert(KlerosCoreSnapshotProxy.GovernorOnly.selector); snapshotProxy.changeCore(IKlerosCore(other)); vm.prank(governor); snapshotProxy.changeCore(IKlerosCore(other)); assertEq(address(snapshotProxy.core()), other, "Wrong core in snapshot proxy after change"); vm.prank(other); - vm.expectRevert(bytes("Access not allowed: Governor only.")); + vm.expectRevert(KlerosCoreSnapshotProxy.GovernorOnly.selector); snapshotProxy.changeGovernor(other); vm.prank(governor); snapshotProxy.changeGovernor(other); @@ -1565,7 +1565,7 @@ contract KlerosCoreTest is Test { voteIDs[0] = 0; bytes32 commit; vm.prank(staker1); - vm.expectRevert(bytes("The dispute should be in Commit period.")); + vm.expectRevert(DisputeKitClassicBase.NotCommitPeriod.selector); disputeKit.castCommit(disputeID, voteIDs, commit); vm.expectRevert(KlerosCoreBase.EvidenceNotPassedAndNotAppeal.selector); @@ -1582,13 +1582,13 @@ contract KlerosCoreTest is Test { assertEq(lastPeriodChange, block.timestamp, "Wrong lastPeriodChange"); vm.prank(staker1); - vm.expectRevert(bytes("Empty commit.")); + vm.expectRevert(DisputeKitClassicBase.EmptyCommit.selector); disputeKit.castCommit(disputeID, voteIDs, commit); commit = keccak256(abi.encodePacked(YES, salt)); vm.prank(other); - vm.expectRevert(bytes("The caller has to own the vote.")); + vm.expectRevert(DisputeKitClassicBase.JurorHasToOwnTheVote.selector); disputeKit.castCommit(disputeID, voteIDs, commit); vm.prank(staker1); @@ -1626,11 +1626,11 @@ contract KlerosCoreTest is Test { // Check the require with the wrong choice and then with the wrong salt vm.prank(staker1); - vm.expectRevert(bytes("The vote hash must match the commitment in courts with hidden votes.")); + vm.expectRevert(DisputeKitClassicBase.HashDoesNotMatchHiddenVoteCommitment.selector); disputeKit.castVote(disputeID, voteIDs, 2, salt, "XYZ"); vm.prank(staker1); - vm.expectRevert(bytes("The vote hash must match the commitment in courts with hidden votes.")); + vm.expectRevert(DisputeKitClassicBase.HashDoesNotMatchHiddenVoteCommitment.selector); disputeKit.castVote(disputeID, voteIDs, YES, salt - 1, "XYZ"); vm.prank(staker1); @@ -1698,7 +1698,7 @@ contract KlerosCoreTest is Test { uint256[] memory voteIDs = new uint256[](0); vm.prank(staker1); - vm.expectRevert(bytes("The dispute should be in Vote period.")); + vm.expectRevert(DisputeKitClassicBase.NotVotePeriod.selector); disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); // Leave salt empty as not needed vm.expectRevert(KlerosCoreBase.DisputeStillDrawing.selector); @@ -1716,17 +1716,17 @@ contract KlerosCoreTest is Test { assertEq(lastPeriodChange, block.timestamp, "Wrong lastPeriodChange"); vm.prank(staker1); - vm.expectRevert(bytes("No voteID provided")); + vm.expectRevert(DisputeKitClassicBase.EmptyVoteIDs.selector); disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); voteIDs = new uint256[](1); voteIDs[0] = 0; // Split vote IDs to see how the winner changes vm.prank(staker1); - vm.expectRevert(bytes("Choice out of bounds")); + vm.expectRevert(DisputeKitClassicBase.ChoiceOutOfBounds.selector); disputeKit.castVote(disputeID, voteIDs, 2 + 1, 0, "XYZ"); vm.prank(other); - vm.expectRevert(bytes("The juror has to own the vote.")); + vm.expectRevert(DisputeKitClassicBase.JurorHasToOwnTheVote.selector); disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); vm.prank(staker1); @@ -1735,7 +1735,7 @@ contract KlerosCoreTest is Test { disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); vm.prank(staker1); - vm.expectRevert(bytes("Vote already cast.")); + vm.expectRevert(DisputeKitClassicBase.VoteAlreadyCast.selector); disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); ( @@ -1970,7 +1970,7 @@ contract KlerosCoreTest is Test { core.appeal{value: 0.21 ether}(disputeID, 2, arbitratorExtraData); vm.prank(crowdfunder1); - vm.expectRevert(bytes("There is no such ruling to fund.")); + vm.expectRevert(DisputeKitClassicBase.ChoiceOutOfBounds.selector); disputeKit.fundAppeal(disputeID, 3); vm.prank(crowdfunder1); @@ -1995,7 +1995,7 @@ contract KlerosCoreTest is Test { assertEq((disputeKit.getFundedChoices(disputeID))[0], 1, "Incorrect funded choice"); vm.prank(crowdfunder1); - vm.expectRevert(bytes("Appeal fee is already paid.")); + vm.expectRevert(DisputeKitClassicBase.AppealFeeIsAlreadyPaid.selector); disputeKit.fundAppeal(disputeID, 1); } @@ -2024,7 +2024,7 @@ contract KlerosCoreTest is Test { disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); vm.prank(crowdfunder1); - vm.expectRevert(bytes("Appeal period is over.")); // Appeal period not started yet + vm.expectRevert(DisputeKitClassicBase.AppealPeriodIsOver.selector); disputeKit.fundAppeal{value: 0.1 ether}(disputeID, 1); core.passPeriod(disputeID); @@ -2032,14 +2032,14 @@ contract KlerosCoreTest is Test { vm.prank(crowdfunder1); vm.warp(block.timestamp + ((end - start) / 2 + 1)); - vm.expectRevert(bytes("Appeal period is over for loser")); + vm.expectRevert(DisputeKitClassicBase.AppealPeriodIsOverForLoser.selector); disputeKit.fundAppeal{value: 0.1 ether}(disputeID, 1); // Losing choice disputeKit.fundAppeal(disputeID, 2); // Winning choice funding should not revert yet vm.prank(crowdfunder1); vm.warp(block.timestamp + (end - start) / 2); // Warp one more to cover the whole period - vm.expectRevert(bytes("Appeal period is over.")); + vm.expectRevert(DisputeKitClassicBase.AppealPeriodIsOver.selector); disputeKit.fundAppeal{value: 0.1 ether}(disputeID, 2); } @@ -2210,7 +2210,7 @@ contract KlerosCoreTest is Test { // Check jump modifier vm.prank(address(core)); - vm.expectRevert(bytes("Dispute jumped to a parent DK!")); + vm.expectRevert(DisputeKitClassicBase.DisputeJumpedToParentDK.selector); newDisputeKit.draw(disputeID, 1); // And check that draw in the new round works @@ -2594,7 +2594,7 @@ contract KlerosCoreTest is Test { assertEq(pinakion.balanceOf(address(core)), 1000, "Wrong token balance of the core"); assertEq(pinakion.balanceOf(staker1), 999999999999999000, "Wrong token balance of staker1"); - vm.expectRevert(bytes("Not eligible for withdrawal.")); + vm.expectRevert(SortitionModuleBase.NotEligibleForWithdrawal.selector); sortitionModule.withdrawLeftoverPNK(staker1); vm.expectEmit(true, true, true, true); @@ -2863,14 +2863,14 @@ contract KlerosCoreTest is Test { vm.warp(block.timestamp + timesPerPeriod[3]); core.passPeriod(disputeID); // Execution - vm.expectRevert(bytes("Dispute should be resolved.")); + vm.expectRevert(DisputeKitClassicBase.DisputeNotResolved.selector); disputeKit.withdrawFeesAndRewards(disputeID, payable(staker1), 0, 1); core.executeRuling(disputeID); vm.prank(governor); core.pause(); - vm.expectRevert(bytes("Core is paused")); + vm.expectRevert(DisputeKitClassicBase.CoreIsPaused.selector); disputeKit.withdrawFeesAndRewards(disputeID, payable(staker1), 0, 1); vm.prank(governor); core.unpause(); @@ -2969,7 +2969,7 @@ contract KlerosCoreTest is Test { // Deliberately cast votes using the old DK to see if the exception will be caught. vm.prank(staker1); - vm.expectRevert(bytes("Not active for core dispute ID")); + vm.expectRevert(DisputeKitClassicBase.NotActiveForCoreDisputeID.selector); disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); // And check the new DK. From a0cb09d84f622eb31d2fbf39aa817db7e1eda055 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Wed, 13 Aug 2025 19:50:03 +0100 Subject: [PATCH 27/46] feat: replace requires with custom errors for the gateways --- contracts/src/gateway/ForeignGateway.sol | 45 +++++++++++++++--------- contracts/src/gateway/HomeGateway.sol | 33 +++++++++++------ 2 files changed, 51 insertions(+), 27 deletions(-) diff --git a/contracts/src/gateway/ForeignGateway.sol b/contracts/src/gateway/ForeignGateway.sol index 5938da773..1e615936f 100644 --- a/contracts/src/gateway/ForeignGateway.sol +++ b/contracts/src/gateway/ForeignGateway.sol @@ -49,17 +49,16 @@ contract ForeignGateway is IForeignGateway, UUPSProxiable, Initializable { // ************************************* // modifier onlyFromVea(address _messageSender) { - require( - veaOutbox == msg.sender || - (block.timestamp < deprecatedVeaOutboxExpiration && deprecatedVeaOutbox == msg.sender), - "Access not allowed: Vea Outbox only." - ); - require(_messageSender == homeGateway, "Access not allowed: HomeGateway only."); + if ( + veaOutbox != msg.sender && + (block.timestamp >= deprecatedVeaOutboxExpiration || deprecatedVeaOutbox != msg.sender) + ) revert VeaOutboxOnly(); + if (_messageSender != homeGateway) revert HomeGatewayMessageSenderOnly(); _; } modifier onlyByGovernor() { - require(governor == msg.sender, "Access not allowed: Governor only."); + if (governor != msg.sender) revert GovernorOnly(); _; } @@ -105,7 +104,7 @@ contract ForeignGateway is IForeignGateway, UUPSProxiable, Initializable { /// @dev Changes the governor. /// @param _governor The address of the new governor. function changeGovernor(address _governor) external { - require(governor == msg.sender, "Access not allowed: Governor only."); + if (governor != msg.sender) revert GovernorOnly(); governor = _governor; } @@ -122,7 +121,7 @@ contract ForeignGateway is IForeignGateway, UUPSProxiable, Initializable { /// @dev 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 (governor != msg.sender) revert GovernorOnly(); homeGateway = _homeGateway; } @@ -143,7 +142,7 @@ contract ForeignGateway is IForeignGateway, UUPSProxiable, Initializable { uint256 _choices, bytes calldata _extraData ) external payable override returns (uint256 disputeID) { - require(msg.value >= arbitrationCost(_extraData), "Not paid enough for arbitration"); + if (msg.value < arbitrationCost(_extraData)) revert ArbitrationFeesNotEnough(); disputeID = localDisputeID++; uint256 chainID; @@ -206,8 +205,8 @@ contract ForeignGateway is IForeignGateway, UUPSProxiable, Initializable { ) external override onlyFromVea(_messageSender) { DisputeData storage dispute = disputeHashtoDisputeData[_disputeHash]; - require(dispute.id != 0, "Dispute does not exist"); - require(!dispute.ruled, "Cannot rule twice"); + if (dispute.id == 0) revert DisputeDoesNotExist(); + if (dispute.ruled) revert CannotRuleTwice(); dispute.ruled = true; dispute.relayer = _relayer; @@ -219,8 +218,8 @@ contract ForeignGateway is IForeignGateway, UUPSProxiable, Initializable { /// @inheritdoc IForeignGateway function withdrawFees(bytes32 _disputeHash) external override { DisputeData storage dispute = disputeHashtoDisputeData[_disputeHash]; - require(dispute.id != 0, "Dispute does not exist"); - require(dispute.ruled, "Not ruled yet"); + if (dispute.id == 0) revert DisputeDoesNotExist(); + if (!dispute.ruled) revert NotRuledYet(); uint256 amount = dispute.paid; dispute.paid = 0; @@ -247,9 +246,9 @@ contract ForeignGateway is IForeignGateway, UUPSProxiable, Initializable { revert("Not supported"); } - // ************************ // - // * Internal * // - // ************************ // + // ************************************* // + // * Internal * // + // ************************************* // function extraDataToCourtIDMinJurors( bytes memory _extraData @@ -268,4 +267,16 @@ contract ForeignGateway is IForeignGateway, UUPSProxiable, Initializable { minJurors = DEFAULT_NB_OF_JURORS; } } + + // ************************************* // + // * Errors * // + // ************************************* // + + error GovernorOnly(); + error HomeGatewayMessageSenderOnly(); + error VeaOutboxOnly(); + error ArbitrationFeesNotEnough(); + error DisputeDoesNotExist(); + error CannotRuleTwice(); + error NotRuledYet(); } diff --git a/contracts/src/gateway/HomeGateway.sol b/contracts/src/gateway/HomeGateway.sol index 40a767790..2ef8e606f 100644 --- a/contracts/src/gateway/HomeGateway.sol +++ b/contracts/src/gateway/HomeGateway.sol @@ -45,7 +45,7 @@ contract HomeGateway is IHomeGateway, UUPSProxiable, Initializable { /// @dev Requires that the sender is the governor. modifier onlyByGovernor() { - require(governor == msg.sender, "No allowed: governor only"); + if (governor != msg.sender) revert GovernorOnly(); _; } @@ -129,8 +129,8 @@ contract HomeGateway is IHomeGateway, UUPSProxiable, Initializable { /// @inheritdoc IHomeGateway function relayCreateDispute(RelayCreateDisputeParams memory _params) external payable override { - require(feeToken == NATIVE_CURRENCY, "Fees paid in ERC20 only"); - require(_params.foreignChainID == foreignChainID, "Foreign chain ID not supported"); + if (feeToken != NATIVE_CURRENCY) revert FeesPaidInNativeCurrencyOnly(); + if (_params.foreignChainID != foreignChainID) revert ForeignChainIDNotSupported(); bytes32 disputeHash = keccak256( abi.encodePacked( @@ -144,7 +144,7 @@ contract HomeGateway is IHomeGateway, UUPSProxiable, Initializable { ) ); RelayedData storage relayedData = disputeHashtoRelayedData[disputeHash]; - require(relayedData.relayer == address(0), "Dispute already relayed"); + if (relayedData.relayer != address(0)) revert DisputeAlreadyRelayed(); uint256 disputeID = arbitrator.createDispute{value: msg.value}(_params.choices, _params.extraData); disputeIDtoHash[disputeID] = disputeHash; @@ -167,8 +167,8 @@ contract HomeGateway is IHomeGateway, UUPSProxiable, Initializable { /// @inheritdoc IHomeGateway function relayCreateDispute(RelayCreateDisputeParams memory _params, uint256 _feeAmount) external { - require(feeToken != NATIVE_CURRENCY, "Fees paid in native currency only"); - require(_params.foreignChainID == foreignChainID, "Foreign chain ID not supported"); + if (feeToken == NATIVE_CURRENCY) revert FeesPaidInERC20Only(); + if (_params.foreignChainID != foreignChainID) revert ForeignChainIDNotSupported(); bytes32 disputeHash = keccak256( abi.encodePacked( @@ -182,10 +182,10 @@ contract HomeGateway is IHomeGateway, UUPSProxiable, Initializable { ) ); RelayedData storage relayedData = disputeHashtoRelayedData[disputeHash]; - require(relayedData.relayer == address(0), "Dispute already relayed"); + if (relayedData.relayer != address(0)) revert DisputeAlreadyRelayed(); - require(feeToken.safeTransferFrom(msg.sender, address(this), _feeAmount), "Transfer failed"); - require(feeToken.increaseAllowance(address(arbitrator), _feeAmount), "Allowance increase failed"); + if (!feeToken.safeTransferFrom(msg.sender, address(this), _feeAmount)) revert TransferFailed(); + if (!feeToken.increaseAllowance(address(arbitrator), _feeAmount)) revert AllowanceIncreaseFailed(); uint256 disputeID = arbitrator.createDispute(_params.choices, _params.extraData, feeToken, _feeAmount); disputeIDtoHash[disputeID] = disputeHash; @@ -209,7 +209,7 @@ contract HomeGateway is IHomeGateway, UUPSProxiable, Initializable { /// @inheritdoc IArbitrableV2 function rule(uint256 _disputeID, uint256 _ruling) external override { - require(msg.sender == address(arbitrator), "Only Arbitrator"); + if (msg.sender != address(arbitrator)) revert ArbitratorOnly(); bytes32 disputeHash = disputeIDtoHash[_disputeID]; RelayedData memory relayedData = disputeHashtoRelayedData[disputeHash]; @@ -234,4 +234,17 @@ contract HomeGateway is IHomeGateway, UUPSProxiable, Initializable { function receiverGateway() external view override returns (address) { return foreignGateway; } + + // ************************************* // + // * Errors * // + // ************************************* // + + error GovernorOnly(); + error ArbitratorOnly(); + error FeesPaidInERC20Only(); + error FeesPaidInNativeCurrencyOnly(); + error ForeignChainIDNotSupported(); + error DisputeAlreadyRelayed(); + error TransferFailed(); + error AllowanceIncreaseFailed(); } From 5353ccf3922180e052bbd868360b77d314eaed41 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Wed, 13 Aug 2025 19:52:40 +0100 Subject: [PATCH 28/46] chore: changelog --- contracts/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/CHANGELOG.md b/contracts/CHANGELOG.md index 800208b3e..0a0250ade 100644 --- a/contracts/CHANGELOG.md +++ b/contracts/CHANGELOG.md @@ -8,6 +8,7 @@ The format is based on [Common Changelog](https://common-changelog.org/). ### Changed +- **Breaking:** Replace `require()` with `revert()` and custom errors outside KlerosCore for consistency and smaller bytecode ([#2084](https://github.com/kleros/kleros-v2/issues/2084)) - Set the Hardhat Solidity version to v0.8.30 and enable the IR pipeline ([#2069](https://github.com/kleros/kleros-v2/issues/2069)) - Set the Foundry Solidity version to v0.8.30 and enable the IR pipeline ([#2073](https://github.com/kleros/kleros-v2/issues/2073)) - Widen the allowed solc version to any v0.8.x for the interfaces only ([#2083](https://github.com/kleros/kleros-v2/issues/2083)) From 3d211cc516589aa9125d55079a54515dd09c7162 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Wed, 13 Aug 2025 23:49:18 +0100 Subject: [PATCH 29/46] refactor: removed CappedMath, moved SortitionSumTreeFactory to `kleros-v1/libraries` --- .../evidence/ModeratedEvidenceModule.sol | 15 ++++---- .../kleros-liquid-xdai/xKlerosLiquidV2.sol | 2 +- .../libraries/SortitionSumTreeFactory.sol | 0 contracts/src/libraries/CappedMath.sol | 36 ------------------- 4 files changed, 8 insertions(+), 45 deletions(-) rename contracts/src/{ => kleros-v1}/libraries/SortitionSumTreeFactory.sol (100%) delete mode 100644 contracts/src/libraries/CappedMath.sol diff --git a/contracts/src/arbitration/evidence/ModeratedEvidenceModule.sol b/contracts/src/arbitration/evidence/ModeratedEvidenceModule.sol index dbfbd8adf..1c612265f 100644 --- a/contracts/src/arbitration/evidence/ModeratedEvidenceModule.sol +++ b/contracts/src/arbitration/evidence/ModeratedEvidenceModule.sol @@ -5,12 +5,9 @@ pragma solidity ^0.8.24; // TODO: standard interfaces should be placed in a separated repo (?) import {IArbitrableV2, IArbitratorV2} from "../interfaces/IArbitrableV2.sol"; import "../interfaces/IDisputeTemplateRegistry.sol"; -import "../../libraries/CappedMath.sol"; /// @title Implementation of the Evidence Standard with Moderated Submissions contract ModeratedEvidenceModule is IArbitrableV2 { - using CappedMath for uint256; - // ************************************* // // * Enums / Structs * // // ************************************* // @@ -205,8 +202,8 @@ contract ModeratedEvidenceModule is IArbitrableV2 { ArbitratorData storage arbitratorData = arbitratorDataList[arbitratorDataList.length - 1]; uint256 arbitrationCost = arbitrator.arbitrationCost(arbitratorData.arbitratorExtraData); - uint256 totalCost = arbitrationCost.mulCap(totalCostMultiplier) / MULTIPLIER_DIVISOR; - uint256 depositRequired = totalCost.mulCap(initialDepositMultiplier) / MULTIPLIER_DIVISOR; + uint256 totalCost = (arbitrationCost * totalCostMultiplier) / MULTIPLIER_DIVISOR; + uint256 depositRequired = (totalCost * initialDepositMultiplier) / MULTIPLIER_DIVISOR; Moderation storage moderation = evidenceData.moderations.push(); // Overpaying is allowed. @@ -245,12 +242,12 @@ contract ModeratedEvidenceModule is IArbitrableV2 { ArbitratorData storage arbitratorData = arbitratorDataList[moderation.arbitratorDataID]; uint256 arbitrationCost = arbitrator.arbitrationCost(arbitratorData.arbitratorExtraData); - uint256 totalCost = arbitrationCost.mulCap(totalCostMultiplier) / MULTIPLIER_DIVISOR; + uint256 totalCost = (arbitrationCost * totalCostMultiplier) / MULTIPLIER_DIVISOR; uint256 opposition = 3 - uint256(_side); uint256 depositRequired = moderation.paidFees[opposition] * 2; if (depositRequired == 0) { - depositRequired = totalCost.mulCap(initialDepositMultiplier) / MULTIPLIER_DIVISOR; + depositRequired = (totalCost * initialDepositMultiplier) / MULTIPLIER_DIVISOR; } else if (depositRequired > totalCost) { depositRequired = totalCost; } @@ -317,7 +314,9 @@ contract ModeratedEvidenceModule is IArbitrableV2 { ) internal returns (uint256) { uint256 contribution; uint256 remainingETH; - uint256 requiredAmount = _totalRequired.subCap(_moderation.paidFees[uint256(_side)]); + uint256 requiredAmount = _moderation.paidFees[uint256(_side)] >= _totalRequired + ? 0 + : _totalRequired - _moderation.paidFees[uint256(_side)]; (contribution, remainingETH) = calculateContribution(_amount, requiredAmount); _moderation.contributions[_contributor][uint256(_side)] += contribution; _moderation.paidFees[uint256(_side)] += contribution; diff --git a/contracts/src/kleros-v1/kleros-liquid-xdai/xKlerosLiquidV2.sol b/contracts/src/kleros-v1/kleros-liquid-xdai/xKlerosLiquidV2.sol index 0d7d97adb..d9c74946b 100644 --- a/contracts/src/kleros-v1/kleros-liquid-xdai/xKlerosLiquidV2.sol +++ b/contracts/src/kleros-v1/kleros-liquid-xdai/xKlerosLiquidV2.sol @@ -9,7 +9,7 @@ import {ITokenController} from "../interfaces/ITokenController.sol"; import {WrappedPinakion} from "./WrappedPinakion.sol"; import {IRandomAuRa} from "./interfaces/IRandomAuRa.sol"; -import {SortitionSumTreeFactory} from "../../libraries/SortitionSumTreeFactory.sol"; +import {SortitionSumTreeFactory} from "../libraries/SortitionSumTreeFactory.sol"; import "../../gateway/interfaces/IForeignGateway.sol"; /// @title xKlerosLiquidV2 diff --git a/contracts/src/libraries/SortitionSumTreeFactory.sol b/contracts/src/kleros-v1/libraries/SortitionSumTreeFactory.sol similarity index 100% rename from contracts/src/libraries/SortitionSumTreeFactory.sol rename to contracts/src/kleros-v1/libraries/SortitionSumTreeFactory.sol diff --git a/contracts/src/libraries/CappedMath.sol b/contracts/src/libraries/CappedMath.sol deleted file mode 100644 index ce0cdf55c..000000000 --- a/contracts/src/libraries/CappedMath.sol +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.24; - -/// @title CappedMath -/// @dev Math operations with caps for under and overflow. -library CappedMath { - uint256 private constant UINT_MAX = type(uint256).max; - - /// @dev Adds two unsigned integers, returns 2^256 - 1 on overflow. - function addCap(uint256 _a, uint256 _b) internal pure returns (uint256) { - unchecked { - uint256 c = _a + _b; - return c >= _a ? c : UINT_MAX; - } - } - - /// @dev Subtracts two integers, returns 0 on underflow. - function subCap(uint256 _a, uint256 _b) internal pure returns (uint256) { - if (_b > _a) return 0; - else return _a - _b; - } - - /// @dev Multiplies two unsigned integers, returns 2^256 - 1 on overflow. - function mulCap(uint256 _a, uint256 _b) internal pure returns (uint256) { - // Gas optimization: this is cheaper than requiring '_a' not being zero, but the - // benefit is lost if '_b' is also tested. - // See: https://github.com/OpenZeppelin/openzeppelin-solidity/pull/522 - if (_a == 0) return 0; - - unchecked { - uint256 c = _a * _b; - return c / _a == _b ? c : UINT_MAX; - } - } -} From 95f2803fa60e1417ad900ec008d6f74e373d4f3c Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Thu, 14 Aug 2025 00:55:06 +0100 Subject: [PATCH 30/46] refactor: consolidated ALPHA_DIVISOR and ONE_BASIS_POINTS --- contracts/src/arbitration/KlerosCoreBase.sol | 15 +++++++-------- .../dispute-kits/DisputeKitClassicBase.sol | 2 +- contracts/src/libraries/Constants.sol | 3 +++ 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/contracts/src/arbitration/KlerosCoreBase.sol b/contracts/src/arbitration/KlerosCoreBase.sol index 2b9998bda..387ff270f 100644 --- a/contracts/src/arbitration/KlerosCoreBase.sol +++ b/contracts/src/arbitration/KlerosCoreBase.sol @@ -88,7 +88,6 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable // * Storage * // // ************************************* // - uint256 private constant ALPHA_DIVISOR = 1e4; // The number to divide `Court.alpha` by. uint256 private constant NON_PAYABLE_AMOUNT = (2 ** 256 - 2) / 2; // An amount higher than the supply of ETH. address public governor; // The governor of the contract. @@ -775,13 +774,13 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable _params.feePerJurorInRound, _params.pnkAtStakePerJurorInRound ); - if (degreeOfCoherence > ALPHA_DIVISOR) { + if (degreeOfCoherence > ONE_BASIS_POINT) { // Make sure the degree doesn't exceed 1, though it should be ensured by the dispute kit. - degreeOfCoherence = ALPHA_DIVISOR; + degreeOfCoherence = ONE_BASIS_POINT; } // Fully coherent jurors won't be penalized. - uint256 penalty = (round.pnkAtStakePerJuror * (ALPHA_DIVISOR - degreeOfCoherence)) / ALPHA_DIVISOR; + uint256 penalty = (round.pnkAtStakePerJuror * (ONE_BASIS_POINT - degreeOfCoherence)) / ONE_BASIS_POINT; // Unlock the PNKs affected by the penalty address account = round.drawnJurors[_params.repartition]; @@ -835,8 +834,8 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable ); // Make sure the degree doesn't exceed 1, though it should be ensured by the dispute kit. - if (degreeOfCoherence > ALPHA_DIVISOR) { - degreeOfCoherence = ALPHA_DIVISOR; + if (degreeOfCoherence > ONE_BASIS_POINT) { + degreeOfCoherence = ONE_BASIS_POINT; } address account = round.drawnJurors[_params.repartition % _params.numberOfVotesInRound]; @@ -1062,7 +1061,7 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable /// @param _degreeOfCoherence The degree of coherence in basis points. /// @return The amount after applying the degree of coherence. function _applyCoherence(uint256 _amount, uint256 _degreeOfCoherence) internal pure returns (uint256) { - return (_amount * _degreeOfCoherence) / ALPHA_DIVISOR; + return (_amount * _degreeOfCoherence) / ONE_BASIS_POINT; } /// @dev Calculates PNK at stake per juror based on court parameters @@ -1070,7 +1069,7 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable /// @param _alpha The alpha parameter for the court in basis points. /// @return The amount of PNK at stake per juror. function _calculatePnkAtStake(uint256 _minStake, uint256 _alpha) internal pure returns (uint256) { - return (_minStake * _alpha) / ALPHA_DIVISOR; + return (_minStake * _alpha) / ONE_BASIS_POINT; } /// @dev Toggles the dispute kit support for a given court. diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol index 2d804891a..59b51e52b 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol @@ -6,6 +6,7 @@ import {KlerosCore, KlerosCoreBase, IDisputeKit, ISortitionModule} from "../Kler import {Initializable} from "../../proxy/Initializable.sol"; import {UUPSProxiable} from "../../proxy/UUPSProxiable.sol"; import {SafeSend} from "../../libraries/SafeSend.sol"; +import {ONE_BASIS_POINT} from "../../libraries/Constants.sol"; /// @title DisputeKitClassicBase /// Abstract Dispute kit classic implementation of the Kleros v1 features including: @@ -57,7 +58,6 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi uint256 public constant WINNER_STAKE_MULTIPLIER = 10000; // Multiplier of the appeal cost that the winner has to pay as fee stake for a round in basis points. Default is 1x of appeal fee. uint256 public constant LOSER_STAKE_MULTIPLIER = 20000; // Multiplier of the appeal cost that the loser has to pay as fee stake for a round in basis points. Default is 2x of appeal fee. uint256 public constant LOSER_APPEAL_PERIOD_MULTIPLIER = 5000; // Multiplier of the appeal period for the choice that wasn't voted for in the previous round, in basis points. Default is 1/2 of original appeal period. - uint256 public constant ONE_BASIS_POINT = 10000; // One basis point, for scaling. address public governor; // The governor of the contract. KlerosCore public core; // The Kleros Core arbitrator diff --git a/contracts/src/libraries/Constants.sol b/contracts/src/libraries/Constants.sol index bed573fa6..10c42d8a9 100644 --- a/contracts/src/libraries/Constants.sol +++ b/contracts/src/libraries/Constants.sol @@ -20,6 +20,9 @@ uint256 constant DEFAULT_K = 6; // Default number of children per node. uint256 constant DEFAULT_NB_OF_JURORS = 3; // The default number of jurors in a dispute. IERC20 constant NATIVE_CURRENCY = IERC20(address(0)); // The native currency, such as ETH on Arbitrum, Optimism and Ethereum L1. +// Units +uint256 constant ONE_BASIS_POINT = 10000; + enum OnError { Revert, Return From 277457c2458140f650c003932b6011c0b6c39445 Mon Sep 17 00:00:00 2001 From: unknownunknown1 Date: Thu, 14 Aug 2025 18:09:36 +1000 Subject: [PATCH 31/46] feat(RNG): foundry test --- contracts/src/test/RNGMock.sol | 19 ++++ contracts/test/foundry/KlerosCore.t.sol | 111 ++++++++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 contracts/src/test/RNGMock.sol diff --git a/contracts/src/test/RNGMock.sol b/contracts/src/test/RNGMock.sol new file mode 100644 index 000000000..df372265d --- /dev/null +++ b/contracts/src/test/RNGMock.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "../rng/IRNG.sol"; + +/// @title Simple mock rng to check fallback +contract RNGMock is IRNG { + uint256 public randomNumber; // The number to return; + + function setRN(uint256 _rn) external { + randomNumber = _rn; + } + + function requestRandomness() external override {} + + function receiveRandomness() external view override returns (uint256) { + return randomNumber; + } +} diff --git a/contracts/test/foundry/KlerosCore.t.sol b/contracts/test/foundry/KlerosCore.t.sol index caa7afafb..7613fb4c3 100644 --- a/contracts/test/foundry/KlerosCore.t.sol +++ b/contracts/test/foundry/KlerosCore.t.sol @@ -12,6 +12,8 @@ import {ISortitionModule} from "../../src/arbitration/interfaces/ISortitionModul import {SortitionModuleMock, SortitionModuleBase} from "../../src/test/SortitionModuleMock.sol"; import {UUPSProxy} from "../../src/proxy/UUPSProxy.sol"; import {BlockHashRNG} from "../../src/rng/BlockHashRNG.sol"; +import {RNGWithFallback} from "../../src/rng/RNGWithFallback.sol"; +import {RNGMock} from "../../src/test/RNGMock.sol"; import {PNK} from "../../src/token/PNK.sol"; import {TestERC20} from "../../src/token/TestERC20.sol"; import {ArbitrableExample, IArbitrableV2} from "../../src/arbitration/arbitrables/ArbitrableExample.sol"; @@ -2992,4 +2994,113 @@ contract KlerosCoreTest is Test { assertEq(totalCommited, 0, "totalCommited should be 0"); assertEq(choiceCount, 3, "choiceCount should be 3"); } + + function test_RNGFallback() public { + RNGWithFallback rngFallback; + uint256 fallbackTimeout = 100; + RNGMock rngMock = new RNGMock(); + rngFallback = new RNGWithFallback(msg.sender, address(sortitionModule), fallbackTimeout, rngMock); + assertEq(rngFallback.governor(), msg.sender, "Wrong governor"); + assertEq(rngFallback.consumer(), address(sortitionModule), "Wrong sortition module address"); + assertEq(address(rngFallback.rng()), address(rngMock), "Wrong RNG in fallback contract"); + assertEq(rngFallback.fallbackTimeoutSeconds(), fallbackTimeout, "Wrong fallback timeout"); + assertEq(rngFallback.requestTimestamp(), 0, "Request timestamp should be 0"); + + vm.prank(governor); + sortitionModule.changeRandomNumberGenerator(rngFallback); + assertEq(address(sortitionModule.rng()), address(rngFallback), "Wrong RNG address"); + + vm.prank(staker1); + core.setStake(GENERAL_COURT, 20000); + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + vm.warp(block.timestamp + minStakingTime); + + sortitionModule.passPhase(); // Generating + assertEq(rngFallback.requestTimestamp(), block.timestamp, "Wrong request timestamp"); + + vm.expectRevert(SortitionModuleBase.RandomNumberNotReady.selector); + sortitionModule.passPhase(); + + vm.warp(block.timestamp + fallbackTimeout + 1); + + // Pass several blocks too to see that correct block.number is still picked up. + vm.roll(block.number + 5); + + vm.expectEmit(true, true, true, true); + emit RNGWithFallback.RNGFallback(); + sortitionModule.passPhase(); // Drawing phase + + assertEq(sortitionModule.randomNumber(), uint256(blockhash(block.number - 1)), "Wrong random number"); + } + + function test_RNGFallback_happyPath() public { + RNGWithFallback rngFallback; + uint256 fallbackTimeout = 100; + RNGMock rngMock = new RNGMock(); + rngFallback = new RNGWithFallback(msg.sender, address(sortitionModule), fallbackTimeout, rngMock); + + vm.prank(governor); + sortitionModule.changeRandomNumberGenerator(rngFallback); + assertEq(address(sortitionModule.rng()), address(rngFallback), "Wrong RNG address"); + + vm.prank(staker1); + core.setStake(GENERAL_COURT, 20000); + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + vm.warp(block.timestamp + minStakingTime); + + assertEq(rngFallback.requestTimestamp(), 0, "Request timestamp should be 0"); + + sortitionModule.passPhase(); // Generating + assertEq(rngFallback.requestTimestamp(), block.timestamp, "Wrong request timestamp"); + + rngMock.setRN(123); + + sortitionModule.passPhase(); // Drawing phase + assertEq(sortitionModule.randomNumber(), 123, "Wrong random number"); + } + + function test_RNGFallback_sanityChecks() public { + RNGWithFallback rngFallback; + uint256 fallbackTimeout = 100; + RNGMock rngMock = new RNGMock(); + rngFallback = new RNGWithFallback(msg.sender, address(sortitionModule), fallbackTimeout, rngMock); + + vm.expectRevert(bytes("Consumer only")); + vm.prank(governor); + rngFallback.requestRandomness(); + + vm.expectRevert(bytes("Consumer only")); + vm.prank(governor); + rngFallback.receiveRandomness(); + + vm.expectRevert(bytes("Governor only")); + vm.prank(other); + rngFallback.changeGovernor(other); + vm.prank(governor); + rngFallback.changeGovernor(other); + assertEq(rngFallback.governor(), other, "Wrong governor"); + + // Change governor back for convenience + vm.prank(other); + rngFallback.changeGovernor(governor); + + vm.expectRevert(bytes("Governor only")); + vm.prank(other); + rngFallback.changeConsumer(other); + vm.prank(governor); + rngFallback.changeConsumer(other); + assertEq(rngFallback.consumer(), other, "Wrong consumer"); + + vm.expectRevert(bytes("Governor only")); + vm.prank(other); + rngFallback.changeFallbackTimeout(5); + + vm.prank(governor); + vm.expectEmit(true, true, true, true); + emit RNGWithFallback.FallbackTimeoutChanged(5); + rngFallback.changeFallbackTimeout(5); + assertEq(rngFallback.fallbackTimeoutSeconds(), 5, "Wrong fallback timeout"); + } } From d1910cad5d90a07d1cd29817e7b70601dd18c0de Mon Sep 17 00:00:00 2001 From: Harman-singh-waraich Date: Thu, 14 Aug 2025 16:45:27 +0530 Subject: [PATCH 32/46] fix(web): timeline-bug-fix --- web/src/pages/Cases/CaseDetails/Timeline.tsx | 34 +++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/web/src/pages/Cases/CaseDetails/Timeline.tsx b/web/src/pages/Cases/CaseDetails/Timeline.tsx index 0dbf8fdfd..315d6df70 100644 --- a/web/src/pages/Cases/CaseDetails/Timeline.tsx +++ b/web/src/pages/Cases/CaseDetails/Timeline.tsx @@ -68,7 +68,7 @@ const Timeline: React.FC<{ currentPeriodIndex: number; }> = ({ currentPeriodIndex, dispute }) => { const currentItemIndex = currentPeriodToCurrentItem(currentPeriodIndex, dispute?.court.hiddenVotes); - const items = useTimeline(dispute, currentItemIndex, currentItemIndex); + const items = useTimeline(dispute, currentPeriodIndex); return ( @@ -103,30 +103,26 @@ const currentPeriodToCurrentItem = (currentPeriodIndex: number, hiddenVotes?: bo else return currentPeriodIndex - 1; }; -const useTimeline = (dispute: DisputeDetailsQuery["dispute"], currentItemIndex: number, currentPeriodIndex: number) => { +const useTimeline = (dispute: DisputeDetailsQuery["dispute"], currentPeriodIndex: number) => { const isDesktop = useIsDesktop(); - const titles = useMemo(() => { - const titles = ["Evidence", "Voting", "Appeal", "Executed"]; - if (dispute?.court.hiddenVotes) { - titles.splice(1, 0, "Commit"); - } - return titles; - }, [dispute]); + const titles = ["Evidence", "Commit", "Voting", "Appeal", "Executed"]; + const deadlineCurrentPeriod = getDeadline( currentPeriodIndex, dispute?.lastPeriodChange, dispute?.court.timesPerPeriod ); + const countdown = useCountdown(deadlineCurrentPeriod); const getSubitems = (index: number): string[] | React.ReactNode[] => { if (typeof countdown !== "undefined" && dispute) { if (index === titles.length - 1) { return []; - } else if (index === currentItemIndex && countdown === 0) { + } else if (index === currentPeriodIndex && countdown === 0) { return ["Time's up!"]; - } else if (index < currentItemIndex) { + } else if (index < currentPeriodIndex) { return []; - } else if (index === currentItemIndex) { + } else if (index === currentPeriodIndex) { return [secondsToDayHourMinute(countdown)]; } else { return [secondsToDayHourMinute(dispute?.court.timesPerPeriod[index])]; @@ -134,10 +130,16 @@ const useTimeline = (dispute: DisputeDetailsQuery["dispute"], currentItemIndex: } return []; }; - return titles.map((title, i) => ({ - title: i + 1 < titles.length && isDesktop ? `${title} Period` : title, - subitems: getSubitems(i), - })); + return titles + .map((title, i) => { + // if not hidden votes, skip commit index + if (!dispute?.court.hiddenVotes && i === Periods.commit) return; + return { + title: i + 1 < titles.length && isDesktop ? `${title} Period` : title, + subitems: getSubitems(i), + }; + }) + .filter((item) => !isUndefined(item)); }; export const getDeadline = ( From 8d0edf4b79f2a2c7918e6567c9505df99e329d5d Mon Sep 17 00:00:00 2001 From: Harman-singh-waraich Date: Thu, 14 Aug 2025 18:48:53 +0530 Subject: [PATCH 33/46] chore: rabbit-review --- web/src/pages/Cases/CaseDetails/Timeline.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/web/src/pages/Cases/CaseDetails/Timeline.tsx b/web/src/pages/Cases/CaseDetails/Timeline.tsx index 315d6df70..3d8af7464 100644 --- a/web/src/pages/Cases/CaseDetails/Timeline.tsx +++ b/web/src/pages/Cases/CaseDetails/Timeline.tsx @@ -130,16 +130,16 @@ const useTimeline = (dispute: DisputeDetailsQuery["dispute"], currentPeriodIndex } return []; }; - return titles - .map((title, i) => { - // if not hidden votes, skip commit index - if (!dispute?.court.hiddenVotes && i === Periods.commit) return; - return { + return titles.flatMap((title, i) => { + // if not hidden votes, skip commit index + if (!dispute?.court.hiddenVotes && i === Periods.commit) return []; + return [ + { title: i + 1 < titles.length && isDesktop ? `${title} Period` : title, subitems: getSubitems(i), - }; - }) - .filter((item) => !isUndefined(item)); + }, + ]; + }); }; export const getDeadline = ( From c1bad1debd64610833b11b3da150c2e8275df0e2 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Thu, 14 Aug 2025 14:46:41 +0100 Subject: [PATCH 34/46] docs: comment --- contracts/src/arbitration/KlerosCoreBase.sol | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/contracts/src/arbitration/KlerosCoreBase.sol b/contracts/src/arbitration/KlerosCoreBase.sol index 387ff270f..fe1c53521 100644 --- a/contracts/src/arbitration/KlerosCoreBase.sol +++ b/contracts/src/arbitration/KlerosCoreBase.sol @@ -774,8 +774,9 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable _params.feePerJurorInRound, _params.pnkAtStakePerJurorInRound ); + + // Guard against degree exceeding 1, though it should be ensured by the dispute kit. if (degreeOfCoherence > ONE_BASIS_POINT) { - // Make sure the degree doesn't exceed 1, though it should be ensured by the dispute kit. degreeOfCoherence = ONE_BASIS_POINT; } @@ -833,7 +834,7 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable _params.pnkAtStakePerJurorInRound ); - // Make sure the degree doesn't exceed 1, though it should be ensured by the dispute kit. + // Guard against degree exceeding 1, though it should be ensured by the dispute kit. if (degreeOfCoherence > ONE_BASIS_POINT) { degreeOfCoherence = ONE_BASIS_POINT; } From be3384723a23c297260a3b6487eb335c9e0a3dd4 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Thu, 14 Aug 2025 14:54:45 +0100 Subject: [PATCH 35/46] fix: typo in local variable --- contracts/src/arbitration/SortitionModuleBase.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/src/arbitration/SortitionModuleBase.sol b/contracts/src/arbitration/SortitionModuleBase.sol index e49fd2c6c..048fd3b40 100644 --- a/contracts/src/arbitration/SortitionModuleBase.sol +++ b/contracts/src/arbitration/SortitionModuleBase.sol @@ -344,14 +344,14 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr // Update the sortition sum tree. bytes32 stakePathID = _accountAndCourtIDToStakePathID(_account, _courtID); bool finished = false; - uint96 currenCourtID = _courtID; + uint96 currentCourtID = _courtID; while (!finished) { // Tokens are also implicitly staked in parent courts through sortition module to increase the chance of being drawn. - _set(bytes32(uint256(currenCourtID)), _newStake, stakePathID); - if (currenCourtID == GENERAL_COURT) { + _set(bytes32(uint256(currentCourtID)), _newStake, stakePathID); + if (currentCourtID == GENERAL_COURT) { finished = true; } else { - (currenCourtID, , , , , , ) = core.courts(currenCourtID); // Get the parent court. + (currentCourtID, , , , , , ) = core.courts(currentCourtID); // Get the parent court. } } emit StakeSet(_account, _courtID, _newStake, juror.stakedPnk); From cabc743c5e55c94d095719a51c44d8db57136a9f Mon Sep 17 00:00:00 2001 From: kemuru <102478601+kemuru@users.noreply.github.com> Date: Fri, 15 Aug 2025 00:32:54 +0200 Subject: [PATCH 36/46] chore: label text improvements --- web/src/components/DisputeView/CardLabels/index.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/web/src/components/DisputeView/CardLabels/index.tsx b/web/src/components/DisputeView/CardLabels/index.tsx index d6ca94a56..99b25b1fb 100644 --- a/web/src/components/DisputeView/CardLabels/index.tsx +++ b/web/src/components/DisputeView/CardLabels/index.tsx @@ -61,13 +61,13 @@ interface ICardLabels { } const LabelArgs: Record>; color: IColors }> = { - EvidenceTime: { text: "Evidence Time", icon: EvidenceIcon, color: "blue" }, - NotDrawn: { text: "Not Drawn", icon: NotDrawnIcon, color: "grey" }, - CanVote: { text: "Time to vote", icon: CanVoteIcon, color: "blue" }, - Voted: { text: "I voted", icon: VotedIcon, color: "purple" }, - DidNotVote: { text: "Didn't cast a vote", icon: ForgotToVoteIcon, color: "purple" }, + EvidenceTime: { text: "Evidence time", icon: EvidenceIcon, color: "blue" }, + NotDrawn: { text: "You were not drawn", icon: NotDrawnIcon, color: "grey" }, + CanVote: { text: "You can vote", icon: CanVoteIcon, color: "blue" }, + Voted: { text: "You voted", icon: VotedIcon, color: "purple" }, + DidNotVote: { text: "You forgot to vote", icon: ForgotToVoteIcon, color: "purple" }, CanFund: { text: "Appeal possible", icon: AppealIcon, color: "lightPurple" }, - Funded: { text: "I funded", icon: FundedIcon, color: "lightPurple" }, + Funded: { text: "You funded an appeal", icon: FundedIcon, color: "lightPurple" }, }; const getFundingRewards = (contributions: ClassicContribution[], closed: boolean) => { From b19b82d4312f0b68e20b21639cadd4153c76c939 Mon Sep 17 00:00:00 2001 From: kemuru <102478601+kemuru@users.noreply.github.com> Date: Fri, 15 Aug 2025 00:41:39 +0200 Subject: [PATCH 37/46] chore: add urgency and more clarity that it's right now --- web/src/components/DisputeView/CardLabels/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/DisputeView/CardLabels/index.tsx b/web/src/components/DisputeView/CardLabels/index.tsx index 99b25b1fb..acc0a3e58 100644 --- a/web/src/components/DisputeView/CardLabels/index.tsx +++ b/web/src/components/DisputeView/CardLabels/index.tsx @@ -63,7 +63,7 @@ interface ICardLabels { const LabelArgs: Record>; color: IColors }> = { EvidenceTime: { text: "Evidence time", icon: EvidenceIcon, color: "blue" }, NotDrawn: { text: "You were not drawn", icon: NotDrawnIcon, color: "grey" }, - CanVote: { text: "You can vote", icon: CanVoteIcon, color: "blue" }, + CanVote: { text: "You can vote now", icon: CanVoteIcon, color: "blue" }, Voted: { text: "You voted", icon: VotedIcon, color: "purple" }, DidNotVote: { text: "You forgot to vote", icon: ForgotToVoteIcon, color: "purple" }, CanFund: { text: "Appeal possible", icon: AppealIcon, color: "lightPurple" }, From 1dbfedf4ef2efa7d431d83e8f1e3af49db500409 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Fri, 15 Aug 2025 00:46:30 +0100 Subject: [PATCH 38/46] feat: multi-dimensional degree of coherence for reward/penalties/pnk/eth --- contracts/src/arbitration/KlerosCoreBase.sol | 33 ++++++------ .../dispute-kits/DisputeKitClassicBase.sol | 31 ++++++++++-- .../arbitration/interfaces/IDisputeKit.sol | 22 ++++++-- .../university/KlerosCoreUniversity.sol | 39 ++++++++------- .../kleros-liquid-xdai/xKlerosLiquidV2.sol | 1 - contracts/test/foundry/KlerosCore.t.sol | 50 ++++++++++++++++--- 6 files changed, 130 insertions(+), 46 deletions(-) diff --git a/contracts/src/arbitration/KlerosCoreBase.sol b/contracts/src/arbitration/KlerosCoreBase.sol index fe1c53521..8b768d8aa 100644 --- a/contracts/src/arbitration/KlerosCoreBase.sol +++ b/contracts/src/arbitration/KlerosCoreBase.sol @@ -767,7 +767,7 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable IDisputeKit disputeKit = disputeKits[round.disputeKitID]; // [0, 1] value that determines how coherent the juror was in this round, in basis points. - uint256 degreeOfCoherence = disputeKit.getDegreeOfCoherence( + uint256 coherence = disputeKit.getDegreeOfCoherencePenalty( _params.disputeID, _params.round, _params.repartition, @@ -776,12 +776,12 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable ); // Guard against degree exceeding 1, though it should be ensured by the dispute kit. - if (degreeOfCoherence > ONE_BASIS_POINT) { - degreeOfCoherence = ONE_BASIS_POINT; + if (coherence > ONE_BASIS_POINT) { + coherence = ONE_BASIS_POINT; } // Fully coherent jurors won't be penalized. - uint256 penalty = (round.pnkAtStakePerJuror * (ONE_BASIS_POINT - degreeOfCoherence)) / ONE_BASIS_POINT; + uint256 penalty = (round.pnkAtStakePerJuror * (ONE_BASIS_POINT - coherence)) / ONE_BASIS_POINT; // Unlock the PNKs affected by the penalty address account = round.drawnJurors[_params.repartition]; @@ -794,7 +794,7 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable account, _params.disputeID, _params.round, - degreeOfCoherence, + coherence, -int256(availablePenalty), 0, round.feeToken @@ -826,7 +826,7 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable IDisputeKit disputeKit = disputeKits[round.disputeKitID]; // [0, 1] value that determines how coherent the juror was in this round, in basis points. - uint256 degreeOfCoherence = disputeKit.getDegreeOfCoherence( + (uint256 pnkCoherence, uint256 feeCoherence) = disputeKit.getDegreeOfCoherenceReward( _params.disputeID, _params.round, _params.repartition % _params.numberOfVotesInRound, @@ -835,20 +835,23 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable ); // Guard against degree exceeding 1, though it should be ensured by the dispute kit. - if (degreeOfCoherence > ONE_BASIS_POINT) { - degreeOfCoherence = ONE_BASIS_POINT; + if (pnkCoherence > ONE_BASIS_POINT) { + pnkCoherence = ONE_BASIS_POINT; + } + if (feeCoherence > ONE_BASIS_POINT) { + feeCoherence = ONE_BASIS_POINT; } address account = round.drawnJurors[_params.repartition % _params.numberOfVotesInRound]; - uint256 pnkLocked = _applyCoherence(round.pnkAtStakePerJuror, degreeOfCoherence); + uint256 pnkLocked = _applyCoherence(round.pnkAtStakePerJuror, pnkCoherence); // Release the rest of the PNKs of the juror for this round. sortitionModule.unlockStake(account, pnkLocked); // Transfer the rewards - uint256 pnkReward = _applyCoherence(_params.pnkPenaltiesInRound / _params.coherentCount, degreeOfCoherence); + uint256 pnkReward = _applyCoherence(_params.pnkPenaltiesInRound / _params.coherentCount, pnkCoherence); round.sumPnkRewardPaid += pnkReward; - uint256 feeReward = _applyCoherence(round.totalFeesForJurors / _params.coherentCount, degreeOfCoherence); + uint256 feeReward = _applyCoherence(round.totalFeesForJurors / _params.coherentCount, feeCoherence); round.sumFeeRewardPaid += feeReward; pinakion.safeTransfer(account, pnkReward); _transferFeeToken(round.feeToken, payable(account), feeReward); @@ -856,7 +859,7 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable account, _params.disputeID, _params.round, - degreeOfCoherence, + pnkCoherence, int256(pnkReward), int256(feeReward), round.feeToken @@ -1059,10 +1062,10 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable /// @dev Applies degree of coherence to an amount /// @param _amount The base amount to apply coherence to. - /// @param _degreeOfCoherence The degree of coherence in basis points. + /// @param _coherence The degree of coherence in basis points. /// @return The amount after applying the degree of coherence. - function _applyCoherence(uint256 _amount, uint256 _degreeOfCoherence) internal pure returns (uint256) { - return (_amount * _degreeOfCoherence) / ONE_BASIS_POINT; + function _applyCoherence(uint256 _amount, uint256 _coherence) internal pure returns (uint256) { + return (_amount * _coherence) / ONE_BASIS_POINT; } /// @dev Calculates PNK at stake per juror based on court parameters diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol index 59b51e52b..0922f047b 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol @@ -526,14 +526,39 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi /// @param _coreDisputeID The ID of the dispute in Kleros Core, not in the Dispute Kit. /// @param _coreRoundID The ID of the round in Kleros Core, not in the Dispute Kit. /// @param _voteID The ID of the vote. - /// @return The degree of coherence in basis points. - function getDegreeOfCoherence( + /// @return pnkCoherence The degree of coherence in basis points for the dispute PNK reward. + /// @return feeCoherence The degree of coherence in basis points for the dispute fee reward. + function getDegreeOfCoherenceReward( uint256 _coreDisputeID, uint256 _coreRoundID, uint256 _voteID, uint256 /* _feePerJuror */, uint256 /* _pnkAtStakePerJuror */ - ) external view override returns (uint256) { + ) external view override returns (uint256 pnkCoherence, uint256 feeCoherence) { + uint256 coherence = _getDegreeOfCoherence(_coreDisputeID, _coreRoundID, _voteID); + return (coherence, coherence); + } + + /// @dev Gets the degree of coherence of a particular voter. This function is called by Kleros Core in order to determine the amount of the penalty. + /// @param _coreDisputeID The ID of the dispute in Kleros Core, not in the Dispute Kit. + /// @param _coreRoundID The ID of the round in Kleros Core, not in the Dispute Kit. + /// @param _voteID The ID of the vote. + /// @return pnkCoherence The degree of coherence in basis points for the dispute PNK reward. + function getDegreeOfCoherencePenalty( + uint256 _coreDisputeID, + uint256 _coreRoundID, + uint256 _voteID, + uint256 /* _feePerJuror */, + uint256 /* _pnkAtStakePerJuror */ + ) external view override returns (uint256 pnkCoherence) { + return _getDegreeOfCoherence(_coreDisputeID, _coreRoundID, _voteID); + } + + function _getDegreeOfCoherence( + uint256 _coreDisputeID, + uint256 _coreRoundID, + uint256 _voteID + ) internal view returns (uint256 coherence) { // In this contract this degree can be either 0 or 1, but in other dispute kits this value can be something in between. Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; Vote storage vote = dispute.rounds[dispute.coreRoundIDToLocal[_coreRoundID]].votes[_voteID]; diff --git a/contracts/src/arbitration/interfaces/IDisputeKit.sol b/contracts/src/arbitration/interfaces/IDisputeKit.sol index 423f38e3e..6a72b35e4 100644 --- a/contracts/src/arbitration/interfaces/IDisputeKit.sol +++ b/contracts/src/arbitration/interfaces/IDisputeKit.sol @@ -67,14 +67,30 @@ interface IDisputeKit { /// @param _voteID The ID of the vote. /// @param _feePerJuror The fee per juror. /// @param _pnkAtStakePerJuror The PNK at stake per juror. - /// @return The degree of coherence in basis points. - function getDegreeOfCoherence( + /// @return pnkCoherence The degree of coherence in basis points for the dispute PNK reward. + /// @return feeCoherence The degree of coherence in basis points for the dispute fee reward. + function getDegreeOfCoherenceReward( uint256 _coreDisputeID, uint256 _coreRoundID, uint256 _voteID, uint256 _feePerJuror, uint256 _pnkAtStakePerJuror - ) external view returns (uint256); + ) external view returns (uint256 pnkCoherence, uint256 feeCoherence); + + /// @dev Gets the degree of coherence of a particular voter. This function is called by Kleros Core in order to determine the amount of the penalty. + /// @param _coreDisputeID The ID of the dispute in Kleros Core, not in the Dispute Kit. + /// @param _coreRoundID The ID of the round in Kleros Core, not in the Dispute Kit. + /// @param _voteID The ID of the vote. + /// @param _feePerJuror The fee per juror. + /// @param _pnkAtStakePerJuror The PNK at stake per juror. + /// @return pnkCoherence The degree of coherence in basis points for the dispute PNK reward. + function getDegreeOfCoherencePenalty( + uint256 _coreDisputeID, + uint256 _coreRoundID, + uint256 _voteID, + uint256 _feePerJuror, + uint256 _pnkAtStakePerJuror + ) external view returns (uint256 pnkCoherence); /// @dev Gets the number of jurors who are eligible to a reward in this round. /// @param _coreDisputeID The ID of the dispute in Kleros Core, not in the Dispute Kit. diff --git a/contracts/src/arbitration/university/KlerosCoreUniversity.sol b/contracts/src/arbitration/university/KlerosCoreUniversity.sol index 35fd5262c..5744088b0 100644 --- a/contracts/src/arbitration/university/KlerosCoreUniversity.sol +++ b/contracts/src/arbitration/university/KlerosCoreUniversity.sol @@ -6,9 +6,9 @@ import {IArbitrableV2, IArbitratorV2} from "../interfaces/IArbitratorV2.sol"; import {IDisputeKit} from "../interfaces/IDisputeKit.sol"; import {ISortitionModuleUniversity} from "./ISortitionModuleUniversity.sol"; import {SafeERC20, IERC20} from "../../libraries/SafeERC20.sol"; -import "../../libraries/Constants.sol"; import {UUPSProxiable} from "../../proxy/UUPSProxiable.sol"; import {Initializable} from "../../proxy/Initializable.sol"; +import "../../libraries/Constants.sol"; /// @title KlerosCoreUniversity /// Core arbitrator contract for educational purposes. @@ -87,7 +87,6 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { // * Storage * // // ************************************* // - uint256 private constant ALPHA_DIVISOR = 1e4; // The number to divide `Court.alpha` by. uint256 private constant NON_PAYABLE_AMOUNT = (2 ** 256 - 2) / 2; // An amount higher than the supply of ETH. address public governor; // The governor of the contract. @@ -526,7 +525,7 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { : convertEthToTokenAmount(_feeToken, court.feeForJuror); round.nbVotes = _feeAmount / feeForJuror; round.disputeKitID = disputeKitID; - round.pnkAtStakePerJuror = (court.minStake * court.alpha) / ALPHA_DIVISOR; + round.pnkAtStakePerJuror = (court.minStake * court.alpha) / ONE_BASIS_POINT; round.totalFeesForJurors = _feeAmount; round.feeToken = IERC20(_feeToken); @@ -655,7 +654,7 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { Court storage court = courts[newCourtID]; extraRound.nbVotes = msg.value / court.feeForJuror; // As many votes that can be afforded by the provided funds. - extraRound.pnkAtStakePerJuror = (court.minStake * court.alpha) / ALPHA_DIVISOR; + extraRound.pnkAtStakePerJuror = (court.minStake * court.alpha) / ONE_BASIS_POINT; extraRound.totalFeesForJurors = msg.value; extraRound.disputeKitID = newDisputeKitID; @@ -754,20 +753,21 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { IDisputeKit disputeKit = disputeKits[round.disputeKitID]; // [0, 1] value that determines how coherent the juror was in this round, in basis points. - uint256 degreeOfCoherence = disputeKit.getDegreeOfCoherence( + uint256 coherence = disputeKit.getDegreeOfCoherencePenalty( _params.disputeID, _params.round, _params.repartition, _params.feePerJurorInRound, _params.pnkAtStakePerJurorInRound ); - if (degreeOfCoherence > ALPHA_DIVISOR) { - // Make sure the degree doesn't exceed 1, though it should be ensured by the dispute kit. - degreeOfCoherence = ALPHA_DIVISOR; + + // Guard against degree exceeding 1, though it should be ensured by the dispute kit. + if (coherence > ONE_BASIS_POINT) { + coherence = ONE_BASIS_POINT; } // Fully coherent jurors won't be penalized. - uint256 penalty = (round.pnkAtStakePerJuror * (ALPHA_DIVISOR - degreeOfCoherence)) / ALPHA_DIVISOR; + uint256 penalty = (round.pnkAtStakePerJuror * (ONE_BASIS_POINT - coherence)) / ONE_BASIS_POINT; // Unlock the PNKs affected by the penalty address account = round.drawnJurors[_params.repartition]; @@ -780,7 +780,7 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { account, _params.disputeID, _params.round, - degreeOfCoherence, + coherence, -int256(availablePenalty), 0, round.feeToken @@ -818,7 +818,7 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { IDisputeKit disputeKit = disputeKits[round.disputeKitID]; // [0, 1] value that determines how coherent the juror was in this round, in basis points. - uint256 degreeOfCoherence = disputeKit.getDegreeOfCoherence( + (uint256 pnkCoherence, uint256 feeCoherence) = disputeKit.getDegreeOfCoherenceReward( _params.disputeID, _params.round, _params.repartition % _params.numberOfVotesInRound, @@ -826,21 +826,24 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { _params.pnkAtStakePerJurorInRound ); - // Make sure the degree doesn't exceed 1, though it should be ensured by the dispute kit. - if (degreeOfCoherence > ALPHA_DIVISOR) { - degreeOfCoherence = ALPHA_DIVISOR; + // Guard against degree exceeding 1, though it should be ensured by the dispute kit. + if (pnkCoherence > ONE_BASIS_POINT) { + pnkCoherence = ONE_BASIS_POINT; + } + if (feeCoherence > ONE_BASIS_POINT) { + feeCoherence = ONE_BASIS_POINT; } address account = round.drawnJurors[_params.repartition % _params.numberOfVotesInRound]; - uint256 pnkLocked = (round.pnkAtStakePerJuror * degreeOfCoherence) / ALPHA_DIVISOR; + uint256 pnkLocked = (round.pnkAtStakePerJuror * pnkCoherence) / ONE_BASIS_POINT; // Release the rest of the PNKs of the juror for this round. sortitionModule.unlockStake(account, pnkLocked); // Transfer the rewards - uint256 pnkReward = ((_params.pnkPenaltiesInRound / _params.coherentCount) * degreeOfCoherence) / ALPHA_DIVISOR; + uint256 pnkReward = ((_params.pnkPenaltiesInRound / _params.coherentCount) * pnkCoherence) / ONE_BASIS_POINT; round.sumPnkRewardPaid += pnkReward; - uint256 feeReward = ((round.totalFeesForJurors / _params.coherentCount) * degreeOfCoherence) / ALPHA_DIVISOR; + uint256 feeReward = ((round.totalFeesForJurors / _params.coherentCount) * feeCoherence) / ONE_BASIS_POINT; round.sumFeeRewardPaid += feeReward; pinakion.safeTransfer(account, pnkReward); if (round.feeToken == NATIVE_CURRENCY) { @@ -854,7 +857,7 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { account, _params.disputeID, _params.round, - degreeOfCoherence, + pnkCoherence, int256(pnkReward), int256(feeReward), round.feeToken diff --git a/contracts/src/kleros-v1/kleros-liquid-xdai/xKlerosLiquidV2.sol b/contracts/src/kleros-v1/kleros-liquid-xdai/xKlerosLiquidV2.sol index d9c74946b..9a25909b7 100644 --- a/contracts/src/kleros-v1/kleros-liquid-xdai/xKlerosLiquidV2.sol +++ b/contracts/src/kleros-v1/kleros-liquid-xdai/xKlerosLiquidV2.sol @@ -141,7 +141,6 @@ contract xKlerosLiquidV2 is Initializable, ITokenController, IArbitratorV2 { uint256 public constant MAX_STAKE_PATHS = 4; // The maximum number of stake paths a juror can have. uint256 public constant DEFAULT_NB_OF_JURORS = 3; // The default number of jurors in a dispute. uint256 public constant NON_PAYABLE_AMOUNT = (2 ** 256 - 2) / 2; // An amount higher than the supply of ETH. - uint256 public constant ALPHA_DIVISOR = 1e4; // The number to divide `Court.alpha` by. // General Contracts address public governor; // The governor of the contract. WrappedPinakion public pinakion; // The Pinakion token contract. diff --git a/contracts/test/foundry/KlerosCore.t.sol b/contracts/test/foundry/KlerosCore.t.sol index 72a6a565c..a2508ad70 100644 --- a/contracts/test/foundry/KlerosCore.t.sol +++ b/contracts/test/foundry/KlerosCore.t.sol @@ -2311,10 +2311,33 @@ contract KlerosCoreTest is Test { core.unpause(); assertEq(disputeKit.getCoherentCount(disputeID, 0), 2, "Wrong coherent count"); + + uint256 pnkCoherence; + uint256 feeCoherence; // dispute, round, voteID, feeForJuror (not used in classic DK), pnkPerJuror (not used in classic DK) - assertEq(disputeKit.getDegreeOfCoherence(disputeID, 0, 0, 0, 0), 0, "Wrong degree of coherence 0 vote ID"); - assertEq(disputeKit.getDegreeOfCoherence(disputeID, 0, 1, 0, 0), 10000, "Wrong degree of coherence 1 vote ID"); - assertEq(disputeKit.getDegreeOfCoherence(disputeID, 0, 2, 0, 0), 10000, "Wrong degree of coherence 2 vote ID"); + (pnkCoherence, feeCoherence) = disputeKit.getDegreeOfCoherenceReward(disputeID, 0, 0, 0, 0); + assertEq(pnkCoherence, 0, "Wrong reward pnk coherence 0 vote ID"); + assertEq(feeCoherence, 0, "Wrong reward fee coherence 0 vote ID"); + + (pnkCoherence, feeCoherence) = disputeKit.getDegreeOfCoherenceReward(disputeID, 0, 1, 0, 0); + assertEq(pnkCoherence, 10000, "Wrong reward pnk coherence 1 vote ID"); + assertEq(feeCoherence, 10000, "Wrong reward fee coherence 1 vote ID"); + + (pnkCoherence, feeCoherence) = disputeKit.getDegreeOfCoherenceReward(disputeID, 0, 2, 0, 0); + assertEq(pnkCoherence, 10000, "Wrong reward pnk coherence 2 vote ID"); + assertEq(feeCoherence, 10000, "Wrong reward fee coherence 2 vote ID"); + + assertEq(disputeKit.getDegreeOfCoherencePenalty(disputeID, 0, 0, 0, 0), 0, "Wrong penalty coherence 0 vote ID"); + assertEq( + disputeKit.getDegreeOfCoherencePenalty(disputeID, 0, 1, 0, 0), + 10000, + "Wrong penalty coherence 1 vote ID" + ); + assertEq( + disputeKit.getDegreeOfCoherencePenalty(disputeID, 0, 2, 0, 0), + 10000, + "Wrong penalty coherence 2 vote ID" + ); vm.expectEmit(true, true, true, true); emit SortitionModuleBase.StakeLocked(staker1, 1000, true); @@ -2398,10 +2421,25 @@ contract KlerosCoreTest is Test { core.passPeriod(disputeID); // Execution assertEq(disputeKit.getCoherentCount(disputeID, 0), 0, "Wrong coherent count"); + + uint256 pnkCoherence; + uint256 feeCoherence; // dispute, round, voteID, feeForJuror (not used in classic DK), pnkPerJuror (not used in classic DK) - assertEq(disputeKit.getDegreeOfCoherence(disputeID, 0, 0, 0, 0), 0, "Wrong degree of coherence 0 vote ID"); - assertEq(disputeKit.getDegreeOfCoherence(disputeID, 0, 1, 0, 0), 0, "Wrong degree of coherence 1 vote ID"); - assertEq(disputeKit.getDegreeOfCoherence(disputeID, 0, 2, 0, 0), 0, "Wrong degree of coherence 2 vote ID"); + (pnkCoherence, feeCoherence) = disputeKit.getDegreeOfCoherenceReward(disputeID, 0, 0, 0, 0); + assertEq(pnkCoherence, 0, "Wrong reward pnk coherence 0 vote ID"); + assertEq(feeCoherence, 0, "Wrong reward fee coherence 0 vote ID"); + + (pnkCoherence, feeCoherence) = disputeKit.getDegreeOfCoherenceReward(disputeID, 0, 1, 0, 0); + assertEq(pnkCoherence, 0, "Wrong reward pnk coherence 1 vote ID"); + assertEq(feeCoherence, 0, "Wrong reward fee coherence 1 vote ID"); + + (pnkCoherence, feeCoherence) = disputeKit.getDegreeOfCoherenceReward(disputeID, 0, 2, 0, 0); + assertEq(pnkCoherence, 0, "Wrong reward pnk coherence 2 vote ID"); + assertEq(feeCoherence, 0, "Wrong reward fee coherence 2 vote ID"); + + assertEq(disputeKit.getDegreeOfCoherencePenalty(disputeID, 0, 0, 0, 0), 0, "Wrong penalty coherence 0 vote ID"); + assertEq(disputeKit.getDegreeOfCoherencePenalty(disputeID, 0, 1, 0, 0), 0, "Wrong penalty coherence 1 vote ID"); + assertEq(disputeKit.getDegreeOfCoherencePenalty(disputeID, 0, 2, 0, 0), 0, "Wrong penalty coherence 2 vote ID"); uint256 governorBalance = governor.balance; uint256 governorTokenBalance = pinakion.balanceOf(governor); From 37bd43f411d6d952e09c78a8c6821c18b38df2e5 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Wed, 13 Aug 2025 23:31:28 +0100 Subject: [PATCH 39/46] fix: no passing to voting period if commits are all cast --- contracts/src/arbitration/KlerosCoreBase.sol | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/contracts/src/arbitration/KlerosCoreBase.sol b/contracts/src/arbitration/KlerosCoreBase.sol index 2b9998bda..22d1d996a 100644 --- a/contracts/src/arbitration/KlerosCoreBase.sol +++ b/contracts/src/arbitration/KlerosCoreBase.sol @@ -566,10 +566,8 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable if (round.drawnJurors.length != round.nbVotes) revert DisputeStillDrawing(); dispute.period = court.hiddenVotes ? Period.commit : Period.vote; } else if (dispute.period == Period.commit) { - if ( - block.timestamp - dispute.lastPeriodChange < court.timesPerPeriod[uint256(dispute.period)] && - !disputeKits[round.disputeKitID].areCommitsAllCast(_disputeID) - ) { + // Note that we do not want to pass to Voting period if all the commits are cast because it breaks the Shutter auto-reveal currently. + if (block.timestamp - dispute.lastPeriodChange < court.timesPerPeriod[uint256(dispute.period)]) { revert CommitPeriodNotPassed(); } dispute.period = Period.vote; From 0a10ad22eabfdadaa518cdf3e3b4294be6c96d39 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Wed, 13 Aug 2025 23:36:46 +0100 Subject: [PATCH 40/46] chore: changelog --- contracts/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/contracts/CHANGELOG.md b/contracts/CHANGELOG.md index 0a0250ade..9d4abf080 100644 --- a/contracts/CHANGELOG.md +++ b/contracts/CHANGELOG.md @@ -15,6 +15,10 @@ The format is based on [Common Changelog](https://common-changelog.org/). - Bump `hardhat` to v2.26.2 ([#2069](https://github.com/kleros/kleros-v2/issues/2069)) - Bump `@kleros/vea-contracts` to v0.7.0 ([#2073](https://github.com/kleros/kleros-v2/issues/2073)) +### Fixed + +- Do not pass to Voting period if all the commits are cast because it breaks the current Shutter auto-reveal process. ([#2085](https://github.com/kleros/kleros-v2/issues/2085)) + ## [0.12.0] - 2025-08-05 ### Changed From 257870c1fad6f234315da1c2bab5d763a427deeb Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Fri, 15 Aug 2025 01:58:52 +0100 Subject: [PATCH 41/46] test: fix by warping to pass the period --- contracts/test/foundry/KlerosCore.t.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/test/foundry/KlerosCore.t.sol b/contracts/test/foundry/KlerosCore.t.sol index 72a6a565c..3387ca187 100644 --- a/contracts/test/foundry/KlerosCore.t.sol +++ b/contracts/test/foundry/KlerosCore.t.sol @@ -1622,6 +1622,7 @@ contract KlerosCoreTest is Test { } // Check reveal in the next period + vm.warp(block.timestamp + timesPerPeriod[1]); core.passPeriod(disputeID); // Check the require with the wrong choice and then with the wrong salt From 4a451a591b02dc16b98e498fe67c68a65dbb05d7 Mon Sep 17 00:00:00 2001 From: kemuru <102478601+kemuru@users.noreply.github.com> Date: Sat, 16 Aug 2025 12:10:53 +0200 Subject: [PATCH 42/46] chore: tweak didnotvote text --- web/src/components/DisputeView/CardLabels/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/DisputeView/CardLabels/index.tsx b/web/src/components/DisputeView/CardLabels/index.tsx index acc0a3e58..6a37e8a8e 100644 --- a/web/src/components/DisputeView/CardLabels/index.tsx +++ b/web/src/components/DisputeView/CardLabels/index.tsx @@ -65,7 +65,7 @@ const LabelArgs: Record Date: Wed, 20 Aug 2025 23:39:45 +1000 Subject: [PATCH 43/46] feat(RNG): custom errors --- contracts/src/rng/BlockhashRNG.sol | 4 ++-- contracts/src/rng/ChainlinkRNG.sol | 4 ++-- contracts/src/rng/IRNG.sol | 3 +++ contracts/src/rng/RNGWithFallback.sol | 12 +++++++++--- contracts/src/rng/RandomizerRNG.sol | 12 +++++++++--- contracts/test/foundry/KlerosCore.t.sol | 12 ++++++------ 6 files changed, 31 insertions(+), 16 deletions(-) diff --git a/contracts/src/rng/BlockhashRNG.sol b/contracts/src/rng/BlockhashRNG.sol index 9070d0751..a36501e6e 100644 --- a/contracts/src/rng/BlockhashRNG.sol +++ b/contracts/src/rng/BlockhashRNG.sol @@ -26,12 +26,12 @@ contract BlockHashRNG is IRNG { // ************************************* // modifier onlyByGovernor() { - require(governor == msg.sender, "Governor only"); + if (governor != msg.sender) revert GovernorOnly(); _; } modifier onlyByConsumer() { - require(consumer == msg.sender, "Consumer only"); + if (consumer != msg.sender) revert ConsumerOnly(); _; } diff --git a/contracts/src/rng/ChainlinkRNG.sol b/contracts/src/rng/ChainlinkRNG.sol index fe5a9ff19..a5bff291f 100644 --- a/contracts/src/rng/ChainlinkRNG.sol +++ b/contracts/src/rng/ChainlinkRNG.sol @@ -42,12 +42,12 @@ contract ChainlinkRNG is IRNG, VRFConsumerBaseV2Plus { // ************************************* // modifier onlyByGovernor() { - require(governor == msg.sender, "Governor only"); + if (governor != msg.sender) revert GovernorOnly(); _; } modifier onlyByConsumer() { - require(consumer == msg.sender, "Consumer only"); + if (consumer != msg.sender) revert ConsumerOnly(); _; } diff --git a/contracts/src/rng/IRNG.sol b/contracts/src/rng/IRNG.sol index 152c1f15b..1a767fee0 100644 --- a/contracts/src/rng/IRNG.sol +++ b/contracts/src/rng/IRNG.sol @@ -10,4 +10,7 @@ interface IRNG { /// @dev Receive the random number. /// @return randomNumber Random number or 0 if not available function receiveRandomness() external returns (uint256 randomNumber); + + error GovernorOnly(); + error ConsumerOnly(); } diff --git a/contracts/src/rng/RNGWithFallback.sol b/contracts/src/rng/RNGWithFallback.sol index 3bd8daed7..c47f9eb53 100644 --- a/contracts/src/rng/RNGWithFallback.sol +++ b/contracts/src/rng/RNGWithFallback.sol @@ -32,7 +32,7 @@ contract RNGWithFallback is IRNG { /// @param _fallbackTimeoutSeconds Time in seconds to wait before falling back to next RNG /// @param _rng The RNG address (e.g. Chainlink) constructor(address _governor, address _consumer, uint256 _fallbackTimeoutSeconds, IRNG _rng) { - require(address(_rng) != address(0), "Invalid default RNG"); + if (address(_rng) == address(0)) revert InvalidDefaultRNG(); governor = _governor; consumer = _consumer; @@ -45,12 +45,12 @@ contract RNGWithFallback is IRNG { // ************************************* // modifier onlyByGovernor() { - require(msg.sender == governor, "Governor only"); + if (governor != msg.sender) revert GovernorOnly(); _; } modifier onlyByConsumer() { - require(msg.sender == consumer, "Consumer only"); + if (consumer != msg.sender) revert ConsumerOnly(); _; } @@ -100,4 +100,10 @@ contract RNGWithFallback is IRNG { } return randomNumber; } + + // ************************************* // + // * Errors * // + // ************************************* // + + error InvalidDefaultRNG(); } diff --git a/contracts/src/rng/RandomizerRNG.sol b/contracts/src/rng/RandomizerRNG.sol index 6db56fa3b..96cbe6321 100644 --- a/contracts/src/rng/RandomizerRNG.sol +++ b/contracts/src/rng/RandomizerRNG.sol @@ -37,12 +37,12 @@ contract RandomizerRNG is IRNG { // ************************************* // modifier onlyByGovernor() { - require(governor == msg.sender, "Governor only"); + if (governor != msg.sender) revert GovernorOnly(); _; } modifier onlyByConsumer() { - require(consumer == msg.sender, "Consumer only"); + if (consumer != msg.sender) revert ConsumerOnly(); _; } @@ -110,7 +110,7 @@ contract RandomizerRNG is IRNG { /// @param _id The ID of the request. /// @param _value The random value answering the request. function randomizerCallback(uint256 _id, bytes32 _value) external { - require(msg.sender == address(randomizer), "Randomizer only"); + if (msg.sender != address(randomizer)) revert RandomizerOnly(); randomNumbers[_id] = uint256(_value); emit RequestFulfilled(_id, uint256(_value)); } @@ -124,4 +124,10 @@ contract RandomizerRNG is IRNG { function receiveRandomness() external view override returns (uint256 randomNumber) { randomNumber = randomNumbers[lastRequestId]; } + + // ************************************* // + // * Errors * // + // ************************************* // + + error RandomizerOnly(); } diff --git a/contracts/test/foundry/KlerosCore.t.sol b/contracts/test/foundry/KlerosCore.t.sol index 7613fb4c3..d30307106 100644 --- a/contracts/test/foundry/KlerosCore.t.sol +++ b/contracts/test/foundry/KlerosCore.t.sol @@ -12,7 +12,7 @@ import {ISortitionModule} from "../../src/arbitration/interfaces/ISortitionModul import {SortitionModuleMock, SortitionModuleBase} from "../../src/test/SortitionModuleMock.sol"; import {UUPSProxy} from "../../src/proxy/UUPSProxy.sol"; import {BlockHashRNG} from "../../src/rng/BlockHashRNG.sol"; -import {RNGWithFallback} from "../../src/rng/RNGWithFallback.sol"; +import {RNGWithFallback, IRNG} from "../../src/rng/RNGWithFallback.sol"; import {RNGMock} from "../../src/test/RNGMock.sol"; import {PNK} from "../../src/token/PNK.sol"; import {TestERC20} from "../../src/token/TestERC20.sol"; @@ -3067,15 +3067,15 @@ contract KlerosCoreTest is Test { RNGMock rngMock = new RNGMock(); rngFallback = new RNGWithFallback(msg.sender, address(sortitionModule), fallbackTimeout, rngMock); - vm.expectRevert(bytes("Consumer only")); + vm.expectRevert(IRNG.ConsumerOnly.selector); vm.prank(governor); rngFallback.requestRandomness(); - vm.expectRevert(bytes("Consumer only")); + vm.expectRevert(IRNG.ConsumerOnly.selector); vm.prank(governor); rngFallback.receiveRandomness(); - vm.expectRevert(bytes("Governor only")); + vm.expectRevert(IRNG.GovernorOnly.selector); vm.prank(other); rngFallback.changeGovernor(other); vm.prank(governor); @@ -3086,14 +3086,14 @@ contract KlerosCoreTest is Test { vm.prank(other); rngFallback.changeGovernor(governor); - vm.expectRevert(bytes("Governor only")); + vm.expectRevert(IRNG.GovernorOnly.selector); vm.prank(other); rngFallback.changeConsumer(other); vm.prank(governor); rngFallback.changeConsumer(other); assertEq(rngFallback.consumer(), other, "Wrong consumer"); - vm.expectRevert(bytes("Governor only")); + vm.expectRevert(IRNG.GovernorOnly.selector); vm.prank(other); rngFallback.changeFallbackTimeout(5); From 0f31e0ee0876a5ca64708f3e68c1efa7d570f075 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Wed, 20 Aug 2025 19:20:43 +0100 Subject: [PATCH 44/46] chore: changelog --- contracts/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/CHANGELOG.md b/contracts/CHANGELOG.md index 9d4abf080..8afc215c5 100644 --- a/contracts/CHANGELOG.md +++ b/contracts/CHANGELOG.md @@ -12,6 +12,8 @@ The format is based on [Common Changelog](https://common-changelog.org/). - Set the Hardhat Solidity version to v0.8.30 and enable the IR pipeline ([#2069](https://github.com/kleros/kleros-v2/issues/2069)) - Set the Foundry Solidity version to v0.8.30 and enable the IR pipeline ([#2073](https://github.com/kleros/kleros-v2/issues/2073)) - Widen the allowed solc version to any v0.8.x for the interfaces only ([#2083](https://github.com/kleros/kleros-v2/issues/2083)) +- Make `IDisputeKit.getDegreeOfCoherenceReward()` multi-dimensional so different calculations may be applied to PNK rewards, fee rewards and PNK penalties (future-proofing) ([#2090](https://github.com/kleros/kleros-v2/issues/2090)) +- Consolidate the constant `ALPHA_DIVISOR` with `ONE_BASIS_POINTS` ([#2090](https://github.com/kleros/kleros-v2/issues/2090)) - Bump `hardhat` to v2.26.2 ([#2069](https://github.com/kleros/kleros-v2/issues/2069)) - Bump `@kleros/vea-contracts` to v0.7.0 ([#2073](https://github.com/kleros/kleros-v2/issues/2073)) From 78f180a8ac18dcf24b24f52c84e23339d961e174 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Wed, 20 Aug 2025 19:59:00 +0100 Subject: [PATCH 45/46] chore: changelog --- contracts/CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/contracts/CHANGELOG.md b/contracts/CHANGELOG.md index 8afc215c5..07201c2f5 100644 --- a/contracts/CHANGELOG.md +++ b/contracts/CHANGELOG.md @@ -9,6 +9,11 @@ The format is based on [Common Changelog](https://common-changelog.org/). ### Changed - **Breaking:** Replace `require()` with `revert()` and custom errors outside KlerosCore for consistency and smaller bytecode ([#2084](https://github.com/kleros/kleros-v2/issues/2084)) +- **Breaking:** Rename the interface from `RNG` to `IRNG` ([#2054](https://github.com/kleros/kleros-v2/issues/2054)) +- **Breaking:** Remove the `_block` parameter from `IRNG.requestRandomness()` and `IRNG.receiveRandomness()`, not needed for the primary VRF-based RNG ([#2054](https://github.com/kleros/kleros-v2/issues/2054)) +- Make the primary VRF-based RNG fall back to `BlockhashRNG` if the VRF request is not fulfilled within a timeout ([#2054](https://github.com/kleros/kleros-v2/issues/2054)) +- Authenticate the calls to the RNGs to prevent 3rd parties from depleting the Chainlink VRF subscription funds ([#2054](https://github.com/kleros/kleros-v2/issues/2054)) +- Use `block.timestamp` rather than `block.number` for `BlockhashRNG` for better reliability on Arbitrum as block production is sporadic depending on network conditions. ([#2054](https://github.com/kleros/kleros-v2/issues/2054)) - Set the Hardhat Solidity version to v0.8.30 and enable the IR pipeline ([#2069](https://github.com/kleros/kleros-v2/issues/2069)) - Set the Foundry Solidity version to v0.8.30 and enable the IR pipeline ([#2073](https://github.com/kleros/kleros-v2/issues/2073)) - Widen the allowed solc version to any v0.8.x for the interfaces only ([#2083](https://github.com/kleros/kleros-v2/issues/2083)) From 9895c7c1ee1420d4a9ad03eb10ccbc4517e1d1fe Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Wed, 20 Aug 2025 19:45:38 +0100 Subject: [PATCH 46/46] refactor: using the template syntax for hardhat-deploy-ethers getContract() --- .../deploy/00-home-chain-arbitration-neo.ts | 4 ++-- .../deploy/00-home-chain-arbitration-ruler.ts | 2 +- .../00-home-chain-arbitration-university.ts | 4 ++-- contracts/deploy/00-home-chain-arbitration.ts | 2 +- .../change-arbitrable-dispute-template.ts | 2 +- contracts/scripts/disputeRelayerBot.ts | 6 +++--- .../test/arbitration/dispute-kit-gated.ts | 16 +++++++-------- contracts/test/arbitration/draw.ts | 14 ++++++------- contracts/test/arbitration/index.ts | 10 +++++----- contracts/test/arbitration/ruler.ts | 4 ++-- contracts/test/evidence/index.ts | 5 ++--- contracts/test/integration/index.ts | 20 +++++++++---------- contracts/test/proxy/index.ts | 12 +++++------ contracts/test/rng/index.ts | 8 ++++---- 14 files changed, 54 insertions(+), 55 deletions(-) diff --git a/contracts/deploy/00-home-chain-arbitration-neo.ts b/contracts/deploy/00-home-chain-arbitration-neo.ts index 68672b841..f5211c4af 100644 --- a/contracts/deploy/00-home-chain-arbitration-neo.ts +++ b/contracts/deploy/00-home-chain-arbitration-neo.ts @@ -85,7 +85,7 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) }); // nonce+2 (implementation), nonce+3 (proxy) // disputeKit.changeCore() only if necessary - const disputeKitContract = (await hre.ethers.getContract("DisputeKitClassicNeo")) as DisputeKitClassic; + const disputeKitContract = await hre.ethers.getContract("DisputeKitClassicNeo"); const currentCore = await disputeKitContract.core(); if (currentCore !== klerosCore.address) { console.log(`disputeKit.changeCore(${klerosCore.address})`); @@ -99,7 +99,7 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) await rngWithFallback.changeConsumer(sortitionModule.address); } - const core = (await hre.ethers.getContract("KlerosCoreNeo")) as KlerosCoreNeo; + const core = await hre.ethers.getContract("KlerosCoreNeo"); try { await changeCurrencyRate(core, await weth.getAddress(), true, 1, 1); } catch (e) { diff --git a/contracts/deploy/00-home-chain-arbitration-ruler.ts b/contracts/deploy/00-home-chain-arbitration-ruler.ts index d49431c46..5e6e5bdc7 100644 --- a/contracts/deploy/00-home-chain-arbitration-ruler.ts +++ b/contracts/deploy/00-home-chain-arbitration-ruler.ts @@ -35,7 +35,7 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) ], log: true, }); - const core = (await hre.ethers.getContract("KlerosCoreRuler")) as KlerosCoreRuler; + const core = await hre.ethers.getContract("KlerosCoreRuler"); try { await changeCurrencyRate(core, await pnk.getAddress(), true, 12225583, 12); diff --git a/contracts/deploy/00-home-chain-arbitration-university.ts b/contracts/deploy/00-home-chain-arbitration-university.ts index 81267ca91..e3fce871e 100644 --- a/contracts/deploy/00-home-chain-arbitration-university.ts +++ b/contracts/deploy/00-home-chain-arbitration-university.ts @@ -64,14 +64,14 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) }); // nonce+2 (implementation), nonce+3 (proxy) // disputeKit.changeCore() only if necessary - const disputeKitContract = (await ethers.getContract("DisputeKitClassicUniversity")) as DisputeKitClassic; + const disputeKitContract = await ethers.getContract("DisputeKitClassicUniversity"); const currentCore = await disputeKitContract.core(); if (currentCore !== klerosCore.address) { console.log(`disputeKit.changeCore(${klerosCore.address})`); await disputeKitContract.changeCore(klerosCore.address); } - const core = (await hre.ethers.getContract("KlerosCoreUniversity")) as KlerosCoreUniversity; + const core = await hre.ethers.getContract("KlerosCoreUniversity"); try { await changeCurrencyRate(core, await pnk.getAddress(), true, 12225583, 12); await changeCurrencyRate(core, await dai.getAddress(), true, 60327783, 11); diff --git a/contracts/deploy/00-home-chain-arbitration.ts b/contracts/deploy/00-home-chain-arbitration.ts index 6bd763a88..1d590813f 100644 --- a/contracts/deploy/00-home-chain-arbitration.ts +++ b/contracts/deploy/00-home-chain-arbitration.ts @@ -93,7 +93,7 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) await rngWithFallback.changeConsumer(sortitionModule.address); } - const core = (await hre.ethers.getContract("KlerosCore")) as KlerosCore; + const core = await hre.ethers.getContract("KlerosCore"); try { await changeCurrencyRate(core, await pnk.getAddress(), true, 12225583, 12); await changeCurrencyRate(core, await dai.getAddress(), true, 60327783, 11); diff --git a/contracts/deploy/change-arbitrable-dispute-template.ts b/contracts/deploy/change-arbitrable-dispute-template.ts index 0ac04fba8..8b41ce2d2 100644 --- a/contracts/deploy/change-arbitrable-dispute-template.ts +++ b/contracts/deploy/change-arbitrable-dispute-template.ts @@ -31,7 +31,7 @@ const deployResolver: DeployFunction = async (hre: HardhatRuntimeEnvironment) => "specification": "KIP88" }`; - const arbitrable = (await ethers.getContract("ArbitrableExample")) as ArbitrableExample; + const arbitrable = await ethers.getContract("ArbitrableExample"); let tx = await (await arbitrable.changeDisputeTemplate(template, "disputeTemplateMapping: TODO")).wait(); tx?.logs?.forEach((event) => { if (event instanceof EventLog) console.log("event: %O", event.args); diff --git a/contracts/scripts/disputeRelayerBot.ts b/contracts/scripts/disputeRelayerBot.ts index 9437bdf52..87848993a 100644 --- a/contracts/scripts/disputeRelayerBot.ts +++ b/contracts/scripts/disputeRelayerBot.ts @@ -33,9 +33,9 @@ export default async function main( homeGatewayArtifact: string, feeTokenArtifact?: string ) { - const core = (await ethers.getContract("KlerosCore")) as KlerosCore; - const homeGateway = (await ethers.getContract(homeGatewayArtifact)) as HomeGateway; - const feeToken = feeTokenArtifact ? ((await ethers.getContract(feeTokenArtifact)) as TestERC20) : undefined; + const core = await ethers.getContract("KlerosCore"); + const homeGateway = await ethers.getContract(homeGatewayArtifact); + const feeToken = feeTokenArtifact ? await ethers.getContract(feeTokenArtifact) : undefined; const foreignChainProvider = new ethers.providers.JsonRpcProvider(foreignNetwork.url); const foreignGatewayDeployment = await foreignDeployments.get(foreignGatewayArtifact); diff --git a/contracts/test/arbitration/dispute-kit-gated.ts b/contracts/test/arbitration/dispute-kit-gated.ts index aa8c504db..4c3d26052 100644 --- a/contracts/test/arbitration/dispute-kit-gated.ts +++ b/contracts/test/arbitration/dispute-kit-gated.ts @@ -47,11 +47,11 @@ describe("DisputeKitGated", async () => { fallbackToGlobal: true, keepExistingDeployments: false, }); - disputeKitGated = (await ethers.getContract("DisputeKitGated")) as DisputeKitGated; - pnk = (await ethers.getContract("PNK")) as PNK; - dai = (await ethers.getContract("DAI")) as TestERC20; - core = (await ethers.getContract("KlerosCore")) as KlerosCore; - sortitionModule = (await ethers.getContract("SortitionModule")) as SortitionModule; + disputeKitGated = await ethers.getContract("DisputeKitGated"); + pnk = await ethers.getContract("PNK"); + dai = await ethers.getContract("DAI"); + core = await ethers.getContract("KlerosCore"); + sortitionModule = await ethers.getContract("SortitionModule"); // Make the tests more deterministic with this dummy RNG await deployments.deploy("IncrementalNG", { @@ -59,16 +59,16 @@ describe("DisputeKitGated", async () => { args: [RANDOM], log: true, }); - rng = (await ethers.getContract("IncrementalNG")) as IncrementalNG; + rng = await ethers.getContract("IncrementalNG"); await sortitionModule.changeRandomNumberGenerator(rng.target).then((tx) => tx.wait()); const hre = require("hardhat"); await deployERC721(hre, deployer, "TestERC721", "Nft721"); - nft721 = (await ethers.getContract("Nft721")) as TestERC721; + nft721 = await ethers.getContract("Nft721"); await deployERC1155(hre, deployer, "TestERC1155", "Nft1155"); - nft1155 = (await ethers.getContract("Nft1155")) as TestERC1155; + nft1155 = await ethers.getContract("Nft1155"); await nft1155.mint(deployer, TOKEN_ID, 1, "0x00"); }); diff --git a/contracts/test/arbitration/draw.ts b/contracts/test/arbitration/draw.ts index 4d2882a57..4992501f1 100644 --- a/contracts/test/arbitration/draw.ts +++ b/contracts/test/arbitration/draw.ts @@ -62,12 +62,12 @@ describe("Draw Benchmark", async () => { fallbackToGlobal: true, keepExistingDeployments: false, }); - disputeKit = (await ethers.getContract("DisputeKitClassic")) as DisputeKitClassic; - pnk = (await ethers.getContract("PNK")) as PNK; - core = (await ethers.getContract("KlerosCore")) as KlerosCore; - homeGateway = (await ethers.getContract("HomeGatewayToEthereum")) as HomeGateway; - arbitrable = (await ethers.getContract("ArbitrableExample")) as ArbitrableExample; - sortitionModule = (await ethers.getContract("SortitionModule")) as SortitionModule; + disputeKit = await ethers.getContract("DisputeKitClassic"); + pnk = await ethers.getContract("PNK"); + core = await ethers.getContract("KlerosCore"); + homeGateway = await ethers.getContract("HomeGatewayToEthereum"); + arbitrable = await ethers.getContract("ArbitrableExample"); + sortitionModule = await ethers.getContract("SortitionModule"); parentCourtMinStake = await core.courts(Courts.GENERAL).then((court) => court.minStake); @@ -79,7 +79,7 @@ describe("Draw Benchmark", async () => { args: [RANDOM], log: true, }); - rng = (await ethers.getContract("IncrementalNG")) as IncrementalNG; + rng = await ethers.getContract("IncrementalNG"); await sortitionModule.changeRandomNumberGenerator(rng.target).then((tx) => tx.wait()); diff --git a/contracts/test/arbitration/index.ts b/contracts/test/arbitration/index.ts index 4af7dfd63..127fa2500 100644 --- a/contracts/test/arbitration/index.ts +++ b/contracts/test/arbitration/index.ts @@ -100,10 +100,10 @@ async function deployContracts(): Promise< fallbackToGlobal: true, keepExistingDeployments: false, }); - const disputeKit = (await ethers.getContract("DisputeKitClassic")) as DisputeKitClassic; - const disputeKitShutter = (await ethers.getContract("DisputeKitShutter")) as DisputeKitShutter; - const disputeKitGated = (await ethers.getContract("DisputeKitGated")) as DisputeKitGated; - const disputeKitGatedShutter = (await ethers.getContract("DisputeKitGatedShutter")) as DisputeKitGatedShutter; - const core = (await ethers.getContract("KlerosCore")) as KlerosCore; + const disputeKit = await ethers.getContract("DisputeKitClassic"); + const disputeKitShutter = await ethers.getContract("DisputeKitShutter"); + const disputeKitGated = await ethers.getContract("DisputeKitGated"); + const disputeKitGatedShutter = await ethers.getContract("DisputeKitGatedShutter"); + const core = await ethers.getContract("KlerosCore"); return [core, disputeKit, disputeKitShutter, disputeKitGated, disputeKitGatedShutter]; } diff --git a/contracts/test/arbitration/ruler.ts b/contracts/test/arbitration/ruler.ts index bf5754295..dafdb59da 100644 --- a/contracts/test/arbitration/ruler.ts +++ b/contracts/test/arbitration/ruler.ts @@ -163,7 +163,7 @@ async function deployContracts(): Promise<[KlerosCoreRuler, DisputeResolver]> { fallbackToGlobal: true, keepExistingDeployments: false, }); - const resolver = (await ethers.getContract("DisputeResolverRuler")) as DisputeResolver; - const core = (await ethers.getContract("KlerosCoreRuler")) as KlerosCoreRuler; + const resolver = await ethers.getContract("DisputeResolverRuler"); + const core = await ethers.getContract("KlerosCoreRuler"); return [core, resolver]; } diff --git a/contracts/test/evidence/index.ts b/contracts/test/evidence/index.ts index 30d79ab28..bc3f4ff3b 100644 --- a/contracts/test/evidence/index.ts +++ b/contracts/test/evidence/index.ts @@ -19,7 +19,6 @@ function getEmittedEvent(eventName: any, receipt: ContractTransactionReceipt): E describe("Home Evidence contract", async () => { const arbitrationFee = 1000n; - const appealFee = arbitrationFee; const arbitratorExtraData = ethers.AbiCoder.defaultAbiCoder().encode( ["uint256", "uint256"], [1, 1] // courtId 1, minJurors 1 @@ -51,8 +50,8 @@ describe("Home Evidence contract", async () => { fallbackToGlobal: true, keepExistingDeployments: false, }); - arbitrator = (await ethers.getContract("KlerosCore")) as KlerosCore; - disputeTemplateRegistry = (await ethers.getContract("DisputeTemplateRegistry")) as DisputeTemplateRegistry; + arbitrator = await ethers.getContract("KlerosCore"); + disputeTemplateRegistry = await ethers.getContract("DisputeTemplateRegistry"); const court = await arbitrator.courts(1); await arbitrator.changeCourtParameters( diff --git a/contracts/test/integration/index.ts b/contracts/test/integration/index.ts index 5b4b7ea9a..f3c97b2a1 100644 --- a/contracts/test/integration/index.ts +++ b/contracts/test/integration/index.ts @@ -56,16 +56,16 @@ describe("Integration tests", async () => { fallbackToGlobal: true, keepExistingDeployments: false, }); - rng = (await ethers.getContract("ChainlinkRNG")) as ChainlinkRNG; - vrfCoordinator = (await ethers.getContract("ChainlinkVRFCoordinator")) as ChainlinkVRFCoordinatorV2Mock; - disputeKit = (await ethers.getContract("DisputeKitClassic")) as DisputeKitClassic; - pnk = (await ethers.getContract("PNK")) as PNK; - core = (await ethers.getContract("KlerosCore")) as KlerosCore; - vea = (await ethers.getContract("VeaMock")) as VeaMock; - foreignGateway = (await ethers.getContract("ForeignGatewayOnEthereum")) as ForeignGateway; - arbitrable = (await ethers.getContract("ArbitrableExample")) as ArbitrableExample; - homeGateway = (await ethers.getContract("HomeGatewayToEthereum")) as HomeGateway; - sortitionModule = (await ethers.getContract("SortitionModule")) as SortitionModule; + rng = await ethers.getContract("ChainlinkRNG"); + vrfCoordinator = await ethers.getContract("ChainlinkVRFCoordinator"); + disputeKit = await ethers.getContract("DisputeKitClassic"); + pnk = await ethers.getContract("PNK"); + core = await ethers.getContract("KlerosCore"); + vea = await ethers.getContract("VeaMock"); + foreignGateway = await ethers.getContract("ForeignGatewayOnEthereum"); + arbitrable = await ethers.getContract("ArbitrableExample"); + homeGateway = await ethers.getContract("HomeGatewayToEthereum"); + sortitionModule = await ethers.getContract("SortitionModule"); }); it("Resolves a dispute on the home chain with no appeal", async () => { diff --git a/contracts/test/proxy/index.ts b/contracts/test/proxy/index.ts index 410753f97..12ba6313c 100644 --- a/contracts/test/proxy/index.ts +++ b/contracts/test/proxy/index.ts @@ -130,9 +130,9 @@ describe("Upgradability", async () => { }); it("Initializes v1", async () => { - proxy = (await ethers.getContract("UpgradedByRewrite")) as UpgradedByRewriteV1; + proxy = await ethers.getContract("UpgradedByRewrite"); - implementation = (await ethers.getContract("UpgradedByRewrite_Implementation")) as UpgradedByRewriteV1; + implementation = await ethers.getContract("UpgradedByRewrite_Implementation"); expect(await proxy.governor()).to.equal(deployer.address); @@ -156,7 +156,7 @@ describe("Upgradability", async () => { if (!proxyDeployment.implementation) { throw new Error("No implementation address"); } - proxy = (await ethers.getContract("UpgradedByRewrite")) as UpgradedByRewriteV2; + proxy = await ethers.getContract("UpgradedByRewrite"); expect(await proxy.governor()).to.equal(deployer.address); expect(await proxy.counter()).to.equal(3); @@ -184,9 +184,9 @@ describe("Upgradability", async () => { }); it("Initializes v1", async () => { - proxy = (await ethers.getContract("UpgradedByInheritanceV1")) as UpgradedByInheritanceV1; + proxy = await ethers.getContract("UpgradedByInheritanceV1"); - implementation = (await ethers.getContract("UpgradedByInheritanceV1_Implementation")) as UpgradedByInheritanceV1; + implementation = await ethers.getContract("UpgradedByInheritanceV1_Implementation"); expect(await proxy.governor()).to.equal(deployer.address); @@ -209,7 +209,7 @@ describe("Upgradability", async () => { log: true, }); - proxy = (await ethers.getContract("UpgradedByInheritanceV1")) as UpgradedByInheritanceV2; + proxy = await ethers.getContract("UpgradedByInheritanceV1"); expect(await proxy.governor()).to.equal(deployer.address); diff --git a/contracts/test/rng/index.ts b/contracts/test/rng/index.ts index 3bb50fd7c..351a2d460 100644 --- a/contracts/test/rng/index.ts +++ b/contracts/test/rng/index.ts @@ -92,8 +92,8 @@ describe("ChainlinkRNG", async () => { fallbackToGlobal: true, keepExistingDeployments: false, }); - rng = (await ethers.getContract("ChainlinkRNG")) as ChainlinkRNG; - vrfCoordinator = (await ethers.getContract("ChainlinkVRFCoordinator")) as ChainlinkVRFCoordinatorV2Mock; + rng = await ethers.getContract("ChainlinkRNG"); + vrfCoordinator = await ethers.getContract("ChainlinkVRFCoordinator"); await rng.changeConsumer(deployer); }); @@ -155,8 +155,8 @@ describe("RandomizerRNG", async () => { fallbackToGlobal: true, keepExistingDeployments: false, }); - rng = (await ethers.getContract("RandomizerRNG")) as RandomizerRNG; - randomizer = (await ethers.getContract("RandomizerOracle")) as RandomizerMock; + rng = await ethers.getContract("RandomizerRNG"); + randomizer = await ethers.getContract("RandomizerOracle"); await rng.changeConsumer(deployer); });