diff --git a/contracts/deploy/00-home-chain-arbitration-neo.ts b/contracts/deploy/00-home-chain-arbitration-neo.ts index bdddaa999..3aa5ceac9 100644 --- a/contracts/deploy/00-home-chain-arbitration-neo.ts +++ b/contracts/deploy/00-home-chain-arbitration-neo.ts @@ -73,7 +73,7 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) deployer, deployer, pnk.target, - ZeroAddress, // KlerosCore is configured later + ZeroAddress, // jurorProsecutionModule is not implemented yet disputeKit.address, false, [minStake, alpha, feeForJuror, jurorsForCourtJump], diff --git a/contracts/deploy/upgrade-dispute-kit.ts b/contracts/deploy/upgrade-dispute-kit.ts new file mode 100644 index 000000000..f6439c91f --- /dev/null +++ b/contracts/deploy/upgrade-dispute-kit.ts @@ -0,0 +1,34 @@ +import { HardhatRuntimeEnvironment } from "hardhat/types"; +import { DeployFunction } from "hardhat-deploy/types"; +import { deployUpgradable } from "./utils/deployUpgradable"; +import { HomeChains, isSkipped } from "./utils"; + +const deployUpgradeDisputeKit: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { + const { deployments, getNamedAccounts, getChainId } = hre; + + // 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("upgrading on %s with deployer %s", HomeChains[chainId], deployer); + + try { + console.log("upgrading DisputeKitClassicNeo..."); + await deployUpgradable(deployments, "DisputeKitClassicNeo", { + contract: "DisputeKitClassic", + initializer: "initialize", + from: deployer, + // Warning: do not reinitialize everything, only the new variables + args: [], + }); + } catch (err) { + console.error(err); + throw err; + } +}; + +deployUpgradeDisputeKit.tags = ["Upgrade", "DisputeKit"]; +deployUpgradeDisputeKit.skip = async ({ network }) => { + return isSkipped(network, !HomeChains[network.config.chainId ?? 0]); +}; + +export default deployUpgradeDisputeKit; diff --git a/contracts/deploy/upgrade-kleros-core.ts b/contracts/deploy/upgrade-kleros-core.ts index 93472be91..9f504e6cc 100644 --- a/contracts/deploy/upgrade-kleros-core.ts +++ b/contracts/deploy/upgrade-kleros-core.ts @@ -4,36 +4,21 @@ import { deployUpgradable } from "./utils/deployUpgradable"; import { HomeChains, isSkipped } from "./utils"; const deployUpgradeKlerosCore: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { - const { ethers, deployments, getNamedAccounts, getChainId } = hre; - const { ZeroAddress } = hre.ethers; + const { deployments, getNamedAccounts, getChainId } = hre; // 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("upgrading to %s with deployer %s", HomeChains[chainId], deployer); + console.log("upgrading on %s with deployer %s", HomeChains[chainId], deployer); try { - const pnk = await deployments.get("PNK"); - const disputeKit = await deployments.get("DisputeKitClassic"); - const minStake = 2n * 10n ** 20n; - const alpha = 10000; - const feeForJuror = 10n * 17n; - const sortitionModule = await deployments.get("SortitionModule"); - - console.log("upgrading the KlerosCore..."); - await deployUpgradable(deployments, "KlerosCore", { + console.log("upgrading KlerosCoreNeo..."); + await deployUpgradable(deployments, "KlerosCoreNeo", { + newImplementation: "KlerosCoreNeo", + initializer: "initialize", from: deployer, - args: [ - deployer, - pnk, - ZeroAddress, - disputeKit.address, - false, - [minStake, alpha, feeForJuror, 256], // minStake, alpha, feeForJuror, jurorsForCourtJump - [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, - ], + // Warning: do not reinitialize everything, only the new variables + args: [], }); } catch (err) { console.error(err); diff --git a/contracts/deploy/upgrade-sortition-module.ts b/contracts/deploy/upgrade-sortition-module.ts index a556d232f..86a277a1b 100644 --- a/contracts/deploy/upgrade-sortition-module.ts +++ b/contracts/deploy/upgrade-sortition-module.ts @@ -5,29 +5,20 @@ import { HomeChains, isSkipped } from "./utils"; const deployUpgradeSortitionModule: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const { deployments, getNamedAccounts, getChainId } = hre; - const RNG_LOOKAHEAD = 20; // 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("upgrading to %s with deployer %s", HomeChains[chainId], deployer); + console.log("upgrading on %s with deployer %s", HomeChains[chainId], deployer); try { - const rng = await deployments.get("RandomizerRNG"); - const klerosCore = await deployments.get("KlerosCore"); - const klerosCoreAddress = klerosCore.address; - - console.log("upgrading the SortitionModule..."); - await deployUpgradable(deployments, "SortitionModule", { + console.log("upgrading SortitionModuleNeo..."); + await deployUpgradable(deployments, "SortitionModuleNeo", { + newImplementation: "SortitionModuleNeo", + initializer: "initialize", from: deployer, - args: [ - deployer, - klerosCoreAddress, - 1800, // minStakingTime - 1800, // maxFreezingTime - rng.address, - RNG_LOOKAHEAD, - ], + // Warning: do not reinitialize everything, only the new variables + args: [], }); } catch (err) { console.error(err); diff --git a/contracts/hardhat.config.ts b/contracts/hardhat.config.ts index ab81d6e95..05b505232 100644 --- a/contracts/hardhat.config.ts +++ b/contracts/hardhat.config.ts @@ -95,7 +95,7 @@ const config: HardhatUserConfig = { // Home chain --------------------------------------------------------------------------------- arbitrumSepolia: { chainId: 421614, - url: process.env.ARBITRUM_SEPOLIA_RPC ?? "https://sepolia-rollup.arbitrum.io/rpc", + url: process.env.ARBITRUM_SEPOLIA_RPC ?? `https://arbitrum-sepolia.infura.io/v3/${process.env.INFURA_API_KEY}`, accounts: (process.env.ARB_GOERLI_PRIVATE_KEY_WALLET_1 && [ process.env.ARB_GOERLI_PRIVATE_KEY_WALLET_1 as string, @@ -121,7 +121,7 @@ const config: HardhatUserConfig = { }, arbitrumSepoliaDevnet: { chainId: 421614, - url: process.env.ARBITRUM_SEPOLIA_RPC ?? "https://sepolia-rollup.arbitrum.io/rpc", + url: process.env.ARBITRUM_SEPOLIA_RPC ?? `https://arbitrum-sepolia.infura.io/v3/${process.env.INFURA_API_KEY}`, accounts: (process.env.ARB_GOERLI_PRIVATE_KEY_WALLET_1 && [ process.env.ARB_GOERLI_PRIVATE_KEY_WALLET_1 as string, @@ -147,7 +147,7 @@ const config: HardhatUserConfig = { }, arbitrum: { chainId: 42161, - url: "https://arb1.arbitrum.io/rpc", + url: process.env.ARBITRUM_RPC ?? `https://arbitrum-mainnet.infura.io/v3/${process.env.INFURA_API_KEY}`, accounts: process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [], live: true, saveDeployments: true, diff --git a/contracts/package.json b/contracts/package.json index e2441d609..76d7414c6 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -27,6 +27,7 @@ "start-local": "hardhat node --tags Arbitration,HomeArbitrable --hostname 0.0.0.0", "deploy": "hardhat deploy", "deploy-local": "hardhat deploy --tags Arbitration,HomeArbitrable --network localhost", + "validate-upgrades": "openzeppelin-upgrades-core validate --exclude 'src/proxy/mock/**/*.sol' --exclude 'src/test/**/*.sol' artifacts/build-info", "simulate": "hardhat simulate:all", "simulate-local": "hardhat simulate:all --network localhost", "viem:generate-devnet": "NODE_NO_WARNINGS=1 wagmi generate -c wagmi.config.devnet.ts", @@ -69,6 +70,7 @@ "@nomicfoundation/hardhat-chai-matchers": "^2.0.8", "@nomicfoundation/hardhat-ethers": "^3.0.8", "@nomiclabs/hardhat-solhint": "^4.0.1", + "@openzeppelin/upgrades-core": "^1.41.0", "@typechain/ethers-v6": "^0.5.1", "@typechain/hardhat": "^9.1.0", "@types/chai": "^4.3.20", @@ -80,23 +82,23 @@ "dotenv": "^16.4.5", "eslint": "^9.15.0", "ethereumjs-util": "^7.1.5", - "ethers": "^6.13.4", + "ethers": "^6.13.5", "graphql": "^16.9.0", "graphql-request": "^7.1.2", - "hardhat": "2.22.16", + "hardhat": "2.22.18", "hardhat-contract-sizer": "^2.10.0", "hardhat-deploy": "^0.14.0", "hardhat-deploy-ethers": "^0.4.2", - "hardhat-deploy-tenderly": "^0.2.0", + "hardhat-deploy-tenderly": "^0.2.1", "hardhat-docgen": "^1.3.0", - "hardhat-gas-reporter": "^2.2.1", + "hardhat-gas-reporter": "^2.2.2", "hardhat-tracer": "^3.1.0", "hardhat-watcher": "^2.5.0", "node-fetch": "^3.3.2", "pino": "^8.21.0", "pino-pretty": "^10.3.1", "prettier": "^3.3.3", - "prettier-plugin-solidity": "^1.4.1", + "prettier-plugin-solidity": "^1.4.2", "shelljs": "^0.8.5", "solhint-plugin-prettier": "^0.1.0", "solidity-coverage": "^0.8.13", @@ -107,7 +109,7 @@ "dependencies": { "@chainlink/contracts": "^1.3.0", "@kleros/vea-contracts": "^0.4.0", - "@openzeppelin/contracts": "^5.1.0", + "@openzeppelin/contracts": "^5.2.0", "viem": "^2.21.48" } } diff --git a/contracts/src/arbitration/DisputeTemplateRegistry.sol b/contracts/src/arbitration/DisputeTemplateRegistry.sol index bf1b77c6b..ca22c6400 100644 --- a/contracts/src/arbitration/DisputeTemplateRegistry.sol +++ b/contracts/src/arbitration/DisputeTemplateRegistry.sol @@ -8,6 +8,8 @@ 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"; + // ************************************* // // * Storage * // // ************************************* // @@ -31,7 +33,7 @@ contract DisputeTemplateRegistry is IDisputeTemplateRegistry, UUPSProxiable, Ini // * Constructor * // // ************************************* // - /// @dev Constructor, initializing the implementation to reduce attack surface. + /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } diff --git a/contracts/src/arbitration/KlerosCore.sol b/contracts/src/arbitration/KlerosCore.sol index e3bc02150..ceaa6b4e0 100644 --- a/contracts/src/arbitration/KlerosCore.sol +++ b/contracts/src/arbitration/KlerosCore.sol @@ -8,19 +8,19 @@ pragma solidity 0.8.24; -import "./KlerosCoreBase.sol"; -import {UUPSProxiable} from "../proxy/UUPSProxiable.sol"; -import {Initializable} from "../proxy/Initializable.sol"; +import {KlerosCoreBase, IDisputeKit, ISortitionModule, IERC20} from "./KlerosCoreBase.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, UUPSProxiable, Initializable { +contract KlerosCore is KlerosCoreBase { + string public constant override version = "0.8.0"; + // ************************************* // // * Constructor * // // ************************************* // - /// @dev Constructor, initializing the implementation to reduce attack surface. + /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } @@ -48,7 +48,7 @@ contract KlerosCore is KlerosCoreBase, UUPSProxiable, Initializable { bytes memory _sortitionExtraData, ISortitionModule _sortitionModuleAddress ) external reinitializer(1) { - _initialize( + __KlerosCoreBase_initialize( _governor, _guardian, _pinakion, diff --git a/contracts/src/arbitration/KlerosCoreBase.sol b/contracts/src/arbitration/KlerosCoreBase.sol index 2e062e473..85541c5fa 100644 --- a/contracts/src/arbitration/KlerosCoreBase.sol +++ b/contracts/src/arbitration/KlerosCoreBase.sol @@ -11,13 +11,15 @@ 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 "../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 { +abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable { using SafeERC20 for IERC20; // ************************************* // @@ -193,7 +195,7 @@ abstract contract KlerosCoreBase is IArbitratorV2 { // * Constructor * // // ************************************* // - function _initialize( + function __KlerosCoreBase_initialize( address _governor, address _guardian, IERC20 _pinakion, @@ -204,7 +206,7 @@ abstract contract KlerosCoreBase is IArbitratorV2 { uint256[4] memory _timesPerPeriod, bytes memory _sortitionExtraData, ISortitionModule _sortitionModuleAddress - ) internal { + ) internal onlyInitializing { governor = _governor; guardian = _guardian; pinakion = _pinakion; diff --git a/contracts/src/arbitration/KlerosCoreNeo.sol b/contracts/src/arbitration/KlerosCoreNeo.sol index 938bf6d60..205a50720 100644 --- a/contracts/src/arbitration/KlerosCoreNeo.sol +++ b/contracts/src/arbitration/KlerosCoreNeo.sol @@ -8,15 +8,15 @@ pragma solidity 0.8.24; -import "./KlerosCoreBase.sol"; -import {UUPSProxiable} from "../proxy/UUPSProxiable.sol"; -import {Initializable} from "../proxy/Initializable.sol"; +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, UUPSProxiable, Initializable { +contract KlerosCoreNeo is KlerosCoreBase { + string public constant override version = "0.8.0"; + // ************************************* // // * Storage * // // ************************************* // @@ -28,7 +28,7 @@ contract KlerosCoreNeo is KlerosCoreBase, UUPSProxiable, Initializable { // * Constructor * // // ************************************* // - /// @dev Constructor, initializing the implementation to reduce attack surface. + /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } @@ -58,7 +58,7 @@ contract KlerosCoreNeo is KlerosCoreBase, UUPSProxiable, Initializable { ISortitionModule _sortitionModuleAddress, IERC721 _jurorNft ) external reinitializer(2) { - super._initialize( + __KlerosCoreBase_initialize( _governor, _guardian, _pinakion, diff --git a/contracts/src/arbitration/PolicyRegistry.sol b/contracts/src/arbitration/PolicyRegistry.sol index cec29e76c..7150ba0f1 100644 --- a/contracts/src/arbitration/PolicyRegistry.sol +++ b/contracts/src/arbitration/PolicyRegistry.sol @@ -7,6 +7,8 @@ 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"; + // ************************************* // // * Events * // // ************************************* // @@ -38,7 +40,7 @@ contract PolicyRegistry is UUPSProxiable, Initializable { // * Constructor * // // ************************************* // - /// @dev Constructor, initializing the implementation to reduce attack surface. + /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } diff --git a/contracts/src/arbitration/SortitionModule.sol b/contracts/src/arbitration/SortitionModule.sol index 4db44c5c4..828f674c0 100644 --- a/contracts/src/arbitration/SortitionModule.sol +++ b/contracts/src/arbitration/SortitionModule.sol @@ -10,18 +10,18 @@ pragma solidity 0.8.24; -import "./SortitionModuleBase.sol"; -import "../proxy/UUPSProxiable.sol"; -import "../proxy/Initializable.sol"; +import {SortitionModuleBase, KlerosCore, RNG} from "./SortitionModuleBase.sol"; /// @title SortitionModule /// @dev A factory of trees that keeps track of staked values for sortition. -contract SortitionModule is SortitionModuleBase, UUPSProxiable, Initializable { +contract SortitionModule is SortitionModuleBase { + string public constant override version = "0.8.0"; + // ************************************* // // * Constructor * // // ************************************* // - /// @dev Constructor, initializing the implementation to reduce attack surface. + /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } @@ -41,7 +41,7 @@ contract SortitionModule is SortitionModuleBase, UUPSProxiable, Initializable { RNG _rng, uint256 _rngLookahead ) external reinitializer(1) { - super._initialize(_governor, _core, _minStakingTime, _maxDrawingTime, _rng, _rngLookahead); + __SortitionModuleBase_initialize(_governor, _core, _minStakingTime, _maxDrawingTime, _rng, _rngLookahead); } // ************************************* // diff --git a/contracts/src/arbitration/SortitionModuleBase.sol b/contracts/src/arbitration/SortitionModuleBase.sol index 642f9b627..2d6e3a9d8 100644 --- a/contracts/src/arbitration/SortitionModuleBase.sol +++ b/contracts/src/arbitration/SortitionModuleBase.sol @@ -10,15 +10,17 @@ pragma solidity 0.8.24; -import "./KlerosCore.sol"; -import "./interfaces/ISortitionModule.sol"; -import "./interfaces/IDisputeKit.sol"; -import "../rng/RNG.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 {RNG} from "../rng/RNG.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 { +abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSProxiable { // ************************************* // // * Enums / Structs * // // ************************************* // @@ -89,14 +91,14 @@ abstract contract SortitionModuleBase is ISortitionModule { // * Constructor * // // ************************************* // - function _initialize( + function __SortitionModuleBase_initialize( address _governor, KlerosCore _core, uint256 _minStakingTime, uint256 _maxDrawingTime, RNG _rng, uint256 _rngLookahead - ) internal { + ) internal onlyInitializing { governor = _governor; core = _core; minStakingTime = _minStakingTime; diff --git a/contracts/src/arbitration/SortitionModuleNeo.sol b/contracts/src/arbitration/SortitionModuleNeo.sol index 0ac13b890..5d725e277 100644 --- a/contracts/src/arbitration/SortitionModuleNeo.sol +++ b/contracts/src/arbitration/SortitionModuleNeo.sol @@ -10,13 +10,13 @@ pragma solidity 0.8.24; -import "./SortitionModuleBase.sol"; -import "../proxy/UUPSProxiable.sol"; -import "../proxy/Initializable.sol"; +import {SortitionModuleBase, KlerosCore, RNG, StakingResult} from "./SortitionModuleBase.sol"; /// @title SortitionModuleNeo /// @dev A factory of trees that keeps track of staked values for sortition. -contract SortitionModuleNeo is SortitionModuleBase, UUPSProxiable, Initializable { +contract SortitionModuleNeo is SortitionModuleBase { + string public constant override version = "0.8.0"; + // ************************************* // // * Storage * // // ************************************* // @@ -29,7 +29,7 @@ contract SortitionModuleNeo is SortitionModuleBase, UUPSProxiable, Initializable // * Constructor * // // ************************************* // - /// @dev Constructor, initializing the implementation to reduce attack surface. + /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } @@ -53,7 +53,7 @@ contract SortitionModuleNeo is SortitionModuleBase, UUPSProxiable, Initializable uint256 _maxStakePerJuror, uint256 _maxTotalStaked ) external reinitializer(2) { - super._initialize(_governor, _core, _minStakingTime, _maxDrawingTime, _rng, _rngLookahead); + __SortitionModuleBase_initialize(_governor, _core, _minStakingTime, _maxDrawingTime, _rng, _rngLookahead); maxStakePerJuror = _maxStakePerJuror; maxTotalStaked = _maxTotalStaked; } diff --git a/contracts/src/arbitration/devtools/KlerosCoreRuler.sol b/contracts/src/arbitration/devtools/KlerosCoreRuler.sol index 3c5aebf8b..2100ad1c3 100644 --- a/contracts/src/arbitration/devtools/KlerosCoreRuler.sol +++ b/contracts/src/arbitration/devtools/KlerosCoreRuler.sol @@ -13,6 +13,8 @@ import "../../libraries/Constants.sol"; contract KlerosCoreRuler is IArbitratorV2, UUPSProxiable, Initializable { using SafeERC20 for IERC20; + string public constant override version = "0.8.0"; + // ************************************* // // * Enums / Structs * // // ************************************* // @@ -164,7 +166,7 @@ contract KlerosCoreRuler is IArbitratorV2, UUPSProxiable, Initializable { // * Constructor * // // ************************************* // - /// @dev Constructor, initializing the implementation to reduce attack surface. + /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitClassic.sol b/contracts/src/arbitration/dispute-kits/DisputeKitClassic.sol index 12d4e45c5..ef4591abd 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitClassic.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitClassic.sol @@ -8,10 +8,7 @@ pragma solidity 0.8.24; -import "../KlerosCore.sol"; -import "../interfaces/IDisputeKit.sol"; -import "../../proxy/UUPSProxiable.sol"; -import "../../proxy/Initializable.sol"; +import {DisputeKitClassicBase, KlerosCore} from "./DisputeKitClassicBase.sol"; /// @title DisputeKitClassic /// Dispute kit implementation of the Kleros v1 features including: @@ -19,130 +16,14 @@ import "../../proxy/Initializable.sol"; /// - a vote aggregation system: plurality, /// - an incentive system: equal split between coherent votes, /// - an appeal system: fund 2 choices only, vote on any choice. -contract DisputeKitClassic is IDisputeKit, Initializable, UUPSProxiable { - // ************************************* // - // * Structs * // - // ************************************* // - - struct Dispute { - Round[] rounds; // Rounds of the dispute. 0 is the default round, and [1, ..n] are the appeal rounds. - uint256 numberOfChoices; // The number of choices jurors have when voting. This does not include choice `0` which is reserved for "refuse to arbitrate". - bool jumped; // True if dispute jumped to a parent dispute kit and won't be handled by this DK anymore. - mapping(uint256 => uint256) coreRoundIDToLocal; // Maps id of the round in the core contract to the index of the round of related local dispute. - bytes extraData; // Extradata for the dispute. - } - - struct Round { - Vote[] votes; // Former votes[_appeal][]. - uint256 winningChoice; // The choice with the most votes. Note that in the case of a tie, it is the choice that reached the tied number of votes first. - mapping(uint256 => uint256) counts; // The sum of votes for each choice in the form `counts[choice]`. - bool tied; // True if there is a tie, false otherwise. - uint256 totalVoted; // Former uint[_appeal] votesInEachRound. - uint256 totalCommitted; // Former commitsInRound. - mapping(uint256 choiceId => uint256) paidFees; // Tracks the fees paid for each choice in this round. - mapping(uint256 choiceId => bool) hasPaid; // True if this choice was fully funded, false otherwise. - mapping(address account => mapping(uint256 choiceId => uint256)) contributions; // Maps contributors to their contributions for each choice. - uint256 feeRewards; // Sum of reimbursable appeal fees available to the parties that made contributions to the ruling that ultimately wins a dispute. - uint256[] fundedChoices; // Stores the choices that are fully funded. - uint256 nbVotes; // Maximal number of votes this dispute can get. - } - - struct Vote { - address account; // The address of the juror. - bytes32 commit; // The commit of the juror. For courts with hidden votes. - uint256 choice; // The choice of the juror. - bool voted; // True if the vote has been cast. - } - - // ************************************* // - // * Storage * // - // ************************************* // - - 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 - 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. - - // ************************************* // - // * Events * // - // ************************************* // - - /// @dev To be emitted when a dispute is created. - /// @param _coreDisputeID The identifier of the dispute in the Arbitrator contract. - /// @param _numberOfChoices The number of choices available in the dispute. - /// @param _extraData The extra data for the dispute. - event DisputeCreation(uint256 indexed _coreDisputeID, uint256 _numberOfChoices, bytes _extraData); - - /// @dev To be emitted when a vote commitment is cast. - /// @param _coreDisputeID The identifier of the dispute in the Arbitrator contract. - /// @param _juror The address of the juror casting the vote commitment. - /// @param _voteIDs The identifiers of the votes in the dispute. - /// @param _commit The commitment of the juror. - event CommitCast(uint256 indexed _coreDisputeID, address indexed _juror, uint256[] _voteIDs, bytes32 _commit); - - /// @dev To be emitted when a funding contribution is made. - /// @param _coreDisputeID The identifier of the dispute in the Arbitrator contract. - /// @param _coreRoundID The identifier of the round in the Arbitrator contract. - /// @param _choice The choice that is being funded. - /// @param _contributor The address of the contributor. - /// @param _amount The amount contributed. - event Contribution( - uint256 indexed _coreDisputeID, - uint256 indexed _coreRoundID, - uint256 _choice, - address indexed _contributor, - uint256 _amount - ); - - /// @dev To be emitted when the contributed funds are withdrawn. - /// @param _coreDisputeID The identifier of the dispute in the Arbitrator contract. - /// @param _coreRoundID The identifier of the round in the Arbitrator contract. - /// @param _choice The choice that is being funded. - /// @param _contributor The address of the contributor. - /// @param _amount The amount withdrawn. - event Withdrawal( - uint256 indexed _coreDisputeID, - uint256 indexed _coreRoundID, - uint256 _choice, - address indexed _contributor, - uint256 _amount - ); - - /// @dev To be emitted when a choice is fully funded for an appeal. - /// @param _coreDisputeID The identifier of the dispute in the Arbitrator contract. - /// @param _coreRoundID The identifier of the round in the Arbitrator contract. - /// @param _choice The choice that is being funded. - event ChoiceFunded(uint256 indexed _coreDisputeID, uint256 indexed _coreRoundID, uint256 indexed _choice); - - // ************************************* // - // * Modifiers * // - // ************************************* // - - modifier onlyByGovernor() { - require(governor == msg.sender, "Access not allowed: Governor only."); - _; - } - - modifier onlyByCore() { - require(address(core) == msg.sender, "Access not allowed: KlerosCore only."); - _; - } - - modifier notJumped(uint256 _coreDisputeID) { - require(!disputes[coreDisputeIDToLocal[_coreDisputeID]].jumped, "Dispute jumped to a parent DK!"); - _; - } +contract DisputeKitClassic is DisputeKitClassicBase { + string public constant override version = "0.8.0"; // ************************************* // // * Constructor * // // ************************************* // - /// @dev Constructor, initializing the implementation to reduce attack surface. + /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } @@ -151,8 +32,7 @@ contract DisputeKitClassic is IDisputeKit, Initializable, UUPSProxiable { /// @param _governor The governor's address. /// @param _core The KlerosCore arbitrator. function initialize(address _governor, KlerosCore _core) external reinitializer(1) { - governor = _governor; - core = _core; + __DisputeKitClassicBase_initialize(_governor, _core); } // ************************ // @@ -164,454 +44,4 @@ contract DisputeKitClassic is IDisputeKit, Initializable, UUPSProxiable { function _authorizeUpgrade(address) internal view override onlyByGovernor { // NOP } - - /// @dev Allows the governor 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 { - (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 payable _governor) external onlyByGovernor { - governor = _governor; - } - - /// @dev Changes the `core` storage variable. - /// @param _core The new value for the `core` storage variable. - function changeCore(address _core) external onlyByGovernor { - core = KlerosCore(_core); - } - - // ************************************* // - // * State Modifiers * // - // ************************************* // - - /// @dev Creates a local dispute and maps it to the dispute ID in the Core contract. - /// Note: Access restricted to Kleros Core only. - /// @param _coreDisputeID The ID of the dispute in Kleros Core. - /// @param _numberOfChoices Number of choices of the dispute - /// @param _extraData Additional info about the dispute, for possible use in future dispute kits. - /// @param _nbVotes Number of votes for this dispute. - function createDispute( - uint256 _coreDisputeID, - uint256 _numberOfChoices, - bytes calldata _extraData, - uint256 _nbVotes - ) external override onlyByCore { - uint256 localDisputeID = disputes.length; - Dispute storage dispute = disputes.push(); - dispute.numberOfChoices = _numberOfChoices; - dispute.extraData = _extraData; - - // New round in the Core should be created before the dispute creation in DK. - dispute.coreRoundIDToLocal[core.getNumberOfRounds(_coreDisputeID) - 1] = dispute.rounds.length; - - Round storage round = dispute.rounds.push(); - round.nbVotes = _nbVotes; - round.tied = true; - - coreDisputeIDToLocal[_coreDisputeID] = localDisputeID; - emit DisputeCreation(_coreDisputeID, _numberOfChoices, _extraData); - } - - /// @dev Draws the juror from the sortition tree. The drawn address is picked up by Kleros Core. - /// Note: Access restricted to Kleros Core only. - /// @param _coreDisputeID The ID of the dispute in Kleros Core. - /// @param _nonce Nonce of the drawing iteration. - /// @return drawnAddress The drawn address. - function draw( - uint256 _coreDisputeID, - uint256 _nonce - ) external override onlyByCore notJumped(_coreDisputeID) returns (address drawnAddress) { - Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; - Round storage round = dispute.rounds[dispute.rounds.length - 1]; - - ISortitionModule sortitionModule = core.sortitionModule(); - (uint96 courtID, , , , ) = core.disputes(_coreDisputeID); - bytes32 key = bytes32(uint256(courtID)); // Get the ID of the tree. - - drawnAddress = sortitionModule.draw(key, _coreDisputeID, _nonce); - - if (_postDrawCheck(_coreDisputeID, drawnAddress)) { - round.votes.push(Vote({account: drawnAddress, commit: bytes32(0), choice: 0, voted: false})); - } else { - drawnAddress = address(0); - } - } - - /// @dev Sets the caller's commit for the specified votes. It can be called multiple times during the - /// commit period, each call overrides the commits of the previous one. - /// `O(n)` where - /// `n` is the number of votes. - /// @param _coreDisputeID The ID of the dispute in Kleros Core. - /// @param _voteIDs The IDs of the votes. - /// @param _commit The commit. Note that justification string is a part of the commit. - function castCommit( - uint256 _coreDisputeID, - uint256[] calldata _voteIDs, - bytes32 _commit - ) external 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."); - - 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."); - round.votes[_voteIDs[i]].commit = _commit; - } - round.totalCommitted += _voteIDs.length; - emit CommitCast(_coreDisputeID, msg.sender, _voteIDs, _commit); - } - - /// @dev Sets the caller's choices for the specified votes. - /// `O(n)` where - /// `n` is the number of votes. - /// @param _coreDisputeID The ID of the dispute in Kleros Core. - /// @param _voteIDs The IDs of the votes. - /// @param _choice The choice. - /// @param _salt The salt for the commit if the votes were hidden. - /// @param _justification Justification of the choice. - function castVote( - uint256 _coreDisputeID, - uint256[] calldata _voteIDs, - uint256 _choice, - uint256 _salt, - string memory _justification - ) external 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"); - - Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; - require(_choice <= dispute.numberOfChoices, "Choice out of bounds"); - - Round storage round = dispute.rounds[dispute.rounds.length - 1]; - (uint96 courtID, , , , ) = core.disputes(_coreDisputeID); - (, bool hiddenVotes, , , , , ) = core.courts(courtID); - - // Save the votes. - for (uint256 i = 0; i < _voteIDs.length; i++) { - require(round.votes[_voteIDs[i]].account == msg.sender, "The caller has to own the vote."); - require( - !hiddenVotes || round.votes[_voteIDs[i]].commit == keccak256(abi.encodePacked(_choice, _salt)), - "The commit must match the choice in courts with hidden votes." - ); - require(!round.votes[_voteIDs[i]].voted, "Vote already cast."); - round.votes[_voteIDs[i]].choice = _choice; - round.votes[_voteIDs[i]].voted = true; - } - - round.totalVoted += _voteIDs.length; - - round.counts[_choice] += _voteIDs.length; - if (_choice == round.winningChoice) { - if (round.tied) round.tied = false; - } else { - // Voted for another choice. - if (round.counts[_choice] == round.counts[round.winningChoice]) { - // Tie. - if (!round.tied) round.tied = true; - } else if (round.counts[_choice] > round.counts[round.winningChoice]) { - // New winner. - round.winningChoice = _choice; - round.tied = false; - } - } - emit VoteCast(_coreDisputeID, msg.sender, _voteIDs, _choice, _justification); - } - - /// @dev Manages contributions, and appeals a dispute if at least two choices are fully funded. - /// Note that the surplus deposit will be reimbursed. - /// @param _coreDisputeID Index of the dispute in Kleros Core. - /// @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."); - - (uint256 appealPeriodStart, uint256 appealPeriodEnd) = core.appealPeriod(_coreDisputeID); - require(block.timestamp >= appealPeriodStart && block.timestamp < appealPeriodEnd, "Appeal period is over."); - - 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" - ); - 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."); - uint256 appealCost = core.appealCost(_coreDisputeID); - uint256 totalCost = appealCost + (appealCost * multiplier) / ONE_BASIS_POINT; - - // Take up to the amount necessary to fund the current round at the current costs. - uint256 contribution; - if (totalCost > round.paidFees[_choice]) { - contribution = totalCost - round.paidFees[_choice] > msg.value // Overflows and underflows will be managed on the compiler level. - ? msg.value - : totalCost - round.paidFees[_choice]; - emit Contribution(_coreDisputeID, coreRoundID, _choice, msg.sender, contribution); - } - - round.contributions[msg.sender][_choice] += contribution; - round.paidFees[_choice] += contribution; - if (round.paidFees[_choice] >= totalCost) { - round.feeRewards += round.paidFees[_choice]; - round.fundedChoices.push(_choice); - round.hasPaid[_choice] = true; - emit ChoiceFunded(_coreDisputeID, coreRoundID, _choice); - } - - if (round.fundedChoices.length > 1) { - // At least two sides are fully funded. - round.feeRewards = round.feeRewards - appealCost; - - if (core.isDisputeKitJumping(_coreDisputeID)) { - // Don't create a new round in case of a jump, and remove local dispute from the flow. - dispute.jumped = true; - } else { - // Don't subtract 1 from length since both round arrays haven't been updated yet. - dispute.coreRoundIDToLocal[coreRoundID + 1] = dispute.rounds.length; - - Round storage newRound = dispute.rounds.push(); - newRound.nbVotes = core.getNumberOfVotes(_coreDisputeID); - newRound.tied = true; - } - core.appeal{value: appealCost}(_coreDisputeID, dispute.numberOfChoices, dispute.extraData); - } - - if (msg.value > contribution) payable(msg.sender).send(msg.value - contribution); - } - - /// @dev Allows those contributors who attempted to fund an appeal round to withdraw any reimbursable fees or rewards after the dispute gets resolved. - /// Note that withdrawals are not possible if the core contract is paused. - /// @param _coreDisputeID Index of the dispute in Kleros Core contract. - /// @param _beneficiary The address whose rewards to withdraw. - /// @param _coreRoundID The round in the Kleros Core contract the caller wants to withdraw from. - /// @param _choice The ruling option that the caller wants to withdraw from. - /// @return amount The withdrawn amount. - function withdrawFeesAndRewards( - uint256 _coreDisputeID, - address payable _beneficiary, - uint256 _coreRoundID, - uint256 _choice - ) external returns (uint256 amount) { - (, , , bool isRuled, ) = core.disputes(_coreDisputeID); - require(isRuled, "Dispute should be resolved."); - require(!core.paused(), "Core is paused"); - - Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; - Round storage round = dispute.rounds[dispute.coreRoundIDToLocal[_coreRoundID]]; - (uint256 finalRuling, , ) = core.currentRuling(_coreDisputeID); - - if (!round.hasPaid[_choice]) { - // Allow to reimburse if funding was unsuccessful for this ruling option. - amount = round.contributions[_beneficiary][_choice]; - } else { - // Funding was successful for this ruling option. - if (_choice == finalRuling) { - // This ruling option is the ultimate winner. - amount = round.paidFees[_choice] > 0 - ? (round.contributions[_beneficiary][_choice] * round.feeRewards) / round.paidFees[_choice] - : 0; - } else if (!round.hasPaid[finalRuling]) { - // The ultimate winner was not funded in this round. In this case funded ruling option(s) are reimbursed. - amount = - (round.contributions[_beneficiary][_choice] * round.feeRewards) / - (round.paidFees[round.fundedChoices[0]] + round.paidFees[round.fundedChoices[1]]); - } - } - round.contributions[_beneficiary][_choice] = 0; - - if (amount != 0) { - _beneficiary.send(amount); // Deliberate use of send to prevent reverting fallback. It's the user's responsibility to accept ETH. - emit Withdrawal(_coreDisputeID, _coreRoundID, _choice, _beneficiary, amount); - } - } - - // ************************************* // - // * Public Views * // - // ************************************* // - - function getFundedChoices(uint256 _coreDisputeID) public view returns (uint256[] memory fundedChoices) { - Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; - Round storage lastRound = dispute.rounds[dispute.rounds.length - 1]; - return lastRound.fundedChoices; - } - - /// @dev Gets the current ruling of a specified dispute. - /// @param _coreDisputeID The ID of the dispute in Kleros Core. - /// @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 _coreDisputeID - ) external view override returns (uint256 ruling, bool tied, bool overridden) { - Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; - Round storage round = dispute.rounds[dispute.rounds.length - 1]; - tied = round.tied; - 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) { - uint256[] memory fundedChoices = getFundedChoices(_coreDisputeID); - if (fundedChoices.length == 1) { - ruling = fundedChoices[0]; - tied = false; - overridden = true; - } - } - } - - /// @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 reward. - /// @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( - uint256 _coreDisputeID, - uint256 _coreRoundID, - uint256 _voteID, - uint256 /* _feePerJuror */, - uint256 /* _pnkAtStakePerJuror */ - ) external view override returns (uint256) { - // 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]; - (uint256 winningChoice, bool tied, ) = core.currentRuling(_coreDisputeID); - - if (vote.voted && (vote.choice == winningChoice || tied)) { - return ONE_BASIS_POINT; - } else { - return 0; - } - } - - /// @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. - /// @param _coreRoundID The ID of the round in Kleros Core, not in the Dispute Kit. - /// @return The number of coherent jurors. - function getCoherentCount(uint256 _coreDisputeID, uint256 _coreRoundID) external view override returns (uint256) { - Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; - Round storage currentRound = dispute.rounds[dispute.coreRoundIDToLocal[_coreRoundID]]; - (uint256 winningChoice, bool tied, ) = core.currentRuling(_coreDisputeID); - - if (currentRound.totalVoted == 0 || (!tied && currentRound.counts[winningChoice] == 0)) { - return 0; - } else if (tied) { - return currentRound.totalVoted; - } else { - return currentRound.counts[winningChoice]; - } - } - - /// @dev Returns true if all of the jurors have cast their commits for the last round. - /// @param _coreDisputeID The ID of the dispute in Kleros Core. - /// @return Whether all of the jurors have cast their commits for the last round. - function areCommitsAllCast(uint256 _coreDisputeID) external view override returns (bool) { - Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; - Round storage round = dispute.rounds[dispute.rounds.length - 1]; - return round.totalCommitted == round.votes.length; - } - - /// @dev Returns true if all of the jurors have cast their votes for the last round. - /// @param _coreDisputeID The ID of the dispute in Kleros Core. - /// @return Whether all of the jurors have cast their votes for the last round. - function areVotesAllCast(uint256 _coreDisputeID) external view override returns (bool) { - Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; - Round storage round = dispute.rounds[dispute.rounds.length - 1]; - return round.totalVoted == round.votes.length; - } - - /// @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. - /// @param _voteID The ID of the voter. - /// @return Whether the voter was active or not. - function isVoteActive( - uint256 _coreDisputeID, - uint256 _coreRoundID, - uint256 _voteID - ) external view override returns (bool) { - Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; - Vote storage vote = dispute.rounds[dispute.coreRoundIDToLocal[_coreRoundID]].votes[_voteID]; - return vote.voted; - } - - function getRoundInfo( - uint256 _coreDisputeID, - uint256 _coreRoundID, - uint256 _choice - ) - external - view - override - returns ( - uint256 winningChoice, - bool tied, - uint256 totalVoted, - uint256 totalCommited, - uint256 nbVoters, - uint256 choiceCount - ) - { - Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; - Round storage round = dispute.rounds[dispute.coreRoundIDToLocal[_coreRoundID]]; - return ( - round.winningChoice, - round.tied, - round.totalVoted, - round.totalCommitted, - round.votes.length, - round.counts[_choice] - ); - } - - function getVoteInfo( - uint256 _coreDisputeID, - uint256 _coreRoundID, - uint256 _voteID - ) external view override returns (address account, bytes32 commit, uint256 choice, bool voted) { - Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; - Vote storage vote = dispute.rounds[dispute.coreRoundIDToLocal[_coreRoundID]].votes[_voteID]; - return (vote.account, vote.commit, vote.choice, vote.voted); - } - - // ************************************* // - // * Internal * // - // ************************************* // - - /// @dev Checks that the chosen address satisfies certain conditions for being drawn. - /// @param _coreDisputeID ID of the dispute in the core contract. - /// @param _juror Chosen address. - /// @return Whether the address can be drawn or not. - /// 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 since we check for insolvency anyway. - function _postDrawCheck(uint256 _coreDisputeID, address _juror) internal view returns (bool) { - (uint96 courtID, , , , ) = core.disputes(_coreDisputeID); - uint256 lockedAmountPerJuror = core - .getRoundInfo(_coreDisputeID, core.getNumberOfRounds(_coreDisputeID) - 1) - .pnkAtStakePerJuror; - (uint256 totalStaked, uint256 totalLocked, , ) = core.sortitionModule().getJurorBalance(_juror, courtID); - return totalStaked >= totalLocked + lockedAmountPerJuror; - } } diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol new file mode 100644 index 000000000..b6865d9f7 --- /dev/null +++ b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol @@ -0,0 +1,617 @@ +// SPDX-License-Identifier: MIT + +/// @custom:authors: [@unknownunknown1, @jaybuidl] +/// @custom:reviewers: [] +/// @custom:auditors: [] +/// @custom:bounties: [] +/// @custom:deployments: [] + +pragma solidity 0.8.24; + +import {KlerosCore, KlerosCoreBase, IDisputeKit, ISortitionModule} from "../KlerosCore.sol"; +import {Initializable} from "../../proxy/Initializable.sol"; +import {UUPSProxiable} from "../../proxy/UUPSProxiable.sol"; + +/// @title DisputeKitClassicBase +/// Abstract Dispute kit classic implementation of the Kleros v1 features including: +/// - a drawing system: proportional to staked PNK, +/// - a vote aggregation system: plurality, +/// - an incentive system: equal split between coherent votes, +/// - an appeal system: fund 2 choices only, vote on any choice. +abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxiable { + // ************************************* // + // * Structs * // + // ************************************* // + + struct Dispute { + Round[] rounds; // Rounds of the dispute. 0 is the default round, and [1, ..n] are the appeal rounds. + uint256 numberOfChoices; // The number of choices jurors have when voting. This does not include choice `0` which is reserved for "refuse to arbitrate". + bool jumped; // True if dispute jumped to a parent dispute kit and won't be handled by this DK anymore. + mapping(uint256 => uint256) coreRoundIDToLocal; // Maps id of the round in the core contract to the index of the round of related local dispute. + bytes extraData; // Extradata for the dispute. + } + + struct Round { + Vote[] votes; // Former votes[_appeal][]. + uint256 winningChoice; // The choice with the most votes. Note that in the case of a tie, it is the choice that reached the tied number of votes first. + mapping(uint256 => uint256) counts; // The sum of votes for each choice in the form `counts[choice]`. + bool tied; // True if there is a tie, false otherwise. + uint256 totalVoted; // Former uint[_appeal] votesInEachRound. + uint256 totalCommitted; // Former commitsInRound. + mapping(uint256 choiceId => uint256) paidFees; // Tracks the fees paid for each choice in this round. + mapping(uint256 choiceId => bool) hasPaid; // True if this choice was fully funded, false otherwise. + mapping(address account => mapping(uint256 choiceId => uint256)) contributions; // Maps contributors to their contributions for each choice. + uint256 feeRewards; // Sum of reimbursable appeal fees available to the parties that made contributions to the ruling that ultimately wins a dispute. + uint256[] fundedChoices; // Stores the choices that are fully funded. + uint256 nbVotes; // Maximal number of votes this dispute can get. + mapping(address drawnAddress => bool) alreadyDrawn; // Set to 'true' if the address has already been drawn, so it can't be drawn more than once. + } + + struct Vote { + address account; // The address of the juror. + bytes32 commit; // The commit of the juror. For courts with hidden votes. + uint256 choice; // The choice of the juror. + bool voted; // True if the vote has been cast. + } + + // ************************************* // + // * Storage * // + // ************************************* // + + 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 + 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. + bool public singleDrawPerJuror; // Whether each juror can only draw once per dispute, false by default. + + // ************************************* // + // * Events * // + // ************************************* // + + /// @dev To be emitted when a dispute is created. + /// @param _coreDisputeID The identifier of the dispute in the Arbitrator contract. + /// @param _numberOfChoices The number of choices available in the dispute. + /// @param _extraData The extra data for the dispute. + event DisputeCreation(uint256 indexed _coreDisputeID, uint256 _numberOfChoices, bytes _extraData); + + /// @dev To be emitted when a vote commitment is cast. + /// @param _coreDisputeID The identifier of the dispute in the Arbitrator contract. + /// @param _juror The address of the juror casting the vote commitment. + /// @param _voteIDs The identifiers of the votes in the dispute. + /// @param _commit The commitment of the juror. + event CommitCast(uint256 indexed _coreDisputeID, address indexed _juror, uint256[] _voteIDs, bytes32 _commit); + + /// @dev To be emitted when a funding contribution is made. + /// @param _coreDisputeID The identifier of the dispute in the Arbitrator contract. + /// @param _coreRoundID The identifier of the round in the Arbitrator contract. + /// @param _choice The choice that is being funded. + /// @param _contributor The address of the contributor. + /// @param _amount The amount contributed. + event Contribution( + uint256 indexed _coreDisputeID, + uint256 indexed _coreRoundID, + uint256 _choice, + address indexed _contributor, + uint256 _amount + ); + + /// @dev To be emitted when the contributed funds are withdrawn. + /// @param _coreDisputeID The identifier of the dispute in the Arbitrator contract. + /// @param _coreRoundID The identifier of the round in the Arbitrator contract. + /// @param _choice The choice that is being funded. + /// @param _contributor The address of the contributor. + /// @param _amount The amount withdrawn. + event Withdrawal( + uint256 indexed _coreDisputeID, + uint256 indexed _coreRoundID, + uint256 _choice, + address indexed _contributor, + uint256 _amount + ); + + /// @dev To be emitted when a choice is fully funded for an appeal. + /// @param _coreDisputeID The identifier of the dispute in the Arbitrator contract. + /// @param _coreRoundID The identifier of the round in the Arbitrator contract. + /// @param _choice The choice that is being funded. + event ChoiceFunded(uint256 indexed _coreDisputeID, uint256 indexed _coreRoundID, uint256 indexed _choice); + + // ************************************* // + // * Modifiers * // + // ************************************* // + + modifier onlyByGovernor() { + require(governor == msg.sender, "Access not allowed: Governor only."); + _; + } + + modifier onlyByCore() { + require(address(core) == msg.sender, "Access not allowed: KlerosCore only."); + _; + } + + modifier notJumped(uint256 _coreDisputeID) { + require(!disputes[coreDisputeIDToLocal[_coreDisputeID]].jumped, "Dispute jumped to a parent DK!"); + _; + } + + // ************************************* // + // * Constructor * // + // ************************************* // + + /// @dev Initializer. + /// @param _governor The governor's address. + /// @param _core The KlerosCore arbitrator. + function __DisputeKitClassicBase_initialize(address _governor, KlerosCore _core) internal onlyInitializing { + governor = _governor; + core = _core; + } + + // ************************ // + // * Governance * // + // ************************ // + + /// @dev Allows the governor 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 { + (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 payable _governor) external onlyByGovernor { + governor = _governor; + } + + /// @dev Changes the `core` storage variable. + /// @param _core The new value for the `core` storage variable. + function changeCore(address _core) external onlyByGovernor { + core = KlerosCore(_core); + } + + // ************************************* // + // * State Modifiers * // + // ************************************* // + + /// @dev Creates a local dispute and maps it to the dispute ID in the Core contract. + /// Note: Access restricted to Kleros Core only. + /// @param _coreDisputeID The ID of the dispute in Kleros Core. + /// @param _numberOfChoices Number of choices of the dispute + /// @param _extraData Additional info about the dispute, for possible use in future dispute kits. + /// @param _nbVotes Number of votes for this dispute. + function createDispute( + uint256 _coreDisputeID, + uint256 _numberOfChoices, + bytes calldata _extraData, + uint256 _nbVotes + ) external override onlyByCore { + uint256 localDisputeID = disputes.length; + Dispute storage dispute = disputes.push(); + dispute.numberOfChoices = _numberOfChoices; + dispute.extraData = _extraData; + + // New round in the Core should be created before the dispute creation in DK. + dispute.coreRoundIDToLocal[core.getNumberOfRounds(_coreDisputeID) - 1] = dispute.rounds.length; + + Round storage round = dispute.rounds.push(); + round.nbVotes = _nbVotes; + round.tied = true; + + coreDisputeIDToLocal[_coreDisputeID] = localDisputeID; + emit DisputeCreation(_coreDisputeID, _numberOfChoices, _extraData); + } + + /// @dev Draws the juror from the sortition tree. The drawn address is picked up by Kleros Core. + /// Note: Access restricted to Kleros Core only. + /// @param _coreDisputeID The ID of the dispute in Kleros Core. + /// @param _nonce Nonce of the drawing iteration. + /// @return drawnAddress The drawn address. + function draw( + uint256 _coreDisputeID, + uint256 _nonce + ) external override onlyByCore notJumped(_coreDisputeID) returns (address drawnAddress) { + Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; + Round storage round = dispute.rounds[dispute.rounds.length - 1]; + + ISortitionModule sortitionModule = core.sortitionModule(); + (uint96 courtID, , , , ) = core.disputes(_coreDisputeID); + bytes32 key = bytes32(uint256(courtID)); // Get the ID of the tree. + + drawnAddress = sortitionModule.draw(key, _coreDisputeID, _nonce); + + if (_postDrawCheck(round, _coreDisputeID, drawnAddress)) { + round.votes.push(Vote({account: drawnAddress, commit: bytes32(0), choice: 0, voted: false})); + round.alreadyDrawn[drawnAddress] = true; + } else { + drawnAddress = address(0); + } + } + + /// @dev Sets the caller's commit for the specified votes. It can be called multiple times during the + /// commit period, each call overrides the commits of the previous one. + /// `O(n)` where + /// `n` is the number of votes. + /// @param _coreDisputeID The ID of the dispute in Kleros Core. + /// @param _voteIDs The IDs of the votes. + /// @param _commit The commit. Note that justification string is a part of the commit. + function castCommit( + uint256 _coreDisputeID, + uint256[] calldata _voteIDs, + bytes32 _commit + ) external 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."); + + 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."); + round.votes[_voteIDs[i]].commit = _commit; + } + round.totalCommitted += _voteIDs.length; + emit CommitCast(_coreDisputeID, msg.sender, _voteIDs, _commit); + } + + /// @dev Sets the caller's choices for the specified votes. + /// `O(n)` where + /// `n` is the number of votes. + /// @param _coreDisputeID The ID of the dispute in Kleros Core. + /// @param _voteIDs The IDs of the votes. + /// @param _choice The choice. + /// @param _salt The salt for the commit if the votes were hidden. + /// @param _justification Justification of the choice. + function castVote( + uint256 _coreDisputeID, + uint256[] calldata _voteIDs, + uint256 _choice, + uint256 _salt, + string memory _justification + ) external 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"); + + Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; + require(_choice <= dispute.numberOfChoices, "Choice out of bounds"); + + Round storage round = dispute.rounds[dispute.rounds.length - 1]; + (uint96 courtID, , , , ) = core.disputes(_coreDisputeID); + (, bool hiddenVotes, , , , , ) = core.courts(courtID); + + // Save the votes. + for (uint256 i = 0; i < _voteIDs.length; i++) { + require(round.votes[_voteIDs[i]].account == msg.sender, "The caller has to own the vote."); + require( + !hiddenVotes || round.votes[_voteIDs[i]].commit == keccak256(abi.encodePacked(_choice, _salt)), + "The commit must match the choice in courts with hidden votes." + ); + require(!round.votes[_voteIDs[i]].voted, "Vote already cast."); + round.votes[_voteIDs[i]].choice = _choice; + round.votes[_voteIDs[i]].voted = true; + } + + round.totalVoted += _voteIDs.length; + + round.counts[_choice] += _voteIDs.length; + if (_choice == round.winningChoice) { + if (round.tied) round.tied = false; + } else { + // Voted for another choice. + if (round.counts[_choice] == round.counts[round.winningChoice]) { + // Tie. + if (!round.tied) round.tied = true; + } else if (round.counts[_choice] > round.counts[round.winningChoice]) { + // New winner. + round.winningChoice = _choice; + round.tied = false; + } + } + emit VoteCast(_coreDisputeID, msg.sender, _voteIDs, _choice, _justification); + } + + /// @dev Manages contributions, and appeals a dispute if at least two choices are fully funded. + /// Note that the surplus deposit will be reimbursed. + /// @param _coreDisputeID Index of the dispute in Kleros Core. + /// @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."); + + (uint256 appealPeriodStart, uint256 appealPeriodEnd) = core.appealPeriod(_coreDisputeID); + require(block.timestamp >= appealPeriodStart && block.timestamp < appealPeriodEnd, "Appeal period is over."); + + 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" + ); + 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."); + uint256 appealCost = core.appealCost(_coreDisputeID); + uint256 totalCost = appealCost + (appealCost * multiplier) / ONE_BASIS_POINT; + + // Take up to the amount necessary to fund the current round at the current costs. + uint256 contribution; + if (totalCost > round.paidFees[_choice]) { + contribution = totalCost - round.paidFees[_choice] > msg.value // Overflows and underflows will be managed on the compiler level. + ? msg.value + : totalCost - round.paidFees[_choice]; + emit Contribution(_coreDisputeID, coreRoundID, _choice, msg.sender, contribution); + } + + round.contributions[msg.sender][_choice] += contribution; + round.paidFees[_choice] += contribution; + if (round.paidFees[_choice] >= totalCost) { + round.feeRewards += round.paidFees[_choice]; + round.fundedChoices.push(_choice); + round.hasPaid[_choice] = true; + emit ChoiceFunded(_coreDisputeID, coreRoundID, _choice); + } + + if (round.fundedChoices.length > 1) { + // At least two sides are fully funded. + round.feeRewards = round.feeRewards - appealCost; + + if (core.isDisputeKitJumping(_coreDisputeID)) { + // Don't create a new round in case of a jump, and remove local dispute from the flow. + dispute.jumped = true; + } else { + // Don't subtract 1 from length since both round arrays haven't been updated yet. + dispute.coreRoundIDToLocal[coreRoundID + 1] = dispute.rounds.length; + + Round storage newRound = dispute.rounds.push(); + newRound.nbVotes = core.getNumberOfVotes(_coreDisputeID); + newRound.tied = true; + } + core.appeal{value: appealCost}(_coreDisputeID, dispute.numberOfChoices, dispute.extraData); + } + + if (msg.value > contribution) payable(msg.sender).send(msg.value - contribution); + } + + /// @dev Allows those contributors who attempted to fund an appeal round to withdraw any reimbursable fees or rewards after the dispute gets resolved. + /// Note that withdrawals are not possible if the core contract is paused. + /// @param _coreDisputeID Index of the dispute in Kleros Core contract. + /// @param _beneficiary The address whose rewards to withdraw. + /// @param _coreRoundID The round in the Kleros Core contract the caller wants to withdraw from. + /// @param _choice The ruling option that the caller wants to withdraw from. + /// @return amount The withdrawn amount. + function withdrawFeesAndRewards( + uint256 _coreDisputeID, + address payable _beneficiary, + uint256 _coreRoundID, + uint256 _choice + ) external returns (uint256 amount) { + (, , , bool isRuled, ) = core.disputes(_coreDisputeID); + require(isRuled, "Dispute should be resolved."); + require(!core.paused(), "Core is paused"); + + Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; + Round storage round = dispute.rounds[dispute.coreRoundIDToLocal[_coreRoundID]]; + (uint256 finalRuling, , ) = core.currentRuling(_coreDisputeID); + + if (!round.hasPaid[_choice]) { + // Allow to reimburse if funding was unsuccessful for this ruling option. + amount = round.contributions[_beneficiary][_choice]; + } else { + // Funding was successful for this ruling option. + if (_choice == finalRuling) { + // This ruling option is the ultimate winner. + amount = round.paidFees[_choice] > 0 + ? (round.contributions[_beneficiary][_choice] * round.feeRewards) / round.paidFees[_choice] + : 0; + } else if (!round.hasPaid[finalRuling]) { + // The ultimate winner was not funded in this round. In this case funded ruling option(s) are reimbursed. + amount = + (round.contributions[_beneficiary][_choice] * round.feeRewards) / + (round.paidFees[round.fundedChoices[0]] + round.paidFees[round.fundedChoices[1]]); + } + } + round.contributions[_beneficiary][_choice] = 0; + + if (amount != 0) { + _beneficiary.send(amount); // Deliberate use of send to prevent reverting fallback. It's the user's responsibility to accept ETH. + emit Withdrawal(_coreDisputeID, _coreRoundID, _choice, _beneficiary, amount); + } + } + + // ************************************* // + // * Public Views * // + // ************************************* // + + function getFundedChoices(uint256 _coreDisputeID) public view returns (uint256[] memory fundedChoices) { + Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; + Round storage lastRound = dispute.rounds[dispute.rounds.length - 1]; + return lastRound.fundedChoices; + } + + /// @dev Gets the current ruling of a specified dispute. + /// @param _coreDisputeID The ID of the dispute in Kleros Core. + /// @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 _coreDisputeID + ) external view override returns (uint256 ruling, bool tied, bool overridden) { + Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; + Round storage round = dispute.rounds[dispute.rounds.length - 1]; + tied = round.tied; + 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) { + uint256[] memory fundedChoices = getFundedChoices(_coreDisputeID); + if (fundedChoices.length == 1) { + ruling = fundedChoices[0]; + tied = false; + overridden = true; + } + } + } + + /// @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 reward. + /// @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( + uint256 _coreDisputeID, + uint256 _coreRoundID, + uint256 _voteID, + uint256 /* _feePerJuror */, + uint256 /* _pnkAtStakePerJuror */ + ) external view override returns (uint256) { + // 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]; + (uint256 winningChoice, bool tied, ) = core.currentRuling(_coreDisputeID); + + if (vote.voted && (vote.choice == winningChoice || tied)) { + return ONE_BASIS_POINT; + } else { + return 0; + } + } + + /// @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. + /// @param _coreRoundID The ID of the round in Kleros Core, not in the Dispute Kit. + /// @return The number of coherent jurors. + function getCoherentCount(uint256 _coreDisputeID, uint256 _coreRoundID) external view override returns (uint256) { + Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; + Round storage currentRound = dispute.rounds[dispute.coreRoundIDToLocal[_coreRoundID]]; + (uint256 winningChoice, bool tied, ) = core.currentRuling(_coreDisputeID); + + if (currentRound.totalVoted == 0 || (!tied && currentRound.counts[winningChoice] == 0)) { + return 0; + } else if (tied) { + return currentRound.totalVoted; + } else { + return currentRound.counts[winningChoice]; + } + } + + /// @dev Returns true if all of the jurors have cast their commits for the last round. + /// @param _coreDisputeID The ID of the dispute in Kleros Core. + /// @return Whether all of the jurors have cast their commits for the last round. + function areCommitsAllCast(uint256 _coreDisputeID) external view override returns (bool) { + Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; + Round storage round = dispute.rounds[dispute.rounds.length - 1]; + return round.totalCommitted == round.votes.length; + } + + /// @dev Returns true if all of the jurors have cast their votes for the last round. + /// @param _coreDisputeID The ID of the dispute in Kleros Core. + /// @return Whether all of the jurors have cast their votes for the last round. + function areVotesAllCast(uint256 _coreDisputeID) external view override returns (bool) { + Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; + Round storage round = dispute.rounds[dispute.rounds.length - 1]; + return round.totalVoted == round.votes.length; + } + + /// @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. + /// @param _voteID The ID of the voter. + /// @return Whether the voter was active or not. + function isVoteActive( + uint256 _coreDisputeID, + uint256 _coreRoundID, + uint256 _voteID + ) external view override returns (bool) { + Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; + Vote storage vote = dispute.rounds[dispute.coreRoundIDToLocal[_coreRoundID]].votes[_voteID]; + return vote.voted; + } + + function getRoundInfo( + uint256 _coreDisputeID, + uint256 _coreRoundID, + uint256 _choice + ) + external + view + override + returns ( + uint256 winningChoice, + bool tied, + uint256 totalVoted, + uint256 totalCommited, + uint256 nbVoters, + uint256 choiceCount + ) + { + Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; + Round storage round = dispute.rounds[dispute.coreRoundIDToLocal[_coreRoundID]]; + return ( + round.winningChoice, + round.tied, + round.totalVoted, + round.totalCommitted, + round.votes.length, + round.counts[_choice] + ); + } + + function getVoteInfo( + uint256 _coreDisputeID, + uint256 _coreRoundID, + uint256 _voteID + ) external view override returns (address account, bytes32 commit, uint256 choice, bool voted) { + Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; + Vote storage vote = dispute.rounds[dispute.coreRoundIDToLocal[_coreRoundID]].votes[_voteID]; + return (vote.account, vote.commit, vote.choice, vote.voted); + } + + // ************************************* // + // * Internal * // + // ************************************* // + + /// @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 since we check for insolvency anyway. + /// @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, + uint256 _coreDisputeID, + address _juror + ) internal view virtual returns (bool result) { + (uint96 courtID, , , , ) = core.disputes(_coreDisputeID); + uint256 lockedAmountPerJuror = core + .getRoundInfo(_coreDisputeID, core.getNumberOfRounds(_coreDisputeID) - 1) + .pnkAtStakePerJuror; + (uint256 totalStaked, uint256 totalLocked, , ) = core.sortitionModule().getJurorBalance(_juror, courtID); + result = totalStaked >= totalLocked + lockedAmountPerJuror; + + if (singleDrawPerJuror) { + result = result && !_round.alreadyDrawn[_juror]; + } + } +} diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitGated.sol b/contracts/src/arbitration/dispute-kits/DisputeKitGated.sol index ffd988298..2318f1dfe 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitGated.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitGated.sol @@ -8,10 +8,7 @@ pragma solidity 0.8.24; -import "../KlerosCore.sol"; -import "../interfaces/IDisputeKit.sol"; -import "../../proxy/UUPSProxiable.sol"; -import "../../proxy/Initializable.sol"; +import {DisputeKitClassicBase, KlerosCore} from "./DisputeKitClassicBase.sol"; interface IBalanceHolder { /// @dev Returns the number of tokens in `owner` account. @@ -35,133 +32,22 @@ interface IBalanceHolderERC1155 { /// - a vote aggregation system: plurality, /// - an incentive system: equal split between coherent votes, /// - an appeal system: fund 2 choices only, vote on any choice. -contract DisputeKitGated is IDisputeKit, Initializable, UUPSProxiable { - // ************************************* // - // * Structs * // - // ************************************* // - - struct Dispute { - Round[] rounds; // Rounds of the dispute. 0 is the default round, and [1, ..n] are the appeal rounds. - uint256 numberOfChoices; // The number of choices jurors have when voting. This does not include choice `0` which is reserved for "refuse to arbitrate". - bool jumped; // True if dispute jumped to a parent dispute kit and won't be handled by this DK anymore. - mapping(uint256 => uint256) coreRoundIDToLocal; // Maps id of the round in the core contract to the index of the round of related local dispute. - bytes extraData; // Extradata for the dispute. - } - - struct Round { - Vote[] votes; // Former votes[_appeal][]. - uint256 winningChoice; // The choice with the most votes. Note that in the case of a tie, it is the choice that reached the tied number of votes first. - mapping(uint256 => uint256) counts; // The sum of votes for each choice in the form `counts[choice]`. - bool tied; // True if there is a tie, false otherwise. - uint256 totalVoted; // Former uint[_appeal] votesInEachRound. - uint256 totalCommitted; // Former commitsInRound. - mapping(uint256 choiceId => uint256) paidFees; // Tracks the fees paid for each choice in this round. - mapping(uint256 choiceId => bool) hasPaid; // True if this choice was fully funded, false otherwise. - mapping(address account => mapping(uint256 choiceId => uint256)) contributions; // Maps contributors to their contributions for each choice. - uint256 feeRewards; // Sum of reimbursable appeal fees available to the parties that made contributions to the ruling that ultimately wins a dispute. - uint256[] fundedChoices; // Stores the choices that are fully funded. - uint256 nbVotes; // Maximal number of votes this dispute can get. - } - - struct Vote { - address account; // The address of the juror. - bytes32 commit; // The commit of the juror. For courts with hidden votes. - uint256 choice; // The choice of the juror. - bool voted; // True if the vote has been cast. - } +contract DisputeKitGated is DisputeKitClassicBase { + string public constant override version = "0.8.0"; // ************************************* // // * Storage * // // ************************************* // - 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 - 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. address public tokenGate; // The token used for gating access. uint256 public tokenId; // Only used for ERC-1155 bool public isERC1155; // True if the tokenGate is an ERC-1155, false otherwise. - // ************************************* // - // * Events * // - // ************************************* // - - /// @dev To be emitted when a dispute is created. - /// @param _coreDisputeID The identifier of the dispute in the Arbitrator contract. - /// @param _numberOfChoices The number of choices available in the dispute. - /// @param _extraData The extra data for the dispute. - event DisputeCreation(uint256 indexed _coreDisputeID, uint256 _numberOfChoices, bytes _extraData); - - /// @dev To be emitted when a vote commitment is cast. - /// @param _coreDisputeID The identifier of the dispute in the Arbitrator contract. - /// @param _juror The address of the juror casting the vote commitment. - /// @param _voteIDs The identifiers of the votes in the dispute. - /// @param _commit The commitment of the juror. - event CommitCast(uint256 indexed _coreDisputeID, address indexed _juror, uint256[] _voteIDs, bytes32 _commit); - - /// @dev To be emitted when a funding contribution is made. - /// @param _coreDisputeID The identifier of the dispute in the Arbitrator contract. - /// @param _coreRoundID The identifier of the round in the Arbitrator contract. - /// @param _choice The choice that is being funded. - /// @param _contributor The address of the contributor. - /// @param _amount The amount contributed. - event Contribution( - uint256 indexed _coreDisputeID, - uint256 indexed _coreRoundID, - uint256 _choice, - address indexed _contributor, - uint256 _amount - ); - - /// @dev To be emitted when the contributed funds are withdrawn. - /// @param _coreDisputeID The identifier of the dispute in the Arbitrator contract. - /// @param _coreRoundID The identifier of the round in the Arbitrator contract. - /// @param _choice The choice that is being funded. - /// @param _contributor The address of the contributor. - /// @param _amount The amount withdrawn. - event Withdrawal( - uint256 indexed _coreDisputeID, - uint256 indexed _coreRoundID, - uint256 _choice, - address indexed _contributor, - uint256 _amount - ); - - /// @dev To be emitted when a choice is fully funded for an appeal. - /// @param _coreDisputeID The identifier of the dispute in the Arbitrator contract. - /// @param _coreRoundID The identifier of the round in the Arbitrator contract. - /// @param _choice The choice that is being funded. - event ChoiceFunded(uint256 indexed _coreDisputeID, uint256 indexed _coreRoundID, uint256 indexed _choice); - - // ************************************* // - // * Modifiers * // - // ************************************* // - - modifier onlyByGovernor() { - require(governor == msg.sender, "Access not allowed: Governor only."); - _; - } - - modifier onlyByCore() { - require(address(core) == msg.sender, "Access not allowed: KlerosCore only."); - _; - } - - modifier notJumped(uint256 _coreDisputeID) { - require(!disputes[coreDisputeIDToLocal[_coreDisputeID]].jumped, "Dispute jumped to a parent DK!"); - _; - } - // ************************************* // // * Constructor * // // ************************************* // - /// @dev Constructor, initializing the implementation to reduce attack surface. + /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } @@ -171,7 +57,7 @@ contract DisputeKitGated is IDisputeKit, Initializable, UUPSProxiable { /// @param _core The KlerosCore arbitrator. /// @param _tokenGate The token used for gating access. /// @param _tokenId The token ID for ERC-1155 (ignored for other token types) - /// @param _isERC1155 Whether the token is ERC-1155 + /// @param _isERC1155 Whether the token is an ERC-1155 function initialize( address _governor, KlerosCore _core, @@ -179,8 +65,7 @@ contract DisputeKitGated is IDisputeKit, Initializable, UUPSProxiable { uint256 _tokenId, bool _isERC1155 ) external reinitializer(1) { - governor = _governor; - core = _core; + __DisputeKitClassicBase_initialize(_governor, _core); tokenGate = _tokenGate; tokenId = _tokenId; isERC1155 = _isERC1155; @@ -196,31 +81,6 @@ contract DisputeKitGated is IDisputeKit, Initializable, UUPSProxiable { // NOP } - /// @dev Allows the governor 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 { - (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 payable _governor) external onlyByGovernor { - governor = _governor; - } - - /// @dev Changes the `core` storage variable. - /// @param _core The new value for the `core` storage variable. - function changeCore(address _core) external onlyByGovernor { - core = KlerosCore(_core); - } - /// @dev Changes the `tokenGate` to an ERC-20 or ERC-721 token. /// @param _tokenGate The new value for the `tokenGate` storage variable. function changeTokenGateERC20OrERC721(address _tokenGate) external onlyByGovernor { @@ -237,429 +97,17 @@ contract DisputeKitGated is IDisputeKit, Initializable, UUPSProxiable { isERC1155 = true; } - // ************************************* // - // * State Modifiers * // - // ************************************* // - - /// @dev Creates a local dispute and maps it to the dispute ID in the Core contract. - /// Note: Access restricted to Kleros Core only. - /// @param _coreDisputeID The ID of the dispute in Kleros Core. - /// @param _numberOfChoices Number of choices of the dispute - /// @param _extraData Additional info about the dispute, for possible use in future dispute kits. - /// @param _nbVotes Number of votes for this dispute. - function createDispute( - uint256 _coreDisputeID, - uint256 _numberOfChoices, - bytes calldata _extraData, - uint256 _nbVotes - ) external override onlyByCore { - uint256 localDisputeID = disputes.length; - Dispute storage dispute = disputes.push(); - dispute.numberOfChoices = _numberOfChoices; - dispute.extraData = _extraData; - - // New round in the Core should be created before the dispute creation in DK. - dispute.coreRoundIDToLocal[core.getNumberOfRounds(_coreDisputeID) - 1] = dispute.rounds.length; - - Round storage round = dispute.rounds.push(); - round.nbVotes = _nbVotes; - round.tied = true; - - coreDisputeIDToLocal[_coreDisputeID] = localDisputeID; - emit DisputeCreation(_coreDisputeID, _numberOfChoices, _extraData); - } - - /// @dev Draws the juror from the sortition tree. The drawn address is picked up by Kleros Core. - /// Note: Access restricted to Kleros Core only. - /// @param _coreDisputeID The ID of the dispute in Kleros Core. - /// @param _nonce Nonce of the drawing iteration. - /// @return drawnAddress The drawn address. - function draw( - uint256 _coreDisputeID, - uint256 _nonce - ) external override onlyByCore notJumped(_coreDisputeID) returns (address drawnAddress) { - Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; - Round storage round = dispute.rounds[dispute.rounds.length - 1]; - - ISortitionModule sortitionModule = core.sortitionModule(); - (uint96 courtID, , , , ) = core.disputes(_coreDisputeID); - bytes32 key = bytes32(uint256(courtID)); // Get the ID of the tree. - - drawnAddress = sortitionModule.draw(key, _coreDisputeID, _nonce); - - if (_postDrawCheck(_coreDisputeID, drawnAddress)) { - round.votes.push(Vote({account: drawnAddress, commit: bytes32(0), choice: 0, voted: false})); - } else { - drawnAddress = address(0); - } - } - - /// @dev Sets the caller's commit for the specified votes. It can be called multiple times during the - /// commit period, each call overrides the commits of the previous one. - /// `O(n)` where - /// `n` is the number of votes. - /// @param _coreDisputeID The ID of the dispute in Kleros Core. - /// @param _voteIDs The IDs of the votes. - /// @param _commit The commit. Note that justification string is a part of the commit. - function castCommit( - uint256 _coreDisputeID, - uint256[] calldata _voteIDs, - bytes32 _commit - ) external 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."); - - 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."); - round.votes[_voteIDs[i]].commit = _commit; - } - round.totalCommitted += _voteIDs.length; - emit CommitCast(_coreDisputeID, msg.sender, _voteIDs, _commit); - } - - /// @dev Sets the caller's choices for the specified votes. - /// `O(n)` where - /// `n` is the number of votes. - /// @param _coreDisputeID The ID of the dispute in Kleros Core. - /// @param _voteIDs The IDs of the votes. - /// @param _choice The choice. - /// @param _salt The salt for the commit if the votes were hidden. - /// @param _justification Justification of the choice. - function castVote( - uint256 _coreDisputeID, - uint256[] calldata _voteIDs, - uint256 _choice, - uint256 _salt, - string memory _justification - ) external 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"); - - Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; - require(_choice <= dispute.numberOfChoices, "Choice out of bounds"); - - Round storage round = dispute.rounds[dispute.rounds.length - 1]; - (uint96 courtID, , , , ) = core.disputes(_coreDisputeID); - (, bool hiddenVotes, , , , , ) = core.courts(courtID); - - // Save the votes. - for (uint256 i = 0; i < _voteIDs.length; i++) { - require(round.votes[_voteIDs[i]].account == msg.sender, "The caller has to own the vote."); - require( - !hiddenVotes || round.votes[_voteIDs[i]].commit == keccak256(abi.encodePacked(_choice, _salt)), - "The commit must match the choice in courts with hidden votes." - ); - require(!round.votes[_voteIDs[i]].voted, "Vote already cast."); - round.votes[_voteIDs[i]].choice = _choice; - round.votes[_voteIDs[i]].voted = true; - } - - round.totalVoted += _voteIDs.length; - - round.counts[_choice] += _voteIDs.length; - if (_choice == round.winningChoice) { - if (round.tied) round.tied = false; - } else { - // Voted for another choice. - if (round.counts[_choice] == round.counts[round.winningChoice]) { - // Tie. - if (!round.tied) round.tied = true; - } else if (round.counts[_choice] > round.counts[round.winningChoice]) { - // New winner. - round.winningChoice = _choice; - round.tied = false; - } - } - emit VoteCast(_coreDisputeID, msg.sender, _voteIDs, _choice, _justification); - } - - /// @dev Manages contributions, and appeals a dispute if at least two choices are fully funded. - /// Note that the surplus deposit will be reimbursed. - /// @param _coreDisputeID Index of the dispute in Kleros Core. - /// @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."); - - (uint256 appealPeriodStart, uint256 appealPeriodEnd) = core.appealPeriod(_coreDisputeID); - require(block.timestamp >= appealPeriodStart && block.timestamp < appealPeriodEnd, "Appeal period is over."); - - 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" - ); - 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."); - uint256 appealCost = core.appealCost(_coreDisputeID); - uint256 totalCost = appealCost + (appealCost * multiplier) / ONE_BASIS_POINT; - - // Take up to the amount necessary to fund the current round at the current costs. - uint256 contribution; - if (totalCost > round.paidFees[_choice]) { - contribution = totalCost - round.paidFees[_choice] > msg.value // Overflows and underflows will be managed on the compiler level. - ? msg.value - : totalCost - round.paidFees[_choice]; - emit Contribution(_coreDisputeID, coreRoundID, _choice, msg.sender, contribution); - } - - round.contributions[msg.sender][_choice] += contribution; - round.paidFees[_choice] += contribution; - if (round.paidFees[_choice] >= totalCost) { - round.feeRewards += round.paidFees[_choice]; - round.fundedChoices.push(_choice); - round.hasPaid[_choice] = true; - emit ChoiceFunded(_coreDisputeID, coreRoundID, _choice); - } - - if (round.fundedChoices.length > 1) { - // At least two sides are fully funded. - round.feeRewards = round.feeRewards - appealCost; - - if (core.isDisputeKitJumping(_coreDisputeID)) { - // Don't create a new round in case of a jump, and remove local dispute from the flow. - dispute.jumped = true; - } else { - // Don't subtract 1 from length since both round arrays haven't been updated yet. - dispute.coreRoundIDToLocal[coreRoundID + 1] = dispute.rounds.length; - - Round storage newRound = dispute.rounds.push(); - newRound.nbVotes = core.getNumberOfVotes(_coreDisputeID); - newRound.tied = true; - } - core.appeal{value: appealCost}(_coreDisputeID, dispute.numberOfChoices, dispute.extraData); - } - - if (msg.value > contribution) payable(msg.sender).send(msg.value - contribution); - } - - /// @dev Allows those contributors who attempted to fund an appeal round to withdraw any reimbursable fees or rewards after the dispute gets resolved. - /// Note that withdrawals are not possible if the core contract is paused. - /// @param _coreDisputeID Index of the dispute in Kleros Core contract. - /// @param _beneficiary The address whose rewards to withdraw. - /// @param _coreRoundID The round in the Kleros Core contract the caller wants to withdraw from. - /// @param _choice The ruling option that the caller wants to withdraw from. - /// @return amount The withdrawn amount. - function withdrawFeesAndRewards( - uint256 _coreDisputeID, - address payable _beneficiary, - uint256 _coreRoundID, - uint256 _choice - ) external returns (uint256 amount) { - (, , , bool isRuled, ) = core.disputes(_coreDisputeID); - require(isRuled, "Dispute should be resolved."); - require(!core.paused(), "Core is paused"); - - Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; - Round storage round = dispute.rounds[dispute.coreRoundIDToLocal[_coreRoundID]]; - (uint256 finalRuling, , ) = core.currentRuling(_coreDisputeID); - - if (!round.hasPaid[_choice]) { - // Allow to reimburse if funding was unsuccessful for this ruling option. - amount = round.contributions[_beneficiary][_choice]; - } else { - // Funding was successful for this ruling option. - if (_choice == finalRuling) { - // This ruling option is the ultimate winner. - amount = round.paidFees[_choice] > 0 - ? (round.contributions[_beneficiary][_choice] * round.feeRewards) / round.paidFees[_choice] - : 0; - } else if (!round.hasPaid[finalRuling]) { - // The ultimate winner was not funded in this round. In this case funded ruling option(s) are reimbursed. - amount = - (round.contributions[_beneficiary][_choice] * round.feeRewards) / - (round.paidFees[round.fundedChoices[0]] + round.paidFees[round.fundedChoices[1]]); - } - } - round.contributions[_beneficiary][_choice] = 0; - - if (amount != 0) { - _beneficiary.send(amount); // Deliberate use of send to prevent reverting fallback. It's the user's responsibility to accept ETH. - emit Withdrawal(_coreDisputeID, _coreRoundID, _choice, _beneficiary, amount); - } - } - - // ************************************* // - // * Public Views * // - // ************************************* // - - function getFundedChoices(uint256 _coreDisputeID) public view returns (uint256[] memory fundedChoices) { - Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; - Round storage lastRound = dispute.rounds[dispute.rounds.length - 1]; - return lastRound.fundedChoices; - } - - /// @dev Gets the current ruling of a specified dispute. - /// @param _coreDisputeID The ID of the dispute in Kleros Core. - /// @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 _coreDisputeID - ) external view override returns (uint256 ruling, bool tied, bool overridden) { - Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; - Round storage round = dispute.rounds[dispute.rounds.length - 1]; - tied = round.tied; - 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) { - uint256[] memory fundedChoices = getFundedChoices(_coreDisputeID); - if (fundedChoices.length == 1) { - ruling = fundedChoices[0]; - tied = false; - overridden = true; - } - } - } - - /// @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 reward. - /// @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( - uint256 _coreDisputeID, - uint256 _coreRoundID, - uint256 _voteID, - uint256 /* _feePerJuror */, - uint256 /* _pnkAtStakePerJuror */ - ) external view override returns (uint256) { - // 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]; - (uint256 winningChoice, bool tied, ) = core.currentRuling(_coreDisputeID); - - if (vote.voted && (vote.choice == winningChoice || tied)) { - return ONE_BASIS_POINT; - } else { - return 0; - } - } - - /// @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. - /// @param _coreRoundID The ID of the round in Kleros Core, not in the Dispute Kit. - /// @return The number of coherent jurors. - function getCoherentCount(uint256 _coreDisputeID, uint256 _coreRoundID) external view override returns (uint256) { - Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; - Round storage currentRound = dispute.rounds[dispute.coreRoundIDToLocal[_coreRoundID]]; - (uint256 winningChoice, bool tied, ) = core.currentRuling(_coreDisputeID); - - if (currentRound.totalVoted == 0 || (!tied && currentRound.counts[winningChoice] == 0)) { - return 0; - } else if (tied) { - return currentRound.totalVoted; - } else { - return currentRound.counts[winningChoice]; - } - } - - /// @dev Returns true if all of the jurors have cast their commits for the last round. - /// @param _coreDisputeID The ID of the dispute in Kleros Core. - /// @return Whether all of the jurors have cast their commits for the last round. - function areCommitsAllCast(uint256 _coreDisputeID) external view override returns (bool) { - Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; - Round storage round = dispute.rounds[dispute.rounds.length - 1]; - return round.totalCommitted == round.votes.length; - } - - /// @dev Returns true if all of the jurors have cast their votes for the last round. - /// @param _coreDisputeID The ID of the dispute in Kleros Core. - /// @return Whether all of the jurors have cast their votes for the last round. - function areVotesAllCast(uint256 _coreDisputeID) external view override returns (bool) { - Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; - Round storage round = dispute.rounds[dispute.rounds.length - 1]; - return round.totalVoted == round.votes.length; - } - - /// @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. - /// @param _voteID The ID of the voter. - /// @return Whether the voter was active or not. - function isVoteActive( - uint256 _coreDisputeID, - uint256 _coreRoundID, - uint256 _voteID - ) external view override returns (bool) { - Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; - Vote storage vote = dispute.rounds[dispute.coreRoundIDToLocal[_coreRoundID]].votes[_voteID]; - return vote.voted; - } - - function getRoundInfo( - uint256 _coreDisputeID, - uint256 _coreRoundID, - uint256 _choice - ) - external - view - override - returns ( - uint256 winningChoice, - bool tied, - uint256 totalVoted, - uint256 totalCommited, - uint256 nbVoters, - uint256 choiceCount - ) - { - Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; - Round storage round = dispute.rounds[dispute.coreRoundIDToLocal[_coreRoundID]]; - return ( - round.winningChoice, - round.tied, - round.totalVoted, - round.totalCommitted, - round.votes.length, - round.counts[_choice] - ); - } - - function getVoteInfo( - uint256 _coreDisputeID, - uint256 _coreRoundID, - uint256 _voteID - ) external view override returns (address account, bytes32 commit, uint256 choice, bool voted) { - Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; - Vote storage vote = dispute.rounds[dispute.coreRoundIDToLocal[_coreRoundID]].votes[_voteID]; - return (vote.account, vote.commit, vote.choice, vote.voted); - } - // ************************************* // // * Internal * // // ************************************* // - /// @dev Checks that the chosen address satisfies certain conditions for being drawn. - /// @param _coreDisputeID ID of the dispute in the core contract. - /// @param _juror Chosen address. - /// @return Whether the address can be drawn or not. - /// 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 since we check for insolvency anyway. - function _postDrawCheck(uint256 _coreDisputeID, address _juror) internal view returns (bool) { - (uint96 courtID, , , , ) = core.disputes(_coreDisputeID); - uint256 lockedAmountPerJuror = core - .getRoundInfo(_coreDisputeID, core.getNumberOfRounds(_coreDisputeID) - 1) - .pnkAtStakePerJuror; - (uint256 totalStaked, uint256 totalLocked, , ) = core.sortitionModule().getJurorBalance(_juror, courtID); - if (totalStaked < totalLocked + lockedAmountPerJuror) return false; + /// @inheritdoc DisputeKitClassicBase + function _postDrawCheck( + Round storage _round, + uint256 _coreDisputeID, + address _juror + ) internal view override returns (bool) { + if (!super._postDrawCheck(_round, _coreDisputeID, _juror)) return false; if (isERC1155) { return IBalanceHolderERC1155(tokenGate).balanceOf(_juror, tokenId) > 0; @@ -667,4 +115,4 @@ contract DisputeKitGated is IDisputeKit, Initializable, UUPSProxiable { return IBalanceHolder(tokenGate).balanceOf(_juror) > 0; } } -} +} \ No newline at end of file diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitSybilResistant.sol b/contracts/src/arbitration/dispute-kits/DisputeKitSybilResistant.sol index dc571cfbf..0c6d98d5e 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitSybilResistant.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitSybilResistant.sol @@ -8,10 +8,7 @@ pragma solidity 0.8.24; -import "../KlerosCore.sol"; -import "../interfaces/IDisputeKit.sol"; -import "../../proxy/UUPSProxiable.sol"; -import "../../proxy/Initializable.sol"; +import {DisputeKitClassicBase, KlerosCore} from "./DisputeKitClassicBase.sol"; interface IProofOfHumanity { /// @dev Return true if the submission is registered and not expired. @@ -26,132 +23,20 @@ interface IProofOfHumanity { /// - a vote aggregation system: plurality, /// - an incentive system: equal split between coherent votes, /// - an appeal system: fund 2 choices only, vote on any choice. -contract DisputeKitSybilResistant is IDisputeKit, Initializable, UUPSProxiable { - // ************************************* // - // * Structs * // - // ************************************* // - - struct Dispute { - Round[] rounds; // Rounds of the dispute. 0 is the default round, and [1, ..n] are the appeal rounds. - uint256 numberOfChoices; // The number of choices jurors have when voting. This does not include choice `0` which is reserved for "refuse to arbitrate". - bool jumped; // True if dispute jumped to a parent dispute kit and won't be handled by this DK anymore. - mapping(uint256 => uint256) coreRoundIDToLocal; // Maps id of the round in the core contract to the index of the round of related local dispute. - bytes extraData; // Extradata for the dispute. - } - - struct Round { - Vote[] votes; // Former votes[_appeal][]. - uint256 winningChoice; // The choice with the most votes. Note that in the case of a tie, it is the choice that reached the tied number of votes first. - mapping(uint256 => uint256) counts; // The sum of votes for each choice in the form `counts[choice]`. - bool tied; // True if there is a tie, false otherwise. - uint256 totalVoted; // Former uint[_appeal] votesInEachRound. - uint256 totalCommitted; // Former commitsInRound. - mapping(uint256 choiceId => uint256) paidFees; // Tracks the fees paid for each choice in this round. - mapping(uint256 choiceId => bool) hasPaid; // True if this choice was fully funded, false otherwise. - mapping(address account => mapping(uint256 choiceId => uint256)) contributions; // Maps contributors to their contributions for each choice. - uint256 feeRewards; // Sum of reimbursable appeal fees available to the parties that made contributions to the ruling that ultimately wins a dispute. - uint256[] fundedChoices; // Stores the choices that are fully funded. - uint256 nbVotes; // Maximal number of votes this dispute can get. - mapping(address drawnAddress => bool) alreadyDrawn; // Set to 'true' if the address has already been drawn, so it can't be drawn more than once. - } - - struct Vote { - address account; // The address of the juror. - bytes32 commit; // The commit of the juror. For courts with hidden votes. - uint256 choice; // The choice of the juror. - bool voted; // True if the vote has been cast. - } +contract DisputeKitSybilResistant is DisputeKitClassicBase { + string public constant override version = "0.8.0"; // ************************************* // // * Storage * // // ************************************* // - 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 - 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. IProofOfHumanity public poh; // The Proof of Humanity registry - // ************************************* // - // * Events * // - // ************************************* // - - /// @dev To be emitted when a dispute is created. - /// @param _coreDisputeID The identifier of the dispute in the Arbitrator contract. - /// @param _numberOfChoices The number of choices available in the dispute. - /// @param _extraData The extra data for the dispute. - event DisputeCreation(uint256 indexed _coreDisputeID, uint256 _numberOfChoices, bytes _extraData); - - /// @dev To be emitted when a vote commitment is cast. - /// @param _coreDisputeID The identifier of the dispute in the Arbitrator contract. - /// @param _juror The address of the juror casting the vote commitment. - /// @param _voteIDs The identifiers of the votes in the dispute. - /// @param _commit The commitment of the juror. - event CommitCast(uint256 indexed _coreDisputeID, address indexed _juror, uint256[] _voteIDs, bytes32 _commit); - - /// @dev To be emitted when a funding contribution is made. - /// @param _coreDisputeID The identifier of the dispute in the Arbitrator contract. - /// @param _coreRoundID The identifier of the round in the Arbitrator contract. - /// @param _choice The choice that is being funded. - /// @param _contributor The address of the contributor. - /// @param _amount The amount contributed. - event Contribution( - uint256 indexed _coreDisputeID, - uint256 indexed _coreRoundID, - uint256 _choice, - address indexed _contributor, - uint256 _amount - ); - - /// @dev To be emitted when the contributed funds are withdrawn. - /// @param _coreDisputeID The identifier of the dispute in the Arbitrator contract. - /// @param _coreRoundID The identifier of the round in the Arbitrator contract. - /// @param _choice The choice that is being funded. - /// @param _contributor The address of the contributor. - /// @param _amount The amount withdrawn. - event Withdrawal( - uint256 indexed _coreDisputeID, - uint256 indexed _coreRoundID, - uint256 _choice, - address indexed _contributor, - uint256 _amount - ); - - /// @dev To be emitted when a choice is fully funded for an appeal. - /// @param _coreDisputeID The identifier of the dispute in the Arbitrator contract. - /// @param _coreRoundID The identifier of the round in the Arbitrator contract. - /// @param _choice The choice that is being funded. - event ChoiceFunded(uint256 indexed _coreDisputeID, uint256 indexed _coreRoundID, uint256 indexed _choice); - - // ************************************* // - // * Modifiers * // - // ************************************* // - - modifier onlyByGovernor() { - require(governor == msg.sender, "Access not allowed: Governor only."); - _; - } - - modifier onlyByCore() { - require(address(core) == msg.sender, "Access not allowed: KlerosCore only."); - _; - } - - modifier notJumped(uint256 _coreDisputeID) { - require(!disputes[coreDisputeIDToLocal[_coreDisputeID]].jumped, "Dispute jumped to a parent DK!"); - _; - } - // ************************************* // // * Constructor * // // ************************************* // - /// @dev Constructor, initializing the implementation to reduce attack surface. + /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } @@ -161,9 +46,9 @@ contract DisputeKitSybilResistant is IDisputeKit, Initializable, UUPSProxiable { /// @param _core The KlerosCore arbitrator. /// @param _poh The Proof of Humanity registry. function initialize(address _governor, KlerosCore _core, IProofOfHumanity _poh) external reinitializer(1) { - governor = _governor; - core = _core; + __DisputeKitClassicBase_initialize(_governor, _core); poh = _poh; + singleDrawPerJuror = true; } // ************************ // @@ -176,471 +61,16 @@ contract DisputeKitSybilResistant is IDisputeKit, Initializable, UUPSProxiable { // NOP } - /// @dev Allows the governor 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 { - (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 payable _governor) external onlyByGovernor { - governor = _governor; - } - - /// @dev Changes the `core` storage variable. - /// @param _core The new value for the `core` storage variable. - function changeCore(address _core) external onlyByGovernor { - core = KlerosCore(_core); - } - - /// @dev Changes the `poh` storage variable. - /// @param _poh The new value for the `poh` storage variable. - function changePoh(address _poh) external onlyByGovernor { - poh = IProofOfHumanity(_poh); - } - - // ************************************* // - // * State Modifiers * // - // ************************************* // - - /// @dev Creates a local dispute and maps it to the dispute ID in the Core contract. - /// Note: Access restricted to Kleros Core only. - /// @param _coreDisputeID The ID of the dispute in Kleros Core. - /// @param _numberOfChoices Number of choices of the dispute - /// @param _extraData Additional info about the dispute, for possible use in future dispute kits. - /// @param _nbVotes Number of votes for this dispute. - function createDispute( - uint256 _coreDisputeID, - uint256 _numberOfChoices, - bytes calldata _extraData, - uint256 _nbVotes - ) external override onlyByCore { - uint256 localDisputeID = disputes.length; - Dispute storage dispute = disputes.push(); - dispute.numberOfChoices = _numberOfChoices; - dispute.extraData = _extraData; - - // New round in the Core should be created before the dispute creation in DK. - dispute.coreRoundIDToLocal[core.getNumberOfRounds(_coreDisputeID) - 1] = dispute.rounds.length; - - Round storage round = dispute.rounds.push(); - round.nbVotes = _nbVotes; - round.tied = true; - - coreDisputeIDToLocal[_coreDisputeID] = localDisputeID; - emit DisputeCreation(_coreDisputeID, _numberOfChoices, _extraData); - } - - /// @dev Draws the juror from the sortition tree. The drawn address is picked up by Kleros Core. - /// Note: Access restricted to Kleros Core only. - /// @param _coreDisputeID The ID of the dispute in Kleros Core. - /// @param _nonce Nonce of the drawing iteration. - /// @return drawnAddress The drawn address. - function draw( - uint256 _coreDisputeID, - uint256 _nonce - ) external override onlyByCore notJumped(_coreDisputeID) returns (address drawnAddress) { - Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; - Round storage round = dispute.rounds[dispute.rounds.length - 1]; - - ISortitionModule sortitionModule = core.sortitionModule(); - (uint96 courtID, , , , ) = core.disputes(_coreDisputeID); - bytes32 key = bytes32(uint256(courtID)); // Get the ID of the tree. - - drawnAddress = sortitionModule.draw(key, _coreDisputeID, _nonce); - - if (_postDrawCheck(_coreDisputeID, drawnAddress) && !round.alreadyDrawn[drawnAddress]) { - round.votes.push(Vote({account: drawnAddress, commit: bytes32(0), choice: 0, voted: false})); - round.alreadyDrawn[drawnAddress] = true; - } else { - drawnAddress = address(0); - } - } - - /// @dev Sets the caller's commit for the specified votes. It can be called multiple times during the - /// commit period, each call overrides the commits of the previous one. - /// `O(n)` where - /// `n` is the number of votes. - /// @param _coreDisputeID The ID of the dispute in Kleros Core. - /// @param _voteIDs The IDs of the votes. - /// @param _commit The commit. Note that justification string is a part of the commit. - function castCommit( - uint256 _coreDisputeID, - uint256[] calldata _voteIDs, - bytes32 _commit - ) external 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."); - - 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."); - round.votes[_voteIDs[i]].commit = _commit; - } - round.totalCommitted += _voteIDs.length; - emit CommitCast(_coreDisputeID, msg.sender, _voteIDs, _commit); - } - - /// @dev Sets the caller's choices for the specified votes. - /// `O(n)` where - /// `n` is the number of votes. - /// @param _coreDisputeID The ID of the dispute in Kleros Core. - /// @param _voteIDs The IDs of the votes. - /// @param _choice The choice. - /// @param _salt The salt for the commit if the votes were hidden. - /// @param _justification Justification of the choice. - function castVote( - uint256 _coreDisputeID, - uint256[] calldata _voteIDs, - uint256 _choice, - uint256 _salt, - string memory _justification - ) external 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"); - - Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; - require(_choice <= dispute.numberOfChoices, "Choice out of bounds"); - - Round storage round = dispute.rounds[dispute.rounds.length - 1]; - (uint96 courtID, , , , ) = core.disputes(_coreDisputeID); - (, bool hiddenVotes, , , , , ) = core.courts(courtID); - - // Save the votes. - for (uint256 i = 0; i < _voteIDs.length; i++) { - require(round.votes[_voteIDs[i]].account == msg.sender, "The caller has to own the vote."); - require( - !hiddenVotes || round.votes[_voteIDs[i]].commit == keccak256(abi.encodePacked(_choice, _salt)), - "The commit must match the choice in courts with hidden votes." - ); - require(!round.votes[_voteIDs[i]].voted, "Vote already cast."); - round.votes[_voteIDs[i]].choice = _choice; - round.votes[_voteIDs[i]].voted = true; - } - - round.totalVoted += _voteIDs.length; - - round.counts[_choice] += _voteIDs.length; - if (_choice == round.winningChoice) { - if (round.tied) round.tied = false; - } else { - // Voted for another choice. - if (round.counts[_choice] == round.counts[round.winningChoice]) { - // Tie. - if (!round.tied) round.tied = true; - } else if (round.counts[_choice] > round.counts[round.winningChoice]) { - // New winner. - round.winningChoice = _choice; - round.tied = false; - } - } - emit VoteCast(_coreDisputeID, msg.sender, _voteIDs, _choice, _justification); - } - - /// @dev Manages contributions, and appeals a dispute if at least two choices are fully funded. - /// Note that the surplus deposit will be reimbursed. - /// @param _coreDisputeID Index of the dispute in Kleros Core. - /// @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."); - - (uint256 appealPeriodStart, uint256 appealPeriodEnd) = core.appealPeriod(_coreDisputeID); - require(block.timestamp >= appealPeriodStart && block.timestamp < appealPeriodEnd, "Appeal period is over."); - - 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" - ); - 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."); - uint256 appealCost = core.appealCost(_coreDisputeID); - uint256 totalCost = appealCost + (appealCost * multiplier) / ONE_BASIS_POINT; - - // Take up to the amount necessary to fund the current round at the current costs. - uint256 contribution; - if (totalCost > round.paidFees[_choice]) { - contribution = totalCost - round.paidFees[_choice] > msg.value // Overflows and underflows will be managed on the compiler level. - ? msg.value - : totalCost - round.paidFees[_choice]; - emit Contribution(_coreDisputeID, coreRoundID, _choice, msg.sender, contribution); - } - - round.contributions[msg.sender][_choice] += contribution; - round.paidFees[_choice] += contribution; - if (round.paidFees[_choice] >= totalCost) { - round.feeRewards += round.paidFees[_choice]; - round.fundedChoices.push(_choice); - round.hasPaid[_choice] = true; - emit ChoiceFunded(_coreDisputeID, coreRoundID, _choice); - } - - if (round.fundedChoices.length > 1) { - // At least two sides are fully funded. - round.feeRewards = round.feeRewards - appealCost; - - if (core.isDisputeKitJumping(_coreDisputeID)) { - // Don't create a new round in case of a jump, and remove local dispute from the flow. - dispute.jumped = true; - } else { - // Don't subtract 1 from length since both round arrays haven't been updated yet. - dispute.coreRoundIDToLocal[coreRoundID + 1] = dispute.rounds.length; - - Round storage newRound = dispute.rounds.push(); - newRound.nbVotes = core.getNumberOfVotes(_coreDisputeID); - newRound.tied = true; - } - core.appeal{value: appealCost}(_coreDisputeID, dispute.numberOfChoices, dispute.extraData); - } - - if (msg.value > contribution) payable(msg.sender).send(msg.value - contribution); - } - - /// @dev Allows those contributors who attempted to fund an appeal round to withdraw any reimbursable fees or rewards after the dispute gets resolved. - /// Note that withdrawals are not possible if the core contract is paused. - /// @param _coreDisputeID Index of the dispute in Kleros Core contract. - /// @param _beneficiary The address whose rewards to withdraw. - /// @param _coreRoundID The round in the Kleros Core contract the caller wants to withdraw from. - /// @param _choice The ruling option that the caller wants to withdraw from. - /// @return amount The withdrawn amount. - function withdrawFeesAndRewards( - uint256 _coreDisputeID, - address payable _beneficiary, - uint256 _coreRoundID, - uint256 _choice - ) external returns (uint256 amount) { - (, , , bool isRuled, ) = core.disputes(_coreDisputeID); - require(isRuled, "Dispute should be resolved."); - require(!core.paused(), "Core is paused"); - - Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; - Round storage round = dispute.rounds[dispute.coreRoundIDToLocal[_coreRoundID]]; - (uint256 finalRuling, , ) = core.currentRuling(_coreDisputeID); - - if (!round.hasPaid[_choice]) { - // Allow to reimburse if funding was unsuccessful for this ruling option. - amount = round.contributions[_beneficiary][_choice]; - } else { - // Funding was successful for this ruling option. - if (_choice == finalRuling) { - // This ruling option is the ultimate winner. - amount = round.paidFees[_choice] > 0 - ? (round.contributions[_beneficiary][_choice] * round.feeRewards) / round.paidFees[_choice] - : 0; - } else if (!round.hasPaid[finalRuling]) { - // The ultimate winner was not funded in this round. In this case funded ruling option(s) are reimbursed. - amount = - (round.contributions[_beneficiary][_choice] * round.feeRewards) / - (round.paidFees[round.fundedChoices[0]] + round.paidFees[round.fundedChoices[1]]); - } - } - round.contributions[_beneficiary][_choice] = 0; - - if (amount != 0) { - _beneficiary.send(amount); // Deliberate use of send to prevent reverting fallback. It's the user's responsibility to accept ETH. - emit Withdrawal(_coreDisputeID, _coreRoundID, _choice, _beneficiary, amount); - } - } - - // ************************************* // - // * Public Views * // - // ************************************* // - - function getFundedChoices(uint256 _coreDisputeID) public view returns (uint256[] memory fundedChoices) { - Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; - Round storage lastRound = dispute.rounds[dispute.rounds.length - 1]; - return lastRound.fundedChoices; - } - - /// @dev Gets the current ruling of a specified dispute. - /// @param _coreDisputeID The ID of the dispute in Kleros Core. - /// @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 _coreDisputeID - ) external view override returns (uint256 ruling, bool tied, bool overridden) { - Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; - Round storage round = dispute.rounds[dispute.rounds.length - 1]; - tied = round.tied; - 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) { - uint256[] memory fundedChoices = getFundedChoices(_coreDisputeID); - if (fundedChoices.length == 1) { - ruling = fundedChoices[0]; - tied = false; - overridden = true; - } - } - } - - /// @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 reward. - /// @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( - uint256 _coreDisputeID, - uint256 _coreRoundID, - uint256 _voteID, - uint256 /* _feePerJuror */, - uint256 /* _pnkAtStakePerJuror */ - ) external view override returns (uint256) { - // 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]; - (uint256 winningChoice, bool tied, ) = core.currentRuling(_coreDisputeID); - - if (vote.voted && (vote.choice == winningChoice || tied)) { - return ONE_BASIS_POINT; - } else { - return 0; - } - } - - /// @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. - /// @param _coreRoundID The ID of the round in Kleros Core, not in the Dispute Kit. - /// @return The number of coherent jurors. - function getCoherentCount(uint256 _coreDisputeID, uint256 _coreRoundID) external view override returns (uint256) { - Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; - Round storage currentRound = dispute.rounds[dispute.coreRoundIDToLocal[_coreRoundID]]; - (uint256 winningChoice, bool tied, ) = core.currentRuling(_coreDisputeID); - - if (currentRound.totalVoted == 0 || (!tied && currentRound.counts[winningChoice] == 0)) { - return 0; - } else if (tied) { - return currentRound.totalVoted; - } else { - return currentRound.counts[winningChoice]; - } - } - - /// @dev Returns true if all of the jurors have cast their commits for the last round. - /// @param _coreDisputeID The ID of the dispute in Kleros Core. - /// @return Whether all of the jurors have cast their commits for the last round. - function areCommitsAllCast(uint256 _coreDisputeID) external view override returns (bool) { - Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; - Round storage round = dispute.rounds[dispute.rounds.length - 1]; - return round.totalCommitted == round.votes.length; - } - - /// @dev Returns true if all of the jurors have cast their votes for the last round. - /// @param _coreDisputeID The ID of the dispute in Kleros Core. - /// @return Whether all of the jurors have cast their votes for the last round. - function areVotesAllCast(uint256 _coreDisputeID) external view override returns (bool) { - Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; - Round storage round = dispute.rounds[dispute.rounds.length - 1]; - return round.totalVoted == round.votes.length; - } - - /// @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. - /// @param _voteID The ID of the voter. - /// @return Whether the voter was active or not. - function isVoteActive( - uint256 _coreDisputeID, - uint256 _coreRoundID, - uint256 _voteID - ) external view override returns (bool) { - Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; - Vote storage vote = dispute.rounds[dispute.coreRoundIDToLocal[_coreRoundID]].votes[_voteID]; - return vote.voted; - } - - function getRoundInfo( - uint256 _coreDisputeID, - uint256 _coreRoundID, - uint256 _choice - ) - external - view - override - returns ( - uint256 winningChoice, - bool tied, - uint256 totalVoted, - uint256 totalCommited, - uint256 nbVoters, - uint256 choiceCount - ) - { - Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; - Round storage round = dispute.rounds[dispute.coreRoundIDToLocal[_coreRoundID]]; - return ( - round.winningChoice, - round.tied, - round.totalVoted, - round.totalCommitted, - round.votes.length, - round.counts[_choice] - ); - } - - function getVoteInfo( - uint256 _coreDisputeID, - uint256 _coreRoundID, - uint256 _voteID - ) external view override returns (address account, bytes32 commit, uint256 choice, bool voted) { - Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; - Vote storage vote = dispute.rounds[dispute.coreRoundIDToLocal[_coreRoundID]].votes[_voteID]; - return (vote.account, vote.commit, vote.choice, vote.voted); - } - // ************************************* // // * Internal * // // ************************************* // - /// @dev Checks that the chosen address satisfies certain conditions for being drawn. - /// @param _coreDisputeID ID of the dispute in the core contract. - /// @param _juror Chosen address. - /// @return Whether the address can be drawn or not. - /// 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 since we check for insolvency anyway. - function _postDrawCheck(uint256 _coreDisputeID, address _juror) internal view returns (bool) { - (uint96 courtID, , , , ) = core.disputes(_coreDisputeID); - uint256 lockedAmountPerJuror = core - .getRoundInfo(_coreDisputeID, core.getNumberOfRounds(_coreDisputeID) - 1) - .pnkAtStakePerJuror; - (uint256 totalStaked, uint256 totalLocked, , ) = core.sortitionModule().getJurorBalance(_juror, courtID); - if (totalStaked < totalLocked + lockedAmountPerJuror) { - return false; - } else { - return _proofOfHumanity(_juror); - } - } - - /// @dev Checks if an address belongs to the Proof of Humanity registry. - /// @param _address The address to check. - /// @return registered True if registered. - function _proofOfHumanity(address _address) internal view returns (bool) { - return poh.isRegistered(_address); + /// @inheritdoc DisputeKitClassicBase + function _postDrawCheck( + Round storage _round, + uint256 _coreDisputeID, + address _juror + ) internal view override returns (bool) { + return super._postDrawCheck(_round, _coreDisputeID, _juror) && poh.isRegistered(_juror); } } diff --git a/contracts/src/arbitration/evidence/EvidenceModule.sol b/contracts/src/arbitration/evidence/EvidenceModule.sol index 3f7e80c29..21405bc6d 100644 --- a/contracts/src/arbitration/evidence/EvidenceModule.sol +++ b/contracts/src/arbitration/evidence/EvidenceModule.sol @@ -16,6 +16,8 @@ import "../../proxy/Initializable.sol"; /// @title Evidence Module contract EvidenceModule is IEvidence, Initializable, UUPSProxiable { + string public constant override version = "0.8.0"; + // ************************************* // // * Storage * // // ************************************* // @@ -35,7 +37,7 @@ contract EvidenceModule is IEvidence, Initializable, UUPSProxiable { // * Constructor * // // ************************************* // - /// @dev Constructor, initializing the implementation to reduce attack surface. + /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } diff --git a/contracts/src/arbitration/university/KlerosCoreUniversity.sol b/contracts/src/arbitration/university/KlerosCoreUniversity.sol index d3e5be37c..1f2e888e2 100644 --- a/contracts/src/arbitration/university/KlerosCoreUniversity.sol +++ b/contracts/src/arbitration/university/KlerosCoreUniversity.sol @@ -15,6 +15,8 @@ import {Initializable} from "../../proxy/Initializable.sol"; contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { using SafeERC20 for IERC20; + string public constant override version = "0.8.0"; + // ************************************* // // * Enums / Structs * // // ************************************* // @@ -180,7 +182,7 @@ contract KlerosCoreUniversity is IArbitratorV2, UUPSProxiable, Initializable { // * Constructor * // // ************************************* // - /// @dev Constructor, initializing the implementation to reduce attack surface. + /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } diff --git a/contracts/src/arbitration/university/SortitionModuleUniversity.sol b/contracts/src/arbitration/university/SortitionModuleUniversity.sol index a2d3f2c63..7c79651d8 100644 --- a/contracts/src/arbitration/university/SortitionModuleUniversity.sol +++ b/contracts/src/arbitration/university/SortitionModuleUniversity.sol @@ -20,6 +20,8 @@ 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"; + // ************************************* // // * Enums / Structs * // // ************************************* // @@ -66,7 +68,7 @@ contract SortitionModuleUniversity is ISortitionModuleUniversity, UUPSProxiable, // * Constructor * // // ************************************* // - /// @dev Constructor, initializing the implementation to reduce attack surface. + /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } diff --git a/contracts/src/gateway/ForeignGateway.sol b/contracts/src/gateway/ForeignGateway.sol index d69889edf..81649b072 100644 --- a/contracts/src/gateway/ForeignGateway.sol +++ b/contracts/src/gateway/ForeignGateway.sol @@ -16,6 +16,8 @@ import "../libraries/Constants.sol"; /// Foreign Gateway /// Counterpart of `HomeGateway` contract ForeignGateway is IForeignGateway, UUPSProxiable, Initializable { + string public constant override version = "0.8.0"; + // ************************************* // // * Enums / Structs * // // ************************************* // @@ -71,7 +73,7 @@ contract ForeignGateway is IForeignGateway, UUPSProxiable, Initializable { // * Constructor * // // ************************************* // - /// @dev Constructor, initializing the implementation to reduce attack surface. + /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } diff --git a/contracts/src/gateway/HomeGateway.sol b/contracts/src/gateway/HomeGateway.sol index 180b0e878..ade0de2e6 100644 --- a/contracts/src/gateway/HomeGateway.sol +++ b/contracts/src/gateway/HomeGateway.sol @@ -20,6 +20,8 @@ import "../proxy/Initializable.sol"; contract HomeGateway is IHomeGateway, UUPSProxiable, Initializable { using SafeERC20 for IERC20; + string public constant override version = "0.8.0"; + // ************************************* // // * Enums / Structs * // // ************************************* // @@ -57,7 +59,7 @@ contract HomeGateway is IHomeGateway, UUPSProxiable, Initializable { // * Constructor * // // ************************************* // - /// @dev Constructor, initializing the implementation to reduce attack surface. + /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } diff --git a/contracts/src/kleros-v1/kleros-liquid-xdai/xKlerosLiquidV2.sol b/contracts/src/kleros-v1/kleros-liquid-xdai/xKlerosLiquidV2.sol index 7bc52279e..17354c3cd 100644 --- a/contracts/src/kleros-v1/kleros-liquid-xdai/xKlerosLiquidV2.sol +++ b/contracts/src/kleros-v1/kleros-liquid-xdai/xKlerosLiquidV2.sol @@ -16,6 +16,7 @@ import "../../gateway/interfaces/IForeignGateway.sol"; /// @dev This contract is an adaption of Mainnet's KlerosLiquid (https://github.com/kleros/kleros/blob/69cfbfb2128c29f1625b3a99a3183540772fda08/contracts/kleros/KlerosLiquid.sol) /// for xDai chain. Notice that variables referring to ETH values in this contract, will hold the native token values of the chain on which xKlerosLiquid is deployed. /// When this contract gets deployed on xDai chain, ETH variables will hold xDai values. +/// @custom:oz-upgrades-unsafe-allow external-library-linking contract xKlerosLiquidV2 is Initializable, ITokenController, IArbitratorV2 { // ************************************* // // * Enums / Structs * // diff --git a/contracts/src/proxy/KlerosProxies.sol b/contracts/src/proxy/KlerosProxies.sol index 7c2bb2ddb..2a1a9381f 100644 --- a/contracts/src/proxy/KlerosProxies.sol +++ b/contracts/src/proxy/KlerosProxies.sol @@ -19,6 +19,14 @@ contract DisputeKitClassicProxy is UUPSProxy { constructor(address _implementation, bytes memory _data) UUPSProxy(_implementation, _data) {} } +contract DisputeKitGatedProxy is UUPSProxy { + constructor(address _implementation, bytes memory _data) UUPSProxy(_implementation, _data) {} +} + +contract DisputeKitSybilResistantProxy is UUPSProxy { + constructor(address _implementation, bytes memory _data) UUPSProxy(_implementation, _data) {} +} + contract DisputeTemplateRegistryProxy is UUPSProxy { constructor(address _implementation, bytes memory _data) UUPSProxy(_implementation, _data) {} } diff --git a/contracts/src/proxy/UUPSProxiable.sol b/contracts/src/proxy/UUPSProxiable.sol index 258a7a622..dc107674b 100644 --- a/contracts/src/proxy/UUPSProxiable.sol +++ b/contracts/src/proxy/UUPSProxiable.sol @@ -1,56 +1,41 @@ //SPDX-License-Identifier: MIT -// Adapted from - -/** - * @authors: [@malatrax] - * @reviewers: [] - * @auditors: [] - * @bounties: [] - * @deployments: [] - */ + pragma solidity 0.8.24; -/** - * @title UUPS Proxiable - * @author Simon Malatrait - * @dev This contract implements an upgradeability mechanism designed for UUPS proxies. - * The functions included here can perform an upgrade of an UUPS Proxy, when this contract is set as the implementation behind such a proxy. - * - * IMPORTANT: A UUPS proxy requires its upgradeability functions to be in the implementation as opposed to the transparent proxy. - * This means that if the proxy is upgraded to an implementation that does not support this interface, it will no longer be upgradeable. - * - * A security mechanism ensures that an upgrade does not turn off upgradeability accidentally, although this risk is - * reinstated if the upgrade retains upgradeability but removes the security mechanism, e.g. by replacing - * `UUPSProxiable` with a custom implementation of upgrades. - * - * The `_authorizeUpgrade` function must be overridden to include access restriction to the upgrade mechanism. - */ +/// @title UUPS Proxiable +/// @author Simon Malatrait +/// @dev This contract implements an upgradeability mechanism designed for UUPS proxies. +/// @dev Adapted from +/// The functions included here can perform an upgrade of an UUPS Proxy, when this contract is set as the implementation behind such a proxy. +/// +/// IMPORTANT: A UUPS proxy requires its upgradeability functions to be in the implementation as opposed to the transparent proxy. +/// This means that if the proxy is upgraded to an implementation that does not support this interface, it will no longer be upgradeable. +/// +/// A security mechanism ensures that an upgrade does not turn off upgradeability accidentally, although this risk is +/// reinstated if the upgrade retains upgradeability but removes the security mechanism, e.g. by replacing +/// `UUPSProxiable` with a custom implementation of upgrades. +/// +/// The `_authorizeUpgrade` function must be overridden to include access restriction to the upgrade mechanism. abstract contract UUPSProxiable { // ************************************* // // * Event * // // ************************************* // - /** - * Emitted when the `implementation` has been successfully upgraded. - * @param newImplementation Address of the new implementation the proxy is now forwarding calls to. - */ + /// @dev Emitted when the `implementation` has been successfully upgraded. + /// @param newImplementation Address of the new implementation the proxy is now forwarding calls to. event Upgraded(address indexed newImplementation); // ************************************* // // * Error * // // ************************************* // - /** - * @dev The call is from an unauthorized context. - */ + /// @dev The call is from an unauthorized context. error UUPSUnauthorizedCallContext(); - /** - * @dev The storage `slot` is unsupported as a UUID. - */ + /// @dev The storage `slot` is unsupported as a UUID. error UUPSUnsupportedProxiableUUID(bytes32 slot); - /// The `implementation` is not UUPS-compliant + /// @dev The `implementation` is not UUPS-compliant error InvalidImplementation(address implementation); /// Failed Delegated call @@ -60,48 +45,40 @@ abstract contract UUPSProxiable { // * Storage * // // ************************************* // - /** - * @dev Storage slot with the address of the current implementation. - * This is the keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1, and is - * validated in the constructor. - * NOTE: bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1) - */ + /// @dev Storage slot with the address of the current implementation. + /// @dev This is the keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1, and is + /// @dev validated in the constructor. + /// @dev NOTE: bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1) bytes32 private constant IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; - /** - * @dev Storage variable of the proxiable contract address. - * It is used to check whether or not the current call is from the proxy. - */ + /// @dev Storage variable of the proxiable contract address. + /// @dev It is used to check whether or not the current call is from the proxy. + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable address private immutable __self = address(this); // ************************************* // // * Governance * // // ************************************* // - /** - * @dev Function that should revert when `msg.sender` is not authorized to upgrade the contract. - * @dev Called by {upgradeToAndCall}. - */ + /// @dev Function that should revert when `msg.sender` is not authorized to upgrade the contract. + /// @dev Called by {upgradeToAndCall}. function _authorizeUpgrade(address newImplementation) internal virtual; // ************************************* // // * State Modifiers * // // ************************************* // - /** - * @dev Upgrade mechanism including access control and UUPS-compliance. - * @param newImplementation Address of the new implementation contract. - * @param data Data used in a delegate call to `newImplementation` if non-empty. This will typically be an encoded - * function call, and allows initializing the storage of the proxy like a Solidity constructor. - * - * @dev Reverts if the execution is not performed via delegatecall or the execution - * context is not of a proxy with an ERC1967-compliant implementation pointing to self. - */ + /// @dev Upgrade mechanism including access control and UUPS-compliance. + /// @param newImplementation Address of the new implementation contract. + /// @param data Data used in a delegate call to `newImplementation` if non-empty. This will typically be an encoded + /// function call, and allows initializing the storage of the proxy like a Solidity constructor. + /// @dev Reverts if the execution is not performed via delegatecall or the execution + /// context is not of a proxy with an ERC1967-compliant implementation pointing to self. function upgradeToAndCall(address newImplementation, bytes memory data) public payable virtual { _authorizeUpgrade(newImplementation); - /* Check that the execution is being performed through a delegatecall call and that the execution context is - a proxy contract with an implementation (as defined in ERC1967) pointing to self. */ + // Check that the execution is being performed through a delegatecall call and that the execution context is + // a proxy contract with an implementation (as defined in ERC1967) pointing to self. if (address(this) == __self || _getImplementation() != __self) { revert UUPSUnauthorizedCallContext(); } @@ -118,6 +95,7 @@ abstract contract UUPSProxiable { if (data.length != 0) { // The return data is not checked (checking, in case of success, that the newImplementation code is non-empty if the return data is empty) because the authorized callee is trusted. + /// @custom:oz-upgrades-unsafe-allow delegatecall (bool success, ) = newImplementation.delegatecall(data); if (!success) { revert FailedDelegateCall(); @@ -132,14 +110,12 @@ abstract contract UUPSProxiable { // * Public Views * // // ************************************* // - /** - * @dev Implementation of the ERC1822 `proxiableUUID` function. This returns the storage slot used by the - * implementation. It is used to validate the implementation's compatibility when performing an upgrade. - * - * IMPORTANT: A proxy pointing at a proxiable contract should not be considered proxiable itself, because this risks - * bricking a proxy that upgrades to it, by delegating to itself until out of gas. Thus it is critical that this - * function revert if invoked through a proxy. This is guaranteed by the if statement. - */ + /// @dev Implementation of the ERC1822 `proxiableUUID` function. This returns the storage slot used by the + /// implementation. It is used to validate the implementation's compatibility when performing an upgrade. + /// + /// IMPORTANT: A proxy pointing at a proxiable contract should not be considered proxiable itself, because this risks + /// bricking a proxy that upgrades to it, by delegating to itself until out of gas. Thus it is critical that this + /// function revert if invoked through a proxy. This is guaranteed by the if statement. function proxiableUUID() external view virtual returns (bytes32) { if (address(this) != __self) { // Must not be called through delegatecall @@ -148,6 +124,10 @@ abstract contract UUPSProxiable { return IMPLEMENTATION_SLOT; } + /// @dev Returns the version of the implementation. + /// @return Version string. + function version() external view virtual returns (string memory); + // ************************************* // // * Internal Views * // // ************************************* // diff --git a/contracts/src/proxy/UUPSProxy.sol b/contracts/src/proxy/UUPSProxy.sol index f04f814e9..6193431b5 100644 --- a/contracts/src/proxy/UUPSProxy.sol +++ b/contracts/src/proxy/UUPSProxy.sol @@ -1,41 +1,27 @@ //SPDX-License-Identifier: MIT -// Adapted from -/** - * @authors: [@malatrax] - * @reviewers: [] - * @auditors: [] - * @bounties: [] - * @deployments: [] - */ pragma solidity 0.8.24; -/** - * @title UUPS Proxy - * @author Simon Malatrait - * @dev This contract implements a UUPS Proxy compliant with ERC-1967 & ERC-1822. - * @dev This contract delegates all calls to another contract (UUPS Proxiable) through a fallback function and the use of the `delegatecall` EVM instruction. - * @dev We refer to the Proxiable contract (as per ERC-1822) with `implementation`. - */ +/// @title UUPS Proxy +/// @author Simon Malatrait +/// @dev This contract implements a UUPS Proxy compliant with ERC-1967 & ERC-1822. +/// @dev This contract delegates all calls to another contract (UUPS Proxiable) through a fallback function and the use of the `delegatecall` EVM instruction. +/// @dev We refer to the Proxiable contract (as per ERC-1822) with `implementation`. +/// @dev Adapted from contract UUPSProxy { - /** - * @dev Storage slot with the address of the current implementation. - * This is the keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1, and is - * validated in the constructor. - * NOTE: bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1) - */ + /// @dev Storage slot with the address of the current implementation. + /// This is the keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1, and is + /// validated in the constructor. + /// NOTE: bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1) bytes32 private constant IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; // ************************************* // // * Constructor * // // ************************************* // - /** - * @dev Initializes the upgradeable proxy with an initial implementation specified by `_implementation`. - * - * If `_data` is nonempty, it's used as data in a delegate call to `_implementation`. This will typically be an encoded - * function call, and allows initializing the storage of the proxy like a Solidity constructor. - */ + /// @dev Initializes the upgradeable proxy with an initial implementation specified by `_implementation`. + /// If `_data` is nonempty, it's used as data in a delegate call to `_implementation`. This will typically be an encoded + /// function call, and allows initializing the storage of the proxy like a Solidity constructor. constructor(address _implementation, bytes memory _data) { assembly { sstore(IMPLEMENTATION_SLOT, _implementation) @@ -51,11 +37,8 @@ contract UUPSProxy { // * State Modifiers * // // ************************************* // - /** - * @dev Delegates the current call to `implementation`. - * - * NOTE: This function does not return to its internal call site, it will return directly to the external caller. - */ + /// @dev Delegates the current call to `implementation`. + /// NOTE: This function does not return to its internal call site, it will return directly to the external caller. function _delegate(address implementation) internal { assembly { // Copy msg.data. We take full control of memory in this inline assembly @@ -95,10 +78,8 @@ contract UUPSProxy { // * Fallback * // // ************************************* // - /** - * @dev Fallback function that delegates calls to the address returned by `_implementation()`. Will run if no other - * function in the contract matches the call data. - */ + /// @dev Fallback function that delegates calls to the address returned by `_implementation()`. + /// @dev Will run if no other function in the contract matches the call data. fallback() external payable { _delegate(_getImplementation()); } diff --git a/contracts/src/proxy/mock/UUPSUpgradeableMocks.sol b/contracts/src/proxy/mock/UUPSUpgradeableMocks.sol index 8133dd5c5..9467f1fe0 100644 --- a/contracts/src/proxy/mock/UUPSUpgradeableMocks.sol +++ b/contracts/src/proxy/mock/UUPSUpgradeableMocks.sol @@ -16,10 +16,6 @@ contract NonUpgradeableMock { function increment() external { _counter++; } - - function version() external pure virtual returns (string memory) { - return "NonUpgradeableMock 0.0.0"; - } } contract UUPSUpgradeableMock is UUPSProxiable, NonUpgradeableMock { diff --git a/contracts/src/proxy/mock/by-inheritance/UpgradedByInheritance.sol b/contracts/src/proxy/mock/by-inheritance/UpgradedByInheritance.sol index 4634138e7..dab7da8d1 100644 --- a/contracts/src/proxy/mock/by-inheritance/UpgradedByInheritance.sol +++ b/contracts/src/proxy/mock/by-inheritance/UpgradedByInheritance.sol @@ -33,7 +33,7 @@ contract UpgradedByInheritanceV1 is UUPSProxiable, Initializable { ++counter; } - function version() external pure virtual returns (string memory) { + function version() external pure virtual override returns (string memory) { return "V1"; } } diff --git a/contracts/src/proxy/mock/by-rewrite/UpgradedByRewrite.sol b/contracts/src/proxy/mock/by-rewrite/UpgradedByRewrite.sol index abf2e53e6..a508bd96c 100644 --- a/contracts/src/proxy/mock/by-rewrite/UpgradedByRewrite.sol +++ b/contracts/src/proxy/mock/by-rewrite/UpgradedByRewrite.sol @@ -36,7 +36,7 @@ contract UpgradedByRewrite is UUPSProxiable, Initializable { ++counter; } - function version() external pure virtual returns (string memory) { + function version() external pure virtual override returns (string memory) { return "V1"; } } diff --git a/contracts/src/proxy/mock/by-rewrite/UpgradedByRewriteV2.sol b/contracts/src/proxy/mock/by-rewrite/UpgradedByRewriteV2.sol index 334a29fdf..09cecd47d 100644 --- a/contracts/src/proxy/mock/by-rewrite/UpgradedByRewriteV2.sol +++ b/contracts/src/proxy/mock/by-rewrite/UpgradedByRewriteV2.sol @@ -36,7 +36,7 @@ contract UpgradedByRewrite is UUPSProxiable, Initializable { ++counter; } - function version() external pure virtual returns (string memory) { + function version() external pure virtual override returns (string memory) { return "V2"; } } diff --git a/yarn.lock b/yarn.lock index bb0a93647..0c89f0d06 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5448,7 +5448,8 @@ __metadata: "@nomicfoundation/hardhat-chai-matchers": "npm:^2.0.8" "@nomicfoundation/hardhat-ethers": "npm:^3.0.8" "@nomiclabs/hardhat-solhint": "npm:^4.0.1" - "@openzeppelin/contracts": "npm:^5.1.0" + "@openzeppelin/contracts": "npm:^5.2.0" + "@openzeppelin/upgrades-core": "npm:^1.41.0" "@typechain/ethers-v6": "npm:^0.5.1" "@typechain/hardhat": "npm:^9.1.0" "@types/chai": "npm:^4.3.20" @@ -5460,23 +5461,23 @@ __metadata: dotenv: "npm:^16.4.5" eslint: "npm:^9.15.0" ethereumjs-util: "npm:^7.1.5" - ethers: "npm:^6.13.4" + ethers: "npm:^6.13.5" graphql: "npm:^16.9.0" graphql-request: "npm:^7.1.2" - hardhat: "npm:2.22.16" + hardhat: "npm:2.22.18" hardhat-contract-sizer: "npm:^2.10.0" hardhat-deploy: "npm:^0.14.0" hardhat-deploy-ethers: "npm:^0.4.2" - hardhat-deploy-tenderly: "npm:^0.2.0" + hardhat-deploy-tenderly: "npm:^0.2.1" hardhat-docgen: "npm:^1.3.0" - hardhat-gas-reporter: "npm:^2.2.1" + hardhat-gas-reporter: "npm:^2.2.2" hardhat-tracer: "npm:^3.1.0" hardhat-watcher: "npm:^2.5.0" node-fetch: "npm:^3.3.2" pino: "npm:^8.21.0" pino-pretty: "npm:^10.3.1" prettier: "npm:^3.3.3" - prettier-plugin-solidity: "npm:^1.4.1" + prettier-plugin-solidity: "npm:^1.4.2" shelljs: "npm:^0.8.5" solhint-plugin-prettier: "npm:^0.1.0" solidity-coverage: "npm:^0.8.13" @@ -7052,67 +7053,67 @@ __metadata: languageName: node linkType: hard -"@nomicfoundation/edr-darwin-arm64@npm:0.6.4": - version: 0.6.4 - resolution: "@nomicfoundation/edr-darwin-arm64@npm:0.6.4" - checksum: 10/9dd7ed96986f12973c715ba12031a5d91dff131baf1fffe58d7e2d1a990ea356b6192e4b74b46105f4831035262fb86f214a59b776aaa961c3a90a72d476794f +"@nomicfoundation/edr-darwin-arm64@npm:0.7.0": + version: 0.7.0 + resolution: "@nomicfoundation/edr-darwin-arm64@npm:0.7.0" + checksum: 10/be9ff1c9ece6875486c3eabd9cdddd90bbdbed9cecc376efc9aec0c7ce54fcb83b33adf4bab26fa134867a538cc26137c027c2aa2b7adf242ef0ef07fe240c56 languageName: node linkType: hard -"@nomicfoundation/edr-darwin-x64@npm:0.6.4": - version: 0.6.4 - resolution: "@nomicfoundation/edr-darwin-x64@npm:0.6.4" - checksum: 10/6b283ea5382f7e0e78a89f6f4935f3eeb68744f7453ba5faddbb9e15d687d8cc096fe78b4ba0386671311200680e9ae7745436259d52a2bddb274394b9c30ca5 +"@nomicfoundation/edr-darwin-x64@npm:0.7.0": + version: 0.7.0 + resolution: "@nomicfoundation/edr-darwin-x64@npm:0.7.0" + checksum: 10/5e89e2c51f470e0a88f089098c8771b55466c082b7d84148b114541f81ff182bab3383623170bf329a78ea6274571993fea20ebfe080f898e775f3457eda358f languageName: node linkType: hard -"@nomicfoundation/edr-linux-arm64-gnu@npm:0.6.4": - version: 0.6.4 - resolution: "@nomicfoundation/edr-linux-arm64-gnu@npm:0.6.4" - checksum: 10/8393b71350366f8029f19e61ebd7ec0f7588af50defab03ccb48d4f4c9b1a59d12aa2b73766cfbef9ae2bd5122798824a33b6cbbcf5fb34126e8532d9db1461b +"@nomicfoundation/edr-linux-arm64-gnu@npm:0.7.0": + version: 0.7.0 + resolution: "@nomicfoundation/edr-linux-arm64-gnu@npm:0.7.0" + checksum: 10/7d77d116bc1b668ec83437795ae17150e70edfad700bd7335f7e7d072731649024c28cc7aca5799480cfef42b7ae52b3e1522051a04ce4f8924c716714176277 languageName: node linkType: hard -"@nomicfoundation/edr-linux-arm64-musl@npm:0.6.4": - version: 0.6.4 - resolution: "@nomicfoundation/edr-linux-arm64-musl@npm:0.6.4" - checksum: 10/ee9d14a0a395499e90019fdd162a8455c03035789ec7bcb3fe39d8967d3ec7edee84d5b4f30aa9d395fa4fc4faac39e8a00159ca8f8d73da794b17edec451134 +"@nomicfoundation/edr-linux-arm64-musl@npm:0.7.0": + version: 0.7.0 + resolution: "@nomicfoundation/edr-linux-arm64-musl@npm:0.7.0" + checksum: 10/de5c8a2a713eb9a6792a79c8b8ebb6f8de38018ab5bfc6bb35cd435a89c62e77bab550e334eadda9493b4134481d39f11208e3b480b86a0b4b703c0b3d05561a languageName: node linkType: hard -"@nomicfoundation/edr-linux-x64-gnu@npm:0.6.4": - version: 0.6.4 - resolution: "@nomicfoundation/edr-linux-x64-gnu@npm:0.6.4" - checksum: 10/161678a3dc357059886e3302b5db661d85f448e5a426217b9a99179e69a8a3a5a081819344e6854ab9b82799102247331e959540f19d9cf7872155070f21e82c +"@nomicfoundation/edr-linux-x64-gnu@npm:0.7.0": + version: 0.7.0 + resolution: "@nomicfoundation/edr-linux-x64-gnu@npm:0.7.0" + checksum: 10/33077f290bbb1e8ce178d50289bb6591c72a18b35b5f31e3e4051a9af6ec10312b21d47ed2d4938a6f64ee5b3e2413c3390fa0f4f5da5fb73dda7eb1c86bc742 languageName: node linkType: hard -"@nomicfoundation/edr-linux-x64-musl@npm:0.6.4": - version: 0.6.4 - resolution: "@nomicfoundation/edr-linux-x64-musl@npm:0.6.4" - checksum: 10/22811238e96028a85af1e6e67cf242b9b24c9b71ee8883d87cf1624a936c5e812b7859e7ccfd6aa4b3f0df7d55e13f215774a2d913c3f546618a0d6871640e5b +"@nomicfoundation/edr-linux-x64-musl@npm:0.7.0": + version: 0.7.0 + resolution: "@nomicfoundation/edr-linux-x64-musl@npm:0.7.0" + checksum: 10/48784d44e3dd8a7a0d52a0f3f7511f02e5d1d94678a1baa29bf5a1c0973a707c96e09835622b5483ed3698622abd34bf4338ed9f688f01cb8ce55edddf78cdb4 languageName: node linkType: hard -"@nomicfoundation/edr-win32-x64-msvc@npm:0.6.4": - version: 0.6.4 - resolution: "@nomicfoundation/edr-win32-x64-msvc@npm:0.6.4" - checksum: 10/509a8dd7c82a401f8c3e63a8621d602fd4398f3f0701109335756fb81795a575df7d6386126dcffee6659f50b0afc68b36702ab0fa7febb83bded82260629bd0 +"@nomicfoundation/edr-win32-x64-msvc@npm:0.7.0": + version: 0.7.0 + resolution: "@nomicfoundation/edr-win32-x64-msvc@npm:0.7.0" + checksum: 10/517897959478810d168f95274762f1565185026a8a908d289120f97344be33865104c2a07eb7277d5ea992f5db55790f63efe460fea61d1c2ed7879567828f15 languageName: node linkType: hard -"@nomicfoundation/edr@npm:^0.6.4": - version: 0.6.4 - resolution: "@nomicfoundation/edr@npm:0.6.4" +"@nomicfoundation/edr@npm:^0.7.0": + version: 0.7.0 + resolution: "@nomicfoundation/edr@npm:0.7.0" dependencies: - "@nomicfoundation/edr-darwin-arm64": "npm:0.6.4" - "@nomicfoundation/edr-darwin-x64": "npm:0.6.4" - "@nomicfoundation/edr-linux-arm64-gnu": "npm:0.6.4" - "@nomicfoundation/edr-linux-arm64-musl": "npm:0.6.4" - "@nomicfoundation/edr-linux-x64-gnu": "npm:0.6.4" - "@nomicfoundation/edr-linux-x64-musl": "npm:0.6.4" - "@nomicfoundation/edr-win32-x64-msvc": "npm:0.6.4" - checksum: 10/9bdcd316bcc8c5f0e137b4234e66455f427bc3046dc8f34b76aeed1e96eeedbe6a2c548970077ec4bd182d86ba630dfdcac6bce4b8d9a465b8f303c6a3aa6971 + "@nomicfoundation/edr-darwin-arm64": "npm:0.7.0" + "@nomicfoundation/edr-darwin-x64": "npm:0.7.0" + "@nomicfoundation/edr-linux-arm64-gnu": "npm:0.7.0" + "@nomicfoundation/edr-linux-arm64-musl": "npm:0.7.0" + "@nomicfoundation/edr-linux-x64-gnu": "npm:0.7.0" + "@nomicfoundation/edr-linux-x64-musl": "npm:0.7.0" + "@nomicfoundation/edr-win32-x64-msvc": "npm:0.7.0" + checksum: 10/b5c9546152574642b6d401b8da7f2a01cb98edd0da25aa2e7c16434e44b4134c699b50e8415449543701e3b722f5d6c8d8e4d8f699df3f7ebdb4acb907f0a794 languageName: node linkType: hard @@ -7578,14 +7579,14 @@ __metadata: languageName: node linkType: hard -"@openzeppelin/contracts@npm:^5.1.0": - version: 5.1.0 - resolution: "@openzeppelin/contracts@npm:5.1.0" - checksum: 10/c3954d09b206af23a98d352177b49e4b2796a818ee01f082b223d54e6d394b3741a4ea6012570359d2a705d5e932a7b906ee38b64e3987ababfa73bcaa5a4805 +"@openzeppelin/contracts@npm:^5.2.0": + version: 5.2.0 + resolution: "@openzeppelin/contracts@npm:5.2.0" + checksum: 10/d77e1bdfca6fa1c40b5a32bb92220ec633ef86cc0bf43a011ad218c26c9e763c70ace3fc1e148a0462394a3aed8c5d7445935ebfeb146ba1454abec66625e420 languageName: node linkType: hard -"@openzeppelin/upgrades-core@npm:^1.24.1": +"@openzeppelin/upgrades-core@npm:^1.24.1, @openzeppelin/upgrades-core@npm:^1.41.0": version: 1.41.0 resolution: "@openzeppelin/upgrades-core@npm:1.41.0" dependencies: @@ -8953,6 +8954,13 @@ __metadata: languageName: node linkType: hard +"@solidity-parser/parser@npm:^0.19.0": + version: 0.19.0 + resolution: "@solidity-parser/parser@npm:0.19.0" + checksum: 10/2136708ecc988b534efcf836e95f4f02a1452ab0c026438014c35ce31b26dc011cc8c512d502fc7bcb968f850ab7e524838292bc36cad6a144fedb4c29685587 + languageName: node + linkType: hard + "@sphinxxxx/color-conversion@npm:^2.2.2": version: 2.2.2 resolution: "@sphinxxxx/color-conversion@npm:2.2.2" @@ -18700,7 +18708,22 @@ __metadata: languageName: node linkType: hard -"ethers@npm:^6.13.4, ethers@npm:^6.9.0": +"ethers@npm:^6.13.5": + version: 6.13.5 + resolution: "ethers@npm:6.13.5" + dependencies: + "@adraffy/ens-normalize": "npm:1.10.1" + "@noble/curves": "npm:1.2.0" + "@noble/hashes": "npm:1.3.2" + "@types/node": "npm:22.7.5" + aes-js: "npm:4.0.0-beta.5" + tslib: "npm:2.7.0" + ws: "npm:8.17.1" + checksum: 10/ccba21a83679fb6a7c3eb9d187593501565d140064f2db28057a64d6707678bacf2adf4555882c4814688246da73173560df81fd3361fd2f227b5d3c75cb8685 + languageName: node + linkType: hard + +"ethers@npm:^6.9.0": version: 6.13.4 resolution: "ethers@npm:6.13.4" dependencies: @@ -20613,16 +20636,16 @@ __metadata: languageName: node linkType: hard -"hardhat-deploy-tenderly@npm:^0.2.0": - version: 0.2.0 - resolution: "hardhat-deploy-tenderly@npm:0.2.0" +"hardhat-deploy-tenderly@npm:^0.2.1": + version: 0.2.1 + resolution: "hardhat-deploy-tenderly@npm:0.2.1" dependencies: axios: "npm:^0.24.0" js-yaml: "npm:^4.1.0" peerDependencies: hardhat: ^2.0.0 - hardhat-deploy: ^0.11.10 - checksum: 10/3d7d49f3787bcc793a36c4514acdd0d19d8e1383e59f8bebc835bb1a555e8c5cbdb83aec3287312ddd9b796bfb0e1b96b41245722a4de38d85060c78bc9055d3 + hardhat-deploy: 0.x + checksum: 10/0c62f8b84e65541e98446b4d4098d1cfb0aa05dbf2eda3fb0825cc86be04939ab9c63aef3a62ed63b7d26f301f65a1bfa0b127e4a2a00a6136d87d1a3672d73f languageName: node linkType: hard @@ -20675,14 +20698,14 @@ __metadata: languageName: node linkType: hard -"hardhat-gas-reporter@npm:^2.2.1": - version: 2.2.1 - resolution: "hardhat-gas-reporter@npm:2.2.1" +"hardhat-gas-reporter@npm:^2.2.2": + version: 2.2.2 + resolution: "hardhat-gas-reporter@npm:2.2.2" dependencies: "@ethersproject/abi": "npm:^5.7.0" "@ethersproject/bytes": "npm:^5.7.0" "@ethersproject/units": "npm:^5.7.0" - "@solidity-parser/parser": "npm:^0.18.0" + "@solidity-parser/parser": "npm:^0.19.0" axios: "npm:^1.6.7" brotli-wasm: "npm:^2.0.1" chalk: "npm:4.1.2" @@ -20696,7 +20719,7 @@ __metadata: viem: "npm:2.7.14" peerDependencies: hardhat: ^2.16.0 - checksum: 10/af8bd86400035ab1ed574c5f782e7ee408fff75ea6cfcd56ab87bf80c2e911e9fbe943b528ad17844abad8ff8f63daa24aae391ccbf3e07a9970be66f949bd3d + checksum: 10/32a13011b414bae887741907bb2ea23cda0e645fb4fdfce87bed5a37be95f26a723ccc4140b413ec215832d6cc0fa5ff517f6d47af1bbedee5508bb5432269d7 languageName: node linkType: hard @@ -20726,13 +20749,13 @@ __metadata: languageName: node linkType: hard -"hardhat@npm:2.22.16": - version: 2.22.16 - resolution: "hardhat@npm:2.22.16" +"hardhat@npm:2.22.18": + version: 2.22.18 + resolution: "hardhat@npm:2.22.18" dependencies: "@ethersproject/abi": "npm:^5.1.2" "@metamask/eth-sig-util": "npm:^4.0.0" - "@nomicfoundation/edr": "npm:^0.6.4" + "@nomicfoundation/edr": "npm:^0.7.0" "@nomicfoundation/ethereumjs-common": "npm:4.0.4" "@nomicfoundation/ethereumjs-tx": "npm:5.0.4" "@nomicfoundation/ethereumjs-util": "npm:9.0.4" @@ -20784,7 +20807,7 @@ __metadata: optional: true bin: hardhat: internal/cli/bootstrap.js - checksum: 10/77e3f0447e42265107242827f8efd8346224c79001a0eea2d0d71bcaeee1ab9ed1e1fff47c28e36d19e1b3c386707dc60dd3d5e22b09da15f8b2ea5feb5ff943 + checksum: 10/521d46b31d15cda4c38b4e1995ba47bd34386bd10aa0554be4e2e7c9c889d25ce57c4018c76eda94168da5bab697ab5ad98b2715a1033247a0414a8853787bdd languageName: node linkType: hard @@ -29391,15 +29414,15 @@ __metadata: languageName: node linkType: hard -"prettier-plugin-solidity@npm:^1.4.1": - version: 1.4.1 - resolution: "prettier-plugin-solidity@npm:1.4.1" +"prettier-plugin-solidity@npm:^1.4.1, prettier-plugin-solidity@npm:^1.4.2": + version: 1.4.2 + resolution: "prettier-plugin-solidity@npm:1.4.2" dependencies: - "@solidity-parser/parser": "npm:^0.18.0" - semver: "npm:^7.5.4" + "@solidity-parser/parser": "npm:^0.19.0" + semver: "npm:^7.6.3" peerDependencies: prettier: ">=2.3.0" - checksum: 10/8bd0b20cd1a0973bfdbb4e035d5dae9ff229a7f6475c4e31e99b85d74f77625c0a874835df23e51d36750139aa2e5c5d900a366d1cdff16edfe52b3550b7e2da + checksum: 10/5f4ca400275c860bbca7ba3ee316682bec04a760a816e903f6a528acc61ef3d2eda9a81edb7e38449b79196d2946db44424fbab78944d2e8bb32f8fed31363bc languageName: node linkType: hard