From f1fe7c9c7160ee700a464cbe54545955fa03d801 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Wed, 23 Jul 2025 01:55:23 +0200 Subject: [PATCH 001/175] feat(web): validation of the token address for ERC20/721/1155 types --- web/src/context/NewDisputeContext.tsx | 1 + web/src/hooks/useTokenAddressValidation.ts | 220 ++++++++++++++++++ .../Resolver/NavigationButtons/NextButton.tsx | 16 +- web/src/pages/Resolver/Parameters/Court.tsx | 149 +++++++++++- 4 files changed, 380 insertions(+), 6 deletions(-) create mode 100644 web/src/hooks/useTokenAddressValidation.ts diff --git a/web/src/context/NewDisputeContext.tsx b/web/src/context/NewDisputeContext.tsx index 52abb696d..5fc109cef 100644 --- a/web/src/context/NewDisputeContext.tsx +++ b/web/src/context/NewDisputeContext.tsx @@ -61,6 +61,7 @@ export interface IGatedDisputeData { isERC1155: boolean; tokenGate: string; tokenId: string; + isTokenGateValid?: boolean | null; // null = not validated, false = invalid, true = valid } // Placeholder diff --git a/web/src/hooks/useTokenAddressValidation.ts b/web/src/hooks/useTokenAddressValidation.ts new file mode 100644 index 000000000..10275025b --- /dev/null +++ b/web/src/hooks/useTokenAddressValidation.ts @@ -0,0 +1,220 @@ +import { useEffect, useState, useMemo } from "react"; + +import { useQuery } from "@tanstack/react-query"; +import { getContract, isAddress } from "viem"; +import { usePublicClient, useChainId } from "wagmi"; + +import { isUndefined } from "utils/index"; + +const ERC1155_ABI = [ + { + inputs: [ + { + internalType: "address", + name: "account", + type: "address", + }, + { + internalType: "uint256", + name: "id", + type: "uint256", + }, + ], + name: "balanceOf", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, +] as const; + +const ERC20_ERC721_ABI = [ + { + inputs: [ + { + internalType: "address", + name: "account", + type: "address", + }, + ], + name: "balanceOf", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, +] as const; + +interface UseTokenValidationParams { + address?: string; + enabled?: boolean; +} + +interface TokenValidationResult { + isValidating: boolean; + isValid: boolean | null; + error: string | null; +} + +/** + * Hook to validate if an address is a valid ERC20 or ERC721 token by attempting to call balanceOf(address) + * @param address The address to validate + * @param enabled Whether validation should be enabled + * @returns Validation state including loading, result, and error + */ +export const useERC20ERC721Validation = ({ + address, + enabled = true, +}: UseTokenValidationParams): TokenValidationResult => { + return useTokenValidation({ + address, + enabled, + abi: ERC20_ERC721_ABI, + contractCall: (contract) => contract.read.balanceOf(["0x0000000000000000000000000000000000000000"]), + tokenType: "ERC-20 or ERC-721", + }); +}; + +/** + * Hook to validate if an address is a valid ERC1155 token by attempting to call balanceOf(address, tokenId) + * @param address The address to validate + * @param enabled Whether validation should be enabled + * @returns Validation state including loading, result, and error + */ +export const useERC1155Validation = ({ address, enabled = true }: UseTokenValidationParams): TokenValidationResult => { + return useTokenValidation({ + address, + enabled, + abi: ERC1155_ABI, + contractCall: (contract) => contract.read.balanceOf(["0x0000000000000000000000000000000000000000", 0]), + tokenType: "ERC-1155", + }); +}; + +/** + * Generic hook for token contract validation + */ +const useTokenValidation = ({ + address, + enabled = true, + abi, + contractCall, + tokenType, +}: UseTokenValidationParams & { + abi: readonly any[]; + contractCall: (contract: any) => Promise; + tokenType: string; +}): TokenValidationResult => { + const publicClient = usePublicClient(); + const chainId = useChainId(); + const [debouncedAddress, setDebouncedAddress] = useState(); + + // Debounce address changes to avoid excessive network calls + useEffect(() => { + const timer = setTimeout(() => { + setDebouncedAddress(address); + }, 500); + + return () => clearTimeout(timer); + }, [address]); + + // Early validation - check format + const isValidFormat = useMemo(() => { + if (!debouncedAddress || debouncedAddress.trim() === "") return null; + return isAddress(debouncedAddress); + }, [debouncedAddress]); + + // Contract validation query + const { + data: isValidContract, + isLoading, + error, + } = useQuery({ + queryKey: [`${tokenType}-validation`, chainId, debouncedAddress], + enabled: + enabled && + !isUndefined(publicClient) && + !isUndefined(debouncedAddress) && + debouncedAddress.trim() !== "" && + isValidFormat === true, + staleTime: 300000, // Cache for 5 minutes + retry: 1, // Only retry once to fail faster + retryDelay: 1000, // Short retry delay + queryFn: async () => { + if (!publicClient || !debouncedAddress) { + throw new Error("Missing required dependencies"); + } + + try { + const contract = getContract({ + address: debouncedAddress as `0x${string}`, + abi, + client: publicClient, + }); + + // Execute the contract call specific to the token type + await contractCall(contract); + + return true; + } catch { + throw new Error(`Address does not implement ${tokenType} interface`); + } + }, + }); + + // Determine final validation state + const isValid = useMemo(() => { + if (!debouncedAddress || debouncedAddress.trim() === "") { + return null; + } + + if (isValidFormat === false) { + return false; + } + + if (isLoading) { + return null; // Still validating + } + + return isValidContract === true; + }, [debouncedAddress, isValidFormat, isLoading, isValidContract]); + + const validationError = useMemo(() => { + if (!debouncedAddress || debouncedAddress.trim() === "") { + return null; + } + + if (isValidFormat === false) { + return "Invalid Ethereum address format"; + } + + if (error) { + const errorMessage = error instanceof Error ? error.message : "Unknown error"; + if (errorMessage.includes("not a contract")) { + return "Address is not a contract"; + } + if (errorMessage.includes(`does not implement ${tokenType}`)) { + return `Not a valid ${tokenType} token address`; + } + return "Network error - please try again"; + } + + return null; + }, [debouncedAddress, isValidFormat, error, tokenType]); + + return { + isValidating: isLoading && enabled && !!debouncedAddress, + isValid, + error: validationError, + }; +}; diff --git a/web/src/pages/Resolver/NavigationButtons/NextButton.tsx b/web/src/pages/Resolver/NavigationButtons/NextButton.tsx index e6d51f8bf..2530281bd 100644 --- a/web/src/pages/Resolver/NavigationButtons/NextButton.tsx +++ b/web/src/pages/Resolver/NavigationButtons/NextButton.tsx @@ -4,7 +4,8 @@ import { useLocation, useNavigate } from "react-router-dom"; import { Button } from "@kleros/ui-components-library"; -import { useNewDisputeContext } from "context/NewDisputeContext"; +import { IGatedDisputeData, useNewDisputeContext } from "context/NewDisputeContext"; + import { isEmpty } from "src/utils"; interface INextButton { @@ -16,6 +17,17 @@ const NextButton: React.FC = ({ nextRoute }) => { const { disputeData, isPolicyUploading } = useNewDisputeContext(); const location = useLocation(); + // Check gated dispute kit validation status + const isGatedTokenValid = React.useMemo(() => { + if (!disputeData.disputeKitData || disputeData.disputeKitData.type !== "gated") return true; + + const gatedData = disputeData.disputeKitData as IGatedDisputeData; + if (!gatedData?.tokenGate?.trim()) return true; // No token address provided, so valid + + // If token address is provided, it must be validated as valid ERC20 + return gatedData.isTokenGateValid === true; + }, [disputeData.disputeKitData]); + //checks if each answer is filled in const areVotingOptionsFilled = disputeData.question !== "" && @@ -29,7 +41,7 @@ const NextButton: React.FC = ({ nextRoute }) => { const isButtonDisabled = (location.pathname.includes("/resolver/title") && !disputeData.title) || (location.pathname.includes("/resolver/description") && !disputeData.description) || - (location.pathname.includes("/resolver/court") && !disputeData.courtId) || + (location.pathname.includes("/resolver/court") && (!disputeData.courtId || !isGatedTokenValid)) || (location.pathname.includes("/resolver/jurors") && !disputeData.arbitrationCost) || (location.pathname.includes("/resolver/voting-options") && !areVotingOptionsFilled) || (location.pathname.includes("/resolver/notable-persons") && !areAliasesValidOrEmpty) || diff --git a/web/src/pages/Resolver/Parameters/Court.tsx b/web/src/pages/Resolver/Parameters/Court.tsx index b82fbb133..805213253 100644 --- a/web/src/pages/Resolver/Parameters/Court.tsx +++ b/web/src/pages/Resolver/Parameters/Court.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from "react"; +import React, { useMemo, useEffect } from "react"; import styled, { css } from "styled-components"; import { AlertMessage, Checkbox, DropdownCascader, DropdownSelect, Field } from "@kleros/ui-components-library"; @@ -7,6 +7,7 @@ import { DisputeKits } from "consts/index"; import { IGatedDisputeData, useNewDisputeContext } from "context/NewDisputeContext"; import { rootCourtToItems, useCourtTree } from "hooks/queries/useCourtTree"; import { useDisputeKitAddressesAll } from "hooks/useDisputeKitAddresses"; +import { useERC20ERC721Validation, useERC1155Validation } from "hooks/useTokenAddressValidation"; import { isUndefined } from "utils/index"; import { useSupportedDisputeKits } from "queries/useSupportedDisputeKits"; @@ -86,6 +87,86 @@ const StyledCheckbox = styled(Checkbox)` )} `; +const ValidationContainer = styled.div` + width: 84vw; + display: flex; + align-items: left; + gap: 8px; + margin-top: 8px; + ${landscapeStyle( + () => css` + width: ${responsiveSize(442, 700, 900)}; + ` + )} +`; + +const ValidationIcon = styled.div<{ $isValid?: boolean | null; $isValidating?: boolean }>` + width: 16px; + height: 16px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + + ${({ $isValidating, $isValid }) => { + if ($isValidating) { + return css` + border: 2px solid #ccc; + border-top-color: #007bff; + animation: spin 1s linear infinite; + + @keyframes spin { + to { + transform: rotate(360deg); + } + } + `; + } + + if ($isValid === true) { + return css` + background-color: #28a745; + color: white; + &::after { + content: "✓"; + } + `; + } + + if ($isValid === false) { + return css` + background-color: #dc3545; + color: white; + &::after { + content: "✗"; + } + `; + } + + return css` + display: none; + `; + }} +`; + +const ValidationMessage = styled.small<{ $isError?: boolean }>` + color: ${({ $isError }) => ($isError ? "#dc3545" : "#28a745")}; + font-size: 14px; + font-style: italic; + font-weight: normal; +`; + +const StyledFieldWithValidation = styled(StyledField)<{ $isValid?: boolean | null }>` + > input { + border-color: ${({ $isValid }) => { + if ($isValid === true) return "#28a745"; + if ($isValid === false) return "#dc3545"; + return "inherit"; + }}; + } +`; + const Court: React.FC = () => { const { disputeData, setDisputeData } = useNewDisputeContext(); const { data: courtTree } = useCourtTree(); @@ -120,6 +201,47 @@ const Court: React.FC = () => { return options?.gated ?? false; }, [disputeKitOptions, selectedDisputeKitId]); + // Token validation for token gate address (conditional based on ERC1155 checkbox) + const tokenGateAddress = (disputeData.disputeKitData as IGatedDisputeData)?.tokenGate ?? ""; + const isERC1155 = (disputeData.disputeKitData as IGatedDisputeData)?.isERC1155 ?? false; + const validationEnabled = isGatedDisputeKit && !!tokenGateAddress.trim(); + + const { + isValidating: isValidatingERC20, + isValid: isValidERC20, + error: validationErrorERC20, + } = useERC20ERC721Validation({ + address: tokenGateAddress, + enabled: validationEnabled && !isERC1155, + }); + + const { + isValidating: isValidatingERC1155, + isValid: isValidERC1155, + error: validationErrorERC1155, + } = useERC1155Validation({ + address: tokenGateAddress, + enabled: validationEnabled && isERC1155, + }); + + // Combine validation results based on token type + const isValidating = isERC1155 ? isValidatingERC1155 : isValidatingERC20; + const isValidToken = isERC1155 ? isValidERC1155 : isValidERC20; + const validationError = isERC1155 ? validationErrorERC1155 : validationErrorERC20; + + // Update validation state in dispute context + useEffect(() => { + if (isGatedDisputeKit && disputeData.disputeKitData) { + const currentData = disputeData.disputeKitData as IGatedDisputeData; + if (currentData.isTokenGateValid !== isValidToken) { + setDisputeData({ + ...disputeData, + disputeKitData: { ...currentData, isTokenGateValid: isValidToken }, + }); + } + } + }, [isValidToken, isGatedDisputeKit, disputeData, setDisputeData]); + const handleCourtChange = (courtId: string) => { if (disputeData.courtId !== courtId) { setDisputeData({ ...disputeData, courtId, disputeKitId: undefined }); @@ -144,7 +266,11 @@ const Court: React.FC = () => { const currentData = disputeData.disputeKitData as IGatedDisputeData; setDisputeData({ ...disputeData, - disputeKitData: { ...currentData, tokenGate: event.target.value }, + disputeKitData: { + ...currentData, + tokenGate: event.target.value, + isTokenGateValid: null, // Reset validation state when address changes + }, }); }; @@ -152,7 +278,11 @@ const Court: React.FC = () => { const currentData = disputeData.disputeKitData as IGatedDisputeData; setDisputeData({ ...disputeData, - disputeKitData: { ...currentData, isERC1155: event.target.checked }, + disputeKitData: { + ...currentData, + isERC1155: event.target.checked, + isTokenGateValid: null, // Reset validation state when token type changes + }, }); }; @@ -187,12 +317,23 @@ const Court: React.FC = () => { )} {isGatedDisputeKit && ( <> - + {tokenGateAddress.trim() !== "" && ( + + + + {isValidating && `Validating ${isERC1155 ? "ERC-1155" : "ERC-20 or ERC-721"} token...`} + {validationError && validationError} + {isValidToken === true && `Valid ${isERC1155 ? "ERC-1155" : "ERC-20 or ERC-721"} token`} + + + )} Date: Tue, 5 Aug 2025 13:23:03 +0100 Subject: [PATCH 002/175] 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 003/175] 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 004/175] 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 005/175] 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 006/175] 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 c99b0c9e702f65f9682956a26ab6baa50fd1730d Mon Sep 17 00:00:00 2001 From: Harman-singh-waraich Date: Tue, 5 Aug 2025 20:32:54 +0530 Subject: [PATCH 007/175] chore: refactors --- web/src/hooks/useTokenAddressValidation.ts | 7 +------ web/src/pages/Resolver/Parameters/Court.tsx | 18 +++++++++--------- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/web/src/hooks/useTokenAddressValidation.ts b/web/src/hooks/useTokenAddressValidation.ts index 10275025b..e3233ef80 100644 --- a/web/src/hooks/useTokenAddressValidation.ts +++ b/web/src/hooks/useTokenAddressValidation.ts @@ -141,12 +141,7 @@ const useTokenValidation = ({ error, } = useQuery({ queryKey: [`${tokenType}-validation`, chainId, debouncedAddress], - enabled: - enabled && - !isUndefined(publicClient) && - !isUndefined(debouncedAddress) && - debouncedAddress.trim() !== "" && - isValidFormat === true, + enabled: enabled && !isUndefined(publicClient) && Boolean(isValidFormat), staleTime: 300000, // Cache for 5 minutes retry: 1, // Only retry once to fail faster retryDelay: 1000, // Short retry delay diff --git a/web/src/pages/Resolver/Parameters/Court.tsx b/web/src/pages/Resolver/Parameters/Court.tsx index 805213253..4e0756c19 100644 --- a/web/src/pages/Resolver/Parameters/Court.tsx +++ b/web/src/pages/Resolver/Parameters/Court.tsx @@ -112,8 +112,8 @@ const ValidationIcon = styled.div<{ $isValid?: boolean | null; $isValidating?: b ${({ $isValidating, $isValid }) => { if ($isValidating) { return css` - border: 2px solid #ccc; - border-top-color: #007bff; + border: 2px solid ${({ theme }) => theme.stroke}; + border-top-color: ${({ theme }) => theme.primaryBlue}; animation: spin 1s linear infinite; @keyframes spin { @@ -126,7 +126,7 @@ const ValidationIcon = styled.div<{ $isValid?: boolean | null; $isValidating?: b if ($isValid === true) { return css` - background-color: #28a745; + background-color: ${({ theme }) => theme.success}; color: white; &::after { content: "✓"; @@ -136,7 +136,7 @@ const ValidationIcon = styled.div<{ $isValid?: boolean | null; $isValidating?: b if ($isValid === false) { return css` - background-color: #dc3545; + background-color: ${({ theme }) => theme.error}; color: white; &::after { content: "✗"; @@ -151,7 +151,7 @@ const ValidationIcon = styled.div<{ $isValid?: boolean | null; $isValidating?: b `; const ValidationMessage = styled.small<{ $isError?: boolean }>` - color: ${({ $isError }) => ($isError ? "#dc3545" : "#28a745")}; + color: ${({ $isError, theme }) => ($isError ? theme.error : theme.success)}; font-size: 14px; font-style: italic; font-weight: normal; @@ -159,9 +159,9 @@ const ValidationMessage = styled.small<{ $isError?: boolean }>` const StyledFieldWithValidation = styled(StyledField)<{ $isValid?: boolean | null }>` > input { - border-color: ${({ $isValid }) => { - if ($isValid === true) return "#28a745"; - if ($isValid === false) return "#dc3545"; + border-color: ${({ $isValid, theme }) => { + if ($isValid === true) return theme.success; + if ($isValid === false) return theme.error; return "inherit"; }}; } @@ -327,7 +327,7 @@ const Court: React.FC = () => { {tokenGateAddress.trim() !== "" && ( - + {isValidating && `Validating ${isERC1155 ? "ERC-1155" : "ERC-20 or ERC-721"} token...`} {validationError && validationError} {isValidToken === true && `Valid ${isERC1155 ? "ERC-1155" : "ERC-20 or ERC-721"} token`} From 7c74b70f7408570a295c85ae207f66017c1ca05a Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Wed, 6 Aug 2025 00:13:57 +0100 Subject: [PATCH 008/175] fix: validation should fail if token gate address is empty --- web/src/pages/Resolver/NavigationButtons/NextButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/pages/Resolver/NavigationButtons/NextButton.tsx b/web/src/pages/Resolver/NavigationButtons/NextButton.tsx index 2530281bd..61d7e9228 100644 --- a/web/src/pages/Resolver/NavigationButtons/NextButton.tsx +++ b/web/src/pages/Resolver/NavigationButtons/NextButton.tsx @@ -22,7 +22,7 @@ const NextButton: React.FC = ({ nextRoute }) => { if (!disputeData.disputeKitData || disputeData.disputeKitData.type !== "gated") return true; const gatedData = disputeData.disputeKitData as IGatedDisputeData; - if (!gatedData?.tokenGate?.trim()) return true; // No token address provided, so valid + if (!gatedData?.tokenGate?.trim()) return false; // No token address provided, so invalid // If token address is provided, it must be validated as valid ERC20 return gatedData.isTokenGateValid === true; From d78e2128efe196213e9ccac10104cbb5322b15cd Mon Sep 17 00:00:00 2001 From: TurbanCoder <51452848+tractorss@users.noreply.github.com> Date: Wed, 6 Aug 2025 13:09:59 +0530 Subject: [PATCH 009/175] Update web/src/pages/Resolver/Parameters/Court.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- web/src/pages/Resolver/Parameters/Court.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/pages/Resolver/Parameters/Court.tsx b/web/src/pages/Resolver/Parameters/Court.tsx index 4e0756c19..b74b22073 100644 --- a/web/src/pages/Resolver/Parameters/Court.tsx +++ b/web/src/pages/Resolver/Parameters/Court.tsx @@ -240,7 +240,7 @@ const Court: React.FC = () => { }); } } - }, [isValidToken, isGatedDisputeKit, disputeData, setDisputeData]); + }, [isValidToken, isGatedDisputeKit, disputeData.disputeKitData, setDisputeData]); const handleCourtChange = (courtId: string) => { if (disputeData.courtId !== courtId) { From 77c85494c78ff3522fb3ebc535c3a562a18a5043 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Thu, 7 Aug 2025 13:21:58 +0100 Subject: [PATCH 010/175] 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 011/175] 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 012/175] 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 013/175] 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 014/175] 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 015/175] 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 016/175] 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 017/175] 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 018/175] 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 019/175] 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 020/175] 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 021/175] 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 022/175] 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 023/175] 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 024/175] 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 025/175] 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 026/175] 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 027/175] 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 028/175] 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 029/175] 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 030/175] 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 52f86ac76bfb5f963756329b2be77335ae728a69 Mon Sep 17 00:00:00 2001 From: Harman-singh-waraich Date: Wed, 13 Aug 2025 15:20:49 +0530 Subject: [PATCH 031/175] fix(web): return-undefined-data-if-not-token-gated --- web/src/utils/extradataToTokenInfo.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/web/src/utils/extradataToTokenInfo.ts b/web/src/utils/extradataToTokenInfo.ts index 7feca7137..f4d3180b0 100644 --- a/web/src/utils/extradataToTokenInfo.ts +++ b/web/src/utils/extradataToTokenInfo.ts @@ -10,16 +10,13 @@ type GatedTokenInfo = { * @dev Decodes token information from encoded extra data. * @param extraData The extraData * @returns GatedTokenInfo object with tokenGate address, isERC1155 flag, and tokenId. + * `undefined` if it's not a gated disputeKit */ -export function extraDataToTokenInfo(extraDataHex: `0x${string}`): GatedTokenInfo { +export function extraDataToTokenInfo(extraDataHex: `0x${string}`): GatedTokenInfo | undefined { const extraDataBytes = hexToBytes(extraDataHex); if (extraDataBytes.length < 160) { - return { - tokenGate: "0x0000000000000000000000000000000000000000", - isERC1155: false, - tokenId: "0", - }; + return; } // Slot 4 (bytes 96–127): packedTokenGateAndFlag From a374113f9eb32fbe7489b46d71083779fe25fe32 Mon Sep 17 00:00:00 2001 From: Harman-singh-waraich Date: Wed, 13 Aug 2025 15:27:50 +0530 Subject: [PATCH 032/175] fix(web): disable-navigation-button-if-dispute-kit-undefined --- web/src/pages/Resolver/NavigationButtons/NextButton.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/src/pages/Resolver/NavigationButtons/NextButton.tsx b/web/src/pages/Resolver/NavigationButtons/NextButton.tsx index 61d7e9228..e54082f44 100644 --- a/web/src/pages/Resolver/NavigationButtons/NextButton.tsx +++ b/web/src/pages/Resolver/NavigationButtons/NextButton.tsx @@ -41,7 +41,8 @@ const NextButton: React.FC = ({ nextRoute }) => { const isButtonDisabled = (location.pathname.includes("/resolver/title") && !disputeData.title) || (location.pathname.includes("/resolver/description") && !disputeData.description) || - (location.pathname.includes("/resolver/court") && (!disputeData.courtId || !isGatedTokenValid)) || + (location.pathname.includes("/resolver/court") && + (!disputeData.courtId || !isGatedTokenValid || !disputeData.disputeKitId)) || (location.pathname.includes("/resolver/jurors") && !disputeData.arbitrationCost) || (location.pathname.includes("/resolver/voting-options") && !areVotingOptionsFilled) || (location.pathname.includes("/resolver/notable-persons") && !areAliasesValidOrEmpty) || From a0cb09d84f622eb31d2fbf39aa817db7e1eda055 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Wed, 13 Aug 2025 19:50:03 +0100 Subject: [PATCH 033/175] 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 034/175] 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 035/175] 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 036/175] 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 037/175] 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 038/175] 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 039/175] 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 040/175] 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 041/175] 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 042/175] 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 043/175] 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 044/175] 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 045/175] 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 046/175] 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 047/175] 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 048/175] 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 049/175] 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 050/175] 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 051/175] 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 052/175] 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); }); From 2f711f34dfc767bea479154964753d8184694782 Mon Sep 17 00:00:00 2001 From: kemuru <102478601+kemuru@users.noreply.github.com> Date: Wed, 20 Aug 2025 21:34:45 +0200 Subject: [PATCH 053/175] feat: add available stake, effectivestake --- web/src/components/NumberDisplay.tsx | 2 +- web/src/pages/Profile/Courts/Header.tsx | 79 ---------- .../CourtCard/CourtName.tsx | 0 .../{Courts => Stakes}/CourtCard/Stake.tsx | 0 .../{Courts => Stakes}/CourtCard/index.tsx | 0 web/src/pages/Profile/Stakes/Header.tsx | 139 ++++++++++++++++++ .../Profile/{Courts => Stakes}/index.tsx | 12 +- web/src/pages/Profile/index.tsx | 13 +- 8 files changed, 156 insertions(+), 89 deletions(-) delete mode 100644 web/src/pages/Profile/Courts/Header.tsx rename web/src/pages/Profile/{Courts => Stakes}/CourtCard/CourtName.tsx (100%) rename web/src/pages/Profile/{Courts => Stakes}/CourtCard/Stake.tsx (100%) rename web/src/pages/Profile/{Courts => Stakes}/CourtCard/index.tsx (100%) create mode 100644 web/src/pages/Profile/Stakes/Header.tsx rename web/src/pages/Profile/{Courts => Stakes}/index.tsx (84%) diff --git a/web/src/components/NumberDisplay.tsx b/web/src/components/NumberDisplay.tsx index fa0951825..d32a0ea23 100644 --- a/web/src/components/NumberDisplay.tsx +++ b/web/src/components/NumberDisplay.tsx @@ -35,7 +35,7 @@ const NumberDisplay: React.FC = ({ }) => { const parsedValue = Number(value); const formattedValue = commify(getFormattedValue(parsedValue, decimals)); - const tooltipValue = isCurrency ? `${unit} ${value}` : `${value} ${unit}`; + const tooltipValue = isCurrency ? `${unit} ${commify(value)}` : `${commify(value)} ${unit}`; const displayUnit = showUnitInDisplay ? unit : ""; const displayValue = isCurrency ? `${displayUnit} ${formattedValue}` : `${formattedValue} ${displayUnit}`; diff --git a/web/src/pages/Profile/Courts/Header.tsx b/web/src/pages/Profile/Courts/Header.tsx deleted file mode 100644 index 07f61e11c..000000000 --- a/web/src/pages/Profile/Courts/Header.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import React from "react"; -import styled, { css } from "styled-components"; - -import { formatUnits } from "viem"; -import { useSearchParams } from "react-router-dom"; - -import LockerIcon from "svgs/icons/locker.svg"; - -import { isUndefined } from "utils/index"; - -import { landscapeStyle } from "styles/landscapeStyle"; -import { responsiveSize } from "styles/responsiveSize"; - -import NumberDisplay from "components/NumberDisplay"; - -const Container = styled.div` - display: flex; - flex-direction: row; - flex-wrap: wrap; - width: 100%; - gap: 4px 16px; - align-items: center; - margin-bottom: ${responsiveSize(16, 24)}; - - ${landscapeStyle( - () => css` - justify-content: space-between; - ` - )} -`; - -const LockedPnk = styled.div` - display: flex; - flex-wrap: nowrap; - gap: 8px; - justify-content: flex-start; - - ${landscapeStyle( - () => css` - align-self: center; - ` - )} -`; - -const StyledTitle = styled.h1` - margin-bottom: 0; - font-size: ${responsiveSize(20, 24)}; -`; - -const StyledLockerIcon = styled(LockerIcon)` - fill: ${({ theme }) => theme.secondaryPurple}; - width: 14px; -`; - -interface IHeader { - lockedStake: bigint; -} - -const Header: React.FC = ({ lockedStake }) => { - const formattedLockedStake = !isUndefined(lockedStake) && formatUnits(lockedStake, 18); - const [searchParams] = useSearchParams(); - const searchParamAddress = searchParams.get("address")?.toLowerCase(); - - return ( - - {searchParamAddress ? "Their" : "My"} Courts - {!isUndefined(lockedStake) ? ( - - - - - - - - ) : null} - - ); -}; -export default Header; diff --git a/web/src/pages/Profile/Courts/CourtCard/CourtName.tsx b/web/src/pages/Profile/Stakes/CourtCard/CourtName.tsx similarity index 100% rename from web/src/pages/Profile/Courts/CourtCard/CourtName.tsx rename to web/src/pages/Profile/Stakes/CourtCard/CourtName.tsx diff --git a/web/src/pages/Profile/Courts/CourtCard/Stake.tsx b/web/src/pages/Profile/Stakes/CourtCard/Stake.tsx similarity index 100% rename from web/src/pages/Profile/Courts/CourtCard/Stake.tsx rename to web/src/pages/Profile/Stakes/CourtCard/Stake.tsx diff --git a/web/src/pages/Profile/Courts/CourtCard/index.tsx b/web/src/pages/Profile/Stakes/CourtCard/index.tsx similarity index 100% rename from web/src/pages/Profile/Courts/CourtCard/index.tsx rename to web/src/pages/Profile/Stakes/CourtCard/index.tsx diff --git a/web/src/pages/Profile/Stakes/Header.tsx b/web/src/pages/Profile/Stakes/Header.tsx new file mode 100644 index 000000000..811ab0e1f --- /dev/null +++ b/web/src/pages/Profile/Stakes/Header.tsx @@ -0,0 +1,139 @@ +import React from "react"; +import styled, { css } from "styled-components"; + +import { useSearchParams } from "react-router-dom"; +import { formatUnits } from "viem"; + +import LockerIcon from "svgs/icons/locker.svg"; +import PnkIcon from "svgs/icons/pnk.svg"; + +import { isUndefined } from "utils/index"; + +import { landscapeStyle } from "styles/landscapeStyle"; +import { responsiveSize } from "styles/responsiveSize"; + +import NumberDisplay from "components/NumberDisplay"; + +const Container = styled.div` + display: flex; + flex-direction: row; + flex-wrap: wrap; + width: 100%; + gap: 4px 16px; + align-items: center; + margin-bottom: ${responsiveSize(16, 24)}; + + ${landscapeStyle( + () => css` + justify-content: space-between; + ` + )} +`; + +const LockedPnk = styled.div` + display: flex; + flex-wrap: nowrap; + gap: 8px; + align-items: center; + justify-content: center; +`; + +const StakesGroup = styled.div` + display: flex; + gap: 12px 24px; + align-items: center; + flex-wrap: wrap; +`; + +const AvailablePnk = styled.div` + display: flex; + flex-wrap: nowrap; + gap: 8px; + align-items: center; + justify-content: center; +`; + +const EffectivePnk = styled.div` + display: flex; + flex-wrap: nowrap; + gap: 8px; + align-items: center; + justify-content: center; +`; + +const StyledTitle = styled.h1` + margin-bottom: 0; + font-size: ${responsiveSize(20, 24)}; +`; + +const StyledLockerIcon = styled(LockerIcon)` + fill: ${({ theme }) => theme.secondaryPurple}; + width: 14px; + height: 14px; + margin-bottom: 1px; +`; + +const StyledPnkIcon = styled(PnkIcon)` + fill: ${({ theme }) => theme.secondaryPurple}; + width: 14px; + height: 14px; + margin-bottom: 1px; +`; + +const StyledEffectivePnkIcon = styled(PnkIcon)` + fill: ${({ theme }) => theme.secondaryPurple}; + width: 14px; + height: 14px; + margin-bottom: 1px; +`; + +interface IHeader { + availableStake?: bigint; + lockedStake?: bigint; + effectiveStake?: bigint; +} + +const Header: React.FC = ({ availableStake, lockedStake, effectiveStake }) => { + const formattedAvailableStake = !isUndefined(availableStake) && formatUnits(availableStake, 18); + const formattedLockedStake = !isUndefined(lockedStake) && formatUnits(lockedStake, 18); + const formattedEffectiveStake = !isUndefined(effectiveStake) && formatUnits(effectiveStake, 18); + const [searchParams] = useSearchParams(); + const searchParamAddress = searchParams.get("address")?.toLowerCase(); + + return ( + + {searchParamAddress ? "Their" : "My"} Stakes + + {!isUndefined(availableStake) ? ( + + + + + + + + ) : null} + {!isUndefined(effectiveStake) ? ( + + + + + + + + ) : null} + {!isUndefined(lockedStake) ? ( + + + + + + + + ) : null} + + + ); +}; + +export default Header; diff --git a/web/src/pages/Profile/Courts/index.tsx b/web/src/pages/Profile/Stakes/index.tsx similarity index 84% rename from web/src/pages/Profile/Courts/index.tsx rename to web/src/pages/Profile/Stakes/index.tsx index 512478b1d..8df01bb0b 100644 --- a/web/src/pages/Profile/Courts/index.tsx +++ b/web/src/pages/Profile/Stakes/index.tsx @@ -35,11 +35,11 @@ const StyledLabel = styled.label` font-size: ${responsiveSize(14, 16)}; `; -interface ICourts { +interface IStakes { addressToQuery: `0x${string}`; } -const Courts: React.FC = ({ addressToQuery }) => { +const Stakes: React.FC = ({ addressToQuery }) => { const { data: stakeData, isLoading } = useJurorStakeDetailsQuery(addressToQuery); const { data: jurorBalance } = useReadSortitionModuleGetJurorBalance({ args: [addressToQuery, BigInt(1)], @@ -48,11 +48,15 @@ const Courts: React.FC = ({ addressToQuery }) => { const searchParamAddress = searchParams.get("address")?.toLowerCase(); const stakedCourts = stakeData?.jurorTokensPerCourts?.filter(({ staked }) => staked > 0); const isStaked = stakedCourts && stakedCourts.length > 0; + const availableStake = jurorBalance?.[0]; const lockedStake = jurorBalance?.[1]; + const effectiveStake = stakeData?.jurorTokensPerCourts?.[0]?.effectiveStake + ? BigInt(stakeData.jurorTokensPerCourts[0].effectiveStake) + : undefined; return ( -
+
{isLoading ? : null} {!isStaked && !isLoading ? ( {searchParamAddress ? "They" : "You"} are not staked in any court @@ -70,4 +74,4 @@ const Courts: React.FC = ({ addressToQuery }) => { ); }; -export default Courts; +export default Stakes; diff --git a/web/src/pages/Profile/index.tsx b/web/src/pages/Profile/index.tsx index 109046748..b52327eda 100644 --- a/web/src/pages/Profile/index.tsx +++ b/web/src/pages/Profile/index.tsx @@ -1,24 +1,27 @@ import React, { useMemo } from "react"; - import styled, { css } from "styled-components"; -import { MAX_WIDTH_LANDSCAPE, landscapeStyle } from "styles/landscapeStyle"; -import { responsiveSize } from "styles/responsiveSize"; import { useNavigate, useParams, useSearchParams } from "react-router-dom"; import { useAccount } from "wagmi"; import { isUndefined } from "utils/index"; import { decodeURIFilter, useRootPath } from "utils/uri"; + import { DisputeDetailsFragment, useMyCasesQuery } from "queries/useCasesQuery"; import { useUserQuery } from "queries/useUser"; + import { Dispute_Filter, OrderDirection, UserDetailsFragment } from "src/graphql/graphql"; +import { MAX_WIDTH_LANDSCAPE, landscapeStyle } from "styles/landscapeStyle"; +import { responsiveSize } from "styles/responsiveSize"; + import CasesDisplay from "components/CasesDisplay"; import ConnectWallet from "components/ConnectWallet"; import FavoriteCases from "components/FavoriteCases"; import ScrollTop from "components/ScrollTop"; -import Courts from "./Courts"; + import JurorInfo from "./JurorInfo"; +import Stakes from "./Stakes"; const Container = styled.div` width: 100%; @@ -110,7 +113,7 @@ const Profile: React.FC = () => { {isConnected || searchParamAddress ? ( <> - + Date: Wed, 20 Aug 2025 21:15:26 +0100 Subject: [PATCH 054/175] feat: automatically stake PNK rewards instead of transferring them --- contracts/src/arbitration/KlerosCoreBase.sol | 11 ++++-- .../src/arbitration/SortitionModuleBase.sol | 24 +++++++++++++ .../interfaces/ISortitionModule.sol | 2 ++ .../university/KlerosCoreUniversity.sol | 11 ++++-- .../university/SortitionModuleUniversity.sol | 34 +++++++++++++++++++ contracts/test/foundry/KlerosCore.t.sol | 4 +-- 6 files changed, 80 insertions(+), 6 deletions(-) diff --git a/contracts/src/arbitration/KlerosCoreBase.sol b/contracts/src/arbitration/KlerosCoreBase.sol index 3164e7245..5816f872a 100644 --- a/contracts/src/arbitration/KlerosCoreBase.sol +++ b/contracts/src/arbitration/KlerosCoreBase.sol @@ -846,13 +846,20 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable // Release the rest of the PNKs of the juror for this round. sortitionModule.unlockStake(account, pnkLocked); - // Transfer the rewards + // Compute the rewards uint256 pnkReward = _applyCoherence(_params.pnkPenaltiesInRound / _params.coherentCount, pnkCoherence); round.sumPnkRewardPaid += pnkReward; uint256 feeReward = _applyCoherence(round.totalFeesForJurors / _params.coherentCount, feeCoherence); round.sumFeeRewardPaid += feeReward; - pinakion.safeTransfer(account, pnkReward); + + // Transfer the fee reward _transferFeeToken(round.feeToken, payable(account), feeReward); + + // Stake the PNK reward if possible, by-passes delayed stakes and other checks usually done by validateStake() + if (!sortitionModule.setStakeReward(account, dispute.courtID, pnkReward)) { + pinakion.safeTransfer(account, pnkReward); + } + emit TokenAndETHShift( account, _params.disputeID, diff --git a/contracts/src/arbitration/SortitionModuleBase.sol b/contracts/src/arbitration/SortitionModuleBase.sol index 2ad7b89b2..af4eb6631 100644 --- a/contracts/src/arbitration/SortitionModuleBase.sol +++ b/contracts/src/arbitration/SortitionModuleBase.sol @@ -306,6 +306,30 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr _setStake(_account, _courtID, _pnkDeposit, _pnkWithdrawal, _newStake); } + /// @dev Update the state of the stakes with a PNK reward deposit, called by KC during rewards execution. + /// `O(n + p * log_k(j))` where + /// `n` is the number of courts the juror has staked in, + /// `p` is the depth of the court tree, + /// `k` is the minimum number of children per node of one of these courts' sortition sum tree, + /// and `j` is the maximum number of jurors that ever staked in one of these courts simultaneously. + /// @param _account The address of the juror. + /// @param _courtID The ID of the court. + /// @param _reward The amount of PNK to be deposited as a reward. + function setStakeReward( + address _account, + uint96 _courtID, + uint256 _reward + ) external override onlyByCore returns (bool success) { + if (_reward == 0) return true; // No reward to add. + + uint256 currentStake = stakeOf(_account, _courtID); + if (currentStake == 0) return false; // Juror has been unstaked, don't increase their stake. + + uint256 newStake = currentStake + _reward; + _setStake(_account, _courtID, _reward, 0, newStake); + return true; + } + function _setStake( address _account, uint96 _courtID, diff --git a/contracts/src/arbitration/interfaces/ISortitionModule.sol b/contracts/src/arbitration/interfaces/ISortitionModule.sol index 5cf10e6ae..a51fadae9 100644 --- a/contracts/src/arbitration/interfaces/ISortitionModule.sol +++ b/contracts/src/arbitration/interfaces/ISortitionModule.sol @@ -29,6 +29,8 @@ interface ISortitionModule { uint256 _newStake ) external; + function setStakeReward(address _account, uint96 _courtID, uint256 _reward) external returns (bool success); + function setJurorInactive(address _account) external; function lockStake(address _account, uint256 _relativeAmount) external; diff --git a/contracts/src/arbitration/university/KlerosCoreUniversity.sol b/contracts/src/arbitration/university/KlerosCoreUniversity.sol index 5744088b0..0cd11b2f1 100644 --- a/contracts/src/arbitration/university/KlerosCoreUniversity.sol +++ b/contracts/src/arbitration/university/KlerosCoreUniversity.sol @@ -840,12 +840,13 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { // Release the rest of the PNKs of the juror for this round. sortitionModule.unlockStake(account, pnkLocked); - // Transfer the rewards + // Compute the rewards uint256 pnkReward = ((_params.pnkPenaltiesInRound / _params.coherentCount) * pnkCoherence) / ONE_BASIS_POINT; round.sumPnkRewardPaid += pnkReward; uint256 feeReward = ((round.totalFeesForJurors / _params.coherentCount) * feeCoherence) / ONE_BASIS_POINT; round.sumFeeRewardPaid += feeReward; - pinakion.safeTransfer(account, pnkReward); + + // Transfer the fee reward if (round.feeToken == NATIVE_CURRENCY) { // The dispute fees were paid in ETH payable(account).send(feeReward); @@ -853,6 +854,12 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { // The dispute fees were paid in ERC20 round.feeToken.safeTransfer(account, feeReward); } + + // Stake the PNK reward if possible, by-passes delayed stakes and other checks usually done by validateStake() + if (!sortitionModule.setStakeReward(account, dispute.courtID, pnkReward)) { + pinakion.safeTransfer(account, pnkReward); + } + emit TokenAndETHShift( account, _params.disputeID, diff --git a/contracts/src/arbitration/university/SortitionModuleUniversity.sol b/contracts/src/arbitration/university/SortitionModuleUniversity.sol index db61958fd..e32ca5a77 100644 --- a/contracts/src/arbitration/university/SortitionModuleUniversity.sol +++ b/contracts/src/arbitration/university/SortitionModuleUniversity.sol @@ -190,6 +190,40 @@ contract SortitionModuleUniversity is ISortitionModuleUniversity, UUPSProxiable, uint256 _pnkWithdrawal, uint256 _newStake ) external override onlyByCore { + _setStake(_account, _courtID, _pnkDeposit, _pnkWithdrawal, _newStake); + } + + /// @dev Update the state of the stakes with a PNK reward deposit, called by KC during rewards execution. + /// `O(n + p * log_k(j))` where + /// `n` is the number of courts the juror has staked in, + /// `p` is the depth of the court tree, + /// `k` is the minimum number of children per node of one of these courts' sortition sum tree, + /// and `j` is the maximum number of jurors that ever staked in one of these courts simultaneously. + /// @param _account The address of the juror. + /// @param _courtID The ID of the court. + /// @param _reward The amount of PNK to be deposited as a reward. + function setStakeReward( + address _account, + uint96 _courtID, + uint256 _reward + ) external override onlyByCore returns (bool success) { + if (_reward == 0) return true; // No reward to add. + + uint256 currentStake = _stakeOf(_account, _courtID); + if (currentStake == 0) return false; // Juror has been unstaked, don't increase their stake. + + uint256 newStake = currentStake + _reward; + _setStake(_account, _courtID, _reward, 0, newStake); + return true; + } + + function _setStake( + address _account, + uint96 _courtID, + uint256 _pnkDeposit, + uint256 _pnkWithdrawal, + uint256 _newStake + ) internal { Juror storage juror = jurors[_account]; uint256 currentStake = _stakeOf(_account, _courtID); if (_pnkDeposit > 0) { diff --git a/contracts/test/foundry/KlerosCore.t.sol b/contracts/test/foundry/KlerosCore.t.sol index d0c3c190b..62a0b4de1 100644 --- a/contracts/test/foundry/KlerosCore.t.sol +++ b/contracts/test/foundry/KlerosCore.t.sol @@ -2397,9 +2397,9 @@ contract KlerosCoreTest is Test { assertEq(staker1.balance, 0, "Wrong balance of the staker1"); assertEq(staker2.balance, 0.09 ether, "Wrong balance of the staker2"); - assertEq(pinakion.balanceOf(address(core)), 20500, "Wrong token balance of the core"); // Was 21500. 1000 was transferred to staker2 + assertEq(pinakion.balanceOf(address(core)), 21500, "Wrong token balance of the core"); // Was 21500. 1000 was transferred to staker2 assertEq(pinakion.balanceOf(staker1), 999999999999998500, "Wrong token balance of staker1"); - assertEq(pinakion.balanceOf(staker2), 999999999999981000, "Wrong token balance of staker2"); // 20k stake and 1k added as a reward, thus -19k from the default + assertEq(pinakion.balanceOf(staker2), 999999999999980000, "Wrong token balance of staker2"); // 20k stake and 1k added as a reward, thus -19k from the default } function test_execute_NoCoherence() public { From e6572d58b4c61b19af7c767c7d484d115243525a Mon Sep 17 00:00:00 2001 From: Harman-singh-waraich Date: Thu, 21 Aug 2025 15:03:37 +0530 Subject: [PATCH 055/175] chore: explicit-dispute-kit-selection --- web/src/pages/Resolver/Parameters/Court.tsx | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/web/src/pages/Resolver/Parameters/Court.tsx b/web/src/pages/Resolver/Parameters/Court.tsx index b74b22073..df4767f9d 100644 --- a/web/src/pages/Resolver/Parameters/Court.tsx +++ b/web/src/pages/Resolver/Parameters/Court.tsx @@ -187,19 +187,10 @@ const Court: React.FC = () => { ); }, [supportedDisputeKits, availableDisputeKits]); - const selectedDisputeKitId = useMemo(() => { - // If there's only 1 supported dispute kit, select it by default - if (disputeKitOptions.length === 1) { - return disputeKitOptions[0].value; - } - // If there's no saved selection, select nothing - return disputeData.disputeKitId ?? -1; - }, [disputeKitOptions, disputeData.disputeKitId]); - const isGatedDisputeKit = useMemo(() => { - const options = disputeKitOptions.find((dk) => String(dk.value) === String(selectedDisputeKitId)); + const options = disputeKitOptions.find((dk) => String(dk.value) === String(disputeData.disputeKitId)); return options?.gated ?? false; - }, [disputeKitOptions, selectedDisputeKitId]); + }, [disputeKitOptions, disputeData.disputeKitId]); // Token validation for token gate address (conditional based on ERC1155 checkbox) const tokenGateAddress = (disputeData.disputeKitData as IGatedDisputeData)?.tokenGate ?? ""; @@ -311,7 +302,7 @@ const Court: React.FC = () => { )} From 8f4993d6e74e9f3ae215e7735262fbdae1b32ac2 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Fri, 22 Aug 2025 02:27:03 +0100 Subject: [PATCH 056/175] fix: validate the token address by querying a non-zero address due to OZ reverting on it --- web/src/hooks/useTokenAddressValidation.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/web/src/hooks/useTokenAddressValidation.ts b/web/src/hooks/useTokenAddressValidation.ts index e3233ef80..bb18addf0 100644 --- a/web/src/hooks/useTokenAddressValidation.ts +++ b/web/src/hooks/useTokenAddressValidation.ts @@ -76,11 +76,12 @@ export const useERC20ERC721Validation = ({ address, enabled = true, }: UseTokenValidationParams): TokenValidationResult => { + // We query the balance for a random non-zero address because many implementations revert on it return useTokenValidation({ address, enabled, abi: ERC20_ERC721_ABI, - contractCall: (contract) => contract.read.balanceOf(["0x0000000000000000000000000000000000000000"]), + contractCall: (contract) => contract.read.balanceOf(["0x0000000000000000000000000000000000001234"]), tokenType: "ERC-20 or ERC-721", }); }; @@ -92,11 +93,12 @@ export const useERC20ERC721Validation = ({ * @returns Validation state including loading, result, and error */ export const useERC1155Validation = ({ address, enabled = true }: UseTokenValidationParams): TokenValidationResult => { + // We query the balance for a random non-zero address because many implementations revert on it return useTokenValidation({ address, enabled, abi: ERC1155_ABI, - contractCall: (contract) => contract.read.balanceOf(["0x0000000000000000000000000000000000000000", 0]), + contractCall: (contract) => contract.read.balanceOf(["0x0000000000000000000000000000000000001234", 0]), tokenType: "ERC-1155", }); }; From f2b21838698db2032707f57430c33db39f83a852 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Sat, 23 Aug 2025 04:52:57 +0100 Subject: [PATCH 057/175] fix: penalties now applied to sortition tree stakes --- contracts/hardhat.config.ts | 2 +- contracts/src/arbitration/KlerosCoreBase.sol | 11 +- .../src/arbitration/SortitionModuleBase.sol | 142 ++++++++++++++---- .../dispute-kits/DisputeKitClassicBase.sol | 6 +- .../arbitration/interfaces/IDisputeKit.sol | 5 +- .../interfaces/ISortitionModule.sol | 17 ++- .../university/KlerosCoreUniversity.sol | 11 +- .../university/SortitionModuleUniversity.sol | 57 ++++--- .../test/arbitration/dispute-kit-gated.ts | 2 +- contracts/test/foundry/KlerosCore.t.sol | 4 +- 10 files changed, 190 insertions(+), 67 deletions(-) diff --git a/contracts/hardhat.config.ts b/contracts/hardhat.config.ts index dc1cce1c6..3bb2787ca 100644 --- a/contracts/hardhat.config.ts +++ b/contracts/hardhat.config.ts @@ -31,7 +31,7 @@ const config: HardhatUserConfig = { viaIR: process.env.VIA_IR !== "false", // Defaults to true optimizer: { enabled: true, - runs: 10000, + runs: 2000, }, outputSelection: { "*": { diff --git a/contracts/src/arbitration/KlerosCoreBase.sol b/contracts/src/arbitration/KlerosCoreBase.sol index 3164e7245..056d49fd5 100644 --- a/contracts/src/arbitration/KlerosCoreBase.sol +++ b/contracts/src/arbitration/KlerosCoreBase.sol @@ -60,6 +60,7 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable uint256 repartitions; // A counter of reward repartitions made in this round. uint256 pnkPenalties; // The amount of PNKs collected from penalties in this round. address[] drawnJurors; // Addresses of the jurors that were drawn in this round. + uint96[] drawnJurorFromCourtIDs; // The courtIDs where the juror was drawn from, possibly their stake in a subcourt. uint256 sumFeeRewardPaid; // Total sum of arbitration fees paid to coherent jurors as a reward in this round. uint256 sumPnkRewardPaid; // Total sum of PNK paid to coherent jurors as a reward in this round. IERC20 feeToken; // The token used for paying fees in this round. @@ -610,13 +611,14 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable uint256 startIndex = round.drawIterations; // for gas: less storage reads uint256 i; while (i < _iterations && round.drawnJurors.length < round.nbVotes) { - address drawnAddress = disputeKit.draw(_disputeID, startIndex + i++); + (address drawnAddress, uint96 fromSubcourtID) = disputeKit.draw(_disputeID, startIndex + i++); if (drawnAddress == address(0)) { continue; } sortitionModule.lockStake(drawnAddress, round.pnkAtStakePerJuror); emit Draw(drawnAddress, _disputeID, currentRound, round.drawnJurors.length); round.drawnJurors.push(drawnAddress); + round.drawnJurorFromCourtIDs.push(fromSubcourtID != 0 ? fromSubcourtID : dispute.courtID); if (round.drawnJurors.length == round.nbVotes) { sortitionModule.postDrawHook(_disputeID, currentRound); } @@ -786,7 +788,12 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable sortitionModule.unlockStake(account, penalty); // Apply the penalty to the staked PNKs. - (uint256 pnkBalance, uint256 availablePenalty) = sortitionModule.penalizeStake(account, penalty); + uint96 penalizedInCourtID = round.drawnJurorFromCourtIDs[_params.repartition]; + (uint256 pnkBalance, uint256 availablePenalty) = sortitionModule.setStakePenalty( + account, + penalizedInCourtID, + penalty + ); _params.pnkPenaltiesInRound += availablePenalty; emit TokenAndETHShift( account, diff --git a/contracts/src/arbitration/SortitionModuleBase.sol b/contracts/src/arbitration/SortitionModuleBase.sol index 2ad7b89b2..9dc938de2 100644 --- a/contracts/src/arbitration/SortitionModuleBase.sol +++ b/contracts/src/arbitration/SortitionModuleBase.sol @@ -17,14 +17,20 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr // * Enums / Structs * // // ************************************* // + struct SubCourtStakes { + uint256 totalStakedInCourts; + uint96[MAX_STAKE_PATHS] courtIDs; + uint256[MAX_STAKE_PATHS] stakedInCourts; + } + struct SortitionSumTree { uint256 K; // The maximum number of children per node. - // We use this to keep track of vacant positions in the tree after removing a leaf. This is for keeping the tree as balanced as possible without spending gas on moving nodes around. - uint256[] stack; - uint256[] nodes; + uint256[] stack; // We use this to keep track of vacant positions in the tree after removing a leaf. This is for keeping the tree as balanced as possible without spending gas on moving nodes around. + uint256[] nodes; // The tree nodes. // Two-way mapping of IDs to node indexes. Note that node index 0 is reserved for the root node, and means the ID does not have a node. - mapping(bytes32 => uint256) IDsToNodeIndexes; - mapping(uint256 => bytes32) nodeIndexesToIDs; + mapping(bytes32 stakePathID => uint256 nodeIndex) IDsToNodeIndexes; + mapping(uint256 nodeIndex => bytes32 stakePathID) nodeIndexesToIDs; + mapping(bytes32 stakePathID => SubCourtStakes subcourtStakes) IDsToSubCourtStakes; } struct DelayedStake { @@ -36,7 +42,7 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr struct Juror { uint96[] courtIDs; // The IDs of courts where the juror's stake path ends. A stake path is a path from the general court to a court the juror directly staked in using `_setStake`. - uint256 stakedPnk; // The juror's total amount of tokens staked in subcourts. Reflects actual pnk balance. + uint256 stakedPnk; // The juror's total amount of tokens staked in subcourts. PNK balance including locked PNK and penalty deductions. uint256 lockedPnk; // The juror's total amount of tokens locked in disputes. } @@ -306,6 +312,28 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr _setStake(_account, _courtID, _pnkDeposit, _pnkWithdrawal, _newStake); } + function setStakePenalty( + address _account, + uint96 _courtID, + uint256 _penalty + ) external override onlyByCore returns (uint256 pnkBalance, uint256 availablePenalty) { + Juror storage juror = jurors[_account]; + availablePenalty = _penalty; + if (juror.stakedPnk < _penalty) { + availablePenalty = juror.stakedPnk; + } + + if (availablePenalty == 0) return (juror.stakedPnk, 0); // No penalty to apply. + + uint256 currentStake = stakeOf(_account, _courtID); + uint256 newStake = 0; + if (currentStake >= availablePenalty) { + newStake = currentStake - availablePenalty; + } + _setStake(_account, _courtID, 0, availablePenalty, newStake); + pnkBalance = juror.stakedPnk; // updated by _setStake() + } + function _setStake( address _account, uint96 _courtID, @@ -339,12 +367,14 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr bytes32 stakePathID = _accountAndCourtIDToStakePathID(_account, _courtID); bool finished = false; uint96 currentCourtID = _courtID; + uint96 fromSubCourtID = 0; // 0 means it is not coming from a subcourt. while (!finished) { // Tokens are also implicitly staked in parent courts through sortition module to increase the chance of being drawn. - _set(bytes32(uint256(currentCourtID)), _newStake, stakePathID); + _set(bytes32(uint256(currentCourtID)), _newStake, stakePathID, fromSubCourtID); if (currentCourtID == GENERAL_COURT) { finished = true; } else { + fromSubCourtID = currentCourtID; (currentCourtID, , , , , , ) = core.courts(currentCourtID); // Get the parent court. } } @@ -367,25 +397,6 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr } } - function penalizeStake( - address _account, - uint256 _relativeAmount - ) external override onlyByCore returns (uint256 pnkBalance, uint256 availablePenalty) { - Juror storage juror = jurors[_account]; - uint256 stakedPnk = juror.stakedPnk; - - if (stakedPnk >= _relativeAmount) { - availablePenalty = _relativeAmount; - juror.stakedPnk -= _relativeAmount; - } else { - availablePenalty = stakedPnk; - juror.stakedPnk = 0; - } - - pnkBalance = juror.stakedPnk; - return (pnkBalance, availablePenalty); - } - /// @dev Unstakes the inactive juror from all courts. /// `O(n * (p * log_k(j)) )` where /// `n` is the number of courts the juror has staked in, @@ -433,12 +444,12 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr bytes32 _key, uint256 _coreDisputeID, uint256 _nonce - ) public view override returns (address drawnAddress) { + ) public view override returns (address drawnAddress, uint96 fromSubcourtID) { if (phase != Phase.drawing) revert NotDrawingPhase(); SortitionSumTree storage tree = sortitionSumTrees[_key]; if (tree.nodes[0] == 0) { - return address(0); // No jurors staked. + return (address(0), 0); // No jurors staked. } uint256 currentDrawnNumber = uint256(keccak256(abi.encodePacked(randomNumber, _coreDisputeID, _nonce))) % @@ -462,7 +473,33 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr } } } - drawnAddress = _stakePathIDToAccount(tree.nodeIndexesToIDs[treeIndex]); + + bytes32 stakePathID = tree.nodeIndexesToIDs[treeIndex]; + drawnAddress = _stakePathIDToAccount(stakePathID); + + // Identify which subcourt was selected based on currentDrawnNumber + SubCourtStakes storage subcourtStakes = tree.IDsToSubCourtStakes[stakePathID]; + + // The current court stake is the node value minus all subcourt stakes + uint256 currentCourtStake = 0; + if (tree.nodes[treeIndex] > subcourtStakes.totalStakedInCourts) { + currentCourtStake = tree.nodes[treeIndex] - subcourtStakes.totalStakedInCourts; + } + + // Check if the drawn number falls within current court range + if (currentDrawnNumber >= currentCourtStake) { + // Find which subcourt range contains the drawn number + uint256 accumulatedStake = currentCourtStake; + for (uint256 i = 0; i < MAX_STAKE_PATHS; i++) { + if (subcourtStakes.stakedInCourts[i] > 0) { + if (currentDrawnNumber < accumulatedStake + subcourtStakes.stakedInCourts[i]) { + fromSubcourtID = subcourtStakes.courtIDs[i]; + break; + } + accumulatedStake += subcourtStakes.stakedInCourts[i]; + } + } + } } /// @dev Get the stake of a juror in a court. @@ -487,6 +524,13 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr return tree.nodes[treeIndex]; } + /// @dev Gets the balance of a juror in a court. + /// @param _juror The address of the juror. + /// @param _courtID The ID of the court. + /// @return totalStaked The total amount of tokens staked including locked tokens and penalty deductions. Equivalent to the effective stake in the General court. + /// @return totalLocked The total amount of tokens locked in disputes. + /// @return stakedInCourt The amount of tokens staked in the specified court including locked tokens and penalty deductions. + /// @return nbCourts The number of courts the juror has directly staked in. function getJurorBalance( address _juror, uint96 _courtID @@ -546,6 +590,41 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr } } + /// @dev Update the subcourt stakes. + /// @param _subcourtStakes The subcourt stakes. + /// @param _value The value to update. + /// @param _fromSubCourtID The ID of the subcourt from which the stake is coming from, or 0 if the stake does not come from a subcourt. + /// `O(1)` complexity with `MAX_STAKE_PATHS` a small constant. + function _updateSubCourtStakes( + SubCourtStakes storage _subcourtStakes, + uint256 _value, + uint96 _fromSubCourtID + ) internal { + // Update existing stake item if found + for (uint256 i = 0; i < MAX_STAKE_PATHS; i++) { + if (_subcourtStakes.courtIDs[i] == _fromSubCourtID) { + if (_value == 0) { + delete _subcourtStakes.courtIDs[i]; + delete _subcourtStakes.stakedInCourts[i]; + } else { + _subcourtStakes.totalStakedInCourts += _value; + _subcourtStakes.totalStakedInCourts -= _subcourtStakes.stakedInCourts[i]; + _subcourtStakes.stakedInCourts[i] = _value; + } + return; + } + } + // Not found so add a new stake item + for (uint256 i = 0; i < MAX_STAKE_PATHS; i++) { + if (_subcourtStakes.courtIDs[i] == 0) { + _subcourtStakes.courtIDs[i] = _fromSubCourtID; + _subcourtStakes.totalStakedInCourts += _value; + _subcourtStakes.stakedInCourts[i] = _value; + return; + } + } + } + /// @dev Retrieves a juror's address from the stake path ID. /// @param _stakePathID The stake path ID to unpack. /// @return account The account. @@ -579,10 +658,11 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr /// @param _key The key of the tree. /// @param _value The new value. /// @param _ID The ID of the value. + /// @param _fromSubCourtID The ID of the subcourt from which the stake is coming from, or 0 if the stake does not come from a subcourt. /// `O(log_k(n))` where /// `k` is the maximum number of children per node in the tree, /// and `n` is the maximum number of nodes ever appended. - function _set(bytes32 _key, uint256 _value, bytes32 _ID) internal { + function _set(bytes32 _key, uint256 _value, bytes32 _ID, uint96 _fromSubCourtID) internal { SortitionSumTree storage tree = sortitionSumTrees[_key]; uint256 treeIndex = tree.IDsToNodeIndexes[_ID]; @@ -652,6 +732,8 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr _updateParents(_key, treeIndex, plusOrMinus, plusOrMinusValue); } } + + _updateSubCourtStakes(tree.IDsToSubCourtStakes[_ID], _value, _fromSubCourtID); } /// @dev Packs an account and a court ID into a stake path ID. diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol index 0922f047b..780c3ad11 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol @@ -228,7 +228,7 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi function draw( uint256 _coreDisputeID, uint256 _nonce - ) external override onlyByCore notJumped(_coreDisputeID) returns (address drawnAddress) { + ) external override onlyByCore notJumped(_coreDisputeID) returns (address drawnAddress, uint96 fromSubcourtID) { uint256 localDisputeID = coreDisputeIDToLocal[_coreDisputeID]; Dispute storage dispute = disputes[localDisputeID]; uint256 localRoundID = dispute.rounds.length - 1; @@ -238,10 +238,10 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi (uint96 courtID, , , , ) = core.disputes(_coreDisputeID); bytes32 key = bytes32(uint256(courtID)); // Get the ID of the tree. - drawnAddress = sortitionModule.draw(key, _coreDisputeID, _nonce); + (drawnAddress, fromSubcourtID) = sortitionModule.draw(key, _coreDisputeID, _nonce); if (drawnAddress == address(0)) { // Sortition can return 0 address if no one has staked yet. - return drawnAddress; + return (drawnAddress, fromSubcourtID); } if (_postDrawCheck(round, _coreDisputeID, drawnAddress)) { diff --git a/contracts/src/arbitration/interfaces/IDisputeKit.sol b/contracts/src/arbitration/interfaces/IDisputeKit.sol index 6a72b35e4..9cc3c891b 100644 --- a/contracts/src/arbitration/interfaces/IDisputeKit.sol +++ b/contracts/src/arbitration/interfaces/IDisputeKit.sol @@ -48,7 +48,10 @@ interface IDisputeKit { /// @param _coreDisputeID The ID of the dispute in Kleros Core, not in the Dispute Kit. /// @param _nonce Nonce. /// @return drawnAddress The drawn address. - function draw(uint256 _coreDisputeID, uint256 _nonce) external returns (address drawnAddress); + function draw( + uint256 _coreDisputeID, + uint256 _nonce + ) external returns (address drawnAddress, uint96 fromSubcourtID); // ************************************* // // * Public Views * // diff --git a/contracts/src/arbitration/interfaces/ISortitionModule.sol b/contracts/src/arbitration/interfaces/ISortitionModule.sol index 5cf10e6ae..e9423877f 100644 --- a/contracts/src/arbitration/interfaces/ISortitionModule.sol +++ b/contracts/src/arbitration/interfaces/ISortitionModule.sol @@ -29,20 +29,25 @@ interface ISortitionModule { uint256 _newStake ) external; + function setStakePenalty( + address _account, + uint96 _courtID, + uint256 _penalty + ) external returns (uint256 pnkBalance, uint256 availablePenalty); + function setJurorInactive(address _account) external; function lockStake(address _account, uint256 _relativeAmount) external; function unlockStake(address _account, uint256 _relativeAmount) external; - function penalizeStake( - address _account, - uint256 _relativeAmount - ) external returns (uint256 pnkBalance, uint256 availablePenalty); - function notifyRandomNumber(uint256 _drawnNumber) external; - function draw(bytes32 _court, uint256 _coreDisputeID, uint256 _nonce) external view returns (address); + function draw( + bytes32 _court, + uint256 _coreDisputeID, + uint256 _nonce + ) external view returns (address drawnAddress, uint96 fromSubcourtID); function getJurorBalance( address _juror, diff --git a/contracts/src/arbitration/university/KlerosCoreUniversity.sol b/contracts/src/arbitration/university/KlerosCoreUniversity.sol index 5744088b0..8e377dac3 100644 --- a/contracts/src/arbitration/university/KlerosCoreUniversity.sol +++ b/contracts/src/arbitration/university/KlerosCoreUniversity.sol @@ -59,6 +59,7 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { uint256 repartitions; // A counter of reward repartitions made in this round. uint256 pnkPenalties; // The amount of PNKs collected from penalties in this round. address[] drawnJurors; // Addresses of the jurors that were drawn in this round. + uint96[] drawnJurorFromCourtIDs; // The courtIDs where the juror was drawn from, possibly their stake in a subcourt. uint256 sumFeeRewardPaid; // Total sum of arbitration fees paid to coherent jurors as a reward in this round. uint256 sumPnkRewardPaid; // Total sum of PNK paid to coherent jurors as a reward in this round. IERC20 feeToken; // The token used for paying fees in this round. @@ -599,13 +600,14 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { { IDisputeKit disputeKit = disputeKits[round.disputeKitID]; uint256 iteration = round.drawIterations + 1; - address drawnAddress = disputeKit.draw(_disputeID, iteration); + (address drawnAddress, uint96 fromSubcourtID) = disputeKit.draw(_disputeID, iteration); if (drawnAddress == address(0)) { revert NoJurorDrawn(); } sortitionModule.lockStake(drawnAddress, round.pnkAtStakePerJuror); emit Draw(drawnAddress, _disputeID, currentRound, round.drawnJurors.length); round.drawnJurors.push(drawnAddress); + round.drawnJurorFromCourtIDs.push(fromSubcourtID != 0 ? fromSubcourtID : dispute.courtID); if (round.drawnJurors.length == round.nbVotes) { sortitionModule.postDrawHook(_disputeID, currentRound); } @@ -774,7 +776,12 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { sortitionModule.unlockStake(account, penalty); // Apply the penalty to the staked PNKs. - (uint256 pnkBalance, uint256 availablePenalty) = sortitionModule.penalizeStake(account, penalty); + uint96 penalizedInCourtID = round.drawnJurorFromCourtIDs[_params.repartition]; + (uint256 pnkBalance, uint256 availablePenalty) = sortitionModule.setStakePenalty( + account, + penalizedInCourtID, + penalty + ); _params.pnkPenaltiesInRound += availablePenalty; emit TokenAndETHShift( account, diff --git a/contracts/src/arbitration/university/SortitionModuleUniversity.sol b/contracts/src/arbitration/university/SortitionModuleUniversity.sol index db61958fd..51d8555fc 100644 --- a/contracts/src/arbitration/university/SortitionModuleUniversity.sol +++ b/contracts/src/arbitration/university/SortitionModuleUniversity.sol @@ -190,6 +190,38 @@ contract SortitionModuleUniversity is ISortitionModuleUniversity, UUPSProxiable, uint256 _pnkWithdrawal, uint256 _newStake ) external override onlyByCore { + _setStake(_account, _courtID, _pnkDeposit, _pnkWithdrawal, _newStake); + } + + function setStakePenalty( + address _account, + uint96 _courtID, + uint256 _penalty + ) external override onlyByCore returns (uint256 pnkBalance, uint256 availablePenalty) { + Juror storage juror = jurors[_account]; + availablePenalty = _penalty; + if (juror.stakedPnk < _penalty) { + availablePenalty = juror.stakedPnk; + } + + if (availablePenalty == 0) return (juror.stakedPnk, 0); // No penalty to apply. + + uint256 currentStake = _stakeOf(_account, _courtID); + uint256 newStake = 0; + if (currentStake >= availablePenalty) { + newStake = currentStake - availablePenalty; + } + _setStake(_account, _courtID, 0, availablePenalty, newStake); + pnkBalance = juror.stakedPnk; // updated by _setStake() + } + + function _setStake( + address _account, + uint96 _courtID, + uint256 _pnkDeposit, + uint256 _pnkWithdrawal, + uint256 _newStake + ) internal { Juror storage juror = jurors[_account]; uint256 currentStake = _stakeOf(_account, _courtID); if (_pnkDeposit > 0) { @@ -237,25 +269,6 @@ contract SortitionModuleUniversity is ISortitionModuleUniversity, UUPSProxiable, emit StakeLocked(_account, _relativeAmount, true); } - function penalizeStake( - address _account, - uint256 _relativeAmount - ) external override onlyByCore returns (uint256 pnkBalance, uint256 availablePenalty) { - Juror storage juror = jurors[_account]; - uint256 stakedPnk = juror.stakedPnk; - - if (stakedPnk >= _relativeAmount) { - availablePenalty = _relativeAmount; - juror.stakedPnk -= _relativeAmount; - } else { - availablePenalty = stakedPnk; - juror.stakedPnk = 0; - } - - pnkBalance = juror.stakedPnk; - return (pnkBalance, availablePenalty); - } - /// @dev Unstakes the inactive juror from all courts. /// `O(n * (p * log_k(j)) )` where /// `n` is the number of courts the juror has staked in, @@ -293,7 +306,11 @@ contract SortitionModuleUniversity is ISortitionModuleUniversity, UUPSProxiable, /// @dev Draw an ID from a tree using a number. /// Note that this function reverts if the sum of all values in the tree is 0. /// @return drawnAddress The drawn address. - function draw(bytes32, uint256, uint256) public view override returns (address drawnAddress) { + function draw( + bytes32, + uint256, + uint256 + ) public view override returns (address drawnAddress, uint96 fromSubcourtID) { drawnAddress = transientJuror; } diff --git a/contracts/test/arbitration/dispute-kit-gated.ts b/contracts/test/arbitration/dispute-kit-gated.ts index 4c3d26052..b7336087c 100644 --- a/contracts/test/arbitration/dispute-kit-gated.ts +++ b/contracts/test/arbitration/dispute-kit-gated.ts @@ -109,7 +109,7 @@ describe("DisputeKitGated", async () => { await core .connect(juror) - .setStake(Courts.GENERAL, thousandPNK(10), { gasLimit: 300000 }) + .setStake(Courts.GENERAL, thousandPNK(10), { gasLimit: 500000 }) .then((tx) => tx.wait()); expect(await sortitionModule.getJurorBalance(juror.address, 1)).to.deep.equal([ diff --git a/contracts/test/foundry/KlerosCore.t.sol b/contracts/test/foundry/KlerosCore.t.sol index d0c3c190b..5b60aad7e 100644 --- a/contracts/test/foundry/KlerosCore.t.sol +++ b/contracts/test/foundry/KlerosCore.t.sol @@ -2521,7 +2521,9 @@ contract KlerosCoreTest is Test { // Note that these events are emitted only after the first iteration of execute() therefore the juror has been penalized only for 1000 PNK her. vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeSet(staker1, newCourtID, 0, 19000); // Starting with 40000 we first nullify the stake and remove 20000 and then remove penalty once since there was only first iteration (40000 - 20000 - 1000) + emit SortitionModuleBase.StakeSet(staker1, newCourtID, 19000, 39000); // 1000 PNK penalty for voteID 0 + vm.expectEmit(true, true, true, true); + emit SortitionModuleBase.StakeSet(staker1, newCourtID, 0, 20000); // Starting with 40000 we first nullify the stake and remove 19000 and then remove penalty once since there was only first iteration (40000 - 20000 - 1000) vm.expectEmit(true, true, true, true); emit SortitionModuleBase.StakeSet(staker1, GENERAL_COURT, 0, 2000); // 2000 PNK should remain in balance to cover penalties since the first 1000 of locked pnk was already unlocked core.execute(disputeID, 0, 3); From 524116cf88032a1bf4291cbf924abf48d3eb017e Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Mon, 25 Aug 2025 20:19:47 +0100 Subject: [PATCH 058/175] chore: changelog --- contracts/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/CHANGELOG.md b/contracts/CHANGELOG.md index 07201c2f5..bea677f34 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:** Stake the juror's PNK rewards instead of transferring them out ([#2099](https://github.com/kleros/kleros-v2/issues/2099)) - **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)) From 5468d5542141310786e5d456748aee38d7e55735 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Mon, 25 Aug 2025 20:44:31 +0100 Subject: [PATCH 059/175] refactor: minor variable rename, natspec --- .../src/arbitration/SortitionModuleBase.sol | 51 +++++++++++-------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/contracts/src/arbitration/SortitionModuleBase.sol b/contracts/src/arbitration/SortitionModuleBase.sol index 9dc938de2..b5b7944c1 100644 --- a/contracts/src/arbitration/SortitionModuleBase.sol +++ b/contracts/src/arbitration/SortitionModuleBase.sol @@ -18,9 +18,9 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr // ************************************* // struct SubCourtStakes { - uint256 totalStakedInCourts; - uint96[MAX_STAKE_PATHS] courtIDs; - uint256[MAX_STAKE_PATHS] stakedInCourts; + uint256 totalStakedInSubCourts; + uint96[MAX_STAKE_PATHS] subCourtIDs; + uint256[MAX_STAKE_PATHS] stakedInSubCourts; } struct SortitionSumTree { @@ -312,6 +312,17 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr _setStake(_account, _courtID, _pnkDeposit, _pnkWithdrawal, _newStake); } + /// @dev Update the state of the stakes with a PNK reward deposit, called by KC during rewards execution. + /// `O(n + p * log_k(j))` where + /// `n` is the number of courts the juror has staked in, + /// `p` is the depth of the court tree, + /// `k` is the minimum number of children per node of one of these courts' sortition sum tree, + /// and `j` is the maximum number of jurors that ever staked in one of these courts simultaneously. + /// @param _account The address of the juror. + /// @param _courtID The ID of the court. + /// @param _penalty The amount of PNK to be deducted. + /// @return pnkBalance The updated total PNK balance of the juror, including the penalty. + /// @return availablePenalty The amount of PNK that was actually deducted. function setStakePenalty( address _account, uint96 _courtID, @@ -331,7 +342,7 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr newStake = currentStake - availablePenalty; } _setStake(_account, _courtID, 0, availablePenalty, newStake); - pnkBalance = juror.stakedPnk; // updated by _setStake() + pnkBalance = juror.stakedPnk; // Updated by _setStake(). } function _setStake( @@ -482,8 +493,8 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr // The current court stake is the node value minus all subcourt stakes uint256 currentCourtStake = 0; - if (tree.nodes[treeIndex] > subcourtStakes.totalStakedInCourts) { - currentCourtStake = tree.nodes[treeIndex] - subcourtStakes.totalStakedInCourts; + if (tree.nodes[treeIndex] > subcourtStakes.totalStakedInSubCourts) { + currentCourtStake = tree.nodes[treeIndex] - subcourtStakes.totalStakedInSubCourts; } // Check if the drawn number falls within current court range @@ -491,12 +502,12 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr // Find which subcourt range contains the drawn number uint256 accumulatedStake = currentCourtStake; for (uint256 i = 0; i < MAX_STAKE_PATHS; i++) { - if (subcourtStakes.stakedInCourts[i] > 0) { - if (currentDrawnNumber < accumulatedStake + subcourtStakes.stakedInCourts[i]) { - fromSubcourtID = subcourtStakes.courtIDs[i]; + if (subcourtStakes.stakedInSubCourts[i] > 0) { + if (currentDrawnNumber < accumulatedStake + subcourtStakes.stakedInSubCourts[i]) { + fromSubcourtID = subcourtStakes.subCourtIDs[i]; break; } - accumulatedStake += subcourtStakes.stakedInCourts[i]; + accumulatedStake += subcourtStakes.stakedInSubCourts[i]; } } } @@ -602,24 +613,24 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr ) internal { // Update existing stake item if found for (uint256 i = 0; i < MAX_STAKE_PATHS; i++) { - if (_subcourtStakes.courtIDs[i] == _fromSubCourtID) { + if (_subcourtStakes.subCourtIDs[i] == _fromSubCourtID) { if (_value == 0) { - delete _subcourtStakes.courtIDs[i]; - delete _subcourtStakes.stakedInCourts[i]; + delete _subcourtStakes.subCourtIDs[i]; + delete _subcourtStakes.stakedInSubCourts[i]; } else { - _subcourtStakes.totalStakedInCourts += _value; - _subcourtStakes.totalStakedInCourts -= _subcourtStakes.stakedInCourts[i]; - _subcourtStakes.stakedInCourts[i] = _value; + _subcourtStakes.totalStakedInSubCourts += _value; + _subcourtStakes.totalStakedInSubCourts -= _subcourtStakes.stakedInSubCourts[i]; + _subcourtStakes.stakedInSubCourts[i] = _value; } return; } } // Not found so add a new stake item for (uint256 i = 0; i < MAX_STAKE_PATHS; i++) { - if (_subcourtStakes.courtIDs[i] == 0) { - _subcourtStakes.courtIDs[i] = _fromSubCourtID; - _subcourtStakes.totalStakedInCourts += _value; - _subcourtStakes.stakedInCourts[i] = _value; + if (_subcourtStakes.subCourtIDs[i] == 0) { + _subcourtStakes.subCourtIDs[i] = _fromSubCourtID; + _subcourtStakes.totalStakedInSubCourts += _value; + _subcourtStakes.stakedInSubCourts[i] = _value; return; } } From 1aae950dff5987fc348b5bfa3c7c4ae10c500d20 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Tue, 26 Aug 2025 21:50:18 +0100 Subject: [PATCH 060/175] feat: unstake juror if below minStake after penalty, simplified sub-court origination logic --- contracts/src/arbitration/KlerosCoreBase.sol | 9 +- .../src/arbitration/SortitionModuleBase.sol | 159 ++++++------------ .../interfaces/ISortitionModule.sol | 2 +- .../university/KlerosCoreUniversity.sol | 9 +- .../university/SortitionModuleUniversity.sol | 6 +- 5 files changed, 74 insertions(+), 111 deletions(-) diff --git a/contracts/src/arbitration/KlerosCoreBase.sol b/contracts/src/arbitration/KlerosCoreBase.sol index 056d49fd5..5a51cdf1a 100644 --- a/contracts/src/arbitration/KlerosCoreBase.sol +++ b/contracts/src/arbitration/KlerosCoreBase.sol @@ -789,7 +789,7 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable // Apply the penalty to the staked PNKs. uint96 penalizedInCourtID = round.drawnJurorFromCourtIDs[_params.repartition]; - (uint256 pnkBalance, uint256 availablePenalty) = sortitionModule.setStakePenalty( + (uint256 pnkBalance, uint256 newCourtStake, uint256 availablePenalty) = sortitionModule.setStakePenalty( account, penalizedInCourtID, penalty @@ -804,10 +804,15 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable 0, round.feeToken ); - // Unstake the juror from all courts if he was inactive or his balance can't cover penalties anymore. + if (pnkBalance == 0 || !disputeKit.isVoteActive(_params.disputeID, _params.round, _params.repartition)) { + // The juror is inactive or their balance is can't cover penalties anymore, unstake them from all courts. sortitionModule.setJurorInactive(account); + } else if (newCourtStake < courts[penalizedInCourtID].minStake) { + // The juror's balance fell below the court minStake, unstake them from the court. + sortitionModule.setStake(account, penalizedInCourtID, 0, 0, 0); } + if (_params.repartition == _params.numberOfVotesInRound - 1 && _params.coherentCount == 0) { // No one was coherent, send the rewards to the governor. _transferFeeToken(round.feeToken, payable(governor), round.totalFeesForJurors); diff --git a/contracts/src/arbitration/SortitionModuleBase.sol b/contracts/src/arbitration/SortitionModuleBase.sol index b5b7944c1..69bdefc8b 100644 --- a/contracts/src/arbitration/SortitionModuleBase.sol +++ b/contracts/src/arbitration/SortitionModuleBase.sol @@ -17,12 +17,6 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr // * Enums / Structs * // // ************************************* // - struct SubCourtStakes { - uint256 totalStakedInSubCourts; - uint96[MAX_STAKE_PATHS] subCourtIDs; - uint256[MAX_STAKE_PATHS] stakedInSubCourts; - } - struct SortitionSumTree { uint256 K; // The maximum number of children per node. uint256[] stack; // We use this to keep track of vacant positions in the tree after removing a leaf. This is for keeping the tree as balanced as possible without spending gas on moving nodes around. @@ -30,7 +24,6 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr // Two-way mapping of IDs to node indexes. Note that node index 0 is reserved for the root node, and means the ID does not have a node. mapping(bytes32 stakePathID => uint256 nodeIndex) IDsToNodeIndexes; mapping(uint256 nodeIndex => bytes32 stakePathID) nodeIndexesToIDs; - mapping(bytes32 stakePathID => SubCourtStakes subcourtStakes) IDsToSubCourtStakes; } struct DelayedStake { @@ -322,19 +315,21 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr /// @param _courtID The ID of the court. /// @param _penalty The amount of PNK to be deducted. /// @return pnkBalance The updated total PNK balance of the juror, including the penalty. + /// @return newCourtStake The updated stake of the juror in the court. /// @return availablePenalty The amount of PNK that was actually deducted. function setStakePenalty( address _account, uint96 _courtID, uint256 _penalty - ) external override onlyByCore returns (uint256 pnkBalance, uint256 availablePenalty) { + ) external override onlyByCore returns (uint256 pnkBalance, uint256 newCourtStake, uint256 availablePenalty) { Juror storage juror = jurors[_account]; availablePenalty = _penalty; + newCourtStake = stakeOf(_account, _courtID); if (juror.stakedPnk < _penalty) { availablePenalty = juror.stakedPnk; } - if (availablePenalty == 0) return (juror.stakedPnk, 0); // No penalty to apply. + if (availablePenalty == 0) return (juror.stakedPnk, newCourtStake, 0); // No penalty to apply. uint256 currentStake = stakeOf(_account, _courtID); uint256 newStake = 0; @@ -342,7 +337,8 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr newStake = currentStake - availablePenalty; } _setStake(_account, _courtID, 0, availablePenalty, newStake); - pnkBalance = juror.stakedPnk; // Updated by _setStake(). + pnkBalance = juror.stakedPnk; // updated by _setStake() + newCourtStake = stakeOf(_account, _courtID); // updated by _setStake() } function _setStake( @@ -378,14 +374,12 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr bytes32 stakePathID = _accountAndCourtIDToStakePathID(_account, _courtID); bool finished = false; uint96 currentCourtID = _courtID; - uint96 fromSubCourtID = 0; // 0 means it is not coming from a subcourt. while (!finished) { // Tokens are also implicitly staked in parent courts through sortition module to increase the chance of being drawn. - _set(bytes32(uint256(currentCourtID)), _newStake, stakePathID, fromSubCourtID); + _set(bytes32(uint256(currentCourtID)), _newStake, stakePathID); if (currentCourtID == GENERAL_COURT) { finished = true; } else { - fromSubCourtID = currentCourtID; (currentCourtID, , , , , , ) = core.courts(currentCourtID); // Get the parent court. } } @@ -486,31 +480,7 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr } bytes32 stakePathID = tree.nodeIndexesToIDs[treeIndex]; - drawnAddress = _stakePathIDToAccount(stakePathID); - - // Identify which subcourt was selected based on currentDrawnNumber - SubCourtStakes storage subcourtStakes = tree.IDsToSubCourtStakes[stakePathID]; - - // The current court stake is the node value minus all subcourt stakes - uint256 currentCourtStake = 0; - if (tree.nodes[treeIndex] > subcourtStakes.totalStakedInSubCourts) { - currentCourtStake = tree.nodes[treeIndex] - subcourtStakes.totalStakedInSubCourts; - } - - // Check if the drawn number falls within current court range - if (currentDrawnNumber >= currentCourtStake) { - // Find which subcourt range contains the drawn number - uint256 accumulatedStake = currentCourtStake; - for (uint256 i = 0; i < MAX_STAKE_PATHS; i++) { - if (subcourtStakes.stakedInSubCourts[i] > 0) { - if (currentDrawnNumber < accumulatedStake + subcourtStakes.stakedInSubCourts[i]) { - fromSubcourtID = subcourtStakes.subCourtIDs[i]; - break; - } - accumulatedStake += subcourtStakes.stakedInSubCourts[i]; - } - } - } + (drawnAddress, fromSubcourtID) = _stakePathIDToAccountAndCourtID(stakePathID); } /// @dev Get the stake of a juror in a court. @@ -524,11 +494,11 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr /// @dev Get the stake of a juror in a court. /// @param _key The key of the tree, corresponding to a court. - /// @param _ID The stake path ID, corresponding to a juror. + /// @param _stakePathID The stake path ID, corresponding to a juror. /// @return The stake of the juror in the court. - function stakeOf(bytes32 _key, bytes32 _ID) public view returns (uint256) { + function stakeOf(bytes32 _key, bytes32 _stakePathID) public view returns (uint256) { SortitionSumTree storage tree = sortitionSumTrees[_key]; - uint treeIndex = tree.IDsToNodeIndexes[_ID]; + uint treeIndex = tree.IDsToNodeIndexes[_stakePathID]; if (treeIndex == 0) { return 0; } @@ -601,59 +571,6 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr } } - /// @dev Update the subcourt stakes. - /// @param _subcourtStakes The subcourt stakes. - /// @param _value The value to update. - /// @param _fromSubCourtID The ID of the subcourt from which the stake is coming from, or 0 if the stake does not come from a subcourt. - /// `O(1)` complexity with `MAX_STAKE_PATHS` a small constant. - function _updateSubCourtStakes( - SubCourtStakes storage _subcourtStakes, - uint256 _value, - uint96 _fromSubCourtID - ) internal { - // Update existing stake item if found - for (uint256 i = 0; i < MAX_STAKE_PATHS; i++) { - if (_subcourtStakes.subCourtIDs[i] == _fromSubCourtID) { - if (_value == 0) { - delete _subcourtStakes.subCourtIDs[i]; - delete _subcourtStakes.stakedInSubCourts[i]; - } else { - _subcourtStakes.totalStakedInSubCourts += _value; - _subcourtStakes.totalStakedInSubCourts -= _subcourtStakes.stakedInSubCourts[i]; - _subcourtStakes.stakedInSubCourts[i] = _value; - } - return; - } - } - // Not found so add a new stake item - for (uint256 i = 0; i < MAX_STAKE_PATHS; i++) { - if (_subcourtStakes.subCourtIDs[i] == 0) { - _subcourtStakes.subCourtIDs[i] = _fromSubCourtID; - _subcourtStakes.totalStakedInSubCourts += _value; - _subcourtStakes.stakedInSubCourts[i] = _value; - return; - } - } - } - - /// @dev Retrieves a juror's address from the stake path ID. - /// @param _stakePathID The stake path ID to unpack. - /// @return account The account. - function _stakePathIDToAccount(bytes32 _stakePathID) internal pure returns (address account) { - assembly { - // solium-disable-line security/no-inline-assembly - let ptr := mload(0x40) - for { - let i := 0x00 - } lt(i, 0x14) { - i := add(i, 0x01) - } { - mstore8(add(add(ptr, 0x0c), i), byte(i, _stakePathID)) - } - account := mload(ptr) - } - } - function _extraDataToTreeK(bytes memory _extraData) internal pure returns (uint256 K) { if (_extraData.length >= 32) { assembly { @@ -668,14 +585,13 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr /// @dev Set a value in a tree. /// @param _key The key of the tree. /// @param _value The new value. - /// @param _ID The ID of the value. - /// @param _fromSubCourtID The ID of the subcourt from which the stake is coming from, or 0 if the stake does not come from a subcourt. + /// @param _stakePathID The ID of the value. /// `O(log_k(n))` where /// `k` is the maximum number of children per node in the tree, /// and `n` is the maximum number of nodes ever appended. - function _set(bytes32 _key, uint256 _value, bytes32 _ID, uint96 _fromSubCourtID) internal { + function _set(bytes32 _key, uint256 _value, bytes32 _stakePathID) internal { SortitionSumTree storage tree = sortitionSumTrees[_key]; - uint256 treeIndex = tree.IDsToNodeIndexes[_ID]; + uint256 treeIndex = tree.IDsToNodeIndexes[_stakePathID]; if (treeIndex == 0) { // No existing node. @@ -709,8 +625,8 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr } // Add label. - tree.IDsToNodeIndexes[_ID] = treeIndex; - tree.nodeIndexesToIDs[treeIndex] = _ID; + tree.IDsToNodeIndexes[_stakePathID] = treeIndex; + tree.nodeIndexesToIDs[treeIndex] = _stakePathID; _updateParents(_key, treeIndex, true, _value); } @@ -727,7 +643,7 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr tree.stack.push(treeIndex); // Clear label. - delete tree.IDsToNodeIndexes[_ID]; + delete tree.IDsToNodeIndexes[_stakePathID]; delete tree.nodeIndexesToIDs[treeIndex]; _updateParents(_key, treeIndex, false, value); @@ -743,11 +659,9 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr _updateParents(_key, treeIndex, plusOrMinus, plusOrMinusValue); } } - - _updateSubCourtStakes(tree.IDsToSubCourtStakes[_ID], _value, _fromSubCourtID); } - /// @dev Packs an account and a court ID into a stake path ID. + /// @dev Packs an account and a court ID into a stake path ID: [20 bytes of address][12 bytes of courtID] = 32 bytes total. /// @param _account The address of the juror to pack. /// @param _courtID The court ID to pack. /// @return stakePathID The stake path ID. @@ -758,6 +672,8 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr assembly { // solium-disable-line security/no-inline-assembly let ptr := mload(0x40) + + // Write account address (first 20 bytes) for { let i := 0x00 } lt(i, 0x14) { @@ -765,6 +681,8 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr } { mstore8(add(ptr, i), byte(add(0x0c, i), _account)) } + + // Write court ID (last 12 bytes) for { let i := 0x14 } lt(i, 0x20) { @@ -776,6 +694,39 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr } } + /// @dev Retrieves both juror's address and court ID from the stake path ID. + /// @param _stakePathID The stake path ID to unpack. + /// @return account The account. + /// @return courtID The court ID. + function _stakePathIDToAccountAndCourtID( + bytes32 _stakePathID + ) internal pure returns (address account, uint96 courtID) { + assembly { + // solium-disable-line security/no-inline-assembly + let ptr := mload(0x40) + + // Read account address (first 20 bytes) + for { + let i := 0x00 + } lt(i, 0x14) { + i := add(i, 0x01) + } { + mstore8(add(add(ptr, 0x0c), i), byte(i, _stakePathID)) + } + account := mload(ptr) + + // Read court ID (last 12 bytes) + for { + let i := 0x00 + } lt(i, 0x0c) { + i := add(i, 0x01) + } { + mstore8(add(add(ptr, 0x14), i), byte(add(i, 0x14), _stakePathID)) + } + courtID := mload(ptr) + } + } + // ************************************* // // * Errors * // // ************************************* // diff --git a/contracts/src/arbitration/interfaces/ISortitionModule.sol b/contracts/src/arbitration/interfaces/ISortitionModule.sol index e9423877f..ead5709a0 100644 --- a/contracts/src/arbitration/interfaces/ISortitionModule.sol +++ b/contracts/src/arbitration/interfaces/ISortitionModule.sol @@ -33,7 +33,7 @@ interface ISortitionModule { address _account, uint96 _courtID, uint256 _penalty - ) external returns (uint256 pnkBalance, uint256 availablePenalty); + ) external returns (uint256 pnkBalance, uint256 newCourtStake, uint256 availablePenalty); function setJurorInactive(address _account) external; diff --git a/contracts/src/arbitration/university/KlerosCoreUniversity.sol b/contracts/src/arbitration/university/KlerosCoreUniversity.sol index 8e377dac3..cf81cf605 100644 --- a/contracts/src/arbitration/university/KlerosCoreUniversity.sol +++ b/contracts/src/arbitration/university/KlerosCoreUniversity.sol @@ -777,7 +777,7 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { // Apply the penalty to the staked PNKs. uint96 penalizedInCourtID = round.drawnJurorFromCourtIDs[_params.repartition]; - (uint256 pnkBalance, uint256 availablePenalty) = sortitionModule.setStakePenalty( + (uint256 pnkBalance, uint256 newCourtStake, uint256 availablePenalty) = sortitionModule.setStakePenalty( account, penalizedInCourtID, penalty @@ -792,10 +792,15 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { 0, round.feeToken ); - // Unstake the juror from all courts if he was inactive or his balance can't cover penalties anymore. + if (pnkBalance == 0 || !disputeKit.isVoteActive(_params.disputeID, _params.round, _params.repartition)) { + // The juror is inactive or their balance is can't cover penalties anymore, unstake them from all courts. sortitionModule.setJurorInactive(account); + } else if (newCourtStake < courts[penalizedInCourtID].minStake) { + // The juror's balance fell below the court minStake, unstake them from the court. + sortitionModule.setStake(account, penalizedInCourtID, 0, 0, 0); } + if (_params.repartition == _params.numberOfVotesInRound - 1 && _params.coherentCount == 0) { // No one was coherent, send the rewards to the governor. if (round.feeToken == NATIVE_CURRENCY) { diff --git a/contracts/src/arbitration/university/SortitionModuleUniversity.sol b/contracts/src/arbitration/university/SortitionModuleUniversity.sol index 51d8555fc..5077954d2 100644 --- a/contracts/src/arbitration/university/SortitionModuleUniversity.sol +++ b/contracts/src/arbitration/university/SortitionModuleUniversity.sol @@ -197,14 +197,15 @@ contract SortitionModuleUniversity is ISortitionModuleUniversity, UUPSProxiable, address _account, uint96 _courtID, uint256 _penalty - ) external override onlyByCore returns (uint256 pnkBalance, uint256 availablePenalty) { + ) external override onlyByCore returns (uint256 pnkBalance, uint256 newCourtStake, uint256 availablePenalty) { Juror storage juror = jurors[_account]; availablePenalty = _penalty; + newCourtStake = _stakeOf(_account, _courtID); if (juror.stakedPnk < _penalty) { availablePenalty = juror.stakedPnk; } - if (availablePenalty == 0) return (juror.stakedPnk, 0); // No penalty to apply. + if (availablePenalty == 0) return (juror.stakedPnk, newCourtStake, 0); // No penalty to apply. uint256 currentStake = _stakeOf(_account, _courtID); uint256 newStake = 0; @@ -213,6 +214,7 @@ contract SortitionModuleUniversity is ISortitionModuleUniversity, UUPSProxiable, } _setStake(_account, _courtID, 0, availablePenalty, newStake); pnkBalance = juror.stakedPnk; // updated by _setStake() + newCourtStake = _stakeOf(_account, _courtID); // updated by _setStake() } function _setStake( From 208e0093ce8c7de0c4902dab256d60fe134b9a69 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Mon, 25 Aug 2025 22:26:08 +0100 Subject: [PATCH 061/175] feat: let the dispute kit force an early court jump and override the next round nbVotes --- contracts/hardhat.config.ts | 2 +- contracts/src/arbitration/KlerosCoreBase.sol | 71 ++++++++++++++----- .../dispute-kits/DisputeKitClassicBase.sol | 13 ++++ .../arbitration/interfaces/IDisputeKit.sol | 10 +++ 4 files changed, 76 insertions(+), 20 deletions(-) diff --git a/contracts/hardhat.config.ts b/contracts/hardhat.config.ts index dc1cce1c6..3bb2787ca 100644 --- a/contracts/hardhat.config.ts +++ b/contracts/hardhat.config.ts @@ -31,7 +31,7 @@ const config: HardhatUserConfig = { viaIR: process.env.VIA_IR !== "false", // Defaults to true optimizer: { enabled: true, - runs: 10000, + runs: 2000, }, outputSelection: { "*": { diff --git a/contracts/src/arbitration/KlerosCoreBase.sol b/contracts/src/arbitration/KlerosCoreBase.sol index 5816f872a..abee79655 100644 --- a/contracts/src/arbitration/KlerosCoreBase.sol +++ b/contracts/src/arbitration/KlerosCoreBase.sol @@ -639,24 +639,17 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable Round storage round = dispute.rounds[dispute.rounds.length - 1]; if (msg.sender != address(disputeKits[round.disputeKitID])) revert DisputeKitOnly(); - uint96 newCourtID = dispute.courtID; - uint256 newDisputeKitID = round.disputeKitID; - // Warning: the extra round must be created before calling disputeKit.createDispute() Round storage extraRound = dispute.rounds.push(); - if (round.nbVotes >= courts[newCourtID].jurorsForCourtJump) { - // Jump to parent court. - newCourtID = courts[newCourtID].parent; - - if (!courts[newCourtID].supportedDisputeKits[newDisputeKitID]) { - // Switch to classic dispute kit if parent court doesn't support the current one. - newDisputeKitID = DISPUTE_KIT_CLASSIC; - } - - if (newCourtID != dispute.courtID) { - emit CourtJump(_disputeID, dispute.rounds.length - 1, dispute.courtID, newCourtID); - } + (uint96 newCourtID, uint256 newDisputeKitID, bool courtJump, ) = _getCourtAndDisputeKitJumps( + dispute, + round, + courts[dispute.courtID], + _disputeID + ); + if (courtJump) { + emit CourtJump(_disputeID, dispute.rounds.length - 1, dispute.courtID, newCourtID); } dispute.courtID = newCourtID; @@ -934,17 +927,22 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable Dispute storage dispute = disputes[_disputeID]; Round storage round = dispute.rounds[dispute.rounds.length - 1]; Court storage court = courts[dispute.courtID]; - if (round.nbVotes >= court.jurorsForCourtJump) { + + (, uint256 newDisputeKitID, bool courtJump, ) = _getCourtAndDisputeKitJumps(dispute, round, court, _disputeID); + + uint256 nbVotesAfterAppeal = disputeKits[newDisputeKitID].getNbVotesAfterAppeal(round.nbVotes); + + if (courtJump) { // Jump to parent court. if (dispute.courtID == GENERAL_COURT) { // TODO: Handle the forking when appealed in General court. cost = NON_PAYABLE_AMOUNT; // Get the cost of the parent court. } else { - cost = courts[court.parent].feeForJuror * ((round.nbVotes * 2) + 1); + cost = courts[court.parent].feeForJuror * nbVotesAfterAppeal; } } else { // Stay in current court. - cost = court.feeForJuror * ((round.nbVotes * 2) + 1); + cost = court.feeForJuror * nbVotesAfterAppeal; } } @@ -1033,7 +1031,7 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable Round storage round = dispute.rounds[dispute.rounds.length - 1]; Court storage court = courts[dispute.courtID]; - if (round.nbVotes < court.jurorsForCourtJump) { + if (!_isCourtJumping(round, court, _disputeID)) { return false; } @@ -1053,6 +1051,41 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable // * Internal * // // ************************************* // + /// @dev Returns true if the round is jumping to a parent court. + /// @param _round The round to check. + /// @param _court The court to check. + /// @return Whether the round is jumping to a parent court or not. + function _isCourtJumping( + Round storage _round, + Court storage _court, + uint256 _disputeID + ) internal view returns (bool) { + return + disputeKits[_round.disputeKitID].earlyCourtJump(_disputeID) || _round.nbVotes >= _court.jurorsForCourtJump; + } + + function _getCourtAndDisputeKitJumps( + Dispute storage _dispute, + Round storage _round, + Court storage _court, + uint256 _disputeID + ) internal view returns (uint96 newCourtID, uint256 newDisputeKitID, bool courtJump, bool disputeKitJump) { + newCourtID = _dispute.courtID; + newDisputeKitID = _round.disputeKitID; + + if (!_isCourtJumping(_round, _court, _disputeID)) return (newCourtID, newDisputeKitID, false, false); + + // Jump to parent court. + newCourtID = courts[newCourtID].parent; + courtJump = true; + + if (!courts[newCourtID].supportedDisputeKits[newDisputeKitID]) { + // Switch to classic dispute kit if parent court doesn't support the current one. + newDisputeKitID = DISPUTE_KIT_CLASSIC; + disputeKitJump = true; + } + } + /// @dev Internal function to transfer fee tokens (ETH or ERC20) /// @param _feeToken The token to transfer (NATIVE_CURRENCY for ETH). /// @param _recipient The recipient address. diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol index 0922f047b..f3e3d00a0 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol @@ -627,6 +627,19 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi ((appealPeriodEnd - appealPeriodStart) * LOSER_APPEAL_PERIOD_MULTIPLIER) / ONE_BASIS_POINT); } + /// @dev Returns true if the dispute is jumping to a parent court. + /// @return Whether the dispute is jumping to a parent court or not. + function earlyCourtJump(uint256 /* _coreDisputeID */) external pure override returns (bool) { + return false; + } + + /// @dev Returns the number of votes after the appeal. + /// @param _currentNbVotes The number of votes before the appeal. + /// @return The number of votes after the appeal. + function getNbVotesAfterAppeal(uint256 _currentNbVotes) external pure override returns (uint256) { + return (_currentNbVotes * 2) + 1; + } + /// @dev Returns true if the specified voter was active in this round. /// @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. diff --git a/contracts/src/arbitration/interfaces/IDisputeKit.sol b/contracts/src/arbitration/interfaces/IDisputeKit.sol index 6a72b35e4..b5e6f03a9 100644 --- a/contracts/src/arbitration/interfaces/IDisputeKit.sol +++ b/contracts/src/arbitration/interfaces/IDisputeKit.sol @@ -113,6 +113,16 @@ interface IDisputeKit { /// @return Whether the appeal funding is finished. function isAppealFunded(uint256 _coreDisputeID) external view returns (bool); + /// @dev Returns true if the dispute is jumping to a parent court. + /// @param _coreDisputeID The ID of the dispute in Kleros Core, not in the Dispute Kit. + /// @return Whether the dispute is jumping to a parent court or not. + function earlyCourtJump(uint256 _coreDisputeID) external view returns (bool); + + /// @dev Returns the number of votes after the appeal. + /// @param _currentNbVotes The number of votes before the appeal. + /// @return The number of votes after the appeal. + function getNbVotesAfterAppeal(uint256 _currentNbVotes) external view returns (uint256); + /// @dev Returns true if the specified voter was active in this round. /// @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. From 42cc971d9542b07ed6c252157b48708b27fd82a7 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Tue, 26 Aug 2025 20:11:58 +0100 Subject: [PATCH 062/175] feat: added the current dispute kit as parameter to getNbVotesAfterAppeal() --- contracts/src/arbitration/KlerosCoreBase.sol | 5 ++++- .../src/arbitration/dispute-kits/DisputeKitClassicBase.sol | 5 ++++- contracts/src/arbitration/interfaces/IDisputeKit.sol | 6 +++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/contracts/src/arbitration/KlerosCoreBase.sol b/contracts/src/arbitration/KlerosCoreBase.sol index abee79655..eb595096e 100644 --- a/contracts/src/arbitration/KlerosCoreBase.sol +++ b/contracts/src/arbitration/KlerosCoreBase.sol @@ -930,7 +930,10 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable (, uint256 newDisputeKitID, bool courtJump, ) = _getCourtAndDisputeKitJumps(dispute, round, court, _disputeID); - uint256 nbVotesAfterAppeal = disputeKits[newDisputeKitID].getNbVotesAfterAppeal(round.nbVotes); + uint256 nbVotesAfterAppeal = disputeKits[newDisputeKitID].getNbVotesAfterAppeal( + disputeKits[round.disputeKitID], + round.nbVotes + ); if (courtJump) { // Jump to parent court. diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol index f3e3d00a0..a9e96f7eb 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol @@ -636,7 +636,10 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi /// @dev Returns the number of votes after the appeal. /// @param _currentNbVotes The number of votes before the appeal. /// @return The number of votes after the appeal. - function getNbVotesAfterAppeal(uint256 _currentNbVotes) external pure override returns (uint256) { + function getNbVotesAfterAppeal( + IDisputeKit /* _previousDisputeKit */, + uint256 _currentNbVotes + ) external pure override returns (uint256) { return (_currentNbVotes * 2) + 1; } diff --git a/contracts/src/arbitration/interfaces/IDisputeKit.sol b/contracts/src/arbitration/interfaces/IDisputeKit.sol index b5e6f03a9..0aab45b9c 100644 --- a/contracts/src/arbitration/interfaces/IDisputeKit.sol +++ b/contracts/src/arbitration/interfaces/IDisputeKit.sol @@ -119,9 +119,13 @@ interface IDisputeKit { function earlyCourtJump(uint256 _coreDisputeID) external view returns (bool); /// @dev Returns the number of votes after the appeal. + /// @param _previousDisputeKit The previous Dispute Kit. /// @param _currentNbVotes The number of votes before the appeal. /// @return The number of votes after the appeal. - function getNbVotesAfterAppeal(uint256 _currentNbVotes) external view returns (uint256); + function getNbVotesAfterAppeal( + IDisputeKit _previousDisputeKit, + uint256 _currentNbVotes + ) external view returns (uint256); // TODO: remove previousDisputeKit /// @dev Returns true if the specified voter was active in this round. /// @param _coreDisputeID The ID of the dispute in Kleros Core, not in the Dispute Kit. From 057957e2f03744aed877244dd12834b5d659ea30 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Wed, 27 Aug 2025 03:47:19 +0100 Subject: [PATCH 063/175] chore: changelog --- contracts/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/contracts/CHANGELOG.md b/contracts/CHANGELOG.md index bea677f34..751bd5473 100644 --- a/contracts/CHANGELOG.md +++ b/contracts/CHANGELOG.md @@ -23,6 +23,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)) +### Added + +- Allow the dispute kits to force an early court jump and to override the number of votes after an appeal (future-proofing) ([#2110](https://github.com/kleros/kleros-v2/issues/2110)) + ### 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)) From d0a9a808564effde2641e2c4f633dd3f8ec316d1 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Wed, 27 Aug 2025 04:50:55 +0100 Subject: [PATCH 064/175] refactor: adopt ERC-5313 by renaming governor to owner --- .../deploy/00-home-chain-arbitration-ruler.ts | 2 +- .../00-home-chain-arbitration-university.ts | 2 +- contracts/hardhat.config.ts | 2 +- contracts/scripts/changeGovernor.ts | 77 ----- contracts/scripts/changeOwner.ts | 77 +++++ contracts/scripts/utils/execution.ts | 6 +- contracts/scripts/utils/tx-builder.ts | 6 +- contracts/scripts/viemTest.ts | 2 +- .../arbitration/DisputeTemplateRegistry.sol | 28 +- contracts/src/arbitration/KlerosCore.sol | 10 +- contracts/src/arbitration/KlerosCoreBase.sol | 74 +++-- contracts/src/arbitration/KlerosCoreNeo.sol | 14 +- contracts/src/arbitration/KlerosGovernor.sol | 18 +- contracts/src/arbitration/PolicyRegistry.sol | 30 +- contracts/src/arbitration/SortitionModule.sol | 10 +- .../src/arbitration/SortitionModuleBase.sol | 26 +- .../src/arbitration/SortitionModuleNeo.sol | 14 +- .../arbitrables/ArbitrableExample.sol | 18 +- .../arbitrables/DisputeResolver.sol | 20 +- .../devtools/DisputeResolverRuler.sol | 2 +- .../arbitration/devtools/KlerosCoreRuler.sol | 45 ++- .../dispute-kits/DisputeKitClassic.sol | 10 +- .../dispute-kits/DisputeKitClassicBase.sol | 32 +- .../dispute-kits/DisputeKitGated.sol | 10 +- .../dispute-kits/DisputeKitGatedShutter.sol | 10 +- .../dispute-kits/DisputeKitShutter.sol | 10 +- .../dispute-kits/DisputeKitSybilResistant.sol | 10 +- .../arbitration/evidence/EvidenceModule.sol | 18 +- .../evidence/ModeratedEvidenceModule.sol | 30 +- .../university/KlerosCoreUniversity.sol | 78 +++-- .../university/SortitionModuleUniversity.sol | 16 +- .../view/KlerosCoreSnapshotProxy.sol | 24 +- contracts/src/gateway/ForeignGateway.sol | 34 +-- contracts/src/gateway/HomeGateway.sol | 36 +-- .../kleros-v1/interfaces/IKlerosLiquid.sol | 2 +- .../kleros-liquid-xdai/xKlerosLiquidV2.sol | 55 ++-- .../KlerosLiquidToV2Governor.sol | 34 +-- .../src/proxy/mock/UUPSUpgradeableMocks.sol | 8 +- .../by-inheritance/UpgradedByInheritance.sol | 8 +- .../mock/by-rewrite/UpgradedByRewrite.sol | 8 +- .../mock/by-rewrite/UpgradedByRewriteV2.sol | 4 +- contracts/src/rng/BlockhashRNG.sol | 22 +- .../src/rng/ChainlinkConsumerBaseV2Plus.sol | 169 +++++++++++ contracts/src/rng/ChainlinkRNG.sol | 34 +-- contracts/src/rng/IRNG.sol | 2 +- contracts/src/rng/RNGWithFallback.sol | 24 +- contracts/src/rng/RandomizerRNG.sol | 30 +- contracts/src/token/Faucet.sol | 18 +- contracts/test/arbitration/staking-neo.ts | 10 +- contracts/test/evidence/index.ts | 26 +- contracts/test/foundry/KlerosCore.t.sol | 284 +++++++++--------- contracts/test/proxy/index.ts | 16 +- contracts/test/rng/index.ts | 2 +- 53 files changed, 848 insertions(+), 709 deletions(-) delete mode 100644 contracts/scripts/changeGovernor.ts create mode 100644 contracts/scripts/changeOwner.ts create mode 100644 contracts/src/rng/ChainlinkConsumerBaseV2Plus.sol diff --git a/contracts/deploy/00-home-chain-arbitration-ruler.ts b/contracts/deploy/00-home-chain-arbitration-ruler.ts index 5e6e5bdc7..a9077dbe6 100644 --- a/contracts/deploy/00-home-chain-arbitration-ruler.ts +++ b/contracts/deploy/00-home-chain-arbitration-ruler.ts @@ -29,7 +29,7 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) await deployUpgradable(deployments, "KlerosCoreRuler", { from: deployer, args: [ - deployer, // governor + deployer, // owner pnk.target, [minStake, alpha, feeForJuror, jurorsForCourtJump], ], diff --git a/contracts/deploy/00-home-chain-arbitration-university.ts b/contracts/deploy/00-home-chain-arbitration-university.ts index e3fce871e..2aa5f747b 100644 --- a/contracts/deploy/00-home-chain-arbitration-university.ts +++ b/contracts/deploy/00-home-chain-arbitration-university.ts @@ -50,7 +50,7 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) const klerosCore = await deployUpgradable(deployments, "KlerosCoreUniversity", { from: deployer, args: [ - deployer, // governor + deployer, // owner deployer, // instructor pnk.target, ZeroAddress, // KlerosCore is configured later diff --git a/contracts/hardhat.config.ts b/contracts/hardhat.config.ts index 3bb2787ca..d451ef05f 100644 --- a/contracts/hardhat.config.ts +++ b/contracts/hardhat.config.ts @@ -15,7 +15,7 @@ import "hardhat-contract-sizer"; import "hardhat-tracer"; require("./scripts/populatePolicyRegistry"); require("./scripts/populateCourts"); -require("./scripts/changeGovernor"); +require("./scripts/changeOwner"); require("./scripts/getDisputeTemplate"); require("./scripts/compareStorageLayout"); require("./scripts/storage-layout"); diff --git a/contracts/scripts/changeGovernor.ts b/contracts/scripts/changeGovernor.ts deleted file mode 100644 index e9a9cd01c..000000000 --- a/contracts/scripts/changeGovernor.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { task } from "hardhat/config"; -import { prompt, print } from "gluegun"; -import { Cores, getContracts } from "./utils/contracts"; -import { isAddress } from "viem"; - -const { bold } = print.colors; - -task("change-governor", "Changes the governor for all the contracts") - .addPositionalParam("newGovernor", "The address of the new governor") - .addOptionalParam("coreType", "The type of core to use between base, neo, university (default: base)", Cores.BASE) - .setAction(async (taskArgs, hre) => { - const newGovernor = taskArgs.newGovernor; - if (!isAddress(newGovernor)) { - throw new Error("Invalid governor address provided"); - } - print.highlight(`💣 Changing governor to ${bold(newGovernor)}`); - - const { confirm } = await prompt.ask({ - type: "confirm", - name: "confirm", - message: "Are you sure you want to proceed?", - }); - if (!confirm) { - console.log("Operation cancelled by user."); - return; - } - - const coreType = Cores[taskArgs.coreType.toUpperCase() as keyof typeof Cores]; - if (coreType === undefined) { - console.error("Invalid core type, must be one of base, neo, university"); - return; - } - console.log("Using core type %s", coreType); - - const { - core, - disputeKitClassic, - disputeResolver, - disputeTemplateRegistry, - policyRegistry, - chainlinkRng, - randomizerRng, - snapshotProxy, - sortition, - evidence, - } = await getContracts(hre, coreType); - - const updateGovernor = async (contractName: string, contractInstance: any) => { - print.info(`Changing governor for ${contractName}`); - - const spinner = print.spin(`Executing transaction for ${contractName}...`); - try { - const tx = await contractInstance.changeGovernor(newGovernor); - await tx.wait(); - spinner.succeed(`Governor changed for ${contractName}, tx hash: ${tx.hash}`); - } catch (error) { - if (error instanceof Error) { - spinner.fail(`Failed to change governor for ${contractName}: ${error.message}`); - } else { - spinner.fail(`Failed to change governor for ${contractName}: ${String(error)}`); - } - } - }; - - await updateGovernor("KlerosCore", core); - await updateGovernor("DisputeKitClassic", disputeKitClassic); - await updateGovernor("DisputeResolver", disputeResolver); - await updateGovernor("DisputeTemplateRegistry", disputeTemplateRegistry); - await updateGovernor("PolicyRegistry", policyRegistry); - await updateGovernor("KlerosCoreSnapshotProxy", snapshotProxy); - await updateGovernor("SortitionModule", sortition); - await updateGovernor("EvidenceModule", evidence); - if (chainlinkRng) await updateGovernor("ChainlinkRNG", chainlinkRng); - if (randomizerRng) await updateGovernor("RandomizerRNG", randomizerRng); - - print.success("Governor changed successfully"); - }); diff --git a/contracts/scripts/changeOwner.ts b/contracts/scripts/changeOwner.ts new file mode 100644 index 000000000..72eeaeef7 --- /dev/null +++ b/contracts/scripts/changeOwner.ts @@ -0,0 +1,77 @@ +import { task } from "hardhat/config"; +import { prompt, print } from "gluegun"; +import { Cores, getContracts } from "./utils/contracts"; +import { isAddress } from "viem"; + +const { bold } = print.colors; + +task("change-owner", "Changes the owner for all the contracts") + .addPositionalParam("newOwner", "The address of the new owner") + .addOptionalParam("coreType", "The type of core to use between base, neo, university (default: base)", Cores.BASE) + .setAction(async (taskArgs, hre) => { + const newOwner = taskArgs.newOwner; + if (!isAddress(newOwner)) { + throw new Error("Invalid owner address provided"); + } + print.highlight(`💣 Changing owner to ${bold(newOwner)}`); + + const { confirm } = await prompt.ask({ + type: "confirm", + name: "confirm", + message: "Are you sure you want to proceed?", + }); + if (!confirm) { + console.log("Operation cancelled by user."); + return; + } + + const coreType = Cores[taskArgs.coreType.toUpperCase() as keyof typeof Cores]; + if (coreType === undefined) { + console.error("Invalid core type, must be one of base, neo, university"); + return; + } + console.log("Using core type %s", coreType); + + const { + core, + disputeKitClassic, + disputeResolver, + disputeTemplateRegistry, + policyRegistry, + chainlinkRng, + randomizerRng, + snapshotProxy, + sortition, + evidence, + } = await getContracts(hre, coreType); + + const updateOwner = async (contractName: string, contractInstance: any) => { + print.info(`Changing owner for ${contractName}`); + + const spinner = print.spin(`Executing transaction for ${contractName}...`); + try { + const tx = await contractInstance.changeOwner(newOwner); + await tx.wait(); + spinner.succeed(`Owner changed for ${contractName}, tx hash: ${tx.hash}`); + } catch (error) { + if (error instanceof Error) { + spinner.fail(`Failed to change owner for ${contractName}: ${error.message}`); + } else { + spinner.fail(`Failed to change owner for ${contractName}: ${String(error)}`); + } + } + }; + + await updateOwner("KlerosCore", core); + await updateOwner("DisputeKitClassic", disputeKitClassic); + await updateOwner("DisputeResolver", disputeResolver); + await updateOwner("DisputeTemplateRegistry", disputeTemplateRegistry); + await updateOwner("PolicyRegistry", policyRegistry); + await updateOwner("KlerosCoreSnapshotProxy", snapshotProxy); + await updateOwner("SortitionModule", sortition); + await updateOwner("EvidenceModule", evidence); + if (chainlinkRng) await updateOwner("ChainlinkRNG", chainlinkRng); + if (randomizerRng) await updateOwner("RandomizerRNG", randomizerRng); + + print.success("Owner changed successfully"); + }); diff --git a/contracts/scripts/utils/execution.ts b/contracts/scripts/utils/execution.ts index f8fab0207..b35709d5d 100644 --- a/contracts/scripts/utils/execution.ts +++ b/contracts/scripts/utils/execution.ts @@ -5,7 +5,7 @@ import { type BuilderTransaction, template, transaction, transactionBuilderUrl } const governableAbi = [ { inputs: [], - name: "governor", + name: "owner", outputs: [ { internalType: "address", @@ -25,8 +25,8 @@ export const execute = async (tx: ContractTransaction) => { const { ethers } = hre; const contract = await ethers.getContractAt(governableAbi, tx.to); - const governor = await contract.governor(); - const isContract = (await ethers.provider.getCode(governor)).length > 2; + const owner = await contract.owner(); + const isContract = (await ethers.provider.getCode(owner)).length > 2; if (isContract) { // Don't execute, just log the tx. It must be submitted for execution separately. const { to, value, data } = tx; diff --git a/contracts/scripts/utils/tx-builder.ts b/contracts/scripts/utils/tx-builder.ts index 06c7e65ab..fd0b52367 100644 --- a/contracts/scripts/utils/tx-builder.ts +++ b/contracts/scripts/utils/tx-builder.ts @@ -1,6 +1,6 @@ import { arbitrum } from "viem/chains"; -const governor = "0x66e8DE9B42308c6Ca913D1EE041d6F6fD037A57e"; +const owner = "0x66e8DE9B42308c6Ca913D1EE041d6F6fD037A57e"; const deployer = "0xf1C7c037891525E360C59f708739Ac09A7670c59"; // Transaction batch example: https://github.com/safe-global/safe-wallet-monorepo/blob/8bbf3b82edc347b70a038629cd9afd45eb1ed38a/apps/web/cypress/fixtures/test-working-batch.json @@ -12,7 +12,7 @@ export const template = ({ name, transactions }: { name: string; transactions: B name, description: "", // Not used because the Safe app doesn't show it txBuilderVersion: "1.18.0", - createdFromSafeAddress: governor, + createdFromSafeAddress: owner, createdFromOwnerAddress: deployer, }, transactions, @@ -42,4 +42,4 @@ export interface BuilderTransaction { contractInputsValues: null; } -export const transactionBuilderUrl = `https://app.safe.global/apps/open?safe=arb1:${governor}&appUrl=https%3A%2F%2Fapps-portal.safe.global%2Ftx-builder`; +export const transactionBuilderUrl = `https://app.safe.global/apps/open?safe=arb1:${owner}&appUrl=https%3A%2F%2Fapps-portal.safe.global%2Ftx-builder`; diff --git a/contracts/scripts/viemTest.ts b/contracts/scripts/viemTest.ts index bbd08d68f..0346ec900 100644 --- a/contracts/scripts/viemTest.ts +++ b/contracts/scripts/viemTest.ts @@ -15,7 +15,7 @@ const main = async () => { client: client, }); - await disputeKit.read.governor().then(console.log); + await disputeKit.read.owner().then(console.log); // -------------------------------------------------- diff --git a/contracts/src/arbitration/DisputeTemplateRegistry.sol b/contracts/src/arbitration/DisputeTemplateRegistry.sol index e63d7c297..b9354aa0a 100644 --- a/contracts/src/arbitration/DisputeTemplateRegistry.sol +++ b/contracts/src/arbitration/DisputeTemplateRegistry.sol @@ -14,8 +14,8 @@ contract DisputeTemplateRegistry is IDisputeTemplateRegistry, UUPSProxiable, Ini // * Storage * // // ************************************* // - /// @dev The governor of the contract. - address public governor; + /// @dev The owner of the contract. + address public owner; /// @dev The number of templates. uint256 public templates; @@ -24,8 +24,8 @@ contract DisputeTemplateRegistry is IDisputeTemplateRegistry, UUPSProxiable, Ini // * Function Modifiers * // // ************************************* // - modifier onlyByGovernor() { - if (governor != msg.sender) revert GovernorOnly(); + modifier onlyByOwner() { + if (owner != msg.sender) revert OwnerOnly(); _; } @@ -39,9 +39,9 @@ contract DisputeTemplateRegistry is IDisputeTemplateRegistry, UUPSProxiable, Ini } /// @dev Initializer - /// @param _governor Governor of the contract. - function initialize(address _governor) external reinitializer(1) { - governor = _governor; + /// @param _owner Owner of the contract. + function initialize(address _owner) external reinitializer(1) { + owner = _owner; } function initialize2() external reinitializer(2) { @@ -53,15 +53,15 @@ contract DisputeTemplateRegistry is IDisputeTemplateRegistry, UUPSProxiable, Ini // ************************ // /// @dev Access Control to perform implementation upgrades (UUPS Proxiable) - /// Only the governor can perform upgrades (`onlyByGovernor`) - function _authorizeUpgrade(address) internal view override onlyByGovernor { + /// Only the owner can perform upgrades (`onlyByOwner`) + function _authorizeUpgrade(address) internal view override onlyByOwner { // NOP } - /// @dev Changes the governor of the contract. - /// @param _governor The new governor. - function changeGovernor(address _governor) external onlyByGovernor { - governor = _governor; + /// @dev Changes the owner of the contract. + /// @param _owner The new owner. + function changeOwner(address _owner) external onlyByOwner { + owner = _owner; } // ************************************* // @@ -85,5 +85,5 @@ contract DisputeTemplateRegistry is IDisputeTemplateRegistry, UUPSProxiable, Ini // * Errors * // // ************************************* // - error GovernorOnly(); + error OwnerOnly(); } diff --git a/contracts/src/arbitration/KlerosCore.sol b/contracts/src/arbitration/KlerosCore.sol index 3853c6b47..912a57269 100644 --- a/contracts/src/arbitration/KlerosCore.sol +++ b/contracts/src/arbitration/KlerosCore.sol @@ -20,7 +20,7 @@ contract KlerosCore is KlerosCoreBase { } /// @dev Initializer (constructor equivalent for upgradable contracts). - /// @param _governor The governor's address. + /// @param _owner The owner's address. /// @param _guardian The guardian's address. /// @param _pinakion The address of the token contract. /// @param _jurorProsecutionModule The address of the juror prosecution module. @@ -32,7 +32,7 @@ contract KlerosCore is KlerosCoreBase { /// @param _sortitionModuleAddress The sortition module responsible for sortition of the jurors. /// @param _wNative The wrapped native token address, typically wETH. function initialize( - address _governor, + address _owner, address _guardian, IERC20 _pinakion, address _jurorProsecutionModule, @@ -45,7 +45,7 @@ contract KlerosCore is KlerosCoreBase { address _wNative ) external reinitializer(1) { __KlerosCoreBase_initialize( - _governor, + _owner, _guardian, _pinakion, _jurorProsecutionModule, @@ -68,8 +68,8 @@ contract KlerosCore is KlerosCoreBase { // ************************************* // /// @dev Access Control to perform implementation upgrades (UUPS Proxiable) - /// Only the governor can perform upgrades (`onlyByGovernor`) - function _authorizeUpgrade(address) internal view override onlyByGovernor { + /// Only the owner can perform upgrades (`onlyByOwner`) + function _authorizeUpgrade(address) internal view override onlyByOwner { // NOP } } diff --git a/contracts/src/arbitration/KlerosCoreBase.sol b/contracts/src/arbitration/KlerosCoreBase.sol index eb595096e..0a9946bbc 100644 --- a/contracts/src/arbitration/KlerosCoreBase.sol +++ b/contracts/src/arbitration/KlerosCoreBase.sol @@ -90,7 +90,7 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable 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. + address public owner; // The owner of the contract. address public guardian; // The guardian able to pause asset withdrawals. IERC20 public pinakion; // The Pinakion token contract. address public jurorProsecutionModule; // The module for juror's prosecution. @@ -167,13 +167,13 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable // * Function Modifiers * // // ************************************* // - modifier onlyByGovernor() { - if (governor != msg.sender) revert GovernorOnly(); + modifier onlyByOwner() { + if (owner != msg.sender) revert OwnerOnly(); _; } - modifier onlyByGuardianOrGovernor() { - if (guardian != msg.sender && governor != msg.sender) revert GuardianOrGovernorOnly(); + modifier onlyByGuardianOrOwner() { + if (guardian != msg.sender && owner != msg.sender) revert GuardianOrOwnerOnly(); _; } @@ -192,7 +192,7 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable // ************************************* // function __KlerosCoreBase_initialize( - address _governor, + address _owner, address _guardian, IERC20 _pinakion, address _jurorProsecutionModule, @@ -204,7 +204,7 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable ISortitionModule _sortitionModuleAddress, address _wNative ) internal onlyInitializing { - governor = _governor; + owner = _owner; guardian = _guardian; pinakion = _pinakion; jurorProsecutionModule = _jurorProsecutionModule; @@ -257,65 +257,61 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable // * Governance * // // ************************************* // - /// @dev Pause staking and reward execution. Can only be done by guardian or governor. - function pause() external onlyByGuardianOrGovernor whenNotPaused { + /// @dev Pause staking and reward execution. Can only be done by guardian or owner. + function pause() external onlyByGuardianOrOwner whenNotPaused { paused = true; emit Paused(); } - /// @dev Unpause staking and reward execution. Can only be done by governor. - function unpause() external onlyByGovernor whenPaused { + /// @dev Unpause staking and reward execution. Can only be done by owner. + function unpause() external onlyByOwner whenPaused { paused = false; emit Unpaused(); } - /// @dev Allows the governor to call anything on behalf of the contract. + /// @dev Allows the owner to call anything on behalf of the contract. /// @param _destination The destination of the call. /// @param _amount The value sent with the call. /// @param _data The data sent with the call. - function executeGovernorProposal( - address _destination, - uint256 _amount, - bytes memory _data - ) external onlyByGovernor { + function executeOwnerProposal(address _destination, uint256 _amount, bytes memory _data) external onlyByOwner { (bool success, ) = _destination.call{value: _amount}(_data); if (!success) revert UnsuccessfulCall(); } - /// @dev Changes the `governor` storage variable. - /// @param _governor The new value for the `governor` storage variable. - function changeGovernor(address payable _governor) external onlyByGovernor { - governor = _governor; + /// @dev Changes the `owner` storage variable. + /// @param _owner The new value for the `owner` storage variable. + function changeOwner(address payable _owner) external onlyByOwner { + owner = _owner; } /// @dev Changes the `guardian` storage variable. /// @param _guardian The new value for the `guardian` storage variable. - function changeGuardian(address _guardian) external onlyByGovernor { + function changeGuardian(address _guardian) external onlyByOwner { guardian = _guardian; } /// @dev Changes the `pinakion` storage variable. /// @param _pinakion The new value for the `pinakion` storage variable. - function changePinakion(IERC20 _pinakion) external onlyByGovernor { + function changePinakion(IERC20 _pinakion) external onlyByOwner { pinakion = _pinakion; } /// @dev Changes the `jurorProsecutionModule` storage variable. /// @param _jurorProsecutionModule The new value for the `jurorProsecutionModule` storage variable. - function changeJurorProsecutionModule(address _jurorProsecutionModule) external onlyByGovernor { + function changeJurorProsecutionModule(address _jurorProsecutionModule) external onlyByOwner { jurorProsecutionModule = _jurorProsecutionModule; } /// @dev Changes the `_sortitionModule` storage variable. /// Note that the new module should be initialized for all courts. /// @param _sortitionModule The new value for the `sortitionModule` storage variable. - function changeSortitionModule(ISortitionModule _sortitionModule) external onlyByGovernor { + function changeSortitionModule(ISortitionModule _sortitionModule) external onlyByOwner { sortitionModule = _sortitionModule; } /// @dev Add a new supported dispute kit module to the court. /// @param _disputeKitAddress The address of the dispute kit contract. - function addNewDisputeKit(IDisputeKit _disputeKitAddress) external onlyByGovernor { + function addNewDisputeKit(IDisputeKit _disputeKitAddress) external onlyByOwner { uint256 disputeKitID = disputeKits.length; disputeKits.push(_disputeKitAddress); emit DisputeKitCreated(disputeKitID, _disputeKitAddress); @@ -341,7 +337,7 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable uint256[4] memory _timesPerPeriod, bytes memory _sortitionExtraData, uint256[] memory _supportedDisputeKits - ) external onlyByGovernor { + ) external onlyByOwner { if (courts[_parent].minStake > _minStake) revert MinStakeLowerThanParentCourt(); if (_supportedDisputeKits.length == 0) revert UnsupportedDisputeKit(); if (_parent == FORKING_COURT) revert InvalidForkingCourtAsParent(); @@ -392,7 +388,7 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable uint256 _feeForJuror, uint256 _jurorsForCourtJump, uint256[4] memory _timesPerPeriod - ) external onlyByGovernor { + ) external onlyByOwner { Court storage court = courts[_courtID]; if (_courtID != GENERAL_COURT && courts[court.parent].minStake > _minStake) { revert MinStakeLowerThanParentCourt(); @@ -423,7 +419,7 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable /// @param _courtID The ID of the court. /// @param _disputeKitIDs The IDs of dispute kits which support should be added/removed. /// @param _enable Whether add or remove the dispute kits from the court. - function enableDisputeKits(uint96 _courtID, uint256[] memory _disputeKitIDs, bool _enable) external onlyByGovernor { + function enableDisputeKits(uint96 _courtID, uint256[] memory _disputeKitIDs, bool _enable) external onlyByOwner { for (uint256 i = 0; i < _disputeKitIDs.length; i++) { if (_enable) { if (_disputeKitIDs[i] == 0 || _disputeKitIDs[i] >= disputeKits.length) { @@ -443,7 +439,7 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable /// @dev Changes the supported fee tokens. /// @param _feeToken The fee token. /// @param _accepted Whether the token is supported or not as a method of fee payment. - function changeAcceptedFeeTokens(IERC20 _feeToken, bool _accepted) external onlyByGovernor { + function changeAcceptedFeeTokens(IERC20 _feeToken, bool _accepted) external onlyByOwner { currencyRates[_feeToken].feePaymentAccepted = _accepted; emit AcceptedFeeToken(_feeToken, _accepted); } @@ -452,7 +448,7 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable /// @param _feeToken The fee token. /// @param _rateInEth The new rate of the fee token in ETH. /// @param _rateDecimals The new decimals of the fee token rate. - function changeCurrencyRates(IERC20 _feeToken, uint64 _rateInEth, uint8 _rateDecimals) external onlyByGovernor { + function changeCurrencyRates(IERC20 _feeToken, uint64 _rateInEth, uint8 _rateDecimals) external onlyByOwner { currencyRates[_feeToken].rateInEth = _rateInEth; currencyRates[_feeToken].rateDecimals = _rateDecimals; emit NewCurrencyRate(_feeToken, _rateInEth, _rateDecimals); @@ -795,9 +791,9 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable sortitionModule.setJurorInactive(account); } if (_params.repartition == _params.numberOfVotesInRound - 1 && _params.coherentCount == 0) { - // No one was coherent, send the rewards to the governor. - _transferFeeToken(round.feeToken, payable(governor), round.totalFeesForJurors); - pinakion.safeTransfer(governor, _params.pnkPenaltiesInRound); + // No one was coherent, send the rewards to the owner. + _transferFeeToken(round.feeToken, payable(owner), round.totalFeesForJurors); + pinakion.safeTransfer(owner, _params.pnkPenaltiesInRound); emit LeftoverRewardSent( _params.disputeID, _params.round, @@ -863,16 +859,16 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable round.feeToken ); - // Transfer any residual rewards to the governor. It may happen due to partial coherence of the jurors. + // Transfer any residual rewards to the owner. It may happen due to partial coherence of the jurors. if (_params.repartition == _params.numberOfVotesInRound * 2 - 1) { uint256 leftoverPnkReward = _params.pnkPenaltiesInRound - round.sumPnkRewardPaid; uint256 leftoverFeeReward = round.totalFeesForJurors - round.sumFeeRewardPaid; if (leftoverPnkReward != 0 || leftoverFeeReward != 0) { if (leftoverPnkReward != 0) { - pinakion.safeTransfer(governor, leftoverPnkReward); + pinakion.safeTransfer(owner, leftoverPnkReward); } if (leftoverFeeReward != 0) { - _transferFeeToken(round.feeToken, payable(governor), leftoverFeeReward); + _transferFeeToken(round.feeToken, payable(owner), leftoverFeeReward); } emit LeftoverRewardSent( _params.disputeID, @@ -1217,8 +1213,8 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable // * Errors * // // ************************************* // - error GovernorOnly(); - error GuardianOrGovernorOnly(); + error OwnerOnly(); + error GuardianOrOwnerOnly(); error DisputeKitOnly(); error SortitionModuleOnly(); error UnsuccessfulCall(); diff --git a/contracts/src/arbitration/KlerosCoreNeo.sol b/contracts/src/arbitration/KlerosCoreNeo.sol index 1d09c2964..a72319fc5 100644 --- a/contracts/src/arbitration/KlerosCoreNeo.sol +++ b/contracts/src/arbitration/KlerosCoreNeo.sol @@ -28,7 +28,7 @@ contract KlerosCoreNeo is KlerosCoreBase { } /// @dev Initializer (constructor equivalent for upgradable contracts). - /// @param _governor The governor's address. + /// @param _owner The owner's address. /// @param _guardian The guardian's address. /// @param _pinakion The address of the token contract. /// @param _jurorProsecutionModule The address of the juror prosecution module. @@ -41,7 +41,7 @@ contract KlerosCoreNeo is KlerosCoreBase { /// @param _jurorNft NFT contract to vet the jurors. /// @param _wNative The wrapped native token address, typically wETH. function initialize( - address _governor, + address _owner, address _guardian, IERC20 _pinakion, address _jurorProsecutionModule, @@ -55,7 +55,7 @@ contract KlerosCoreNeo is KlerosCoreBase { address _wNative ) external reinitializer(2) { __KlerosCoreBase_initialize( - _governor, + _owner, _guardian, _pinakion, _jurorProsecutionModule, @@ -79,21 +79,21 @@ contract KlerosCoreNeo is KlerosCoreBase { // ************************************* // /// @dev Access Control to perform implementation upgrades (UUPS Proxiable) - /// Only the governor can perform upgrades (`onlyByGovernor`) - function _authorizeUpgrade(address) internal view override onlyByGovernor { + /// Only the owner can perform upgrades (`onlyByOwner`) + function _authorizeUpgrade(address) internal view override onlyByOwner { // NOP } /// @dev Changes the `jurorNft` storage variable. /// @param _jurorNft The new value for the `jurorNft` storage variable. - function changeJurorNft(IERC721 _jurorNft) external onlyByGovernor { + function changeJurorNft(IERC721 _jurorNft) external onlyByOwner { jurorNft = _jurorNft; } /// @dev Adds or removes an arbitrable from whitelist. /// @param _arbitrable Arbitrable address. /// @param _allowed Whether add or remove permission. - function changeArbitrableWhitelist(address _arbitrable, bool _allowed) external onlyByGovernor { + function changeArbitrableWhitelist(address _arbitrable, bool _allowed) external onlyByOwner { arbitrableWhitelist[_arbitrable] = _allowed; } diff --git a/contracts/src/arbitration/KlerosGovernor.sol b/contracts/src/arbitration/KlerosGovernor.sol index a0a23061e..11957a745 100644 --- a/contracts/src/arbitration/KlerosGovernor.sol +++ b/contracts/src/arbitration/KlerosGovernor.sol @@ -80,8 +80,8 @@ contract KlerosGovernor is IArbitrableV2 { _; } - modifier onlyByGovernor() { - if (address(this) != msg.sender) revert GovernorOnly(); + modifier onlyByOwner() { + if (address(this) != msg.sender) revert OwnerOnly(); _; } @@ -147,26 +147,26 @@ contract KlerosGovernor is IArbitrableV2 { /// @dev Changes the value of the base deposit required for submitting a list. /// @param _submissionBaseDeposit The new value of the base deposit, in wei. - function changeSubmissionDeposit(uint256 _submissionBaseDeposit) external onlyByGovernor { + function changeSubmissionDeposit(uint256 _submissionBaseDeposit) external onlyByOwner { submissionBaseDeposit = _submissionBaseDeposit; } /// @dev Changes the time allocated for submission. Note that it can't be changed during approval period because there can be an active dispute in the old arbitrator contract /// and prolonging submission timeout might switch it back to submission period. /// @param _submissionTimeout The new duration of the submission period, in seconds. - function changeSubmissionTimeout(uint256 _submissionTimeout) external onlyByGovernor duringSubmissionPeriod { + function changeSubmissionTimeout(uint256 _submissionTimeout) external onlyByOwner duringSubmissionPeriod { submissionTimeout = _submissionTimeout; } /// @dev Changes the time allocated for list's execution. /// @param _executionTimeout The new duration of the execution timeout, in seconds. - function changeExecutionTimeout(uint256 _executionTimeout) external onlyByGovernor { + function changeExecutionTimeout(uint256 _executionTimeout) external onlyByOwner { executionTimeout = _executionTimeout; } /// @dev Changes list withdrawal timeout. Note that withdrawals are only possible in the first half of the submission period. /// @param _withdrawTimeout The new duration of withdraw period, in seconds. - function changeWithdrawTimeout(uint256 _withdrawTimeout) external onlyByGovernor { + function changeWithdrawTimeout(uint256 _withdrawTimeout) external onlyByOwner { withdrawTimeout = _withdrawTimeout; } @@ -176,7 +176,7 @@ contract KlerosGovernor is IArbitrableV2 { function changeArbitrator( IArbitratorV2 _arbitrator, bytes memory _arbitratorExtraData - ) external onlyByGovernor duringSubmissionPeriod { + ) external onlyByOwner duringSubmissionPeriod { arbitrator = _arbitrator; arbitratorExtraData = _arbitratorExtraData; } @@ -187,7 +187,7 @@ contract KlerosGovernor is IArbitrableV2 { function changeDisputeTemplate( string memory _templateData, string memory _templateDataMappings - ) external onlyByGovernor { + ) external onlyByOwner { templateId = templateRegistry.setDisputeTemplate("", _templateData, _templateDataMappings); } @@ -414,7 +414,7 @@ contract KlerosGovernor is IArbitrableV2 { error SubmissionTimeHasEnded(); error ApprovalTimeNotStarted(); - error GovernorOnly(); + error OwnerOnly(); error WrongInputTargetAndValue(); error WrongInputTargetAndDatasize(); error InsufficientDeposit(); diff --git a/contracts/src/arbitration/PolicyRegistry.sol b/contracts/src/arbitration/PolicyRegistry.sol index f4ed36322..acea8d01b 100644 --- a/contracts/src/arbitration/PolicyRegistry.sol +++ b/contracts/src/arbitration/PolicyRegistry.sol @@ -23,16 +23,16 @@ contract PolicyRegistry is UUPSProxiable, Initializable { // * Storage * // // ************************************* // - address public governor; + address public owner; mapping(uint256 => string) public policies; // ************************************* // // * Function Modifiers * // // ************************************* // - /// @dev Requires that the sender is the governor. - modifier onlyByGovernor() { - if (governor != msg.sender) revert GovernorOnly(); + /// @dev Requires that the sender is the owner. + modifier onlyByOwner() { + if (owner != msg.sender) revert OwnerOnly(); _; } @@ -46,9 +46,9 @@ contract PolicyRegistry is UUPSProxiable, Initializable { } /// @dev Constructs the `PolicyRegistry` contract. - /// @param _governor The governor's address. - function initialize(address _governor) external reinitializer(1) { - governor = _governor; + /// @param _owner The owner's address. + function initialize(address _owner) external reinitializer(1) { + owner = _owner; } function initialize2() external reinitializer(2) { @@ -61,16 +61,16 @@ contract PolicyRegistry is UUPSProxiable, Initializable { /** * @dev Access Control to perform implementation upgrades (UUPS Proxiable) - * @dev Only the governor can perform upgrades (`onlyByGovernor`) + * @dev Only the owner can perform upgrades (`onlyByOwner`) */ - function _authorizeUpgrade(address) internal view override onlyByGovernor { + function _authorizeUpgrade(address) internal view override onlyByOwner { // NOP } - /// @dev Changes the `governor` storage variable. - /// @param _governor The new value for the `governor` storage variable. - function changeGovernor(address _governor) external onlyByGovernor { - governor = _governor; + /// @dev Changes the `owner` storage variable. + /// @param _owner The new value for the `owner` storage variable. + function changeOwner(address _owner) external onlyByOwner { + owner = _owner; } // ************************************* // @@ -81,7 +81,7 @@ contract PolicyRegistry is UUPSProxiable, Initializable { /// @param _courtID The ID of the specified court. /// @param _courtName The name of the specified court. /// @param _policy The URI of the policy JSON. - function setPolicy(uint256 _courtID, string calldata _courtName, string calldata _policy) external onlyByGovernor { + function setPolicy(uint256 _courtID, string calldata _courtName, string calldata _policy) external onlyByOwner { policies[_courtID] = _policy; emit PolicyUpdate(_courtID, _courtName, policies[_courtID]); } @@ -90,5 +90,5 @@ contract PolicyRegistry is UUPSProxiable, Initializable { // * Errors * // // ************************************* // - error GovernorOnly(); + error OwnerOnly(); } diff --git a/contracts/src/arbitration/SortitionModule.sol b/contracts/src/arbitration/SortitionModule.sol index 7e881264b..ae89335b0 100644 --- a/contracts/src/arbitration/SortitionModule.sol +++ b/contracts/src/arbitration/SortitionModule.sol @@ -19,19 +19,19 @@ contract SortitionModule is SortitionModuleBase { } /// @dev Initializer (constructor equivalent for upgradable contracts). - /// @param _governor The governor. + /// @param _owner The owner. /// @param _core The KlerosCore. /// @param _minStakingTime Minimal time to stake /// @param _maxDrawingTime Time after which the drawing phase can be switched /// @param _rng The random number generator. function initialize( - address _governor, + address _owner, KlerosCore _core, uint256 _minStakingTime, uint256 _maxDrawingTime, IRNG _rng ) external reinitializer(1) { - __SortitionModuleBase_initialize(_governor, _core, _minStakingTime, _maxDrawingTime, _rng); + __SortitionModuleBase_initialize(_owner, _core, _minStakingTime, _maxDrawingTime, _rng); } function initialize4() external reinitializer(4) { @@ -43,8 +43,8 @@ contract SortitionModule is SortitionModuleBase { // ************************************* // /// @dev Access Control to perform implementation upgrades (UUPS Proxiable) - /// Only the governor can perform upgrades (`onlyByGovernor`) - function _authorizeUpgrade(address) internal view virtual override onlyByGovernor { + /// Only the owner can perform upgrades (`onlyByOwner`) + function _authorizeUpgrade(address) internal view virtual override onlyByOwner { // NOP } } diff --git a/contracts/src/arbitration/SortitionModuleBase.sol b/contracts/src/arbitration/SortitionModuleBase.sol index af4eb6631..ae749fb85 100644 --- a/contracts/src/arbitration/SortitionModuleBase.sol +++ b/contracts/src/arbitration/SortitionModuleBase.sol @@ -44,7 +44,7 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr // * Storage * // // ************************************* // - address public governor; // The governor of the contract. + address public owner; // The owner of the contract. KlerosCore public core; // The core arbitrator contract. Phase public phase; // The current phase. uint256 public minStakingTime; // The time after which the phase can be switched to Drawing if there are open disputes. @@ -100,13 +100,13 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr // ************************************* // function __SortitionModuleBase_initialize( - address _governor, + address _owner, KlerosCore _core, uint256 _minStakingTime, uint256 _maxDrawingTime, IRNG _rng ) internal onlyInitializing { - governor = _governor; + owner = _owner; core = _core; minStakingTime = _minStakingTime; maxDrawingTime = _maxDrawingTime; @@ -119,8 +119,8 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr // * Function Modifiers * // // ************************************* // - modifier onlyByGovernor() { - if (governor != msg.sender) revert GovernorOnly(); + modifier onlyByOwner() { + if (owner != msg.sender) revert OwnerOnly(); _; } @@ -133,27 +133,27 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr // * Governance * // // ************************************* // - /// @dev Changes the governor of the contract. - /// @param _governor The new governor. - function changeGovernor(address _governor) external onlyByGovernor { - governor = _governor; + /// @dev Changes the owner of the contract. + /// @param _owner The new owner. + function changeOwner(address _owner) external onlyByOwner { + owner = _owner; } /// @dev Changes the `minStakingTime` storage variable. /// @param _minStakingTime The new value for the `minStakingTime` storage variable. - function changeMinStakingTime(uint256 _minStakingTime) external onlyByGovernor { + function changeMinStakingTime(uint256 _minStakingTime) external onlyByOwner { minStakingTime = _minStakingTime; } /// @dev Changes the `maxDrawingTime` storage variable. /// @param _maxDrawingTime The new value for the `maxDrawingTime` storage variable. - function changeMaxDrawingTime(uint256 _maxDrawingTime) external onlyByGovernor { + function changeMaxDrawingTime(uint256 _maxDrawingTime) external onlyByOwner { maxDrawingTime = _maxDrawingTime; } /// @dev Changes the `rng` storage variable. /// @param _rng The new random number generator. - function changeRandomNumberGenerator(IRNG _rng) external onlyByGovernor { + function changeRandomNumberGenerator(IRNG _rng) external onlyByOwner { rng = _rng; if (phase == Phase.generating) { rng.requestRandomness(); @@ -711,7 +711,7 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr // * Errors * // // ************************************* // - error GovernorOnly(); + error OwnerOnly(); error KlerosCoreOnly(); error MinStakingTimeNotPassed(); error NoDisputesThatNeedJurors(); diff --git a/contracts/src/arbitration/SortitionModuleNeo.sol b/contracts/src/arbitration/SortitionModuleNeo.sol index 9758882fe..d106d5d9b 100644 --- a/contracts/src/arbitration/SortitionModuleNeo.sol +++ b/contracts/src/arbitration/SortitionModuleNeo.sol @@ -27,7 +27,7 @@ contract SortitionModuleNeo is SortitionModuleBase { } /// @dev Initializer (constructor equivalent for upgradable contracts). - /// @param _governor The governor. + /// @param _owner The owner. /// @param _core The KlerosCore. /// @param _minStakingTime Minimal time to stake /// @param _maxDrawingTime Time after which the drawing phase can be switched @@ -35,7 +35,7 @@ contract SortitionModuleNeo is SortitionModuleBase { /// @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( - address _governor, + address _owner, KlerosCore _core, uint256 _minStakingTime, uint256 _maxDrawingTime, @@ -43,7 +43,7 @@ contract SortitionModuleNeo is SortitionModuleBase { uint256 _maxStakePerJuror, uint256 _maxTotalStaked ) external reinitializer(2) { - __SortitionModuleBase_initialize(_governor, _core, _minStakingTime, _maxDrawingTime, _rng); + __SortitionModuleBase_initialize(_owner, _core, _minStakingTime, _maxDrawingTime, _rng); maxStakePerJuror = _maxStakePerJuror; maxTotalStaked = _maxTotalStaked; } @@ -57,16 +57,16 @@ contract SortitionModuleNeo is SortitionModuleBase { // ************************************* // /// @dev Access Control to perform implementation upgrades (UUPS Proxiable) - /// Only the governor can perform upgrades (`onlyByGovernor`) - function _authorizeUpgrade(address) internal view override onlyByGovernor { + /// Only the owner can perform upgrades (`onlyByOwner`) + function _authorizeUpgrade(address) internal view override onlyByOwner { // NOP } - function changeMaxStakePerJuror(uint256 _maxStakePerJuror) external onlyByGovernor { + function changeMaxStakePerJuror(uint256 _maxStakePerJuror) external onlyByOwner { maxStakePerJuror = _maxStakePerJuror; } - function changeMaxTotalStaked(uint256 _maxTotalStaked) external onlyByGovernor { + function changeMaxTotalStaked(uint256 _maxTotalStaked) external onlyByOwner { maxTotalStaked = _maxTotalStaked; } diff --git a/contracts/src/arbitration/arbitrables/ArbitrableExample.sol b/contracts/src/arbitration/arbitrables/ArbitrableExample.sol index b7596566e..4dd60cb82 100644 --- a/contracts/src/arbitration/arbitrables/ArbitrableExample.sol +++ b/contracts/src/arbitration/arbitrables/ArbitrableExample.sol @@ -23,7 +23,7 @@ contract ArbitrableExample is IArbitrableV2 { event Action(string indexed _action); - address public immutable governor; + address public immutable owner; IArbitratorV2 public arbitrator; // Arbitrator is set in constructor. IDisputeTemplateRegistry public templateRegistry; // The dispute template registry. uint256 public templateId; // The current dispute template identifier. @@ -36,8 +36,8 @@ contract ArbitrableExample is IArbitrableV2 { // * Function Modifiers * // // ************************************* // - modifier onlyByGovernor() { - if (governor != msg.sender) revert GovernorOnly(); + modifier onlyByOwner() { + if (owner != msg.sender) revert OwnerOnly(); _; } @@ -60,7 +60,7 @@ contract ArbitrableExample is IArbitrableV2 { IDisputeTemplateRegistry _templateRegistry, IERC20 _weth ) { - governor = msg.sender; + owner = msg.sender; arbitrator = _arbitrator; arbitratorExtraData = _arbitratorExtraData; templateRegistry = _templateRegistry; @@ -73,22 +73,22 @@ contract ArbitrableExample is IArbitrableV2 { // * Governance * // // ************************************* // - function changeArbitrator(IArbitratorV2 _arbitrator) external onlyByGovernor { + function changeArbitrator(IArbitratorV2 _arbitrator) external onlyByOwner { arbitrator = _arbitrator; } - function changeArbitratorExtraData(bytes calldata _arbitratorExtraData) external onlyByGovernor { + function changeArbitratorExtraData(bytes calldata _arbitratorExtraData) external onlyByOwner { arbitratorExtraData = _arbitratorExtraData; } - function changeTemplateRegistry(IDisputeTemplateRegistry _templateRegistry) external onlyByGovernor { + function changeTemplateRegistry(IDisputeTemplateRegistry _templateRegistry) external onlyByOwner { templateRegistry = _templateRegistry; } function changeDisputeTemplate( string memory _templateData, string memory _templateDataMappings - ) external onlyByGovernor { + ) external onlyByOwner { templateId = templateRegistry.setDisputeTemplate("", _templateData, _templateDataMappings); } @@ -156,7 +156,7 @@ contract ArbitrableExample is IArbitrableV2 { // * Errors * // // ************************************* // - error GovernorOnly(); + error OwnerOnly(); error TransferFailed(); error AllowanceIncreaseFailed(); error ArbitratorOnly(); diff --git a/contracts/src/arbitration/arbitrables/DisputeResolver.sol b/contracts/src/arbitration/arbitrables/DisputeResolver.sol index 7248356cd..949d282b2 100644 --- a/contracts/src/arbitration/arbitrables/DisputeResolver.sol +++ b/contracts/src/arbitration/arbitrables/DisputeResolver.sol @@ -23,7 +23,7 @@ contract DisputeResolver is IArbitrableV2 { // * Storage * // // ************************************* // - address public governor; // The governor. + address public owner; // The owner. IArbitratorV2 public arbitrator; // The arbitrator. IDisputeTemplateRegistry public templateRegistry; // The dispute template registry. DisputeStruct[] public disputes; // Local disputes. @@ -36,7 +36,7 @@ contract DisputeResolver is IArbitrableV2 { /// @dev Constructor /// @param _arbitrator Target global arbitrator for any disputes. constructor(IArbitratorV2 _arbitrator, IDisputeTemplateRegistry _templateRegistry) { - governor = msg.sender; + owner = msg.sender; arbitrator = _arbitrator; templateRegistry = _templateRegistry; } @@ -45,20 +45,20 @@ contract DisputeResolver is IArbitrableV2 { // * Governance * // // ************************************* // - /// @dev Changes the governor. - /// @param _governor The address of the new governor. - function changeGovernor(address _governor) external { - if (governor != msg.sender) revert GovernorOnly(); - governor = _governor; + /// @dev Changes the owner. + /// @param _owner The address of the new owner. + function changeOwner(address _owner) external { + if (owner != msg.sender) revert OwnerOnly(); + owner = _owner; } function changeArbitrator(IArbitratorV2 _arbitrator) external { - if (governor != msg.sender) revert GovernorOnly(); + if (owner != msg.sender) revert OwnerOnly(); arbitrator = _arbitrator; } function changeTemplateRegistry(IDisputeTemplateRegistry _templateRegistry) external { - if (governor != msg.sender) revert GovernorOnly(); + if (owner != msg.sender) revert OwnerOnly(); templateRegistry = _templateRegistry; } @@ -151,7 +151,7 @@ contract DisputeResolver is IArbitrableV2 { // * Errors * // // ************************************* // - error GovernorOnly(); + error OwnerOnly(); error ArbitratorOnly(); error RulingOutOfBounds(); error DisputeAlreadyRuled(); diff --git a/contracts/src/arbitration/devtools/DisputeResolverRuler.sol b/contracts/src/arbitration/devtools/DisputeResolverRuler.sol index 05c033c38..eed2c8092 100644 --- a/contracts/src/arbitration/devtools/DisputeResolverRuler.sol +++ b/contracts/src/arbitration/devtools/DisputeResolverRuler.sol @@ -22,7 +22,7 @@ contract DisputeResolverRuler is DisputeResolver { IArbitratorV2 _arbitrator, IDisputeTemplateRegistry _templateRegistry ) DisputeResolver(_arbitrator, _templateRegistry) { - governor = msg.sender; + owner = msg.sender; } // ************************************* // diff --git a/contracts/src/arbitration/devtools/KlerosCoreRuler.sol b/contracts/src/arbitration/devtools/KlerosCoreRuler.sol index 2382970cf..9350ebd57 100644 --- a/contracts/src/arbitration/devtools/KlerosCoreRuler.sol +++ b/contracts/src/arbitration/devtools/KlerosCoreRuler.sol @@ -85,7 +85,7 @@ contract KlerosCoreRuler is IArbitratorV2, UUPSProxiable, Initializable { 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. + address public owner; // The owner of the contract. IERC20 public pinakion; // The Pinakion token contract. Court[] public courts; // The courts. Dispute[] public disputes; // The disputes. @@ -157,8 +157,8 @@ contract KlerosCoreRuler is IArbitratorV2, UUPSProxiable, Initializable { // * Function Modifiers * // // ************************************* // - modifier onlyByGovernor() { - if (governor != msg.sender) revert GovernorOnly(); + modifier onlyByOwner() { + if (owner != msg.sender) revert OwnerOnly(); _; } @@ -172,15 +172,15 @@ contract KlerosCoreRuler is IArbitratorV2, UUPSProxiable, Initializable { } /// @dev Initializer (constructor equivalent for upgradable contracts). - /// @param _governor The governor's address. + /// @param _owner The owner's address. /// @param _pinakion The address of the token contract. /// @param _courtParameters Numeric parameters of General court (minStake, alpha, feeForJuror and jurorsForCourtJump respectively). function initialize( - address _governor, + address _owner, IERC20 _pinakion, uint256[4] memory _courtParameters ) external reinitializer(1) { - governor = _governor; + owner = _owner; pinakion = _pinakion; // FORKING_COURT @@ -219,34 +219,30 @@ contract KlerosCoreRuler is IArbitratorV2, UUPSProxiable, Initializable { // ************************************* // /* @dev Access Control to perform implementation upgrades (UUPS Proxiable) - * @dev Only the governor can perform upgrades (`onlyByGovernor`) + * @dev Only the owner can perform upgrades (`onlyByOwner`) */ - function _authorizeUpgrade(address) internal view override onlyByGovernor { + function _authorizeUpgrade(address) internal view override onlyByOwner { // NOP } - /// @dev Allows the governor to call anything on behalf of the contract. + /// @dev Allows the owner to call anything on behalf of the contract. /// @param _destination The destination of the call. /// @param _amount The value sent with the call. /// @param _data The data sent with the call. - function executeGovernorProposal( - address _destination, - uint256 _amount, - bytes memory _data - ) external onlyByGovernor { + function executeOwnerProposal(address _destination, uint256 _amount, bytes memory _data) external onlyByOwner { (bool success, ) = _destination.call{value: _amount}(_data); if (!success) revert UnsuccessfulCall(); } - /// @dev Changes the `governor` storage variable. - /// @param _governor The new value for the `governor` storage variable. - function changeGovernor(address payable _governor) external onlyByGovernor { - governor = _governor; + /// @dev Changes the `owner` storage variable. + /// @param _owner The new value for the `owner` storage variable. + function changeOwner(address payable _owner) external onlyByOwner { + owner = _owner; } /// @dev Changes the `pinakion` storage variable. /// @param _pinakion The new value for the `pinakion` storage variable. - function changePinakion(IERC20 _pinakion) external onlyByGovernor { + function changePinakion(IERC20 _pinakion) external onlyByOwner { pinakion = _pinakion; } @@ -266,7 +262,7 @@ contract KlerosCoreRuler is IArbitratorV2, UUPSProxiable, Initializable { uint256 _feeForJuror, uint256 _jurorsForCourtJump, uint256[4] memory _timesPerPeriod - ) external onlyByGovernor { + ) external onlyByOwner { if (_parent == FORKING_COURT) revert InvalidForkingCourtAsParent(); uint256 courtID = courts.length; @@ -303,7 +299,7 @@ contract KlerosCoreRuler is IArbitratorV2, UUPSProxiable, Initializable { uint256 _feeForJuror, uint256 _jurorsForCourtJump, uint256[4] memory _timesPerPeriod - ) external onlyByGovernor { + ) external onlyByOwner { Court storage court = courts[_courtID]; court.minStake = _minStake; court.hiddenVotes = _hiddenVotes; @@ -325,7 +321,7 @@ contract KlerosCoreRuler is IArbitratorV2, UUPSProxiable, Initializable { /// @dev Changes the supported fee tokens. /// @param _feeToken The fee token. /// @param _accepted Whether the token is supported or not as a method of fee payment. - function changeAcceptedFeeTokens(IERC20 _feeToken, bool _accepted) external onlyByGovernor { + function changeAcceptedFeeTokens(IERC20 _feeToken, bool _accepted) external onlyByOwner { currencyRates[_feeToken].feePaymentAccepted = _accepted; emit AcceptedFeeToken(_feeToken, _accepted); } @@ -334,7 +330,7 @@ contract KlerosCoreRuler is IArbitratorV2, UUPSProxiable, Initializable { /// @param _feeToken The fee token. /// @param _rateInEth The new rate of the fee token in ETH. /// @param _rateDecimals The new decimals of the fee token rate. - function changeCurrencyRates(IERC20 _feeToken, uint64 _rateInEth, uint8 _rateDecimals) external onlyByGovernor { + function changeCurrencyRates(IERC20 _feeToken, uint64 _rateInEth, uint8 _rateDecimals) external onlyByOwner { currencyRates[_feeToken].rateInEth = _rateInEth; currencyRates[_feeToken].rateDecimals = _rateDecimals; emit NewCurrencyRate(_feeToken, _rateInEth, _rateDecimals); @@ -675,8 +671,7 @@ contract KlerosCoreRuler is IArbitratorV2, UUPSProxiable, Initializable { // * Errors * // // ************************************* // - error GovernorOnly(); - error GovernorOrInstructorOnly(); + error OwnerOnly(); error RulerOnly(); error NoRulerSet(); error RulingModeNotSet(); diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitClassic.sol b/contracts/src/arbitration/dispute-kits/DisputeKitClassic.sol index 2479ab07d..ce5932c92 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitClassic.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitClassic.sol @@ -23,11 +23,11 @@ contract DisputeKitClassic is DisputeKitClassicBase { } /// @dev Initializer. - /// @param _governor The governor's address. + /// @param _owner The owner's address. /// @param _core The KlerosCore arbitrator. /// @param _wNative The wrapped native token address, typically wETH. - function initialize(address _governor, KlerosCore _core, address _wNative) external reinitializer(1) { - __DisputeKitClassicBase_initialize(_governor, _core, _wNative); + function initialize(address _owner, KlerosCore _core, address _wNative) external reinitializer(1) { + __DisputeKitClassicBase_initialize(_owner, _core, _wNative); } function reinitialize(address _wNative) external reinitializer(9) { @@ -39,8 +39,8 @@ contract DisputeKitClassic is DisputeKitClassicBase { // ************************ // /// @dev Access Control to perform implementation upgrades (UUPS Proxiable) - /// Only the governor can perform upgrades (`onlyByGovernor`) - function _authorizeUpgrade(address) internal view override onlyByGovernor { + /// Only the owner can perform upgrades (`onlyByOwner`) + function _authorizeUpgrade(address) internal view override onlyByOwner { // NOP } } diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol index a9e96f7eb..f2d7a154c 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol @@ -59,7 +59,7 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi 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. - address public governor; // The governor of the contract. + address public owner; // The owner of the contract. KlerosCore public core; // The Kleros Core arbitrator Dispute[] public disputes; // Array of the locally created disputes. mapping(uint256 => uint256) public coreDisputeIDToLocal; // Maps the dispute ID in Kleros Core to the local dispute ID. @@ -124,8 +124,8 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi // * Modifiers * // // ************************************* // - modifier onlyByGovernor() { - if (governor != msg.sender) revert GovernorOnly(); + modifier onlyByOwner() { + if (owner != msg.sender) revert OwnerOnly(); _; } @@ -144,15 +144,15 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi // ************************************* // /// @dev Initializer. - /// @param _governor The governor's address. + /// @param _owner The owner's address. /// @param _core The KlerosCore arbitrator. /// @param _wNative The wrapped native token address, typically wETH. function __DisputeKitClassicBase_initialize( - address _governor, + address _owner, KlerosCore _core, address _wNative ) internal onlyInitializing { - governor = _governor; + owner = _owner; core = _core; wNative = _wNative; } @@ -161,28 +161,24 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi // * Governance * // // ************************ // - /// @dev Allows the governor to call anything on behalf of the contract. + /// @dev Allows the owner to call anything on behalf of the contract. /// @param _destination The destination of the call. /// @param _amount The value sent with the call. /// @param _data The data sent with the call. - function executeGovernorProposal( - address _destination, - uint256 _amount, - bytes memory _data - ) external onlyByGovernor { + function executeOwnerProposal(address _destination, uint256 _amount, bytes memory _data) external onlyByOwner { (bool success, ) = _destination.call{value: _amount}(_data); if (!success) revert UnsuccessfulCall(); } - /// @dev Changes the `governor` storage variable. - /// @param _governor The new value for the `governor` storage variable. - function changeGovernor(address payable _governor) external onlyByGovernor { - governor = _governor; + /// @dev Changes the `owner` storage variable. + /// @param _owner The new value for the `owner` storage variable. + function changeOwner(address payable _owner) external onlyByOwner { + owner = _owner; } /// @dev Changes the `core` storage variable. /// @param _core The new value for the `core` storage variable. - function changeCore(address _core) external onlyByGovernor { + function changeCore(address _core) external onlyByOwner { core = KlerosCore(_core); } @@ -755,7 +751,7 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi // * Errors * // // ************************************* // - error GovernorOnly(); + error OwnerOnly(); error KlerosCoreOnly(); error DisputeJumpedToParentDK(); error UnsuccessfulCall(); diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitGated.sol b/contracts/src/arbitration/dispute-kits/DisputeKitGated.sol index 037f79585..c0e70f741 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitGated.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitGated.sol @@ -39,11 +39,11 @@ contract DisputeKitGated is DisputeKitClassicBase { } /// @dev Initializer. - /// @param _governor The governor's address. + /// @param _owner The owner's address. /// @param _core The KlerosCore arbitrator. /// @param _wNative The wrapped native token address, typically wETH. - function initialize(address _governor, KlerosCore _core, address _wNative) external reinitializer(1) { - __DisputeKitClassicBase_initialize(_governor, _core, _wNative); + function initialize(address _owner, KlerosCore _core, address _wNative) external reinitializer(1) { + __DisputeKitClassicBase_initialize(_owner, _core, _wNative); } function reinitialize(address _wNative) external reinitializer(9) { @@ -55,8 +55,8 @@ contract DisputeKitGated is DisputeKitClassicBase { // ************************ // /// @dev Access Control to perform implementation upgrades (UUPS Proxiable) - /// Only the governor can perform upgrades (`onlyByGovernor`) - function _authorizeUpgrade(address) internal view override onlyByGovernor { + /// Only the owner can perform upgrades (`onlyByOwner`) + function _authorizeUpgrade(address) internal view override onlyByOwner { // NOP } diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitGatedShutter.sol b/contracts/src/arbitration/dispute-kits/DisputeKitGatedShutter.sol index 3ad37d56d..c5de2483b 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitGatedShutter.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitGatedShutter.sol @@ -58,11 +58,11 @@ contract DisputeKitGatedShutter is DisputeKitClassicBase { } /// @dev Initializer. - /// @param _governor The governor's address. + /// @param _owner The owner's address. /// @param _core The KlerosCore arbitrator. /// @param _wNative The wrapped native token address, typically wETH. - function initialize(address _governor, KlerosCore _core, address _wNative) external reinitializer(1) { - __DisputeKitClassicBase_initialize(_governor, _core, _wNative); + function initialize(address _owner, KlerosCore _core, address _wNative) external reinitializer(1) { + __DisputeKitClassicBase_initialize(_owner, _core, _wNative); } function reinitialize(address _wNative) external reinitializer(9) { @@ -74,8 +74,8 @@ contract DisputeKitGatedShutter is DisputeKitClassicBase { // ************************ // /// @dev Access Control to perform implementation upgrades (UUPS Proxiable) - /// Only the governor can perform upgrades (`onlyByGovernor`) - function _authorizeUpgrade(address) internal view override onlyByGovernor { + /// Only the owner can perform upgrades (`onlyByOwner`) + function _authorizeUpgrade(address) internal view override onlyByOwner { // NOP } diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitShutter.sol b/contracts/src/arbitration/dispute-kits/DisputeKitShutter.sol index 057a06921..886f5dbec 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitShutter.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitShutter.sol @@ -42,11 +42,11 @@ contract DisputeKitShutter is DisputeKitClassicBase { } /// @dev Initializer. - /// @param _governor The governor's address. + /// @param _owner The owner's address. /// @param _core The KlerosCore arbitrator. /// @param _wNative The wrapped native token address, typically wETH. - function initialize(address _governor, KlerosCore _core, address _wNative) external reinitializer(1) { - __DisputeKitClassicBase_initialize(_governor, _core, _wNative); + function initialize(address _owner, KlerosCore _core, address _wNative) external reinitializer(1) { + __DisputeKitClassicBase_initialize(_owner, _core, _wNative); } function reinitialize(address _wNative) external reinitializer(9) { @@ -58,8 +58,8 @@ contract DisputeKitShutter is DisputeKitClassicBase { // ************************ // /// @dev Access Control to perform implementation upgrades (UUPS Proxiable) - /// Only the governor can perform upgrades (`onlyByGovernor`) - function _authorizeUpgrade(address) internal view override onlyByGovernor { + /// Only the owner can perform upgrades (`onlyByOwner`) + function _authorizeUpgrade(address) internal view override onlyByOwner { // NOP } diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitSybilResistant.sol b/contracts/src/arbitration/dispute-kits/DisputeKitSybilResistant.sol index 8568c55af..e213f6d11 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitSybilResistant.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitSybilResistant.sol @@ -36,17 +36,17 @@ contract DisputeKitSybilResistant is DisputeKitClassicBase { } /// @dev Initializer. - /// @param _governor The governor's address. + /// @param _owner The owner's address. /// @param _core The KlerosCore arbitrator. /// @param _poh The Proof of Humanity registry. /// @param _wNative The wrapped native token address, typically wETH. function initialize( - address _governor, + address _owner, KlerosCore _core, IProofOfHumanity _poh, address _wNative ) external reinitializer(1) { - __DisputeKitClassicBase_initialize(_governor, _core, _wNative); + __DisputeKitClassicBase_initialize(_owner, _core, _wNative); poh = _poh; singleDrawPerJuror = true; } @@ -56,8 +56,8 @@ contract DisputeKitSybilResistant is DisputeKitClassicBase { // ************************ // /// @dev Access Control to perform implementation upgrades (UUPS Proxiable) - /// Only the governor can perform upgrades (`onlyByGovernor`) - function _authorizeUpgrade(address) internal view override onlyByGovernor { + /// Only the owner can perform upgrades (`onlyByOwner`) + function _authorizeUpgrade(address) internal view override onlyByOwner { // NOP } diff --git a/contracts/src/arbitration/evidence/EvidenceModule.sol b/contracts/src/arbitration/evidence/EvidenceModule.sol index 4967597ab..d1eb5f99c 100644 --- a/contracts/src/arbitration/evidence/EvidenceModule.sol +++ b/contracts/src/arbitration/evidence/EvidenceModule.sol @@ -15,14 +15,14 @@ contract EvidenceModule is IEvidence, Initializable, UUPSProxiable { // * Storage * // // ************************************* // - address public governor; // The governor of the contract. + address public owner; // The owner of the contract. // ************************************* // // * Modifiers * // // ************************************* // - modifier onlyByGovernor() { - if (governor != msg.sender) revert GovernorOnly(); + modifier onlyByOwner() { + if (owner != msg.sender) revert OwnerOnly(); _; } @@ -36,9 +36,9 @@ contract EvidenceModule is IEvidence, Initializable, UUPSProxiable { } /// @dev Initializer. - /// @param _governor The governor's address. - function initialize(address _governor) external reinitializer(1) { - governor = _governor; + /// @param _owner The owner's address. + function initialize(address _owner) external reinitializer(1) { + owner = _owner; } function initialize2() external reinitializer(2) { @@ -51,9 +51,9 @@ contract EvidenceModule is IEvidence, Initializable, UUPSProxiable { /** * @dev Access Control to perform implementation upgrades (UUPS Proxiable) - * @dev Only the governor can perform upgrades (`onlyByGovernor`) + * @dev Only the owner can perform upgrades (`onlyByOwner`) */ - function _authorizeUpgrade(address) internal view override onlyByGovernor { + function _authorizeUpgrade(address) internal view override onlyByOwner { // NOP } @@ -72,5 +72,5 @@ contract EvidenceModule is IEvidence, Initializable, UUPSProxiable { // * Errors * // // ************************************* // - error GovernorOnly(); + error OwnerOnly(); } diff --git a/contracts/src/arbitration/evidence/ModeratedEvidenceModule.sol b/contracts/src/arbitration/evidence/ModeratedEvidenceModule.sol index 1c612265f..b2b64a4d3 100644 --- a/contracts/src/arbitration/evidence/ModeratedEvidenceModule.sol +++ b/contracts/src/arbitration/evidence/ModeratedEvidenceModule.sol @@ -51,7 +51,7 @@ contract ModeratedEvidenceModule is IArbitrableV2 { mapping(uint256 => bytes32) public disputeIDtoEvidenceID; // One-to-one relationship between the dispute and the evidence. ArbitratorData[] public arbitratorDataList; // Stores the arbitrator data of the contract. Updated each time the data is changed. IArbitratorV2 public immutable arbitrator; // The trusted arbitrator to resolve potential disputes. If it needs to be changed, a new contract can be deployed. - address public governor; // The address that can make governance changes to the parameters of the contract. + address public owner; // The address that can make governance changes to the parameters of the contract. IDisputeTemplateRegistry public templateRegistry; // The dispute template registry. uint256 public bondTimeout; // The time in seconds during which the last moderation status can be challenged. uint256 public totalCostMultiplier; // Multiplier of arbitration fees that must be ultimately paid as fee stake. In basis points. @@ -61,8 +61,8 @@ contract ModeratedEvidenceModule is IArbitrableV2 { // * Function Modifiers * // // ************************************* // - modifier onlyGovernor() { - require(msg.sender == governor, "The caller must be the governor"); + modifier onlyOwner() { + require(msg.sender == owner, "The caller must be the owner"); _; } @@ -93,7 +93,7 @@ contract ModeratedEvidenceModule is IArbitrableV2 { /// @dev Constructor. /// @param _arbitrator The trusted arbitrator to resolve potential disputes. - /// @param _governor The trusted governor of the contract. + /// @param _owner The trusted owner of the contract. /// @param _totalCostMultiplier Multiplier of arbitration fees that must be ultimately paid as fee stake. In basis points. /// @param _initialDepositMultiplier Multiplier of arbitration fees that must be paid as initial stake for submitting evidence. In basis points. /// @param _bondTimeout The time in seconds during which the last moderation status can be challenged. @@ -102,7 +102,7 @@ contract ModeratedEvidenceModule is IArbitrableV2 { /// @param _templateDataMappings The dispute template data mappings. constructor( IArbitratorV2 _arbitrator, - address _governor, + address _owner, IDisputeTemplateRegistry _templateRegistry, uint256 _totalCostMultiplier, uint256 _initialDepositMultiplier, @@ -112,7 +112,7 @@ contract ModeratedEvidenceModule is IArbitrableV2 { string memory _templateDataMappings ) { arbitrator = _arbitrator; - governor = _governor; + owner = _owner; templateRegistry = _templateRegistry; totalCostMultiplier = _totalCostMultiplier; // For example 15000, which would provide a 100% reward to the dispute winner. @@ -132,28 +132,28 @@ contract ModeratedEvidenceModule is IArbitrableV2 { // * Governance * // // ************************************* // - /// @dev Change the governor of the contract. - /// @param _governor The address of the new governor. - function changeGovernor(address _governor) external onlyGovernor { - governor = _governor; + /// @dev Change the owner of the contract. + /// @param _owner The address of the new owner. + function changeOwner(address _owner) external onlyOwner { + owner = _owner; } /// @dev Change the proportion of arbitration fees that must be paid as fee stake by parties when there is no winner or loser (e.g. when the arbitrator refused to rule). /// @param _initialDepositMultiplier Multiplier of arbitration fees that must be paid as fee stake. In basis points. - function changeInitialDepositMultiplier(uint256 _initialDepositMultiplier) external onlyGovernor { + function changeInitialDepositMultiplier(uint256 _initialDepositMultiplier) external onlyOwner { initialDepositMultiplier = _initialDepositMultiplier; } /// @dev Change the proportion of arbitration fees that must be paid as fee stake by the winner of the previous round. /// @param _totalCostMultiplier Multiplier of arbitration fees that must be paid as fee stake. In basis points. - function changeTotalCostMultiplier(uint256 _totalCostMultiplier) external onlyGovernor { + function changeTotalCostMultiplier(uint256 _totalCostMultiplier) external onlyOwner { totalCostMultiplier = _totalCostMultiplier; } /// @dev Change the time window within which evidence submissions and removals can be contested. /// Ongoing moderations will start using the latest bondTimeout available after calling moderate() again. /// @param _bondTimeout Multiplier of arbitration fees that must be paid as fee stake. In basis points. - function changeBondTimeout(uint256 _bondTimeout) external onlyGovernor { + function changeBondTimeout(uint256 _bondTimeout) external onlyOwner { bondTimeout = _bondTimeout; } @@ -162,7 +162,7 @@ contract ModeratedEvidenceModule is IArbitrableV2 { function changeDisputeTemplate( string calldata _templateData, string memory _templateDataMappings - ) external onlyGovernor { + ) external onlyOwner { ArbitratorData storage arbitratorData = arbitratorDataList[arbitratorDataList.length - 1]; uint256 newDisputeTemplateId = templateRegistry.setDisputeTemplate("", _templateData, _templateDataMappings); arbitratorDataList.push( @@ -175,7 +175,7 @@ contract ModeratedEvidenceModule is IArbitrableV2 { /// @dev Change the arbitrator to be used for disputes that may be raised in the next requests. The arbitrator is trusted to support appeal period and not reenter. /// @param _arbitratorExtraData The extra data used by the new arbitrator. - function changeArbitratorExtraData(bytes calldata _arbitratorExtraData) external onlyGovernor { + function changeArbitratorExtraData(bytes calldata _arbitratorExtraData) external onlyOwner { ArbitratorData storage arbitratorData = arbitratorDataList[arbitratorDataList.length - 1]; arbitratorDataList.push( ArbitratorData({ diff --git a/contracts/src/arbitration/university/KlerosCoreUniversity.sol b/contracts/src/arbitration/university/KlerosCoreUniversity.sol index 0cd11b2f1..d6366753a 100644 --- a/contracts/src/arbitration/university/KlerosCoreUniversity.sol +++ b/contracts/src/arbitration/university/KlerosCoreUniversity.sol @@ -89,7 +89,7 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { 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. + address public owner; // The owner of the contract. address public instructor; // The instructor who is allowed to choose the jurors. IERC20 public pinakion; // The Pinakion token contract. address public jurorProsecutionModule; // The module for juror's prosecution. @@ -162,8 +162,8 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { // * Function Modifiers * // // ************************************* // - modifier onlyByGovernor() { - if (governor != msg.sender) revert GovernorOnly(); + modifier onlyByOwner() { + if (owner != msg.sender) revert OwnerOnly(); _; } @@ -172,8 +172,8 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { _; } - modifier onlyByGovernorOrInstructor() { - if (msg.sender != governor && msg.sender != instructor) revert GovernorOrInstructorOnly(); + modifier onlyByOwnerOrInstructor() { + if (msg.sender != owner && msg.sender != instructor) revert OwnerOrInstructorOnly(); _; } @@ -187,7 +187,7 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { } /// @dev Initializer (constructor equivalent for upgradable contracts). - /// @param _governor The governor's address. + /// @param _owner The owner's address. /// @param _instructor The address of the instructor. /// @param _pinakion The address of the token contract. /// @param _jurorProsecutionModule The address of the juror prosecution module. @@ -197,7 +197,7 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { /// @param _timesPerPeriod The `timesPerPeriod` property value of the general court. /// @param _sortitionModuleAddress The sortition module responsible for sortition of the jurors. function initialize( - address _governor, + address _owner, address _instructor, IERC20 _pinakion, address _jurorProsecutionModule, @@ -207,7 +207,7 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { uint256[4] memory _timesPerPeriod, ISortitionModuleUniversity _sortitionModuleAddress ) external reinitializer(1) { - governor = _governor; + owner = _owner; instructor = _instructor; pinakion = _pinakion; jurorProsecutionModule = _jurorProsecutionModule; @@ -255,59 +255,55 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { // ************************************* // /* @dev Access Control to perform implementation upgrades (UUPS Proxiable) - * @dev Only the governor can perform upgrades (`onlyByGovernor`) + * @dev Only the owner can perform upgrades (`onlyByOwner`) */ - function _authorizeUpgrade(address) internal view override onlyByGovernor { + function _authorizeUpgrade(address) internal view override onlyByOwner { // NOP } - /// @dev Allows the governor to call anything on behalf of the contract. + /// @dev Allows the owner to call anything on behalf of the contract. /// @param _destination The destination of the call. /// @param _amount The value sent with the call. /// @param _data The data sent with the call. - function executeGovernorProposal( - address _destination, - uint256 _amount, - bytes memory _data - ) external onlyByGovernor { + function executeOwnerProposal(address _destination, uint256 _amount, bytes memory _data) external onlyByOwner { (bool success, ) = _destination.call{value: _amount}(_data); if (!success) revert UnsuccessfulCall(); } - /// @dev Changes the `governor` storage variable. - /// @param _governor The new value for the `governor` storage variable. - function changeGovernor(address payable _governor) external onlyByGovernor { - governor = _governor; + /// @dev Changes the `owner` storage variable. + /// @param _owner The new value for the `owner` storage variable. + function changeOwner(address payable _owner) external onlyByOwner { + owner = _owner; } /// @dev Changes the `instructor` storage variable. /// @param _instructor The new value for the `instructor` storage variable. - function changeInstructor(address _instructor) external onlyByGovernorOrInstructor { + function changeInstructor(address _instructor) external onlyByOwnerOrInstructor { instructor = _instructor; } /// @dev Changes the `pinakion` storage variable. /// @param _pinakion The new value for the `pinakion` storage variable. - function changePinakion(IERC20 _pinakion) external onlyByGovernor { + function changePinakion(IERC20 _pinakion) external onlyByOwner { pinakion = _pinakion; } /// @dev Changes the `jurorProsecutionModule` storage variable. /// @param _jurorProsecutionModule The new value for the `jurorProsecutionModule` storage variable. - function changeJurorProsecutionModule(address _jurorProsecutionModule) external onlyByGovernor { + function changeJurorProsecutionModule(address _jurorProsecutionModule) external onlyByOwner { jurorProsecutionModule = _jurorProsecutionModule; } /// @dev Changes the `_sortitionModule` storage variable. /// Note that the new module should be initialized for all courts. /// @param _sortitionModule The new value for the `sortitionModule` storage variable. - function changeSortitionModule(ISortitionModuleUniversity _sortitionModule) external onlyByGovernor { + function changeSortitionModule(ISortitionModuleUniversity _sortitionModule) external onlyByOwner { sortitionModule = _sortitionModule; } /// @dev Add a new supported dispute kit module to the court. /// @param _disputeKitAddress The address of the dispute kit contract. - function addNewDisputeKit(IDisputeKit _disputeKitAddress) external onlyByGovernor { + function addNewDisputeKit(IDisputeKit _disputeKitAddress) external onlyByOwner { uint256 disputeKitID = disputeKits.length; disputeKits.push(_disputeKitAddress); emit DisputeKitCreated(disputeKitID, _disputeKitAddress); @@ -331,7 +327,7 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { uint256 _jurorsForCourtJump, uint256[4] memory _timesPerPeriod, uint256[] memory _supportedDisputeKits - ) external onlyByGovernor { + ) external onlyByOwner { if (courts[_parent].minStake > _minStake) revert MinStakeLowerThanParentCourt(); if (_supportedDisputeKits.length == 0) revert UnsupportedDisputeKit(); if (_parent == FORKING_COURT) revert InvalidForkingCourtAsParent(); @@ -380,7 +376,7 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { uint256 _feeForJuror, uint256 _jurorsForCourtJump, uint256[4] memory _timesPerPeriod - ) external onlyByGovernor { + ) external onlyByOwner { Court storage court = courts[_courtID]; if (_courtID != GENERAL_COURT && courts[court.parent].minStake > _minStake) { revert MinStakeLowerThanParentCourt(); @@ -411,7 +407,7 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { /// @param _courtID The ID of the court. /// @param _disputeKitIDs The IDs of dispute kits which support should be added/removed. /// @param _enable Whether add or remove the dispute kits from the court. - function enableDisputeKits(uint96 _courtID, uint256[] memory _disputeKitIDs, bool _enable) external onlyByGovernor { + function enableDisputeKits(uint96 _courtID, uint256[] memory _disputeKitIDs, bool _enable) external onlyByOwner { for (uint256 i = 0; i < _disputeKitIDs.length; i++) { if (_enable) { if (_disputeKitIDs[i] == 0 || _disputeKitIDs[i] >= disputeKits.length) { @@ -431,7 +427,7 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { /// @dev Changes the supported fee tokens. /// @param _feeToken The fee token. /// @param _accepted Whether the token is supported or not as a method of fee payment. - function changeAcceptedFeeTokens(IERC20 _feeToken, bool _accepted) external onlyByGovernor { + function changeAcceptedFeeTokens(IERC20 _feeToken, bool _accepted) external onlyByOwner { currencyRates[_feeToken].feePaymentAccepted = _accepted; emit AcceptedFeeToken(_feeToken, _accepted); } @@ -440,7 +436,7 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { /// @param _feeToken The fee token. /// @param _rateInEth The new rate of the fee token in ETH. /// @param _rateDecimals The new decimals of the fee token rate. - function changeCurrencyRates(IERC20 _feeToken, uint64 _rateInEth, uint8 _rateDecimals) external onlyByGovernor { + function changeCurrencyRates(IERC20 _feeToken, uint64 _rateInEth, uint8 _rateDecimals) external onlyByOwner { currencyRates[_feeToken].rateInEth = _rateInEth; currencyRates[_feeToken].rateDecimals = _rateDecimals; emit NewCurrencyRate(_feeToken, _rateInEth, _rateDecimals); @@ -588,7 +584,7 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { /// @dev Draws one juror for the dispute until the number votes paid for is reached. /// @param _disputeID The ID of the dispute. /// @param _juror The address of the juror to draw. - function draw(uint256 _disputeID, address _juror) external onlyByGovernorOrInstructor { + function draw(uint256 _disputeID, address _juror) external onlyByOwnerOrInstructor { Dispute storage dispute = disputes[_disputeID]; uint256 currentRound = dispute.rounds.length - 1; Round storage round = dispute.rounds[currentRound]; @@ -790,15 +786,15 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { sortitionModule.setJurorInactive(account); } if (_params.repartition == _params.numberOfVotesInRound - 1 && _params.coherentCount == 0) { - // No one was coherent, send the rewards to the governor. + // No one was coherent, send the rewards to the owner. if (round.feeToken == NATIVE_CURRENCY) { // The dispute fees were paid in ETH - payable(governor).send(round.totalFeesForJurors); + payable(owner).send(round.totalFeesForJurors); } else { // The dispute fees were paid in ERC20 - round.feeToken.safeTransfer(governor, round.totalFeesForJurors); + round.feeToken.safeTransfer(owner, round.totalFeesForJurors); } - pinakion.safeTransfer(governor, _params.pnkPenaltiesInRound); + pinakion.safeTransfer(owner, _params.pnkPenaltiesInRound); emit LeftoverRewardSent( _params.disputeID, _params.round, @@ -870,21 +866,21 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { round.feeToken ); - // Transfer any residual rewards to the governor. It may happen due to partial coherence of the jurors. + // Transfer any residual rewards to the owner. It may happen due to partial coherence of the jurors. if (_params.repartition == _params.numberOfVotesInRound * 2 - 1) { uint256 leftoverPnkReward = _params.pnkPenaltiesInRound - round.sumPnkRewardPaid; uint256 leftoverFeeReward = round.totalFeesForJurors - round.sumFeeRewardPaid; if (leftoverPnkReward != 0 || leftoverFeeReward != 0) { if (leftoverPnkReward != 0) { - pinakion.safeTransfer(governor, leftoverPnkReward); + pinakion.safeTransfer(owner, leftoverPnkReward); } if (leftoverFeeReward != 0) { if (round.feeToken == NATIVE_CURRENCY) { // The dispute fees were paid in ETH - payable(governor).send(leftoverFeeReward); + payable(owner).send(leftoverFeeReward); } else { // The dispute fees were paid in ERC20 - round.feeToken.safeTransfer(governor, leftoverFeeReward); + round.feeToken.safeTransfer(owner, leftoverFeeReward); } } emit LeftoverRewardSent( @@ -1157,9 +1153,9 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { // * Errors * // // ************************************* // - error GovernorOnly(); + error OwnerOnly(); error InstructorOnly(); - error GovernorOrInstructorOnly(); + error OwnerOrInstructorOnly(); error DisputeKitOnly(); error SortitionModuleOnly(); error UnsuccessfulCall(); diff --git a/contracts/src/arbitration/university/SortitionModuleUniversity.sol b/contracts/src/arbitration/university/SortitionModuleUniversity.sol index e32ca5a77..a7a049bb0 100644 --- a/contracts/src/arbitration/university/SortitionModuleUniversity.sol +++ b/contracts/src/arbitration/university/SortitionModuleUniversity.sol @@ -29,7 +29,7 @@ contract SortitionModuleUniversity is ISortitionModuleUniversity, UUPSProxiable, // * Storage * // // ************************************* // - address public governor; // The governor of the contract. + address public owner; // The owner of the contract. KlerosCoreUniversity public core; // The core arbitrator contract. uint256 public disputesWithoutJurors; // The number of disputes that have not finished drawing jurors. mapping(address account => Juror) public jurors; // The jurors. @@ -66,8 +66,8 @@ contract SortitionModuleUniversity is ISortitionModuleUniversity, UUPSProxiable, // * Function Modifiers * // // ************************************* // - modifier onlyByGovernor() { - if (governor != msg.sender) revert GovernorOnly(); + modifier onlyByOwner() { + if (owner != msg.sender) revert OwnerOnly(); _; } @@ -87,8 +87,8 @@ contract SortitionModuleUniversity is ISortitionModuleUniversity, UUPSProxiable, /// @dev Initializer (constructor equivalent for upgradable contracts). /// @param _core The KlerosCore. - function initialize(address _governor, KlerosCoreUniversity _core) external reinitializer(1) { - governor = _governor; + function initialize(address _owner, KlerosCoreUniversity _core) external reinitializer(1) { + owner = _owner; core = _core; } @@ -98,9 +98,9 @@ contract SortitionModuleUniversity is ISortitionModuleUniversity, UUPSProxiable, /** * @dev Access Control to perform implementation upgrades (UUPS Proxiable) - * @dev Only the governor can perform upgrades (`onlyByGovernor`) + * @dev Only the owner can perform upgrades (`onlyByOwner`) */ - function _authorizeUpgrade(address) internal view override onlyByGovernor { + function _authorizeUpgrade(address) internal view override onlyByOwner { // NOP } @@ -403,7 +403,7 @@ contract SortitionModuleUniversity is ISortitionModuleUniversity, UUPSProxiable, // * Errors * // // ************************************* // - error GovernorOnly(); + error OwnerOnly(); error KlerosCoreOnly(); error NotEligibleForWithdrawal(); } diff --git a/contracts/src/arbitration/view/KlerosCoreSnapshotProxy.sol b/contracts/src/arbitration/view/KlerosCoreSnapshotProxy.sol index 74cdb84ce..70c3c77ce 100644 --- a/contracts/src/arbitration/view/KlerosCoreSnapshotProxy.sol +++ b/contracts/src/arbitration/view/KlerosCoreSnapshotProxy.sol @@ -16,7 +16,7 @@ contract KlerosCoreSnapshotProxy { // ************************************* // IKlerosCore public core; - address public governor; + address public owner; string public constant name = "Staked Pinakion"; string public constant symbol = "stPNK"; uint8 public constant decimals = 18; @@ -25,8 +25,8 @@ contract KlerosCoreSnapshotProxy { // * Modifiers * // // ************************************* // - modifier onlyByGovernor() { - if (governor != msg.sender) revert GovernorOnly(); + modifier onlyByOwner() { + if (owner != msg.sender) revert OwnerOnly(); _; } @@ -35,10 +35,10 @@ contract KlerosCoreSnapshotProxy { // ************************************* // /// @dev Constructor - /// @param _governor The governor of the contract. + /// @param _owner The owner of the contract. /// @param _core KlerosCore to read the balance from. - constructor(address _governor, IKlerosCore _core) { - governor = _governor; + constructor(address _owner, IKlerosCore _core) { + owner = _owner; core = _core; } @@ -46,15 +46,15 @@ contract KlerosCoreSnapshotProxy { // * Governance * // // ************************************* // - /// @dev Changes the `governor` storage variable. - /// @param _governor The new value for the `governor` storage variable. - function changeGovernor(address _governor) external onlyByGovernor { - governor = _governor; + /// @dev Changes the `owner` storage variable. + /// @param _owner The new value for the `owner` storage variable. + function changeOwner(address _owner) external onlyByOwner { + owner = _owner; } /// @dev Changes the `core` storage variable. /// @param _core The new value for the `core` storage variable. - function changeCore(IKlerosCore _core) external onlyByGovernor { + function changeCore(IKlerosCore _core) external onlyByOwner { core = _core; } @@ -74,5 +74,5 @@ contract KlerosCoreSnapshotProxy { // * Errors * // // ************************************* // - error GovernorOnly(); + error OwnerOnly(); } diff --git a/contracts/src/gateway/ForeignGateway.sol b/contracts/src/gateway/ForeignGateway.sol index 1e615936f..712790d0d 100644 --- a/contracts/src/gateway/ForeignGateway.sol +++ b/contracts/src/gateway/ForeignGateway.sol @@ -36,7 +36,7 @@ contract ForeignGateway is IForeignGateway, UUPSProxiable, Initializable { uint256 internal localDisputeID; // The disputeID must start from 1 as the KlerosV1 proxy governor depends on this implementation. We now also depend on localDisputeID not ever being zero. mapping(uint96 courtId => uint256) public feeForJuror; // feeForJuror[v2CourtID], it mirrors the value on KlerosCore. - address public governor; + address public owner; address public veaOutbox; uint256 public override homeChainID; address public override homeGateway; @@ -57,8 +57,8 @@ contract ForeignGateway is IForeignGateway, UUPSProxiable, Initializable { _; } - modifier onlyByGovernor() { - if (governor != msg.sender) revert GovernorOnly(); + modifier onlyByOwner() { + if (owner != msg.sender) revert OwnerOnly(); _; } @@ -72,17 +72,17 @@ contract ForeignGateway is IForeignGateway, UUPSProxiable, Initializable { } /// @dev Constructs the `PolicyRegistry` contract. - /// @param _governor The governor's address. + /// @param _owner The owner's address. /// @param _veaOutbox The address of the VeaOutbox. /// @param _homeChainID The chainID of the home chain. /// @param _homeGateway The address of the home gateway. function initialize( - address _governor, + address _owner, address _veaOutbox, uint256 _homeChainID, address _homeGateway ) external reinitializer(1) { - governor = _governor; + owner = _owner; veaOutbox = _veaOutbox; homeChainID = _homeChainID; homeGateway = _homeGateway; @@ -95,23 +95,23 @@ contract ForeignGateway is IForeignGateway, UUPSProxiable, Initializable { /** * @dev Access Control to perform implementation upgrades (UUPS Proxiable) - * @dev Only the governor can perform upgrades (`onlyByGovernor`) + * @dev Only the owner can perform upgrades (`onlyByOwner`) */ - function _authorizeUpgrade(address) internal view override onlyByGovernor { + function _authorizeUpgrade(address) internal view override onlyByOwner { // NOP } - /// @dev Changes the governor. - /// @param _governor The address of the new governor. - function changeGovernor(address _governor) external { - if (governor != msg.sender) revert GovernorOnly(); - governor = _governor; + /// @dev Changes the owner. + /// @param _owner The address of the new owner. + function changeOwner(address _owner) external { + if (owner != msg.sender) revert OwnerOnly(); + owner = _owner; } /// @dev Changes the outbox. /// @param _veaOutbox The address of the new outbox. /// @param _gracePeriod The duration to accept messages from the deprecated bridge (if at all). - function changeVea(address _veaOutbox, uint256 _gracePeriod) external onlyByGovernor { + function changeVea(address _veaOutbox, uint256 _gracePeriod) external onlyByOwner { // grace period to relay the remaining messages which are still going through the deprecated bridge. deprecatedVeaOutboxExpiration = block.timestamp + _gracePeriod; deprecatedVeaOutbox = veaOutbox; @@ -121,14 +121,14 @@ 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 { - if (governor != msg.sender) revert GovernorOnly(); + if (owner != msg.sender) revert OwnerOnly(); homeGateway = _homeGateway; } /// @dev Changes the `feeForJuror` property value of a specified court. /// @param _courtID The ID of the court on the v2 arbitrator. Not to be confused with the courtID on KlerosLiquid. /// @param _feeForJuror The new value for the `feeForJuror` property value. - function changeCourtJurorFee(uint96 _courtID, uint256 _feeForJuror) external onlyByGovernor { + function changeCourtJurorFee(uint96 _courtID, uint256 _feeForJuror) external onlyByOwner { feeForJuror[_courtID] = _feeForJuror; emit ArbitrationCostModified(_courtID, _feeForJuror); } @@ -272,7 +272,7 @@ contract ForeignGateway is IForeignGateway, UUPSProxiable, Initializable { // * Errors * // // ************************************* // - error GovernorOnly(); + error OwnerOnly(); error HomeGatewayMessageSenderOnly(); error VeaOutboxOnly(); error ArbitrationFeesNotEnough(); diff --git a/contracts/src/gateway/HomeGateway.sol b/contracts/src/gateway/HomeGateway.sol index 2ef8e606f..d0062dc52 100644 --- a/contracts/src/gateway/HomeGateway.sol +++ b/contracts/src/gateway/HomeGateway.sol @@ -29,7 +29,7 @@ contract HomeGateway is IHomeGateway, UUPSProxiable, Initializable { // * Storage * // // ************************************* // - address public governor; + address public owner; IArbitratorV2 public arbitrator; IVeaInbox public veaInbox; uint256 public override foreignChainID; @@ -43,9 +43,9 @@ contract HomeGateway is IHomeGateway, UUPSProxiable, Initializable { // * Function Modifiers * // // ************************************* // - /// @dev Requires that the sender is the governor. - modifier onlyByGovernor() { - if (governor != msg.sender) revert GovernorOnly(); + /// @dev Requires that the sender is the owner. + modifier onlyByOwner() { + if (owner != msg.sender) revert OwnerOnly(); _; } @@ -59,21 +59,21 @@ contract HomeGateway is IHomeGateway, UUPSProxiable, Initializable { } /// @dev Constructs the `PolicyRegistry` contract. - /// @param _governor The governor's address. + /// @param _owner The owner's address. /// @param _arbitrator The address of the arbitrator. /// @param _veaInbox The address of the vea inbox. /// @param _foreignChainID The ID of the foreign chain. /// @param _foreignGateway The address of the foreign gateway. /// @param _feeToken The address of the fee token. function initialize( - address _governor, + address _owner, IArbitratorV2 _arbitrator, IVeaInbox _veaInbox, uint256 _foreignChainID, address _foreignGateway, IERC20 _feeToken ) external reinitializer(1) { - governor = _governor; + owner = _owner; arbitrator = _arbitrator; veaInbox = _veaInbox; foreignChainID = _foreignChainID; @@ -87,39 +87,39 @@ contract HomeGateway is IHomeGateway, UUPSProxiable, Initializable { /** * @dev Access Control to perform implementation upgrades (UUPS Proxiable) - * @dev Only the governor can perform upgrades (`onlyByGovernor`) + * @dev Only the owner can perform upgrades (`onlyByOwner`) */ - function _authorizeUpgrade(address) internal view override onlyByGovernor { + function _authorizeUpgrade(address) internal view override onlyByOwner { // NOP } - /// @dev Changes the governor. - /// @param _governor The address of the new governor. - function changeGovernor(address _governor) external onlyByGovernor { - governor = _governor; + /// @dev Changes the owner. + /// @param _owner The address of the new owner. + function changeOwner(address _owner) external onlyByOwner { + owner = _owner; } /// @dev Changes the arbitrator. /// @param _arbitrator The address of the new arbitrator. - function changeArbitrator(IArbitratorV2 _arbitrator) external onlyByGovernor { + function changeArbitrator(IArbitratorV2 _arbitrator) external onlyByOwner { arbitrator = _arbitrator; } /// @dev Changes the vea inbox, useful to increase the claim deposit. /// @param _veaInbox The address of the new vea inbox. - function changeVea(IVeaInbox _veaInbox) external onlyByGovernor { + function changeVea(IVeaInbox _veaInbox) external onlyByOwner { veaInbox = _veaInbox; } /// @dev Changes the foreign gateway. /// @param _foreignGateway The address of the new foreign gateway. - function changeForeignGateway(address _foreignGateway) external onlyByGovernor { + function changeForeignGateway(address _foreignGateway) external onlyByOwner { foreignGateway = _foreignGateway; } /// @dev Changes the fee token. /// @param _feeToken The address of the new fee token. - function changeFeeToken(IERC20 _feeToken) external onlyByGovernor { + function changeFeeToken(IERC20 _feeToken) external onlyByOwner { feeToken = _feeToken; } @@ -239,7 +239,7 @@ contract HomeGateway is IHomeGateway, UUPSProxiable, Initializable { // * Errors * // // ************************************* // - error GovernorOnly(); + error OwnerOnly(); error ArbitratorOnly(); error FeesPaidInERC20Only(); error FeesPaidInNativeCurrencyOnly(); diff --git a/contracts/src/kleros-v1/interfaces/IKlerosLiquid.sol b/contracts/src/kleros-v1/interfaces/IKlerosLiquid.sol index 0422c95b3..56d3c7789 100644 --- a/contracts/src/kleros-v1/interfaces/IKlerosLiquid.sol +++ b/contracts/src/kleros-v1/interfaces/IKlerosLiquid.sol @@ -77,7 +77,7 @@ interface IKlerosLiquid is IArbitratorV1 { function changeSubcourtTimesPerPeriod(uint96 _subcourtID, uint256[4] calldata _timesPerPeriod) external; - function executeGovernorProposal(address _destination, uint256 _amount, bytes calldata _data) external; + function executeOwnerProposal(address _destination, uint256 _amount, bytes calldata _data) external; // Getters function getVote( diff --git a/contracts/src/kleros-v1/kleros-liquid-xdai/xKlerosLiquidV2.sol b/contracts/src/kleros-v1/kleros-liquid-xdai/xKlerosLiquidV2.sol index 9a25909b7..59ff80353 100644 --- a/contracts/src/kleros-v1/kleros-liquid-xdai/xKlerosLiquidV2.sol +++ b/contracts/src/kleros-v1/kleros-liquid-xdai/xKlerosLiquidV2.sol @@ -142,7 +142,7 @@ contract xKlerosLiquidV2 is Initializable, ITokenController, IArbitratorV2 { 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. // General Contracts - address public governor; // The governor of the contract. + address public owner; // The owner of the contract. WrappedPinakion public pinakion; // The Pinakion token contract. IRandomAuRa public RNGenerator; // The random number generator contract. // General Dynamic @@ -197,9 +197,9 @@ contract xKlerosLiquidV2 is Initializable, ITokenController, IArbitratorV2 { _; } - /// @dev Requires that the sender is the governor. Note that the governor is expected to not be malicious. - modifier onlyByGovernor() { - require(governor == msg.sender); + /// @dev Requires that the sender is the owner. Note that the owner is expected to not be malicious. + modifier onlyByOwner() { + require(owner == msg.sender); _; } @@ -208,7 +208,7 @@ contract xKlerosLiquidV2 is Initializable, ITokenController, IArbitratorV2 { // ************************************* // /// @dev Constructs the KlerosLiquid contract. - /// @param _governor The governor's address. + /// @param _owner The owner's address. /// @param _pinakion The address of the token contract. /// @param _RNGenerator The address of the random number generator contract. /// @param _minStakingTime The minimum time that the staking phase should last. @@ -219,7 +219,7 @@ contract xKlerosLiquidV2 is Initializable, ITokenController, IArbitratorV2 { /// @param _sortitionSumTreeK The number of children per node of the general court's sortition sum tree. /// @param _foreignGateway Foreign gateway on xDai. function initialize( - address _governor, + address _owner, WrappedPinakion _pinakion, IRandomAuRa _RNGenerator, uint256 _minStakingTime, @@ -231,7 +231,7 @@ contract xKlerosLiquidV2 is Initializable, ITokenController, IArbitratorV2 { IForeignGateway _foreignGateway ) public initializer { // Initialize contract. - governor = _governor; + owner = _owner; pinakion = _pinakion; RNGenerator = _RNGenerator; minStakingTime = _minStakingTime; @@ -264,34 +264,30 @@ contract xKlerosLiquidV2 is Initializable, ITokenController, IArbitratorV2 { // * Governance * // // ************************************* // - /// @dev Lets the governor call anything on behalf of the contract. + /// @dev Lets the owner call anything on behalf of the contract. /// @param _destination The destination of the call. /// @param _amount The value sent with the call. /// @param _data The data sent with the call. - function executeGovernorProposal( - address _destination, - uint256 _amount, - bytes memory _data - ) external onlyByGovernor { + function executeOwnerProposal(address _destination, uint256 _amount, bytes memory _data) external onlyByOwner { (bool success, ) = _destination.call{value: _amount}(_data); require(success, "Unsuccessful call"); } - /// @dev Changes the `governor` storage variable. - /// @param _governor The new value for the `governor` storage variable. - function changeGovernor(address _governor) external onlyByGovernor { - governor = _governor; + /// @dev Changes the `owner` storage variable. + /// @param _owner The new value for the `owner` storage variable. + function changeOwner(address _owner) external onlyByOwner { + owner = _owner; } /// @dev Changes the `pinakion` storage variable. /// @param _pinakion The new value for the `pinakion` storage variable. - function changePinakion(WrappedPinakion _pinakion) external onlyByGovernor { + function changePinakion(WrappedPinakion _pinakion) external onlyByOwner { pinakion = _pinakion; } /// @dev Changes the `RNGenerator` storage variable. /// @param _RNGenerator The new value for the `RNGenerator` storage variable. - function changeRNGenerator(IRandomAuRa _RNGenerator) external onlyByGovernor { + function changeRNGenerator(IRandomAuRa _RNGenerator) external onlyByOwner { RNGenerator = _RNGenerator; if (phase == Phase.generating) { RNBlock = RNGenerator.nextCommitPhaseStartBlock() + RNGenerator.collectRoundLength(); @@ -300,19 +296,19 @@ contract xKlerosLiquidV2 is Initializable, ITokenController, IArbitratorV2 { /// @dev Changes the `minStakingTime` storage variable. /// @param _minStakingTime The new value for the `minStakingTime` storage variable. - function changeMinStakingTime(uint256 _minStakingTime) external onlyByGovernor { + function changeMinStakingTime(uint256 _minStakingTime) external onlyByOwner { minStakingTime = _minStakingTime; } /// @dev Changes the `maxDrawingTime` storage variable. /// @param _maxDrawingTime The new value for the `maxDrawingTime` storage variable. - function changeMaxDrawingTime(uint256 _maxDrawingTime) external onlyByGovernor { + function changeMaxDrawingTime(uint256 _maxDrawingTime) external onlyByOwner { maxDrawingTime = _maxDrawingTime; } /// @dev Changes the `foreignGateway` storage variable. /// @param _foreignGateway The new value for the `foreignGateway` storage variable. - function changeForeignGateway(IForeignGateway _foreignGateway) external onlyByGovernor { + function changeForeignGateway(IForeignGateway _foreignGateway) external onlyByOwner { foreignGateway = _foreignGateway; } @@ -334,7 +330,7 @@ contract xKlerosLiquidV2 is Initializable, ITokenController, IArbitratorV2 { uint256 _jurorsForCourtJump, uint256[4] memory _timesPerPeriod, uint256 _sortitionSumTreeK - ) external onlyByGovernor { + ) external onlyByOwner { require( courts[_parent].minStake <= _minStake, "A subcourt cannot be a child of a subcourt with a higher minimum stake." @@ -360,7 +356,7 @@ contract xKlerosLiquidV2 is Initializable, ITokenController, IArbitratorV2 { /// @dev Changes the `minStake` property value of a specified subcourt. Don't set to a value lower than its parent's `minStake` property value. /// @param _subcourtID The ID of the subcourt. /// @param _minStake The new value for the `minStake` property value. - function changeSubcourtMinStake(uint96 _subcourtID, uint256 _minStake) external onlyByGovernor { + function changeSubcourtMinStake(uint96 _subcourtID, uint256 _minStake) external onlyByOwner { require(_subcourtID == 0 || courts[courts[_subcourtID].parent].minStake <= _minStake); for (uint256 i = 0; i < courts[_subcourtID].children.length; i++) { require( @@ -375,31 +371,28 @@ contract xKlerosLiquidV2 is Initializable, ITokenController, IArbitratorV2 { /// @dev Changes the `alpha` property value of a specified subcourt. /// @param _subcourtID The ID of the subcourt. /// @param _alpha The new value for the `alpha` property value. - function changeSubcourtAlpha(uint96 _subcourtID, uint256 _alpha) external onlyByGovernor { + function changeSubcourtAlpha(uint96 _subcourtID, uint256 _alpha) external onlyByOwner { courts[_subcourtID].alpha = _alpha; } /// @dev Changes the `feeForJuror` property value of a specified subcourt. /// @param _subcourtID The ID of the subcourt. /// @param _feeForJuror The new value for the `feeForJuror` property value. - function changeSubcourtJurorFee(uint96 _subcourtID, uint256 _feeForJuror) external onlyByGovernor { + function changeSubcourtJurorFee(uint96 _subcourtID, uint256 _feeForJuror) external onlyByOwner { courts[_subcourtID].feeForJuror = _feeForJuror; } /// @dev Changes the `jurorsForCourtJump` property value of a specified subcourt. /// @param _subcourtID The ID of the subcourt. /// @param _jurorsForCourtJump The new value for the `jurorsForCourtJump` property value. - function changeSubcourtJurorsForJump(uint96 _subcourtID, uint256 _jurorsForCourtJump) external onlyByGovernor { + function changeSubcourtJurorsForJump(uint96 _subcourtID, uint256 _jurorsForCourtJump) external onlyByOwner { courts[_subcourtID].jurorsForCourtJump = _jurorsForCourtJump; } /// @dev Changes the `timesPerPeriod` property value of a specified subcourt. /// @param _subcourtID The ID of the subcourt. /// @param _timesPerPeriod The new value for the `timesPerPeriod` property value. - function changeSubcourtTimesPerPeriod( - uint96 _subcourtID, - uint256[4] memory _timesPerPeriod - ) external onlyByGovernor { + function changeSubcourtTimesPerPeriod(uint96 _subcourtID, uint256[4] memory _timesPerPeriod) external onlyByOwner { courts[_subcourtID].timesPerPeriod = _timesPerPeriod; } diff --git a/contracts/src/kleros-v1/kleros-liquid/KlerosLiquidToV2Governor.sol b/contracts/src/kleros-v1/kleros-liquid/KlerosLiquidToV2Governor.sol index 509b85101..def339931 100644 --- a/contracts/src/kleros-v1/kleros-liquid/KlerosLiquidToV2Governor.sol +++ b/contracts/src/kleros-v1/kleros-liquid/KlerosLiquidToV2Governor.sol @@ -26,7 +26,7 @@ contract KlerosLiquidToV2Governor is IArbitrableV2, ITokenController { IArbitratorV2 public immutable foreignGateway; IKlerosLiquid public immutable klerosLiquid; - address public governor; + address public owner; mapping(uint256 disputeId => uint256 gatewayDisputeId) public klerosLiquidDisputeIDtoGatewayDisputeID; mapping(uint256 gatewayDisputeId => DisputeData) public disputes; // disputes[gatewayDisputeID] mapping(address account => uint256 tokenAmount) public frozenTokens; // frozenTokens[account] locked token which shouldn't have been blocked. @@ -36,8 +36,8 @@ contract KlerosLiquidToV2Governor is IArbitrableV2, ITokenController { // * Function Modifiers * // // ************************************* // - modifier onlyByGovernor() { - require(governor == msg.sender); + modifier onlyByOwner() { + require(owner == msg.sender); _; } @@ -45,13 +45,13 @@ contract KlerosLiquidToV2Governor is IArbitrableV2, ITokenController { // * Constructor * // // ************************************* // - /// @dev Constructor. Before this contract is made the new governor of KlerosLiquid, the evidence period of all subcourts has to be set to uint(-1). + /// @dev Constructor. Before this contract is made the new owner of KlerosLiquid, the evidence period of all subcourts has to be set to uint(-1). /// @param _klerosLiquid The trusted arbitrator to resolve potential disputes. - /// @param _governor The trusted governor of the contract. + /// @param _owner The trusted owner of the contract. /// @param _foreignGateway The trusted gateway that acts as an arbitrator, relaying disputes to v2. - constructor(IKlerosLiquid _klerosLiquid, address _governor, IArbitratorV2 _foreignGateway) { + constructor(IKlerosLiquid _klerosLiquid, address _owner, IArbitratorV2 _foreignGateway) { klerosLiquid = _klerosLiquid; - governor = _governor; + owner = _owner; foreignGateway = _foreignGateway; } @@ -59,23 +59,19 @@ contract KlerosLiquidToV2Governor is IArbitrableV2, ITokenController { // * Governance * // // ************************************* // - /// @dev Lets the governor call anything on behalf of the contract. + /// @dev Lets the owner call anything on behalf of the contract. /// @param _destination The destination of the call. /// @param _amount The value sent with the call. /// @param _data The data sent with the call. - function executeGovernorProposal( - address _destination, - uint256 _amount, - bytes calldata _data - ) external onlyByGovernor { + function executeOwnerProposal(address _destination, uint256 _amount, bytes calldata _data) external onlyByOwner { (bool success, ) = _destination.call{value: _amount}(_data); // solium-disable-line security/no-call-value require(success, "Call execution failed."); } - /// @dev Changes the `governor` storage variable. - /// @param _governor The new value for the `governor` storage variable. - function changeGovernor(address _governor) external onlyByGovernor { - governor = _governor; + /// @dev Changes the `owner` storage variable. + /// @param _owner The new value for the `owner` storage variable. + function changeOwner(address _owner) external onlyByOwner { + owner = _owner; } // ************************************* // @@ -94,7 +90,7 @@ contract KlerosLiquidToV2Governor is IArbitrableV2, ITokenController { require(KlerosLiquidDispute.period == IKlerosLiquid.Period.evidence, "Invalid dispute period."); require(votesLengths.length == 1, "Cannot relay appeals."); - klerosLiquid.executeGovernorProposal(address(this), totalFeesForJurors[0], ""); + klerosLiquid.executeOwnerProposal(address(this), totalFeesForJurors[0], ""); uint256 minJurors = votesLengths[0]; bytes memory extraData = abi.encode(KlerosLiquidDispute.subcourtID, minJurors); @@ -125,7 +121,7 @@ contract KlerosLiquidToV2Governor is IArbitrableV2, ITokenController { IKlerosLiquid.Dispute memory klerosLiquidDispute = klerosLiquid.disputes(dispute.klerosLiquidDisputeID); bytes memory data = abi.encodeCall(IArbitrableV2.rule, (dispute.klerosLiquidDisputeID, _ruling)); - klerosLiquid.executeGovernorProposal(klerosLiquidDispute.arbitrated, 0, data); + klerosLiquid.executeOwnerProposal(klerosLiquidDispute.arbitrated, 0, data); } /// @dev Registers jurors' tokens which where locked due to relaying a given dispute. These tokens don't count as locked. diff --git a/contracts/src/proxy/mock/UUPSUpgradeableMocks.sol b/contracts/src/proxy/mock/UUPSUpgradeableMocks.sol index 0764c8cd1..3b2bc7959 100644 --- a/contracts/src/proxy/mock/UUPSUpgradeableMocks.sol +++ b/contracts/src/proxy/mock/UUPSUpgradeableMocks.sol @@ -20,7 +20,7 @@ contract NonUpgradeableMock { contract UUPSUpgradeableMock is UUPSProxiable, NonUpgradeableMock { bool public initialized; - address public governor; + address public owner; uint256[50] __gap; @@ -28,14 +28,14 @@ contract UUPSUpgradeableMock is UUPSProxiable, NonUpgradeableMock { initialized = true; } - function initialize(address _governor) external { + function initialize(address _owner) external { require(!initialized, "Contract instance has already been initialized"); - governor = _governor; + owner = _owner; initialized = true; } function _authorizeUpgrade(address) internal view override { - require(governor == msg.sender, "No privilege to upgrade"); + require(owner == msg.sender, "No privilege to upgrade"); } function version() external pure virtual override returns (string memory) { diff --git a/contracts/src/proxy/mock/by-inheritance/UpgradedByInheritance.sol b/contracts/src/proxy/mock/by-inheritance/UpgradedByInheritance.sol index 2cb9adf03..c5f796995 100644 --- a/contracts/src/proxy/mock/by-inheritance/UpgradedByInheritance.sol +++ b/contracts/src/proxy/mock/by-inheritance/UpgradedByInheritance.sol @@ -12,7 +12,7 @@ contract UpgradedByInheritanceV1Proxy is UUPSProxy { } contract UpgradedByInheritanceV1 is UUPSProxiable, Initializable { - address public governor; + address public owner; uint256 public counter; uint256[50] __gap; @@ -20,13 +20,13 @@ contract UpgradedByInheritanceV1 is UUPSProxiable, Initializable { _disableInitializers(); } - function initialize(address _governor) external virtual reinitializer(1) { - governor = _governor; + function initialize(address _owner) external virtual reinitializer(1) { + owner = _owner; counter = 1; } function _authorizeUpgrade(address) internal view override { - require(governor == msg.sender, "No privilege to upgrade"); + require(owner == msg.sender, "No privilege to upgrade"); } function increment() external { diff --git a/contracts/src/proxy/mock/by-rewrite/UpgradedByRewrite.sol b/contracts/src/proxy/mock/by-rewrite/UpgradedByRewrite.sol index ca0731da8..f5e5eae77 100644 --- a/contracts/src/proxy/mock/by-rewrite/UpgradedByRewrite.sol +++ b/contracts/src/proxy/mock/by-rewrite/UpgradedByRewrite.sol @@ -15,7 +15,7 @@ contract UpgradedByRewrite is UUPSProxiable, Initializable { //------------------------ // V1 State //------------------------ - address public governor; + address public owner; uint256 public counter; uint256[50] __gap; @@ -23,13 +23,13 @@ contract UpgradedByRewrite is UUPSProxiable, Initializable { _disableInitializers(); } - function initialize(address _governor) external virtual reinitializer(1) { - governor = _governor; + function initialize(address _owner) external virtual reinitializer(1) { + owner = _owner; counter = 1; } function _authorizeUpgrade(address) internal view override { - require(governor == msg.sender, "No privilege to upgrade"); + require(owner == msg.sender, "No privilege to upgrade"); } function increment() external { diff --git a/contracts/src/proxy/mock/by-rewrite/UpgradedByRewriteV2.sol b/contracts/src/proxy/mock/by-rewrite/UpgradedByRewriteV2.sol index 1b3c85e0a..5158c73fb 100644 --- a/contracts/src/proxy/mock/by-rewrite/UpgradedByRewriteV2.sol +++ b/contracts/src/proxy/mock/by-rewrite/UpgradedByRewriteV2.sol @@ -10,7 +10,7 @@ contract UpgradedByRewrite is UUPSProxiable, Initializable { //------------------------ // V1 State //------------------------ - address public governor; + address public owner; uint256 public counter; uint256[50] __gap; @@ -29,7 +29,7 @@ contract UpgradedByRewrite is UUPSProxiable, Initializable { } function _authorizeUpgrade(address) internal view override { - require(governor == msg.sender, "No privilege to upgrade"); + require(owner == msg.sender, "No privilege to upgrade"); } function increment() external { diff --git a/contracts/src/rng/BlockhashRNG.sol b/contracts/src/rng/BlockhashRNG.sol index a36501e6e..d104780f4 100644 --- a/contracts/src/rng/BlockhashRNG.sol +++ b/contracts/src/rng/BlockhashRNG.sol @@ -15,7 +15,7 @@ contract BlockHashRNG is IRNG { // * Storage * // // ************************************* // - address public governor; // The address that can withdraw funds. + address public owner; // 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 @@ -25,8 +25,8 @@ contract BlockHashRNG is IRNG { // * Function Modifiers * // // ************************************* // - modifier onlyByGovernor() { - if (governor != msg.sender) revert GovernorOnly(); + modifier onlyByOwner() { + if (owner != msg.sender) revert OwnerOnly(); _; } @@ -40,11 +40,11 @@ contract BlockHashRNG is IRNG { // ************************************* // /// @dev Constructor. - /// @param _governor The Governor of the contract. + /// @param _owner The Owner 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; + constructor(address _owner, address _consumer, uint256 _lookaheadTime) { + owner = _owner; consumer = _consumer; lookaheadTime = _lookaheadTime; } @@ -53,15 +53,15 @@ contract BlockHashRNG is IRNG { // * Governance * // // ************************************* // - /// @dev Changes the governor of the contract. - /// @param _governor The new governor. - function changeGovernor(address _governor) external onlyByGovernor { - governor = _governor; + /// @dev Changes the owner of the contract. + /// @param _owner The new owner. + function changeOwner(address _owner) external onlyByOwner { + owner = _owner; } /// @dev Changes the consumer of the RNG. /// @param _consumer The new consumer. - function changeConsumer(address _consumer) external onlyByGovernor { + function changeConsumer(address _consumer) external onlyByOwner { consumer = _consumer; } diff --git a/contracts/src/rng/ChainlinkConsumerBaseV2Plus.sol b/contracts/src/rng/ChainlinkConsumerBaseV2Plus.sol new file mode 100644 index 000000000..73df28caf --- /dev/null +++ b/contracts/src/rng/ChainlinkConsumerBaseV2Plus.sol @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +// This contract is adapted from `@chainlink/contracts/src/v0.8/vrf/dev/VRFConsumerBaseV2Plus.sol` to remove the `ConfirmedOwner` dependency. + +import {IVRFCoordinatorV2Plus} from "@chainlink/contracts/src/v0.8/vrf/dev/interfaces/IVRFCoordinatorV2Plus.sol"; +import {IVRFMigratableConsumerV2Plus} from "@chainlink/contracts/src/v0.8/vrf/dev/interfaces/IVRFMigratableConsumerV2Plus.sol"; + +/** **************************************************************************** + * @notice Interface for contracts using VRF randomness + * ***************************************************************************** + * @dev PURPOSE + * + * @dev Reggie the Random Oracle (not his real job) wants to provide randomness + * @dev to Vera the verifier in such a way that Vera can be sure he's not + * @dev making his output up to suit himself. Reggie provides Vera a public key + * @dev to which he knows the secret key. Each time Vera provides a seed to + * @dev Reggie, he gives back a value which is computed completely + * @dev deterministically from the seed and the secret key. + * + * @dev Reggie provides a proof by which Vera can verify that the output was + * @dev correctly computed once Reggie tells it to her, but without that proof, + * @dev the output is indistinguishable to her from a uniform random sample + * @dev from the output space. + * + * @dev The purpose of this contract is to make it easy for unrelated contracts + * @dev to talk to Vera the verifier about the work Reggie is doing, to provide + * @dev simple access to a verifiable source of randomness. It ensures 2 things: + * @dev 1. The fulfillment came from the VRFCoordinatorV2Plus. + * @dev 2. The consumer contract implements fulfillRandomWords. + * ***************************************************************************** + * @dev USAGE + * + * @dev Calling contracts must inherit from VRFConsumerBaseV2Plus, and can + * @dev initialize VRFConsumerBaseV2Plus's attributes in their constructor as + * @dev shown: + * + * @dev contract VRFConsumerV2Plus is VRFConsumerBaseV2Plus { + * @dev constructor(, address _vrfCoordinator, address _subOwner) + * @dev VRFConsumerBaseV2Plus(_vrfCoordinator, _subOwner) public { + * @dev + * @dev } + * @dev } + * + * @dev The oracle will have given you an ID for the VRF keypair they have + * @dev committed to (let's call it keyHash). Create a subscription, fund it + * @dev and your consumer contract as a consumer of it (see VRFCoordinatorInterface + * @dev subscription management functions). + * @dev Call requestRandomWords(keyHash, subId, minimumRequestConfirmations, + * @dev callbackGasLimit, numWords, extraArgs), + * @dev see (IVRFCoordinatorV2Plus for a description of the arguments). + * + * @dev Once the VRFCoordinatorV2Plus has received and validated the oracle's response + * @dev to your request, it will call your contract's fulfillRandomWords method. + * + * @dev The randomness argument to fulfillRandomWords is a set of random words + * @dev generated from your requestId and the blockHash of the request. + * + * @dev If your contract could have concurrent requests open, you can use the + * @dev requestId returned from requestRandomWords to track which response is associated + * @dev with which randomness request. + * @dev See "SECURITY CONSIDERATIONS" for principles to keep in mind, + * @dev if your contract could have multiple requests in flight simultaneously. + * + * @dev Colliding `requestId`s are cryptographically impossible as long as seeds + * @dev differ. + * + * ***************************************************************************** + * @dev SECURITY CONSIDERATIONS + * + * @dev A method with the ability to call your fulfillRandomness method directly + * @dev could spoof a VRF response with any random value, so it's critical that + * @dev it cannot be directly called by anything other than this base contract + * @dev (specifically, by the VRFConsumerBaseV2Plus.rawFulfillRandomness method). + * + * @dev For your users to trust that your contract's random behavior is free + * @dev from malicious interference, it's best if you can write it so that all + * @dev behaviors implied by a VRF response are executed *during* your + * @dev fulfillRandomness method. If your contract must store the response (or + * @dev anything derived from it) and use it later, you must ensure that any + * @dev user-significant behavior which depends on that stored value cannot be + * @dev manipulated by a subsequent VRF request. + * + * @dev Similarly, both miners and the VRF oracle itself have some influence + * @dev over the order in which VRF responses appear on the blockchain, so if + * @dev your contract could have multiple VRF requests in flight simultaneously, + * @dev you must ensure that the order in which the VRF responses arrive cannot + * @dev be used to manipulate your contract's user-significant behavior. + * + * @dev Since the block hash of the block which contains the requestRandomness + * @dev call is mixed into the input to the VRF *last*, a sufficiently powerful + * @dev miner could, in principle, fork the blockchain to evict the block + * @dev containing the request, forcing the request to be included in a + * @dev different block with a different hash, and therefore a different input + * @dev to the VRF. However, such an attack would incur a substantial economic + * @dev cost. This cost scales with the number of blocks the VRF oracle waits + * @dev until it calls responds to a request. It is for this reason that + * @dev that you can signal to an oracle you'd like them to wait longer before + * @dev responding to the request (however this is not enforced in the contract + * @dev and so remains effective only in the case of unmodified oracle software). + */ +abstract contract VRFConsumerBaseV2Plus is IVRFMigratableConsumerV2Plus { + error OnlyCoordinatorCanFulfill(address have, address want); + error OnlyOwnerOrCoordinator(address have, address owner, address coordinator); + error ZeroAddress(); + + address public owner; + + // s_vrfCoordinator should be used by consumers to make requests to vrfCoordinator + // so that coordinator reference is updated after migration + IVRFCoordinatorV2Plus public s_vrfCoordinator; + + /** + * @param _vrfCoordinator address of VRFCoordinator contract + */ + constructor(address _owner, address _vrfCoordinator) { + owner = _owner; + if (_vrfCoordinator == address(0)) { + revert ZeroAddress(); + } + s_vrfCoordinator = IVRFCoordinatorV2Plus(_vrfCoordinator); + } + + /** + * @notice fulfillRandomness handles the VRF response. Your contract must + * @notice implement it. See "SECURITY CONSIDERATIONS" above for important + * @notice principles to keep in mind when implementing your fulfillRandomness + * @notice method. + * + * @dev VRFConsumerBaseV2Plus expects its subcontracts to have a method with this + * @dev signature, and will call it once it has verified the proof + * @dev associated with the randomness. (It is triggered via a call to + * @dev rawFulfillRandomness, below.) + * + * @param requestId The Id initially returned by requestRandomness + * @param randomWords the VRF output expanded to the requested number of words + */ + // solhint-disable-next-line chainlink-solidity/prefix-internal-functions-with-underscore + function fulfillRandomWords(uint256 requestId, uint256[] calldata randomWords) internal virtual; + + // rawFulfillRandomness is called by VRFCoordinator when it receives a valid VRF + // proof. rawFulfillRandomness then calls fulfillRandomness, after validating + // the origin of the call + function rawFulfillRandomWords(uint256 requestId, uint256[] calldata randomWords) external { + if (msg.sender != address(s_vrfCoordinator)) { + revert OnlyCoordinatorCanFulfill(msg.sender, address(s_vrfCoordinator)); + } + fulfillRandomWords(requestId, randomWords); + } + + /** + * @inheritdoc IVRFMigratableConsumerV2Plus + */ + function setCoordinator(address _vrfCoordinator) external override onlyOwnerOrCoordinator { + if (_vrfCoordinator == address(0)) { + revert ZeroAddress(); + } + s_vrfCoordinator = IVRFCoordinatorV2Plus(_vrfCoordinator); + + emit CoordinatorSet(_vrfCoordinator); + } + + modifier onlyOwnerOrCoordinator() { + if (msg.sender != owner && msg.sender != address(s_vrfCoordinator)) { + revert OnlyOwnerOrCoordinator(msg.sender, owner, address(s_vrfCoordinator)); + } + _; + } +} diff --git a/contracts/src/rng/ChainlinkRNG.sol b/contracts/src/rng/ChainlinkRNG.sol index a5bff291f..744bc5248 100644 --- a/contracts/src/rng/ChainlinkRNG.sol +++ b/contracts/src/rng/ChainlinkRNG.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.24; -import {VRFConsumerBaseV2Plus, IVRFCoordinatorV2Plus} from "@chainlink/contracts/src/v0.8/vrf/dev/VRFConsumerBaseV2Plus.sol"; +import {VRFConsumerBaseV2Plus, IVRFCoordinatorV2Plus} from "./ChainlinkConsumerBaseV2Plus.sol"; import {VRFV2PlusClient} from "@chainlink/contracts/src/v0.8/vrf/dev/libraries/VRFV2PlusClient.sol"; import "./IRNG.sol"; @@ -14,7 +14,6 @@ contract ChainlinkRNG is IRNG, VRFConsumerBaseV2Plus { // * Storage * // // ************************************* // - address public governor; // The address that can withdraw funds. 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. @@ -41,8 +40,8 @@ contract ChainlinkRNG is IRNG, VRFConsumerBaseV2Plus { // * Function Modifiers * // // ************************************* // - modifier onlyByGovernor() { - if (governor != msg.sender) revert GovernorOnly(); + modifier onlyByOwner() { + if (owner != msg.sender) revert OwnerOnly(); _; } @@ -56,7 +55,7 @@ contract ChainlinkRNG is IRNG, VRFConsumerBaseV2Plus { // ************************************* // /// @dev Constructor, initializing the implementation to reduce attack surface. - /// @param _governor The Governor of the contract. + /// @param _owner The owner of the 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). @@ -65,15 +64,14 @@ contract ChainlinkRNG is IRNG, VRFConsumerBaseV2Plus { /// @param _callbackGasLimit The limit for how much gas to use for the callback request to the contract's fulfillRandomWords() function. /// @dev https://docs.chain.link/vrf/v2-5/subscription/get-a-random-number constructor( - address _governor, + address _owner, address _consumer, address _vrfCoordinator, bytes32 _keyHash, uint256 _subscriptionId, uint16 _requestConfirmations, uint32 _callbackGasLimit - ) VRFConsumerBaseV2Plus(_vrfCoordinator) { - governor = _governor; + ) VRFConsumerBaseV2Plus(_owner, _vrfCoordinator) { consumer = _consumer; keyHash = _keyHash; subscriptionId = _subscriptionId; @@ -85,46 +83,46 @@ contract ChainlinkRNG is IRNG, VRFConsumerBaseV2Plus { // * Governance * // // ************************************* // - /// @dev Changes the governor of the contract. - /// @param _governor The new governor. - function changeGovernor(address _governor) external onlyByGovernor { - governor = _governor; + /// @dev Changes the owner of the contract. + /// @param _owner The new owner. + function changeOwner(address _owner) external onlyByOwner { + owner = _owner; } /// @dev Changes the consumer of the RNG. /// @param _consumer The new consumer. - function changeConsumer(address _consumer) external onlyByGovernor { + function changeConsumer(address _consumer) external onlyByOwner { consumer = _consumer; } /// @dev Changes the VRF Coordinator of the contract. /// @param _vrfCoordinator The new VRF Coordinator. - function changeVrfCoordinator(address _vrfCoordinator) external onlyByGovernor { + function changeVrfCoordinator(address _vrfCoordinator) external onlyByOwner { s_vrfCoordinator = IVRFCoordinatorV2Plus(_vrfCoordinator); emit CoordinatorSet(_vrfCoordinator); } /// @dev Changes the key hash of the contract. /// @param _keyHash The new key hash. - function changeKeyHash(bytes32 _keyHash) external onlyByGovernor { + function changeKeyHash(bytes32 _keyHash) external onlyByOwner { keyHash = _keyHash; } /// @dev Changes the subscription ID of the contract. /// @param _subscriptionId The new subscription ID. - function changeSubscriptionId(uint256 _subscriptionId) external onlyByGovernor { + function changeSubscriptionId(uint256 _subscriptionId) external onlyByOwner { subscriptionId = _subscriptionId; } /// @dev Changes the request confirmations of the contract. /// @param _requestConfirmations The new request confirmations. - function changeRequestConfirmations(uint16 _requestConfirmations) external onlyByGovernor { + function changeRequestConfirmations(uint16 _requestConfirmations) external onlyByOwner { requestConfirmations = _requestConfirmations; } /// @dev Changes the callback gas limit of the contract. /// @param _callbackGasLimit The new callback gas limit. - function changeCallbackGasLimit(uint32 _callbackGasLimit) external onlyByGovernor { + function changeCallbackGasLimit(uint32 _callbackGasLimit) external onlyByOwner { callbackGasLimit = _callbackGasLimit; } diff --git a/contracts/src/rng/IRNG.sol b/contracts/src/rng/IRNG.sol index 1a767fee0..c407b763f 100644 --- a/contracts/src/rng/IRNG.sol +++ b/contracts/src/rng/IRNG.sol @@ -11,6 +11,6 @@ interface IRNG { /// @return randomNumber Random number or 0 if not available function receiveRandomness() external returns (uint256 randomNumber); - error GovernorOnly(); + error OwnerOnly(); error ConsumerOnly(); } diff --git a/contracts/src/rng/RNGWithFallback.sol b/contracts/src/rng/RNGWithFallback.sol index c47f9eb53..8501d4db9 100644 --- a/contracts/src/rng/RNGWithFallback.sol +++ b/contracts/src/rng/RNGWithFallback.sol @@ -11,7 +11,7 @@ contract RNGWithFallback is IRNG { // ************************************* // IRNG public immutable rng; // RNG address. - address public governor; // Governor address + address public owner; // Owner address address public consumer; // Consumer address uint256 public fallbackTimeoutSeconds; // Time in seconds to wait before falling back to next RNG uint256 public requestTimestamp; // Timestamp of the current request @@ -27,14 +27,14 @@ contract RNGWithFallback is IRNG { // * Constructor * // // ************************************* // - /// @param _governor Governor address + /// @param _owner Owner address /// @param _consumer Consumer address /// @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) { + constructor(address _owner, address _consumer, uint256 _fallbackTimeoutSeconds, IRNG _rng) { if (address(_rng) == address(0)) revert InvalidDefaultRNG(); - governor = _governor; + owner = _owner; consumer = _consumer; fallbackTimeoutSeconds = _fallbackTimeoutSeconds; rng = _rng; @@ -44,8 +44,8 @@ contract RNGWithFallback is IRNG { // * Function Modifiers * // // ************************************* // - modifier onlyByGovernor() { - if (governor != msg.sender) revert GovernorOnly(); + modifier onlyByOwner() { + if (owner != msg.sender) revert OwnerOnly(); _; } @@ -58,21 +58,21 @@ contract RNGWithFallback is IRNG { // * Governance Functions * // // ************************************* // - /// @dev Change the governor - /// @param _newGovernor Address of the new governor - function changeGovernor(address _newGovernor) external onlyByGovernor { - governor = _newGovernor; + /// @dev Change the owner + /// @param _newOwner Address of the new owner + function changeOwner(address _newOwner) external onlyByOwner { + owner = _newOwner; } /// @dev Change the consumer /// @param _consumer Address of the new consumer - function changeConsumer(address _consumer) external onlyByGovernor { + function changeConsumer(address _consumer) external onlyByOwner { consumer = _consumer; } /// @dev Change the fallback timeout /// @param _fallbackTimeoutSeconds New timeout in seconds - function changeFallbackTimeout(uint256 _fallbackTimeoutSeconds) external onlyByGovernor { + function changeFallbackTimeout(uint256 _fallbackTimeoutSeconds) external onlyByOwner { fallbackTimeoutSeconds = _fallbackTimeoutSeconds; emit FallbackTimeoutChanged(_fallbackTimeoutSeconds); } diff --git a/contracts/src/rng/RandomizerRNG.sol b/contracts/src/rng/RandomizerRNG.sol index 96cbe6321..648ce5c3d 100644 --- a/contracts/src/rng/RandomizerRNG.sol +++ b/contracts/src/rng/RandomizerRNG.sol @@ -12,7 +12,7 @@ contract RandomizerRNG is IRNG { // * Storage * // // ************************************* // - address public governor; // The address that can withdraw funds. + address public owner; // The address that can withdraw funds. 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. @@ -36,8 +36,8 @@ contract RandomizerRNG is IRNG { // * Function Modifiers * // // ************************************* // - modifier onlyByGovernor() { - if (governor != msg.sender) revert GovernorOnly(); + modifier onlyByOwner() { + if (owner != msg.sender) revert OwnerOnly(); _; } @@ -51,11 +51,11 @@ contract RandomizerRNG is IRNG { // ************************************* // /// @dev Constructor - /// @param _governor The Governor of the contract. + /// @param _owner The Owner of the contract. /// @param _consumer The address that can request random numbers. /// @param _randomizer The Randomizer.ai oracle contract. - constructor(address _governor, address _consumer, IRandomizer _randomizer) { - governor = _governor; + constructor(address _owner, address _consumer, IRandomizer _randomizer) { + owner = _owner; consumer = _consumer; randomizer = _randomizer; callbackGasLimit = 50000; @@ -65,33 +65,33 @@ contract RandomizerRNG is IRNG { // * Governance * // // ************************ // - /// @dev Changes the governor of the contract. - /// @param _governor The new governor. - function changeGovernor(address _governor) external onlyByGovernor { - governor = _governor; + /// @dev Changes the owner of the contract. + /// @param _owner The new owner. + function changeOwner(address _owner) external onlyByOwner { + owner = _owner; } /// @dev Changes the consumer of the RNG. /// @param _consumer The new consumer. - function changeConsumer(address _consumer) external onlyByGovernor { + function changeConsumer(address _consumer) external onlyByOwner { consumer = _consumer; } /// @dev Change the Randomizer callback gas limit. /// @param _callbackGasLimit the new limit. - function setCallbackGasLimit(uint256 _callbackGasLimit) external onlyByGovernor { + function setCallbackGasLimit(uint256 _callbackGasLimit) external onlyByOwner { callbackGasLimit = _callbackGasLimit; } /// @dev Change the Randomizer address. /// @param _randomizer the new Randomizer address. - function setRandomizer(address _randomizer) external onlyByGovernor { + function setRandomizer(address _randomizer) external onlyByOwner { randomizer = IRandomizer(_randomizer); } - /// @dev Allows the governor to withdraw randomizer funds. + /// @dev Allows the owner to withdraw randomizer funds. /// @param _amount Amount to withdraw in wei. - function randomizerWithdraw(uint256 _amount) external onlyByGovernor { + function randomizerWithdraw(uint256 _amount) external onlyByOwner { randomizer.clientWithdrawTo(msg.sender, _amount); } diff --git a/contracts/src/token/Faucet.sol b/contracts/src/token/Faucet.sol index 092f93891..b64a7e2ab 100644 --- a/contracts/src/token/Faucet.sol +++ b/contracts/src/token/Faucet.sol @@ -10,7 +10,7 @@ contract Faucet { // ************************************* // IERC20 public token; - address public governor; + address public owner; mapping(address => bool) public withdrewAlready; uint256 public amount = 10_000 ether; @@ -18,8 +18,8 @@ contract Faucet { // * Function Modifiers * // // ************************************* // - modifier onlyByGovernor() { - require(address(governor) == msg.sender, "Access not allowed: Governor only."); + modifier onlyByOwner() { + require(address(owner) == msg.sender, "Access not allowed: Owner only."); _; } @@ -29,23 +29,23 @@ contract Faucet { constructor(IERC20 _token) { token = _token; - governor = msg.sender; + owner = msg.sender; } // ************************************* // // * Governance * // // ************************************* // - function changeGovernor(address _governor) public onlyByGovernor { - governor = _governor; + function changeOwner(address _owner) public onlyByOwner { + owner = _owner; } - function changeAmount(uint256 _amount) public onlyByGovernor { + function changeAmount(uint256 _amount) public onlyByOwner { amount = _amount; } - function withdraw() public onlyByGovernor { - token.transfer(governor, token.balanceOf(address(this))); + function withdraw() public onlyByOwner { + token.transfer(owner, token.balanceOf(address(this))); } // ************************************* // diff --git a/contracts/test/arbitration/staking-neo.ts b/contracts/test/arbitration/staking-neo.ts index 696935b40..5537742a6 100644 --- a/contracts/test/arbitration/staking-neo.ts +++ b/contracts/test/arbitration/staking-neo.ts @@ -359,8 +359,8 @@ describe("Staking", async () => { await deploy(); }); - it("Should not allow anyone except the guardian or the governor to pause", async () => { - await expect(core.connect(juror).pause()).to.be.revertedWithCustomError(core, "GuardianOrGovernorOnly"); + it("Should not allow anyone except the guardian or the owner to pause", async () => { + await expect(core.connect(juror).pause()).to.be.revertedWithCustomError(core, "GuardianOrOwnerOnly"); }); it("Should allow the guardian to pause", async () => { @@ -368,7 +368,7 @@ describe("Staking", async () => { expect(await core.paused()).to.equal(true); }); - it("Should allow the governor to pause", async () => { + it("Should allow the owner to pause", async () => { expect(await core.pause()).to.emit(core, "Paused"); expect(await core.paused()).to.equal(true); }); @@ -384,8 +384,8 @@ describe("Staking", async () => { await core.connect(guardian).pause(); }); - it("Should allow only the governor to unpause", async () => { - await expect(core.connect(guardian).unpause()).to.be.revertedWithCustomError(core, "GovernorOnly"); + it("Should allow only the owner to unpause", async () => { + await expect(core.connect(guardian).unpause()).to.be.revertedWithCustomError(core, "OwnerOnly"); expect(await core.unpause()).to.emit(core, "Unpaused"); expect(await core.paused()).to.equal(false); }); diff --git a/contracts/test/evidence/index.ts b/contracts/test/evidence/index.ts index bc3f4ff3b..60c1cb0f5 100644 --- a/contracts/test/evidence/index.ts +++ b/contracts/test/evidence/index.ts @@ -67,7 +67,7 @@ describe("Home Evidence contract", async () => { const EvidenceModule = await ethers.getContractFactory("ModeratedEvidenceModule"); evidenceModule = await EvidenceModule.deploy( arbitrator.target, - deployer.address, // governor + deployer.address, // owner disputeTemplateRegistry.target, totalCostMultiplier, initialDepositMultiplier, @@ -80,10 +80,10 @@ describe("Home Evidence contract", async () => { describe("Governance", async () => { it("Should change parameters correctly", async () => { - const newGovernor = await user2.getAddress(); - await evidenceModule.changeGovernor(newGovernor); - expect(await evidenceModule.governor()).to.equal(newGovernor); - await evidenceModule.connect(user2).changeGovernor(await deployer.getAddress()); + const newOwner = await user2.getAddress(); + await evidenceModule.changeOwner(newOwner); + expect(await evidenceModule.owner()).to.equal(newOwner); + await evidenceModule.connect(user2).changeOwner(await deployer.getAddress()); await evidenceModule.changeInitialDepositMultiplier(1); expect(await evidenceModule.initialDepositMultiplier()).to.equal(1); @@ -117,29 +117,29 @@ describe("Home Evidence contract", async () => { expect(newArbitratorData.arbitratorExtraData).to.equal(newArbitratorExtraData, "Wrong extraData"); }); - it("Should revert if the caller is not the governor", async () => { - await expect(evidenceModule.connect(user2).changeGovernor(await user2.getAddress())).to.be.revertedWith( - "The caller must be the governor" + it("Should revert if the caller is not the owner", async () => { + await expect(evidenceModule.connect(user2).changeOwner(await user2.getAddress())).to.be.revertedWith( + "The caller must be the owner" ); await expect(evidenceModule.connect(user2).changeInitialDepositMultiplier(0)).to.be.revertedWith( - "The caller must be the governor" + "The caller must be the owner" ); await expect(evidenceModule.connect(user2).changeTotalCostMultiplier(0)).to.be.revertedWith( - "The caller must be the governor" + "The caller must be the owner" ); await expect(evidenceModule.connect(user2).changeBondTimeout(0)).to.be.revertedWith( - "The caller must be the governor" + "The caller must be the owner" ); await expect(evidenceModule.connect(user2).changeDisputeTemplate(disputeTemplate, "")).to.be.revertedWith( - "The caller must be the governor" + "The caller must be the owner" ); await expect(evidenceModule.connect(user2).changeArbitratorExtraData(arbitratorExtraData)).to.be.revertedWith( - "The caller must be the governor" + "The caller must be the owner" ); }); }); diff --git a/contracts/test/foundry/KlerosCore.t.sol b/contracts/test/foundry/KlerosCore.t.sol index 62a0b4de1..30e84abdc 100644 --- a/contracts/test/foundry/KlerosCore.t.sol +++ b/contracts/test/foundry/KlerosCore.t.sol @@ -33,7 +33,7 @@ contract KlerosCoreTest is Test { TestERC20 wNative; ArbitrableExample arbitrable; DisputeTemplateRegistry registry; - address governor; + address owner; address guardian; address staker1; address staker2; @@ -69,7 +69,7 @@ contract KlerosCoreTest is Test { feeToken = new TestERC20("Test", "TST"); wNative = new TestERC20("wrapped ETH", "wETH"); - governor = msg.sender; + owner = msg.sender; guardian = vm.addr(1); staker1 = vm.addr(2); staker2 = vm.addr(3); @@ -103,7 +103,7 @@ contract KlerosCoreTest is Test { bytes memory initDataDk = abi.encodeWithSignature( "initialize(address,address,address)", - governor, + owner, address(proxyCore), address(wNative) ); @@ -113,7 +113,7 @@ contract KlerosCoreTest is Test { bytes memory initDataSm = abi.encodeWithSignature( "initialize(address,address,uint256,uint256,address)", - governor, + owner, address(proxyCore), minStakingTime, maxDrawingTime, @@ -122,12 +122,12 @@ contract KlerosCoreTest is Test { UUPSProxy proxySm = new UUPSProxy(address(smLogic), initDataSm); sortitionModule = SortitionModuleMock(address(proxySm)); - vm.prank(governor); + vm.prank(owner); rng.changeConsumer(address(sortitionModule)); core = KlerosCoreMock(address(proxyCore)); core.initialize( - governor, + owner, guardian, pinakion, jurorProsecutionModule, @@ -148,7 +148,7 @@ contract KlerosCoreTest is Test { templateDataMappings = "BBB"; arbitratorExtraData = abi.encodePacked(uint256(GENERAL_COURT), DEFAULT_NB_OF_JURORS, DISPUTE_KIT_CLASSIC); - bytes memory initDataRegistry = abi.encodeWithSignature("initialize(address)", governor); + bytes memory initDataRegistry = abi.encodeWithSignature("initialize(address)", owner); UUPSProxy proxyRegistry = new UUPSProxy(address(registryLogic), initDataRegistry); registry = DisputeTemplateRegistry(address(proxyRegistry)); @@ -163,7 +163,7 @@ contract KlerosCoreTest is Test { } function test_initialize() public { - assertEq(core.governor(), msg.sender, "Wrong governor"); + assertEq(core.owner(), msg.sender, "Wrong owner"); assertEq(core.guardian(), guardian, "Wrong guardian"); assertEq(address(core.pinakion()), address(pinakion), "Wrong pinakion address"); assertEq(core.jurorProsecutionModule(), jurorProsecutionModule, "Wrong jurorProsecutionModule address"); @@ -228,16 +228,16 @@ contract KlerosCoreTest is Test { assertEq(pinakion.name(), "Pinakion", "Wrong token name"); assertEq(pinakion.symbol(), "PNK", "Wrong token symbol"); assertEq(pinakion.totalSupply(), 1000000 ether, "Wrong total supply"); - assertEq(pinakion.balanceOf(msg.sender), 999998 ether, "Wrong token balance of governor"); + assertEq(pinakion.balanceOf(msg.sender), 999998 ether, "Wrong token balance of owner"); assertEq(pinakion.balanceOf(staker1), 1 ether, "Wrong token balance of staker1"); assertEq(pinakion.allowance(staker1, address(core)), 1 ether, "Wrong allowance for staker1"); assertEq(pinakion.balanceOf(staker2), 1 ether, "Wrong token balance of staker2"); assertEq(pinakion.allowance(staker2, address(core)), 1 ether, "Wrong allowance for staker2"); - assertEq(disputeKit.governor(), msg.sender, "Wrong DK governor"); + assertEq(disputeKit.owner(), msg.sender, "Wrong DK owner"); assertEq(address(disputeKit.core()), address(core), "Wrong core in DK"); - assertEq(sortitionModule.governor(), msg.sender, "Wrong SM governor"); + assertEq(sortitionModule.owner(), msg.sender, "Wrong SM owner"); assertEq(address(sortitionModule.core()), address(core), "Wrong core in SM"); assertEq(uint256(sortitionModule.phase()), uint256(ISortitionModule.Phase.staking), "Phase should be 0"); assertEq(sortitionModule.minStakingTime(), 18, "Wrong minStakingTime"); @@ -264,7 +264,7 @@ contract KlerosCoreTest is Test { DisputeKitClassic dkLogic = new DisputeKitClassic(); pinakion = new PNK(); - governor = msg.sender; + owner = msg.sender; guardian = vm.addr(1); staker1 = vm.addr(2); other = vm.addr(9); @@ -290,7 +290,7 @@ contract KlerosCoreTest is Test { bytes memory initDataDk = abi.encodeWithSignature( "initialize(address,address,address)", - governor, + owner, address(proxyCore), address(wNative) ); @@ -300,7 +300,7 @@ contract KlerosCoreTest is Test { bytes memory initDataSm = abi.encodeWithSignature( "initialize(address,address,uint256,uint256,address)", - governor, + owner, address(proxyCore), minStakingTime, maxDrawingTime, @@ -309,7 +309,7 @@ contract KlerosCoreTest is Test { UUPSProxy proxySm = new UUPSProxy(address(smLogic), initDataSm); sortitionModule = SortitionModuleMock(address(proxySm)); - vm.prank(governor); + vm.prank(owner); rng.changeConsumer(address(sortitionModule)); core = KlerosCoreMock(address(proxyCore)); @@ -333,7 +333,7 @@ contract KlerosCoreTest is Test { vm.expectEmit(true, true, true, true); emit KlerosCoreBase.DisputeKitEnabled(GENERAL_COURT, DISPUTE_KIT_CLASSIC, true); core.initialize( - governor, + owner, guardian, pinakion, jurorProsecutionModule, @@ -352,109 +352,109 @@ contract KlerosCoreTest is Test { // ****************************************** // function test_pause() public { - vm.expectRevert(KlerosCoreBase.GuardianOrGovernorOnly.selector); + vm.expectRevert(KlerosCoreBase.GuardianOrOwnerOnly.selector); vm.prank(other); core.pause(); - // Note that we must explicitly switch to the governor/guardian address to make the call, otherwise Foundry treats UUPS proxy as msg.sender. + // Note that we must explicitly switch to the owner/guardian address to make the call, otherwise Foundry treats UUPS proxy as msg.sender. vm.prank(guardian); vm.expectEmit(true, true, true, true); emit KlerosCoreBase.Paused(); core.pause(); assertEq(core.paused(), true, "Wrong paused value"); - // Switch between governor and guardian to test both. WhenNotPausedOnly modifier is triggered after governor's check. - vm.prank(governor); + // Switch between owner and guardian to test both. WhenNotPausedOnly modifier is triggered after owner's check. + vm.prank(owner); vm.expectRevert(KlerosCoreBase.WhenNotPausedOnly.selector); core.pause(); } function test_unpause() public { - vm.expectRevert(KlerosCoreBase.GovernorOnly.selector); + vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); vm.prank(other); core.unpause(); vm.expectRevert(KlerosCoreBase.WhenPausedOnly.selector); - vm.prank(governor); + vm.prank(owner); core.unpause(); - vm.prank(governor); + vm.prank(owner); core.pause(); - vm.prank(governor); + vm.prank(owner); vm.expectEmit(true, true, true, true); emit KlerosCoreBase.Unpaused(); core.unpause(); assertEq(core.paused(), false, "Wrong paused value"); } - function test_executeGovernorProposal() public { - bytes memory data = abi.encodeWithSignature("changeGovernor(address)", other); - vm.expectRevert(KlerosCoreBase.GovernorOnly.selector); + function test_executeOwnerProposal() public { + bytes memory data = abi.encodeWithSignature("changeOwner(address)", other); + vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); vm.prank(other); - core.executeGovernorProposal(address(core), 0, data); + core.executeOwnerProposal(address(core), 0, data); vm.expectRevert(KlerosCoreBase.UnsuccessfulCall.selector); - vm.prank(governor); - core.executeGovernorProposal(address(core), 0, data); // It'll fail because the core is not its own governor + vm.prank(owner); + core.executeOwnerProposal(address(core), 0, data); // It'll fail because the core is not its own owner - vm.prank(governor); - core.changeGovernor(payable(address(core))); + vm.prank(owner); + core.changeOwner(payable(address(core))); vm.prank(address(core)); - core.executeGovernorProposal(address(core), 0, data); - assertEq(core.governor(), other, "Wrong governor"); + core.executeOwnerProposal(address(core), 0, data); + assertEq(core.owner(), other, "Wrong owner"); } - function test_changeGovernor() public { - vm.expectRevert(KlerosCoreBase.GovernorOnly.selector); + function test_changeOwner() public { + vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); vm.prank(other); - core.changeGovernor(payable(other)); - vm.prank(governor); - core.changeGovernor(payable(other)); - assertEq(core.governor(), other, "Wrong governor"); + core.changeOwner(payable(other)); + vm.prank(owner); + core.changeOwner(payable(other)); + assertEq(core.owner(), other, "Wrong owner"); } function test_changeGuardian() public { - vm.expectRevert(KlerosCoreBase.GovernorOnly.selector); + vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); vm.prank(other); core.changeGuardian(other); - vm.prank(governor); + vm.prank(owner); core.changeGuardian(other); assertEq(core.guardian(), other, "Wrong guardian"); } function test_changePinakion() public { PNK fakePNK = new PNK(); - vm.expectRevert(KlerosCoreBase.GovernorOnly.selector); + vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); vm.prank(other); core.changePinakion(fakePNK); - vm.prank(governor); + vm.prank(owner); core.changePinakion(fakePNK); assertEq(address(core.pinakion()), address(fakePNK), "Wrong PNK"); } function test_changeJurorProsecutionModule() public { - vm.expectRevert(KlerosCoreBase.GovernorOnly.selector); + vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); vm.prank(other); core.changeJurorProsecutionModule(other); - vm.prank(governor); + vm.prank(owner); core.changeJurorProsecutionModule(other); assertEq(core.jurorProsecutionModule(), other, "Wrong jurorProsecutionModule"); } function test_changeSortitionModule() public { SortitionModuleMock fakeSM = new SortitionModuleMock(); - vm.expectRevert(KlerosCoreBase.GovernorOnly.selector); + vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); vm.prank(other); core.changeSortitionModule(fakeSM); - vm.prank(governor); + vm.prank(owner); core.changeSortitionModule(fakeSM); assertEq(address(core.sortitionModule()), address(fakeSM), "Wrong sortitionModule"); } function test_addNewDisputeKit() public { DisputeKitSybilResistant newDK = new DisputeKitSybilResistant(); - vm.expectRevert(KlerosCoreBase.GovernorOnly.selector); + vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); vm.prank(other); core.addNewDisputeKit(newDK); - vm.prank(governor); + vm.prank(owner); vm.expectEmit(true, true, true, true); emit KlerosCoreBase.DisputeKitCreated(2, newDK); core.addNewDisputeKit(newDK); @@ -463,7 +463,7 @@ contract KlerosCoreTest is Test { } function test_createCourt() public { - vm.expectRevert(KlerosCoreBase.GovernorOnly.selector); + vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); vm.prank(other); uint256[] memory supportedDK = new uint256[](2); supportedDK[0] = DISPUTE_KIT_CLASSIC; @@ -481,7 +481,7 @@ contract KlerosCoreTest is Test { ); vm.expectRevert(KlerosCoreBase.MinStakeLowerThanParentCourt.selector); - vm.prank(governor); + vm.prank(owner); core.createCourt( GENERAL_COURT, true, // Hidden votes @@ -495,7 +495,7 @@ contract KlerosCoreTest is Test { ); vm.expectRevert(KlerosCoreBase.UnsupportedDisputeKit.selector); - vm.prank(governor); + vm.prank(owner); uint256[] memory emptySupportedDK = new uint256[](0); core.createCourt( GENERAL_COURT, @@ -510,7 +510,7 @@ contract KlerosCoreTest is Test { ); vm.expectRevert(KlerosCoreBase.InvalidForkingCourtAsParent.selector); - vm.prank(governor); + vm.prank(owner); core.createCourt( FORKING_COURT, true, // Hidden votes @@ -527,7 +527,7 @@ contract KlerosCoreTest is Test { badSupportedDK[0] = NULL_DISPUTE_KIT; // Include NULL_DK to check that it reverts badSupportedDK[1] = DISPUTE_KIT_CLASSIC; vm.expectRevert(KlerosCoreBase.WrongDisputeKitIndex.selector); - vm.prank(governor); + vm.prank(owner); core.createCourt( GENERAL_COURT, true, // Hidden votes @@ -543,7 +543,7 @@ contract KlerosCoreTest is Test { badSupportedDK[0] = DISPUTE_KIT_CLASSIC; badSupportedDK[1] = 2; // Check out of bounds index vm.expectRevert(KlerosCoreBase.WrongDisputeKitIndex.selector); - vm.prank(governor); + vm.prank(owner); core.createCourt( GENERAL_COURT, true, // Hidden votes @@ -558,12 +558,12 @@ contract KlerosCoreTest is Test { // Add new DK to check the requirement for classic DK DisputeKitSybilResistant newDK = new DisputeKitSybilResistant(); - vm.prank(governor); + vm.prank(owner); core.addNewDisputeKit(newDK); badSupportedDK = new uint256[](1); badSupportedDK[0] = 2; // Include only sybil resistant dk vm.expectRevert(KlerosCoreBase.MustSupportDisputeKitClassic.selector); - vm.prank(governor); + vm.prank(owner); core.createCourt( GENERAL_COURT, true, // Hidden votes @@ -576,7 +576,7 @@ contract KlerosCoreTest is Test { badSupportedDK ); - vm.prank(governor); + vm.prank(owner); vm.expectEmit(true, true, true, true); emit KlerosCoreBase.DisputeKitEnabled(2, DISPUTE_KIT_CLASSIC, true); vm.expectEmit(true, true, true, true); @@ -641,7 +641,7 @@ contract KlerosCoreTest is Test { function test_changeCourtParameters() public { // Create a 2nd court to check the minStake requirements - vm.prank(governor); + vm.prank(owner); uint96 newCourtID = 2; uint256[] memory supportedDK = new uint256[](1); supportedDK[0] = DISPUTE_KIT_CLASSIC; @@ -657,7 +657,7 @@ contract KlerosCoreTest is Test { supportedDK ); - vm.expectRevert(KlerosCoreBase.GovernorOnly.selector); + vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); vm.prank(other); core.changeCourtParameters( GENERAL_COURT, @@ -669,7 +669,7 @@ contract KlerosCoreTest is Test { [uint256(10), uint256(20), uint256(30), uint256(40)] // Times per period ); vm.expectRevert(KlerosCoreBase.MinStakeLowerThanParentCourt.selector); - vm.prank(governor); + vm.prank(owner); // Min stake of a parent became higher than of a child core.changeCourtParameters( GENERAL_COURT, @@ -682,7 +682,7 @@ contract KlerosCoreTest is Test { ); // Min stake of a child became lower than of a parent vm.expectRevert(KlerosCoreBase.MinStakeLowerThanParentCourt.selector); - vm.prank(governor); + vm.prank(owner); core.changeCourtParameters( newCourtID, true, // Hidden votes @@ -693,7 +693,7 @@ contract KlerosCoreTest is Test { [uint256(10), uint256(20), uint256(30), uint256(40)] // Times per period ); - vm.prank(governor); + vm.prank(owner); vm.expectEmit(true, true, true, true); emit KlerosCoreBase.CourtModified( GENERAL_COURT, @@ -740,38 +740,38 @@ contract KlerosCoreTest is Test { function test_enableDisputeKits() public { DisputeKitSybilResistant newDK = new DisputeKitSybilResistant(); uint256 newDkID = 2; - vm.prank(governor); + vm.prank(owner); core.addNewDisputeKit(newDK); - vm.expectRevert(KlerosCoreBase.GovernorOnly.selector); + vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); vm.prank(other); uint256[] memory supportedDK = new uint256[](1); supportedDK[0] = newDkID; core.enableDisputeKits(GENERAL_COURT, supportedDK, true); vm.expectRevert(KlerosCoreBase.WrongDisputeKitIndex.selector); - vm.prank(governor); + vm.prank(owner); supportedDK[0] = NULL_DISPUTE_KIT; core.enableDisputeKits(GENERAL_COURT, supportedDK, true); vm.expectRevert(KlerosCoreBase.WrongDisputeKitIndex.selector); - vm.prank(governor); + vm.prank(owner); supportedDK[0] = 3; // Out of bounds core.enableDisputeKits(GENERAL_COURT, supportedDK, true); vm.expectRevert(KlerosCoreBase.CannotDisableClassicDK.selector); - vm.prank(governor); + vm.prank(owner); supportedDK[0] = DISPUTE_KIT_CLASSIC; core.enableDisputeKits(GENERAL_COURT, supportedDK, false); - vm.prank(governor); + vm.prank(owner); vm.expectEmit(true, true, true, true); emit KlerosCoreBase.DisputeKitEnabled(GENERAL_COURT, newDkID, true); supportedDK[0] = newDkID; core.enableDisputeKits(GENERAL_COURT, supportedDK, true); assertEq(core.isSupported(GENERAL_COURT, newDkID), true, "New DK should be supported by General court"); - vm.prank(governor); + vm.prank(owner); vm.expectEmit(true, true, true, true); emit KlerosCoreBase.DisputeKitEnabled(GENERAL_COURT, newDkID, false); core.enableDisputeKits(GENERAL_COURT, supportedDK, false); @@ -779,14 +779,14 @@ contract KlerosCoreTest is Test { } function test_changeAcceptedFeeTokens() public { - vm.expectRevert(KlerosCoreBase.GovernorOnly.selector); + vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); vm.prank(other); core.changeAcceptedFeeTokens(feeToken, true); (bool accepted, , ) = core.currencyRates(feeToken); assertEq(accepted, false, "Token should not be accepted yet"); - vm.prank(governor); + vm.prank(owner); vm.expectEmit(true, true, true, true); emit IArbitratorV2.AcceptedFeeToken(feeToken, true); core.changeAcceptedFeeTokens(feeToken, true); @@ -795,7 +795,7 @@ contract KlerosCoreTest is Test { } function test_changeCurrencyRates() public { - vm.expectRevert(KlerosCoreBase.GovernorOnly.selector); + vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); vm.prank(other); core.changeCurrencyRates(feeToken, 100, 200); @@ -803,7 +803,7 @@ contract KlerosCoreTest is Test { assertEq(rateInEth, 0, "rateInEth should be 0"); assertEq(rateDecimals, 0, "rateDecimals should be 0"); - vm.prank(governor); + vm.prank(owner); vm.expectEmit(true, true, true, true); emit IArbitratorV2.NewCurrencyRate(feeToken, 100, 200); core.changeCurrencyRates(feeToken, 100, 200); @@ -833,7 +833,7 @@ contract KlerosCoreTest is Test { assertEq(disputeKitID, DISPUTE_KIT_CLASSIC, "Wrong disputeKitID"); // Custom values. - vm.startPrank(governor); + vm.startPrank(owner); core.addNewDisputeKit(disputeKit); core.addNewDisputeKit(disputeKit); core.addNewDisputeKit(disputeKit); @@ -852,12 +852,12 @@ contract KlerosCoreTest is Test { // *************************************** // function test_setStake_increase() public { - vm.prank(governor); + vm.prank(owner); core.pause(); vm.expectRevert(KlerosCoreBase.WhenNotPausedOnly.selector); vm.prank(staker1); core.setStake(GENERAL_COURT, 1000); - vm.prank(governor); + vm.prank(owner); core.unpause(); vm.expectRevert(KlerosCoreBase.StakingNotPossibleInThisCourt.selector); @@ -898,8 +898,8 @@ contract KlerosCoreTest is Test { assertEq(pinakion.balanceOf(staker1), 999999999999998999, "Wrong token balance of staker1"); // 1 eth - 1001 wei assertEq(pinakion.allowance(staker1, address(core)), 999999999999998999, "Wrong allowance for staker1"); - vm.expectRevert(KlerosCoreBase.StakingTransferFailed.selector); // This error will be caught because governor didn't approve any tokens for KlerosCore - vm.prank(governor); + vm.expectRevert(KlerosCoreBase.StakingTransferFailed.selector); // This error will be caught because owner didn't approve any tokens for KlerosCore + vm.prank(owner); core.setStake(GENERAL_COURT, 1000); // Increase stake one more time to verify the correct behavior @@ -985,7 +985,7 @@ contract KlerosCoreTest is Test { // Create 4 courts to check the require for (uint96 i = GENERAL_COURT; i <= 4; i++) { - vm.prank(governor); + vm.prank(owner); core.createCourt( GENERAL_COURT, true, @@ -1219,7 +1219,7 @@ contract KlerosCoreTest is Test { sortitionModule.passPhase(); // Staking. Delayed stakes can be executed now vm.prank(address(core)); - pinakion.transfer(governor, 10000); // Dispose of the tokens of 2nd staker to make the execution fail for the 2nd delayed stake + pinakion.transfer(owner, 10000); // Dispose of the tokens of 2nd staker to make the execution fail for the 2nd delayed stake assertEq(pinakion.balanceOf(address(core)), 0, "Wrong token balance of the core"); // 2 events should be emitted but the 2nd stake supersedes the first one in the end. @@ -1257,7 +1257,7 @@ contract KlerosCoreTest is Test { function test_setStakeBySortitionModule() public { // Note that functionality of this function was checked during delayed stakes execution vm.expectRevert(KlerosCoreBase.SortitionModuleOnly.selector); - vm.prank(governor); + vm.prank(owner); core.setStakeBySortitionModule(staker1, GENERAL_COURT, 1000); } @@ -1265,27 +1265,27 @@ contract KlerosCoreTest is Test { vm.prank(staker1); core.setStake(GENERAL_COURT, 12346); - KlerosCoreSnapshotProxy snapshotProxy = new KlerosCoreSnapshotProxy(governor, IKlerosCore(address(core))); + KlerosCoreSnapshotProxy snapshotProxy = new KlerosCoreSnapshotProxy(owner, IKlerosCore(address(core))); assertEq(snapshotProxy.name(), "Staked Pinakion", "Wrong name of the proxy token"); assertEq(snapshotProxy.symbol(), "stPNK", "Wrong symbol of the proxy token"); assertEq(snapshotProxy.decimals(), 18, "Wrong decimals of the proxy token"); - assertEq(snapshotProxy.governor(), msg.sender, "Wrong governor"); + assertEq(snapshotProxy.owner(), msg.sender, "Wrong owner"); assertEq(address(snapshotProxy.core()), address(core), "Wrong core in snapshot proxy"); assertEq(snapshotProxy.balanceOf(staker1), 12346, "Wrong stPNK balance"); vm.prank(other); - vm.expectRevert(KlerosCoreSnapshotProxy.GovernorOnly.selector); + vm.expectRevert(KlerosCoreSnapshotProxy.OwnerOnly.selector); snapshotProxy.changeCore(IKlerosCore(other)); - vm.prank(governor); + vm.prank(owner); snapshotProxy.changeCore(IKlerosCore(other)); assertEq(address(snapshotProxy.core()), other, "Wrong core in snapshot proxy after change"); vm.prank(other); - vm.expectRevert(KlerosCoreSnapshotProxy.GovernorOnly.selector); - snapshotProxy.changeGovernor(other); - vm.prank(governor); - snapshotProxy.changeGovernor(other); - assertEq(snapshotProxy.governor(), other, "Wrong governor after change"); + vm.expectRevert(KlerosCoreSnapshotProxy.OwnerOnly.selector); + snapshotProxy.changeOwner(other); + vm.prank(owner); + snapshotProxy.changeOwner(other); + assertEq(snapshotProxy.owner(), other, "Wrong owner after change"); } // *************************************** // @@ -1302,9 +1302,9 @@ contract KlerosCoreTest is Test { supportedDK[0] = DISPUTE_KIT_CLASSIC; bytes memory newExtraData = abi.encodePacked(uint256(newCourtID), newNbJurors, newDkID); - vm.prank(governor); + vm.prank(owner); core.addNewDisputeKit(disputeKit); // Just add the same dk to avoid dealing with initialization - vm.prank(governor); + vm.prank(owner); core.createCourt( GENERAL_COURT, true, // Hidden votes @@ -1327,7 +1327,7 @@ contract KlerosCoreTest is Test { vm.prank(disputer); arbitrable.createDispute{value: 0.04 ether}("Action"); - vm.prank(governor); + vm.prank(owner); supportedDK = new uint256[](1); supportedDK[0] = newDkID; core.enableDisputeKits(newCourtID, supportedDK, true); @@ -1401,9 +1401,9 @@ contract KlerosCoreTest is Test { vm.prank(disputer); arbitrable.createDispute("Action", 0.18 ether); - vm.prank(governor); + vm.prank(owner); core.changeAcceptedFeeTokens(feeToken, true); - vm.prank(governor); + vm.prank(owner); core.changeCurrencyRates(feeToken, 500, 3); vm.expectRevert(KlerosCoreBase.ArbitrationFeesNotEnough.selector); @@ -1491,7 +1491,7 @@ contract KlerosCoreTest is Test { uint256 roundID = 0; // Create a child court and stake exclusively there to check that parent courts hold drawing power. - vm.prank(governor); + vm.prank(owner); uint256[] memory supportedDK = new uint256[](1); supportedDK[0] = DISPUTE_KIT_CLASSIC; core.createCourt( @@ -1542,7 +1542,7 @@ contract KlerosCoreTest is Test { function test_castCommit() public { // Change hidden votes in general court uint256 disputeID = 0; - vm.prank(governor); + vm.prank(owner); core.changeCourtParameters( GENERAL_COURT, true, // Hidden votes @@ -1653,7 +1653,7 @@ contract KlerosCoreTest is Test { function test_castCommit_timeoutCheck() public { // Change hidden votes in general court uint256 disputeID = 0; - vm.prank(governor); + vm.prank(owner); core.changeCourtParameters( GENERAL_COURT, true, // Hidden votes @@ -1857,7 +1857,7 @@ contract KlerosCoreTest is Test { function test_castVote_quickPassPeriod() public { // Change hidden votes in general court uint256 disputeID = 0; - vm.prank(governor); + vm.prank(owner); core.changeCourtParameters( GENERAL_COURT, true, // Hidden votes @@ -2116,7 +2116,7 @@ contract KlerosCoreTest is Test { // Create a new DK and court to check the switch bytes memory initDataDk = abi.encodeWithSignature( "initialize(address,address,address)", - governor, + owner, address(core), address(wNative) ); @@ -2130,9 +2130,9 @@ contract KlerosCoreTest is Test { supportedDK[0] = DISPUTE_KIT_CLASSIC; bytes memory newExtraData = abi.encodePacked(uint256(newCourtID), DEFAULT_NB_OF_JURORS, newDkID); - vm.prank(governor); + vm.prank(owner); core.addNewDisputeKit(newDisputeKit); - vm.prank(governor); + vm.prank(owner); core.createCourt( GENERAL_COURT, hiddenVotes, @@ -2147,7 +2147,7 @@ contract KlerosCoreTest is Test { arbitrable.changeArbitratorExtraData(newExtraData); - vm.prank(governor); + vm.prank(owner); supportedDK = new uint256[](1); supportedDK[0] = newDkID; core.enableDisputeKits(newCourtID, supportedDK, true); @@ -2308,11 +2308,11 @@ contract KlerosCoreTest is Test { vm.warp(block.timestamp + timesPerPeriod[3]); core.passPeriod(disputeID); // Execution - vm.prank(governor); + vm.prank(owner); core.pause(); vm.expectRevert(KlerosCoreBase.WhenNotPausedOnly.selector); core.execute(disputeID, 0, 1); - vm.prank(governor); + vm.prank(owner); core.unpause(); assertEq(disputeKit.getCoherentCount(disputeID, 0), 2, "Wrong coherent count"); @@ -2446,8 +2446,8 @@ contract KlerosCoreTest is Test { 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); + uint256 ownerBalance = owner.balance; + uint256 ownerTokenBalance = pinakion.balanceOf(owner); vm.expectEmit(true, true, true, true); emit KlerosCoreBase.LeftoverRewardSent(disputeID, 0, 3000, 0.09 ether, IERC20(address(0))); @@ -2460,16 +2460,16 @@ contract KlerosCoreTest is Test { assertEq(address(core).balance, 0, "Wrong balance of the core"); assertEq(staker1.balance, 0, "Wrong balance of the staker1"); - assertEq(governor.balance, governorBalance + 0.09 ether, "Wrong balance of the governor"); + assertEq(owner.balance, ownerBalance + 0.09 ether, "Wrong balance of the owner"); assertEq(pinakion.balanceOf(address(core)), 17000, "Wrong token balance of the core"); assertEq(pinakion.balanceOf(staker1), 999999999999980000, "Wrong token balance of staker1"); - assertEq(pinakion.balanceOf(governor), governorTokenBalance + 3000, "Wrong token balance of governor"); + assertEq(pinakion.balanceOf(owner), ownerTokenBalance + 3000, "Wrong token balance of owner"); } function test_execute_UnstakeInactive() public { // Create a 2nd court so unstaking is done in multiple courts. - vm.prank(governor); + vm.prank(owner); uint256[] memory supportedDK = new uint256[](1); supportedDK[0] = DISPUTE_KIT_CLASSIC; core.createCourt( @@ -2517,7 +2517,7 @@ contract KlerosCoreTest is Test { vm.warp(block.timestamp + timesPerPeriod[3]); core.passPeriod(disputeID); // Execution - uint256 governorTokenBalance = pinakion.balanceOf(governor); + uint256 ownerTokenBalance = pinakion.balanceOf(owner); // Note that these events are emitted only after the first iteration of execute() therefore the juror has been penalized only for 1000 PNK her. vm.expectEmit(true, true, true, true); @@ -2527,8 +2527,8 @@ contract KlerosCoreTest is Test { core.execute(disputeID, 0, 3); assertEq(pinakion.balanceOf(address(core)), 0, "Wrong token balance of the core"); - assertEq(pinakion.balanceOf(staker1), 999999999999997000, "Wrong token balance of staker1"); // 3000 locked PNK was withheld by the contract and given to governor. - assertEq(pinakion.balanceOf(governor), governorTokenBalance + 3000, "Wrong token balance of governor"); + assertEq(pinakion.balanceOf(staker1), 999999999999997000, "Wrong token balance of staker1"); // 3000 locked PNK was withheld by the contract and given to owner. + assertEq(pinakion.balanceOf(owner), ownerTokenBalance + 3000, "Wrong token balance of owner"); (, , , nbCourts) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); assertEq(nbCourts, 0, "Should unstake from all courts"); @@ -2658,7 +2658,7 @@ contract KlerosCoreTest is Test { assertEq(pinakion.balanceOf(staker1), 999999999999999000, "Wrong token balance of staker1"); vm.expectRevert(KlerosCoreBase.SortitionModuleOnly.selector); - vm.prank(governor); + vm.prank(owner); core.transferBySortitionModule(staker1, 1000); vm.expectEmit(true, true, true, true); @@ -2680,9 +2680,9 @@ contract KlerosCoreTest is Test { vm.prank(disputer); feeToken.approve(address(arbitrable), 1 ether); - vm.prank(governor); + vm.prank(owner); core.changeAcceptedFeeTokens(feeToken, true); - vm.prank(governor); + vm.prank(owner); core.changeCurrencyRates(feeToken, 500, 3); vm.prank(disputer); @@ -2734,9 +2734,9 @@ contract KlerosCoreTest is Test { vm.prank(disputer); feeToken.approve(address(arbitrable), 1 ether); - vm.prank(governor); + vm.prank(owner); core.changeAcceptedFeeTokens(feeToken, true); - vm.prank(governor); + vm.prank(owner); core.changeCurrencyRates(feeToken, 500, 3); vm.prank(disputer); @@ -2773,7 +2773,7 @@ contract KlerosCoreTest is Test { assertEq(feeToken.balanceOf(address(core)), 0, "Wrong token balance of the core"); assertEq(feeToken.balanceOf(staker1), 0, "Wrong token balance of staker1"); assertEq(feeToken.balanceOf(disputer), 0.82 ether, "Wrong token balance of disputer"); - assertEq(feeToken.balanceOf(governor), 0.18 ether, "Wrong token balance of governor"); + assertEq(feeToken.balanceOf(owner), 0.18 ether, "Wrong token balance of owner"); } function test_executeRuling() public { @@ -2911,11 +2911,11 @@ contract KlerosCoreTest is Test { core.executeRuling(disputeID); - vm.prank(governor); + vm.prank(owner); core.pause(); vm.expectRevert(DisputeKitClassicBase.CoreIsPaused.selector); disputeKit.withdrawFeesAndRewards(disputeID, payable(staker1), 0, 1); - vm.prank(governor); + vm.prank(owner); core.unpause(); assertEq(crowdfunder1.balance, 9.37 ether, "Wrong balance of the crowdfunder1"); @@ -2940,7 +2940,7 @@ contract KlerosCoreTest is Test { // Create a new DK to check castVote. bytes memory initDataDk = abi.encodeWithSignature( "initialize(address,address,address)", - governor, + owner, address(core), address(wNative) ); @@ -2948,14 +2948,14 @@ contract KlerosCoreTest is Test { UUPSProxy proxyDk = new UUPSProxy(address(dkLogic), initDataDk); DisputeKitClassic newDisputeKit = DisputeKitClassic(address(proxyDk)); - vm.prank(governor); + vm.prank(owner); core.addNewDisputeKit(newDisputeKit); uint256 newDkID = 2; uint256[] memory supportedDK = new uint256[](1); bytes memory newExtraData = abi.encodePacked(uint256(GENERAL_COURT), DEFAULT_NB_OF_JURORS, newDkID); - vm.prank(governor); + vm.prank(owner); vm.expectEmit(true, true, true, true); emit KlerosCoreBase.DisputeKitEnabled(GENERAL_COURT, newDkID, true); supportedDK[0] = newDkID; @@ -3039,13 +3039,13 @@ contract KlerosCoreTest is Test { 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.owner(), msg.sender, "Wrong owner"); 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); + vm.prank(owner); sortitionModule.changeRandomNumberGenerator(rngFallback); assertEq(address(sortitionModule.rng()), address(rngFallback), "Wrong RNG address"); @@ -3079,7 +3079,7 @@ contract KlerosCoreTest is Test { RNGMock rngMock = new RNGMock(); rngFallback = new RNGWithFallback(msg.sender, address(sortitionModule), fallbackTimeout, rngMock); - vm.prank(governor); + vm.prank(owner); sortitionModule.changeRandomNumberGenerator(rngFallback); assertEq(address(sortitionModule.rng()), address(rngFallback), "Wrong RNG address"); @@ -3107,36 +3107,36 @@ contract KlerosCoreTest is Test { rngFallback = new RNGWithFallback(msg.sender, address(sortitionModule), fallbackTimeout, rngMock); vm.expectRevert(IRNG.ConsumerOnly.selector); - vm.prank(governor); + vm.prank(owner); rngFallback.requestRandomness(); vm.expectRevert(IRNG.ConsumerOnly.selector); - vm.prank(governor); + vm.prank(owner); rngFallback.receiveRandomness(); - vm.expectRevert(IRNG.GovernorOnly.selector); + vm.expectRevert(IRNG.OwnerOnly.selector); vm.prank(other); - rngFallback.changeGovernor(other); - vm.prank(governor); - rngFallback.changeGovernor(other); - assertEq(rngFallback.governor(), other, "Wrong governor"); + rngFallback.changeOwner(other); + vm.prank(owner); + rngFallback.changeOwner(other); + assertEq(rngFallback.owner(), other, "Wrong owner"); - // Change governor back for convenience + // Change owner back for convenience vm.prank(other); - rngFallback.changeGovernor(governor); + rngFallback.changeOwner(owner); - vm.expectRevert(IRNG.GovernorOnly.selector); + vm.expectRevert(IRNG.OwnerOnly.selector); vm.prank(other); rngFallback.changeConsumer(other); - vm.prank(governor); + vm.prank(owner); rngFallback.changeConsumer(other); assertEq(rngFallback.consumer(), other, "Wrong consumer"); - vm.expectRevert(IRNG.GovernorOnly.selector); + vm.expectRevert(IRNG.OwnerOnly.selector); vm.prank(other); rngFallback.changeFallbackTimeout(5); - vm.prank(governor); + vm.prank(owner); vm.expectEmit(true, true, true, true); emit RNGWithFallback.FallbackTimeoutChanged(5); rngFallback.changeFallbackTimeout(5); diff --git a/contracts/test/proxy/index.ts b/contracts/test/proxy/index.ts index 12ba6313c..a58886789 100644 --- a/contracts/test/proxy/index.ts +++ b/contracts/test/proxy/index.ts @@ -46,7 +46,7 @@ describe("Upgradability", async () => { }); describe("Initialization", async () => { - it("Governor cannot re-initialize the proxy", async () => { + it("Owner cannot re-initialize the proxy", async () => { await expect(proxy.connect(deployer).initialize(deployer.address)).to.be.revertedWith( "Contract instance has already been initialized" ); @@ -79,7 +79,7 @@ describe("Upgradability", async () => { .withArgs(nonUpgradeableMock.target); }); it("Should revert if upgrade is performed directly through the implementation", async () => { - // In the implementation, the `governor` storage slot is not initialized so `governor === address(0)`, which fails _authorizeUpgrade() + // In the implementation, the `owner` storage slot is not initialized so `owner === address(0)`, which fails _authorizeUpgrade() const UUPSUpgradeableMockV2Factory = await ethers.getContractFactory("UUPSUpgradeableMockV2"); const newImplementation = await UUPSUpgradeableMockV2Factory.connect(deployer).deploy(); await expect( @@ -89,7 +89,7 @@ describe("Upgradability", async () => { }); describe("Authentication", async () => { - it("Only the governor (deployer here) can perform upgrades", async () => { + it("Only the owner (deployer here) can perform upgrades", async () => { // Unauthorized user try to upgrade the implementation const UUPSUpgradeableMockV2Factory = await ethers.getContractFactory("UUPSUpgradeableMockV2"); let upgradable = await UUPSUpgradeableMockV2Factory.connect(user1).deploy(); @@ -97,7 +97,7 @@ describe("Upgradability", async () => { "No privilege to upgrade" ); - // Governor updates the implementation + // Owner updates the implementation upgradable = await UUPSUpgradeableMockV2Factory.connect(deployer).deploy(); await expect(proxy.connect(deployer).upgradeToAndCall(upgradable.target, "0x")) .to.emit(proxy, "Upgraded") @@ -134,7 +134,7 @@ describe("Upgradability", async () => { implementation = await ethers.getContract("UpgradedByRewrite_Implementation"); - expect(await proxy.governor()).to.equal(deployer.address); + expect(await proxy.owner()).to.equal(deployer.address); expect(await proxy.counter()).to.equal(1); await proxy.increment(); @@ -157,7 +157,7 @@ describe("Upgradability", async () => { throw new Error("No implementation address"); } proxy = await ethers.getContract("UpgradedByRewrite"); - expect(await proxy.governor()).to.equal(deployer.address); + expect(await proxy.owner()).to.equal(deployer.address); expect(await proxy.counter()).to.equal(3); await proxy.increment(); @@ -188,7 +188,7 @@ describe("Upgradability", async () => { implementation = await ethers.getContract("UpgradedByInheritanceV1_Implementation"); - expect(await proxy.governor()).to.equal(deployer.address); + expect(await proxy.owner()).to.equal(deployer.address); expect(await proxy.counter()).to.equal(1); await proxy.increment(); @@ -211,7 +211,7 @@ describe("Upgradability", async () => { proxy = await ethers.getContract("UpgradedByInheritanceV1"); - expect(await proxy.governor()).to.equal(deployer.address); + expect(await proxy.owner()).to.equal(deployer.address); expect(await proxy.counter()).to.equal(3); await proxy.increment(); diff --git a/contracts/test/rng/index.ts b/contracts/test/rng/index.ts index 351a2d460..f61ec9b95 100644 --- a/contracts/test/rng/index.ts +++ b/contracts/test/rng/index.ts @@ -40,7 +40,7 @@ describe("BlockHashRNG", async () => { await deployments.delete("BlockHashRNG"); await deployments.deploy("BlockHashRNG", { from: deployer.address, - args: [deployer.address, deployer.address, 10], // governor, consumer, lookaheadTime (seconds) + args: [deployer.address, deployer.address, 10], // owner, consumer, lookaheadTime (seconds) }); rng = await ethers.getContract("BlockHashRNG"); }); From f73487315c92bfbcbf37bf9569fa4cf5272c3f66 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Wed, 27 Aug 2025 04:53:33 +0100 Subject: [PATCH 065/175] chore: changelog --- contracts/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/CHANGELOG.md b/contracts/CHANGELOG.md index 751bd5473..79845b880 100644 --- a/contracts/CHANGELOG.md +++ b/contracts/CHANGELOG.md @@ -12,6 +12,7 @@ The format is based on [Common Changelog](https://common-changelog.org/). - **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)) +- **Breaking:** Rename `governor` to `owner` in order to comply with the lightweight ownership standard [ERC-5313](https://eipsinsight.com/ercs/erc-5313) ([#2112](https://github.com/kleros/kleros-v2/issues/2112)) - 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)) From e4d4bdbb10090ece1c2e54ba231f19553db5b9bb Mon Sep 17 00:00:00 2001 From: unknownunknown1 Date: Wed, 27 Aug 2025 18:29:54 +1000 Subject: [PATCH 066/175] feat(KlerosCore): allow to jump to a non-classic DK --- .../deploy/00-home-chain-arbitration-neo.ts | 9 ++++---- contracts/deploy/00-home-chain-arbitration.ts | 9 ++++---- contracts/src/arbitration/KlerosCoreBase.sol | 7 ++++-- .../dispute-kits/DisputeKitClassic.sol | 10 +++++++-- .../dispute-kits/DisputeKitClassicBase.sol | 19 ++++++++++++++-- .../dispute-kits/DisputeKitGated.sol | 10 +++++++-- .../dispute-kits/DisputeKitGatedShutter.sol | 10 +++++++-- .../dispute-kits/DisputeKitShutter.sol | 10 +++++++-- .../dispute-kits/DisputeKitSybilResistant.sol | 6 +++-- .../arbitration/interfaces/IDisputeKit.sol | 4 ++++ contracts/test/foundry/KlerosCore.t.sol | 22 ++++++++++++------- 11 files changed, 86 insertions(+), 30 deletions(-) diff --git a/contracts/deploy/00-home-chain-arbitration-neo.ts b/contracts/deploy/00-home-chain-arbitration-neo.ts index f5211c4af..a47e9e4b3 100644 --- a/contracts/deploy/00-home-chain-arbitration-neo.ts +++ b/contracts/deploy/00-home-chain-arbitration-neo.ts @@ -28,10 +28,11 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) await deployUpgradable(deployments, "EvidenceModule", { from: deployer, args: [deployer], log: true }); + const jumpDisputeKitID = 1; // Classic DK const disputeKit = await deployUpgradable(deployments, "DisputeKitClassicNeo", { from: deployer, contract: "DisputeKitClassic", - args: [deployer, ZeroAddress, weth.target], + args: [deployer, ZeroAddress, weth.target, jumpDisputeKitID], log: true, }); @@ -124,21 +125,21 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) // Extra dispute kits const disputeKitShutter = await deployUpgradable(deployments, "DisputeKitShutter", { from: deployer, - args: [deployer, core.target, weth.target], + args: [deployer, core.target, weth.target, jumpDisputeKitID], log: true, }); await core.addNewDisputeKit(disputeKitShutter.address); const disputeKitGated = await deployUpgradable(deployments, "DisputeKitGated", { from: deployer, - args: [deployer, core.target, weth.target], + args: [deployer, core.target, weth.target, jumpDisputeKitID], log: true, }); await core.addNewDisputeKit(disputeKitGated.address); const disputeKitGatedShutter = await deployUpgradable(deployments, "DisputeKitGatedShutter", { from: deployer, - args: [deployer, core.target, weth.target], + args: [deployer, core.target, weth.target, jumpDisputeKitID], log: true, }); await core.addNewDisputeKit(disputeKitGatedShutter.address); diff --git a/contracts/deploy/00-home-chain-arbitration.ts b/contracts/deploy/00-home-chain-arbitration.ts index 1d590813f..8f8587877 100644 --- a/contracts/deploy/00-home-chain-arbitration.ts +++ b/contracts/deploy/00-home-chain-arbitration.ts @@ -34,9 +34,10 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) log: true, }); + const jumpDisputeKitID = 1; // Classic DK const disputeKit = await deployUpgradable(deployments, "DisputeKitClassic", { from: deployer, - args: [deployer, ZeroAddress, weth.target], + args: [deployer, ZeroAddress, weth.target, jumpDisputeKitID], log: true, }); @@ -105,7 +106,7 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) // Extra dispute kits const disputeKitShutter = await deployUpgradable(deployments, "DisputeKitShutter", { from: deployer, - args: [deployer, core.target, weth.target], + args: [deployer, core.target, weth.target, jumpDisputeKitID], log: true, }); await core.addNewDisputeKit(disputeKitShutter.address); @@ -113,7 +114,7 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) const disputeKitGated = await deployUpgradable(deployments, "DisputeKitGated", { from: deployer, - args: [deployer, core.target, weth.target], + args: [deployer, core.target, weth.target, jumpDisputeKitID], log: true, }); await core.addNewDisputeKit(disputeKitGated.address); @@ -121,7 +122,7 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) const disputeKitGatedShutter = await deployUpgradable(deployments, "DisputeKitGatedShutter", { from: deployer, - args: [deployer, core.target, weth.target], + args: [deployer, core.target, weth.target, jumpDisputeKitID], log: true, }); await core.addNewDisputeKit(disputeKitGatedShutter.address); diff --git a/contracts/src/arbitration/KlerosCoreBase.sol b/contracts/src/arbitration/KlerosCoreBase.sol index 0a9946bbc..cbec7646c 100644 --- a/contracts/src/arbitration/KlerosCoreBase.sol +++ b/contracts/src/arbitration/KlerosCoreBase.sol @@ -1079,8 +1079,11 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable courtJump = true; if (!courts[newCourtID].supportedDisputeKits[newDisputeKitID]) { - // Switch to classic dispute kit if parent court doesn't support the current one. - newDisputeKitID = DISPUTE_KIT_CLASSIC; + newDisputeKitID = disputeKits[_round.disputeKitID].getJumpDisputeKitID(); + if (!courts[newCourtID].supportedDisputeKits[newDisputeKitID]) { + // Switch to classic dispute kit if parent court doesn't support the jump DK. + newDisputeKitID = DISPUTE_KIT_CLASSIC; + } disputeKitJump = true; } } diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitClassic.sol b/contracts/src/arbitration/dispute-kits/DisputeKitClassic.sol index ce5932c92..5f2a86536 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitClassic.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitClassic.sol @@ -26,8 +26,14 @@ contract DisputeKitClassic is DisputeKitClassicBase { /// @param _owner The owner's address. /// @param _core The KlerosCore arbitrator. /// @param _wNative The wrapped native token address, typically wETH. - function initialize(address _owner, KlerosCore _core, address _wNative) external reinitializer(1) { - __DisputeKitClassicBase_initialize(_owner, _core, _wNative); + /// @param _jumpDisputeKitID The ID of the dispute kit to switch to after the court jump. + function initialize( + address _owner, + KlerosCore _core, + address _wNative, + uint256 _jumpDisputeKitID + ) external reinitializer(1) { + __DisputeKitClassicBase_initialize(_owner, _core, _wNative, _jumpDisputeKitID); } function reinitialize(address _wNative) external reinitializer(9) { diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol index f2d7a154c..34ffd5e96 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol @@ -6,7 +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"; +import {ONE_BASIS_POINT, DISPUTE_KIT_CLASSIC} from "../../libraries/Constants.sol"; /// @title DisputeKitClassicBase /// Abstract Dispute kit classic implementation of the Kleros v1 features including: @@ -68,6 +68,7 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi public alreadyDrawn; // True if the address has already been drawn, false by default. To be added to the Round struct when fully redeploying rather than upgrading. mapping(uint256 coreDisputeID => bool) public coreDisputeIDToActive; // True if this dispute kit is active for this core dispute ID. address public wNative; // The wrapped native token for safeSend(). + uint256 public jumpDisputeKitID; // The ID of the dispute kit in Kleros Core disputeKits array that the dispute should switch to after the court jump, in case the new court doesn't support this dispute kit. // ************************************* // // * Events * // @@ -147,14 +148,17 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi /// @param _owner The owner's address. /// @param _core The KlerosCore arbitrator. /// @param _wNative The wrapped native token address, typically wETH. + /// @param _jumpDisputeKitID The ID of the dispute kit to switch to after the court jump. function __DisputeKitClassicBase_initialize( address _owner, KlerosCore _core, - address _wNative + address _wNative, + uint256 _jumpDisputeKitID ) internal onlyInitializing { owner = _owner; core = _core; wNative = _wNative; + jumpDisputeKitID = _jumpDisputeKitID; } // ************************ // @@ -176,6 +180,12 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi owner = _owner; } + /// @dev Changes the dispute kit ID used for the jump. + /// @param _jumpDisputeKitID The new value for the `jumpDisputeKitID` storage variable. + function changeJumpDisputeKitID(uint256 _jumpDisputeKitID) external onlyByOwner { + jumpDisputeKitID = _jumpDisputeKitID; + } + /// @dev Changes the `core` storage variable. /// @param _core The new value for the `core` storage variable. function changeCore(address _core) external onlyByOwner { @@ -639,6 +649,11 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi return (_currentNbVotes * 2) + 1; } + function getJumpDisputeKitID() external view returns (uint256) { + // Fall back to classic DK in case the jump ID is not defined. + return jumpDisputeKitID == 0 ? DISPUTE_KIT_CLASSIC : jumpDisputeKitID; + } + /// @dev Returns true if the specified voter was active in this round. /// @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. diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitGated.sol b/contracts/src/arbitration/dispute-kits/DisputeKitGated.sol index c0e70f741..ae97a5a5e 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitGated.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitGated.sol @@ -42,8 +42,14 @@ contract DisputeKitGated is DisputeKitClassicBase { /// @param _owner The owner's address. /// @param _core The KlerosCore arbitrator. /// @param _wNative The wrapped native token address, typically wETH. - function initialize(address _owner, KlerosCore _core, address _wNative) external reinitializer(1) { - __DisputeKitClassicBase_initialize(_owner, _core, _wNative); + /// @param _jumpDisputeKitID The ID of the dispute kit to switch to after the court jump. + function initialize( + address _owner, + KlerosCore _core, + address _wNative, + uint256 _jumpDisputeKitID + ) external reinitializer(1) { + __DisputeKitClassicBase_initialize(_owner, _core, _wNative, _jumpDisputeKitID); } function reinitialize(address _wNative) external reinitializer(9) { diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitGatedShutter.sol b/contracts/src/arbitration/dispute-kits/DisputeKitGatedShutter.sol index c5de2483b..ee4e9c089 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitGatedShutter.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitGatedShutter.sol @@ -61,8 +61,14 @@ contract DisputeKitGatedShutter is DisputeKitClassicBase { /// @param _owner The owner's address. /// @param _core The KlerosCore arbitrator. /// @param _wNative The wrapped native token address, typically wETH. - function initialize(address _owner, KlerosCore _core, address _wNative) external reinitializer(1) { - __DisputeKitClassicBase_initialize(_owner, _core, _wNative); + /// @param _jumpDisputeKitID The ID of the dispute kit to switch to after the court jump. + function initialize( + address _owner, + KlerosCore _core, + address _wNative, + uint256 _jumpDisputeKitID + ) external reinitializer(1) { + __DisputeKitClassicBase_initialize(_owner, _core, _wNative, _jumpDisputeKitID); } function reinitialize(address _wNative) external reinitializer(9) { diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitShutter.sol b/contracts/src/arbitration/dispute-kits/DisputeKitShutter.sol index 886f5dbec..d98e20132 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitShutter.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitShutter.sol @@ -45,8 +45,14 @@ contract DisputeKitShutter is DisputeKitClassicBase { /// @param _owner The owner's address. /// @param _core The KlerosCore arbitrator. /// @param _wNative The wrapped native token address, typically wETH. - function initialize(address _owner, KlerosCore _core, address _wNative) external reinitializer(1) { - __DisputeKitClassicBase_initialize(_owner, _core, _wNative); + /// @param _jumpDisputeKitID The ID of the dispute kit to switch to after the court jump. + function initialize( + address _owner, + KlerosCore _core, + address _wNative, + uint256 _jumpDisputeKitID + ) external reinitializer(1) { + __DisputeKitClassicBase_initialize(_owner, _core, _wNative, _jumpDisputeKitID); } function reinitialize(address _wNative) external reinitializer(9) { diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitSybilResistant.sol b/contracts/src/arbitration/dispute-kits/DisputeKitSybilResistant.sol index e213f6d11..38d728444 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitSybilResistant.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitSybilResistant.sol @@ -40,13 +40,15 @@ contract DisputeKitSybilResistant is DisputeKitClassicBase { /// @param _core The KlerosCore arbitrator. /// @param _poh The Proof of Humanity registry. /// @param _wNative The wrapped native token address, typically wETH. + /// @param _jumpDisputeKitID The ID of the dispute kit to switch to after the court jump. function initialize( address _owner, KlerosCore _core, IProofOfHumanity _poh, - address _wNative + address _wNative, + uint256 _jumpDisputeKitID ) external reinitializer(1) { - __DisputeKitClassicBase_initialize(_owner, _core, _wNative); + __DisputeKitClassicBase_initialize(_owner, _core, _wNative, _jumpDisputeKitID); poh = _poh; singleDrawPerJuror = true; } diff --git a/contracts/src/arbitration/interfaces/IDisputeKit.sol b/contracts/src/arbitration/interfaces/IDisputeKit.sol index 0aab45b9c..ca3047176 100644 --- a/contracts/src/arbitration/interfaces/IDisputeKit.sol +++ b/contracts/src/arbitration/interfaces/IDisputeKit.sol @@ -127,6 +127,10 @@ interface IDisputeKit { uint256 _currentNbVotes ) external view returns (uint256); // TODO: remove previousDisputeKit + /// @dev Returns the dispute kid ID for Kleros Core, that will be used after court jump. + /// @return The ID of the dispute kit in Kleros Core disputeKits array. + function getJumpDisputeKitID() external view returns (uint256); + /// @dev Returns true if the specified voter was active in this round. /// @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. diff --git a/contracts/test/foundry/KlerosCore.t.sol b/contracts/test/foundry/KlerosCore.t.sol index 30e84abdc..e5b5e3c1a 100644 --- a/contracts/test/foundry/KlerosCore.t.sol +++ b/contracts/test/foundry/KlerosCore.t.sol @@ -102,10 +102,11 @@ contract KlerosCoreTest is Test { UUPSProxy proxyCore = new UUPSProxy(address(coreLogic), ""); bytes memory initDataDk = abi.encodeWithSignature( - "initialize(address,address,address)", + "initialize(address,address,address,uint256)", owner, address(proxyCore), - address(wNative) + address(wNative), + DISPUTE_KIT_CLASSIC ); UUPSProxy proxyDk = new UUPSProxy(address(dkLogic), initDataDk); @@ -235,6 +236,8 @@ contract KlerosCoreTest is Test { assertEq(pinakion.allowance(staker2, address(core)), 1 ether, "Wrong allowance for staker2"); assertEq(disputeKit.owner(), msg.sender, "Wrong DK owner"); + assertEq(disputeKit.getJumpDisputeKitID(), DISPUTE_KIT_CLASSIC, "Wrong jump DK"); + assertEq(disputeKit.jumpDisputeKitID(), DISPUTE_KIT_CLASSIC, "Wrong jump DK storage var"); assertEq(address(disputeKit.core()), address(core), "Wrong core in DK"); assertEq(sortitionModule.owner(), msg.sender, "Wrong SM owner"); @@ -289,10 +292,11 @@ contract KlerosCoreTest is Test { UUPSProxy proxyCore = new UUPSProxy(address(coreLogic), ""); bytes memory initDataDk = abi.encodeWithSignature( - "initialize(address,address,address)", + "initialize(address,address,address,uint256)", owner, address(proxyCore), - address(wNative) + address(wNative), + DISPUTE_KIT_CLASSIC ); UUPSProxy proxyDk = new UUPSProxy(address(dkLogic), initDataDk); @@ -2115,10 +2119,11 @@ contract KlerosCoreTest is Test { DisputeKitClassic dkLogic = new DisputeKitClassic(); // Create a new DK and court to check the switch bytes memory initDataDk = abi.encodeWithSignature( - "initialize(address,address,address)", + "initialize(address,address,address,uint256)", owner, address(core), - address(wNative) + address(wNative), + DISPUTE_KIT_CLASSIC ); UUPSProxy proxyDk = new UUPSProxy(address(dkLogic), initDataDk); @@ -2939,10 +2944,11 @@ contract KlerosCoreTest is Test { DisputeKitClassic dkLogic = new DisputeKitClassic(); // Create a new DK to check castVote. bytes memory initDataDk = abi.encodeWithSignature( - "initialize(address,address,address)", + "initialize(address,address,address,uint256)", owner, address(core), - address(wNative) + address(wNative), + DISPUTE_KIT_CLASSIC ); UUPSProxy proxyDk = new UUPSProxy(address(dkLogic), initDataDk); From 1d37d89b6c35057a679b63cdd7ceb2cd74054b7b Mon Sep 17 00:00:00 2001 From: unknownunknown1 Date: Wed, 27 Aug 2025 21:22:36 +1000 Subject: [PATCH 067/175] feat(DK): update reinitializer --- .../src/arbitration/dispute-kits/DisputeKitClassic.sol | 6 +++--- contracts/src/arbitration/dispute-kits/DisputeKitGated.sol | 6 +++--- .../src/arbitration/dispute-kits/DisputeKitGatedShutter.sol | 6 +++--- .../src/arbitration/dispute-kits/DisputeKitShutter.sol | 6 +++--- .../arbitration/dispute-kits/DisputeKitSybilResistant.sol | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitClassic.sol b/contracts/src/arbitration/dispute-kits/DisputeKitClassic.sol index 5f2a86536..af89d2816 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitClassic.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitClassic.sol @@ -11,7 +11,7 @@ import {DisputeKitClassicBase, KlerosCore} from "./DisputeKitClassicBase.sol"; /// - an incentive system: equal split between coherent votes, /// - an appeal system: fund 2 choices only, vote on any choice. contract DisputeKitClassic is DisputeKitClassicBase { - string public constant override version = "0.12.0"; + string public constant override version = "0.13.0"; // ************************************* // // * Constructor * // @@ -36,8 +36,8 @@ contract DisputeKitClassic is DisputeKitClassicBase { __DisputeKitClassicBase_initialize(_owner, _core, _wNative, _jumpDisputeKitID); } - function reinitialize(address _wNative) external reinitializer(9) { - wNative = _wNative; + function reinitialize(uint256 _jumpDisputeKitID) external reinitializer(10) { + jumpDisputeKitID = _jumpDisputeKitID; } // ************************ // diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitGated.sol b/contracts/src/arbitration/dispute-kits/DisputeKitGated.sol index ae97a5a5e..9097a8fd3 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitGated.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitGated.sol @@ -27,7 +27,7 @@ interface IBalanceHolderERC1155 { /// - an incentive system: equal split between coherent votes, /// - an appeal system: fund 2 choices only, vote on any choice. contract DisputeKitGated is DisputeKitClassicBase { - string public constant override version = "0.12.0"; + string public constant override version = "0.13.0"; // ************************************* // // * Constructor * // @@ -52,8 +52,8 @@ contract DisputeKitGated is DisputeKitClassicBase { __DisputeKitClassicBase_initialize(_owner, _core, _wNative, _jumpDisputeKitID); } - function reinitialize(address _wNative) external reinitializer(9) { - wNative = _wNative; + function reinitialize(uint256 _jumpDisputeKitID) external reinitializer(10) { + jumpDisputeKitID = _jumpDisputeKitID; } // ************************ // diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitGatedShutter.sol b/contracts/src/arbitration/dispute-kits/DisputeKitGatedShutter.sol index ee4e9c089..158bfc5a6 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitGatedShutter.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitGatedShutter.sol @@ -28,7 +28,7 @@ interface IBalanceHolderERC1155 { /// - an incentive system: equal split between coherent votes, /// - an appeal system: fund 2 choices only, vote on any choice. contract DisputeKitGatedShutter is DisputeKitClassicBase { - string public constant override version = "0.12.0"; + string public constant override version = "0.13.0"; // ************************************* // // * Events * // @@ -71,8 +71,8 @@ contract DisputeKitGatedShutter is DisputeKitClassicBase { __DisputeKitClassicBase_initialize(_owner, _core, _wNative, _jumpDisputeKitID); } - function reinitialize(address _wNative) external reinitializer(9) { - wNative = _wNative; + function reinitialize(uint256 _jumpDisputeKitID) external reinitializer(10) { + jumpDisputeKitID = _jumpDisputeKitID; } // ************************ // diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitShutter.sol b/contracts/src/arbitration/dispute-kits/DisputeKitShutter.sol index d98e20132..0dfbf6985 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitShutter.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitShutter.sol @@ -12,7 +12,7 @@ import {DisputeKitClassicBase, KlerosCore} from "./DisputeKitClassicBase.sol"; /// - an incentive system: equal split between coherent votes, /// - an appeal system: fund 2 choices only, vote on any choice. contract DisputeKitShutter is DisputeKitClassicBase { - string public constant override version = "0.12.0"; + string public constant override version = "0.13.0"; // ************************************* // // * Events * // @@ -55,8 +55,8 @@ contract DisputeKitShutter is DisputeKitClassicBase { __DisputeKitClassicBase_initialize(_owner, _core, _wNative, _jumpDisputeKitID); } - function reinitialize(address _wNative) external reinitializer(9) { - wNative = _wNative; + function reinitialize(uint256 _jumpDisputeKitID) external reinitializer(10) { + jumpDisputeKitID = _jumpDisputeKitID; } // ************************ // diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitSybilResistant.sol b/contracts/src/arbitration/dispute-kits/DisputeKitSybilResistant.sol index 38d728444..66553f02a 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitSybilResistant.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitSybilResistant.sol @@ -18,7 +18,7 @@ interface IProofOfHumanity { /// - an incentive system: equal split between coherent votes, /// - an appeal system: fund 2 choices only, vote on any choice. contract DisputeKitSybilResistant is DisputeKitClassicBase { - string public constant override version = "0.12.0"; + string public constant override version = "0.13.0"; // ************************************* // // * Storage * // From 3ed7310a7d471ec5e57ba8b9a8a6458f32a79e71 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Fri, 29 Aug 2025 01:30:46 +0100 Subject: [PATCH 068/175] chore: minor comments tweaks --- contracts/src/arbitration/KlerosCoreBase.sol | 3 ++- .../dispute-kits/DisputeKitClassicBase.sol | 16 +++++++++------- .../src/arbitration/interfaces/IDisputeKit.sol | 2 +- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/contracts/src/arbitration/KlerosCoreBase.sol b/contracts/src/arbitration/KlerosCoreBase.sol index cbec7646c..891d7173f 100644 --- a/contracts/src/arbitration/KlerosCoreBase.sol +++ b/contracts/src/arbitration/KlerosCoreBase.sol @@ -1079,9 +1079,10 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable courtJump = true; if (!courts[newCourtID].supportedDisputeKits[newDisputeKitID]) { + // The current Dispute Kit is not compatible with the new court, jump to another Dispute Kit. newDisputeKitID = disputeKits[_round.disputeKitID].getJumpDisputeKitID(); if (!courts[newCourtID].supportedDisputeKits[newDisputeKitID]) { - // Switch to classic dispute kit if parent court doesn't support the jump DK. + // The new Dispute Kit is still not compatible, fall back to `DisputeKitClassic` which is always supported. newDisputeKitID = DISPUTE_KIT_CLASSIC; } disputeKitJump = true; diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol index 34ffd5e96..9afbfa3e3 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol @@ -180,18 +180,18 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi owner = _owner; } - /// @dev Changes the dispute kit ID used for the jump. - /// @param _jumpDisputeKitID The new value for the `jumpDisputeKitID` storage variable. - function changeJumpDisputeKitID(uint256 _jumpDisputeKitID) external onlyByOwner { - jumpDisputeKitID = _jumpDisputeKitID; - } - /// @dev Changes the `core` storage variable. /// @param _core The new value for the `core` storage variable. function changeCore(address _core) external onlyByOwner { core = KlerosCore(_core); } + /// @dev Changes the dispute kit ID used for the jump. + /// @param _jumpDisputeKitID The new value for the `jumpDisputeKitID` storage variable. + function changeJumpDisputeKitID(uint256 _jumpDisputeKitID) external onlyByOwner { + jumpDisputeKitID = _jumpDisputeKitID; + } + // ************************************* // // * State Modifiers * // // ************************************* // @@ -649,7 +649,9 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi return (_currentNbVotes * 2) + 1; } - function getJumpDisputeKitID() external view returns (uint256) { + /// @dev Returns the dispute kid ID be used after court jump by Kleros Core. + /// @return The ID of the dispute kit in Kleros Core disputeKits array. + function getJumpDisputeKitID() external view override returns (uint256) { // Fall back to classic DK in case the jump ID is not defined. return jumpDisputeKitID == 0 ? DISPUTE_KIT_CLASSIC : jumpDisputeKitID; } diff --git a/contracts/src/arbitration/interfaces/IDisputeKit.sol b/contracts/src/arbitration/interfaces/IDisputeKit.sol index ca3047176..6add5c22e 100644 --- a/contracts/src/arbitration/interfaces/IDisputeKit.sol +++ b/contracts/src/arbitration/interfaces/IDisputeKit.sol @@ -127,7 +127,7 @@ interface IDisputeKit { uint256 _currentNbVotes ) external view returns (uint256); // TODO: remove previousDisputeKit - /// @dev Returns the dispute kid ID for Kleros Core, that will be used after court jump. + /// @dev Returns the dispute kid ID be used after court jump by Kleros Core. /// @return The ID of the dispute kit in Kleros Core disputeKits array. function getJumpDisputeKitID() external view returns (uint256); From c9e73347d1892ac2e0b40b21aec912a508033deb Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Fri, 29 Aug 2025 03:05:53 +0100 Subject: [PATCH 069/175] chore: deploy script tweak to make DisputeKitGatedShutter jump to DisputeKitShutter --- .../deploy/00-home-chain-arbitration-neo.ts | 13 ++++++++----- contracts/deploy/00-home-chain-arbitration.ts | 19 +++++++++++-------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/contracts/deploy/00-home-chain-arbitration-neo.ts b/contracts/deploy/00-home-chain-arbitration-neo.ts index a47e9e4b3..22c2ffade 100644 --- a/contracts/deploy/00-home-chain-arbitration-neo.ts +++ b/contracts/deploy/00-home-chain-arbitration-neo.ts @@ -28,11 +28,11 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) await deployUpgradable(deployments, "EvidenceModule", { from: deployer, args: [deployer], log: true }); - const jumpDisputeKitID = 1; // Classic DK + const classicDisputeKitID = 1; // Classic DK const disputeKit = await deployUpgradable(deployments, "DisputeKitClassicNeo", { from: deployer, contract: "DisputeKitClassic", - args: [deployer, ZeroAddress, weth.target, jumpDisputeKitID], + args: [deployer, ZeroAddress, weth.target, classicDisputeKitID], log: true, }); @@ -123,23 +123,26 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) await core.changeArbitrableWhitelist(resolver.address, true); // Extra dispute kits + const disputeKitShutterID = 2; const disputeKitShutter = await deployUpgradable(deployments, "DisputeKitShutter", { from: deployer, - args: [deployer, core.target, weth.target, jumpDisputeKitID], + args: [deployer, core.target, weth.target, classicDisputeKitID], log: true, }); await core.addNewDisputeKit(disputeKitShutter.address); + const disputeKitGatedID = 3; const disputeKitGated = await deployUpgradable(deployments, "DisputeKitGated", { from: deployer, - args: [deployer, core.target, weth.target, jumpDisputeKitID], + args: [deployer, core.target, weth.target, classicDisputeKitID], log: true, }); await core.addNewDisputeKit(disputeKitGated.address); + const disputeKitGatedShutterID = 4; const disputeKitGatedShutter = await deployUpgradable(deployments, "DisputeKitGatedShutter", { from: deployer, - args: [deployer, core.target, weth.target, jumpDisputeKitID], + args: [deployer, core.target, weth.target, disputeKitShutterID], // Does not jump to DKClassic log: true, }); await core.addNewDisputeKit(disputeKitGatedShutter.address); diff --git a/contracts/deploy/00-home-chain-arbitration.ts b/contracts/deploy/00-home-chain-arbitration.ts index 8f8587877..8ec753194 100644 --- a/contracts/deploy/00-home-chain-arbitration.ts +++ b/contracts/deploy/00-home-chain-arbitration.ts @@ -34,10 +34,10 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) log: true, }); - const jumpDisputeKitID = 1; // Classic DK + const classicDisputeKitID = 1; // Classic DK const disputeKit = await deployUpgradable(deployments, "DisputeKitClassic", { from: deployer, - args: [deployer, ZeroAddress, weth.target, jumpDisputeKitID], + args: [deployer, ZeroAddress, weth.target, classicDisputeKitID], log: true, }); @@ -104,29 +104,32 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) } // Extra dispute kits + const disputeKitShutterID = 2; const disputeKitShutter = await deployUpgradable(deployments, "DisputeKitShutter", { from: deployer, - args: [deployer, core.target, weth.target, jumpDisputeKitID], + args: [deployer, core.target, weth.target, classicDisputeKitID], log: true, }); await core.addNewDisputeKit(disputeKitShutter.address); - await core.enableDisputeKits(Courts.GENERAL, [2], true); // enable disputeKitShutter on the General Court + await core.enableDisputeKits(Courts.GENERAL, [disputeKitShutterID], true); // enable disputeKitShutter on the General Court + const disputeKitGatedID = 3; const disputeKitGated = await deployUpgradable(deployments, "DisputeKitGated", { from: deployer, - args: [deployer, core.target, weth.target, jumpDisputeKitID], + args: [deployer, core.target, weth.target, classicDisputeKitID], log: true, }); await core.addNewDisputeKit(disputeKitGated.address); - await core.enableDisputeKits(Courts.GENERAL, [3], true); // enable disputeKitGated on the General Court + await core.enableDisputeKits(Courts.GENERAL, [disputeKitGatedID], true); // enable disputeKitGated on the General Court + const disputeKitGatedShutterID = 4; const disputeKitGatedShutter = await deployUpgradable(deployments, "DisputeKitGatedShutter", { from: deployer, - args: [deployer, core.target, weth.target, jumpDisputeKitID], + args: [deployer, core.target, weth.target, disputeKitShutterID], // Does not jump to DKClassic log: true, }); await core.addNewDisputeKit(disputeKitGatedShutter.address); - await core.enableDisputeKits(Courts.GENERAL, [4], true); // enable disputeKitGatedShutter on the General Court + await core.enableDisputeKits(Courts.GENERAL, [disputeKitGatedShutterID], true); // enable disputeKitGatedShutter on the General Court // Snapshot proxy await deploy("KlerosCoreSnapshotProxy", { From ea5b3a7fc4d105464ffd982df863f501157913f2 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Fri, 29 Aug 2025 03:53:35 +0100 Subject: [PATCH 070/175] refactor: split the KlerosCore foundry tests into several files as it was over 3000 lines --- contracts/test/foundry/KlerosCore.t.sol | 3145 ----------------- .../test/foundry/KlerosCore_Appeals.t.sol | 361 ++ .../test/foundry/KlerosCore_Disputes.t.sol | 148 + .../test/foundry/KlerosCore_Drawing.t.sol | 124 + .../test/foundry/KlerosCore_Execution.t.sol | 688 ++++ .../test/foundry/KlerosCore_Governance.t.sol | 472 +++ .../foundry/KlerosCore_Initialization.t.sol | 170 + contracts/test/foundry/KlerosCore_RNG.t.sol | 121 + .../test/foundry/KlerosCore_Staking.t.sol | 450 +++ .../test/foundry/KlerosCore_TestBase.sol | 251 ++ .../test/foundry/KlerosCore_Voting.t.sol | 485 +++ 11 files changed, 3270 insertions(+), 3145 deletions(-) delete mode 100644 contracts/test/foundry/KlerosCore.t.sol create mode 100644 contracts/test/foundry/KlerosCore_Appeals.t.sol create mode 100644 contracts/test/foundry/KlerosCore_Disputes.t.sol create mode 100644 contracts/test/foundry/KlerosCore_Drawing.t.sol create mode 100644 contracts/test/foundry/KlerosCore_Execution.t.sol create mode 100644 contracts/test/foundry/KlerosCore_Governance.t.sol create mode 100644 contracts/test/foundry/KlerosCore_Initialization.t.sol create mode 100644 contracts/test/foundry/KlerosCore_RNG.t.sol create mode 100644 contracts/test/foundry/KlerosCore_Staking.t.sol create mode 100644 contracts/test/foundry/KlerosCore_TestBase.sol create mode 100644 contracts/test/foundry/KlerosCore_Voting.t.sol diff --git a/contracts/test/foundry/KlerosCore.t.sol b/contracts/test/foundry/KlerosCore.t.sol deleted file mode 100644 index 30e84abdc..000000000 --- a/contracts/test/foundry/KlerosCore.t.sol +++ /dev/null @@ -1,3145 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; - -import {Test} from "forge-std/Test.sol"; -import {console} from "forge-std/console.sol"; // Import the console for logging -import {KlerosCoreMock, KlerosCoreBase} from "../../src/test/KlerosCoreMock.sol"; -import {IArbitratorV2} from "../../src/arbitration/KlerosCoreBase.sol"; -import {IDisputeKit} from "../../src/arbitration/interfaces/IDisputeKit.sol"; -import {DisputeKitClassic, DisputeKitClassicBase} from "../../src/arbitration/dispute-kits/DisputeKitClassic.sol"; -import {DisputeKitSybilResistant} from "../../src/arbitration/dispute-kits/DisputeKitSybilResistant.sol"; -import {ISortitionModule} from "../../src/arbitration/interfaces/ISortitionModule.sol"; -import {SortitionModuleMock, SortitionModuleBase} from "../../src/test/SortitionModuleMock.sol"; -import {UUPSProxy} from "../../src/proxy/UUPSProxy.sol"; -import {BlockHashRNG} from "../../src/rng/BlockHashRNG.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"; -import {ArbitrableExample, IArbitrableV2} from "../../src/arbitration/arbitrables/ArbitrableExample.sol"; -import {DisputeTemplateRegistry} from "../../src/arbitration/DisputeTemplateRegistry.sol"; -import "../../src/libraries/Constants.sol"; -import {IKlerosCore, KlerosCoreSnapshotProxy} from "../../src/arbitration/view/KlerosCoreSnapshotProxy.sol"; - -contract KlerosCoreTest is Test { - event Initialized(uint64 version); - - KlerosCoreMock core; - DisputeKitClassic disputeKit; - SortitionModuleMock sortitionModule; - BlockHashRNG rng; - PNK pinakion; - TestERC20 feeToken; - TestERC20 wNative; - ArbitrableExample arbitrable; - DisputeTemplateRegistry registry; - address owner; - address guardian; - address staker1; - address staker2; - address disputer; - address crowdfunder1; - address crowdfunder2; - address other; - address jurorProsecutionModule; - uint256 minStake; - uint256 alpha; - uint256 feeForJuror; - uint256 jurorsForCourtJump; - bytes sortitionExtraData; - bytes arbitratorExtraData; - uint256[4] timesPerPeriod; - bool hiddenVotes; - - uint256 totalSupply = 1000000 ether; - - uint256 minStakingTime; - uint256 maxDrawingTime; - uint256 rngLookahead; // Time in seconds - - string templateData; - string templateDataMappings; - - function setUp() public { - KlerosCoreMock coreLogic = new KlerosCoreMock(); - SortitionModuleMock smLogic = new SortitionModuleMock(); - DisputeKitClassic dkLogic = new DisputeKitClassic(); - DisputeTemplateRegistry registryLogic = new DisputeTemplateRegistry(); - pinakion = new PNK(); - feeToken = new TestERC20("Test", "TST"); - wNative = new TestERC20("wrapped ETH", "wETH"); - - owner = msg.sender; - guardian = vm.addr(1); - staker1 = vm.addr(2); - staker2 = vm.addr(3); - disputer = vm.addr(4); - crowdfunder1 = vm.addr(5); - crowdfunder2 = vm.addr(6); - vm.deal(disputer, 10 ether); - vm.deal(crowdfunder1, 10 ether); - vm.deal(crowdfunder2, 10 ether); - jurorProsecutionModule = vm.addr(8); - other = vm.addr(9); - minStake = 1000; - alpha = 10000; - feeForJuror = 0.03 ether; - jurorsForCourtJump = 511; - timesPerPeriod = [60, 120, 180, 240]; - - pinakion.transfer(msg.sender, totalSupply - 2 ether); - pinakion.transfer(staker1, 1 ether); - pinakion.transfer(staker2, 1 ether); - - sortitionExtraData = abi.encode(uint256(5)); - minStakingTime = 18; - maxDrawingTime = 24; - hiddenVotes = false; - - rngLookahead = 30; - rng = new BlockHashRNG(msg.sender, address(sortitionModule), rngLookahead); - - UUPSProxy proxyCore = new UUPSProxy(address(coreLogic), ""); - - bytes memory initDataDk = abi.encodeWithSignature( - "initialize(address,address,address)", - owner, - address(proxyCore), - address(wNative) - ); - - UUPSProxy proxyDk = new UUPSProxy(address(dkLogic), initDataDk); - disputeKit = DisputeKitClassic(address(proxyDk)); - - bytes memory initDataSm = abi.encodeWithSignature( - "initialize(address,address,uint256,uint256,address)", - owner, - address(proxyCore), - minStakingTime, - maxDrawingTime, - rng - ); - - UUPSProxy proxySm = new UUPSProxy(address(smLogic), initDataSm); - sortitionModule = SortitionModuleMock(address(proxySm)); - vm.prank(owner); - rng.changeConsumer(address(sortitionModule)); - - core = KlerosCoreMock(address(proxyCore)); - core.initialize( - owner, - guardian, - pinakion, - jurorProsecutionModule, - disputeKit, - hiddenVotes, - [minStake, alpha, feeForJuror, jurorsForCourtJump], - timesPerPeriod, - sortitionExtraData, - sortitionModule, - address(wNative) - ); - vm.prank(staker1); - pinakion.approve(address(core), 1 ether); - vm.prank(staker2); - pinakion.approve(address(core), 1 ether); - - templateData = "AAA"; - templateDataMappings = "BBB"; - arbitratorExtraData = abi.encodePacked(uint256(GENERAL_COURT), DEFAULT_NB_OF_JURORS, DISPUTE_KIT_CLASSIC); - - bytes memory initDataRegistry = abi.encodeWithSignature("initialize(address)", owner); - UUPSProxy proxyRegistry = new UUPSProxy(address(registryLogic), initDataRegistry); - registry = DisputeTemplateRegistry(address(proxyRegistry)); - - arbitrable = new ArbitrableExample( - core, - templateData, - templateDataMappings, - arbitratorExtraData, - registry, - feeToken - ); - } - - function test_initialize() public { - assertEq(core.owner(), msg.sender, "Wrong owner"); - assertEq(core.guardian(), guardian, "Wrong guardian"); - assertEq(address(core.pinakion()), address(pinakion), "Wrong pinakion address"); - assertEq(core.jurorProsecutionModule(), jurorProsecutionModule, "Wrong jurorProsecutionModule address"); - assertEq(address(core.sortitionModule()), address(sortitionModule), "Wrong sortitionModule address"); - assertEq(core.getDisputeKitsLength(), 2, "Wrong DK array length"); - ( - uint96 courtParent, - bool courtHiddenVotes, - uint256 courtMinStake, - uint256 courtAlpha, - uint256 courtFeeForJuror, - uint256 courtJurorsForCourtJump, - bool courtDisabled - ) = core.courts(FORKING_COURT); - assertEq(courtParent, FORKING_COURT, "Wrong court parent"); - assertEq(courtHiddenVotes, false, "Wrong hiddenVotes value"); - assertEq(courtMinStake, 0, "Wrong minStake value"); - assertEq(courtAlpha, 0, "Wrong alpha value"); - assertEq(courtFeeForJuror, 0, "Wrong feeForJuror value"); - assertEq(courtJurorsForCourtJump, 0, "Wrong jurorsForCourtJump value"); - assertEq(courtDisabled, false, "Court should not be disabled"); - ( - courtParent, - courtHiddenVotes, - courtMinStake, - courtAlpha, - courtFeeForJuror, - courtJurorsForCourtJump, - courtDisabled - ) = core.courts(GENERAL_COURT); - assertEq(courtParent, FORKING_COURT, "Wrong court parent"); - assertEq(courtHiddenVotes, false, "Wrong hiddenVotes value"); - assertEq(courtMinStake, 1000, "Wrong minStake value"); - assertEq(courtAlpha, 10000, "Wrong alpha value"); - assertEq(courtFeeForJuror, 0.03 ether, "Wrong feeForJuror value"); - assertEq(courtJurorsForCourtJump, 511, "Wrong jurorsForCourtJump value"); - assertEq(courtDisabled, false, "Court should not be disabled"); - - uint256[] memory children = core.getCourtChildren(GENERAL_COURT); - assertEq(children.length, 0, "No children"); - uint256[4] memory courtTimesPerPeriod = core.getTimesPerPeriod(GENERAL_COURT); - for (uint256 i = 0; i < 4; i++) { - assertEq(courtTimesPerPeriod[i], timesPerPeriod[i], "Wrong times per period"); - } - - assertEq(address(core.disputeKits(NULL_DISPUTE_KIT)), address(0), "Wrong address NULL_DISPUTE_KIT"); - assertEq( - address(core.disputeKits(DISPUTE_KIT_CLASSIC)), - address(disputeKit), - "Wrong address DISPUTE_KIT_CLASSIC" - ); - assertEq(core.isSupported(FORKING_COURT, NULL_DISPUTE_KIT), false, "Forking court null dk should be false"); - assertEq( - core.isSupported(FORKING_COURT, DISPUTE_KIT_CLASSIC), - false, - "Forking court classic dk should be false" - ); - assertEq(core.isSupported(GENERAL_COURT, NULL_DISPUTE_KIT), false, "General court null dk should be false"); - assertEq(core.isSupported(GENERAL_COURT, DISPUTE_KIT_CLASSIC), true, "General court classic dk should be true"); - assertEq(core.paused(), false, "Wrong paused value"); - - assertEq(pinakion.name(), "Pinakion", "Wrong token name"); - assertEq(pinakion.symbol(), "PNK", "Wrong token symbol"); - assertEq(pinakion.totalSupply(), 1000000 ether, "Wrong total supply"); - assertEq(pinakion.balanceOf(msg.sender), 999998 ether, "Wrong token balance of owner"); - assertEq(pinakion.balanceOf(staker1), 1 ether, "Wrong token balance of staker1"); - assertEq(pinakion.allowance(staker1, address(core)), 1 ether, "Wrong allowance for staker1"); - assertEq(pinakion.balanceOf(staker2), 1 ether, "Wrong token balance of staker2"); - assertEq(pinakion.allowance(staker2, address(core)), 1 ether, "Wrong allowance for staker2"); - - assertEq(disputeKit.owner(), msg.sender, "Wrong DK owner"); - assertEq(address(disputeKit.core()), address(core), "Wrong core in DK"); - - assertEq(sortitionModule.owner(), msg.sender, "Wrong SM owner"); - assertEq(address(sortitionModule.core()), address(core), "Wrong core in SM"); - assertEq(uint256(sortitionModule.phase()), uint256(ISortitionModule.Phase.staking), "Phase should be 0"); - assertEq(sortitionModule.minStakingTime(), 18, "Wrong minStakingTime"); - assertEq(sortitionModule.maxDrawingTime(), 24, "Wrong maxDrawingTime"); - assertEq(sortitionModule.lastPhaseChange(), block.timestamp, "Wrong lastPhaseChange"); - 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.delayedStakeWriteIndex(), 0, "delayedStakeWriteIndex should be 0"); - assertEq(sortitionModule.delayedStakeReadIndex(), 1, "Wrong delayedStakeReadIndex"); - - (uint256 K, uint256 nodeLength) = sortitionModule.getSortitionProperties(bytes32(uint256(FORKING_COURT))); - assertEq(K, 5, "Wrong tree K FORKING_COURT"); - assertEq(nodeLength, 1, "Wrong node length for created tree FORKING_COURT"); - - (K, nodeLength) = sortitionModule.getSortitionProperties(bytes32(uint256(GENERAL_COURT))); - assertEq(K, 5, "Wrong tree K GENERAL_COURT"); - assertEq(nodeLength, 1, "Wrong node length for created tree GENERAL_COURT"); - } - - function test_initialize_events() public { - KlerosCoreMock coreLogic = new KlerosCoreMock(); - SortitionModuleMock smLogic = new SortitionModuleMock(); - DisputeKitClassic dkLogic = new DisputeKitClassic(); - pinakion = new PNK(); - - owner = msg.sender; - guardian = vm.addr(1); - staker1 = vm.addr(2); - other = vm.addr(9); - jurorProsecutionModule = vm.addr(8); - minStake = 1000; - alpha = 10000; - feeForJuror = 0.03 ether; - jurorsForCourtJump = 511; - timesPerPeriod = [60, 120, 180, 240]; - - pinakion.transfer(msg.sender, totalSupply - 1 ether); - pinakion.transfer(staker1, 1 ether); - - sortitionExtraData = abi.encode(uint256(5)); - minStakingTime = 18; - maxDrawingTime = 24; - hiddenVotes = false; - - rngLookahead = 20; - rng = new BlockHashRNG(msg.sender, address(sortitionModule), rngLookahead); - - UUPSProxy proxyCore = new UUPSProxy(address(coreLogic), ""); - - bytes memory initDataDk = abi.encodeWithSignature( - "initialize(address,address,address)", - owner, - address(proxyCore), - address(wNative) - ); - - UUPSProxy proxyDk = new UUPSProxy(address(dkLogic), initDataDk); - disputeKit = DisputeKitClassic(address(proxyDk)); - - bytes memory initDataSm = abi.encodeWithSignature( - "initialize(address,address,uint256,uint256,address)", - owner, - address(proxyCore), - minStakingTime, - maxDrawingTime, - rng - ); - - UUPSProxy proxySm = new UUPSProxy(address(smLogic), initDataSm); - sortitionModule = SortitionModuleMock(address(proxySm)); - vm.prank(owner); - rng.changeConsumer(address(sortitionModule)); - - core = KlerosCoreMock(address(proxyCore)); - vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.DisputeKitCreated(DISPUTE_KIT_CLASSIC, disputeKit); - vm.expectEmit(true, true, true, true); - - uint256[] memory supportedDK = new uint256[](1); - supportedDK[0] = DISPUTE_KIT_CLASSIC; - emit KlerosCoreBase.CourtCreated( - GENERAL_COURT, - FORKING_COURT, - false, - 1000, - 10000, - 0.03 ether, - 511, - [uint256(60), uint256(120), uint256(180), uint256(240)], // Explicitly convert otherwise it throws - supportedDK - ); - vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.DisputeKitEnabled(GENERAL_COURT, DISPUTE_KIT_CLASSIC, true); - core.initialize( - owner, - guardian, - pinakion, - jurorProsecutionModule, - disputeKit, - hiddenVotes, - [minStake, alpha, feeForJuror, jurorsForCourtJump], - timesPerPeriod, - sortitionExtraData, - sortitionModule, - address(wNative) - ); - } - - // ****************************************** // - // * Governance test * // - // ****************************************** // - - function test_pause() public { - vm.expectRevert(KlerosCoreBase.GuardianOrOwnerOnly.selector); - vm.prank(other); - core.pause(); - // Note that we must explicitly switch to the owner/guardian address to make the call, otherwise Foundry treats UUPS proxy as msg.sender. - vm.prank(guardian); - vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.Paused(); - core.pause(); - assertEq(core.paused(), true, "Wrong paused value"); - // Switch between owner and guardian to test both. WhenNotPausedOnly modifier is triggered after owner's check. - vm.prank(owner); - vm.expectRevert(KlerosCoreBase.WhenNotPausedOnly.selector); - core.pause(); - } - - function test_unpause() public { - vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); - vm.prank(other); - core.unpause(); - - vm.expectRevert(KlerosCoreBase.WhenPausedOnly.selector); - vm.prank(owner); - core.unpause(); - - vm.prank(owner); - core.pause(); - vm.prank(owner); - vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.Unpaused(); - core.unpause(); - assertEq(core.paused(), false, "Wrong paused value"); - } - - function test_executeOwnerProposal() public { - bytes memory data = abi.encodeWithSignature("changeOwner(address)", other); - vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); - vm.prank(other); - core.executeOwnerProposal(address(core), 0, data); - - vm.expectRevert(KlerosCoreBase.UnsuccessfulCall.selector); - vm.prank(owner); - core.executeOwnerProposal(address(core), 0, data); // It'll fail because the core is not its own owner - - vm.prank(owner); - core.changeOwner(payable(address(core))); - vm.prank(address(core)); - core.executeOwnerProposal(address(core), 0, data); - assertEq(core.owner(), other, "Wrong owner"); - } - - function test_changeOwner() public { - vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); - vm.prank(other); - core.changeOwner(payable(other)); - vm.prank(owner); - core.changeOwner(payable(other)); - assertEq(core.owner(), other, "Wrong owner"); - } - - function test_changeGuardian() public { - vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); - vm.prank(other); - core.changeGuardian(other); - vm.prank(owner); - core.changeGuardian(other); - assertEq(core.guardian(), other, "Wrong guardian"); - } - - function test_changePinakion() public { - PNK fakePNK = new PNK(); - vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); - vm.prank(other); - core.changePinakion(fakePNK); - vm.prank(owner); - core.changePinakion(fakePNK); - assertEq(address(core.pinakion()), address(fakePNK), "Wrong PNK"); - } - - function test_changeJurorProsecutionModule() public { - vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); - vm.prank(other); - core.changeJurorProsecutionModule(other); - vm.prank(owner); - core.changeJurorProsecutionModule(other); - assertEq(core.jurorProsecutionModule(), other, "Wrong jurorProsecutionModule"); - } - - function test_changeSortitionModule() public { - SortitionModuleMock fakeSM = new SortitionModuleMock(); - vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); - vm.prank(other); - core.changeSortitionModule(fakeSM); - vm.prank(owner); - core.changeSortitionModule(fakeSM); - assertEq(address(core.sortitionModule()), address(fakeSM), "Wrong sortitionModule"); - } - - function test_addNewDisputeKit() public { - DisputeKitSybilResistant newDK = new DisputeKitSybilResistant(); - vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); - vm.prank(other); - core.addNewDisputeKit(newDK); - vm.prank(owner); - vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.DisputeKitCreated(2, newDK); - core.addNewDisputeKit(newDK); - assertEq(address(core.disputeKits(2)), address(newDK), "Wrong address of new DK"); - assertEq(core.getDisputeKitsLength(), 3, "Wrong DK array length"); - } - - function test_createCourt() public { - vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); - vm.prank(other); - uint256[] memory supportedDK = new uint256[](2); - supportedDK[0] = DISPUTE_KIT_CLASSIC; - supportedDK[1] = 2; // New DK is added below. - core.createCourt( - GENERAL_COURT, - true, // Hidden votes - 2000, // min stake - 10000, // alpha - 0.03 ether, // fee for juror - 50, // jurors for jump - [uint256(10), uint256(20), uint256(30), uint256(40)], // Times per period - abi.encode(uint256(4)), // Sortition extra data - supportedDK - ); - - vm.expectRevert(KlerosCoreBase.MinStakeLowerThanParentCourt.selector); - vm.prank(owner); - core.createCourt( - GENERAL_COURT, - true, // Hidden votes - 800, // min stake - 10000, // alpha - 0.03 ether, // fee for juror - 50, // jurors for jump - [uint256(10), uint256(20), uint256(30), uint256(40)], // Times per period - abi.encode(uint256(4)), // Sortition extra data - supportedDK - ); - - vm.expectRevert(KlerosCoreBase.UnsupportedDisputeKit.selector); - vm.prank(owner); - uint256[] memory emptySupportedDK = new uint256[](0); - core.createCourt( - GENERAL_COURT, - true, // Hidden votes - 2000, // min stake - 10000, // alpha - 0.03 ether, // fee for juror - 50, // jurors for jump - [uint256(10), uint256(20), uint256(30), uint256(40)], // Times per period - abi.encode(uint256(4)), // Sortition extra data - emptySupportedDK - ); - - vm.expectRevert(KlerosCoreBase.InvalidForkingCourtAsParent.selector); - vm.prank(owner); - core.createCourt( - FORKING_COURT, - true, // Hidden votes - 2000, // min stake - 10000, // alpha - 0.03 ether, // fee for juror - 50, // jurors for jump - [uint256(10), uint256(20), uint256(30), uint256(40)], // Times per period - abi.encode(uint256(4)), // Sortition extra data - supportedDK - ); - - uint256[] memory badSupportedDK = new uint256[](2); - badSupportedDK[0] = NULL_DISPUTE_KIT; // Include NULL_DK to check that it reverts - badSupportedDK[1] = DISPUTE_KIT_CLASSIC; - vm.expectRevert(KlerosCoreBase.WrongDisputeKitIndex.selector); - vm.prank(owner); - core.createCourt( - GENERAL_COURT, - true, // Hidden votes - 2000, // min stake - 10000, // alpha - 0.03 ether, // fee for juror - 50, // jurors for jump - [uint256(10), uint256(20), uint256(30), uint256(40)], // Times per period - abi.encode(uint256(4)), // Sortition extra data - badSupportedDK - ); - - badSupportedDK[0] = DISPUTE_KIT_CLASSIC; - badSupportedDK[1] = 2; // Check out of bounds index - vm.expectRevert(KlerosCoreBase.WrongDisputeKitIndex.selector); - vm.prank(owner); - core.createCourt( - GENERAL_COURT, - true, // Hidden votes - 2000, // min stake - 10000, // alpha - 0.03 ether, // fee for juror - 50, // jurors for jump - [uint256(10), uint256(20), uint256(30), uint256(40)], // Times per period - abi.encode(uint256(4)), // Sortition extra data - badSupportedDK - ); - - // Add new DK to check the requirement for classic DK - DisputeKitSybilResistant newDK = new DisputeKitSybilResistant(); - vm.prank(owner); - core.addNewDisputeKit(newDK); - badSupportedDK = new uint256[](1); - badSupportedDK[0] = 2; // Include only sybil resistant dk - vm.expectRevert(KlerosCoreBase.MustSupportDisputeKitClassic.selector); - vm.prank(owner); - core.createCourt( - GENERAL_COURT, - true, // Hidden votes - 2000, // min stake - 10000, // alpha - 0.03 ether, // fee for juror - 50, // jurors for jump - [uint256(10), uint256(20), uint256(30), uint256(40)], // Times per period - abi.encode(uint256(4)), // Sortition extra data - badSupportedDK - ); - - vm.prank(owner); - vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.DisputeKitEnabled(2, DISPUTE_KIT_CLASSIC, true); - vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.DisputeKitEnabled(2, 2, true); - vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.CourtCreated( - 2, - GENERAL_COURT, - true, - 2000, - 20000, - 0.04 ether, - 50, - [uint256(10), uint256(20), uint256(30), uint256(40)], // Explicitly convert otherwise it throws - supportedDK - ); - core.createCourt( - GENERAL_COURT, - true, // Hidden votes - 2000, // min stake - 20000, // alpha - 0.04 ether, // fee for juror - 50, // jurors for jump - [uint256(10), uint256(20), uint256(30), uint256(40)], // Times per period - abi.encode(uint256(4)), // Sortition extra data - supportedDK - ); - - ( - uint96 courtParent, - bool courtHiddenVotes, - uint256 courtMinStake, - uint256 courtAlpha, - uint256 courtFeeForJuror, - uint256 courtJurorsForCourtJump, - bool courtDisabled - ) = core.courts(2); - assertEq(courtParent, GENERAL_COURT, "Wrong court parent"); - assertEq(courtHiddenVotes, true, "Wrong hiddenVotes value"); - assertEq(courtMinStake, 2000, "Wrong minStake value"); - assertEq(courtAlpha, 20000, "Wrong alpha value"); - assertEq(courtFeeForJuror, 0.04 ether, "Wrong feeForJuror value"); - assertEq(courtJurorsForCourtJump, 50, "Wrong jurorsForCourtJump value"); - assertEq(courtDisabled, false, "Court should not be disabled"); - - uint256[] memory children = core.getCourtChildren(2); - assertEq(children.length, 0, "No children"); - uint256[4] memory courtTimesPerPeriod = core.getTimesPerPeriod(2); - assertEq(courtTimesPerPeriod[0], uint256(10), "Wrong times per period 0"); - assertEq(courtTimesPerPeriod[1], uint256(20), "Wrong times per period 1"); - assertEq(courtTimesPerPeriod[2], uint256(30), "Wrong times per period 2"); - assertEq(courtTimesPerPeriod[3], uint256(40), "Wrong times per period 3"); - - children = core.getCourtChildren(GENERAL_COURT); // Check that parent updated children - assertEq(children.length, 1, "Wrong children count"); - assertEq(children[0], 2, "Wrong child id"); - - (uint256 K, uint256 nodeLength) = sortitionModule.getSortitionProperties(bytes32(uint256(2))); - assertEq(K, 4, "Wrong tree K of the new court"); - assertEq(nodeLength, 1, "Wrong node length for created tree of the new court"); - } - - function test_changeCourtParameters() public { - // Create a 2nd court to check the minStake requirements - vm.prank(owner); - uint96 newCourtID = 2; - uint256[] memory supportedDK = new uint256[](1); - supportedDK[0] = DISPUTE_KIT_CLASSIC; - core.createCourt( - GENERAL_COURT, - true, // Hidden votes - 2000, // min stake - 20000, // alpha - 0.04 ether, // fee for juror - 50, // jurors for jump - [uint256(10), uint256(20), uint256(30), uint256(40)], // Times per period - abi.encode(uint256(4)), // Sortition extra data - supportedDK - ); - - vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); - vm.prank(other); - core.changeCourtParameters( - GENERAL_COURT, - true, // Hidden votes - 2000, // min stake - 10000, // alpha - 0.03 ether, // fee for juror - 50, // jurors for jump - [uint256(10), uint256(20), uint256(30), uint256(40)] // Times per period - ); - vm.expectRevert(KlerosCoreBase.MinStakeLowerThanParentCourt.selector); - vm.prank(owner); - // Min stake of a parent became higher than of a child - core.changeCourtParameters( - GENERAL_COURT, - true, // Hidden votes - 3000, // min stake - 10000, // alpha - 0.03 ether, // fee for juror - 50, // jurors for jump - [uint256(10), uint256(20), uint256(30), uint256(40)] // Times per period - ); - // Min stake of a child became lower than of a parent - vm.expectRevert(KlerosCoreBase.MinStakeLowerThanParentCourt.selector); - vm.prank(owner); - core.changeCourtParameters( - newCourtID, - true, // Hidden votes - 800, // min stake - 10000, // alpha - 0.03 ether, // fee for juror - 50, // jurors for jump - [uint256(10), uint256(20), uint256(30), uint256(40)] // Times per period - ); - - vm.prank(owner); - vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.CourtModified( - GENERAL_COURT, - true, - 2000, - 20000, - 0.04 ether, - 50, - [uint256(10), uint256(20), uint256(30), uint256(40)] // Explicitly convert otherwise it throws - ); - core.changeCourtParameters( - GENERAL_COURT, - true, // Hidden votes - 2000, // min stake - 20000, // alpha - 0.04 ether, // fee for juror - 50, // jurors for jump - [uint256(10), uint256(20), uint256(30), uint256(40)] // Times per period - ); - - ( - uint96 courtParent, - bool courtHiddenVotes, - uint256 courtMinStake, - uint256 courtAlpha, - uint256 courtFeeForJuror, - uint256 courtJurorsForCourtJump, - bool courtDisabled - ) = core.courts(GENERAL_COURT); - assertEq(courtHiddenVotes, true, "Wrong hiddenVotes value"); - assertEq(courtMinStake, 2000, "Wrong minStake value"); - assertEq(courtAlpha, 20000, "Wrong alpha value"); - assertEq(courtFeeForJuror, 0.04 ether, "Wrong feeForJuror value"); - assertEq(courtJurorsForCourtJump, 50, "Wrong jurorsForCourtJump value"); - assertEq(courtDisabled, false, "Court should not be disabled"); - - uint256[4] memory courtTimesPerPeriod = core.getTimesPerPeriod(GENERAL_COURT); - assertEq(courtTimesPerPeriod[0], uint256(10), "Wrong times per period 0"); - assertEq(courtTimesPerPeriod[1], uint256(20), "Wrong times per period 1"); - assertEq(courtTimesPerPeriod[2], uint256(30), "Wrong times per period 2"); - assertEq(courtTimesPerPeriod[3], uint256(40), "Wrong times per period 3"); - } - - function test_enableDisputeKits() public { - DisputeKitSybilResistant newDK = new DisputeKitSybilResistant(); - uint256 newDkID = 2; - vm.prank(owner); - core.addNewDisputeKit(newDK); - - vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); - vm.prank(other); - uint256[] memory supportedDK = new uint256[](1); - supportedDK[0] = newDkID; - core.enableDisputeKits(GENERAL_COURT, supportedDK, true); - - vm.expectRevert(KlerosCoreBase.WrongDisputeKitIndex.selector); - vm.prank(owner); - supportedDK[0] = NULL_DISPUTE_KIT; - core.enableDisputeKits(GENERAL_COURT, supportedDK, true); - - vm.expectRevert(KlerosCoreBase.WrongDisputeKitIndex.selector); - vm.prank(owner); - supportedDK[0] = 3; // Out of bounds - core.enableDisputeKits(GENERAL_COURT, supportedDK, true); - - vm.expectRevert(KlerosCoreBase.CannotDisableClassicDK.selector); - vm.prank(owner); - supportedDK[0] = DISPUTE_KIT_CLASSIC; - core.enableDisputeKits(GENERAL_COURT, supportedDK, false); - - vm.prank(owner); - vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.DisputeKitEnabled(GENERAL_COURT, newDkID, true); - supportedDK[0] = newDkID; - core.enableDisputeKits(GENERAL_COURT, supportedDK, true); - assertEq(core.isSupported(GENERAL_COURT, newDkID), true, "New DK should be supported by General court"); - - vm.prank(owner); - vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.DisputeKitEnabled(GENERAL_COURT, newDkID, false); - core.enableDisputeKits(GENERAL_COURT, supportedDK, false); - assertEq(core.isSupported(GENERAL_COURT, newDkID), false, "New DK should be disabled in General court"); - } - - function test_changeAcceptedFeeTokens() public { - vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); - vm.prank(other); - core.changeAcceptedFeeTokens(feeToken, true); - - (bool accepted, , ) = core.currencyRates(feeToken); - assertEq(accepted, false, "Token should not be accepted yet"); - - vm.prank(owner); - vm.expectEmit(true, true, true, true); - emit IArbitratorV2.AcceptedFeeToken(feeToken, true); - core.changeAcceptedFeeTokens(feeToken, true); - (accepted, , ) = core.currencyRates(feeToken); - assertEq(accepted, true, "Token should be accepted"); - } - - function test_changeCurrencyRates() public { - vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); - vm.prank(other); - core.changeCurrencyRates(feeToken, 100, 200); - - (, uint256 rateInEth, uint256 rateDecimals) = core.currencyRates(feeToken); - assertEq(rateInEth, 0, "rateInEth should be 0"); - assertEq(rateDecimals, 0, "rateDecimals should be 0"); - - vm.prank(owner); - vm.expectEmit(true, true, true, true); - emit IArbitratorV2.NewCurrencyRate(feeToken, 100, 200); - core.changeCurrencyRates(feeToken, 100, 200); - - (, rateInEth, rateDecimals) = core.currencyRates(feeToken); - assertEq(rateInEth, 100, "rateInEth is incorrect"); - assertEq(rateDecimals, 200, "rateDecimals is incorrect"); - } - - function test_extraDataToCourtIDMinJurorsDisputeKit() public { - // Standard values - bytes memory extraData = abi.encodePacked(uint256(GENERAL_COURT), DEFAULT_NB_OF_JURORS, DISPUTE_KIT_CLASSIC); - - (uint96 courtID, uint256 minJurors, uint256 disputeKitID) = core.extraDataToCourtIDMinJurorsDisputeKit( - extraData - ); - assertEq(courtID, GENERAL_COURT, "Wrong courtID"); - assertEq(minJurors, DEFAULT_NB_OF_JURORS, "Wrong minJurors"); - assertEq(disputeKitID, DISPUTE_KIT_CLASSIC, "Wrong disputeKitID"); - - // Botched extraData. Values should fall into standard - extraData = "0xfa"; - - (courtID, minJurors, disputeKitID) = core.extraDataToCourtIDMinJurorsDisputeKit(extraData); - assertEq(courtID, GENERAL_COURT, "Wrong courtID"); - assertEq(minJurors, DEFAULT_NB_OF_JURORS, "Wrong minJurors"); - assertEq(disputeKitID, DISPUTE_KIT_CLASSIC, "Wrong disputeKitID"); - - // Custom values. - vm.startPrank(owner); - core.addNewDisputeKit(disputeKit); - core.addNewDisputeKit(disputeKit); - core.addNewDisputeKit(disputeKit); - core.addNewDisputeKit(disputeKit); - core.addNewDisputeKit(disputeKit); - extraData = abi.encodePacked(uint256(50), uint256(41), uint256(6)); - - (courtID, minJurors, disputeKitID) = core.extraDataToCourtIDMinJurorsDisputeKit(extraData); - assertEq(courtID, GENERAL_COURT, "Wrong courtID"); // Value in extra data is out of scope so fall back - assertEq(minJurors, 41, "Wrong minJurors"); - assertEq(disputeKitID, 6, "Wrong disputeKitID"); - } - - // *************************************** // - // * Staking test * // - // *************************************** // - - function test_setStake_increase() public { - vm.prank(owner); - core.pause(); - vm.expectRevert(KlerosCoreBase.WhenNotPausedOnly.selector); - vm.prank(staker1); - core.setStake(GENERAL_COURT, 1000); - vm.prank(owner); - core.unpause(); - - vm.expectRevert(KlerosCoreBase.StakingNotPossibleInThisCourt.selector); - vm.prank(staker1); - core.setStake(FORKING_COURT, 1000); - - uint96 badCourtID = 2; - vm.expectRevert(KlerosCoreBase.StakingNotPossibleInThisCourt.selector); - vm.prank(staker1); - core.setStake(badCourtID, 1000); - - vm.expectRevert(KlerosCoreBase.StakingLessThanCourtMinStake.selector); - vm.prank(staker1); - core.setStake(GENERAL_COURT, 800); - - vm.expectRevert(KlerosCoreBase.StakingZeroWhenNoStake.selector); - vm.prank(staker1); - core.setStake(GENERAL_COURT, 0); - - vm.prank(staker1); - vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeSet(staker1, GENERAL_COURT, 1001, 1001); - core.setStake(GENERAL_COURT, 1001); - - (uint256 totalStaked, uint256 totalLocked, uint256 stakedInCourt, uint256 nbCourts) = sortitionModule - .getJurorBalance(staker1, GENERAL_COURT); - assertEq(totalStaked, 1001, "Wrong amount total staked"); - assertEq(totalLocked, 0, "Wrong amount locked"); - assertEq(stakedInCourt, 1001, "Wrong amount staked in court"); - assertEq(nbCourts, 1, "Wrong number of courts"); - - uint96[] memory courts = sortitionModule.getJurorCourtIDs(staker1); - assertEq(courts.length, 1, "Wrong courts count"); - assertEq(courts[0], GENERAL_COURT, "Wrong court id"); - assertEq(sortitionModule.isJurorStaked(staker1), true, "Juror should be staked"); - - assertEq(pinakion.balanceOf(address(core)), 1001, "Wrong token balance of the core"); - assertEq(pinakion.balanceOf(staker1), 999999999999998999, "Wrong token balance of staker1"); // 1 eth - 1001 wei - assertEq(pinakion.allowance(staker1, address(core)), 999999999999998999, "Wrong allowance for staker1"); - - vm.expectRevert(KlerosCoreBase.StakingTransferFailed.selector); // This error will be caught because owner didn't approve any tokens for KlerosCore - vm.prank(owner); - core.setStake(GENERAL_COURT, 1000); - - // Increase stake one more time to verify the correct behavior - vm.prank(staker1); - vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeSet(staker1, GENERAL_COURT, 2000, 2000); - core.setStake(GENERAL_COURT, 2000); - - (totalStaked, totalLocked, stakedInCourt, nbCourts) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); - assertEq(totalStaked, 2000, "Wrong amount total staked"); - assertEq(totalLocked, 0, "Wrong amount locked"); - assertEq(stakedInCourt, 2000, "Wrong amount staked in court"); - assertEq(nbCourts, 1, "Number of courts should not increase"); - - assertEq(pinakion.balanceOf(address(core)), 2000, "Wrong token balance of the core"); - assertEq(pinakion.balanceOf(staker1), 999999999999998000, "Wrong token balance of staker1"); // 1 eth - 2000 wei - assertEq(pinakion.allowance(staker1, address(core)), 999999999999998000, "Wrong allowance for staker1"); - } - - function test_setStake_decrease() public { - vm.prank(staker1); - core.setStake(GENERAL_COURT, 2000); - assertEq(pinakion.balanceOf(address(core)), 2000, "Wrong token balance of the core"); - assertEq(pinakion.balanceOf(staker1), 999999999999998000, "Wrong token balance of staker1"); - assertEq(pinakion.allowance(staker1, address(core)), 999999999999998000, "Wrong allowance for staker1"); - - vm.prank(staker1); - core.setStake(GENERAL_COURT, 1500); // Decrease the stake to see if it's reflected correctly - (uint256 totalStaked, uint256 totalLocked, uint256 stakedInCourt, uint256 nbCourts) = sortitionModule - .getJurorBalance(staker1, GENERAL_COURT); - assertEq(totalStaked, 1500, "Wrong amount total staked"); - assertEq(totalLocked, 0, "Wrong amount locked"); - assertEq(stakedInCourt, 1500, "Wrong amount staked in court"); - assertEq(nbCourts, 1, "Wrong number of courts"); - - uint96[] memory courts = sortitionModule.getJurorCourtIDs(staker1); - assertEq(courts.length, 1, "Wrong courts count"); - assertEq(courts[0], GENERAL_COURT, "Wrong court id"); - assertEq(sortitionModule.isJurorStaked(staker1), true, "Juror should be staked"); - - assertEq(pinakion.balanceOf(address(core)), 1500, "Wrong token balance of the core"); - assertEq(pinakion.balanceOf(staker1), 999999999999998500, "Wrong token balance of staker1"); - assertEq( - pinakion.allowance(staker1, address(core)), - 999999999999998000, - "Allowance should not change during withdrawal" - ); - - vm.prank(address(core)); - pinakion.transfer(staker1, 1); // Manually send 1 token to make the withdrawal fail - - vm.expectRevert(KlerosCoreBase.UnstakingTransferFailed.selector); - vm.prank(staker1); - core.setStake(GENERAL_COURT, 0); - - vm.prank(address(staker1)); - pinakion.transfer(address(core), 1); // Manually give the token back - vm.prank(staker1); - core.setStake(GENERAL_COURT, 0); - - (totalStaked, totalLocked, stakedInCourt, nbCourts) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); - assertEq(totalStaked, 0, "Wrong amount total staked"); - assertEq(totalLocked, 0, "Wrong amount locked"); - assertEq(stakedInCourt, 0, "Wrong amount staked in court"); - assertEq(nbCourts, 0, "Wrong number of courts"); - - courts = sortitionModule.getJurorCourtIDs(staker1); - assertEq(courts.length, 0, "Wrong courts count"); - assertEq(sortitionModule.isJurorStaked(staker1), false, "Juror should not be staked"); - - assertEq(pinakion.balanceOf(address(core)), 0, "Wrong token balance of the core"); - assertEq(pinakion.balanceOf(staker1), 1 ether, "Wrong token balance of staker1"); - assertEq( - pinakion.allowance(staker1, address(core)), - 999999999999998000, - "Allowance should not change during withdrawal" - ); - } - - function test_setStake_maxStakePathCheck() public { - uint256[] memory supportedDK = new uint256[](1); - supportedDK[0] = DISPUTE_KIT_CLASSIC; - - // Create 4 courts to check the require - for (uint96 i = GENERAL_COURT; i <= 4; i++) { - vm.prank(owner); - core.createCourt( - GENERAL_COURT, - true, - 2000, - 20000, - 0.04 ether, - 50, - [uint256(10), uint256(20), uint256(30), uint256(40)], - abi.encode(uint256(4)), - supportedDK - ); - vm.prank(staker1); - core.setStake(i, 2000); - } - - uint96[] memory courts = sortitionModule.getJurorCourtIDs(staker1); - assertEq(courts.length, 4, "Wrong courts count"); - - uint96 excessiveCourtID = 5; - vm.expectRevert(KlerosCoreBase.StakingInTooManyCourts.selector); - vm.prank(staker1); - core.setStake(excessiveCourtID, 2000); - } - - function test_setStake_increaseDrawingPhase() public { - // Set the stake and create a dispute to advance the phase - vm.prank(staker1); - core.setStake(GENERAL_COURT, 1000); - vm.prank(disputer); - arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); - assertEq(sortitionModule.disputesWithoutJurors(), 1, "Wrong disputesWithoutJurors count"); - vm.warp(block.timestamp + minStakingTime); - sortitionModule.passPhase(); // Generating - vm.warp(block.timestamp + rngLookahead); - sortitionModule.passPhase(); // Drawing phase - - assertEq(pinakion.balanceOf(address(core)), 1000, "Wrong token balance of the core"); - assertEq(pinakion.balanceOf(staker1), 999999999999999000, "Wrong token balance of staker1"); - assertEq(uint256(sortitionModule.phase()), uint256(ISortitionModule.Phase.drawing), "Wrong phase"); - - vm.prank(staker1); - vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeDelayed(staker1, GENERAL_COURT, 1500); - core.setStake(GENERAL_COURT, 1500); - - uint256 delayedStakeId = sortitionModule.delayedStakeWriteIndex(); - assertEq(delayedStakeId, 1, "Wrong delayedStakeWriteIndex"); - assertEq(sortitionModule.delayedStakeReadIndex(), 1, "Wrong delayedStakeReadIndex"); - (address account, uint96 courtID, uint256 stake, bool alreadyTransferred) = sortitionModule.delayedStakes( - delayedStakeId - ); - assertEq(account, staker1, "Wrong staker account"); - assertEq(courtID, GENERAL_COURT, "Wrong court id"); - assertEq(stake, 1500, "Wrong amount staked in court"); - assertEq(alreadyTransferred, false, "Should be flagged as transferred"); - - (uint256 totalStaked, uint256 totalLocked, uint256 stakedInCourt, uint256 nbCourts) = sortitionModule - .getJurorBalance(staker1, GENERAL_COURT); - assertEq(totalStaked, 1000, "Wrong amount total staked"); - assertEq(totalLocked, 0, "Wrong amount locked"); - assertEq(stakedInCourt, 1000, "Amount staked in court should not change until delayed stake is executed"); - assertEq(nbCourts, 1, "Wrong number of courts"); - - uint96[] memory courts = sortitionModule.getJurorCourtIDs(staker1); - assertEq(courts.length, 1, "Wrong courts count"); - assertEq(courts[0], GENERAL_COURT, "Wrong court id"); - assertEq(sortitionModule.isJurorStaked(staker1), true, "Juror should be staked"); - - assertEq(pinakion.balanceOf(address(core)), 1000, "Wrong token balance of the core"); - assertEq(pinakion.balanceOf(staker1), 999999999999999000, "Wrong token balance of staker1"); - } - - function test_setStake_decreaseDrawingPhase() public { - // Set the stake and create a dispute to advance the phase - vm.prank(staker1); - core.setStake(GENERAL_COURT, 2000); - vm.prank(disputer); - arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); - vm.warp(block.timestamp + minStakingTime); - sortitionModule.passPhase(); // Generating - vm.warp(block.timestamp + rngLookahead); - sortitionModule.passPhase(); // Drawing phase - - assertEq(pinakion.balanceOf(address(core)), 2000, "Wrong token balance of the core"); - assertEq(pinakion.balanceOf(staker1), 999999999999998000, "Wrong token balance of staker1"); - - vm.prank(staker1); - vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeDelayed(staker1, GENERAL_COURT, 1800); - core.setStake(GENERAL_COURT, 1800); - - (uint256 totalStaked, , uint256 stakedInCourt, ) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); - assertEq(totalStaked, 2000, "Total staked amount should not change"); - assertEq(stakedInCourt, 2000, "Amount staked in court should not change"); - - assertEq(pinakion.balanceOf(address(core)), 2000, "Token balance of the core should not change"); - assertEq(pinakion.balanceOf(staker1), 999999999999998000, "Wrong token balance of staker1"); - } - - function test_setStake_LockedTokens() public { - // Check that correct amount is taken when locked tokens amount exceeds the staked amount - vm.prank(staker1); - core.setStake(GENERAL_COURT, 10000); - vm.prank(disputer); - arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); - vm.warp(block.timestamp + minStakingTime); - sortitionModule.passPhase(); // Generating - vm.warp(block.timestamp + rngLookahead); - sortitionModule.passPhase(); // Drawing phase - - uint256 disputeID = 0; - core.draw(disputeID, DEFAULT_NB_OF_JURORS); - (uint256 totalStaked, uint256 totalLocked, uint256 stakedInCourt, uint256 nbCourts) = sortitionModule - .getJurorBalance(staker1, GENERAL_COURT); - assertEq(totalStaked, 10000, "Wrong amount total staked"); - assertEq(totalLocked, 3000, "Wrong amount locked"); // 1000 per draw and the juror was drawn 3 times - assertEq(stakedInCourt, 10000, "Wrong amount staked in court"); - - sortitionModule.passPhase(); // Staking - - assertEq(pinakion.balanceOf(address(core)), 10000, "Wrong token balance of the core"); - assertEq(pinakion.balanceOf(staker1), 999999999999990000, "Wrong token balance of staker1"); - - // Unstake to check that locked tokens won't be withdrawn - vm.prank(staker1); - core.setStake(GENERAL_COURT, 0); - - (totalStaked, totalLocked, stakedInCourt, nbCourts) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); - assertEq(totalStaked, 3000, "Wrong amount total staked"); - assertEq(totalLocked, 3000, "Wrong amount locked"); - assertEq(stakedInCourt, 0, "Wrong amount staked in court"); - assertEq(nbCourts, 0, "Wrong amount staked in court"); - - assertEq(pinakion.balanceOf(address(core)), 3000, "Wrong token balance of the core"); - assertEq(pinakion.balanceOf(staker1), 999999999999997000, "Wrong token balance of staker1"); - - // Stake again to check the behaviour. - vm.prank(staker1); - core.setStake(GENERAL_COURT, 5000); - - (totalStaked, totalLocked, stakedInCourt, nbCourts) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); - assertEq(totalStaked, 8000, "Wrong amount total staked"); // 5000 were added to the previous 3000. - assertEq(totalLocked, 3000, "Wrong amount locked"); - assertEq(stakedInCourt, 5000, "Wrong amount staked in court"); - assertEq(nbCourts, 1, "Wrong amount staked in court"); - - assertEq(pinakion.balanceOf(address(core)), 8000, "Wrong amount of tokens in Core"); - assertEq(pinakion.balanceOf(staker1), 999999999999992000, "Wrong token balance of staker1"); - } - - function test_executeDelayedStakes() public { - // Stake as staker2 as well to diversify the execution of delayed stakes - vm.prank(staker2); - core.setStake(GENERAL_COURT, 10000); - - vm.expectRevert(SortitionModuleBase.NoDelayedStakeToExecute.selector); - sortitionModule.executeDelayedStakes(5); - - // Set the stake and create a dispute to advance the phase - vm.prank(disputer); - arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); - vm.warp(block.timestamp + minStakingTime); - sortitionModule.passPhase(); // Generating - vm.warp(block.timestamp + rngLookahead); - sortitionModule.passPhase(); // Drawing phase - uint256 disputeID = 0; - core.draw(disputeID, DEFAULT_NB_OF_JURORS); - - vm.expectRevert(SortitionModuleBase.NotStakingPhase.selector); - sortitionModule.executeDelayedStakes(5); - - // Create delayed stake - vm.prank(staker1); - vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeDelayed(staker1, GENERAL_COURT, 1500); - core.setStake(GENERAL_COURT, 1500); - - assertEq(pinakion.balanceOf(address(core)), 10000, "Wrong token balance of the core"); // Balance should not increase because the stake was delayed - assertEq(pinakion.balanceOf(staker1), 1 ether, "Wrong token balance of staker1"); - - // Create delayed stake for another staker - vm.prank(staker2); - vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeDelayed(staker2, GENERAL_COURT, 0); - core.setStake(GENERAL_COURT, 0); - assertEq(pinakion.balanceOf(staker2), 999999999999990000, "Wrong token balance of staker2"); // Balance should not change since wrong phase - - // Create another delayed stake for staker1 on top of it to check the execution - vm.prank(staker1); - vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeDelayed(staker1, GENERAL_COURT, 1800); - core.setStake(GENERAL_COURT, 1800); - - assertEq(sortitionModule.delayedStakeWriteIndex(), 3, "Wrong delayedStakeWriteIndex"); - assertEq(sortitionModule.delayedStakeReadIndex(), 1, "Wrong delayedStakeReadIndex"); - - (address account, uint96 courtID, uint256 stake, bool alreadyTransferred) = sortitionModule.delayedStakes(1); - - // Check each delayed stake - assertEq(account, staker1, "Wrong staker account for the first delayed stake"); - assertEq(courtID, GENERAL_COURT, "Wrong court ID"); - assertEq(stake, 1500, "Wrong staking amount"); - assertEq(alreadyTransferred, false, "Should be false"); - - (account, courtID, stake, alreadyTransferred) = sortitionModule.delayedStakes(2); - assertEq(account, staker2, "Wrong staker2 account"); - assertEq(courtID, GENERAL_COURT, "Wrong court id for staker2"); - assertEq(stake, 0, "Wrong amount for delayed stake of staker2"); - assertEq(alreadyTransferred, false, "Should be false"); - - (account, courtID, stake, alreadyTransferred) = sortitionModule.delayedStakes(3); - assertEq(account, staker1, "Wrong staker1 account"); - assertEq(courtID, GENERAL_COURT, "Wrong court id for staker1"); - assertEq(stake, 1800, "Wrong amount for delayed stake of staker1"); - assertEq(alreadyTransferred, false, "Should be false"); - - // So far the only amount transferred was 10000 by staker2. Staker 1 has two delayed stakes, for 1500 and 1800 pnk. - assertEq(pinakion.balanceOf(address(core)), 10000, "Wrong token balance of the core"); - assertEq(pinakion.balanceOf(staker1), 1 ether, "Wrong token balance of staker1"); - assertEq(pinakion.balanceOf(staker2), 999999999999990000, "Wrong token balance of staker2"); - - (uint256 totalStaked, uint256 totalLocked, uint256 stakedInCourt, uint256 nbCourts) = sortitionModule - .getJurorBalance(staker1, GENERAL_COURT); // Only check the first staker to check how consecutive delayed stakes are handled. - // Balances shouldn't be updated yet. - assertEq(totalStaked, 0, "Wrong amount total staked"); - assertEq(totalLocked, 0, "Wrong amount locked"); - assertEq(stakedInCourt, 0, "Wrong amount staked in court"); - assertEq(nbCourts, 0, "Wrong number of courts"); - - vm.warp(block.timestamp + minStakingTime); - sortitionModule.passPhase(); // Staking. Delayed stakes can be executed now - - vm.prank(address(core)); - pinakion.transfer(owner, 10000); // Dispose of the tokens of 2nd staker to make the execution fail for the 2nd delayed stake - assertEq(pinakion.balanceOf(address(core)), 0, "Wrong token balance of the core"); - - // 2 events should be emitted but the 2nd stake supersedes the first one in the end. - vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeSet(staker1, GENERAL_COURT, 1500, 1500); - vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeSet(staker1, GENERAL_COURT, 1800, 1800); - sortitionModule.executeDelayedStakes(20); // Deliberately ask for more iterations than needed - - assertEq(sortitionModule.delayedStakeWriteIndex(), 3, "Wrong delayedStakeWriteIndex"); - assertEq(sortitionModule.delayedStakeReadIndex(), 4, "Wrong delayedStakeReadIndex"); - - // Check that delayed stakes are nullified - for (uint i = 2; i <= sortitionModule.delayedStakeWriteIndex(); i++) { - (account, courtID, stake, alreadyTransferred) = sortitionModule.delayedStakes(i); - - assertEq(account, address(0), "Wrong staker account after delayed stake deletion"); - assertEq(courtID, 0, "Court id should be nullified"); - assertEq(stake, 0, "No amount to stake"); - assertEq(alreadyTransferred, false, "Should be false"); - } - - assertEq(pinakion.balanceOf(staker1), 999999999999998200, "Wrong token balance of staker1"); - - (totalStaked, totalLocked, stakedInCourt, nbCourts) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); - assertEq(totalStaked, 1800, "Wrong amount total staked"); - assertEq(totalLocked, 0, "Wrong amount locked"); - assertEq(stakedInCourt, 1800, "Wrong amount staked in court"); - assertEq(nbCourts, 1, "Wrong amount staked in court"); - - // Staker2 not getting the tokens back indicates that his delayed stake was skipped and the flow wasn't disrupted - assertEq(pinakion.balanceOf(staker2), 999999999999990000, "Wrong token balance of staker2"); - } - - function test_setStakeBySortitionModule() public { - // Note that functionality of this function was checked during delayed stakes execution - vm.expectRevert(KlerosCoreBase.SortitionModuleOnly.selector); - vm.prank(owner); - core.setStakeBySortitionModule(staker1, GENERAL_COURT, 1000); - } - - function test_setStake_snapshotProxyCheck() public { - vm.prank(staker1); - core.setStake(GENERAL_COURT, 12346); - - KlerosCoreSnapshotProxy snapshotProxy = new KlerosCoreSnapshotProxy(owner, IKlerosCore(address(core))); - assertEq(snapshotProxy.name(), "Staked Pinakion", "Wrong name of the proxy token"); - assertEq(snapshotProxy.symbol(), "stPNK", "Wrong symbol of the proxy token"); - assertEq(snapshotProxy.decimals(), 18, "Wrong decimals of the proxy token"); - assertEq(snapshotProxy.owner(), msg.sender, "Wrong owner"); - assertEq(address(snapshotProxy.core()), address(core), "Wrong core in snapshot proxy"); - assertEq(snapshotProxy.balanceOf(staker1), 12346, "Wrong stPNK balance"); - - vm.prank(other); - vm.expectRevert(KlerosCoreSnapshotProxy.OwnerOnly.selector); - snapshotProxy.changeCore(IKlerosCore(other)); - vm.prank(owner); - snapshotProxy.changeCore(IKlerosCore(other)); - assertEq(address(snapshotProxy.core()), other, "Wrong core in snapshot proxy after change"); - - vm.prank(other); - vm.expectRevert(KlerosCoreSnapshotProxy.OwnerOnly.selector); - snapshotProxy.changeOwner(other); - vm.prank(owner); - snapshotProxy.changeOwner(other); - assertEq(snapshotProxy.owner(), other, "Wrong owner after change"); - } - - // *************************************** // - // * Disputes * // - // *************************************** // - - function test_createDispute_eth() public { - // Create a new court and DK to test non-standard extra data - uint256 newFee = 0.01 ether; - uint96 newCourtID = 2; - uint256 newNbJurors = 4; - uint256 newDkID = 2; - uint256[] memory supportedDK = new uint256[](1); - supportedDK[0] = DISPUTE_KIT_CLASSIC; - bytes memory newExtraData = abi.encodePacked(uint256(newCourtID), newNbJurors, newDkID); - - vm.prank(owner); - core.addNewDisputeKit(disputeKit); // Just add the same dk to avoid dealing with initialization - vm.prank(owner); - core.createCourt( - GENERAL_COURT, - true, // Hidden votes - 2000, // min stake - 20000, // alpha - newFee, // fee for juror - 50, // jurors for jump - [uint256(10), uint256(20), uint256(30), uint256(40)], // Times per period - abi.encode(uint256(4)), // Sortition extra data - supportedDK - ); - - arbitrable.changeArbitratorExtraData(newExtraData); - - vm.expectRevert(KlerosCoreBase.ArbitrationFeesNotEnough.selector); - vm.prank(disputer); - arbitrable.createDispute{value: newFee * newNbJurors - 1}("Action"); - - vm.expectRevert(KlerosCoreBase.DisputeKitNotSupportedByCourt.selector); - vm.prank(disputer); - arbitrable.createDispute{value: 0.04 ether}("Action"); - - vm.prank(owner); - supportedDK = new uint256[](1); - supportedDK[0] = newDkID; - core.enableDisputeKits(newCourtID, supportedDK, true); - - uint256 disputeID = 0; - uint256 nbChoices = 2; - vm.prank(disputer); - vm.expectEmit(true, true, true, true); - emit DisputeKitClassicBase.DisputeCreation(disputeID, nbChoices, newExtraData); - vm.expectEmit(true, true, true, true); - emit IArbitratorV2.DisputeCreation(disputeID, arbitrable); - arbitrable.createDispute{value: 0.04 ether}("Action"); - - assertEq(sortitionModule.disputesWithoutJurors(), 1, "Wrong disputesWithoutJurors count"); - ( - uint96 courtID, - IArbitrableV2 arbitrated, - KlerosCoreBase.Period period, - bool ruled, - uint256 lastPeriodChange - ) = core.disputes(disputeID); - - assertEq(courtID, newCourtID, "Wrong court ID"); - assertEq(address(arbitrated), address(arbitrable), "Wrong arbitrable"); - assertEq(uint256(period), uint256(KlerosCoreBase.Period.evidence), "Wrong period"); - assertEq(ruled, false, "Should not be ruled"); - assertEq(lastPeriodChange, block.timestamp, "Wrong lastPeriodChange"); - - KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 0); - assertEq(round.disputeKitID, newDkID, "Wrong DK ID"); - assertEq(round.pnkAtStakePerJuror, 4000, "Wrong pnkAtStakePerJuror"); // minStake * alpha / divisor = 2000 * 20000/10000 - assertEq(round.totalFeesForJurors, 0.04 ether, "Wrong totalFeesForJurors"); - assertEq(round.nbVotes, 4, "Wrong nbVotes"); - assertEq(round.repartitions, 0, "repartitions should be 0"); - assertEq(round.pnkPenalties, 0, "pnkPenalties should be 0"); - assertEq(round.sumFeeRewardPaid, 0, "sumFeeRewardPaid should be 0"); - assertEq(round.sumPnkRewardPaid, 0, "sumPnkRewardPaid should be 0"); - assertEq(address(round.feeToken), address(0), "feeToken should be 0"); - assertEq(round.drawIterations, 0, "drawIterations should be 0"); - - (uint256 numberOfChoices, bool jumped, bytes memory extraData) = disputeKit.disputes(disputeID); - - assertEq(numberOfChoices, 2, "Wrong numberOfChoices"); - assertEq(jumped, false, "jumped should be false"); - assertEq(extraData, newExtraData, "Wrong extra data"); - assertEq(disputeKit.coreDisputeIDToLocal(0), disputeID, "Wrong local disputeID"); - assertEq(disputeKit.coreDisputeIDToActive(0), true, "Wrong disputes length"); - - ( - uint256 winningChoice, - bool tied, - uint256 totalVoted, - uint256 totalCommited, - uint256 nbVoters, - uint256 choiceCount - ) = disputeKit.getRoundInfo(0, 0, 0); - assertEq(winningChoice, 0, "winningChoice should be 0"); - assertEq(tied, true, "tied should be true"); - assertEq(totalVoted, 0, "totalVoted should be 0"); - assertEq(totalCommited, 0, "totalCommited should be 0"); - assertEq(nbVoters, 0, "nbVoters should be 0"); - assertEq(choiceCount, 0, "choiceCount should be 0"); - } - - function test_createDispute_tokens() public { - feeToken.transfer(disputer, 1 ether); - vm.prank(disputer); - feeToken.approve(address(arbitrable), 1 ether); - - vm.expectRevert(KlerosCoreBase.TokenNotAccepted.selector); - vm.prank(disputer); - arbitrable.createDispute("Action", 0.18 ether); - - vm.prank(owner); - core.changeAcceptedFeeTokens(feeToken, true); - vm.prank(owner); - core.changeCurrencyRates(feeToken, 500, 3); - - vm.expectRevert(KlerosCoreBase.ArbitrationFeesNotEnough.selector); - vm.prank(disputer); - arbitrable.createDispute("Action", 0.18 ether - 1); - - vm.expectRevert(KlerosCoreBase.TransferFailed.selector); - vm.prank(address(arbitrable)); // Bypass createDispute in arbitrable to avoid transfer checks there and make the arbitrable call KC directly - core.createDispute(2, arbitratorExtraData, feeToken, 0.18 ether); - - assertEq(core.arbitrationCost(arbitratorExtraData, feeToken), 0.18 ether, "Wrong token cost"); - vm.prank(disputer); - arbitrable.createDispute("Action", 0.18 ether); - - KlerosCoreBase.Round memory round = core.getRoundInfo(0, 0); - assertEq(round.totalFeesForJurors, 0.18 ether, "Wrong totalFeesForJurors"); - assertEq(round.nbVotes, 3, "Wrong nbVotes"); - assertEq(address(round.feeToken), address(feeToken), "Wrong feeToken"); - - assertEq(feeToken.balanceOf(address(core)), 0.18 ether, "Wrong token balance of the core"); - assertEq(feeToken.balanceOf(disputer), 0.82 ether, "Wrong token balance of the disputer"); - } - - function test_draw() public { - uint256 disputeID = 0; - uint256 roundID = 0; - - vm.prank(staker1); - core.setStake(GENERAL_COURT, 1500); - vm.prank(disputer); - arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); - vm.warp(block.timestamp + minStakingTime); - sortitionModule.passPhase(); // Generating - vm.warp(block.timestamp + rngLookahead); - sortitionModule.passPhase(); // Drawing phase - - vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeLocked(staker1, 1000, false); - vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.Draw(staker1, disputeID, roundID, 0); // VoteID = 0 - - core.draw(disputeID, DEFAULT_NB_OF_JURORS); // Do 3 iterations and see that the juror will get drawn 3 times despite low stake. - - (uint256 totalStaked, uint256 totalLocked, uint256 stakedInCourt, ) = sortitionModule.getJurorBalance( - staker1, - GENERAL_COURT - ); - assertEq(totalStaked, 1500, "Wrong amount total staked"); - assertEq(totalLocked, 3000, "Wrong amount locked"); // 1000 per draw - assertEq(stakedInCourt, 1500, "Wrong amount staked in court"); - assertEq(sortitionModule.disputesWithoutJurors(), 0, "Wrong disputesWithoutJurors count"); - - for (uint256 i = 0; i < DEFAULT_NB_OF_JURORS; i++) { - (address account, bytes32 commit, uint256 choice, bool voted) = disputeKit.getVoteInfo(0, 0, i); - assertEq(account, staker1, "Wrong drawn account"); - assertEq(commit, bytes32(0), "Commit should be empty"); - assertEq(choice, 0, "Choice should be empty"); - assertEq(voted, false, "Voted should be false"); - } - } - - function test_draw_noEmptyAddresses() public { - uint256 disputeID = 0; - uint256 roundID = 0; - - vm.prank(disputer); - arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); - vm.warp(block.timestamp + minStakingTime); - sortitionModule.passPhase(); // Generating - 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. - - KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, roundID); - assertEq(round.drawIterations, 3, "Wrong drawIterations number"); - - (, , , , uint256 nbVoters, ) = disputeKit.getRoundInfo(disputeID, roundID, 0); - assertEq(nbVoters, 0, "nbVoters should be 0"); - } - - function test_draw_parentCourts() public { - uint96 newCourtID = 2; - uint256 disputeID = 0; - uint256 roundID = 0; - - // Create a child court and stake exclusively there to check that parent courts hold drawing power. - vm.prank(owner); - uint256[] memory supportedDK = new uint256[](1); - supportedDK[0] = DISPUTE_KIT_CLASSIC; - core.createCourt( - GENERAL_COURT, - true, // Hidden votes - 1000, // min stake - 10000, // alpha - 0.03 ether, // fee for juror - 50, // jurors for jump - [uint256(10), uint256(20), uint256(30), uint256(40)], // Times per period - sortitionExtraData, // Sortition extra data - supportedDK - ); - - uint256[] memory children = core.getCourtChildren(GENERAL_COURT); - assertEq(children.length, 1, "Wrong children count"); - assertEq(children[0], 2, "Wrong child ID"); - - vm.prank(staker1); - core.setStake(newCourtID, 3000); - vm.prank(disputer); - arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); // Dispute uses general court by default - vm.warp(block.timestamp + minStakingTime); - sortitionModule.passPhase(); // Generating - vm.warp(block.timestamp + rngLookahead); - sortitionModule.passPhase(); // Drawing phase - - (uint96 courtID, , , , ) = core.disputes(disputeID); - assertEq(courtID, GENERAL_COURT, "Wrong court ID of the dispute"); - - vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.Draw(staker1, disputeID, roundID, 0); - vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.Draw(staker1, disputeID, roundID, 1); - vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.Draw(staker1, disputeID, roundID, 2); - core.draw(disputeID, DEFAULT_NB_OF_JURORS); - - assertEq(sortitionModule.disputesWithoutJurors(), 0, "Wrong disputesWithoutJurors count"); - - KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, roundID); - assertEq(round.drawIterations, 3, "Wrong drawIterations number"); - - (, , , , uint256 nbVoters, ) = disputeKit.getRoundInfo(disputeID, roundID, 0); - assertEq(nbVoters, 3, "nbVoters should be 3"); - } - - function test_castCommit() public { - // Change hidden votes in general court - uint256 disputeID = 0; - vm.prank(owner); - core.changeCourtParameters( - GENERAL_COURT, - true, // Hidden votes - 1000, // min stake - 10000, // alpha - 0.03 ether, // fee for juror - 511, // jurors for jump - [uint256(60), uint256(120), uint256(180), uint256(240)] // Times per period - ); - - vm.prank(staker1); - core.setStake(GENERAL_COURT, 10000); - vm.prank(disputer); - arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); - vm.warp(block.timestamp + minStakingTime); - sortitionModule.passPhase(); // Generating - vm.warp(block.timestamp + rngLookahead); - sortitionModule.passPhase(); // Drawing phase - core.draw(disputeID, DEFAULT_NB_OF_JURORS); - - uint256 YES = 1; - uint256 salt = 123455678; - uint256[] memory voteIDs = new uint256[](1); - voteIDs[0] = 0; - bytes32 commit; - vm.prank(staker1); - vm.expectRevert(DisputeKitClassicBase.NotCommitPeriod.selector); - disputeKit.castCommit(disputeID, voteIDs, commit); - - vm.expectRevert(KlerosCoreBase.EvidenceNotPassedAndNotAppeal.selector); - core.passPeriod(disputeID); - vm.warp(block.timestamp + timesPerPeriod[0]); - - vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.commit); - core.passPeriod(disputeID); - - (, , KlerosCoreBase.Period period, , uint256 lastPeriodChange) = core.disputes(disputeID); - - assertEq(uint256(period), uint256(KlerosCoreBase.Period.commit), "Wrong period"); - assertEq(lastPeriodChange, block.timestamp, "Wrong lastPeriodChange"); - - vm.prank(staker1); - vm.expectRevert(DisputeKitClassicBase.EmptyCommit.selector); - disputeKit.castCommit(disputeID, voteIDs, commit); - - commit = keccak256(abi.encodePacked(YES, salt)); - - vm.prank(other); - vm.expectRevert(DisputeKitClassicBase.JurorHasToOwnTheVote.selector); - disputeKit.castCommit(disputeID, voteIDs, commit); - - vm.prank(staker1); - vm.expectEmit(true, true, true, true); - emit DisputeKitClassicBase.CommitCast(disputeID, staker1, voteIDs, commit); - disputeKit.castCommit(disputeID, voteIDs, commit); - - (, , , uint256 totalCommited, uint256 nbVoters, uint256 choiceCount) = disputeKit.getRoundInfo(disputeID, 0, 0); - assertEq(totalCommited, 1, "totalCommited should be 1"); - assertEq(disputeKit.areCommitsAllCast(disputeID), false, "Commits should not all be cast"); - - (, bytes32 commitStored, , ) = disputeKit.getVoteInfo(0, 0, 0); - assertEq(commitStored, keccak256(abi.encodePacked(YES, salt)), "Incorrect commit"); - - voteIDs = new uint256[](2); // Create the leftover votes subset - voteIDs[0] = 1; - voteIDs[1] = 2; - - vm.prank(staker1); - vm.expectEmit(true, true, true, true); - emit DisputeKitClassicBase.CommitCast(disputeID, staker1, voteIDs, commit); - disputeKit.castCommit(disputeID, voteIDs, commit); - - (, , , totalCommited, nbVoters, choiceCount) = disputeKit.getRoundInfo(disputeID, 0, 0); - assertEq(totalCommited, DEFAULT_NB_OF_JURORS, "totalCommited should be 3"); - assertEq(disputeKit.areCommitsAllCast(disputeID), true, "Commits should all be cast"); - - for (uint256 i = 1; i < DEFAULT_NB_OF_JURORS; i++) { - (, commitStored, , ) = disputeKit.getVoteInfo(0, 0, i); - assertEq(commitStored, keccak256(abi.encodePacked(YES, salt)), "Incorrect commit"); - } - - // 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 - vm.prank(staker1); - vm.expectRevert(DisputeKitClassicBase.HashDoesNotMatchHiddenVoteCommitment.selector); - disputeKit.castVote(disputeID, voteIDs, 2, salt, "XYZ"); - - vm.prank(staker1); - vm.expectRevert(DisputeKitClassicBase.HashDoesNotMatchHiddenVoteCommitment.selector); - disputeKit.castVote(disputeID, voteIDs, YES, salt - 1, "XYZ"); - - vm.prank(staker1); - disputeKit.castVote(disputeID, voteIDs, YES, salt, "XYZ"); - - for (uint256 i = 1; i < DEFAULT_NB_OF_JURORS; i++) { - // 0 voteID was skipped when casting a vote - (address account, , uint256 choice, bool voted) = disputeKit.getVoteInfo(0, 0, i); - assertEq(account, staker1, "Wrong drawn account"); - assertEq(choice, YES, "Wrong choice"); - assertEq(voted, true, "Voted should be true"); - } - } - - function test_castCommit_timeoutCheck() public { - // Change hidden votes in general court - uint256 disputeID = 0; - vm.prank(owner); - core.changeCourtParameters( - GENERAL_COURT, - true, // Hidden votes - 1000, // min stake - 10000, // alpha - 0.03 ether, // fee for juror - 511, // jurors for jump - [uint256(60), uint256(120), uint256(180), uint256(240)] // Times per period - ); - - vm.prank(staker1); - core.setStake(GENERAL_COURT, 10000); - vm.prank(disputer); - arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); - vm.warp(block.timestamp + minStakingTime); - sortitionModule.passPhase(); // Generating - vm.warp(block.timestamp + rngLookahead); - sortitionModule.passPhase(); // Drawing phase - core.draw(disputeID, DEFAULT_NB_OF_JURORS); - - vm.warp(block.timestamp + timesPerPeriod[0]); - core.passPeriod(disputeID); // Commit - - vm.expectRevert(KlerosCoreBase.CommitPeriodNotPassed.selector); - core.passPeriod(disputeID); - - vm.warp(block.timestamp + timesPerPeriod[1]); - vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.vote); - core.passPeriod(disputeID); - } - - function test_castVote() public { - uint256 disputeID = 0; - - vm.prank(staker1); - core.setStake(GENERAL_COURT, 10000); - vm.prank(disputer); - arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); - vm.warp(block.timestamp + minStakingTime); - sortitionModule.passPhase(); // Generating - vm.warp(block.timestamp + rngLookahead); - sortitionModule.passPhase(); // Drawing phase - - core.draw(disputeID, DEFAULT_NB_OF_JURORS - 1); // Draw less to check the require later - vm.warp(block.timestamp + timesPerPeriod[0]); - - uint256[] memory voteIDs = new uint256[](0); - vm.prank(staker1); - vm.expectRevert(DisputeKitClassicBase.NotVotePeriod.selector); - disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); // Leave salt empty as not needed - - vm.expectRevert(KlerosCoreBase.DisputeStillDrawing.selector); - core.passPeriod(disputeID); - - core.draw(disputeID, 1); // Draw the last juror - - vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.vote); - core.passPeriod(disputeID); // Vote - - (, , KlerosCoreBase.Period period, , uint256 lastPeriodChange) = core.disputes(disputeID); - - assertEq(uint256(period), uint256(KlerosCoreBase.Period.vote), "Wrong period"); - assertEq(lastPeriodChange, block.timestamp, "Wrong lastPeriodChange"); - - vm.prank(staker1); - 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(DisputeKitClassicBase.ChoiceOutOfBounds.selector); - disputeKit.castVote(disputeID, voteIDs, 2 + 1, 0, "XYZ"); - - vm.prank(other); - vm.expectRevert(DisputeKitClassicBase.JurorHasToOwnTheVote.selector); - disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); - - vm.prank(staker1); - vm.expectEmit(true, true, true, true); - emit IDisputeKit.VoteCast(disputeID, staker1, voteIDs, 2, "XYZ"); - disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); - - vm.prank(staker1); - vm.expectRevert(DisputeKitClassicBase.VoteAlreadyCast.selector); - disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); - - ( - uint256 winningChoice, - bool tied, - uint256 totalVoted, - uint256 totalCommited, - , - uint256 choiceCount - ) = disputeKit.getRoundInfo(disputeID, 0, 2); - assertEq(winningChoice, 2, "Wrong winning choice"); - assertEq(tied, false, "tied should be false"); - assertEq(totalVoted, 1, "totalVoted should be 1"); - assertEq(totalCommited, 0, "totalCommited should be 0"); - assertEq(choiceCount, 1, "choiceCount should be 1"); - - (address account, bytes32 commit, uint256 choice, bool voted) = disputeKit.getVoteInfo(0, 0, 0); // Dispute - Round - VoteID - assertEq(account, staker1, "Wrong drawn account"); - assertEq(commit, bytes32(0), "Commit should be empty"); - assertEq(choice, 2, "Choice should be 2"); - assertEq(voted, true, "Voted should be true"); - - assertEq(disputeKit.isVoteActive(0, 0, 0), true, "Vote should be active"); // Dispute - Round - VoteID - - voteIDs = new uint256[](1); - voteIDs[0] = 1; // Cast another vote to check the tie. - - vm.prank(staker1); - vm.expectEmit(true, true, true, true); - emit IDisputeKit.VoteCast(disputeID, staker1, voteIDs, 1, "XYZZ"); - disputeKit.castVote(disputeID, voteIDs, 1, 0, "XYZZ"); - - (, tied, totalVoted, , , choiceCount) = disputeKit.getRoundInfo(disputeID, 0, 1); - assertEq(tied, true, "tied should be true"); - assertEq(totalVoted, 2, "totalVoted should be 2"); - assertEq(choiceCount, 1, "choiceCount should be 1 for first choice"); - - vm.expectRevert(KlerosCoreBase.VotePeriodNotPassed.selector); - core.passPeriod(disputeID); - - voteIDs = new uint256[](1); - voteIDs[0] = 2; // Cast another vote to declare a new winner. - - vm.prank(staker1); - vm.expectEmit(true, true, true, true); - emit IDisputeKit.VoteCast(disputeID, staker1, voteIDs, 1, "XYZZ"); - disputeKit.castVote(disputeID, voteIDs, 1, 0, "XYZZ"); - - (winningChoice, tied, totalVoted, , , choiceCount) = disputeKit.getRoundInfo(disputeID, 0, 1); - assertEq(winningChoice, 1, "Wrong winning choice"); - assertEq(tied, false, "tied should be false"); - assertEq(totalVoted, 3, "totalVoted should be 3"); - assertEq(choiceCount, 2, "choiceCount should be 2 for first choice"); - assertEq(disputeKit.areVotesAllCast(disputeID), true, "Votes should all be cast"); - } - - function test_castVote_timeoutCheck() public { - // Change hidden votes in general court - uint256 disputeID = 0; - vm.prank(staker1); - core.setStake(GENERAL_COURT, 10000); - vm.prank(disputer); - arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); - vm.warp(block.timestamp + minStakingTime); - sortitionModule.passPhase(); // Generating - vm.warp(block.timestamp + rngLookahead); - sortitionModule.passPhase(); // Drawing phase - core.draw(disputeID, DEFAULT_NB_OF_JURORS); - - vm.warp(block.timestamp + timesPerPeriod[0]); - core.passPeriod(disputeID); // Votes - - vm.expectRevert(KlerosCoreBase.VotePeriodNotPassed.selector); - core.passPeriod(disputeID); - - vm.warp(block.timestamp + timesPerPeriod[2]); - vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.AppealPossible(disputeID, arbitrable); - vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.appeal); - core.passPeriod(disputeID); - } - - function test_castVote_rulingCheck() public { - // Change hidden votes in general court - uint256 disputeID = 0; - vm.prank(staker1); - core.setStake(GENERAL_COURT, 10000); - vm.prank(disputer); - arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); - vm.warp(block.timestamp + minStakingTime); - sortitionModule.passPhase(); // Generating - vm.warp(block.timestamp + rngLookahead); - sortitionModule.passPhase(); // Drawing phase - core.draw(disputeID, DEFAULT_NB_OF_JURORS); - - vm.warp(block.timestamp + timesPerPeriod[0]); - core.passPeriod(disputeID); // Votes - - uint256[] memory voteIDs = new uint256[](3); - voteIDs[0] = 0; - voteIDs[1] = 1; - voteIDs[2] = 2; - - vm.prank(staker1); - disputeKit.castVote(disputeID, voteIDs, 1, 0, "XYZZ"); - - (uint256 ruling, bool tied, bool overridden) = disputeKit.currentRuling(disputeID); - assertEq(ruling, 1, "Wrong ruling"); - assertEq(tied, false, "Not tied"); - assertEq(overridden, false, "Not overridden"); - } - - function test_castVote_quickPassPeriod() public { - // Change hidden votes in general court - uint256 disputeID = 0; - vm.prank(owner); - core.changeCourtParameters( - GENERAL_COURT, - true, // Hidden votes - 1000, // min stake - 10000, // alpha - 0.03 ether, // fee for juror - 511, // jurors for jump - [uint256(60), uint256(120), uint256(180), uint256(240)] // Times per period - ); - - vm.prank(staker1); - core.setStake(GENERAL_COURT, 10000); - vm.prank(disputer); - arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); - vm.warp(block.timestamp + minStakingTime); - sortitionModule.passPhase(); // Generating - vm.warp(block.timestamp + rngLookahead); - sortitionModule.passPhase(); // Drawing phase - core.draw(disputeID, DEFAULT_NB_OF_JURORS); - - uint256 YES = 1; - uint256 salt = 123455678; - uint256[] memory voteIDs = new uint256[](1); - voteIDs[0] = 0; - bytes32 commit; - - vm.warp(block.timestamp + timesPerPeriod[0]); - core.passPeriod(disputeID); - - commit = keccak256(abi.encodePacked(YES, salt)); - - vm.prank(staker1); - disputeKit.castCommit(disputeID, voteIDs, commit); - - (, , , uint256 totalCommited, uint256 nbVoters, uint256 choiceCount) = disputeKit.getRoundInfo(disputeID, 0, 0); - assertEq(totalCommited, 1, "totalCommited should be 1"); - assertEq(disputeKit.areCommitsAllCast(disputeID), false, "Commits should not all be cast"); - - vm.warp(block.timestamp + timesPerPeriod[1]); - core.passPeriod(disputeID); - - vm.prank(staker1); - disputeKit.castVote(disputeID, voteIDs, YES, salt, "XYZ"); - - (, , uint256 totalVoted, , , ) = disputeKit.getRoundInfo(disputeID, 0, 0); - assertEq(totalVoted, 1, "totalVoted should be 1"); - assertEq(disputeKit.areVotesAllCast(disputeID), true, "Every committed vote was cast"); - - // Should pass period by counting only committed votes. - vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.appeal); - core.passPeriod(disputeID); - } - - function test_appeal_fundOneSide() public { - uint256 disputeID = 0; - vm.deal(address(disputeKit), 1 ether); - vm.deal(staker1, 1 ether); - - vm.prank(staker1); - core.setStake(GENERAL_COURT, 10000); - vm.prank(disputer); - arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); - vm.warp(block.timestamp + minStakingTime); - sortitionModule.passPhase(); // Generating - vm.warp(block.timestamp + rngLookahead); - sortitionModule.passPhase(); // Drawing phase - - core.draw(disputeID, DEFAULT_NB_OF_JURORS); - vm.warp(block.timestamp + timesPerPeriod[0]); - core.passPeriod(disputeID); // Vote - - uint256[] memory voteIDs = new uint256[](3); - voteIDs[0] = 0; - voteIDs[1] = 1; - voteIDs[2] = 2; - - vm.prank(staker1); - disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); - - (uint256 start, uint256 end) = core.appealPeriod(0); - assertEq(start, 0, "Appeal period start should be 0"); - assertEq(end, 0, "Appeal period end should be 0"); - - // Simulate the call from dispute kit to check the requires unrelated to caller - vm.prank(address(disputeKit)); - vm.expectRevert(KlerosCoreBase.DisputeNotAppealable.selector); - core.appeal{value: 0.21 ether}(disputeID, 2, arbitratorExtraData); - - vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.AppealPossible(disputeID, arbitrable); - vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.appeal); - core.passPeriod(disputeID); - - (, , KlerosCoreBase.Period period, , uint256 lastPeriodChange) = core.disputes(disputeID); - (start, end) = core.appealPeriod(0); - assertEq(uint256(period), uint256(KlerosCoreBase.Period.appeal), "Wrong period"); - assertEq(lastPeriodChange, block.timestamp, "Wrong lastPeriodChange"); - assertEq(core.appealCost(0), 0.21 ether, "Wrong appealCost"); - assertEq(start, lastPeriodChange, "Appeal period start is incorrect"); - assertEq(end, lastPeriodChange + timesPerPeriod[3], "Appeal period end is incorrect"); - - vm.expectRevert(KlerosCoreBase.AppealPeriodNotPassed.selector); - core.passPeriod(disputeID); - - // Simulate the call from dispute kit to check the requires unrelated to caller - vm.prank(address(disputeKit)); - vm.expectRevert(KlerosCoreBase.AppealFeesNotEnough.selector); - core.appeal{value: 0.21 ether - 1}(disputeID, 2, arbitratorExtraData); - vm.deal(address(disputeKit), 0); // Nullify the balance so it doesn't get in the way. - - vm.prank(staker1); - vm.expectRevert(KlerosCoreBase.DisputeKitOnly.selector); - core.appeal{value: 0.21 ether}(disputeID, 2, arbitratorExtraData); - - vm.prank(crowdfunder1); - vm.expectRevert(DisputeKitClassicBase.ChoiceOutOfBounds.selector); - disputeKit.fundAppeal(disputeID, 3); - - vm.prank(crowdfunder1); - vm.expectEmit(true, true, true, true); - emit DisputeKitClassicBase.Contribution(disputeID, 0, 1, crowdfunder1, 0.21 ether); - disputeKit.fundAppeal{value: 0.21 ether}(disputeID, 1); // Fund the losing choice. Total cost will be 0.63 (0.21 + 0.21 * (20000/10000)) - - assertEq(crowdfunder1.balance, 9.79 ether, "Wrong balance of the crowdfunder"); - assertEq(address(disputeKit).balance, 0.21 ether, "Wrong balance of the DK"); - assertEq((disputeKit.getFundedChoices(disputeID)).length, 0, "No funded choices"); - - vm.prank(crowdfunder1); - vm.expectEmit(true, true, true, true); - emit DisputeKitClassicBase.Contribution(disputeID, 0, 1, crowdfunder1, 0.42 ether); - vm.expectEmit(true, true, true, true); - emit DisputeKitClassicBase.ChoiceFunded(disputeID, 0, 1); - disputeKit.fundAppeal{value: 5 ether}(disputeID, 1); // Deliberately overpay to check reimburse - - assertEq(crowdfunder1.balance, 9.37 ether, "Wrong balance of the crowdfunder"); - assertEq(address(disputeKit).balance, 0.63 ether, "Wrong balance of the DK"); - assertEq((disputeKit.getFundedChoices(disputeID)).length, 1, "One choice should be funded"); - assertEq((disputeKit.getFundedChoices(disputeID))[0], 1, "Incorrect funded choice"); - - vm.prank(crowdfunder1); - vm.expectRevert(DisputeKitClassicBase.AppealFeeIsAlreadyPaid.selector); - disputeKit.fundAppeal(disputeID, 1); - } - - function test_appeal_timeoutCheck() public { - uint256 disputeID = 0; - - vm.prank(staker1); - core.setStake(GENERAL_COURT, 10000); - vm.prank(disputer); - arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); - vm.warp(block.timestamp + minStakingTime); - sortitionModule.passPhase(); // Generating - vm.warp(block.timestamp + rngLookahead); - sortitionModule.passPhase(); // Drawing phase - - core.draw(disputeID, DEFAULT_NB_OF_JURORS); - vm.warp(block.timestamp + timesPerPeriod[0]); - core.passPeriod(disputeID); // Vote - - uint256[] memory voteIDs = new uint256[](3); - voteIDs[0] = 0; - voteIDs[1] = 1; - voteIDs[2] = 2; - - vm.prank(staker1); - disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); - - vm.prank(crowdfunder1); - vm.expectRevert(DisputeKitClassicBase.AppealPeriodIsOver.selector); - disputeKit.fundAppeal{value: 0.1 ether}(disputeID, 1); - core.passPeriod(disputeID); - - (uint256 start, uint256 end) = core.appealPeriod(0); - - vm.prank(crowdfunder1); - vm.warp(block.timestamp + ((end - start) / 2 + 1)); - 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(DisputeKitClassicBase.AppealPeriodIsOver.selector); - disputeKit.fundAppeal{value: 0.1 ether}(disputeID, 2); - } - - function test_appeal_fullFundingNoSwitch() public { - uint256 disputeID = 0; - - 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 - vm.warp(block.timestamp + rngLookahead); - sortitionModule.passPhase(); // Drawing phase - - core.draw(disputeID, DEFAULT_NB_OF_JURORS); - vm.warp(block.timestamp + timesPerPeriod[0]); - core.passPeriod(disputeID); // Vote - - uint256[] memory voteIDs = new uint256[](3); - voteIDs[0] = 0; - voteIDs[1] = 1; - voteIDs[2] = 2; - - vm.prank(staker1); - disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); - - core.passPeriod(disputeID); // Appeal - - vm.prank(crowdfunder1); - disputeKit.fundAppeal{value: 0.63 ether}(disputeID, 1); - - vm.prank(crowdfunder2); - vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.AppealDecision(disputeID, arbitrable); - vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.evidence); - disputeKit.fundAppeal{value: 0.42 ether}(disputeID, 2); - - assertEq((disputeKit.getFundedChoices(disputeID)).length, 0, "No funded choices in the fresh round"); - (uint256 ruling, bool tied, bool overridden) = disputeKit.currentRuling(disputeID); - assertEq(ruling, 0, "Should be 0 ruling in the fresh round"); - assertEq(tied, true, "Should be tied"); - assertEq(overridden, false, "Not overridden"); - - assertEq(address(disputeKit).balance, 0.84 ether, "Wrong balance of the DK"); // 0.63 + 0.42 - 0.21 - assertEq(address(core).balance, 0.3 ether, "Wrong balance of the core"); // 0.09 arbFee + 0.21 appealFee - - assertEq(sortitionModule.disputesWithoutJurors(), 1, "Wrong disputesWithoutJurors count after appeal"); - assertEq(core.getNumberOfRounds(disputeID), 2, "Wrong number of rounds"); - - (, , KlerosCoreBase.Period period, , uint256 lastPeriodChange) = core.disputes(disputeID); - assertEq(uint256(period), uint256(KlerosCoreBase.Period.evidence), "Wrong period"); - assertEq(lastPeriodChange, block.timestamp, "Wrong lastPeriodChange"); - - KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 1); // Check the new round - assertEq(round.pnkAtStakePerJuror, 1000, "Wrong pnkAtStakePerJuror"); - assertEq(round.totalFeesForJurors, 0.21 ether, "Wrong totalFeesForJurors"); - assertEq(round.nbVotes, 7, "Wrong nbVotes"); - - core.draw(disputeID, 7); - emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.vote); // Check that we don't have to wait for the timeout to pass the evidence period after appeal - core.passPeriod(disputeID); - } - - function test_appeal_fullFundingDKCourtSwitch() public { - uint256 disputeID = 0; - DisputeKitClassic dkLogic = new DisputeKitClassic(); - // Create a new DK and court to check the switch - bytes memory initDataDk = abi.encodeWithSignature( - "initialize(address,address,address)", - owner, - address(core), - address(wNative) - ); - - UUPSProxy proxyDk = new UUPSProxy(address(dkLogic), initDataDk); - DisputeKitClassic newDisputeKit = DisputeKitClassic(address(proxyDk)); - - uint96 newCourtID = 2; - uint256 newDkID = 2; - uint256[] memory supportedDK = new uint256[](1); - supportedDK[0] = DISPUTE_KIT_CLASSIC; - bytes memory newExtraData = abi.encodePacked(uint256(newCourtID), DEFAULT_NB_OF_JURORS, newDkID); - - vm.prank(owner); - core.addNewDisputeKit(newDisputeKit); - vm.prank(owner); - core.createCourt( - GENERAL_COURT, - hiddenVotes, - minStake, - alpha, - feeForJuror, - 3, // jurors for jump. Low number to ensure jump after the first appeal - [uint256(60), uint256(120), uint256(180), uint256(240)], // Times per period - sortitionExtraData, - supportedDK - ); - - arbitrable.changeArbitratorExtraData(newExtraData); - - vm.prank(owner); - supportedDK = new uint256[](1); - supportedDK[0] = newDkID; - core.enableDisputeKits(newCourtID, supportedDK, true); - - vm.prank(staker1); - core.setStake(newCourtID, 20000); - vm.prank(disputer); - arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); - vm.warp(block.timestamp + minStakingTime); - sortitionModule.passPhase(); // Generating - vm.warp(block.timestamp + rngLookahead); - sortitionModule.passPhase(); // Drawing phase - - KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 0); - assertEq(round.disputeKitID, newDkID, "Wrong DK ID"); - - core.draw(disputeID, DEFAULT_NB_OF_JURORS); - vm.warp(block.timestamp + timesPerPeriod[0]); - core.passPeriod(disputeID); // Vote - - uint256[] memory voteIDs = new uint256[](3); - voteIDs[0] = 0; - voteIDs[1] = 1; - voteIDs[2] = 2; - - vm.prank(staker1); - newDisputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); - - core.passPeriod(disputeID); // Appeal - - vm.prank(crowdfunder1); - newDisputeKit.fundAppeal{value: 0.63 ether}(disputeID, 1); - vm.prank(crowdfunder2); - - assertEq(core.isDisputeKitJumping(disputeID), true, "Should be jumping"); - - vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.CourtJump(disputeID, 1, newCourtID, GENERAL_COURT); - vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.DisputeKitJump(disputeID, 1, newDkID, DISPUTE_KIT_CLASSIC); - vm.expectEmit(true, true, true, true); - emit DisputeKitClassicBase.DisputeCreation(disputeID, 2, newExtraData); - vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.AppealDecision(disputeID, arbitrable); - vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.evidence); - newDisputeKit.fundAppeal{value: 0.42 ether}(disputeID, 2); - - (, bool jumped, ) = newDisputeKit.disputes(disputeID); - assertEq(jumped, true, "jumped should be true"); - assertEq( - (newDisputeKit.getFundedChoices(disputeID)).length, - 2, - "No fresh round created so the number of funded choices should be 2" - ); - - round = core.getRoundInfo(disputeID, 1); - assertEq(round.disputeKitID, DISPUTE_KIT_CLASSIC, "Wrong DK ID"); - assertEq(sortitionModule.disputesWithoutJurors(), 1, "Wrong disputesWithoutJurors count"); - (uint96 courtID, , , , ) = core.disputes(disputeID); - assertEq(courtID, GENERAL_COURT, "Wrong court ID"); - - (, jumped, ) = disputeKit.disputes(disputeID); - assertEq(jumped, false, "jumped should be false in the DK that dispute jumped to"); - - // Check jump modifier - vm.prank(address(core)); - vm.expectRevert(DisputeKitClassicBase.DisputeJumpedToParentDK.selector); - newDisputeKit.draw(disputeID, 1); - - // And check that draw in the new round works - vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.Draw(staker1, disputeID, 1, 0); // roundID = 1 VoteID = 0 - core.draw(disputeID, 1); - - (address account, , , ) = disputeKit.getVoteInfo(disputeID, 1, 0); - assertEq(account, staker1, "Wrong drawn account in the classic DK"); - } - - function test_appeal_quickPassPeriod() public { - uint256 disputeID = 0; - - vm.prank(staker1); - core.setStake(GENERAL_COURT, 10000); - vm.prank(disputer); - arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); - vm.warp(block.timestamp + minStakingTime); - sortitionModule.passPhase(); // Generating - vm.warp(block.timestamp + rngLookahead); - sortitionModule.passPhase(); // Drawing phase - - core.draw(disputeID, DEFAULT_NB_OF_JURORS); - vm.warp(block.timestamp + timesPerPeriod[0]); - core.passPeriod(disputeID); // Vote - - uint256[] memory voteIDs = new uint256[](3); - voteIDs[0] = 0; - voteIDs[1] = 1; - voteIDs[2] = 2; - - vm.prank(staker1); - disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); - - core.passPeriod(disputeID); // Appeal - - vm.warp(block.timestamp + timesPerPeriod[3] / 2); - - // Should pass to execution period without waiting for the 2nd half of the appeal. - vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.execution); - core.passPeriod(disputeID); - } - - function test_execute() public { - uint256 disputeID = 0; - - vm.prank(staker1); - core.setStake(GENERAL_COURT, 1500); - vm.prank(disputer); - arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); - vm.warp(block.timestamp + minStakingTime); - sortitionModule.passPhase(); // Generating - 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. - core.draw(disputeID, 1); - - vm.warp(block.timestamp + maxDrawingTime); - sortitionModule.passPhase(); // Staking phase to stake the 2nd voter - vm.prank(staker2); - core.setStake(GENERAL_COURT, 20000); - vm.warp(block.timestamp + minStakingTime); - sortitionModule.passPhase(); // Generating - vm.warp(block.timestamp + rngLookahead); - sortitionModule.passPhase(); // Drawing phase - - core.draw(disputeID, 2); // Assign leftover votes to staker2 - - vm.warp(block.timestamp + timesPerPeriod[0]); - core.passPeriod(disputeID); // Vote - - uint256[] memory voteIDs = new uint256[](1); - voteIDs[0] = 0; - vm.prank(staker1); - disputeKit.castVote(disputeID, voteIDs, 1, 0, "XYZ"); // Staker1 only got 1 vote because of low stake - - voteIDs = new uint256[](2); - voteIDs[0] = 1; - voteIDs[1] = 2; - vm.prank(staker2); - disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); - core.passPeriod(disputeID); // Appeal - - vm.expectRevert(KlerosCoreBase.NotExecutionPeriod.selector); - core.execute(disputeID, 0, 1); - - vm.warp(block.timestamp + timesPerPeriod[3]); - core.passPeriod(disputeID); // Execution - - vm.prank(owner); - core.pause(); - vm.expectRevert(KlerosCoreBase.WhenNotPausedOnly.selector); - core.execute(disputeID, 0, 1); - vm.prank(owner); - 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) - (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); - vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.TokenAndETHShift(staker1, disputeID, 0, 0, -int256(1000), 0, IERC20(address(0))); - // Check iterations for the winning staker to see the shifts - vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeLocked(staker2, 0, true); - vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.TokenAndETHShift(staker2, disputeID, 0, 10000, 0, 0, IERC20(address(0))); - vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeLocked(staker2, 0, true); - vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.TokenAndETHShift(staker2, disputeID, 0, 10000, 0, 0, IERC20(address(0))); - core.execute(disputeID, 0, 3); // Do 3 iterations to check penalties first - - (uint256 totalStaked, uint256 totalLocked, , ) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); - assertEq(totalStaked, 500, "totalStaked should be penalized"); // 1500 - 1000 - assertEq(totalLocked, 0, "Tokens should be released for staker1"); - (, totalLocked, , ) = sortitionModule.getJurorBalance(staker2, GENERAL_COURT); - assertEq(totalLocked, 2000, "Tokens should still be locked for staker2"); - - KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 0); - assertEq(round.repartitions, 3, "Wrong repartitions"); - assertEq(round.pnkPenalties, 1000, "Wrong pnkPenalties"); - - vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeLocked(staker1, 0, true); - vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.TokenAndETHShift(staker1, disputeID, 0, 0, 0, 0, IERC20(address(0))); - // Check iterations for the winning staker to see the shifts - vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeLocked(staker2, 1000, true); - vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.TokenAndETHShift(staker2, disputeID, 0, 10000, 500, 0.045 ether, IERC20(address(0))); - vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeLocked(staker2, 1000, true); - vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.TokenAndETHShift(staker2, disputeID, 0, 10000, 500, 0.045 ether, IERC20(address(0))); - core.execute(disputeID, 0, 10); // Finish the iterations. We need only 3 but check that it corrects the count. - - (, totalLocked, , ) = sortitionModule.getJurorBalance(staker2, GENERAL_COURT); - assertEq(totalLocked, 0, "Tokens should be unlocked for staker2"); - - round = core.getRoundInfo(disputeID, 0); - assertEq(round.repartitions, 6, "Wrong repartitions"); - assertEq(round.pnkPenalties, 1000, "Wrong pnkPenalties"); - assertEq(round.sumFeeRewardPaid, 0.09 ether, "Wrong sumFeeRewardPaid"); - assertEq(round.sumPnkRewardPaid, 1000, "Wrong sumPnkRewardPaid"); - - assertEq(address(core).balance, 0, "Wrong balance of the core"); - assertEq(staker1.balance, 0, "Wrong balance of the staker1"); - assertEq(staker2.balance, 0.09 ether, "Wrong balance of the staker2"); - - assertEq(pinakion.balanceOf(address(core)), 21500, "Wrong token balance of the core"); // Was 21500. 1000 was transferred to staker2 - assertEq(pinakion.balanceOf(staker1), 999999999999998500, "Wrong token balance of staker1"); - assertEq(pinakion.balanceOf(staker2), 999999999999980000, "Wrong token balance of staker2"); // 20k stake and 1k added as a reward, thus -19k from the default - } - - function test_execute_NoCoherence() public { - uint256 disputeID = 0; - - 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 - vm.warp(block.timestamp + rngLookahead); - sortitionModule.passPhase(); // Drawing phase - - core.draw(disputeID, DEFAULT_NB_OF_JURORS); - - vm.warp(block.timestamp + timesPerPeriod[0]); - core.passPeriod(disputeID); // Vote - - vm.warp(block.timestamp + timesPerPeriod[2]); // Don't vote at all so no one is coherent - core.passPeriod(disputeID); // Appeal - - vm.warp(block.timestamp + timesPerPeriod[3]); - 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) - (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 ownerBalance = owner.balance; - uint256 ownerTokenBalance = pinakion.balanceOf(owner); - - vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.LeftoverRewardSent(disputeID, 0, 3000, 0.09 ether, IERC20(address(0))); - core.execute(disputeID, 0, 3); - - KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 0); - assertEq(round.pnkPenalties, 3000, "Wrong pnkPenalties"); - assertEq(round.sumFeeRewardPaid, 0, "Wrong sumFeeRewardPaid"); - assertEq(round.sumPnkRewardPaid, 0, "Wrong sumPnkRewardPaid"); - - assertEq(address(core).balance, 0, "Wrong balance of the core"); - assertEq(staker1.balance, 0, "Wrong balance of the staker1"); - assertEq(owner.balance, ownerBalance + 0.09 ether, "Wrong balance of the owner"); - - assertEq(pinakion.balanceOf(address(core)), 17000, "Wrong token balance of the core"); - assertEq(pinakion.balanceOf(staker1), 999999999999980000, "Wrong token balance of staker1"); - assertEq(pinakion.balanceOf(owner), ownerTokenBalance + 3000, "Wrong token balance of owner"); - } - - function test_execute_UnstakeInactive() public { - // Create a 2nd court so unstaking is done in multiple courts. - vm.prank(owner); - uint256[] memory supportedDK = new uint256[](1); - supportedDK[0] = DISPUTE_KIT_CLASSIC; - core.createCourt( - GENERAL_COURT, - true, // Hidden votes - 1000, // min stake - 10000, // alpha - 0.03 ether, // fee for juror - 50, // jurors for jump - [uint256(10), uint256(20), uint256(30), uint256(40)], // Times per period - sortitionExtraData, // Sortition extra data - supportedDK - ); - - uint256 disputeID = 0; - uint96 newCourtID = 2; - - vm.prank(staker1); - core.setStake(GENERAL_COURT, 20000); - vm.prank(staker1); - core.setStake(newCourtID, 20000); - (, , , uint256 nbCourts) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); - assertEq(nbCourts, 2, "Wrong number of courts"); - - assertEq(pinakion.balanceOf(address(core)), 40000, "Wrong token balance of the core"); - assertEq(pinakion.balanceOf(staker1), 999999999999960000, "Wrong token balance of staker1"); - - vm.prank(disputer); - arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); - vm.warp(block.timestamp + minStakingTime); - sortitionModule.passPhase(); // Generating - vm.warp(block.timestamp + rngLookahead); - sortitionModule.passPhase(); // Drawing phase - - core.draw(disputeID, DEFAULT_NB_OF_JURORS); - - sortitionModule.passPhase(); // Staking phase. Change to staking so we don't have to deal with delayed stakes. - - vm.warp(block.timestamp + timesPerPeriod[0]); - core.passPeriod(disputeID); // Vote - - vm.warp(block.timestamp + timesPerPeriod[2]); // Don't vote at all so no one is coherent - core.passPeriod(disputeID); // Appeal - - vm.warp(block.timestamp + timesPerPeriod[3]); - core.passPeriod(disputeID); // Execution - - uint256 ownerTokenBalance = pinakion.balanceOf(owner); - - // Note that these events are emitted only after the first iteration of execute() therefore the juror has been penalized only for 1000 PNK her. - vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeSet(staker1, newCourtID, 0, 19000); // Starting with 40000 we first nullify the stake and remove 20000 and then remove penalty once since there was only first iteration (40000 - 20000 - 1000) - vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeSet(staker1, GENERAL_COURT, 0, 2000); // 2000 PNK should remain in balance to cover penalties since the first 1000 of locked pnk was already unlocked - core.execute(disputeID, 0, 3); - - assertEq(pinakion.balanceOf(address(core)), 0, "Wrong token balance of the core"); - assertEq(pinakion.balanceOf(staker1), 999999999999997000, "Wrong token balance of staker1"); // 3000 locked PNK was withheld by the contract and given to owner. - assertEq(pinakion.balanceOf(owner), ownerTokenBalance + 3000, "Wrong token balance of owner"); - - (, , , nbCourts) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); - assertEq(nbCourts, 0, "Should unstake from all courts"); - } - - function test_execute_UnstakeInsolvent() public { - uint256 disputeID = 0; - - vm.prank(staker1); - core.setStake(GENERAL_COURT, 1000); - - assertEq(pinakion.balanceOf(address(core)), 1000, "Wrong token balance of the core"); - assertEq(pinakion.balanceOf(staker1), 999999999999999000, "Wrong token balance of staker1"); - - vm.prank(disputer); - arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); - vm.warp(block.timestamp + minStakingTime); - sortitionModule.passPhase(); // Generating - vm.warp(block.timestamp + rngLookahead); - sortitionModule.passPhase(); // Drawing phase - - core.draw(disputeID, DEFAULT_NB_OF_JURORS); - - (uint256 totalStaked, uint256 totalLocked, , uint256 nbCourts) = sortitionModule.getJurorBalance( - staker1, - GENERAL_COURT - ); - assertEq(totalStaked, 1000, "Wrong totalStaked"); - assertEq(totalLocked, 3000, "totalLocked should exceed totalStaked"); // Juror only staked 1000 but was drawn 3x of minStake (3000 locked) - assertEq(nbCourts, 1, "Wrong number of courts"); - - sortitionModule.passPhase(); // Staking phase. Change to staking so we don't have to deal with delayed stakes. - - vm.warp(block.timestamp + timesPerPeriod[0]); - core.passPeriod(disputeID); // Vote - - uint256[] memory voteIDs = new uint256[](1); - voteIDs[0] = 0; - vm.prank(staker1); - disputeKit.castVote(disputeID, voteIDs, 1, 0, "XYZ"); // 1 incoherent vote should make the juror insolvent - - voteIDs = new uint256[](2); - voteIDs[0] = 1; - voteIDs[1] = 2; - vm.prank(staker1); - disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); - - core.passPeriod(disputeID); // Appeal - - vm.warp(block.timestamp + timesPerPeriod[3]); - core.passPeriod(disputeID); // Execution - - vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeSet(staker1, GENERAL_COURT, 0, 0); // Juror should have no stake left and should be unstaked from the court automatically. - core.execute(disputeID, 0, 6); - - assertEq(pinakion.balanceOf(address(core)), 0, "Wrong token balance of the core"); - assertEq(pinakion.balanceOf(staker1), 1 ether, "Wrong token balance of staker1"); // The juror should have his penalty back as a reward - - (, , , nbCourts) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); - assertEq(nbCourts, 0, "Should unstake from all courts"); - } - - function test_execute_withdrawLeftoverPNK() public { - // Return the previously locked tokens - uint256 disputeID = 0; - - vm.prank(staker1); - core.setStake(GENERAL_COURT, 1000); - vm.prank(disputer); - arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); - vm.warp(block.timestamp + minStakingTime); - sortitionModule.passPhase(); // Generating - vm.warp(block.timestamp + rngLookahead); - sortitionModule.passPhase(); // Drawing phase - - core.draw(disputeID, DEFAULT_NB_OF_JURORS); - - sortitionModule.passPhase(); // Staking. Pass the phase so the juror can unstake before execution - - vm.warp(block.timestamp + timesPerPeriod[0]); - core.passPeriod(disputeID); // Vote - - uint256[] memory voteIDs = new uint256[](3); - voteIDs[0] = 0; - voteIDs[1] = 1; - voteIDs[2] = 2; - vm.prank(staker1); - disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); - - core.passPeriod(disputeID); // Appeal - - vm.warp(block.timestamp + timesPerPeriod[3]); - core.passPeriod(disputeID); // Execution - - vm.prank(staker1); - core.setStake(GENERAL_COURT, 0); // Set stake to 0 to check if it will be withdrawn later. - - (uint256 totalStaked, uint256 totalLocked, uint256 stakedInCourt, uint256 nbCourts) = sortitionModule - .getJurorBalance(staker1, GENERAL_COURT); - assertEq(totalStaked, 1000, "Wrong amount staked"); - assertEq(totalLocked, 3000, "Wrong amount locked"); - assertEq(stakedInCourt, 0, "Should be unstaked"); - assertEq(nbCourts, 0, "Should be 0 courts"); - - assertEq(pinakion.balanceOf(address(core)), 1000, "Wrong token balance of the core"); - assertEq(pinakion.balanceOf(staker1), 999999999999999000, "Wrong token balance of staker1"); - - vm.expectRevert(SortitionModuleBase.NotEligibleForWithdrawal.selector); - sortitionModule.withdrawLeftoverPNK(staker1); - - vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.LeftoverPNK(staker1, 1000); - core.execute(disputeID, 0, 6); - - (totalStaked, totalLocked, , ) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); - assertEq(totalStaked, 1000, "Wrong amount staked"); - assertEq(totalLocked, 0, "Should be fully unlocked"); - - KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 0); - assertEq(round.pnkPenalties, 0, "Wrong pnkPenalties"); - assertEq(round.sumFeeRewardPaid, 0.09 ether, "Wrong sumFeeRewardPaid"); - assertEq(round.sumPnkRewardPaid, 0, "Wrong sumPnkRewardPaid"); // No penalty so no rewards in pnk - - // Execute() shouldn't withdraw the tokens. - assertEq(pinakion.balanceOf(address(core)), 1000, "Wrong token balance of the core"); - assertEq(pinakion.balanceOf(staker1), 999999999999999000, "Wrong token balance of staker1"); - - vm.expectRevert(KlerosCoreBase.SortitionModuleOnly.selector); - vm.prank(owner); - core.transferBySortitionModule(staker1, 1000); - - vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.LeftoverPNKWithdrawn(staker1, 1000); - sortitionModule.withdrawLeftoverPNK(staker1); - - (totalStaked, , , ) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); - assertEq(totalStaked, 0, "Should be unstaked fully"); - - // Check that everything is withdrawn now - assertEq(pinakion.balanceOf(address(core)), 0, "Core balance should be empty"); - assertEq(pinakion.balanceOf(staker1), 1 ether, "All PNK should be withdrawn"); - } - - function test_execute_feeToken() public { - uint256 disputeID = 0; - - feeToken.transfer(disputer, 1 ether); - vm.prank(disputer); - feeToken.approve(address(arbitrable), 1 ether); - - vm.prank(owner); - core.changeAcceptedFeeTokens(feeToken, true); - vm.prank(owner); - core.changeCurrencyRates(feeToken, 500, 3); - - vm.prank(disputer); - arbitrable.createDispute("Action", 0.18 ether); - - vm.prank(staker1); - core.setStake(GENERAL_COURT, 20000); - vm.warp(block.timestamp + minStakingTime); - sortitionModule.passPhase(); // Generating - vm.warp(block.timestamp + rngLookahead); - sortitionModule.passPhase(); // Drawing phase - - core.draw(disputeID, DEFAULT_NB_OF_JURORS); - - vm.warp(block.timestamp + timesPerPeriod[0]); - core.passPeriod(disputeID); // Vote - - uint256[] memory voteIDs = new uint256[](3); - voteIDs[0] = 0; - voteIDs[1] = 1; - voteIDs[2] = 2; - vm.prank(staker1); - disputeKit.castVote(disputeID, voteIDs, 1, 0, "XYZ"); // Staker1 only got 1 vote because of low stake - - core.passPeriod(disputeID); // Appeal - - vm.warp(block.timestamp + timesPerPeriod[3]); - core.passPeriod(disputeID); // Execution - - // Check only once per penalty and per reward - vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.TokenAndETHShift(staker1, disputeID, 0, 10000, 0, 0, feeToken); - vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.TokenAndETHShift(staker1, disputeID, 0, 10000, 0, 0.06 ether, feeToken); - core.execute(disputeID, 0, 6); - - KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 0); - assertEq(round.sumFeeRewardPaid, 0.18 ether, "Wrong sumFeeRewardPaid"); - - assertEq(feeToken.balanceOf(address(core)), 0, "Wrong fee token balance of the core"); - assertEq(feeToken.balanceOf(staker1), 0.18 ether, "Wrong fee token balance of staker1"); - assertEq(feeToken.balanceOf(disputer), 0.82 ether, "Wrong fee token balance of disputer"); - } - - function test_execute_NoCoherence_feeToken() public { - uint256 disputeID = 0; - - feeToken.transfer(disputer, 1 ether); - vm.prank(disputer); - feeToken.approve(address(arbitrable), 1 ether); - - vm.prank(owner); - core.changeAcceptedFeeTokens(feeToken, true); - vm.prank(owner); - core.changeCurrencyRates(feeToken, 500, 3); - - vm.prank(disputer); - arbitrable.createDispute("Action", 0.18 ether); - - vm.prank(staker1); - core.setStake(GENERAL_COURT, 20000); - vm.warp(block.timestamp + minStakingTime); - sortitionModule.passPhase(); // Generating - vm.warp(block.timestamp + rngLookahead); - sortitionModule.passPhase(); // Drawing phase - - core.draw(disputeID, DEFAULT_NB_OF_JURORS); - - vm.warp(block.timestamp + timesPerPeriod[0]); - core.passPeriod(disputeID); // Vote - - vm.warp(block.timestamp + timesPerPeriod[2]); // Don't vote at all so no one is coherent - core.passPeriod(disputeID); // Appeal - - vm.warp(block.timestamp + timesPerPeriod[3]); - core.passPeriod(disputeID); // Execution - - vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.LeftoverRewardSent(disputeID, 0, 3000, 0.18 ether, feeToken); - core.execute(disputeID, 0, 10); // Put more iterations to check that they're capped - - KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 0); - assertEq(round.pnkPenalties, 3000, "Wrong pnkPenalties"); - assertEq(round.sumFeeRewardPaid, 0, "Wrong sumFeeRewardPaid"); - assertEq(round.sumPnkRewardPaid, 0, "Wrong sumPnkRewardPaid"); - assertEq(round.repartitions, 3, "Wrong repartitions"); - - assertEq(feeToken.balanceOf(address(core)), 0, "Wrong token balance of the core"); - assertEq(feeToken.balanceOf(staker1), 0, "Wrong token balance of staker1"); - assertEq(feeToken.balanceOf(disputer), 0.82 ether, "Wrong token balance of disputer"); - assertEq(feeToken.balanceOf(owner), 0.18 ether, "Wrong token balance of owner"); - } - - function test_executeRuling() public { - uint256 disputeID = 0; - - 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 - vm.warp(block.timestamp + rngLookahead); - sortitionModule.passPhase(); // Drawing phase - - core.draw(disputeID, DEFAULT_NB_OF_JURORS); - vm.warp(block.timestamp + timesPerPeriod[0]); - core.passPeriod(disputeID); // Vote - - uint256[] memory voteIDs = new uint256[](3); - voteIDs[0] = 0; - voteIDs[1] = 1; - voteIDs[2] = 2; - - vm.prank(staker1); - disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); - core.passPeriod(disputeID); // Appeal - - vm.expectRevert(KlerosCoreBase.NotExecutionPeriod.selector); - core.executeRuling(disputeID); - - vm.warp(block.timestamp + timesPerPeriod[3]); - vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.execution); - core.passPeriod(disputeID); // Execution - - (, , KlerosCoreBase.Period period, , uint256 lastPeriodChange) = core.disputes(disputeID); - assertEq(uint256(period), uint256(KlerosCoreBase.Period.execution), "Wrong period"); - assertEq(lastPeriodChange, block.timestamp, "Wrong lastPeriodChange"); - - vm.expectRevert(KlerosCoreBase.DisputePeriodIsFinal.selector); - core.passPeriod(disputeID); - - vm.expectEmit(true, true, true, true); - emit IArbitratorV2.Ruling(arbitrable, disputeID, 2); // Winning choice = 2 - vm.expectEmit(true, true, true, true); - emit IArbitrableV2.Ruling(core, disputeID, 2); - core.executeRuling(disputeID); - - vm.expectRevert(KlerosCoreBase.RulingAlreadyExecuted.selector); - core.executeRuling(disputeID); - - (, , , bool ruled, ) = core.disputes(disputeID); - assertEq(ruled, true, "Should be ruled"); - } - - function test_executeRuling_appealSwitch() public { - // Check that the ruling switches if only one side was funded - uint256 disputeID = 0; - - 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 - vm.warp(block.timestamp + rngLookahead); - sortitionModule.passPhase(); // Drawing phase - - core.draw(disputeID, DEFAULT_NB_OF_JURORS); - vm.warp(block.timestamp + timesPerPeriod[0]); - core.passPeriod(disputeID); // Vote - - uint256[] memory voteIDs = new uint256[](3); - voteIDs[0] = 0; - voteIDs[1] = 1; - voteIDs[2] = 2; - - vm.prank(staker1); - disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); - core.passPeriod(disputeID); // Appeal - - vm.prank(crowdfunder1); - disputeKit.fundAppeal{value: 0.63 ether}(disputeID, 1); // Fund the losing choice - - vm.warp(block.timestamp + timesPerPeriod[3]); - core.passPeriod(disputeID); // Execution - - vm.expectEmit(true, true, true, true); - emit IArbitratorV2.Ruling(arbitrable, disputeID, 1); // Winning choice is switched to 1 - vm.expectEmit(true, true, true, true); - emit IArbitrableV2.Ruling(core, disputeID, 1); - core.executeRuling(disputeID); - - (uint256 ruling, bool tied, bool overridden) = disputeKit.currentRuling(disputeID); - assertEq(ruling, 1, "Wrong ruling"); - assertEq(tied, false, "Not tied"); - assertEq(overridden, true, "Should be overridden"); - } - - function test_withdrawFeesAndRewards() public { - uint256 disputeID = 0; - - 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 - vm.warp(block.timestamp + rngLookahead); - sortitionModule.passPhase(); // Drawing phase - - core.draw(disputeID, DEFAULT_NB_OF_JURORS); - vm.warp(block.timestamp + timesPerPeriod[0]); - core.passPeriod(disputeID); // Vote - - uint256[] memory voteIDs = new uint256[](3); - voteIDs[0] = 0; - voteIDs[1] = 1; - voteIDs[2] = 2; - - vm.prank(staker1); - disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); - core.passPeriod(disputeID); // Appeal - - vm.prank(crowdfunder1); - disputeKit.fundAppeal{value: 0.63 ether}(disputeID, 1); // Fund the losing choice. The ruling will be overridden here - vm.prank(crowdfunder2); - disputeKit.fundAppeal{value: 0.41 ether}(disputeID, 2); // Underpay a bit to not create an appeal and withdraw the funded sum fully - - vm.warp(block.timestamp + timesPerPeriod[3]); - core.passPeriod(disputeID); // Execution - - vm.expectRevert(DisputeKitClassicBase.DisputeNotResolved.selector); - disputeKit.withdrawFeesAndRewards(disputeID, payable(staker1), 0, 1); - - core.executeRuling(disputeID); - - vm.prank(owner); - core.pause(); - vm.expectRevert(DisputeKitClassicBase.CoreIsPaused.selector); - disputeKit.withdrawFeesAndRewards(disputeID, payable(staker1), 0, 1); - vm.prank(owner); - core.unpause(); - - assertEq(crowdfunder1.balance, 9.37 ether, "Wrong balance of the crowdfunder1"); - assertEq(crowdfunder2.balance, 9.59 ether, "Wrong balance of the crowdfunder2"); - assertEq(address(disputeKit).balance, 1.04 ether, "Wrong balance of the DK"); - - vm.expectEmit(true, true, true, true); - emit DisputeKitClassicBase.Withdrawal(disputeID, 0, 1, crowdfunder1, 0.63 ether); - disputeKit.withdrawFeesAndRewards(disputeID, payable(crowdfunder1), 0, 1); - - vm.expectEmit(true, true, true, true); - emit DisputeKitClassicBase.Withdrawal(disputeID, 0, 2, crowdfunder2, 0.41 ether); - disputeKit.withdrawFeesAndRewards(disputeID, payable(crowdfunder2), 0, 2); - - assertEq(crowdfunder1.balance, 10 ether, "Wrong balance of the crowdfunder1"); - assertEq(crowdfunder2.balance, 10 ether, "Wrong balance of the crowdfunder2"); - assertEq(address(disputeKit).balance, 0, "Wrong balance of the DK"); - } - - function test_castVote_differentDK() public { - DisputeKitClassic dkLogic = new DisputeKitClassic(); - // Create a new DK to check castVote. - bytes memory initDataDk = abi.encodeWithSignature( - "initialize(address,address,address)", - owner, - address(core), - address(wNative) - ); - - UUPSProxy proxyDk = new UUPSProxy(address(dkLogic), initDataDk); - DisputeKitClassic newDisputeKit = DisputeKitClassic(address(proxyDk)); - - vm.prank(owner); - core.addNewDisputeKit(newDisputeKit); - - uint256 newDkID = 2; - uint256[] memory supportedDK = new uint256[](1); - bytes memory newExtraData = abi.encodePacked(uint256(GENERAL_COURT), DEFAULT_NB_OF_JURORS, newDkID); - - vm.prank(owner); - vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.DisputeKitEnabled(GENERAL_COURT, newDkID, true); - supportedDK[0] = newDkID; - core.enableDisputeKits(GENERAL_COURT, supportedDK, true); - assertEq(core.isSupported(GENERAL_COURT, newDkID), true, "New DK should be supported by General court"); - - vm.prank(staker1); - core.setStake(GENERAL_COURT, 20000); - - // Create one dispute for the old DK and two disputes for the new DK. - vm.prank(disputer); - arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); - - arbitrable.changeArbitratorExtraData(newExtraData); - - vm.prank(disputer); - arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); - - vm.prank(disputer); - arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); - - uint256 disputeID = 2; // Use the latest dispute for reference. This is the ID in the core contract - - vm.warp(block.timestamp + minStakingTime); - sortitionModule.passPhase(); // Generating - vm.warp(block.timestamp + rngLookahead); - sortitionModule.passPhase(); // Drawing phase - - KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 0); - assertEq(round.disputeKitID, newDkID, "Wrong DK ID"); - - core.draw(disputeID, DEFAULT_NB_OF_JURORS); - // Draw jurors for the old DK as well to prepare round.votes array - core.draw(0, DEFAULT_NB_OF_JURORS); - - vm.warp(block.timestamp + timesPerPeriod[0]); - core.passPeriod(disputeID); // Vote - - // Check that the new DK has the info but not the old one. - - assertEq(disputeKit.coreDisputeIDToActive(disputeID), false, "Should be false for old DK"); - - // This is the DK where dispute was created. Core dispute points to index 1 because new DK has two disputes. - assertEq(newDisputeKit.coreDisputeIDToLocal(disputeID), 1, "Wrong local dispute ID for new DK"); - assertEq(newDisputeKit.coreDisputeIDToActive(disputeID), true, "Should be active for new DK"); - (uint256 numberOfChoices, , bytes memory extraData) = newDisputeKit.disputes(1); - assertEq(numberOfChoices, 2, "Wrong numberOfChoices in new DK"); - assertEq(extraData, newExtraData, "Wrong extra data"); - - uint256[] memory voteIDs = new uint256[](3); - voteIDs[0] = 0; - voteIDs[1] = 1; - voteIDs[2] = 2; - - // Deliberately cast votes using the old DK to see if the exception will be caught. - vm.prank(staker1); - vm.expectRevert(DisputeKitClassicBase.NotActiveForCoreDisputeID.selector); - disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); - - // And check the new DK. - vm.prank(staker1); - newDisputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); - - ( - uint256 winningChoice, - bool tied, - uint256 totalVoted, - uint256 totalCommited, - , - uint256 choiceCount - ) = newDisputeKit.getRoundInfo(disputeID, 0, 2); - assertEq(winningChoice, 2, "Wrong winning choice"); - assertEq(tied, false, "tied should be false"); - assertEq(totalVoted, 3, "totalVoted should be 3"); - 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.owner(), msg.sender, "Wrong owner"); - 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(owner); - 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(owner); - 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(IRNG.ConsumerOnly.selector); - vm.prank(owner); - rngFallback.requestRandomness(); - - vm.expectRevert(IRNG.ConsumerOnly.selector); - vm.prank(owner); - rngFallback.receiveRandomness(); - - vm.expectRevert(IRNG.OwnerOnly.selector); - vm.prank(other); - rngFallback.changeOwner(other); - vm.prank(owner); - rngFallback.changeOwner(other); - assertEq(rngFallback.owner(), other, "Wrong owner"); - - // Change owner back for convenience - vm.prank(other); - rngFallback.changeOwner(owner); - - vm.expectRevert(IRNG.OwnerOnly.selector); - vm.prank(other); - rngFallback.changeConsumer(other); - vm.prank(owner); - rngFallback.changeConsumer(other); - assertEq(rngFallback.consumer(), other, "Wrong consumer"); - - vm.expectRevert(IRNG.OwnerOnly.selector); - vm.prank(other); - rngFallback.changeFallbackTimeout(5); - - vm.prank(owner); - vm.expectEmit(true, true, true, true); - emit RNGWithFallback.FallbackTimeoutChanged(5); - rngFallback.changeFallbackTimeout(5); - assertEq(rngFallback.fallbackTimeoutSeconds(), 5, "Wrong fallback timeout"); - } -} diff --git a/contracts/test/foundry/KlerosCore_Appeals.t.sol b/contracts/test/foundry/KlerosCore_Appeals.t.sol new file mode 100644 index 000000000..f76649e03 --- /dev/null +++ b/contracts/test/foundry/KlerosCore_Appeals.t.sol @@ -0,0 +1,361 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {KlerosCore_TestBase} from "./KlerosCore_TestBase.sol"; +import {KlerosCoreBase} from "../../src/arbitration/KlerosCoreBase.sol"; +import {DisputeKitClassic, DisputeKitClassicBase} from "../../src/arbitration/dispute-kits/DisputeKitClassic.sol"; +import {UUPSProxy} from "../../src/proxy/UUPSProxy.sol"; +import "../../src/libraries/Constants.sol"; + +/// @title KlerosCore_AppealsTest +/// @dev Tests for KlerosCore appeal system, funding, and court/DK jumping +contract KlerosCore_AppealsTest is KlerosCore_TestBase { + function test_appeal_fundOneSide() public { + uint256 disputeID = 0; + vm.deal(address(disputeKit), 1 ether); + vm.deal(staker1, 1 ether); + + vm.prank(staker1); + core.setStake(GENERAL_COURT, 10000); + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.warp(block.timestamp + rngLookahead); + sortitionModule.passPhase(); // Drawing phase + + core.draw(disputeID, DEFAULT_NB_OF_JURORS); + vm.warp(block.timestamp + timesPerPeriod[0]); + core.passPeriod(disputeID); // Vote + + uint256[] memory voteIDs = new uint256[](3); + voteIDs[0] = 0; + voteIDs[1] = 1; + voteIDs[2] = 2; + + vm.prank(staker1); + disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); + + (uint256 start, uint256 end) = core.appealPeriod(0); + assertEq(start, 0, "Appeal period start should be 0"); + assertEq(end, 0, "Appeal period end should be 0"); + + // Simulate the call from dispute kit to check the requires unrelated to caller + vm.prank(address(disputeKit)); + vm.expectRevert(KlerosCoreBase.DisputeNotAppealable.selector); + core.appeal{value: 0.21 ether}(disputeID, 2, arbitratorExtraData); + + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.AppealPossible(disputeID, arbitrable); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.appeal); + core.passPeriod(disputeID); + + (, , KlerosCoreBase.Period period, , uint256 lastPeriodChange) = core.disputes(disputeID); + (start, end) = core.appealPeriod(0); + assertEq(uint256(period), uint256(KlerosCoreBase.Period.appeal), "Wrong period"); + assertEq(lastPeriodChange, block.timestamp, "Wrong lastPeriodChange"); + assertEq(core.appealCost(0), 0.21 ether, "Wrong appealCost"); + assertEq(start, lastPeriodChange, "Appeal period start is incorrect"); + assertEq(end, lastPeriodChange + timesPerPeriod[3], "Appeal period end is incorrect"); + + vm.expectRevert(KlerosCoreBase.AppealPeriodNotPassed.selector); + core.passPeriod(disputeID); + + // Simulate the call from dispute kit to check the requires unrelated to caller + vm.prank(address(disputeKit)); + vm.expectRevert(KlerosCoreBase.AppealFeesNotEnough.selector); + core.appeal{value: 0.21 ether - 1}(disputeID, 2, arbitratorExtraData); + vm.deal(address(disputeKit), 0); // Nullify the balance so it doesn't get in the way. + + vm.prank(staker1); + vm.expectRevert(KlerosCoreBase.DisputeKitOnly.selector); + core.appeal{value: 0.21 ether}(disputeID, 2, arbitratorExtraData); + + vm.prank(crowdfunder1); + vm.expectRevert(DisputeKitClassicBase.ChoiceOutOfBounds.selector); + disputeKit.fundAppeal(disputeID, 3); + + vm.prank(crowdfunder1); + vm.expectEmit(true, true, true, true); + emit DisputeKitClassicBase.Contribution(disputeID, 0, 1, crowdfunder1, 0.21 ether); + disputeKit.fundAppeal{value: 0.21 ether}(disputeID, 1); // Fund the losing choice. Total cost will be 0.63 (0.21 + 0.21 * (20000/10000)) + + assertEq(crowdfunder1.balance, 9.79 ether, "Wrong balance of the crowdfunder"); + assertEq(address(disputeKit).balance, 0.21 ether, "Wrong balance of the DK"); + assertEq((disputeKit.getFundedChoices(disputeID)).length, 0, "No funded choices"); + + vm.prank(crowdfunder1); + vm.expectEmit(true, true, true, true); + emit DisputeKitClassicBase.Contribution(disputeID, 0, 1, crowdfunder1, 0.42 ether); + vm.expectEmit(true, true, true, true); + emit DisputeKitClassicBase.ChoiceFunded(disputeID, 0, 1); + disputeKit.fundAppeal{value: 5 ether}(disputeID, 1); // Deliberately overpay to check reimburse + + assertEq(crowdfunder1.balance, 9.37 ether, "Wrong balance of the crowdfunder"); + assertEq(address(disputeKit).balance, 0.63 ether, "Wrong balance of the DK"); + assertEq((disputeKit.getFundedChoices(disputeID)).length, 1, "One choice should be funded"); + assertEq((disputeKit.getFundedChoices(disputeID))[0], 1, "Incorrect funded choice"); + + vm.prank(crowdfunder1); + vm.expectRevert(DisputeKitClassicBase.AppealFeeIsAlreadyPaid.selector); + disputeKit.fundAppeal(disputeID, 1); + } + + function test_appeal_timeoutCheck() public { + uint256 disputeID = 0; + + vm.prank(staker1); + core.setStake(GENERAL_COURT, 10000); + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.warp(block.timestamp + rngLookahead); + sortitionModule.passPhase(); // Drawing phase + + core.draw(disputeID, DEFAULT_NB_OF_JURORS); + vm.warp(block.timestamp + timesPerPeriod[0]); + core.passPeriod(disputeID); // Vote + + uint256[] memory voteIDs = new uint256[](3); + voteIDs[0] = 0; + voteIDs[1] = 1; + voteIDs[2] = 2; + + vm.prank(staker1); + disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); + + vm.prank(crowdfunder1); + vm.expectRevert(DisputeKitClassicBase.AppealPeriodIsOver.selector); + disputeKit.fundAppeal{value: 0.1 ether}(disputeID, 1); + core.passPeriod(disputeID); + + (uint256 start, uint256 end) = core.appealPeriod(0); + + vm.prank(crowdfunder1); + vm.warp(block.timestamp + ((end - start) / 2 + 1)); + 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(DisputeKitClassicBase.AppealPeriodIsOver.selector); + disputeKit.fundAppeal{value: 0.1 ether}(disputeID, 2); + } + + function test_appeal_fullFundingNoSwitch() public { + uint256 disputeID = 0; + + 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 + vm.warp(block.timestamp + rngLookahead); + sortitionModule.passPhase(); // Drawing phase + + core.draw(disputeID, DEFAULT_NB_OF_JURORS); + vm.warp(block.timestamp + timesPerPeriod[0]); + core.passPeriod(disputeID); // Vote + + uint256[] memory voteIDs = new uint256[](3); + voteIDs[0] = 0; + voteIDs[1] = 1; + voteIDs[2] = 2; + + vm.prank(staker1); + disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); + + core.passPeriod(disputeID); // Appeal + + vm.prank(crowdfunder1); + disputeKit.fundAppeal{value: 0.63 ether}(disputeID, 1); + + vm.prank(crowdfunder2); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.AppealDecision(disputeID, arbitrable); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.evidence); + disputeKit.fundAppeal{value: 0.42 ether}(disputeID, 2); + + assertEq((disputeKit.getFundedChoices(disputeID)).length, 0, "No funded choices in the fresh round"); + (uint256 ruling, bool tied, bool overridden) = disputeKit.currentRuling(disputeID); + assertEq(ruling, 0, "Should be 0 ruling in the fresh round"); + assertEq(tied, true, "Should be tied"); + assertEq(overridden, false, "Not overridden"); + + assertEq(address(disputeKit).balance, 0.84 ether, "Wrong balance of the DK"); // 0.63 + 0.42 - 0.21 + assertEq(address(core).balance, 0.3 ether, "Wrong balance of the core"); // 0.09 arbFee + 0.21 appealFee + + assertEq(sortitionModule.disputesWithoutJurors(), 1, "Wrong disputesWithoutJurors count after appeal"); + assertEq(core.getNumberOfRounds(disputeID), 2, "Wrong number of rounds"); + + (, , KlerosCoreBase.Period period, , uint256 lastPeriodChange) = core.disputes(disputeID); + assertEq(uint256(period), uint256(KlerosCoreBase.Period.evidence), "Wrong period"); + assertEq(lastPeriodChange, block.timestamp, "Wrong lastPeriodChange"); + + KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 1); // Check the new round + assertEq(round.pnkAtStakePerJuror, 1000, "Wrong pnkAtStakePerJuror"); + assertEq(round.totalFeesForJurors, 0.21 ether, "Wrong totalFeesForJurors"); + assertEq(round.nbVotes, 7, "Wrong nbVotes"); + + core.draw(disputeID, 7); + emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.vote); // Check that we don't have to wait for the timeout to pass the evidence period after appeal + core.passPeriod(disputeID); + } + + function test_appeal_fullFundingDKCourtSwitch() public { + uint256 disputeID = 0; + DisputeKitClassic dkLogic = new DisputeKitClassic(); + // Create a new DK and court to check the switch + bytes memory initDataDk = abi.encodeWithSignature( + "initialize(address,address,address)", + owner, + address(core), + address(wNative) + ); + + UUPSProxy proxyDk = new UUPSProxy(address(dkLogic), initDataDk); + DisputeKitClassic newDisputeKit = DisputeKitClassic(address(proxyDk)); + + uint96 newCourtID = 2; + uint256 newDkID = 2; + uint256[] memory supportedDK = new uint256[](1); + supportedDK[0] = DISPUTE_KIT_CLASSIC; + bytes memory newExtraData = abi.encodePacked(uint256(newCourtID), DEFAULT_NB_OF_JURORS, newDkID); + + vm.prank(owner); + core.addNewDisputeKit(newDisputeKit); + vm.prank(owner); + core.createCourt( + GENERAL_COURT, + hiddenVotes, + minStake, + alpha, + feeForJuror, + 3, // jurors for jump. Low number to ensure jump after the first appeal + [uint256(60), uint256(120), uint256(180), uint256(240)], // Times per period + sortitionExtraData, + supportedDK + ); + + arbitrable.changeArbitratorExtraData(newExtraData); + + vm.prank(owner); + supportedDK = new uint256[](1); + supportedDK[0] = newDkID; + core.enableDisputeKits(newCourtID, supportedDK, true); + + vm.prank(staker1); + core.setStake(newCourtID, 20000); + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.warp(block.timestamp + rngLookahead); + sortitionModule.passPhase(); // Drawing phase + + KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 0); + assertEq(round.disputeKitID, newDkID, "Wrong DK ID"); + + core.draw(disputeID, DEFAULT_NB_OF_JURORS); + vm.warp(block.timestamp + timesPerPeriod[0]); + core.passPeriod(disputeID); // Vote + + uint256[] memory voteIDs = new uint256[](3); + voteIDs[0] = 0; + voteIDs[1] = 1; + voteIDs[2] = 2; + + vm.prank(staker1); + newDisputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); + + core.passPeriod(disputeID); // Appeal + + vm.prank(crowdfunder1); + newDisputeKit.fundAppeal{value: 0.63 ether}(disputeID, 1); + vm.prank(crowdfunder2); + + assertEq(core.isDisputeKitJumping(disputeID), true, "Should be jumping"); + + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.CourtJump(disputeID, 1, newCourtID, GENERAL_COURT); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.DisputeKitJump(disputeID, 1, newDkID, DISPUTE_KIT_CLASSIC); + vm.expectEmit(true, true, true, true); + emit DisputeKitClassicBase.DisputeCreation(disputeID, 2, newExtraData); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.AppealDecision(disputeID, arbitrable); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.evidence); + newDisputeKit.fundAppeal{value: 0.42 ether}(disputeID, 2); + + (, bool jumped, ) = newDisputeKit.disputes(disputeID); + assertEq(jumped, true, "jumped should be true"); + assertEq( + (newDisputeKit.getFundedChoices(disputeID)).length, + 2, + "No fresh round created so the number of funded choices should be 2" + ); + + round = core.getRoundInfo(disputeID, 1); + assertEq(round.disputeKitID, DISPUTE_KIT_CLASSIC, "Wrong DK ID"); + assertEq(sortitionModule.disputesWithoutJurors(), 1, "Wrong disputesWithoutJurors count"); + (uint96 courtID, , , , ) = core.disputes(disputeID); + assertEq(courtID, GENERAL_COURT, "Wrong court ID"); + + (, jumped, ) = disputeKit.disputes(disputeID); + assertEq(jumped, false, "jumped should be false in the DK that dispute jumped to"); + + // Check jump modifier + vm.prank(address(core)); + vm.expectRevert(DisputeKitClassicBase.DisputeJumpedToParentDK.selector); + newDisputeKit.draw(disputeID, 1); + + // And check that draw in the new round works + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.Draw(staker1, disputeID, 1, 0); // roundID = 1 VoteID = 0 + core.draw(disputeID, 1); + + (address account, , , ) = disputeKit.getVoteInfo(disputeID, 1, 0); + assertEq(account, staker1, "Wrong drawn account in the classic DK"); + } + + function test_appeal_quickPassPeriod() public { + uint256 disputeID = 0; + + vm.prank(staker1); + core.setStake(GENERAL_COURT, 10000); + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.warp(block.timestamp + rngLookahead); + sortitionModule.passPhase(); // Drawing phase + + core.draw(disputeID, DEFAULT_NB_OF_JURORS); + vm.warp(block.timestamp + timesPerPeriod[0]); + core.passPeriod(disputeID); // Vote + + uint256[] memory voteIDs = new uint256[](3); + voteIDs[0] = 0; + voteIDs[1] = 1; + voteIDs[2] = 2; + + vm.prank(staker1); + disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); + + core.passPeriod(disputeID); // Appeal + + vm.warp(block.timestamp + timesPerPeriod[3] / 2); + + // Should pass to execution period without waiting for the 2nd half of the appeal. + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.execution); + core.passPeriod(disputeID); + } +} diff --git a/contracts/test/foundry/KlerosCore_Disputes.t.sol b/contracts/test/foundry/KlerosCore_Disputes.t.sol new file mode 100644 index 000000000..954789f92 --- /dev/null +++ b/contracts/test/foundry/KlerosCore_Disputes.t.sol @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {KlerosCore_TestBase} from "./KlerosCore_TestBase.sol"; +import {KlerosCoreBase} from "../../src/arbitration/KlerosCoreBase.sol"; +import {IArbitratorV2} from "../../src/arbitration/KlerosCoreBase.sol"; +import {DisputeKitClassicBase} from "../../src/arbitration/dispute-kits/DisputeKitClassicBase.sol"; +import {IArbitrableV2} from "../../src/arbitration/arbitrables/ArbitrableExample.sol"; +import "../../src/libraries/Constants.sol"; + +/// @title KlerosCore_DisputesTest +/// @dev Tests for KlerosCore dispute creation and management +contract KlerosCore_DisputesTest is KlerosCore_TestBase { + function test_createDispute_eth() public { + // Create a new court and DK to test non-standard extra data + uint256 newFee = 0.01 ether; + uint96 newCourtID = 2; + uint256 newNbJurors = 4; + uint256 newDkID = 2; + uint256[] memory supportedDK = new uint256[](1); + supportedDK[0] = DISPUTE_KIT_CLASSIC; + bytes memory newExtraData = abi.encodePacked(uint256(newCourtID), newNbJurors, newDkID); + + vm.prank(owner); + core.addNewDisputeKit(disputeKit); // Just add the same dk to avoid dealing with initialization + vm.prank(owner); + core.createCourt( + GENERAL_COURT, + true, // Hidden votes + 2000, // min stake + 20000, // alpha + newFee, // fee for juror + 50, // jurors for jump + [uint256(10), uint256(20), uint256(30), uint256(40)], // Times per period + abi.encode(uint256(4)), // Sortition extra data + supportedDK + ); + + arbitrable.changeArbitratorExtraData(newExtraData); + + vm.expectRevert(KlerosCoreBase.ArbitrationFeesNotEnough.selector); + vm.prank(disputer); + arbitrable.createDispute{value: newFee * newNbJurors - 1}("Action"); + + vm.expectRevert(KlerosCoreBase.DisputeKitNotSupportedByCourt.selector); + vm.prank(disputer); + arbitrable.createDispute{value: 0.04 ether}("Action"); + + vm.prank(owner); + supportedDK = new uint256[](1); + supportedDK[0] = newDkID; + core.enableDisputeKits(newCourtID, supportedDK, true); + + uint256 disputeID = 0; + uint256 nbChoices = 2; + vm.prank(disputer); + vm.expectEmit(true, true, true, true); + emit DisputeKitClassicBase.DisputeCreation(disputeID, nbChoices, newExtraData); + vm.expectEmit(true, true, true, true); + emit IArbitratorV2.DisputeCreation(disputeID, arbitrable); + arbitrable.createDispute{value: 0.04 ether}("Action"); + + assertEq(sortitionModule.disputesWithoutJurors(), 1, "Wrong disputesWithoutJurors count"); + ( + uint96 courtID, + IArbitrableV2 arbitrated, + KlerosCoreBase.Period period, + bool ruled, + uint256 lastPeriodChange + ) = core.disputes(disputeID); + + assertEq(courtID, newCourtID, "Wrong court ID"); + assertEq(address(arbitrated), address(arbitrable), "Wrong arbitrable"); + assertEq(uint256(period), uint256(KlerosCoreBase.Period.evidence), "Wrong period"); + assertEq(ruled, false, "Should not be ruled"); + assertEq(lastPeriodChange, block.timestamp, "Wrong lastPeriodChange"); + + KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 0); + assertEq(round.disputeKitID, newDkID, "Wrong DK ID"); + assertEq(round.pnkAtStakePerJuror, 4000, "Wrong pnkAtStakePerJuror"); // minStake * alpha / divisor = 2000 * 20000/10000 + assertEq(round.totalFeesForJurors, 0.04 ether, "Wrong totalFeesForJurors"); + assertEq(round.nbVotes, 4, "Wrong nbVotes"); + assertEq(round.repartitions, 0, "repartitions should be 0"); + assertEq(round.pnkPenalties, 0, "pnkPenalties should be 0"); + assertEq(round.sumFeeRewardPaid, 0, "sumFeeRewardPaid should be 0"); + assertEq(round.sumPnkRewardPaid, 0, "sumPnkRewardPaid should be 0"); + assertEq(address(round.feeToken), address(0), "feeToken should be 0"); + assertEq(round.drawIterations, 0, "drawIterations should be 0"); + + (uint256 numberOfChoices, bool jumped, bytes memory extraData) = disputeKit.disputes(disputeID); + + assertEq(numberOfChoices, 2, "Wrong numberOfChoices"); + assertEq(jumped, false, "jumped should be false"); + assertEq(extraData, newExtraData, "Wrong extra data"); + assertEq(disputeKit.coreDisputeIDToLocal(0), disputeID, "Wrong local disputeID"); + assertEq(disputeKit.coreDisputeIDToActive(0), true, "Wrong disputes length"); + + ( + uint256 winningChoice, + bool tied, + uint256 totalVoted, + uint256 totalCommited, + uint256 nbVoters, + uint256 choiceCount + ) = disputeKit.getRoundInfo(0, 0, 0); + assertEq(winningChoice, 0, "winningChoice should be 0"); + assertEq(tied, true, "tied should be true"); + assertEq(totalVoted, 0, "totalVoted should be 0"); + assertEq(totalCommited, 0, "totalCommited should be 0"); + assertEq(nbVoters, 0, "nbVoters should be 0"); + assertEq(choiceCount, 0, "choiceCount should be 0"); + } + + function test_createDispute_tokens() public { + feeToken.transfer(disputer, 1 ether); + vm.prank(disputer); + feeToken.approve(address(arbitrable), 1 ether); + + vm.expectRevert(KlerosCoreBase.TokenNotAccepted.selector); + vm.prank(disputer); + arbitrable.createDispute("Action", 0.18 ether); + + vm.prank(owner); + core.changeAcceptedFeeTokens(feeToken, true); + vm.prank(owner); + core.changeCurrencyRates(feeToken, 500, 3); + + vm.expectRevert(KlerosCoreBase.ArbitrationFeesNotEnough.selector); + vm.prank(disputer); + arbitrable.createDispute("Action", 0.18 ether - 1); + + vm.expectRevert(KlerosCoreBase.TransferFailed.selector); + vm.prank(address(arbitrable)); // Bypass createDispute in arbitrable to avoid transfer checks there and make the arbitrable call KC directly + core.createDispute(2, arbitratorExtraData, feeToken, 0.18 ether); + + assertEq(core.arbitrationCost(arbitratorExtraData, feeToken), 0.18 ether, "Wrong token cost"); + vm.prank(disputer); + arbitrable.createDispute("Action", 0.18 ether); + + KlerosCoreBase.Round memory round = core.getRoundInfo(0, 0); + assertEq(round.totalFeesForJurors, 0.18 ether, "Wrong totalFeesForJurors"); + assertEq(round.nbVotes, 3, "Wrong nbVotes"); + assertEq(address(round.feeToken), address(feeToken), "Wrong feeToken"); + + assertEq(feeToken.balanceOf(address(core)), 0.18 ether, "Wrong token balance of the core"); + assertEq(feeToken.balanceOf(disputer), 0.82 ether, "Wrong token balance of the disputer"); + } +} diff --git a/contracts/test/foundry/KlerosCore_Drawing.t.sol b/contracts/test/foundry/KlerosCore_Drawing.t.sol new file mode 100644 index 000000000..d6ffc9ea9 --- /dev/null +++ b/contracts/test/foundry/KlerosCore_Drawing.t.sol @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {KlerosCore_TestBase} from "./KlerosCore_TestBase.sol"; +import {KlerosCoreBase} from "../../src/arbitration/KlerosCoreBase.sol"; +import {SortitionModuleBase} from "../../src/arbitration/SortitionModuleBase.sol"; +import {ISortitionModule} from "../../src/arbitration/interfaces/ISortitionModule.sol"; +import "../../src/libraries/Constants.sol"; + +/// @title KlerosCore_DrawingTest +/// @dev Tests for KlerosCore jury drawing and selection mechanisms +contract KlerosCore_DrawingTest is KlerosCore_TestBase { + function test_draw() public { + uint256 disputeID = 0; + uint256 roundID = 0; + + vm.prank(staker1); + core.setStake(GENERAL_COURT, 1500); + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.warp(block.timestamp + rngLookahead); + sortitionModule.passPhase(); // Drawing phase + + vm.expectEmit(true, true, true, true); + emit SortitionModuleBase.StakeLocked(staker1, 1000, false); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.Draw(staker1, disputeID, roundID, 0); // VoteID = 0 + + core.draw(disputeID, DEFAULT_NB_OF_JURORS); // Do 3 iterations and see that the juror will get drawn 3 times despite low stake. + + (uint256 totalStaked, uint256 totalLocked, uint256 stakedInCourt, ) = sortitionModule.getJurorBalance( + staker1, + GENERAL_COURT + ); + assertEq(totalStaked, 1500, "Wrong amount total staked"); + assertEq(totalLocked, 3000, "Wrong amount locked"); // 1000 per draw + assertEq(stakedInCourt, 1500, "Wrong amount staked in court"); + assertEq(sortitionModule.disputesWithoutJurors(), 0, "Wrong disputesWithoutJurors count"); + + for (uint256 i = 0; i < DEFAULT_NB_OF_JURORS; i++) { + (address account, bytes32 commit, uint256 choice, bool voted) = disputeKit.getVoteInfo(0, 0, i); + assertEq(account, staker1, "Wrong drawn account"); + assertEq(commit, bytes32(0), "Commit should be empty"); + assertEq(choice, 0, "Choice should be empty"); + assertEq(voted, false, "Voted should be false"); + } + } + + function test_draw_noEmptyAddresses() public { + uint256 disputeID = 0; + uint256 roundID = 0; + + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + 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. + + KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, roundID); + assertEq(round.drawIterations, 3, "Wrong drawIterations number"); + + (, , , , uint256 nbVoters, ) = disputeKit.getRoundInfo(disputeID, roundID, 0); + assertEq(nbVoters, 0, "nbVoters should be 0"); + } + + function test_draw_parentCourts() public { + uint96 newCourtID = 2; + uint256 disputeID = 0; + uint256 roundID = 0; + + // Create a child court and stake exclusively there to check that parent courts hold drawing power. + vm.prank(owner); + uint256[] memory supportedDK = new uint256[](1); + supportedDK[0] = DISPUTE_KIT_CLASSIC; + core.createCourt( + GENERAL_COURT, + true, // Hidden votes + 1000, // min stake + 10000, // alpha + 0.03 ether, // fee for juror + 50, // jurors for jump + [uint256(10), uint256(20), uint256(30), uint256(40)], // Times per period + sortitionExtraData, // Sortition extra data + supportedDK + ); + + uint256[] memory children = core.getCourtChildren(GENERAL_COURT); + assertEq(children.length, 1, "Wrong children count"); + assertEq(children[0], 2, "Wrong child ID"); + + vm.prank(staker1); + core.setStake(newCourtID, 3000); + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); // Dispute uses general court by default + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.warp(block.timestamp + rngLookahead); + sortitionModule.passPhase(); // Drawing phase + + (uint96 courtID, , , , ) = core.disputes(disputeID); + assertEq(courtID, GENERAL_COURT, "Wrong court ID of the dispute"); + + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.Draw(staker1, disputeID, roundID, 0); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.Draw(staker1, disputeID, roundID, 1); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.Draw(staker1, disputeID, roundID, 2); + core.draw(disputeID, DEFAULT_NB_OF_JURORS); + + assertEq(sortitionModule.disputesWithoutJurors(), 0, "Wrong disputesWithoutJurors count"); + + KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, roundID); + assertEq(round.drawIterations, 3, "Wrong drawIterations number"); + + (, , , , uint256 nbVoters, ) = disputeKit.getRoundInfo(disputeID, roundID, 0); + assertEq(nbVoters, 3, "nbVoters should be 3"); + } +} diff --git a/contracts/test/foundry/KlerosCore_Execution.t.sol b/contracts/test/foundry/KlerosCore_Execution.t.sol new file mode 100644 index 000000000..21e9b7ab0 --- /dev/null +++ b/contracts/test/foundry/KlerosCore_Execution.t.sol @@ -0,0 +1,688 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {KlerosCore_TestBase} from "./KlerosCore_TestBase.sol"; +import {KlerosCoreBase} from "../../src/arbitration/KlerosCoreBase.sol"; +import {SortitionModuleBase} from "../../src/arbitration/SortitionModuleBase.sol"; +import {DisputeKitClassicBase} from "../../src/arbitration/dispute-kits/DisputeKitClassicBase.sol"; +import {IArbitratorV2, IArbitrableV2} from "../../src/arbitration/KlerosCoreBase.sol"; +import {IERC20} from "../../src/libraries/SafeERC20.sol"; +import "../../src/libraries/Constants.sol"; + +/// @title KlerosCore_ExecutionTest +/// @dev Tests for KlerosCore execution, rewards, and ruling finalization +contract KlerosCore_ExecutionTest is KlerosCore_TestBase { + function test_execute() public { + uint256 disputeID = 0; + + vm.prank(staker1); + core.setStake(GENERAL_COURT, 1500); + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + 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. + core.draw(disputeID, 1); + + vm.warp(block.timestamp + maxDrawingTime); + sortitionModule.passPhase(); // Staking phase to stake the 2nd voter + vm.prank(staker2); + core.setStake(GENERAL_COURT, 20000); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.warp(block.timestamp + rngLookahead); + sortitionModule.passPhase(); // Drawing phase + + core.draw(disputeID, 2); // Assign leftover votes to staker2 + + vm.warp(block.timestamp + timesPerPeriod[0]); + core.passPeriod(disputeID); // Vote + + uint256[] memory voteIDs = new uint256[](1); + voteIDs[0] = 0; + vm.prank(staker1); + disputeKit.castVote(disputeID, voteIDs, 1, 0, "XYZ"); // Staker1 only got 1 vote because of low stake + + voteIDs = new uint256[](2); + voteIDs[0] = 1; + voteIDs[1] = 2; + vm.prank(staker2); + disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); + core.passPeriod(disputeID); // Appeal + + vm.expectRevert(KlerosCoreBase.NotExecutionPeriod.selector); + core.execute(disputeID, 0, 1); + + vm.warp(block.timestamp + timesPerPeriod[3]); + core.passPeriod(disputeID); // Execution + + vm.prank(owner); + core.pause(); + vm.expectRevert(KlerosCoreBase.WhenNotPausedOnly.selector); + core.execute(disputeID, 0, 1); + vm.prank(owner); + 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) + (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); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.TokenAndETHShift(staker1, disputeID, 0, 0, -int256(1000), 0, IERC20(address(0))); + // Check iterations for the winning staker to see the shifts + vm.expectEmit(true, true, true, true); + emit SortitionModuleBase.StakeLocked(staker2, 0, true); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.TokenAndETHShift(staker2, disputeID, 0, 10000, 0, 0, IERC20(address(0))); + vm.expectEmit(true, true, true, true); + emit SortitionModuleBase.StakeLocked(staker2, 0, true); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.TokenAndETHShift(staker2, disputeID, 0, 10000, 0, 0, IERC20(address(0))); + core.execute(disputeID, 0, 3); // Do 3 iterations to check penalties first + + (uint256 totalStaked, uint256 totalLocked, , ) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); + assertEq(totalStaked, 500, "totalStaked should be penalized"); // 1500 - 1000 + assertEq(totalLocked, 0, "Tokens should be released for staker1"); + (, totalLocked, , ) = sortitionModule.getJurorBalance(staker2, GENERAL_COURT); + assertEq(totalLocked, 2000, "Tokens should still be locked for staker2"); + + KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 0); + assertEq(round.repartitions, 3, "Wrong repartitions"); + assertEq(round.pnkPenalties, 1000, "Wrong pnkPenalties"); + + vm.expectEmit(true, true, true, true); + emit SortitionModuleBase.StakeLocked(staker1, 0, true); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.TokenAndETHShift(staker1, disputeID, 0, 0, 0, 0, IERC20(address(0))); + // Check iterations for the winning staker to see the shifts + vm.expectEmit(true, true, true, true); + emit SortitionModuleBase.StakeLocked(staker2, 1000, true); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.TokenAndETHShift(staker2, disputeID, 0, 10000, 500, 0.045 ether, IERC20(address(0))); + vm.expectEmit(true, true, true, true); + emit SortitionModuleBase.StakeLocked(staker2, 1000, true); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.TokenAndETHShift(staker2, disputeID, 0, 10000, 500, 0.045 ether, IERC20(address(0))); + core.execute(disputeID, 0, 10); // Finish the iterations. We need only 3 but check that it corrects the count. + + (, totalLocked, , ) = sortitionModule.getJurorBalance(staker2, GENERAL_COURT); + assertEq(totalLocked, 0, "Tokens should be unlocked for staker2"); + + round = core.getRoundInfo(disputeID, 0); + assertEq(round.repartitions, 6, "Wrong repartitions"); + assertEq(round.pnkPenalties, 1000, "Wrong pnkPenalties"); + assertEq(round.sumFeeRewardPaid, 0.09 ether, "Wrong sumFeeRewardPaid"); + assertEq(round.sumPnkRewardPaid, 1000, "Wrong sumPnkRewardPaid"); + + assertEq(address(core).balance, 0, "Wrong balance of the core"); + assertEq(staker1.balance, 0, "Wrong balance of the staker1"); + assertEq(staker2.balance, 0.09 ether, "Wrong balance of the staker2"); + + assertEq(pinakion.balanceOf(address(core)), 21500, "Wrong token balance of the core"); // Was 21500. 1000 was transferred to staker2 + assertEq(pinakion.balanceOf(staker1), 999999999999998500, "Wrong token balance of staker1"); + assertEq(pinakion.balanceOf(staker2), 999999999999980000, "Wrong token balance of staker2"); // 20k stake and 1k added as a reward, thus -19k from the default + } + + function test_execute_NoCoherence() public { + uint256 disputeID = 0; + + 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 + vm.warp(block.timestamp + rngLookahead); + sortitionModule.passPhase(); // Drawing phase + + core.draw(disputeID, DEFAULT_NB_OF_JURORS); + + vm.warp(block.timestamp + timesPerPeriod[0]); + core.passPeriod(disputeID); // Vote + + vm.warp(block.timestamp + timesPerPeriod[2]); // Don't vote at all so no one is coherent + core.passPeriod(disputeID); // Appeal + + vm.warp(block.timestamp + timesPerPeriod[3]); + 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) + (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 ownerBalance = owner.balance; + uint256 ownerTokenBalance = pinakion.balanceOf(owner); + + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.LeftoverRewardSent(disputeID, 0, 3000, 0.09 ether, IERC20(address(0))); + core.execute(disputeID, 0, 3); + + KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 0); + assertEq(round.pnkPenalties, 3000, "Wrong pnkPenalties"); + assertEq(round.sumFeeRewardPaid, 0, "Wrong sumFeeRewardPaid"); + assertEq(round.sumPnkRewardPaid, 0, "Wrong sumPnkRewardPaid"); + + assertEq(address(core).balance, 0, "Wrong balance of the core"); + assertEq(staker1.balance, 0, "Wrong balance of the staker1"); + assertEq(owner.balance, ownerBalance + 0.09 ether, "Wrong balance of the owner"); + + assertEq(pinakion.balanceOf(address(core)), 17000, "Wrong token balance of the core"); + assertEq(pinakion.balanceOf(staker1), 999999999999980000, "Wrong token balance of staker1"); + assertEq(pinakion.balanceOf(owner), ownerTokenBalance + 3000, "Wrong token balance of owner"); + } + + function test_execute_UnstakeInactive() public { + // Create a 2nd court so unstaking is done in multiple courts. + vm.prank(owner); + uint256[] memory supportedDK = new uint256[](1); + supportedDK[0] = DISPUTE_KIT_CLASSIC; + core.createCourt( + GENERAL_COURT, + true, // Hidden votes + 1000, // min stake + 10000, // alpha + 0.03 ether, // fee for juror + 50, // jurors for jump + [uint256(10), uint256(20), uint256(30), uint256(40)], // Times per period + sortitionExtraData, // Sortition extra data + supportedDK + ); + + uint256 disputeID = 0; + uint96 newCourtID = 2; + + vm.prank(staker1); + core.setStake(GENERAL_COURT, 20000); + vm.prank(staker1); + core.setStake(newCourtID, 20000); + (, , , uint256 nbCourts) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); + assertEq(nbCourts, 2, "Wrong number of courts"); + + assertEq(pinakion.balanceOf(address(core)), 40000, "Wrong token balance of the core"); + assertEq(pinakion.balanceOf(staker1), 999999999999960000, "Wrong token balance of staker1"); + + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.warp(block.timestamp + rngLookahead); + sortitionModule.passPhase(); // Drawing phase + + core.draw(disputeID, DEFAULT_NB_OF_JURORS); + + sortitionModule.passPhase(); // Staking phase. Change to staking so we don't have to deal with delayed stakes. + + vm.warp(block.timestamp + timesPerPeriod[0]); + core.passPeriod(disputeID); // Vote + + vm.warp(block.timestamp + timesPerPeriod[2]); // Don't vote at all so no one is coherent + core.passPeriod(disputeID); // Appeal + + vm.warp(block.timestamp + timesPerPeriod[3]); + core.passPeriod(disputeID); // Execution + + uint256 ownerTokenBalance = pinakion.balanceOf(owner); + + // Note that these events are emitted only after the first iteration of execute() therefore the juror has been penalized only for 1000 PNK her. + vm.expectEmit(true, true, true, true); + emit SortitionModuleBase.StakeSet(staker1, newCourtID, 0, 19000); // Starting with 40000 we first nullify the stake and remove 20000 and then remove penalty once since there was only first iteration (40000 - 20000 - 1000) + vm.expectEmit(true, true, true, true); + emit SortitionModuleBase.StakeSet(staker1, GENERAL_COURT, 0, 2000); // 2000 PNK should remain in balance to cover penalties since the first 1000 of locked pnk was already unlocked + core.execute(disputeID, 0, 3); + + assertEq(pinakion.balanceOf(address(core)), 0, "Wrong token balance of the core"); + assertEq(pinakion.balanceOf(staker1), 999999999999997000, "Wrong token balance of staker1"); // 3000 locked PNK was withheld by the contract and given to owner. + assertEq(pinakion.balanceOf(owner), ownerTokenBalance + 3000, "Wrong token balance of owner"); + + (, , , nbCourts) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); + assertEq(nbCourts, 0, "Should unstake from all courts"); + } + + function test_execute_UnstakeInsolvent() public { + uint256 disputeID = 0; + + vm.prank(staker1); + core.setStake(GENERAL_COURT, 1000); + + assertEq(pinakion.balanceOf(address(core)), 1000, "Wrong token balance of the core"); + assertEq(pinakion.balanceOf(staker1), 999999999999999000, "Wrong token balance of staker1"); + + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.warp(block.timestamp + rngLookahead); + sortitionModule.passPhase(); // Drawing phase + + core.draw(disputeID, DEFAULT_NB_OF_JURORS); + + (uint256 totalStaked, uint256 totalLocked, , uint256 nbCourts) = sortitionModule.getJurorBalance( + staker1, + GENERAL_COURT + ); + assertEq(totalStaked, 1000, "Wrong totalStaked"); + assertEq(totalLocked, 3000, "totalLocked should exceed totalStaked"); // Juror only staked 1000 but was drawn 3x of minStake (3000 locked) + assertEq(nbCourts, 1, "Wrong number of courts"); + + sortitionModule.passPhase(); // Staking phase. Change to staking so we don't have to deal with delayed stakes. + + vm.warp(block.timestamp + timesPerPeriod[0]); + core.passPeriod(disputeID); // Vote + + uint256[] memory voteIDs = new uint256[](1); + voteIDs[0] = 0; + vm.prank(staker1); + disputeKit.castVote(disputeID, voteIDs, 1, 0, "XYZ"); // 1 incoherent vote should make the juror insolvent + + voteIDs = new uint256[](2); + voteIDs[0] = 1; + voteIDs[1] = 2; + vm.prank(staker1); + disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); + + core.passPeriod(disputeID); // Appeal + + vm.warp(block.timestamp + timesPerPeriod[3]); + core.passPeriod(disputeID); // Execution + + vm.expectEmit(true, true, true, true); + emit SortitionModuleBase.StakeSet(staker1, GENERAL_COURT, 0, 0); // Juror should have no stake left and should be unstaked from the court automatically. + core.execute(disputeID, 0, 6); + + assertEq(pinakion.balanceOf(address(core)), 0, "Wrong token balance of the core"); + assertEq(pinakion.balanceOf(staker1), 1 ether, "Wrong token balance of staker1"); // The juror should have his penalty back as a reward + + (, , , nbCourts) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); + assertEq(nbCourts, 0, "Should unstake from all courts"); + } + + function test_execute_withdrawLeftoverPNK() public { + // Return the previously locked tokens + uint256 disputeID = 0; + + vm.prank(staker1); + core.setStake(GENERAL_COURT, 1000); + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.warp(block.timestamp + rngLookahead); + sortitionModule.passPhase(); // Drawing phase + + core.draw(disputeID, DEFAULT_NB_OF_JURORS); + + sortitionModule.passPhase(); // Staking. Pass the phase so the juror can unstake before execution + + vm.warp(block.timestamp + timesPerPeriod[0]); + core.passPeriod(disputeID); // Vote + + uint256[] memory voteIDs = new uint256[](3); + voteIDs[0] = 0; + voteIDs[1] = 1; + voteIDs[2] = 2; + vm.prank(staker1); + disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); + + core.passPeriod(disputeID); // Appeal + + vm.warp(block.timestamp + timesPerPeriod[3]); + core.passPeriod(disputeID); // Execution + + vm.prank(staker1); + core.setStake(GENERAL_COURT, 0); // Set stake to 0 to check if it will be withdrawn later. + + (uint256 totalStaked, uint256 totalLocked, uint256 stakedInCourt, uint256 nbCourts) = sortitionModule + .getJurorBalance(staker1, GENERAL_COURT); + assertEq(totalStaked, 1000, "Wrong amount staked"); + assertEq(totalLocked, 3000, "Wrong amount locked"); + assertEq(stakedInCourt, 0, "Should be unstaked"); + assertEq(nbCourts, 0, "Should be 0 courts"); + + assertEq(pinakion.balanceOf(address(core)), 1000, "Wrong token balance of the core"); + assertEq(pinakion.balanceOf(staker1), 999999999999999000, "Wrong token balance of staker1"); + + vm.expectRevert(SortitionModuleBase.NotEligibleForWithdrawal.selector); + sortitionModule.withdrawLeftoverPNK(staker1); + + vm.expectEmit(true, true, true, true); + emit SortitionModuleBase.LeftoverPNK(staker1, 1000); + core.execute(disputeID, 0, 6); + + (totalStaked, totalLocked, , ) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); + assertEq(totalStaked, 1000, "Wrong amount staked"); + assertEq(totalLocked, 0, "Should be fully unlocked"); + + KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 0); + assertEq(round.pnkPenalties, 0, "Wrong pnkPenalties"); + assertEq(round.sumFeeRewardPaid, 0.09 ether, "Wrong sumFeeRewardPaid"); + assertEq(round.sumPnkRewardPaid, 0, "Wrong sumPnkRewardPaid"); // No penalty so no rewards in pnk + + // Execute() shouldn't withdraw the tokens. + assertEq(pinakion.balanceOf(address(core)), 1000, "Wrong token balance of the core"); + assertEq(pinakion.balanceOf(staker1), 999999999999999000, "Wrong token balance of staker1"); + + vm.expectRevert(KlerosCoreBase.SortitionModuleOnly.selector); + vm.prank(owner); + core.transferBySortitionModule(staker1, 1000); + + vm.expectEmit(true, true, true, true); + emit SortitionModuleBase.LeftoverPNKWithdrawn(staker1, 1000); + sortitionModule.withdrawLeftoverPNK(staker1); + + (totalStaked, , , ) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); + assertEq(totalStaked, 0, "Should be unstaked fully"); + + // Check that everything is withdrawn now + assertEq(pinakion.balanceOf(address(core)), 0, "Core balance should be empty"); + assertEq(pinakion.balanceOf(staker1), 1 ether, "All PNK should be withdrawn"); + } + + function test_execute_feeToken() public { + uint256 disputeID = 0; + + feeToken.transfer(disputer, 1 ether); + vm.prank(disputer); + feeToken.approve(address(arbitrable), 1 ether); + + vm.prank(owner); + core.changeAcceptedFeeTokens(feeToken, true); + vm.prank(owner); + core.changeCurrencyRates(feeToken, 500, 3); + + vm.prank(disputer); + arbitrable.createDispute("Action", 0.18 ether); + + vm.prank(staker1); + core.setStake(GENERAL_COURT, 20000); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.warp(block.timestamp + rngLookahead); + sortitionModule.passPhase(); // Drawing phase + + core.draw(disputeID, DEFAULT_NB_OF_JURORS); + + vm.warp(block.timestamp + timesPerPeriod[0]); + core.passPeriod(disputeID); // Vote + + uint256[] memory voteIDs = new uint256[](3); + voteIDs[0] = 0; + voteIDs[1] = 1; + voteIDs[2] = 2; + vm.prank(staker1); + disputeKit.castVote(disputeID, voteIDs, 1, 0, "XYZ"); // Staker1 only got 1 vote because of low stake + + core.passPeriod(disputeID); // Appeal + + vm.warp(block.timestamp + timesPerPeriod[3]); + core.passPeriod(disputeID); // Execution + + // Check only once per penalty and per reward + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.TokenAndETHShift(staker1, disputeID, 0, 10000, 0, 0, feeToken); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.TokenAndETHShift(staker1, disputeID, 0, 10000, 0, 0.06 ether, feeToken); + core.execute(disputeID, 0, 6); + + KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 0); + assertEq(round.sumFeeRewardPaid, 0.18 ether, "Wrong sumFeeRewardPaid"); + + assertEq(feeToken.balanceOf(address(core)), 0, "Wrong fee token balance of the core"); + assertEq(feeToken.balanceOf(staker1), 0.18 ether, "Wrong fee token balance of staker1"); + assertEq(feeToken.balanceOf(disputer), 0.82 ether, "Wrong fee token balance of disputer"); + } + + function test_execute_NoCoherence_feeToken() public { + uint256 disputeID = 0; + + feeToken.transfer(disputer, 1 ether); + vm.prank(disputer); + feeToken.approve(address(arbitrable), 1 ether); + + vm.prank(owner); + core.changeAcceptedFeeTokens(feeToken, true); + vm.prank(owner); + core.changeCurrencyRates(feeToken, 500, 3); + + vm.prank(disputer); + arbitrable.createDispute("Action", 0.18 ether); + + vm.prank(staker1); + core.setStake(GENERAL_COURT, 20000); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.warp(block.timestamp + rngLookahead); + sortitionModule.passPhase(); // Drawing phase + + core.draw(disputeID, DEFAULT_NB_OF_JURORS); + + vm.warp(block.timestamp + timesPerPeriod[0]); + core.passPeriod(disputeID); // Vote + + vm.warp(block.timestamp + timesPerPeriod[2]); // Don't vote at all so no one is coherent + core.passPeriod(disputeID); // Appeal + + vm.warp(block.timestamp + timesPerPeriod[3]); + core.passPeriod(disputeID); // Execution + + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.LeftoverRewardSent(disputeID, 0, 3000, 0.18 ether, feeToken); + core.execute(disputeID, 0, 10); // Put more iterations to check that they're capped + + KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 0); + assertEq(round.pnkPenalties, 3000, "Wrong pnkPenalties"); + assertEq(round.sumFeeRewardPaid, 0, "Wrong sumFeeRewardPaid"); + assertEq(round.sumPnkRewardPaid, 0, "Wrong sumPnkRewardPaid"); + assertEq(round.repartitions, 3, "Wrong repartitions"); + + assertEq(feeToken.balanceOf(address(core)), 0, "Wrong token balance of the core"); + assertEq(feeToken.balanceOf(staker1), 0, "Wrong token balance of staker1"); + assertEq(feeToken.balanceOf(disputer), 0.82 ether, "Wrong token balance of disputer"); + assertEq(feeToken.balanceOf(owner), 0.18 ether, "Wrong token balance of owner"); + } + + function test_executeRuling() public { + uint256 disputeID = 0; + + 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 + vm.warp(block.timestamp + rngLookahead); + sortitionModule.passPhase(); // Drawing phase + + core.draw(disputeID, DEFAULT_NB_OF_JURORS); + vm.warp(block.timestamp + timesPerPeriod[0]); + core.passPeriod(disputeID); // Vote + + uint256[] memory voteIDs = new uint256[](3); + voteIDs[0] = 0; + voteIDs[1] = 1; + voteIDs[2] = 2; + + vm.prank(staker1); + disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); + core.passPeriod(disputeID); // Appeal + + vm.expectRevert(KlerosCoreBase.NotExecutionPeriod.selector); + core.executeRuling(disputeID); + + vm.warp(block.timestamp + timesPerPeriod[3]); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.execution); + core.passPeriod(disputeID); // Execution + + (, , KlerosCoreBase.Period period, , uint256 lastPeriodChange) = core.disputes(disputeID); + assertEq(uint256(period), uint256(KlerosCoreBase.Period.execution), "Wrong period"); + assertEq(lastPeriodChange, block.timestamp, "Wrong lastPeriodChange"); + + vm.expectRevert(KlerosCoreBase.DisputePeriodIsFinal.selector); + core.passPeriod(disputeID); + + vm.expectEmit(true, true, true, true); + emit IArbitratorV2.Ruling(arbitrable, disputeID, 2); // Winning choice = 2 + vm.expectEmit(true, true, true, true); + emit IArbitrableV2.Ruling(core, disputeID, 2); + core.executeRuling(disputeID); + + vm.expectRevert(KlerosCoreBase.RulingAlreadyExecuted.selector); + core.executeRuling(disputeID); + + (, , , bool ruled, ) = core.disputes(disputeID); + assertEq(ruled, true, "Should be ruled"); + } + + function test_executeRuling_appealSwitch() public { + // Check that the ruling switches if only one side was funded + uint256 disputeID = 0; + + 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 + vm.warp(block.timestamp + rngLookahead); + sortitionModule.passPhase(); // Drawing phase + + core.draw(disputeID, DEFAULT_NB_OF_JURORS); + vm.warp(block.timestamp + timesPerPeriod[0]); + core.passPeriod(disputeID); // Vote + + uint256[] memory voteIDs = new uint256[](3); + voteIDs[0] = 0; + voteIDs[1] = 1; + voteIDs[2] = 2; + + vm.prank(staker1); + disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); + core.passPeriod(disputeID); // Appeal + + vm.prank(crowdfunder1); + disputeKit.fundAppeal{value: 0.63 ether}(disputeID, 1); // Fund the losing choice + + vm.warp(block.timestamp + timesPerPeriod[3]); + core.passPeriod(disputeID); // Execution + + vm.expectEmit(true, true, true, true); + emit IArbitratorV2.Ruling(arbitrable, disputeID, 1); // Winning choice is switched to 1 + vm.expectEmit(true, true, true, true); + emit IArbitrableV2.Ruling(core, disputeID, 1); + core.executeRuling(disputeID); + + (uint256 ruling, bool tied, bool overridden) = disputeKit.currentRuling(disputeID); + assertEq(ruling, 1, "Wrong ruling"); + assertEq(tied, false, "Not tied"); + assertEq(overridden, true, "Should be overridden"); + } + + function test_withdrawFeesAndRewards() public { + uint256 disputeID = 0; + + 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 + vm.warp(block.timestamp + rngLookahead); + sortitionModule.passPhase(); // Drawing phase + + core.draw(disputeID, DEFAULT_NB_OF_JURORS); + vm.warp(block.timestamp + timesPerPeriod[0]); + core.passPeriod(disputeID); // Vote + + uint256[] memory voteIDs = new uint256[](3); + voteIDs[0] = 0; + voteIDs[1] = 1; + voteIDs[2] = 2; + + vm.prank(staker1); + disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); + core.passPeriod(disputeID); // Appeal + + vm.prank(crowdfunder1); + disputeKit.fundAppeal{value: 0.63 ether}(disputeID, 1); // Fund the losing choice. The ruling will be overridden here + vm.prank(crowdfunder2); + disputeKit.fundAppeal{value: 0.41 ether}(disputeID, 2); // Underpay a bit to not create an appeal and withdraw the funded sum fully + + vm.warp(block.timestamp + timesPerPeriod[3]); + core.passPeriod(disputeID); // Execution + + vm.expectRevert(DisputeKitClassicBase.DisputeNotResolved.selector); + disputeKit.withdrawFeesAndRewards(disputeID, payable(staker1), 0, 1); + + core.executeRuling(disputeID); + + vm.prank(owner); + core.pause(); + vm.expectRevert(DisputeKitClassicBase.CoreIsPaused.selector); + disputeKit.withdrawFeesAndRewards(disputeID, payable(staker1), 0, 1); + vm.prank(owner); + core.unpause(); + + assertEq(crowdfunder1.balance, 9.37 ether, "Wrong balance of the crowdfunder1"); + assertEq(crowdfunder2.balance, 9.59 ether, "Wrong balance of the crowdfunder2"); + assertEq(address(disputeKit).balance, 1.04 ether, "Wrong balance of the DK"); + + vm.expectEmit(true, true, true, true); + emit DisputeKitClassicBase.Withdrawal(disputeID, 0, 1, crowdfunder1, 0.63 ether); + disputeKit.withdrawFeesAndRewards(disputeID, payable(crowdfunder1), 0, 1); + + vm.expectEmit(true, true, true, true); + emit DisputeKitClassicBase.Withdrawal(disputeID, 0, 2, crowdfunder2, 0.41 ether); + disputeKit.withdrawFeesAndRewards(disputeID, payable(crowdfunder2), 0, 2); + + assertEq(crowdfunder1.balance, 10 ether, "Wrong balance of the crowdfunder1"); + assertEq(crowdfunder2.balance, 10 ether, "Wrong balance of the crowdfunder2"); + assertEq(address(disputeKit).balance, 0, "Wrong balance of the DK"); + } +} diff --git a/contracts/test/foundry/KlerosCore_Governance.t.sol b/contracts/test/foundry/KlerosCore_Governance.t.sol new file mode 100644 index 000000000..f34d64e79 --- /dev/null +++ b/contracts/test/foundry/KlerosCore_Governance.t.sol @@ -0,0 +1,472 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {KlerosCore_TestBase} from "./KlerosCore_TestBase.sol"; +import {KlerosCoreBase} from "../../src/arbitration/KlerosCoreBase.sol"; +import {IArbitratorV2} from "../../src/arbitration/KlerosCoreBase.sol"; +import {DisputeKitSybilResistant} from "../../src/arbitration/dispute-kits/DisputeKitSybilResistant.sol"; +import {SortitionModuleMock} from "../../src/test/SortitionModuleMock.sol"; +import {PNK} from "../../src/token/PNK.sol"; +import "../../src/libraries/Constants.sol"; + +/// @title KlerosCore_GovernanceTest +/// @dev Tests for KlerosCore governance functions (owner/guardian operations) +contract KlerosCore_GovernanceTest is KlerosCore_TestBase { + function test_pause() public { + vm.expectRevert(KlerosCoreBase.GuardianOrOwnerOnly.selector); + vm.prank(other); + core.pause(); + // Note that we must explicitly switch to the owner/guardian address to make the call, otherwise Foundry treats UUPS proxy as msg.sender. + vm.prank(guardian); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.Paused(); + core.pause(); + assertEq(core.paused(), true, "Wrong paused value"); + // Switch between owner and guardian to test both. WhenNotPausedOnly modifier is triggered after owner's check. + vm.prank(owner); + vm.expectRevert(KlerosCoreBase.WhenNotPausedOnly.selector); + core.pause(); + } + + function test_unpause() public { + vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); + vm.prank(other); + core.unpause(); + + vm.expectRevert(KlerosCoreBase.WhenPausedOnly.selector); + vm.prank(owner); + core.unpause(); + + vm.prank(owner); + core.pause(); + vm.prank(owner); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.Unpaused(); + core.unpause(); + assertEq(core.paused(), false, "Wrong paused value"); + } + + function test_executeOwnerProposal() public { + bytes memory data = abi.encodeWithSignature("changeOwner(address)", other); + vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); + vm.prank(other); + core.executeOwnerProposal(address(core), 0, data); + + vm.expectRevert(KlerosCoreBase.UnsuccessfulCall.selector); + vm.prank(owner); + core.executeOwnerProposal(address(core), 0, data); // It'll fail because the core is not its own owner + + vm.prank(owner); + core.changeOwner(payable(address(core))); + vm.prank(address(core)); + core.executeOwnerProposal(address(core), 0, data); + assertEq(core.owner(), other, "Wrong owner"); + } + + function test_changeOwner() public { + vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); + vm.prank(other); + core.changeOwner(payable(other)); + vm.prank(owner); + core.changeOwner(payable(other)); + assertEq(core.owner(), other, "Wrong owner"); + } + + function test_changeGuardian() public { + vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); + vm.prank(other); + core.changeGuardian(other); + vm.prank(owner); + core.changeGuardian(other); + assertEq(core.guardian(), other, "Wrong guardian"); + } + + function test_changePinakion() public { + PNK fakePNK = new PNK(); + vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); + vm.prank(other); + core.changePinakion(fakePNK); + vm.prank(owner); + core.changePinakion(fakePNK); + assertEq(address(core.pinakion()), address(fakePNK), "Wrong PNK"); + } + + function test_changeJurorProsecutionModule() public { + vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); + vm.prank(other); + core.changeJurorProsecutionModule(other); + vm.prank(owner); + core.changeJurorProsecutionModule(other); + assertEq(core.jurorProsecutionModule(), other, "Wrong jurorProsecutionModule"); + } + + function test_changeSortitionModule() public { + SortitionModuleMock fakeSM = new SortitionModuleMock(); + vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); + vm.prank(other); + core.changeSortitionModule(fakeSM); + vm.prank(owner); + core.changeSortitionModule(fakeSM); + assertEq(address(core.sortitionModule()), address(fakeSM), "Wrong sortitionModule"); + } + + function test_addNewDisputeKit() public { + DisputeKitSybilResistant newDK = new DisputeKitSybilResistant(); + vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); + vm.prank(other); + core.addNewDisputeKit(newDK); + vm.prank(owner); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.DisputeKitCreated(2, newDK); + core.addNewDisputeKit(newDK); + assertEq(address(core.disputeKits(2)), address(newDK), "Wrong address of new DK"); + assertEq(core.getDisputeKitsLength(), 3, "Wrong DK array length"); + } + + function test_createCourt() public { + vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); + vm.prank(other); + uint256[] memory supportedDK = new uint256[](2); + supportedDK[0] = DISPUTE_KIT_CLASSIC; + supportedDK[1] = 2; // New DK is added below. + core.createCourt( + GENERAL_COURT, + true, // Hidden votes + 2000, // min stake + 10000, // alpha + 0.03 ether, // fee for juror + 50, // jurors for jump + [uint256(10), uint256(20), uint256(30), uint256(40)], // Times per period + abi.encode(uint256(4)), // Sortition extra data + supportedDK + ); + + vm.expectRevert(KlerosCoreBase.MinStakeLowerThanParentCourt.selector); + vm.prank(owner); + core.createCourt( + GENERAL_COURT, + true, // Hidden votes + 800, // min stake + 10000, // alpha + 0.03 ether, // fee for juror + 50, // jurors for jump + [uint256(10), uint256(20), uint256(30), uint256(40)], // Times per period + abi.encode(uint256(4)), // Sortition extra data + supportedDK + ); + + vm.expectRevert(KlerosCoreBase.UnsupportedDisputeKit.selector); + vm.prank(owner); + uint256[] memory emptySupportedDK = new uint256[](0); + core.createCourt( + GENERAL_COURT, + true, // Hidden votes + 2000, // min stake + 10000, // alpha + 0.03 ether, // fee for juror + 50, // jurors for jump + [uint256(10), uint256(20), uint256(30), uint256(40)], // Times per period + abi.encode(uint256(4)), // Sortition extra data + emptySupportedDK + ); + + vm.expectRevert(KlerosCoreBase.InvalidForkingCourtAsParent.selector); + vm.prank(owner); + core.createCourt( + FORKING_COURT, + true, // Hidden votes + 2000, // min stake + 10000, // alpha + 0.03 ether, // fee for juror + 50, // jurors for jump + [uint256(10), uint256(20), uint256(30), uint256(40)], // Times per period + abi.encode(uint256(4)), // Sortition extra data + supportedDK + ); + + uint256[] memory badSupportedDK = new uint256[](2); + badSupportedDK[0] = NULL_DISPUTE_KIT; // Include NULL_DK to check that it reverts + badSupportedDK[1] = DISPUTE_KIT_CLASSIC; + vm.expectRevert(KlerosCoreBase.WrongDisputeKitIndex.selector); + vm.prank(owner); + core.createCourt( + GENERAL_COURT, + true, // Hidden votes + 2000, // min stake + 10000, // alpha + 0.03 ether, // fee for juror + 50, // jurors for jump + [uint256(10), uint256(20), uint256(30), uint256(40)], // Times per period + abi.encode(uint256(4)), // Sortition extra data + badSupportedDK + ); + + badSupportedDK[0] = DISPUTE_KIT_CLASSIC; + badSupportedDK[1] = 2; // Check out of bounds index + vm.expectRevert(KlerosCoreBase.WrongDisputeKitIndex.selector); + vm.prank(owner); + core.createCourt( + GENERAL_COURT, + true, // Hidden votes + 2000, // min stake + 10000, // alpha + 0.03 ether, // fee for juror + 50, // jurors for jump + [uint256(10), uint256(20), uint256(30), uint256(40)], // Times per period + abi.encode(uint256(4)), // Sortition extra data + badSupportedDK + ); + + // Add new DK to check the requirement for classic DK + DisputeKitSybilResistant newDK = new DisputeKitSybilResistant(); + vm.prank(owner); + core.addNewDisputeKit(newDK); + badSupportedDK = new uint256[](1); + badSupportedDK[0] = 2; // Include only sybil resistant dk + vm.expectRevert(KlerosCoreBase.MustSupportDisputeKitClassic.selector); + vm.prank(owner); + core.createCourt( + GENERAL_COURT, + true, // Hidden votes + 2000, // min stake + 10000, // alpha + 0.03 ether, // fee for juror + 50, // jurors for jump + [uint256(10), uint256(20), uint256(30), uint256(40)], // Times per period + abi.encode(uint256(4)), // Sortition extra data + badSupportedDK + ); + + vm.prank(owner); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.DisputeKitEnabled(2, DISPUTE_KIT_CLASSIC, true); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.DisputeKitEnabled(2, 2, true); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.CourtCreated( + 2, + GENERAL_COURT, + true, + 2000, + 20000, + 0.04 ether, + 50, + [uint256(10), uint256(20), uint256(30), uint256(40)], // Explicitly convert otherwise it throws + supportedDK + ); + core.createCourt( + GENERAL_COURT, + true, // Hidden votes + 2000, // min stake + 20000, // alpha + 0.04 ether, // fee for juror + 50, // jurors for jump + [uint256(10), uint256(20), uint256(30), uint256(40)], // Times per period + abi.encode(uint256(4)), // Sortition extra data + supportedDK + ); + + _assertCourtParameters(2, GENERAL_COURT, true, 2000, 20000, 0.04 ether, 50, false); + + uint256[] memory children = core.getCourtChildren(2); + assertEq(children.length, 0, "No children"); + _assertTimesPerPeriod(2, [uint256(10), uint256(20), uint256(30), uint256(40)]); + + children = core.getCourtChildren(GENERAL_COURT); // Check that parent updated children + assertEq(children.length, 1, "Wrong children count"); + assertEq(children[0], 2, "Wrong child id"); + + (uint256 K, uint256 nodeLength) = sortitionModule.getSortitionProperties(bytes32(uint256(2))); + assertEq(K, 4, "Wrong tree K of the new court"); + assertEq(nodeLength, 1, "Wrong node length for created tree of the new court"); + } + + function test_changeCourtParameters() public { + // Create a 2nd court to check the minStake requirements + vm.prank(owner); + uint96 newCourtID = 2; + uint256[] memory supportedDK = new uint256[](1); + supportedDK[0] = DISPUTE_KIT_CLASSIC; + core.createCourt( + GENERAL_COURT, + true, // Hidden votes + 2000, // min stake + 20000, // alpha + 0.04 ether, // fee for juror + 50, // jurors for jump + [uint256(10), uint256(20), uint256(30), uint256(40)], // Times per period + abi.encode(uint256(4)), // Sortition extra data + supportedDK + ); + + vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); + vm.prank(other); + core.changeCourtParameters( + GENERAL_COURT, + true, // Hidden votes + 2000, // min stake + 10000, // alpha + 0.03 ether, // fee for juror + 50, // jurors for jump + [uint256(10), uint256(20), uint256(30), uint256(40)] // Times per period + ); + vm.expectRevert(KlerosCoreBase.MinStakeLowerThanParentCourt.selector); + vm.prank(owner); + // Min stake of a parent became higher than of a child + core.changeCourtParameters( + GENERAL_COURT, + true, // Hidden votes + 3000, // min stake + 10000, // alpha + 0.03 ether, // fee for juror + 50, // jurors for jump + [uint256(10), uint256(20), uint256(30), uint256(40)] // Times per period + ); + // Min stake of a child became lower than of a parent + vm.expectRevert(KlerosCoreBase.MinStakeLowerThanParentCourt.selector); + vm.prank(owner); + core.changeCourtParameters( + newCourtID, + true, // Hidden votes + 800, // min stake + 10000, // alpha + 0.03 ether, // fee for juror + 50, // jurors for jump + [uint256(10), uint256(20), uint256(30), uint256(40)] // Times per period + ); + + vm.prank(owner); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.CourtModified( + GENERAL_COURT, + true, + 2000, + 20000, + 0.04 ether, + 50, + [uint256(10), uint256(20), uint256(30), uint256(40)] // Explicitly convert otherwise it throws + ); + core.changeCourtParameters( + GENERAL_COURT, + true, // Hidden votes + 2000, // min stake + 20000, // alpha + 0.04 ether, // fee for juror + 50, // jurors for jump + [uint256(10), uint256(20), uint256(30), uint256(40)] // Times per period + ); + + _assertCourtParameters(GENERAL_COURT, FORKING_COURT, true, 2000, 20000, 0.04 ether, 50, false); + _assertTimesPerPeriod(GENERAL_COURT, [uint256(10), uint256(20), uint256(30), uint256(40)]); + } + + function test_enableDisputeKits() public { + DisputeKitSybilResistant newDK = new DisputeKitSybilResistant(); + uint256 newDkID = 2; + vm.prank(owner); + core.addNewDisputeKit(newDK); + + vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); + vm.prank(other); + uint256[] memory supportedDK = new uint256[](1); + supportedDK[0] = newDkID; + core.enableDisputeKits(GENERAL_COURT, supportedDK, true); + + vm.expectRevert(KlerosCoreBase.WrongDisputeKitIndex.selector); + vm.prank(owner); + supportedDK[0] = NULL_DISPUTE_KIT; + core.enableDisputeKits(GENERAL_COURT, supportedDK, true); + + vm.expectRevert(KlerosCoreBase.WrongDisputeKitIndex.selector); + vm.prank(owner); + supportedDK[0] = 3; // Out of bounds + core.enableDisputeKits(GENERAL_COURT, supportedDK, true); + + vm.expectRevert(KlerosCoreBase.CannotDisableClassicDK.selector); + vm.prank(owner); + supportedDK[0] = DISPUTE_KIT_CLASSIC; + core.enableDisputeKits(GENERAL_COURT, supportedDK, false); + + vm.prank(owner); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.DisputeKitEnabled(GENERAL_COURT, newDkID, true); + supportedDK[0] = newDkID; + core.enableDisputeKits(GENERAL_COURT, supportedDK, true); + assertEq(core.isSupported(GENERAL_COURT, newDkID), true, "New DK should be supported by General court"); + + vm.prank(owner); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.DisputeKitEnabled(GENERAL_COURT, newDkID, false); + core.enableDisputeKits(GENERAL_COURT, supportedDK, false); + assertEq(core.isSupported(GENERAL_COURT, newDkID), false, "New DK should be disabled in General court"); + } + + function test_changeAcceptedFeeTokens() public { + vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); + vm.prank(other); + core.changeAcceptedFeeTokens(feeToken, true); + + (bool accepted, , ) = core.currencyRates(feeToken); + assertEq(accepted, false, "Token should not be accepted yet"); + + vm.prank(owner); + vm.expectEmit(true, true, true, true); + emit IArbitratorV2.AcceptedFeeToken(feeToken, true); + core.changeAcceptedFeeTokens(feeToken, true); + (accepted, , ) = core.currencyRates(feeToken); + assertEq(accepted, true, "Token should be accepted"); + } + + function test_changeCurrencyRates() public { + vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); + vm.prank(other); + core.changeCurrencyRates(feeToken, 100, 200); + + (, uint256 rateInEth, uint256 rateDecimals) = core.currencyRates(feeToken); + assertEq(rateInEth, 0, "rateInEth should be 0"); + assertEq(rateDecimals, 0, "rateDecimals should be 0"); + + vm.prank(owner); + vm.expectEmit(true, true, true, true); + emit IArbitratorV2.NewCurrencyRate(feeToken, 100, 200); + core.changeCurrencyRates(feeToken, 100, 200); + + (, rateInEth, rateDecimals) = core.currencyRates(feeToken); + assertEq(rateInEth, 100, "rateInEth is incorrect"); + assertEq(rateDecimals, 200, "rateDecimals is incorrect"); + } + + function test_extraDataToCourtIDMinJurorsDisputeKit() public { + // Standard values + bytes memory extraData = abi.encodePacked(uint256(GENERAL_COURT), DEFAULT_NB_OF_JURORS, DISPUTE_KIT_CLASSIC); + + (uint96 courtID, uint256 minJurors, uint256 disputeKitID) = core.extraDataToCourtIDMinJurorsDisputeKit( + extraData + ); + assertEq(courtID, GENERAL_COURT, "Wrong courtID"); + assertEq(minJurors, DEFAULT_NB_OF_JURORS, "Wrong minJurors"); + assertEq(disputeKitID, DISPUTE_KIT_CLASSIC, "Wrong disputeKitID"); + + // Botched extraData. Values should fall into standard + extraData = "0xfa"; + + (courtID, minJurors, disputeKitID) = core.extraDataToCourtIDMinJurorsDisputeKit(extraData); + assertEq(courtID, GENERAL_COURT, "Wrong courtID"); + assertEq(minJurors, DEFAULT_NB_OF_JURORS, "Wrong minJurors"); + assertEq(disputeKitID, DISPUTE_KIT_CLASSIC, "Wrong disputeKitID"); + + // Custom values. + vm.startPrank(owner); + core.addNewDisputeKit(disputeKit); + core.addNewDisputeKit(disputeKit); + core.addNewDisputeKit(disputeKit); + core.addNewDisputeKit(disputeKit); + core.addNewDisputeKit(disputeKit); + extraData = abi.encodePacked(uint256(50), uint256(41), uint256(6)); + + (courtID, minJurors, disputeKitID) = core.extraDataToCourtIDMinJurorsDisputeKit(extraData); + assertEq(courtID, GENERAL_COURT, "Wrong courtID"); // Value in extra data is out of scope so fall back + assertEq(minJurors, 41, "Wrong minJurors"); + assertEq(disputeKitID, 6, "Wrong disputeKitID"); + } +} diff --git a/contracts/test/foundry/KlerosCore_Initialization.t.sol b/contracts/test/foundry/KlerosCore_Initialization.t.sol new file mode 100644 index 000000000..76711351e --- /dev/null +++ b/contracts/test/foundry/KlerosCore_Initialization.t.sol @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {KlerosCore_TestBase} from "./KlerosCore_TestBase.sol"; +import {KlerosCoreBase} from "../../src/arbitration/KlerosCoreBase.sol"; +import {KlerosCoreMock} from "../../src/test/KlerosCoreMock.sol"; +import {DisputeKitClassic} from "../../src/arbitration/dispute-kits/DisputeKitClassic.sol"; +import {SortitionModuleMock} from "../../src/test/SortitionModuleMock.sol"; +import {UUPSProxy} from "../../src/proxy/UUPSProxy.sol"; +import {BlockHashRNG} from "../../src/rng/BlockHashRNG.sol"; +import {ISortitionModule} from "../../src/arbitration/interfaces/ISortitionModule.sol"; +import {PNK} from "../../src/token/PNK.sol"; +import "../../src/libraries/Constants.sol"; + +/// @title KlerosCore_InitializationTest +/// @dev Tests for KlerosCore initialization and basic configuration +contract KlerosCore_InitializationTest is KlerosCore_TestBase { + function test_initialize() public { + assertEq(core.owner(), msg.sender, "Wrong owner"); + assertEq(core.guardian(), guardian, "Wrong guardian"); + assertEq(address(core.pinakion()), address(pinakion), "Wrong pinakion address"); + assertEq(core.jurorProsecutionModule(), jurorProsecutionModule, "Wrong jurorProsecutionModule address"); + assertEq(address(core.sortitionModule()), address(sortitionModule), "Wrong sortitionModule address"); + assertEq(core.getDisputeKitsLength(), 2, "Wrong DK array length"); + + _assertCourtParameters(FORKING_COURT, FORKING_COURT, false, 0, 0, 0, 0, false); + _assertCourtParameters(GENERAL_COURT, FORKING_COURT, false, 1000, 10000, 0.03 ether, 511, false); + + uint256[] memory children = core.getCourtChildren(GENERAL_COURT); + assertEq(children.length, 0, "No children"); + _assertTimesPerPeriod(GENERAL_COURT, timesPerPeriod); + + assertEq(address(core.disputeKits(NULL_DISPUTE_KIT)), address(0), "Wrong address NULL_DISPUTE_KIT"); + assertEq( + address(core.disputeKits(DISPUTE_KIT_CLASSIC)), + address(disputeKit), + "Wrong address DISPUTE_KIT_CLASSIC" + ); + assertEq(core.isSupported(FORKING_COURT, NULL_DISPUTE_KIT), false, "Forking court null dk should be false"); + assertEq( + core.isSupported(FORKING_COURT, DISPUTE_KIT_CLASSIC), + false, + "Forking court classic dk should be false" + ); + assertEq(core.isSupported(GENERAL_COURT, NULL_DISPUTE_KIT), false, "General court null dk should be false"); + assertEq(core.isSupported(GENERAL_COURT, DISPUTE_KIT_CLASSIC), true, "General court classic dk should be true"); + assertEq(core.paused(), false, "Wrong paused value"); + + assertEq(pinakion.name(), "Pinakion", "Wrong token name"); + assertEq(pinakion.symbol(), "PNK", "Wrong token symbol"); + assertEq(pinakion.totalSupply(), 1000000 ether, "Wrong total supply"); + assertEq(pinakion.balanceOf(msg.sender), 999998 ether, "Wrong token balance of owner"); + assertEq(pinakion.balanceOf(staker1), 1 ether, "Wrong token balance of staker1"); + assertEq(pinakion.allowance(staker1, address(core)), 1 ether, "Wrong allowance for staker1"); + assertEq(pinakion.balanceOf(staker2), 1 ether, "Wrong token balance of staker2"); + assertEq(pinakion.allowance(staker2, address(core)), 1 ether, "Wrong allowance for staker2"); + + assertEq(disputeKit.owner(), msg.sender, "Wrong DK owner"); + assertEq(address(disputeKit.core()), address(core), "Wrong core in DK"); + + assertEq(sortitionModule.owner(), msg.sender, "Wrong SM owner"); + assertEq(address(sortitionModule.core()), address(core), "Wrong core in SM"); + assertEq(uint256(sortitionModule.phase()), uint256(ISortitionModule.Phase.staking), "Phase should be 0"); + assertEq(sortitionModule.minStakingTime(), 18, "Wrong minStakingTime"); + assertEq(sortitionModule.maxDrawingTime(), 24, "Wrong maxDrawingTime"); + assertEq(sortitionModule.lastPhaseChange(), block.timestamp, "Wrong lastPhaseChange"); + 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.delayedStakeWriteIndex(), 0, "delayedStakeWriteIndex should be 0"); + assertEq(sortitionModule.delayedStakeReadIndex(), 1, "Wrong delayedStakeReadIndex"); + + (uint256 K, uint256 nodeLength) = sortitionModule.getSortitionProperties(bytes32(uint256(FORKING_COURT))); + assertEq(K, 5, "Wrong tree K FORKING_COURT"); + assertEq(nodeLength, 1, "Wrong node length for created tree FORKING_COURT"); + + (K, nodeLength) = sortitionModule.getSortitionProperties(bytes32(uint256(GENERAL_COURT))); + assertEq(K, 5, "Wrong tree K GENERAL_COURT"); + assertEq(nodeLength, 1, "Wrong node length for created tree GENERAL_COURT"); + } + + function test_initialize_events() public { + KlerosCoreMock coreLogic = new KlerosCoreMock(); + SortitionModuleMock smLogic = new SortitionModuleMock(); + DisputeKitClassic dkLogic = new DisputeKitClassic(); + PNK newPinakion = new PNK(); + + address newOwner = msg.sender; + address newGuardian = vm.addr(1); + address newStaker1 = vm.addr(2); + address newOther = vm.addr(9); + address newJurorProsecutionModule = vm.addr(8); + uint256 newMinStake = 1000; + uint256 newAlpha = 10000; + uint256 newFeeForJuror = 0.03 ether; + uint256 newJurorsForCourtJump = 511; + uint256[4] memory newTimesPerPeriod = [uint256(60), uint256(120), uint256(180), uint256(240)]; + + newPinakion.transfer(msg.sender, totalSupply - 1 ether); + newPinakion.transfer(newStaker1, 1 ether); + + bytes memory newSortitionExtraData = abi.encode(uint256(5)); + uint256 newMinStakingTime = 18; + uint256 newMaxDrawingTime = 24; + bool newHiddenVotes = false; + + uint256 newRngLookahead = 20; + BlockHashRNG newRng = new BlockHashRNG(msg.sender, address(sortitionModule), newRngLookahead); + + UUPSProxy proxyCore = new UUPSProxy(address(coreLogic), ""); + + bytes memory initDataDk = abi.encodeWithSignature( + "initialize(address,address,address)", + newOwner, + address(proxyCore), + address(wNative) + ); + + UUPSProxy proxyDk = new UUPSProxy(address(dkLogic), initDataDk); + DisputeKitClassic newDisputeKit = DisputeKitClassic(address(proxyDk)); + + bytes memory initDataSm = abi.encodeWithSignature( + "initialize(address,address,uint256,uint256,address)", + newOwner, + address(proxyCore), + newMinStakingTime, + newMaxDrawingTime, + newRng + ); + + UUPSProxy proxySm = new UUPSProxy(address(smLogic), initDataSm); + SortitionModuleMock newSortitionModule = SortitionModuleMock(address(proxySm)); + vm.prank(newOwner); + newRng.changeConsumer(address(newSortitionModule)); + + KlerosCoreMock newCore = KlerosCoreMock(address(proxyCore)); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.DisputeKitCreated(DISPUTE_KIT_CLASSIC, newDisputeKit); + vm.expectEmit(true, true, true, true); + + uint256[] memory supportedDK = new uint256[](1); + supportedDK[0] = DISPUTE_KIT_CLASSIC; + emit KlerosCoreBase.CourtCreated( + GENERAL_COURT, + FORKING_COURT, + false, + 1000, + 10000, + 0.03 ether, + 511, + [uint256(60), uint256(120), uint256(180), uint256(240)], // Explicitly convert otherwise it throws + supportedDK + ); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.DisputeKitEnabled(GENERAL_COURT, DISPUTE_KIT_CLASSIC, true); + newCore.initialize( + newOwner, + newGuardian, + newPinakion, + newJurorProsecutionModule, + newDisputeKit, + newHiddenVotes, + [newMinStake, newAlpha, newFeeForJuror, newJurorsForCourtJump], + newTimesPerPeriod, + newSortitionExtraData, + newSortitionModule, + address(wNative) + ); + } +} diff --git a/contracts/test/foundry/KlerosCore_RNG.t.sol b/contracts/test/foundry/KlerosCore_RNG.t.sol new file mode 100644 index 000000000..da3df03ed --- /dev/null +++ b/contracts/test/foundry/KlerosCore_RNG.t.sol @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {KlerosCore_TestBase} from "./KlerosCore_TestBase.sol"; +import {SortitionModuleBase} from "../../src/arbitration/SortitionModuleBase.sol"; +import {RNGWithFallback, IRNG} from "../../src/rng/RNGWithFallback.sol"; +import {RNGMock} from "../../src/test/RNGMock.sol"; +import "../../src/libraries/Constants.sol"; + +/// @title KlerosCore_RNGTest +/// @dev Tests for KlerosCore random number generation and fallback mechanisms +contract KlerosCore_RNGTest is KlerosCore_TestBase { + function test_RNGFallback() public { + RNGWithFallback rngFallback; + uint256 fallbackTimeout = 100; + RNGMock rngMock = new RNGMock(); + rngFallback = new RNGWithFallback(msg.sender, address(sortitionModule), fallbackTimeout, rngMock); + assertEq(rngFallback.owner(), msg.sender, "Wrong owner"); + 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(owner); + 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(owner); + 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(IRNG.ConsumerOnly.selector); + vm.prank(owner); + rngFallback.requestRandomness(); + + vm.expectRevert(IRNG.ConsumerOnly.selector); + vm.prank(owner); + rngFallback.receiveRandomness(); + + vm.expectRevert(IRNG.OwnerOnly.selector); + vm.prank(other); + rngFallback.changeOwner(other); + vm.prank(owner); + rngFallback.changeOwner(other); + assertEq(rngFallback.owner(), other, "Wrong owner"); + + // Change owner back for convenience + vm.prank(other); + rngFallback.changeOwner(owner); + + vm.expectRevert(IRNG.OwnerOnly.selector); + vm.prank(other); + rngFallback.changeConsumer(other); + vm.prank(owner); + rngFallback.changeConsumer(other); + assertEq(rngFallback.consumer(), other, "Wrong consumer"); + + vm.expectRevert(IRNG.OwnerOnly.selector); + vm.prank(other); + rngFallback.changeFallbackTimeout(5); + + vm.prank(owner); + vm.expectEmit(true, true, true, true); + emit RNGWithFallback.FallbackTimeoutChanged(5); + rngFallback.changeFallbackTimeout(5); + assertEq(rngFallback.fallbackTimeoutSeconds(), 5, "Wrong fallback timeout"); + } +} diff --git a/contracts/test/foundry/KlerosCore_Staking.t.sol b/contracts/test/foundry/KlerosCore_Staking.t.sol new file mode 100644 index 000000000..a315debcf --- /dev/null +++ b/contracts/test/foundry/KlerosCore_Staking.t.sol @@ -0,0 +1,450 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {KlerosCore_TestBase} from "./KlerosCore_TestBase.sol"; +import {KlerosCoreBase} from "../../src/arbitration/KlerosCoreBase.sol"; +import {SortitionModuleBase} from "../../src/arbitration/SortitionModuleBase.sol"; +import {ISortitionModule} from "../../src/arbitration/interfaces/ISortitionModule.sol"; +import {IKlerosCore, KlerosCoreSnapshotProxy} from "../../src/arbitration/view/KlerosCoreSnapshotProxy.sol"; +import "../../src/libraries/Constants.sol"; + +/// @title KlerosCore_StakingTest +/// @dev Tests for KlerosCore staking mechanics and stake management +contract KlerosCore_StakingTest is KlerosCore_TestBase { + function test_setStake_increase() public { + vm.prank(owner); + core.pause(); + vm.expectRevert(KlerosCoreBase.WhenNotPausedOnly.selector); + vm.prank(staker1); + core.setStake(GENERAL_COURT, 1000); + vm.prank(owner); + core.unpause(); + + vm.expectRevert(KlerosCoreBase.StakingNotPossibleInThisCourt.selector); + vm.prank(staker1); + core.setStake(FORKING_COURT, 1000); + + uint96 badCourtID = 2; + vm.expectRevert(KlerosCoreBase.StakingNotPossibleInThisCourt.selector); + vm.prank(staker1); + core.setStake(badCourtID, 1000); + + vm.expectRevert(KlerosCoreBase.StakingLessThanCourtMinStake.selector); + vm.prank(staker1); + core.setStake(GENERAL_COURT, 800); + + vm.expectRevert(KlerosCoreBase.StakingZeroWhenNoStake.selector); + vm.prank(staker1); + core.setStake(GENERAL_COURT, 0); + + vm.prank(staker1); + vm.expectEmit(true, true, true, true); + emit SortitionModuleBase.StakeSet(staker1, GENERAL_COURT, 1001, 1001); + core.setStake(GENERAL_COURT, 1001); + + (uint256 totalStaked, uint256 totalLocked, uint256 stakedInCourt, uint256 nbCourts) = sortitionModule + .getJurorBalance(staker1, GENERAL_COURT); + assertEq(totalStaked, 1001, "Wrong amount total staked"); + assertEq(totalLocked, 0, "Wrong amount locked"); + assertEq(stakedInCourt, 1001, "Wrong amount staked in court"); + assertEq(nbCourts, 1, "Wrong number of courts"); + + uint96[] memory courts = sortitionModule.getJurorCourtIDs(staker1); + assertEq(courts.length, 1, "Wrong courts count"); + assertEq(courts[0], GENERAL_COURT, "Wrong court id"); + assertEq(sortitionModule.isJurorStaked(staker1), true, "Juror should be staked"); + + assertEq(pinakion.balanceOf(address(core)), 1001, "Wrong token balance of the core"); + assertEq(pinakion.balanceOf(staker1), 999999999999998999, "Wrong token balance of staker1"); // 1 eth - 1001 wei + assertEq(pinakion.allowance(staker1, address(core)), 999999999999998999, "Wrong allowance for staker1"); + + vm.expectRevert(KlerosCoreBase.StakingTransferFailed.selector); // This error will be caught because owner didn't approve any tokens for KlerosCore + vm.prank(owner); + core.setStake(GENERAL_COURT, 1000); + + // Increase stake one more time to verify the correct behavior + vm.prank(staker1); + vm.expectEmit(true, true, true, true); + emit SortitionModuleBase.StakeSet(staker1, GENERAL_COURT, 2000, 2000); + core.setStake(GENERAL_COURT, 2000); + + (totalStaked, totalLocked, stakedInCourt, nbCourts) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); + assertEq(totalStaked, 2000, "Wrong amount total staked"); + assertEq(totalLocked, 0, "Wrong amount locked"); + assertEq(stakedInCourt, 2000, "Wrong amount staked in court"); + assertEq(nbCourts, 1, "Number of courts should not increase"); + + assertEq(pinakion.balanceOf(address(core)), 2000, "Wrong token balance of the core"); + assertEq(pinakion.balanceOf(staker1), 999999999999998000, "Wrong token balance of staker1"); // 1 eth - 2000 wei + assertEq(pinakion.allowance(staker1, address(core)), 999999999999998000, "Wrong allowance for staker1"); + } + + function test_setStake_decrease() public { + vm.prank(staker1); + core.setStake(GENERAL_COURT, 2000); + assertEq(pinakion.balanceOf(address(core)), 2000, "Wrong token balance of the core"); + assertEq(pinakion.balanceOf(staker1), 999999999999998000, "Wrong token balance of staker1"); + assertEq(pinakion.allowance(staker1, address(core)), 999999999999998000, "Wrong allowance for staker1"); + + vm.prank(staker1); + core.setStake(GENERAL_COURT, 1500); // Decrease the stake to see if it's reflected correctly + (uint256 totalStaked, uint256 totalLocked, uint256 stakedInCourt, uint256 nbCourts) = sortitionModule + .getJurorBalance(staker1, GENERAL_COURT); + assertEq(totalStaked, 1500, "Wrong amount total staked"); + assertEq(totalLocked, 0, "Wrong amount locked"); + assertEq(stakedInCourt, 1500, "Wrong amount staked in court"); + assertEq(nbCourts, 1, "Wrong number of courts"); + + uint96[] memory courts = sortitionModule.getJurorCourtIDs(staker1); + assertEq(courts.length, 1, "Wrong courts count"); + assertEq(courts[0], GENERAL_COURT, "Wrong court id"); + assertEq(sortitionModule.isJurorStaked(staker1), true, "Juror should be staked"); + + assertEq(pinakion.balanceOf(address(core)), 1500, "Wrong token balance of the core"); + assertEq(pinakion.balanceOf(staker1), 999999999999998500, "Wrong token balance of staker1"); + assertEq( + pinakion.allowance(staker1, address(core)), + 999999999999998000, + "Allowance should not change during withdrawal" + ); + + vm.prank(address(core)); + pinakion.transfer(staker1, 1); // Manually send 1 token to make the withdrawal fail + + vm.expectRevert(KlerosCoreBase.UnstakingTransferFailed.selector); + vm.prank(staker1); + core.setStake(GENERAL_COURT, 0); + + vm.prank(address(staker1)); + pinakion.transfer(address(core), 1); // Manually give the token back + vm.prank(staker1); + core.setStake(GENERAL_COURT, 0); + + (totalStaked, totalLocked, stakedInCourt, nbCourts) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); + assertEq(totalStaked, 0, "Wrong amount total staked"); + assertEq(totalLocked, 0, "Wrong amount locked"); + assertEq(stakedInCourt, 0, "Wrong amount staked in court"); + assertEq(nbCourts, 0, "Wrong number of courts"); + + courts = sortitionModule.getJurorCourtIDs(staker1); + assertEq(courts.length, 0, "Wrong courts count"); + assertEq(sortitionModule.isJurorStaked(staker1), false, "Juror should not be staked"); + + assertEq(pinakion.balanceOf(address(core)), 0, "Wrong token balance of the core"); + assertEq(pinakion.balanceOf(staker1), 1 ether, "Wrong token balance of staker1"); + assertEq( + pinakion.allowance(staker1, address(core)), + 999999999999998000, + "Allowance should not change during withdrawal" + ); + } + + function test_setStake_maxStakePathCheck() public { + uint256[] memory supportedDK = new uint256[](1); + supportedDK[0] = DISPUTE_KIT_CLASSIC; + + // Create 4 courts to check the require + for (uint96 i = GENERAL_COURT; i <= 4; i++) { + vm.prank(owner); + core.createCourt( + GENERAL_COURT, + true, + 2000, + 20000, + 0.04 ether, + 50, + [uint256(10), uint256(20), uint256(30), uint256(40)], + abi.encode(uint256(4)), + supportedDK + ); + vm.prank(staker1); + core.setStake(i, 2000); + } + + uint96[] memory courts = sortitionModule.getJurorCourtIDs(staker1); + assertEq(courts.length, 4, "Wrong courts count"); + + uint96 excessiveCourtID = 5; + vm.expectRevert(KlerosCoreBase.StakingInTooManyCourts.selector); + vm.prank(staker1); + core.setStake(excessiveCourtID, 2000); + } + + function test_setStake_increaseDrawingPhase() public { + // Set the stake and create a dispute to advance the phase + vm.prank(staker1); + core.setStake(GENERAL_COURT, 1000); + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + assertEq(sortitionModule.disputesWithoutJurors(), 1, "Wrong disputesWithoutJurors count"); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.warp(block.timestamp + rngLookahead); + sortitionModule.passPhase(); // Drawing phase + + assertEq(pinakion.balanceOf(address(core)), 1000, "Wrong token balance of the core"); + assertEq(pinakion.balanceOf(staker1), 999999999999999000, "Wrong token balance of staker1"); + assertEq(uint256(sortitionModule.phase()), uint256(ISortitionModule.Phase.drawing), "Wrong phase"); + + vm.prank(staker1); + vm.expectEmit(true, true, true, true); + emit SortitionModuleBase.StakeDelayed(staker1, GENERAL_COURT, 1500); + core.setStake(GENERAL_COURT, 1500); + + uint256 delayedStakeId = sortitionModule.delayedStakeWriteIndex(); + assertEq(delayedStakeId, 1, "Wrong delayedStakeWriteIndex"); + assertEq(sortitionModule.delayedStakeReadIndex(), 1, "Wrong delayedStakeReadIndex"); + (address account, uint96 courtID, uint256 stake, bool alreadyTransferred) = sortitionModule.delayedStakes( + delayedStakeId + ); + assertEq(account, staker1, "Wrong staker account"); + assertEq(courtID, GENERAL_COURT, "Wrong court id"); + assertEq(stake, 1500, "Wrong amount staked in court"); + assertEq(alreadyTransferred, false, "Should be flagged as transferred"); + + (uint256 totalStaked, uint256 totalLocked, uint256 stakedInCourt, uint256 nbCourts) = sortitionModule + .getJurorBalance(staker1, GENERAL_COURT); + assertEq(totalStaked, 1000, "Wrong amount total staked"); + assertEq(totalLocked, 0, "Wrong amount locked"); + assertEq(stakedInCourt, 1000, "Amount staked in court should not change until delayed stake is executed"); + assertEq(nbCourts, 1, "Wrong number of courts"); + + uint96[] memory courts = sortitionModule.getJurorCourtIDs(staker1); + assertEq(courts.length, 1, "Wrong courts count"); + assertEq(courts[0], GENERAL_COURT, "Wrong court id"); + assertEq(sortitionModule.isJurorStaked(staker1), true, "Juror should be staked"); + + assertEq(pinakion.balanceOf(address(core)), 1000, "Wrong token balance of the core"); + assertEq(pinakion.balanceOf(staker1), 999999999999999000, "Wrong token balance of staker1"); + } + + function test_setStake_decreaseDrawingPhase() public { + // Set the stake and create a dispute to advance the phase + vm.prank(staker1); + core.setStake(GENERAL_COURT, 2000); + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.warp(block.timestamp + rngLookahead); + sortitionModule.passPhase(); // Drawing phase + + assertEq(pinakion.balanceOf(address(core)), 2000, "Wrong token balance of the core"); + assertEq(pinakion.balanceOf(staker1), 999999999999998000, "Wrong token balance of staker1"); + + vm.prank(staker1); + vm.expectEmit(true, true, true, true); + emit SortitionModuleBase.StakeDelayed(staker1, GENERAL_COURT, 1800); + core.setStake(GENERAL_COURT, 1800); + + (uint256 totalStaked, , uint256 stakedInCourt, ) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); + assertEq(totalStaked, 2000, "Total staked amount should not change"); + assertEq(stakedInCourt, 2000, "Amount staked in court should not change"); + + assertEq(pinakion.balanceOf(address(core)), 2000, "Token balance of the core should not change"); + assertEq(pinakion.balanceOf(staker1), 999999999999998000, "Wrong token balance of staker1"); + } + + function test_setStake_LockedTokens() public { + // Check that correct amount is taken when locked tokens amount exceeds the staked amount + vm.prank(staker1); + core.setStake(GENERAL_COURT, 10000); + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.warp(block.timestamp + rngLookahead); + sortitionModule.passPhase(); // Drawing phase + + uint256 disputeID = 0; + core.draw(disputeID, DEFAULT_NB_OF_JURORS); + (uint256 totalStaked, uint256 totalLocked, uint256 stakedInCourt, uint256 nbCourts) = sortitionModule + .getJurorBalance(staker1, GENERAL_COURT); + assertEq(totalStaked, 10000, "Wrong amount total staked"); + assertEq(totalLocked, 3000, "Wrong amount locked"); // 1000 per draw and the juror was drawn 3 times + assertEq(stakedInCourt, 10000, "Wrong amount staked in court"); + + sortitionModule.passPhase(); // Staking + + assertEq(pinakion.balanceOf(address(core)), 10000, "Wrong token balance of the core"); + assertEq(pinakion.balanceOf(staker1), 999999999999990000, "Wrong token balance of staker1"); + + // Unstake to check that locked tokens won't be withdrawn + vm.prank(staker1); + core.setStake(GENERAL_COURT, 0); + + (totalStaked, totalLocked, stakedInCourt, nbCourts) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); + assertEq(totalStaked, 3000, "Wrong amount total staked"); + assertEq(totalLocked, 3000, "Wrong amount locked"); + assertEq(stakedInCourt, 0, "Wrong amount staked in court"); + assertEq(nbCourts, 0, "Wrong amount staked in court"); + + assertEq(pinakion.balanceOf(address(core)), 3000, "Wrong token balance of the core"); + assertEq(pinakion.balanceOf(staker1), 999999999999997000, "Wrong token balance of staker1"); + + // Stake again to check the behaviour. + vm.prank(staker1); + core.setStake(GENERAL_COURT, 5000); + + (totalStaked, totalLocked, stakedInCourt, nbCourts) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); + assertEq(totalStaked, 8000, "Wrong amount total staked"); // 5000 were added to the previous 3000. + assertEq(totalLocked, 3000, "Wrong amount locked"); + assertEq(stakedInCourt, 5000, "Wrong amount staked in court"); + assertEq(nbCourts, 1, "Wrong amount staked in court"); + + assertEq(pinakion.balanceOf(address(core)), 8000, "Wrong amount of tokens in Core"); + assertEq(pinakion.balanceOf(staker1), 999999999999992000, "Wrong token balance of staker1"); + } + + function test_executeDelayedStakes() public { + // Stake as staker2 as well to diversify the execution of delayed stakes + vm.prank(staker2); + core.setStake(GENERAL_COURT, 10000); + + vm.expectRevert(SortitionModuleBase.NoDelayedStakeToExecute.selector); + sortitionModule.executeDelayedStakes(5); + + // Set the stake and create a dispute to advance the phase + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.warp(block.timestamp + rngLookahead); + sortitionModule.passPhase(); // Drawing phase + uint256 disputeID = 0; + core.draw(disputeID, DEFAULT_NB_OF_JURORS); + + vm.expectRevert(SortitionModuleBase.NotStakingPhase.selector); + sortitionModule.executeDelayedStakes(5); + + // Create delayed stake + vm.prank(staker1); + vm.expectEmit(true, true, true, true); + emit SortitionModuleBase.StakeDelayed(staker1, GENERAL_COURT, 1500); + core.setStake(GENERAL_COURT, 1500); + + assertEq(pinakion.balanceOf(address(core)), 10000, "Wrong token balance of the core"); // Balance should not increase because the stake was delayed + assertEq(pinakion.balanceOf(staker1), 1 ether, "Wrong token balance of staker1"); + + // Create delayed stake for another staker + vm.prank(staker2); + vm.expectEmit(true, true, true, true); + emit SortitionModuleBase.StakeDelayed(staker2, GENERAL_COURT, 0); + core.setStake(GENERAL_COURT, 0); + assertEq(pinakion.balanceOf(staker2), 999999999999990000, "Wrong token balance of staker2"); // Balance should not change since wrong phase + + // Create another delayed stake for staker1 on top of it to check the execution + vm.prank(staker1); + vm.expectEmit(true, true, true, true); + emit SortitionModuleBase.StakeDelayed(staker1, GENERAL_COURT, 1800); + core.setStake(GENERAL_COURT, 1800); + + assertEq(sortitionModule.delayedStakeWriteIndex(), 3, "Wrong delayedStakeWriteIndex"); + assertEq(sortitionModule.delayedStakeReadIndex(), 1, "Wrong delayedStakeReadIndex"); + + (address account, uint96 courtID, uint256 stake, bool alreadyTransferred) = sortitionModule.delayedStakes(1); + + // Check each delayed stake + assertEq(account, staker1, "Wrong staker account for the first delayed stake"); + assertEq(courtID, GENERAL_COURT, "Wrong court ID"); + assertEq(stake, 1500, "Wrong staking amount"); + assertEq(alreadyTransferred, false, "Should be false"); + + (account, courtID, stake, alreadyTransferred) = sortitionModule.delayedStakes(2); + assertEq(account, staker2, "Wrong staker2 account"); + assertEq(courtID, GENERAL_COURT, "Wrong court id for staker2"); + assertEq(stake, 0, "Wrong amount for delayed stake of staker2"); + assertEq(alreadyTransferred, false, "Should be false"); + + (account, courtID, stake, alreadyTransferred) = sortitionModule.delayedStakes(3); + assertEq(account, staker1, "Wrong staker1 account"); + assertEq(courtID, GENERAL_COURT, "Wrong court id for staker1"); + assertEq(stake, 1800, "Wrong amount for delayed stake of staker1"); + assertEq(alreadyTransferred, false, "Should be false"); + + // So far the only amount transferred was 10000 by staker2. Staker 1 has two delayed stakes, for 1500 and 1800 pnk. + assertEq(pinakion.balanceOf(address(core)), 10000, "Wrong token balance of the core"); + assertEq(pinakion.balanceOf(staker1), 1 ether, "Wrong token balance of staker1"); + assertEq(pinakion.balanceOf(staker2), 999999999999990000, "Wrong token balance of staker2"); + + (uint256 totalStaked, uint256 totalLocked, uint256 stakedInCourt, uint256 nbCourts) = sortitionModule + .getJurorBalance(staker1, GENERAL_COURT); // Only check the first staker to check how consecutive delayed stakes are handled. + // Balances shouldn't be updated yet. + assertEq(totalStaked, 0, "Wrong amount total staked"); + assertEq(totalLocked, 0, "Wrong amount locked"); + assertEq(stakedInCourt, 0, "Wrong amount staked in court"); + assertEq(nbCourts, 0, "Wrong number of courts"); + + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Staking. Delayed stakes can be executed now + + vm.prank(address(core)); + pinakion.transfer(owner, 10000); // Dispose of the tokens of 2nd staker to make the execution fail for the 2nd delayed stake + assertEq(pinakion.balanceOf(address(core)), 0, "Wrong token balance of the core"); + + // 2 events should be emitted but the 2nd stake supersedes the first one in the end. + vm.expectEmit(true, true, true, true); + emit SortitionModuleBase.StakeSet(staker1, GENERAL_COURT, 1500, 1500); + vm.expectEmit(true, true, true, true); + emit SortitionModuleBase.StakeSet(staker1, GENERAL_COURT, 1800, 1800); + sortitionModule.executeDelayedStakes(20); // Deliberately ask for more iterations than needed + + assertEq(sortitionModule.delayedStakeWriteIndex(), 3, "Wrong delayedStakeWriteIndex"); + assertEq(sortitionModule.delayedStakeReadIndex(), 4, "Wrong delayedStakeReadIndex"); + + // Check that delayed stakes are nullified + for (uint i = 2; i <= sortitionModule.delayedStakeWriteIndex(); i++) { + (account, courtID, stake, alreadyTransferred) = sortitionModule.delayedStakes(i); + + assertEq(account, address(0), "Wrong staker account after delayed stake deletion"); + assertEq(courtID, 0, "Court id should be nullified"); + assertEq(stake, 0, "No amount to stake"); + assertEq(alreadyTransferred, false, "Should be false"); + } + + assertEq(pinakion.balanceOf(staker1), 999999999999998200, "Wrong token balance of staker1"); + + (totalStaked, totalLocked, stakedInCourt, nbCourts) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); + assertEq(totalStaked, 1800, "Wrong amount total staked"); + assertEq(totalLocked, 0, "Wrong amount locked"); + assertEq(stakedInCourt, 1800, "Wrong amount staked in court"); + assertEq(nbCourts, 1, "Wrong amount staked in court"); + + // Staker2 not getting the tokens back indicates that his delayed stake was skipped and the flow wasn't disrupted + assertEq(pinakion.balanceOf(staker2), 999999999999990000, "Wrong token balance of staker2"); + } + + function test_setStakeBySortitionModule() public { + // Note that functionality of this function was checked during delayed stakes execution + vm.expectRevert(KlerosCoreBase.SortitionModuleOnly.selector); + vm.prank(owner); + core.setStakeBySortitionModule(staker1, GENERAL_COURT, 1000); + } + + function test_setStake_snapshotProxyCheck() public { + vm.prank(staker1); + core.setStake(GENERAL_COURT, 12346); + + KlerosCoreSnapshotProxy snapshotProxy = new KlerosCoreSnapshotProxy(owner, IKlerosCore(address(core))); + assertEq(snapshotProxy.name(), "Staked Pinakion", "Wrong name of the proxy token"); + assertEq(snapshotProxy.symbol(), "stPNK", "Wrong symbol of the proxy token"); + assertEq(snapshotProxy.decimals(), 18, "Wrong decimals of the proxy token"); + assertEq(snapshotProxy.owner(), msg.sender, "Wrong owner"); + assertEq(address(snapshotProxy.core()), address(core), "Wrong core in snapshot proxy"); + assertEq(snapshotProxy.balanceOf(staker1), 12346, "Wrong stPNK balance"); + + vm.prank(other); + vm.expectRevert(KlerosCoreSnapshotProxy.OwnerOnly.selector); + snapshotProxy.changeCore(IKlerosCore(other)); + vm.prank(owner); + snapshotProxy.changeCore(IKlerosCore(other)); + assertEq(address(snapshotProxy.core()), other, "Wrong core in snapshot proxy after change"); + + vm.prank(other); + vm.expectRevert(KlerosCoreSnapshotProxy.OwnerOnly.selector); + snapshotProxy.changeOwner(other); + vm.prank(owner); + snapshotProxy.changeOwner(other); + assertEq(snapshotProxy.owner(), other, "Wrong owner after change"); + } +} diff --git a/contracts/test/foundry/KlerosCore_TestBase.sol b/contracts/test/foundry/KlerosCore_TestBase.sol new file mode 100644 index 000000000..ad297c86a --- /dev/null +++ b/contracts/test/foundry/KlerosCore_TestBase.sol @@ -0,0 +1,251 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {Test} from "forge-std/Test.sol"; +import {console} from "forge-std/console.sol"; // Import the console for logging +import {KlerosCoreMock, KlerosCoreBase} from "../../src/test/KlerosCoreMock.sol"; +import {IArbitratorV2} from "../../src/arbitration/KlerosCoreBase.sol"; +import {IDisputeKit} from "../../src/arbitration/interfaces/IDisputeKit.sol"; +import {DisputeKitClassic, DisputeKitClassicBase} from "../../src/arbitration/dispute-kits/DisputeKitClassic.sol"; +import {DisputeKitSybilResistant} from "../../src/arbitration/dispute-kits/DisputeKitSybilResistant.sol"; +import {ISortitionModule} from "../../src/arbitration/interfaces/ISortitionModule.sol"; +import {SortitionModuleMock, SortitionModuleBase} from "../../src/test/SortitionModuleMock.sol"; +import {UUPSProxy} from "../../src/proxy/UUPSProxy.sol"; +import {BlockHashRNG} from "../../src/rng/BlockHashRNG.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"; +import {ArbitrableExample, IArbitrableV2} from "../../src/arbitration/arbitrables/ArbitrableExample.sol"; +import {DisputeTemplateRegistry} from "../../src/arbitration/DisputeTemplateRegistry.sol"; +import "../../src/libraries/Constants.sol"; +import {IKlerosCore, KlerosCoreSnapshotProxy} from "../../src/arbitration/view/KlerosCoreSnapshotProxy.sol"; + +/// @title KlerosCore_TestBase +/// @dev Abstract base contract for KlerosCore tests containing shared setup and utilities +abstract contract KlerosCore_TestBase is Test { + event Initialized(uint64 version); + + // ************************************* // + // * Test Contracts * // + // ************************************* // + + KlerosCoreMock core; + DisputeKitClassic disputeKit; + SortitionModuleMock sortitionModule; + BlockHashRNG rng; + PNK pinakion; + TestERC20 feeToken; + TestERC20 wNative; + ArbitrableExample arbitrable; + DisputeTemplateRegistry registry; + + // ************************************* // + // * Test Accounts * // + // ************************************* // + + address owner; + address guardian; + address staker1; + address staker2; + address disputer; + address crowdfunder1; + address crowdfunder2; + address other; + address jurorProsecutionModule; + + // ************************************* // + // * Test Parameters * // + // ************************************* // + + uint256 minStake; + uint256 alpha; + uint256 feeForJuror; + uint256 jurorsForCourtJump; + bytes sortitionExtraData; + bytes arbitratorExtraData; + uint256[4] timesPerPeriod; + bool hiddenVotes; + uint256 totalSupply = 1000000 ether; + uint256 minStakingTime; + uint256 maxDrawingTime; + uint256 rngLookahead; // Time in seconds + string templateData; + string templateDataMappings; + + function setUp() public virtual { + KlerosCoreMock coreLogic = new KlerosCoreMock(); + SortitionModuleMock smLogic = new SortitionModuleMock(); + DisputeKitClassic dkLogic = new DisputeKitClassic(); + DisputeTemplateRegistry registryLogic = new DisputeTemplateRegistry(); + pinakion = new PNK(); + feeToken = new TestERC20("Test", "TST"); + wNative = new TestERC20("wrapped ETH", "wETH"); + + owner = msg.sender; + guardian = vm.addr(1); + staker1 = vm.addr(2); + staker2 = vm.addr(3); + disputer = vm.addr(4); + crowdfunder1 = vm.addr(5); + crowdfunder2 = vm.addr(6); + vm.deal(disputer, 10 ether); + vm.deal(crowdfunder1, 10 ether); + vm.deal(crowdfunder2, 10 ether); + jurorProsecutionModule = vm.addr(8); + other = vm.addr(9); + minStake = 1000; + alpha = 10000; + feeForJuror = 0.03 ether; + jurorsForCourtJump = 511; + timesPerPeriod = [60, 120, 180, 240]; + + pinakion.transfer(msg.sender, totalSupply - 2 ether); + pinakion.transfer(staker1, 1 ether); + pinakion.transfer(staker2, 1 ether); + + sortitionExtraData = abi.encode(uint256(5)); + minStakingTime = 18; + maxDrawingTime = 24; + hiddenVotes = false; + + rngLookahead = 30; + rng = new BlockHashRNG(msg.sender, address(sortitionModule), rngLookahead); + + UUPSProxy proxyCore = new UUPSProxy(address(coreLogic), ""); + + bytes memory initDataDk = abi.encodeWithSignature( + "initialize(address,address,address)", + owner, + address(proxyCore), + address(wNative) + ); + + UUPSProxy proxyDk = new UUPSProxy(address(dkLogic), initDataDk); + disputeKit = DisputeKitClassic(address(proxyDk)); + + bytes memory initDataSm = abi.encodeWithSignature( + "initialize(address,address,uint256,uint256,address)", + owner, + address(proxyCore), + minStakingTime, + maxDrawingTime, + rng + ); + + UUPSProxy proxySm = new UUPSProxy(address(smLogic), initDataSm); + sortitionModule = SortitionModuleMock(address(proxySm)); + vm.prank(owner); + rng.changeConsumer(address(sortitionModule)); + + core = KlerosCoreMock(address(proxyCore)); + core.initialize( + owner, + guardian, + pinakion, + jurorProsecutionModule, + disputeKit, + hiddenVotes, + [minStake, alpha, feeForJuror, jurorsForCourtJump], + timesPerPeriod, + sortitionExtraData, + sortitionModule, + address(wNative) + ); + vm.prank(staker1); + pinakion.approve(address(core), 1 ether); + vm.prank(staker2); + pinakion.approve(address(core), 1 ether); + + templateData = "AAA"; + templateDataMappings = "BBB"; + arbitratorExtraData = abi.encodePacked(uint256(GENERAL_COURT), DEFAULT_NB_OF_JURORS, DISPUTE_KIT_CLASSIC); + + bytes memory initDataRegistry = abi.encodeWithSignature("initialize(address)", owner); + UUPSProxy proxyRegistry = new UUPSProxy(address(registryLogic), initDataRegistry); + registry = DisputeTemplateRegistry(address(proxyRegistry)); + + arbitrable = new ArbitrableExample( + core, + templateData, + templateDataMappings, + arbitratorExtraData, + registry, + feeToken + ); + } + + // ************************************* // + // * Helper Functions * // + // ************************************* // + + /// @dev Helper function to create a new dispute kit + function _createNewDisputeKit() internal returns (DisputeKitSybilResistant) { + return new DisputeKitSybilResistant(); + } + + /// @dev Helper function to create a new court with standard parameters + function _createStandardCourt( + uint96 parent, + uint256 minStakeValue, + uint256 alphaValue, + uint256 feeForJurorValue, + uint256 jurorsForJumpValue + ) internal returns (uint96) { + uint256[] memory supportedDK = new uint256[](1); + supportedDK[0] = DISPUTE_KIT_CLASSIC; + + vm.prank(owner); + core.createCourt( + parent, + hiddenVotes, + minStakeValue, + alphaValue, + feeForJurorValue, + jurorsForJumpValue, + timesPerPeriod, + sortitionExtraData, + supportedDK + ); + + return uint96(core.getCourtChildren(parent)[core.getCourtChildren(parent).length - 1]); + } + + /// @dev Helper function to check court parameters + function _assertCourtParameters( + uint96 courtId, + uint96 expectedParent, + bool expectedHiddenVotes, + uint256 expectedMinStake, + uint256 expectedAlpha, + uint256 expectedFeeForJuror, + uint256 expectedJurorsForJump, + bool expectedDisabled + ) internal { + ( + uint96 courtParent, + bool courtHiddenVotes, + uint256 courtMinStake, + uint256 courtAlpha, + uint256 courtFeeForJuror, + uint256 courtJurorsForCourtJump, + bool courtDisabled + ) = core.courts(courtId); + + assertEq(courtParent, expectedParent, "Wrong court parent"); + assertEq(courtHiddenVotes, expectedHiddenVotes, "Wrong hiddenVotes value"); + assertEq(courtMinStake, expectedMinStake, "Wrong minStake value"); + assertEq(courtAlpha, expectedAlpha, "Wrong alpha value"); + assertEq(courtFeeForJuror, expectedFeeForJuror, "Wrong feeForJuror value"); + assertEq(courtJurorsForCourtJump, expectedJurorsForJump, "Wrong jurorsForCourtJump value"); + assertEq(courtDisabled, expectedDisabled, "Wrong disabled state"); + } + + /// @dev Helper function to check times per period + function _assertTimesPerPeriod(uint96 courtId, uint256[4] memory expectedTimes) internal { + uint256[4] memory courtTimesPerPeriod = core.getTimesPerPeriod(courtId); + for (uint256 i = 0; i < 4; i++) { + assertEq(courtTimesPerPeriod[i], expectedTimes[i], "Wrong times per period"); + } + } +} diff --git a/contracts/test/foundry/KlerosCore_Voting.t.sol b/contracts/test/foundry/KlerosCore_Voting.t.sol new file mode 100644 index 000000000..9ede69305 --- /dev/null +++ b/contracts/test/foundry/KlerosCore_Voting.t.sol @@ -0,0 +1,485 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {KlerosCore_TestBase} from "./KlerosCore_TestBase.sol"; +import {KlerosCoreBase} from "../../src/arbitration/KlerosCoreBase.sol"; +import {DisputeKitClassic, DisputeKitClassicBase} from "../../src/arbitration/dispute-kits/DisputeKitClassic.sol"; +import {IDisputeKit} from "../../src/arbitration/interfaces/IDisputeKit.sol"; +import {UUPSProxy} from "../../src/proxy/UUPSProxy.sol"; +import "../../src/libraries/Constants.sol"; + +/// @title KlerosCore_VotingTest +/// @dev Tests for KlerosCore voting system (commit/reveal and direct voting) +contract KlerosCore_VotingTest is KlerosCore_TestBase { + function test_castCommit() public { + // Change hidden votes in general court + uint256 disputeID = 0; + vm.prank(owner); + core.changeCourtParameters( + GENERAL_COURT, + true, // Hidden votes + 1000, // min stake + 10000, // alpha + 0.03 ether, // fee for juror + 511, // jurors for jump + [uint256(60), uint256(120), uint256(180), uint256(240)] // Times per period + ); + + vm.prank(staker1); + core.setStake(GENERAL_COURT, 10000); + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.warp(block.timestamp + rngLookahead); + sortitionModule.passPhase(); // Drawing phase + core.draw(disputeID, DEFAULT_NB_OF_JURORS); + + uint256 YES = 1; + uint256 salt = 123455678; + uint256[] memory voteIDs = new uint256[](1); + voteIDs[0] = 0; + bytes32 commit; + vm.prank(staker1); + vm.expectRevert(DisputeKitClassicBase.NotCommitPeriod.selector); + disputeKit.castCommit(disputeID, voteIDs, commit); + + vm.expectRevert(KlerosCoreBase.EvidenceNotPassedAndNotAppeal.selector); + core.passPeriod(disputeID); + vm.warp(block.timestamp + timesPerPeriod[0]); + + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.commit); + core.passPeriod(disputeID); + + (, , KlerosCoreBase.Period period, , uint256 lastPeriodChange) = core.disputes(disputeID); + + assertEq(uint256(period), uint256(KlerosCoreBase.Period.commit), "Wrong period"); + assertEq(lastPeriodChange, block.timestamp, "Wrong lastPeriodChange"); + + vm.prank(staker1); + vm.expectRevert(DisputeKitClassicBase.EmptyCommit.selector); + disputeKit.castCommit(disputeID, voteIDs, commit); + + commit = keccak256(abi.encodePacked(YES, salt)); + + vm.prank(other); + vm.expectRevert(DisputeKitClassicBase.JurorHasToOwnTheVote.selector); + disputeKit.castCommit(disputeID, voteIDs, commit); + + vm.prank(staker1); + vm.expectEmit(true, true, true, true); + emit DisputeKitClassicBase.CommitCast(disputeID, staker1, voteIDs, commit); + disputeKit.castCommit(disputeID, voteIDs, commit); + + (, , , uint256 totalCommited, uint256 nbVoters, uint256 choiceCount) = disputeKit.getRoundInfo(disputeID, 0, 0); + assertEq(totalCommited, 1, "totalCommited should be 1"); + assertEq(disputeKit.areCommitsAllCast(disputeID), false, "Commits should not all be cast"); + + (, bytes32 commitStored, , ) = disputeKit.getVoteInfo(0, 0, 0); + assertEq(commitStored, keccak256(abi.encodePacked(YES, salt)), "Incorrect commit"); + + voteIDs = new uint256[](2); // Create the leftover votes subset + voteIDs[0] = 1; + voteIDs[1] = 2; + + vm.prank(staker1); + vm.expectEmit(true, true, true, true); + emit DisputeKitClassicBase.CommitCast(disputeID, staker1, voteIDs, commit); + disputeKit.castCommit(disputeID, voteIDs, commit); + + (, , , totalCommited, nbVoters, choiceCount) = disputeKit.getRoundInfo(disputeID, 0, 0); + assertEq(totalCommited, DEFAULT_NB_OF_JURORS, "totalCommited should be 3"); + assertEq(disputeKit.areCommitsAllCast(disputeID), true, "Commits should all be cast"); + + for (uint256 i = 1; i < DEFAULT_NB_OF_JURORS; i++) { + (, commitStored, , ) = disputeKit.getVoteInfo(0, 0, i); + assertEq(commitStored, keccak256(abi.encodePacked(YES, salt)), "Incorrect commit"); + } + + // 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 + vm.prank(staker1); + vm.expectRevert(DisputeKitClassicBase.HashDoesNotMatchHiddenVoteCommitment.selector); + disputeKit.castVote(disputeID, voteIDs, 2, salt, "XYZ"); + + vm.prank(staker1); + vm.expectRevert(DisputeKitClassicBase.HashDoesNotMatchHiddenVoteCommitment.selector); + disputeKit.castVote(disputeID, voteIDs, YES, salt - 1, "XYZ"); + + vm.prank(staker1); + disputeKit.castVote(disputeID, voteIDs, YES, salt, "XYZ"); + + for (uint256 i = 1; i < DEFAULT_NB_OF_JURORS; i++) { + // 0 voteID was skipped when casting a vote + (address account, , uint256 choice, bool voted) = disputeKit.getVoteInfo(0, 0, i); + assertEq(account, staker1, "Wrong drawn account"); + assertEq(choice, YES, "Wrong choice"); + assertEq(voted, true, "Voted should be true"); + } + } + + function test_castCommit_timeoutCheck() public { + // Change hidden votes in general court + uint256 disputeID = 0; + vm.prank(owner); + core.changeCourtParameters( + GENERAL_COURT, + true, // Hidden votes + 1000, // min stake + 10000, // alpha + 0.03 ether, // fee for juror + 511, // jurors for jump + [uint256(60), uint256(120), uint256(180), uint256(240)] // Times per period + ); + + vm.prank(staker1); + core.setStake(GENERAL_COURT, 10000); + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.warp(block.timestamp + rngLookahead); + sortitionModule.passPhase(); // Drawing phase + core.draw(disputeID, DEFAULT_NB_OF_JURORS); + + vm.warp(block.timestamp + timesPerPeriod[0]); + core.passPeriod(disputeID); // Commit + + vm.expectRevert(KlerosCoreBase.CommitPeriodNotPassed.selector); + core.passPeriod(disputeID); + + vm.warp(block.timestamp + timesPerPeriod[1]); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.vote); + core.passPeriod(disputeID); + } + + function test_castVote() public { + uint256 disputeID = 0; + + vm.prank(staker1); + core.setStake(GENERAL_COURT, 10000); + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.warp(block.timestamp + rngLookahead); + sortitionModule.passPhase(); // Drawing phase + + core.draw(disputeID, DEFAULT_NB_OF_JURORS - 1); // Draw less to check the require later + vm.warp(block.timestamp + timesPerPeriod[0]); + + uint256[] memory voteIDs = new uint256[](0); + vm.prank(staker1); + vm.expectRevert(DisputeKitClassicBase.NotVotePeriod.selector); + disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); // Leave salt empty as not needed + + vm.expectRevert(KlerosCoreBase.DisputeStillDrawing.selector); + core.passPeriod(disputeID); + + core.draw(disputeID, 1); // Draw the last juror + + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.vote); + core.passPeriod(disputeID); // Vote + + (, , KlerosCoreBase.Period period, , uint256 lastPeriodChange) = core.disputes(disputeID); + + assertEq(uint256(period), uint256(KlerosCoreBase.Period.vote), "Wrong period"); + assertEq(lastPeriodChange, block.timestamp, "Wrong lastPeriodChange"); + + vm.prank(staker1); + 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(DisputeKitClassicBase.ChoiceOutOfBounds.selector); + disputeKit.castVote(disputeID, voteIDs, 2 + 1, 0, "XYZ"); + + vm.prank(other); + vm.expectRevert(DisputeKitClassicBase.JurorHasToOwnTheVote.selector); + disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); + + vm.prank(staker1); + vm.expectEmit(true, true, true, true); + emit IDisputeKit.VoteCast(disputeID, staker1, voteIDs, 2, "XYZ"); + disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); + + vm.prank(staker1); + vm.expectRevert(DisputeKitClassicBase.VoteAlreadyCast.selector); + disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); + + ( + uint256 winningChoice, + bool tied, + uint256 totalVoted, + uint256 totalCommited, + , + uint256 choiceCount + ) = disputeKit.getRoundInfo(disputeID, 0, 2); + assertEq(winningChoice, 2, "Wrong winning choice"); + assertEq(tied, false, "tied should be false"); + assertEq(totalVoted, 1, "totalVoted should be 1"); + assertEq(totalCommited, 0, "totalCommited should be 0"); + assertEq(choiceCount, 1, "choiceCount should be 1"); + + (address account, bytes32 commit, uint256 choice, bool voted) = disputeKit.getVoteInfo(0, 0, 0); // Dispute - Round - VoteID + assertEq(account, staker1, "Wrong drawn account"); + assertEq(commit, bytes32(0), "Commit should be empty"); + assertEq(choice, 2, "Choice should be 2"); + assertEq(voted, true, "Voted should be true"); + + assertEq(disputeKit.isVoteActive(0, 0, 0), true, "Vote should be active"); // Dispute - Round - VoteID + + voteIDs = new uint256[](1); + voteIDs[0] = 1; // Cast another vote to check the tie. + + vm.prank(staker1); + vm.expectEmit(true, true, true, true); + emit IDisputeKit.VoteCast(disputeID, staker1, voteIDs, 1, "XYZZ"); + disputeKit.castVote(disputeID, voteIDs, 1, 0, "XYZZ"); + + (, tied, totalVoted, , , choiceCount) = disputeKit.getRoundInfo(disputeID, 0, 1); + assertEq(tied, true, "tied should be true"); + assertEq(totalVoted, 2, "totalVoted should be 2"); + assertEq(choiceCount, 1, "choiceCount should be 1 for first choice"); + + vm.expectRevert(KlerosCoreBase.VotePeriodNotPassed.selector); + core.passPeriod(disputeID); + + voteIDs = new uint256[](1); + voteIDs[0] = 2; // Cast another vote to declare a new winner. + + vm.prank(staker1); + vm.expectEmit(true, true, true, true); + emit IDisputeKit.VoteCast(disputeID, staker1, voteIDs, 1, "XYZZ"); + disputeKit.castVote(disputeID, voteIDs, 1, 0, "XYZZ"); + + (winningChoice, tied, totalVoted, , , choiceCount) = disputeKit.getRoundInfo(disputeID, 0, 1); + assertEq(winningChoice, 1, "Wrong winning choice"); + assertEq(tied, false, "tied should be false"); + assertEq(totalVoted, 3, "totalVoted should be 3"); + assertEq(choiceCount, 2, "choiceCount should be 2 for first choice"); + assertEq(disputeKit.areVotesAllCast(disputeID), true, "Votes should all be cast"); + } + + function test_castVote_timeoutCheck() public { + // Change hidden votes in general court + uint256 disputeID = 0; + vm.prank(staker1); + core.setStake(GENERAL_COURT, 10000); + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.warp(block.timestamp + rngLookahead); + sortitionModule.passPhase(); // Drawing phase + core.draw(disputeID, DEFAULT_NB_OF_JURORS); + + vm.warp(block.timestamp + timesPerPeriod[0]); + core.passPeriod(disputeID); // Votes + + vm.expectRevert(KlerosCoreBase.VotePeriodNotPassed.selector); + core.passPeriod(disputeID); + + vm.warp(block.timestamp + timesPerPeriod[2]); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.AppealPossible(disputeID, arbitrable); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.appeal); + core.passPeriod(disputeID); + } + + function test_castVote_rulingCheck() public { + // Change hidden votes in general court + uint256 disputeID = 0; + vm.prank(staker1); + core.setStake(GENERAL_COURT, 10000); + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.warp(block.timestamp + rngLookahead); + sortitionModule.passPhase(); // Drawing phase + core.draw(disputeID, DEFAULT_NB_OF_JURORS); + + vm.warp(block.timestamp + timesPerPeriod[0]); + core.passPeriod(disputeID); // Votes + + uint256[] memory voteIDs = new uint256[](3); + voteIDs[0] = 0; + voteIDs[1] = 1; + voteIDs[2] = 2; + + vm.prank(staker1); + disputeKit.castVote(disputeID, voteIDs, 1, 0, "XYZZ"); + + (uint256 ruling, bool tied, bool overridden) = disputeKit.currentRuling(disputeID); + assertEq(ruling, 1, "Wrong ruling"); + assertEq(tied, false, "Not tied"); + assertEq(overridden, false, "Not overridden"); + } + + function test_castVote_quickPassPeriod() public { + // Change hidden votes in general court + uint256 disputeID = 0; + vm.prank(owner); + core.changeCourtParameters( + GENERAL_COURT, + true, // Hidden votes + 1000, // min stake + 10000, // alpha + 0.03 ether, // fee for juror + 511, // jurors for jump + [uint256(60), uint256(120), uint256(180), uint256(240)] // Times per period + ); + + vm.prank(staker1); + core.setStake(GENERAL_COURT, 10000); + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.warp(block.timestamp + rngLookahead); + sortitionModule.passPhase(); // Drawing phase + core.draw(disputeID, DEFAULT_NB_OF_JURORS); + + uint256 YES = 1; + uint256 salt = 123455678; + uint256[] memory voteIDs = new uint256[](1); + voteIDs[0] = 0; + bytes32 commit; + + vm.warp(block.timestamp + timesPerPeriod[0]); + core.passPeriod(disputeID); + + commit = keccak256(abi.encodePacked(YES, salt)); + + vm.prank(staker1); + disputeKit.castCommit(disputeID, voteIDs, commit); + + (, , , uint256 totalCommited, uint256 nbVoters, uint256 choiceCount) = disputeKit.getRoundInfo(disputeID, 0, 0); + assertEq(totalCommited, 1, "totalCommited should be 1"); + assertEq(disputeKit.areCommitsAllCast(disputeID), false, "Commits should not all be cast"); + + vm.warp(block.timestamp + timesPerPeriod[1]); + core.passPeriod(disputeID); + + vm.prank(staker1); + disputeKit.castVote(disputeID, voteIDs, YES, salt, "XYZ"); + + (, , uint256 totalVoted, , , ) = disputeKit.getRoundInfo(disputeID, 0, 0); + assertEq(totalVoted, 1, "totalVoted should be 1"); + assertEq(disputeKit.areVotesAllCast(disputeID), true, "Every committed vote was cast"); + + // Should pass period by counting only committed votes. + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.appeal); + core.passPeriod(disputeID); + } + + function test_castVote_differentDK() public { + DisputeKitClassic dkLogic = new DisputeKitClassic(); + // Create a new DK to check castVote. + bytes memory initDataDk = abi.encodeWithSignature( + "initialize(address,address,address)", + owner, + address(core), + address(wNative) + ); + + UUPSProxy proxyDk = new UUPSProxy(address(dkLogic), initDataDk); + DisputeKitClassic newDisputeKit = DisputeKitClassic(address(proxyDk)); + + vm.prank(owner); + core.addNewDisputeKit(newDisputeKit); + + uint256 newDkID = 2; + uint256[] memory supportedDK = new uint256[](1); + bytes memory newExtraData = abi.encodePacked(uint256(GENERAL_COURT), DEFAULT_NB_OF_JURORS, newDkID); + + vm.prank(owner); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.DisputeKitEnabled(GENERAL_COURT, newDkID, true); + supportedDK[0] = newDkID; + core.enableDisputeKits(GENERAL_COURT, supportedDK, true); + assertEq(core.isSupported(GENERAL_COURT, newDkID), true, "New DK should be supported by General court"); + + vm.prank(staker1); + core.setStake(GENERAL_COURT, 20000); + + // Create one dispute for the old DK and two disputes for the new DK. + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + + arbitrable.changeArbitratorExtraData(newExtraData); + + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + + uint256 disputeID = 2; // Use the latest dispute for reference. This is the ID in the core contract + + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.warp(block.timestamp + rngLookahead); + sortitionModule.passPhase(); // Drawing phase + + KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 0); + assertEq(round.disputeKitID, newDkID, "Wrong DK ID"); + + core.draw(disputeID, DEFAULT_NB_OF_JURORS); + // Draw jurors for the old DK as well to prepare round.votes array + core.draw(0, DEFAULT_NB_OF_JURORS); + + vm.warp(block.timestamp + timesPerPeriod[0]); + core.passPeriod(disputeID); // Vote + + // Check that the new DK has the info but not the old one. + + assertEq(disputeKit.coreDisputeIDToActive(disputeID), false, "Should be false for old DK"); + + // This is the DK where dispute was created. Core dispute points to index 1 because new DK has two disputes. + assertEq(newDisputeKit.coreDisputeIDToLocal(disputeID), 1, "Wrong local dispute ID for new DK"); + assertEq(newDisputeKit.coreDisputeIDToActive(disputeID), true, "Should be active for new DK"); + (uint256 numberOfChoices, , bytes memory extraData) = newDisputeKit.disputes(1); + assertEq(numberOfChoices, 2, "Wrong numberOfChoices in new DK"); + assertEq(extraData, newExtraData, "Wrong extra data"); + + uint256[] memory voteIDs = new uint256[](3); + voteIDs[0] = 0; + voteIDs[1] = 1; + voteIDs[2] = 2; + + // Deliberately cast votes using the old DK to see if the exception will be caught. + vm.prank(staker1); + vm.expectRevert(DisputeKitClassicBase.NotActiveForCoreDisputeID.selector); + disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); + + // And check the new DK. + vm.prank(staker1); + newDisputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); + + ( + uint256 winningChoice, + bool tied, + uint256 totalVoted, + uint256 totalCommited, + , + uint256 choiceCount + ) = newDisputeKit.getRoundInfo(disputeID, 0, 2); + assertEq(winningChoice, 2, "Wrong winning choice"); + assertEq(tied, false, "tied should be false"); + assertEq(totalVoted, 3, "totalVoted should be 3"); + assertEq(totalCommited, 0, "totalCommited should be 0"); + assertEq(choiceCount, 3, "choiceCount should be 3"); + } +} From e6114bab12da4179bee66ab8ada4b6334eae4297 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Fri, 29 Aug 2025 04:09:54 +0100 Subject: [PATCH 071/175] fix: defensive guard added against jumpDisputeKitID not set --- contracts/src/arbitration/KlerosCoreBase.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/src/arbitration/KlerosCoreBase.sol b/contracts/src/arbitration/KlerosCoreBase.sol index 891d7173f..5551d5257 100644 --- a/contracts/src/arbitration/KlerosCoreBase.sol +++ b/contracts/src/arbitration/KlerosCoreBase.sol @@ -1081,8 +1081,8 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable if (!courts[newCourtID].supportedDisputeKits[newDisputeKitID]) { // The current Dispute Kit is not compatible with the new court, jump to another Dispute Kit. newDisputeKitID = disputeKits[_round.disputeKitID].getJumpDisputeKitID(); - if (!courts[newCourtID].supportedDisputeKits[newDisputeKitID]) { - // The new Dispute Kit is still not compatible, fall back to `DisputeKitClassic` which is always supported. + if (newDisputeKitID == NULL_DISPUTE_KIT || !courts[newCourtID].supportedDisputeKits[newDisputeKitID]) { + // The new Dispute Kit is not defined or still not compatible, fall back to `DisputeKitClassic` which is always supported. newDisputeKitID = DISPUTE_KIT_CLASSIC; } disputeKitJump = true; From 9d31252bb310aef00d9c175340fc602545ac26a0 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Fri, 29 Aug 2025 04:28:31 +0100 Subject: [PATCH 072/175] chore: dynamically retrieve the disputeKitID in the deployment script --- contracts/deploy/00-home-chain-arbitration-neo.ts | 6 +++--- contracts/deploy/00-home-chain-arbitration.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/contracts/deploy/00-home-chain-arbitration-neo.ts b/contracts/deploy/00-home-chain-arbitration-neo.ts index 22c2ffade..c5e50d6a2 100644 --- a/contracts/deploy/00-home-chain-arbitration-neo.ts +++ b/contracts/deploy/00-home-chain-arbitration-neo.ts @@ -123,29 +123,29 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) await core.changeArbitrableWhitelist(resolver.address, true); // Extra dispute kits - const disputeKitShutterID = 2; const disputeKitShutter = await deployUpgradable(deployments, "DisputeKitShutter", { from: deployer, args: [deployer, core.target, weth.target, classicDisputeKitID], log: true, }); await core.addNewDisputeKit(disputeKitShutter.address); + const disputeKitShutterID = Number(await core.getDisputeKitsLength()); - const disputeKitGatedID = 3; const disputeKitGated = await deployUpgradable(deployments, "DisputeKitGated", { from: deployer, args: [deployer, core.target, weth.target, classicDisputeKitID], log: true, }); await core.addNewDisputeKit(disputeKitGated.address); + const disputeKitGatedID = Number(await core.getDisputeKitsLength()); - const disputeKitGatedShutterID = 4; const disputeKitGatedShutter = await deployUpgradable(deployments, "DisputeKitGatedShutter", { from: deployer, args: [deployer, core.target, weth.target, disputeKitShutterID], // Does not jump to DKClassic log: true, }); await core.addNewDisputeKit(disputeKitGatedShutter.address); + const disputeKitGatedShutterID = Number(await core.getDisputeKitsLength()); // Snapshot proxy await deploy("KlerosCoreSnapshotProxy", { diff --git a/contracts/deploy/00-home-chain-arbitration.ts b/contracts/deploy/00-home-chain-arbitration.ts index 8ec753194..8163c6f66 100644 --- a/contracts/deploy/00-home-chain-arbitration.ts +++ b/contracts/deploy/00-home-chain-arbitration.ts @@ -104,31 +104,31 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) } // Extra dispute kits - const disputeKitShutterID = 2; const disputeKitShutter = await deployUpgradable(deployments, "DisputeKitShutter", { from: deployer, args: [deployer, core.target, weth.target, classicDisputeKitID], log: true, }); await core.addNewDisputeKit(disputeKitShutter.address); + const disputeKitShutterID = Number(await core.getDisputeKitsLength()); await core.enableDisputeKits(Courts.GENERAL, [disputeKitShutterID], true); // enable disputeKitShutter on the General Court - const disputeKitGatedID = 3; const disputeKitGated = await deployUpgradable(deployments, "DisputeKitGated", { from: deployer, args: [deployer, core.target, weth.target, classicDisputeKitID], log: true, }); await core.addNewDisputeKit(disputeKitGated.address); + const disputeKitGatedID = Number(await core.getDisputeKitsLength()); await core.enableDisputeKits(Courts.GENERAL, [disputeKitGatedID], true); // enable disputeKitGated on the General Court - const disputeKitGatedShutterID = 4; const disputeKitGatedShutter = await deployUpgradable(deployments, "DisputeKitGatedShutter", { from: deployer, args: [deployer, core.target, weth.target, disputeKitShutterID], // Does not jump to DKClassic log: true, }); await core.addNewDisputeKit(disputeKitGatedShutter.address); + const disputeKitGatedShutterID = Number(await core.getDisputeKitsLength()); await core.enableDisputeKits(Courts.GENERAL, [disputeKitGatedShutterID], true); // enable disputeKitGatedShutter on the General Court // Snapshot proxy From baf96954ff25006b02d6c40ee1e60773cb4e5377 Mon Sep 17 00:00:00 2001 From: Harman-singh-waraich Date: Tue, 2 Sep 2025 21:18:32 +0530 Subject: [PATCH 073/175] feat: initial-features-implementation --- web/src/context/NewDisputeContext.tsx | 29 ++ web/src/pages/Resolver/Parameters/Court.tsx | 356 ------------------ .../FeatureSelection/JurorEligibility.tsx | 248 ++++++++++++ .../Court/FeatureSelection/ShieldedVoting.tsx | 107 ++++++ .../Court/FeatureSelection/index.tsx | 71 ++++ .../pages/Resolver/Parameters/Court/index.tsx | 92 +++++ 6 files changed, 547 insertions(+), 356 deletions(-) delete mode 100644 web/src/pages/Resolver/Parameters/Court.tsx create mode 100644 web/src/pages/Resolver/Parameters/Court/FeatureSelection/JurorEligibility.tsx create mode 100644 web/src/pages/Resolver/Parameters/Court/FeatureSelection/ShieldedVoting.tsx create mode 100644 web/src/pages/Resolver/Parameters/Court/FeatureSelection/index.tsx create mode 100644 web/src/pages/Resolver/Parameters/Court/index.tsx diff --git a/web/src/context/NewDisputeContext.tsx b/web/src/context/NewDisputeContext.tsx index 5fc109cef..e59c9cfc3 100644 --- a/web/src/context/NewDisputeContext.tsx +++ b/web/src/context/NewDisputeContext.tsx @@ -5,9 +5,13 @@ import { Address } from "viem"; import { DEFAULT_CHAIN } from "consts/chains"; import { klerosCoreAddress } from "hooks/contracts/generated"; +import { useSupportedDisputeKits } from "hooks/queries/useSupportedDisputeKits"; +import { useDisputeKitAddressesAll } from "hooks/useDisputeKitAddresses"; import { useLocalStorage } from "hooks/useLocalStorage"; import { isEmpty, isUndefined } from "utils/index"; +import { DisputeKits } from "src/consts"; + export const MIN_DISPUTE_BATCH_SIZE = 2; export type Answer = { @@ -24,6 +28,12 @@ export type AliasArray = { isValid?: boolean; }; +export type DisputeKitOption = { + text: DisputeKits; + value: number; + gated: boolean; +}; + export type Alias = Record; export interface IDisputeTemplate { answers: Answer[]; @@ -83,6 +93,7 @@ interface INewDisputeContext { setIsBatchCreation: (isBatchCreation: boolean) => void; batchSize: number; setBatchSize: (batchSize?: number) => void; + disputeKitOptions: DisputeKitOption[]; } const getInitialDisputeData = (): IDisputeData => ({ @@ -137,6 +148,22 @@ export const NewDisputeProvider: React.FC<{ children: React.ReactNode }> = ({ ch // eslint-disable-next-line react-hooks/exhaustive-deps }, [location.pathname]); + const { data: supportedDisputeKits } = useSupportedDisputeKits(disputeData.courtId); + const { availableDisputeKits } = useDisputeKitAddressesAll(); + + const disputeKitOptions: DisputeKitOption[] = useMemo(() => { + return ( + supportedDisputeKits?.court?.supportedDisputeKits.map((dk) => { + const text = availableDisputeKits[dk.address.toLowerCase()] ?? ""; + return { + text, + value: Number(dk.id), + gated: text === DisputeKits.Gated || text === DisputeKits.GatedShutter, + }; + }) || [] + ); + }, [supportedDisputeKits, availableDisputeKits]); + const contextValues = useMemo( () => ({ disputeData, @@ -151,6 +178,7 @@ export const NewDisputeProvider: React.FC<{ children: React.ReactNode }> = ({ ch setIsBatchCreation, batchSize, setBatchSize, + disputeKitOptions, }), [ disputeData, @@ -163,6 +191,7 @@ export const NewDisputeProvider: React.FC<{ children: React.ReactNode }> = ({ ch setIsBatchCreation, batchSize, setBatchSize, + disputeKitOptions, ] ); diff --git a/web/src/pages/Resolver/Parameters/Court.tsx b/web/src/pages/Resolver/Parameters/Court.tsx deleted file mode 100644 index df4767f9d..000000000 --- a/web/src/pages/Resolver/Parameters/Court.tsx +++ /dev/null @@ -1,356 +0,0 @@ -import React, { useMemo, useEffect } from "react"; -import styled, { css } from "styled-components"; - -import { AlertMessage, Checkbox, DropdownCascader, DropdownSelect, Field } from "@kleros/ui-components-library"; - -import { DisputeKits } from "consts/index"; -import { IGatedDisputeData, useNewDisputeContext } from "context/NewDisputeContext"; -import { rootCourtToItems, useCourtTree } from "hooks/queries/useCourtTree"; -import { useDisputeKitAddressesAll } from "hooks/useDisputeKitAddresses"; -import { useERC20ERC721Validation, useERC1155Validation } from "hooks/useTokenAddressValidation"; -import { isUndefined } from "utils/index"; - -import { useSupportedDisputeKits } from "queries/useSupportedDisputeKits"; - -import { landscapeStyle } from "styles/landscapeStyle"; -import { responsiveSize } from "styles/responsiveSize"; - -import { StyledSkeleton } from "components/StyledSkeleton"; -import Header from "pages/Resolver/Header"; - -import NavigationButtons from "../NavigationButtons"; - -const Container = styled.div` - display: flex; - flex-direction: column; - align-items: center; - - ${landscapeStyle( - () => css` - padding-bottom: 115px; - ` - )} -`; - -const StyledDropdownCascader = styled(DropdownCascader)` - width: 84vw; - ${landscapeStyle( - () => css` - width: ${responsiveSize(442, 700, 900)}; - ` - )} - > button { - width: 100%; - } -`; - -const AlertMessageContainer = styled.div` - width: 84vw; - ${landscapeStyle( - () => css` - width: ${responsiveSize(442, 700, 900)}; - ` - )} - margin-top: 24px; -`; - -const StyledDropdownSelect = styled(DropdownSelect)` - width: 84vw; - margin-top: 24px; - ${landscapeStyle( - () => css` - width: ${responsiveSize(442, 700, 900)}; - ` - )} -`; - -const StyledField = styled(Field)` - width: 84vw; - margin-top: 24px; - ${landscapeStyle( - () => css` - width: ${responsiveSize(442, 700, 900)}; - ` - )} - > small { - margin-top: 16px; - } -`; - -const StyledCheckbox = styled(Checkbox)` - width: 84vw; - margin-top: 24px; - ${landscapeStyle( - () => css` - width: ${responsiveSize(442, 700, 900)}; - ` - )} -`; - -const ValidationContainer = styled.div` - width: 84vw; - display: flex; - align-items: left; - gap: 8px; - margin-top: 8px; - ${landscapeStyle( - () => css` - width: ${responsiveSize(442, 700, 900)}; - ` - )} -`; - -const ValidationIcon = styled.div<{ $isValid?: boolean | null; $isValidating?: boolean }>` - width: 16px; - height: 16px; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - font-size: 12px; - - ${({ $isValidating, $isValid }) => { - if ($isValidating) { - return css` - border: 2px solid ${({ theme }) => theme.stroke}; - border-top-color: ${({ theme }) => theme.primaryBlue}; - animation: spin 1s linear infinite; - - @keyframes spin { - to { - transform: rotate(360deg); - } - } - `; - } - - if ($isValid === true) { - return css` - background-color: ${({ theme }) => theme.success}; - color: white; - &::after { - content: "✓"; - } - `; - } - - if ($isValid === false) { - return css` - background-color: ${({ theme }) => theme.error}; - color: white; - &::after { - content: "✗"; - } - `; - } - - return css` - display: none; - `; - }} -`; - -const ValidationMessage = styled.small<{ $isError?: boolean }>` - color: ${({ $isError, theme }) => ($isError ? theme.error : theme.success)}; - font-size: 14px; - font-style: italic; - font-weight: normal; -`; - -const StyledFieldWithValidation = styled(StyledField)<{ $isValid?: boolean | null }>` - > input { - border-color: ${({ $isValid, theme }) => { - if ($isValid === true) return theme.success; - if ($isValid === false) return theme.error; - return "inherit"; - }}; - } -`; - -const Court: React.FC = () => { - const { disputeData, setDisputeData } = useNewDisputeContext(); - const { data: courtTree } = useCourtTree(); - const { data: supportedDisputeKits } = useSupportedDisputeKits(disputeData.courtId); - const items = useMemo(() => !isUndefined(courtTree?.court) && [rootCourtToItems(courtTree.court)], [courtTree]); - const { availableDisputeKits } = useDisputeKitAddressesAll(); - - const disputeKitOptions = useMemo(() => { - return ( - supportedDisputeKits?.court?.supportedDisputeKits.map((dk) => { - const text = availableDisputeKits[dk.address.toLowerCase()] ?? ""; - return { - text, - value: Number(dk.id), - gated: text === DisputeKits.Gated || text === DisputeKits.GatedShutter, - }; - }) || [] - ); - }, [supportedDisputeKits, availableDisputeKits]); - - const isGatedDisputeKit = useMemo(() => { - const options = disputeKitOptions.find((dk) => String(dk.value) === String(disputeData.disputeKitId)); - return options?.gated ?? false; - }, [disputeKitOptions, disputeData.disputeKitId]); - - // Token validation for token gate address (conditional based on ERC1155 checkbox) - const tokenGateAddress = (disputeData.disputeKitData as IGatedDisputeData)?.tokenGate ?? ""; - const isERC1155 = (disputeData.disputeKitData as IGatedDisputeData)?.isERC1155 ?? false; - const validationEnabled = isGatedDisputeKit && !!tokenGateAddress.trim(); - - const { - isValidating: isValidatingERC20, - isValid: isValidERC20, - error: validationErrorERC20, - } = useERC20ERC721Validation({ - address: tokenGateAddress, - enabled: validationEnabled && !isERC1155, - }); - - const { - isValidating: isValidatingERC1155, - isValid: isValidERC1155, - error: validationErrorERC1155, - } = useERC1155Validation({ - address: tokenGateAddress, - enabled: validationEnabled && isERC1155, - }); - - // Combine validation results based on token type - const isValidating = isERC1155 ? isValidatingERC1155 : isValidatingERC20; - const isValidToken = isERC1155 ? isValidERC1155 : isValidERC20; - const validationError = isERC1155 ? validationErrorERC1155 : validationErrorERC20; - - // Update validation state in dispute context - useEffect(() => { - if (isGatedDisputeKit && disputeData.disputeKitData) { - const currentData = disputeData.disputeKitData as IGatedDisputeData; - if (currentData.isTokenGateValid !== isValidToken) { - setDisputeData({ - ...disputeData, - disputeKitData: { ...currentData, isTokenGateValid: isValidToken }, - }); - } - } - }, [isValidToken, isGatedDisputeKit, disputeData.disputeKitData, setDisputeData]); - - const handleCourtChange = (courtId: string) => { - if (disputeData.courtId !== courtId) { - setDisputeData({ ...disputeData, courtId, disputeKitId: undefined }); - } - }; - - const handleDisputeKitChange = (newValue: string | number) => { - const options = disputeKitOptions.find((dk) => String(dk.value) === String(newValue)); - const gatedDisputeKitData: IGatedDisputeData | undefined = - (options?.gated ?? false) - ? { - type: "gated", - tokenGate: "", - isERC1155: false, - tokenId: "0", - } - : undefined; - setDisputeData({ ...disputeData, disputeKitId: Number(newValue), disputeKitData: gatedDisputeKitData }); - }; - - const handleTokenAddressChange = (event: React.ChangeEvent) => { - const currentData = disputeData.disputeKitData as IGatedDisputeData; - setDisputeData({ - ...disputeData, - disputeKitData: { - ...currentData, - tokenGate: event.target.value, - isTokenGateValid: null, // Reset validation state when address changes - }, - }); - }; - - const handleERC1155TokenChange = (event: React.ChangeEvent) => { - const currentData = disputeData.disputeKitData as IGatedDisputeData; - setDisputeData({ - ...disputeData, - disputeKitData: { - ...currentData, - isERC1155: event.target.checked, - isTokenGateValid: null, // Reset validation state when token type changes - }, - }); - }; - - const handleTokenIdChange = (event: React.ChangeEvent) => { - const currentData = disputeData.disputeKitData as IGatedDisputeData; - setDisputeData({ - ...disputeData, - disputeKitData: { ...currentData, tokenId: event.target.value }, - }); - }; - - return ( - -
- {items ? ( - typeof path === "string" && handleCourtChange(path.split("/").pop()!)} - placeholder="Select Court" - value={`/courts/${disputeData.courtId}`} - /> - ) : ( - - )} - {disputeData?.courtId && disputeKitOptions.length > 0 && ( - - )} - {isGatedDisputeKit && ( - <> - - {tokenGateAddress.trim() !== "" && ( - - - - {isValidating && `Validating ${isERC1155 ? "ERC-1155" : "ERC-20 or ERC-721"} token...`} - {validationError && validationError} - {isValidToken === true && `Valid ${isERC1155 ? "ERC-1155" : "ERC-20 or ERC-721"} token`} - - - )} - - {(disputeData.disputeKitData as IGatedDisputeData)?.isERC1155 && ( - - )} - - )} - - - - - - ); -}; - -export default Court; diff --git a/web/src/pages/Resolver/Parameters/Court/FeatureSelection/JurorEligibility.tsx b/web/src/pages/Resolver/Parameters/Court/FeatureSelection/JurorEligibility.tsx new file mode 100644 index 000000000..3e1febbd4 --- /dev/null +++ b/web/src/pages/Resolver/Parameters/Court/FeatureSelection/JurorEligibility.tsx @@ -0,0 +1,248 @@ +import React, { useEffect, useMemo, useState } from "react"; +import styled from "styled-components"; + +import { Field, Radio } from "@kleros/ui-components-library"; + +import { IGatedDisputeData, useNewDisputeContext } from "context/NewDisputeContext"; +import { useERC1155Validation, useERC20ERC721Validation } from "hooks/useTokenAddressValidation"; + +import { DisputeKits } from "src/consts"; +import { isUndefined } from "src/utils"; + +import WithHelpTooltip from "components/WithHelpTooltip"; + +const Container = styled.div` + width: 100%; + display: flex; + flex-direction: column; + gap: 16px; + align-items: start; + padding-top: 16px; +`; + +const HeaderContainer = styled.div` + width: 100%; + padding-top: 16px; +`; + +const Header = styled.h2` + font-size: 16px; + font-weight: 600; + margin: 0; +`; + +const SubTitle = styled.p` + font-size: 14px; + color: ${({ theme }) => theme.secondaryText}; + padding: 0; + margin: 0; +`; + +const FieldContainer = styled.div` + width: 100%; + padding-left: 32px; +`; +const StyledField = styled(Field)` + width: 100%; + margin-top: 8px; + margin-bottom: 32px; + > small { + margin-top: 16px; + } +`; + +const StyledRadio = styled(Radio)` + font-size: 14px; +`; + +enum EligibilityType { + Classic, + GatedERC20, + GatedERC1155, +} + +const JurorEligibility: React.FC = () => { + const [eligibilityType, setEligibilityType] = useState(); + const { disputeData, setDisputeData, disputeKitOptions } = useNewDisputeContext(); + + const tokenGateAddress = (disputeData.disputeKitData as IGatedDisputeData)?.tokenGate ?? ""; + const validationEnabled = !isUndefined(tokenGateAddress) && tokenGateAddress.trim() !== ""; + const isERC1155 = eligibilityType === EligibilityType.GatedERC1155; + const { + isValidating: isValidatingERC20, + isValid: isValidERC20, + error: validationErrorERC20, + } = useERC20ERC721Validation({ + address: tokenGateAddress, + enabled: validationEnabled && !isERC1155, + }); + + const { + isValidating: isValidatingERC1155, + isValid: isValidERC1155, + error: validationErrorERC1155, + } = useERC1155Validation({ + address: tokenGateAddress, + enabled: validationEnabled && isERC1155, + }); + + // Combine validation results based on token type + const isValidating = isERC1155 ? isValidatingERC1155 : isValidatingERC20; + const isValidToken = isERC1155 ? isValidERC1155 : isValidERC20; + const validationError = isERC1155 ? validationErrorERC1155 : validationErrorERC20; + + const [validationMessage, variant] = useMemo(() => { + if (isValidating) return [`Validating ${isERC1155 ? "ERC-1155" : "ERC-20 or ERC-721"} token...`, "info"]; + else if (validationError) return [validationError, "error"]; + else if (isValidToken === true) return [`Valid ${isERC1155 ? "ERC-1155" : "ERC-20 or ERC-721"} token`, "success"]; + else return [undefined, "info"]; + }, [isValidating, validationError, isERC1155, isValidToken]); + + // Update validation state in dispute context + useEffect(() => { + if (disputeData.disputeKitData) { + const currentData = disputeData.disputeKitData as IGatedDisputeData; + if (currentData.isTokenGateValid !== isValidToken) { + setDisputeData({ + ...disputeData, + disputeKitData: { ...currentData, isTokenGateValid: isValidToken }, + }); + } + } + }, [isValidToken, disputeData.disputeKitData, setDisputeData]); + + const handleTokenAddressChange = (event: React.ChangeEvent) => { + const currentData = disputeData.disputeKitData as IGatedDisputeData; + setDisputeData({ + ...disputeData, + disputeKitData: { + ...currentData, + tokenGate: event.target.value, + isTokenGateValid: null, // Reset validation state when address changes + }, + }); + }; + + const handleTokenIdChange = (event: React.ChangeEvent) => { + const currentData = disputeData.disputeKitData as IGatedDisputeData; + setDisputeData({ + ...disputeData, + disputeKitData: { ...currentData, tokenId: event.target.value }, + }); + }; + + useEffect(() => { + if (eligibilityType === EligibilityType.Classic) { + const disputeKit = disputeKitOptions.find((dk) => dk.text === DisputeKits.Classic); + + setDisputeData({ ...disputeData, disputeKitId: disputeKit?.value, disputeKitData: undefined }); + } else if (eligibilityType === EligibilityType.GatedERC20 || eligibilityType === EligibilityType.GatedERC1155) { + const disputeKitGated = disputeKitOptions.find((dk) => dk.text === DisputeKits.Gated); + const disputeKitGatedShutter = disputeKitOptions.find((dk) => dk.text === DisputeKits.GatedShutter); + + const currentDisputeKit = disputeKitOptions.find((dk) => dk.value === disputeData.disputeKitId); + + const disputeKitData: IGatedDisputeData = { + ...(disputeData.disputeKitData as IGatedDisputeData), + type: "gated", + isERC1155: eligibilityType === EligibilityType.GatedERC1155, + }; + // classic is selected, so here we change it to TokenGated + if (currentDisputeKit?.text === DisputeKits.Classic) { + setDisputeData({ + ...disputeData, + disputeKitId: disputeKitGated?.value, + disputeKitData, + }); + } + // shutter is selected, so here we change it to TokenGatedShutter + else if (currentDisputeKit?.text === DisputeKits.Shutter) { + setDisputeData({ + ...disputeData, + disputeKitId: disputeKitGatedShutter?.value, + disputeKitData, + }); + } else { + setDisputeData({ + ...disputeData, + disputeKitId: disputeKitGated?.value, + disputeKitData, + }); + } + } + }, [eligibilityType, disputeKitOptions]); + + return ( + + +
Jurors Eligibility
+ Who can be selected as a juror?. +
+ + setEligibilityType(EligibilityType.Classic)} + checked={eligibilityType === EligibilityType.Classic} + /> + + + setEligibilityType(EligibilityType.GatedERC20)} + checked={eligibilityType === EligibilityType.GatedERC20} + /> + + {eligibilityType === EligibilityType.GatedERC20 ? ( + + + + ) : null} + + setEligibilityType(EligibilityType.GatedERC1155)} + checked={eligibilityType === EligibilityType.GatedERC1155} + /> + + {eligibilityType === EligibilityType.GatedERC1155 ? ( + + + + + ) : null} +
+ ); +}; + +export default JurorEligibility; diff --git a/web/src/pages/Resolver/Parameters/Court/FeatureSelection/ShieldedVoting.tsx b/web/src/pages/Resolver/Parameters/Court/FeatureSelection/ShieldedVoting.tsx new file mode 100644 index 000000000..85a5caf98 --- /dev/null +++ b/web/src/pages/Resolver/Parameters/Court/FeatureSelection/ShieldedVoting.tsx @@ -0,0 +1,107 @@ +import React, { useEffect, useState } from "react"; +import styled from "styled-components"; + +import { Radio } from "@kleros/ui-components-library"; + +import { useNewDisputeContext } from "context/NewDisputeContext"; + +import { DisputeKits } from "src/consts"; + +import WithHelpTooltip from "components/WithHelpTooltip"; + +const VotingContainer = styled.div` + width: 100%; + display: flex; + flex-direction: column; + gap: 16px; + align-items: start; + border-bottom: 1px solid ${({ theme }) => theme.stroke}; + padding-bottom: 16px; +`; + +const VotingHeaderContainer = styled.div` + width: 100%; + padding-top: 16px; +`; + +const VotingHeader = styled.h2` + font-size: 16px; + font-weight: 600; + margin: 0; +`; + +const VotingSubTitle = styled.p` + font-size: 14px; + color: ${({ theme }) => theme.secondaryText}; + padding: 0; + margin: 0; +`; + +const StyledRadio = styled(Radio)` + font-size: 14px; +`; + +enum VotingType { + OneStep = "Shutter", + TwoStep = "Classic", +} + +// Selects one of Classic or Shutter DisputeKit +const ShieldedVoting: React.FC = () => { + const [votingType, setVotingType] = useState(); + const { disputeData, setDisputeData, disputeKitOptions } = useNewDisputeContext(); + + // we try to keep this structure among feature components + // keep in mind the other options that will need to be disabled if a certain feature is selected + useEffect(() => { + // disable TokenGatedShutter if selected, if TokenGated Selected do nothing + if (votingType === VotingType.TwoStep && disputeData.disputeKitData?.type !== "gated") { + const disputeKit = disputeKitOptions.find((dk) => dk.text === DisputeKits.Classic); + + setDisputeData({ ...disputeData, disputeKitId: disputeKit?.value }); + } + + if (votingType === VotingType.OneStep) { + // user has already selected TokenGated, so selecting Shutter here, we need to select TokenGatedShutter + if (disputeData.disputeKitData?.type === "gated") { + const disputeKit = disputeKitOptions.find((dk) => dk.text === DisputeKits.GatedShutter); + + // no need to set DisputeKitData, will already be set by JurorEligibility + setDisputeData({ ...disputeData, disputeKitId: disputeKit?.value }); + } else { + const disputeKit = disputeKitOptions.find((dk) => dk.text === DisputeKits.Shutter); + + setDisputeData({ ...disputeData, disputeKitId: disputeKit?.value }); + } + } + }, [votingType]); + + return ( + + + Shielded Voting + It hides the jurors' votes until the end of the voting period. + + + setVotingType(VotingType.OneStep)} + checked={votingType === VotingType.OneStep} + /> + + + setVotingType(VotingType.TwoStep)} + checked={votingType === VotingType.TwoStep} + /> + + + ); +}; + +export default ShieldedVoting; diff --git a/web/src/pages/Resolver/Parameters/Court/FeatureSelection/index.tsx b/web/src/pages/Resolver/Parameters/Court/FeatureSelection/index.tsx new file mode 100644 index 000000000..8fdb8d7af --- /dev/null +++ b/web/src/pages/Resolver/Parameters/Court/FeatureSelection/index.tsx @@ -0,0 +1,71 @@ +import React, { useEffect, useMemo, useState } from "react"; +import styled from "styled-components"; + +import { Card } from "@kleros/ui-components-library"; + +import { useNewDisputeContext } from "context/NewDisputeContext"; + +import { DisputeKits } from "src/consts"; + +import JurorEligibility from "./JurorEligibility"; +import ShieldedVoting from "./ShieldedVoting"; + +const Container = styled(Card)` + width: 100%; + height: auto; + padding: 32px; + display: flex; + flex-direction: column; + margin-top: 16px; +`; + +const SubTitle = styled.p` + font-size: 14px; + color: ${({ theme }) => theme.secondaryBlue}; + padding: 0; + margin: 0; +`; + +const FeatureSelection: React.FC = () => { + const [showFeatures, setShowFeatures] = useState(false); + const { disputeKitOptions, disputeData, setDisputeData } = useNewDisputeContext(); + + // if supported dispute kits have Classic and Shutter + const showVotingFeatures = useMemo(() => { + return ( + disputeKitOptions.some((dk) => dk.text === DisputeKits.Classic) && + disputeKitOptions.some((dk) => dk.text === DisputeKits.Shutter) + ); + }, [disputeKitOptions]); + + // if supported dispute kits have Classic, TokenGated, TokenGatedShutter + const showEligibilityFeatures = useMemo(() => { + return ( + disputeKitOptions.some((dk) => dk.text === DisputeKits.Classic) && + disputeKitOptions.some((dk) => dk.text === DisputeKits.GatedShutter) && + disputeKitOptions.some((dk) => dk.text === DisputeKits.Gated) + ); + }, [disputeKitOptions]); + + useEffect(() => { + // there's only one, NOTE: what happens when here only TokenGated is support? we need the value + if (disputeKitOptions.length === 1) { + const disputeKit = disputeKitOptions[0]; + setDisputeData({ ...disputeData, disputeKitId: disputeKit.value }); + setShowFeatures(false); + } else { + setShowFeatures(true); + } + }, [disputeKitOptions]); + + if (!showFeatures) return null; + return ( + + Additional features available in this court: + {showVotingFeatures ? : null} + {showEligibilityFeatures ? : null} + + ); +}; + +export default FeatureSelection; diff --git a/web/src/pages/Resolver/Parameters/Court/index.tsx b/web/src/pages/Resolver/Parameters/Court/index.tsx new file mode 100644 index 000000000..4e50c0bdc --- /dev/null +++ b/web/src/pages/Resolver/Parameters/Court/index.tsx @@ -0,0 +1,92 @@ +import React, { useMemo } from "react"; +import styled, { css } from "styled-components"; + +import { AlertMessage, DropdownCascader } from "@kleros/ui-components-library"; + +import { useNewDisputeContext } from "context/NewDisputeContext"; +import { rootCourtToItems, useCourtTree } from "hooks/queries/useCourtTree"; +import { isUndefined } from "utils/index"; + +import { landscapeStyle } from "styles/landscapeStyle"; +import { responsiveSize } from "styles/responsiveSize"; + +import { StyledSkeleton } from "components/StyledSkeleton"; +import Header from "pages/Resolver/Header"; + +import NavigationButtons from "../../NavigationButtons"; + +import FeatureSelection from "./FeatureSelection"; + +const Container = styled.div` + display: flex; + flex-direction: column; + align-items: center; + + ${landscapeStyle( + () => css` + padding-bottom: 115px; + ` + )} +`; + +const StyledDropdownCascader = styled(DropdownCascader)` + width: 84vw; + ${landscapeStyle( + () => css` + width: ${responsiveSize(442, 700, 900)}; + ` + )} + > button { + width: 100%; + } +`; + +const AlertMessageContainer = styled.div` + width: 84vw; + ${landscapeStyle( + () => css` + width: ${responsiveSize(442, 700, 900)}; + ` + )} + margin-top: 24px; +`; + +const Court: React.FC = () => { + const { disputeData, setDisputeData } = useNewDisputeContext(); + const { data: courtTree } = useCourtTree(); + const items = useMemo(() => !isUndefined(courtTree?.court) && [rootCourtToItems(courtTree.court)], [courtTree]); + + const handleCourtChange = (courtId: string) => { + if (disputeData.courtId !== courtId) { + setDisputeData({ ...disputeData, courtId, disputeKitId: undefined }); + } + }; + + return ( + +
+ {items ? ( + typeof path === "string" && handleCourtChange(path.split("/").pop()!)} + placeholder="Select Court" + value={`/courts/${disputeData.courtId}`} + /> + ) : ( + + )} + + + + + + + + ); +}; + +export default Court; From 14fedbb966cc088d5ec7c4103740b74e26d31b2b Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Tue, 2 Sep 2025 23:20:04 +0100 Subject: [PATCH 074/175] fix: minStake check after setStakePenalty should return the remaining PNK to the juror --- contracts/src/arbitration/KlerosCoreBase.sol | 22 +++-- contracts/src/arbitration/KlerosCoreNeo.sol | 2 +- .../src/arbitration/SortitionModuleBase.sol | 25 ++++-- .../src/arbitration/SortitionModuleNeo.sol | 5 +- .../interfaces/ISortitionModule.sol | 7 +- .../university/KlerosCoreUniversity.sol | 18 ++-- .../university/SortitionModuleUniversity.sol | 17 +++- .../test/foundry/KlerosCore_Execution.t.sol | 82 +++++++++++++++++-- 8 files changed, 148 insertions(+), 30 deletions(-) diff --git a/contracts/src/arbitration/KlerosCoreBase.sol b/contracts/src/arbitration/KlerosCoreBase.sol index f8b7f028b..d6821b59c 100644 --- a/contracts/src/arbitration/KlerosCoreBase.sol +++ b/contracts/src/arbitration/KlerosCoreBase.sol @@ -464,16 +464,16 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable /// @param _newStake The new stake. /// Note that the existing delayed stake will be nullified as non-relevant. function setStake(uint96 _courtID, uint256 _newStake) external virtual whenNotPaused { - _setStake(msg.sender, _courtID, _newStake, OnError.Revert); + _setStake(msg.sender, _courtID, _newStake, false, OnError.Revert); } - /// @dev Sets the stake of a specified account in a court, typically to apply a delayed stake or unstake inactive jurors. + /// @dev Sets the stake of a specified account in a court without delaying stake changes, typically to apply a delayed stake or unstake inactive jurors. /// @param _account The account whose stake is being set. /// @param _courtID The ID of the court. /// @param _newStake The new stake. function setStakeBySortitionModule(address _account, uint96 _courtID, uint256 _newStake) external { if (msg.sender != address(sortitionModule)) revert SortitionModuleOnly(); - _setStake(_account, _courtID, _newStake, OnError.Return); + _setStake(_account, _courtID, _newStake, true, OnError.Return); } /// @dev Transfers PNK to the juror by SortitionModule. @@ -796,10 +796,10 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable if (pnkBalance == 0 || !disputeKit.isVoteActive(_params.disputeID, _params.round, _params.repartition)) { // The juror is inactive or their balance is can't cover penalties anymore, unstake them from all courts. - sortitionModule.setJurorInactive(account); + sortitionModule.unstakeByCoreFromAllCourts(account); } else if (newCourtStake < courts[penalizedInCourtID].minStake) { // The juror's balance fell below the court minStake, unstake them from the court. - sortitionModule.setStake(account, penalizedInCourtID, 0, 0, 0); + sortitionModule.unstakeByCore(account, penalizedInCourtID); } if (_params.repartition == _params.numberOfVotesInRound - 1 && _params.coherentCount == 0) { @@ -1138,9 +1138,16 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable /// @param _account The account to set the stake for. /// @param _courtID The ID of the court to set the stake for. /// @param _newStake The new stake. + /// @param _noDelay True if the stake change should not be delayed. /// @param _onError Whether to revert or return false on error. /// @return Whether the stake was successfully set or not. - function _setStake(address _account, uint96 _courtID, uint256 _newStake, OnError _onError) internal returns (bool) { + function _setStake( + address _account, + uint96 _courtID, + uint256 _newStake, + bool _noDelay, + OnError _onError + ) internal returns (bool) { if (_courtID == FORKING_COURT || _courtID >= courts.length) { _stakingFailed(_onError, StakingResult.CannotStakeInThisCourt); // Staking directly into the forking court is not allowed. return false; @@ -1152,7 +1159,8 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) = sortitionModule.validateStake( _account, _courtID, - _newStake + _newStake, + _noDelay ); if (stakingResult != StakingResult.Successful && stakingResult != StakingResult.Delayed) { _stakingFailed(_onError, stakingResult); diff --git a/contracts/src/arbitration/KlerosCoreNeo.sol b/contracts/src/arbitration/KlerosCoreNeo.sol index a72319fc5..ebfd1bb72 100644 --- a/contracts/src/arbitration/KlerosCoreNeo.sol +++ b/contracts/src/arbitration/KlerosCoreNeo.sol @@ -108,7 +108,7 @@ contract KlerosCoreNeo is KlerosCoreBase { /// Note that the existing delayed stake will be nullified as non-relevant. function setStake(uint96 _courtID, uint256 _newStake) external override whenNotPaused { if (jurorNft.balanceOf(msg.sender) == 0) revert NotEligibleForStaking(); - super._setStake(msg.sender, _courtID, _newStake, OnError.Revert); + super._setStake(msg.sender, _courtID, _newStake, false, OnError.Revert); } // ************************************* // diff --git a/contracts/src/arbitration/SortitionModuleBase.sol b/contracts/src/arbitration/SortitionModuleBase.sol index 58e5bb564..59c394722 100644 --- a/contracts/src/arbitration/SortitionModuleBase.sol +++ b/contracts/src/arbitration/SortitionModuleBase.sol @@ -232,21 +232,24 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr /// @param _account The address of the juror. /// @param _courtID The ID of the court. /// @param _newStake The new stake. + /// @param _noDelay True if the stake change should not be delayed. /// @return pnkDeposit The amount of PNK to be deposited. /// @return pnkWithdrawal The amount of PNK to be withdrawn. /// @return stakingResult The result of the staking operation. function validateStake( address _account, uint96 _courtID, - uint256 _newStake + uint256 _newStake, + bool _noDelay ) external override onlyByCore returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { - (pnkDeposit, pnkWithdrawal, stakingResult) = _validateStake(_account, _courtID, _newStake); + (pnkDeposit, pnkWithdrawal, stakingResult) = _validateStake(_account, _courtID, _newStake, _noDelay); } function _validateStake( address _account, uint96 _courtID, - uint256 _newStake + uint256 _newStake, + bool _noDelay ) internal virtual returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { Juror storage juror = jurors[_account]; uint256 currentStake = stakeOf(_account, _courtID); @@ -260,7 +263,7 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr return (0, 0, StakingResult.CannotStakeZeroWhenNoStake); // Forbid staking 0 amount when current stake is 0 to avoid flaky behaviour. } - if (phase != Phase.staking) { + if (phase != Phase.staking && !_noDelay) { // Store the stake change as delayed, to be applied when the phase switches back to Staking. DelayedStake storage delayedStake = delayedStakes[++delayedStakeWriteIndex]; delayedStake.account = _account; @@ -433,13 +436,25 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr /// `k` is the minimum number of children per node of one of these courts' sortition sum tree, /// and `j` is the maximum number of jurors that ever staked in one of these courts simultaneously. /// @param _account The juror to unstake. - function setJurorInactive(address _account) external override onlyByCore { + function unstakeByCoreFromAllCourts(address _account) external override onlyByCore { uint96[] memory courtIDs = getJurorCourtIDs(_account); for (uint256 j = courtIDs.length; j > 0; j--) { core.setStakeBySortitionModule(_account, courtIDs[j - 1], 0); } } + /// @dev Unstakes the inactive juror from a specific court. + /// `O(n * (p * log_k(j)) )` where + /// `n` is the number of courts the juror has staked in, + /// `p` is the depth of the court tree, + /// `k` is the minimum number of children per node of one of these courts' sortition sum tree, + /// and `j` is the maximum number of jurors that ever staked in one of these courts simultaneously. + /// @param _account The juror to unstake. + /// @param _courtID The ID of the court. + function unstakeByCore(address _account, uint96 _courtID) external override onlyByCore { + core.setStakeBySortitionModule(_account, _courtID, 0); + } + /// @dev Gives back the locked PNKs in case the juror fully unstaked earlier. /// Note that since locked and staked PNK are async it is possible for the juror to have positive staked PNK balance /// while having 0 stake in courts and 0 locked tokens (eg. when the juror fully unstaked during dispute and later got his tokens unlocked). diff --git a/contracts/src/arbitration/SortitionModuleNeo.sol b/contracts/src/arbitration/SortitionModuleNeo.sol index d106d5d9b..7c2a3b53b 100644 --- a/contracts/src/arbitration/SortitionModuleNeo.sol +++ b/contracts/src/arbitration/SortitionModuleNeo.sol @@ -77,7 +77,8 @@ contract SortitionModuleNeo is SortitionModuleBase { function _validateStake( address _account, uint96 _courtID, - uint256 _newStake + uint256 _newStake, + bool _noDelay ) internal override onlyByCore returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { uint256 currentStake = stakeOf(_account, _courtID); bool stakeIncrease = _newStake > currentStake; @@ -98,6 +99,6 @@ contract SortitionModuleNeo is SortitionModuleBase { totalStaked -= stakeChange; } } - (pnkDeposit, pnkWithdrawal, stakingResult) = super._validateStake(_account, _courtID, _newStake); + (pnkDeposit, pnkWithdrawal, stakingResult) = super._validateStake(_account, _courtID, _newStake, _noDelay); } } diff --git a/contracts/src/arbitration/interfaces/ISortitionModule.sol b/contracts/src/arbitration/interfaces/ISortitionModule.sol index 39646dcb4..52a077ec3 100644 --- a/contracts/src/arbitration/interfaces/ISortitionModule.sol +++ b/contracts/src/arbitration/interfaces/ISortitionModule.sol @@ -18,7 +18,8 @@ interface ISortitionModule { function validateStake( address _account, uint96 _courtID, - uint256 _newStake + uint256 _newStake, + bool _noDelay ) external returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult); function setStake( @@ -37,7 +38,9 @@ interface ISortitionModule { function setStakeReward(address _account, uint96 _courtID, uint256 _reward) external returns (bool success); - function setJurorInactive(address _account) external; + function unstakeByCoreFromAllCourts(address _account) external; + + function unstakeByCore(address _account, uint96 _courtID) external; function lockStake(address _account, uint256 _relativeAmount) external; diff --git a/contracts/src/arbitration/university/KlerosCoreUniversity.sol b/contracts/src/arbitration/university/KlerosCoreUniversity.sol index 57626a958..b75f16418 100644 --- a/contracts/src/arbitration/university/KlerosCoreUniversity.sol +++ b/contracts/src/arbitration/university/KlerosCoreUniversity.sol @@ -452,7 +452,7 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { /// @param _newStake The new stake. /// Note that the existing delayed stake will be nullified as non-relevant. function setStake(uint96 _courtID, uint256 _newStake) external { - _setStake(msg.sender, _courtID, _newStake, OnError.Revert); + _setStake(msg.sender, _courtID, _newStake, false, OnError.Revert); } /// @dev Sets the stake of a specified account in a court, typically to apply a delayed stake or unstake inactive jurors. @@ -461,7 +461,7 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { /// @param _newStake The new stake. function setStakeBySortitionModule(address _account, uint96 _courtID, uint256 _newStake) external { if (msg.sender != address(sortitionModule)) revert SortitionModuleOnly(); - _setStake(_account, _courtID, _newStake, OnError.Return); + _setStake(_account, _courtID, _newStake, true, OnError.Return); } /// @dev Transfers PNK to the juror by SortitionModule. @@ -791,7 +791,7 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { if (pnkBalance == 0 || !disputeKit.isVoteActive(_params.disputeID, _params.round, _params.repartition)) { // The juror is inactive or their balance is can't cover penalties anymore, unstake them from all courts. - sortitionModule.setJurorInactive(account); + sortitionModule.unstakeByCoreFromAllCourts(account); } else if (newCourtStake < courts[penalizedInCourtID].minStake) { // The juror's balance fell below the court minStake, unstake them from the court. sortitionModule.setStake(account, penalizedInCourtID, 0, 0, 0); @@ -1080,9 +1080,16 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { /// @param _account The account to set the stake for. /// @param _courtID The ID of the court to set the stake for. /// @param _newStake The new stake. + /// @param _noDelay True if the stake change should not be delayed. /// @param _onError Whether to revert or return false on error. /// @return Whether the stake was successfully set or not. - function _setStake(address _account, uint96 _courtID, uint256 _newStake, OnError _onError) internal returns (bool) { + function _setStake( + address _account, + uint96 _courtID, + uint256 _newStake, + bool _noDelay, + OnError _onError + ) internal returns (bool) { if (_courtID == FORKING_COURT || _courtID >= courts.length) { _stakingFailed(_onError, StakingResult.CannotStakeInThisCourt); // Staking directly into the forking court is not allowed. return false; @@ -1094,7 +1101,8 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) = sortitionModule.validateStake( _account, _courtID, - _newStake + _newStake, + _noDelay ); if (stakingResult != StakingResult.Successful) { _stakingFailed(_onError, stakingResult); diff --git a/contracts/src/arbitration/university/SortitionModuleUniversity.sol b/contracts/src/arbitration/university/SortitionModuleUniversity.sol index d6a220bf6..b127b7cf5 100644 --- a/contracts/src/arbitration/university/SortitionModuleUniversity.sol +++ b/contracts/src/arbitration/university/SortitionModuleUniversity.sol @@ -139,7 +139,8 @@ contract SortitionModuleUniversity is ISortitionModuleUniversity, UUPSProxiable, function validateStake( address _account, uint96 _courtID, - uint256 _newStake + uint256 _newStake, + bool /*_noDelay*/ ) external view @@ -302,13 +303,25 @@ contract SortitionModuleUniversity is ISortitionModuleUniversity, UUPSProxiable, /// `k` is the minimum number of children per node of one of these courts' sortition sum tree, /// and `j` is the maximum number of jurors that ever staked in one of these courts simultaneously. /// @param _account The juror to unstake. - function setJurorInactive(address _account) external override onlyByCore { + function unstakeByCoreFromAllCourts(address _account) external override onlyByCore { uint96[] memory courtIDs = getJurorCourtIDs(_account); for (uint256 j = courtIDs.length; j > 0; j--) { core.setStakeBySortitionModule(_account, courtIDs[j - 1], 0); } } + /// @dev Unstakes the inactive juror from a specific court. + /// `O(n * (p * log_k(j)) )` where + /// `n` is the number of courts the juror has staked in, + /// `p` is the depth of the court tree, + /// `k` is the minimum number of children per node of one of these courts' sortition sum tree, + /// and `j` is the maximum number of jurors that ever staked in one of these courts simultaneously. + /// @param _account The juror to unstake. + /// @param _courtID The ID of the court. + function unstakeByCore(address _account, uint96 _courtID) external override onlyByCore { + core.setStakeBySortitionModule(_account, _courtID, 0); + } + /// @dev Gives back the locked PNKs in case the juror fully unstaked earlier. /// Note that since locked and staked PNK are async it is possible for the juror to have positive staked PNK balance /// while having 0 stake in courts and 0 locked tokens (eg. when the juror fully unstaked during dispute and later got his tokens unlocked). diff --git a/contracts/test/foundry/KlerosCore_Execution.t.sol b/contracts/test/foundry/KlerosCore_Execution.t.sol index 4bf2a452a..c5763964e 100644 --- a/contracts/test/foundry/KlerosCore_Execution.t.sol +++ b/contracts/test/foundry/KlerosCore_Execution.t.sol @@ -16,7 +16,7 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { uint256 disputeID = 0; vm.prank(staker1); - core.setStake(GENERAL_COURT, 1500); + core.setStake(GENERAL_COURT, 2000); vm.prank(disputer); arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); vm.warp(block.timestamp + minStakingTime); @@ -111,7 +111,7 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { core.execute(disputeID, 0, 3); // Do 3 iterations to check penalties first (uint256 totalStaked, uint256 totalLocked, , ) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); - assertEq(totalStaked, 500, "totalStaked should be penalized"); // 1500 - 1000 + assertEq(totalStaked, 1000, "totalStaked should be penalized"); // 2000 - 1000 assertEq(totalLocked, 0, "Tokens should be released for staker1"); (, totalLocked, , ) = sortitionModule.getJurorBalance(staker2, GENERAL_COURT); assertEq(totalLocked, 2000, "Tokens should still be locked for staker2"); @@ -148,8 +148,8 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { assertEq(staker1.balance, 0, "Wrong balance of the staker1"); assertEq(staker2.balance, 0.09 ether, "Wrong balance of the staker2"); - assertEq(pinakion.balanceOf(address(core)), 21500, "Wrong token balance of the core"); // Was 21500. 1000 was transferred to staker2 - assertEq(pinakion.balanceOf(staker1), 999999999999998500, "Wrong token balance of staker1"); + assertEq(pinakion.balanceOf(address(core)), 22000, "Wrong token balance of the core"); // Was 21500. 1000 was transferred to staker2 + assertEq(pinakion.balanceOf(staker1), 999999999999998000, "Wrong token balance of staker1"); assertEq(pinakion.balanceOf(staker2), 999999999999980000, "Wrong token balance of staker2"); // 20k stake and 1k added as a reward, thus -19k from the default } @@ -213,8 +213,8 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { assertEq(staker1.balance, 0, "Wrong balance of the staker1"); assertEq(owner.balance, ownerBalance + 0.09 ether, "Wrong balance of the owner"); - assertEq(pinakion.balanceOf(address(core)), 17000, "Wrong token balance of the core"); - assertEq(pinakion.balanceOf(staker1), 999999999999980000, "Wrong token balance of staker1"); + assertEq(pinakion.balanceOf(address(core)), 0, "Wrong token balance of the core"); // The inactive juror got unstaked regardless of the phase (`noDelay` is true) + assertEq(pinakion.balanceOf(staker1), 999999999999997000, "Wrong token balance of staker1"); assertEq(pinakion.balanceOf(owner), ownerTokenBalance + 3000, "Wrong token balance of owner"); } @@ -345,6 +345,76 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { assertEq(nbCourts, 0, "Should unstake from all courts"); } + function test_execute_UnstakeBelowMinStake() public { + uint256 disputeID = 0; + + vm.prank(staker1); + core.setStake(GENERAL_COURT, 1200); + + vm.prank(staker2); + core.setStake(GENERAL_COURT, 10000); + + assertEq(pinakion.balanceOf(address(core)), 11200, "Wrong token balance of the core"); + assertEq(pinakion.balanceOf(staker1), 999999999999998800, "Wrong token balance of staker1"); + assertEq(pinakion.balanceOf(staker2), 999999999999990000, "Wrong token balance of staker2"); + + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.warp(block.timestamp + rngLookahead); + sortitionModule.passPhase(); // Drawing phase + + core.draw(disputeID, DEFAULT_NB_OF_JURORS); + + (uint256 totalStaked, uint256 totalLocked, , uint256 nbCourts) = sortitionModule.getJurorBalance( + staker1, + GENERAL_COURT + ); + assertEq(totalStaked, 1200, "Wrong totalStaked"); + assertEq(totalLocked, 1000, "Wrong totalLocked"); // Juror only staked 1000 but will fall below minStake with a bad vote + assertEq(nbCourts, 1, "Wrong number of courts"); + + sortitionModule.passPhase(); // Staking phase. Change to staking so we don't have to deal with delayed stakes. + + vm.warp(block.timestamp + timesPerPeriod[0]); + core.passPeriod(disputeID); // Vote + + uint256[] memory voteIDs = new uint256[](1); + voteIDs[0] = 0; + vm.prank(staker1); + disputeKit.castVote(disputeID, voteIDs, 1, 0, "XYZ"); // 1 incoherent vote should make the juror's stake below minStake + + voteIDs = new uint256[](2); + voteIDs[0] = 1; + voteIDs[1] = 2; + vm.prank(staker2); + disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); + + core.passPeriod(disputeID); // Appeal + + vm.warp(block.timestamp + timesPerPeriod[3]); + core.passPeriod(disputeID); // Execution + + vm.expectEmit(true, true, true, true); + emit SortitionModuleBase.StakeSet(staker1, GENERAL_COURT, 0, 0); // Juror balance should be below minStake and should be unstaked from the court automatically. + core.execute(disputeID, 0, 6); + + assertEq(pinakion.balanceOf(address(core)), 11000, "Wrong token balance of the core"); + assertEq(pinakion.balanceOf(staker1), 1 ether - 1000, "Wrong token balance of staker1"); // The juror should have his penalty back as a reward + assertEq(pinakion.balanceOf(staker2), 999999999999990000, "Wrong token balance of staker2"); // No change + + (totalStaked, totalLocked, , nbCourts) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); + assertEq(totalStaked, 0, "Wrong staker1 totalStaked"); + assertEq(totalLocked, 0, "Wrong staker1 totalLocked"); + assertEq(nbCourts, 0, "Wrong staker1 nbCourts"); + + (totalStaked, totalLocked, , nbCourts) = sortitionModule.getJurorBalance(staker2, GENERAL_COURT); + assertEq(totalStaked, 11000, "Wrong staker2 totalStaked"); + assertEq(totalLocked, 0, "Wrong staker2 totalLocked"); + assertEq(nbCourts, 1, "Wrong staker2 nbCourts"); + } + function test_execute_withdrawLeftoverPNK() public { // Return the previously locked tokens uint256 disputeID = 0; From 63d3a642d0cd5602e4d20d2ae9035cca3d331eab Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Wed, 3 Sep 2025 00:38:03 +0100 Subject: [PATCH 075/175] test: cover dispute kit jump to non-classic scenario --- .../test/foundry/KlerosCore_Appeals.t.sol | 149 +++++++++++++++++- 1 file changed, 145 insertions(+), 4 deletions(-) diff --git a/contracts/test/foundry/KlerosCore_Appeals.t.sol b/contracts/test/foundry/KlerosCore_Appeals.t.sol index 189e9cb89..6421cb119 100644 --- a/contracts/test/foundry/KlerosCore_Appeals.t.sol +++ b/contracts/test/foundry/KlerosCore_Appeals.t.sol @@ -146,7 +146,7 @@ contract KlerosCore_AppealsTest is KlerosCore_TestBase { disputeKit.fundAppeal{value: 0.1 ether}(disputeID, 2); } - function test_appeal_fullFundingNoSwitch() public { + function test_appeal_fullFundingNoJump() public { uint256 disputeID = 0; vm.prank(staker1); @@ -208,10 +208,12 @@ contract KlerosCore_AppealsTest is KlerosCore_TestBase { core.passPeriod(disputeID); } - function test_appeal_fullFundingDKCourtSwitch() public { + function test_appeal_fullFundingCourtJumpAndDKJumpToClassic() public { + // Setup: dk2 supported by court2 with dk2._jumpDisputeKitID == DISPUTE_KIT_CLASSIC + // Ensure that court2 jumps to GENERAL_COURT and dk2 jumps to DISPUTE_KIT_CLASSIC uint256 disputeID = 0; DisputeKitClassic dkLogic = new DisputeKitClassic(); - // Create a new DK and court to check the switch + // Create a new DK and court to check the jump bytes memory initDataDk = abi.encodeWithSignature( "initialize(address,address,address,uint256)", owner, @@ -250,6 +252,7 @@ contract KlerosCore_AppealsTest is KlerosCore_TestBase { supportedDK = new uint256[](1); supportedDK[0] = newDkID; core.enableDisputeKits(newCourtID, supportedDK, true); + assertEq(core.isSupported(newCourtID, newDkID), true, "New DK should be supported by new court"); vm.prank(staker1); core.setStake(newCourtID, 20000); @@ -279,7 +282,6 @@ contract KlerosCore_AppealsTest is KlerosCore_TestBase { vm.prank(crowdfunder1); newDisputeKit.fundAppeal{value: 0.63 ether}(disputeID, 1); - vm.prank(crowdfunder2); assertEq(core.isDisputeKitJumping(disputeID), true, "Should be jumping"); @@ -293,6 +295,7 @@ contract KlerosCore_AppealsTest is KlerosCore_TestBase { emit KlerosCoreBase.AppealDecision(disputeID, arbitrable); vm.expectEmit(true, true, true, true); emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.evidence); + vm.prank(crowdfunder2); newDisputeKit.fundAppeal{value: 0.42 ether}(disputeID, 2); (, bool jumped, ) = newDisputeKit.disputes(disputeID); @@ -326,6 +329,144 @@ contract KlerosCore_AppealsTest is KlerosCore_TestBase { assertEq(account, staker1, "Wrong drawn account in the classic DK"); } + function test_appeal_fullFundingCourtJumpAndDKJumpToNonClassic() public { + // Setup: + // dk2 supported by GENERAL_COURT, which is a non-DISPUTE_KIT_CLASSIC + // dk3 supported by court2, with dk3._jumpDisputeKitID == dk2 + // Ensure that court2 jumps to GENERAL_COURT and dk3 jumps to dk2 + uint256 disputeID = 0; + uint96 newCourtID = 2; + uint256 dkID2 = 2; + uint256 dkID3 = 3; + + DisputeKitClassic dkLogic = new DisputeKitClassic(); + + bytes memory initDataDk2 = abi.encodeWithSignature( + "initialize(address,address,address,uint256)", + owner, + address(core), + address(wNative), + DISPUTE_KIT_CLASSIC + ); + UUPSProxy proxyDk2 = new UUPSProxy(address(dkLogic), initDataDk2); + DisputeKitClassic disputeKit2 = DisputeKitClassic(address(proxyDk2)); + + bytes memory initDataDk3 = abi.encodeWithSignature( + "initialize(address,address,address,uint256)", + owner, + address(core), + address(wNative), + dkID2 + ); + UUPSProxy proxyDk3 = new UUPSProxy(address(dkLogic), initDataDk3); + DisputeKitClassic disputeKit3 = DisputeKitClassic(address(proxyDk3)); + + vm.prank(owner); + core.addNewDisputeKit(disputeKit2); + vm.prank(owner); + core.addNewDisputeKit(disputeKit3); + + uint256[] memory supportedDK = new uint256[](2); + supportedDK[0] = DISPUTE_KIT_CLASSIC; + supportedDK[1] = dkID3; + vm.prank(owner); + core.createCourt( + GENERAL_COURT, + hiddenVotes, + minStake, + alpha, + feeForJuror, + 3, // jurors for jump. Low number to ensure jump after the first appeal + [uint256(60), uint256(120), uint256(180), uint256(240)], // Times per period + sortitionExtraData, + supportedDK + ); + assertEq(core.isSupported(newCourtID, dkID3), true, "dkID3 should be supported by new court"); + + vm.prank(owner); + supportedDK[0] = DISPUTE_KIT_CLASSIC; + supportedDK[1] = dkID2; + core.enableDisputeKits(GENERAL_COURT, supportedDK, true); + assertEq(core.isSupported(GENERAL_COURT, dkID2), true, "dkID2 should be supported by GENERAL_COURT"); + + bytes memory newExtraData = abi.encodePacked(uint256(newCourtID), DEFAULT_NB_OF_JURORS, dkID3); + arbitrable.changeArbitratorExtraData(newExtraData); + + vm.prank(staker1); + core.setStake(newCourtID, 20000); + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + vm.warp(block.timestamp + minStakingTime); + sortitionModule.passPhase(); // Generating + vm.warp(block.timestamp + rngLookahead); + sortitionModule.passPhase(); // Drawing phase + + KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 0); + assertEq(round.disputeKitID, dkID3, "Wrong DK ID"); + + core.draw(disputeID, DEFAULT_NB_OF_JURORS); + vm.warp(block.timestamp + timesPerPeriod[0]); + core.passPeriod(disputeID); // Vote + + uint256[] memory voteIDs = new uint256[](3); + voteIDs[0] = 0; + voteIDs[1] = 1; + voteIDs[2] = 2; + + vm.prank(staker1); + disputeKit3.castVote(disputeID, voteIDs, 2, 0, "XYZ"); + + core.passPeriod(disputeID); // Appeal + + vm.prank(crowdfunder1); + disputeKit3.fundAppeal{value: 0.63 ether}(disputeID, 1); + + assertEq(core.isDisputeKitJumping(disputeID), true, "Should be jumping"); + + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.CourtJump(disputeID, 1, newCourtID, GENERAL_COURT); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.DisputeKitJump(disputeID, 1, dkID3, dkID2); + vm.expectEmit(true, true, true, true); + emit DisputeKitClassicBase.DisputeCreation(disputeID, 2, newExtraData); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.AppealDecision(disputeID, arbitrable); + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.evidence); + vm.prank(crowdfunder2); + disputeKit3.fundAppeal{value: 0.42 ether}(disputeID, 2); + + (, bool jumped, ) = disputeKit3.disputes(disputeID); + assertEq(jumped, true, "jumped should be true"); + assertEq( + (disputeKit3.getFundedChoices(disputeID)).length, + 2, + "No fresh round created so the number of funded choices should be 2" + ); + + round = core.getRoundInfo(disputeID, 1); + assertEq(round.disputeKitID, dkID2, "Wrong DK ID"); + assertEq(sortitionModule.disputesWithoutJurors(), 1, "Wrong disputesWithoutJurors count"); + (uint96 courtID, , , , ) = core.disputes(disputeID); + assertEq(courtID, GENERAL_COURT, "Wrong court ID"); + + (, jumped, ) = disputeKit2.disputes(disputeID); + assertEq(jumped, false, "jumped should be false in the DK that dispute jumped to"); + + // Check jump modifier + vm.prank(address(core)); + vm.expectRevert(DisputeKitClassicBase.DisputeJumpedToParentDK.selector); + disputeKit3.draw(disputeID, 1); + + // And check that draw in the new round works + vm.expectEmit(true, true, true, true); + emit KlerosCoreBase.Draw(staker1, disputeID, 1, 0); // roundID = 1 VoteID = 0 + core.draw(disputeID, 1); + + (address account, , , ) = disputeKit2.getVoteInfo(disputeID, 1, 0); + assertEq(account, staker1, "Wrong drawn account in the classic DK"); + } + function test_appeal_quickPassPeriod() public { uint256 disputeID = 0; From e6e83aea553f4af1b482f091bc5475effe2ec2bc Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Wed, 3 Sep 2025 21:31:00 +0100 Subject: [PATCH 076/175] refactor: small functions rename --- contracts/src/arbitration/KlerosCoreBase.sol | 4 ++-- contracts/src/arbitration/SortitionModuleBase.sol | 4 ++-- contracts/src/arbitration/interfaces/ISortitionModule.sol | 4 ++-- contracts/src/arbitration/university/KlerosCoreUniversity.sol | 4 ++-- .../src/arbitration/university/SortitionModuleUniversity.sol | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/contracts/src/arbitration/KlerosCoreBase.sol b/contracts/src/arbitration/KlerosCoreBase.sol index d6821b59c..4efac9477 100644 --- a/contracts/src/arbitration/KlerosCoreBase.sol +++ b/contracts/src/arbitration/KlerosCoreBase.sol @@ -796,10 +796,10 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable if (pnkBalance == 0 || !disputeKit.isVoteActive(_params.disputeID, _params.round, _params.repartition)) { // The juror is inactive or their balance is can't cover penalties anymore, unstake them from all courts. - sortitionModule.unstakeByCoreFromAllCourts(account); + sortitionModule.forcedUnstakeAllCourts(account); } else if (newCourtStake < courts[penalizedInCourtID].minStake) { // The juror's balance fell below the court minStake, unstake them from the court. - sortitionModule.unstakeByCore(account, penalizedInCourtID); + sortitionModule.forcedUnstake(account, penalizedInCourtID); } if (_params.repartition == _params.numberOfVotesInRound - 1 && _params.coherentCount == 0) { diff --git a/contracts/src/arbitration/SortitionModuleBase.sol b/contracts/src/arbitration/SortitionModuleBase.sol index 59c394722..cf48c624c 100644 --- a/contracts/src/arbitration/SortitionModuleBase.sol +++ b/contracts/src/arbitration/SortitionModuleBase.sol @@ -436,7 +436,7 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr /// `k` is the minimum number of children per node of one of these courts' sortition sum tree, /// and `j` is the maximum number of jurors that ever staked in one of these courts simultaneously. /// @param _account The juror to unstake. - function unstakeByCoreFromAllCourts(address _account) external override onlyByCore { + function forcedUnstakeAllCourts(address _account) external override onlyByCore { uint96[] memory courtIDs = getJurorCourtIDs(_account); for (uint256 j = courtIDs.length; j > 0; j--) { core.setStakeBySortitionModule(_account, courtIDs[j - 1], 0); @@ -451,7 +451,7 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr /// and `j` is the maximum number of jurors that ever staked in one of these courts simultaneously. /// @param _account The juror to unstake. /// @param _courtID The ID of the court. - function unstakeByCore(address _account, uint96 _courtID) external override onlyByCore { + function forcedUnstake(address _account, uint96 _courtID) external override onlyByCore { core.setStakeBySortitionModule(_account, _courtID, 0); } diff --git a/contracts/src/arbitration/interfaces/ISortitionModule.sol b/contracts/src/arbitration/interfaces/ISortitionModule.sol index 52a077ec3..e830891df 100644 --- a/contracts/src/arbitration/interfaces/ISortitionModule.sol +++ b/contracts/src/arbitration/interfaces/ISortitionModule.sol @@ -38,9 +38,9 @@ interface ISortitionModule { function setStakeReward(address _account, uint96 _courtID, uint256 _reward) external returns (bool success); - function unstakeByCoreFromAllCourts(address _account) external; + function forcedUnstakeAllCourts(address _account) external; - function unstakeByCore(address _account, uint96 _courtID) external; + function forcedUnstake(address _account, uint96 _courtID) external; function lockStake(address _account, uint256 _relativeAmount) external; diff --git a/contracts/src/arbitration/university/KlerosCoreUniversity.sol b/contracts/src/arbitration/university/KlerosCoreUniversity.sol index b75f16418..b2519fdd7 100644 --- a/contracts/src/arbitration/university/KlerosCoreUniversity.sol +++ b/contracts/src/arbitration/university/KlerosCoreUniversity.sol @@ -791,10 +791,10 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { if (pnkBalance == 0 || !disputeKit.isVoteActive(_params.disputeID, _params.round, _params.repartition)) { // The juror is inactive or their balance is can't cover penalties anymore, unstake them from all courts. - sortitionModule.unstakeByCoreFromAllCourts(account); + sortitionModule.forcedUnstakeAllCourts(account); } else if (newCourtStake < courts[penalizedInCourtID].minStake) { // The juror's balance fell below the court minStake, unstake them from the court. - sortitionModule.setStake(account, penalizedInCourtID, 0, 0, 0); + sortitionModule.forcedUnstake(account, penalizedInCourtID); } if (_params.repartition == _params.numberOfVotesInRound - 1 && _params.coherentCount == 0) { diff --git a/contracts/src/arbitration/university/SortitionModuleUniversity.sol b/contracts/src/arbitration/university/SortitionModuleUniversity.sol index b127b7cf5..dd1caa832 100644 --- a/contracts/src/arbitration/university/SortitionModuleUniversity.sol +++ b/contracts/src/arbitration/university/SortitionModuleUniversity.sol @@ -303,7 +303,7 @@ contract SortitionModuleUniversity is ISortitionModuleUniversity, UUPSProxiable, /// `k` is the minimum number of children per node of one of these courts' sortition sum tree, /// and `j` is the maximum number of jurors that ever staked in one of these courts simultaneously. /// @param _account The juror to unstake. - function unstakeByCoreFromAllCourts(address _account) external override onlyByCore { + function forcedUnstakeAllCourts(address _account) external override onlyByCore { uint96[] memory courtIDs = getJurorCourtIDs(_account); for (uint256 j = courtIDs.length; j > 0; j--) { core.setStakeBySortitionModule(_account, courtIDs[j - 1], 0); @@ -318,7 +318,7 @@ contract SortitionModuleUniversity is ISortitionModuleUniversity, UUPSProxiable, /// and `j` is the maximum number of jurors that ever staked in one of these courts simultaneously. /// @param _account The juror to unstake. /// @param _courtID The ID of the court. - function unstakeByCore(address _account, uint96 _courtID) external override onlyByCore { + function forcedUnstake(address _account, uint96 _courtID) external override onlyByCore { core.setStakeBySortitionModule(_account, _courtID, 0); } From 1d1f9f2d909715117e2dddfe28ad3938823b2f02 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Wed, 3 Sep 2025 23:08:04 +0100 Subject: [PATCH 077/175] chore: changelog --- contracts/CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/contracts/CHANGELOG.md b/contracts/CHANGELOG.md index 79845b880..086502e21 100644 --- a/contracts/CHANGELOG.md +++ b/contracts/CHANGELOG.md @@ -13,6 +13,12 @@ The format is based on [Common Changelog](https://common-changelog.org/). - **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)) - **Breaking:** Rename `governor` to `owner` in order to comply with the lightweight ownership standard [ERC-5313](https://eipsinsight.com/ercs/erc-5313) ([#2112](https://github.com/kleros/kleros-v2/issues/2112)) +- **Breaking:** Apply the penalties to the stakes in the Sortition Tree ([#2107](https://github.com/kleros/kleros-v2/issues/2107)) +- **Breaking:** Make `SortitionModule.getJurorBalance().stakedInCourt` include the penalties ([#2107](https://github.com/kleros/kleros-v2/issues/2107)) +- **Breaking:** Add a new field `drawnJurorFromCourtIDs` to the `Round` struct in `KlerosCoreBase` and `KlerosCoreUniversity` ([#2107](https://github.com/kleros/kleros-v2/issues/2107)) +- Make `IDisputeKit.draw()` and `ISortitionModule.draw()` return the court ID from which the juror was drawn ([#2107](https://github.com/kleros/kleros-v2/issues/2107)) +- Rename `SortitionModule.setJurorInactive()` to `SortitionModule.forcedUnstakeAllCourts()` ([#2107](https://github.com/kleros/kleros-v2/issues/2107)) +- Allow stake changes to by-pass delayed stakes when initiated by the SortitionModule by setting the `_noDelay` parameter to `true` in `SortitionModule.validateStake()` ([#2107](https://github.com/kleros/kleros-v2/issues/2107)) - 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)) From b7888c282e372c549184f1f3ce3ca4cc84205421 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Wed, 27 Aug 2025 06:34:29 +0100 Subject: [PATCH 078/175] refactor: extracted the sortition tree to a library --- contracts/src/arbitration/KlerosCoreBase.sol | 8 +- .../src/arbitration/SortitionModuleBase.sol | 251 ++--------------- .../dispute-kits/DisputeKitClassicBase.sol | 4 +- .../interfaces/ISortitionModule.sol | 4 +- .../university/SortitionModuleUniversity.sol | 8 +- contracts/src/libraries/SortitionTrees.sol | 252 ++++++++++++++++++ contracts/src/test/SortitionModuleMock.sol | 3 +- 7 files changed, 282 insertions(+), 248 deletions(-) create mode 100644 contracts/src/libraries/SortitionTrees.sol diff --git a/contracts/src/arbitration/KlerosCoreBase.sol b/contracts/src/arbitration/KlerosCoreBase.sol index 4efac9477..a76930051 100644 --- a/contracts/src/arbitration/KlerosCoreBase.sol +++ b/contracts/src/arbitration/KlerosCoreBase.sol @@ -223,7 +223,7 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable // FORKING_COURT // TODO: Fill the properties for the Forking court, emit CourtCreated. courts.push(); - sortitionModule.createTree(bytes32(uint256(FORKING_COURT)), _sortitionExtraData); + sortitionModule.createTree(FORKING_COURT, _sortitionExtraData); // GENERAL_COURT Court storage court = courts.push(); @@ -236,7 +236,7 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable court.jurorsForCourtJump = _courtParameters[3]; court.timesPerPeriod = _timesPerPeriod; - sortitionModule.createTree(bytes32(uint256(GENERAL_COURT)), _sortitionExtraData); + sortitionModule.createTree(GENERAL_COURT, _sortitionExtraData); uint256[] memory supportedDisputeKits = new uint256[](1); supportedDisputeKits[0] = DISPUTE_KIT_CLASSIC; @@ -343,7 +343,7 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable if (_supportedDisputeKits.length == 0) revert UnsupportedDisputeKit(); if (_parent == FORKING_COURT) revert InvalidForkingCourtAsParent(); - uint256 courtID = courts.length; + uint96 courtID = uint96(courts.length); Court storage court = courts.push(); for (uint256 i = 0; i < _supportedDisputeKits.length; i++) { @@ -364,7 +364,7 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable court.jurorsForCourtJump = _jurorsForCourtJump; court.timesPerPeriod = _timesPerPeriod; - sortitionModule.createTree(bytes32(courtID), _sortitionExtraData); + sortitionModule.createTree(courtID, _sortitionExtraData); // Update the parent. courts[_parent].children.push(courtID); diff --git a/contracts/src/arbitration/SortitionModuleBase.sol b/contracts/src/arbitration/SortitionModuleBase.sol index cf48c624c..3c528b017 100644 --- a/contracts/src/arbitration/SortitionModuleBase.sol +++ b/contracts/src/arbitration/SortitionModuleBase.sol @@ -7,25 +7,20 @@ 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 {SortitionTrees, TreeKey, CourtID} from "../libraries/SortitionTrees.sol"; import {IRNG} from "../rng/IRNG.sol"; import "../libraries/Constants.sol"; /// @title SortitionModuleBase /// @dev A factory of trees that keeps track of staked values for sortition. abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSProxiable { + using SortitionTrees for SortitionTrees.Tree; + using SortitionTrees for mapping(TreeKey key => SortitionTrees.Tree); + // ************************************* // // * Enums / Structs * // // ************************************* // - struct SortitionSumTree { - uint256 K; // The maximum number of children per node. - uint256[] stack; // We use this to keep track of vacant positions in the tree after removing a leaf. This is for keeping the tree as balanced as possible without spending gas on moving nodes around. - uint256[] nodes; // The tree nodes. - // Two-way mapping of IDs to node indexes. Note that node index 0 is reserved for the root node, and means the ID does not have a node. - mapping(bytes32 stakePathID => uint256 nodeIndex) IDsToNodeIndexes; - mapping(uint256 nodeIndex => bytes32 stakePathID) nodeIndexesToIDs; - } - struct DelayedStake { address account; // The address of the juror. uint96 courtID; // The ID of the court. @@ -56,7 +51,7 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr 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. + mapping(TreeKey key => SortitionTrees.Tree) sortitionSumTrees; // The mapping of sortition trees by keys. mapping(address account => Juror) public jurors; // The jurors. mapping(uint256 => DelayedStake) public delayedStakes; // Stores the stakes that were changed during Drawing phase, to update them when the phase is switched to Staking. mapping(address jurorAccount => mapping(uint96 courtId => uint256)) public latestDelayedStakeIndex; // DEPRECATED. Maps the juror to its latest delayed stake. If there is already a delayed stake for this juror then it'll be replaced. latestDelayedStakeIndex[juror][courtID]. @@ -185,15 +180,12 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr } /// @dev Create a sortition sum tree at the specified key. - /// @param _key The key of the new tree. + /// @param _courtID The ID of the court. /// @param _extraData Extra data that contains the number of children each node in the tree should have. - function createTree(bytes32 _key, bytes memory _extraData) external override onlyByCore { - SortitionSumTree storage tree = sortitionSumTrees[_key]; + function createTree(uint96 _courtID, bytes memory _extraData) external override onlyByCore { + TreeKey key = CourtID.wrap(_courtID).toTreeKey(); uint256 K = _extraDataToTreeK(_extraData); - if (tree.K != 0) revert TreeAlreadyExists(); - if (K <= 1) revert KMustBeGreaterThanOne(); - tree.K = K; - tree.nodes.push(0); + sortitionSumTrees.createTree(key, K); } /// @dev Executes the next delayed stakes. @@ -398,12 +390,13 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr } // Update the sortition sum tree. - bytes32 stakePathID = _accountAndCourtIDToStakePathID(_account, _courtID); + bytes32 stakePathID = SortitionTrees.toStakePathID(_account, _courtID); bool finished = false; 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(currentCourtID)), _newStake, stakePathID); + TreeKey key = CourtID.wrap(currentCourtID).toTreeKey(); + sortitionSumTrees[key].set(_newStake, stakePathID); if (currentCourtID == GENERAL_COURT) { finished = true; } else { @@ -477,7 +470,7 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr /// @dev Draw an ID from a tree using a number. /// Note that this function reverts if the sum of all values in the tree is 0. - /// @param _key The key of the tree. + /// @param _courtID The ID of the court. /// @param _coreDisputeID Index of the dispute in Kleros Core. /// @param _nonce Nonce to hash with random number. /// @return drawnAddress The drawn address. @@ -485,41 +478,14 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr /// `k` is the maximum number of children per node in the tree, /// and `n` is the maximum number of nodes ever appended. function draw( - bytes32 _key, + uint96 _courtID, uint256 _coreDisputeID, uint256 _nonce ) public view override returns (address drawnAddress, uint96 fromSubcourtID) { if (phase != Phase.drawing) revert NotDrawingPhase(); - SortitionSumTree storage tree = sortitionSumTrees[_key]; - - if (tree.nodes[0] == 0) { - return (address(0), 0); // No jurors staked. - } - - uint256 currentDrawnNumber = uint256(keccak256(abi.encodePacked(randomNumber, _coreDisputeID, _nonce))) % - tree.nodes[0]; - - // While it still has children - uint256 treeIndex = 0; - while ((tree.K * treeIndex) + 1 < tree.nodes.length) { - for (uint256 i = 1; i <= tree.K; i++) { - // Loop over children. - uint256 nodeIndex = (tree.K * treeIndex) + i; - uint256 nodeValue = tree.nodes[nodeIndex]; - - if (currentDrawnNumber >= nodeValue) { - // Go to the next child. - currentDrawnNumber -= nodeValue; - } else { - // Pick this child. - treeIndex = nodeIndex; - break; - } - } - } - bytes32 stakePathID = tree.nodeIndexesToIDs[treeIndex]; - (drawnAddress, fromSubcourtID) = _stakePathIDToAccountAndCourtID(stakePathID); + TreeKey key = CourtID.wrap(_courtID).toTreeKey(); + (drawnAddress, fromSubcourtID) = sortitionSumTrees[key].draw(_coreDisputeID, _nonce, randomNumber); } /// @dev Get the stake of a juror in a court. @@ -527,21 +493,9 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr /// @param _courtID The ID of the court. /// @return value The stake of the juror in the court. function stakeOf(address _juror, uint96 _courtID) public view returns (uint256) { - bytes32 stakePathID = _accountAndCourtIDToStakePathID(_juror, _courtID); - return stakeOf(bytes32(uint256(_courtID)), stakePathID); - } - - /// @dev Get the stake of a juror in a court. - /// @param _key The key of the tree, corresponding to a court. - /// @param _stakePathID The stake path ID, corresponding to a juror. - /// @return The stake of the juror in the court. - function stakeOf(bytes32 _key, bytes32 _stakePathID) public view returns (uint256) { - SortitionSumTree storage tree = sortitionSumTrees[_key]; - uint treeIndex = tree.IDsToNodeIndexes[_stakePathID]; - if (treeIndex == 0) { - return 0; - } - return tree.nodes[treeIndex]; + bytes32 stakePathID = SortitionTrees.toStakePathID(_juror, _courtID); + TreeKey key = CourtID.wrap(_courtID).toTreeKey(); + return sortitionSumTrees[key].stakeOf(stakePathID); } /// @dev Gets the balance of a juror in a court. @@ -590,26 +544,6 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr // * Internal * // // ************************************* // - /// @dev Update all the parents of a node. - /// @param _key The key of the tree to update. - /// @param _treeIndex The index of the node to start from. - /// @param _plusOrMinus Whether to add (true) or substract (false). - /// @param _value The value to add or substract. - /// `O(log_k(n))` where - /// `k` is the maximum number of children per node in the tree, - /// and `n` is the maximum number of nodes ever appended. - function _updateParents(bytes32 _key, uint256 _treeIndex, bool _plusOrMinus, uint256 _value) private { - SortitionSumTree storage tree = sortitionSumTrees[_key]; - - uint256 parentIndex = _treeIndex; - while (parentIndex != 0) { - parentIndex = (parentIndex - 1) / tree.K; - tree.nodes[parentIndex] = _plusOrMinus - ? tree.nodes[parentIndex] + _value - : tree.nodes[parentIndex] - _value; - } - } - function _extraDataToTreeK(bytes memory _extraData) internal pure returns (uint256 K) { if (_extraData.length >= 32) { assembly { @@ -621,151 +555,6 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr } } - /// @dev Set a value in a tree. - /// @param _key The key of the tree. - /// @param _value The new value. - /// @param _stakePathID The ID of the value. - /// `O(log_k(n))` where - /// `k` is the maximum number of children per node in the tree, - /// and `n` is the maximum number of nodes ever appended. - function _set(bytes32 _key, uint256 _value, bytes32 _stakePathID) internal { - SortitionSumTree storage tree = sortitionSumTrees[_key]; - uint256 treeIndex = tree.IDsToNodeIndexes[_stakePathID]; - - if (treeIndex == 0) { - // No existing node. - if (_value != 0) { - // Non zero value. - // Append. - // Add node. - if (tree.stack.length == 0) { - // No vacant spots. - // Get the index and append the value. - treeIndex = tree.nodes.length; - tree.nodes.push(_value); - - // Potentially append a new node and make the parent a sum node. - if (treeIndex != 1 && (treeIndex - 1) % tree.K == 0) { - // Is first child. - uint256 parentIndex = treeIndex / tree.K; - bytes32 parentID = tree.nodeIndexesToIDs[parentIndex]; - uint256 newIndex = treeIndex + 1; - tree.nodes.push(tree.nodes[parentIndex]); - delete tree.nodeIndexesToIDs[parentIndex]; - tree.IDsToNodeIndexes[parentID] = newIndex; - tree.nodeIndexesToIDs[newIndex] = parentID; - } - } else { - // Some vacant spot. - // Pop the stack and append the value. - treeIndex = tree.stack[tree.stack.length - 1]; - tree.stack.pop(); - tree.nodes[treeIndex] = _value; - } - - // Add label. - tree.IDsToNodeIndexes[_stakePathID] = treeIndex; - tree.nodeIndexesToIDs[treeIndex] = _stakePathID; - - _updateParents(_key, treeIndex, true, _value); - } - } else { - // Existing node. - if (_value == 0) { - // Zero value. - // Remove. - // Remember value and set to 0. - uint256 value = tree.nodes[treeIndex]; - tree.nodes[treeIndex] = 0; - - // Push to stack. - tree.stack.push(treeIndex); - - // Clear label. - delete tree.IDsToNodeIndexes[_stakePathID]; - delete tree.nodeIndexesToIDs[treeIndex]; - - _updateParents(_key, treeIndex, false, value); - } else if (_value != tree.nodes[treeIndex]) { - // New, non zero value. - // Set. - bool plusOrMinus = tree.nodes[treeIndex] <= _value; - uint256 plusOrMinusValue = plusOrMinus - ? _value - tree.nodes[treeIndex] - : tree.nodes[treeIndex] - _value; - tree.nodes[treeIndex] = _value; - - _updateParents(_key, treeIndex, plusOrMinus, plusOrMinusValue); - } - } - } - - /// @dev Packs an account and a court ID into a stake path ID: [20 bytes of address][12 bytes of courtID] = 32 bytes total. - /// @param _account The address of the juror to pack. - /// @param _courtID The court ID to pack. - /// @return stakePathID The stake path ID. - function _accountAndCourtIDToStakePathID( - address _account, - uint96 _courtID - ) internal pure returns (bytes32 stakePathID) { - assembly { - // solium-disable-line security/no-inline-assembly - let ptr := mload(0x40) - - // Write account address (first 20 bytes) - for { - let i := 0x00 - } lt(i, 0x14) { - i := add(i, 0x01) - } { - mstore8(add(ptr, i), byte(add(0x0c, i), _account)) - } - - // Write court ID (last 12 bytes) - for { - let i := 0x14 - } lt(i, 0x20) { - i := add(i, 0x01) - } { - mstore8(add(ptr, i), byte(i, _courtID)) - } - stakePathID := mload(ptr) - } - } - - /// @dev Retrieves both juror's address and court ID from the stake path ID. - /// @param _stakePathID The stake path ID to unpack. - /// @return account The account. - /// @return courtID The court ID. - function _stakePathIDToAccountAndCourtID( - bytes32 _stakePathID - ) internal pure returns (address account, uint96 courtID) { - assembly { - // solium-disable-line security/no-inline-assembly - let ptr := mload(0x40) - - // Read account address (first 20 bytes) - for { - let i := 0x00 - } lt(i, 0x14) { - i := add(i, 0x01) - } { - mstore8(add(add(ptr, 0x0c), i), byte(i, _stakePathID)) - } - account := mload(ptr) - - // Read court ID (last 12 bytes) - for { - let i := 0x00 - } lt(i, 0x0c) { - i := add(i, 0x01) - } { - mstore8(add(add(ptr, 0x14), i), byte(add(i, 0x14), _stakePathID)) - } - courtID := mload(ptr) - } - } - // ************************************* // // * Errors * // // ************************************* // @@ -776,8 +565,6 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr error NoDisputesThatNeedJurors(); error RandomNumberNotReady(); error DisputesWithoutJurorsAndMaxDrawingTimeNotPassed(); - error TreeAlreadyExists(); - error KMustBeGreaterThanOne(); error NotStakingPhase(); error NoDelayedStakeToExecute(); error NotEligibleForWithdrawal(); diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol index 03a284878..6fca9b02b 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol @@ -232,9 +232,7 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi ISortitionModule sortitionModule = core.sortitionModule(); (uint96 courtID, , , , ) = core.disputes(_coreDisputeID); - bytes32 key = bytes32(uint256(courtID)); // Get the ID of the tree. - - (drawnAddress, fromSubcourtID) = sortitionModule.draw(key, _coreDisputeID, _nonce); + (drawnAddress, fromSubcourtID) = sortitionModule.draw(courtID, _coreDisputeID, _nonce); if (drawnAddress == address(0)) { // Sortition can return 0 address if no one has staked yet. return (drawnAddress, fromSubcourtID); diff --git a/contracts/src/arbitration/interfaces/ISortitionModule.sol b/contracts/src/arbitration/interfaces/ISortitionModule.sol index e830891df..d9fe7e485 100644 --- a/contracts/src/arbitration/interfaces/ISortitionModule.sol +++ b/contracts/src/arbitration/interfaces/ISortitionModule.sol @@ -13,7 +13,7 @@ interface ISortitionModule { event NewPhase(Phase _phase); - function createTree(bytes32 _key, bytes memory _extraData) external; + function createTree(uint96 _courtID, bytes memory _extraData) external; function validateStake( address _account, @@ -49,7 +49,7 @@ interface ISortitionModule { function notifyRandomNumber(uint256 _drawnNumber) external; function draw( - bytes32 _court, + uint96 _courtID, uint256 _coreDisputeID, uint256 _nonce ) external view returns (address drawnAddress, uint96 fromSubcourtID); diff --git a/contracts/src/arbitration/university/SortitionModuleUniversity.sol b/contracts/src/arbitration/university/SortitionModuleUniversity.sol index dd1caa832..63720eb18 100644 --- a/contracts/src/arbitration/university/SortitionModuleUniversity.sol +++ b/contracts/src/arbitration/university/SortitionModuleUniversity.sol @@ -112,7 +112,7 @@ contract SortitionModuleUniversity is ISortitionModuleUniversity, UUPSProxiable, transientJuror = _juror; } - function createTree(bytes32 _key, bytes memory _extraData) external { + function createTree(uint96 _courtID, bytes memory _extraData) external { // NOP } @@ -345,11 +345,7 @@ contract SortitionModuleUniversity is ISortitionModuleUniversity, UUPSProxiable, /// @dev Draw an ID from a tree using a number. /// Note that this function reverts if the sum of all values in the tree is 0. /// @return drawnAddress The drawn address. - function draw( - bytes32, - uint256, - uint256 - ) public view override returns (address drawnAddress, uint96 fromSubcourtID) { + function draw(uint96, uint256, uint256) public view override returns (address drawnAddress, uint96 fromSubcourtID) { drawnAddress = transientJuror; } diff --git a/contracts/src/libraries/SortitionTrees.sol b/contracts/src/libraries/SortitionTrees.sol new file mode 100644 index 000000000..6a1b97bc8 --- /dev/null +++ b/contracts/src/libraries/SortitionTrees.sol @@ -0,0 +1,252 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.24; + +type TreeKey is bytes32; +type CourtID is uint96; + +using {toTreeKey} for CourtID global; + +function toTreeKey(CourtID _courtID) pure returns (TreeKey) { + return TreeKey.wrap(bytes32(uint256(CourtID.unwrap(_courtID)))); +} + +library SortitionTrees { + struct Tree { + uint256 K; // The maximum number of children per node. + uint256[] stack; // We use this to keep track of vacant positions in the tree after removing a leaf. This is for keeping the tree as balanced as possible without spending gas on moving nodes around. + uint256[] nodes; // The tree nodes. + // Two-way mapping of IDs to node indexes. Note that node index 0 is reserved for the root node, and means the ID does not have a node. + mapping(bytes32 stakePathID => uint256 nodeIndex) IDsToNodeIndexes; + mapping(uint256 nodeIndex => bytes32 stakePathID) nodeIndexesToIDs; + } + + /// @dev Create a sortition sum tree at the specified key. + /// @param _trees The mapping of sortition sum trees. + /// @param _key The key of the new tree. + /// @param _k The maximum number of children per node. + function createTree(mapping(TreeKey key => Tree) storage _trees, TreeKey _key, uint256 _k) internal { + Tree storage tree = _trees[_key]; + if (tree.K != 0) revert TreeAlreadyExists(); + if (_k <= 1) revert KMustBeGreaterThanOne(); + tree.K = _k; + tree.nodes.push(0); + } + + /// @dev Draw an ID from a tree using a number. + /// Note that this function reverts if the sum of all values in the tree is 0. + /// @param _tree The sortition sum tree. + /// @param _coreDisputeID Index of the dispute in Kleros Core. + /// @param _nonce Nonce to hash with random number. + /// @return drawnAddress The drawn address. + /// `O(k * log_k(n))` where + /// `k` is the maximum number of children per node in the tree, + /// and `n` is the maximum number of nodes ever appended. + function draw( + Tree storage _tree, + uint256 _coreDisputeID, + uint256 _nonce, + uint256 _randomNumber + ) internal view returns (address drawnAddress, uint96 fromSubcourtID) { + if (_tree.nodes[0] == 0) { + return (address(0), 0); // No jurors staked. + } + + uint256 currentDrawnNumber = uint256(keccak256(abi.encodePacked(_randomNumber, _coreDisputeID, _nonce))) % + _tree.nodes[0]; + + // While it still has children + uint256 treeIndex = 0; + while ((_tree.K * treeIndex) + 1 < _tree.nodes.length) { + for (uint256 i = 1; i <= _tree.K; i++) { + // Loop over children. + uint256 nodeIndex = (_tree.K * treeIndex) + i; + uint256 nodeValue = _tree.nodes[nodeIndex]; + + if (currentDrawnNumber >= nodeValue) { + // Go to the next child. + currentDrawnNumber -= nodeValue; + } else { + // Pick this child. + treeIndex = nodeIndex; + break; + } + } + } + + bytes32 stakePathID = _tree.nodeIndexesToIDs[treeIndex]; + (drawnAddress, fromSubcourtID) = toAccountAndCourtID(stakePathID); + } + + /// @dev Set a value in a tree. + /// @param _tree The sortition sum tree. + /// @param _value The new value. + /// @param _stakePathID The ID of the value. + /// `O(log_k(n))` where + /// `k` is the maximum number of children per node in the tree, + /// and `n` is the maximum number of nodes ever appended. + function set(Tree storage _tree, uint256 _value, bytes32 _stakePathID) internal { + uint256 treeIndex = _tree.IDsToNodeIndexes[_stakePathID]; + + if (treeIndex == 0) { + // No existing node. + if (_value != 0) { + // Non zero value. + // Append. + // Add node. + if (_tree.stack.length == 0) { + // No vacant spots. + // Get the index and append the value. + treeIndex = _tree.nodes.length; + _tree.nodes.push(_value); + + // Potentially append a new node and make the parent a sum node. + if (treeIndex != 1 && (treeIndex - 1) % _tree.K == 0) { + // Is first child. + uint256 parentIndex = treeIndex / _tree.K; + bytes32 parentID = _tree.nodeIndexesToIDs[parentIndex]; + uint256 newIndex = treeIndex + 1; + _tree.nodes.push(_tree.nodes[parentIndex]); + delete _tree.nodeIndexesToIDs[parentIndex]; + _tree.IDsToNodeIndexes[parentID] = newIndex; + _tree.nodeIndexesToIDs[newIndex] = parentID; + } + } else { + // Some vacant spot. + // Pop the stack and append the value. + treeIndex = _tree.stack[_tree.stack.length - 1]; + _tree.stack.pop(); + _tree.nodes[treeIndex] = _value; + } + + // Add label. + _tree.IDsToNodeIndexes[_stakePathID] = treeIndex; + _tree.nodeIndexesToIDs[treeIndex] = _stakePathID; + + updateParents(_tree, treeIndex, true, _value); + } + } else { + // Existing node. + if (_value == 0) { + // Zero value. + // Remove. + // Remember value and set to 0. + uint256 value = _tree.nodes[treeIndex]; + _tree.nodes[treeIndex] = 0; + + // Push to stack. + _tree.stack.push(treeIndex); + + // Clear label. + delete _tree.IDsToNodeIndexes[_stakePathID]; + delete _tree.nodeIndexesToIDs[treeIndex]; + + updateParents(_tree, treeIndex, false, value); + } else if (_value != _tree.nodes[treeIndex]) { + // New, non zero value. + // Set. + bool plusOrMinus = _tree.nodes[treeIndex] <= _value; + uint256 plusOrMinusValue = plusOrMinus + ? _value - _tree.nodes[treeIndex] + : _tree.nodes[treeIndex] - _value; + _tree.nodes[treeIndex] = _value; + + updateParents(_tree, treeIndex, plusOrMinus, plusOrMinusValue); + } + } + } + + /// @dev Update all the parents of a node. + /// @param _tree The sortition sum tree. + /// @param _treeIndex The index of the node to start from. + /// @param _plusOrMinus Whether to add (true) or substract (false). + /// @param _value The value to add or substract. + /// `O(log_k(n))` where + /// `k` is the maximum number of children per node in the tree, + /// and `n` is the maximum number of nodes ever appended. + function updateParents(Tree storage _tree, uint256 _treeIndex, bool _plusOrMinus, uint256 _value) private { + uint256 parentIndex = _treeIndex; + while (parentIndex != 0) { + parentIndex = (parentIndex - 1) / _tree.K; + _tree.nodes[parentIndex] = _plusOrMinus + ? _tree.nodes[parentIndex] + _value + : _tree.nodes[parentIndex] - _value; + } + } + + /// @dev Get the stake of a juror in a court. + /// @param _tree The sortition sum tree. + /// @param _stakePathID The stake path ID, corresponding to a juror. + /// @return The stake of the juror in the court. + function stakeOf(Tree storage _tree, bytes32 _stakePathID) internal view returns (uint256) { + uint256 treeIndex = _tree.IDsToNodeIndexes[_stakePathID]; + if (treeIndex == 0) { + return 0; + } + return _tree.nodes[treeIndex]; + } + + /// @dev Packs an account and a court ID into a stake path ID: [20 bytes of address][12 bytes of courtID] = 32 bytes total. + /// @param _account The address of the juror to pack. + /// @param _courtID The court ID to pack. + /// @return stakePathID The stake path ID. + function toStakePathID(address _account, uint96 _courtID) internal pure returns (bytes32 stakePathID) { + assembly { + // solium-disable-line security/no-inline-assembly + let ptr := mload(0x40) + + // Write account address (first 20 bytes) + for { + let i := 0x00 + } lt(i, 0x14) { + i := add(i, 0x01) + } { + mstore8(add(ptr, i), byte(add(0x0c, i), _account)) + } + + // Write court ID (last 12 bytes) + for { + let i := 0x14 + } lt(i, 0x20) { + i := add(i, 0x01) + } { + mstore8(add(ptr, i), byte(i, _courtID)) + } + stakePathID := mload(ptr) + } + } + + /// @dev Retrieves both juror's address and court ID from the stake path ID. + /// @param _stakePathID The stake path ID to unpack. + /// @return account The account. + /// @return courtID The court ID. + function toAccountAndCourtID(bytes32 _stakePathID) internal pure returns (address account, uint96 courtID) { + assembly { + // solium-disable-line security/no-inline-assembly + let ptr := mload(0x40) + + // Read account address (first 20 bytes) + for { + let i := 0x00 + } lt(i, 0x14) { + i := add(i, 0x01) + } { + mstore8(add(add(ptr, 0x0c), i), byte(i, _stakePathID)) + } + account := mload(ptr) + + // Read court ID (last 12 bytes) + for { + let i := 0x00 + } lt(i, 0x0c) { + i := add(i, 0x01) + } { + mstore8(add(add(ptr, 0x14), i), byte(add(i, 0x14), _stakePathID)) + } + courtID := mload(ptr) + } + } + + error TreeAlreadyExists(); + error KMustBeGreaterThanOne(); +} diff --git a/contracts/src/test/SortitionModuleMock.sol b/contracts/src/test/SortitionModuleMock.sol index d55cbc152..73c9e859c 100644 --- a/contracts/src/test/SortitionModuleMock.sol +++ b/contracts/src/test/SortitionModuleMock.sol @@ -3,12 +3,13 @@ pragma solidity ^0.8.24; import "../arbitration/SortitionModule.sol"; +import {SortitionTrees, TreeKey} from "../libraries/SortitionTrees.sol"; /// @title SortitionModuleMock /// @dev Adds getter functions to sortition module for Foundry tests. contract SortitionModuleMock is SortitionModule { function getSortitionProperties(bytes32 _key) external view returns (uint256 K, uint256 nodeLength) { - SortitionSumTree storage tree = sortitionSumTrees[_key]; + SortitionTrees.Tree storage tree = sortitionSumTrees[TreeKey.wrap(_key)]; K = tree.K; nodeLength = tree.nodes.length; } From d3c215eb3d08c2089eac02014bb92bfa1850655b Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Fri, 29 Aug 2025 00:07:05 +0100 Subject: [PATCH 079/175] chore: changelog, natspec decorators --- contracts/CHANGELOG.md | 2 ++ contracts/src/libraries/SortitionTrees.sol | 26 +++++++++++++++++----- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/contracts/CHANGELOG.md b/contracts/CHANGELOG.md index 086502e21..14e0ff78c 100644 --- a/contracts/CHANGELOG.md +++ b/contracts/CHANGELOG.md @@ -22,6 +22,8 @@ The format is based on [Common Changelog](https://common-changelog.org/). - 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)) +- Replace the `bytes32 _key` parameter in `SortitionTrees.createTree()` and `SortitionTrees.draw()` by `uint96 courtID` ([#2113](https://github.com/kleros/kleros-v2/issues/2113)) +- Extract the sortition sum trees logic into a library `SortitionTrees` ([#2113](https://github.com/kleros/kleros-v2/issues/2113)) - 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)) diff --git a/contracts/src/libraries/SortitionTrees.sol b/contracts/src/libraries/SortitionTrees.sol index 6a1b97bc8..a1a2a1cb8 100644 --- a/contracts/src/libraries/SortitionTrees.sol +++ b/contracts/src/libraries/SortitionTrees.sol @@ -5,13 +5,13 @@ pragma solidity ^0.8.24; type TreeKey is bytes32; type CourtID is uint96; -using {toTreeKey} for CourtID global; - -function toTreeKey(CourtID _courtID) pure returns (TreeKey) { - return TreeKey.wrap(bytes32(uint256(CourtID.unwrap(_courtID)))); -} +using {SortitionTrees.toTreeKey} for CourtID global; library SortitionTrees { + // ************************************* // + // * Enums / Structs * // + // ************************************* // + struct Tree { uint256 K; // The maximum number of children per node. uint256[] stack; // We use this to keep track of vacant positions in the tree after removing a leaf. This is for keeping the tree as balanced as possible without spending gas on moving nodes around. @@ -21,6 +21,14 @@ library SortitionTrees { mapping(uint256 nodeIndex => bytes32 stakePathID) nodeIndexesToIDs; } + function toTreeKey(CourtID _courtID) internal pure returns (TreeKey) { + return TreeKey.wrap(bytes32(uint256(CourtID.unwrap(_courtID)))); + } + + // ************************************* // + // * State Modifiers * // + // ************************************* // + /// @dev Create a sortition sum tree at the specified key. /// @param _trees The mapping of sortition sum trees. /// @param _key The key of the new tree. @@ -174,6 +182,10 @@ library SortitionTrees { } } + // ************************************* // + // * Public Views * // + // ************************************* // + /// @dev Get the stake of a juror in a court. /// @param _tree The sortition sum tree. /// @param _stakePathID The stake path ID, corresponding to a juror. @@ -247,6 +259,10 @@ library SortitionTrees { } } + // ************************************* // + // * Errors * // + // ************************************* // + error TreeAlreadyExists(); error KMustBeGreaterThanOne(); } From 323ba1879118455458b939e208a05f0a5d8c57fa Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Fri, 29 Aug 2025 01:10:20 +0100 Subject: [PATCH 080/175] test: coverage of SortitionTrees and optimization of stakePathID packing/unpacking --- contracts/src/libraries/SortitionTrees.sol | 46 +- contracts/src/test/SortitionTreesMock.sol | 154 +++++ contracts/test/sortition/index.ts | 698 +++++++++++++++++++++ 3 files changed, 857 insertions(+), 41 deletions(-) create mode 100644 contracts/src/test/SortitionTreesMock.sol create mode 100644 contracts/test/sortition/index.ts diff --git a/contracts/src/libraries/SortitionTrees.sol b/contracts/src/libraries/SortitionTrees.sol index a1a2a1cb8..f8d9706fe 100644 --- a/contracts/src/libraries/SortitionTrees.sol +++ b/contracts/src/libraries/SortitionTrees.sol @@ -205,26 +205,8 @@ library SortitionTrees { function toStakePathID(address _account, uint96 _courtID) internal pure returns (bytes32 stakePathID) { assembly { // solium-disable-line security/no-inline-assembly - let ptr := mload(0x40) - - // Write account address (first 20 bytes) - for { - let i := 0x00 - } lt(i, 0x14) { - i := add(i, 0x01) - } { - mstore8(add(ptr, i), byte(add(0x0c, i), _account)) - } - - // Write court ID (last 12 bytes) - for { - let i := 0x14 - } lt(i, 0x20) { - i := add(i, 0x01) - } { - mstore8(add(ptr, i), byte(i, _courtID)) - } - stakePathID := mload(ptr) + // Pack address (20 bytes) and courtID (12 bytes) into a single bytes32 + stakePathID := or(shl(96, _account), _courtID) } } @@ -235,27 +217,9 @@ library SortitionTrees { function toAccountAndCourtID(bytes32 _stakePathID) internal pure returns (address account, uint96 courtID) { assembly { // solium-disable-line security/no-inline-assembly - let ptr := mload(0x40) - - // Read account address (first 20 bytes) - for { - let i := 0x00 - } lt(i, 0x14) { - i := add(i, 0x01) - } { - mstore8(add(add(ptr, 0x0c), i), byte(i, _stakePathID)) - } - account := mload(ptr) - - // Read court ID (last 12 bytes) - for { - let i := 0x00 - } lt(i, 0x0c) { - i := add(i, 0x01) - } { - mstore8(add(add(ptr, 0x14), i), byte(add(i, 0x14), _stakePathID)) - } - courtID := mload(ptr) + // Unpack address (first 20 bytes) and courtID (last 12 bytes) from the stake path ID + account := shr(96, _stakePathID) // Right shift by 96 bits to get the address + courtID := and(_stakePathID, 0xffffffffffffffffffffffff) // Mask the lower 96 bits to get the court ID } } diff --git a/contracts/src/test/SortitionTreesMock.sol b/contracts/src/test/SortitionTreesMock.sol new file mode 100644 index 000000000..3dcfe8741 --- /dev/null +++ b/contracts/src/test/SortitionTreesMock.sol @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "../libraries/SortitionTrees.sol"; + +/// @title SortitionTreesMock +/// @dev Test contract to expose SortitionTrees library functions for testing +contract SortitionTreesMock { + using SortitionTrees for mapping(TreeKey => SortitionTrees.Tree); + + // Storage for multiple test trees + mapping(TreeKey => SortitionTrees.Tree) public trees; + + // Court hierarchy helpers (for testing parent-child relationships) + mapping(uint96 => uint96[]) public childCourts; + mapping(uint96 => uint96) public parentCourt; + + // ************************************* // + // * Main Functions * // + // ************************************* // + + /// @dev Create a sortition sum tree for a court + function createTree(uint96 _courtID, uint256 _k) external { + TreeKey key = CourtID.wrap(_courtID).toTreeKey(); + trees.createTree(key, _k); + } + + /// @dev Set stake for a juror in a specific court + function set(uint96 _courtID, address _account, uint256 _value) external { + TreeKey key = CourtID.wrap(_courtID).toTreeKey(); + bytes32 stakePathID = SortitionTrees.toStakePathID(_account, _courtID); + SortitionTrees.set(trees[key], _value, stakePathID); + } + + /// @dev Draw a juror from a court's tree + function draw( + uint96 _courtID, + uint256 _disputeID, + uint256 _nonce, + uint256 _randomNumber + ) external view returns (address drawnAddress, uint96 fromSubcourtID) { + TreeKey key = CourtID.wrap(_courtID).toTreeKey(); + return SortitionTrees.draw(trees[key], _disputeID, _nonce, _randomNumber); + } + + /// @dev Get stake of a juror in a specific court + function stakeOf(uint96 _courtID, address _account) external view returns (uint256) { + TreeKey key = CourtID.wrap(_courtID).toTreeKey(); + bytes32 stakePathID = SortitionTrees.toStakePathID(_account, _courtID); + return SortitionTrees.stakeOf(trees[key], stakePathID); + } + + // ************************************* // + // * Multi-Court Operations * // + // ************************************* // + + /// @dev Set stake for a juror across multiple courts (for testing hierarchy) + function setStakeInHierarchy(uint96[] calldata _courtIDs, address _account, uint256 _value) external { + for (uint256 i = 0; i < _courtIDs.length; i++) { + this.set(_courtIDs[i], _account, _value); + } + } + + /// @dev Get stakes of a juror across multiple courts + function getStakesAcrossCourts( + address _account, + uint96[] calldata _courtIDs + ) external view returns (uint256[] memory stakes) { + stakes = new uint256[](_courtIDs.length); + for (uint256 i = 0; i < _courtIDs.length; i++) { + stakes[i] = this.stakeOf(_courtIDs[i], _account); + } + } + + // ************************************* // + // * Court Hierarchy Setup * // + // ************************************* // + + /// @dev Set parent court for testing hierarchy + function setParentCourt(uint96 _childCourt, uint96 _parentCourt) external { + parentCourt[_childCourt] = _parentCourt; + } + + /// @dev Add child court for testing hierarchy + function addChildCourt(uint96 _parentCourt, uint96 _childCourt) external { + childCourts[_parentCourt].push(_childCourt); + } + + /// @dev Get child courts + function getChildCourts(uint96 _parentCourt) external view returns (uint96[] memory) { + return childCourts[_parentCourt]; + } + + // ************************************* // + // * Tree State Inspection * // + // ************************************* // + + /// @dev Get all nodes in a tree + function getTreeNodes(uint96 _courtID) external view returns (uint256[] memory) { + TreeKey key = CourtID.wrap(_courtID).toTreeKey(); + return trees[key].nodes; + } + + /// @dev Get tree K value + function getTreeK(uint96 _courtID) external view returns (uint256) { + TreeKey key = CourtID.wrap(_courtID).toTreeKey(); + return trees[key].K; + } + + /// @dev Get tree stack + function getTreeStack(uint96 _courtID) external view returns (uint256[] memory) { + TreeKey key = CourtID.wrap(_courtID).toTreeKey(); + return trees[key].stack; + } + + /// @dev Get node index for a juror in a court + function getNodeIndex(uint96 _courtID, address _account) external view returns (uint256) { + TreeKey key = CourtID.wrap(_courtID).toTreeKey(); + bytes32 stakePathID = SortitionTrees.toStakePathID(_account, _courtID); + return trees[key].IDsToNodeIndexes[stakePathID]; + } + + /// @dev Check if a court tree exists + function courtExists(uint96 _courtID) external view returns (bool) { + TreeKey key = CourtID.wrap(_courtID).toTreeKey(); + return trees[key].K != 0; + } + + /// @dev Get the root sum (total stakes) of a court + function getRootSum(uint96 _courtID) external view returns (uint256) { + TreeKey key = CourtID.wrap(_courtID).toTreeKey(); + if (trees[key].nodes.length == 0) return 0; + return trees[key].nodes[0]; + } + + // ************************************* // + // * Utility Functions * // + // ************************************* // + + /// @dev Test function to pack address and court ID + function testToStakePathID(address _account, uint96 _courtID) external pure returns (bytes32) { + return SortitionTrees.toStakePathID(_account, _courtID); + } + + /// @dev Test function to unpack stake path ID + function testToAccountAndCourtID(bytes32 _stakePathID) external pure returns (address account, uint96 courtID) { + return SortitionTrees.toAccountAndCourtID(_stakePathID); + } + + /// @dev Test function to convert court ID to tree key + function testToTreeKey(uint96 _courtID) external pure returns (TreeKey) { + return CourtID.wrap(_courtID).toTreeKey(); + } +} diff --git a/contracts/test/sortition/index.ts b/contracts/test/sortition/index.ts new file mode 100644 index 000000000..e33d1a0d5 --- /dev/null +++ b/contracts/test/sortition/index.ts @@ -0,0 +1,698 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { SortitionTreesMock } from "../../typechain-types"; + +describe("SortitionTrees", function () { + let sortitionTree: SortitionTreesMock; + let accounts: any[]; + + beforeEach("Setup", async () => { + const factory = await ethers.getContractFactory("SortitionTreesMock"); + sortitionTree = (await factory.deploy()) as SortitionTreesMock; + accounts = await ethers.getSigners(); + }); + + // Helper function to create a test juror address + const getTestAddress = (index: number): string => accounts[index % accounts.length].address; + + describe("Stake Path ID Utilities", function () { + it("Should convert correctly between stakePathID and account+courtID", async function () { + // Test various combinations of addresses and court IDs + const testCases = [ + { + address: "0x1234567890123456789012345678901234567890", + courtID: 0, + }, + { + address: "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", + courtID: 0xffffffffffff, // max uint96 + }, + { + address: "0x0000000000000000000000000000000000000001", + courtID: 1, + }, + { + address: "0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF", + courtID: 123456789, + }, + ]; + + for (const testCase of testCases) { + // Test packing + const stakePathID = await sortitionTree.testToStakePathID(testCase.address, testCase.courtID); + + // Test unpacking + const [unpackedAddress, unpackedCourtID] = await sortitionTree.testToAccountAndCourtID(stakePathID); + + // Verify round-trip equivalence + expect(unpackedAddress.toLowerCase()).to.equal(testCase.address.toLowerCase()); + expect(unpackedCourtID).to.equal(testCase.courtID); + + // Verify the packed format is as expected: [20 bytes address][12 bytes courtID] + const expectedPackedValue = ethers.solidityPacked(["address", "uint96"], [testCase.address, testCase.courtID]); + expect(stakePathID).to.equal(expectedPackedValue); + } + }); + + it("Should handle TreeKey conversion correctly", async function () { + const courtIDs = [ + 0, + 1, + 100, + 0xffffffffffff, // max uint96 + ]; + + for (const courtID of courtIDs) { + const treeKey = await sortitionTree.testToTreeKey(courtID); + + // TreeKey should be the courtID padded to 32 bytes + const expectedTreeKey = ethers.zeroPadValue(ethers.toBeHex(courtID), 32); + expect(treeKey).to.equal(expectedTreeKey); + + // Court ID 0 will result in zero key, others should not + if (courtID === 0) { + expect(treeKey).to.equal("0x0000000000000000000000000000000000000000000000000000000000000000"); + } else { + expect(treeKey).to.not.equal("0x0000000000000000000000000000000000000000000000000000000000000000"); + } + } + }); + }); + + describe("Tree Creation & Validation", function () { + it("Should create trees with valid K values", async function () { + const testCases = [ + { courtID: 0, k: 2 }, + { courtID: 1, k: 5 }, + { courtID: 100, k: 10 }, + { courtID: 999, k: 100 }, + ]; + + for (const testCase of testCases) { + await sortitionTree.createTree(testCase.courtID, testCase.k); + + // Verify tree was created + expect(await sortitionTree.courtExists(testCase.courtID)).to.be.true; + expect(await sortitionTree.getTreeK(testCase.courtID)).to.equal(testCase.k); + + // Verify initial state + const nodes = await sortitionTree.getTreeNodes(testCase.courtID); + expect(nodes.length).to.equal(1); + expect(nodes[0]).to.equal(0); // Root starts at 0 + + const stack = await sortitionTree.getTreeStack(testCase.courtID); + expect(stack.length).to.equal(0); // Empty stack initially + } + }); + + it("Should reject invalid K values", async function () { + // K must be greater than 1 + await expect(sortitionTree.createTree(0, 0)).to.be.reverted; + await expect(sortitionTree.createTree(1, 1)).to.be.reverted; + }); + + it("Should reject creating duplicate trees", async function () { + await sortitionTree.createTree(0, 2); + await expect(sortitionTree.createTree(0, 3)).to.be.reverted; + }); + + it("Should create multiple independent trees", async function () { + await sortitionTree.createTree(0, 2); + await sortitionTree.createTree(1, 3); + await sortitionTree.createTree(2, 5); + + expect(await sortitionTree.getTreeK(0)).to.equal(2); + expect(await sortitionTree.getTreeK(1)).to.equal(3); + expect(await sortitionTree.getTreeK(2)).to.equal(5); + }); + }); + + describe("Single Court Stake Management", function () { + beforeEach(async function () { + // Create a test tree with K=2 for court 0 + await sortitionTree.createTree(0, 2); + }); + + describe("Adding New Stakes", function () { + it("Should add first juror to empty tree", async function () { + const juror = getTestAddress(0); + const stake = 100; + + await sortitionTree.set(0, juror, stake); + + // Verify stake was set + expect(await sortitionTree.stakeOf(0, juror)).to.equal(stake); + expect(await sortitionTree.getRootSum(0)).to.equal(stake); + + // Verify tree structure + const nodes = await sortitionTree.getTreeNodes(0); + expect(nodes[0]).to.equal(stake); // Root should equal juror stake + expect(nodes[1]).to.equal(stake); // First leaf + }); + + it("Should add multiple jurors sequentially", async function () { + const stakes = [100, 200, 300]; + + for (let i = 0; i < stakes.length; i++) { + const juror = getTestAddress(i); + await sortitionTree.set(0, juror, stakes[i]); + expect(await sortitionTree.stakeOf(0, juror)).to.equal(stakes[i]); + } + + // Verify total stake + const expectedTotal = stakes.reduce((sum, stake) => sum + stake, 0); + expect(await sortitionTree.getRootSum(0)).to.equal(expectedTotal); + }); + + it("Should handle tree restructuring when K threshold is reached", async function () { + // Add enough jurors to trigger tree restructuring + const stakes = [100, 200, 300, 400]; // More than K=2 + + for (let i = 0; i < stakes.length; i++) { + const juror = getTestAddress(i); + await sortitionTree.set(0, juror, stakes[i]); + } + + const nodes = await sortitionTree.getTreeNodes(0); + expect(nodes.length).to.be.greaterThan(4); // Should have expanded + + const expectedTotal = stakes.reduce((sum, stake) => sum + stake, 0); + expect(await sortitionTree.getRootSum(0)).to.equal(expectedTotal); + }); + }); + + describe("Updating Existing Stakes", function () { + beforeEach(async function () { + // Add initial jurors + await sortitionTree.set(0, getTestAddress(0), 100); + await sortitionTree.set(0, getTestAddress(1), 200); + }); + + it("Should increase stake values correctly", async function () { + const juror = getTestAddress(0); + const oldStake = await sortitionTree.stakeOf(0, juror); + const newStake = 250; + + await sortitionTree.set(0, juror, newStake); + + expect(await sortitionTree.stakeOf(0, juror)).to.equal(newStake); + + // Root should reflect the change + const rootSum = await sortitionTree.getRootSum(0); + expect(rootSum).to.equal(200 + newStake); + }); + + it("Should decrease stake values correctly", async function () { + const juror = getTestAddress(0); + const newStake = 50; + + await sortitionTree.set(0, juror, newStake); + + expect(await sortitionTree.stakeOf(0, juror)).to.equal(newStake); + expect(await sortitionTree.getRootSum(0)).to.equal(200 + newStake); + }); + + it("Should be no-op when setting same value", async function () { + const juror = getTestAddress(0); + const currentStake = await sortitionTree.stakeOf(0, juror); + const initialRoot = await sortitionTree.getRootSum(0); + + await sortitionTree.set(0, juror, currentStake); + + expect(await sortitionTree.stakeOf(0, juror)).to.equal(currentStake); + expect(await sortitionTree.getRootSum(0)).to.equal(initialRoot); + }); + }); + + describe("Removing Stakes", function () { + beforeEach(async function () { + // Add initial jurors + await sortitionTree.set(0, getTestAddress(0), 100); + await sortitionTree.set(0, getTestAddress(1), 200); + await sortitionTree.set(0, getTestAddress(2), 300); + }); + + it("Should remove juror by setting stake to 0", async function () { + const juror = getTestAddress(1); + const initialRoot = await sortitionTree.getRootSum(0); + + await sortitionTree.set(0, juror, 0); + + expect(await sortitionTree.stakeOf(0, juror)).to.equal(0); + expect(await sortitionTree.getRootSum(0)).to.equal(initialRoot - 200n); + expect(await sortitionTree.getNodeIndex(0, juror)).to.equal(0); // Should be cleared + }); + + it("Should manage stack for removed positions", async function () { + const juror = getTestAddress(1); + const initialStackLength = (await sortitionTree.getTreeStack(0)).length; + + await sortitionTree.set(0, juror, 0); + + const newStackLength = (await sortitionTree.getTreeStack(0)).length; + expect(newStackLength).to.be.greaterThan(initialStackLength); + }); + + it("Should reuse vacant positions from stack", async function () { + const juror1 = getTestAddress(1); + const juror4 = getTestAddress(4); + + // Remove a juror + await sortitionTree.set(0, juror1, 0); + const stackAfterRemoval = await sortitionTree.getTreeStack(0); + + // Add a new juror (should reuse vacant position) + await sortitionTree.set(0, juror4, 150); + const stackAfterAdd = await sortitionTree.getTreeStack(0); + + expect(stackAfterAdd.length).to.equal(stackAfterRemoval.length - 1); + expect(await sortitionTree.stakeOf(0, juror4)).to.equal(150); + }); + }); + }); + + describe("Drawing Algorithm", function () { + beforeEach(async function () { + await sortitionTree.createTree(0, 2); + }); + + it("Should return zero address for empty tree", async function () { + const [drawnAddress, courtID] = await sortitionTree.draw(0, 1, 1, 12345); + expect(drawnAddress).to.equal(ethers.ZeroAddress); + expect(courtID).to.equal(0); + }); + + it("Should draw single juror from single-juror tree", async function () { + const juror = getTestAddress(0); + await sortitionTree.set(0, juror, 100); + + // Multiple draws should always return the same juror + for (let i = 0; i < 5; i++) { + const [drawnAddress, courtID] = await sortitionTree.draw(0, 1, i, 12345 + i); + expect(drawnAddress.toLowerCase()).to.equal(juror.toLowerCase()); + expect(courtID).to.equal(0); + } + }); + + it("Should draw deterministically with same inputs", async function () { + // Add multiple jurors + await sortitionTree.set(0, getTestAddress(0), 100); + await sortitionTree.set(0, getTestAddress(1), 200); + await sortitionTree.set(0, getTestAddress(2), 300); + + const disputeID = 1; + const nonce = 2; + const randomNumber = 12345; + + // Multiple calls with same parameters should return same result + const [address1] = await sortitionTree.draw(0, disputeID, nonce, randomNumber); + const [address2] = await sortitionTree.draw(0, disputeID, nonce, randomNumber); + const [address3] = await sortitionTree.draw(0, disputeID, nonce, randomNumber); + + expect(address1).to.equal(address2); + expect(address2).to.equal(address3); + }); + + it("Should respect weighted probability distribution", async function () { + // Add jurors with very different stakes to test weighting + await sortitionTree.set(0, getTestAddress(0), 1); // Very low stake + await sortitionTree.set(0, getTestAddress(1), 1000); // Very high stake + + let draws = { [getTestAddress(0).toLowerCase()]: 0, [getTestAddress(1).toLowerCase()]: 0 }; + + // Perform many draws with different random numbers + const numDraws = 100; + for (let i = 0; i < numDraws; i++) { + const [drawnAddress] = await sortitionTree.draw(0, 1, 1, i); + draws[drawnAddress.toLowerCase()]++; + } + + // Juror with higher stake should be drawn more frequently + // With stakes of 1:1000, we expect roughly 0.1% vs 99.9% distribution + expect(draws[getTestAddress(1).toLowerCase()]).to.be.greaterThan(draws[getTestAddress(0).toLowerCase()]); + expect(draws[getTestAddress(1).toLowerCase()]).to.be.greaterThan(numDraws * 0.8); // At least 80% for high stake + }); + + it("Should handle edge case random numbers", async function () { + await sortitionTree.set(0, getTestAddress(0), 100); + await sortitionTree.set(0, getTestAddress(1), 200); + + // Test with boundary random numbers + const testNumbers = [0, 1, ethers.MaxUint256]; + + for (const randomNumber of testNumbers) { + const [drawnAddress] = await sortitionTree.draw(0, 1, 1, randomNumber); + expect(drawnAddress).to.not.equal(ethers.ZeroAddress); + + // Should be one of our jurors + const isValidJuror = + drawnAddress.toLowerCase() === getTestAddress(0).toLowerCase() || + drawnAddress.toLowerCase() === getTestAddress(1).toLowerCase(); + expect(isValidJuror).to.be.true; + } + }); + }); + + describe("Multi-Court Scenarios", function () { + beforeEach(async function () { + // Create multiple courts with different K values + await sortitionTree.createTree(0, 2); // General Court + await sortitionTree.createTree(1, 3); // Tech Court + await sortitionTree.createTree(2, 2); // Legal Court + }); + + describe("Independent Court Operations", function () { + it("Should handle same juror staking in multiple courts", async function () { + const juror = getTestAddress(0); + const stakes = [100, 250, 75]; // Different stakes in different courts + + // Set stakes in different courts + for (let courtID = 0; courtID < 3; courtID++) { + await sortitionTree.set(courtID, juror, stakes[courtID]); + } + + // Verify stakes are independent + for (let courtID = 0; courtID < 3; courtID++) { + expect(await sortitionTree.stakeOf(courtID, juror)).to.equal(stakes[courtID]); + expect(await sortitionTree.getRootSum(courtID)).to.equal(stakes[courtID]); + } + }); + + it("Should maintain court isolation", async function () { + const juror1 = getTestAddress(0); + const juror2 = getTestAddress(1); + + // Add different jurors to different courts + await sortitionTree.set(0, juror1, 100); + await sortitionTree.set(1, juror2, 200); + + // Court 0 should only have juror1 + expect(await sortitionTree.stakeOf(0, juror1)).to.equal(100); + expect(await sortitionTree.stakeOf(0, juror2)).to.equal(0); + + // Court 1 should only have juror2 + expect(await sortitionTree.stakeOf(1, juror1)).to.equal(0); + expect(await sortitionTree.stakeOf(1, juror2)).to.equal(200); + }); + + it("Should handle different tree structures independently", async function () { + const jurors = [getTestAddress(0), getTestAddress(1), getTestAddress(2), getTestAddress(3)]; + + // Add different numbers of jurors to each court + await sortitionTree.set(0, jurors[0], 100); // 1 juror in court 0 + + await sortitionTree.set(1, jurors[0], 150); // 2 jurors in court 1 + await sortitionTree.set(1, jurors[1], 250); + + await sortitionTree.set(2, jurors[0], 75); // 3 jurors in court 2 + await sortitionTree.set(2, jurors[1], 125); + await sortitionTree.set(2, jurors[2], 175); + + // Verify independent tree structures + expect(await sortitionTree.getRootSum(0)).to.equal(100); + expect(await sortitionTree.getRootSum(1)).to.equal(400); + expect(await sortitionTree.getRootSum(2)).to.equal(375); + + // Verify each court has correct tree structure + const nodes0 = await sortitionTree.getTreeNodes(0); + const nodes1 = await sortitionTree.getTreeNodes(1); + const nodes2 = await sortitionTree.getTreeNodes(2); + + expect(nodes0.length).to.be.lessThan(nodes2.length); // Fewer jurors = smaller tree + expect(nodes1.length).to.be.greaterThan(nodes0.length); // More jurors = larger tree + }); + }); + + describe("Cross-Court Stake Updates", function () { + it("Should handle simultaneous updates across multiple courts", async function () { + const juror = getTestAddress(0); + const courtIDs = [0, 1, 2]; + const initialStakes = [100, 200, 300]; + const updatedStakes = [150, 250, 350]; + + // Set initial stakes + for (let i = 0; i < courtIDs.length; i++) { + await sortitionTree.set(courtIDs[i], juror, initialStakes[i]); + } + + // Update all stakes + for (let i = 0; i < courtIDs.length; i++) { + await sortitionTree.set(courtIDs[i], juror, updatedStakes[i]); + } + + // Verify all updates took effect + for (let i = 0; i < courtIDs.length; i++) { + expect(await sortitionTree.stakeOf(courtIDs[i], juror)).to.equal(updatedStakes[i]); + } + }); + + it("Should handle partial removals across courts", async function () { + const juror = getTestAddress(0); + + // Add juror to all courts + await sortitionTree.set(0, juror, 100); + await sortitionTree.set(1, juror, 200); + await sortitionTree.set(2, juror, 300); + + // Remove from middle court only + await sortitionTree.set(1, juror, 0); + + // Verify partial removal + expect(await sortitionTree.stakeOf(0, juror)).to.equal(100); + expect(await sortitionTree.stakeOf(1, juror)).to.equal(0); + expect(await sortitionTree.stakeOf(2, juror)).to.equal(300); + }); + + it("Should use batch operations correctly", async function () { + const juror = getTestAddress(0); + const courtIDs = [0, 1, 2]; + const stake = 500; + + // Use batch operation to set stakes + await sortitionTree.setStakeInHierarchy(courtIDs, juror, stake); + + // Verify all stakes were set + const stakes = await sortitionTree.getStakesAcrossCourts(juror, courtIDs); + for (const retrievedStake of stakes) { + expect(retrievedStake).to.equal(stake); + } + }); + }); + + describe("Multi-Court Drawing", function () { + beforeEach(async function () { + // Setup jurors in different courts + await sortitionTree.set(0, getTestAddress(0), 100); // Court 0: 1 juror + await sortitionTree.set(0, getTestAddress(1), 200); + + await sortitionTree.set(1, getTestAddress(1), 300); // Court 1: 2 jurors + await sortitionTree.set(1, getTestAddress(2), 400); + + await sortitionTree.set(2, getTestAddress(0), 500); // Court 2: 1 juror only + }); + + it("Should draw correctly from different courts", async function () { + // Draw from each court + const [addr0] = await sortitionTree.draw(0, 1, 1, 12345); + const [addr1] = await sortitionTree.draw(1, 1, 1, 12345); + const [addr2] = await sortitionTree.draw(2, 1, 1, 12345); + + // Should get valid addresses from each court + expect(addr0).to.not.equal(ethers.ZeroAddress); + expect(addr1).to.not.equal(ethers.ZeroAddress); + expect(addr2).to.not.equal(ethers.ZeroAddress); + + // Court 2 should always return getTestAddress(0) since it's the only juror + expect(addr2.toLowerCase()).to.equal(getTestAddress(0).toLowerCase()); + }); + + it("Should return correct court ID in draw results", async function () { + const [, courtID0] = await sortitionTree.draw(0, 1, 1, 12345); + const [, courtID1] = await sortitionTree.draw(1, 1, 1, 12345); + const [, courtID2] = await sortitionTree.draw(2, 1, 1, 12345); + + // The returned court IDs should match the stake path IDs + // Since we're using the same court for staking and drawing, they should match + expect([0, 1]).to.include(Number(courtID0)); + expect([1, 2]).to.include(Number(courtID1)); + expect(Number(courtID2)).to.equal(2); + }); + + it("Should maintain independent probability distributions", async function () { + // Test drawing many times from court 0 where juror 1 has 2x stake of juror 0 + let draws = { [getTestAddress(0).toLowerCase()]: 0, [getTestAddress(1).toLowerCase()]: 0 }; + + const numDraws = 50; + for (let i = 0; i < numDraws; i++) { + const [drawnAddress] = await sortitionTree.draw(0, 1, 1, i); + draws[drawnAddress.toLowerCase()]++; + } + + // Juror 1 (200 stake) should be drawn more than juror 0 (100 stake) + expect(draws[getTestAddress(1).toLowerCase()]).to.be.greaterThan(draws[getTestAddress(0).toLowerCase()]); + }); + }); + + describe("Complex Multi-Court Scenarios", function () { + it("Should handle realistic court hierarchy simulation", async function () { + // Simulate: General Court (0) ← Tech Court (1) ← Blockchain Court (2) + const courts = [0, 1, 2]; + const jurors = [getTestAddress(0), getTestAddress(1), getTestAddress(2), getTestAddress(3)]; + + // General lawyers (stake in General Court only) + await sortitionTree.set(0, jurors[0], 100); + await sortitionTree.set(0, jurors[1], 150); + + // Tech specialists (stake in both General and Tech) + await sortitionTree.set(0, jurors[2], 200); + await sortitionTree.set(1, jurors[2], 300); + + // Blockchain experts (stake in all three) + await sortitionTree.set(0, jurors[3], 250); + await sortitionTree.set(1, jurors[3], 350); + await sortitionTree.set(2, jurors[3], 450); + + // Verify hierarchy totals + expect(await sortitionTree.getRootSum(0)).to.equal(700); // All jurors + expect(await sortitionTree.getRootSum(1)).to.equal(650); // Tech specialists + experts + expect(await sortitionTree.getRootSum(2)).to.equal(450); // Blockchain experts only + + // Test drawing from most specialized court (should only get experts) + const [blockchainExpert] = await sortitionTree.draw(2, 1, 1, 12345); + expect(blockchainExpert.toLowerCase()).to.equal(jurors[3].toLowerCase()); + }); + + it("Should handle dynamic court operations", async function () { + const juror = getTestAddress(0); + + // Juror starts in general court + await sortitionTree.set(0, juror, 100); + expect(await sortitionTree.stakeOf(0, juror)).to.equal(100); + + // Juror specializes in tech court + await sortitionTree.set(1, juror, 200); + expect(await sortitionTree.stakeOf(0, juror)).to.equal(100); + expect(await sortitionTree.stakeOf(1, juror)).to.equal(200); + + // Juror reduces general court involvement but increases tech specialization + await sortitionTree.set(0, juror, 50); + await sortitionTree.set(1, juror, 400); + expect(await sortitionTree.stakeOf(0, juror)).to.equal(50); + expect(await sortitionTree.stakeOf(1, juror)).to.equal(400); + + // Juror completely leaves general court + await sortitionTree.set(0, juror, 0); + expect(await sortitionTree.stakeOf(0, juror)).to.equal(0); + expect(await sortitionTree.stakeOf(1, juror)).to.equal(400); + }); + + it("Should handle large multi-court operations efficiently", async function () { + const numCourts = 5; + const numJurors = 10; + + // Create additional courts + for (let courtID = 3; courtID < numCourts; courtID++) { + await sortitionTree.createTree(courtID, 2); + } + + // Add many jurors across many courts + for (let jurorIdx = 0; jurorIdx < numJurors; jurorIdx++) { + for (let courtID = 0; courtID < numCourts; courtID++) { + const stake = (jurorIdx + 1) * (courtID + 1) * 10; // Varied stakes + await sortitionTree.set(courtID, getTestAddress(jurorIdx), stake); + } + } + + // Verify all operations completed successfully + for (let courtID = 0; courtID < numCourts; courtID++) { + const rootSum = await sortitionTree.getRootSum(courtID); + expect(rootSum).to.be.greaterThan(0); + + // Should be able to draw from each court + const [drawnAddress] = await sortitionTree.draw(courtID, 1, 1, 12345 + courtID); + expect(drawnAddress).to.not.equal(ethers.ZeroAddress); + } + }); + }); + }); + + describe("Edge Cases & Error Conditions", function () { + it("Should handle operations on non-existent courts", async function () { + const juror = getTestAddress(0); + + // Operations on non-existent court should behave predictably + expect(await sortitionTree.courtExists(999)).to.be.false; + expect(await sortitionTree.stakeOf(999, juror)).to.equal(0); + + // Drawing from non-existent court should revert (no tree = no nodes array) + await expect(sortitionTree.draw(999, 1, 1, 12345)).to.be.reverted; + }); + + it("Should handle boundary values correctly", async function () { + await sortitionTree.createTree(0, 2); + const juror = getTestAddress(0); + + // Test with maximum stake value + const maxStake = ethers.MaxUint256; + + // This might fail due to gas limits, but should not revert due to overflow + // Note: In practice, stakes would be much smaller + try { + await sortitionTree.set(0, juror, maxStake); + expect(await sortitionTree.stakeOf(0, juror)).to.equal(maxStake); + } catch (error) { + // Expected to fail due to gas limits, not due to overflow + expect(error).to.match(/gas/i); + } + }); + + it("Should maintain tree invariants under stress", async function () { + await sortitionTree.createTree(0, 3); + + const operations = []; + const stakes = [100, 200, 300, 400, 500]; + const jurors = stakes.map((_, i) => getTestAddress(i)); + + // Perform many random operations + for (let i = 0; i < 20; i++) { + const juror = jurors[i % jurors.length]; + const stake = stakes[i % stakes.length]; + + await sortitionTree.set(0, juror, stake); + operations.push({ juror, stake }); + } + + // Verify tree integrity + let expectedTotal = 0; + const finalStakes = new Map(); + + // Calculate expected final state + for (const op of operations) { + const prevStake = finalStakes.get(op.juror) || 0; + expectedTotal = expectedTotal - prevStake + op.stake; + finalStakes.set(op.juror, op.stake); + } + + // Verify actual state matches expected + expect(await sortitionTree.getRootSum(0)).to.equal(expectedTotal); + + for (const [juror, expectedStake] of finalStakes) { + expect(await sortitionTree.stakeOf(0, juror)).to.equal(expectedStake); + } + }); + + it("Should handle rapid stake changes correctly", async function () { + await sortitionTree.createTree(0, 2); + const juror = getTestAddress(0); + + // Rapid stake changes + const stakeSequence = [100, 0, 200, 300, 0, 150, 0, 500]; + + for (const stake of stakeSequence) { + await sortitionTree.set(0, juror, stake); + expect(await sortitionTree.stakeOf(0, juror)).to.equal(stake); + expect(await sortitionTree.getRootSum(0)).to.equal(stake); + } + }); + }); +}); From d79497542273b66d021cb711b99d6acb71656643 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Wed, 3 Sep 2025 23:24:36 +0100 Subject: [PATCH 081/175] chore: changelog --- contracts/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/CHANGELOG.md b/contracts/CHANGELOG.md index 79845b880..d919c236a 100644 --- a/contracts/CHANGELOG.md +++ b/contracts/CHANGELOG.md @@ -13,6 +13,7 @@ The format is based on [Common Changelog](https://common-changelog.org/). - **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)) - **Breaking:** Rename `governor` to `owner` in order to comply with the lightweight ownership standard [ERC-5313](https://eipsinsight.com/ercs/erc-5313) ([#2112](https://github.com/kleros/kleros-v2/issues/2112)) +- **Breaking:** Add a new state variable `jumpDisputeKitID` to the `DisputeKitClassicBase` contract ([#2114](https://github.com/kleros/kleros-v2/issues/2114)) - 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)) @@ -27,6 +28,7 @@ The format is based on [Common Changelog](https://common-changelog.org/). ### Added - Allow the dispute kits to force an early court jump and to override the number of votes after an appeal (future-proofing) ([#2110](https://github.com/kleros/kleros-v2/issues/2110)) +- Allow the dispute kits to specify which new dispute kit to use when a court jump occurs ([#2114](https://github.com/kleros/kleros-v2/issues/2114)) ### Fixed From 62b0a50ab52b40e4ed70dc53b004b60874e1f805 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Wed, 3 Sep 2025 23:33:39 +0100 Subject: [PATCH 082/175] fix: deploy script --- contracts/deploy/00-home-chain-arbitration-neo.ts | 6 +++--- contracts/deploy/00-home-chain-arbitration.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/contracts/deploy/00-home-chain-arbitration-neo.ts b/contracts/deploy/00-home-chain-arbitration-neo.ts index c5e50d6a2..36a895c74 100644 --- a/contracts/deploy/00-home-chain-arbitration-neo.ts +++ b/contracts/deploy/00-home-chain-arbitration-neo.ts @@ -129,7 +129,7 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) log: true, }); await core.addNewDisputeKit(disputeKitShutter.address); - const disputeKitShutterID = Number(await core.getDisputeKitsLength()); + const disputeKitShutterID = (await core.getDisputeKitsLength()) - 1n; const disputeKitGated = await deployUpgradable(deployments, "DisputeKitGated", { from: deployer, @@ -137,7 +137,7 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) log: true, }); await core.addNewDisputeKit(disputeKitGated.address); - const disputeKitGatedID = Number(await core.getDisputeKitsLength()); + const disputeKitGatedID = (await core.getDisputeKitsLength()) - 1n; const disputeKitGatedShutter = await deployUpgradable(deployments, "DisputeKitGatedShutter", { from: deployer, @@ -145,7 +145,7 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) log: true, }); await core.addNewDisputeKit(disputeKitGatedShutter.address); - const disputeKitGatedShutterID = Number(await core.getDisputeKitsLength()); + const disputeKitGatedShutterID = (await core.getDisputeKitsLength()) - 1n; // Snapshot proxy await deploy("KlerosCoreSnapshotProxy", { diff --git a/contracts/deploy/00-home-chain-arbitration.ts b/contracts/deploy/00-home-chain-arbitration.ts index 8163c6f66..25291052d 100644 --- a/contracts/deploy/00-home-chain-arbitration.ts +++ b/contracts/deploy/00-home-chain-arbitration.ts @@ -110,7 +110,7 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) log: true, }); await core.addNewDisputeKit(disputeKitShutter.address); - const disputeKitShutterID = Number(await core.getDisputeKitsLength()); + const disputeKitShutterID = (await core.getDisputeKitsLength()) - 1n; await core.enableDisputeKits(Courts.GENERAL, [disputeKitShutterID], true); // enable disputeKitShutter on the General Court const disputeKitGated = await deployUpgradable(deployments, "DisputeKitGated", { @@ -119,7 +119,7 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) log: true, }); await core.addNewDisputeKit(disputeKitGated.address); - const disputeKitGatedID = Number(await core.getDisputeKitsLength()); + const disputeKitGatedID = (await core.getDisputeKitsLength()) - 1n; await core.enableDisputeKits(Courts.GENERAL, [disputeKitGatedID], true); // enable disputeKitGated on the General Court const disputeKitGatedShutter = await deployUpgradable(deployments, "DisputeKitGatedShutter", { @@ -128,7 +128,7 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) log: true, }); await core.addNewDisputeKit(disputeKitGatedShutter.address); - const disputeKitGatedShutterID = Number(await core.getDisputeKitsLength()); + const disputeKitGatedShutterID = (await core.getDisputeKitsLength()) - 1n; await core.enableDisputeKits(Courts.GENERAL, [disputeKitGatedShutterID], true); // enable disputeKitGatedShutter on the General Court // Snapshot proxy From 188f8d0674c1e0dcd9e822d6e6ab35d0b0fad11e Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Wed, 20 Aug 2025 23:02:03 +0100 Subject: [PATCH 083/175] feat: support for recovery hash in Shutter DK --- contracts/foundry.toml | 3 +- contracts/hardhat.config.ts | 1 + .../dispute-kits/DisputeKitClassicBase.sol | 31 +++++--- .../dispute-kits/DisputeKitShutter.sol | 72 +++++++++++++++++-- 4 files changed, 91 insertions(+), 16 deletions(-) diff --git a/contracts/foundry.toml b/contracts/foundry.toml index 00b0c68b8..236208534 100644 --- a/contracts/foundry.toml +++ b/contracts/foundry.toml @@ -1,9 +1,10 @@ [profile.default] solc = "0.8.30" +evm_version = "cancun" via_ir = true optimizer = true -optimizer_runs = 500 +optimizer_runs = 10000 optimizer_details = { yulDetails = { stackAllocation = true } } additional_compiler_profiles = [ { name = "tests", via_ir = false } diff --git a/contracts/hardhat.config.ts b/contracts/hardhat.config.ts index d451ef05f..9d8e9c49d 100644 --- a/contracts/hardhat.config.ts +++ b/contracts/hardhat.config.ts @@ -28,6 +28,7 @@ const config: HardhatUserConfig = { { version: "0.8.30", settings: { + evmVersion: "cancun", viaIR: process.env.VIA_IR !== "false", // Defaults to true optimizer: { enabled: true, diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol index 577364cde..d04ccf981 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol @@ -318,19 +318,21 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi if (_voteIDs.length == 0) revert EmptyVoteIDs(); if (!coreDisputeIDToActive[_coreDisputeID]) revert NotActiveForCoreDisputeID(); - Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; + uint256 localDisputeID = coreDisputeIDToLocal[_coreDisputeID]; + Dispute storage dispute = disputes[localDisputeID]; if (_choice > dispute.numberOfChoices) revert ChoiceOutOfBounds(); - Round storage round = dispute.rounds[dispute.rounds.length - 1]; + uint256 localRoundID = dispute.rounds.length - 1; + Round storage round = dispute.rounds[localRoundID]; { (uint96 courtID, , , , ) = core.disputes(_coreDisputeID); (, bool hiddenVotes, , , , , ) = core.courts(courtID); - bytes32 voteHash = hashVote(_choice, _salt, _justification); + bytes32 actualVoteHash = hashVote(_choice, _salt, _justification); // Save the votes. for (uint256 i = 0; i < _voteIDs.length; i++) { if (round.votes[_voteIDs[i]].account != _juror) revert JurorHasToOwnTheVote(); - if (hiddenVotes && round.votes[_voteIDs[i]].commit != voteHash) + if (hiddenVotes && getExpectedVoteHash(localDisputeID, localRoundID, _voteIDs[i]) != actualVoteHash) revert HashDoesNotMatchHiddenVoteCommitment(); if (round.votes[_voteIDs[i]].voted) revert VoteAlreadyCast(); round.votes[_voteIDs[i]].choice = _choice; @@ -484,15 +486,14 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi * @dev Computes the hash of a vote using ABI encoding * @dev The unused parameters may be used by overriding contracts. * @param _choice The choice being voted for - * @param _justification The justification for the vote * @param _salt A random salt for commitment * @return bytes32 The hash of the encoded vote parameters */ function hashVote( uint256 _choice, uint256 _salt, - string memory _justification - ) public pure virtual returns (bytes32) { + string memory /*_justification*/ + ) public view virtual returns (bytes32) { return keccak256(abi.encodePacked(_choice, _salt)); } @@ -738,17 +739,29 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi // * Internal * // // ************************************* // + /// @dev Returns the expected vote hash for a given vote. + /// @param _localDisputeID The ID of the dispute in the Dispute Kit. + /// @param _localRoundID The ID of the round in the Dispute Kit. + /// @param _voteID The ID of the vote. + /// @return The expected vote hash. + function getExpectedVoteHash( + uint256 _localDisputeID, + uint256 _localRoundID, + uint256 _voteID + ) internal view virtual returns (bytes32) { + return disputes[_localDisputeID].rounds[_localRoundID].votes[_voteID].commit; + } + /// @dev Checks that the chosen address satisfies certain conditions for being drawn. /// Note that we don't check the minStake requirement here because of the implicit staking in parent courts. /// minStake is checked directly during staking process however it's possible for the juror to get drawn /// while having < minStake if it is later increased by governance. /// This issue is expected and harmless. - /// @param _round The round in which the juror is being drawn. /// @param _coreDisputeID ID of the dispute in the core contract. /// @param _juror Chosen address. /// @return result Whether the address passes the check or not. function _postDrawCheck( - Round storage _round, + Round storage /*_round*/, uint256 _coreDisputeID, address _juror ) internal view virtual returns (bool result) { diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitShutter.sol b/contracts/src/arbitration/dispute-kits/DisputeKitShutter.sol index 0dfbf6985..2b1a6d063 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitShutter.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitShutter.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; +pragma solidity ^0.8.28; import {DisputeKitClassicBase, KlerosCore} from "./DisputeKitClassicBase.sol"; @@ -14,6 +14,19 @@ import {DisputeKitClassicBase, KlerosCore} from "./DisputeKitClassicBase.sol"; contract DisputeKitShutter is DisputeKitClassicBase { string public constant override version = "0.13.0"; + // ************************************* // + // * Storage * // + // ************************************* // + + mapping(uint256 localDisputeID => mapping(uint256 localRoundID => mapping(uint256 voteID => bytes32 recoveryCommitment))) + public recoveryCommitments; + + // ************************************* // + // * Transient Storage * // + // ************************************* // + + bool transient callerIsJuror; + // ************************************* // // * Events * // // ************************************* // @@ -22,12 +35,14 @@ contract DisputeKitShutter is DisputeKitClassicBase { /// @param _coreDisputeID The identifier of the dispute in the Arbitrator contract. /// @param _juror The address of the juror casting the vote commitment. /// @param _commit The commitment hash. + /// @param _recoveryCommit The commitment hash without the justification. /// @param _identity The Shutter identity used for encryption. /// @param _encryptedVote The Shutter encrypted vote. event CommitCastShutter( uint256 indexed _coreDisputeID, address indexed _juror, bytes32 indexed _commit, + bytes32 _recoveryCommit, bytes32 _identity, bytes _encryptedVote ); @@ -80,17 +95,29 @@ contract DisputeKitShutter is DisputeKitClassicBase { /// @param _coreDisputeID The ID of the dispute in Kleros Core. /// @param _voteIDs The IDs of the votes. /// @param _commit The commitment hash including the justification. + /// @param _recoveryCommit The commitment hash without the justification. /// @param _identity The Shutter identity used for encryption. /// @param _encryptedVote The Shutter encrypted vote. function castCommitShutter( uint256 _coreDisputeID, uint256[] calldata _voteIDs, bytes32 _commit, + bytes32 _recoveryCommit, bytes32 _identity, bytes calldata _encryptedVote ) external notJumped(_coreDisputeID) { + if (_recoveryCommit == bytes32(0)) revert EmptyRecoveryCommit(); + + uint256 localDisputeID = coreDisputeIDToLocal[_coreDisputeID]; + Dispute storage dispute = disputes[localDisputeID]; + uint256 localRoundID = dispute.rounds.length - 1; + for (uint256 i = 0; i < _voteIDs.length; i++) { + recoveryCommitments[localDisputeID][localRoundID][_voteIDs[i]] = _recoveryCommit; + } + + // `_castCommit()` ensures that the caller owns the vote _castCommit(_coreDisputeID, _voteIDs, _commit); - emit CommitCastShutter(_coreDisputeID, msg.sender, _commit, _identity, _encryptedVote); + emit CommitCastShutter(_coreDisputeID, msg.sender, _commit, _recoveryCommit, _identity, _encryptedVote); } function castVoteShutter( @@ -103,8 +130,12 @@ contract DisputeKitShutter is DisputeKitClassicBase { Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; address juror = dispute.rounds[dispute.rounds.length - 1].votes[_voteIDs[0]].account; - // _castVote() ensures that all the _voteIDs do belong to `juror` + callerIsJuror = juror == msg.sender; + + // `_castVote()` ensures that all the `_voteIDs` do belong to `juror` _castVote(_coreDisputeID, _voteIDs, _choice, _salt, _justification, juror); + + callerIsJuror = false; } // ************************************* // @@ -122,8 +153,37 @@ contract DisputeKitShutter is DisputeKitClassicBase { uint256 _choice, uint256 _salt, string memory _justification - ) public pure override returns (bytes32) { - bytes32 justificationHash = keccak256(bytes(_justification)); - return keccak256(abi.encode(_choice, _salt, justificationHash)); + ) public view override returns (bytes32) { + if (callerIsJuror) { + // Caller is the juror, hash without `_justification` to facilitate recovery. + return keccak256(abi.encodePacked(_choice, _salt)); + } else { + // Caller is not the juror, hash with `_justification`. + bytes32 justificationHash = keccak256(bytes(_justification)); + return keccak256(abi.encode(_choice, _salt, justificationHash)); + } + } + + /// @dev Returns the expected vote hash for a given vote. + /// @param _localDisputeID The ID of the dispute in the Dispute Kit. + /// @param _localRoundID The ID of the round in the Dispute Kit. + /// @param _voteID The ID of the vote. + /// @return The expected vote hash. + function getExpectedVoteHash( + uint256 _localDisputeID, + uint256 _localRoundID, + uint256 _voteID + ) internal view override returns (bytes32) { + if (callerIsJuror) { + return recoveryCommitments[_localDisputeID][_localRoundID][_voteID]; + } else { + return disputes[_localDisputeID].rounds[_localRoundID].votes[_voteID].commit; + } } + + // ************************************* // + // * Errors * // + // ************************************* // + + error EmptyRecoveryCommit(); } From bd589e7b8c75e86ebd450c4995c29b2c322cec4d Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Thu, 21 Aug 2025 19:35:02 +0100 Subject: [PATCH 084/175] chore: removing foundry compilation restrictions, not working as expected, causing stack too deep --- contracts/foundry.toml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/contracts/foundry.toml b/contracts/foundry.toml index 236208534..2dcdd2711 100644 --- a/contracts/foundry.toml +++ b/contracts/foundry.toml @@ -5,13 +5,7 @@ evm_version = "cancun" via_ir = true optimizer = true optimizer_runs = 10000 -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'] From 6f925141f37d66a03eadff093c1147353f0466f1 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Thu, 4 Sep 2025 00:02:03 +0100 Subject: [PATCH 085/175] refactor: add underscore prefix to internal function getExpectedVoteHash() --- .../src/arbitration/dispute-kits/DisputeKitClassicBase.sol | 4 ++-- .../src/arbitration/dispute-kits/DisputeKitShutter.sol | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol index d04ccf981..369789ab1 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol @@ -332,7 +332,7 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi // Save the votes. for (uint256 i = 0; i < _voteIDs.length; i++) { if (round.votes[_voteIDs[i]].account != _juror) revert JurorHasToOwnTheVote(); - if (hiddenVotes && getExpectedVoteHash(localDisputeID, localRoundID, _voteIDs[i]) != actualVoteHash) + if (hiddenVotes && _getExpectedVoteHash(localDisputeID, localRoundID, _voteIDs[i]) != actualVoteHash) revert HashDoesNotMatchHiddenVoteCommitment(); if (round.votes[_voteIDs[i]].voted) revert VoteAlreadyCast(); round.votes[_voteIDs[i]].choice = _choice; @@ -744,7 +744,7 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi /// @param _localRoundID The ID of the round in the Dispute Kit. /// @param _voteID The ID of the vote. /// @return The expected vote hash. - function getExpectedVoteHash( + function _getExpectedVoteHash( uint256 _localDisputeID, uint256 _localRoundID, uint256 _voteID diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitShutter.sol b/contracts/src/arbitration/dispute-kits/DisputeKitShutter.sol index 2b1a6d063..b8bef461f 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitShutter.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitShutter.sol @@ -164,12 +164,16 @@ contract DisputeKitShutter is DisputeKitClassicBase { } } + // ************************************* // + // * Internal * // + // ************************************* // + /// @dev Returns the expected vote hash for a given vote. /// @param _localDisputeID The ID of the dispute in the Dispute Kit. /// @param _localRoundID The ID of the round in the Dispute Kit. /// @param _voteID The ID of the vote. /// @return The expected vote hash. - function getExpectedVoteHash( + function _getExpectedVoteHash( uint256 _localDisputeID, uint256 _localRoundID, uint256 _voteID From 2cfeec4032ecdfe8e51ce45aed5fe4705491b40b Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Thu, 4 Sep 2025 00:02:22 +0100 Subject: [PATCH 086/175] chore: changelog --- contracts/CHANGELOG.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/contracts/CHANGELOG.md b/contracts/CHANGELOG.md index e6db23764..2e61786fb 100644 --- a/contracts/CHANGELOG.md +++ b/contracts/CHANGELOG.md @@ -15,28 +15,31 @@ The format is based on [Common Changelog](https://common-changelog.org/). - **Breaking:** Rename `governor` to `owner` in order to comply with the lightweight ownership standard [ERC-5313](https://eipsinsight.com/ercs/erc-5313) ([#2112](https://github.com/kleros/kleros-v2/issues/2112)) - **Breaking:** Apply the penalties to the stakes in the Sortition Tree ([#2107](https://github.com/kleros/kleros-v2/issues/2107)) - **Breaking:** Make `SortitionModule.getJurorBalance().stakedInCourt` include the penalties ([#2107](https://github.com/kleros/kleros-v2/issues/2107)) -- **Breaking:** Add a new field `drawnJurorFromCourtIDs` to the `Round` struct in `KlerosCoreBase` and `KlerosCoreUniversity` ([#2107](https://github.com/kleros/kleros-v2/issues/2107)) -- **Breaking:** Add a new state variable `jumpDisputeKitID` to the `DisputeKitClassicBase` contract ([#2114](https://github.com/kleros/kleros-v2/issues/2114)) - Make `IDisputeKit.draw()` and `ISortitionModule.draw()` return the court ID from which the juror was drawn ([#2107](https://github.com/kleros/kleros-v2/issues/2107)) - Rename `SortitionModule.setJurorInactive()` to `SortitionModule.forcedUnstakeAllCourts()` ([#2107](https://github.com/kleros/kleros-v2/issues/2107)) -- Allow stake changes to by-pass delayed stakes when initiated by the SortitionModule by setting the `_noDelay` parameter to `true` in `SortitionModule.validateStake()` ([#2107](https://github.com/kleros/kleros-v2/issues/2107)) - 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)) - Replace the `bytes32 _key` parameter in `SortitionTrees.createTree()` and `SortitionTrees.draw()` by `uint96 courtID` ([#2113](https://github.com/kleros/kleros-v2/issues/2113)) - Extract the sortition sum trees logic into a library `SortitionTrees` ([#2113](https://github.com/kleros/kleros-v2/issues/2113)) +- 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)) - 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)) ### Added +- **Breaking:** Add a new field `drawnJurorFromCourtIDs` to the `Round` struct in `KlerosCoreBase` and `KlerosCoreUniversity` ([#2107](https://github.com/kleros/kleros-v2/issues/2107)) +- **Breaking:** Add a new state variable `jumpDisputeKitID` to the `DisputeKitClassicBase` contract ([#2114](https://github.com/kleros/kleros-v2/issues/2114)) +- **Breaking:** Add a parameter `_recoveryCommit` to the event `DisputeKitShutter.CommitCastShutter` ([#2100](https://github.com/kleros/kleros-v2/issues/2100)) +- **Breaking:** Add a storage variable `recoveryCommitments` to `DisputeKitShutter` ([#2100](https://github.com/kleros/kleros-v2/issues/2100)) +- Allow the Shutter commitment to be recovered by the juror using only the salt and the choice, without having to provide the justification ([#2100](https://github.com/kleros/kleros-v2/issues/2100)) - Allow the dispute kits to force an early court jump and to override the number of votes after an appeal (future-proofing) ([#2110](https://github.com/kleros/kleros-v2/issues/2110)) - Allow the dispute kits to specify which new dispute kit to use when a court jump occurs ([#2114](https://github.com/kleros/kleros-v2/issues/2114)) +- Allow stake changes to by-pass delayed stakes when initiated by the SortitionModule by setting the `_noDelay` parameter to `true` in `SortitionModule.validateStake()` ([#2107](https://github.com/kleros/kleros-v2/issues/2107)) ### Fixed From 4aa2cd245cc0c5564cb2631ae0b5ce8bcde55ea1 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Thu, 4 Sep 2025 00:08:04 +0100 Subject: [PATCH 087/175] feat: applied the changes to the GatedShutter DK --- .../dispute-kits/DisputeKitGatedShutter.sol | 70 +++++++++++++++++-- 1 file changed, 65 insertions(+), 5 deletions(-) diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitGatedShutter.sol b/contracts/src/arbitration/dispute-kits/DisputeKitGatedShutter.sol index 158bfc5a6..67d9ddbea 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitGatedShutter.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitGatedShutter.sol @@ -30,6 +30,19 @@ interface IBalanceHolderERC1155 { contract DisputeKitGatedShutter is DisputeKitClassicBase { string public constant override version = "0.13.0"; + // ************************************* // + // * Storage * // + // ************************************* // + + mapping(uint256 localDisputeID => mapping(uint256 localRoundID => mapping(uint256 voteID => bytes32 recoveryCommitment))) + public recoveryCommitments; + + // ************************************* // + // * Transient Storage * // + // ************************************* // + + bool transient callerIsJuror; + // ************************************* // // * Events * // // ************************************* // @@ -38,12 +51,14 @@ contract DisputeKitGatedShutter is DisputeKitClassicBase { /// @param _coreDisputeID The identifier of the dispute in the Arbitrator contract. /// @param _juror The address of the juror casting the vote commitment. /// @param _commit The commitment hash. + /// @param _recoveryCommit The commitment hash without the justification. /// @param _identity The Shutter identity used for encryption. /// @param _encryptedVote The Shutter encrypted vote. event CommitCastShutter( uint256 indexed _coreDisputeID, address indexed _juror, bytes32 indexed _commit, + bytes32 _recoveryCommit, bytes32 _identity, bytes _encryptedVote ); @@ -96,17 +111,29 @@ contract DisputeKitGatedShutter is DisputeKitClassicBase { /// @param _coreDisputeID The ID of the dispute in Kleros Core. /// @param _voteIDs The IDs of the votes. /// @param _commit The commitment hash including the justification. + /// @param _recoveryCommit The commitment hash without the justification. /// @param _identity The Shutter identity used for encryption. /// @param _encryptedVote The Shutter encrypted vote. function castCommitShutter( uint256 _coreDisputeID, uint256[] calldata _voteIDs, bytes32 _commit, + bytes32 _recoveryCommit, bytes32 _identity, bytes calldata _encryptedVote ) external notJumped(_coreDisputeID) { + if (_recoveryCommit == bytes32(0)) revert EmptyRecoveryCommit(); + + uint256 localDisputeID = coreDisputeIDToLocal[_coreDisputeID]; + Dispute storage dispute = disputes[localDisputeID]; + uint256 localRoundID = dispute.rounds.length - 1; + for (uint256 i = 0; i < _voteIDs.length; i++) { + recoveryCommitments[localDisputeID][localRoundID][_voteIDs[i]] = _recoveryCommit; + } + + // `_castCommit()` ensures that the caller owns the vote _castCommit(_coreDisputeID, _voteIDs, _commit); - emit CommitCastShutter(_coreDisputeID, msg.sender, _commit, _identity, _encryptedVote); + emit CommitCastShutter(_coreDisputeID, msg.sender, _commit, _recoveryCommit, _identity, _encryptedVote); } function castVoteShutter( @@ -119,8 +146,12 @@ contract DisputeKitGatedShutter is DisputeKitClassicBase { Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; address juror = dispute.rounds[dispute.rounds.length - 1].votes[_voteIDs[0]].account; - // _castVote() ensures that all the _voteIDs do belong to `juror` + callerIsJuror = juror == msg.sender; + + // `_castVote()` ensures that all the `_voteIDs` do belong to `juror` _castVote(_coreDisputeID, _voteIDs, _choice, _salt, _justification, juror); + + callerIsJuror = false; } // ************************************* // @@ -138,15 +169,38 @@ contract DisputeKitGatedShutter is DisputeKitClassicBase { uint256 _choice, uint256 _salt, string memory _justification - ) public pure override returns (bytes32) { - bytes32 justificationHash = keccak256(bytes(_justification)); - return keccak256(abi.encode(_choice, _salt, justificationHash)); + ) public view override returns (bytes32) { + if (callerIsJuror) { + // Caller is the juror, hash without `_justification` to facilitate recovery. + return keccak256(abi.encodePacked(_choice, _salt)); + } else { + // Caller is not the juror, hash with `_justification`. + bytes32 justificationHash = keccak256(bytes(_justification)); + return keccak256(abi.encode(_choice, _salt, justificationHash)); + } } // ************************************* // // * Internal * // // ************************************* // + /// @dev Returns the expected vote hash for a given vote. + /// @param _localDisputeID The ID of the dispute in the Dispute Kit. + /// @param _localRoundID The ID of the round in the Dispute Kit. + /// @param _voteID The ID of the vote. + /// @return The expected vote hash. + function _getExpectedVoteHash( + uint256 _localDisputeID, + uint256 _localRoundID, + uint256 _voteID + ) internal view override returns (bytes32) { + if (callerIsJuror) { + return recoveryCommitments[_localDisputeID][_localRoundID][_voteID]; + } else { + return disputes[_localDisputeID].rounds[_localRoundID].votes[_voteID].commit; + } + } + /// @dev Extracts token gating information from the extra data. /// @param _extraData The extra data bytes array with the following encoding: /// - bytes 0-31: uint96 courtID, not used here @@ -197,4 +251,10 @@ contract DisputeKitGatedShutter is DisputeKitClassicBase { return IBalanceHolder(tokenGate).balanceOf(_juror) > 0; } } + + // ************************************* // + // * Errors * // + // ************************************* // + + error EmptyRecoveryCommit(); } From b490c9628b51e63807b03b1fa53381761f00f6fb Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Thu, 4 Sep 2025 01:41:02 +0100 Subject: [PATCH 088/175] test: coverage for the shutter dispute kit including recovery flow --- .../test/arbitration/dispute-kit-shutter.ts | 733 ++++++++++++++++++ 1 file changed, 733 insertions(+) create mode 100644 contracts/test/arbitration/dispute-kit-shutter.ts diff --git a/contracts/test/arbitration/dispute-kit-shutter.ts b/contracts/test/arbitration/dispute-kit-shutter.ts new file mode 100644 index 000000000..aab7efa58 --- /dev/null +++ b/contracts/test/arbitration/dispute-kit-shutter.ts @@ -0,0 +1,733 @@ +import { deployments, ethers, getNamedAccounts, network } from "hardhat"; +import { toBigInt, BigNumberish } from "ethers"; +import { PNK, KlerosCore, SortitionModule, IncrementalNG, DisputeKitShutter } from "../../typechain-types"; +import { expect } from "chai"; +import { Courts } from "../../deploy/utils"; +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; + +/* eslint-disable no-unused-vars */ +/* eslint-disable no-unused-expressions */ // https://github.com/standard/standard/issues/690#issuecomment-278533482 + +describe("DisputeKitShutter", async () => { + const ONE_THOUSAND_PNK = 10n ** 21n; + const thousandPNK = (amount: BigNumberish) => toBigInt(amount) * ONE_THOUSAND_PNK; + + let deployer: string; + let juror1: HardhatEthersSigner; + let juror2: HardhatEthersSigner; + let bot: HardhatEthersSigner; + let attacker: HardhatEthersSigner; + let disputeKitShutter: DisputeKitShutter; + let pnk: PNK; + let core: KlerosCore; + let sortitionModule: SortitionModule; + let rng: IncrementalNG; + const RANDOM = 424242n; + const SHUTTER_DK_ID = 2; + const SHUTTER_COURT_ID = 2; // Court with hidden votes for testing + + // Test data + const choice = 1n; + const salt = 12345n; + const justification = "This is my justification for the vote"; + const identity = ethers.keccak256(ethers.toUtf8Bytes("shutter-identity")); + const encryptedVote = ethers.toUtf8Bytes("encrypted-vote-data"); + + beforeEach("Setup", async () => { + ({ deployer } = await getNamedAccounts()); + [, juror1, juror2, bot, attacker] = await ethers.getSigners(); + + await deployments.fixture(["Arbitration", "VeaMock"], { + fallbackToGlobal: true, + keepExistingDeployments: false, + }); + disputeKitShutter = await ethers.getContract("DisputeKitShutter"); + pnk = await ethers.getContract("PNK"); + core = await ethers.getContract("KlerosCore"); + sortitionModule = await ethers.getContract("SortitionModule"); + + // Make the tests more deterministic with this dummy RNG + await deployments.deploy("IncrementalNG", { + from: deployer, + args: [RANDOM], + log: true, + }); + rng = await ethers.getContract("IncrementalNG"); + + await sortitionModule.changeRandomNumberGenerator(rng.target).then((tx) => tx.wait()); + + // Create a court with hidden votes enabled for testing DisputeKitShutter + // Parameters: parent, hiddenVotes, minStake, alpha, feeForJuror, jurorsForCourtJump, timesPerPeriod, sortitionExtraData, supportedDisputeKits + await core.createCourt( + Courts.GENERAL, // parent + true, // hiddenVotes - MUST be true for DisputeKitShutter + ethers.parseEther("200"), // minStake + 10000, // alpha + ethers.parseEther("0.1"), // feeForJuror + 16, // jurorsForCourtJump + [300, 300, 300, 300], // timesPerPeriod for evidence, commit, vote, appeal + ethers.toBeHex(5), // sortitionExtraData + [1, SHUTTER_DK_ID] // supportedDisputeKits - must include Classic (1) and Shutter (2) + ); + + // The new court ID should be 2 (after GENERAL court which is 1) + }); + + // ************************************* // + // * Constants * // + // ************************************* // + + const enum Period { + evidence = 0, + commit = 1, + vote = 2, + appeal = 3, + execution = 4, + } + + // ************************************* // + // * Helper Functions * // + // ************************************* // + + const encodeExtraData = (courtId: BigNumberish, minJurors: BigNumberish, disputeKitId: number) => + ethers.AbiCoder.defaultAbiCoder().encode(["uint256", "uint256", "uint256"], [courtId, minJurors, disputeKitId]); + + const generateCommitments = (choice: bigint, salt: bigint, justification: string) => { + // Recovery commitment: hash(choice, salt) - no justification + const recoveryCommit = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(["uint256", "uint256"], [choice, salt]) + ); + + // Full commitment: hash(choice, salt, justificationHash) + const justificationHash = ethers.keccak256(ethers.toUtf8Bytes(justification)); + const fullCommit = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(["uint256", "uint256", "bytes32"], [choice, salt, justificationHash]) + ); + + return { fullCommit, recoveryCommit }; + }; + + const createDisputeAndDraw = async (courtId: BigNumberish, minJurors: BigNumberish, disputeKitId: number) => { + // Stake jurors + for (const juror of [juror1, juror2]) { + await pnk.transfer(juror.address, thousandPNK(10)).then((tx) => tx.wait()); + expect(await pnk.balanceOf(juror.address)).to.equal(thousandPNK(10)); + + await pnk + .connect(juror) + .approve(core.target, thousandPNK(10), { gasLimit: 300000 }) + .then((tx) => tx.wait()); + + await core + .connect(juror) + .setStake(SHUTTER_COURT_ID, thousandPNK(10), { gasLimit: 500000 }) + .then((tx) => tx.wait()); + + expect(await sortitionModule.getJurorBalance(juror.address, SHUTTER_COURT_ID)).to.deep.equal([ + thousandPNK(10), // totalStaked + 0, // totalLocked + thousandPNK(10), // stakedInCourt + 1, // nbOfCourts + ]); + } + + const extraData = encodeExtraData(courtId, minJurors, disputeKitId); + const arbitrationCost = await core["arbitrationCost(bytes)"](extraData); + + // Create dispute via core contract + await core["createDispute(uint256,bytes)"](2, extraData, { value: arbitrationCost }).then((tx) => tx.wait()); + const disputeId = 0; + + await network.provider.send("evm_increaseTime", [2000]); // Wait for minStakingTime + await network.provider.send("evm_mine"); + await sortitionModule.passPhase().then((tx) => tx.wait()); // Staking -> Generating + + await sortitionModule.passPhase().then((tx) => tx.wait()); // Generating -> Drawing + await core.draw(disputeId, 70, { gasLimit: 10000000 }); + + return disputeId; + }; + + const advanceToCommitPeriod = async (disputeId: number) => { + // Advance from evidence to commit period + await core.passPeriod(disputeId).then((tx) => tx.wait()); + + // Verify we're in commit period + const dispute = await core.disputes(disputeId); + expect(dispute[2]).to.equal(Period.commit); // period is at index 2 + }; + + const advanceToVotePeriod = async (disputeId: number) => { + // Advance from commit to vote period + const dispute = await core.disputes(disputeId); + const courtId = dispute[0]; // courtID is at index 0 + const court = await core.courts(courtId); + // Court struct: parent, hiddenVotes, children[], minStake, alpha, feeForJuror, jurorsForCourtJump, disabled, timesPerPeriod[] + // timesPerPeriod is a mapping, we need to check the actual structure + const timesPerPeriod = [300, 300, 300, 300]; // Default times from deployment + const commitPeriod = timesPerPeriod[Period.commit]; + + await network.provider.send("evm_increaseTime", [Number(commitPeriod)]); + await network.provider.send("evm_mine"); + + await core.passPeriod(disputeId).then((tx) => tx.wait()); + + // Verify we're in vote period + const updatedDispute = await core.disputes(disputeId); + expect(updatedDispute[2]).to.equal(Period.vote); // period is at index 2 + }; + + const getVoteIDsForJuror = async (disputeId: number, juror: HardhatEthersSigner) => { + const localDisputeId = await disputeKitShutter.coreDisputeIDToLocal(disputeId); + const nbRounds = await disputeKitShutter.getNumberOfRounds(localDisputeId); + const roundIndex = Number(nbRounds) - 1; + + // Get all votes for this round and filter by juror + const voteIDs: bigint[] = []; + const maxVotes = 10; // Reasonable limit for testing + + for (let i = 0; i < maxVotes; i++) { + try { + const voteInfo = await disputeKitShutter.getVoteInfo(disputeId, roundIndex, i); + if (voteInfo[0] === juror.address) { + // account is at index 0 + voteIDs.push(BigInt(i)); + } + } catch { + // No more votes + break; + } + } + + return voteIDs; + }; + + // ************************************* // + // * Tests * // + // ************************************* // + + describe("Commit Phase - castCommitShutter()", () => { + describe("Successful commits", () => { + it("Should allow juror to commit vote with recovery commitment", async () => { + // Use the court with hidden votes (court ID 2) + const disputeId = await createDisputeAndDraw(2, 3, SHUTTER_DK_ID); + await advanceToCommitPeriod(disputeId); + + const voteIDs = await getVoteIDsForJuror(disputeId, juror1); + expect(voteIDs.length).to.be.greaterThan(0); + + const { fullCommit, recoveryCommit } = generateCommitments(choice, salt, justification); + + await expect( + disputeKitShutter + .connect(juror1) + .castCommitShutter(disputeId, voteIDs, fullCommit, recoveryCommit, identity, encryptedVote) + ) + .to.emit(disputeKitShutter, "CommitCastShutter") + .withArgs(disputeId, juror1.address, fullCommit, recoveryCommit, identity, encryptedVote); + + // Verify recovery commitment was stored + const localDisputeId = await disputeKitShutter.coreDisputeIDToLocal(disputeId); + const storedRecoveryCommit = await disputeKitShutter.recoveryCommitments(localDisputeId, 0, voteIDs[0]); + expect(storedRecoveryCommit).to.equal(recoveryCommit); + }); + + it("Should allow juror to update commitment multiple times", async () => { + // Use the court with hidden votes (court ID 2) + const disputeId = await createDisputeAndDraw(2, 3, SHUTTER_DK_ID); + await advanceToCommitPeriod(disputeId); + + const voteIDs = await getVoteIDsForJuror(disputeId, juror1); + + // First commitment + const { fullCommit: commit1, recoveryCommit: recovery1 } = generateCommitments(1n, 111n, "First justification"); + await disputeKitShutter + .connect(juror1) + .castCommitShutter(disputeId, voteIDs, commit1, recovery1, identity, encryptedVote); + + // Second commitment (overwrites first) + const { fullCommit: commit2, recoveryCommit: recovery2 } = generateCommitments( + 2n, + 222n, + "Second justification" + ); + await disputeKitShutter + .connect(juror1) + .castCommitShutter(disputeId, voteIDs, commit2, recovery2, identity, encryptedVote); + + // Verify only the second commitment is stored + const localDisputeId = await disputeKitShutter.coreDisputeIDToLocal(disputeId); + const storedRecoveryCommit = await disputeKitShutter.recoveryCommitments(localDisputeId, 0, voteIDs[0]); + expect(storedRecoveryCommit).to.equal(recovery2); + }); + }); + + describe("Failed commits", () => { + it("Should revert if recovery commitment is empty", async () => { + // Use the court with hidden votes (court ID 2) + const disputeId = await createDisputeAndDraw(2, 3, SHUTTER_DK_ID); + await advanceToCommitPeriod(disputeId); + + const voteIDs = await getVoteIDsForJuror(disputeId, juror1); + const { fullCommit } = generateCommitments(choice, salt, justification); + + await expect( + disputeKitShutter.connect(juror1).castCommitShutter( + disputeId, + voteIDs, + fullCommit, + ethers.ZeroHash, // Empty recovery commit + identity, + encryptedVote + ) + ).to.be.revertedWithCustomError(disputeKitShutter, "EmptyRecoveryCommit"); + }); + + it("Should revert if not in commit period", async () => { + // Use the court with hidden votes (court ID 2) + const disputeId = await createDisputeAndDraw(2, 3, SHUTTER_DK_ID); + // Still in evidence period + + const voteIDs = await getVoteIDsForJuror(disputeId, juror1); + const { fullCommit, recoveryCommit } = generateCommitments(choice, salt, justification); + + await expect( + disputeKitShutter + .connect(juror1) + .castCommitShutter(disputeId, voteIDs, fullCommit, recoveryCommit, identity, encryptedVote) + ).to.be.revertedWithCustomError(disputeKitShutter, "NotCommitPeriod"); + }); + + it("Should revert if juror doesn't own the vote", async () => { + // Use the court with hidden votes (court ID 2) + const disputeId = await createDisputeAndDraw(2, 3, SHUTTER_DK_ID); + await advanceToCommitPeriod(disputeId); + + const voteIDs = await getVoteIDsForJuror(disputeId, juror1); + const { fullCommit, recoveryCommit } = generateCommitments(choice, salt, justification); + + await expect( + disputeKitShutter.connect(juror2).castCommitShutter( + disputeId, + voteIDs, // Using juror1's vote IDs + fullCommit, + recoveryCommit, + identity, + encryptedVote + ) + ).to.be.revertedWithCustomError(disputeKitShutter, "JurorHasToOwnTheVote"); + }); + }); + }); + + describe("Normal Flow - Bot Reveals", () => { + describe("Successful reveals", () => { + it("Should allow bot to reveal vote with full justification", async () => { + // Use the court with hidden votes (court ID 2) + const disputeId = await createDisputeAndDraw(2, 3, SHUTTER_DK_ID); + await advanceToCommitPeriod(disputeId); + + const voteIDs = await getVoteIDsForJuror(disputeId, juror1); + const { fullCommit, recoveryCommit } = generateCommitments(choice, salt, justification); + + // Juror commits + await disputeKitShutter + .connect(juror1) + .castCommitShutter(disputeId, voteIDs, fullCommit, recoveryCommit, identity, encryptedVote); + + await advanceToVotePeriod(disputeId); + + // Bot reveals vote + await expect(disputeKitShutter.connect(bot).castVoteShutter(disputeId, voteIDs, choice, salt, justification)) + .to.emit(disputeKitShutter, "VoteCast") + .withArgs(disputeId, juror1.address, voteIDs, choice, justification); + + // Verify vote was counted + const voteInfo = await disputeKitShutter.getVoteInfo(disputeId, 0, Number(voteIDs[0])); + expect(voteInfo[3]).to.be.true; // voted is at index 3 + expect(voteInfo[2]).to.equal(choice); // choice is at index 2 + }); + }); + + describe("Failed reveals", () => { + it("Should revert if wrong choice provided", async () => { + // Use the court with hidden votes (court ID 2) + const disputeId = await createDisputeAndDraw(2, 3, SHUTTER_DK_ID); + await advanceToCommitPeriod(disputeId); + + const voteIDs = await getVoteIDsForJuror(disputeId, juror1); + const { fullCommit, recoveryCommit } = generateCommitments(choice, salt, justification); + + await disputeKitShutter + .connect(juror1) + .castCommitShutter(disputeId, voteIDs, fullCommit, recoveryCommit, identity, encryptedVote); + + await advanceToVotePeriod(disputeId); + + const wrongChoice = 2n; + await expect( + disputeKitShutter.connect(bot).castVoteShutter( + disputeId, + voteIDs, + wrongChoice, // Wrong choice + salt, + justification + ) + ).to.be.revertedWithCustomError(disputeKitShutter, "HashDoesNotMatchHiddenVoteCommitment"); + }); + + it("Should revert if wrong salt provided", async () => { + // Use the court with hidden votes (court ID 2) + const disputeId = await createDisputeAndDraw(2, 3, SHUTTER_DK_ID); + await advanceToCommitPeriod(disputeId); + + const voteIDs = await getVoteIDsForJuror(disputeId, juror1); + const { fullCommit, recoveryCommit } = generateCommitments(choice, salt, justification); + + await disputeKitShutter + .connect(juror1) + .castCommitShutter(disputeId, voteIDs, fullCommit, recoveryCommit, identity, encryptedVote); + + await advanceToVotePeriod(disputeId); + + const wrongSalt = 99999n; + await expect( + disputeKitShutter.connect(bot).castVoteShutter( + disputeId, + voteIDs, + choice, + wrongSalt, // Wrong salt + justification + ) + ).to.be.revertedWithCustomError(disputeKitShutter, "HashDoesNotMatchHiddenVoteCommitment"); + }); + + it("Should revert if wrong justification provided", async () => { + // Use the court with hidden votes (court ID 2) + const disputeId = await createDisputeAndDraw(2, 3, SHUTTER_DK_ID); + await advanceToCommitPeriod(disputeId); + + const voteIDs = await getVoteIDsForJuror(disputeId, juror1); + const { fullCommit, recoveryCommit } = generateCommitments(choice, salt, justification); + + await disputeKitShutter + .connect(juror1) + .castCommitShutter(disputeId, voteIDs, fullCommit, recoveryCommit, identity, encryptedVote); + + await advanceToVotePeriod(disputeId); + + const wrongJustification = "Wrong justification"; + await expect( + disputeKitShutter.connect(bot).castVoteShutter( + disputeId, + voteIDs, + choice, + salt, + wrongJustification // Wrong justification + ) + ).to.be.revertedWithCustomError(disputeKitShutter, "HashDoesNotMatchHiddenVoteCommitment"); + }); + + it("Should revert if vote already cast", async () => { + // Use the court with hidden votes (court ID 2) + const disputeId = await createDisputeAndDraw(2, 3, SHUTTER_DK_ID); + await advanceToCommitPeriod(disputeId); + + const voteIDs = await getVoteIDsForJuror(disputeId, juror1); + const { fullCommit, recoveryCommit } = generateCommitments(choice, salt, justification); + + await disputeKitShutter + .connect(juror1) + .castCommitShutter(disputeId, voteIDs, fullCommit, recoveryCommit, identity, encryptedVote); + + await advanceToVotePeriod(disputeId); + + // First vote succeeds + await disputeKitShutter.connect(bot).castVoteShutter(disputeId, voteIDs, choice, salt, justification); + + // Second vote fails + await expect( + disputeKitShutter.connect(bot).castVoteShutter(disputeId, voteIDs, choice, salt, justification) + ).to.be.revertedWithCustomError(disputeKitShutter, "VoteAlreadyCast"); + }); + }); + }); + + describe("Recovery Flow - Juror Reveals", () => { + describe("Successful recovery reveals", () => { + it("Should allow juror to recover vote without justification", async () => { + // Use the court with hidden votes (court ID 2) + const disputeId = await createDisputeAndDraw(2, 3, SHUTTER_DK_ID); + await advanceToCommitPeriod(disputeId); + + const voteIDs = await getVoteIDsForJuror(disputeId, juror1); + const { fullCommit, recoveryCommit } = generateCommitments(choice, salt, justification); + + // Juror commits + await disputeKitShutter + .connect(juror1) + .castCommitShutter(disputeId, voteIDs, fullCommit, recoveryCommit, identity, encryptedVote); + + await advanceToVotePeriod(disputeId); + + // Juror reveals vote (Shutter failed, so juror must reveal) + // Note: justification can be anything as it won't be validated + await expect( + disputeKitShutter.connect(juror1).castVoteShutter( + disputeId, + voteIDs, + choice, + salt, + "" // Empty justification is fine for recovery + ) + ) + .to.emit(disputeKitShutter, "VoteCast") + .withArgs(disputeId, juror1.address, voteIDs, choice, ""); + + // Verify vote was counted + const voteInfo = await disputeKitShutter.getVoteInfo(disputeId, 0, Number(voteIDs[0])); + expect(voteInfo[3]).to.be.true; // voted is at index 3 + expect(voteInfo[2]).to.equal(choice); // choice is at index 2 + }); + + it("Should validate against recovery commitment when juror reveals", async () => { + // Use the court with hidden votes (court ID 2) + const disputeId = await createDisputeAndDraw(2, 3, SHUTTER_DK_ID); + await advanceToCommitPeriod(disputeId); + + const voteIDs = await getVoteIDsForJuror(disputeId, juror1); + const { fullCommit, recoveryCommit } = generateCommitments(choice, salt, justification); + + await disputeKitShutter + .connect(juror1) + .castCommitShutter(disputeId, voteIDs, fullCommit, recoveryCommit, identity, encryptedVote); + + await advanceToVotePeriod(disputeId); + + // Juror can provide any justification - it won't be validated + const differentJustification = "This is a different justification that won't be checked"; + await expect( + disputeKitShutter.connect(juror1).castVoteShutter( + disputeId, + voteIDs, + choice, + salt, + differentJustification // Different justification is OK for recovery + ) + ).to.not.be.reverted; + }); + }); + + describe("Failed recovery reveals", () => { + it("Should revert if wrong choice in recovery", async () => { + // Use the court with hidden votes (court ID 2) + const disputeId = await createDisputeAndDraw(2, 3, SHUTTER_DK_ID); + await advanceToCommitPeriod(disputeId); + + const voteIDs = await getVoteIDsForJuror(disputeId, juror1); + const { fullCommit, recoveryCommit } = generateCommitments(choice, salt, justification); + + await disputeKitShutter + .connect(juror1) + .castCommitShutter(disputeId, voteIDs, fullCommit, recoveryCommit, identity, encryptedVote); + + await advanceToVotePeriod(disputeId); + + const wrongChoice = 2n; + await expect( + disputeKitShutter.connect(juror1).castVoteShutter( + disputeId, + voteIDs, + wrongChoice, // Wrong choice + salt, + "" + ) + ).to.be.revertedWithCustomError(disputeKitShutter, "HashDoesNotMatchHiddenVoteCommitment"); + }); + + it("Should revert if wrong salt in recovery", async () => { + // Use the court with hidden votes (court ID 2) + const disputeId = await createDisputeAndDraw(2, 3, SHUTTER_DK_ID); + await advanceToCommitPeriod(disputeId); + + const voteIDs = await getVoteIDsForJuror(disputeId, juror1); + const { fullCommit, recoveryCommit } = generateCommitments(choice, salt, justification); + + await disputeKitShutter + .connect(juror1) + .castCommitShutter(disputeId, voteIDs, fullCommit, recoveryCommit, identity, encryptedVote); + + await advanceToVotePeriod(disputeId); + + const wrongSalt = 99999n; + await expect( + disputeKitShutter.connect(juror1).castVoteShutter( + disputeId, + voteIDs, + choice, + wrongSalt, // Wrong salt + "" + ) + ).to.be.revertedWithCustomError(disputeKitShutter, "HashDoesNotMatchHiddenVoteCommitment"); + }); + + it("Should revert if non-juror tries to reveal without correct full commitment", async () => { + // Use the court with hidden votes (court ID 2) + const disputeId = await createDisputeAndDraw(2, 3, SHUTTER_DK_ID); + await advanceToCommitPeriod(disputeId); + + const voteIDs = await getVoteIDsForJuror(disputeId, juror1); + const { fullCommit, recoveryCommit } = generateCommitments(choice, salt, justification); + + await disputeKitShutter + .connect(juror1) + .castCommitShutter(disputeId, voteIDs, fullCommit, recoveryCommit, identity, encryptedVote); + + await advanceToVotePeriod(disputeId); + + // Attacker tries to reveal with only choice and salt (no justification) + await expect( + disputeKitShutter.connect(attacker).castVoteShutter( + disputeId, + voteIDs, + choice, + salt, + "" // No justification - would work for juror but not for others + ) + ).to.be.revertedWithCustomError(disputeKitShutter, "HashDoesNotMatchHiddenVoteCommitment"); + }); + }); + }); + + describe("Hash Function Behavior", () => { + it("Should return different hashes for juror vs non-juror callers", async () => { + const disputeId = await createDisputeAndDraw(2, 3, SHUTTER_DK_ID); + await advanceToCommitPeriod(disputeId); + + const voteIDs = await getVoteIDsForJuror(disputeId, juror1); + const { fullCommit, recoveryCommit } = generateCommitments(choice, salt, justification); + + await disputeKitShutter + .connect(juror1) + .castCommitShutter(disputeId, voteIDs, fullCommit, recoveryCommit, identity, encryptedVote); + + await advanceToVotePeriod(disputeId); + + // During castVoteShutter, the contract should use different hash logic + // For juror: hash(choice, salt) + // For non-juror: hash(choice, salt, justificationHash) + + // This is tested implicitly by the recovery flow tests above + // The juror can reveal with any justification, while non-juror must provide exact justification + }); + + it("Should correctly compute hash for normal flow", async () => { + // Test hashVote function directly + const justificationHash = ethers.keccak256(ethers.toUtf8Bytes(justification)); + const expectedHash = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(["uint256", "uint256", "bytes32"], [choice, salt, justificationHash]) + ); + + // When called by non-juror (normal case), should include justification + const computedHash = await disputeKitShutter.hashVote(choice, salt, justification); + expect(computedHash).to.equal(expectedHash); + }); + }); + + describe("Edge Cases and Security", () => { + it("Should handle mixed normal and recovery reveals in same dispute", async () => { + const disputeId = await createDisputeAndDraw(2, 3, SHUTTER_DK_ID); + await advanceToCommitPeriod(disputeId); + + const voteIDsJuror1 = await getVoteIDsForJuror(disputeId, juror1); + const voteIDsJuror2 = await getVoteIDsForJuror(disputeId, juror2); + + const { fullCommit: commit1, recoveryCommit: recovery1 } = generateCommitments(1n, 111n, "Juror 1 justification"); + const { fullCommit: commit2, recoveryCommit: recovery2 } = generateCommitments(2n, 222n, "Juror 2 justification"); + + // Both jurors commit + await disputeKitShutter + .connect(juror1) + .castCommitShutter(disputeId, voteIDsJuror1, commit1, recovery1, identity, encryptedVote); + + await disputeKitShutter + .connect(juror2) + .castCommitShutter(disputeId, voteIDsJuror2, commit2, recovery2, identity, encryptedVote); + + await advanceToVotePeriod(disputeId); + + // Juror1 uses recovery flow (Shutter failed for them) + await disputeKitShutter.connect(juror1).castVoteShutter( + disputeId, + voteIDsJuror1, + 1n, + 111n, + "Different justification" // Recovery doesn't check this + ); + + // Bot reveals juror2's vote normally + await disputeKitShutter.connect(bot).castVoteShutter( + disputeId, + voteIDsJuror2, + 2n, + 222n, + "Juror 2 justification" // Must match exactly + ); + + // Verify both votes were counted + const vote1Info = await disputeKitShutter.getVoteInfo(disputeId, 0, Number(voteIDsJuror1[0])); + const vote2Info = await disputeKitShutter.getVoteInfo(disputeId, 0, Number(voteIDsJuror2[0])); + + expect(vote1Info[3]).to.be.true; // voted is at index 3 + expect(vote1Info[2]).to.equal(1n); // choice is at index 2 + expect(vote2Info[3]).to.be.true; + expect(vote2Info[2]).to.equal(2n); + }); + + it("Should allow anyone to reveal vote with correct data only", async () => { + const disputeId = await createDisputeAndDraw(2, 3, SHUTTER_DK_ID); + await advanceToCommitPeriod(disputeId); + + const voteIDsJuror1 = await getVoteIDsForJuror(disputeId, juror1); + const { fullCommit, recoveryCommit } = generateCommitments(choice, salt, justification); + + await disputeKitShutter + .connect(juror1) + .castCommitShutter(disputeId, voteIDsJuror1, fullCommit, recoveryCommit, identity, encryptedVote); + + // Juror2 commits with a different choice + const differentChoice = 2n; + const voteIDsJuror2 = await getVoteIDsForJuror(disputeId, juror2); + const { fullCommit: commit2, recoveryCommit: recovery2 } = generateCommitments( + differentChoice, + salt, + justification + ); + + await disputeKitShutter + .connect(juror2) + .castCommitShutter(disputeId, voteIDsJuror2, commit2, recovery2, identity, encryptedVote); + + await advanceToVotePeriod(disputeId); + + // In normal Shutter operation, anyone (bot/attacker) can reveal the vote if they have the correct data + // This is by design - the security comes from the fact that only Shutter knows the decryption key + await expect( + disputeKitShutter.connect(attacker).castVoteShutter(disputeId, voteIDsJuror1, choice, salt, justification) + ) + .to.emit(disputeKitShutter, "VoteCast") + .withArgs(disputeId, juror1.address, voteIDsJuror1, choice, justification); + + // Attacker cannot change juror2's vote to a different choice + await expect( + disputeKitShutter.connect(attacker).castVoteShutter( + disputeId, + voteIDsJuror2, + 1n, // Wrong choice + salt, + justification + ) + ).to.be.revertedWithCustomError(disputeKitShutter, "HashDoesNotMatchHiddenVoteCommitment"); + }); + }); +}); From ef3a64c112eb327965bb995890a9e39863959697 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Thu, 4 Sep 2025 02:00:05 +0100 Subject: [PATCH 089/175] chore: github contract testing workflow without coverage due to via-ir issues --- .github/workflows/contracts-testing.yml | 184 +++++++++++++++--------- 1 file changed, 119 insertions(+), 65 deletions(-) diff --git a/.github/workflows/contracts-testing.yml b/.github/workflows/contracts-testing.yml index 47646906c..13783db5e 100644 --- a/.github/workflows/contracts-testing.yml +++ b/.github/workflows/contracts-testing.yml @@ -15,73 +15,127 @@ on: pull_request: branches: - "*" - -permissions: # added using https://github.com/step-security/secure-workflows + +permissions: # added using https://github.com/step-security/secure-workflows contents: read jobs: - contracts-testing: + # *********************************************************************************** # + # ******************************* Hardhat Tests ************************************* # + # *********************************************************************************** # + hardhat-tests: + runs-on: ubuntu-latest + steps: + - name: Harden Runner + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + with: + disable-sudo: false + egress-policy: block + allowed-endpoints: > + binaries.soliditylang.org:443 + classic.yarnpkg.com:443 + github.com:443 + nightly.yarnpkg.com:443 + nodejs.org:443 + objects.githubusercontent.com:443 + registry.yarnpkg.com:443 + registry.npmjs.org:443 + 54.185.253.63:443 + + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + submodules: recursive + + - name: Set up corepack (for yarn) + run: | + corepack enable + corepack prepare yarn@4.9.2 --activate + yarn set version 4.9.2 + + - name: Setup Node.js environment + uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 + with: + node-version: 20.x + cache: yarn + + - name: Cache node modules + uses: actions/cache@0c907a75c2c80ebcb7f088228285e798b750cf8f # v4.2.1 + env: + cache-name: cache-node-modules + with: + path: | + ~/.npm + **/node_modules + key: ${{ runner.os }}-build-${{ secrets.CACHE_VERSION }}-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-build-${{ secrets.CACHE_VERSION }}-${{ env.cache-name }}- + + - name: Install contracts dependencies + run: yarn workspace @kleros/kleros-v2-contracts install + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@de808b1eea699e761c404bda44ba8f21aba30b2c # v1.3.1 + + - name: Run Hardhat tests + run: yarn test + working-directory: contracts + + # *********************************************************************************** # + # ******************************* Foundry Tests ************************************* # + # *********************************************************************************** # + foundry-tests: runs-on: ubuntu-latest steps: - - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 - with: - disable-sudo: false - egress-policy: block - allowed-endpoints: > - binaries.soliditylang.org:443 - classic.yarnpkg.com:443 - github.com:443 - nightly.yarnpkg.com:443 - nodejs.org:443 - objects.githubusercontent.com:443 - registry.yarnpkg.com:443 - registry.npmjs.org:443 - 54.185.253.63:443 - - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - submodules: recursive - - - name: Set up corepack (for yarn) - run: | - corepack enable - corepack prepare yarn@4.9.2 --activate - yarn set version 4.9.2 - - - name: Setup Node.js environment - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 - with: - node-version: 20.x - cache: yarn - - - name: Cache node modules - uses: actions/cache@0c907a75c2c80ebcb7f088228285e798b750cf8f # v4.2.1 - env: - cache-name: cache-node-modules - with: - path: | - ~/.npm - **/node_modules - key: ${{ runner.os }}-build-${{ secrets.CACHE_VERSION }}-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-build-${{ secrets.CACHE_VERSION }}-${{ env.cache-name }}- - - - name: Install contracts dependencies - run: yarn workspace @kleros/kleros-v2-contracts install - - - name: Install Foundry - uses: foundry-rs/foundry-toolchain@de808b1eea699e761c404bda44ba8f21aba30b2c # v1.3.1 - - - name: Install lcov - run: sudo apt-get install -y lcov - - - name: Run Hardhat and Foundry tests with coverage - run: yarn coverage - working-directory: contracts - - - name: Upload a build artifact - uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 - with: - name: code-coverage-report - path: contracts/coverage + - name: Harden Runner + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + with: + disable-sudo: false + egress-policy: block + allowed-endpoints: > + binaries.soliditylang.org:443 + classic.yarnpkg.com:443 + github.com:443 + nightly.yarnpkg.com:443 + nodejs.org:443 + objects.githubusercontent.com:443 + registry.yarnpkg.com:443 + registry.npmjs.org:443 + 54.185.253.63:443 + + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + submodules: recursive + + - name: Set up corepack (for yarn) + run: | + corepack enable + corepack prepare yarn@4.9.2 --activate + yarn set version 4.9.2 + + - name: Setup Node.js environment + uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 + with: + node-version: 20.x + cache: yarn + + - name: Cache node modules + uses: actions/cache@0c907a75c2c80ebcb7f088228285e798b750cf8f # v4.2.1 + env: + cache-name: cache-node-modules + with: + path: | + ~/.npm + **/node_modules + key: ${{ runner.os }}-build-${{ secrets.CACHE_VERSION }}-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-build-${{ secrets.CACHE_VERSION }}-${{ env.cache-name }}- + + - name: Install contracts dependencies + run: yarn workspace @kleros/kleros-v2-contracts install + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@82dee4ba654bd2146511f85f0d013af94670c4de # v1.4.0 + + - name: Run Foundry tests + run: forge test + working-directory: contracts From fb6ca6ab348e6e93a26b070c01245234232252b7 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Thu, 4 Sep 2025 02:09:19 +0100 Subject: [PATCH 090/175] chore: testing --- .github/workflows/contracts-testing.yml | 120 ++++++++++++------------ 1 file changed, 61 insertions(+), 59 deletions(-) diff --git a/.github/workflows/contracts-testing.yml b/.github/workflows/contracts-testing.yml index 13783db5e..26e1fb313 100644 --- a/.github/workflows/contracts-testing.yml +++ b/.github/workflows/contracts-testing.yml @@ -73,9 +73,6 @@ jobs: - name: Install contracts dependencies run: yarn workspace @kleros/kleros-v2-contracts install - - name: Install Foundry - uses: foundry-rs/foundry-toolchain@de808b1eea699e761c404bda44ba8f21aba30b2c # v1.3.1 - - name: Run Hardhat tests run: yarn test working-directory: contracts @@ -83,59 +80,64 @@ jobs: # *********************************************************************************** # # ******************************* Foundry Tests ************************************* # # *********************************************************************************** # - foundry-tests: - runs-on: ubuntu-latest - steps: - - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 - with: - disable-sudo: false - egress-policy: block - allowed-endpoints: > - binaries.soliditylang.org:443 - classic.yarnpkg.com:443 - github.com:443 - nightly.yarnpkg.com:443 - nodejs.org:443 - objects.githubusercontent.com:443 - registry.yarnpkg.com:443 - registry.npmjs.org:443 - 54.185.253.63:443 - - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - submodules: recursive - - - name: Set up corepack (for yarn) - run: | - corepack enable - corepack prepare yarn@4.9.2 --activate - yarn set version 4.9.2 - - - name: Setup Node.js environment - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 - with: - node-version: 20.x - cache: yarn - - - name: Cache node modules - uses: actions/cache@0c907a75c2c80ebcb7f088228285e798b750cf8f # v4.2.1 - env: - cache-name: cache-node-modules - with: - path: | - ~/.npm - **/node_modules - key: ${{ runner.os }}-build-${{ secrets.CACHE_VERSION }}-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-build-${{ secrets.CACHE_VERSION }}-${{ env.cache-name }}- - - - name: Install contracts dependencies - run: yarn workspace @kleros/kleros-v2-contracts install - - - name: Install Foundry - uses: foundry-rs/foundry-toolchain@82dee4ba654bd2146511f85f0d013af94670c4de # v1.4.0 - - - name: Run Foundry tests - run: forge test - working-directory: contracts + # COMPILATION FAILS 🤬 + # foundry-tests: + # runs-on: ubuntu-latest + # steps: + # - name: Harden Runner + # uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + # with: + # disable-sudo: false + # egress-policy: block + # allowed-endpoints: > + # binaries.soliditylang.org:443 + # classic.yarnpkg.com:443 + # github.com:443 + # nightly.yarnpkg.com:443 + # nodejs.org:443 + # objects.githubusercontent.com:443 + # registry.yarnpkg.com:443 + # registry.npmjs.org:443 + # 54.185.253.63:443 + + # - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + # with: + # submodules: recursive + + # - name: Set up corepack (for yarn) + # run: | + # corepack enable + # corepack prepare yarn@4.9.2 --activate + # yarn set version 4.9.2 + + # - name: Setup Node.js environment + # uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 + # with: + # node-version: 20.x + # cache: yarn + + # - name: Cache node modules + # uses: actions/cache@0c907a75c2c80ebcb7f088228285e798b750cf8f # v4.2.1 + # env: + # cache-name: cache-node-modules + # with: + # path: | + # ~/.npm + # **/node_modules + # key: ${{ runner.os }}-build-${{ secrets.CACHE_VERSION }}-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }} + # restore-keys: | + # ${{ runner.os }}-build-${{ secrets.CACHE_VERSION }}-${{ env.cache-name }}- + + # # - name: Install contracts dependencies + # # run: yarn workspace @kleros/kleros-v2-contracts install + + # - name: Install Foundry + # uses: foundry-rs/foundry-toolchain@82dee4ba654bd2146511f85f0d013af94670c4de # v1.4.0 + + # - name: Run Foundry tests + # run: forge test --config-path ./foundry.toml + # working-directory: contracts + + # - name: Run snapshot + # run: NO_COLOR=1 forge snapshot >> $GITHUB_STEP_SUMMARY + # working-directory: contracts From 98f7eacccecaf686ecfda844bcbe8aa5b6583182 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Thu, 4 Sep 2025 16:00:42 +0100 Subject: [PATCH 091/175] fix: guard against currentStake exceeding SortitionModuleNeo.maxStakePerJuror --- contracts/src/arbitration/SortitionModuleNeo.sol | 2 +- contracts/src/libraries/Constants.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/src/arbitration/SortitionModuleNeo.sol b/contracts/src/arbitration/SortitionModuleNeo.sol index 7c2a3b53b..4d28f97b5 100644 --- a/contracts/src/arbitration/SortitionModuleNeo.sol +++ b/contracts/src/arbitration/SortitionModuleNeo.sol @@ -85,7 +85,7 @@ contract SortitionModuleNeo is SortitionModuleBase { uint256 stakeChange = stakeIncrease ? _newStake - currentStake : currentStake - _newStake; Juror storage juror = jurors[_account]; if (stakeIncrease) { - if (juror.stakedPnk + stakeChange > maxStakePerJuror) { + if (juror.stakedPnk + stakeChange > maxStakePerJuror || currentStake + stakeChange > maxStakePerJuror) { return (0, 0, StakingResult.CannotStakeMoreThanMaxStakePerJuror); } if (totalStaked + stakeChange > maxTotalStaked) { diff --git a/contracts/src/libraries/Constants.sol b/contracts/src/libraries/Constants.sol index 10c42d8a9..1f0132668 100644 --- a/contracts/src/libraries/Constants.sol +++ b/contracts/src/libraries/Constants.sol @@ -9,7 +9,7 @@ uint96 constant FORKING_COURT = 0; // Index of the forking court. uint96 constant GENERAL_COURT = 1; // Index of the default (general) court. // Dispute Kits -uint256 constant NULL_DISPUTE_KIT = 0; // Null pattern to indicate a top-level DK which has no parent. DEPRECATED, as its main purpose was to accommodate forest structure which is not used now. +uint256 constant NULL_DISPUTE_KIT = 0; // Null pattern to indicate a top-level DK which has no parent. uint256 constant DISPUTE_KIT_CLASSIC = 1; // Index of the default DK. 0 index is skipped. // Sortition Module From 030eedf2380b59800249e015b1a4ee693d671095 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Thu, 4 Sep 2025 18:00:53 +0100 Subject: [PATCH 092/175] feat: reset of the proxies version and initializers ahead of the migration --- contracts/deploy/upgrade-all.ts | 8 ++++---- .../src/arbitration/DisputeTemplateRegistry.sol | 8 ++------ contracts/src/arbitration/KlerosCore.sol | 8 ++------ contracts/src/arbitration/KlerosCoreNeo.sol | 6 +----- contracts/src/arbitration/PolicyRegistry.sol | 8 ++------ contracts/src/arbitration/SortitionModule.sol | 8 ++------ contracts/src/arbitration/SortitionModuleNeo.sol | 6 +----- .../src/arbitration/devtools/KlerosCoreRuler.sol | 12 ++---------- .../arbitration/dispute-kits/DisputeKitClassic.sol | 8 ++------ .../src/arbitration/dispute-kits/DisputeKitGated.sol | 8 ++------ .../dispute-kits/DisputeKitGatedShutter.sol | 8 ++------ .../arbitration/dispute-kits/DisputeKitShutter.sol | 8 ++------ .../dispute-kits/DisputeKitSybilResistant.sol | 4 ++-- .../src/arbitration/evidence/EvidenceModule.sol | 8 ++------ .../arbitration/university/KlerosCoreUniversity.sol | 4 ++-- .../university/SortitionModuleUniversity.sol | 4 ++-- contracts/src/gateway/ForeignGateway.sol | 2 +- contracts/src/gateway/HomeGateway.sol | 2 +- contracts/src/proxy/Initializable.sol | 2 +- .../mock/by-inheritance/UpgradedByInheritance.sol | 4 ++-- .../src/proxy/mock/by-rewrite/UpgradedByRewrite.sol | 2 +- 21 files changed, 38 insertions(+), 90 deletions(-) diff --git a/contracts/deploy/upgrade-all.ts b/contracts/deploy/upgrade-all.ts index 74433c413..dfd0d882c 100644 --- a/contracts/deploy/upgrade-all.ts +++ b/contracts/deploy/upgrade-all.ts @@ -98,11 +98,11 @@ const deployUpgradeAll: DeployFunction = async (hre: HardhatRuntimeEnvironment) await upgrade(disputeKitShutter, "reinitialize", [wETH.address]); await upgrade(disputeKitGated, "reinitialize", [wETH.address]); await upgrade(disputeKitGatedShutter, "reinitialize", [wETH.address]); - await upgrade(disputeTemplateRegistry, "initialize2", []); - await upgrade(evidence, "initialize2", []); + await upgrade(disputeTemplateRegistry, "reinitialize", []); + await upgrade(evidence, "reinitialize", []); await upgrade(core, "reinitialize", [wETH.address]); - await upgrade(policyRegistry, "initialize2", []); - await upgrade(sortition, "initialize4", []); + await upgrade(policyRegistry, "reinitialize", []); + await upgrade(sortition, "reinitialize", []); }; deployUpgradeAll.tags = ["UpgradeAll"]; diff --git a/contracts/src/arbitration/DisputeTemplateRegistry.sol b/contracts/src/arbitration/DisputeTemplateRegistry.sol index b9354aa0a..8d3d68d58 100644 --- a/contracts/src/arbitration/DisputeTemplateRegistry.sol +++ b/contracts/src/arbitration/DisputeTemplateRegistry.sol @@ -8,7 +8,7 @@ import "./interfaces/IDisputeTemplateRegistry.sol"; /// @title Dispute Template Registry /// @dev A contract to maintain a registry of dispute templates. contract DisputeTemplateRegistry is IDisputeTemplateRegistry, UUPSProxiable, Initializable { - string public constant override version = "0.8.0"; + string public constant override version = "2.0.0"; // ************************************* // // * Storage * // @@ -40,14 +40,10 @@ contract DisputeTemplateRegistry is IDisputeTemplateRegistry, UUPSProxiable, Ini /// @dev Initializer /// @param _owner Owner of the contract. - function initialize(address _owner) external reinitializer(1) { + function initialize(address _owner) external initializer { owner = _owner; } - function initialize2() external reinitializer(2) { - // NOP - } - // ************************ // // * Governance * // // ************************ // diff --git a/contracts/src/arbitration/KlerosCore.sol b/contracts/src/arbitration/KlerosCore.sol index 912a57269..4dd30f61b 100644 --- a/contracts/src/arbitration/KlerosCore.sol +++ b/contracts/src/arbitration/KlerosCore.sol @@ -8,7 +8,7 @@ import {KlerosCoreBase, IDisputeKit, ISortitionModule, IERC20} from "./KlerosCor /// Core arbitrator contract for Kleros v2. /// Note that this contract trusts the PNK token, the dispute kit and the sortition module contracts. contract KlerosCore is KlerosCoreBase { - string public constant override version = "0.10.0"; + string public constant override version = "2.0.0"; // ************************************* // // * Constructor * // @@ -43,7 +43,7 @@ contract KlerosCore is KlerosCoreBase { bytes memory _sortitionExtraData, ISortitionModule _sortitionModuleAddress, address _wNative - ) external reinitializer(1) { + ) external initializer { __KlerosCoreBase_initialize( _owner, _guardian, @@ -59,10 +59,6 @@ contract KlerosCore is KlerosCoreBase { ); } - function reinitialize(address _wNative) external reinitializer(6) { - wNative = _wNative; - } - // ************************************* // // * Governance * // // ************************************* // diff --git a/contracts/src/arbitration/KlerosCoreNeo.sol b/contracts/src/arbitration/KlerosCoreNeo.sol index ebfd1bb72..8f0a413b3 100644 --- a/contracts/src/arbitration/KlerosCoreNeo.sol +++ b/contracts/src/arbitration/KlerosCoreNeo.sol @@ -9,7 +9,7 @@ import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; /// Core arbitrator contract for Kleros v2. /// Note that this contract trusts the PNK token, the dispute kit and the sortition module contracts. contract KlerosCoreNeo is KlerosCoreBase { - string public constant override version = "0.10.0"; + string public constant override version = "2.0.0"; // ************************************* // // * Storage * // @@ -70,10 +70,6 @@ contract KlerosCoreNeo is KlerosCoreBase { jurorNft = _jurorNft; } - function reinitialize(address _wNative) external reinitializer(6) { - wNative = _wNative; - } - // ************************************* // // * Governance * // // ************************************* // diff --git a/contracts/src/arbitration/PolicyRegistry.sol b/contracts/src/arbitration/PolicyRegistry.sol index acea8d01b..9c8d09f3e 100644 --- a/contracts/src/arbitration/PolicyRegistry.sol +++ b/contracts/src/arbitration/PolicyRegistry.sol @@ -7,7 +7,7 @@ import "../proxy/Initializable.sol"; /// @title PolicyRegistry /// @dev A contract to maintain a policy for each court. contract PolicyRegistry is UUPSProxiable, Initializable { - string public constant override version = "0.8.0"; + string public constant override version = "2.0.0"; // ************************************* // // * Events * // @@ -47,14 +47,10 @@ contract PolicyRegistry is UUPSProxiable, Initializable { /// @dev Constructs the `PolicyRegistry` contract. /// @param _owner The owner's address. - function initialize(address _owner) external reinitializer(1) { + function initialize(address _owner) external initializer { owner = _owner; } - function initialize2() external reinitializer(2) { - // NOP - } - // ************************************* // // * Governance * // // ************************************* // diff --git a/contracts/src/arbitration/SortitionModule.sol b/contracts/src/arbitration/SortitionModule.sol index ae89335b0..bd0f6e007 100644 --- a/contracts/src/arbitration/SortitionModule.sol +++ b/contracts/src/arbitration/SortitionModule.sol @@ -7,7 +7,7 @@ import {SortitionModuleBase, KlerosCore, IRNG} from "./SortitionModuleBase.sol"; /// @title SortitionModule /// @dev A factory of trees that keeps track of staked values for sortition. contract SortitionModule is SortitionModuleBase { - string public constant override version = "0.9.0"; + string public constant override version = "2.0.0"; // ************************************* // // * Constructor * // @@ -30,14 +30,10 @@ contract SortitionModule is SortitionModuleBase { uint256 _minStakingTime, uint256 _maxDrawingTime, IRNG _rng - ) external reinitializer(1) { + ) external initializer { __SortitionModuleBase_initialize(_owner, _core, _minStakingTime, _maxDrawingTime, _rng); } - function initialize4() external reinitializer(4) { - // NOP - } - // ************************************* // // * Governance * // // ************************************* // diff --git a/contracts/src/arbitration/SortitionModuleNeo.sol b/contracts/src/arbitration/SortitionModuleNeo.sol index 4d28f97b5..66dbba7e5 100644 --- a/contracts/src/arbitration/SortitionModuleNeo.sol +++ b/contracts/src/arbitration/SortitionModuleNeo.sol @@ -7,7 +7,7 @@ import {SortitionModuleBase, KlerosCore, IRNG, StakingResult} from "./SortitionM /// @title SortitionModuleNeo /// @dev A factory of trees that keeps track of staked values for sortition. contract SortitionModuleNeo is SortitionModuleBase { - string public constant override version = "0.9.0"; + string public constant override version = "2.0.0"; // ************************************* // // * Storage * // @@ -48,10 +48,6 @@ contract SortitionModuleNeo is SortitionModuleBase { maxTotalStaked = _maxTotalStaked; } - function initialize4() external reinitializer(4) { - // NOP - } - // ************************************* // // * Governance * // // ************************************* // diff --git a/contracts/src/arbitration/devtools/KlerosCoreRuler.sol b/contracts/src/arbitration/devtools/KlerosCoreRuler.sol index 9350ebd57..42a428299 100644 --- a/contracts/src/arbitration/devtools/KlerosCoreRuler.sol +++ b/contracts/src/arbitration/devtools/KlerosCoreRuler.sol @@ -13,7 +13,7 @@ import "../../libraries/Constants.sol"; contract KlerosCoreRuler is IArbitratorV2, UUPSProxiable, Initializable { using SafeERC20 for IERC20; - string public constant override version = "0.8.0"; + string public constant override version = "2.0.0"; // ************************************* // // * Enums / Structs * // @@ -175,11 +175,7 @@ contract KlerosCoreRuler is IArbitratorV2, UUPSProxiable, Initializable { /// @param _owner The owner's address. /// @param _pinakion The address of the token contract. /// @param _courtParameters Numeric parameters of General court (minStake, alpha, feeForJuror and jurorsForCourtJump respectively). - function initialize( - address _owner, - IERC20 _pinakion, - uint256[4] memory _courtParameters - ) external reinitializer(1) { + function initialize(address _owner, IERC20 _pinakion, uint256[4] memory _courtParameters) external initializer { owner = _owner; pinakion = _pinakion; @@ -210,10 +206,6 @@ contract KlerosCoreRuler is IArbitratorV2, UUPSProxiable, Initializable { ); } - function initialize2() external reinitializer(2) { - // NOP - } - // ************************************* // // * Governance * // // ************************************* // diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitClassic.sol b/contracts/src/arbitration/dispute-kits/DisputeKitClassic.sol index af89d2816..dd2edbab8 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitClassic.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitClassic.sol @@ -11,7 +11,7 @@ import {DisputeKitClassicBase, KlerosCore} from "./DisputeKitClassicBase.sol"; /// - an incentive system: equal split between coherent votes, /// - an appeal system: fund 2 choices only, vote on any choice. contract DisputeKitClassic is DisputeKitClassicBase { - string public constant override version = "0.13.0"; + string public constant override version = "2.0.0"; // ************************************* // // * Constructor * // @@ -32,14 +32,10 @@ contract DisputeKitClassic is DisputeKitClassicBase { KlerosCore _core, address _wNative, uint256 _jumpDisputeKitID - ) external reinitializer(1) { + ) external initializer { __DisputeKitClassicBase_initialize(_owner, _core, _wNative, _jumpDisputeKitID); } - function reinitialize(uint256 _jumpDisputeKitID) external reinitializer(10) { - jumpDisputeKitID = _jumpDisputeKitID; - } - // ************************ // // * Governance * // // ************************ // diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitGated.sol b/contracts/src/arbitration/dispute-kits/DisputeKitGated.sol index 9097a8fd3..8abfdecac 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitGated.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitGated.sol @@ -27,7 +27,7 @@ interface IBalanceHolderERC1155 { /// - an incentive system: equal split between coherent votes, /// - an appeal system: fund 2 choices only, vote on any choice. contract DisputeKitGated is DisputeKitClassicBase { - string public constant override version = "0.13.0"; + string public constant override version = "2.0.0"; // ************************************* // // * Constructor * // @@ -48,14 +48,10 @@ contract DisputeKitGated is DisputeKitClassicBase { KlerosCore _core, address _wNative, uint256 _jumpDisputeKitID - ) external reinitializer(1) { + ) external initializer { __DisputeKitClassicBase_initialize(_owner, _core, _wNative, _jumpDisputeKitID); } - function reinitialize(uint256 _jumpDisputeKitID) external reinitializer(10) { - jumpDisputeKitID = _jumpDisputeKitID; - } - // ************************ // // * Governance * // // ************************ // diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitGatedShutter.sol b/contracts/src/arbitration/dispute-kits/DisputeKitGatedShutter.sol index 67d9ddbea..573f81f11 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitGatedShutter.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitGatedShutter.sol @@ -28,7 +28,7 @@ interface IBalanceHolderERC1155 { /// - an incentive system: equal split between coherent votes, /// - an appeal system: fund 2 choices only, vote on any choice. contract DisputeKitGatedShutter is DisputeKitClassicBase { - string public constant override version = "0.13.0"; + string public constant override version = "2.0.0"; // ************************************* // // * Storage * // @@ -82,14 +82,10 @@ contract DisputeKitGatedShutter is DisputeKitClassicBase { KlerosCore _core, address _wNative, uint256 _jumpDisputeKitID - ) external reinitializer(1) { + ) external initializer { __DisputeKitClassicBase_initialize(_owner, _core, _wNative, _jumpDisputeKitID); } - function reinitialize(uint256 _jumpDisputeKitID) external reinitializer(10) { - jumpDisputeKitID = _jumpDisputeKitID; - } - // ************************ // // * Governance * // // ************************ // diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitShutter.sol b/contracts/src/arbitration/dispute-kits/DisputeKitShutter.sol index b8bef461f..bbcb28b24 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitShutter.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitShutter.sol @@ -12,7 +12,7 @@ import {DisputeKitClassicBase, KlerosCore} from "./DisputeKitClassicBase.sol"; /// - an incentive system: equal split between coherent votes, /// - an appeal system: fund 2 choices only, vote on any choice. contract DisputeKitShutter is DisputeKitClassicBase { - string public constant override version = "0.13.0"; + string public constant override version = "2.0.0"; // ************************************* // // * Storage * // @@ -66,14 +66,10 @@ contract DisputeKitShutter is DisputeKitClassicBase { KlerosCore _core, address _wNative, uint256 _jumpDisputeKitID - ) external reinitializer(1) { + ) external initializer { __DisputeKitClassicBase_initialize(_owner, _core, _wNative, _jumpDisputeKitID); } - function reinitialize(uint256 _jumpDisputeKitID) external reinitializer(10) { - jumpDisputeKitID = _jumpDisputeKitID; - } - // ************************ // // * Governance * // // ************************ // diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitSybilResistant.sol b/contracts/src/arbitration/dispute-kits/DisputeKitSybilResistant.sol index 66553f02a..10cc67c5c 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitSybilResistant.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitSybilResistant.sol @@ -18,7 +18,7 @@ interface IProofOfHumanity { /// - an incentive system: equal split between coherent votes, /// - an appeal system: fund 2 choices only, vote on any choice. contract DisputeKitSybilResistant is DisputeKitClassicBase { - string public constant override version = "0.13.0"; + string public constant override version = "2.0.0"; // ************************************* // // * Storage * // @@ -47,7 +47,7 @@ contract DisputeKitSybilResistant is DisputeKitClassicBase { IProofOfHumanity _poh, address _wNative, uint256 _jumpDisputeKitID - ) external reinitializer(1) { + ) external initializer { __DisputeKitClassicBase_initialize(_owner, _core, _wNative, _jumpDisputeKitID); poh = _poh; singleDrawPerJuror = true; diff --git a/contracts/src/arbitration/evidence/EvidenceModule.sol b/contracts/src/arbitration/evidence/EvidenceModule.sol index d1eb5f99c..8321e55ba 100644 --- a/contracts/src/arbitration/evidence/EvidenceModule.sol +++ b/contracts/src/arbitration/evidence/EvidenceModule.sol @@ -9,7 +9,7 @@ import "../../proxy/Initializable.sol"; /// @title Evidence Module contract EvidenceModule is IEvidence, Initializable, UUPSProxiable { - string public constant override version = "0.8.0"; + string public constant override version = "2.0.0"; // ************************************* // // * Storage * // @@ -37,14 +37,10 @@ contract EvidenceModule is IEvidence, Initializable, UUPSProxiable { /// @dev Initializer. /// @param _owner The owner's address. - function initialize(address _owner) external reinitializer(1) { + function initialize(address _owner) external initializer { owner = _owner; } - function initialize2() external reinitializer(2) { - // NOP - } - // ************************ // // * Governance * // // ************************ // diff --git a/contracts/src/arbitration/university/KlerosCoreUniversity.sol b/contracts/src/arbitration/university/KlerosCoreUniversity.sol index b2519fdd7..afe7fc4d4 100644 --- a/contracts/src/arbitration/university/KlerosCoreUniversity.sol +++ b/contracts/src/arbitration/university/KlerosCoreUniversity.sol @@ -15,7 +15,7 @@ import "../../libraries/Constants.sol"; contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { using SafeERC20 for IERC20; - string public constant override version = "0.8.0"; + string public constant override version = "2.0.0"; // ************************************* // // * Enums / Structs * // @@ -207,7 +207,7 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { uint256[4] memory _courtParameters, uint256[4] memory _timesPerPeriod, ISortitionModuleUniversity _sortitionModuleAddress - ) external reinitializer(1) { + ) external initializer { owner = _owner; instructor = _instructor; pinakion = _pinakion; diff --git a/contracts/src/arbitration/university/SortitionModuleUniversity.sol b/contracts/src/arbitration/university/SortitionModuleUniversity.sol index 63720eb18..66e815e4a 100644 --- a/contracts/src/arbitration/university/SortitionModuleUniversity.sol +++ b/contracts/src/arbitration/university/SortitionModuleUniversity.sol @@ -12,7 +12,7 @@ import "../../libraries/Constants.sol"; /// @title SortitionModuleUniversity /// @dev An adapted version of the SortitionModule contract for educational purposes. contract SortitionModuleUniversity is ISortitionModuleUniversity, UUPSProxiable, Initializable { - string public constant override version = "0.8.0"; + string public constant override version = "2.0.0"; // ************************************* // // * Enums / Structs * // @@ -87,7 +87,7 @@ contract SortitionModuleUniversity is ISortitionModuleUniversity, UUPSProxiable, /// @dev Initializer (constructor equivalent for upgradable contracts). /// @param _core The KlerosCore. - function initialize(address _owner, KlerosCoreUniversity _core) external reinitializer(1) { + function initialize(address _owner, KlerosCoreUniversity _core) external initializer { owner = _owner; core = _core; } diff --git a/contracts/src/gateway/ForeignGateway.sol b/contracts/src/gateway/ForeignGateway.sol index 712790d0d..366f3e6f0 100644 --- a/contracts/src/gateway/ForeignGateway.sol +++ b/contracts/src/gateway/ForeignGateway.sol @@ -81,7 +81,7 @@ contract ForeignGateway is IForeignGateway, UUPSProxiable, Initializable { address _veaOutbox, uint256 _homeChainID, address _homeGateway - ) external reinitializer(1) { + ) external initializer { owner = _owner; veaOutbox = _veaOutbox; homeChainID = _homeChainID; diff --git a/contracts/src/gateway/HomeGateway.sol b/contracts/src/gateway/HomeGateway.sol index d0062dc52..2a23913ef 100644 --- a/contracts/src/gateway/HomeGateway.sol +++ b/contracts/src/gateway/HomeGateway.sol @@ -72,7 +72,7 @@ contract HomeGateway is IHomeGateway, UUPSProxiable, Initializable { uint256 _foreignChainID, address _foreignGateway, IERC20 _feeToken - ) external reinitializer(1) { + ) external initializer { owner = _owner; arbitrator = _arbitrator; veaInbox = _veaInbox; diff --git a/contracts/src/proxy/Initializable.sol b/contracts/src/proxy/Initializable.sol index 47769a9f2..7bdf08eb4 100644 --- a/contracts/src/proxy/Initializable.sol +++ b/contracts/src/proxy/Initializable.sol @@ -93,7 +93,7 @@ abstract contract Initializable { * @dev A modifier that defines a protected initializer function that can be invoked at most once. In its scope, * `onlyInitializing` functions can be used to initialize parent contracts. * - * Similar to `reinitializer(1)`, except that functions marked with `initializer` can be nested in the context of a + * Similar to `initializer()`, except that functions marked with `initializer` can be nested in the context of a * constructor. * * Emits an {Initialized} event. diff --git a/contracts/src/proxy/mock/by-inheritance/UpgradedByInheritance.sol b/contracts/src/proxy/mock/by-inheritance/UpgradedByInheritance.sol index c5f796995..b443051a8 100644 --- a/contracts/src/proxy/mock/by-inheritance/UpgradedByInheritance.sol +++ b/contracts/src/proxy/mock/by-inheritance/UpgradedByInheritance.sol @@ -20,7 +20,7 @@ contract UpgradedByInheritanceV1 is UUPSProxiable, Initializable { _disableInitializers(); } - function initialize(address _owner) external virtual reinitializer(1) { + function initialize(address _owner) external virtual initializer { owner = _owner; counter = 1; } @@ -61,7 +61,7 @@ contract UpgradedByInheritanceV3Bad is UpgradedByInheritanceV2 { _disableInitializers(); } - function initializeV3() external reinitializer(1) { + function initializeV3() external initializer { // Wrong reinitializer version. } } diff --git a/contracts/src/proxy/mock/by-rewrite/UpgradedByRewrite.sol b/contracts/src/proxy/mock/by-rewrite/UpgradedByRewrite.sol index f5e5eae77..d8930796d 100644 --- a/contracts/src/proxy/mock/by-rewrite/UpgradedByRewrite.sol +++ b/contracts/src/proxy/mock/by-rewrite/UpgradedByRewrite.sol @@ -23,7 +23,7 @@ contract UpgradedByRewrite is UUPSProxiable, Initializable { _disableInitializers(); } - function initialize(address _owner) external virtual reinitializer(1) { + function initialize(address _owner) external virtual initializer { owner = _owner; counter = 1; } From 63744e9f1292f83f5488ed325fa078250c6cac81 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Thu, 4 Sep 2025 18:12:38 +0100 Subject: [PATCH 093/175] feat: added a arbitrableWhitelistEnabled flag to KlerosCoreNeo --- .../deploy/00-home-chain-arbitration-neo.ts | 2 + contracts/src/arbitration/KlerosCoreNeo.sol | 8 +++- contracts/test/arbitration/staking-neo.ts | 45 +++++++++++++------ 3 files changed, 41 insertions(+), 14 deletions(-) diff --git a/contracts/deploy/00-home-chain-arbitration-neo.ts b/contracts/deploy/00-home-chain-arbitration-neo.ts index 36a895c74..d5d8a136c 100644 --- a/contracts/deploy/00-home-chain-arbitration-neo.ts +++ b/contracts/deploy/00-home-chain-arbitration-neo.ts @@ -119,6 +119,8 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) args: [core.target, disputeTemplateRegistry.target], log: true, }); + console.log(`core.changeArbitrableWhitelistEnabled(true)`); + await core.changeArbitrableWhitelistEnabled(true); console.log(`core.changeArbitrableWhitelist(${resolver.address}, true)`); await core.changeArbitrableWhitelist(resolver.address, true); diff --git a/contracts/src/arbitration/KlerosCoreNeo.sol b/contracts/src/arbitration/KlerosCoreNeo.sol index 8f0a413b3..25cef5b25 100644 --- a/contracts/src/arbitration/KlerosCoreNeo.sol +++ b/contracts/src/arbitration/KlerosCoreNeo.sol @@ -16,6 +16,7 @@ contract KlerosCoreNeo is KlerosCoreBase { // ************************************* // mapping(address => bool) public arbitrableWhitelist; // Arbitrable whitelist. + bool public arbitrableWhitelistEnabled; // Whether the arbitrable whitelist is enabled. IERC721 public jurorNft; // Eligible jurors NFT. // ************************************* // @@ -93,6 +94,11 @@ contract KlerosCoreNeo is KlerosCoreBase { arbitrableWhitelist[_arbitrable] = _allowed; } + /// @dev Enables or disables the arbitrable whitelist. + function changeArbitrableWhitelistEnabled(bool _enabled) external onlyByOwner { + arbitrableWhitelistEnabled = _enabled; + } + // ************************************* // // * State Modifiers * // // ************************************* // @@ -117,7 +123,7 @@ contract KlerosCoreNeo is KlerosCoreBase { IERC20 _feeToken, uint256 _feeAmount ) internal override returns (uint256 disputeID) { - if (!arbitrableWhitelist[msg.sender]) revert ArbitrableNotWhitelisted(); + if (arbitrableWhitelistEnabled && !arbitrableWhitelist[msg.sender]) revert ArbitrableNotWhitelisted(); return super._createDispute(_numberOfChoices, _extraData, _feeToken, _feeAmount); } diff --git a/contracts/test/arbitration/staking-neo.ts b/contracts/test/arbitration/staking-neo.ts index 5537742a6..a21bd0c7b 100644 --- a/contracts/test/arbitration/staking-neo.ts +++ b/contracts/test/arbitration/staking-neo.ts @@ -128,31 +128,50 @@ describe("Staking", async () => { SHOULD BEHAVE LIKE A NEO ARBITRATOR ************************************************************************************************/ - describe("When arbitrable is not whitelisted", () => { + describe("When arbitrable whitelist is disabled", () => { before("Setup", async () => { await deployUnhappy(); - await core.changeArbitrableWhitelist(resolver.target, false); + await core.changeArbitrableWhitelistEnabled(false); }); - it("Should fail to create a dispute", async () => { + it("Should create a dispute", async () => { const arbitrationCost = ETH(0.5); - await expect( - resolver.createDisputeForTemplate(extraData, "", "", 2, { value: arbitrationCost }) - ).to.be.revertedWithCustomError(core, "ArbitrableNotWhitelisted"); + expect(await resolver.createDisputeForTemplate(extraData, "", "", 2, { value: arbitrationCost })) + .to.emit(core, "DisputeCreation") + .withArgs(0, resolver.target); }); }); - describe("When arbitrable is whitelisted", () => { + describe("When arbitrable whitelist is enabled", () => { before("Setup", async () => { await deployUnhappy(); - await core.changeArbitrableWhitelist(resolver.target, true); + await core.changeArbitrableWhitelistEnabled(true); }); - it("Should create a dispute", async () => { - const arbitrationCost = ETH(0.5); - expect(await resolver.createDisputeForTemplate(extraData, "", "", 2, { value: arbitrationCost })) - .to.emit(core, "DisputeCreation") - .withArgs(0, resolver.target); + describe("When arbitrable is not whitelisted", () => { + before("Setup", async () => { + await core.changeArbitrableWhitelist(resolver.target, false); + }); + + it("Should fail to create a dispute", async () => { + const arbitrationCost = ETH(0.5); + await expect( + resolver.createDisputeForTemplate(extraData, "", "", 2, { value: arbitrationCost }) + ).to.be.revertedWithCustomError(core, "ArbitrableNotWhitelisted"); + }); + }); + + describe("When arbitrable is whitelisted", () => { + before("Setup", async () => { + await core.changeArbitrableWhitelist(resolver.target, true); + }); + + it("Should create a dispute", async () => { + const arbitrationCost = ETH(0.5); + expect(await resolver.createDisputeForTemplate(extraData, "", "", 2, { value: arbitrationCost })) + .to.emit(core, "DisputeCreation") + .withArgs(0, resolver.target); + }); }); }); From 262f940fcc918205f0b41aa60394c155456ec8c4 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Thu, 4 Sep 2025 18:20:39 +0100 Subject: [PATCH 094/175] feat: allow jurorNft to be address(0) which disables gating --- contracts/src/arbitration/KlerosCoreNeo.sol | 2 +- contracts/test/arbitration/staking-neo.ts | 53 ++++++++++++++------- 2 files changed, 37 insertions(+), 18 deletions(-) diff --git a/contracts/src/arbitration/KlerosCoreNeo.sol b/contracts/src/arbitration/KlerosCoreNeo.sol index 25cef5b25..6f43132d1 100644 --- a/contracts/src/arbitration/KlerosCoreNeo.sol +++ b/contracts/src/arbitration/KlerosCoreNeo.sol @@ -109,7 +109,7 @@ contract KlerosCoreNeo is KlerosCoreBase { /// @param _newStake The new stake. /// Note that the existing delayed stake will be nullified as non-relevant. function setStake(uint96 _courtID, uint256 _newStake) external override whenNotPaused { - if (jurorNft.balanceOf(msg.sender) == 0) revert NotEligibleForStaking(); + if (address(jurorNft) != address(0) && jurorNft.balanceOf(msg.sender) == 0) revert NotEligibleForStaking(); super._setStake(msg.sender, _courtID, _newStake, false, OnError.Revert); } diff --git a/contracts/test/arbitration/staking-neo.ts b/contracts/test/arbitration/staking-neo.ts index a21bd0c7b..fb36787d8 100644 --- a/contracts/test/arbitration/staking-neo.ts +++ b/contracts/test/arbitration/staking-neo.ts @@ -175,32 +175,51 @@ describe("Staking", async () => { }); }); - describe("When juror has no NFT", async () => { + describe("When juror NFT is not set", async () => { before("Setup", async () => { await deployUnhappy(); + await core.changeJurorNft(ethers.ZeroAddress); }); - it("Should not be able to stake", async () => { - await pnk.connect(juror).approve(core.target, PNK(1000)); - await expect(core.connect(juror).setStake(1, PNK(1000))).to.be.revertedWithCustomError( - core, - "NotEligibleForStaking" - ); + describe("When juror has no NFT", async () => { + it("Should be able to stake", async () => { + await pnk.connect(juror).approve(core.target, PNK(1000)); + await expect(await core.connect(juror).setStake(1, PNK(1000))) + .to.emit(sortition, "StakeSet") + .withArgs(juror.address, 1, PNK(1000), PNK(1000)); + expect(await sortition.totalStaked()).to.be.equal(PNK(1000)); + }); }); }); - describe("When juror does have a NFT", async () => { - before("Setup", async () => { - await deployUnhappy(); - await nft.safeMint(juror.address); + describe("When juror NFT is set", async () => { + describe("When juror has no NFT", async () => { + before("Setup", async () => { + await deployUnhappy(); + }); + + it("Should not be able to stake", async () => { + await pnk.connect(juror).approve(core.target, PNK(1000)); + await expect(core.connect(juror).setStake(1, PNK(1000))).to.be.revertedWithCustomError( + core, + "NotEligibleForStaking" + ); + }); }); - it("Should be able to stake", async () => { - await pnk.connect(juror).approve(core.target, PNK(1000)); - await expect(await core.connect(juror).setStake(1, PNK(1000))) - .to.emit(sortition, "StakeSet") - .withArgs(juror.address, 1, PNK(1000), PNK(1000)); - expect(await sortition.totalStaked()).to.be.equal(PNK(1000)); + describe("When juror does have a NFT", async () => { + before("Setup", async () => { + await deployUnhappy(); + await nft.safeMint(juror.address); + }); + + it("Should be able to stake", async () => { + await pnk.connect(juror).approve(core.target, PNK(1000)); + await expect(await core.connect(juror).setStake(1, PNK(1000))) + .to.emit(sortition, "StakeSet") + .withArgs(juror.address, 1, PNK(1000), PNK(1000)); + expect(await sortition.totalStaked()).to.be.equal(PNK(1000)); + }); }); }); From bd210be7bd26adfa33232606ce582b699e61fc9b Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Thu, 4 Sep 2025 18:39:43 +0100 Subject: [PATCH 095/175] feat: only KlerosCore, no more KlerosCoreBase and KlerosCoreNeo --- contracts/src/arbitration/KlerosCore.sol | 1282 ++++++++++++++++- contracts/src/arbitration/KlerosCoreBase.sol | 1275 ---------------- contracts/src/arbitration/KlerosCoreNeo.sol | 144 -- .../dispute-kits/DisputeKitClassicBase.sol | 8 +- contracts/src/proxy/KlerosProxies.sol | 12 - .../test/foundry/KlerosCore_Appeals.t.sol | 56 +- .../test/foundry/KlerosCore_Disputes.t.sol | 22 +- .../test/foundry/KlerosCore_Drawing.t.sol | 14 +- .../test/foundry/KlerosCore_Execution.t.sol | 52 +- .../test/foundry/KlerosCore_Governance.t.sol | 78 +- .../foundry/KlerosCore_Initialization.t.sol | 11 +- .../test/foundry/KlerosCore_Staking.t.sol | 20 +- .../test/foundry/KlerosCore_TestBase.sol | 7 +- .../test/foundry/KlerosCore_Voting.t.sol | 36 +- 14 files changed, 1422 insertions(+), 1595 deletions(-) delete mode 100644 contracts/src/arbitration/KlerosCoreBase.sol delete mode 100644 contracts/src/arbitration/KlerosCoreNeo.sol diff --git a/contracts/src/arbitration/KlerosCore.sol b/contracts/src/arbitration/KlerosCore.sol index 4dd30f61b..f7d6cf493 100644 --- a/contracts/src/arbitration/KlerosCore.sol +++ b/contracts/src/arbitration/KlerosCore.sol @@ -2,14 +2,198 @@ pragma solidity ^0.8.24; -import {KlerosCoreBase, IDisputeKit, ISortitionModule, IERC20} from "./KlerosCoreBase.sol"; +import {IArbitrableV2, IArbitratorV2} from "./interfaces/IArbitratorV2.sol"; +import {IDisputeKit} from "./interfaces/IDisputeKit.sol"; +import {ISortitionModule} from "./interfaces/ISortitionModule.sol"; +import {Initializable} from "../proxy/Initializable.sol"; +import {UUPSProxiable} from "../proxy/UUPSProxiable.sol"; +import {SafeERC20, IERC20} from "../libraries/SafeERC20.sol"; +import {SafeSend} from "../libraries/SafeSend.sol"; +import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import "../libraries/Constants.sol"; /// @title KlerosCore /// Core arbitrator contract for Kleros v2. /// Note that this contract trusts the PNK token, the dispute kit and the sortition module contracts. -contract KlerosCore is KlerosCoreBase { +contract KlerosCore is IArbitratorV2, Initializable, UUPSProxiable { + using SafeERC20 for IERC20; + using SafeSend for address payable; + string public constant override version = "2.0.0"; + // ************************************* // + // * Enums / Structs * // + // ************************************* // + + enum Period { + evidence, // Evidence can be submitted. This is also when drawing has to take place. + commit, // Jurors commit a hashed vote. This is skipped for courts without hidden votes. + vote, // Jurors reveal/cast their vote depending on whether the court has hidden votes or not. + appeal, // The dispute can be appealed. + execution // Tokens are redistributed and the ruling is executed. + } + + struct Court { + uint96 parent; // The parent court. + bool hiddenVotes; // Whether to use commit and reveal or not. + uint256[] children; // List of child courts. + uint256 minStake; // Minimum PNKs needed to stake in the court. + uint256 alpha; // Basis point of PNKs that are lost when incoherent. + uint256 feeForJuror; // Arbitration fee paid per juror. + uint256 jurorsForCourtJump; // The appeal after the one that reaches this number of jurors will go to the parent court if any. + uint256[4] timesPerPeriod; // The time allotted to each dispute period in the form `timesPerPeriod[period]`. + mapping(uint256 disputeKitId => bool) supportedDisputeKits; // True if DK with this ID is supported by the court. Note that each court must support classic dispute kit. + bool disabled; // True if the court is disabled. Unused for now, will be implemented later. + } + + struct Dispute { + uint96 courtID; // The ID of the court the dispute is in. + IArbitrableV2 arbitrated; // The arbitrable contract. + Period period; // The current period of the dispute. + bool ruled; // True if the ruling has been executed, false otherwise. + uint256 lastPeriodChange; // The last time the period was changed. + Round[] rounds; + } + + struct Round { + uint256 disputeKitID; // Index of the dispute kit in the array. + uint256 pnkAtStakePerJuror; // The amount of PNKs at stake for each juror in this round. + uint256 totalFeesForJurors; // The total juror fees paid in this round. + uint256 nbVotes; // The total number of votes the dispute can possibly have in the current round. Former votes[_round].length. + uint256 repartitions; // A counter of reward repartitions made in this round. + uint256 pnkPenalties; // The amount of PNKs collected from penalties in this round. + address[] drawnJurors; // Addresses of the jurors that were drawn in this round. + uint96[] drawnJurorFromCourtIDs; // The courtIDs where the juror was drawn from, possibly their stake in a subcourt. + uint256 sumFeeRewardPaid; // Total sum of arbitration fees paid to coherent jurors as a reward in this round. + uint256 sumPnkRewardPaid; // Total sum of PNK paid to coherent jurors as a reward in this round. + IERC20 feeToken; // The token used for paying fees in this round. + uint256 drawIterations; // The number of iterations passed drawing the jurors for this round. + } + + // Workaround "stack too deep" errors + struct ExecuteParams { + uint256 disputeID; // The ID of the dispute to execute. + uint256 round; // The round to execute. + uint256 coherentCount; // The number of coherent votes in the round. + uint256 numberOfVotesInRound; // The number of votes in the round. + uint256 feePerJurorInRound; // The fee per juror in the round. + uint256 pnkAtStakePerJurorInRound; // The amount of PNKs at stake for each juror in the round. + uint256 pnkPenaltiesInRound; // The amount of PNKs collected from penalties in the round. + uint256 repartition; // The index of the repartition to execute. + } + + struct CurrencyRate { + bool feePaymentAccepted; + uint64 rateInEth; + uint8 rateDecimals; + } + + // ************************************* // + // * Storage * // + // ************************************* // + + uint256 private constant NON_PAYABLE_AMOUNT = (2 ** 256 - 2) / 2; // An amount higher than the supply of ETH. + + address public owner; // The owner of the contract. + address public guardian; // The guardian able to pause asset withdrawals. + IERC20 public pinakion; // The Pinakion token contract. + address public jurorProsecutionModule; // The module for juror's prosecution. + ISortitionModule public sortitionModule; // Sortition module for drawing. + Court[] public courts; // The courts. + IDisputeKit[] public disputeKits; // Array of dispute kits. + Dispute[] public disputes; // The disputes. + mapping(IERC20 => CurrencyRate) public currencyRates; // The price of each token in ETH. + bool public paused; // Whether asset withdrawals are paused. + address public wNative; // The wrapped native token for safeSend(). + mapping(address => bool) public arbitrableWhitelist; // Arbitrable whitelist. + bool public arbitrableWhitelistEnabled; // Whether the arbitrable whitelist is enabled. + IERC721 public jurorNft; // Eligible jurors NFT. + + // ************************************* // + // * Events * // + // ************************************* // + + event NewPeriod(uint256 indexed _disputeID, Period _period); + event AppealPossible(uint256 indexed _disputeID, IArbitrableV2 indexed _arbitrable); + event AppealDecision(uint256 indexed _disputeID, IArbitrableV2 indexed _arbitrable); + event Draw(address indexed _address, uint256 indexed _disputeID, uint256 _roundID, uint256 _voteID); + event CourtCreated( + uint96 indexed _courtID, + uint96 indexed _parent, + bool _hiddenVotes, + uint256 _minStake, + uint256 _alpha, + uint256 _feeForJuror, + uint256 _jurorsForCourtJump, + uint256[4] _timesPerPeriod, + uint256[] _supportedDisputeKits + ); + event CourtModified( + uint96 indexed _courtID, + bool _hiddenVotes, + uint256 _minStake, + uint256 _alpha, + uint256 _feeForJuror, + uint256 _jurorsForCourtJump, + uint256[4] _timesPerPeriod + ); + event DisputeKitCreated(uint256 indexed _disputeKitID, IDisputeKit indexed _disputeKitAddress); + event DisputeKitEnabled(uint96 indexed _courtID, uint256 indexed _disputeKitID, bool indexed _enable); + event CourtJump( + uint256 indexed _disputeID, + uint256 indexed _roundID, + uint96 indexed _fromCourtID, + uint96 _toCourtID + ); + event DisputeKitJump( + uint256 indexed _disputeID, + uint256 indexed _roundID, + uint256 indexed _fromDisputeKitID, + uint256 _toDisputeKitID + ); + event TokenAndETHShift( + address indexed _account, + uint256 indexed _disputeID, + uint256 indexed _roundID, + uint256 _degreeOfCoherency, + int256 _pnkAmount, + int256 _feeAmount, + IERC20 _feeToken + ); + event LeftoverRewardSent( + uint256 indexed _disputeID, + uint256 indexed _roundID, + uint256 _pnkAmount, + uint256 _feeAmount, + IERC20 _feeToken + ); + event Paused(); + event Unpaused(); + + // ************************************* // + // * Function Modifiers * // + // ************************************* // + + modifier onlyByOwner() { + if (owner != msg.sender) revert OwnerOnly(); + _; + } + + modifier onlyByGuardianOrOwner() { + if (guardian != msg.sender && owner != msg.sender) revert GuardianOrOwnerOnly(); + _; + } + + modifier whenPaused() { + if (!paused) revert WhenPausedOnly(); + _; + } + + modifier whenNotPaused() { + if (paused) revert WhenNotPausedOnly(); + _; + } + // ************************************* // // * Constructor * // // ************************************* // @@ -31,6 +215,7 @@ contract KlerosCore is KlerosCoreBase { /// @param _sortitionExtraData The extra data for sortition module. /// @param _sortitionModuleAddress The sortition module responsible for sortition of the jurors. /// @param _wNative The wrapped native token address, typically wETH. + /// @param _jurorNft NFT contract to vet the jurors. function initialize( address _owner, address _guardian, @@ -42,21 +227,57 @@ contract KlerosCore is KlerosCoreBase { uint256[4] memory _timesPerPeriod, bytes memory _sortitionExtraData, ISortitionModule _sortitionModuleAddress, - address _wNative + address _wNative, + IERC721 _jurorNft ) external initializer { - __KlerosCoreBase_initialize( - _owner, - _guardian, - _pinakion, - _jurorProsecutionModule, - _disputeKit, + owner = _owner; + guardian = _guardian; + pinakion = _pinakion; + jurorProsecutionModule = _jurorProsecutionModule; + sortitionModule = _sortitionModuleAddress; + wNative = _wNative; + jurorNft = _jurorNft; + + // NULL_DISPUTE_KIT: an empty element at index 0 to indicate when a dispute kit is not supported. + disputeKits.push(); + + // DISPUTE_KIT_CLASSIC + disputeKits.push(_disputeKit); + + emit DisputeKitCreated(DISPUTE_KIT_CLASSIC, _disputeKit); + + // FORKING_COURT + // TODO: Fill the properties for the Forking court, emit CourtCreated. + courts.push(); + sortitionModule.createTree(FORKING_COURT, _sortitionExtraData); + + // GENERAL_COURT + Court storage court = courts.push(); + court.parent = FORKING_COURT; + court.children = new uint256[](0); + court.hiddenVotes = _hiddenVotes; + court.minStake = _courtParameters[0]; + court.alpha = _courtParameters[1]; + court.feeForJuror = _courtParameters[2]; + court.jurorsForCourtJump = _courtParameters[3]; + court.timesPerPeriod = _timesPerPeriod; + + sortitionModule.createTree(GENERAL_COURT, _sortitionExtraData); + + uint256[] memory supportedDisputeKits = new uint256[](1); + supportedDisputeKits[0] = DISPUTE_KIT_CLASSIC; + emit CourtCreated( + GENERAL_COURT, + court.parent, _hiddenVotes, - _courtParameters, + _courtParameters[0], + _courtParameters[1], + _courtParameters[2], + _courtParameters[3], _timesPerPeriod, - _sortitionExtraData, - _sortitionModuleAddress, - _wNative + supportedDisputeKits ); + _enableDisputeKit(GENERAL_COURT, DISPUTE_KIT_CLASSIC, true); } // ************************************* // @@ -68,4 +289,1039 @@ contract KlerosCore is KlerosCoreBase { function _authorizeUpgrade(address) internal view override onlyByOwner { // NOP } + + /// @dev Pause staking and reward execution. Can only be done by guardian or owner. + function pause() external onlyByGuardianOrOwner whenNotPaused { + paused = true; + emit Paused(); + } + + /// @dev Unpause staking and reward execution. Can only be done by owner. + function unpause() external onlyByOwner whenPaused { + paused = false; + emit Unpaused(); + } + + /// @dev Allows the owner to call anything on behalf of the contract. + /// @param _destination The destination of the call. + /// @param _amount The value sent with the call. + /// @param _data The data sent with the call. + function executeOwnerProposal(address _destination, uint256 _amount, bytes memory _data) external onlyByOwner { + (bool success, ) = _destination.call{value: _amount}(_data); + if (!success) revert UnsuccessfulCall(); + } + + /// @dev Changes the `owner` storage variable. + /// @param _owner The new value for the `owner` storage variable. + function changeOwner(address payable _owner) external onlyByOwner { + owner = _owner; + } + + /// @dev Changes the `guardian` storage variable. + /// @param _guardian The new value for the `guardian` storage variable. + function changeGuardian(address _guardian) external onlyByOwner { + guardian = _guardian; + } + + /// @dev Changes the `pinakion` storage variable. + /// @param _pinakion The new value for the `pinakion` storage variable. + function changePinakion(IERC20 _pinakion) external onlyByOwner { + pinakion = _pinakion; + } + + /// @dev Changes the `jurorProsecutionModule` storage variable. + /// @param _jurorProsecutionModule The new value for the `jurorProsecutionModule` storage variable. + function changeJurorProsecutionModule(address _jurorProsecutionModule) external onlyByOwner { + jurorProsecutionModule = _jurorProsecutionModule; + } + + /// @dev Changes the `_sortitionModule` storage variable. + /// Note that the new module should be initialized for all courts. + /// @param _sortitionModule The new value for the `sortitionModule` storage variable. + function changeSortitionModule(ISortitionModule _sortitionModule) external onlyByOwner { + sortitionModule = _sortitionModule; + } + + /// @dev Add a new supported dispute kit module to the court. + /// @param _disputeKitAddress The address of the dispute kit contract. + function addNewDisputeKit(IDisputeKit _disputeKitAddress) external onlyByOwner { + uint256 disputeKitID = disputeKits.length; + disputeKits.push(_disputeKitAddress); + emit DisputeKitCreated(disputeKitID, _disputeKitAddress); + } + + /// @dev Creates a court under a specified parent court. + /// @param _parent The `parent` property value of the court. + /// @param _hiddenVotes The `hiddenVotes` property value of the court. + /// @param _minStake The `minStake` property value of the court. + /// @param _alpha The `alpha` property value of the court. + /// @param _feeForJuror The `feeForJuror` property value of the court. + /// @param _jurorsForCourtJump The `jurorsForCourtJump` property value of the court. + /// @param _timesPerPeriod The `timesPerPeriod` property value of the court. + /// @param _sortitionExtraData Extra data for sortition module. + /// @param _supportedDisputeKits Indexes of dispute kits that this court will support. + function createCourt( + uint96 _parent, + bool _hiddenVotes, + uint256 _minStake, + uint256 _alpha, + uint256 _feeForJuror, + uint256 _jurorsForCourtJump, + uint256[4] memory _timesPerPeriod, + bytes memory _sortitionExtraData, + uint256[] memory _supportedDisputeKits + ) external onlyByOwner { + if (courts[_parent].minStake > _minStake) revert MinStakeLowerThanParentCourt(); + if (_supportedDisputeKits.length == 0) revert UnsupportedDisputeKit(); + if (_parent == FORKING_COURT) revert InvalidForkingCourtAsParent(); + + uint96 courtID = uint96(courts.length); + Court storage court = courts.push(); + + for (uint256 i = 0; i < _supportedDisputeKits.length; i++) { + if (_supportedDisputeKits[i] == 0 || _supportedDisputeKits[i] >= disputeKits.length) { + revert WrongDisputeKitIndex(); + } + _enableDisputeKit(uint96(courtID), _supportedDisputeKits[i], true); + } + // Check that Classic DK support was added. + if (!court.supportedDisputeKits[DISPUTE_KIT_CLASSIC]) revert MustSupportDisputeKitClassic(); + + court.parent = _parent; + court.children = new uint256[](0); + court.hiddenVotes = _hiddenVotes; + court.minStake = _minStake; + court.alpha = _alpha; + court.feeForJuror = _feeForJuror; + court.jurorsForCourtJump = _jurorsForCourtJump; + court.timesPerPeriod = _timesPerPeriod; + + sortitionModule.createTree(courtID, _sortitionExtraData); + + // Update the parent. + courts[_parent].children.push(courtID); + emit CourtCreated( + uint96(courtID), + _parent, + _hiddenVotes, + _minStake, + _alpha, + _feeForJuror, + _jurorsForCourtJump, + _timesPerPeriod, + _supportedDisputeKits + ); + } + + function changeCourtParameters( + uint96 _courtID, + bool _hiddenVotes, + uint256 _minStake, + uint256 _alpha, + uint256 _feeForJuror, + uint256 _jurorsForCourtJump, + uint256[4] memory _timesPerPeriod + ) external onlyByOwner { + Court storage court = courts[_courtID]; + if (_courtID != GENERAL_COURT && courts[court.parent].minStake > _minStake) { + revert MinStakeLowerThanParentCourt(); + } + for (uint256 i = 0; i < court.children.length; i++) { + if (courts[court.children[i]].minStake < _minStake) { + revert MinStakeLowerThanParentCourt(); + } + } + court.minStake = _minStake; + court.hiddenVotes = _hiddenVotes; + court.alpha = _alpha; + court.feeForJuror = _feeForJuror; + court.jurorsForCourtJump = _jurorsForCourtJump; + court.timesPerPeriod = _timesPerPeriod; + emit CourtModified( + _courtID, + _hiddenVotes, + _minStake, + _alpha, + _feeForJuror, + _jurorsForCourtJump, + _timesPerPeriod + ); + } + + /// @dev Adds/removes court's support for specified dispute kits. + /// @param _courtID The ID of the court. + /// @param _disputeKitIDs The IDs of dispute kits which support should be added/removed. + /// @param _enable Whether add or remove the dispute kits from the court. + function enableDisputeKits(uint96 _courtID, uint256[] memory _disputeKitIDs, bool _enable) external onlyByOwner { + for (uint256 i = 0; i < _disputeKitIDs.length; i++) { + if (_enable) { + if (_disputeKitIDs[i] == 0 || _disputeKitIDs[i] >= disputeKits.length) { + revert WrongDisputeKitIndex(); + } + _enableDisputeKit(_courtID, _disputeKitIDs[i], true); + } else { + // Classic dispute kit must be supported by all courts. + if (_disputeKitIDs[i] == DISPUTE_KIT_CLASSIC) { + revert CannotDisableClassicDK(); + } + _enableDisputeKit(_courtID, _disputeKitIDs[i], false); + } + } + } + + /// @dev Changes the supported fee tokens. + /// @param _feeToken The fee token. + /// @param _accepted Whether the token is supported or not as a method of fee payment. + function changeAcceptedFeeTokens(IERC20 _feeToken, bool _accepted) external onlyByOwner { + currencyRates[_feeToken].feePaymentAccepted = _accepted; + emit AcceptedFeeToken(_feeToken, _accepted); + } + + /// @dev Changes the currency rate of a fee token. + /// @param _feeToken The fee token. + /// @param _rateInEth The new rate of the fee token in ETH. + /// @param _rateDecimals The new decimals of the fee token rate. + function changeCurrencyRates(IERC20 _feeToken, uint64 _rateInEth, uint8 _rateDecimals) external onlyByOwner { + currencyRates[_feeToken].rateInEth = _rateInEth; + currencyRates[_feeToken].rateDecimals = _rateDecimals; + emit NewCurrencyRate(_feeToken, _rateInEth, _rateDecimals); + } + + /// @dev Adds or removes an arbitrable from whitelist. + /// @param _arbitrable Arbitrable address. + /// @param _allowed Whether add or remove permission. + function changeArbitrableWhitelist(address _arbitrable, bool _allowed) external onlyByOwner { + arbitrableWhitelist[_arbitrable] = _allowed; + } + + /// @dev Enables or disables the arbitrable whitelist. + function changeArbitrableWhitelistEnabled(bool _enabled) external onlyByOwner { + arbitrableWhitelistEnabled = _enabled; + } + + // ************************************* // + // * State Modifiers * // + // ************************************* // + + /// @dev Sets the caller's stake in a court. + /// @param _courtID The ID of the court. + /// @param _newStake The new stake. + /// Note that the existing delayed stake will be nullified as non-relevant. + function setStake(uint96 _courtID, uint256 _newStake) external virtual whenNotPaused { + if (address(jurorNft) != address(0) && jurorNft.balanceOf(msg.sender) == 0) revert NotEligibleForStaking(); + _setStake(msg.sender, _courtID, _newStake, false, OnError.Revert); + } + + /// @dev Sets the stake of a specified account in a court without delaying stake changes, typically to apply a delayed stake or unstake inactive jurors. + /// @param _account The account whose stake is being set. + /// @param _courtID The ID of the court. + /// @param _newStake The new stake. + function setStakeBySortitionModule(address _account, uint96 _courtID, uint256 _newStake) external { + if (msg.sender != address(sortitionModule)) revert SortitionModuleOnly(); + _setStake(_account, _courtID, _newStake, true, OnError.Return); + } + + /// @dev Transfers PNK to the juror by SortitionModule. + /// @param _account The account of the juror whose PNK to transfer. + /// @param _amount The amount to transfer. + function transferBySortitionModule(address _account, uint256 _amount) external { + if (msg.sender != address(sortitionModule)) revert SortitionModuleOnly(); + // Note eligibility is checked in SortitionModule. + pinakion.safeTransfer(_account, _amount); + } + + /// @inheritdoc IArbitratorV2 + function createDispute( + uint256 _numberOfChoices, + bytes memory _extraData + ) external payable override returns (uint256 disputeID) { + if (msg.value < arbitrationCost(_extraData)) revert ArbitrationFeesNotEnough(); + + return _createDispute(_numberOfChoices, _extraData, NATIVE_CURRENCY, msg.value); + } + + /// @inheritdoc IArbitratorV2 + function createDispute( + uint256 _numberOfChoices, + bytes calldata _extraData, + IERC20 _feeToken, + uint256 _feeAmount + ) external override returns (uint256 disputeID) { + if (!currencyRates[_feeToken].feePaymentAccepted) revert TokenNotAccepted(); + if (_feeAmount < arbitrationCost(_extraData, _feeToken)) revert ArbitrationFeesNotEnough(); + + if (!_feeToken.safeTransferFrom(msg.sender, address(this), _feeAmount)) revert TransferFailed(); + return _createDispute(_numberOfChoices, _extraData, _feeToken, _feeAmount); + } + + function _createDispute( + uint256 _numberOfChoices, + bytes memory _extraData, + IERC20 _feeToken, + uint256 _feeAmount + ) internal virtual returns (uint256 disputeID) { + if (arbitrableWhitelistEnabled && !arbitrableWhitelist[msg.sender]) revert ArbitrableNotWhitelisted(); + (uint96 courtID, , uint256 disputeKitID) = _extraDataToCourtIDMinJurorsDisputeKit(_extraData); + if (!courts[courtID].supportedDisputeKits[disputeKitID]) revert DisputeKitNotSupportedByCourt(); + + disputeID = disputes.length; + Dispute storage dispute = disputes.push(); + dispute.courtID = courtID; + dispute.arbitrated = IArbitrableV2(msg.sender); + dispute.lastPeriodChange = block.timestamp; + + IDisputeKit disputeKit = disputeKits[disputeKitID]; + Court storage court = courts[courtID]; + Round storage round = dispute.rounds.push(); + + // Obtain the feeForJuror in the same currency as the _feeAmount + uint256 feeForJuror = (_feeToken == NATIVE_CURRENCY) + ? court.feeForJuror + : convertEthToTokenAmount(_feeToken, court.feeForJuror); + round.nbVotes = _feeAmount / feeForJuror; + round.disputeKitID = disputeKitID; + round.pnkAtStakePerJuror = _calculatePnkAtStake(court.minStake, court.alpha); + round.totalFeesForJurors = _feeAmount; + round.feeToken = IERC20(_feeToken); + + sortitionModule.createDisputeHook(disputeID, 0); // Default round ID. + + disputeKit.createDispute(disputeID, _numberOfChoices, _extraData, round.nbVotes); + emit DisputeCreation(disputeID, IArbitrableV2(msg.sender)); + } + + /// @dev Passes the period of a specified dispute. + /// @param _disputeID The ID of the dispute. + function passPeriod(uint256 _disputeID) external { + Dispute storage dispute = disputes[_disputeID]; + Court storage court = courts[dispute.courtID]; + + uint256 currentRound = dispute.rounds.length - 1; + Round storage round = dispute.rounds[currentRound]; + if (dispute.period == Period.evidence) { + if ( + currentRound == 0 && + block.timestamp - dispute.lastPeriodChange < court.timesPerPeriod[uint256(dispute.period)] + ) { + revert EvidenceNotPassedAndNotAppeal(); + } + if (round.drawnJurors.length != round.nbVotes) revert DisputeStillDrawing(); + dispute.period = court.hiddenVotes ? Period.commit : Period.vote; + } else if (dispute.period == Period.commit) { + // 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; + } else if (dispute.period == Period.vote) { + if ( + block.timestamp - dispute.lastPeriodChange < court.timesPerPeriod[uint256(dispute.period)] && + !disputeKits[round.disputeKitID].areVotesAllCast(_disputeID) + ) { + revert VotePeriodNotPassed(); + } + dispute.period = Period.appeal; + emit AppealPossible(_disputeID, dispute.arbitrated); + } else if (dispute.period == Period.appeal) { + if ( + block.timestamp - dispute.lastPeriodChange < court.timesPerPeriod[uint256(dispute.period)] && + !disputeKits[round.disputeKitID].isAppealFunded(_disputeID) + ) { + revert AppealPeriodNotPassed(); + } + dispute.period = Period.execution; + } else if (dispute.period == Period.execution) { + revert DisputePeriodIsFinal(); + } + + dispute.lastPeriodChange = block.timestamp; + emit NewPeriod(_disputeID, dispute.period); + } + + /// @dev Draws jurors for the dispute. Can be called in parts. + /// @param _disputeID The ID of the dispute. + /// @param _iterations The number of iterations to run. + /// @return nbDrawnJurors The total number of jurors drawn in the round. + function draw(uint256 _disputeID, uint256 _iterations) external returns (uint256 nbDrawnJurors) { + Dispute storage dispute = disputes[_disputeID]; + uint256 currentRound = dispute.rounds.length - 1; + Round storage round = dispute.rounds[currentRound]; + if (dispute.period != Period.evidence) revert NotEvidencePeriod(); + + IDisputeKit disputeKit = disputeKits[round.disputeKitID]; + + uint256 startIndex = round.drawIterations; // for gas: less storage reads + uint256 i; + while (i < _iterations && round.drawnJurors.length < round.nbVotes) { + (address drawnAddress, uint96 fromSubcourtID) = disputeKit.draw(_disputeID, startIndex + i++); + if (drawnAddress == address(0)) { + continue; + } + sortitionModule.lockStake(drawnAddress, round.pnkAtStakePerJuror); + emit Draw(drawnAddress, _disputeID, currentRound, round.drawnJurors.length); + round.drawnJurors.push(drawnAddress); + round.drawnJurorFromCourtIDs.push(fromSubcourtID != 0 ? fromSubcourtID : dispute.courtID); + if (round.drawnJurors.length == round.nbVotes) { + sortitionModule.postDrawHook(_disputeID, currentRound); + } + } + round.drawIterations += i; + return round.drawnJurors.length; + } + + /// @dev Appeals the ruling of a specified dispute. + /// Note: Access restricted to the Dispute Kit for this `disputeID`. + /// @param _disputeID The ID of the dispute. + /// @param _numberOfChoices Number of choices for the dispute. Can be required during court jump. + /// @param _extraData Extradata for the dispute. Can be required during court jump. + function appeal(uint256 _disputeID, uint256 _numberOfChoices, bytes memory _extraData) external payable { + if (msg.value < appealCost(_disputeID)) revert AppealFeesNotEnough(); + + Dispute storage dispute = disputes[_disputeID]; + if (dispute.period != Period.appeal) revert DisputeNotAppealable(); + + Round storage round = dispute.rounds[dispute.rounds.length - 1]; + if (msg.sender != address(disputeKits[round.disputeKitID])) revert DisputeKitOnly(); + + // Warning: the extra round must be created before calling disputeKit.createDispute() + Round storage extraRound = dispute.rounds.push(); + + (uint96 newCourtID, uint256 newDisputeKitID, bool courtJump, ) = _getCourtAndDisputeKitJumps( + dispute, + round, + courts[dispute.courtID], + _disputeID + ); + if (courtJump) { + emit CourtJump(_disputeID, dispute.rounds.length - 1, dispute.courtID, newCourtID); + } + + dispute.courtID = newCourtID; + dispute.period = Period.evidence; + dispute.lastPeriodChange = block.timestamp; + + Court storage court = courts[newCourtID]; + extraRound.nbVotes = msg.value / court.feeForJuror; // As many votes that can be afforded by the provided funds. + extraRound.pnkAtStakePerJuror = _calculatePnkAtStake(court.minStake, court.alpha); + extraRound.totalFeesForJurors = msg.value; + extraRound.disputeKitID = newDisputeKitID; + + sortitionModule.createDisputeHook(_disputeID, dispute.rounds.length - 1); + + // Dispute kit was changed, so create a dispute in the new DK contract. + if (extraRound.disputeKitID != round.disputeKitID) { + emit DisputeKitJump(_disputeID, dispute.rounds.length - 1, round.disputeKitID, extraRound.disputeKitID); + disputeKits[extraRound.disputeKitID].createDispute( + _disputeID, + _numberOfChoices, + _extraData, + extraRound.nbVotes + ); + } + + emit AppealDecision(_disputeID, dispute.arbitrated); + emit NewPeriod(_disputeID, Period.evidence); + } + + /// @dev Distribute the PNKs at stake and the dispute fees for the specific round of the dispute. Can be called in parts. + /// Note: Reward distributions are forbidden during pause. + /// @param _disputeID The ID of the dispute. + /// @param _round The appeal round. + /// @param _iterations The number of iterations to run. + function execute(uint256 _disputeID, uint256 _round, uint256 _iterations) external whenNotPaused { + Round storage round; + { + Dispute storage dispute = disputes[_disputeID]; + if (dispute.period != Period.execution) revert NotExecutionPeriod(); + + round = dispute.rounds[_round]; + } // stack too deep workaround + + uint256 start = round.repartitions; + uint256 end = round.repartitions + _iterations; + + uint256 pnkPenaltiesInRound = round.pnkPenalties; // Keep in memory to save gas. + uint256 numberOfVotesInRound = round.drawnJurors.length; + uint256 feePerJurorInRound = round.totalFeesForJurors / numberOfVotesInRound; + uint256 pnkAtStakePerJurorInRound = round.pnkAtStakePerJuror; + uint256 coherentCount; + { + IDisputeKit disputeKit = disputeKits[round.disputeKitID]; + coherentCount = disputeKit.getCoherentCount(_disputeID, _round); // Total number of jurors that are eligible to a reward in this round. + } // stack too deep workaround + + if (coherentCount == 0) { + // We loop over the votes once as there are no rewards because it is not a tie and no one in this round is coherent with the final outcome. + if (end > numberOfVotesInRound) end = numberOfVotesInRound; + } else { + // We loop over the votes twice, first to collect the PNK penalties, and second to distribute them as rewards along with arbitration fees. + if (end > numberOfVotesInRound * 2) end = numberOfVotesInRound * 2; + } + round.repartitions = end; + + for (uint256 i = start; i < end; i++) { + if (i < numberOfVotesInRound) { + pnkPenaltiesInRound = _executePenalties( + ExecuteParams({ + disputeID: _disputeID, + round: _round, + coherentCount: coherentCount, + numberOfVotesInRound: numberOfVotesInRound, + feePerJurorInRound: feePerJurorInRound, + pnkAtStakePerJurorInRound: pnkAtStakePerJurorInRound, + pnkPenaltiesInRound: pnkPenaltiesInRound, + repartition: i + }) + ); + } else { + _executeRewards( + ExecuteParams({ + disputeID: _disputeID, + round: _round, + coherentCount: coherentCount, + numberOfVotesInRound: numberOfVotesInRound, + feePerJurorInRound: feePerJurorInRound, + pnkAtStakePerJurorInRound: pnkAtStakePerJurorInRound, + pnkPenaltiesInRound: pnkPenaltiesInRound, + repartition: i + }) + ); + } + } + if (round.pnkPenalties != pnkPenaltiesInRound) { + round.pnkPenalties = pnkPenaltiesInRound; // Reentrancy risk: breaks Check-Effect-Interact + } + } + + /// @dev Distribute the PNKs at stake and the dispute fees for the specific round of the dispute, penalties only. + /// @param _params The parameters for the execution, see `ExecuteParams`. + /// @return pnkPenaltiesInRoundCache The updated penalties in round cache. + function _executePenalties(ExecuteParams memory _params) internal returns (uint256) { + Dispute storage dispute = disputes[_params.disputeID]; + Round storage round = dispute.rounds[_params.round]; + IDisputeKit disputeKit = disputeKits[round.disputeKitID]; + + // [0, 1] value that determines how coherent the juror was in this round, in basis points. + uint256 coherence = disputeKit.getDegreeOfCoherencePenalty( + _params.disputeID, + _params.round, + _params.repartition, + _params.feePerJurorInRound, + _params.pnkAtStakePerJurorInRound + ); + + // 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 * (ONE_BASIS_POINT - coherence)) / ONE_BASIS_POINT; + + // Unlock the PNKs affected by the penalty + address account = round.drawnJurors[_params.repartition]; + sortitionModule.unlockStake(account, penalty); + + // Apply the penalty to the staked PNKs. + uint96 penalizedInCourtID = round.drawnJurorFromCourtIDs[_params.repartition]; + (uint256 pnkBalance, uint256 newCourtStake, uint256 availablePenalty) = sortitionModule.setStakePenalty( + account, + penalizedInCourtID, + penalty + ); + _params.pnkPenaltiesInRound += availablePenalty; + emit TokenAndETHShift( + account, + _params.disputeID, + _params.round, + coherence, + -int256(availablePenalty), + 0, + round.feeToken + ); + + if (pnkBalance == 0 || !disputeKit.isVoteActive(_params.disputeID, _params.round, _params.repartition)) { + // The juror is inactive or their balance is can't cover penalties anymore, unstake them from all courts. + sortitionModule.forcedUnstakeAllCourts(account); + } else if (newCourtStake < courts[penalizedInCourtID].minStake) { + // The juror's balance fell below the court minStake, unstake them from the court. + sortitionModule.forcedUnstake(account, penalizedInCourtID); + } + + if (_params.repartition == _params.numberOfVotesInRound - 1 && _params.coherentCount == 0) { + // No one was coherent, send the rewards to the owner. + _transferFeeToken(round.feeToken, payable(owner), round.totalFeesForJurors); + pinakion.safeTransfer(owner, _params.pnkPenaltiesInRound); + emit LeftoverRewardSent( + _params.disputeID, + _params.round, + _params.pnkPenaltiesInRound, + round.totalFeesForJurors, + round.feeToken + ); + } + return _params.pnkPenaltiesInRound; + } + + /// @dev Distribute the PNKs at stake and the dispute fees for the specific round of the dispute, rewards only. + /// @param _params The parameters for the execution, see `ExecuteParams`. + function _executeRewards(ExecuteParams memory _params) internal { + Dispute storage dispute = disputes[_params.disputeID]; + Round storage round = dispute.rounds[_params.round]; + IDisputeKit disputeKit = disputeKits[round.disputeKitID]; + + // [0, 1] value that determines how coherent the juror was in this round, in basis points. + (uint256 pnkCoherence, uint256 feeCoherence) = disputeKit.getDegreeOfCoherenceReward( + _params.disputeID, + _params.round, + _params.repartition % _params.numberOfVotesInRound, + _params.feePerJurorInRound, + _params.pnkAtStakePerJurorInRound + ); + + // 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 = _applyCoherence(round.pnkAtStakePerJuror, pnkCoherence); + + // Release the rest of the PNKs of the juror for this round. + sortitionModule.unlockStake(account, pnkLocked); + + // Compute the rewards + uint256 pnkReward = _applyCoherence(_params.pnkPenaltiesInRound / _params.coherentCount, pnkCoherence); + round.sumPnkRewardPaid += pnkReward; + uint256 feeReward = _applyCoherence(round.totalFeesForJurors / _params.coherentCount, feeCoherence); + round.sumFeeRewardPaid += feeReward; + + // Transfer the fee reward + _transferFeeToken(round.feeToken, payable(account), feeReward); + + // Stake the PNK reward if possible, by-passes delayed stakes and other checks usually done by validateStake() + if (!sortitionModule.setStakeReward(account, dispute.courtID, pnkReward)) { + pinakion.safeTransfer(account, pnkReward); + } + + emit TokenAndETHShift( + account, + _params.disputeID, + _params.round, + pnkCoherence, + int256(pnkReward), + int256(feeReward), + round.feeToken + ); + + // Transfer any residual rewards to the owner. It may happen due to partial coherence of the jurors. + if (_params.repartition == _params.numberOfVotesInRound * 2 - 1) { + uint256 leftoverPnkReward = _params.pnkPenaltiesInRound - round.sumPnkRewardPaid; + uint256 leftoverFeeReward = round.totalFeesForJurors - round.sumFeeRewardPaid; + if (leftoverPnkReward != 0 || leftoverFeeReward != 0) { + if (leftoverPnkReward != 0) { + pinakion.safeTransfer(owner, leftoverPnkReward); + } + if (leftoverFeeReward != 0) { + _transferFeeToken(round.feeToken, payable(owner), leftoverFeeReward); + } + emit LeftoverRewardSent( + _params.disputeID, + _params.round, + leftoverPnkReward, + leftoverFeeReward, + round.feeToken + ); + } + } + } + + /// @dev Executes a specified dispute's ruling. + /// @param _disputeID The ID of the dispute. + function executeRuling(uint256 _disputeID) external { + Dispute storage dispute = disputes[_disputeID]; + if (dispute.period != Period.execution) revert NotExecutionPeriod(); + if (dispute.ruled) revert RulingAlreadyExecuted(); + + (uint256 winningChoice, , ) = currentRuling(_disputeID); + dispute.ruled = true; + emit Ruling(dispute.arbitrated, _disputeID, winningChoice); + dispute.arbitrated.rule(_disputeID, winningChoice); + } + + // ************************************* // + // * Public Views * // + // ************************************* // + + /// @dev Compute the cost of arbitration denominated in ETH. + /// It is recommended not to increase it often, as it can be highly time and gas consuming for the arbitrated contracts to cope with fee augmentation. + /// @param _extraData Additional info about the dispute. We use it to pass the ID of the dispute's court (first 32 bytes), the minimum number of jurors required (next 32 bytes) and the ID of the specific dispute kit (last 32 bytes). + /// @return cost The arbitration cost in ETH. + function arbitrationCost(bytes memory _extraData) public view override returns (uint256 cost) { + (uint96 courtID, uint256 minJurors, ) = _extraDataToCourtIDMinJurorsDisputeKit(_extraData); + cost = courts[courtID].feeForJuror * minJurors; + } + + /// @dev Compute the cost of arbitration denominated in `_feeToken`. + /// It is recommended not to increase it often, as it can be highly time and gas consuming for the arbitrated contracts to cope with fee augmentation. + /// @param _extraData Additional info about the dispute. We use it to pass the ID of the dispute's court (first 32 bytes), the minimum number of jurors required (next 32 bytes) and the ID of the specific dispute kit (last 32 bytes). + /// @param _feeToken The ERC20 token used to pay fees. + /// @return cost The arbitration cost in `_feeToken`. + function arbitrationCost(bytes calldata _extraData, IERC20 _feeToken) public view override returns (uint256 cost) { + cost = convertEthToTokenAmount(_feeToken, arbitrationCost(_extraData)); + } + + /// @dev Gets the cost of appealing a specified dispute. + /// @param _disputeID The ID of the dispute. + /// @return cost The appeal cost. + function appealCost(uint256 _disputeID) public view returns (uint256 cost) { + Dispute storage dispute = disputes[_disputeID]; + Round storage round = dispute.rounds[dispute.rounds.length - 1]; + Court storage court = courts[dispute.courtID]; + + (, uint256 newDisputeKitID, bool courtJump, ) = _getCourtAndDisputeKitJumps(dispute, round, court, _disputeID); + + uint256 nbVotesAfterAppeal = disputeKits[newDisputeKitID].getNbVotesAfterAppeal( + disputeKits[round.disputeKitID], + round.nbVotes + ); + + if (courtJump) { + // Jump to parent court. + if (dispute.courtID == GENERAL_COURT) { + // TODO: Handle the forking when appealed in General court. + cost = NON_PAYABLE_AMOUNT; // Get the cost of the parent court. + } else { + cost = courts[court.parent].feeForJuror * nbVotesAfterAppeal; + } + } else { + // Stay in current court. + cost = court.feeForJuror * nbVotesAfterAppeal; + } + } + + /// @dev Gets the start and the end of a specified dispute's current appeal period. + /// @param _disputeID The ID of the dispute. + /// @return start The start of the appeal period. + /// @return end The end of the appeal period. + function appealPeriod(uint256 _disputeID) external view returns (uint256 start, uint256 end) { + Dispute storage dispute = disputes[_disputeID]; + if (dispute.period == Period.appeal) { + start = dispute.lastPeriodChange; + end = dispute.lastPeriodChange + courts[dispute.courtID].timesPerPeriod[uint256(Period.appeal)]; + } else { + start = 0; + end = 0; + } + } + + /// @dev Gets the current ruling of a specified dispute. + /// @param _disputeID The ID of the dispute. + /// @return ruling The current ruling. + /// @return tied Whether it's a tie or not. + /// @return overridden Whether the ruling was overridden by appeal funding or not. + function currentRuling(uint256 _disputeID) public view returns (uint256 ruling, bool tied, bool overridden) { + Dispute storage dispute = disputes[_disputeID]; + Round storage round = dispute.rounds[dispute.rounds.length - 1]; + IDisputeKit disputeKit = disputeKits[round.disputeKitID]; + (ruling, tied, overridden) = disputeKit.currentRuling(_disputeID); + } + + /// @dev Gets the round info for a specified dispute and round. + /// @dev This function must not be called from a non-view function because it returns a dynamic array which might be very large, theoretically exceeding the block gas limit. + /// @param _disputeID The ID of the dispute. + /// @param _round The round to get the info for. + /// @return round The round info. + function getRoundInfo(uint256 _disputeID, uint256 _round) external view returns (Round memory) { + return disputes[_disputeID].rounds[_round]; + } + + /// @dev Gets the PNK at stake per juror for a specified dispute and round. + /// @param _disputeID The ID of the dispute. + /// @param _round The round to get the info for. + /// @return pnkAtStakePerJuror The PNK at stake per juror. + function getPnkAtStakePerJuror(uint256 _disputeID, uint256 _round) external view returns (uint256) { + return disputes[_disputeID].rounds[_round].pnkAtStakePerJuror; + } + + /// @dev Gets the number of rounds for a specified dispute. + /// @param _disputeID The ID of the dispute. + /// @return The number of rounds. + function getNumberOfRounds(uint256 _disputeID) external view returns (uint256) { + return disputes[_disputeID].rounds.length; + } + + /// @dev Checks if a given dispute kit is supported by a given court. + /// @param _courtID The ID of the court to check the support for. + /// @param _disputeKitID The ID of the dispute kit to check the support for. + /// @return Whether the dispute kit is supported or not. + function isSupported(uint96 _courtID, uint256 _disputeKitID) external view returns (bool) { + return courts[_courtID].supportedDisputeKits[_disputeKitID]; + } + + /// @dev Gets the timesPerPeriod array for a given court. + /// @param _courtID The ID of the court to get the times from. + /// @return timesPerPeriod The timesPerPeriod array for the given court. + function getTimesPerPeriod(uint96 _courtID) external view returns (uint256[4] memory timesPerPeriod) { + timesPerPeriod = courts[_courtID].timesPerPeriod; + } + + // ************************************* // + // * Public Views for Dispute Kits * // + // ************************************* // + + /// @dev Gets the number of votes permitted for the specified dispute in the latest round. + /// @param _disputeID The ID of the dispute. + function getNumberOfVotes(uint256 _disputeID) external view returns (uint256) { + Dispute storage dispute = disputes[_disputeID]; + return dispute.rounds[dispute.rounds.length - 1].nbVotes; + } + + /// @dev Returns true if the dispute kit will be switched to a parent DK. + /// @param _disputeID The ID of the dispute. + /// @return Whether DK will be switched or not. + function isDisputeKitJumping(uint256 _disputeID) external view returns (bool) { + Dispute storage dispute = disputes[_disputeID]; + Round storage round = dispute.rounds[dispute.rounds.length - 1]; + Court storage court = courts[dispute.courtID]; + + if (!_isCourtJumping(round, court, _disputeID)) { + return false; + } + + // Jump if the parent court doesn't support the current DK. + return !courts[court.parent].supportedDisputeKits[round.disputeKitID]; + } + + function getDisputeKitsLength() external view returns (uint256) { + return disputeKits.length; + } + + function convertEthToTokenAmount(IERC20 _toToken, uint256 _amountInEth) public view returns (uint256) { + return (_amountInEth * 10 ** currencyRates[_toToken].rateDecimals) / currencyRates[_toToken].rateInEth; + } + + // ************************************* // + // * Internal * // + // ************************************* // + + /// @dev Returns true if the round is jumping to a parent court. + /// @param _round The round to check. + /// @param _court The court to check. + /// @return Whether the round is jumping to a parent court or not. + function _isCourtJumping( + Round storage _round, + Court storage _court, + uint256 _disputeID + ) internal view returns (bool) { + return + disputeKits[_round.disputeKitID].earlyCourtJump(_disputeID) || _round.nbVotes >= _court.jurorsForCourtJump; + } + + function _getCourtAndDisputeKitJumps( + Dispute storage _dispute, + Round storage _round, + Court storage _court, + uint256 _disputeID + ) internal view returns (uint96 newCourtID, uint256 newDisputeKitID, bool courtJump, bool disputeKitJump) { + newCourtID = _dispute.courtID; + newDisputeKitID = _round.disputeKitID; + + if (!_isCourtJumping(_round, _court, _disputeID)) return (newCourtID, newDisputeKitID, false, false); + + // Jump to parent court. + newCourtID = courts[newCourtID].parent; + courtJump = true; + + if (!courts[newCourtID].supportedDisputeKits[newDisputeKitID]) { + // The current Dispute Kit is not compatible with the new court, jump to another Dispute Kit. + newDisputeKitID = disputeKits[_round.disputeKitID].getJumpDisputeKitID(); + if (newDisputeKitID == NULL_DISPUTE_KIT || !courts[newCourtID].supportedDisputeKits[newDisputeKitID]) { + // The new Dispute Kit is not defined or still not compatible, fall back to `DisputeKitClassic` which is always supported. + newDisputeKitID = DISPUTE_KIT_CLASSIC; + } + disputeKitJump = true; + } + } + + /// @dev Internal function to transfer fee tokens (ETH or ERC20) + /// @param _feeToken The token to transfer (NATIVE_CURRENCY for ETH). + /// @param _recipient The recipient address. + /// @param _amount The amount to transfer. + function _transferFeeToken(IERC20 _feeToken, address payable _recipient, uint256 _amount) internal { + if (_feeToken == NATIVE_CURRENCY) { + _recipient.safeSend(_amount, wNative); + } else { + _feeToken.safeTransfer(_recipient, _amount); + } + } + + /// @dev Applies degree of coherence to an amount + /// @param _amount The base amount to apply coherence to. + /// @param _coherence The degree of coherence in basis points. + /// @return The amount after applying the degree of coherence. + 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 + /// @param _minStake The minimum stake for the court. + /// @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) / ONE_BASIS_POINT; + } + + /// @dev Toggles the dispute kit support for a given court. + /// @param _courtID The ID of the court to toggle the support for. + /// @param _disputeKitID The ID of the dispute kit to toggle the support for. + /// @param _enable Whether to enable or disable the support. Note that classic dispute kit should always be enabled. + function _enableDisputeKit(uint96 _courtID, uint256 _disputeKitID, bool _enable) internal { + courts[_courtID].supportedDisputeKits[_disputeKitID] = _enable; + emit DisputeKitEnabled(_courtID, _disputeKitID, _enable); + } + + /// @dev If called only once then set _onError to Revert, otherwise set it to Return + /// @param _account The account to set the stake for. + /// @param _courtID The ID of the court to set the stake for. + /// @param _newStake The new stake. + /// @param _noDelay True if the stake change should not be delayed. + /// @param _onError Whether to revert or return false on error. + /// @return Whether the stake was successfully set or not. + function _setStake( + address _account, + uint96 _courtID, + uint256 _newStake, + bool _noDelay, + OnError _onError + ) internal returns (bool) { + if (_courtID == FORKING_COURT || _courtID >= courts.length) { + _stakingFailed(_onError, StakingResult.CannotStakeInThisCourt); // Staking directly into the forking court is not allowed. + return false; + } + if (_newStake != 0 && _newStake < courts[_courtID].minStake) { + _stakingFailed(_onError, StakingResult.CannotStakeLessThanMinStake); // Staking less than the minimum stake is not allowed. + return false; + } + (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) = sortitionModule.validateStake( + _account, + _courtID, + _newStake, + _noDelay + ); + if (stakingResult != StakingResult.Successful && stakingResult != StakingResult.Delayed) { + _stakingFailed(_onError, stakingResult); + return false; + } else if (stakingResult == StakingResult.Delayed) { + return true; + } + if (pnkDeposit > 0) { + if (!pinakion.safeTransferFrom(_account, address(this), pnkDeposit)) { + _stakingFailed(_onError, StakingResult.StakingTransferFailed); + return false; + } + } + if (pnkWithdrawal > 0) { + if (!pinakion.safeTransfer(_account, pnkWithdrawal)) { + _stakingFailed(_onError, StakingResult.UnstakingTransferFailed); + return false; + } + } + sortitionModule.setStake(_account, _courtID, pnkDeposit, pnkWithdrawal, _newStake); + + return true; + } + + /// @dev It may revert depending on the _onError parameter. + function _stakingFailed(OnError _onError, StakingResult _result) internal pure virtual { + if (_onError == OnError.Return) return; + if (_result == StakingResult.StakingTransferFailed) revert StakingTransferFailed(); + if (_result == StakingResult.UnstakingTransferFailed) revert UnstakingTransferFailed(); + if (_result == StakingResult.CannotStakeInMoreCourts) revert StakingInTooManyCourts(); + if (_result == StakingResult.CannotStakeInThisCourt) revert StakingNotPossibleInThisCourt(); + if (_result == StakingResult.CannotStakeLessThanMinStake) revert StakingLessThanCourtMinStake(); + if (_result == StakingResult.CannotStakeZeroWhenNoStake) revert StakingZeroWhenNoStake(); + if (_result == StakingResult.CannotStakeMoreThanMaxStakePerJuror) revert StakingMoreThanMaxStakePerJuror(); + if (_result == StakingResult.CannotStakeMoreThanMaxTotalStaked) revert StakingMoreThanMaxTotalStaked(); + } + + /// @dev Gets a court ID, the minimum number of jurors and an ID of a dispute kit from a specified extra data bytes array. + /// Note that if extradata contains an incorrect value then this value will be switched to default. + /// @param _extraData The extra data bytes array. The first 32 bytes are the court ID, the next are the minimum number of jurors and the last are the dispute kit ID. + /// @return courtID The court ID. + /// @return minJurors The minimum number of jurors required. + /// @return disputeKitID The ID of the dispute kit. + function _extraDataToCourtIDMinJurorsDisputeKit( + bytes memory _extraData + ) internal view returns (uint96 courtID, uint256 minJurors, uint256 disputeKitID) { + // Note that if the extradata doesn't contain 32 bytes for the dispute kit ID it'll return the default 0 index. + if (_extraData.length >= 64) { + assembly { + // solium-disable-line security/no-inline-assembly + courtID := mload(add(_extraData, 0x20)) + minJurors := mload(add(_extraData, 0x40)) + disputeKitID := mload(add(_extraData, 0x60)) + } + if (courtID == FORKING_COURT || courtID >= courts.length) { + courtID = GENERAL_COURT; + } + if (minJurors == 0) { + minJurors = DEFAULT_NB_OF_JURORS; + } + if (disputeKitID == NULL_DISPUTE_KIT || disputeKitID >= disputeKits.length) { + disputeKitID = DISPUTE_KIT_CLASSIC; // 0 index is not used. + } + } else { + courtID = GENERAL_COURT; + minJurors = DEFAULT_NB_OF_JURORS; + disputeKitID = DISPUTE_KIT_CLASSIC; + } + } + + // ************************************* // + // * Errors * // + // ************************************* // + + error OwnerOnly(); + error GuardianOrOwnerOnly(); + error DisputeKitOnly(); + error SortitionModuleOnly(); + error UnsuccessfulCall(); + error InvalidDisputKitParent(); + error MinStakeLowerThanParentCourt(); + error UnsupportedDisputeKit(); + error InvalidForkingCourtAsParent(); + error WrongDisputeKitIndex(); + error CannotDisableClassicDK(); + error NotEligibleForStaking(); + error StakingMoreThanMaxStakePerJuror(); + error StakingMoreThanMaxTotalStaked(); + error StakingInTooManyCourts(); + error StakingNotPossibleInThisCourt(); + error StakingLessThanCourtMinStake(); + error StakingTransferFailed(); + error UnstakingTransferFailed(); + error ArbitrableNotWhitelisted(); + error ArbitrationFeesNotEnough(); + error DisputeKitNotSupportedByCourt(); + error MustSupportDisputeKitClassic(); + error TokenNotAccepted(); + error EvidenceNotPassedAndNotAppeal(); + error DisputeStillDrawing(); + error CommitPeriodNotPassed(); + error VotePeriodNotPassed(); + error AppealPeriodNotPassed(); + error NotEvidencePeriod(); + error AppealFeesNotEnough(); + error DisputeNotAppealable(); + error NotExecutionPeriod(); + error RulingAlreadyExecuted(); + error DisputePeriodIsFinal(); + error TransferFailed(); + error WhenNotPausedOnly(); + error WhenPausedOnly(); + error StakingZeroWhenNoStake(); } diff --git a/contracts/src/arbitration/KlerosCoreBase.sol b/contracts/src/arbitration/KlerosCoreBase.sol deleted file mode 100644 index fc8095715..000000000 --- a/contracts/src/arbitration/KlerosCoreBase.sol +++ /dev/null @@ -1,1275 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.24; - -import {IArbitrableV2, IArbitratorV2} from "./interfaces/IArbitratorV2.sol"; -import {IDisputeKit} from "./interfaces/IDisputeKit.sol"; -import {ISortitionModule} from "./interfaces/ISortitionModule.sol"; -import {Initializable} from "../proxy/Initializable.sol"; -import {UUPSProxiable} from "../proxy/UUPSProxiable.sol"; -import {SafeERC20, IERC20} from "../libraries/SafeERC20.sol"; -import {SafeSend} from "../libraries/SafeSend.sol"; -import "../libraries/Constants.sol"; - -/// @title KlerosCoreBase -/// Core arbitrator contract for Kleros v2. -/// Note that this contract trusts the PNK token, the dispute kit and the sortition module contracts. -abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable { - using SafeERC20 for IERC20; - using SafeSend for address payable; - - // ************************************* // - // * Enums / Structs * // - // ************************************* // - - enum Period { - evidence, // Evidence can be submitted. This is also when drawing has to take place. - commit, // Jurors commit a hashed vote. This is skipped for courts without hidden votes. - vote, // Jurors reveal/cast their vote depending on whether the court has hidden votes or not. - appeal, // The dispute can be appealed. - execution // Tokens are redistributed and the ruling is executed. - } - - struct Court { - uint96 parent; // The parent court. - bool hiddenVotes; // Whether to use commit and reveal or not. - uint256[] children; // List of child courts. - uint256 minStake; // Minimum PNKs needed to stake in the court. - uint256 alpha; // Basis point of PNKs that are lost when incoherent. - uint256 feeForJuror; // Arbitration fee paid per juror. - uint256 jurorsForCourtJump; // The appeal after the one that reaches this number of jurors will go to the parent court if any. - uint256[4] timesPerPeriod; // The time allotted to each dispute period in the form `timesPerPeriod[period]`. - mapping(uint256 disputeKitId => bool) supportedDisputeKits; // True if DK with this ID is supported by the court. Note that each court must support classic dispute kit. - bool disabled; // True if the court is disabled. Unused for now, will be implemented later. - } - - struct Dispute { - uint96 courtID; // The ID of the court the dispute is in. - IArbitrableV2 arbitrated; // The arbitrable contract. - Period period; // The current period of the dispute. - bool ruled; // True if the ruling has been executed, false otherwise. - uint256 lastPeriodChange; // The last time the period was changed. - Round[] rounds; - } - - struct Round { - uint256 disputeKitID; // Index of the dispute kit in the array. - uint256 pnkAtStakePerJuror; // The amount of PNKs at stake for each juror in this round. - uint256 totalFeesForJurors; // The total juror fees paid in this round. - uint256 nbVotes; // The total number of votes the dispute can possibly have in the current round. Former votes[_round].length. - uint256 repartitions; // A counter of reward repartitions made in this round. - uint256 pnkPenalties; // The amount of PNKs collected from penalties in this round. - address[] drawnJurors; // Addresses of the jurors that were drawn in this round. - uint96[] drawnJurorFromCourtIDs; // The courtIDs where the juror was drawn from, possibly their stake in a subcourt. - uint256 sumFeeRewardPaid; // Total sum of arbitration fees paid to coherent jurors as a reward in this round. - uint256 sumPnkRewardPaid; // Total sum of PNK paid to coherent jurors as a reward in this round. - IERC20 feeToken; // The token used for paying fees in this round. - uint256 drawIterations; // The number of iterations passed drawing the jurors for this round. - } - - // Workaround "stack too deep" errors - struct ExecuteParams { - uint256 disputeID; // The ID of the dispute to execute. - uint256 round; // The round to execute. - uint256 coherentCount; // The number of coherent votes in the round. - uint256 numberOfVotesInRound; // The number of votes in the round. - uint256 feePerJurorInRound; // The fee per juror in the round. - uint256 pnkAtStakePerJurorInRound; // The amount of PNKs at stake for each juror in the round. - uint256 pnkPenaltiesInRound; // The amount of PNKs collected from penalties in the round. - uint256 repartition; // The index of the repartition to execute. - } - - struct CurrencyRate { - bool feePaymentAccepted; - uint64 rateInEth; - uint8 rateDecimals; - } - - // ************************************* // - // * Storage * // - // ************************************* // - - uint256 private constant NON_PAYABLE_AMOUNT = (2 ** 256 - 2) / 2; // An amount higher than the supply of ETH. - - address public owner; // The owner of the contract. - address public guardian; // The guardian able to pause asset withdrawals. - IERC20 public pinakion; // The Pinakion token contract. - address public jurorProsecutionModule; // The module for juror's prosecution. - ISortitionModule public sortitionModule; // Sortition module for drawing. - Court[] public courts; // The courts. - IDisputeKit[] public disputeKits; // Array of dispute kits. - Dispute[] public disputes; // The disputes. - mapping(IERC20 => CurrencyRate) public currencyRates; // The price of each token in ETH. - bool public paused; // Whether asset withdrawals are paused. - address public wNative; // The wrapped native token for safeSend(). - - // ************************************* // - // * Events * // - // ************************************* // - - event NewPeriod(uint256 indexed _disputeID, Period _period); - event AppealPossible(uint256 indexed _disputeID, IArbitrableV2 indexed _arbitrable); - event AppealDecision(uint256 indexed _disputeID, IArbitrableV2 indexed _arbitrable); - event Draw(address indexed _address, uint256 indexed _disputeID, uint256 _roundID, uint256 _voteID); - event CourtCreated( - uint96 indexed _courtID, - uint96 indexed _parent, - bool _hiddenVotes, - uint256 _minStake, - uint256 _alpha, - uint256 _feeForJuror, - uint256 _jurorsForCourtJump, - uint256[4] _timesPerPeriod, - uint256[] _supportedDisputeKits - ); - event CourtModified( - uint96 indexed _courtID, - bool _hiddenVotes, - uint256 _minStake, - uint256 _alpha, - uint256 _feeForJuror, - uint256 _jurorsForCourtJump, - uint256[4] _timesPerPeriod - ); - event DisputeKitCreated(uint256 indexed _disputeKitID, IDisputeKit indexed _disputeKitAddress); - event DisputeKitEnabled(uint96 indexed _courtID, uint256 indexed _disputeKitID, bool indexed _enable); - event CourtJump( - uint256 indexed _disputeID, - uint256 indexed _roundID, - uint96 indexed _fromCourtID, - uint96 _toCourtID - ); - event DisputeKitJump( - uint256 indexed _disputeID, - uint256 indexed _roundID, - uint256 indexed _fromDisputeKitID, - uint256 _toDisputeKitID - ); - event TokenAndETHShift( - address indexed _account, - uint256 indexed _disputeID, - uint256 indexed _roundID, - uint256 _degreeOfCoherency, - int256 _pnkAmount, - int256 _feeAmount, - IERC20 _feeToken - ); - event LeftoverRewardSent( - uint256 indexed _disputeID, - uint256 indexed _roundID, - uint256 _pnkAmount, - uint256 _feeAmount, - IERC20 _feeToken - ); - event Paused(); - event Unpaused(); - - // ************************************* // - // * Function Modifiers * // - // ************************************* // - - modifier onlyByOwner() { - if (owner != msg.sender) revert OwnerOnly(); - _; - } - - modifier onlyByGuardianOrOwner() { - if (guardian != msg.sender && owner != msg.sender) revert GuardianOrOwnerOnly(); - _; - } - - modifier whenPaused() { - if (!paused) revert WhenPausedOnly(); - _; - } - - modifier whenNotPaused() { - if (paused) revert WhenNotPausedOnly(); - _; - } - - // ************************************* // - // * Constructor * // - // ************************************* // - - function __KlerosCoreBase_initialize( - address _owner, - address _guardian, - IERC20 _pinakion, - address _jurorProsecutionModule, - IDisputeKit _disputeKit, - bool _hiddenVotes, - uint256[4] memory _courtParameters, - uint256[4] memory _timesPerPeriod, - bytes memory _sortitionExtraData, - ISortitionModule _sortitionModuleAddress, - address _wNative - ) internal onlyInitializing { - owner = _owner; - guardian = _guardian; - pinakion = _pinakion; - jurorProsecutionModule = _jurorProsecutionModule; - sortitionModule = _sortitionModuleAddress; - wNative = _wNative; - - // NULL_DISPUTE_KIT: an empty element at index 0 to indicate when a dispute kit is not supported. - disputeKits.push(); - - // DISPUTE_KIT_CLASSIC - disputeKits.push(_disputeKit); - - emit DisputeKitCreated(DISPUTE_KIT_CLASSIC, _disputeKit); - - // FORKING_COURT - // TODO: Fill the properties for the Forking court, emit CourtCreated. - courts.push(); - sortitionModule.createTree(FORKING_COURT, _sortitionExtraData); - - // GENERAL_COURT - Court storage court = courts.push(); - court.parent = FORKING_COURT; - court.children = new uint256[](0); - court.hiddenVotes = _hiddenVotes; - court.minStake = _courtParameters[0]; - court.alpha = _courtParameters[1]; - court.feeForJuror = _courtParameters[2]; - court.jurorsForCourtJump = _courtParameters[3]; - court.timesPerPeriod = _timesPerPeriod; - - sortitionModule.createTree(GENERAL_COURT, _sortitionExtraData); - - uint256[] memory supportedDisputeKits = new uint256[](1); - supportedDisputeKits[0] = DISPUTE_KIT_CLASSIC; - emit CourtCreated( - GENERAL_COURT, - court.parent, - _hiddenVotes, - _courtParameters[0], - _courtParameters[1], - _courtParameters[2], - _courtParameters[3], - _timesPerPeriod, - supportedDisputeKits - ); - _enableDisputeKit(GENERAL_COURT, DISPUTE_KIT_CLASSIC, true); - } - - // ************************************* // - // * Governance * // - // ************************************* // - - /// @dev Pause staking and reward execution. Can only be done by guardian or owner. - function pause() external onlyByGuardianOrOwner whenNotPaused { - paused = true; - emit Paused(); - } - - /// @dev Unpause staking and reward execution. Can only be done by owner. - function unpause() external onlyByOwner whenPaused { - paused = false; - emit Unpaused(); - } - - /// @dev Allows the owner to call anything on behalf of the contract. - /// @param _destination The destination of the call. - /// @param _amount The value sent with the call. - /// @param _data The data sent with the call. - function executeOwnerProposal(address _destination, uint256 _amount, bytes memory _data) external onlyByOwner { - (bool success, ) = _destination.call{value: _amount}(_data); - if (!success) revert UnsuccessfulCall(); - } - - /// @dev Changes the `owner` storage variable. - /// @param _owner The new value for the `owner` storage variable. - function changeOwner(address payable _owner) external onlyByOwner { - owner = _owner; - } - - /// @dev Changes the `guardian` storage variable. - /// @param _guardian The new value for the `guardian` storage variable. - function changeGuardian(address _guardian) external onlyByOwner { - guardian = _guardian; - } - - /// @dev Changes the `pinakion` storage variable. - /// @param _pinakion The new value for the `pinakion` storage variable. - function changePinakion(IERC20 _pinakion) external onlyByOwner { - pinakion = _pinakion; - } - - /// @dev Changes the `jurorProsecutionModule` storage variable. - /// @param _jurorProsecutionModule The new value for the `jurorProsecutionModule` storage variable. - function changeJurorProsecutionModule(address _jurorProsecutionModule) external onlyByOwner { - jurorProsecutionModule = _jurorProsecutionModule; - } - - /// @dev Changes the `_sortitionModule` storage variable. - /// Note that the new module should be initialized for all courts. - /// @param _sortitionModule The new value for the `sortitionModule` storage variable. - function changeSortitionModule(ISortitionModule _sortitionModule) external onlyByOwner { - sortitionModule = _sortitionModule; - } - - /// @dev Add a new supported dispute kit module to the court. - /// @param _disputeKitAddress The address of the dispute kit contract. - function addNewDisputeKit(IDisputeKit _disputeKitAddress) external onlyByOwner { - uint256 disputeKitID = disputeKits.length; - disputeKits.push(_disputeKitAddress); - emit DisputeKitCreated(disputeKitID, _disputeKitAddress); - } - - /// @dev Creates a court under a specified parent court. - /// @param _parent The `parent` property value of the court. - /// @param _hiddenVotes The `hiddenVotes` property value of the court. - /// @param _minStake The `minStake` property value of the court. - /// @param _alpha The `alpha` property value of the court. - /// @param _feeForJuror The `feeForJuror` property value of the court. - /// @param _jurorsForCourtJump The `jurorsForCourtJump` property value of the court. - /// @param _timesPerPeriod The `timesPerPeriod` property value of the court. - /// @param _sortitionExtraData Extra data for sortition module. - /// @param _supportedDisputeKits Indexes of dispute kits that this court will support. - function createCourt( - uint96 _parent, - bool _hiddenVotes, - uint256 _minStake, - uint256 _alpha, - uint256 _feeForJuror, - uint256 _jurorsForCourtJump, - uint256[4] memory _timesPerPeriod, - bytes memory _sortitionExtraData, - uint256[] memory _supportedDisputeKits - ) external onlyByOwner { - if (courts[_parent].minStake > _minStake) revert MinStakeLowerThanParentCourt(); - if (_supportedDisputeKits.length == 0) revert UnsupportedDisputeKit(); - if (_parent == FORKING_COURT) revert InvalidForkingCourtAsParent(); - - uint96 courtID = uint96(courts.length); - Court storage court = courts.push(); - - for (uint256 i = 0; i < _supportedDisputeKits.length; i++) { - if (_supportedDisputeKits[i] == 0 || _supportedDisputeKits[i] >= disputeKits.length) { - revert WrongDisputeKitIndex(); - } - _enableDisputeKit(uint96(courtID), _supportedDisputeKits[i], true); - } - // Check that Classic DK support was added. - if (!court.supportedDisputeKits[DISPUTE_KIT_CLASSIC]) revert MustSupportDisputeKitClassic(); - - court.parent = _parent; - court.children = new uint256[](0); - court.hiddenVotes = _hiddenVotes; - court.minStake = _minStake; - court.alpha = _alpha; - court.feeForJuror = _feeForJuror; - court.jurorsForCourtJump = _jurorsForCourtJump; - court.timesPerPeriod = _timesPerPeriod; - - sortitionModule.createTree(courtID, _sortitionExtraData); - - // Update the parent. - courts[_parent].children.push(courtID); - emit CourtCreated( - uint96(courtID), - _parent, - _hiddenVotes, - _minStake, - _alpha, - _feeForJuror, - _jurorsForCourtJump, - _timesPerPeriod, - _supportedDisputeKits - ); - } - - function changeCourtParameters( - uint96 _courtID, - bool _hiddenVotes, - uint256 _minStake, - uint256 _alpha, - uint256 _feeForJuror, - uint256 _jurorsForCourtJump, - uint256[4] memory _timesPerPeriod - ) external onlyByOwner { - Court storage court = courts[_courtID]; - if (_courtID != GENERAL_COURT && courts[court.parent].minStake > _minStake) { - revert MinStakeLowerThanParentCourt(); - } - for (uint256 i = 0; i < court.children.length; i++) { - if (courts[court.children[i]].minStake < _minStake) { - revert MinStakeLowerThanParentCourt(); - } - } - court.minStake = _minStake; - court.hiddenVotes = _hiddenVotes; - court.alpha = _alpha; - court.feeForJuror = _feeForJuror; - court.jurorsForCourtJump = _jurorsForCourtJump; - court.timesPerPeriod = _timesPerPeriod; - emit CourtModified( - _courtID, - _hiddenVotes, - _minStake, - _alpha, - _feeForJuror, - _jurorsForCourtJump, - _timesPerPeriod - ); - } - - /// @dev Adds/removes court's support for specified dispute kits. - /// @param _courtID The ID of the court. - /// @param _disputeKitIDs The IDs of dispute kits which support should be added/removed. - /// @param _enable Whether add or remove the dispute kits from the court. - function enableDisputeKits(uint96 _courtID, uint256[] memory _disputeKitIDs, bool _enable) external onlyByOwner { - for (uint256 i = 0; i < _disputeKitIDs.length; i++) { - if (_enable) { - if (_disputeKitIDs[i] == 0 || _disputeKitIDs[i] >= disputeKits.length) { - revert WrongDisputeKitIndex(); - } - _enableDisputeKit(_courtID, _disputeKitIDs[i], true); - } else { - // Classic dispute kit must be supported by all courts. - if (_disputeKitIDs[i] == DISPUTE_KIT_CLASSIC) { - revert CannotDisableClassicDK(); - } - _enableDisputeKit(_courtID, _disputeKitIDs[i], false); - } - } - } - - /// @dev Changes the supported fee tokens. - /// @param _feeToken The fee token. - /// @param _accepted Whether the token is supported or not as a method of fee payment. - function changeAcceptedFeeTokens(IERC20 _feeToken, bool _accepted) external onlyByOwner { - currencyRates[_feeToken].feePaymentAccepted = _accepted; - emit AcceptedFeeToken(_feeToken, _accepted); - } - - /// @dev Changes the currency rate of a fee token. - /// @param _feeToken The fee token. - /// @param _rateInEth The new rate of the fee token in ETH. - /// @param _rateDecimals The new decimals of the fee token rate. - function changeCurrencyRates(IERC20 _feeToken, uint64 _rateInEth, uint8 _rateDecimals) external onlyByOwner { - currencyRates[_feeToken].rateInEth = _rateInEth; - currencyRates[_feeToken].rateDecimals = _rateDecimals; - emit NewCurrencyRate(_feeToken, _rateInEth, _rateDecimals); - } - - // ************************************* // - // * State Modifiers * // - // ************************************* // - - /// @dev Sets the caller's stake in a court. - /// @param _courtID The ID of the court. - /// @param _newStake The new stake. - /// Note that the existing delayed stake will be nullified as non-relevant. - function setStake(uint96 _courtID, uint256 _newStake) external virtual whenNotPaused { - _setStake(msg.sender, _courtID, _newStake, false, OnError.Revert); - } - - /// @dev Sets the stake of a specified account in a court without delaying stake changes, typically to apply a delayed stake or unstake inactive jurors. - /// @param _account The account whose stake is being set. - /// @param _courtID The ID of the court. - /// @param _newStake The new stake. - function setStakeBySortitionModule(address _account, uint96 _courtID, uint256 _newStake) external { - if (msg.sender != address(sortitionModule)) revert SortitionModuleOnly(); - _setStake(_account, _courtID, _newStake, true, OnError.Return); - } - - /// @dev Transfers PNK to the juror by SortitionModule. - /// @param _account The account of the juror whose PNK to transfer. - /// @param _amount The amount to transfer. - function transferBySortitionModule(address _account, uint256 _amount) external { - if (msg.sender != address(sortitionModule)) revert SortitionModuleOnly(); - // Note eligibility is checked in SortitionModule. - pinakion.safeTransfer(_account, _amount); - } - - /// @inheritdoc IArbitratorV2 - function createDispute( - uint256 _numberOfChoices, - bytes memory _extraData - ) external payable override returns (uint256 disputeID) { - if (msg.value < arbitrationCost(_extraData)) revert ArbitrationFeesNotEnough(); - - return _createDispute(_numberOfChoices, _extraData, NATIVE_CURRENCY, msg.value); - } - - /// @inheritdoc IArbitratorV2 - function createDispute( - uint256 _numberOfChoices, - bytes calldata _extraData, - IERC20 _feeToken, - uint256 _feeAmount - ) external override returns (uint256 disputeID) { - if (!currencyRates[_feeToken].feePaymentAccepted) revert TokenNotAccepted(); - if (_feeAmount < arbitrationCost(_extraData, _feeToken)) revert ArbitrationFeesNotEnough(); - - if (!_feeToken.safeTransferFrom(msg.sender, address(this), _feeAmount)) revert TransferFailed(); - return _createDispute(_numberOfChoices, _extraData, _feeToken, _feeAmount); - } - - function _createDispute( - uint256 _numberOfChoices, - bytes memory _extraData, - IERC20 _feeToken, - uint256 _feeAmount - ) internal virtual returns (uint256 disputeID) { - (uint96 courtID, , uint256 disputeKitID) = _extraDataToCourtIDMinJurorsDisputeKit(_extraData); - if (!courts[courtID].supportedDisputeKits[disputeKitID]) revert DisputeKitNotSupportedByCourt(); - - disputeID = disputes.length; - Dispute storage dispute = disputes.push(); - dispute.courtID = courtID; - dispute.arbitrated = IArbitrableV2(msg.sender); - dispute.lastPeriodChange = block.timestamp; - - IDisputeKit disputeKit = disputeKits[disputeKitID]; - Court storage court = courts[courtID]; - Round storage round = dispute.rounds.push(); - - // Obtain the feeForJuror in the same currency as the _feeAmount - uint256 feeForJuror = (_feeToken == NATIVE_CURRENCY) - ? court.feeForJuror - : convertEthToTokenAmount(_feeToken, court.feeForJuror); - round.nbVotes = _feeAmount / feeForJuror; - round.disputeKitID = disputeKitID; - round.pnkAtStakePerJuror = _calculatePnkAtStake(court.minStake, court.alpha); - round.totalFeesForJurors = _feeAmount; - round.feeToken = IERC20(_feeToken); - - sortitionModule.createDisputeHook(disputeID, 0); // Default round ID. - - disputeKit.createDispute(disputeID, _numberOfChoices, _extraData, round.nbVotes); - emit DisputeCreation(disputeID, IArbitrableV2(msg.sender)); - } - - /// @dev Passes the period of a specified dispute. - /// @param _disputeID The ID of the dispute. - function passPeriod(uint256 _disputeID) external { - Dispute storage dispute = disputes[_disputeID]; - Court storage court = courts[dispute.courtID]; - - uint256 currentRound = dispute.rounds.length - 1; - Round storage round = dispute.rounds[currentRound]; - if (dispute.period == Period.evidence) { - if ( - currentRound == 0 && - block.timestamp - dispute.lastPeriodChange < court.timesPerPeriod[uint256(dispute.period)] - ) { - revert EvidenceNotPassedAndNotAppeal(); - } - if (round.drawnJurors.length != round.nbVotes) revert DisputeStillDrawing(); - dispute.period = court.hiddenVotes ? Period.commit : Period.vote; - } else if (dispute.period == Period.commit) { - // 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; - } else if (dispute.period == Period.vote) { - if ( - block.timestamp - dispute.lastPeriodChange < court.timesPerPeriod[uint256(dispute.period)] && - !disputeKits[round.disputeKitID].areVotesAllCast(_disputeID) - ) { - revert VotePeriodNotPassed(); - } - dispute.period = Period.appeal; - emit AppealPossible(_disputeID, dispute.arbitrated); - } else if (dispute.period == Period.appeal) { - if ( - block.timestamp - dispute.lastPeriodChange < court.timesPerPeriod[uint256(dispute.period)] && - !disputeKits[round.disputeKitID].isAppealFunded(_disputeID) - ) { - revert AppealPeriodNotPassed(); - } - dispute.period = Period.execution; - } else if (dispute.period == Period.execution) { - revert DisputePeriodIsFinal(); - } - - dispute.lastPeriodChange = block.timestamp; - emit NewPeriod(_disputeID, dispute.period); - } - - /// @dev Draws jurors for the dispute. Can be called in parts. - /// @param _disputeID The ID of the dispute. - /// @param _iterations The number of iterations to run. - /// @return nbDrawnJurors The total number of jurors drawn in the round. - function draw(uint256 _disputeID, uint256 _iterations) external returns (uint256 nbDrawnJurors) { - Dispute storage dispute = disputes[_disputeID]; - uint256 currentRound = dispute.rounds.length - 1; - Round storage round = dispute.rounds[currentRound]; - if (dispute.period != Period.evidence) revert NotEvidencePeriod(); - - IDisputeKit disputeKit = disputeKits[round.disputeKitID]; - - uint256 startIndex = round.drawIterations; // for gas: less storage reads - uint256 i; - while (i < _iterations && round.drawnJurors.length < round.nbVotes) { - (address drawnAddress, uint96 fromSubcourtID) = disputeKit.draw(_disputeID, startIndex + i++); - if (drawnAddress == address(0)) { - continue; - } - sortitionModule.lockStake(drawnAddress, round.pnkAtStakePerJuror); - emit Draw(drawnAddress, _disputeID, currentRound, round.drawnJurors.length); - round.drawnJurors.push(drawnAddress); - round.drawnJurorFromCourtIDs.push(fromSubcourtID != 0 ? fromSubcourtID : dispute.courtID); - if (round.drawnJurors.length == round.nbVotes) { - sortitionModule.postDrawHook(_disputeID, currentRound); - } - } - round.drawIterations += i; - return round.drawnJurors.length; - } - - /// @dev Appeals the ruling of a specified dispute. - /// Note: Access restricted to the Dispute Kit for this `disputeID`. - /// @param _disputeID The ID of the dispute. - /// @param _numberOfChoices Number of choices for the dispute. Can be required during court jump. - /// @param _extraData Extradata for the dispute. Can be required during court jump. - function appeal(uint256 _disputeID, uint256 _numberOfChoices, bytes memory _extraData) external payable { - if (msg.value < appealCost(_disputeID)) revert AppealFeesNotEnough(); - - Dispute storage dispute = disputes[_disputeID]; - if (dispute.period != Period.appeal) revert DisputeNotAppealable(); - - Round storage round = dispute.rounds[dispute.rounds.length - 1]; - if (msg.sender != address(disputeKits[round.disputeKitID])) revert DisputeKitOnly(); - - // Warning: the extra round must be created before calling disputeKit.createDispute() - Round storage extraRound = dispute.rounds.push(); - - (uint96 newCourtID, uint256 newDisputeKitID, bool courtJump, ) = _getCourtAndDisputeKitJumps( - dispute, - round, - courts[dispute.courtID], - _disputeID - ); - if (courtJump) { - emit CourtJump(_disputeID, dispute.rounds.length - 1, dispute.courtID, newCourtID); - } - - dispute.courtID = newCourtID; - dispute.period = Period.evidence; - dispute.lastPeriodChange = block.timestamp; - - Court storage court = courts[newCourtID]; - extraRound.nbVotes = msg.value / court.feeForJuror; // As many votes that can be afforded by the provided funds. - extraRound.pnkAtStakePerJuror = _calculatePnkAtStake(court.minStake, court.alpha); - extraRound.totalFeesForJurors = msg.value; - extraRound.disputeKitID = newDisputeKitID; - - sortitionModule.createDisputeHook(_disputeID, dispute.rounds.length - 1); - - // Dispute kit was changed, so create a dispute in the new DK contract. - if (extraRound.disputeKitID != round.disputeKitID) { - emit DisputeKitJump(_disputeID, dispute.rounds.length - 1, round.disputeKitID, extraRound.disputeKitID); - disputeKits[extraRound.disputeKitID].createDispute( - _disputeID, - _numberOfChoices, - _extraData, - extraRound.nbVotes - ); - } - - emit AppealDecision(_disputeID, dispute.arbitrated); - emit NewPeriod(_disputeID, Period.evidence); - } - - /// @dev Distribute the PNKs at stake and the dispute fees for the specific round of the dispute. Can be called in parts. - /// Note: Reward distributions are forbidden during pause. - /// @param _disputeID The ID of the dispute. - /// @param _round The appeal round. - /// @param _iterations The number of iterations to run. - function execute(uint256 _disputeID, uint256 _round, uint256 _iterations) external whenNotPaused { - Round storage round; - { - Dispute storage dispute = disputes[_disputeID]; - if (dispute.period != Period.execution) revert NotExecutionPeriod(); - - round = dispute.rounds[_round]; - } // stack too deep workaround - - uint256 start = round.repartitions; - uint256 end = round.repartitions + _iterations; - - uint256 pnkPenaltiesInRound = round.pnkPenalties; // Keep in memory to save gas. - uint256 numberOfVotesInRound = round.drawnJurors.length; - uint256 feePerJurorInRound = round.totalFeesForJurors / numberOfVotesInRound; - uint256 pnkAtStakePerJurorInRound = round.pnkAtStakePerJuror; - uint256 coherentCount; - { - IDisputeKit disputeKit = disputeKits[round.disputeKitID]; - coherentCount = disputeKit.getCoherentCount(_disputeID, _round); // Total number of jurors that are eligible to a reward in this round. - } // stack too deep workaround - - if (coherentCount == 0) { - // We loop over the votes once as there are no rewards because it is not a tie and no one in this round is coherent with the final outcome. - if (end > numberOfVotesInRound) end = numberOfVotesInRound; - } else { - // We loop over the votes twice, first to collect the PNK penalties, and second to distribute them as rewards along with arbitration fees. - if (end > numberOfVotesInRound * 2) end = numberOfVotesInRound * 2; - } - round.repartitions = end; - - for (uint256 i = start; i < end; i++) { - if (i < numberOfVotesInRound) { - pnkPenaltiesInRound = _executePenalties( - ExecuteParams({ - disputeID: _disputeID, - round: _round, - coherentCount: coherentCount, - numberOfVotesInRound: numberOfVotesInRound, - feePerJurorInRound: feePerJurorInRound, - pnkAtStakePerJurorInRound: pnkAtStakePerJurorInRound, - pnkPenaltiesInRound: pnkPenaltiesInRound, - repartition: i - }) - ); - } else { - _executeRewards( - ExecuteParams({ - disputeID: _disputeID, - round: _round, - coherentCount: coherentCount, - numberOfVotesInRound: numberOfVotesInRound, - feePerJurorInRound: feePerJurorInRound, - pnkAtStakePerJurorInRound: pnkAtStakePerJurorInRound, - pnkPenaltiesInRound: pnkPenaltiesInRound, - repartition: i - }) - ); - } - } - if (round.pnkPenalties != pnkPenaltiesInRound) { - round.pnkPenalties = pnkPenaltiesInRound; // Reentrancy risk: breaks Check-Effect-Interact - } - } - - /// @dev Distribute the PNKs at stake and the dispute fees for the specific round of the dispute, penalties only. - /// @param _params The parameters for the execution, see `ExecuteParams`. - /// @return pnkPenaltiesInRoundCache The updated penalties in round cache. - function _executePenalties(ExecuteParams memory _params) internal returns (uint256) { - Dispute storage dispute = disputes[_params.disputeID]; - Round storage round = dispute.rounds[_params.round]; - IDisputeKit disputeKit = disputeKits[round.disputeKitID]; - - // [0, 1] value that determines how coherent the juror was in this round, in basis points. - uint256 coherence = disputeKit.getDegreeOfCoherencePenalty( - _params.disputeID, - _params.round, - _params.repartition, - _params.feePerJurorInRound, - _params.pnkAtStakePerJurorInRound - ); - - // 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 * (ONE_BASIS_POINT - coherence)) / ONE_BASIS_POINT; - - // Unlock the PNKs affected by the penalty - address account = round.drawnJurors[_params.repartition]; - sortitionModule.unlockStake(account, penalty); - - // Apply the penalty to the staked PNKs. - uint96 penalizedInCourtID = round.drawnJurorFromCourtIDs[_params.repartition]; - (uint256 pnkBalance, uint256 newCourtStake, uint256 availablePenalty) = sortitionModule.setStakePenalty( - account, - penalizedInCourtID, - penalty - ); - _params.pnkPenaltiesInRound += availablePenalty; - emit TokenAndETHShift( - account, - _params.disputeID, - _params.round, - coherence, - -int256(availablePenalty), - 0, - round.feeToken - ); - - if (pnkBalance == 0 || !disputeKit.isVoteActive(_params.disputeID, _params.round, _params.repartition)) { - // The juror is inactive or their balance is can't cover penalties anymore, unstake them from all courts. - sortitionModule.forcedUnstakeAllCourts(account); - } else if (newCourtStake < courts[penalizedInCourtID].minStake) { - // The juror's balance fell below the court minStake, unstake them from the court. - sortitionModule.forcedUnstake(account, penalizedInCourtID); - } - - if (_params.repartition == _params.numberOfVotesInRound - 1 && _params.coherentCount == 0) { - // No one was coherent, send the rewards to the owner. - _transferFeeToken(round.feeToken, payable(owner), round.totalFeesForJurors); - pinakion.safeTransfer(owner, _params.pnkPenaltiesInRound); - emit LeftoverRewardSent( - _params.disputeID, - _params.round, - _params.pnkPenaltiesInRound, - round.totalFeesForJurors, - round.feeToken - ); - } - return _params.pnkPenaltiesInRound; - } - - /// @dev Distribute the PNKs at stake and the dispute fees for the specific round of the dispute, rewards only. - /// @param _params The parameters for the execution, see `ExecuteParams`. - function _executeRewards(ExecuteParams memory _params) internal { - Dispute storage dispute = disputes[_params.disputeID]; - Round storage round = dispute.rounds[_params.round]; - IDisputeKit disputeKit = disputeKits[round.disputeKitID]; - - // [0, 1] value that determines how coherent the juror was in this round, in basis points. - (uint256 pnkCoherence, uint256 feeCoherence) = disputeKit.getDegreeOfCoherenceReward( - _params.disputeID, - _params.round, - _params.repartition % _params.numberOfVotesInRound, - _params.feePerJurorInRound, - _params.pnkAtStakePerJurorInRound - ); - - // 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 = _applyCoherence(round.pnkAtStakePerJuror, pnkCoherence); - - // Release the rest of the PNKs of the juror for this round. - sortitionModule.unlockStake(account, pnkLocked); - - // Compute the rewards - uint256 pnkReward = _applyCoherence(_params.pnkPenaltiesInRound / _params.coherentCount, pnkCoherence); - round.sumPnkRewardPaid += pnkReward; - uint256 feeReward = _applyCoherence(round.totalFeesForJurors / _params.coherentCount, feeCoherence); - round.sumFeeRewardPaid += feeReward; - - // Transfer the fee reward - _transferFeeToken(round.feeToken, payable(account), feeReward); - - // Stake the PNK reward if possible, by-passes delayed stakes and other checks usually done by validateStake() - if (!sortitionModule.setStakeReward(account, dispute.courtID, pnkReward)) { - pinakion.safeTransfer(account, pnkReward); - } - - emit TokenAndETHShift( - account, - _params.disputeID, - _params.round, - pnkCoherence, - int256(pnkReward), - int256(feeReward), - round.feeToken - ); - - // Transfer any residual rewards to the owner. It may happen due to partial coherence of the jurors. - if (_params.repartition == _params.numberOfVotesInRound * 2 - 1) { - uint256 leftoverPnkReward = _params.pnkPenaltiesInRound - round.sumPnkRewardPaid; - uint256 leftoverFeeReward = round.totalFeesForJurors - round.sumFeeRewardPaid; - if (leftoverPnkReward != 0 || leftoverFeeReward != 0) { - if (leftoverPnkReward != 0) { - pinakion.safeTransfer(owner, leftoverPnkReward); - } - if (leftoverFeeReward != 0) { - _transferFeeToken(round.feeToken, payable(owner), leftoverFeeReward); - } - emit LeftoverRewardSent( - _params.disputeID, - _params.round, - leftoverPnkReward, - leftoverFeeReward, - round.feeToken - ); - } - } - } - - /// @dev Executes a specified dispute's ruling. - /// @param _disputeID The ID of the dispute. - function executeRuling(uint256 _disputeID) external { - Dispute storage dispute = disputes[_disputeID]; - if (dispute.period != Period.execution) revert NotExecutionPeriod(); - if (dispute.ruled) revert RulingAlreadyExecuted(); - - (uint256 winningChoice, , ) = currentRuling(_disputeID); - dispute.ruled = true; - emit Ruling(dispute.arbitrated, _disputeID, winningChoice); - dispute.arbitrated.rule(_disputeID, winningChoice); - } - - // ************************************* // - // * Public Views * // - // ************************************* // - - /// @dev Compute the cost of arbitration denominated in ETH. - /// It is recommended not to increase it often, as it can be highly time and gas consuming for the arbitrated contracts to cope with fee augmentation. - /// @param _extraData Additional info about the dispute. We use it to pass the ID of the dispute's court (first 32 bytes), the minimum number of jurors required (next 32 bytes) and the ID of the specific dispute kit (last 32 bytes). - /// @return cost The arbitration cost in ETH. - function arbitrationCost(bytes memory _extraData) public view override returns (uint256 cost) { - (uint96 courtID, uint256 minJurors, ) = _extraDataToCourtIDMinJurorsDisputeKit(_extraData); - cost = courts[courtID].feeForJuror * minJurors; - } - - /// @dev Compute the cost of arbitration denominated in `_feeToken`. - /// It is recommended not to increase it often, as it can be highly time and gas consuming for the arbitrated contracts to cope with fee augmentation. - /// @param _extraData Additional info about the dispute. We use it to pass the ID of the dispute's court (first 32 bytes), the minimum number of jurors required (next 32 bytes) and the ID of the specific dispute kit (last 32 bytes). - /// @param _feeToken The ERC20 token used to pay fees. - /// @return cost The arbitration cost in `_feeToken`. - function arbitrationCost(bytes calldata _extraData, IERC20 _feeToken) public view override returns (uint256 cost) { - cost = convertEthToTokenAmount(_feeToken, arbitrationCost(_extraData)); - } - - /// @dev Gets the cost of appealing a specified dispute. - /// @param _disputeID The ID of the dispute. - /// @return cost The appeal cost. - function appealCost(uint256 _disputeID) public view returns (uint256 cost) { - Dispute storage dispute = disputes[_disputeID]; - Round storage round = dispute.rounds[dispute.rounds.length - 1]; - Court storage court = courts[dispute.courtID]; - - (, uint256 newDisputeKitID, bool courtJump, ) = _getCourtAndDisputeKitJumps(dispute, round, court, _disputeID); - - uint256 nbVotesAfterAppeal = disputeKits[newDisputeKitID].getNbVotesAfterAppeal( - disputeKits[round.disputeKitID], - round.nbVotes - ); - - if (courtJump) { - // Jump to parent court. - if (dispute.courtID == GENERAL_COURT) { - // TODO: Handle the forking when appealed in General court. - cost = NON_PAYABLE_AMOUNT; // Get the cost of the parent court. - } else { - cost = courts[court.parent].feeForJuror * nbVotesAfterAppeal; - } - } else { - // Stay in current court. - cost = court.feeForJuror * nbVotesAfterAppeal; - } - } - - /// @dev Gets the start and the end of a specified dispute's current appeal period. - /// @param _disputeID The ID of the dispute. - /// @return start The start of the appeal period. - /// @return end The end of the appeal period. - function appealPeriod(uint256 _disputeID) external view returns (uint256 start, uint256 end) { - Dispute storage dispute = disputes[_disputeID]; - if (dispute.period == Period.appeal) { - start = dispute.lastPeriodChange; - end = dispute.lastPeriodChange + courts[dispute.courtID].timesPerPeriod[uint256(Period.appeal)]; - } else { - start = 0; - end = 0; - } - } - - /// @dev Gets the current ruling of a specified dispute. - /// @param _disputeID The ID of the dispute. - /// @return ruling The current ruling. - /// @return tied Whether it's a tie or not. - /// @return overridden Whether the ruling was overridden by appeal funding or not. - function currentRuling(uint256 _disputeID) public view returns (uint256 ruling, bool tied, bool overridden) { - Dispute storage dispute = disputes[_disputeID]; - Round storage round = dispute.rounds[dispute.rounds.length - 1]; - IDisputeKit disputeKit = disputeKits[round.disputeKitID]; - (ruling, tied, overridden) = disputeKit.currentRuling(_disputeID); - } - - /// @dev Gets the round info for a specified dispute and round. - /// @dev This function must not be called from a non-view function because it returns a dynamic array which might be very large, theoretically exceeding the block gas limit. - /// @param _disputeID The ID of the dispute. - /// @param _round The round to get the info for. - /// @return round The round info. - function getRoundInfo(uint256 _disputeID, uint256 _round) external view returns (Round memory) { - return disputes[_disputeID].rounds[_round]; - } - - /// @dev Gets the PNK at stake per juror for a specified dispute and round. - /// @param _disputeID The ID of the dispute. - /// @param _round The round to get the info for. - /// @return pnkAtStakePerJuror The PNK at stake per juror. - function getPnkAtStakePerJuror(uint256 _disputeID, uint256 _round) external view returns (uint256) { - return disputes[_disputeID].rounds[_round].pnkAtStakePerJuror; - } - - /// @dev Gets the number of rounds for a specified dispute. - /// @param _disputeID The ID of the dispute. - /// @return The number of rounds. - function getNumberOfRounds(uint256 _disputeID) external view returns (uint256) { - return disputes[_disputeID].rounds.length; - } - - /// @dev Checks if a given dispute kit is supported by a given court. - /// @param _courtID The ID of the court to check the support for. - /// @param _disputeKitID The ID of the dispute kit to check the support for. - /// @return Whether the dispute kit is supported or not. - function isSupported(uint96 _courtID, uint256 _disputeKitID) external view returns (bool) { - return courts[_courtID].supportedDisputeKits[_disputeKitID]; - } - - /// @dev Gets the timesPerPeriod array for a given court. - /// @param _courtID The ID of the court to get the times from. - /// @return timesPerPeriod The timesPerPeriod array for the given court. - function getTimesPerPeriod(uint96 _courtID) external view returns (uint256[4] memory timesPerPeriod) { - timesPerPeriod = courts[_courtID].timesPerPeriod; - } - - // ************************************* // - // * Public Views for Dispute Kits * // - // ************************************* // - - /// @dev Gets the number of votes permitted for the specified dispute in the latest round. - /// @param _disputeID The ID of the dispute. - function getNumberOfVotes(uint256 _disputeID) external view returns (uint256) { - Dispute storage dispute = disputes[_disputeID]; - return dispute.rounds[dispute.rounds.length - 1].nbVotes; - } - - /// @dev Returns true if the dispute kit will be switched to a parent DK. - /// @param _disputeID The ID of the dispute. - /// @return Whether DK will be switched or not. - function isDisputeKitJumping(uint256 _disputeID) external view returns (bool) { - Dispute storage dispute = disputes[_disputeID]; - Round storage round = dispute.rounds[dispute.rounds.length - 1]; - Court storage court = courts[dispute.courtID]; - - if (!_isCourtJumping(round, court, _disputeID)) { - return false; - } - - // Jump if the parent court doesn't support the current DK. - return !courts[court.parent].supportedDisputeKits[round.disputeKitID]; - } - - function getDisputeKitsLength() external view returns (uint256) { - return disputeKits.length; - } - - function convertEthToTokenAmount(IERC20 _toToken, uint256 _amountInEth) public view returns (uint256) { - return (_amountInEth * 10 ** currencyRates[_toToken].rateDecimals) / currencyRates[_toToken].rateInEth; - } - - // ************************************* // - // * Internal * // - // ************************************* // - - /// @dev Returns true if the round is jumping to a parent court. - /// @param _round The round to check. - /// @param _court The court to check. - /// @return Whether the round is jumping to a parent court or not. - function _isCourtJumping( - Round storage _round, - Court storage _court, - uint256 _disputeID - ) internal view returns (bool) { - return - disputeKits[_round.disputeKitID].earlyCourtJump(_disputeID) || _round.nbVotes >= _court.jurorsForCourtJump; - } - - function _getCourtAndDisputeKitJumps( - Dispute storage _dispute, - Round storage _round, - Court storage _court, - uint256 _disputeID - ) internal view returns (uint96 newCourtID, uint256 newDisputeKitID, bool courtJump, bool disputeKitJump) { - newCourtID = _dispute.courtID; - newDisputeKitID = _round.disputeKitID; - - if (!_isCourtJumping(_round, _court, _disputeID)) return (newCourtID, newDisputeKitID, false, false); - - // Jump to parent court. - newCourtID = courts[newCourtID].parent; - courtJump = true; - - if (!courts[newCourtID].supportedDisputeKits[newDisputeKitID]) { - // The current Dispute Kit is not compatible with the new court, jump to another Dispute Kit. - newDisputeKitID = disputeKits[_round.disputeKitID].getJumpDisputeKitID(); - if (newDisputeKitID == NULL_DISPUTE_KIT || !courts[newCourtID].supportedDisputeKits[newDisputeKitID]) { - // The new Dispute Kit is not defined or still not compatible, fall back to `DisputeKitClassic` which is always supported. - newDisputeKitID = DISPUTE_KIT_CLASSIC; - } - disputeKitJump = true; - } - } - - /// @dev Internal function to transfer fee tokens (ETH or ERC20) - /// @param _feeToken The token to transfer (NATIVE_CURRENCY for ETH). - /// @param _recipient The recipient address. - /// @param _amount The amount to transfer. - function _transferFeeToken(IERC20 _feeToken, address payable _recipient, uint256 _amount) internal { - if (_feeToken == NATIVE_CURRENCY) { - _recipient.safeSend(_amount, wNative); - } else { - _feeToken.safeTransfer(_recipient, _amount); - } - } - - /// @dev Applies degree of coherence to an amount - /// @param _amount The base amount to apply coherence to. - /// @param _coherence The degree of coherence in basis points. - /// @return The amount after applying the degree of coherence. - 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 - /// @param _minStake The minimum stake for the court. - /// @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) / ONE_BASIS_POINT; - } - - /// @dev Toggles the dispute kit support for a given court. - /// @param _courtID The ID of the court to toggle the support for. - /// @param _disputeKitID The ID of the dispute kit to toggle the support for. - /// @param _enable Whether to enable or disable the support. Note that classic dispute kit should always be enabled. - function _enableDisputeKit(uint96 _courtID, uint256 _disputeKitID, bool _enable) internal { - courts[_courtID].supportedDisputeKits[_disputeKitID] = _enable; - emit DisputeKitEnabled(_courtID, _disputeKitID, _enable); - } - - /// @dev If called only once then set _onError to Revert, otherwise set it to Return - /// @param _account The account to set the stake for. - /// @param _courtID The ID of the court to set the stake for. - /// @param _newStake The new stake. - /// @param _noDelay True if the stake change should not be delayed. - /// @param _onError Whether to revert or return false on error. - /// @return Whether the stake was successfully set or not. - function _setStake( - address _account, - uint96 _courtID, - uint256 _newStake, - bool _noDelay, - OnError _onError - ) internal returns (bool) { - if (_courtID == FORKING_COURT || _courtID >= courts.length) { - _stakingFailed(_onError, StakingResult.CannotStakeInThisCourt); // Staking directly into the forking court is not allowed. - return false; - } - if (_newStake != 0 && _newStake < courts[_courtID].minStake) { - _stakingFailed(_onError, StakingResult.CannotStakeLessThanMinStake); // Staking less than the minimum stake is not allowed. - return false; - } - (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) = sortitionModule.validateStake( - _account, - _courtID, - _newStake, - _noDelay - ); - if (stakingResult != StakingResult.Successful && stakingResult != StakingResult.Delayed) { - _stakingFailed(_onError, stakingResult); - return false; - } else if (stakingResult == StakingResult.Delayed) { - return true; - } - if (pnkDeposit > 0) { - if (!pinakion.safeTransferFrom(_account, address(this), pnkDeposit)) { - _stakingFailed(_onError, StakingResult.StakingTransferFailed); - return false; - } - } - if (pnkWithdrawal > 0) { - if (!pinakion.safeTransfer(_account, pnkWithdrawal)) { - _stakingFailed(_onError, StakingResult.UnstakingTransferFailed); - return false; - } - } - sortitionModule.setStake(_account, _courtID, pnkDeposit, pnkWithdrawal, _newStake); - - return true; - } - - /// @dev It may revert depending on the _onError parameter. - function _stakingFailed(OnError _onError, StakingResult _result) internal pure virtual { - if (_onError == OnError.Return) return; - if (_result == StakingResult.StakingTransferFailed) revert StakingTransferFailed(); - if (_result == StakingResult.UnstakingTransferFailed) revert UnstakingTransferFailed(); - if (_result == StakingResult.CannotStakeInMoreCourts) revert StakingInTooManyCourts(); - if (_result == StakingResult.CannotStakeInThisCourt) revert StakingNotPossibleInThisCourt(); - if (_result == StakingResult.CannotStakeLessThanMinStake) revert StakingLessThanCourtMinStake(); - if (_result == StakingResult.CannotStakeZeroWhenNoStake) revert StakingZeroWhenNoStake(); - } - - /// @dev Gets a court ID, the minimum number of jurors and an ID of a dispute kit from a specified extra data bytes array. - /// Note that if extradata contains an incorrect value then this value will be switched to default. - /// @param _extraData The extra data bytes array. The first 32 bytes are the court ID, the next are the minimum number of jurors and the last are the dispute kit ID. - /// @return courtID The court ID. - /// @return minJurors The minimum number of jurors required. - /// @return disputeKitID The ID of the dispute kit. - function _extraDataToCourtIDMinJurorsDisputeKit( - bytes memory _extraData - ) internal view returns (uint96 courtID, uint256 minJurors, uint256 disputeKitID) { - // Note that if the extradata doesn't contain 32 bytes for the dispute kit ID it'll return the default 0 index. - if (_extraData.length >= 64) { - assembly { - // solium-disable-line security/no-inline-assembly - courtID := mload(add(_extraData, 0x20)) - minJurors := mload(add(_extraData, 0x40)) - disputeKitID := mload(add(_extraData, 0x60)) - } - if (courtID == FORKING_COURT || courtID >= courts.length) { - courtID = GENERAL_COURT; - } - if (minJurors == 0) { - minJurors = DEFAULT_NB_OF_JURORS; - } - if (disputeKitID == NULL_DISPUTE_KIT || disputeKitID >= disputeKits.length) { - disputeKitID = DISPUTE_KIT_CLASSIC; // 0 index is not used. - } - } else { - courtID = GENERAL_COURT; - minJurors = DEFAULT_NB_OF_JURORS; - disputeKitID = DISPUTE_KIT_CLASSIC; - } - } - - // ************************************* // - // * Errors * // - // ************************************* // - - error OwnerOnly(); - error GuardianOrOwnerOnly(); - error DisputeKitOnly(); - error SortitionModuleOnly(); - error UnsuccessfulCall(); - error InvalidDisputKitParent(); - error MinStakeLowerThanParentCourt(); - error UnsupportedDisputeKit(); - error InvalidForkingCourtAsParent(); - error WrongDisputeKitIndex(); - error CannotDisableClassicDK(); - error StakingInTooManyCourts(); - error StakingNotPossibleInThisCourt(); - error StakingLessThanCourtMinStake(); - error StakingTransferFailed(); - error UnstakingTransferFailed(); - error ArbitrationFeesNotEnough(); - error DisputeKitNotSupportedByCourt(); - error MustSupportDisputeKitClassic(); - error TokenNotAccepted(); - error EvidenceNotPassedAndNotAppeal(); - error DisputeStillDrawing(); - error CommitPeriodNotPassed(); - error VotePeriodNotPassed(); - error AppealPeriodNotPassed(); - error NotEvidencePeriod(); - error AppealFeesNotEnough(); - error DisputeNotAppealable(); - error NotExecutionPeriod(); - error RulingAlreadyExecuted(); - error DisputePeriodIsFinal(); - error TransferFailed(); - error WhenNotPausedOnly(); - error WhenPausedOnly(); - error StakingZeroWhenNoStake(); -} diff --git a/contracts/src/arbitration/KlerosCoreNeo.sol b/contracts/src/arbitration/KlerosCoreNeo.sol deleted file mode 100644 index 6f43132d1..000000000 --- a/contracts/src/arbitration/KlerosCoreNeo.sol +++ /dev/null @@ -1,144 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.24; - -import {KlerosCoreBase, IDisputeKit, ISortitionModule, IERC20, OnError, StakingResult} from "./KlerosCoreBase.sol"; -import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; - -/// @title KlerosCoreNeo -/// Core arbitrator contract for Kleros v2. -/// Note that this contract trusts the PNK token, the dispute kit and the sortition module contracts. -contract KlerosCoreNeo is KlerosCoreBase { - string public constant override version = "2.0.0"; - - // ************************************* // - // * Storage * // - // ************************************* // - - mapping(address => bool) public arbitrableWhitelist; // Arbitrable whitelist. - bool public arbitrableWhitelistEnabled; // Whether the arbitrable whitelist is enabled. - IERC721 public jurorNft; // Eligible jurors NFT. - - // ************************************* // - // * Constructor * // - // ************************************* // - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() { - _disableInitializers(); - } - - /// @dev Initializer (constructor equivalent for upgradable contracts). - /// @param _owner The owner's address. - /// @param _guardian The guardian's address. - /// @param _pinakion The address of the token contract. - /// @param _jurorProsecutionModule The address of the juror prosecution module. - /// @param _disputeKit The address of the default dispute kit. - /// @param _hiddenVotes The `hiddenVotes` property value of the general court. - /// @param _courtParameters Numeric parameters of General court (minStake, alpha, feeForJuror and jurorsForCourtJump respectively). - /// @param _timesPerPeriod The `timesPerPeriod` property value of the general court. - /// @param _sortitionExtraData The extra data for sortition module. - /// @param _sortitionModuleAddress The sortition module responsible for sortition of the jurors. - /// @param _jurorNft NFT contract to vet the jurors. - /// @param _wNative The wrapped native token address, typically wETH. - function initialize( - address _owner, - address _guardian, - IERC20 _pinakion, - address _jurorProsecutionModule, - IDisputeKit _disputeKit, - bool _hiddenVotes, - uint256[4] memory _courtParameters, - uint256[4] memory _timesPerPeriod, - bytes memory _sortitionExtraData, - ISortitionModule _sortitionModuleAddress, - IERC721 _jurorNft, - address _wNative - ) external reinitializer(2) { - __KlerosCoreBase_initialize( - _owner, - _guardian, - _pinakion, - _jurorProsecutionModule, - _disputeKit, - _hiddenVotes, - _courtParameters, - _timesPerPeriod, - _sortitionExtraData, - _sortitionModuleAddress, - _wNative - ); - jurorNft = _jurorNft; - } - - // ************************************* // - // * Governance * // - // ************************************* // - - /// @dev Access Control to perform implementation upgrades (UUPS Proxiable) - /// Only the owner can perform upgrades (`onlyByOwner`) - function _authorizeUpgrade(address) internal view override onlyByOwner { - // NOP - } - - /// @dev Changes the `jurorNft` storage variable. - /// @param _jurorNft The new value for the `jurorNft` storage variable. - function changeJurorNft(IERC721 _jurorNft) external onlyByOwner { - jurorNft = _jurorNft; - } - - /// @dev Adds or removes an arbitrable from whitelist. - /// @param _arbitrable Arbitrable address. - /// @param _allowed Whether add or remove permission. - function changeArbitrableWhitelist(address _arbitrable, bool _allowed) external onlyByOwner { - arbitrableWhitelist[_arbitrable] = _allowed; - } - - /// @dev Enables or disables the arbitrable whitelist. - function changeArbitrableWhitelistEnabled(bool _enabled) external onlyByOwner { - arbitrableWhitelistEnabled = _enabled; - } - - // ************************************* // - // * State Modifiers * // - // ************************************* // - - /// @dev Sets the caller's stake in a court. - /// Note: Staking and unstaking is forbidden during pause. - /// @param _courtID The ID of the court. - /// @param _newStake The new stake. - /// Note that the existing delayed stake will be nullified as non-relevant. - function setStake(uint96 _courtID, uint256 _newStake) external override whenNotPaused { - if (address(jurorNft) != address(0) && jurorNft.balanceOf(msg.sender) == 0) revert NotEligibleForStaking(); - super._setStake(msg.sender, _courtID, _newStake, false, OnError.Revert); - } - - // ************************************* // - // * Internal * // - // ************************************* // - - function _createDispute( - uint256 _numberOfChoices, - bytes memory _extraData, - IERC20 _feeToken, - uint256 _feeAmount - ) internal override returns (uint256 disputeID) { - if (arbitrableWhitelistEnabled && !arbitrableWhitelist[msg.sender]) revert ArbitrableNotWhitelisted(); - return super._createDispute(_numberOfChoices, _extraData, _feeToken, _feeAmount); - } - - function _stakingFailed(OnError _onError, StakingResult _result) internal pure override { - super._stakingFailed(_onError, _result); - if (_result == StakingResult.CannotStakeMoreThanMaxStakePerJuror) revert StakingMoreThanMaxStakePerJuror(); - if (_result == StakingResult.CannotStakeMoreThanMaxTotalStaked) revert StakingMoreThanMaxTotalStaked(); - } - - // ************************************* // - // * Errors * // - // ************************************* // - - error NotEligibleForStaking(); - error StakingMoreThanMaxStakePerJuror(); - error StakingMoreThanMaxTotalStaked(); - error ArbitrableNotWhitelisted(); -} diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol index 369789ab1..5dc1c3522 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.24; -import {KlerosCore, KlerosCoreBase, IDisputeKit, ISortitionModule} from "../KlerosCore.sol"; +import {KlerosCore, IDisputeKit, ISortitionModule} from "../KlerosCore.sol"; import {Initializable} from "../../proxy/Initializable.sol"; import {UUPSProxiable} from "../../proxy/UUPSProxiable.sol"; import {SafeSend} from "../../libraries/SafeSend.sol"; @@ -273,7 +273,7 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi bytes32 _commit ) internal notJumped(_coreDisputeID) { (, , KlerosCore.Period period, , ) = core.disputes(_coreDisputeID); - if (period != KlerosCoreBase.Period.commit) revert NotCommitPeriod(); + if (period != KlerosCore.Period.commit) revert NotCommitPeriod(); if (_commit == bytes32(0)) revert EmptyCommit(); if (!coreDisputeIDToActive[_coreDisputeID]) revert NotActiveForCoreDisputeID(); @@ -314,7 +314,7 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi address _juror ) internal notJumped(_coreDisputeID) { (, , KlerosCore.Period period, , ) = core.disputes(_coreDisputeID); - if (period != KlerosCoreBase.Period.vote) revert NotVotePeriod(); + if (period != KlerosCore.Period.vote) revert NotVotePeriod(); if (_voteIDs.length == 0) revert EmptyVoteIDs(); if (!coreDisputeIDToActive[_coreDisputeID]) revert NotActiveForCoreDisputeID(); @@ -517,7 +517,7 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi ruling = tied ? 0 : round.winningChoice; (, , KlerosCore.Period period, , ) = core.disputes(_coreDisputeID); // Override the final ruling if only one side funded the appeals. - if (period == KlerosCoreBase.Period.execution) { + if (period == KlerosCore.Period.execution) { uint256[] memory fundedChoices = getFundedChoices(_coreDisputeID); if (fundedChoices.length == 1) { ruling = fundedChoices[0]; diff --git a/contracts/src/proxy/KlerosProxies.sol b/contracts/src/proxy/KlerosProxies.sol index 57b521ffd..7db79b67c 100644 --- a/contracts/src/proxy/KlerosProxies.sol +++ b/contracts/src/proxy/KlerosProxies.sol @@ -7,10 +7,6 @@ import "./UUPSProxy.sol"; /// Workaround to get meaningful names for the proxy contracts /// Otherwise all the contracts are called `UUPSProxy` on the chain explorers -contract DisputeKitClassicNeoProxy is UUPSProxy { - constructor(address _implementation, bytes memory _data) UUPSProxy(_implementation, _data) {} -} - contract DisputeKitClassicUniversityProxy is UUPSProxy { constructor(address _implementation, bytes memory _data) UUPSProxy(_implementation, _data) {} } @@ -51,10 +47,6 @@ contract HomeGatewayToEthereumProxy is UUPSProxy { constructor(address _implementation, bytes memory _data) UUPSProxy(_implementation, _data) {} } -contract KlerosCoreNeoProxy is UUPSProxy { - constructor(address _implementation, bytes memory _data) UUPSProxy(_implementation, _data) {} -} - contract KlerosCoreRulerProxy is UUPSProxy { constructor(address _implementation, bytes memory _data) UUPSProxy(_implementation, _data) {} } @@ -75,10 +67,6 @@ contract RandomizerRNGProxy is UUPSProxy { constructor(address _implementation, bytes memory _data) UUPSProxy(_implementation, _data) {} } -contract SortitionModuleNeoProxy is UUPSProxy { - constructor(address _implementation, bytes memory _data) UUPSProxy(_implementation, _data) {} -} - contract SortitionModuleUniversityProxy is UUPSProxy { constructor(address _implementation, bytes memory _data) UUPSProxy(_implementation, _data) {} } diff --git a/contracts/test/foundry/KlerosCore_Appeals.t.sol b/contracts/test/foundry/KlerosCore_Appeals.t.sol index 6421cb119..c5016f23c 100644 --- a/contracts/test/foundry/KlerosCore_Appeals.t.sol +++ b/contracts/test/foundry/KlerosCore_Appeals.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.24; import {KlerosCore_TestBase} from "./KlerosCore_TestBase.sol"; -import {KlerosCoreBase} from "../../src/arbitration/KlerosCoreBase.sol"; +import {KlerosCore} from "../../src/arbitration/KlerosCore.sol"; import {DisputeKitClassic, DisputeKitClassicBase} from "../../src/arbitration/dispute-kits/DisputeKitClassic.sol"; import {UUPSProxy} from "../../src/proxy/UUPSProxy.sol"; import "../../src/libraries/Constants.sol"; @@ -42,34 +42,34 @@ contract KlerosCore_AppealsTest is KlerosCore_TestBase { // Simulate the call from dispute kit to check the requires unrelated to caller vm.prank(address(disputeKit)); - vm.expectRevert(KlerosCoreBase.DisputeNotAppealable.selector); + vm.expectRevert(KlerosCore.DisputeNotAppealable.selector); core.appeal{value: 0.21 ether}(disputeID, 2, arbitratorExtraData); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.AppealPossible(disputeID, arbitrable); + emit KlerosCore.AppealPossible(disputeID, arbitrable); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.appeal); + emit KlerosCore.NewPeriod(disputeID, KlerosCore.Period.appeal); core.passPeriod(disputeID); - (, , KlerosCoreBase.Period period, , uint256 lastPeriodChange) = core.disputes(disputeID); + (, , KlerosCore.Period period, , uint256 lastPeriodChange) = core.disputes(disputeID); (start, end) = core.appealPeriod(0); - assertEq(uint256(period), uint256(KlerosCoreBase.Period.appeal), "Wrong period"); + assertEq(uint256(period), uint256(KlerosCore.Period.appeal), "Wrong period"); assertEq(lastPeriodChange, block.timestamp, "Wrong lastPeriodChange"); assertEq(core.appealCost(0), 0.21 ether, "Wrong appealCost"); assertEq(start, lastPeriodChange, "Appeal period start is incorrect"); assertEq(end, lastPeriodChange + timesPerPeriod[3], "Appeal period end is incorrect"); - vm.expectRevert(KlerosCoreBase.AppealPeriodNotPassed.selector); + vm.expectRevert(KlerosCore.AppealPeriodNotPassed.selector); core.passPeriod(disputeID); // Simulate the call from dispute kit to check the requires unrelated to caller vm.prank(address(disputeKit)); - vm.expectRevert(KlerosCoreBase.AppealFeesNotEnough.selector); + vm.expectRevert(KlerosCore.AppealFeesNotEnough.selector); core.appeal{value: 0.21 ether - 1}(disputeID, 2, arbitratorExtraData); vm.deal(address(disputeKit), 0); // Nullify the balance so it doesn't get in the way. vm.prank(staker1); - vm.expectRevert(KlerosCoreBase.DisputeKitOnly.selector); + vm.expectRevert(KlerosCore.DisputeKitOnly.selector); core.appeal{value: 0.21 ether}(disputeID, 2, arbitratorExtraData); vm.prank(crowdfunder1); @@ -177,9 +177,9 @@ contract KlerosCore_AppealsTest is KlerosCore_TestBase { vm.prank(crowdfunder2); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.AppealDecision(disputeID, arbitrable); + emit KlerosCore.AppealDecision(disputeID, arbitrable); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.evidence); + emit KlerosCore.NewPeriod(disputeID, KlerosCore.Period.evidence); disputeKit.fundAppeal{value: 0.42 ether}(disputeID, 2); assertEq((disputeKit.getFundedChoices(disputeID)).length, 0, "No funded choices in the fresh round"); @@ -194,17 +194,17 @@ contract KlerosCore_AppealsTest is KlerosCore_TestBase { assertEq(sortitionModule.disputesWithoutJurors(), 1, "Wrong disputesWithoutJurors count after appeal"); assertEq(core.getNumberOfRounds(disputeID), 2, "Wrong number of rounds"); - (, , KlerosCoreBase.Period period, , uint256 lastPeriodChange) = core.disputes(disputeID); - assertEq(uint256(period), uint256(KlerosCoreBase.Period.evidence), "Wrong period"); + (, , KlerosCore.Period period, , uint256 lastPeriodChange) = core.disputes(disputeID); + assertEq(uint256(period), uint256(KlerosCore.Period.evidence), "Wrong period"); assertEq(lastPeriodChange, block.timestamp, "Wrong lastPeriodChange"); - KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 1); // Check the new round + KlerosCore.Round memory round = core.getRoundInfo(disputeID, 1); // Check the new round assertEq(round.pnkAtStakePerJuror, 1000, "Wrong pnkAtStakePerJuror"); assertEq(round.totalFeesForJurors, 0.21 ether, "Wrong totalFeesForJurors"); assertEq(round.nbVotes, 7, "Wrong nbVotes"); core.draw(disputeID, 7); - emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.vote); // Check that we don't have to wait for the timeout to pass the evidence period after appeal + emit KlerosCore.NewPeriod(disputeID, KlerosCore.Period.vote); // Check that we don't have to wait for the timeout to pass the evidence period after appeal core.passPeriod(disputeID); } @@ -263,7 +263,7 @@ contract KlerosCore_AppealsTest is KlerosCore_TestBase { vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase - KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 0); + KlerosCore.Round memory round = core.getRoundInfo(disputeID, 0); assertEq(round.disputeKitID, newDkID, "Wrong DK ID"); core.draw(disputeID, DEFAULT_NB_OF_JURORS); @@ -286,15 +286,15 @@ contract KlerosCore_AppealsTest is KlerosCore_TestBase { assertEq(core.isDisputeKitJumping(disputeID), true, "Should be jumping"); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.CourtJump(disputeID, 1, newCourtID, GENERAL_COURT); + emit KlerosCore.CourtJump(disputeID, 1, newCourtID, GENERAL_COURT); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.DisputeKitJump(disputeID, 1, newDkID, DISPUTE_KIT_CLASSIC); + emit KlerosCore.DisputeKitJump(disputeID, 1, newDkID, DISPUTE_KIT_CLASSIC); vm.expectEmit(true, true, true, true); emit DisputeKitClassicBase.DisputeCreation(disputeID, 2, newExtraData); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.AppealDecision(disputeID, arbitrable); + emit KlerosCore.AppealDecision(disputeID, arbitrable); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.evidence); + emit KlerosCore.NewPeriod(disputeID, KlerosCore.Period.evidence); vm.prank(crowdfunder2); newDisputeKit.fundAppeal{value: 0.42 ether}(disputeID, 2); @@ -322,7 +322,7 @@ contract KlerosCore_AppealsTest is KlerosCore_TestBase { // And check that draw in the new round works vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.Draw(staker1, disputeID, 1, 0); // roundID = 1 VoteID = 0 + emit KlerosCore.Draw(staker1, disputeID, 1, 0); // roundID = 1 VoteID = 0 core.draw(disputeID, 1); (address account, , , ) = disputeKit.getVoteInfo(disputeID, 1, 0); @@ -401,7 +401,7 @@ contract KlerosCore_AppealsTest is KlerosCore_TestBase { vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase - KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 0); + KlerosCore.Round memory round = core.getRoundInfo(disputeID, 0); assertEq(round.disputeKitID, dkID3, "Wrong DK ID"); core.draw(disputeID, DEFAULT_NB_OF_JURORS); @@ -424,15 +424,15 @@ contract KlerosCore_AppealsTest is KlerosCore_TestBase { assertEq(core.isDisputeKitJumping(disputeID), true, "Should be jumping"); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.CourtJump(disputeID, 1, newCourtID, GENERAL_COURT); + emit KlerosCore.CourtJump(disputeID, 1, newCourtID, GENERAL_COURT); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.DisputeKitJump(disputeID, 1, dkID3, dkID2); + emit KlerosCore.DisputeKitJump(disputeID, 1, dkID3, dkID2); vm.expectEmit(true, true, true, true); emit DisputeKitClassicBase.DisputeCreation(disputeID, 2, newExtraData); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.AppealDecision(disputeID, arbitrable); + emit KlerosCore.AppealDecision(disputeID, arbitrable); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.evidence); + emit KlerosCore.NewPeriod(disputeID, KlerosCore.Period.evidence); vm.prank(crowdfunder2); disputeKit3.fundAppeal{value: 0.42 ether}(disputeID, 2); @@ -460,7 +460,7 @@ contract KlerosCore_AppealsTest is KlerosCore_TestBase { // And check that draw in the new round works vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.Draw(staker1, disputeID, 1, 0); // roundID = 1 VoteID = 0 + emit KlerosCore.Draw(staker1, disputeID, 1, 0); // roundID = 1 VoteID = 0 core.draw(disputeID, 1); (address account, , , ) = disputeKit2.getVoteInfo(disputeID, 1, 0); @@ -497,7 +497,7 @@ contract KlerosCore_AppealsTest is KlerosCore_TestBase { // Should pass to execution period without waiting for the 2nd half of the appeal. vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.execution); + emit KlerosCore.NewPeriod(disputeID, KlerosCore.Period.execution); core.passPeriod(disputeID); } } diff --git a/contracts/test/foundry/KlerosCore_Disputes.t.sol b/contracts/test/foundry/KlerosCore_Disputes.t.sol index 954789f92..f9840014d 100644 --- a/contracts/test/foundry/KlerosCore_Disputes.t.sol +++ b/contracts/test/foundry/KlerosCore_Disputes.t.sol @@ -2,8 +2,8 @@ pragma solidity ^0.8.24; import {KlerosCore_TestBase} from "./KlerosCore_TestBase.sol"; -import {KlerosCoreBase} from "../../src/arbitration/KlerosCoreBase.sol"; -import {IArbitratorV2} from "../../src/arbitration/KlerosCoreBase.sol"; +import {KlerosCore} from "../../src/arbitration/KlerosCore.sol"; +import {IArbitratorV2} from "../../src/arbitration/KlerosCore.sol"; import {DisputeKitClassicBase} from "../../src/arbitration/dispute-kits/DisputeKitClassicBase.sol"; import {IArbitrableV2} from "../../src/arbitration/arbitrables/ArbitrableExample.sol"; import "../../src/libraries/Constants.sol"; @@ -38,11 +38,11 @@ contract KlerosCore_DisputesTest is KlerosCore_TestBase { arbitrable.changeArbitratorExtraData(newExtraData); - vm.expectRevert(KlerosCoreBase.ArbitrationFeesNotEnough.selector); + vm.expectRevert(KlerosCore.ArbitrationFeesNotEnough.selector); vm.prank(disputer); arbitrable.createDispute{value: newFee * newNbJurors - 1}("Action"); - vm.expectRevert(KlerosCoreBase.DisputeKitNotSupportedByCourt.selector); + vm.expectRevert(KlerosCore.DisputeKitNotSupportedByCourt.selector); vm.prank(disputer); arbitrable.createDispute{value: 0.04 ether}("Action"); @@ -64,18 +64,18 @@ contract KlerosCore_DisputesTest is KlerosCore_TestBase { ( uint96 courtID, IArbitrableV2 arbitrated, - KlerosCoreBase.Period period, + KlerosCore.Period period, bool ruled, uint256 lastPeriodChange ) = core.disputes(disputeID); assertEq(courtID, newCourtID, "Wrong court ID"); assertEq(address(arbitrated), address(arbitrable), "Wrong arbitrable"); - assertEq(uint256(period), uint256(KlerosCoreBase.Period.evidence), "Wrong period"); + assertEq(uint256(period), uint256(KlerosCore.Period.evidence), "Wrong period"); assertEq(ruled, false, "Should not be ruled"); assertEq(lastPeriodChange, block.timestamp, "Wrong lastPeriodChange"); - KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 0); + KlerosCore.Round memory round = core.getRoundInfo(disputeID, 0); assertEq(round.disputeKitID, newDkID, "Wrong DK ID"); assertEq(round.pnkAtStakePerJuror, 4000, "Wrong pnkAtStakePerJuror"); // minStake * alpha / divisor = 2000 * 20000/10000 assertEq(round.totalFeesForJurors, 0.04 ether, "Wrong totalFeesForJurors"); @@ -116,7 +116,7 @@ contract KlerosCore_DisputesTest is KlerosCore_TestBase { vm.prank(disputer); feeToken.approve(address(arbitrable), 1 ether); - vm.expectRevert(KlerosCoreBase.TokenNotAccepted.selector); + vm.expectRevert(KlerosCore.TokenNotAccepted.selector); vm.prank(disputer); arbitrable.createDispute("Action", 0.18 ether); @@ -125,11 +125,11 @@ contract KlerosCore_DisputesTest is KlerosCore_TestBase { vm.prank(owner); core.changeCurrencyRates(feeToken, 500, 3); - vm.expectRevert(KlerosCoreBase.ArbitrationFeesNotEnough.selector); + vm.expectRevert(KlerosCore.ArbitrationFeesNotEnough.selector); vm.prank(disputer); arbitrable.createDispute("Action", 0.18 ether - 1); - vm.expectRevert(KlerosCoreBase.TransferFailed.selector); + vm.expectRevert(KlerosCore.TransferFailed.selector); vm.prank(address(arbitrable)); // Bypass createDispute in arbitrable to avoid transfer checks there and make the arbitrable call KC directly core.createDispute(2, arbitratorExtraData, feeToken, 0.18 ether); @@ -137,7 +137,7 @@ contract KlerosCore_DisputesTest is KlerosCore_TestBase { vm.prank(disputer); arbitrable.createDispute("Action", 0.18 ether); - KlerosCoreBase.Round memory round = core.getRoundInfo(0, 0); + KlerosCore.Round memory round = core.getRoundInfo(0, 0); assertEq(round.totalFeesForJurors, 0.18 ether, "Wrong totalFeesForJurors"); assertEq(round.nbVotes, 3, "Wrong nbVotes"); assertEq(address(round.feeToken), address(feeToken), "Wrong feeToken"); diff --git a/contracts/test/foundry/KlerosCore_Drawing.t.sol b/contracts/test/foundry/KlerosCore_Drawing.t.sol index d6ffc9ea9..b8c644392 100644 --- a/contracts/test/foundry/KlerosCore_Drawing.t.sol +++ b/contracts/test/foundry/KlerosCore_Drawing.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.24; import {KlerosCore_TestBase} from "./KlerosCore_TestBase.sol"; -import {KlerosCoreBase} from "../../src/arbitration/KlerosCoreBase.sol"; +import {KlerosCore} from "../../src/arbitration/KlerosCore.sol"; import {SortitionModuleBase} from "../../src/arbitration/SortitionModuleBase.sol"; import {ISortitionModule} from "../../src/arbitration/interfaces/ISortitionModule.sol"; import "../../src/libraries/Constants.sol"; @@ -26,7 +26,7 @@ contract KlerosCore_DrawingTest is KlerosCore_TestBase { vm.expectEmit(true, true, true, true); emit SortitionModuleBase.StakeLocked(staker1, 1000, false); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.Draw(staker1, disputeID, roundID, 0); // VoteID = 0 + emit KlerosCore.Draw(staker1, disputeID, roundID, 0); // VoteID = 0 core.draw(disputeID, DEFAULT_NB_OF_JURORS); // Do 3 iterations and see that the juror will get drawn 3 times despite low stake. @@ -61,7 +61,7 @@ contract KlerosCore_DrawingTest is KlerosCore_TestBase { core.draw(disputeID, DEFAULT_NB_OF_JURORS); // No one is staked so check that the empty addresses are not drawn. - KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, roundID); + KlerosCore.Round memory round = core.getRoundInfo(disputeID, roundID); assertEq(round.drawIterations, 3, "Wrong drawIterations number"); (, , , , uint256 nbVoters, ) = disputeKit.getRoundInfo(disputeID, roundID, 0); @@ -106,16 +106,16 @@ contract KlerosCore_DrawingTest is KlerosCore_TestBase { assertEq(courtID, GENERAL_COURT, "Wrong court ID of the dispute"); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.Draw(staker1, disputeID, roundID, 0); + emit KlerosCore.Draw(staker1, disputeID, roundID, 0); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.Draw(staker1, disputeID, roundID, 1); + emit KlerosCore.Draw(staker1, disputeID, roundID, 1); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.Draw(staker1, disputeID, roundID, 2); + emit KlerosCore.Draw(staker1, disputeID, roundID, 2); core.draw(disputeID, DEFAULT_NB_OF_JURORS); assertEq(sortitionModule.disputesWithoutJurors(), 0, "Wrong disputesWithoutJurors count"); - KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, roundID); + KlerosCore.Round memory round = core.getRoundInfo(disputeID, roundID); assertEq(round.drawIterations, 3, "Wrong drawIterations number"); (, , , , uint256 nbVoters, ) = disputeKit.getRoundInfo(disputeID, roundID, 0); diff --git a/contracts/test/foundry/KlerosCore_Execution.t.sol b/contracts/test/foundry/KlerosCore_Execution.t.sol index c5763964e..33b2cda65 100644 --- a/contracts/test/foundry/KlerosCore_Execution.t.sol +++ b/contracts/test/foundry/KlerosCore_Execution.t.sol @@ -2,10 +2,10 @@ pragma solidity ^0.8.24; import {KlerosCore_TestBase} from "./KlerosCore_TestBase.sol"; -import {KlerosCoreBase} from "../../src/arbitration/KlerosCoreBase.sol"; +import {KlerosCore} from "../../src/arbitration/KlerosCore.sol"; import {SortitionModuleBase} from "../../src/arbitration/SortitionModuleBase.sol"; import {DisputeKitClassicBase} from "../../src/arbitration/dispute-kits/DisputeKitClassicBase.sol"; -import {IArbitratorV2, IArbitrableV2} from "../../src/arbitration/KlerosCoreBase.sol"; +import {IArbitratorV2, IArbitrableV2} from "../../src/arbitration/KlerosCore.sol"; import {IERC20} from "../../src/libraries/SafeERC20.sol"; import "../../src/libraries/Constants.sol"; @@ -53,7 +53,7 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); core.passPeriod(disputeID); // Appeal - vm.expectRevert(KlerosCoreBase.NotExecutionPeriod.selector); + vm.expectRevert(KlerosCore.NotExecutionPeriod.selector); core.execute(disputeID, 0, 1); vm.warp(block.timestamp + timesPerPeriod[3]); @@ -61,7 +61,7 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { vm.prank(owner); core.pause(); - vm.expectRevert(KlerosCoreBase.WhenNotPausedOnly.selector); + vm.expectRevert(KlerosCore.WhenNotPausedOnly.selector); core.execute(disputeID, 0, 1); vm.prank(owner); core.unpause(); @@ -98,16 +98,16 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { vm.expectEmit(true, true, true, true); emit SortitionModuleBase.StakeLocked(staker1, 1000, true); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.TokenAndETHShift(staker1, disputeID, 0, 0, -int256(1000), 0, IERC20(address(0))); + emit KlerosCore.TokenAndETHShift(staker1, disputeID, 0, 0, -int256(1000), 0, IERC20(address(0))); // Check iterations for the winning staker to see the shifts vm.expectEmit(true, true, true, true); emit SortitionModuleBase.StakeLocked(staker2, 0, true); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.TokenAndETHShift(staker2, disputeID, 0, 10000, 0, 0, IERC20(address(0))); + emit KlerosCore.TokenAndETHShift(staker2, disputeID, 0, 10000, 0, 0, IERC20(address(0))); vm.expectEmit(true, true, true, true); emit SortitionModuleBase.StakeLocked(staker2, 0, true); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.TokenAndETHShift(staker2, disputeID, 0, 10000, 0, 0, IERC20(address(0))); + emit KlerosCore.TokenAndETHShift(staker2, disputeID, 0, 10000, 0, 0, IERC20(address(0))); core.execute(disputeID, 0, 3); // Do 3 iterations to check penalties first (uint256 totalStaked, uint256 totalLocked, , ) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); @@ -116,23 +116,23 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { (, totalLocked, , ) = sortitionModule.getJurorBalance(staker2, GENERAL_COURT); assertEq(totalLocked, 2000, "Tokens should still be locked for staker2"); - KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 0); + KlerosCore.Round memory round = core.getRoundInfo(disputeID, 0); assertEq(round.repartitions, 3, "Wrong repartitions"); assertEq(round.pnkPenalties, 1000, "Wrong pnkPenalties"); vm.expectEmit(true, true, true, true); emit SortitionModuleBase.StakeLocked(staker1, 0, true); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.TokenAndETHShift(staker1, disputeID, 0, 0, 0, 0, IERC20(address(0))); + emit KlerosCore.TokenAndETHShift(staker1, disputeID, 0, 0, 0, 0, IERC20(address(0))); // Check iterations for the winning staker to see the shifts vm.expectEmit(true, true, true, true); emit SortitionModuleBase.StakeLocked(staker2, 1000, true); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.TokenAndETHShift(staker2, disputeID, 0, 10000, 500, 0.045 ether, IERC20(address(0))); + emit KlerosCore.TokenAndETHShift(staker2, disputeID, 0, 10000, 500, 0.045 ether, IERC20(address(0))); vm.expectEmit(true, true, true, true); emit SortitionModuleBase.StakeLocked(staker2, 1000, true); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.TokenAndETHShift(staker2, disputeID, 0, 10000, 500, 0.045 ether, IERC20(address(0))); + emit KlerosCore.TokenAndETHShift(staker2, disputeID, 0, 10000, 500, 0.045 ether, IERC20(address(0))); core.execute(disputeID, 0, 10); // Finish the iterations. We need only 3 but check that it corrects the count. (, totalLocked, , ) = sortitionModule.getJurorBalance(staker2, GENERAL_COURT); @@ -201,10 +201,10 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { uint256 ownerTokenBalance = pinakion.balanceOf(owner); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.LeftoverRewardSent(disputeID, 0, 3000, 0.09 ether, IERC20(address(0))); + emit KlerosCore.LeftoverRewardSent(disputeID, 0, 3000, 0.09 ether, IERC20(address(0))); core.execute(disputeID, 0, 3); - KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 0); + KlerosCore.Round memory round = core.getRoundInfo(disputeID, 0); assertEq(round.pnkPenalties, 3000, "Wrong pnkPenalties"); assertEq(round.sumFeeRewardPaid, 0, "Wrong sumFeeRewardPaid"); assertEq(round.sumPnkRewardPaid, 0, "Wrong sumPnkRewardPaid"); @@ -471,7 +471,7 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { assertEq(totalStaked, 1000, "Wrong amount staked"); assertEq(totalLocked, 0, "Should be fully unlocked"); - KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 0); + KlerosCore.Round memory round = core.getRoundInfo(disputeID, 0); assertEq(round.pnkPenalties, 0, "Wrong pnkPenalties"); assertEq(round.sumFeeRewardPaid, 0.09 ether, "Wrong sumFeeRewardPaid"); assertEq(round.sumPnkRewardPaid, 0, "Wrong sumPnkRewardPaid"); // No penalty so no rewards in pnk @@ -480,7 +480,7 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { assertEq(pinakion.balanceOf(address(core)), 1000, "Wrong token balance of the core"); assertEq(pinakion.balanceOf(staker1), 999999999999999000, "Wrong token balance of staker1"); - vm.expectRevert(KlerosCoreBase.SortitionModuleOnly.selector); + vm.expectRevert(KlerosCore.SortitionModuleOnly.selector); vm.prank(owner); core.transferBySortitionModule(staker1, 1000); @@ -537,12 +537,12 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { // Check only once per penalty and per reward vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.TokenAndETHShift(staker1, disputeID, 0, 10000, 0, 0, feeToken); + emit KlerosCore.TokenAndETHShift(staker1, disputeID, 0, 10000, 0, 0, feeToken); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.TokenAndETHShift(staker1, disputeID, 0, 10000, 0, 0.06 ether, feeToken); + emit KlerosCore.TokenAndETHShift(staker1, disputeID, 0, 10000, 0, 0.06 ether, feeToken); core.execute(disputeID, 0, 6); - KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 0); + KlerosCore.Round memory round = core.getRoundInfo(disputeID, 0); assertEq(round.sumFeeRewardPaid, 0.18 ether, "Wrong sumFeeRewardPaid"); assertEq(feeToken.balanceOf(address(core)), 0, "Wrong fee token balance of the core"); @@ -584,10 +584,10 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { core.passPeriod(disputeID); // Execution vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.LeftoverRewardSent(disputeID, 0, 3000, 0.18 ether, feeToken); + emit KlerosCore.LeftoverRewardSent(disputeID, 0, 3000, 0.18 ether, feeToken); core.execute(disputeID, 0, 10); // Put more iterations to check that they're capped - KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 0); + KlerosCore.Round memory round = core.getRoundInfo(disputeID, 0); assertEq(round.pnkPenalties, 3000, "Wrong pnkPenalties"); assertEq(round.sumFeeRewardPaid, 0, "Wrong sumFeeRewardPaid"); assertEq(round.sumPnkRewardPaid, 0, "Wrong sumPnkRewardPaid"); @@ -624,19 +624,19 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); core.passPeriod(disputeID); // Appeal - vm.expectRevert(KlerosCoreBase.NotExecutionPeriod.selector); + vm.expectRevert(KlerosCore.NotExecutionPeriod.selector); core.executeRuling(disputeID); vm.warp(block.timestamp + timesPerPeriod[3]); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.execution); + emit KlerosCore.NewPeriod(disputeID, KlerosCore.Period.execution); core.passPeriod(disputeID); // Execution - (, , KlerosCoreBase.Period period, , uint256 lastPeriodChange) = core.disputes(disputeID); - assertEq(uint256(period), uint256(KlerosCoreBase.Period.execution), "Wrong period"); + (, , KlerosCore.Period period, , uint256 lastPeriodChange) = core.disputes(disputeID); + assertEq(uint256(period), uint256(KlerosCore.Period.execution), "Wrong period"); assertEq(lastPeriodChange, block.timestamp, "Wrong lastPeriodChange"); - vm.expectRevert(KlerosCoreBase.DisputePeriodIsFinal.selector); + vm.expectRevert(KlerosCore.DisputePeriodIsFinal.selector); core.passPeriod(disputeID); vm.expectEmit(true, true, true, true); @@ -645,7 +645,7 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { emit IArbitrableV2.Ruling(core, disputeID, 2); core.executeRuling(disputeID); - vm.expectRevert(KlerosCoreBase.RulingAlreadyExecuted.selector); + vm.expectRevert(KlerosCore.RulingAlreadyExecuted.selector); core.executeRuling(disputeID); (, , , bool ruled, ) = core.disputes(disputeID); diff --git a/contracts/test/foundry/KlerosCore_Governance.t.sol b/contracts/test/foundry/KlerosCore_Governance.t.sol index f34d64e79..53d438c4d 100644 --- a/contracts/test/foundry/KlerosCore_Governance.t.sol +++ b/contracts/test/foundry/KlerosCore_Governance.t.sol @@ -2,8 +2,8 @@ pragma solidity ^0.8.24; import {KlerosCore_TestBase} from "./KlerosCore_TestBase.sol"; -import {KlerosCoreBase} from "../../src/arbitration/KlerosCoreBase.sol"; -import {IArbitratorV2} from "../../src/arbitration/KlerosCoreBase.sol"; +import {KlerosCore} from "../../src/arbitration/KlerosCore.sol"; +import {IArbitratorV2} from "../../src/arbitration/KlerosCore.sol"; import {DisputeKitSybilResistant} from "../../src/arbitration/dispute-kits/DisputeKitSybilResistant.sol"; import {SortitionModuleMock} from "../../src/test/SortitionModuleMock.sol"; import {PNK} from "../../src/token/PNK.sol"; @@ -13,27 +13,27 @@ import "../../src/libraries/Constants.sol"; /// @dev Tests for KlerosCore governance functions (owner/guardian operations) contract KlerosCore_GovernanceTest is KlerosCore_TestBase { function test_pause() public { - vm.expectRevert(KlerosCoreBase.GuardianOrOwnerOnly.selector); + vm.expectRevert(KlerosCore.GuardianOrOwnerOnly.selector); vm.prank(other); core.pause(); // Note that we must explicitly switch to the owner/guardian address to make the call, otherwise Foundry treats UUPS proxy as msg.sender. vm.prank(guardian); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.Paused(); + emit KlerosCore.Paused(); core.pause(); assertEq(core.paused(), true, "Wrong paused value"); // Switch between owner and guardian to test both. WhenNotPausedOnly modifier is triggered after owner's check. vm.prank(owner); - vm.expectRevert(KlerosCoreBase.WhenNotPausedOnly.selector); + vm.expectRevert(KlerosCore.WhenNotPausedOnly.selector); core.pause(); } function test_unpause() public { - vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); + vm.expectRevert(KlerosCore.OwnerOnly.selector); vm.prank(other); core.unpause(); - vm.expectRevert(KlerosCoreBase.WhenPausedOnly.selector); + vm.expectRevert(KlerosCore.WhenPausedOnly.selector); vm.prank(owner); core.unpause(); @@ -41,18 +41,18 @@ contract KlerosCore_GovernanceTest is KlerosCore_TestBase { core.pause(); vm.prank(owner); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.Unpaused(); + emit KlerosCore.Unpaused(); core.unpause(); assertEq(core.paused(), false, "Wrong paused value"); } function test_executeOwnerProposal() public { bytes memory data = abi.encodeWithSignature("changeOwner(address)", other); - vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); + vm.expectRevert(KlerosCore.OwnerOnly.selector); vm.prank(other); core.executeOwnerProposal(address(core), 0, data); - vm.expectRevert(KlerosCoreBase.UnsuccessfulCall.selector); + vm.expectRevert(KlerosCore.UnsuccessfulCall.selector); vm.prank(owner); core.executeOwnerProposal(address(core), 0, data); // It'll fail because the core is not its own owner @@ -64,7 +64,7 @@ contract KlerosCore_GovernanceTest is KlerosCore_TestBase { } function test_changeOwner() public { - vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); + vm.expectRevert(KlerosCore.OwnerOnly.selector); vm.prank(other); core.changeOwner(payable(other)); vm.prank(owner); @@ -73,7 +73,7 @@ contract KlerosCore_GovernanceTest is KlerosCore_TestBase { } function test_changeGuardian() public { - vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); + vm.expectRevert(KlerosCore.OwnerOnly.selector); vm.prank(other); core.changeGuardian(other); vm.prank(owner); @@ -83,7 +83,7 @@ contract KlerosCore_GovernanceTest is KlerosCore_TestBase { function test_changePinakion() public { PNK fakePNK = new PNK(); - vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); + vm.expectRevert(KlerosCore.OwnerOnly.selector); vm.prank(other); core.changePinakion(fakePNK); vm.prank(owner); @@ -92,7 +92,7 @@ contract KlerosCore_GovernanceTest is KlerosCore_TestBase { } function test_changeJurorProsecutionModule() public { - vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); + vm.expectRevert(KlerosCore.OwnerOnly.selector); vm.prank(other); core.changeJurorProsecutionModule(other); vm.prank(owner); @@ -102,7 +102,7 @@ contract KlerosCore_GovernanceTest is KlerosCore_TestBase { function test_changeSortitionModule() public { SortitionModuleMock fakeSM = new SortitionModuleMock(); - vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); + vm.expectRevert(KlerosCore.OwnerOnly.selector); vm.prank(other); core.changeSortitionModule(fakeSM); vm.prank(owner); @@ -112,19 +112,19 @@ contract KlerosCore_GovernanceTest is KlerosCore_TestBase { function test_addNewDisputeKit() public { DisputeKitSybilResistant newDK = new DisputeKitSybilResistant(); - vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); + vm.expectRevert(KlerosCore.OwnerOnly.selector); vm.prank(other); core.addNewDisputeKit(newDK); vm.prank(owner); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.DisputeKitCreated(2, newDK); + emit KlerosCore.DisputeKitCreated(2, newDK); core.addNewDisputeKit(newDK); assertEq(address(core.disputeKits(2)), address(newDK), "Wrong address of new DK"); assertEq(core.getDisputeKitsLength(), 3, "Wrong DK array length"); } function test_createCourt() public { - vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); + vm.expectRevert(KlerosCore.OwnerOnly.selector); vm.prank(other); uint256[] memory supportedDK = new uint256[](2); supportedDK[0] = DISPUTE_KIT_CLASSIC; @@ -141,7 +141,7 @@ contract KlerosCore_GovernanceTest is KlerosCore_TestBase { supportedDK ); - vm.expectRevert(KlerosCoreBase.MinStakeLowerThanParentCourt.selector); + vm.expectRevert(KlerosCore.MinStakeLowerThanParentCourt.selector); vm.prank(owner); core.createCourt( GENERAL_COURT, @@ -155,7 +155,7 @@ contract KlerosCore_GovernanceTest is KlerosCore_TestBase { supportedDK ); - vm.expectRevert(KlerosCoreBase.UnsupportedDisputeKit.selector); + vm.expectRevert(KlerosCore.UnsupportedDisputeKit.selector); vm.prank(owner); uint256[] memory emptySupportedDK = new uint256[](0); core.createCourt( @@ -170,7 +170,7 @@ contract KlerosCore_GovernanceTest is KlerosCore_TestBase { emptySupportedDK ); - vm.expectRevert(KlerosCoreBase.InvalidForkingCourtAsParent.selector); + vm.expectRevert(KlerosCore.InvalidForkingCourtAsParent.selector); vm.prank(owner); core.createCourt( FORKING_COURT, @@ -187,7 +187,7 @@ contract KlerosCore_GovernanceTest is KlerosCore_TestBase { uint256[] memory badSupportedDK = new uint256[](2); badSupportedDK[0] = NULL_DISPUTE_KIT; // Include NULL_DK to check that it reverts badSupportedDK[1] = DISPUTE_KIT_CLASSIC; - vm.expectRevert(KlerosCoreBase.WrongDisputeKitIndex.selector); + vm.expectRevert(KlerosCore.WrongDisputeKitIndex.selector); vm.prank(owner); core.createCourt( GENERAL_COURT, @@ -203,7 +203,7 @@ contract KlerosCore_GovernanceTest is KlerosCore_TestBase { badSupportedDK[0] = DISPUTE_KIT_CLASSIC; badSupportedDK[1] = 2; // Check out of bounds index - vm.expectRevert(KlerosCoreBase.WrongDisputeKitIndex.selector); + vm.expectRevert(KlerosCore.WrongDisputeKitIndex.selector); vm.prank(owner); core.createCourt( GENERAL_COURT, @@ -223,7 +223,7 @@ contract KlerosCore_GovernanceTest is KlerosCore_TestBase { core.addNewDisputeKit(newDK); badSupportedDK = new uint256[](1); badSupportedDK[0] = 2; // Include only sybil resistant dk - vm.expectRevert(KlerosCoreBase.MustSupportDisputeKitClassic.selector); + vm.expectRevert(KlerosCore.MustSupportDisputeKitClassic.selector); vm.prank(owner); core.createCourt( GENERAL_COURT, @@ -239,11 +239,11 @@ contract KlerosCore_GovernanceTest is KlerosCore_TestBase { vm.prank(owner); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.DisputeKitEnabled(2, DISPUTE_KIT_CLASSIC, true); + emit KlerosCore.DisputeKitEnabled(2, DISPUTE_KIT_CLASSIC, true); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.DisputeKitEnabled(2, 2, true); + emit KlerosCore.DisputeKitEnabled(2, 2, true); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.CourtCreated( + emit KlerosCore.CourtCreated( 2, GENERAL_COURT, true, @@ -299,7 +299,7 @@ contract KlerosCore_GovernanceTest is KlerosCore_TestBase { supportedDK ); - vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); + vm.expectRevert(KlerosCore.OwnerOnly.selector); vm.prank(other); core.changeCourtParameters( GENERAL_COURT, @@ -310,7 +310,7 @@ contract KlerosCore_GovernanceTest is KlerosCore_TestBase { 50, // jurors for jump [uint256(10), uint256(20), uint256(30), uint256(40)] // Times per period ); - vm.expectRevert(KlerosCoreBase.MinStakeLowerThanParentCourt.selector); + vm.expectRevert(KlerosCore.MinStakeLowerThanParentCourt.selector); vm.prank(owner); // Min stake of a parent became higher than of a child core.changeCourtParameters( @@ -323,7 +323,7 @@ contract KlerosCore_GovernanceTest is KlerosCore_TestBase { [uint256(10), uint256(20), uint256(30), uint256(40)] // Times per period ); // Min stake of a child became lower than of a parent - vm.expectRevert(KlerosCoreBase.MinStakeLowerThanParentCourt.selector); + vm.expectRevert(KlerosCore.MinStakeLowerThanParentCourt.selector); vm.prank(owner); core.changeCourtParameters( newCourtID, @@ -337,7 +337,7 @@ contract KlerosCore_GovernanceTest is KlerosCore_TestBase { vm.prank(owner); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.CourtModified( + emit KlerosCore.CourtModified( GENERAL_COURT, true, 2000, @@ -366,43 +366,43 @@ contract KlerosCore_GovernanceTest is KlerosCore_TestBase { vm.prank(owner); core.addNewDisputeKit(newDK); - vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); + vm.expectRevert(KlerosCore.OwnerOnly.selector); vm.prank(other); uint256[] memory supportedDK = new uint256[](1); supportedDK[0] = newDkID; core.enableDisputeKits(GENERAL_COURT, supportedDK, true); - vm.expectRevert(KlerosCoreBase.WrongDisputeKitIndex.selector); + vm.expectRevert(KlerosCore.WrongDisputeKitIndex.selector); vm.prank(owner); supportedDK[0] = NULL_DISPUTE_KIT; core.enableDisputeKits(GENERAL_COURT, supportedDK, true); - vm.expectRevert(KlerosCoreBase.WrongDisputeKitIndex.selector); + vm.expectRevert(KlerosCore.WrongDisputeKitIndex.selector); vm.prank(owner); supportedDK[0] = 3; // Out of bounds core.enableDisputeKits(GENERAL_COURT, supportedDK, true); - vm.expectRevert(KlerosCoreBase.CannotDisableClassicDK.selector); + vm.expectRevert(KlerosCore.CannotDisableClassicDK.selector); vm.prank(owner); supportedDK[0] = DISPUTE_KIT_CLASSIC; core.enableDisputeKits(GENERAL_COURT, supportedDK, false); vm.prank(owner); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.DisputeKitEnabled(GENERAL_COURT, newDkID, true); + emit KlerosCore.DisputeKitEnabled(GENERAL_COURT, newDkID, true); supportedDK[0] = newDkID; core.enableDisputeKits(GENERAL_COURT, supportedDK, true); assertEq(core.isSupported(GENERAL_COURT, newDkID), true, "New DK should be supported by General court"); vm.prank(owner); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.DisputeKitEnabled(GENERAL_COURT, newDkID, false); + emit KlerosCore.DisputeKitEnabled(GENERAL_COURT, newDkID, false); core.enableDisputeKits(GENERAL_COURT, supportedDK, false); assertEq(core.isSupported(GENERAL_COURT, newDkID), false, "New DK should be disabled in General court"); } function test_changeAcceptedFeeTokens() public { - vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); + vm.expectRevert(KlerosCore.OwnerOnly.selector); vm.prank(other); core.changeAcceptedFeeTokens(feeToken, true); @@ -418,7 +418,7 @@ contract KlerosCore_GovernanceTest is KlerosCore_TestBase { } function test_changeCurrencyRates() public { - vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); + vm.expectRevert(KlerosCore.OwnerOnly.selector); vm.prank(other); core.changeCurrencyRates(feeToken, 100, 200); diff --git a/contracts/test/foundry/KlerosCore_Initialization.t.sol b/contracts/test/foundry/KlerosCore_Initialization.t.sol index d35190870..d9113bbb7 100644 --- a/contracts/test/foundry/KlerosCore_Initialization.t.sol +++ b/contracts/test/foundry/KlerosCore_Initialization.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.24; import {KlerosCore_TestBase} from "./KlerosCore_TestBase.sol"; -import {KlerosCoreBase} from "../../src/arbitration/KlerosCoreBase.sol"; +import {KlerosCore, IERC721} from "../../src/arbitration/KlerosCore.sol"; import {KlerosCoreMock} from "../../src/test/KlerosCoreMock.sol"; import {DisputeKitClassic} from "../../src/arbitration/dispute-kits/DisputeKitClassic.sol"; import {SortitionModuleMock} from "../../src/test/SortitionModuleMock.sol"; @@ -138,12 +138,12 @@ contract KlerosCore_InitializationTest is KlerosCore_TestBase { KlerosCoreMock newCore = KlerosCoreMock(address(proxyCore)); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.DisputeKitCreated(DISPUTE_KIT_CLASSIC, newDisputeKit); + emit KlerosCore.DisputeKitCreated(DISPUTE_KIT_CLASSIC, newDisputeKit); vm.expectEmit(true, true, true, true); uint256[] memory supportedDK = new uint256[](1); supportedDK[0] = DISPUTE_KIT_CLASSIC; - emit KlerosCoreBase.CourtCreated( + emit KlerosCore.CourtCreated( GENERAL_COURT, FORKING_COURT, false, @@ -155,7 +155,7 @@ contract KlerosCore_InitializationTest is KlerosCore_TestBase { supportedDK ); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.DisputeKitEnabled(GENERAL_COURT, DISPUTE_KIT_CLASSIC, true); + emit KlerosCore.DisputeKitEnabled(GENERAL_COURT, DISPUTE_KIT_CLASSIC, true); newCore.initialize( newOwner, newGuardian, @@ -167,7 +167,8 @@ contract KlerosCore_InitializationTest is KlerosCore_TestBase { newTimesPerPeriod, newSortitionExtraData, newSortitionModule, - address(wNative) + address(wNative), + IERC721(address(0)) ); } } diff --git a/contracts/test/foundry/KlerosCore_Staking.t.sol b/contracts/test/foundry/KlerosCore_Staking.t.sol index a315debcf..805b8ae7e 100644 --- a/contracts/test/foundry/KlerosCore_Staking.t.sol +++ b/contracts/test/foundry/KlerosCore_Staking.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.24; import {KlerosCore_TestBase} from "./KlerosCore_TestBase.sol"; -import {KlerosCoreBase} from "../../src/arbitration/KlerosCoreBase.sol"; +import {KlerosCore} from "../../src/arbitration/KlerosCore.sol"; import {SortitionModuleBase} from "../../src/arbitration/SortitionModuleBase.sol"; import {ISortitionModule} from "../../src/arbitration/interfaces/ISortitionModule.sol"; import {IKlerosCore, KlerosCoreSnapshotProxy} from "../../src/arbitration/view/KlerosCoreSnapshotProxy.sol"; @@ -14,26 +14,26 @@ contract KlerosCore_StakingTest is KlerosCore_TestBase { function test_setStake_increase() public { vm.prank(owner); core.pause(); - vm.expectRevert(KlerosCoreBase.WhenNotPausedOnly.selector); + vm.expectRevert(KlerosCore.WhenNotPausedOnly.selector); vm.prank(staker1); core.setStake(GENERAL_COURT, 1000); vm.prank(owner); core.unpause(); - vm.expectRevert(KlerosCoreBase.StakingNotPossibleInThisCourt.selector); + vm.expectRevert(KlerosCore.StakingNotPossibleInThisCourt.selector); vm.prank(staker1); core.setStake(FORKING_COURT, 1000); uint96 badCourtID = 2; - vm.expectRevert(KlerosCoreBase.StakingNotPossibleInThisCourt.selector); + vm.expectRevert(KlerosCore.StakingNotPossibleInThisCourt.selector); vm.prank(staker1); core.setStake(badCourtID, 1000); - vm.expectRevert(KlerosCoreBase.StakingLessThanCourtMinStake.selector); + vm.expectRevert(KlerosCore.StakingLessThanCourtMinStake.selector); vm.prank(staker1); core.setStake(GENERAL_COURT, 800); - vm.expectRevert(KlerosCoreBase.StakingZeroWhenNoStake.selector); + vm.expectRevert(KlerosCore.StakingZeroWhenNoStake.selector); vm.prank(staker1); core.setStake(GENERAL_COURT, 0); @@ -58,7 +58,7 @@ contract KlerosCore_StakingTest is KlerosCore_TestBase { assertEq(pinakion.balanceOf(staker1), 999999999999998999, "Wrong token balance of staker1"); // 1 eth - 1001 wei assertEq(pinakion.allowance(staker1, address(core)), 999999999999998999, "Wrong allowance for staker1"); - vm.expectRevert(KlerosCoreBase.StakingTransferFailed.selector); // This error will be caught because owner didn't approve any tokens for KlerosCore + vm.expectRevert(KlerosCore.StakingTransferFailed.selector); // This error will be caught because owner didn't approve any tokens for KlerosCore vm.prank(owner); core.setStake(GENERAL_COURT, 1000); @@ -111,7 +111,7 @@ contract KlerosCore_StakingTest is KlerosCore_TestBase { vm.prank(address(core)); pinakion.transfer(staker1, 1); // Manually send 1 token to make the withdrawal fail - vm.expectRevert(KlerosCoreBase.UnstakingTransferFailed.selector); + vm.expectRevert(KlerosCore.UnstakingTransferFailed.selector); vm.prank(staker1); core.setStake(GENERAL_COURT, 0); @@ -165,7 +165,7 @@ contract KlerosCore_StakingTest is KlerosCore_TestBase { assertEq(courts.length, 4, "Wrong courts count"); uint96 excessiveCourtID = 5; - vm.expectRevert(KlerosCoreBase.StakingInTooManyCourts.selector); + vm.expectRevert(KlerosCore.StakingInTooManyCourts.selector); vm.prank(staker1); core.setStake(excessiveCourtID, 2000); } @@ -416,7 +416,7 @@ contract KlerosCore_StakingTest is KlerosCore_TestBase { function test_setStakeBySortitionModule() public { // Note that functionality of this function was checked during delayed stakes execution - vm.expectRevert(KlerosCoreBase.SortitionModuleOnly.selector); + vm.expectRevert(KlerosCore.SortitionModuleOnly.selector); vm.prank(owner); core.setStakeBySortitionModule(staker1, GENERAL_COURT, 1000); } diff --git a/contracts/test/foundry/KlerosCore_TestBase.sol b/contracts/test/foundry/KlerosCore_TestBase.sol index 762d71db9..612db5bb7 100644 --- a/contracts/test/foundry/KlerosCore_TestBase.sol +++ b/contracts/test/foundry/KlerosCore_TestBase.sol @@ -3,8 +3,8 @@ pragma solidity ^0.8.24; import {Test} from "forge-std/Test.sol"; import {console} from "forge-std/console.sol"; // Import the console for logging -import {KlerosCoreMock, KlerosCoreBase} from "../../src/test/KlerosCoreMock.sol"; -import {IArbitratorV2} from "../../src/arbitration/KlerosCoreBase.sol"; +import {KlerosCoreMock, KlerosCore, IERC721} from "../../src/test/KlerosCoreMock.sol"; +import {IArbitratorV2} from "../../src/arbitration/KlerosCore.sol"; import {IDisputeKit} from "../../src/arbitration/interfaces/IDisputeKit.sol"; import {DisputeKitClassic, DisputeKitClassicBase} from "../../src/arbitration/dispute-kits/DisputeKitClassic.sol"; import {DisputeKitSybilResistant} from "../../src/arbitration/dispute-kits/DisputeKitSybilResistant.sol"; @@ -151,7 +151,8 @@ abstract contract KlerosCore_TestBase is Test { timesPerPeriod, sortitionExtraData, sortitionModule, - address(wNative) + address(wNative), + IERC721(address(0)) ); vm.prank(staker1); pinakion.approve(address(core), 1 ether); diff --git a/contracts/test/foundry/KlerosCore_Voting.t.sol b/contracts/test/foundry/KlerosCore_Voting.t.sol index 7d84f18e1..504c9c644 100644 --- a/contracts/test/foundry/KlerosCore_Voting.t.sol +++ b/contracts/test/foundry/KlerosCore_Voting.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.24; import {KlerosCore_TestBase} from "./KlerosCore_TestBase.sol"; -import {KlerosCoreBase} from "../../src/arbitration/KlerosCoreBase.sol"; +import {KlerosCore} from "../../src/arbitration/KlerosCore.sol"; import {DisputeKitClassic, DisputeKitClassicBase} from "../../src/arbitration/dispute-kits/DisputeKitClassic.sol"; import {IDisputeKit} from "../../src/arbitration/interfaces/IDisputeKit.sol"; import {UUPSProxy} from "../../src/proxy/UUPSProxy.sol"; @@ -44,17 +44,17 @@ contract KlerosCore_VotingTest is KlerosCore_TestBase { vm.expectRevert(DisputeKitClassicBase.NotCommitPeriod.selector); disputeKit.castCommit(disputeID, voteIDs, commit); - vm.expectRevert(KlerosCoreBase.EvidenceNotPassedAndNotAppeal.selector); + vm.expectRevert(KlerosCore.EvidenceNotPassedAndNotAppeal.selector); core.passPeriod(disputeID); vm.warp(block.timestamp + timesPerPeriod[0]); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.commit); + emit KlerosCore.NewPeriod(disputeID, KlerosCore.Period.commit); core.passPeriod(disputeID); - (, , KlerosCoreBase.Period period, , uint256 lastPeriodChange) = core.disputes(disputeID); + (, , KlerosCore.Period period, , uint256 lastPeriodChange) = core.disputes(disputeID); - assertEq(uint256(period), uint256(KlerosCoreBase.Period.commit), "Wrong period"); + assertEq(uint256(period), uint256(KlerosCore.Period.commit), "Wrong period"); assertEq(lastPeriodChange, block.timestamp, "Wrong lastPeriodChange"); vm.prank(staker1); @@ -149,12 +149,12 @@ contract KlerosCore_VotingTest is KlerosCore_TestBase { vm.warp(block.timestamp + timesPerPeriod[0]); core.passPeriod(disputeID); // Commit - vm.expectRevert(KlerosCoreBase.CommitPeriodNotPassed.selector); + vm.expectRevert(KlerosCore.CommitPeriodNotPassed.selector); core.passPeriod(disputeID); vm.warp(block.timestamp + timesPerPeriod[1]); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.vote); + emit KlerosCore.NewPeriod(disputeID, KlerosCore.Period.vote); core.passPeriod(disputeID); } @@ -178,18 +178,18 @@ contract KlerosCore_VotingTest is KlerosCore_TestBase { vm.expectRevert(DisputeKitClassicBase.NotVotePeriod.selector); disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); // Leave salt empty as not needed - vm.expectRevert(KlerosCoreBase.DisputeStillDrawing.selector); + vm.expectRevert(KlerosCore.DisputeStillDrawing.selector); core.passPeriod(disputeID); core.draw(disputeID, 1); // Draw the last juror vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.vote); + emit KlerosCore.NewPeriod(disputeID, KlerosCore.Period.vote); core.passPeriod(disputeID); // Vote - (, , KlerosCoreBase.Period period, , uint256 lastPeriodChange) = core.disputes(disputeID); + (, , KlerosCore.Period period, , uint256 lastPeriodChange) = core.disputes(disputeID); - assertEq(uint256(period), uint256(KlerosCoreBase.Period.vote), "Wrong period"); + assertEq(uint256(period), uint256(KlerosCore.Period.vote), "Wrong period"); assertEq(lastPeriodChange, block.timestamp, "Wrong lastPeriodChange"); vm.prank(staker1); @@ -250,7 +250,7 @@ contract KlerosCore_VotingTest is KlerosCore_TestBase { assertEq(totalVoted, 2, "totalVoted should be 2"); assertEq(choiceCount, 1, "choiceCount should be 1 for first choice"); - vm.expectRevert(KlerosCoreBase.VotePeriodNotPassed.selector); + vm.expectRevert(KlerosCore.VotePeriodNotPassed.selector); core.passPeriod(disputeID); voteIDs = new uint256[](1); @@ -285,14 +285,14 @@ contract KlerosCore_VotingTest is KlerosCore_TestBase { vm.warp(block.timestamp + timesPerPeriod[0]); core.passPeriod(disputeID); // Votes - vm.expectRevert(KlerosCoreBase.VotePeriodNotPassed.selector); + vm.expectRevert(KlerosCore.VotePeriodNotPassed.selector); core.passPeriod(disputeID); vm.warp(block.timestamp + timesPerPeriod[2]); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.AppealPossible(disputeID, arbitrable); + emit KlerosCore.AppealPossible(disputeID, arbitrable); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.appeal); + emit KlerosCore.NewPeriod(disputeID, KlerosCore.Period.appeal); core.passPeriod(disputeID); } @@ -380,7 +380,7 @@ contract KlerosCore_VotingTest is KlerosCore_TestBase { // Should pass period by counting only committed votes. vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.appeal); + emit KlerosCore.NewPeriod(disputeID, KlerosCore.Period.appeal); core.passPeriod(disputeID); } @@ -407,7 +407,7 @@ contract KlerosCore_VotingTest is KlerosCore_TestBase { vm.prank(owner); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.DisputeKitEnabled(GENERAL_COURT, newDkID, true); + emit KlerosCore.DisputeKitEnabled(GENERAL_COURT, newDkID, true); supportedDK[0] = newDkID; core.enableDisputeKits(GENERAL_COURT, supportedDK, true); assertEq(core.isSupported(GENERAL_COURT, newDkID), true, "New DK should be supported by General court"); @@ -434,7 +434,7 @@ contract KlerosCore_VotingTest is KlerosCore_TestBase { vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase - KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 0); + KlerosCore.Round memory round = core.getRoundInfo(disputeID, 0); assertEq(round.disputeKitID, newDkID, "Wrong DK ID"); core.draw(disputeID, DEFAULT_NB_OF_JURORS); From 40714da2b2122a34b10047f9a639b17fc6b18718 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Thu, 4 Sep 2025 18:55:39 +0100 Subject: [PATCH 096/175] feat: only SortitionModule, no more SortitionModuleBase and SortitionModuleNeo --- contracts/src/arbitration/SortitionModule.sol | 587 +++++++++++++++++- .../src/arbitration/SortitionModuleBase.sol | 572 ----------------- .../src/arbitration/SortitionModuleNeo.sol | 100 --- .../test/foundry/KlerosCore_Drawing.t.sol | 4 +- .../test/foundry/KlerosCore_Execution.t.sol | 30 +- .../foundry/KlerosCore_Initialization.t.sol | 6 +- contracts/test/foundry/KlerosCore_RNG.t.sol | 4 +- .../test/foundry/KlerosCore_Staking.t.sol | 24 +- .../test/foundry/KlerosCore_TestBase.sol | 8 +- 9 files changed, 622 insertions(+), 713 deletions(-) delete mode 100644 contracts/src/arbitration/SortitionModuleBase.sol delete mode 100644 contracts/src/arbitration/SortitionModuleNeo.sol diff --git a/contracts/src/arbitration/SortitionModule.sol b/contracts/src/arbitration/SortitionModule.sol index bd0f6e007..8c0f4de86 100644 --- a/contracts/src/arbitration/SortitionModule.sol +++ b/contracts/src/arbitration/SortitionModule.sol @@ -2,13 +2,98 @@ pragma solidity ^0.8.24; -import {SortitionModuleBase, KlerosCore, IRNG} from "./SortitionModuleBase.sol"; +import {KlerosCore} from "./KlerosCore.sol"; +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 {SortitionTrees, TreeKey, CourtID} from "../libraries/SortitionTrees.sol"; +import {IRNG} from "../rng/IRNG.sol"; +import "../libraries/Constants.sol"; /// @title SortitionModule /// @dev A factory of trees that keeps track of staked values for sortition. -contract SortitionModule is SortitionModuleBase { +contract SortitionModule is ISortitionModule, Initializable, UUPSProxiable { + using SortitionTrees for SortitionTrees.Tree; + using SortitionTrees for mapping(TreeKey key => SortitionTrees.Tree); + string public constant override version = "2.0.0"; + // ************************************* // + // * Enums / Structs * // + // ************************************* // + + struct DelayedStake { + address account; // The address of the juror. + uint96 courtID; // The ID of the court. + uint256 stake; // The new stake. + bool alreadyTransferred; // DEPRECATED. True if tokens were already transferred before delayed stake's execution. + } + + struct Juror { + uint96[] courtIDs; // The IDs of courts where the juror's stake path ends. A stake path is a path from the general court to a court the juror directly staked in using `_setStake`. + uint256 stakedPnk; // The juror's total amount of tokens staked in subcourts. PNK balance including locked PNK and penalty deductions. + uint256 lockedPnk; // The juror's total amount of tokens locked in disputes. + } + + // ************************************* // + // * Storage * // + // ************************************* // + + address public owner; // The owner of the contract. + KlerosCore public core; // The core arbitrator contract. + Phase public phase; // The current phase. + 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(TreeKey key => SortitionTrees.Tree) sortitionSumTrees; // The mapping of sortition trees by keys. + mapping(address account => Juror) public jurors; // The jurors. + mapping(uint256 => DelayedStake) public delayedStakes; // Stores the stakes that were changed during Drawing phase, to update them when the phase is switched to Staking. + mapping(address jurorAccount => mapping(uint96 courtId => uint256)) public latestDelayedStakeIndex; // DEPRECATED. Maps the juror to its latest delayed stake. If there is already a delayed stake for this juror then it'll be replaced. latestDelayedStakeIndex[juror][courtID]. + uint256 public maxStakePerJuror; + uint256 public maxTotalStaked; + uint256 public totalStaked; + + // ************************************* // + // * Events * // + // ************************************* // + + /// @notice Emitted when a juror stakes in a court. + /// @param _address The address of the juror. + /// @param _courtID The ID of the court. + /// @param _amount The amount of tokens staked in the court. + /// @param _amountAllCourts The amount of tokens staked in all courts. + event StakeSet(address indexed _address, uint256 _courtID, uint256 _amount, uint256 _amountAllCourts); + + /// @notice Emitted when a juror's stake is delayed. + /// @param _address The address of the juror. + /// @param _courtID The ID of the court. + /// @param _amount The amount of tokens staked in the court. + event StakeDelayed(address indexed _address, uint96 indexed _courtID, uint256 _amount); + + /// @notice Emitted when a juror's stake is locked. + /// @param _address The address of the juror. + /// @param _relativeAmount The amount of tokens locked. + /// @param _unlock Whether the stake is locked or unlocked. + event StakeLocked(address indexed _address, uint256 _relativeAmount, bool _unlock); + + /// @dev Emitted when leftover PNK is available. + /// @param _account The account of the juror. + /// @param _amount The amount of PNK available. + event LeftoverPNK(address indexed _account, uint256 _amount); + + /// @dev Emitted when leftover PNK is withdrawn. + /// @param _account The account of the juror withdrawing PNK. + /// @param _amount The amount of PNK withdrawn. + event LeftoverPNKWithdrawn(address indexed _account, uint256 _amount); + // ************************************* // // * Constructor * // // ************************************* // @@ -24,14 +109,40 @@ 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 _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( address _owner, KlerosCore _core, uint256 _minStakingTime, uint256 _maxDrawingTime, - IRNG _rng + IRNG _rng, + uint256 _maxStakePerJuror, + uint256 _maxTotalStaked ) external initializer { - __SortitionModuleBase_initialize(_owner, _core, _minStakingTime, _maxDrawingTime, _rng); + owner = _owner; + core = _core; + minStakingTime = _minStakingTime; + maxDrawingTime = _maxDrawingTime; + lastPhaseChange = block.timestamp; + rng = _rng; + maxStakePerJuror = _maxStakePerJuror; + maxTotalStaked = _maxTotalStaked; + delayedStakeReadIndex = 1; + } + + // ************************************* // + // * Function Modifiers * // + // ************************************* // + + modifier onlyByOwner() { + if (owner != msg.sender) revert OwnerOnly(); + _; + } + + modifier onlyByCore() { + if (address(core) != msg.sender) revert KlerosCoreOnly(); + _; } // ************************************* // @@ -40,7 +151,473 @@ contract SortitionModule is SortitionModuleBase { /// @dev Access Control to perform implementation upgrades (UUPS Proxiable) /// Only the owner can perform upgrades (`onlyByOwner`) - function _authorizeUpgrade(address) internal view virtual override onlyByOwner { + function _authorizeUpgrade(address) internal view override onlyByOwner { // NOP } + + /// @dev Changes the owner of the contract. + /// @param _owner The new owner. + function changeOwner(address _owner) external onlyByOwner { + owner = _owner; + } + + /// @dev Changes the `minStakingTime` storage variable. + /// @param _minStakingTime The new value for the `minStakingTime` storage variable. + function changeMinStakingTime(uint256 _minStakingTime) external onlyByOwner { + minStakingTime = _minStakingTime; + } + + /// @dev Changes the `maxDrawingTime` storage variable. + /// @param _maxDrawingTime The new value for the `maxDrawingTime` storage variable. + function changeMaxDrawingTime(uint256 _maxDrawingTime) external onlyByOwner { + maxDrawingTime = _maxDrawingTime; + } + + /// @dev Changes the `rng` storage variable. + /// @param _rng The new random number generator. + function changeRandomNumberGenerator(IRNG _rng) external onlyByOwner { + rng = _rng; + if (phase == Phase.generating) { + rng.requestRandomness(); + } + } + + function changeMaxStakePerJuror(uint256 _maxStakePerJuror) external onlyByOwner { + maxStakePerJuror = _maxStakePerJuror; + } + + function changeMaxTotalStaked(uint256 _maxTotalStaked) external onlyByOwner { + maxTotalStaked = _maxTotalStaked; + } + + // ************************************* // + // * State Modifiers * // + // ************************************* // + + function passPhase() external { + if (phase == Phase.staking) { + if (block.timestamp - lastPhaseChange < minStakingTime) revert MinStakingTimeNotPassed(); + if (disputesWithoutJurors == 0) revert NoDisputesThatNeedJurors(); + rng.requestRandomness(); + phase = Phase.generating; + } else if (phase == Phase.generating) { + randomNumber = rng.receiveRandomness(); + if (randomNumber == 0) revert RandomNumberNotReady(); + phase = Phase.drawing; + } else if (phase == Phase.drawing) { + if (disputesWithoutJurors > 0 && block.timestamp - lastPhaseChange < maxDrawingTime) { + revert DisputesWithoutJurorsAndMaxDrawingTimeNotPassed(); + } + phase = Phase.staking; + } + + lastPhaseChange = block.timestamp; + emit NewPhase(phase); + } + + /// @dev Create a sortition sum tree at the specified key. + /// @param _courtID The ID of the court. + /// @param _extraData Extra data that contains the number of children each node in the tree should have. + function createTree(uint96 _courtID, bytes memory _extraData) external override onlyByCore { + TreeKey key = CourtID.wrap(_courtID).toTreeKey(); + uint256 K = _extraDataToTreeK(_extraData); + sortitionSumTrees.createTree(key, K); + } + + /// @dev Executes the next delayed stakes. + /// @param _iterations The number of delayed stakes to execute. + function executeDelayedStakes(uint256 _iterations) external { + if (phase != Phase.staking) revert NotStakingPhase(); + if (delayedStakeWriteIndex < delayedStakeReadIndex) revert NoDelayedStakeToExecute(); + + uint256 actualIterations = (delayedStakeReadIndex + _iterations) - 1 > delayedStakeWriteIndex + ? (delayedStakeWriteIndex - delayedStakeReadIndex) + 1 + : _iterations; + uint256 newDelayedStakeReadIndex = delayedStakeReadIndex + actualIterations; + + for (uint256 i = delayedStakeReadIndex; i < newDelayedStakeReadIndex; i++) { + DelayedStake storage delayedStake = delayedStakes[i]; + core.setStakeBySortitionModule(delayedStake.account, delayedStake.courtID, delayedStake.stake); + delete delayedStakes[i]; + } + delayedStakeReadIndex = newDelayedStakeReadIndex; + } + + function createDisputeHook(uint256 /*_disputeID*/, uint256 /*_roundID*/) external override onlyByCore { + disputesWithoutJurors++; + } + + function postDrawHook(uint256 /*_disputeID*/, uint256 /*_roundID*/) external override onlyByCore { + disputesWithoutJurors--; + } + + /// @dev Saves the random number to use it in sortition. Not used by this contract because the storing of the number is inlined in passPhase(). + /// @param _randomNumber Random number returned by RNG contract. + function notifyRandomNumber(uint256 _randomNumber) public override {} + + /// @dev Validate the specified juror's new stake for a court. + /// Note: no state changes should be made when returning stakingResult != Successful, otherwise delayed stakes might break invariants. + /// @param _account The address of the juror. + /// @param _courtID The ID of the court. + /// @param _newStake The new stake. + /// @param _noDelay True if the stake change should not be delayed. + /// @return pnkDeposit The amount of PNK to be deposited. + /// @return pnkWithdrawal The amount of PNK to be withdrawn. + /// @return stakingResult The result of the staking operation. + function validateStake( + address _account, + uint96 _courtID, + uint256 _newStake, + bool _noDelay + ) external override onlyByCore returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { + (pnkDeposit, pnkWithdrawal, stakingResult) = _validateStake(_account, _courtID, _newStake, _noDelay); + } + + function _validateStake( + address _account, + uint96 _courtID, + uint256 _newStake, + bool _noDelay + ) internal virtual returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { + Juror storage juror = jurors[_account]; + uint256 currentStake = stakeOf(_account, _courtID); + bool stakeIncrease = _newStake > currentStake; + uint256 stakeChange = stakeIncrease ? _newStake - currentStake : currentStake - _newStake; + + uint256 nbCourts = juror.courtIDs.length; + if (currentStake == 0 && nbCourts >= MAX_STAKE_PATHS) { + return (0, 0, StakingResult.CannotStakeInMoreCourts); // Prevent staking beyond MAX_STAKE_PATHS but unstaking is always allowed. + } + + if (currentStake == 0 && _newStake == 0) { + return (0, 0, StakingResult.CannotStakeZeroWhenNoStake); // Forbid staking 0 amount when current stake is 0 to avoid flaky behaviour. + } + + if (stakeIncrease) { + // Check if the stake increase is within the limits. + if (juror.stakedPnk + stakeChange > maxStakePerJuror || currentStake + stakeChange > maxStakePerJuror) { + return (0, 0, StakingResult.CannotStakeMoreThanMaxStakePerJuror); + } + if (totalStaked + stakeChange > maxTotalStaked) { + return (0, 0, StakingResult.CannotStakeMoreThanMaxTotalStaked); + } + } + + if (phase != Phase.staking && !_noDelay) { + // Store the stake change as delayed, to be applied when the phase switches back to Staking. + DelayedStake storage delayedStake = delayedStakes[++delayedStakeWriteIndex]; + delayedStake.account = _account; + delayedStake.courtID = _courtID; + delayedStake.stake = _newStake; + emit StakeDelayed(_account, _courtID, _newStake); + return (pnkDeposit, pnkWithdrawal, StakingResult.Delayed); + } + + // Current phase is Staking: set stakes. + if (stakeIncrease) { + pnkDeposit = stakeChange; + totalStaked += stakeChange; + } else { + pnkWithdrawal = stakeChange; + totalStaked -= stakeChange; + + // Ensure locked tokens remain in the contract. They can only be released during Execution. + uint256 possibleWithdrawal = juror.stakedPnk > juror.lockedPnk ? juror.stakedPnk - juror.lockedPnk : 0; + if (pnkWithdrawal > possibleWithdrawal) { + pnkWithdrawal = possibleWithdrawal; + } + } + return (pnkDeposit, pnkWithdrawal, StakingResult.Successful); + } + + /// @dev Update the state of the stakes, called by KC at the end of setStake flow. + /// `O(n + p * log_k(j))` where + /// `n` is the number of courts the juror has staked in, + /// `p` is the depth of the court tree, + /// `k` is the minimum number of children per node of one of these courts' sortition sum tree, + /// and `j` is the maximum number of jurors that ever staked in one of these courts simultaneously. + /// @param _account The address of the juror. + /// @param _courtID The ID of the court. + /// @param _pnkDeposit The amount of PNK to be deposited. + /// @param _pnkWithdrawal The amount of PNK to be withdrawn. + /// @param _newStake The new stake. + function setStake( + address _account, + uint96 _courtID, + uint256 _pnkDeposit, + uint256 _pnkWithdrawal, + uint256 _newStake + ) external override onlyByCore { + _setStake(_account, _courtID, _pnkDeposit, _pnkWithdrawal, _newStake); + } + + /// @dev Update the state of the stakes with a PNK reward deposit, called by KC during rewards execution. + /// `O(n + p * log_k(j))` where + /// `n` is the number of courts the juror has staked in, + /// `p` is the depth of the court tree, + /// `k` is the minimum number of children per node of one of these courts' sortition sum tree, + /// and `j` is the maximum number of jurors that ever staked in one of these courts simultaneously. + /// @param _account The address of the juror. + /// @param _courtID The ID of the court. + /// @param _penalty The amount of PNK to be deducted. + /// @return pnkBalance The updated total PNK balance of the juror, including the penalty. + /// @return newCourtStake The updated stake of the juror in the court. + /// @return availablePenalty The amount of PNK that was actually deducted. + function setStakePenalty( + address _account, + uint96 _courtID, + uint256 _penalty + ) external override onlyByCore returns (uint256 pnkBalance, uint256 newCourtStake, uint256 availablePenalty) { + Juror storage juror = jurors[_account]; + availablePenalty = _penalty; + newCourtStake = stakeOf(_account, _courtID); + if (juror.stakedPnk < _penalty) { + availablePenalty = juror.stakedPnk; + } + + if (availablePenalty == 0) return (juror.stakedPnk, newCourtStake, 0); // No penalty to apply. + + uint256 currentStake = stakeOf(_account, _courtID); + uint256 newStake = 0; + if (currentStake >= availablePenalty) { + newStake = currentStake - availablePenalty; + } + _setStake(_account, _courtID, 0, availablePenalty, newStake); + pnkBalance = juror.stakedPnk; // updated by _setStake() + newCourtStake = stakeOf(_account, _courtID); // updated by _setStake() + } + + /// @dev Update the state of the stakes with a PNK reward deposit, called by KC during rewards execution. + /// `O(n + p * log_k(j))` where + /// `n` is the number of courts the juror has staked in, + /// `p` is the depth of the court tree, + /// `k` is the minimum number of children per node of one of these courts' sortition sum tree, + /// and `j` is the maximum number of jurors that ever staked in one of these courts simultaneously. + /// @param _account The address of the juror. + /// @param _courtID The ID of the court. + /// @param _reward The amount of PNK to be deposited as a reward. + function setStakeReward( + address _account, + uint96 _courtID, + uint256 _reward + ) external override onlyByCore returns (bool success) { + if (_reward == 0) return true; // No reward to add. + + uint256 currentStake = stakeOf(_account, _courtID); + if (currentStake == 0) return false; // Juror has been unstaked, don't increase their stake. + + uint256 newStake = currentStake + _reward; + _setStake(_account, _courtID, _reward, 0, newStake); + return true; + } + + function _setStake( + address _account, + uint96 _courtID, + uint256 _pnkDeposit, + uint256 _pnkWithdrawal, + uint256 _newStake + ) internal virtual { + Juror storage juror = jurors[_account]; + if (_pnkDeposit > 0) { + uint256 currentStake = stakeOf(_account, _courtID); + if (currentStake == 0) { + juror.courtIDs.push(_courtID); + } + // Increase juror's balance by deposited amount. + juror.stakedPnk += _pnkDeposit; + } else { + juror.stakedPnk -= _pnkWithdrawal; + if (_newStake == 0) { + // Cleanup + for (uint256 i = juror.courtIDs.length; i > 0; i--) { + if (juror.courtIDs[i - 1] == _courtID) { + juror.courtIDs[i - 1] = juror.courtIDs[juror.courtIDs.length - 1]; + juror.courtIDs.pop(); + break; + } + } + } + } + + // Update the sortition sum tree. + bytes32 stakePathID = SortitionTrees.toStakePathID(_account, _courtID); + bool finished = false; + uint96 currentCourtID = _courtID; + while (!finished) { + // Tokens are also implicitly staked in parent courts through sortition module to increase the chance of being drawn. + TreeKey key = CourtID.wrap(currentCourtID).toTreeKey(); + sortitionSumTrees[key].set(_newStake, stakePathID); + if (currentCourtID == GENERAL_COURT) { + finished = true; + } else { + (currentCourtID, , , , , , ) = core.courts(currentCourtID); // Get the parent court. + } + } + emit StakeSet(_account, _courtID, _newStake, juror.stakedPnk); + } + + function lockStake(address _account, uint256 _relativeAmount) external override onlyByCore { + jurors[_account].lockedPnk += _relativeAmount; + emit StakeLocked(_account, _relativeAmount, false); + } + + function unlockStake(address _account, uint256 _relativeAmount) external override onlyByCore { + Juror storage juror = jurors[_account]; + juror.lockedPnk -= _relativeAmount; + emit StakeLocked(_account, _relativeAmount, true); + + uint256 amount = getJurorLeftoverPNK(_account); + if (amount > 0) { + emit LeftoverPNK(_account, amount); + } + } + + /// @dev Unstakes the inactive juror from all courts. + /// `O(n * (p * log_k(j)) )` where + /// `n` is the number of courts the juror has staked in, + /// `p` is the depth of the court tree, + /// `k` is the minimum number of children per node of one of these courts' sortition sum tree, + /// and `j` is the maximum number of jurors that ever staked in one of these courts simultaneously. + /// @param _account The juror to unstake. + function forcedUnstakeAllCourts(address _account) external override onlyByCore { + uint96[] memory courtIDs = getJurorCourtIDs(_account); + for (uint256 j = courtIDs.length; j > 0; j--) { + core.setStakeBySortitionModule(_account, courtIDs[j - 1], 0); + } + } + + /// @dev Unstakes the inactive juror from a specific court. + /// `O(n * (p * log_k(j)) )` where + /// `n` is the number of courts the juror has staked in, + /// `p` is the depth of the court tree, + /// `k` is the minimum number of children per node of one of these courts' sortition sum tree, + /// and `j` is the maximum number of jurors that ever staked in one of these courts simultaneously. + /// @param _account The juror to unstake. + /// @param _courtID The ID of the court. + function forcedUnstake(address _account, uint96 _courtID) external override onlyByCore { + core.setStakeBySortitionModule(_account, _courtID, 0); + } + + /// @dev Gives back the locked PNKs in case the juror fully unstaked earlier. + /// Note that since locked and staked PNK are async it is possible for the juror to have positive staked PNK balance + /// while having 0 stake in courts and 0 locked tokens (eg. when the juror fully unstaked during dispute and later got his tokens unlocked). + /// In this case the juror can use this function to withdraw the leftover tokens. + /// Also note that if the juror has some leftover PNK while not fully unstaked he'll have to manually unstake from all courts to trigger this function. + /// @param _account The juror whose PNK to withdraw. + function withdrawLeftoverPNK(address _account) external override { + // 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); + if (amount == 0) revert NotEligibleForWithdrawal(); + jurors[_account].stakedPnk = 0; + core.transferBySortitionModule(_account, amount); + emit LeftoverPNKWithdrawn(_account, amount); + } + + // ************************************* // + // * Public Views * // + // ************************************* // + + /// @dev Draw an ID from a tree using a number. + /// Note that this function reverts if the sum of all values in the tree is 0. + /// @param _courtID The ID of the court. + /// @param _coreDisputeID Index of the dispute in Kleros Core. + /// @param _nonce Nonce to hash with random number. + /// @return drawnAddress The drawn address. + /// `O(k * log_k(n))` where + /// `k` is the maximum number of children per node in the tree, + /// and `n` is the maximum number of nodes ever appended. + function draw( + uint96 _courtID, + uint256 _coreDisputeID, + uint256 _nonce + ) public view override returns (address drawnAddress, uint96 fromSubcourtID) { + if (phase != Phase.drawing) revert NotDrawingPhase(); + + TreeKey key = CourtID.wrap(_courtID).toTreeKey(); + (drawnAddress, fromSubcourtID) = sortitionSumTrees[key].draw(_coreDisputeID, _nonce, randomNumber); + } + + /// @dev Get the stake of a juror in a court. + /// @param _juror The address of the juror. + /// @param _courtID The ID of the court. + /// @return value The stake of the juror in the court. + function stakeOf(address _juror, uint96 _courtID) public view returns (uint256) { + bytes32 stakePathID = SortitionTrees.toStakePathID(_juror, _courtID); + TreeKey key = CourtID.wrap(_courtID).toTreeKey(); + return sortitionSumTrees[key].stakeOf(stakePathID); + } + + /// @dev Gets the balance of a juror in a court. + /// @param _juror The address of the juror. + /// @param _courtID The ID of the court. + /// @return totalStaked The total amount of tokens staked including locked tokens and penalty deductions. Equivalent to the effective stake in the General court. + /// @return totalLocked The total amount of tokens locked in disputes. + /// @return stakedInCourt The amount of tokens staked in the specified court including locked tokens and penalty deductions. + /// @return nbCourts The number of courts the juror has directly staked in. + function getJurorBalance( + address _juror, + uint96 _courtID + ) + external + view + override + returns (uint256 totalStaked, uint256 totalLocked, uint256 stakedInCourt, uint256 nbCourts) + { + Juror storage juror = jurors[_juror]; + totalStaked = juror.stakedPnk; + totalLocked = juror.lockedPnk; + stakedInCourt = stakeOf(_juror, _courtID); + nbCourts = juror.courtIDs.length; + } + + /// @dev Gets the court identifiers where a specific `_juror` has staked. + /// @param _juror The address of the juror. + function getJurorCourtIDs(address _juror) public view override returns (uint96[] memory) { + return jurors[_juror].courtIDs; + } + + function isJurorStaked(address _juror) external view override returns (bool) { + return jurors[_juror].stakedPnk > 0; + } + + function getJurorLeftoverPNK(address _juror) public view override returns (uint256) { + Juror storage juror = jurors[_juror]; + if (juror.courtIDs.length == 0 && juror.lockedPnk == 0) { + return juror.stakedPnk; + } else { + return 0; + } + } + + // ************************************* // + // * Internal * // + // ************************************* // + + function _extraDataToTreeK(bytes memory _extraData) internal pure returns (uint256 K) { + if (_extraData.length >= 32) { + assembly { + // solium-disable-line security/no-inline-assembly + K := mload(add(_extraData, 0x20)) + } + } else { + K = DEFAULT_K; + } + } + + // ************************************* // + // * Errors * // + // ************************************* // + + error OwnerOnly(); + error KlerosCoreOnly(); + error MinStakingTimeNotPassed(); + error NoDisputesThatNeedJurors(); + error RandomNumberNotReady(); + error DisputesWithoutJurorsAndMaxDrawingTimeNotPassed(); + error NotStakingPhase(); + error NoDelayedStakeToExecute(); + error NotEligibleForWithdrawal(); + error NotDrawingPhase(); } diff --git a/contracts/src/arbitration/SortitionModuleBase.sol b/contracts/src/arbitration/SortitionModuleBase.sol deleted file mode 100644 index 3c528b017..000000000 --- a/contracts/src/arbitration/SortitionModuleBase.sol +++ /dev/null @@ -1,572 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.24; - -import {KlerosCore} from "./KlerosCore.sol"; -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 {SortitionTrees, TreeKey, CourtID} from "../libraries/SortitionTrees.sol"; -import {IRNG} from "../rng/IRNG.sol"; -import "../libraries/Constants.sol"; - -/// @title SortitionModuleBase -/// @dev A factory of trees that keeps track of staked values for sortition. -abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSProxiable { - using SortitionTrees for SortitionTrees.Tree; - using SortitionTrees for mapping(TreeKey key => SortitionTrees.Tree); - - // ************************************* // - // * Enums / Structs * // - // ************************************* // - - struct DelayedStake { - address account; // The address of the juror. - uint96 courtID; // The ID of the court. - uint256 stake; // The new stake. - bool alreadyTransferred; // DEPRECATED. True if tokens were already transferred before delayed stake's execution. - } - - struct Juror { - uint96[] courtIDs; // The IDs of courts where the juror's stake path ends. A stake path is a path from the general court to a court the juror directly staked in using `_setStake`. - uint256 stakedPnk; // The juror's total amount of tokens staked in subcourts. PNK balance including locked PNK and penalty deductions. - uint256 lockedPnk; // The juror's total amount of tokens locked in disputes. - } - - // ************************************* // - // * Storage * // - // ************************************* // - - address public owner; // The owner of the contract. - KlerosCore public core; // The core arbitrator contract. - Phase public phase; // The current phase. - 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(TreeKey key => SortitionTrees.Tree) sortitionSumTrees; // The mapping of sortition trees by keys. - mapping(address account => Juror) public jurors; // The jurors. - mapping(uint256 => DelayedStake) public delayedStakes; // Stores the stakes that were changed during Drawing phase, to update them when the phase is switched to Staking. - mapping(address jurorAccount => mapping(uint96 courtId => uint256)) public latestDelayedStakeIndex; // DEPRECATED. Maps the juror to its latest delayed stake. If there is already a delayed stake for this juror then it'll be replaced. latestDelayedStakeIndex[juror][courtID]. - - // ************************************* // - // * Events * // - // ************************************* // - - /// @notice Emitted when a juror stakes in a court. - /// @param _address The address of the juror. - /// @param _courtID The ID of the court. - /// @param _amount The amount of tokens staked in the court. - /// @param _amountAllCourts The amount of tokens staked in all courts. - event StakeSet(address indexed _address, uint256 _courtID, uint256 _amount, uint256 _amountAllCourts); - - /// @notice Emitted when a juror's stake is delayed. - /// @param _address The address of the juror. - /// @param _courtID The ID of the court. - /// @param _amount The amount of tokens staked in the court. - event StakeDelayed(address indexed _address, uint96 indexed _courtID, uint256 _amount); - - /// @notice Emitted when a juror's stake is locked. - /// @param _address The address of the juror. - /// @param _relativeAmount The amount of tokens locked. - /// @param _unlock Whether the stake is locked or unlocked. - event StakeLocked(address indexed _address, uint256 _relativeAmount, bool _unlock); - - /// @dev Emitted when leftover PNK is available. - /// @param _account The account of the juror. - /// @param _amount The amount of PNK available. - event LeftoverPNK(address indexed _account, uint256 _amount); - - /// @dev Emitted when leftover PNK is withdrawn. - /// @param _account The account of the juror withdrawing PNK. - /// @param _amount The amount of PNK withdrawn. - event LeftoverPNKWithdrawn(address indexed _account, uint256 _amount); - - // ************************************* // - // * Constructor * // - // ************************************* // - - function __SortitionModuleBase_initialize( - address _owner, - KlerosCore _core, - uint256 _minStakingTime, - uint256 _maxDrawingTime, - IRNG _rng - ) internal onlyInitializing { - owner = _owner; - core = _core; - minStakingTime = _minStakingTime; - maxDrawingTime = _maxDrawingTime; - lastPhaseChange = block.timestamp; - rng = _rng; - delayedStakeReadIndex = 1; - } - - // ************************************* // - // * Function Modifiers * // - // ************************************* // - - modifier onlyByOwner() { - if (owner != msg.sender) revert OwnerOnly(); - _; - } - - modifier onlyByCore() { - if (address(core) != msg.sender) revert KlerosCoreOnly(); - _; - } - - // ************************************* // - // * Governance * // - // ************************************* // - - /// @dev Changes the owner of the contract. - /// @param _owner The new owner. - function changeOwner(address _owner) external onlyByOwner { - owner = _owner; - } - - /// @dev Changes the `minStakingTime` storage variable. - /// @param _minStakingTime The new value for the `minStakingTime` storage variable. - function changeMinStakingTime(uint256 _minStakingTime) external onlyByOwner { - minStakingTime = _minStakingTime; - } - - /// @dev Changes the `maxDrawingTime` storage variable. - /// @param _maxDrawingTime The new value for the `maxDrawingTime` storage variable. - function changeMaxDrawingTime(uint256 _maxDrawingTime) external onlyByOwner { - maxDrawingTime = _maxDrawingTime; - } - - /// @dev Changes the `rng` storage variable. - /// @param _rng The new random number generator. - function changeRandomNumberGenerator(IRNG _rng) external onlyByOwner { - rng = _rng; - if (phase == Phase.generating) { - rng.requestRandomness(); - } - } - - // ************************************* // - // * State Modifiers * // - // ************************************* // - - function passPhase() external { - if (phase == Phase.staking) { - if (block.timestamp - lastPhaseChange < minStakingTime) revert MinStakingTimeNotPassed(); - if (disputesWithoutJurors == 0) revert NoDisputesThatNeedJurors(); - rng.requestRandomness(); - phase = Phase.generating; - } else if (phase == Phase.generating) { - randomNumber = rng.receiveRandomness(); - if (randomNumber == 0) revert RandomNumberNotReady(); - phase = Phase.drawing; - } else if (phase == Phase.drawing) { - if (disputesWithoutJurors > 0 && block.timestamp - lastPhaseChange < maxDrawingTime) { - revert DisputesWithoutJurorsAndMaxDrawingTimeNotPassed(); - } - phase = Phase.staking; - } - - lastPhaseChange = block.timestamp; - emit NewPhase(phase); - } - - /// @dev Create a sortition sum tree at the specified key. - /// @param _courtID The ID of the court. - /// @param _extraData Extra data that contains the number of children each node in the tree should have. - function createTree(uint96 _courtID, bytes memory _extraData) external override onlyByCore { - TreeKey key = CourtID.wrap(_courtID).toTreeKey(); - uint256 K = _extraDataToTreeK(_extraData); - sortitionSumTrees.createTree(key, K); - } - - /// @dev Executes the next delayed stakes. - /// @param _iterations The number of delayed stakes to execute. - function executeDelayedStakes(uint256 _iterations) external { - if (phase != Phase.staking) revert NotStakingPhase(); - if (delayedStakeWriteIndex < delayedStakeReadIndex) revert NoDelayedStakeToExecute(); - - uint256 actualIterations = (delayedStakeReadIndex + _iterations) - 1 > delayedStakeWriteIndex - ? (delayedStakeWriteIndex - delayedStakeReadIndex) + 1 - : _iterations; - uint256 newDelayedStakeReadIndex = delayedStakeReadIndex + actualIterations; - - for (uint256 i = delayedStakeReadIndex; i < newDelayedStakeReadIndex; i++) { - DelayedStake storage delayedStake = delayedStakes[i]; - core.setStakeBySortitionModule(delayedStake.account, delayedStake.courtID, delayedStake.stake); - delete delayedStakes[i]; - } - delayedStakeReadIndex = newDelayedStakeReadIndex; - } - - function createDisputeHook(uint256 /*_disputeID*/, uint256 /*_roundID*/) external override onlyByCore { - disputesWithoutJurors++; - } - - function postDrawHook(uint256 /*_disputeID*/, uint256 /*_roundID*/) external override onlyByCore { - disputesWithoutJurors--; - } - - /// @dev Saves the random number to use it in sortition. Not used by this contract because the storing of the number is inlined in passPhase(). - /// @param _randomNumber Random number returned by RNG contract. - function notifyRandomNumber(uint256 _randomNumber) public override {} - - /// @dev Validate the specified juror's new stake for a court. - /// Note: no state changes should be made when returning stakingResult != Successful, otherwise delayed stakes might break invariants. - /// @param _account The address of the juror. - /// @param _courtID The ID of the court. - /// @param _newStake The new stake. - /// @param _noDelay True if the stake change should not be delayed. - /// @return pnkDeposit The amount of PNK to be deposited. - /// @return pnkWithdrawal The amount of PNK to be withdrawn. - /// @return stakingResult The result of the staking operation. - function validateStake( - address _account, - uint96 _courtID, - uint256 _newStake, - bool _noDelay - ) external override onlyByCore returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { - (pnkDeposit, pnkWithdrawal, stakingResult) = _validateStake(_account, _courtID, _newStake, _noDelay); - } - - function _validateStake( - address _account, - uint96 _courtID, - uint256 _newStake, - bool _noDelay - ) internal virtual returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { - Juror storage juror = jurors[_account]; - uint256 currentStake = stakeOf(_account, _courtID); - - uint256 nbCourts = juror.courtIDs.length; - if (currentStake == 0 && nbCourts >= MAX_STAKE_PATHS) { - return (0, 0, StakingResult.CannotStakeInMoreCourts); // Prevent staking beyond MAX_STAKE_PATHS but unstaking is always allowed. - } - - if (currentStake == 0 && _newStake == 0) { - return (0, 0, StakingResult.CannotStakeZeroWhenNoStake); // Forbid staking 0 amount when current stake is 0 to avoid flaky behaviour. - } - - if (phase != Phase.staking && !_noDelay) { - // Store the stake change as delayed, to be applied when the phase switches back to Staking. - DelayedStake storage delayedStake = delayedStakes[++delayedStakeWriteIndex]; - delayedStake.account = _account; - delayedStake.courtID = _courtID; - delayedStake.stake = _newStake; - emit StakeDelayed(_account, _courtID, _newStake); - return (pnkDeposit, pnkWithdrawal, StakingResult.Delayed); - } - - // Current phase is Staking: set stakes. - if (_newStake >= currentStake) { - pnkDeposit = _newStake - currentStake; - } else { - pnkWithdrawal = currentStake - _newStake; - // Ensure locked tokens remain in the contract. They can only be released during Execution. - uint256 possibleWithdrawal = juror.stakedPnk > juror.lockedPnk ? juror.stakedPnk - juror.lockedPnk : 0; - if (pnkWithdrawal > possibleWithdrawal) { - pnkWithdrawal = possibleWithdrawal; - } - } - return (pnkDeposit, pnkWithdrawal, StakingResult.Successful); - } - - /// @dev Update the state of the stakes, called by KC at the end of setStake flow. - /// `O(n + p * log_k(j))` where - /// `n` is the number of courts the juror has staked in, - /// `p` is the depth of the court tree, - /// `k` is the minimum number of children per node of one of these courts' sortition sum tree, - /// and `j` is the maximum number of jurors that ever staked in one of these courts simultaneously. - /// @param _account The address of the juror. - /// @param _courtID The ID of the court. - /// @param _pnkDeposit The amount of PNK to be deposited. - /// @param _pnkWithdrawal The amount of PNK to be withdrawn. - /// @param _newStake The new stake. - function setStake( - address _account, - uint96 _courtID, - uint256 _pnkDeposit, - uint256 _pnkWithdrawal, - uint256 _newStake - ) external override onlyByCore { - _setStake(_account, _courtID, _pnkDeposit, _pnkWithdrawal, _newStake); - } - - /// @dev Update the state of the stakes with a PNK reward deposit, called by KC during rewards execution. - /// `O(n + p * log_k(j))` where - /// `n` is the number of courts the juror has staked in, - /// `p` is the depth of the court tree, - /// `k` is the minimum number of children per node of one of these courts' sortition sum tree, - /// and `j` is the maximum number of jurors that ever staked in one of these courts simultaneously. - /// @param _account The address of the juror. - /// @param _courtID The ID of the court. - /// @param _penalty The amount of PNK to be deducted. - /// @return pnkBalance The updated total PNK balance of the juror, including the penalty. - /// @return newCourtStake The updated stake of the juror in the court. - /// @return availablePenalty The amount of PNK that was actually deducted. - function setStakePenalty( - address _account, - uint96 _courtID, - uint256 _penalty - ) external override onlyByCore returns (uint256 pnkBalance, uint256 newCourtStake, uint256 availablePenalty) { - Juror storage juror = jurors[_account]; - availablePenalty = _penalty; - newCourtStake = stakeOf(_account, _courtID); - if (juror.stakedPnk < _penalty) { - availablePenalty = juror.stakedPnk; - } - - if (availablePenalty == 0) return (juror.stakedPnk, newCourtStake, 0); // No penalty to apply. - - uint256 currentStake = stakeOf(_account, _courtID); - uint256 newStake = 0; - if (currentStake >= availablePenalty) { - newStake = currentStake - availablePenalty; - } - _setStake(_account, _courtID, 0, availablePenalty, newStake); - pnkBalance = juror.stakedPnk; // updated by _setStake() - newCourtStake = stakeOf(_account, _courtID); // updated by _setStake() - } - - /// @dev Update the state of the stakes with a PNK reward deposit, called by KC during rewards execution. - /// `O(n + p * log_k(j))` where - /// `n` is the number of courts the juror has staked in, - /// `p` is the depth of the court tree, - /// `k` is the minimum number of children per node of one of these courts' sortition sum tree, - /// and `j` is the maximum number of jurors that ever staked in one of these courts simultaneously. - /// @param _account The address of the juror. - /// @param _courtID The ID of the court. - /// @param _reward The amount of PNK to be deposited as a reward. - function setStakeReward( - address _account, - uint96 _courtID, - uint256 _reward - ) external override onlyByCore returns (bool success) { - if (_reward == 0) return true; // No reward to add. - - uint256 currentStake = stakeOf(_account, _courtID); - if (currentStake == 0) return false; // Juror has been unstaked, don't increase their stake. - - uint256 newStake = currentStake + _reward; - _setStake(_account, _courtID, _reward, 0, newStake); - return true; - } - - function _setStake( - address _account, - uint96 _courtID, - uint256 _pnkDeposit, - uint256 _pnkWithdrawal, - uint256 _newStake - ) internal virtual { - Juror storage juror = jurors[_account]; - if (_pnkDeposit > 0) { - uint256 currentStake = stakeOf(_account, _courtID); - if (currentStake == 0) { - juror.courtIDs.push(_courtID); - } - // Increase juror's balance by deposited amount. - juror.stakedPnk += _pnkDeposit; - } else { - juror.stakedPnk -= _pnkWithdrawal; - if (_newStake == 0) { - // Cleanup - for (uint256 i = juror.courtIDs.length; i > 0; i--) { - if (juror.courtIDs[i - 1] == _courtID) { - juror.courtIDs[i - 1] = juror.courtIDs[juror.courtIDs.length - 1]; - juror.courtIDs.pop(); - break; - } - } - } - } - - // Update the sortition sum tree. - bytes32 stakePathID = SortitionTrees.toStakePathID(_account, _courtID); - bool finished = false; - uint96 currentCourtID = _courtID; - while (!finished) { - // Tokens are also implicitly staked in parent courts through sortition module to increase the chance of being drawn. - TreeKey key = CourtID.wrap(currentCourtID).toTreeKey(); - sortitionSumTrees[key].set(_newStake, stakePathID); - if (currentCourtID == GENERAL_COURT) { - finished = true; - } else { - (currentCourtID, , , , , , ) = core.courts(currentCourtID); // Get the parent court. - } - } - emit StakeSet(_account, _courtID, _newStake, juror.stakedPnk); - } - - function lockStake(address _account, uint256 _relativeAmount) external override onlyByCore { - jurors[_account].lockedPnk += _relativeAmount; - emit StakeLocked(_account, _relativeAmount, false); - } - - function unlockStake(address _account, uint256 _relativeAmount) external override onlyByCore { - Juror storage juror = jurors[_account]; - juror.lockedPnk -= _relativeAmount; - emit StakeLocked(_account, _relativeAmount, true); - - uint256 amount = getJurorLeftoverPNK(_account); - if (amount > 0) { - emit LeftoverPNK(_account, amount); - } - } - - /// @dev Unstakes the inactive juror from all courts. - /// `O(n * (p * log_k(j)) )` where - /// `n` is the number of courts the juror has staked in, - /// `p` is the depth of the court tree, - /// `k` is the minimum number of children per node of one of these courts' sortition sum tree, - /// and `j` is the maximum number of jurors that ever staked in one of these courts simultaneously. - /// @param _account The juror to unstake. - function forcedUnstakeAllCourts(address _account) external override onlyByCore { - uint96[] memory courtIDs = getJurorCourtIDs(_account); - for (uint256 j = courtIDs.length; j > 0; j--) { - core.setStakeBySortitionModule(_account, courtIDs[j - 1], 0); - } - } - - /// @dev Unstakes the inactive juror from a specific court. - /// `O(n * (p * log_k(j)) )` where - /// `n` is the number of courts the juror has staked in, - /// `p` is the depth of the court tree, - /// `k` is the minimum number of children per node of one of these courts' sortition sum tree, - /// and `j` is the maximum number of jurors that ever staked in one of these courts simultaneously. - /// @param _account The juror to unstake. - /// @param _courtID The ID of the court. - function forcedUnstake(address _account, uint96 _courtID) external override onlyByCore { - core.setStakeBySortitionModule(_account, _courtID, 0); - } - - /// @dev Gives back the locked PNKs in case the juror fully unstaked earlier. - /// Note that since locked and staked PNK are async it is possible for the juror to have positive staked PNK balance - /// while having 0 stake in courts and 0 locked tokens (eg. when the juror fully unstaked during dispute and later got his tokens unlocked). - /// In this case the juror can use this function to withdraw the leftover tokens. - /// Also note that if the juror has some leftover PNK while not fully unstaked he'll have to manually unstake from all courts to trigger this function. - /// @param _account The juror whose PNK to withdraw. - function withdrawLeftoverPNK(address _account) external override { - // 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); - if (amount == 0) revert NotEligibleForWithdrawal(); - jurors[_account].stakedPnk = 0; - core.transferBySortitionModule(_account, amount); - emit LeftoverPNKWithdrawn(_account, amount); - } - - // ************************************* // - // * Public Views * // - // ************************************* // - - /// @dev Draw an ID from a tree using a number. - /// Note that this function reverts if the sum of all values in the tree is 0. - /// @param _courtID The ID of the court. - /// @param _coreDisputeID Index of the dispute in Kleros Core. - /// @param _nonce Nonce to hash with random number. - /// @return drawnAddress The drawn address. - /// `O(k * log_k(n))` where - /// `k` is the maximum number of children per node in the tree, - /// and `n` is the maximum number of nodes ever appended. - function draw( - uint96 _courtID, - uint256 _coreDisputeID, - uint256 _nonce - ) public view override returns (address drawnAddress, uint96 fromSubcourtID) { - if (phase != Phase.drawing) revert NotDrawingPhase(); - - TreeKey key = CourtID.wrap(_courtID).toTreeKey(); - (drawnAddress, fromSubcourtID) = sortitionSumTrees[key].draw(_coreDisputeID, _nonce, randomNumber); - } - - /// @dev Get the stake of a juror in a court. - /// @param _juror The address of the juror. - /// @param _courtID The ID of the court. - /// @return value The stake of the juror in the court. - function stakeOf(address _juror, uint96 _courtID) public view returns (uint256) { - bytes32 stakePathID = SortitionTrees.toStakePathID(_juror, _courtID); - TreeKey key = CourtID.wrap(_courtID).toTreeKey(); - return sortitionSumTrees[key].stakeOf(stakePathID); - } - - /// @dev Gets the balance of a juror in a court. - /// @param _juror The address of the juror. - /// @param _courtID The ID of the court. - /// @return totalStaked The total amount of tokens staked including locked tokens and penalty deductions. Equivalent to the effective stake in the General court. - /// @return totalLocked The total amount of tokens locked in disputes. - /// @return stakedInCourt The amount of tokens staked in the specified court including locked tokens and penalty deductions. - /// @return nbCourts The number of courts the juror has directly staked in. - function getJurorBalance( - address _juror, - uint96 _courtID - ) - external - view - override - returns (uint256 totalStaked, uint256 totalLocked, uint256 stakedInCourt, uint256 nbCourts) - { - Juror storage juror = jurors[_juror]; - totalStaked = juror.stakedPnk; - totalLocked = juror.lockedPnk; - stakedInCourt = stakeOf(_juror, _courtID); - nbCourts = juror.courtIDs.length; - } - - /// @dev Gets the court identifiers where a specific `_juror` has staked. - /// @param _juror The address of the juror. - function getJurorCourtIDs(address _juror) public view override returns (uint96[] memory) { - return jurors[_juror].courtIDs; - } - - function isJurorStaked(address _juror) external view override returns (bool) { - return jurors[_juror].stakedPnk > 0; - } - - function getJurorLeftoverPNK(address _juror) public view override returns (uint256) { - Juror storage juror = jurors[_juror]; - if (juror.courtIDs.length == 0 && juror.lockedPnk == 0) { - return juror.stakedPnk; - } else { - return 0; - } - } - - // ************************************* // - // * Internal * // - // ************************************* // - - function _extraDataToTreeK(bytes memory _extraData) internal pure returns (uint256 K) { - if (_extraData.length >= 32) { - assembly { - // solium-disable-line security/no-inline-assembly - K := mload(add(_extraData, 0x20)) - } - } else { - K = DEFAULT_K; - } - } - - // ************************************* // - // * Errors * // - // ************************************* // - - error OwnerOnly(); - error KlerosCoreOnly(); - error MinStakingTimeNotPassed(); - error NoDisputesThatNeedJurors(); - error RandomNumberNotReady(); - error DisputesWithoutJurorsAndMaxDrawingTimeNotPassed(); - error NotStakingPhase(); - error NoDelayedStakeToExecute(); - error NotEligibleForWithdrawal(); - error NotDrawingPhase(); -} diff --git a/contracts/src/arbitration/SortitionModuleNeo.sol b/contracts/src/arbitration/SortitionModuleNeo.sol deleted file mode 100644 index 66dbba7e5..000000000 --- a/contracts/src/arbitration/SortitionModuleNeo.sol +++ /dev/null @@ -1,100 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.24; - -import {SortitionModuleBase, KlerosCore, IRNG, StakingResult} from "./SortitionModuleBase.sol"; - -/// @title SortitionModuleNeo -/// @dev A factory of trees that keeps track of staked values for sortition. -contract SortitionModuleNeo is SortitionModuleBase { - string public constant override version = "2.0.0"; - - // ************************************* // - // * Storage * // - // ************************************* // - - uint256 public maxStakePerJuror; - uint256 public maxTotalStaked; - uint256 public totalStaked; - - // ************************************* // - // * Constructor * // - // ************************************* // - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() { - _disableInitializers(); - } - - /// @dev Initializer (constructor equivalent for upgradable contracts). - /// @param _owner The owner. - /// @param _core The KlerosCore. - /// @param _minStakingTime Minimal time to stake - /// @param _maxDrawingTime Time after which the drawing phase can be switched - /// @param _rng The random number generator. - /// @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( - address _owner, - KlerosCore _core, - uint256 _minStakingTime, - uint256 _maxDrawingTime, - IRNG _rng, - uint256 _maxStakePerJuror, - uint256 _maxTotalStaked - ) external reinitializer(2) { - __SortitionModuleBase_initialize(_owner, _core, _minStakingTime, _maxDrawingTime, _rng); - maxStakePerJuror = _maxStakePerJuror; - maxTotalStaked = _maxTotalStaked; - } - - // ************************************* // - // * Governance * // - // ************************************* // - - /// @dev Access Control to perform implementation upgrades (UUPS Proxiable) - /// Only the owner can perform upgrades (`onlyByOwner`) - function _authorizeUpgrade(address) internal view override onlyByOwner { - // NOP - } - - function changeMaxStakePerJuror(uint256 _maxStakePerJuror) external onlyByOwner { - maxStakePerJuror = _maxStakePerJuror; - } - - function changeMaxTotalStaked(uint256 _maxTotalStaked) external onlyByOwner { - maxTotalStaked = _maxTotalStaked; - } - - // ************************************* // - // * State Modifiers * // - // ************************************* // - - function _validateStake( - address _account, - uint96 _courtID, - uint256 _newStake, - bool _noDelay - ) internal override onlyByCore returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { - uint256 currentStake = stakeOf(_account, _courtID); - bool stakeIncrease = _newStake > currentStake; - uint256 stakeChange = stakeIncrease ? _newStake - currentStake : currentStake - _newStake; - Juror storage juror = jurors[_account]; - if (stakeIncrease) { - if (juror.stakedPnk + stakeChange > maxStakePerJuror || currentStake + stakeChange > maxStakePerJuror) { - return (0, 0, StakingResult.CannotStakeMoreThanMaxStakePerJuror); - } - if (totalStaked + stakeChange > maxTotalStaked) { - return (0, 0, StakingResult.CannotStakeMoreThanMaxTotalStaked); - } - } - if (phase == Phase.staking) { - if (stakeIncrease) { - totalStaked += stakeChange; - } else { - totalStaked -= stakeChange; - } - } - (pnkDeposit, pnkWithdrawal, stakingResult) = super._validateStake(_account, _courtID, _newStake, _noDelay); - } -} diff --git a/contracts/test/foundry/KlerosCore_Drawing.t.sol b/contracts/test/foundry/KlerosCore_Drawing.t.sol index b8c644392..9bffbc27d 100644 --- a/contracts/test/foundry/KlerosCore_Drawing.t.sol +++ b/contracts/test/foundry/KlerosCore_Drawing.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.24; import {KlerosCore_TestBase} from "./KlerosCore_TestBase.sol"; import {KlerosCore} from "../../src/arbitration/KlerosCore.sol"; -import {SortitionModuleBase} from "../../src/arbitration/SortitionModuleBase.sol"; +import {SortitionModule} from "../../src/arbitration/SortitionModule.sol"; import {ISortitionModule} from "../../src/arbitration/interfaces/ISortitionModule.sol"; import "../../src/libraries/Constants.sol"; @@ -24,7 +24,7 @@ contract KlerosCore_DrawingTest is KlerosCore_TestBase { sortitionModule.passPhase(); // Drawing phase vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeLocked(staker1, 1000, false); + emit SortitionModule.StakeLocked(staker1, 1000, false); vm.expectEmit(true, true, true, true); emit KlerosCore.Draw(staker1, disputeID, roundID, 0); // VoteID = 0 diff --git a/contracts/test/foundry/KlerosCore_Execution.t.sol b/contracts/test/foundry/KlerosCore_Execution.t.sol index 33b2cda65..01efd6409 100644 --- a/contracts/test/foundry/KlerosCore_Execution.t.sol +++ b/contracts/test/foundry/KlerosCore_Execution.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.24; import {KlerosCore_TestBase} from "./KlerosCore_TestBase.sol"; import {KlerosCore} from "../../src/arbitration/KlerosCore.sol"; -import {SortitionModuleBase} from "../../src/arbitration/SortitionModuleBase.sol"; +import {SortitionModule} from "../../src/arbitration/SortitionModule.sol"; import {DisputeKitClassicBase} from "../../src/arbitration/dispute-kits/DisputeKitClassicBase.sol"; import {IArbitratorV2, IArbitrableV2} from "../../src/arbitration/KlerosCore.sol"; import {IERC20} from "../../src/libraries/SafeERC20.sol"; @@ -96,16 +96,16 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { ); vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeLocked(staker1, 1000, true); + emit SortitionModule.StakeLocked(staker1, 1000, true); vm.expectEmit(true, true, true, true); emit KlerosCore.TokenAndETHShift(staker1, disputeID, 0, 0, -int256(1000), 0, IERC20(address(0))); // Check iterations for the winning staker to see the shifts vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeLocked(staker2, 0, true); + emit SortitionModule.StakeLocked(staker2, 0, true); vm.expectEmit(true, true, true, true); emit KlerosCore.TokenAndETHShift(staker2, disputeID, 0, 10000, 0, 0, IERC20(address(0))); vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeLocked(staker2, 0, true); + emit SortitionModule.StakeLocked(staker2, 0, true); vm.expectEmit(true, true, true, true); emit KlerosCore.TokenAndETHShift(staker2, disputeID, 0, 10000, 0, 0, IERC20(address(0))); core.execute(disputeID, 0, 3); // Do 3 iterations to check penalties first @@ -121,16 +121,16 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { assertEq(round.pnkPenalties, 1000, "Wrong pnkPenalties"); vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeLocked(staker1, 0, true); + emit SortitionModule.StakeLocked(staker1, 0, true); vm.expectEmit(true, true, true, true); emit KlerosCore.TokenAndETHShift(staker1, disputeID, 0, 0, 0, 0, IERC20(address(0))); // Check iterations for the winning staker to see the shifts vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeLocked(staker2, 1000, true); + emit SortitionModule.StakeLocked(staker2, 1000, true); vm.expectEmit(true, true, true, true); emit KlerosCore.TokenAndETHShift(staker2, disputeID, 0, 10000, 500, 0.045 ether, IERC20(address(0))); vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeLocked(staker2, 1000, true); + emit SortitionModule.StakeLocked(staker2, 1000, true); vm.expectEmit(true, true, true, true); emit KlerosCore.TokenAndETHShift(staker2, disputeID, 0, 10000, 500, 0.045 ether, IERC20(address(0))); core.execute(disputeID, 0, 10); // Finish the iterations. We need only 3 but check that it corrects the count. @@ -272,11 +272,11 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { // Note that these events are emitted only after the first iteration of execute() therefore the juror has been penalized only for 1000 PNK her. vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeSet(staker1, newCourtID, 19000, 39000); // 1000 PNK penalty for voteID 0 + emit SortitionModule.StakeSet(staker1, newCourtID, 19000, 39000); // 1000 PNK penalty for voteID 0 vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeSet(staker1, newCourtID, 0, 20000); // Starting with 40000 we first nullify the stake and remove 19000 and then remove penalty once since there was only first iteration (40000 - 20000 - 1000) + emit SortitionModule.StakeSet(staker1, newCourtID, 0, 20000); // Starting with 40000 we first nullify the stake and remove 19000 and then remove penalty once since there was only first iteration (40000 - 20000 - 1000) vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeSet(staker1, GENERAL_COURT, 0, 2000); // 2000 PNK should remain in balance to cover penalties since the first 1000 of locked pnk was already unlocked + emit SortitionModule.StakeSet(staker1, GENERAL_COURT, 0, 2000); // 2000 PNK should remain in balance to cover penalties since the first 1000 of locked pnk was already unlocked core.execute(disputeID, 0, 3); assertEq(pinakion.balanceOf(address(core)), 0, "Wrong token balance of the core"); @@ -335,7 +335,7 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { core.passPeriod(disputeID); // Execution vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeSet(staker1, GENERAL_COURT, 0, 0); // Juror should have no stake left and should be unstaked from the court automatically. + emit SortitionModule.StakeSet(staker1, GENERAL_COURT, 0, 0); // Juror should have no stake left and should be unstaked from the court automatically. core.execute(disputeID, 0, 6); assertEq(pinakion.balanceOf(address(core)), 0, "Wrong token balance of the core"); @@ -397,7 +397,7 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { core.passPeriod(disputeID); // Execution vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeSet(staker1, GENERAL_COURT, 0, 0); // Juror balance should be below minStake and should be unstaked from the court automatically. + emit SortitionModule.StakeSet(staker1, GENERAL_COURT, 0, 0); // Juror balance should be below minStake and should be unstaked from the court automatically. core.execute(disputeID, 0, 6); assertEq(pinakion.balanceOf(address(core)), 11000, "Wrong token balance of the core"); @@ -460,11 +460,11 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { assertEq(pinakion.balanceOf(address(core)), 1000, "Wrong token balance of the core"); assertEq(pinakion.balanceOf(staker1), 999999999999999000, "Wrong token balance of staker1"); - vm.expectRevert(SortitionModuleBase.NotEligibleForWithdrawal.selector); + vm.expectRevert(SortitionModule.NotEligibleForWithdrawal.selector); sortitionModule.withdrawLeftoverPNK(staker1); vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.LeftoverPNK(staker1, 1000); + emit SortitionModule.LeftoverPNK(staker1, 1000); core.execute(disputeID, 0, 6); (totalStaked, totalLocked, , ) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); @@ -485,7 +485,7 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { core.transferBySortitionModule(staker1, 1000); vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.LeftoverPNKWithdrawn(staker1, 1000); + emit SortitionModule.LeftoverPNKWithdrawn(staker1, 1000); sortitionModule.withdrawLeftoverPNK(staker1); (totalStaked, , , ) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); diff --git a/contracts/test/foundry/KlerosCore_Initialization.t.sol b/contracts/test/foundry/KlerosCore_Initialization.t.sol index d9113bbb7..d6df4a932 100644 --- a/contracts/test/foundry/KlerosCore_Initialization.t.sol +++ b/contracts/test/foundry/KlerosCore_Initialization.t.sol @@ -123,12 +123,14 @@ contract KlerosCore_InitializationTest is KlerosCore_TestBase { DisputeKitClassic newDisputeKit = DisputeKitClassic(address(proxyDk)); bytes memory initDataSm = abi.encodeWithSignature( - "initialize(address,address,uint256,uint256,address)", + "initialize(address,address,uint256,uint256,address,uint256,uint256)", newOwner, address(proxyCore), newMinStakingTime, newMaxDrawingTime, - newRng + newRng, + type(int256).max, + type(int256).max ); UUPSProxy proxySm = new UUPSProxy(address(smLogic), initDataSm); diff --git a/contracts/test/foundry/KlerosCore_RNG.t.sol b/contracts/test/foundry/KlerosCore_RNG.t.sol index da3df03ed..0afd0ba43 100644 --- a/contracts/test/foundry/KlerosCore_RNG.t.sol +++ b/contracts/test/foundry/KlerosCore_RNG.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.24; import {KlerosCore_TestBase} from "./KlerosCore_TestBase.sol"; -import {SortitionModuleBase} from "../../src/arbitration/SortitionModuleBase.sol"; +import {SortitionModule} from "../../src/arbitration/SortitionModule.sol"; import {RNGWithFallback, IRNG} from "../../src/rng/RNGWithFallback.sol"; import {RNGMock} from "../../src/test/RNGMock.sol"; import "../../src/libraries/Constants.sol"; @@ -34,7 +34,7 @@ contract KlerosCore_RNGTest is KlerosCore_TestBase { sortitionModule.passPhase(); // Generating assertEq(rngFallback.requestTimestamp(), block.timestamp, "Wrong request timestamp"); - vm.expectRevert(SortitionModuleBase.RandomNumberNotReady.selector); + vm.expectRevert(SortitionModule.RandomNumberNotReady.selector); sortitionModule.passPhase(); vm.warp(block.timestamp + fallbackTimeout + 1); diff --git a/contracts/test/foundry/KlerosCore_Staking.t.sol b/contracts/test/foundry/KlerosCore_Staking.t.sol index 805b8ae7e..070a38716 100644 --- a/contracts/test/foundry/KlerosCore_Staking.t.sol +++ b/contracts/test/foundry/KlerosCore_Staking.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.24; import {KlerosCore_TestBase} from "./KlerosCore_TestBase.sol"; import {KlerosCore} from "../../src/arbitration/KlerosCore.sol"; -import {SortitionModuleBase} from "../../src/arbitration/SortitionModuleBase.sol"; +import {SortitionModule} from "../../src/arbitration/SortitionModule.sol"; import {ISortitionModule} from "../../src/arbitration/interfaces/ISortitionModule.sol"; import {IKlerosCore, KlerosCoreSnapshotProxy} from "../../src/arbitration/view/KlerosCoreSnapshotProxy.sol"; import "../../src/libraries/Constants.sol"; @@ -39,7 +39,7 @@ contract KlerosCore_StakingTest is KlerosCore_TestBase { vm.prank(staker1); vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeSet(staker1, GENERAL_COURT, 1001, 1001); + emit SortitionModule.StakeSet(staker1, GENERAL_COURT, 1001, 1001); core.setStake(GENERAL_COURT, 1001); (uint256 totalStaked, uint256 totalLocked, uint256 stakedInCourt, uint256 nbCourts) = sortitionModule @@ -65,7 +65,7 @@ contract KlerosCore_StakingTest is KlerosCore_TestBase { // Increase stake one more time to verify the correct behavior vm.prank(staker1); vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeSet(staker1, GENERAL_COURT, 2000, 2000); + emit SortitionModule.StakeSet(staker1, GENERAL_COURT, 2000, 2000); core.setStake(GENERAL_COURT, 2000); (totalStaked, totalLocked, stakedInCourt, nbCourts) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); @@ -188,7 +188,7 @@ contract KlerosCore_StakingTest is KlerosCore_TestBase { vm.prank(staker1); vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeDelayed(staker1, GENERAL_COURT, 1500); + emit SortitionModule.StakeDelayed(staker1, GENERAL_COURT, 1500); core.setStake(GENERAL_COURT, 1500); uint256 delayedStakeId = sortitionModule.delayedStakeWriteIndex(); @@ -234,7 +234,7 @@ contract KlerosCore_StakingTest is KlerosCore_TestBase { vm.prank(staker1); vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeDelayed(staker1, GENERAL_COURT, 1800); + emit SortitionModule.StakeDelayed(staker1, GENERAL_COURT, 1800); core.setStake(GENERAL_COURT, 1800); (uint256 totalStaked, , uint256 stakedInCourt, ) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); @@ -301,7 +301,7 @@ contract KlerosCore_StakingTest is KlerosCore_TestBase { vm.prank(staker2); core.setStake(GENERAL_COURT, 10000); - vm.expectRevert(SortitionModuleBase.NoDelayedStakeToExecute.selector); + vm.expectRevert(SortitionModule.NoDelayedStakeToExecute.selector); sortitionModule.executeDelayedStakes(5); // Set the stake and create a dispute to advance the phase @@ -314,13 +314,13 @@ contract KlerosCore_StakingTest is KlerosCore_TestBase { uint256 disputeID = 0; core.draw(disputeID, DEFAULT_NB_OF_JURORS); - vm.expectRevert(SortitionModuleBase.NotStakingPhase.selector); + vm.expectRevert(SortitionModule.NotStakingPhase.selector); sortitionModule.executeDelayedStakes(5); // Create delayed stake vm.prank(staker1); vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeDelayed(staker1, GENERAL_COURT, 1500); + emit SortitionModule.StakeDelayed(staker1, GENERAL_COURT, 1500); core.setStake(GENERAL_COURT, 1500); assertEq(pinakion.balanceOf(address(core)), 10000, "Wrong token balance of the core"); // Balance should not increase because the stake was delayed @@ -329,14 +329,14 @@ contract KlerosCore_StakingTest is KlerosCore_TestBase { // Create delayed stake for another staker vm.prank(staker2); vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeDelayed(staker2, GENERAL_COURT, 0); + emit SortitionModule.StakeDelayed(staker2, GENERAL_COURT, 0); core.setStake(GENERAL_COURT, 0); assertEq(pinakion.balanceOf(staker2), 999999999999990000, "Wrong token balance of staker2"); // Balance should not change since wrong phase // Create another delayed stake for staker1 on top of it to check the execution vm.prank(staker1); vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeDelayed(staker1, GENERAL_COURT, 1800); + emit SortitionModule.StakeDelayed(staker1, GENERAL_COURT, 1800); core.setStake(GENERAL_COURT, 1800); assertEq(sortitionModule.delayedStakeWriteIndex(), 3, "Wrong delayedStakeWriteIndex"); @@ -384,9 +384,9 @@ contract KlerosCore_StakingTest is KlerosCore_TestBase { // 2 events should be emitted but the 2nd stake supersedes the first one in the end. vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeSet(staker1, GENERAL_COURT, 1500, 1500); + emit SortitionModule.StakeSet(staker1, GENERAL_COURT, 1500, 1500); vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeSet(staker1, GENERAL_COURT, 1800, 1800); + emit SortitionModule.StakeSet(staker1, GENERAL_COURT, 1800, 1800); sortitionModule.executeDelayedStakes(20); // Deliberately ask for more iterations than needed assertEq(sortitionModule.delayedStakeWriteIndex(), 3, "Wrong delayedStakeWriteIndex"); diff --git a/contracts/test/foundry/KlerosCore_TestBase.sol b/contracts/test/foundry/KlerosCore_TestBase.sol index 612db5bb7..17132cb28 100644 --- a/contracts/test/foundry/KlerosCore_TestBase.sol +++ b/contracts/test/foundry/KlerosCore_TestBase.sol @@ -9,7 +9,7 @@ import {IDisputeKit} from "../../src/arbitration/interfaces/IDisputeKit.sol"; import {DisputeKitClassic, DisputeKitClassicBase} from "../../src/arbitration/dispute-kits/DisputeKitClassic.sol"; import {DisputeKitSybilResistant} from "../../src/arbitration/dispute-kits/DisputeKitSybilResistant.sol"; import {ISortitionModule} from "../../src/arbitration/interfaces/ISortitionModule.sol"; -import {SortitionModuleMock, SortitionModuleBase} from "../../src/test/SortitionModuleMock.sol"; +import {SortitionModuleMock, SortitionModule} from "../../src/test/SortitionModuleMock.sol"; import {UUPSProxy} from "../../src/proxy/UUPSProxy.sol"; import {BlockHashRNG} from "../../src/rng/BlockHashRNG.sol"; import {RNGWithFallback, IRNG} from "../../src/rng/RNGWithFallback.sol"; @@ -126,12 +126,14 @@ abstract contract KlerosCore_TestBase is Test { disputeKit = DisputeKitClassic(address(proxyDk)); bytes memory initDataSm = abi.encodeWithSignature( - "initialize(address,address,uint256,uint256,address)", + "initialize(address,address,uint256,uint256,address,uint256,uint256)", owner, address(proxyCore), minStakingTime, maxDrawingTime, - rng + rng, + type(int256).max, + type(int256).max ); UUPSProxy proxySm = new UUPSProxy(address(smLogic), initDataSm); From 758a809a6237ed2d171adb214b23a6c629a64613 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Thu, 4 Sep 2025 19:17:02 +0100 Subject: [PATCH 097/175] test: extra init asserts in foundry tests --- contracts/test/foundry/KlerosCore_Initialization.t.sol | 10 ++++++++-- contracts/test/foundry/KlerosCore_TestBase.sol | 4 ++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/contracts/test/foundry/KlerosCore_Initialization.t.sol b/contracts/test/foundry/KlerosCore_Initialization.t.sol index d6df4a932..7e521063d 100644 --- a/contracts/test/foundry/KlerosCore_Initialization.t.sol +++ b/contracts/test/foundry/KlerosCore_Initialization.t.sol @@ -45,6 +45,9 @@ contract KlerosCore_InitializationTest is KlerosCore_TestBase { assertEq(core.isSupported(GENERAL_COURT, NULL_DISPUTE_KIT), false, "General court null dk should be false"); assertEq(core.isSupported(GENERAL_COURT, DISPUTE_KIT_CLASSIC), true, "General court classic dk should be true"); assertEq(core.paused(), false, "Wrong paused value"); + assertEq(core.wNative(), address(wNative), "Wrong wNative"); + assertEq(address(core.jurorNft()), address(0), "Wrong jurorNft"); + assertEq(core.arbitrableWhitelistEnabled(), false, "Wrong arbitrableWhitelistEnabled"); assertEq(pinakion.name(), "Pinakion", "Wrong token name"); assertEq(pinakion.symbol(), "PNK", "Wrong token symbol"); @@ -71,6 +74,9 @@ contract KlerosCore_InitializationTest is KlerosCore_TestBase { assertEq(sortitionModule.randomNumber(), 0, "randomNumber should be 0"); assertEq(sortitionModule.delayedStakeWriteIndex(), 0, "delayedStakeWriteIndex should be 0"); assertEq(sortitionModule.delayedStakeReadIndex(), 1, "Wrong delayedStakeReadIndex"); + assertEq(sortitionModule.maxStakePerJuror(), type(uint256).max, "Wrong maxStakePerJuror"); + assertEq(sortitionModule.maxTotalStaked(), type(uint256).max, "Wrong maxTotalStaked"); + assertEq(sortitionModule.totalStaked(), 0, "Wrong totalStaked"); (uint256 K, uint256 nodeLength) = sortitionModule.getSortitionProperties(bytes32(uint256(FORKING_COURT))); assertEq(K, 5, "Wrong tree K FORKING_COURT"); @@ -129,8 +135,8 @@ contract KlerosCore_InitializationTest is KlerosCore_TestBase { newMinStakingTime, newMaxDrawingTime, newRng, - type(int256).max, - type(int256).max + type(uint256).max, + type(uint256).max ); UUPSProxy proxySm = new UUPSProxy(address(smLogic), initDataSm); diff --git a/contracts/test/foundry/KlerosCore_TestBase.sol b/contracts/test/foundry/KlerosCore_TestBase.sol index 17132cb28..9418f2444 100644 --- a/contracts/test/foundry/KlerosCore_TestBase.sol +++ b/contracts/test/foundry/KlerosCore_TestBase.sol @@ -132,8 +132,8 @@ abstract contract KlerosCore_TestBase is Test { minStakingTime, maxDrawingTime, rng, - type(int256).max, - type(int256).max + type(uint256).max, + type(uint256).max ); UUPSProxy proxySm = new UUPSProxy(address(smLogic), initDataSm); From 7a5d08df33f6872630ba38e0b880b7aad5bf137e Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Thu, 4 Sep 2025 22:55:57 +0100 Subject: [PATCH 098/175] fix: hardhat tests, deploy scripts, unnecessary virtual keyword --- .../deploy/00-home-chain-arbitration-neo.ts | 24 +++++++++---------- contracts/deploy/00-home-chain-arbitration.ts | 11 ++++++++- contracts/src/arbitration/KlerosCore.sol | 12 +++++++--- contracts/src/arbitration/SortitionModule.sol | 9 ++++--- contracts/test/arbitration/staking-neo.ts | 16 ++++++------- 5 files changed, 42 insertions(+), 30 deletions(-) diff --git a/contracts/deploy/00-home-chain-arbitration-neo.ts b/contracts/deploy/00-home-chain-arbitration-neo.ts index d5d8a136c..879573676 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, RNGWithFallback } from "../typechain-types"; +import { ChainlinkRNG, DisputeKitClassic, KlerosCore, RNGWithFallback } from "../typechain-types"; const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const { ethers, deployments, getNamedAccounts, getChainId } = hre; @@ -29,18 +29,17 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) await deployUpgradable(deployments, "EvidenceModule", { from: deployer, args: [deployer], log: true }); const classicDisputeKitID = 1; // Classic DK - const disputeKit = await deployUpgradable(deployments, "DisputeKitClassicNeo", { + const disputeKit = await deployUpgradable(deployments, "DisputeKitClassic", { from: deployer, - contract: "DisputeKitClassic", args: [deployer, ZeroAddress, weth.target, classicDisputeKitID], log: true, }); - let klerosCoreAddress = await deployments.getOrNull("KlerosCoreNeo").then((deployment) => deployment?.address); + let klerosCoreAddress = await deployments.getOrNull("KlerosCore").then((deployment) => deployment?.address); if (!klerosCoreAddress) { const nonce = await ethers.provider.getTransactionCount(deployer); klerosCoreAddress = getContractAddress(deployer, nonce + 3); // deployed on the 4th tx (nonce+3): SortitionModule Impl tx, SortitionModule Proxy tx, KlerosCore Impl tx, KlerosCore Proxy tx - console.log("calculated future KlerosCoreNeo address for nonce %d: %s", nonce + 3, klerosCoreAddress); + console.log("calculated future KlerosCore address for nonce %d: %s", nonce + 3, klerosCoreAddress); } const devnet = isDevnet(hre.network); const minStakingTime = devnet ? 180 : 1800; @@ -48,7 +47,7 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) const rngWithFallback = await ethers.getContract("RNGWithFallback"); const maxStakePerJuror = PNK(2_000); const maxTotalStaked = PNK(2_000_000); - const sortitionModule = await deployUpgradable(deployments, "SortitionModuleNeo", { + const sortitionModule = await deployUpgradable(deployments, "SortitionModule", { from: deployer, args: [ deployer, @@ -66,7 +65,7 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) const alpha = 10000; const feeForJuror = ETH(0.1); const jurorsForCourtJump = 256; - const klerosCore = await deployUpgradable(deployments, "KlerosCoreNeo", { + const klerosCore = await deployUpgradable(deployments, "KlerosCore", { from: deployer, args: [ deployer, @@ -79,14 +78,14 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) [0, 0, 0, 10], // evidencePeriod, commitPeriod, votePeriod, appealPeriod ethers.toBeHex(5), // Extra data for sortition module will return the default value of K sortitionModule.address, - nft.target, weth.target, + nft.target, ], log: true, }); // nonce+2 (implementation), nonce+3 (proxy) // disputeKit.changeCore() only if necessary - const disputeKitContract = await hre.ethers.getContract("DisputeKitClassicNeo"); + const disputeKitContract = await hre.ethers.getContract("DisputeKitClassic"); const currentCore = await disputeKitContract.core(); if (currentCore !== klerosCore.address) { console.log(`disputeKit.changeCore(${klerosCore.address})`); @@ -100,7 +99,7 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) await rngWithFallback.changeConsumer(sortitionModule.address); } - const core = await hre.ethers.getContract("KlerosCoreNeo"); + const core = await hre.ethers.getContract("KlerosCore"); try { await changeCurrencyRate(core, await weth.getAddress(), true, 1, 1); } catch (e) { @@ -113,9 +112,8 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) log: true, }); - const resolver = await deploy("DisputeResolverNeo", { + const resolver = await deploy("DisputeResolver", { from: deployer, - contract: "DisputeResolver", args: [core.target, disputeTemplateRegistry.target], log: true, }); @@ -157,7 +155,7 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) }); }; -deployArbitration.tags = ["ArbitrationNeo"]; +deployArbitration.tags = ["ArbitrationMainnet"]; deployArbitration.dependencies = ["ChainlinkRNG"]; deployArbitration.skip = async ({ network }) => { return isSkipped(network, !HomeChains[network.config.chainId ?? 0]); diff --git a/contracts/deploy/00-home-chain-arbitration.ts b/contracts/deploy/00-home-chain-arbitration.ts index 25291052d..72334eb77 100644 --- a/contracts/deploy/00-home-chain-arbitration.ts +++ b/contracts/deploy/00-home-chain-arbitration.ts @@ -53,7 +53,15 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) const rngWithFallback = await ethers.getContract("RNGWithFallback"); const sortitionModule = await deployUpgradable(deployments, "SortitionModule", { from: deployer, - args: [deployer, klerosCoreAddress, minStakingTime, maxFreezingTime, rngWithFallback.target], + args: [ + deployer, + klerosCoreAddress, + minStakingTime, + maxFreezingTime, + rngWithFallback.target, + ethers.MaxUint256, // maxStakePerJuror + ethers.MaxUint256, // maxTotalStaked + ], log: true, }); // nonce (implementation), nonce+1 (proxy) @@ -75,6 +83,7 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) ethers.toBeHex(5), // Extra data for sortition module will return the default value of K sortitionModule.address, weth.target, + ZeroAddress, // jurorNft ], log: true, }); // nonce+2 (implementation), nonce+3 (proxy) diff --git a/contracts/src/arbitration/KlerosCore.sol b/contracts/src/arbitration/KlerosCore.sol index f7d6cf493..a8c78a910 100644 --- a/contracts/src/arbitration/KlerosCore.sol +++ b/contracts/src/arbitration/KlerosCore.sol @@ -487,6 +487,12 @@ contract KlerosCore is IArbitratorV2, Initializable, UUPSProxiable { emit NewCurrencyRate(_feeToken, _rateInEth, _rateDecimals); } + /// @dev Changes the `jurorNft` storage variable. + /// @param _jurorNft The new value for the `jurorNft` storage variable. + function changeJurorNft(IERC721 _jurorNft) external onlyByOwner { + jurorNft = _jurorNft; + } + /// @dev Adds or removes an arbitrable from whitelist. /// @param _arbitrable Arbitrable address. /// @param _allowed Whether add or remove permission. @@ -507,7 +513,7 @@ contract KlerosCore is IArbitratorV2, Initializable, UUPSProxiable { /// @param _courtID The ID of the court. /// @param _newStake The new stake. /// Note that the existing delayed stake will be nullified as non-relevant. - function setStake(uint96 _courtID, uint256 _newStake) external virtual whenNotPaused { + function setStake(uint96 _courtID, uint256 _newStake) external whenNotPaused { if (address(jurorNft) != address(0) && jurorNft.balanceOf(msg.sender) == 0) revert NotEligibleForStaking(); _setStake(msg.sender, _courtID, _newStake, false, OnError.Revert); } @@ -559,7 +565,7 @@ contract KlerosCore is IArbitratorV2, Initializable, UUPSProxiable { bytes memory _extraData, IERC20 _feeToken, uint256 _feeAmount - ) internal virtual returns (uint256 disputeID) { + ) internal returns (uint256 disputeID) { if (arbitrableWhitelistEnabled && !arbitrableWhitelist[msg.sender]) revert ArbitrableNotWhitelisted(); (uint96 courtID, , uint256 disputeKitID) = _extraDataToCourtIDMinJurorsDisputeKit(_extraData); if (!courts[courtID].supportedDisputeKits[disputeKitID]) revert DisputeKitNotSupportedByCourt(); @@ -1236,7 +1242,7 @@ contract KlerosCore is IArbitratorV2, Initializable, UUPSProxiable { } /// @dev It may revert depending on the _onError parameter. - function _stakingFailed(OnError _onError, StakingResult _result) internal pure virtual { + function _stakingFailed(OnError _onError, StakingResult _result) internal pure { if (_onError == OnError.Return) return; if (_result == StakingResult.StakingTransferFailed) revert StakingTransferFailed(); if (_result == StakingResult.UnstakingTransferFailed) revert UnstakingTransferFailed(); diff --git a/contracts/src/arbitration/SortitionModule.sol b/contracts/src/arbitration/SortitionModule.sol index 8c0f4de86..bf61255ae 100644 --- a/contracts/src/arbitration/SortitionModule.sol +++ b/contracts/src/arbitration/SortitionModule.sol @@ -278,7 +278,7 @@ contract SortitionModule is ISortitionModule, Initializable, UUPSProxiable { uint96 _courtID, uint256 _newStake, bool _noDelay - ) internal virtual returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { + ) internal returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { Juror storage juror = jurors[_account]; uint256 currentStake = stakeOf(_account, _courtID); bool stakeIncrease = _newStake > currentStake; @@ -319,13 +319,12 @@ contract SortitionModule is ISortitionModule, Initializable, UUPSProxiable { totalStaked += stakeChange; } else { pnkWithdrawal = stakeChange; - totalStaked -= stakeChange; - - // Ensure locked tokens remain in the contract. They can only be released during Execution. uint256 possibleWithdrawal = juror.stakedPnk > juror.lockedPnk ? juror.stakedPnk - juror.lockedPnk : 0; if (pnkWithdrawal > possibleWithdrawal) { + // Ensure locked tokens remain in the contract. They can only be released during Execution. pnkWithdrawal = possibleWithdrawal; } + totalStaked -= pnkWithdrawal; } return (pnkDeposit, pnkWithdrawal, StakingResult.Successful); } @@ -417,7 +416,7 @@ contract SortitionModule is ISortitionModule, Initializable, UUPSProxiable { uint256 _pnkDeposit, uint256 _pnkWithdrawal, uint256 _newStake - ) internal virtual { + ) internal { Juror storage juror = jurors[_account]; if (_pnkDeposit > 0) { uint256 currentStake = stakeOf(_account, _courtID); diff --git a/contracts/test/arbitration/staking-neo.ts b/contracts/test/arbitration/staking-neo.ts index fb36787d8..9cd83efeb 100644 --- a/contracts/test/arbitration/staking-neo.ts +++ b/contracts/test/arbitration/staking-neo.ts @@ -3,8 +3,8 @@ import { PNK, RandomizerRNG, RandomizerMock, - SortitionModuleNeo, - KlerosCoreNeo, + SortitionModule, + KlerosCore, TestERC721, DisputeResolver, ChainlinkRNG, @@ -41,8 +41,8 @@ describe("Staking", async () => { let juror: HardhatEthersSigner; let guardian: HardhatEthersSigner; let pnk: PNK; - let core: KlerosCoreNeo; - let sortition: SortitionModuleNeo; + let core: KlerosCore; + let sortition: SortitionModule; let rng: ChainlinkRNG; let vrfCoordinator: ChainlinkVRFCoordinatorV2Mock; let nft: TestERC721; @@ -52,16 +52,16 @@ describe("Staking", async () => { const deployUnhappy = async () => { ({ deployer } = await getNamedAccounts()); - await deployments.fixture(["ArbitrationNeo"], { + await deployments.fixture(["ArbitrationMainnet"], { fallbackToGlobal: true, keepExistingDeployments: false, }); pnk = await ethers.getContract("PNK"); - core = await ethers.getContract("KlerosCoreNeo"); - sortition = await ethers.getContract("SortitionModuleNeo"); + core = await ethers.getContract("KlerosCore"); + sortition = await ethers.getContract("SortitionModule"); rng = await ethers.getContract("ChainlinkRNG"); vrfCoordinator = await ethers.getContract("ChainlinkVRFCoordinator"); - resolver = await ethers.getContract("DisputeResolverNeo"); + resolver = await ethers.getContract("DisputeResolver"); nft = await ethers.getContract("KlerosV2NeoEarlyUser"); // Juror signer setup and funding From e9f8a0a186f1cbf1ad3c5853e4035f931d7cd894 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Thu, 4 Sep 2025 22:58:16 +0100 Subject: [PATCH 099/175] refactor: renamed 00-home-chain-arbitration-neo into 00-home-chain-arbitration-mainnet --- ...in-arbitration-neo.ts => 00-home-chain-arbitration-mainnet.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename contracts/deploy/{00-home-chain-arbitration-neo.ts => 00-home-chain-arbitration-mainnet.ts} (100%) diff --git a/contracts/deploy/00-home-chain-arbitration-neo.ts b/contracts/deploy/00-home-chain-arbitration-mainnet.ts similarity index 100% rename from contracts/deploy/00-home-chain-arbitration-neo.ts rename to contracts/deploy/00-home-chain-arbitration-mainnet.ts From 923bd7bdc2f3af082c050a1d2247f55347e72edd Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Thu, 4 Sep 2025 23:14:58 +0100 Subject: [PATCH 100/175] chore: removed traces of Neo in many scripts and filenames --- ...ainnet-neo.json => courts.v2.mainnet.json} | 0 ...nnet-neo.json => policies.v2.mainnet.json} | 0 .../00-home-chain-arbitration-mainnet.ts | 2 +- contracts/deploy/00-home-chain-arbitration.ts | 2 +- .../deploy/change-sortition-module-rng.ts | 14 ++----- contracts/deploy/upgrade-all.ts | 41 ++++--------------- contracts/deploy/utils/klerosCoreHelper.ts | 4 +- contracts/package.json | 6 +-- .../scripts/generateDeploymentsMarkdown.sh | 6 +-- contracts/scripts/storage-layout.ts | 4 +- 10 files changed, 22 insertions(+), 57 deletions(-) rename contracts/config/{courts.v2.mainnet-neo.json => courts.v2.mainnet.json} (100%) rename contracts/config/{policies.v2.mainnet-neo.json => policies.v2.mainnet.json} (100%) diff --git a/contracts/config/courts.v2.mainnet-neo.json b/contracts/config/courts.v2.mainnet.json similarity index 100% rename from contracts/config/courts.v2.mainnet-neo.json rename to contracts/config/courts.v2.mainnet.json diff --git a/contracts/config/policies.v2.mainnet-neo.json b/contracts/config/policies.v2.mainnet.json similarity index 100% rename from contracts/config/policies.v2.mainnet-neo.json rename to contracts/config/policies.v2.mainnet.json diff --git a/contracts/deploy/00-home-chain-arbitration-mainnet.ts b/contracts/deploy/00-home-chain-arbitration-mainnet.ts index 879573676..a3d87f51f 100644 --- a/contracts/deploy/00-home-chain-arbitration-mainnet.ts +++ b/contracts/deploy/00-home-chain-arbitration-mainnet.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, KlerosCore, RNGWithFallback } from "../typechain-types"; +import { DisputeKitClassic, KlerosCore, RNGWithFallback } from "../typechain-types"; const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const { ethers, deployments, getNamedAccounts, getChainId } = hre; diff --git a/contracts/deploy/00-home-chain-arbitration.ts b/contracts/deploy/00-home-chain-arbitration.ts index 72334eb77..1c4c29695 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, RNGWithFallback } from "../typechain-types"; +import { DisputeKitClassic, KlerosCore, RNGWithFallback } from "../typechain-types"; const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const { ethers, deployments, getNamedAccounts, getChainId } = hre; diff --git a/contracts/deploy/change-sortition-module-rng.ts b/contracts/deploy/change-sortition-module-rng.ts index 2b5e72435..986e408ec 100644 --- a/contracts/deploy/change-sortition-module-rng.ts +++ b/contracts/deploy/change-sortition-module-rng.ts @@ -1,8 +1,8 @@ import { HardhatRuntimeEnvironment } from "hardhat/types"; import { DeployFunction } from "hardhat-deploy/types"; -import { HomeChains, isMainnet, isSkipped } from "./utils"; +import { HomeChains, isSkipped } from "./utils"; import { ethers } from "hardhat"; -import { ChainlinkRNG, SortitionModule, SortitionModuleNeo } from "../typechain-types"; +import { ChainlinkRNG, SortitionModule } from "../typechain-types"; const task: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const { getNamedAccounts, getChainId } = hre; @@ -13,15 +13,7 @@ const task: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { console.log("deploying to %s with deployer %s", HomeChains[chainId], deployer); const chainlinkRng = await ethers.getContract("ChainlinkRNG"); - - let sortitionModule: SortitionModule | SortitionModuleNeo; - if (isMainnet(hre.network)) { - console.log("Using SortitionModuleNeo"); - sortitionModule = await ethers.getContract("SortitionModuleNeo"); - } else { - console.log("Using SortitionModule"); - sortitionModule = await ethers.getContract("SortitionModule"); - } + const sortitionModule = await ethers.getContract("SortitionModule"); console.log(`chainlinkRng.changeConsumer(${sortitionModule.target})`); await chainlinkRng.changeConsumer(sortitionModule.target); diff --git a/contracts/deploy/upgrade-all.ts b/contracts/deploy/upgrade-all.ts index dfd0d882c..572ed7187 100644 --- a/contracts/deploy/upgrade-all.ts +++ b/contracts/deploy/upgrade-all.ts @@ -3,7 +3,7 @@ import { DeployFunction } from "hardhat-deploy/types"; import { prompt, print } from "gluegun"; import { deployUpgradable } from "./utils/deployUpgradable"; import { HomeChains, isSkipped } from "./utils"; -import { getContractNames, getContractNamesFromNetwork } from "../scripts/utils/contracts"; +import { getContractNamesFromNetwork } from "../scripts/utils/contracts"; const { bold } = print.colors; @@ -36,21 +36,7 @@ const deployUpgradeAll: DeployFunction = async (hre: HardhatRuntimeEnvironment) try { print.highlight(`🔍 Validating upgrade of ${bold(contractName)}`); - const contractNameWithoutNeo = contractName.replace(/Neo.*$/, ""); let compareStorageOptions = { contract: contractName } as any; - if (hre.network.name === "arbitrum") { - switch (contractName) { - case "KlerosCoreNeo": - case "SortitionModuleNeo": - compareStorageOptions = { deployedArtifact: `${contractName}_Implementation`, contract: contractName }; - break; - default: - compareStorageOptions = { - deployedArtifact: `${contractName}_Implementation`, - contract: contractNameWithoutNeo, - }; - } - } await hre.run("compare-storage", compareStorageOptions); print.newline(); print.highlight(`💣 Upgrading ${bold(contractName)}`); @@ -65,25 +51,12 @@ const deployUpgradeAll: DeployFunction = async (hre: HardhatRuntimeEnvironment) } print.info(`Upgrading ${contractName}...`); - switch (contractName) { - case "DisputeKitClassicNeo": - case "DisputeResolverNeo": - await deployUpgradable(deployments, contractName, { - contract: contractName, - newImplementation: contractNameWithoutNeo, - initializer, - from: deployer, - args, // Warning: do not reinitialize existing state variables, only the new ones - }); - break; - default: - await deployUpgradable(deployments, contractName, { - newImplementation: contractName, - initializer, - from: deployer, - args, // Warning: do not reinitialize existing state variables, only the new ones - }); - } + await deployUpgradable(deployments, contractName, { + newImplementation: contractName, + initializer, + from: deployer, + args, // Warning: do not reinitialize existing state variables, only the new ones + }); print.info(`Verifying ${contractName} on Etherscan...`); await hre.run("etherscan-verify", { contractName: `${contractName}_Implementation` }); } catch (err) { diff --git a/contracts/deploy/utils/klerosCoreHelper.ts b/contracts/deploy/utils/klerosCoreHelper.ts index 3419ae64e..3325652a3 100644 --- a/contracts/deploy/utils/klerosCoreHelper.ts +++ b/contracts/deploy/utils/klerosCoreHelper.ts @@ -1,8 +1,8 @@ -import { KlerosCore, KlerosCoreNeo, KlerosCoreRuler, KlerosCoreUniversity } from "../../typechain-types"; +import { KlerosCore, KlerosCoreRuler, KlerosCoreUniversity } from "../../typechain-types"; import { BigNumberish, toBigInt } from "ethers"; export const changeCurrencyRate = async ( - core: KlerosCore | KlerosCoreNeo | KlerosCoreRuler | KlerosCoreUniversity, + core: KlerosCore | KlerosCoreRuler | KlerosCoreUniversity, erc20: string, accepted: boolean, rateInEth: BigNumberish, diff --git a/contracts/package.json b/contracts/package.json index 0bca4e700..fd40b404d 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -91,11 +91,11 @@ "docbuild": "scripts/docPreprocess.sh && forge doc --build --out dist && scripts/docPostprocess.sh", "populate:courts:devnet": "hardhat populate:courts --from v2_devnet --network arbitrumSepoliaDevnet", "populate:courts:testnet": "hardhat populate:courts --from v2_testnet --network arbitrumSepolia", - "populate:courts:mainnetNeo": "hardhat populate:courts --core-type neo --from v2_mainnet_neo --network arbitrum", - "populate:policiesUris": "scripts/setPoliciesURIs.sh config/policies.v2.{devnet,testnet,mainnet-neo}.json", + "populate:courts:mainnet": "hardhat populate:courts --from v2_mainnet --network arbitrum", + "populate:policiesUris": "scripts/setPoliciesURIs.sh config/policies.v2.{devnet,testnet,mainnet}.json", "populate:policies:devnet": "hardhat populate:policy-registry --from v2_devnet --network arbitrumSepoliaDevnet", "populate:policies:testnet": "hardhat populate:policy-registry --from v2_testnet --network arbitrumSepolia", - "populate:policies:mainnetNeo": "hardhat populate:policy-registry --from v2_mainnet_neo --network arbitrum", + "populate:policies:mainnet": "hardhat populate:policy-registry --from v2_mainnet --network arbitrum", "release:patch": "scripts/publish.sh patch", "release:minor": "scripts/publish.sh minor", "release:major": "scripts/publish.sh major", diff --git a/contracts/scripts/generateDeploymentsMarkdown.sh b/contracts/scripts/generateDeploymentsMarkdown.sh index 6fa3c2932..43c3bf9b7 100755 --- a/contracts/scripts/generateDeploymentsMarkdown.sh +++ b/contracts/scripts/generateDeploymentsMarkdown.sh @@ -28,12 +28,12 @@ function generate() { #deploymentDir #explorerUrl done } -echo "### V2 Neo (prelaunch)" +echo "### V2 Mainnet" echo "#### Arbitrum One" echo generate "$SCRIPT_DIR/../deployments/arbitrum" "https://arbiscan.io/address/" | grep -v 'DAI\|WETH\|PNKFaucet' echo -echo "### Official Testnet" +echo "### V2 Testnet" echo "#### Arbitrum Sepolia" echo generate "$SCRIPT_DIR/../deployments/arbitrumSepolia" "https://sepolia.arbiscan.io/address/" @@ -47,7 +47,7 @@ echo generate "$SCRIPT_DIR/../deployments/chiado" "https://gnosis-chiado.blockscout.com/address/" echo -echo "### Devnet" +echo "### V2 Devnet (unstable)" echo "#### Arbitrum Sepolia" echo generate "$SCRIPT_DIR/../deployments/arbitrumSepoliaDevnet" "https://sepolia.arbiscan.io/address/" diff --git a/contracts/scripts/storage-layout.ts b/contracts/scripts/storage-layout.ts index 697b7d48f..70cf166e7 100644 --- a/contracts/scripts/storage-layout.ts +++ b/contracts/scripts/storage-layout.ts @@ -4,7 +4,7 @@ import { HardhatRuntimeEnvironment } from "hardhat/types"; task("storage-layout", "Prints the storage layout of a contract").setAction( async ({}, hre: HardhatRuntimeEnvironment) => { await hre.run("compile"); - const buildInfo = await hre.artifacts.getBuildInfo(`src/arbitration/KlerosCoreNeo.sol:KlerosCoreNeo`); - console.log(buildInfo.output.contracts["src/arbitration/KlerosCoreNeo.sol"]["KlerosCoreNeo"].storageLayout); + const buildInfo = await hre.artifacts.getBuildInfo(`src/arbitration/KlerosCore.sol:KlerosCore`); + console.log(buildInfo.output.contracts["src/arbitration/KlerosCore.sol"]["KlerosCore"].storageLayout); } ); From daa8bf944327075a403298a4bdf3f9a017a81db2 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Thu, 4 Sep 2025 23:46:24 +0100 Subject: [PATCH 101/175] chore: removed traces of Neo in more scripts --- contracts/deployments/contractsEthers.ts | 14 +++---- contracts/deployments/contractsViem.ts | 2 +- contracts/deployments/utils.ts | 2 +- contracts/scripts/changeOwner.ts | 4 +- .../scripts/generateDeploymentArtifact.sh | 2 +- .../scripts/generateDeploymentsMarkdown.sh | 1 + contracts/scripts/keeperBot.ts | 7 +--- contracts/scripts/keeperBotShutter.ts | 5 +-- contracts/scripts/populateCourts.ts | 18 ++++----- contracts/scripts/populatePolicyRegistry.ts | 12 +++--- contracts/scripts/utils/contracts.ts | 40 +++++-------------- .../integration/getContractsEthers.test.ts | 14 +++---- .../test/integration/getContractsViem.test.ts | 8 ++-- 13 files changed, 50 insertions(+), 79 deletions(-) diff --git a/contracts/deployments/contractsEthers.ts b/contracts/deployments/contractsEthers.ts index 49bc0552e..35e204488 100644 --- a/contracts/deployments/contractsEthers.ts +++ b/contracts/deployments/contractsEthers.ts @@ -92,10 +92,6 @@ import { KlerosCoreUniversity__factory, SortitionModuleUniversity, SortitionModuleUniversity__factory, - KlerosCoreNeo, - KlerosCoreNeo__factory, - SortitionModuleNeo, - SortitionModuleNeo__factory, } from "../typechain-types"; import { type ContractConfig, type DeploymentName, deployments, getAddress } from "./utils"; @@ -171,8 +167,8 @@ function getCommonFactories( export const getContracts = async (provider: ethers.Provider, deployment: DeploymentName) => { const { chainId } = deployments[deployment]; - let klerosCore: KlerosCore | KlerosCoreNeo | KlerosCoreUniversity; - let sortition: SortitionModule | SortitionModuleNeo | SortitionModuleUniversity; + let klerosCore: KlerosCore | KlerosCoreUniversity; + let sortition: SortitionModule | SortitionModuleUniversity; let commonFactories: CommonFactories; switch (deployment) { @@ -247,9 +243,9 @@ export const getContracts = async (provider: ethers.Provider, deployment: Deploy chainId ); break; - case "mainnetNeo": - klerosCore = KlerosCoreNeo__factory.connect(getAddress(mainnetCoreConfig, chainId), provider); - sortition = SortitionModuleNeo__factory.connect(getAddress(mainnetSortitionConfig, chainId), provider); + case "mainnet": + klerosCore = KlerosCore__factory.connect(getAddress(mainnetCoreConfig, chainId), provider); + sortition = SortitionModule__factory.connect(getAddress(mainnetSortitionConfig, chainId), provider); commonFactories = getCommonFactories( { dkClassicConfig: mainnetDkcConfig, diff --git a/contracts/deployments/contractsViem.ts b/contracts/deployments/contractsViem.ts index 792ce5b3e..9e3abc4f8 100644 --- a/contracts/deployments/contractsViem.ts +++ b/contracts/deployments/contractsViem.ts @@ -203,7 +203,7 @@ export const getConfigs = ({ deployment }: { deployment: DeploymentName }): Cont }, }); - case "mainnetNeo": + case "mainnet": return getCommonConfigs({ chainId, configs: { diff --git a/contracts/deployments/utils.ts b/contracts/deployments/utils.ts index e2711a377..a876fbd5a 100644 --- a/contracts/deployments/utils.ts +++ b/contracts/deployments/utils.ts @@ -10,7 +10,7 @@ export const deployments = { testnet: { chainId: arbitrumSepolia.id, }, - mainnetNeo: { + mainnet: { chainId: arbitrum.id, }, } as const; diff --git a/contracts/scripts/changeOwner.ts b/contracts/scripts/changeOwner.ts index 72eeaeef7..fcb64137b 100644 --- a/contracts/scripts/changeOwner.ts +++ b/contracts/scripts/changeOwner.ts @@ -7,7 +7,7 @@ const { bold } = print.colors; task("change-owner", "Changes the owner for all the contracts") .addPositionalParam("newOwner", "The address of the new owner") - .addOptionalParam("coreType", "The type of core to use between base, neo, university (default: base)", Cores.BASE) + .addOptionalParam("coreType", "The type of core to use between base, university (default: base)", Cores.BASE) .setAction(async (taskArgs, hre) => { const newOwner = taskArgs.newOwner; if (!isAddress(newOwner)) { @@ -27,7 +27,7 @@ task("change-owner", "Changes the owner for all the contracts") const coreType = Cores[taskArgs.coreType.toUpperCase() as keyof typeof Cores]; if (coreType === undefined) { - console.error("Invalid core type, must be one of base, neo, university"); + console.error("Invalid core type, must be one of base, university"); return; } console.log("Using core type %s", coreType); diff --git a/contracts/scripts/generateDeploymentArtifact.sh b/contracts/scripts/generateDeploymentArtifact.sh index dedcf1c0b..6daf2a585 100755 --- a/contracts/scripts/generateDeploymentArtifact.sh +++ b/contracts/scripts/generateDeploymentArtifact.sh @@ -2,7 +2,7 @@ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -if [[ $# < 2 ]] +if [[ $# -lt 2 ]] then echo "usage: $(basename $0) network address" exit 1 diff --git a/contracts/scripts/generateDeploymentsMarkdown.sh b/contracts/scripts/generateDeploymentsMarkdown.sh index 43c3bf9b7..5ae9dbf7f 100755 --- a/contracts/scripts/generateDeploymentsMarkdown.sh +++ b/contracts/scripts/generateDeploymentsMarkdown.sh @@ -15,6 +15,7 @@ IGNORED_ARTIFACTS=( function generate() { #deploymentDir #explorerUrl deploymentDir=$1 explorerUrl=$2 + # shellcheck disable=SC2068 for f in $(ls -1 $deploymentDir/*.json 2>/dev/null | grep -v ${IGNORED_ARTIFACTS[@]/#/-e } | sort); do contractName=$(basename $f .json) address=$(cat $f | jq -r .address) diff --git a/contracts/scripts/keeperBot.ts b/contracts/scripts/keeperBot.ts index 593d97e4a..5d1b2751f 100644 --- a/contracts/scripts/keeperBot.ts +++ b/contracts/scripts/keeperBot.ts @@ -6,7 +6,6 @@ import { DisputeKitGatedShutter, DisputeKitShutter, SortitionModule, - SortitionModuleNeo, } from "../typechain-types"; import env from "./utils/env"; import loggerFactory from "./utils/logger"; @@ -49,7 +48,7 @@ const getContracts = async () => { throw new Error("University is not supported yet"); } const contracts = await getContractsForCoreType(hre, coreType); - return { ...contracts, sortition: contracts.sortition as SortitionModule | SortitionModuleNeo }; + return { ...contracts, sortition: contracts.sortition as SortitionModule }; }; type Contribution = { @@ -277,9 +276,7 @@ const isRngReady = async () => { return true; } } else if (currentRng === blockHashRNG?.target && blockHashRNG !== null) { - const requestBlock = await sortition.randomNumberRequestBlock(); - const lookahead = await sortition.rngLookahead(); - const n = await blockHashRNG.receiveRandomness.staticCall(requestBlock + lookahead); + const n = await blockHashRNG.receiveRandomness.staticCall(); if (Number(n) === 0) { logger.info("BlockHashRNG is NOT ready yet"); return false; diff --git a/contracts/scripts/keeperBotShutter.ts b/contracts/scripts/keeperBotShutter.ts index 0b9724cae..fff463513 100644 --- a/contracts/scripts/keeperBotShutter.ts +++ b/contracts/scripts/keeperBotShutter.ts @@ -1,6 +1,6 @@ import hre from "hardhat"; import { getBytes } from "ethers"; -import { DisputeKitGatedShutter, DisputeKitShutter, SortitionModule, SortitionModuleNeo } from "../typechain-types"; +import { DisputeKitGatedShutter, DisputeKitShutter } from "../typechain-types"; import { decrypt } from "./shutter"; import env from "./utils/env"; import loggerFactory from "./utils/logger"; @@ -312,8 +312,7 @@ const getContracts = async () => { if (coreType === Cores.UNIVERSITY) { throw new Error("University is not supported yet"); } - const contracts = await getContractsForCoreType(hre, coreType); - return { ...contracts, sortition: contracts.sortition as SortitionModule | SortitionModuleNeo }; + return await getContractsForCoreType(hre, coreType); }; async function main() { diff --git a/contracts/scripts/populateCourts.ts b/contracts/scripts/populateCourts.ts index ee95dee71..cf86c9cbb 100644 --- a/contracts/scripts/populateCourts.ts +++ b/contracts/scripts/populateCourts.ts @@ -1,11 +1,11 @@ import { task, types } from "hardhat/config"; -import { KlerosCore, KlerosCoreNeo, KlerosCoreUniversity } from "../typechain-types"; +import { KlerosCore, KlerosCoreUniversity } from "../typechain-types"; import { BigNumberish, toBigInt, toNumber } from "ethers"; import courtsV1Mainnet from "../config/courts.v1.mainnet.json"; import courtsV1GnosisChain from "../config/courts.v1.gnosischain.json"; import courtsV2ArbitrumTestnet from "../config/courts.v2.testnet.json"; import courtsV2ArbitrumDevnet from "../config/courts.v2.devnet.json"; -import courtsV2MainnetNeo from "../config/courts.v2.mainnet-neo.json"; +import courtsV2Mainnet from "../config/courts.v2.mainnet.json"; import { isDevnet } from "../deploy/utils"; import { execute, writeTransactionBatch } from "./utils/execution"; import { getContracts, Cores } from "./utils/contracts"; @@ -21,7 +21,7 @@ enum Sources { V1_GNOSIS, V2_DEVNET, V2_TESTNET, - V2_MAINNET_NEO, + V2_MAINNET, } type Court = { @@ -43,7 +43,7 @@ const TEN_THOUSAND_GWEI = 10n ** 13n; task("populate:courts", "Populates the courts and their parameters") .addOptionalParam( "from", - "The source of the policies between v1_mainnet, v1_gnosis, v2_devnet, v2_testnet, v2_mainnet_neo (default: auto depending on the network)", + "The source of the policies between v1_mainnet, v1_gnosis, v2_devnet, v2_testnet, v2_mainnet (default: auto depending on the network)", undefined ) .addOptionalParam("start", "The starting index for the courts to populate (default: 0)", 0, types.int) @@ -53,7 +53,7 @@ task("populate:courts", "Populates the courts and their parameters") undefined, types.int ) - .addOptionalParam("coreType", "The type of core to use between base, neo, university (default: base)", Cores.BASE) + .addOptionalParam("coreType", "The type of core to use between base, university (default: base)", Cores.BASE) .addFlag("reverse", "Iterates the courts in reverse order, useful to increase minStake in the child courts first") .addFlag("forceV1ParametersToDev", "Use development values for the v1 courts parameters") .setAction(async (taskArgs, hre) => { @@ -74,7 +74,7 @@ task("populate:courts", "Populates the courts and their parameters") if (taskArgs.from) { from = Sources[taskArgs.from.toUpperCase() as keyof typeof Sources]; if (from === undefined) { - console.error("Invalid source, must be one of v1_mainnet, v1_gnosis, v2_devnet, v2_testnet, v2_mainnet_neo"); + console.error("Invalid source, must be one of v1_mainnet, v1_gnosis, v2_devnet, v2_testnet, v2_mainnet"); return; } } else { @@ -84,7 +84,7 @@ task("populate:courts", "Populates the courts and their parameters") const coreType = Cores[taskArgs.coreType.toUpperCase() as keyof typeof Cores]; if (coreType === undefined) { - console.error("Invalid core type, must be one of base, neo, university"); + console.error("Invalid core type, must be one of base, university"); return; } console.log("Using core type %s", coreType); @@ -133,8 +133,8 @@ task("populate:courts", "Populates the courts and their parameters") courtsV2 = courtsV2ArbitrumTestnet; break; } - case Sources.V2_MAINNET_NEO: { - courtsV2 = courtsV2MainnetNeo; + case Sources.V2_MAINNET: { + courtsV2 = courtsV2Mainnet; break; } default: diff --git a/contracts/scripts/populatePolicyRegistry.ts b/contracts/scripts/populatePolicyRegistry.ts index 6ffdacd8e..43087268a 100644 --- a/contracts/scripts/populatePolicyRegistry.ts +++ b/contracts/scripts/populatePolicyRegistry.ts @@ -4,7 +4,7 @@ import policiesV1Mainnet from "../config/policies.v1.mainnet.json"; import policiesV1GnosisChain from "../config/policies.v1.gnosischain.json"; import policiesV2ArbitrumTestnet from "../config/policies.v2.testnet.json"; import policiesV2ArbitrumDevnet from "../config/policies.v2.devnet.json"; -import policiesV2MainnetNeo from "../config/policies.v2.mainnet-neo.json"; +import policiesV2Mainnet from "../config/policies.v2.mainnet.json"; import { isDevnet } from "../deploy/utils"; import { execute, writeTransactionBatch } from "./utils/execution"; @@ -19,13 +19,13 @@ enum Sources { V1_GNOSIS, V2_DEVNET, V2_TESTNET, - V2_MAINNET_NEO, + V2_MAINNET, } task("populate:policy-registry", "Populates the policy registry for each court") .addOptionalParam( "from", - "The source of the policies between v1_mainnet, v1_gnosis, v2_devnet, v2_testnet, v2_mainnet_neo (default: auto depending on the network)", + "The source of the policies between v1_mainnet, v1_gnosis, v2_devnet, v2_testnet, v2_mainnet (default: auto depending on the network)", undefined ) .addOptionalParam("start", "The starting index for the courts to populate (default: 0)", 0, types.int) @@ -53,7 +53,7 @@ task("populate:policy-registry", "Populates the policy registry for each court") if (taskArgs.from) { from = Sources[taskArgs.from.toUpperCase() as keyof typeof Sources]; if (from === undefined) { - console.error("Invalid source, must be one of v1_mainnet, v1_gnosis, v2_devnet, v2_testnet, v2_mainnet_neo"); + console.error("Invalid source, must be one of v1_mainnet, v1_gnosis, v2_devnet, v2_testnet, v2_mainnet"); return; } } else { @@ -88,8 +88,8 @@ task("populate:policy-registry", "Populates the policy registry for each court") policiesV2 = policiesV2ArbitrumTestnet; break; } - case Sources.V2_MAINNET_NEO: { - policiesV2 = policiesV2MainnetNeo; + case Sources.V2_MAINNET: { + policiesV2 = policiesV2Mainnet; break; } default: diff --git a/contracts/scripts/utils/contracts.ts b/contracts/scripts/utils/contracts.ts index cc7bf2429..50bcac2f6 100644 --- a/contracts/scripts/utils/contracts.ts +++ b/contracts/scripts/utils/contracts.ts @@ -7,13 +7,11 @@ import { DisputeResolver, DisputeTemplateRegistry, KlerosCore, - KlerosCoreNeo, KlerosCoreUniversity, PNK, PolicyRegistry, RandomizerRNG, SortitionModule, - SortitionModuleNeo, SortitionModuleUniversity, TransactionBatcher, KlerosCoreSnapshotProxy, @@ -24,28 +22,18 @@ import { export const Cores = { BASE: "BASE", - NEO: "NEO", UNIVERSITY: "UNIVERSITY", } as const; export type Core = (typeof Cores)[keyof typeof Cores]; /** - * Get contract names by specifying the coreType (BASE, NEO, UNIVERSITY). + * Get contract names by specifying the coreType (BASE, UNIVERSITY). * @param coreType - Core type * @returns Contract names */ export const getContractNames = (coreType: Core) => { const coreSpecificNames = { - [Cores.NEO]: { - core: "KlerosCoreNeo", - sortition: "SortitionModuleNeo", - disputeKitClassic: "DisputeKitClassicNeo", - disputeKitShutter: "DisputeKitShutterNeo", - disputeKitGated: "DisputeKitGatedNeo", - disputeKitGatedShutter: "DisputeKitGatedShutterNeo", - disputeResolver: "DisputeResolverNeo", - }, [Cores.BASE]: { core: "KlerosCore", sortition: "SortitionModule", @@ -66,7 +54,7 @@ export const getContractNames = (coreType: Core) => { }, }; - if (!(coreType in coreSpecificNames)) throw new Error("Invalid core type, must be one of BASE, NEO, or UNIVERSITY"); + if (!(coreType in coreSpecificNames)) throw new Error("Invalid core type, must be one of BASE, or UNIVERSITY"); return { ...coreSpecificNames[coreType], @@ -83,20 +71,16 @@ export const getContractNames = (coreType: Core) => { }; /** - * Get contracts by specifying the coreType (BASE, NEO, UNIVERSITY). + * Get contracts by specifying the coreType (BASE, UNIVERSITY). * @param hre - Hardhat runtime environment * @param coreType - Core type * @returns Contracts */ export const getContracts = async (hre: HardhatRuntimeEnvironment, coreType: Core) => { const { ethers } = hre; - let core: KlerosCore | KlerosCoreNeo | KlerosCoreUniversity; - let sortition: SortitionModule | SortitionModuleNeo | SortitionModuleUniversity; + let core: KlerosCore | KlerosCoreUniversity; + let sortition: SortitionModule | SortitionModuleUniversity; switch (coreType) { - case Cores.NEO: - core = await ethers.getContract(getContractNames(coreType).core); - sortition = await ethers.getContract(getContractNames(coreType).sortition); - break; case Cores.BASE: core = await ethers.getContract(getContractNames(coreType).core); sortition = await ethers.getContract(getContractNames(coreType).sortition); @@ -106,7 +90,7 @@ export const getContracts = async (hre: HardhatRuntimeEnvironment, coreType: Cor sortition = await ethers.getContract(getContractNames(coreType).sortition); break; default: - throw new Error("Invalid core type, must be one of BASE, NEO, or UNIVERSITY"); + throw new Error("Invalid core type, must be one of BASE, or UNIVERSITY"); } const disputeKitClassic = await ethers.getContract(getContractNames(coreType).disputeKitClassic); const disputeKitShutter = await ethers.getContractOrNull( @@ -151,32 +135,28 @@ export const getContracts = async (hre: HardhatRuntimeEnvironment, coreType: Cor }; /** - * Get contracts by inferring the coreType (BASE, NEO, UNIVERSITY) from the network, most convenient for most cases. + * Get contracts by inferring the coreType (BASE, UNIVERSITY) from the network, most convenient for most cases. * @param hre - Hardhat runtime environment * @returns Contracts */ export const getContractsFromNetwork = async (hre: HardhatRuntimeEnvironment) => { const { network } = hre; - if (network.name === "arbitrumSepoliaDevnet" || network.name === "arbitrumSepolia") { + if (["arbitrumSepoliaDevnet", "arbitrumSepolia", "arbitrum"].includes(network.name)) { return getContracts(hre, Cores.BASE); - } else if (network.name === "arbitrum") { - return getContracts(hre, Cores.NEO); } else { throw new Error("Invalid network"); } }; /** - * Get contract names by inferring the coreType (BASE, NEO, UNIVERSITY) from the network, most convenient for most cases. + * Get contract names by inferring the coreType (BASE, UNIVERSITY) from the network, most convenient for most cases. * @param hre - Hardhat runtime environment * @returns Contract names */ export const getContractNamesFromNetwork = async (hre: HardhatRuntimeEnvironment) => { const { network } = hre; - if (network.name === "arbitrumSepoliaDevnet" || network.name === "arbitrumSepolia") { + if (["arbitrumSepoliaDevnet", "arbitrumSepolia", "arbitrum"].includes(network.name)) { return getContractNames(Cores.BASE); - } else if (network.name === "arbitrum") { - return getContractNames(Cores.NEO); } else { throw new Error("Invalid network"); } diff --git a/contracts/test/integration/getContractsEthers.test.ts b/contracts/test/integration/getContractsEthers.test.ts index 736717626..59a04dfab 100644 --- a/contracts/test/integration/getContractsEthers.test.ts +++ b/contracts/test/integration/getContractsEthers.test.ts @@ -4,10 +4,8 @@ import { arbitrum, arbitrumSepolia } from "viem/chains"; import { getContracts } from "../../deployments/contractsEthers"; import { KlerosCore__factory, - KlerosCoreNeo__factory, KlerosCoreUniversity__factory, SortitionModule__factory, - SortitionModuleNeo__factory, SortitionModuleUniversity__factory, DisputeKitClassic__factory, DisputeResolver__factory, @@ -99,7 +97,7 @@ const universityContractMapping: ContractMapping = { klerosCoreSnapshotProxy: { name: "KlerosCoreSnapshotProxy" }, }; -const neoContractMapping: ContractMapping = { +const mainnetContractMapping: ContractMapping = { klerosCore: { name: "KlerosCoreNeo" }, sortition: { name: "SortitionModuleNeo" }, disputeKitClassic: { name: "DisputeKitClassicNeo" }, @@ -291,16 +289,16 @@ describe("getContractsEthers", async () => { await verifyDeployedAddresses(contracts, NETWORKS.TESTNET, testnetContractMapping); }); - it("should return correct contract instances for mainnetNeo", async () => { - const contracts = await getContracts(arbitrumProvider, "mainnetNeo"); + it("should return correct contract instances for mainnet", async () => { + const contracts = await getContracts(arbitrumProvider, "mainnet"); // Verify chain ID const network = await arbitrumProvider.getNetwork(); expect(network.chainId).to.equal(arbitrum.id); // Verify contract instances - expect(contracts.klerosCore).to.be.instanceOf(getConstructor(KlerosCoreNeo__factory, arbitrumProvider)); - expect(contracts.sortition).to.be.instanceOf(getConstructor(SortitionModuleNeo__factory, arbitrumProvider)); + expect(contracts.klerosCore).to.be.instanceOf(getConstructor(KlerosCore__factory, arbitrumProvider)); + expect(contracts.sortition).to.be.instanceOf(getConstructor(SortitionModule__factory, arbitrumProvider)); verifyCommonContractInstances(contracts, arbitrumProvider); expect(contracts.disputeKitShutter).to.not.be.null; expect(contracts.disputeKitGated).to.not.be.null; @@ -310,7 +308,7 @@ describe("getContractsEthers", async () => { // Verify all contract addresses await verifyAllContractAddresses(contracts); - await verifyDeployedAddresses(contracts, NETWORKS.MAINNET, neoContractMapping); + await verifyDeployedAddresses(contracts, NETWORKS.MAINNET, mainnetContractMapping); }); it("should throw error for unsupported deployment", async () => { diff --git a/contracts/test/integration/getContractsViem.test.ts b/contracts/test/integration/getContractsViem.test.ts index a32db3c86..4231164ad 100644 --- a/contracts/test/integration/getContractsViem.test.ts +++ b/contracts/test/integration/getContractsViem.test.ts @@ -77,7 +77,7 @@ const universityContractMapping: ContractMapping = { klerosCoreSnapshotProxy: { name: "KlerosCoreSnapshotProxy" }, }; -const neoContractMapping: ContractMapping = { +const mainnetContractMapping: ContractMapping = { klerosCore: { name: "KlerosCoreNeo" }, sortition: { name: "SortitionModuleNeo" }, disputeKitClassic: { name: "DisputeKitClassicNeo" }, @@ -240,10 +240,10 @@ describe("getContractsViem", () => { await verifyDeployedAddresses(contracts, NETWORKS.TESTNET, testnetContractMapping); }); - it("should return correct contract instances for mainnetNeo", async () => { + it("should return correct contract instances for mainnet", async () => { const contracts = getContracts({ publicClient: arbitrumClient, - deployment: "mainnetNeo", + deployment: "mainnet", }); // Verify chain ID @@ -262,7 +262,7 @@ describe("getContractsViem", () => { expect(contracts.randomizerRng).to.not.be.undefined; // Verify deployed addresses - await verifyDeployedAddresses(contracts, NETWORKS.MAINNET, neoContractMapping); + await verifyDeployedAddresses(contracts, NETWORKS.MAINNET, mainnetContractMapping); }); it("should throw error for unsupported deployment", () => { From 4e384c90ad03532ac51ebfd7a8ef5c561ebc00fb Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Thu, 4 Sep 2025 23:59:43 +0100 Subject: [PATCH 102/175] chore: updated audit METRICS files --- contracts/audit/METRICS.html | 832 +++++++++++++++++------------------ contracts/audit/METRICS.md | 779 ++++++++++++++++---------------- 2 files changed, 789 insertions(+), 822 deletions(-) diff --git a/contracts/audit/METRICS.html b/contracts/audit/METRICS.html index df48b7b11..31a6b6e73 100644 --- a/contracts/audit/METRICS.html +++ b/contracts/audit/METRICS.html @@ -208793,25 +208793,24 @@