From a255c1dfad2bd26b453dc25c966761e19bb1c977 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Wed, 4 Jun 2025 09:53:10 +0100 Subject: [PATCH 01/24] refactor: new staking and sortition architecture (wip) --- .../00-home-chain-arbitration-v2-neo.ts | 210 +++ .../deploy/00-home-chain-arbitration-v2.ts | 178 +++ .../src/arbitration/core-v2/KlerosCoreV2.sol | 68 + .../arbitration/core-v2/KlerosCoreV2Base.sol | 1203 +++++++++++++++++ .../arbitration/core-v2/KlerosCoreV2Neo.sol | 139 ++ .../src/arbitration/interfaces/IPNKVault.sol | 84 ++ .../interfaces/ISortitionModuleV2.sol | 115 ++ .../interfaces/IStakeController.sol | 165 +++ .../sortition-v2/SortitionModuleV2.sol | 41 + .../sortition-v2/SortitionModuleV2Base.sol | 449 ++++++ .../sortition-v2/SortitionModuleV2Neo.sol | 119 ++ contracts/src/arbitration/stPNK.sol | 151 +++ .../stake-controller/StakeController.sol | 66 + .../stake-controller/StakeControllerBase.sol | 539 ++++++++ .../stake-controller/StakeControllerNeo.sol | 218 +++ contracts/src/arbitration/vault/PNKVault.sol | 45 + .../src/arbitration/vault/PNKVaultBase.sol | 250 ++++ .../src/arbitration/vault/PNKVaultNeo.sol | 205 +++ 18 files changed, 4245 insertions(+) create mode 100644 contracts/deploy/00-home-chain-arbitration-v2-neo.ts create mode 100644 contracts/deploy/00-home-chain-arbitration-v2.ts create mode 100644 contracts/src/arbitration/core-v2/KlerosCoreV2.sol create mode 100644 contracts/src/arbitration/core-v2/KlerosCoreV2Base.sol create mode 100644 contracts/src/arbitration/core-v2/KlerosCoreV2Neo.sol create mode 100644 contracts/src/arbitration/interfaces/IPNKVault.sol create mode 100644 contracts/src/arbitration/interfaces/ISortitionModuleV2.sol create mode 100644 contracts/src/arbitration/interfaces/IStakeController.sol create mode 100644 contracts/src/arbitration/sortition-v2/SortitionModuleV2.sol create mode 100644 contracts/src/arbitration/sortition-v2/SortitionModuleV2Base.sol create mode 100644 contracts/src/arbitration/sortition-v2/SortitionModuleV2Neo.sol create mode 100644 contracts/src/arbitration/stPNK.sol create mode 100644 contracts/src/arbitration/stake-controller/StakeController.sol create mode 100644 contracts/src/arbitration/stake-controller/StakeControllerBase.sol create mode 100644 contracts/src/arbitration/stake-controller/StakeControllerNeo.sol create mode 100644 contracts/src/arbitration/vault/PNKVault.sol create mode 100644 contracts/src/arbitration/vault/PNKVaultBase.sol create mode 100644 contracts/src/arbitration/vault/PNKVaultNeo.sol diff --git a/contracts/deploy/00-home-chain-arbitration-v2-neo.ts b/contracts/deploy/00-home-chain-arbitration-v2-neo.ts new file mode 100644 index 000000000..322dcdcff --- /dev/null +++ b/contracts/deploy/00-home-chain-arbitration-v2-neo.ts @@ -0,0 +1,210 @@ +import { HardhatRuntimeEnvironment } from "hardhat/types"; +import { DeployFunction } from "hardhat-deploy/types"; +import { getContractAddress } from "./utils/getContractAddress"; +import { deployUpgradable } from "./utils/deployUpgradable"; +import { HomeChains, isSkipped, isDevnet, PNK, ETH } from "./utils"; +import { getContractOrDeploy, getContractOrDeployUpgradable } from "./utils/getContractOrDeploy"; +import { deployERC20AndFaucet, deployERC721 } from "./utils/deployTokens"; +import { ChainlinkRNG, DisputeKitClassic, KlerosCoreV2Neo, StakeControllerNeo, PNKVaultNeo } from "../typechain-types"; + +const deployArbitrationV2Neo: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { + const { ethers, deployments, getNamedAccounts, getChainId } = hre; + const { deploy } = deployments; + const { ZeroAddress } = hre.ethers; + const RNG_LOOKAHEAD = 20; + + // fallback to hardhat node signers on local network + const deployer = (await getNamedAccounts()).deployer ?? (await hre.ethers.getSigners())[0].address; + const chainId = Number(await getChainId()); + console.log("deploying to %s with deployer %s", HomeChains[chainId], deployer); + + const pnk = await deployERC20AndFaucet(hre, deployer, "PNK"); + const weth = await deployERC20AndFaucet(hre, deployer, "WETH"); + const nft = await deployERC721(hre, deployer, "Kleros V2 Neo Early User", "KlerosV2NeoEarlyUser"); + + await getContractOrDeploy(hre, "TransactionBatcher", { from: deployer, args: [], log: true }); + + await deployUpgradable(deployments, "PolicyRegistry", { from: deployer, args: [deployer], log: true }); + + await deployUpgradable(deployments, "EvidenceModule", { from: deployer, args: [deployer], log: true }); + + const disputeKit = await deployUpgradable(deployments, "DisputeKitClassicV2Neo", { + from: deployer, + contract: "DisputeKitClassic", + args: [deployer, ZeroAddress], + log: true, + }); + + // Deploy stPNK token + const stPNK = await deploy("stPNK", { + from: deployer, + contract: "stPNK", + args: [], + log: true, + }); + + // Deploy PNKVaultNeo + const pnkVaultNeo = await deployUpgradable(deployments, "PNKVaultNeo", { + from: deployer, + args: [deployer, pnk.target, stPNK.address], + log: true, + }); + + // Calculate future addresses for circular dependencies + let stakeControllerNeoAddress = await deployments + .getOrNull("StakeControllerNeo") + .then((deployment) => deployment?.address); + let klerosCoreV2NeoAddress = await deployments.getOrNull("KlerosCoreV2Neo").then((deployment) => deployment?.address); + + const nonce = await ethers.provider.getTransactionCount(deployer); + let currentNonce = nonce + 2; // After SortitionModuleV2Neo impl+proxy + + if (!stakeControllerNeoAddress) { + stakeControllerNeoAddress = getContractAddress(deployer, currentNonce + 1); // proxy address + console.log( + "calculated future StakeControllerNeo address for nonce %d: %s", + currentNonce + 1, + stakeControllerNeoAddress + ); + currentNonce += 2; // impl + proxy + } else { + console.log("using existing StakeControllerNeo address: %s", stakeControllerNeoAddress); + } + + if (!klerosCoreV2NeoAddress) { + klerosCoreV2NeoAddress = getContractAddress(deployer, currentNonce + 1); // proxy address + console.log("calculated future KlerosCoreV2Neo address for nonce %d: %s", currentNonce + 1, klerosCoreV2NeoAddress); + } else { + console.log("using existing KlerosCoreV2Neo address: %s", klerosCoreV2NeoAddress); + } + + const devnet = isDevnet(hre.network); + const minStakingTime = devnet ? 180 : 1800; + const maxDrawingTime = devnet ? 600 : 1800; + const rng = (await ethers.getContract("ChainlinkRNG")) as ChainlinkRNG; + const maxStakePerJuror = PNK(2_000); + const maxTotalStaked = PNK(2_000_000); + + // Deploy SortitionModuleV2Neo + const sortitionModuleV2Neo = await deployUpgradable(deployments, "SortitionModuleV2Neo", { + from: deployer, + args: [deployer, stakeControllerNeoAddress, maxStakePerJuror, maxTotalStaked], + log: true, + }); // nonce (implementation), nonce+1 (proxy) + + // Deploy StakeControllerNeo (only if not already deployed) + const stakeControllerNeo = await getContractOrDeployUpgradable(hre, "StakeControllerNeo", { + from: deployer, + args: [ + deployer, + klerosCoreV2NeoAddress, + pnkVaultNeo.address, + sortitionModuleV2Neo.address, + rng.target, + minStakingTime, + maxDrawingTime, + RNG_LOOKAHEAD, + ], + log: true, + }); + + const minStake = PNK(200); + const alpha = 10000; + const feeForJuror = ETH(0.1); + const jurorsForCourtJump = 256; + + // Deploy KlerosCoreV2Neo (only if not already deployed) + const klerosCoreV2Neo = await getContractOrDeployUpgradable(hre, "KlerosCoreV2Neo", { + from: deployer, + args: [ + deployer, + deployer, + pnk.target, + ZeroAddress, // jurorProsecutionModule is not implemented yet + disputeKit.address, + false, + [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 + stakeControllerNeo.target, + nft.target, + ], + log: true, + }); + + // Configure cross-dependencies + console.log("Configuring cross-dependencies..."); + + // Configure stPNK token to allow PNKVaultNeo operations + const stPNKContract = await ethers.getContractAt("stPNK", stPNK.address); + const currentVault = await stPNKContract.vault(); + if (currentVault !== pnkVaultNeo.address) { + console.log(`stPNK.setVault(${pnkVaultNeo.address})`); + await stPNKContract.setVault(pnkVaultNeo.address); + } + + // disputeKit.changeCore() only if necessary + const disputeKitContract = (await ethers.getContract("DisputeKitClassicV2Neo")) as DisputeKitClassic; + const currentCore = await disputeKitContract.core(); + if (currentCore !== klerosCoreV2Neo.target) { + console.log(`disputeKit.changeCore(${klerosCoreV2Neo.target})`); + await disputeKitContract.changeCore(klerosCoreV2Neo.target); + } + + // rng.changeSortitionModule() only if necessary + const rngSortitionModule = await rng.sortitionModule(); + if (rngSortitionModule !== stakeControllerNeo.target) { + console.log(`rng.changeSortitionModule(${stakeControllerNeo.target})`); + await rng.changeSortitionModule(stakeControllerNeo.target); + } + + const core = (await hre.ethers.getContract("KlerosCoreV2Neo")) as KlerosCoreV2Neo; + try { + // Manually set currency rates + console.log("Setting WETH currency rate..."); + await core.changeAcceptedFeeTokens(await weth.getAddress(), true); + await core.changeCurrencyRates(await weth.getAddress(), 1, 1); + } catch (e) { + console.error("failed to change currency rates:", e); + } + + const disputeTemplateRegistry = await getContractOrDeployUpgradable(hre, "DisputeTemplateRegistry", { + from: deployer, + args: [deployer], + log: true, + }); + + const resolver = await deploy("DisputeResolverV2Neo", { + from: deployer, + contract: "DisputeResolver", + args: [core.target, disputeTemplateRegistry.target], + log: true, + }); + + console.log(`core.changeArbitrableWhitelist(${resolver.address}, true)`); + await core.changeArbitrableWhitelist(resolver.address, true); + + await deploy("KlerosCoreV2NeoSnapshotProxy", { + from: deployer, + contract: "KlerosCoreSnapshotProxy", + args: [deployer, core.target], + log: true, + }); + + console.log("✅ V2 Neo Architecture deployment completed successfully!"); + console.log(`📦 PNKVaultNeo: ${pnkVaultNeo.address}`); + console.log(`🎫 stPNKNeo: ${stPNK.address}`); + console.log(`🎯 SortitionModuleV2Neo: ${sortitionModuleV2Neo.address}`); + console.log(`🎮 StakeControllerNeo: ${stakeControllerNeo.target}`); + console.log(`⚖️ KlerosCoreV2Neo: ${klerosCoreV2Neo.target}`); + console.log(`🎨 JurorNFT: ${nft.target}`); + console.log(`🔐 DisputeResolver: ${resolver.address}`); +}; + +deployArbitrationV2Neo.tags = ["ArbitrationV2Neo"]; +deployArbitrationV2Neo.dependencies = ["ChainlinkRNG"]; +deployArbitrationV2Neo.skip = async ({ network }) => { + return isSkipped(network, !HomeChains[network.config.chainId ?? 0]); +}; + +export default deployArbitrationV2Neo; diff --git a/contracts/deploy/00-home-chain-arbitration-v2.ts b/contracts/deploy/00-home-chain-arbitration-v2.ts new file mode 100644 index 000000000..f8b3b817f --- /dev/null +++ b/contracts/deploy/00-home-chain-arbitration-v2.ts @@ -0,0 +1,178 @@ +import { HardhatRuntimeEnvironment } from "hardhat/types"; +import { DeployFunction } from "hardhat-deploy/types"; +import { getContractAddress } from "./utils/getContractAddress"; +import { deployUpgradable } from "./utils/deployUpgradable"; +import { HomeChains, isSkipped, isDevnet, PNK, ETH } from "./utils"; +import { getContractOrDeploy, getContractOrDeployUpgradable } from "./utils/getContractOrDeploy"; +import { deployERC20AndFaucet } from "./utils/deployTokens"; +import { ChainlinkRNG, DisputeKitClassic, KlerosCoreV2, StakeController, PNKVault } from "../typechain-types"; + +const deployArbitrationV2: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { + const { ethers, deployments, getNamedAccounts, getChainId } = hre; + const { deploy } = deployments; + const { ZeroAddress } = hre.ethers; + const RNG_LOOKAHEAD = 20; + + // fallback to hardhat node signers on local network + const deployer = (await getNamedAccounts()).deployer ?? (await hre.ethers.getSigners())[0].address; + const chainId = Number(await getChainId()); + console.log("deploying to %s with deployer %s", HomeChains[chainId], deployer); + + const pnk = await deployERC20AndFaucet(hre, deployer, "PNK"); + const dai = await deployERC20AndFaucet(hre, deployer, "DAI"); + const weth = await deployERC20AndFaucet(hre, deployer, "WETH"); + + await getContractOrDeploy(hre, "TransactionBatcher", { from: deployer, args: [], log: true }); + + await getContractOrDeployUpgradable(hre, "PolicyRegistry", { from: deployer, args: [deployer], log: true }); + + await getContractOrDeployUpgradable(hre, "EvidenceModule", { from: deployer, args: [deployer], log: true }); + + const disputeKit = await deployUpgradable(deployments, "DisputeKitClassicV2", { + from: deployer, + contract: "DisputeKitClassic", + args: [deployer, ZeroAddress], + log: true, + }); + + // Deploy stPNK token + const stPNK = await deploy("stPNK", { + from: deployer, + args: [], + log: true, + }); + + // Deploy PNKVault + const pnkVault = await deployUpgradable(deployments, "PNKVault", { + from: deployer, + args: [deployer, pnk.target, stPNK.address], + log: true, + }); + + // Calculate future addresses for circular dependencies + let klerosCoreV2Address = await deployments.getOrNull("KlerosCoreV2").then((deployment) => deployment?.address); + if (!klerosCoreV2Address) { + const nonce = await ethers.provider.getTransactionCount(deployer); + klerosCoreV2Address = getContractAddress(deployer, nonce + 5); // // deployed on the 6th tx (nonce+3): SortitionModule Impl tx, SortitionModule Proxy tx, StakeController Impl tx, StakeController Proxy tx, KlerosCore Impl tx, KlerosCore Proxy tx + console.log("calculated future KlerosCoreV2 address for nonce %d: %s", nonce + 5, klerosCoreV2Address); + } else { + console.log("using existing KlerosCoreV2 address: %s", klerosCoreV2Address); + } + + const devnet = isDevnet(hre.network); + const minStakingTime = devnet ? 180 : 1800; + const maxDrawingTime = devnet ? 600 : 1800; + const rng = (await ethers.getContract("ChainlinkRNG")) as ChainlinkRNG; + + // Deploy SortitionModuleV2 + const sortitionModuleV2 = await deployUpgradable(deployments, "SortitionModuleV2", { + from: deployer, + args: [deployer, stakeControllerAddress], + log: true, + }); // nonce (implementation), nonce+1 (proxy) + + // Deploy StakeController (only if not already deployed) + const stakeController = await getContractOrDeployUpgradable(hre, "StakeController", { + from: deployer, + args: [ + deployer, + klerosCoreV2Address, + pnkVault.address, + sortitionModuleV2.address, + rng.target, + minStakingTime, + maxDrawingTime, + RNG_LOOKAHEAD, + ], + log: true, + }); + + const minStake = PNK(200); + const alpha = 10000; + const feeForJuror = ETH(0.1); + const jurorsForCourtJump = 256; + + // Deploy KlerosCoreV2 (only if not already deployed) + const klerosCoreV2 = await getContractOrDeployUpgradable(hre, "KlerosCoreV2", { + from: deployer, + args: [ + deployer, + deployer, + pnk.target, + ZeroAddress, // KlerosCoreV2 is configured later + disputeKit.address, + false, + [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 + stakeController.target, + ], + log: true, + }); + + // Configure cross-dependencies + console.log("Configuring cross-dependencies..."); + + // Configure stPNK token to allow PNKVault operations + const stPNKContract = await ethers.getContractAt("stPNK", stPNK.address); + const currentVault = await stPNKContract.vault(); + if (currentVault !== pnkVault.address) { + console.log(`stPNK.changeVault(${pnkVault.address})`); + await stPNKContract.changeVault(pnkVault.address); + } + + // disputeKit.changeCore() only if necessary + const disputeKitContract = (await ethers.getContract("DisputeKitClassicV2")) as DisputeKitClassic; + const currentCore = await disputeKitContract.core(); + if (currentCore !== klerosCoreV2.target) { + console.log(`disputeKit.changeCore(${klerosCoreV2.target})`); + await disputeKitContract.changeCore(klerosCoreV2.target); + } + + // rng.changeSortitionModule() only if necessary + const rngSortitionModule = await rng.sortitionModule(); + if (rngSortitionModule !== stakeController.target) { + console.log(`rng.changeSortitionModule(${stakeController.target})`); + await rng.changeSortitionModule(stakeController.target); + } + + const core = (await hre.ethers.getContract("KlerosCoreV2")) as KlerosCoreV2; + try { + // Manually set currency rates since changeCurrencyRate helper doesn't support V2 types yet + console.log("Setting PNK currency rate..."); + await core.changeAcceptedFeeTokens(await pnk.getAddress(), true); + await core.changeCurrencyRates(await pnk.getAddress(), 12225583, 12); + + console.log("Setting DAI currency rate..."); + await core.changeAcceptedFeeTokens(await dai.getAddress(), true); + await core.changeCurrencyRates(await dai.getAddress(), 60327783, 11); + + console.log("Setting WETH currency rate..."); + await core.changeAcceptedFeeTokens(await weth.getAddress(), true); + await core.changeCurrencyRates(await weth.getAddress(), 1, 1); + } catch (e) { + console.error("failed to change currency rates:", e); + } + + await deploy("KlerosCoreV2SnapshotProxy", { + from: deployer, + contract: "KlerosCoreSnapshotProxy", + args: [deployer, core.target], + log: true, + }); + + console.log("✅ V2 Architecture deployment completed successfully!"); + console.log(`📦 PNKVault: ${pnkVault.address}`); + console.log(`🎫 stPNK: ${stPNK.address}`); + console.log(`🎯 SortitionModuleV2: ${sortitionModuleV2.address}`); + console.log(`🎮 StakeController: ${stakeController.target}`); + console.log(`⚖️ KlerosCoreV2: ${klerosCoreV2.target}`); +}; + +deployArbitrationV2.tags = ["ArbitrationV2"]; +deployArbitrationV2.dependencies = ["ChainlinkRNG"]; +deployArbitrationV2.skip = async ({ network }) => { + return isSkipped(network, !HomeChains[network.config.chainId ?? 0]); +}; + +export default deployArbitrationV2; diff --git a/contracts/src/arbitration/core-v2/KlerosCoreV2.sol b/contracts/src/arbitration/core-v2/KlerosCoreV2.sol new file mode 100644 index 000000000..95ff803c7 --- /dev/null +++ b/contracts/src/arbitration/core-v2/KlerosCoreV2.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import "./KlerosCoreV2Base.sol"; + +/// @title KlerosCoreV2 +/// @notice KlerosCore implementation with new StakeController architecture for testing environments +contract KlerosCoreV2 is KlerosCoreV2Base { + /// @notice Version of the implementation contract + string public constant override version = "0.0.1"; + + // ************************************* // + // * Constructor * // + // ************************************* // + + /// @notice Constructor, initializing the implementation to reduce attack surface. + constructor() { + _disableInitializers(); + } + + /// @notice Initialization function for UUPS proxy + /// @param _governor The governor of the contract. + /// @param _guardian The guardian able to pause asset withdrawals. + /// @param _pinakion The Pinakion token contract. + /// @param _jurorProsecutionModule The module for juror's prosecution. + /// @param _disputeKit The dispute kit responsible for the dispute logic. + /// @param _hiddenVotes Whether to use commit and reveal or not. + /// @param _courtParameters [0]: minStake, [1]: alpha, [2]: feeForJuror, [3]: jurorsForCourtJump + /// @param _timesPerPeriod The timesPerPeriod array for courts + /// @param _sortitionExtraData Extra data for sortition module setup + /// @param _stakeController The stake controller for coordination + function initialize( + address _governor, + address _guardian, + IERC20 _pinakion, + address _jurorProsecutionModule, + IDisputeKit _disputeKit, + bool _hiddenVotes, + uint256[4] memory _courtParameters, + uint256[4] memory _timesPerPeriod, + bytes memory _sortitionExtraData, + IStakeController _stakeController + ) external initializer { + __KlerosCoreV2Base_initialize( + _governor, + _guardian, + _pinakion, + _jurorProsecutionModule, + _disputeKit, + _hiddenVotes, + _courtParameters, + _timesPerPeriod, + _sortitionExtraData, + _stakeController + ); + } + + // ************************************* // + // * Governance * // + // ************************************* // + + /// @notice Access Control to perform implementation upgrades (UUPS Proxiable) + /// Only the governor can perform upgrades (`onlyByGovernor`) + function _authorizeUpgrade(address) internal view override onlyByGovernor { + // Empty block: access control implemented by `onlyByGovernor` modifier + } +} diff --git a/contracts/src/arbitration/core-v2/KlerosCoreV2Base.sol b/contracts/src/arbitration/core-v2/KlerosCoreV2Base.sol new file mode 100644 index 000000000..d66a6064c --- /dev/null +++ b/contracts/src/arbitration/core-v2/KlerosCoreV2Base.sol @@ -0,0 +1,1203 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {IArbitrableV2, IArbitratorV2} from "../interfaces/IArbitratorV2.sol"; +import {IDisputeKit} from "../interfaces/IDisputeKit.sol"; +import {IStakeController} from "../interfaces/IStakeController.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 KlerosCoreV2Base +/// Core arbitrator contract for Kleros v2 with new StakeController architecture. +/// Note that this contract trusts the PNK token, the dispute kit and the stake controller contracts. +abstract contract KlerosCoreV2Base is IArbitratorV2, Initializable, UUPSProxiable { + using SafeERC20 for IERC20; + + // ************************************* // + // * Enums / Structs * // + // ************************************* // + + enum Period { + evidence, // Evidence can be submitted. This is also when drawing has to take place. + commit, // Jurors commit a hashed vote. This is skipped for courts without hidden votes. + vote, // Jurors reveal/cast their vote depending on whether the court has hidden votes or not. + appeal, // The dispute can be appealed. + execution // Tokens are redistributed and the ruling is executed. + } + + struct Court { + uint96 parent; // The parent court. + bool hiddenVotes; // Whether to use commit and reveal or not. + uint256[] children; // List of child courts. + uint256 minStake; // Minimum PNKs needed to stake in the court. + uint256 alpha; // Basis point of PNKs that are lost when incoherent. + uint256 feeForJuror; // Arbitration fee paid per juror. + uint256 jurorsForCourtJump; // The appeal after the one that reaches this number of jurors will go to the parent court if any. + uint256[4] timesPerPeriod; // The time allotted to each dispute period in the form `timesPerPeriod[period]`. + mapping(uint256 disputeKitId => bool) supportedDisputeKits; // True if DK with this ID is supported by the court. Note that each court must support classic dispute kit. + bool disabled; // True if the court is disabled. Unused for now, will be implemented later. + } + + struct Dispute { + uint96 courtID; // The ID of the court the dispute is in. + IArbitrableV2 arbitrated; // The arbitrable contract. + Period period; // The current period of the dispute. + bool ruled; // True if the ruling has been executed, false otherwise. + uint256 lastPeriodChange; // The last time the period was changed. + Round[] rounds; + } + + struct Round { + uint256 disputeKitID; // Index of the dispute kit in the array. + uint256 pnkAtStakePerJuror; // The amount of PNKs at stake for each juror in this round. + uint256 totalFeesForJurors; // The total juror fees paid in this round. + uint256 nbVotes; // The total number of votes the dispute can possibly have in the current round. Former votes[_round].length. + uint256 repartitions; // A counter of reward repartitions made in this round. + uint256 pnkPenalties; // The amount of PNKs collected from penalties in this round. + address[] drawnJurors; // Addresses of the jurors that were drawn in this round. + uint256 sumFeeRewardPaid; // Total sum of arbitration fees paid to coherent jurors as a reward in this round. + uint256 sumPnkRewardPaid; // Total sum of PNK paid to coherent jurors as a reward in this round. + IERC20 feeToken; // The token used for paying fees in this round. + uint256 drawIterations; // The number of iterations passed drawing the jurors for this round. + } + + // Workaround "stack too deep" errors + struct ExecuteParams { + uint256 disputeID; // The ID of the dispute to execute. + uint256 round; // The round to execute. + uint256 coherentCount; // The number of coherent votes in the round. + uint256 numberOfVotesInRound; // The number of votes in the round. + uint256 feePerJurorInRound; // The fee per juror in the round. + uint256 pnkAtStakePerJurorInRound; // The amount of PNKs at stake for each juror in the round. + uint256 pnkPenaltiesInRound; // The amount of PNKs collected from penalties in the round. + uint256 repartition; // The index of the repartition to execute. + } + + struct CurrencyRate { + bool feePaymentAccepted; + uint64 rateInEth; + uint8 rateDecimals; + } + + // ************************************* // + // * Storage * // + // ************************************* // + + uint256 private constant ALPHA_DIVISOR = 1e4; // The number to divide `Court.alpha` by. + uint256 private constant NON_PAYABLE_AMOUNT = (2 ** 256 - 2) / 2; // An amount higher than the supply of ETH. + + address public governor; // The governor of the contract. + address public guardian; // The guardian able to pause asset withdrawals. + IERC20 public pinakion; // The Pinakion token contract. + address public jurorProsecutionModule; // The module for juror's prosecution. + IStakeController public stakeController; // Stake controller for coordination. + Court[] public courts; // The courts. + IDisputeKit[] public disputeKits; // Array of dispute kits. + Dispute[] public disputes; // The disputes. + mapping(IERC20 => CurrencyRate) public currencyRates; // The price of each token in ETH. + bool public paused; // Whether asset withdrawals are paused. + + // ************************************* // + // * Events * // + // ************************************* // + + event NewPeriod(uint256 indexed _disputeID, Period _period); + event AppealPossible(uint256 indexed _disputeID, IArbitrableV2 indexed _arbitrable); + event AppealDecision(uint256 indexed _disputeID, IArbitrableV2 indexed _arbitrable); + event Draw(address indexed _address, uint256 indexed _disputeID, uint256 _roundID, uint256 _voteID); + event CourtCreated( + uint96 indexed _courtID, + uint96 indexed _parent, + bool _hiddenVotes, + uint256 _minStake, + uint256 _alpha, + uint256 _feeForJuror, + uint256 _jurorsForCourtJump, + uint256[4] _timesPerPeriod, + uint256[] _supportedDisputeKits + ); + event CourtModified( + uint96 indexed _courtID, + bool _hiddenVotes, + uint256 _minStake, + uint256 _alpha, + uint256 _feeForJuror, + uint256 _jurorsForCourtJump, + uint256[4] _timesPerPeriod + ); + event DisputeKitCreated(uint256 indexed _disputeKitID, IDisputeKit indexed _disputeKitAddress); + event DisputeKitEnabled(uint96 indexed _courtID, uint256 indexed _disputeKitID, bool indexed _enable); + event CourtJump( + uint256 indexed _disputeID, + uint256 indexed _roundID, + uint96 indexed _fromCourtID, + uint96 _toCourtID + ); + event DisputeKitJump( + uint256 indexed _disputeID, + uint256 indexed _roundID, + uint256 indexed _fromDisputeKitID, + uint256 _toDisputeKitID + ); + event TokenAndETHShift( + address indexed _account, + uint256 indexed _disputeID, + uint256 indexed _roundID, + uint256 _degreeOfCoherency, + int256 _pnkAmount, + int256 _feeAmount, + IERC20 _feeToken + ); + event LeftoverRewardSent( + uint256 indexed _disputeID, + uint256 indexed _roundID, + uint256 _pnkAmount, + uint256 _feeAmount, + IERC20 _feeToken + ); + event Paused(); + event Unpaused(); + + // ************************************* // + // * Function Modifiers * // + // ************************************* // + + modifier onlyByGovernor() { + if (governor != msg.sender) revert GovernorOnly(); + _; + } + + modifier onlyByGuardianOrGovernor() { + if (guardian != msg.sender && governor != msg.sender) revert GuardianOrGovernorOnly(); + _; + } + + modifier whenPaused() { + if (!paused) revert WhenPausedOnly(); + _; + } + + modifier whenNotPaused() { + if (paused) revert WhenNotPausedOnly(); + _; + } + + // ************************************* // + // * Constructor * // + // ************************************* // + + function __KlerosCoreV2Base_initialize( + address _governor, + address _guardian, + IERC20 _pinakion, + address _jurorProsecutionModule, + IDisputeKit _disputeKit, + bool _hiddenVotes, + uint256[4] memory _courtParameters, + uint256[4] memory _timesPerPeriod, + bytes memory _sortitionExtraData, + IStakeController _stakeController + ) internal onlyInitializing { + governor = _governor; + guardian = _guardian; + pinakion = _pinakion; + jurorProsecutionModule = _jurorProsecutionModule; + stakeController = _stakeController; + + // NULL_DISPUTE_KIT: an empty element at index 0 to indicate when a dispute kit is not supported. + disputeKits.push(); + + // DISPUTE_KIT_CLASSIC + disputeKits.push(_disputeKit); + + emit DisputeKitCreated(DISPUTE_KIT_CLASSIC, _disputeKit); + + // FORKING_COURT + // TODO: Fill the properties for the Forking court, emit CourtCreated. + courts.push(); + stakeController.createTree(bytes32(uint256(FORKING_COURT)), _sortitionExtraData); + + // GENERAL_COURT + Court storage court = courts.push(); + court.parent = FORKING_COURT; + court.children = new uint256[](0); + court.hiddenVotes = _hiddenVotes; + court.minStake = _courtParameters[0]; + court.alpha = _courtParameters[1]; + court.feeForJuror = _courtParameters[2]; + court.jurorsForCourtJump = _courtParameters[3]; + court.timesPerPeriod = _timesPerPeriod; + + stakeController.createTree(bytes32(uint256(GENERAL_COURT)), _sortitionExtraData); + + uint256[] memory supportedDisputeKits = new uint256[](1); + supportedDisputeKits[0] = DISPUTE_KIT_CLASSIC; + emit CourtCreated( + GENERAL_COURT, + court.parent, + _hiddenVotes, + _courtParameters[0], + _courtParameters[1], + _courtParameters[2], + _courtParameters[3], + _timesPerPeriod, + supportedDisputeKits + ); + _enableDisputeKit(GENERAL_COURT, DISPUTE_KIT_CLASSIC, true); + } + + // ************************************* // + // * Governance * // + // ************************************* // + + /// @dev Pause staking and reward execution. Can only be done by guardian or governor. + function pause() external onlyByGuardianOrGovernor whenNotPaused { + paused = true; + emit Paused(); + } + + /// @dev Unpause staking and reward execution. Can only be done by governor. + function unpause() external onlyByGovernor whenPaused { + paused = false; + emit Unpaused(); + } + + /// @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); + if (!success) revert UnsuccessfulCall(); + } + + /// @dev Changes the `governor` storage variable. + /// @param _governor The new value for the `governor` storage variable. + function changeGovernor(address payable _governor) external onlyByGovernor { + governor = _governor; + } + + /// @dev Changes the `guardian` storage variable. + /// @param _guardian The new value for the `guardian` storage variable. + function changeGuardian(address _guardian) external onlyByGovernor { + guardian = _guardian; + } + + /// @dev Changes the `pinakion` storage variable. + /// @param _pinakion The new value for the `pinakion` storage variable. + function changePinakion(IERC20 _pinakion) external onlyByGovernor { + pinakion = _pinakion; + } + + /// @dev Changes the `jurorProsecutionModule` storage variable. + /// @param _jurorProsecutionModule The new value for the `jurorProsecutionModule` storage variable. + function changeJurorProsecutionModule(address _jurorProsecutionModule) external onlyByGovernor { + jurorProsecutionModule = _jurorProsecutionModule; + } + + /// @dev Changes the `stakeController` storage variable. + /// Note that the new controller should be initialized for all courts. + /// @param _stakeController The new value for the `stakeController` storage variable. + function changeStakeController(IStakeController _stakeController) external onlyByGovernor { + stakeController = _stakeController; + } + + /// @dev Add a new supported dispute kit module to the court. + /// @param _disputeKitAddress The address of the dispute kit contract. + function addNewDisputeKit(IDisputeKit _disputeKitAddress) external onlyByGovernor { + uint256 disputeKitID = disputeKits.length; + disputeKits.push(_disputeKitAddress); + emit DisputeKitCreated(disputeKitID, _disputeKitAddress); + } + + /// @dev Creates a court under a specified parent court. + /// @param _parent The `parent` property value of the court. + /// @param _hiddenVotes The `hiddenVotes` property value of the court. + /// @param _minStake The `minStake` property value of the court. + /// @param _alpha The `alpha` property value of the court. + /// @param _feeForJuror The `feeForJuror` property value of the court. + /// @param _jurorsForCourtJump The `jurorsForCourtJump` property value of the court. + /// @param _timesPerPeriod The `timesPerPeriod` property value of the court. + /// @param _sortitionExtraData Extra data for sortition module. + /// @param _supportedDisputeKits Indexes of dispute kits that this court will support. + function createCourt( + uint96 _parent, + bool _hiddenVotes, + uint256 _minStake, + uint256 _alpha, + uint256 _feeForJuror, + uint256 _jurorsForCourtJump, + uint256[4] memory _timesPerPeriod, + bytes memory _sortitionExtraData, + uint256[] memory _supportedDisputeKits + ) external onlyByGovernor { + if (courts[_parent].minStake > _minStake) revert MinStakeLowerThanParentCourt(); + if (_supportedDisputeKits.length == 0) revert UnsupportedDisputeKit(); + if (_parent == FORKING_COURT) revert InvalidForkingCourtAsParent(); + + uint256 courtID = courts.length; + Court storage court = courts.push(); + + for (uint256 i = 0; i < _supportedDisputeKits.length; i++) { + if (_supportedDisputeKits[i] == 0 || _supportedDisputeKits[i] >= disputeKits.length) { + revert WrongDisputeKitIndex(); + } + _enableDisputeKit(uint96(courtID), _supportedDisputeKits[i], true); + } + // Check that Classic DK support was added. + if (!court.supportedDisputeKits[DISPUTE_KIT_CLASSIC]) revert MustSupportDisputeKitClassic(); + + court.parent = _parent; + court.children = new uint256[](0); + court.hiddenVotes = _hiddenVotes; + court.minStake = _minStake; + court.alpha = _alpha; + court.feeForJuror = _feeForJuror; + court.jurorsForCourtJump = _jurorsForCourtJump; + court.timesPerPeriod = _timesPerPeriod; + + stakeController.createTree(bytes32(courtID), _sortitionExtraData); + if (_parent != FORKING_COURT) { + courts[_parent].children.push(courtID); + } + + emit CourtCreated( + uint96(courtID), + _parent, + _hiddenVotes, + _minStake, + _alpha, + _feeForJuror, + _jurorsForCourtJump, + _timesPerPeriod, + _supportedDisputeKits + ); + } + + function changeCourtParameters( + uint96 _courtID, + bool _hiddenVotes, + uint256 _minStake, + uint256 _alpha, + uint256 _feeForJuror, + uint256 _jurorsForCourtJump, + uint256[4] memory _timesPerPeriod + ) external onlyByGovernor { + Court storage court = courts[_courtID]; + if (_courtID != GENERAL_COURT && courts[court.parent].minStake > _minStake) { + revert MinStakeLowerThanParentCourt(); + } + for (uint256 i = 0; i < court.children.length; i++) { + if (courts[court.children[i]].minStake < _minStake) { + revert MinStakeLowerThanParentCourt(); + } + } + court.minStake = _minStake; + court.hiddenVotes = _hiddenVotes; + court.alpha = _alpha; + court.feeForJuror = _feeForJuror; + court.jurorsForCourtJump = _jurorsForCourtJump; + court.timesPerPeriod = _timesPerPeriod; + emit CourtModified( + _courtID, + _hiddenVotes, + _minStake, + _alpha, + _feeForJuror, + _jurorsForCourtJump, + _timesPerPeriod + ); + } + + /// @dev Adds/removes court's support for specified dispute kits. + /// @param _courtID The ID of the court. + /// @param _disputeKitIDs The IDs of dispute kits which support should be added/removed. + /// @param _enable Whether add or remove the dispute kits from the court. + function enableDisputeKits(uint96 _courtID, uint256[] memory _disputeKitIDs, bool _enable) external onlyByGovernor { + for (uint256 i = 0; i < _disputeKitIDs.length; i++) { + if (_enable) { + if (_disputeKitIDs[i] == 0 || _disputeKitIDs[i] >= disputeKits.length) { + revert WrongDisputeKitIndex(); + } + _enableDisputeKit(_courtID, _disputeKitIDs[i], true); + } else { + // Classic dispute kit must be supported by all courts. + if (_disputeKitIDs[i] == DISPUTE_KIT_CLASSIC) { + revert CannotDisableClassicDK(); + } + _enableDisputeKit(_courtID, _disputeKitIDs[i], false); + } + } + } + + /// @dev Changes the supported fee tokens. + /// @param _feeToken The fee token. + /// @param _accepted Whether the token is supported or not as a method of fee payment. + function changeAcceptedFeeTokens(IERC20 _feeToken, bool _accepted) external onlyByGovernor { + currencyRates[_feeToken].feePaymentAccepted = _accepted; + emit AcceptedFeeToken(_feeToken, _accepted); + } + + /// @dev Changes the currency rate of a fee token. + /// @param _feeToken The fee token. + /// @param _rateInEth The new rate of the fee token in ETH. + /// @param _rateDecimals The new decimals of the fee token rate. + function changeCurrencyRates(IERC20 _feeToken, uint64 _rateInEth, uint8 _rateDecimals) external onlyByGovernor { + currencyRates[_feeToken].rateInEth = _rateInEth; + currencyRates[_feeToken].rateDecimals = _rateDecimals; + emit NewCurrencyRate(_feeToken, _rateInEth, _rateDecimals); + } + + // ************************************* // + // * State Modifiers * // + // ************************************* // + + /// @dev Sets the caller's stake in a court. + /// @param _courtID The ID of the court. + /// @param _newStake The new stake. + /// Note that the existing delayed stake will be nullified as non-relevant. + function setStake(uint96 _courtID, uint256 _newStake) external virtual whenNotPaused { + _setStake(msg.sender, _courtID, _newStake, false, OnError.Revert); + } + + /// @dev Sets the stake of a specified account in a court, typically to apply a delayed stake or unstake inactive jurors. + /// @param _account The account whose stake is being set. + /// @param _courtID The ID of the court. + /// @param _newStake The new stake. + /// @param _alreadyTransferred Whether the PNKs have already been transferred to the contract. + function setStakeBySortitionModule( + address _account, + uint96 _courtID, + uint256 _newStake, + bool _alreadyTransferred + ) external { + if (msg.sender != address(stakeController)) revert SortitionModuleOnly(); + _setStake(_account, _courtID, _newStake, _alreadyTransferred, OnError.Return); + } + + /// @inheritdoc IArbitratorV2 + function createDispute( + uint256 _numberOfChoices, + bytes memory _extraData + ) external payable override returns (uint256 disputeID) { + if (msg.value < arbitrationCost(_extraData)) revert ArbitrationFeesNotEnough(); + + return _createDispute(_numberOfChoices, _extraData, NATIVE_CURRENCY, msg.value); + } + + /// @inheritdoc IArbitratorV2 + function createDispute( + uint256 _numberOfChoices, + bytes calldata _extraData, + IERC20 _feeToken, + uint256 _feeAmount + ) external override returns (uint256 disputeID) { + if (!currencyRates[_feeToken].feePaymentAccepted) revert TokenNotAccepted(); + if (_feeAmount < arbitrationCost(_extraData, _feeToken)) revert ArbitrationFeesNotEnough(); + + if (!_feeToken.safeTransferFrom(msg.sender, address(this), _feeAmount)) revert TransferFailed(); + return _createDispute(_numberOfChoices, _extraData, _feeToken, _feeAmount); + } + + function _createDispute( + uint256 _numberOfChoices, + bytes memory _extraData, + IERC20 _feeToken, + uint256 _feeAmount + ) internal virtual returns (uint256 disputeID) { + (uint96 courtID, , uint256 disputeKitID) = _extraDataToCourtIDMinJurorsDisputeKit(_extraData); + if (!courts[courtID].supportedDisputeKits[disputeKitID]) revert DisputeKitNotSupportedByCourt(); + + disputeID = disputes.length; + Dispute storage dispute = disputes.push(); + dispute.courtID = courtID; + dispute.arbitrated = IArbitrableV2(msg.sender); + dispute.lastPeriodChange = block.timestamp; + + IDisputeKit disputeKit = disputeKits[disputeKitID]; + Court storage court = courts[courtID]; + Round storage round = dispute.rounds.push(); + + // Obtain the feeForJuror in the same currency as the _feeAmount + uint256 feeForJuror = (_feeToken == NATIVE_CURRENCY) + ? court.feeForJuror + : convertEthToTokenAmount(_feeToken, court.feeForJuror); + round.nbVotes = _feeAmount / feeForJuror; + round.disputeKitID = disputeKitID; + round.pnkAtStakePerJuror = (court.minStake * court.alpha) / ALPHA_DIVISOR; + round.totalFeesForJurors = _feeAmount; + round.feeToken = IERC20(_feeToken); + + stakeController.createDisputeHook(disputeID, 0); // Default round ID. + + disputeKit.createDispute(disputeID, _numberOfChoices, _extraData, round.nbVotes); + emit DisputeCreation(disputeID, IArbitrableV2(msg.sender)); + } + + /// @dev Passes the period of a specified dispute. + /// @param _disputeID The ID of the dispute. + function passPeriod(uint256 _disputeID) external { + Dispute storage dispute = disputes[_disputeID]; + Court storage court = courts[dispute.courtID]; + + uint256 currentRound = dispute.rounds.length - 1; + Round storage round = dispute.rounds[currentRound]; + if (dispute.period == Period.evidence) { + if ( + currentRound == 0 && + block.timestamp - dispute.lastPeriodChange < court.timesPerPeriod[uint256(dispute.period)] + ) { + revert EvidenceNotPassedAndNotAppeal(); + } + if (round.drawnJurors.length != round.nbVotes) revert DisputeStillDrawing(); + dispute.period = court.hiddenVotes ? Period.commit : Period.vote; + } else if (dispute.period == Period.commit) { + if ( + block.timestamp - dispute.lastPeriodChange < court.timesPerPeriod[uint256(dispute.period)] && + !disputeKits[round.disputeKitID].areCommitsAllCast(_disputeID) + ) { + revert CommitPeriodNotPassed(); + } + dispute.period = Period.vote; + } else if (dispute.period == Period.vote) { + if ( + block.timestamp - dispute.lastPeriodChange < court.timesPerPeriod[uint256(dispute.period)] && + !disputeKits[round.disputeKitID].areVotesAllCast(_disputeID) + ) { + revert VotePeriodNotPassed(); + } + dispute.period = Period.appeal; + emit AppealPossible(_disputeID, dispute.arbitrated); + } else if (dispute.period == Period.appeal) { + if ( + block.timestamp - dispute.lastPeriodChange < court.timesPerPeriod[uint256(dispute.period)] && + !disputeKits[round.disputeKitID].isAppealFunded(_disputeID) + ) { + revert AppealPeriodNotPassed(); + } + dispute.period = Period.execution; + } else if (dispute.period == Period.execution) { + revert DisputePeriodIsFinal(); + } + + dispute.lastPeriodChange = block.timestamp; + emit NewPeriod(_disputeID, dispute.period); + } + + /// @dev Draws jurors for the dispute. Can be called in parts. + /// @param _disputeID The ID of the dispute. + /// @param _iterations The number of iterations to run. + /// @return nbDrawnJurors The total number of jurors drawn in the round. + function draw(uint256 _disputeID, uint256 _iterations) external returns (uint256 nbDrawnJurors) { + Dispute storage dispute = disputes[_disputeID]; + uint256 currentRound = dispute.rounds.length - 1; + Round storage round = dispute.rounds[currentRound]; + if (dispute.period != Period.evidence) revert NotEvidencePeriod(); + + IDisputeKit disputeKit = disputeKits[round.disputeKitID]; + + uint256 startIndex = round.drawIterations; // for gas: less storage reads + uint256 i; + while (i < _iterations && round.drawnJurors.length < round.nbVotes) { + address drawnAddress = disputeKit.draw(_disputeID, startIndex + i++); + if (drawnAddress == address(0)) { + continue; + } + stakeController.lockStake(drawnAddress, round.pnkAtStakePerJuror); + emit Draw(drawnAddress, _disputeID, currentRound, round.drawnJurors.length); + round.drawnJurors.push(drawnAddress); + if (round.drawnJurors.length == round.nbVotes) { + stakeController.postDrawHook(_disputeID, currentRound); + } + } + round.drawIterations += i; + return round.drawnJurors.length; + } + + /// @dev Appeals the ruling of a specified dispute. + /// Note: Access restricted to the Dispute Kit for this `disputeID`. + /// @param _disputeID The ID of the dispute. + /// @param _numberOfChoices Number of choices for the dispute. Can be required during court jump. + /// @param _extraData Extradata for the dispute. Can be required during court jump. + function appeal(uint256 _disputeID, uint256 _numberOfChoices, bytes memory _extraData) external payable { + if (msg.value < appealCost(_disputeID)) revert AppealFeesNotEnough(); + + Dispute storage dispute = disputes[_disputeID]; + if (dispute.period != Period.appeal) revert DisputeNotAppealable(); + + Round storage round = dispute.rounds[dispute.rounds.length - 1]; + if (msg.sender != address(disputeKits[round.disputeKitID])) revert DisputeKitOnly(); + + uint96 newCourtID = dispute.courtID; + uint256 newDisputeKitID = round.disputeKitID; + + // Warning: the extra round must be created before calling disputeKit.createDispute() + Round storage extraRound = dispute.rounds.push(); + + if (round.nbVotes >= courts[newCourtID].jurorsForCourtJump) { + // Jump to parent court. + newCourtID = courts[newCourtID].parent; + + if (!courts[newCourtID].supportedDisputeKits[newDisputeKitID]) { + // Switch to classic dispute kit if parent court doesn't support the current one. + newDisputeKitID = DISPUTE_KIT_CLASSIC; + } + + if (newCourtID != dispute.courtID) { + emit CourtJump(_disputeID, dispute.rounds.length - 1, dispute.courtID, newCourtID); + } + } + + dispute.courtID = newCourtID; + dispute.period = Period.evidence; + dispute.lastPeriodChange = block.timestamp; + + Court storage court = courts[newCourtID]; + extraRound.nbVotes = msg.value / court.feeForJuror; // As many votes that can be afforded by the provided funds. + extraRound.pnkAtStakePerJuror = (court.minStake * court.alpha) / ALPHA_DIVISOR; + extraRound.totalFeesForJurors = msg.value; + extraRound.disputeKitID = newDisputeKitID; + + stakeController.createDisputeHook(_disputeID, dispute.rounds.length - 1); + + // Dispute kit was changed, so create a dispute in the new DK contract. + if (extraRound.disputeKitID != round.disputeKitID) { + emit DisputeKitJump(_disputeID, dispute.rounds.length - 1, round.disputeKitID, extraRound.disputeKitID); + disputeKits[extraRound.disputeKitID].createDispute( + _disputeID, + _numberOfChoices, + _extraData, + extraRound.nbVotes + ); + } + + emit AppealDecision(_disputeID, dispute.arbitrated); + emit NewPeriod(_disputeID, Period.evidence); + } + + /// @dev Distribute the PNKs at stake and the dispute fees for the specific round of the dispute. Can be called in parts. + /// Note: Reward distributions are forbidden during pause. + /// @param _disputeID The ID of the dispute. + /// @param _round The appeal round. + /// @param _iterations The number of iterations to run. + function execute(uint256 _disputeID, uint256 _round, uint256 _iterations) external whenNotPaused { + Round storage round; + { + Dispute storage dispute = disputes[_disputeID]; + if (dispute.period != Period.execution) revert NotExecutionPeriod(); + + round = dispute.rounds[_round]; + } // stack too deep workaround + + uint256 start = round.repartitions; + uint256 end = round.repartitions + _iterations; + + uint256 pnkPenaltiesInRound = round.pnkPenalties; // Keep in memory to save gas. + uint256 numberOfVotesInRound = round.drawnJurors.length; + uint256 feePerJurorInRound = round.totalFeesForJurors / numberOfVotesInRound; + uint256 pnkAtStakePerJurorInRound = round.pnkAtStakePerJuror; + uint256 coherentCount; + { + IDisputeKit disputeKit = disputeKits[round.disputeKitID]; + coherentCount = disputeKit.getCoherentCount(_disputeID, _round); // Total number of jurors that are eligible to a reward in this round. + } // stack too deep workaround + + if (coherentCount == 0) { + // We loop over the votes once as there are no rewards because it is not a tie and no one in this round is coherent with the final outcome. + if (end > numberOfVotesInRound) end = numberOfVotesInRound; + } else { + // We loop over the votes twice, first to collect the PNK penalties, and second to distribute them as rewards along with arbitration fees. + if (end > numberOfVotesInRound * 2) end = numberOfVotesInRound * 2; + } + round.repartitions = end; + + for (uint256 i = start; i < end; i++) { + if (i < numberOfVotesInRound) { + pnkPenaltiesInRound = _executePenalties( + ExecuteParams({ + disputeID: _disputeID, + round: _round, + coherentCount: coherentCount, + numberOfVotesInRound: numberOfVotesInRound, + feePerJurorInRound: feePerJurorInRound, + pnkAtStakePerJurorInRound: pnkAtStakePerJurorInRound, + pnkPenaltiesInRound: pnkPenaltiesInRound, + repartition: i + }) + ); + } else { + _executeRewards( + ExecuteParams({ + disputeID: _disputeID, + round: _round, + coherentCount: coherentCount, + numberOfVotesInRound: numberOfVotesInRound, + feePerJurorInRound: feePerJurorInRound, + pnkAtStakePerJurorInRound: pnkAtStakePerJurorInRound, + pnkPenaltiesInRound: pnkPenaltiesInRound, + repartition: i + }) + ); + } + } + if (round.pnkPenalties != pnkPenaltiesInRound) { + round.pnkPenalties = pnkPenaltiesInRound; // Reentrancy risk: breaks Check-Effect-Interact + } + } + + /// @dev Distribute the PNKs at stake and the dispute fees for the specific round of the dispute, penalties only. + /// @param _params The parameters for the execution, see `ExecuteParams`. + /// @return pnkPenaltiesInRoundCache The updated penalties in round cache. + function _executePenalties(ExecuteParams memory _params) internal returns (uint256) { + Dispute storage dispute = disputes[_params.disputeID]; + Round storage round = dispute.rounds[_params.round]; + IDisputeKit disputeKit = disputeKits[round.disputeKitID]; + + // [0, 1] value that determines how coherent the juror was in this round, in basis points. + uint256 degreeOfCoherence = disputeKit.getDegreeOfCoherence( + _params.disputeID, + _params.round, + _params.repartition, + _params.feePerJurorInRound, + _params.pnkAtStakePerJurorInRound + ); + if (degreeOfCoherence > ALPHA_DIVISOR) { + // Make sure the degree doesn't exceed 1, though it should be ensured by the dispute kit. + degreeOfCoherence = ALPHA_DIVISOR; + } + + // Fully coherent jurors won't be penalized. + uint256 penalty = (round.pnkAtStakePerJuror * (ALPHA_DIVISOR - degreeOfCoherence)) / ALPHA_DIVISOR; + + // Execute penalty through StakeController coordination + address account = round.drawnJurors[_params.repartition]; + uint256 actualPenalty = stakeController.executeJurorPenalty(account, penalty, round.pnkAtStakePerJuror); + _params.pnkPenaltiesInRound += actualPenalty; + + emit TokenAndETHShift( + account, + _params.disputeID, + _params.round, + degreeOfCoherence, + -int256(actualPenalty), + 0, + round.feeToken + ); + + // Check if juror should be set inactive + if (stakeController.shouldSetJurorInactive(account, _params.disputeID, _params.round, _params.repartition)) { + stakeController.setJurorInactive(account); + } + + if (_params.repartition == _params.numberOfVotesInRound - 1 && _params.coherentCount == 0) { + // No one was coherent, send the rewards to the governor. + if (round.feeToken == NATIVE_CURRENCY) { + // The dispute fees were paid in ETH + payable(governor).send(round.totalFeesForJurors); + } else { + // The dispute fees were paid in ERC20 + round.feeToken.safeTransfer(governor, round.totalFeesForJurors); + } + pinakion.safeTransfer(governor, _params.pnkPenaltiesInRound); + emit LeftoverRewardSent( + _params.disputeID, + _params.round, + _params.pnkPenaltiesInRound, + round.totalFeesForJurors, + round.feeToken + ); + } + return _params.pnkPenaltiesInRound; + } + + /// @dev Distribute the PNKs at stake and the dispute fees for the specific round of the dispute, rewards only. + /// @param _params The parameters for the execution, see `ExecuteParams`. + function _executeRewards(ExecuteParams memory _params) internal { + Dispute storage dispute = disputes[_params.disputeID]; + Round storage round = dispute.rounds[_params.round]; + IDisputeKit disputeKit = disputeKits[round.disputeKitID]; + + // [0, 1] value that determines how coherent the juror was in this round, in basis points. + uint256 degreeOfCoherence = disputeKit.getDegreeOfCoherence( + _params.disputeID, + _params.round, + _params.repartition % _params.numberOfVotesInRound, + _params.feePerJurorInRound, + _params.pnkAtStakePerJurorInRound + ); + + // Make sure the degree doesn't exceed 1, though it should be ensured by the dispute kit. + if (degreeOfCoherence > ALPHA_DIVISOR) { + degreeOfCoherence = ALPHA_DIVISOR; + } + + address account = round.drawnJurors[_params.repartition % _params.numberOfVotesInRound]; + uint256 pnkLocked = (round.pnkAtStakePerJuror * degreeOfCoherence) / ALPHA_DIVISOR; + + // Release the rest of the PNKs of the juror for this round. + stakeController.unlockStake(account, pnkLocked); + + // Give back the locked PNKs in case the juror fully unstaked earlier. + if (!stakeController.isJurorStaked(account)) { + pinakion.safeTransfer(account, pnkLocked); + } + + // Transfer the rewards + uint256 pnkReward = ((_params.pnkPenaltiesInRound / _params.coherentCount) * degreeOfCoherence) / ALPHA_DIVISOR; + round.sumPnkRewardPaid += pnkReward; + uint256 feeReward = ((round.totalFeesForJurors / _params.coherentCount) * degreeOfCoherence) / ALPHA_DIVISOR; + round.sumFeeRewardPaid += feeReward; + pinakion.safeTransfer(account, pnkReward); + if (round.feeToken == NATIVE_CURRENCY) { + // The dispute fees were paid in ETH + payable(account).send(feeReward); + } else { + // The dispute fees were paid in ERC20 + round.feeToken.safeTransfer(account, feeReward); + } + emit TokenAndETHShift( + account, + _params.disputeID, + _params.round, + degreeOfCoherence, + int256(pnkReward), + int256(feeReward), + round.feeToken + ); + + // Transfer any residual rewards to the governor. It may happen due to partial coherence of the jurors. + if (_params.repartition == _params.numberOfVotesInRound * 2 - 1) { + uint256 leftoverPnkReward = _params.pnkPenaltiesInRound - round.sumPnkRewardPaid; + uint256 leftoverFeeReward = round.totalFeesForJurors - round.sumFeeRewardPaid; + if (leftoverPnkReward != 0 || leftoverFeeReward != 0) { + if (leftoverPnkReward != 0) { + pinakion.safeTransfer(governor, leftoverPnkReward); + } + if (leftoverFeeReward != 0) { + if (round.feeToken == NATIVE_CURRENCY) { + // The dispute fees were paid in ETH + payable(governor).send(leftoverFeeReward); + } else { + // The dispute fees were paid in ERC20 + round.feeToken.safeTransfer(governor, leftoverFeeReward); + } + } + emit LeftoverRewardSent( + _params.disputeID, + _params.round, + leftoverPnkReward, + leftoverFeeReward, + round.feeToken + ); + } + } + } + + /// @dev Executes a specified dispute's ruling. + /// @param _disputeID The ID of the dispute. + function executeRuling(uint256 _disputeID) external { + Dispute storage dispute = disputes[_disputeID]; + if (dispute.period != Period.execution) revert NotExecutionPeriod(); + if (dispute.ruled) revert RulingAlreadyExecuted(); + + (uint256 winningChoice, , ) = currentRuling(_disputeID); + dispute.ruled = true; + emit Ruling(dispute.arbitrated, _disputeID, winningChoice); + dispute.arbitrated.rule(_disputeID, winningChoice); + } + + // ************************************* // + // * Public Views * // + // ************************************* // + + /// @dev Compute the cost of arbitration denominated in ETH. + /// It is recommended not to increase it often, as it can be highly time and gas consuming for the arbitrated contracts to cope with fee augmentation. + /// @param _extraData Additional info about the dispute. We use it to pass the ID of the dispute's court (first 32 bytes), the minimum number of jurors required (next 32 bytes) and the ID of the specific dispute kit (last 32 bytes). + /// @return cost The arbitration cost in ETH. + function arbitrationCost(bytes memory _extraData) public view override returns (uint256 cost) { + (uint96 courtID, uint256 minJurors, ) = _extraDataToCourtIDMinJurorsDisputeKit(_extraData); + cost = courts[courtID].feeForJuror * minJurors; + } + + /// @dev Compute the cost of arbitration denominated in `_feeToken`. + /// It is recommended not to increase it often, as it can be highly time and gas consuming for the arbitrated contracts to cope with fee augmentation. + /// @param _extraData Additional info about the dispute. We use it to pass the ID of the dispute's court (first 32 bytes), the minimum number of jurors required (next 32 bytes) and the ID of the specific dispute kit (last 32 bytes). + /// @param _feeToken The ERC20 token used to pay fees. + /// @return cost The arbitration cost in `_feeToken`. + function arbitrationCost(bytes calldata _extraData, IERC20 _feeToken) public view override returns (uint256 cost) { + cost = convertEthToTokenAmount(_feeToken, arbitrationCost(_extraData)); + } + + /// @dev Gets the cost of appealing a specified dispute. + /// @param _disputeID The ID of the dispute. + /// @return cost The appeal cost. + function appealCost(uint256 _disputeID) public view returns (uint256 cost) { + Dispute storage dispute = disputes[_disputeID]; + Round storage round = dispute.rounds[dispute.rounds.length - 1]; + Court storage court = courts[dispute.courtID]; + if (round.nbVotes >= court.jurorsForCourtJump) { + // Jump to parent court. + if (dispute.courtID == GENERAL_COURT) { + // TODO: Handle the forking when appealed in General court. + cost = NON_PAYABLE_AMOUNT; // Get the cost of the parent court. + } else { + cost = courts[court.parent].feeForJuror * ((round.nbVotes * 2) + 1); + } + } else { + // Stay in current court. + cost = court.feeForJuror * ((round.nbVotes * 2) + 1); + } + } + + /// @dev Gets the start and the end of a specified dispute's current appeal period. + /// @param _disputeID The ID of the dispute. + /// @return start The start of the appeal period. + /// @return end The end of the appeal period. + function appealPeriod(uint256 _disputeID) public view returns (uint256 start, uint256 end) { + Dispute storage dispute = disputes[_disputeID]; + if (dispute.period == Period.appeal) { + start = dispute.lastPeriodChange; + end = dispute.lastPeriodChange + courts[dispute.courtID].timesPerPeriod[uint256(Period.appeal)]; + } else { + start = 0; + end = 0; + } + } + + /// @dev Gets the current ruling of a specified dispute. + /// @param _disputeID The ID of the dispute. + /// @return ruling The current ruling. + /// @return tied Whether it's a tie or not. + /// @return overridden Whether the ruling was overridden by appeal funding or not. + function currentRuling(uint256 _disputeID) public view returns (uint256 ruling, bool tied, bool overridden) { + Dispute storage dispute = disputes[_disputeID]; + Round storage round = dispute.rounds[dispute.rounds.length - 1]; + IDisputeKit disputeKit = disputeKits[round.disputeKitID]; + (ruling, tied, overridden) = disputeKit.currentRuling(_disputeID); + } + + /// @dev Gets the round info for a specified dispute and round. + /// @dev This function must not be called from a non-view function because it returns a dynamic array which might be very large, theoretically exceeding the block gas limit. + /// @param _disputeID The ID of the dispute. + /// @param _round The round to get the info for. + /// @return round The round info. + function getRoundInfo(uint256 _disputeID, uint256 _round) external view returns (Round memory) { + return disputes[_disputeID].rounds[_round]; + } + + /// @dev Gets the PNK at stake per juror for a specified dispute and round. + /// @param _disputeID The ID of the dispute. + /// @param _round The round to get the info for. + /// @return pnkAtStakePerJuror The PNK at stake per juror. + function getPnkAtStakePerJuror(uint256 _disputeID, uint256 _round) external view returns (uint256) { + return disputes[_disputeID].rounds[_round].pnkAtStakePerJuror; + } + + /// @dev Gets the number of rounds for a specified dispute. + /// @param _disputeID The ID of the dispute. + /// @return The number of rounds. + function getNumberOfRounds(uint256 _disputeID) external view returns (uint256) { + return disputes[_disputeID].rounds.length; + } + + /// @dev Checks if a given dispute kit is supported by a given court. + /// @param _courtID The ID of the court to check the support for. + /// @param _disputeKitID The ID of the dispute kit to check the support for. + /// @return Whether the dispute kit is supported or not. + function isSupported(uint96 _courtID, uint256 _disputeKitID) external view returns (bool) { + return courts[_courtID].supportedDisputeKits[_disputeKitID]; + } + + /// @dev Gets the timesPerPeriod array for a given court. + /// @param _courtID The ID of the court to get the times from. + /// @return timesPerPeriod The timesPerPeriod array for the given court. + function getTimesPerPeriod(uint96 _courtID) external view returns (uint256[4] memory timesPerPeriod) { + timesPerPeriod = courts[_courtID].timesPerPeriod; + } + + // ************************************* // + // * Public Views for Dispute Kits * // + // ************************************* // + + /// @dev Gets the number of votes permitted for the specified dispute in the latest round. + /// @param _disputeID The ID of the dispute. + function getNumberOfVotes(uint256 _disputeID) external view returns (uint256) { + Dispute storage dispute = disputes[_disputeID]; + return dispute.rounds[dispute.rounds.length - 1].nbVotes; + } + + /// @dev Returns true if the dispute kit will be switched to a parent DK. + /// @param _disputeID The ID of the dispute. + /// @return Whether DK will be switched or not. + function isDisputeKitJumping(uint256 _disputeID) external view returns (bool) { + Dispute storage dispute = disputes[_disputeID]; + Round storage round = dispute.rounds[dispute.rounds.length - 1]; + Court storage court = courts[dispute.courtID]; + + if (round.nbVotes < court.jurorsForCourtJump) { + return false; + } + + // Jump if the parent court doesn't support the current DK. + return !courts[court.parent].supportedDisputeKits[round.disputeKitID]; + } + + function getDisputeKitsLength() external view returns (uint256) { + return disputeKits.length; + } + + function convertEthToTokenAmount(IERC20 _toToken, uint256 _amountInEth) public view returns (uint256) { + return (_amountInEth * 10 ** currencyRates[_toToken].rateDecimals) / currencyRates[_toToken].rateInEth; + } + + // ************************************* // + // * Internal * // + // ************************************* // + + /// @dev Toggles the dispute kit support for a given court. + /// @param _courtID The ID of the court to toggle the support for. + /// @param _disputeKitID The ID of the dispute kit to toggle the support for. + /// @param _enable Whether to enable or disable the support. Note that classic dispute kit should always be enabled. + function _enableDisputeKit(uint96 _courtID, uint256 _disputeKitID, bool _enable) internal { + courts[_courtID].supportedDisputeKits[_disputeKitID] = _enable; + emit DisputeKitEnabled(_courtID, _disputeKitID, _enable); + } + + /// @dev If called only once then set _onError to Revert, otherwise set it to Return + /// @param _account The account to set the stake for. + /// @param _courtID The ID of the court to set the stake for. + /// @param _newStake The new stake. + /// @param _alreadyTransferred Whether the PNKs were already transferred to/from the staking contract. + /// @param _onError Whether to revert or return false on error. + /// @return Whether the stake was successfully set or not. + function _setStake( + address _account, + uint96 _courtID, + uint256 _newStake, + bool _alreadyTransferred, + OnError _onError + ) internal returns (bool) { + if (_courtID == FORKING_COURT || _courtID >= courts.length) { + _stakingFailed(_onError, StakingResult.CannotStakeInThisCourt); // Staking directly into the forking court is not allowed. + return false; + } + if (_newStake != 0 && _newStake < courts[_courtID].minStake) { + _stakingFailed(_onError, StakingResult.CannotStakeLessThanMinStake); // Staking less than the minimum stake is not allowed. + return false; + } + (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) = stakeController.setStake( + _account, + _courtID, + _newStake, + _alreadyTransferred + ); + if (stakingResult != StakingResult.Successful) { + _stakingFailed(_onError, stakingResult); + return false; + } + if (pnkDeposit > 0) { + if (!pinakion.safeTransferFrom(_account, address(this), pnkDeposit)) { + _stakingFailed(_onError, StakingResult.StakingTransferFailed); + return false; + } + } + if (pnkWithdrawal > 0) { + if (!pinakion.safeTransfer(_account, pnkWithdrawal)) { + _stakingFailed(_onError, StakingResult.UnstakingTransferFailed); + return false; + } + } + return true; + } + + /// @dev It may revert depending on the _onError parameter. + function _stakingFailed(OnError _onError, StakingResult _result) internal pure virtual { + if (_onError == OnError.Return) return; + if (_result == StakingResult.StakingTransferFailed) revert StakingTransferFailed(); + if (_result == StakingResult.UnstakingTransferFailed) revert UnstakingTransferFailed(); + if (_result == StakingResult.CannotStakeInMoreCourts) revert StakingInTooManyCourts(); + if (_result == StakingResult.CannotStakeInThisCourt) revert StakingNotPossibleInThisCourt(); + if (_result == StakingResult.CannotStakeLessThanMinStake) revert StakingLessThanCourtMinStake(); + if (_result == StakingResult.CannotStakeZeroWhenNoStake) revert StakingZeroWhenNoStake(); + } + + /// @dev Gets a court ID, the minimum number of jurors and an ID of a dispute kit from a specified extra data bytes array. + /// Note that if extradata contains an incorrect value then this value will be switched to default. + /// @param _extraData The extra data bytes array. The first 32 bytes are the court ID, the next are the minimum number of jurors and the last are the dispute kit ID. + /// @return courtID The court ID. + /// @return minJurors The minimum number of jurors required. + /// @return disputeKitID The ID of the dispute kit. + function _extraDataToCourtIDMinJurorsDisputeKit( + bytes memory _extraData + ) internal view returns (uint96 courtID, uint256 minJurors, uint256 disputeKitID) { + // Note that if the extradata doesn't contain 32 bytes for the dispute kit ID it'll return the default 0 index. + if (_extraData.length >= 64) { + assembly { + // solium-disable-line security/no-inline-assembly + courtID := mload(add(_extraData, 0x20)) + minJurors := mload(add(_extraData, 0x40)) + disputeKitID := mload(add(_extraData, 0x60)) + } + if (courtID == FORKING_COURT || courtID >= courts.length) { + courtID = GENERAL_COURT; + } + if (minJurors == 0) { + minJurors = DEFAULT_NB_OF_JURORS; + } + if (disputeKitID == NULL_DISPUTE_KIT || disputeKitID >= disputeKits.length) { + disputeKitID = DISPUTE_KIT_CLASSIC; // 0 index is not used. + } + } else { + courtID = GENERAL_COURT; + minJurors = DEFAULT_NB_OF_JURORS; + disputeKitID = DISPUTE_KIT_CLASSIC; + } + } + + // ************************************* // + // * Errors * // + // ************************************* // + + error GovernorOnly(); + error GuardianOrGovernorOnly(); + error DisputeKitOnly(); + error SortitionModuleOnly(); + error UnsuccessfulCall(); + error InvalidDisputKitParent(); + error MinStakeLowerThanParentCourt(); + error UnsupportedDisputeKit(); + error InvalidForkingCourtAsParent(); + error WrongDisputeKitIndex(); + error CannotDisableClassicDK(); + error StakingInTooManyCourts(); + error StakingNotPossibleInThisCourt(); + error StakingLessThanCourtMinStake(); + error StakingTransferFailed(); + error UnstakingTransferFailed(); + error ArbitrationFeesNotEnough(); + error DisputeKitNotSupportedByCourt(); + error MustSupportDisputeKitClassic(); + error TokenNotAccepted(); + error EvidenceNotPassedAndNotAppeal(); + error DisputeStillDrawing(); + error CommitPeriodNotPassed(); + error VotePeriodNotPassed(); + error AppealPeriodNotPassed(); + error NotEvidencePeriod(); + error AppealFeesNotEnough(); + error DisputeNotAppealable(); + error NotExecutionPeriod(); + error RulingAlreadyExecuted(); + error DisputePeriodIsFinal(); + error TransferFailed(); + error WhenNotPausedOnly(); + error WhenPausedOnly(); + error StakingZeroWhenNoStake(); +} diff --git a/contracts/src/arbitration/core-v2/KlerosCoreV2Neo.sol b/contracts/src/arbitration/core-v2/KlerosCoreV2Neo.sol new file mode 100644 index 000000000..15155224d --- /dev/null +++ b/contracts/src/arbitration/core-v2/KlerosCoreV2Neo.sol @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import "./KlerosCoreV2Base.sol"; +import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; + +/// @title KlerosCoreV2Neo +/// @notice KlerosCore implementation with new StakeController architecture for production environments +contract KlerosCoreV2Neo is KlerosCoreV2Base { + /// @notice Version of the implementation contract + string public constant override version = "0.0.1"; + + // ************************************* // + // * Storage * // + // ************************************* // + + mapping(address => bool) public arbitrableWhitelist; // Arbitrable whitelist. + IERC721 public jurorNft; // Eligible jurors NFT. + + // ************************************* // + // * Constructor * // + // ************************************* // + + /// @notice Constructor, initializing the implementation to reduce attack surface. + constructor() { + _disableInitializers(); + } + + /// @notice Initialization function for UUPS proxy + /// @param _governor The governor of the contract. + /// @param _guardian The guardian able to pause asset withdrawals. + /// @param _pinakion The Pinakion token contract. + /// @param _jurorProsecutionModule The module for juror's prosecution. + /// @param _disputeKit The dispute kit responsible for the dispute logic. + /// @param _hiddenVotes Whether to use commit and reveal or not. + /// @param _courtParameters [0]: minStake, [1]: alpha, [2]: feeForJuror, [3]: jurorsForCourtJump + /// @param _timesPerPeriod The timesPerPeriod array for courts + /// @param _sortitionExtraData Extra data for sortition module setup + /// @param _stakeController The stake controller for coordination + /// @param _jurorNft NFT contract to vet the jurors + function initialize( + address _governor, + address _guardian, + IERC20 _pinakion, + address _jurorProsecutionModule, + IDisputeKit _disputeKit, + bool _hiddenVotes, + uint256[4] memory _courtParameters, + uint256[4] memory _timesPerPeriod, + bytes memory _sortitionExtraData, + IStakeController _stakeController, + IERC721 _jurorNft + ) external reinitializer(2) { + __KlerosCoreV2Base_initialize( + _governor, + _guardian, + _pinakion, + _jurorProsecutionModule, + _disputeKit, + _hiddenVotes, + _courtParameters, + _timesPerPeriod, + _sortitionExtraData, + _stakeController + ); + jurorNft = _jurorNft; + } + + function initialize5() external reinitializer(5) { + // NOP + } + + // ************************************* // + // * Governance * // + // ************************************* // + + /// @notice Access Control to perform implementation upgrades (UUPS Proxiable) + /// Only the governor can perform upgrades (`onlyByGovernor`) + function _authorizeUpgrade(address) internal view override onlyByGovernor { + // Empty block: access control implemented by `onlyByGovernor` modifier + } + + /// @dev Changes the `jurorNft` storage variable. + /// @param _jurorNft The new value for the `jurorNft` storage variable. + function changeJurorNft(IERC721 _jurorNft) external onlyByGovernor { + jurorNft = _jurorNft; + } + + /// @dev Adds or removes an arbitrable from whitelist. + /// @param _arbitrable Arbitrable address. + /// @param _allowed Whether add or remove permission. + function changeArbitrableWhitelist(address _arbitrable, bool _allowed) external onlyByGovernor { + arbitrableWhitelist[_arbitrable] = _allowed; + } + + // ************************************* // + // * State Modifiers * // + // ************************************* // + + /// @dev Sets the caller's stake in a court. + /// Note: Staking and unstaking is forbidden during pause. + /// @param _courtID The ID of the court. + /// @param _newStake The new stake. + /// Note that the existing delayed stake will be nullified as non-relevant. + function setStake(uint96 _courtID, uint256 _newStake) external override whenNotPaused { + if (jurorNft.balanceOf(msg.sender) == 0) revert NotEligibleForStaking(); + _setStake(msg.sender, _courtID, _newStake, false, OnError.Revert); + } + + // ************************************* // + // * Internal * // + // ************************************* // + + function _createDispute( + uint256 _numberOfChoices, + bytes memory _extraData, + IERC20 _feeToken, + uint256 _feeAmount + ) internal override returns (uint256 disputeID) { + if (!arbitrableWhitelist[msg.sender]) revert ArbitrableNotWhitelisted(); + return super._createDispute(_numberOfChoices, _extraData, _feeToken, _feeAmount); + } + + function _stakingFailed(OnError _onError, StakingResult _result) internal pure override { + super._stakingFailed(_onError, _result); + if (_result == StakingResult.CannotStakeMoreThanMaxStakePerJuror) revert StakingMoreThanMaxStakePerJuror(); + if (_result == StakingResult.CannotStakeMoreThanMaxTotalStaked) revert StakingMoreThanMaxTotalStaked(); + } + + // ************************************* // + // * Errors * // + // ************************************* // + + error NotEligibleForStaking(); + error StakingMoreThanMaxStakePerJuror(); + error StakingMoreThanMaxTotalStaked(); + error ArbitrableNotWhitelisted(); +} diff --git a/contracts/src/arbitration/interfaces/IPNKVault.sol b/contracts/src/arbitration/interfaces/IPNKVault.sol new file mode 100644 index 000000000..fba1cc566 --- /dev/null +++ b/contracts/src/arbitration/interfaces/IPNKVault.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/// @title IPNKVault +/// @notice Interface for the PNK Vault that handles PNK deposits, withdrawals, locks, and penalties +interface IPNKVault { + // ************************************* // + // * Events * // + // ************************************* // + + event Deposit(address indexed account, uint256 amount); + event Withdraw(address indexed account, uint256 amount); + event Lock(address indexed account, uint256 amount); + event Unlock(address indexed account, uint256 amount); + event Penalty(address indexed account, uint256 amount); + event RewardTransferred(address indexed account, uint256 amount); + + // ************************************* // + // * State Modifiers * // + // ************************************* // + + /// @notice Deposit PNK and mint stPNK + /// @param _amount The amount of PNK to deposit + /// @return stPnkAmount The amount of stPNK minted + function deposit(uint256 _amount) external returns (uint256 stPnkAmount); + + /// @notice Withdraw PNK by burning stPNK + /// @param _amount The amount to withdraw + /// @return pnkAmount The amount of PNK withdrawn + function withdraw(uint256 _amount) external returns (uint256 pnkAmount); + + /// @notice Lock tokens for dispute participation (only StakeController) + /// @param _account The account to lock tokens for + /// @param _amount The amount to lock + function lockTokens(address _account, uint256 _amount) external; + + /// @notice Unlock tokens after dispute resolution (only StakeController) + /// @param _account The account to unlock tokens for + /// @param _amount The amount to unlock + function unlockTokens(address _account, uint256 _amount) external; + + /// @notice Apply penalty by reducing deposited balance (only StakeController) + /// @param _account The account to penalize + /// @param _amount The penalty amount + /// @return actualPenalty The actual penalty applied + function applyPenalty(address _account, uint256 _amount) external returns (uint256 actualPenalty); + + /// @notice Transfer PNK rewards directly to account (only KlerosCore) + /// @param _account The account to receive rewards + /// @param _amount The reward amount + function transferReward(address _account, uint256 _amount) external; + + // ************************************* // + // * Public Views * // + // ************************************* // + + /// @notice Get available balance for withdrawal + /// @param _account The account to check + /// @return The available balance + function getAvailableBalance(address _account) external view returns (uint256); + + /// @notice Get total deposited balance + /// @param _account The account to check + /// @return The deposited balance + function getDepositedBalance(address _account) external view returns (uint256); + + /// @notice Get locked balance + /// @param _account The account to check + /// @return The locked balance + function getLockedBalance(address _account) external view returns (uint256); + + /// @notice Get stPNK balance (same as deposited - penalties) + /// @param _account The account to check + /// @return The stPNK balance + function getStPNKBalance(address _account) external view returns (uint256); + + /// @notice Get penalty balance + /// @param _account The account to check + /// @return The penalty balance + function getPenaltyBalance(address _account) external view returns (uint256); +} diff --git a/contracts/src/arbitration/interfaces/ISortitionModuleV2.sol b/contracts/src/arbitration/interfaces/ISortitionModuleV2.sol new file mode 100644 index 000000000..99f251f9d --- /dev/null +++ b/contracts/src/arbitration/interfaces/ISortitionModuleV2.sol @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import "../../libraries/Constants.sol"; + +/// @title ISortitionModuleV2 +/// @notice Interface for pure sortition operations without phase management or token operations +/// @dev This interface contains only tree management and drawing logic +interface ISortitionModuleV2 { + // ************************************* // + // * Events * // + // ************************************* // + + /// @notice Emitted when a juror's stake is set in a court + /// @param _address The address of the juror + /// @param _courtID The ID of the court + /// @param _amount The amount of tokens staked in the court + /// @param _amountAllCourts The amount of tokens staked in all courts + event StakeSet(address indexed _address, uint256 _courtID, uint256 _amount, uint256 _amountAllCourts); + + // ************************************* // + // * Tree Management * // + // ************************************* // + + /// @notice Create a sortition sum tree + /// @param _key The key of the new tree + /// @param _extraData Extra data that contains the number of children each node in the tree should have + function createTree(bytes32 _key, bytes memory _extraData) external; + + /// @notice Set a juror's stake in a court (pure sortition tree operation) + /// @param _account The address of the juror + /// @param _courtID The ID of the court + /// @param _newStake The new stake amount + /// @return success Whether the operation was successful + function setStake(address _account, uint96 _courtID, uint256 _newStake) external returns (bool success); + + // ************************************* // + // * Drawing * // + // ************************************* // + + /// @notice Draw a juror from a court's sortition tree + /// @param _court The court identifier + /// @param _coreDisputeID Index of the dispute in Kleros Core + /// @param _nonce Nonce to hash with random number + /// @param _randomNumber The random number to use for drawing + /// @return drawnAddress The drawn juror address + function draw( + bytes32 _court, + uint256 _coreDisputeID, + uint256 _nonce, + uint256 _randomNumber + ) external view returns (address drawnAddress); + + // ************************************* // + // * View Functions * // + // ************************************* // + + /// @notice Get the stake of a juror in a court + /// @param _juror The address of the juror + /// @param _courtID The ID of the court + /// @return value The stake of the juror in the court + function stakeOf(address _juror, uint96 _courtID) external view returns (uint256 value); + + /// @notice Get the stake of a juror in a court by tree key and stake path ID + /// @param _key The key of the tree, corresponding to a court + /// @param _ID The stake path ID, corresponding to a juror + /// @return value The stake of the juror in the court + function stakeOf(bytes32 _key, bytes32 _ID) external view returns (uint256 value); + + /// @notice Get juror information for a specific court + /// @param _juror The juror address + /// @param _courtID The court ID + /// @return totalStaked Total staked amount (from external source) + /// @return totalLocked Total locked amount (from external source) + /// @return stakedInCourt Amount staked in specific court + /// @return nbCourts Number of courts staked in + function getJurorInfo( + address _juror, + uint96 _courtID + ) external view returns (uint256 totalStaked, uint256 totalLocked, uint256 stakedInCourt, uint256 nbCourts); + + /// @notice Get court IDs where juror has stakes + /// @param _juror The juror address + /// @return Array of court IDs + function getJurorCourtIDs(address _juror) external view returns (uint96[] memory); + + /// @notice Check if juror has any stakes in sortition trees + /// @param _juror The juror address + /// @return Whether the juror has stakes + function hasStakes(address _juror) external view returns (bool); + + /// @notice Get the total stake in a court's tree + /// @param _courtID The court ID + /// @return Total stake in the court + function getTotalStakeInCourt(uint96 _courtID) external view returns (uint256); + + // ************************************* // + // * Utility Functions * // + // ************************************* // + + /// @notice Convert account and court ID to stake path ID + /// @param _account The juror address + /// @param _courtID The court ID + /// @return stakePathID The generated stake path ID + function accountAndCourtIDToStakePathID( + address _account, + uint96 _courtID + ) external pure returns (bytes32 stakePathID); + + /// @notice Convert stake path ID back to account address + /// @param _stakePathID The stake path ID + /// @return account The account address + function stakePathIDToAccount(bytes32 _stakePathID) external pure returns (address account); +} diff --git a/contracts/src/arbitration/interfaces/IStakeController.sol b/contracts/src/arbitration/interfaces/IStakeController.sol new file mode 100644 index 000000000..ab1a3b240 --- /dev/null +++ b/contracts/src/arbitration/interfaces/IStakeController.sol @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {ISortitionModule} from "./ISortitionModule.sol"; +import "../../libraries/Constants.sol"; + +/// @title IStakeController +/// @notice Interface for the Stake Controller that coordinates between PNKVault and SortitionModule +/// @dev Combines phase management, delayed stakes, and coordination between vault and sortition +interface IStakeController { + // ************************************* // + // * Events * // + // ************************************* // + + event NewPhase(ISortitionModule.Phase phase); + event JurorPenaltyExecuted(address indexed account, uint256 requestedPenalty, uint256 actualPenalty); + event StakeUnlocked(address indexed account, uint256 amount); + event JurorSetInactive(address indexed account); + + // Migration events + event StakeImported(address indexed account, uint96 indexed courtID, uint256 stake); + event DelayedStakeImported(address indexed account, uint96 indexed courtID, uint256 stake, uint256 index); + event MigrationCompleted(uint256 totalAttempted, uint256 totalImported); + event PhaseStateMigrated(ISortitionModule.Phase phase, uint256 lastPhaseChange, uint256 disputesWithoutJurors); + event EmergencyReset(uint256 timestamp); + + // ************************************* // + // * Phase Management * // + // ************************************* // + + /// @notice Pass to the next phase + function passPhase() external; + + /// @notice Get the current phase + /// @return The current phase + function getPhase() external view returns (ISortitionModule.Phase); + + /// @notice Execute delayed stakes during staking phase + /// @param _iterations The number of delayed stakes to execute + function executeDelayedStakes(uint256 _iterations) external; + + // ************************************* // + // * Stake Management * // + // ************************************* // + + /// @notice Set stake for a juror with vault coordination + /// @param _account The address of the juror + /// @param _courtID The ID of the court + /// @param _newStake The new stake amount + /// @param _alreadyTransferred Whether the tokens were already transferred + /// @return pnkDeposit The amount of PNK to deposit + /// @return pnkWithdrawal The amount of PNK to withdraw + /// @return stakingResult The result of the staking operation + function setStake( + address _account, + uint96 _courtID, + uint256 _newStake, + bool _alreadyTransferred + ) external returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult); + + /// @notice Lock stake for dispute participation + /// @param _account The account to lock stake for + /// @param _amount The amount to lock + function lockStake(address _account, uint256 _amount) external; + + /// @notice Unlock stake after dispute resolution + /// @param _account The account to unlock stake for + /// @param _amount The amount to unlock + function unlockStake(address _account, uint256 _amount) external; + + /// @notice Execute penalty on juror through vault coordination + /// @param _account The account to penalize + /// @param _penalty The penalty amount + /// @param _totalStake The total stake amount (for inactivity check) + /// @return actualPenalty The actual penalty applied + function executeJurorPenalty( + address _account, + uint256 _penalty, + uint256 _totalStake + ) external returns (uint256 actualPenalty); + + /// @notice Set juror as inactive and remove from all sortition trees + /// @param _account The juror to set inactive + function setJurorInactive(address _account) external; + + /// @notice Check if a juror should be set inactive after penalty + /// @param _account The juror account + /// @param _disputeID The dispute ID + /// @param _round The round number + /// @param _repartition The repartition index + /// @return shouldSet Whether the juror should be set inactive + function shouldSetJurorInactive( + address _account, + uint256 _disputeID, + uint256 _round, + uint256 _repartition + ) external view returns (bool shouldSet); + + // ************************************* // + // * Sortition Delegation * // + // ************************************* // + + /// @notice Create a sortition tree (delegated to SortitionModule) + /// @param _key The key of the tree + /// @param _extraData Extra data for tree configuration + function createTree(bytes32 _key, bytes memory _extraData) external; + + /// @notice Draw a juror for dispute (delegated to SortitionModule) + /// @param _court The court identifier + /// @param _coreDisputeID The core dispute ID + /// @param _nonce The drawing nonce + /// @return The drawn juror address + function draw(bytes32 _court, uint256 _coreDisputeID, uint256 _nonce) external view returns (address); + + /// @notice Create dispute hook (delegated to SortitionModule) + /// @param _disputeID The dispute ID + /// @param _roundID The round ID + function createDisputeHook(uint256 _disputeID, uint256 _roundID) external; + + /// @notice Post draw hook (delegated to SortitionModule) + /// @param _disputeID The dispute ID + /// @param _roundID The round ID + function postDrawHook(uint256 _disputeID, uint256 _roundID) external; + + /// @notice Notify random number (delegated to SortitionModule) + /// @param _drawnNumber The random number + function notifyRandomNumber(uint256 _drawnNumber) external; + + // ************************************* // + // * View Functions * // + // ************************************* // + + /// @notice Get juror balance information + /// @param _juror The juror address + /// @param _courtID The court ID + /// @return totalStaked Total staked amount + /// @return totalLocked Total locked amount + /// @return stakedInCourt Amount staked in specific court + /// @return nbCourts Number of courts staked in + function getJurorBalance( + address _juror, + uint96 _courtID + ) external view returns (uint256 totalStaked, uint256 totalLocked, uint256 stakedInCourt, uint256 nbCourts); + + /// @notice Get court IDs where juror has stakes + /// @param _juror The juror address + /// @return Array of court IDs + function getJurorCourtIDs(address _juror) external view returns (uint96[] memory); + + /// @notice Check if juror is staked + /// @param _juror The juror address + /// @return Whether the juror is staked + function isJurorStaked(address _juror) external view returns (bool); + + /// @notice Get available balance from vault + /// @param _account The account to check + /// @return The available balance + function getAvailableBalance(address _account) external view returns (uint256); + + /// @notice Get deposited balance from vault + /// @param _account The account to check + /// @return The deposited balance + function getDepositedBalance(address _account) external view returns (uint256); +} diff --git a/contracts/src/arbitration/sortition-v2/SortitionModuleV2.sol b/contracts/src/arbitration/sortition-v2/SortitionModuleV2.sol new file mode 100644 index 000000000..f5ad9178d --- /dev/null +++ b/contracts/src/arbitration/sortition-v2/SortitionModuleV2.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {SortitionModuleV2Base} from "./SortitionModuleV2Base.sol"; +import {IStakeController} from "../interfaces/IStakeController.sol"; +import {KlerosCore} from "../KlerosCore.sol"; + +/// @title SortitionModuleV2 +/// @notice Basic implementation of the pure sortition module +/// @dev Contains only tree management and drawing logic, no phase management or token operations +contract SortitionModuleV2 is SortitionModuleV2Base { + string public constant override version = "2.0.0"; + + // ************************************* // + // * Constructor * // + // ************************************* // + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /// @dev Initializer (constructor equivalent for upgradable contracts). + /// @param _governor The governor's address. + /// @param _core The KlerosCore contract. + /// @param _stakeController The StakeController contract. + function initialize(address _governor, KlerosCore _core, IStakeController _stakeController) external initializer { + __SortitionModuleV2Base_initialize(_governor, _core, _stakeController); + } + + // ************************************* // + // * Governance * // + // ************************************* // + + /// @dev Access Control to perform implementation upgrades (UUPS Proxiable) + /// Only the governor can perform upgrades (`onlyByGovernor`) + function _authorizeUpgrade(address) internal view override onlyByGovernor { + // NOP + } +} diff --git a/contracts/src/arbitration/sortition-v2/SortitionModuleV2Base.sol b/contracts/src/arbitration/sortition-v2/SortitionModuleV2Base.sol new file mode 100644 index 000000000..d4483d703 --- /dev/null +++ b/contracts/src/arbitration/sortition-v2/SortitionModuleV2Base.sol @@ -0,0 +1,449 @@ +// SPDX-License-Identifier: MIT + +/** + * @custom:authors: [@epiqueras, @unknownunknown1, @jaybuidl, @shotaronowhere] + * @custom:reviewers: [] + * @custom:auditors: [] + * @custom:bounties: [] + * @custom:deployments: [] + */ + +pragma solidity 0.8.24; + +import {ISortitionModuleV2} from "../interfaces/ISortitionModuleV2.sol"; +import {IStakeController} from "../interfaces/IStakeController.sol"; +import {KlerosCore} from "../KlerosCore.sol"; +import {Initializable} from "../../proxy/Initializable.sol"; +import {UUPSProxiable} from "../../proxy/UUPSProxiable.sol"; +import "../../libraries/Constants.sol"; + +/// @title SortitionModuleV2Base +/// @notice Abstract base contract for pure sortition operations +/// @dev Contains only tree management and drawing logic, no phase management or token operations +abstract contract SortitionModuleV2Base is ISortitionModuleV2, Initializable, UUPSProxiable { + // ************************************* // + // * Enums / Structs * // + // ************************************* // + + struct SortitionSumTree { + uint256 K; // The maximum number of children per node. + // We use this to keep track of vacant positions in the tree after removing a leaf. This is for keeping the tree as balanced as possible without spending gas on moving nodes around. + uint256[] stack; + uint256[] nodes; + // Two-way mapping of IDs to node indexes. Note that node index 0 is reserved for the root node, and means the ID does not have a node. + mapping(bytes32 => uint256) IDsToNodeIndexes; + mapping(uint256 => bytes32) nodeIndexesToIDs; + } + + struct JurorStakeInfo { + uint96[] courtIDs; // The IDs of courts where the juror has stakes + mapping(uint96 => uint256) stakes; // Court ID -> stake amount + } + + // ************************************* // + // * Storage * // + // ************************************* // + + address public governor; // The governor of the contract. + KlerosCore public core; // The core arbitrator contract. + IStakeController public stakeController; // The stake controller for coordination. + + mapping(bytes32 treeHash => SortitionSumTree) internal sortitionSumTrees; // The mapping trees by keys. + mapping(address account => JurorStakeInfo) internal jurorStakeInfo; // Juror stake information for sortition + + // ************************************* // + // * Function Modifiers * // + // ************************************* // + + modifier onlyByGovernor() { + if (governor != msg.sender) revert GovernorOnly(); + _; + } + + modifier onlyByStakeController() { + if (address(stakeController) != msg.sender) revert StakeControllerOnly(); + _; + } + + modifier onlyByCoreOrController() { + if (address(core) != msg.sender && address(stakeController) != msg.sender) revert CoreOrStakeControllerOnly(); + _; + } + + // ************************************* // + // * Constructor * // + // ************************************* // + + function __SortitionModuleV2Base_initialize( + address _governor, + KlerosCore _core, + IStakeController _stakeController + ) internal onlyInitializing { + governor = _governor; + core = _core; + stakeController = _stakeController; + } + + // ************************************* // + // * Governance * // + // ************************************* // + + /// @dev Changes the governor of the contract. + /// @param _governor The new governor. + function changeGovernor(address _governor) external onlyByGovernor { + governor = _governor; + } + + /// @dev Changes the `stakeController` storage variable. + /// @param _stakeController The new stake controller address. + function changeStakeController(IStakeController _stakeController) external onlyByGovernor { + stakeController = _stakeController; + } + + // ************************************* // + // * Tree Management * // + // ************************************* // + + /// @inheritdoc ISortitionModuleV2 + function createTree(bytes32 _key, bytes memory _extraData) external override onlyByCoreOrController { + SortitionSumTree storage tree = sortitionSumTrees[_key]; + uint256 K = _extraDataToTreeK(_extraData); + if (tree.K != 0) revert TreeAlreadyExists(); + if (K <= 1) revert InvalidTreeK(); + tree.K = K; + tree.nodes.push(0); + } + + /// @inheritdoc ISortitionModuleV2 + function setStake( + address _account, + uint96 _courtID, + uint256 _newStake + ) external virtual override onlyByStakeController returns (bool success) { + return _setStake(_account, _courtID, _newStake); + } + + // ************************************* // + // * Drawing * // + // ************************************* // + + /// @inheritdoc ISortitionModuleV2 + function draw( + bytes32 _court, + uint256 _coreDisputeID, + uint256 _nonce, + uint256 _randomNumber + ) external view virtual override returns (address drawnAddress) { + SortitionSumTree storage tree = sortitionSumTrees[_court]; + + if (tree.nodes.length == 0 || tree.nodes[0] == 0) { + return address(0); // No jurors staked. + } + + uint256 currentDrawnNumber = uint256(keccak256(abi.encodePacked(_randomNumber, _coreDisputeID, _nonce))) % + tree.nodes[0]; + + // While it still has children + uint256 treeIndex = 0; + while ((tree.K * treeIndex) + 1 < tree.nodes.length) { + for (uint256 i = 1; i <= tree.K; i++) { + // Loop over children. + uint256 nodeIndex = (tree.K * treeIndex) + i; + uint256 nodeValue = tree.nodes[nodeIndex]; + + if (currentDrawnNumber >= nodeValue) { + // Go to the next child. + currentDrawnNumber -= nodeValue; + } else { + // Pick this child. + treeIndex = nodeIndex; + break; + } + } + } + drawnAddress = _stakePathIDToAccount(tree.nodeIndexesToIDs[treeIndex]); + } + + // ************************************* // + // * View Functions * // + // ************************************* // + + /// @inheritdoc ISortitionModuleV2 + function stakeOf(address _juror, uint96 _courtID) external view override returns (uint256 value) { + bytes32 stakePathID = _accountAndCourtIDToStakePathID(_juror, _courtID); + return stakeOf(bytes32(uint256(_courtID)), stakePathID); + } + + /// @inheritdoc ISortitionModuleV2 + function stakeOf(bytes32 _key, bytes32 _ID) public view override returns (uint256) { + SortitionSumTree storage tree = sortitionSumTrees[_key]; + uint treeIndex = tree.IDsToNodeIndexes[_ID]; + if (treeIndex == 0) { + return 0; + } + return tree.nodes[treeIndex]; + } + + /// @inheritdoc ISortitionModuleV2 + function getJurorInfo( + address _juror, + uint96 _courtID + ) + external + view + override + returns (uint256 totalStaked, uint256 totalLocked, uint256 stakedInCourt, uint256 nbCourts) + { + JurorStakeInfo storage info = jurorStakeInfo[_juror]; + + // Get total staked and locked from stake controller + totalStaked = stakeController.getDepositedBalance(_juror); + totalLocked = 0; // Will be retrieved from vault through stake controller if needed + + // Get stake in specific court from sortition tree + stakedInCourt = this.stakeOf(_juror, _courtID); + + // Number of courts from local tracking + nbCourts = info.courtIDs.length; + } + + /// @inheritdoc ISortitionModuleV2 + function getJurorCourtIDs(address _juror) external view override returns (uint96[] memory) { + return jurorStakeInfo[_juror].courtIDs; + } + + /// @inheritdoc ISortitionModuleV2 + function hasStakes(address _juror) external view override returns (bool) { + return jurorStakeInfo[_juror].courtIDs.length > 0; + } + + /// @inheritdoc ISortitionModuleV2 + function getTotalStakeInCourt(uint96 _courtID) external view override returns (uint256) { + SortitionSumTree storage tree = sortitionSumTrees[bytes32(uint256(_courtID))]; + if (tree.nodes.length == 0) return 0; + return tree.nodes[0]; // Root node contains total stake + } + + /// @inheritdoc ISortitionModuleV2 + function accountAndCourtIDToStakePathID( + address _account, + uint96 _courtID + ) external pure override returns (bytes32 stakePathID) { + return _accountAndCourtIDToStakePathID(_account, _courtID); + } + + /// @inheritdoc ISortitionModuleV2 + function stakePathIDToAccount(bytes32 _stakePathID) external pure override returns (address account) { + return _stakePathIDToAccount(_stakePathID); + } + + // ************************************* // + // * Internal * // + // ************************************* // + + /// @dev Internal implementation of setStake with court hierarchy updates + function _setStake(address _account, uint96 _courtID, uint256 _newStake) internal returns (bool success) { + uint256 currentStake = this.stakeOf(_account, _courtID); + + if (currentStake == 0 && _newStake == 0) { + return false; // No change needed + } + + JurorStakeInfo storage info = jurorStakeInfo[_account]; + uint256 nbCourts = info.courtIDs.length; + + if (currentStake == 0 && nbCourts >= MAX_STAKE_PATHS) { + return false; // Prevent staking beyond MAX_STAKE_PATHS + } + + // Update the sortition sum tree in court hierarchy + bytes32 stakePathID = _accountAndCourtIDToStakePathID(_account, _courtID); + bool finished = false; + uint96 currentCourtID = _courtID; + + while (!finished) { + // Tokens are also implicitly staked in parent courts through sortition module to increase the chance of being drawn. + _set(bytes32(uint256(currentCourtID)), _newStake, stakePathID); + if (currentCourtID == GENERAL_COURT) { + finished = true; + } else { + (currentCourtID, , , , , , ) = core.courts(currentCourtID); // Get the parent court. + } + } + + // Update local stake tracking (inlined from _updateJurorStakeInfo) + uint256 currentStakeInInfo = info.stakes[_courtID]; + + if (currentStakeInInfo == 0 && _newStake > 0) { + // Adding new court + info.courtIDs.push(_courtID); + } else if (currentStakeInInfo > 0 && _newStake == 0) { + // Removing court + for (uint256 i = 0; i < info.courtIDs.length; i++) { + if (info.courtIDs[i] == _courtID) { + info.courtIDs[i] = info.courtIDs[info.courtIDs.length - 1]; + info.courtIDs.pop(); + break; + } + } + } + + info.stakes[_courtID] = _newStake; + + // Get total staked amount from stake controller for event + uint256 totalStaked = stakeController.getDepositedBalance(_account); + emit StakeSet(_account, _courtID, _newStake, totalStaked); + + return true; + } + + /// @dev Set a value in a tree. + function _set(bytes32 _key, uint256 _value, bytes32 _ID) internal { + SortitionSumTree storage tree = sortitionSumTrees[_key]; + uint256 treeIndex = tree.IDsToNodeIndexes[_ID]; + + if (treeIndex == 0) { + // No existing node. + if (_value != 0) { + // Non zero value. + // Append. + // Add node. + if (tree.stack.length == 0) { + // No vacant spots. + // Get the index and append the value. + treeIndex = tree.nodes.length; + tree.nodes.push(_value); + + // Potentially append a new node and make the parent a sum node. + if (treeIndex != 1 && (treeIndex - 1) % tree.K == 0) { + // Is first child. + uint256 parentIndex = treeIndex / tree.K; + bytes32 parentID = tree.nodeIndexesToIDs[parentIndex]; + uint256 newIndex = treeIndex + 1; + tree.nodes.push(tree.nodes[parentIndex]); + delete tree.nodeIndexesToIDs[parentIndex]; + tree.IDsToNodeIndexes[parentID] = newIndex; + tree.nodeIndexesToIDs[newIndex] = parentID; + } + } else { + // Some vacant spot. + // Pop the stack and append the value. + treeIndex = tree.stack[tree.stack.length - 1]; + tree.stack.pop(); + tree.nodes[treeIndex] = _value; + } + + // Add label. + tree.IDsToNodeIndexes[_ID] = treeIndex; + tree.nodeIndexesToIDs[treeIndex] = _ID; + + _updateParents(_key, treeIndex, true, _value); + } + } else { + // Existing node. + if (_value == 0) { + // Zero value. + // Remove. + // Remember value and set to 0. + uint256 value = tree.nodes[treeIndex]; + tree.nodes[treeIndex] = 0; + + // Push to stack. + tree.stack.push(treeIndex); + + // Clear label. + delete tree.IDsToNodeIndexes[_ID]; + delete tree.nodeIndexesToIDs[treeIndex]; + + _updateParents(_key, treeIndex, false, value); + } else if (_value != tree.nodes[treeIndex]) { + // New, non zero value. + // Set. + bool plusOrMinus = tree.nodes[treeIndex] <= _value; + uint256 plusOrMinusValue = plusOrMinus + ? _value - tree.nodes[treeIndex] + : tree.nodes[treeIndex] - _value; + tree.nodes[treeIndex] = _value; + + _updateParents(_key, treeIndex, plusOrMinus, plusOrMinusValue); + } + } + } + + /// @dev Update all the parents of a node. + function _updateParents(bytes32 _key, uint256 _treeIndex, bool _plusOrMinus, uint256 _value) internal { + SortitionSumTree storage tree = sortitionSumTrees[_key]; + + uint256 parentIndex = _treeIndex; + while (parentIndex != 0) { + parentIndex = (parentIndex - 1) / tree.K; + tree.nodes[parentIndex] = _plusOrMinus + ? tree.nodes[parentIndex] + _value + : tree.nodes[parentIndex] - _value; + } + } + + /// @dev Retrieves a juror's address from the stake path ID. + function _stakePathIDToAccount(bytes32 _stakePathID) internal pure returns (address account) { + assembly { + // solium-disable-line security/no-inline-assembly + let ptr := mload(0x40) + for { + let i := 0x00 + } lt(i, 0x14) { + i := add(i, 0x01) + } { + mstore8(add(add(ptr, 0x0c), i), byte(i, _stakePathID)) + } + account := mload(ptr) + } + } + + /// @dev Extract tree K parameter from extra data + function _extraDataToTreeK(bytes memory _extraData) internal pure returns (uint256 K) { + if (_extraData.length >= 32) { + assembly { + // solium-disable-line security/no-inline-assembly + K := mload(add(_extraData, 0x20)) + } + } else { + K = DEFAULT_K; + } + } + + /// @dev Packs an account and a court ID into a stake path ID. + function _accountAndCourtIDToStakePathID( + address _account, + uint96 _courtID + ) internal pure returns (bytes32 stakePathID) { + assembly { + // solium-disable-line security/no-inline-assembly + let ptr := mload(0x40) + for { + let i := 0x00 + } lt(i, 0x14) { + i := add(i, 0x01) + } { + mstore8(add(ptr, i), byte(add(0x0c, i), _account)) + } + for { + let i := 0x14 + } lt(i, 0x20) { + i := add(i, 0x01) + } { + mstore8(add(ptr, i), byte(i, _courtID)) + } + stakePathID := mload(ptr) + } + } + + // ************************************* // + // * Errors * // + // ************************************* // + + error GovernorOnly(); + error StakeControllerOnly(); + error CoreOrStakeControllerOnly(); + error TreeAlreadyExists(); + error InvalidTreeK(); +} diff --git a/contracts/src/arbitration/sortition-v2/SortitionModuleV2Neo.sol b/contracts/src/arbitration/sortition-v2/SortitionModuleV2Neo.sol new file mode 100644 index 000000000..9128a4753 --- /dev/null +++ b/contracts/src/arbitration/sortition-v2/SortitionModuleV2Neo.sol @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {SortitionModuleV2Base} from "./SortitionModuleV2Base.sol"; +import {IStakeController} from "../interfaces/IStakeController.sol"; +import {KlerosCore} from "../KlerosCore.sol"; + +/// @title SortitionModuleV2Neo +/// @notice Enhanced implementation of the pure sortition module with stake limits +/// @dev Contains tree management and drawing logic with additional sortition constraints +contract SortitionModuleV2Neo is SortitionModuleV2Base { + string public constant override version = "2.1.0"; + + // ************************************* // + // * Storage * // + // ************************************* // + + uint256 public maxStakePerJuror; // Maximum stake amount per juror in any court + uint256 public maxTotalStakedInCourt; // Maximum total stake allowed per court + + // ************************************* // + // * Events * // + // ************************************* // + + /// @notice Emitted when stake limits are updated + /// @param maxStakePerJuror New maximum stake per juror + /// @param maxTotalStakedInCourt New maximum total stake per court + event StakeLimitsUpdated(uint256 maxStakePerJuror, uint256 maxTotalStakedInCourt); + + // ************************************* // + // * Constructor * // + // ************************************* // + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /// @dev Initializer (constructor equivalent for upgradable contracts). + /// @param _governor The governor's address. + /// @param _core The KlerosCore contract. + /// @param _stakeController The StakeController contract. + /// @param _maxStakePerJuror Maximum stake amount per juror. + /// @param _maxTotalStakedInCourt Maximum total stake per court. + function initialize( + address _governor, + KlerosCore _core, + IStakeController _stakeController, + uint256 _maxStakePerJuror, + uint256 _maxTotalStakedInCourt + ) external initializer { + __SortitionModuleV2Base_initialize(_governor, _core, _stakeController); + + maxStakePerJuror = _maxStakePerJuror; + maxTotalStakedInCourt = _maxTotalStakedInCourt; + + emit StakeLimitsUpdated(_maxStakePerJuror, _maxTotalStakedInCourt); + } + + // ************************************* // + // * Governance * // + // ************************************* // + + /// @dev Access Control to perform implementation upgrades (UUPS Proxiable) + /// Only the governor can perform upgrades (`onlyByGovernor`) + function _authorizeUpgrade(address) internal view override onlyByGovernor { + // NOP + } + + /// @dev Change the maximum stake per juror + /// @param _maxStakePerJuror The new maximum stake per juror + function changeMaxStakePerJuror(uint256 _maxStakePerJuror) external onlyByGovernor { + maxStakePerJuror = _maxStakePerJuror; + emit StakeLimitsUpdated(_maxStakePerJuror, maxTotalStakedInCourt); + } + + /// @dev Change the maximum total stake per court + /// @param _maxTotalStakedInCourt The new maximum total stake per court + function changeMaxTotalStakedInCourt(uint256 _maxTotalStakedInCourt) external onlyByGovernor { + maxTotalStakedInCourt = _maxTotalStakedInCourt; + emit StakeLimitsUpdated(maxStakePerJuror, _maxTotalStakedInCourt); + } + + // ************************************* // + // * Enhanced Sortition * // + // ************************************* // + + /// @dev Enhanced setStake with validation of stake limits + /// @param _account The juror address + /// @param _courtID The court ID + /// @param _newStake The new stake amount + /// @return success Whether the operation was successful + function setStake( + address _account, + uint96 _courtID, + uint256 _newStake + ) external override onlyByStakeController returns (bool success) { + // Validate stake limits before setting + if (_newStake > 0) { + // Check maximum stake per juror + if (_newStake > maxStakePerJuror) { + return false; // Above maximum stake per juror + } + + // Check maximum total stake in court + uint256 currentTotalStake = this.getTotalStakeInCourt(_courtID); + uint256 currentJurorStake = this.stakeOf(_account, _courtID); + uint256 stakeIncrease = _newStake > currentJurorStake ? _newStake - currentJurorStake : 0; + + if (currentTotalStake + stakeIncrease > maxTotalStakedInCourt) { + return false; // Would exceed court total stake limit + } + } + + // If all validations pass, use the base implementation + return _setStake(_account, _courtID, _newStake); + } +} diff --git a/contracts/src/arbitration/stPNK.sol b/contracts/src/arbitration/stPNK.sol new file mode 100644 index 000000000..1940af155 --- /dev/null +++ b/contracts/src/arbitration/stPNK.sol @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +/// @title stPNK +/// @notice Non-transferable liquid staking token representing staked PNK +/// @dev Only transferable within Kleros protocol contracts to prevent external trading +/// @custom:security-contact contact@kleros.io +contract stPNK is ERC20 { + // ************************************* // + // * Storage * // + // ************************************* // + + address public immutable vault; + + // Whitelist of protocol contracts that can receive stPNK + mapping(address => bool) public protocolContracts; + + // Track if an address is a regular user (not a protocol contract) + mapping(address => bool) public isUser; + + // ************************************* // + // * Events * // + // ************************************* // + + event ProtocolContractUpdated(address indexed contract_, bool allowed); + + // ************************************* // + // * Errors * // + // ************************************* // + + error TransferNotAllowed(); + error OnlyVault(); + error OnlyProtocolContracts(); + error ArrayLengthMismatch(); + + // ************************************* // + // * Function Modifiers * // + // ************************************* // + + modifier onlyVault() { + if (msg.sender != vault) revert OnlyVault(); + _; + } + + modifier onlyProtocolContracts() { + if (!protocolContracts[msg.sender]) revert OnlyProtocolContracts(); + _; + } + + // ************************************* // + // * Constructor * // + // ************************************* // + + constructor(address _vault) ERC20("Staked Pinakion", "stPNK") { + vault = _vault; + + // Automatically whitelist vault + protocolContracts[_vault] = true; + } + + // ************************************* // + // * State Modifiers * // + // ************************************* // + + /// @notice Mint stPNK tokens (only vault) + /// @param _to The address to mint tokens to + /// @param _amount The amount of tokens to mint + function mint(address _to, uint256 _amount) external onlyVault { + isUser[_to] = true; + _mint(_to, _amount); + } + + /// @notice Burn stPNK tokens from account (only vault) + /// @param _from The address to burn tokens from + /// @param _amount The amount of tokens to burn + function burnFrom(address _from, uint256 _amount) external onlyVault { + _burn(_from, _amount); + } + + /// @notice Update protocol contract whitelist for multiple contracts (only via governance) + /// @param _contracts Array of contract addresses to update + /// @param _allowed Array of boolean values for each contract + function setProtocolContracts( + address[] calldata _contracts, + bool[] calldata _allowed + ) external onlyProtocolContracts { + if (_contracts.length != _allowed.length) revert ArrayLengthMismatch(); + + for (uint256 i = 0; i < _contracts.length; i++) { + protocolContracts[_contracts[i]] = _allowed[i]; + emit ProtocolContractUpdated(_contracts[i], _allowed[i]); + } + } + + // ************************************* // + // * Public Views * // + // ************************************* // + + /// @notice Check if transfer is allowed (view function for frontends) + /// @param _from The sender address + /// @param _to The recipient address + /// @return Whether the transfer is allowed + function isTransferAllowed(address _from, address _to) external view returns (bool) { + if (_from == address(0) || _to == address(0)) return true; + if (protocolContracts[_from] && protocolContracts[_to]) return true; + if (protocolContracts[_from] && !protocolContracts[_to]) return true; + if (!protocolContracts[_from] && protocolContracts[_to]) return true; + return false; + } + + // ************************************* // + // * Internal * // + // ************************************* // + + /// @notice Override update to prevent external trading + /// @param _from The sender address + /// @param _to The recipient address + /// @param _value The amount to transfer + function _update(address _from, address _to, uint256 _value) internal override { + // Allow minting (_from == address(0)) and burning (_to == address(0)) + if (_from == address(0) || _to == address(0)) { + super._update(_from, _to, _value); + return; + } + + // Allow transfers between protocol contracts + if (protocolContracts[_from] && protocolContracts[_to]) { + super._update(_from, _to, _value); + return; + } + + // Allow transfers from protocol contracts to users (e.g., rewards) + if (protocolContracts[_from] && !protocolContracts[_to]) { + isUser[_to] = true; + super._update(_from, _to, _value); + return; + } + + // Allow transfers from users to protocol contracts (e.g., for operations) + if (!protocolContracts[_from] && protocolContracts[_to]) { + super._update(_from, _to, _value); + return; + } + + // Block all other transfers (user-to-user, external contracts) + revert TransferNotAllowed(); + } +} diff --git a/contracts/src/arbitration/stake-controller/StakeController.sol b/contracts/src/arbitration/stake-controller/StakeController.sol new file mode 100644 index 000000000..1bbaaa0de --- /dev/null +++ b/contracts/src/arbitration/stake-controller/StakeController.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {StakeControllerBase} from "./StakeControllerBase.sol"; +import {IPNKVault} from "../interfaces/IPNKVault.sol"; +import {ISortitionModule} from "../interfaces/ISortitionModule.sol"; +import {KlerosCore} from "../KlerosCore.sol"; +import {RNG} from "../../rng/RNG.sol"; + +/// @title StakeController +/// @notice Basic implementation of the Stake Controller +/// @dev Coordinates between PNKVault and SortitionModule for the new architecture +contract StakeController is StakeControllerBase { + string public constant override version = "1.0.0"; + + // ************************************* // + // * Constructor * // + // ************************************* // + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /// @dev Initializer (constructor equivalent for upgradable contracts). + /// @param _governor The governor's address. + /// @param _core The KlerosCore contract. + /// @param _vault The PNKVault contract. + /// @param _sortitionModule The SortitionModule contract. + /// @param _minStakingTime The minimum staking time. + /// @param _maxDrawingTime The maximum drawing time. + /// @param _rng The random number generator. + /// @param _rngLookahead The RNG lookahead time. + function initialize( + address _governor, + KlerosCore _core, + IPNKVault _vault, + ISortitionModule _sortitionModule, + uint256 _minStakingTime, + uint256 _maxDrawingTime, + RNG _rng, + uint256 _rngLookahead + ) external initializer { + __StakeControllerBase_initialize( + _governor, + _core, + _vault, + _sortitionModule, + _minStakingTime, + _maxDrawingTime, + _rng, + _rngLookahead + ); + } + + // ************************************* // + // * Governance * // + // ************************************* // + + /// @dev Access Control to perform implementation upgrades (UUPS Proxiable) + /// Only the governor can perform upgrades (`onlyByGovernor`) + function _authorizeUpgrade(address) internal view override onlyByGovernor { + // NOP + } +} diff --git a/contracts/src/arbitration/stake-controller/StakeControllerBase.sol b/contracts/src/arbitration/stake-controller/StakeControllerBase.sol new file mode 100644 index 000000000..d3d245a06 --- /dev/null +++ b/contracts/src/arbitration/stake-controller/StakeControllerBase.sol @@ -0,0 +1,539 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {IStakeController} from "../interfaces/IStakeController.sol"; +import {IPNKVault} from "../interfaces/IPNKVault.sol"; +import {ISortitionModule} from "../interfaces/ISortitionModule.sol"; +import {IDisputeKit} from "../interfaces/IDisputeKit.sol"; +import {KlerosCore} from "../KlerosCore.sol"; +import {Initializable} from "../../proxy/Initializable.sol"; +import {UUPSProxiable} from "../../proxy/UUPSProxiable.sol"; +import {RNG} from "../../rng/RNG.sol"; +import "../../libraries/Constants.sol"; + +/// @title StakeControllerBase +/// @notice Abstract base contract for coordinating between PNKVault and SortitionModule +/// @dev Manages phases, delayed stakes, and coordination logic +abstract contract StakeControllerBase is IStakeController, Initializable, UUPSProxiable { + // ************************************* // + // * Enums / Structs * // + // ************************************* // + + struct DelayedStake { + address account; // The address of the juror. + uint96 courtID; // The ID of the court. + uint256 stake; // The new stake. + bool alreadyTransferred; // True if tokens were already transferred before delayed stake's execution. + } + + // ************************************* // + // * Storage * // + // ************************************* // + + address public governor; // The governor of the contract. + KlerosCore public core; // The core arbitrator contract. + IPNKVault public vault; // The PNK vault for token management. + ISortitionModule public sortitionModule; // The sortition module for drawing logic. + + // Phase management (moved from SortitionModule) + ISortitionModule.Phase public phase; // The current phase. + uint256 public minStakingTime; // The time after which the phase can be switched to Drawing if there are open disputes. + uint256 public maxDrawingTime; // The time after which the phase can be switched back to Staking. + uint256 public lastPhaseChange; // The last time the phase was changed. + uint256 public randomNumberRequestBlock; // Number of the block when RNG request was made. + uint256 public disputesWithoutJurors; // The number of disputes that have not finished drawing jurors. + RNG public rng; // The random number generator. + uint256 public randomNumber; // Random number returned by RNG. + uint256 public rngLookahead; // Minimal block distance between requesting and obtaining a random number. + + // Delayed stakes management (moved from SortitionModule) + uint256 public delayedStakeWriteIndex; // The index of the last `delayedStake` item that was written to the array. 0 index is skipped. + uint256 public delayedStakeReadIndex; // The index of the next `delayedStake` item that should be processed. Starts at 1 because 0 index is skipped. + mapping(uint256 => DelayedStake) public delayedStakes; // Stores the stakes that were changed during Drawing phase, to update them when the phase is switched to Staking. + mapping(address jurorAccount => mapping(uint96 courtId => uint256)) public latestDelayedStakeIndex; // Maps the juror to its latest delayed stake. If there is already a delayed stake for this juror then it'll be replaced. + + // ************************************* // + // * Function Modifiers * // + // ************************************* // + + modifier onlyByGovernor() { + if (governor != msg.sender) revert GovernorOnly(); + _; + } + + modifier onlyByCore() { + if (address(core) != msg.sender) revert CoreOnly(); + _; + } + + // ************************************* // + // * Constructor * // + // ************************************* // + + function __StakeControllerBase_initialize( + address _governor, + KlerosCore _core, + IPNKVault _vault, + ISortitionModule _sortitionModule, + uint256 _minStakingTime, + uint256 _maxDrawingTime, + RNG _rng, + uint256 _rngLookahead + ) internal onlyInitializing { + governor = _governor; + core = _core; + vault = _vault; + sortitionModule = _sortitionModule; + minStakingTime = _minStakingTime; + maxDrawingTime = _maxDrawingTime; + lastPhaseChange = block.timestamp; + rng = _rng; + rngLookahead = _rngLookahead; + delayedStakeReadIndex = 1; + } + + // ************************************* // + // * Governance * // + // ************************************* // + + /// @dev Changes the governor of the contract. + /// @param _governor The new governor. + function changeGovernor(address _governor) external onlyByGovernor { + governor = _governor; + } + + /// @dev Changes the `vault` storage variable. + /// @param _vault The new vault address. + function changeVault(IPNKVault _vault) external onlyByGovernor { + vault = _vault; + } + + /// @dev Changes the `sortitionModule` storage variable. + /// @param _sortitionModule The new sortition module address. + function changeSortitionModule(ISortitionModule _sortitionModule) external onlyByGovernor { + sortitionModule = _sortitionModule; + } + + /// @dev Changes the `minStakingTime` storage variable. + /// @param _minStakingTime The new value for the `minStakingTime` storage variable. + function changeMinStakingTime(uint256 _minStakingTime) external onlyByGovernor { + minStakingTime = _minStakingTime; + } + + /// @dev Changes the `maxDrawingTime` storage variable. + /// @param _maxDrawingTime The new value for the `maxDrawingTime` storage variable. + function changeMaxDrawingTime(uint256 _maxDrawingTime) external onlyByGovernor { + maxDrawingTime = _maxDrawingTime; + } + + /// @dev Changes the `_rng` and `_rngLookahead` storage variables. + /// @param _rng The new value for the `RNGenerator` storage variable. + /// @param _rngLookahead The new value for the `rngLookahead` storage variable. + function changeRandomNumberGenerator(RNG _rng, uint256 _rngLookahead) external onlyByGovernor { + rng = _rng; + rngLookahead = _rngLookahead; + if (phase == ISortitionModule.Phase.generating) { + rng.requestRandomness(block.number + rngLookahead); + randomNumberRequestBlock = block.number; + } + } + + // ************************************* // + // * Phase Management * // + // ************************************* // + + /// @inheritdoc IStakeController + function passPhase() external override { + if (phase == ISortitionModule.Phase.staking) { + if (block.timestamp - lastPhaseChange < minStakingTime) revert MinStakingTimeNotPassed(); + if (disputesWithoutJurors == 0) revert NoDisputesNeedingJurors(); + rng.requestRandomness(block.number + rngLookahead); + randomNumberRequestBlock = block.number; + phase = ISortitionModule.Phase.generating; + } else if (phase == ISortitionModule.Phase.generating) { + randomNumber = rng.receiveRandomness(randomNumberRequestBlock + rngLookahead); + if (randomNumber == 0) revert RandomNumberNotReady(); + phase = ISortitionModule.Phase.drawing; + } else if (phase == ISortitionModule.Phase.drawing) { + if (disputesWithoutJurors > 0 && block.timestamp - lastPhaseChange < maxDrawingTime) { + revert StillDrawingDisputes(); + } + phase = ISortitionModule.Phase.staking; + } + + lastPhaseChange = block.timestamp; + emit NewPhase(phase); + } + + /// @inheritdoc IStakeController + function getPhase() external view override returns (ISortitionModule.Phase) { + return phase; + } + + /// @inheritdoc IStakeController + function executeDelayedStakes(uint256 _iterations) external override { + if (phase != ISortitionModule.Phase.staking) revert NotInStakingPhase(); + if (delayedStakeWriteIndex < delayedStakeReadIndex) revert NoDelayedStakes(); + + uint256 actualIterations = (delayedStakeReadIndex + _iterations) - 1 > delayedStakeWriteIndex + ? (delayedStakeWriteIndex - delayedStakeReadIndex) + 1 + : _iterations; + uint256 newDelayedStakeReadIndex = delayedStakeReadIndex + actualIterations; + + for (uint256 i = delayedStakeReadIndex; i < newDelayedStakeReadIndex; i++) { + DelayedStake storage delayedStake = delayedStakes[i]; + // Delayed stake could've been manually removed already. In this case simply move on to the next item. + if (delayedStake.account != address(0)) { + // Nullify the index so the delayed stake won't get deleted before its own execution. + delete latestDelayedStakeIndex[delayedStake.account][delayedStake.courtID]; + + // Execute the delayed stake through the controller + (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) = _setStake( + delayedStake.account, + delayedStake.courtID, + delayedStake.stake, + delayedStake.alreadyTransferred + ); + + // Note: In delayed stake execution, we don't revert on failures to maintain batch processing + delete delayedStakes[i]; + } + } + delayedStakeReadIndex = newDelayedStakeReadIndex; + } + + // ************************************* // + // * Stake Management * // + // ************************************* // + + /// @inheritdoc IStakeController + function setStake( + address _account, + uint96 _courtID, + uint256 _newStake, + bool _alreadyTransferred + ) external override onlyByCore returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { + return _setStake(_account, _courtID, _newStake, _alreadyTransferred); + } + + /// @dev Internal implementation of setStake with phase-aware delayed stake logic + function _setStake( + address _account, + uint96 _courtID, + uint256 _newStake, + bool _alreadyTransferred + ) internal virtual returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { + (, , uint256 currentStake, ) = sortitionModule.getJurorBalance(_account, _courtID); + + // Delete any existing delayed stake for this juror/court + pnkWithdrawal = _deleteDelayedStake(_courtID, _account); + + if (phase != ISortitionModule.Phase.staking) { + // Store the stake change as delayed, to be applied when the phase switches back to Staking. + DelayedStake storage delayedStake = delayedStakes[++delayedStakeWriteIndex]; + delayedStake.account = _account; + delayedStake.courtID = _courtID; + delayedStake.stake = _newStake; + latestDelayedStakeIndex[_account][_courtID] = delayedStakeWriteIndex; + + if (_newStake > currentStake) { + // PNK deposit: tokens are transferred now via vault coordination + delayedStake.alreadyTransferred = true; + pnkDeposit = _newStake - currentStake; + // Note: Actual PNK transfer is handled by KlerosCore through vault + } else { + // PNK withdrawal: tokens are not transferred yet. + delayedStake.alreadyTransferred = false; + } + return (pnkDeposit, pnkWithdrawal, StakingResult.Successful); + } + + // Current phase is Staking: set normal stakes through sortition module + return sortitionModule.setStake(_account, _courtID, _newStake, _alreadyTransferred); + } + + /// @inheritdoc IStakeController + function lockStake(address _account, uint256 _amount) external override onlyByCore { + vault.lockTokens(_account, _amount); + emit StakeUnlocked(_account, _amount); // Note: Event name preserved for compatibility + } + + /// @inheritdoc IStakeController + function unlockStake(address _account, uint256 _amount) external override onlyByCore { + vault.unlockTokens(_account, _amount); + emit StakeUnlocked(_account, _amount); + } + + /// @inheritdoc IStakeController + function executeJurorPenalty( + address _account, + uint256 _penalty, + uint256 _totalStake + ) external virtual override onlyByCore returns (uint256 actualPenalty) { + // First unlock the penalty amount + vault.unlockTokens(_account, _penalty); + + // Then apply the penalty through vault + actualPenalty = vault.applyPenalty(_account, _penalty); + + emit JurorPenaltyExecuted(_account, _penalty, actualPenalty); + return actualPenalty; + } + + /// @inheritdoc IStakeController + function setJurorInactive(address _account) external override onlyByCore { + uint96[] memory courtIds = sortitionModule.getJurorCourtIDs(_account); + + for (uint256 i = 0; i < courtIds.length; i++) { + (, , uint256 currentStake, ) = sortitionModule.getJurorBalance(_account, courtIds[i]); + if (currentStake > 0) { + // Set stake to 0 in sortition module to remove from trees + sortitionModule.setStake(_account, courtIds[i], 0, true); + } + } + + emit JurorSetInactive(_account); + } + + /// @inheritdoc IStakeController + function shouldSetJurorInactive( + address _account, + uint256 _disputeID, + uint256 _round, + uint256 _repartition + ) external view virtual override returns (bool shouldSet) { + // Check if juror has any remaining deposited balance + uint256 remainingBalance = vault.getDepositedBalance(_account); + + // Check if juror's vote is still active for this dispute + // This requires access to the dispute kit to check vote activity + // For now, we'll use a simplified check based on balance + bool isActive = remainingBalance > 0; + + return remainingBalance == 0 || !isActive; + } + + // ************************************* // + // * Sortition Delegation * // + // ************************************* // + + /// @inheritdoc IStakeController + function createTree(bytes32 _key, bytes memory _extraData) external override onlyByCore { + sortitionModule.createTree(_key, _extraData); + } + + /// @inheritdoc IStakeController + function draw(bytes32 _court, uint256 _coreDisputeID, uint256 _nonce) external view override returns (address) { + return sortitionModule.draw(_court, _coreDisputeID, _nonce); + } + + /// @inheritdoc IStakeController + function createDisputeHook(uint256 _disputeID, uint256 _roundID) external override onlyByCore { + disputesWithoutJurors++; + sortitionModule.createDisputeHook(_disputeID, _roundID); + } + + /// @inheritdoc IStakeController + function postDrawHook(uint256 _disputeID, uint256 _roundID) external override onlyByCore { + disputesWithoutJurors--; + sortitionModule.postDrawHook(_disputeID, _roundID); + } + + /// @inheritdoc IStakeController + function notifyRandomNumber(uint256 _drawnNumber) external override { + sortitionModule.notifyRandomNumber(_drawnNumber); + } + + // ************************************* // + // * View Functions * // + // ************************************* // + + /// @inheritdoc IStakeController + function getJurorBalance( + address _juror, + uint96 _courtID + ) + external + view + override + returns (uint256 totalStaked, uint256 totalLocked, uint256 stakedInCourt, uint256 nbCourts) + { + return sortitionModule.getJurorBalance(_juror, _courtID); + } + + /// @inheritdoc IStakeController + function getJurorCourtIDs(address _juror) external view override returns (uint96[] memory) { + return sortitionModule.getJurorCourtIDs(_juror); + } + + /// @inheritdoc IStakeController + function isJurorStaked(address _juror) external view override returns (bool) { + return sortitionModule.isJurorStaked(_juror); + } + + /// @inheritdoc IStakeController + function getAvailableBalance(address _account) external view override returns (uint256) { + return vault.getAvailableBalance(_account); + } + + /// @inheritdoc IStakeController + function getDepositedBalance(address _account) external view override returns (uint256) { + return vault.getDepositedBalance(_account); + } + + // ************************************* // + // * Internal * // + // ************************************* // + + /// @dev Enhanced delayed stake storage with better tracking + function _storeDelayedStakeEnhanced( + address _account, + uint96 _courtID, + uint256 _newStake + ) internal returns (bool success) { + delayedStakeWriteIndex++; + DelayedStake storage delayedStake = delayedStakes[delayedStakeWriteIndex]; + delayedStake.account = _account; + delayedStake.courtID = _courtID; + delayedStake.stake = _newStake; + delayedStake.alreadyTransferred = false; + latestDelayedStakeIndex[_account][_courtID] = delayedStakeWriteIndex; + + return true; + } + + /// @dev Enhanced inactivity check with dispute kit integration + function _shouldSetJurorInactiveEnhanced( + address _account, + uint256 _disputeID, + uint256 _round, + uint256 _repartition + ) internal view returns (bool shouldSet) { + // Check if juror has any remaining deposited balance + uint256 remainingBalance = vault.getDepositedBalance(_account); + if (remainingBalance == 0) { + return true; + } + + // Enhanced check - could be extended with dispute kit integration + // For now, simplified implementation based on balance + bool isActive = remainingBalance > 0; + + return !isActive; + } + + /// @dev Checks if there is already a delayed stake. In this case consider it irrelevant and remove it. + /// @param _courtID ID of the court. + /// @param _juror Juror whose stake to check. + function _deleteDelayedStake(uint96 _courtID, address _juror) internal returns (uint256 actualAmountToWithdraw) { + uint256 latestIndex = latestDelayedStakeIndex[_juror][_courtID]; + if (latestIndex != 0) { + DelayedStake storage delayedStake = delayedStakes[latestIndex]; + if (delayedStake.alreadyTransferred) { + // Calculate amount to withdraw based on the difference + (, , uint256 sortitionStake, ) = sortitionModule.getJurorBalance(_juror, _courtID); + uint256 amountToWithdraw = delayedStake.stake > sortitionStake + ? delayedStake.stake - sortitionStake + : 0; + actualAmountToWithdraw = amountToWithdraw; + + // Note: Actual token withdrawal is handled by KlerosCore through vault coordination + } + delete delayedStakes[latestIndex]; + delete latestDelayedStakeIndex[_juror][_courtID]; + } + } + + // ************************************* // + // * Migration Utilities * // + // ************************************* // + + /// @dev Import existing stakes from old sortition module for migration + /// @param _accounts Array of juror accounts + /// @param _courtIDs Array of court IDs + /// @param _stakes Array of stake amounts + function importExistingStakes( + address[] calldata _accounts, + uint96[] calldata _courtIDs, + uint256[] calldata _stakes + ) external onlyByGovernor { + if (_accounts.length != _courtIDs.length || _accounts.length != _stakes.length) { + revert InvalidMigrationData(); + } + + uint256 totalImported = 0; + for (uint256 i = 0; i < _accounts.length; i++) { + if (_stakes[i] > 0) { + // Direct stake import bypassing normal validation for migration + (, , StakingResult result) = sortitionModule.setStake(_accounts[i], _courtIDs[i], _stakes[i], true); + if (result == StakingResult.Successful) { + totalImported++; + emit StakeImported(_accounts[i], _courtIDs[i], _stakes[i]); + } + } + } + + emit MigrationCompleted(_accounts.length, totalImported); + } + + /// @dev Import delayed stakes from old system for migration + /// @param _delayedStakes Array of delayed stake data + function importDelayedStakes(DelayedStake[] calldata _delayedStakes) external onlyByGovernor { + for (uint256 i = 0; i < _delayedStakes.length; i++) { + DelayedStake memory delayedStake = _delayedStakes[i]; + if (delayedStake.account != address(0)) { + delayedStakeWriteIndex++; + delayedStakes[delayedStakeWriteIndex] = delayedStake; + latestDelayedStakeIndex[delayedStake.account][delayedStake.courtID] = delayedStakeWriteIndex; + + emit DelayedStakeImported( + delayedStake.account, + delayedStake.courtID, + delayedStake.stake, + delayedStakeWriteIndex + ); + } + } + } + + /// @dev Migrate phase state from old sortition module + /// @param _phase The phase to set + /// @param _lastPhaseChange The last phase change timestamp + /// @param _disputesWithoutJurors Number of disputes without jurors + function migratePhaseState( + ISortitionModule.Phase _phase, + uint256 _lastPhaseChange, + uint256 _disputesWithoutJurors + ) external onlyByGovernor { + phase = _phase; + lastPhaseChange = _lastPhaseChange; + disputesWithoutJurors = _disputesWithoutJurors; + + emit PhaseStateMigrated(_phase, _lastPhaseChange, _disputesWithoutJurors); + } + + /// @dev Emergency coordination reset for critical issues + function emergencyCoordinationReset() external onlyByGovernor { + phase = ISortitionModule.Phase.staking; + lastPhaseChange = block.timestamp; + disputesWithoutJurors = 0; + randomNumber = 0; + + emit EmergencyReset(block.timestamp); + } + + // ************************************* // + // * Errors * // + // ************************************* // + + error GovernorOnly(); + error CoreOnly(); + error MinStakingTimeNotPassed(); + error NoDisputesNeedingJurors(); + error RandomNumberNotReady(); + error StillDrawingDisputes(); + error NotInStakingPhase(); + error NoDelayedStakes(); + error InvalidMigrationData(); +} diff --git a/contracts/src/arbitration/stake-controller/StakeControllerNeo.sol b/contracts/src/arbitration/stake-controller/StakeControllerNeo.sol new file mode 100644 index 000000000..a669b0b4e --- /dev/null +++ b/contracts/src/arbitration/stake-controller/StakeControllerNeo.sol @@ -0,0 +1,218 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {StakeControllerBase} from "./StakeControllerBase.sol"; +import {IPNKVault} from "../interfaces/IPNKVault.sol"; +import {ISortitionModule} from "../interfaces/ISortitionModule.sol"; +import {IDisputeKit} from "../interfaces/IDisputeKit.sol"; +import {KlerosCore} from "../KlerosCore.sol"; +import {RNG} from "../../rng/RNG.sol"; + +/// @title StakeControllerNeo +/// @notice Enhanced implementation of the Stake Controller with additional features +/// @dev Coordinates between PNKVault and SortitionModule with improved penalty logic +contract StakeControllerNeo is StakeControllerBase { + string public constant override version = "1.0.0"; + + // ************************************* // + // * Storage * // + // ************************************* // + + mapping(uint256 => IDisputeKit) public disputeKits; // Mapping from dispute kit ID to dispute kit contract + uint256 public disputeKitsLength; // Number of registered dispute kits + + // Enhanced penalty tracking + mapping(address => uint256) public totalPenaltiesApplied; // Track total penalties per juror + mapping(address => uint256) public lastPenaltyBlock; // Track when last penalty was applied + + // Coordination enhancement flags + bool public enableEnhancedInactivityChecks; // Whether to use enhanced inactivity detection + uint256 public penaltyCooldownBlocks; // Minimum blocks between penalties for same juror + + // ************************************* // + // * Events * // + // ************************************* // + + event DisputeKitRegistered(uint256 indexed disputeKitID, IDisputeKit indexed disputeKit); + event EnhancedInactivityChecksToggled(bool enabled); + event PenaltyCooldownChanged(uint256 newCooldown); + + // ************************************* // + // * Constructor * // + // ************************************* // + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /// @dev Initializer (constructor equivalent for upgradable contracts). + /// @param _governor The governor's address. + /// @param _core The KlerosCore contract. + /// @param _vault The PNKVault contract. + /// @param _sortitionModule The SortitionModule contract. + /// @param _minStakingTime The minimum staking time. + /// @param _maxDrawingTime The maximum drawing time. + /// @param _rng The random number generator. + /// @param _rngLookahead The RNG lookahead time. + /// @param _enableEnhancedInactivityChecks Whether to enable enhanced inactivity checks. + /// @param _penaltyCooldownBlocks Minimum blocks between penalties. + function initialize( + address _governor, + KlerosCore _core, + IPNKVault _vault, + ISortitionModule _sortitionModule, + uint256 _minStakingTime, + uint256 _maxDrawingTime, + RNG _rng, + uint256 _rngLookahead, + bool _enableEnhancedInactivityChecks, + uint256 _penaltyCooldownBlocks + ) external reinitializer(2) { + __StakeControllerBase_initialize( + _governor, + _core, + _vault, + _sortitionModule, + _minStakingTime, + _maxDrawingTime, + _rng, + _rngLookahead + ); + + enableEnhancedInactivityChecks = _enableEnhancedInactivityChecks; + penaltyCooldownBlocks = _penaltyCooldownBlocks; + } + + // ************************************* // + // * Governance * // + // ************************************* // + + /// @dev Access Control to perform implementation upgrades (UUPS Proxiable) + /// Only the governor can perform upgrades (`onlyByGovernor`) + function _authorizeUpgrade(address) internal view override onlyByGovernor { + // NOP + } + + /// @dev Register a dispute kit for enhanced inactivity checks + /// @param _disputeKitID The ID of the dispute kit + /// @param _disputeKit The dispute kit contract + function registerDisputeKit(uint256 _disputeKitID, IDisputeKit _disputeKit) external onlyByGovernor { + disputeKits[_disputeKitID] = _disputeKit; + if (_disputeKitID >= disputeKitsLength) { + disputeKitsLength = _disputeKitID + 1; + } + emit DisputeKitRegistered(_disputeKitID, _disputeKit); + } + + /// @dev Toggle enhanced inactivity checks + /// @param _enabled Whether to enable enhanced checks + function toggleEnhancedInactivityChecks(bool _enabled) external onlyByGovernor { + enableEnhancedInactivityChecks = _enabled; + emit EnhancedInactivityChecksToggled(_enabled); + } + + /// @dev Change penalty cooldown blocks + /// @param _penaltyCooldownBlocks New cooldown period in blocks + function changePenaltyCooldownBlocks(uint256 _penaltyCooldownBlocks) external onlyByGovernor { + penaltyCooldownBlocks = _penaltyCooldownBlocks; + emit PenaltyCooldownChanged(_penaltyCooldownBlocks); + } + + // ************************************* // + // * Enhanced Functions * // + // ************************************* // + + /// @dev Enhanced penalty execution with cooldown and tracking + function executeJurorPenalty( + address _account, + uint256 _penalty, + uint256 _totalStake + ) external override onlyByCore returns (uint256 actualPenalty) { + // Check penalty cooldown + if (penaltyCooldownBlocks > 0 && lastPenaltyBlock[_account] + penaltyCooldownBlocks > block.number) { + revert PenaltyCooldownActive(); + } + + // Execute penalty through vault coordination (base implementation) + vault.unlockTokens(_account, _penalty); + actualPenalty = vault.applyPenalty(_account, _penalty); + + // Update tracking + totalPenaltiesApplied[_account] += actualPenalty; + lastPenaltyBlock[_account] = block.number; + + emit JurorPenaltyExecuted(_account, _penalty, actualPenalty); + return actualPenalty; + } + + /// @dev Enhanced inactivity check with dispute kit integration + function shouldSetJurorInactive( + address _account, + uint256 _disputeID, + uint256 _round, + uint256 _repartition + ) external view override returns (bool shouldSet) { + // Check if juror has any remaining deposited balance + uint256 remainingBalance = vault.getDepositedBalance(_account); + if (remainingBalance == 0) { + return true; + } + + // If enhanced checks are disabled, use basic check + if (!enableEnhancedInactivityChecks) { + return false; + } + + // Enhanced check: query dispute kit for vote activity + // Note: This requires the dispute kit to be properly registered + // For now, we'll implement a basic version that can be extended + + // If juror has been penalized heavily, consider setting inactive + uint256 penaltyRatio = (totalPenaltiesApplied[_account] * 10000) / + (remainingBalance + totalPenaltiesApplied[_account]); + if (penaltyRatio > 5000) { + // If more than 50% of original stake was penalized + return true; + } + + return false; + } + + // ************************************* // + // * View Functions * // + // ************************************* // + + /// @notice Get penalty statistics for a juror + /// @param _account The juror address + /// @return totalPenalties Total penalties applied + /// @return lastPenaltyBlock_ Block when last penalty was applied + /// @return cooldownRemaining Blocks remaining in cooldown (0 if no cooldown) + function getPenaltyStats( + address _account + ) external view returns (uint256 totalPenalties, uint256 lastPenaltyBlock_, uint256 cooldownRemaining) { + totalPenalties = totalPenaltiesApplied[_account]; + lastPenaltyBlock_ = lastPenaltyBlock[_account]; + + if (penaltyCooldownBlocks > 0 && lastPenaltyBlock_ + penaltyCooldownBlocks > block.number) { + cooldownRemaining = (lastPenaltyBlock_ + penaltyCooldownBlocks) - block.number; + } else { + cooldownRemaining = 0; + } + } + + /// @notice Check if a juror is in penalty cooldown + /// @param _account The juror address + /// @return Whether the juror is in cooldown + function isInPenaltyCooldown(address _account) external view returns (bool) { + if (penaltyCooldownBlocks == 0) return false; + return lastPenaltyBlock[_account] + penaltyCooldownBlocks > block.number; + } + + // ************************************* // + // * Errors * // + // ************************************* // + + error PenaltyCooldownActive(); +} diff --git a/contracts/src/arbitration/vault/PNKVault.sol b/contracts/src/arbitration/vault/PNKVault.sol new file mode 100644 index 000000000..852aec5df --- /dev/null +++ b/contracts/src/arbitration/vault/PNKVault.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {PNKVaultBase, IERC20} from "./PNKVaultBase.sol"; + +/// @title PNKVault +/// @notice PNK Vault for handling deposits, withdrawals, locks, and penalties +/// @dev Follows the same pattern as KlerosCore for upgradeable contracts +contract PNKVault is PNKVaultBase { + string public constant override version = "1.0.0"; + + // ************************************* // + // * Constructor * // + // ************************************* // + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /// @dev Initializer (constructor equivalent for upgradable contracts). + /// @param _governor The governor's address. + /// @param _pnk The address of the PNK token contract. + /// @param _stakeController The address of the stake controller. + /// @param _core The address of the KlerosCore contract. + function initialize( + address _governor, + IERC20 _pnk, + address _stakeController, + address _core + ) external reinitializer(1) { + __PNKVaultBase_initialize(_governor, _pnk, _stakeController, _core); + } + + // ************************************* // + // * Governance * // + // ************************************* // + + /// @dev Access Control to perform implementation upgrades (UUPS Proxiable) + /// Only the governor can perform upgrades (`onlyByGovernor`) + function _authorizeUpgrade(address) internal view override onlyByGovernor { + // NOP + } +} diff --git a/contracts/src/arbitration/vault/PNKVaultBase.sol b/contracts/src/arbitration/vault/PNKVaultBase.sol new file mode 100644 index 000000000..d747f8079 --- /dev/null +++ b/contracts/src/arbitration/vault/PNKVaultBase.sol @@ -0,0 +1,250 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {IPNKVault} from "../interfaces/IPNKVault.sol"; +import {stPNK} from "../stPNK.sol"; +import {Initializable} from "../../proxy/Initializable.sol"; +import {UUPSProxiable} from "../../proxy/UUPSProxiable.sol"; +import {SafeERC20, IERC20} from "../../libraries/SafeERC20.sol"; + +/// @title PNKVaultBase +/// @notice Abstract base contract for PNK vault that handles deposits, withdrawals, locks, and penalties +/// @dev Follows the same pattern as KlerosCoreBase for upgradeable contracts +abstract contract PNKVaultBase is IPNKVault, Initializable, UUPSProxiable { + using SafeERC20 for IERC20; + + // ************************************* // + // * Enums / Structs * // + // ************************************* // + + struct JurorBalance { + uint256 deposited; // Total PNK deposited + uint256 locked; // PNK locked in disputes + uint256 penalties; // Accumulated penalties + } + + // ************************************* // + // * Storage * // + // ************************************* // + + address public governor; // The governor of the contract. + IERC20 public pnk; // The PNK token contract. + stPNK public stPnkToken; // The stPNK token contract. + address public stakeController; // The stake controller authorized to lock/unlock/penalize. + address public core; // The KlerosCore authorized to transfer rewards. + + mapping(address => JurorBalance) public jurorBalances; // Juror balance tracking. + + // ************************************* // + // * Events * // + // ************************************* // + + // Events are defined in IPNKVault interface + + // ************************************* // + // * Function Modifiers * // + // ************************************* // + + modifier onlyByGovernor() { + if (governor != msg.sender) revert GovernorOnly(); + _; + } + + modifier onlyStakeController() { + if (msg.sender != stakeController) revert OnlyStakeController(); + _; + } + + modifier onlyCore() { + if (msg.sender != core) revert OnlyCore(); + _; + } + + // ************************************* // + // * Constructor * // + // ************************************* // + + function __PNKVaultBase_initialize( + address _governor, + IERC20 _pnk, + address _stakeController, + address _core + ) internal onlyInitializing { + governor = _governor; + pnk = _pnk; + stakeController = _stakeController; + core = _core; + + // Deploy stPNK token + stPnkToken = new stPNK(address(this)); + + // Add stakeController and core as protocol contracts in stPNK + address[] memory contracts = new address[](2); + bool[] memory allowed = new bool[](2); + contracts[0] = _stakeController; + contracts[1] = _core; + allowed[0] = true; + allowed[1] = true; + stPnkToken.setProtocolContracts(contracts, allowed); + } + + // ************************************* // + // * Governance * // + // ************************************* // + + /// @dev Changes the `governor` storage variable. + /// @param _governor The new value for the `governor` storage variable. + function changeGovernor(address _governor) external onlyByGovernor { + governor = _governor; + } + + /// @dev Changes the `stakeController` storage variable. + /// @param _stakeController The new value for the `stakeController` storage variable. + function changeStakeController(address _stakeController) external onlyByGovernor { + stakeController = _stakeController; + address[] memory contracts = new address[](1); + bool[] memory allowed = new bool[](1); + contracts[0] = _stakeController; + allowed[0] = true; + stPnkToken.setProtocolContracts(contracts, allowed); + } + + /// @dev Changes the `core` storage variable. + /// @param _core The new value for the `core` storage variable. + function changeCore(address _core) external onlyByGovernor { + core = _core; + address[] memory contracts = new address[](1); + bool[] memory allowed = new bool[](1); + contracts[0] = _core; + allowed[0] = true; + stPnkToken.setProtocolContracts(contracts, allowed); + } + + // ************************************* // + // * State Modifiers * // + // ************************************* // + + /// @inheritdoc IPNKVault + function deposit(uint256 _amount) external virtual override returns (uint256 stPnkAmount) { + if (_amount == 0) revert InvalidAmount(); + + pnk.safeTransferFrom(msg.sender, address(this), _amount); + jurorBalances[msg.sender].deposited += _amount; + + // Mint 1:1 stPNK + stPnkAmount = _amount; + stPnkToken.mint(msg.sender, stPnkAmount); + + emit Deposit(msg.sender, _amount); + } + + /// @inheritdoc IPNKVault + function withdraw(uint256 _amount) external virtual override returns (uint256 pnkAmount) { + if (_amount == 0) revert InvalidAmount(); + + JurorBalance storage balance = jurorBalances[msg.sender]; + + // Check available balance (deposited - locked - penalties) + uint256 available = getAvailableBalance(msg.sender); + if (_amount > available) revert InsufficientAvailableBalance(); + + // Check stPNK balance + if (stPnkToken.balanceOf(msg.sender) < _amount) revert InsufficientStPNKBalance(); + + // Burn stPNK and transfer PNK + stPnkToken.burnFrom(msg.sender, _amount); + balance.deposited -= _amount; + pnk.safeTransfer(msg.sender, _amount); + + emit Withdraw(msg.sender, _amount); + return _amount; + } + + /// @inheritdoc IPNKVault + function lockTokens(address _account, uint256 _amount) external virtual override onlyStakeController { + jurorBalances[_account].locked += _amount; + emit Lock(_account, _amount); + } + + /// @inheritdoc IPNKVault + function unlockTokens(address _account, uint256 _amount) external virtual override onlyStakeController { + jurorBalances[_account].locked -= _amount; + emit Unlock(_account, _amount); + } + + /// @inheritdoc IPNKVault + function applyPenalty( + address _account, + uint256 _amount + ) external virtual override onlyStakeController returns (uint256 actualPenalty) { + JurorBalance storage balance = jurorBalances[_account]; + + // Calculate actual penalty (cannot exceed deposited amount) + actualPenalty = _amount > balance.deposited ? balance.deposited : _amount; + + // Update balances + balance.deposited -= actualPenalty; + balance.penalties += actualPenalty; + + // Burn equivalent stPNK if user still holds it + uint256 userStPnkBalance = stPnkToken.balanceOf(_account); + uint256 toBurn = actualPenalty > userStPnkBalance ? userStPnkBalance : actualPenalty; + if (toBurn > 0) { + stPnkToken.burnFrom(_account, toBurn); + } + + // Note: Penalized PNK stays in vault to fund rewards pool + emit Penalty(_account, actualPenalty); + } + + /// @inheritdoc IPNKVault + function transferReward(address _account, uint256 _amount) external virtual override onlyCore { + if (pnk.balanceOf(address(this)) < _amount) revert InsufficientVaultBalance(); + pnk.safeTransfer(_account, _amount); + emit RewardTransferred(_account, _amount); + } + + // ************************************* // + // * Public Views * // + // ************************************* // + + /// @inheritdoc IPNKVault + function getAvailableBalance(address _account) public view override returns (uint256) { + JurorBalance storage balance = jurorBalances[_account]; + uint256 locked = balance.locked + balance.penalties; + return balance.deposited > locked ? balance.deposited - locked : 0; + } + + /// @inheritdoc IPNKVault + function getDepositedBalance(address _account) external view override returns (uint256) { + return jurorBalances[_account].deposited; + } + + /// @inheritdoc IPNKVault + function getLockedBalance(address _account) external view override returns (uint256) { + return jurorBalances[_account].locked; + } + + /// @inheritdoc IPNKVault + function getStPNKBalance(address _account) external view override returns (uint256) { + return stPnkToken.balanceOf(_account); + } + + /// @inheritdoc IPNKVault + function getPenaltyBalance(address _account) external view override returns (uint256) { + return jurorBalances[_account].penalties; + } + + // ************************************* // + // * Errors * // + // ************************************* // + + error GovernorOnly(); + error OnlyStakeController(); + error OnlyCore(); + error InvalidAmount(); + error InsufficientAvailableBalance(); + error InsufficientStPNKBalance(); + error InsufficientVaultBalance(); +} diff --git a/contracts/src/arbitration/vault/PNKVaultNeo.sol b/contracts/src/arbitration/vault/PNKVaultNeo.sol new file mode 100644 index 000000000..e1294a391 --- /dev/null +++ b/contracts/src/arbitration/vault/PNKVaultNeo.sol @@ -0,0 +1,205 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {PNKVaultBase, IERC20} from "./PNKVaultBase.sol"; +import {SafeERC20} from "../../libraries/SafeERC20.sol"; +import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; + +/// @title PNKVaultNeo +/// @notice Enhanced PNK Vault with additional features like NFT-gated deposits +/// @dev Follows the same pattern as KlerosCoreNeo for upgradeable contracts +contract PNKVaultNeo is PNKVaultBase { + using SafeERC20 for IERC20; + + string public constant override version = "1.0.0"; + + // ************************************* // + // * Storage * // + // ************************************* // + + IERC721 public depositNft; // NFT required to deposit (optional) + uint256 public maxDepositPerUser; // Maximum deposit per user (0 = unlimited) + uint256 public totalDepositCap; // Total deposit cap across all users (0 = unlimited) + uint256 public totalDeposited; // Total amount deposited across all users + + // ************************************* // + // * Events * // + // ************************************* // + + event DepositNftChanged(IERC721 indexed oldNft, IERC721 indexed newNft); + event MaxDepositPerUserChanged(uint256 oldMax, uint256 newMax); + event TotalDepositCapChanged(uint256 oldCap, uint256 newCap); + + // ************************************* // + // * Constructor * // + // ************************************* // + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /// @dev Initializer (constructor equivalent for upgradable contracts). + /// @param _governor The governor's address. + /// @param _pnk The address of the PNK token contract. + /// @param _stakeController The address of the stake controller. + /// @param _core The address of the KlerosCore contract. + /// @param _depositNft The NFT contract for deposit gating (optional, can be zero address). + /// @param _maxDepositPerUser Maximum deposit per user (0 = unlimited). + /// @param _totalDepositCap Total deposit cap (0 = unlimited). + function initialize( + address _governor, + IERC20 _pnk, + address _stakeController, + address _core, + IERC721 _depositNft, + uint256 _maxDepositPerUser, + uint256 _totalDepositCap + ) external reinitializer(2) { + __PNKVaultBase_initialize(_governor, _pnk, _stakeController, _core); + + depositNft = _depositNft; + maxDepositPerUser = _maxDepositPerUser; + totalDepositCap = _totalDepositCap; + } + + // ************************************* // + // * Governance * // + // ************************************* // + + /// @dev Access Control to perform implementation upgrades (UUPS Proxiable) + /// Only the governor can perform upgrades (`onlyByGovernor`) + function _authorizeUpgrade(address) internal view override onlyByGovernor { + // NOP + } + + /// @dev Changes the `depositNft` storage variable. + /// @param _depositNft The new NFT contract for deposit gating. + function changeDepositNft(IERC721 _depositNft) external onlyByGovernor { + emit DepositNftChanged(depositNft, _depositNft); + depositNft = _depositNft; + } + + /// @dev Changes the `maxDepositPerUser` storage variable. + /// @param _maxDepositPerUser The new maximum deposit per user. + function changeMaxDepositPerUser(uint256 _maxDepositPerUser) external onlyByGovernor { + emit MaxDepositPerUserChanged(maxDepositPerUser, _maxDepositPerUser); + maxDepositPerUser = _maxDepositPerUser; + } + + /// @dev Changes the `totalDepositCap` storage variable. + /// @param _totalDepositCap The new total deposit cap. + function changeTotalDepositCap(uint256 _totalDepositCap) external onlyByGovernor { + emit TotalDepositCapChanged(totalDepositCap, _totalDepositCap); + totalDepositCap = _totalDepositCap; + } + + // ************************************* // + // * State Modifiers * // + // ************************************* // + + /// @notice Deposit PNK and mint stPNK with additional validation checks + function deposit(uint256 _amount) external override returns (uint256 stPnkAmount) { + // Check NFT requirement if set + if (address(depositNft) != address(0) && depositNft.balanceOf(msg.sender) == 0) { + revert DepositNftRequired(); + } + + // Check per-user deposit limit + if (maxDepositPerUser > 0) { + uint256 currentUserDeposit = jurorBalances[msg.sender].deposited; + if (currentUserDeposit + _amount > maxDepositPerUser) { + revert ExceedsMaxDepositPerUser(); + } + } + + // Check total deposit cap + if (totalDepositCap > 0 && totalDeposited + _amount > totalDepositCap) { + revert ExceedsTotalDepositCap(); + } + + // Update total deposited + totalDeposited += _amount; + + // Execute deposit logic + if (_amount == 0) revert InvalidAmount(); + + pnk.safeTransferFrom(msg.sender, address(this), _amount); + jurorBalances[msg.sender].deposited += _amount; + + // Mint 1:1 stPNK + stPnkAmount = _amount; + stPnkToken.mint(msg.sender, stPnkAmount); + + emit Deposit(msg.sender, _amount); + return stPnkAmount; + } + + /// @notice Withdraw PNK by burning stPNK and update total deposited + function withdraw(uint256 _amount) external override returns (uint256 pnkAmount) { + if (_amount == 0) revert InvalidAmount(); + + JurorBalance storage balance = jurorBalances[msg.sender]; + + // Check available balance (deposited - locked - penalties) + uint256 available = getAvailableBalance(msg.sender); + if (_amount > available) revert InsufficientAvailableBalance(); + + // Check stPNK balance + if (stPnkToken.balanceOf(msg.sender) < _amount) revert InsufficientStPNKBalance(); + + // Burn stPNK and transfer PNK + stPnkToken.burnFrom(msg.sender, _amount); + balance.deposited -= _amount; + pnk.safeTransfer(msg.sender, _amount); + + // Update total deposited + totalDeposited -= _amount; + + emit Withdraw(msg.sender, _amount); + return _amount; + } + + // ************************************* // + // * Public Views * // + // ************************************* // + + /// @notice Check if an account is eligible to deposit + /// @param _account The account to check + /// @param _amount The amount they want to deposit + /// @return eligible Whether they can deposit + /// @return reason Reason for ineligibility (empty if eligible) + function checkDepositEligibility( + address _account, + uint256 _amount + ) external view returns (bool eligible, string memory reason) { + // Check NFT requirement + if (address(depositNft) != address(0) && depositNft.balanceOf(_account) == 0) { + return (false, "NFT required for deposit"); + } + + // Check per-user limit + if (maxDepositPerUser > 0) { + uint256 currentUserDeposit = jurorBalances[_account].deposited; + if (currentUserDeposit + _amount > maxDepositPerUser) { + return (false, "Exceeds max deposit per user"); + } + } + + // Check total cap + if (totalDepositCap > 0 && totalDeposited + _amount > totalDepositCap) { + return (false, "Exceeds total deposit cap"); + } + + return (true, ""); + } + + // ************************************* // + // * Errors * // + // ************************************* // + + error DepositNftRequired(); + error ExceedsMaxDepositPerUser(); + error ExceedsTotalDepositCap(); +} From c38ecba7b87dba2fc187b08aa05eff79c5174282 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Wed, 4 Jun 2025 18:15:11 +0100 Subject: [PATCH 02/24] refactor: new staking and sortition architecture (wip) --- .../00-home-chain-arbitration-v2-neo.ts | 14 +-- .../deploy/00-home-chain-arbitration-v2.ts | 109 ++++++++---------- contracts/deploy/utils/klerosCoreHelper.ts | 4 +- contracts/package.json | 1 + .../interfaces/IStakeController.sol | 47 +++++--- .../interfaces/{IPNKVault.sol => IVault.sol} | 4 +- .../sortition-v2/SortitionModuleV2.sol | 5 +- .../sortition-v2/SortitionModuleV2Base.sol | 15 +-- .../sortition-v2/SortitionModuleV2Neo.sol | 4 +- contracts/src/arbitration/stPNK.sol | 81 +++++++++---- .../stake-controller/StakeController.sol | 16 +-- .../stake-controller/StakeControllerBase.sol | 100 +++++++++------- .../stake-controller/StakeControllerNeo.sol | 16 +-- .../vault/{PNKVault.sol => Vault.sol} | 9 +- .../vault/{PNKVaultBase.sol => VaultBase.sol} | 57 +++++---- .../vault/{PNKVaultNeo.sol => VaultNeo.sol} | 77 ++----------- yarn.lock | 10 ++ 17 files changed, 277 insertions(+), 292 deletions(-) rename contracts/src/arbitration/interfaces/{IPNKVault.sol => IVault.sol} (98%) rename contracts/src/arbitration/vault/{PNKVault.sol => Vault.sol} (87%) rename contracts/src/arbitration/vault/{PNKVaultBase.sol => VaultBase.sol} (86%) rename contracts/src/arbitration/vault/{PNKVaultNeo.sol => VaultNeo.sol} (67%) diff --git a/contracts/deploy/00-home-chain-arbitration-v2-neo.ts b/contracts/deploy/00-home-chain-arbitration-v2-neo.ts index 322dcdcff..867a8fdd5 100644 --- a/contracts/deploy/00-home-chain-arbitration-v2-neo.ts +++ b/contracts/deploy/00-home-chain-arbitration-v2-neo.ts @@ -5,7 +5,7 @@ import { deployUpgradable } from "./utils/deployUpgradable"; import { HomeChains, isSkipped, isDevnet, PNK, ETH } from "./utils"; import { getContractOrDeploy, getContractOrDeployUpgradable } from "./utils/getContractOrDeploy"; import { deployERC20AndFaucet, deployERC721 } from "./utils/deployTokens"; -import { ChainlinkRNG, DisputeKitClassic, KlerosCoreV2Neo, StakeControllerNeo, PNKVaultNeo } from "../typechain-types"; +import { ChainlinkRNG, DisputeKitClassic, KlerosCoreV2Neo, StakeControllerNeo, VaultNeo } from "../typechain-types"; const deployArbitrationV2Neo: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const { ethers, deployments, getNamedAccounts, getChainId } = hre; @@ -43,8 +43,8 @@ const deployArbitrationV2Neo: DeployFunction = async (hre: HardhatRuntimeEnviron log: true, }); - // Deploy PNKVaultNeo - const pnkVaultNeo = await deployUpgradable(deployments, "PNKVaultNeo", { + // Deploy VaultNeo + const pnkVaultNeo = await deployUpgradable(deployments, "VaultNeo", { from: deployer, args: [deployer, pnk.target, stPNK.address], log: true, @@ -135,12 +135,12 @@ const deployArbitrationV2Neo: DeployFunction = async (hre: HardhatRuntimeEnviron // Configure cross-dependencies console.log("Configuring cross-dependencies..."); - // Configure stPNK token to allow PNKVaultNeo operations + // Configure stPNK token to allow VaultNeo operations const stPNKContract = await ethers.getContractAt("stPNK", stPNK.address); const currentVault = await stPNKContract.vault(); if (currentVault !== pnkVaultNeo.address) { - console.log(`stPNK.setVault(${pnkVaultNeo.address})`); - await stPNKContract.setVault(pnkVaultNeo.address); + console.log(`stPNK.changeVault(${pnkVaultNeo.address})`); + await stPNKContract.changeVault(pnkVaultNeo.address); } // disputeKit.changeCore() only if necessary @@ -192,7 +192,7 @@ const deployArbitrationV2Neo: DeployFunction = async (hre: HardhatRuntimeEnviron }); console.log("✅ V2 Neo Architecture deployment completed successfully!"); - console.log(`📦 PNKVaultNeo: ${pnkVaultNeo.address}`); + console.log(`📦 VaultNeo: ${pnkVaultNeo.address}`); console.log(`🎫 stPNKNeo: ${stPNK.address}`); console.log(`🎯 SortitionModuleV2Neo: ${sortitionModuleV2Neo.address}`); console.log(`🎮 StakeControllerNeo: ${stakeControllerNeo.target}`); diff --git a/contracts/deploy/00-home-chain-arbitration-v2.ts b/contracts/deploy/00-home-chain-arbitration-v2.ts index f8b3b817f..8785b001d 100644 --- a/contracts/deploy/00-home-chain-arbitration-v2.ts +++ b/contracts/deploy/00-home-chain-arbitration-v2.ts @@ -5,7 +5,8 @@ import { deployUpgradable } from "./utils/deployUpgradable"; import { HomeChains, isSkipped, isDevnet, PNK, ETH } from "./utils"; import { getContractOrDeploy, getContractOrDeployUpgradable } from "./utils/getContractOrDeploy"; import { deployERC20AndFaucet } from "./utils/deployTokens"; -import { ChainlinkRNG, DisputeKitClassic, KlerosCoreV2, StakeController, PNKVault } from "../typechain-types"; +import { ChainlinkRNG, DisputeKitClassic, KlerosCoreV2, StakeController, Vault } from "../typechain-types"; +import { changeCurrencyRate } from "./utils/klerosCoreHelper"; const deployArbitrationV2: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const { ethers, deployments, getNamedAccounts, getChainId } = hre; @@ -31,53 +32,55 @@ const deployArbitrationV2: DeployFunction = async (hre: HardhatRuntimeEnvironmen const disputeKit = await deployUpgradable(deployments, "DisputeKitClassicV2", { from: deployer, contract: "DisputeKitClassic", - args: [deployer, ZeroAddress], + args: [ + deployer, + ZeroAddress, // Placeholder for KlerosCoreV2 address, configured later + ], log: true, }); - // Deploy stPNK token - const stPNK = await deploy("stPNK", { + // Calculate future addresses for circular dependencies + const nonce = await ethers.provider.getTransactionCount(deployer); + + const vaultAddress = getContractAddress(deployer, nonce + 3); // deployed on the 4th tx (nonce+3): stPNK Impl tx, stPNK Proxy tx, Vault Impl tx, Vault Proxy tx + console.log("calculated future Vault address for nonce %d: %s", nonce + 3, vaultAddress); + + const stakeControllerAddress = getContractAddress(deployer, nonce + 7); // deployed on the 8th tx (nonce+7): StakeController Impl tx, StakeController Proxy tx + console.log("calculated future StakeController address for nonce %d: %s", nonce + 7, stakeControllerAddress); + + const klerosCoreV2Address = getContractAddress(deployer, nonce + 9); // deployed on the 10th tx (nonce+9): SortitionModule Impl tx, SortitionModule Proxy tx, StakeController Impl tx, StakeController Proxy tx, KlerosCore Impl tx, KlerosCore Proxy tx + console.log("calculated future KlerosCoreV2 address for nonce %d: %s", nonce + 9, klerosCoreV2Address); + + const stPNK = await deployUpgradable(deployments, "stPNK", { from: deployer, - args: [], + args: [deployer, vaultAddress], log: true, - }); + }); // nonce (implementation), nonce+1 (proxy) - // Deploy PNKVault - const pnkVault = await deployUpgradable(deployments, "PNKVault", { + const vault = await deployUpgradable(deployments, "Vault", { from: deployer, - args: [deployer, pnk.target, stPNK.address], + args: [deployer, pnk.target, stPNK.address, stakeControllerAddress, klerosCoreV2Address], log: true, - }); - - // Calculate future addresses for circular dependencies - let klerosCoreV2Address = await deployments.getOrNull("KlerosCoreV2").then((deployment) => deployment?.address); - if (!klerosCoreV2Address) { - const nonce = await ethers.provider.getTransactionCount(deployer); - klerosCoreV2Address = getContractAddress(deployer, nonce + 5); // // deployed on the 6th tx (nonce+3): SortitionModule Impl tx, SortitionModule Proxy tx, StakeController Impl tx, StakeController Proxy tx, KlerosCore Impl tx, KlerosCore Proxy tx - console.log("calculated future KlerosCoreV2 address for nonce %d: %s", nonce + 5, klerosCoreV2Address); - } else { - console.log("using existing KlerosCoreV2 address: %s", klerosCoreV2Address); - } - - const devnet = isDevnet(hre.network); - const minStakingTime = devnet ? 180 : 1800; - const maxDrawingTime = devnet ? 600 : 1800; - const rng = (await ethers.getContract("ChainlinkRNG")) as ChainlinkRNG; + }); // nonce + 2 (implementation), nonce + 3 (proxy) // Deploy SortitionModuleV2 const sortitionModuleV2 = await deployUpgradable(deployments, "SortitionModuleV2", { from: deployer, args: [deployer, stakeControllerAddress], log: true, - }); // nonce (implementation), nonce+1 (proxy) + }); // nonce + 4 (implementation), nonce + 5 (proxy) // Deploy StakeController (only if not already deployed) - const stakeController = await getContractOrDeployUpgradable(hre, "StakeController", { + const devnet = isDevnet(hre.network); + const minStakingTime = devnet ? 180 : 1800; + const maxDrawingTime = devnet ? 600 : 1800; + const rng = (await ethers.getContract("ChainlinkRNG")) as ChainlinkRNG; + const stakeController = await deployUpgradable(deployments, "StakeController", { from: deployer, args: [ deployer, klerosCoreV2Address, - pnkVault.address, + vault.address, sortitionModuleV2.address, rng.target, minStakingTime, @@ -85,7 +88,7 @@ const deployArbitrationV2: DeployFunction = async (hre: HardhatRuntimeEnvironmen RNG_LOOKAHEAD, ], log: true, - }); + }); // nonce + 6 (implementation), nonce + 7 (proxy) const minStake = PNK(200); const alpha = 10000; @@ -93,19 +96,19 @@ const deployArbitrationV2: DeployFunction = async (hre: HardhatRuntimeEnvironmen const jurorsForCourtJump = 256; // Deploy KlerosCoreV2 (only if not already deployed) - const klerosCoreV2 = await getContractOrDeployUpgradable(hre, "KlerosCoreV2", { + const klerosCoreV2 = await deployUpgradable(deployments, "KlerosCoreV2", { from: deployer, args: [ deployer, deployer, pnk.target, - ZeroAddress, // KlerosCoreV2 is configured later + ZeroAddress, // JurorProsecutionModule, not implemented yet disputeKit.address, false, [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 - stakeController.target, + stakeController.address, ], log: true, }); @@ -113,43 +116,27 @@ const deployArbitrationV2: DeployFunction = async (hre: HardhatRuntimeEnvironmen // Configure cross-dependencies console.log("Configuring cross-dependencies..."); - // Configure stPNK token to allow PNKVault operations - const stPNKContract = await ethers.getContractAt("stPNK", stPNK.address); - const currentVault = await stPNKContract.vault(); - if (currentVault !== pnkVault.address) { - console.log(`stPNK.changeVault(${pnkVault.address})`); - await stPNKContract.changeVault(pnkVault.address); - } - // disputeKit.changeCore() only if necessary const disputeKitContract = (await ethers.getContract("DisputeKitClassicV2")) as DisputeKitClassic; const currentCore = await disputeKitContract.core(); - if (currentCore !== klerosCoreV2.target) { - console.log(`disputeKit.changeCore(${klerosCoreV2.target})`); - await disputeKitContract.changeCore(klerosCoreV2.target); + if (currentCore !== klerosCoreV2.address) { + console.log(`disputeKit.changeCore(${klerosCoreV2.address})`); + await disputeKitContract.changeCore(klerosCoreV2.address); } // rng.changeSortitionModule() only if necessary + // Note: the RNG's `sortitionModule` variable is misleading, it's only for access control and should be renamed to `consumer`. const rngSortitionModule = await rng.sortitionModule(); - if (rngSortitionModule !== stakeController.target) { - console.log(`rng.changeSortitionModule(${stakeController.target})`); - await rng.changeSortitionModule(stakeController.target); + if (rngSortitionModule !== stakeController.address) { + console.log(`rng.changeSortitionModule(${stakeController.address})`); + await rng.changeSortitionModule(stakeController.address); } const core = (await hre.ethers.getContract("KlerosCoreV2")) as KlerosCoreV2; try { - // Manually set currency rates since changeCurrencyRate helper doesn't support V2 types yet - console.log("Setting PNK currency rate..."); - await core.changeAcceptedFeeTokens(await pnk.getAddress(), true); - await core.changeCurrencyRates(await pnk.getAddress(), 12225583, 12); - - console.log("Setting DAI currency rate..."); - await core.changeAcceptedFeeTokens(await dai.getAddress(), true); - await core.changeCurrencyRates(await dai.getAddress(), 60327783, 11); - - console.log("Setting WETH currency rate..."); - await core.changeAcceptedFeeTokens(await weth.getAddress(), true); - await core.changeCurrencyRates(await weth.getAddress(), 1, 1); + await changeCurrencyRate(core, await pnk.getAddress(), true, 12225583, 12); + await changeCurrencyRate(core, await dai.getAddress(), true, 60327783, 11); + await changeCurrencyRate(core, await weth.getAddress(), true, 1, 1); } catch (e) { console.error("failed to change currency rates:", e); } @@ -162,11 +149,11 @@ const deployArbitrationV2: DeployFunction = async (hre: HardhatRuntimeEnvironmen }); console.log("✅ V2 Architecture deployment completed successfully!"); - console.log(`📦 PNKVault: ${pnkVault.address}`); + console.log(`📦 Vault: ${vault.address}`); console.log(`🎫 stPNK: ${stPNK.address}`); console.log(`🎯 SortitionModuleV2: ${sortitionModuleV2.address}`); - console.log(`🎮 StakeController: ${stakeController.target}`); - console.log(`⚖️ KlerosCoreV2: ${klerosCoreV2.target}`); + console.log(`🎮 StakeController: ${stakeController.address}`); + console.log(`⚖️ KlerosCoreV2: ${klerosCoreV2.address}`); }; deployArbitrationV2.tags = ["ArbitrationV2"]; diff --git a/contracts/deploy/utils/klerosCoreHelper.ts b/contracts/deploy/utils/klerosCoreHelper.ts index 3419ae64e..8da7d45ad 100644 --- a/contracts/deploy/utils/klerosCoreHelper.ts +++ b/contracts/deploy/utils/klerosCoreHelper.ts @@ -1,8 +1,8 @@ -import { KlerosCore, KlerosCoreNeo, KlerosCoreRuler, KlerosCoreUniversity } from "../../typechain-types"; +import { KlerosCore, KlerosCoreNeo, KlerosCoreRuler, KlerosCoreUniversity, KlerosCoreV2 } from "../../typechain-types"; import { BigNumberish, toBigInt } from "ethers"; export const changeCurrencyRate = async ( - core: KlerosCore | KlerosCoreNeo | KlerosCoreRuler | KlerosCoreUniversity, + core: KlerosCore | KlerosCoreNeo | KlerosCoreRuler | KlerosCoreUniversity | KlerosCoreV2, erc20: string, accepted: boolean, rateInEth: BigNumberish, diff --git a/contracts/package.json b/contracts/package.json index 0b4d3dd75..e8532680e 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -157,6 +157,7 @@ "@chainlink/contracts": "^1.3.0", "@kleros/vea-contracts": "^0.6.0", "@openzeppelin/contracts": "^5.2.0", + "@openzeppelin/contracts-upgradeable": "^5.2.0", "viem": "^2.24.1" } } diff --git a/contracts/src/arbitration/interfaces/IStakeController.sol b/contracts/src/arbitration/interfaces/IStakeController.sol index ab1a3b240..616d21a27 100644 --- a/contracts/src/arbitration/interfaces/IStakeController.sol +++ b/contracts/src/arbitration/interfaces/IStakeController.sol @@ -3,17 +3,28 @@ pragma solidity 0.8.24; import {ISortitionModule} from "./ISortitionModule.sol"; +import {KlerosCoreV2Base} from "../core-v2/KlerosCoreV2Base.sol"; import "../../libraries/Constants.sol"; /// @title IStakeController -/// @notice Interface for the Stake Controller that coordinates between PNKVault and SortitionModule +/// @notice Interface for the Stake Controller that coordinates between Vault and SortitionModule /// @dev Combines phase management, delayed stakes, and coordination between vault and sortition interface IStakeController { + // ************************************* // + // * Enums * // + // ************************************* // + + enum Phase { + staking, // Stake sum trees can be updated. Pass after `minStakingTime` passes and there is at least one dispute without jurors. + generating, // Waiting for a random number. Pass as soon as it is ready. + drawing // Jurors can be drawn. Pass after all disputes have jurors or `maxDrawingTime` passes. + } + // ************************************* // // * Events * // // ************************************* // - event NewPhase(ISortitionModule.Phase phase); + event NewPhase(Phase phase); event JurorPenaltyExecuted(address indexed account, uint256 requestedPenalty, uint256 actualPenalty); event StakeUnlocked(address indexed account, uint256 amount); event JurorSetInactive(address indexed account); @@ -22,7 +33,7 @@ interface IStakeController { event StakeImported(address indexed account, uint96 indexed courtID, uint256 stake); event DelayedStakeImported(address indexed account, uint96 indexed courtID, uint256 stake, uint256 index); event MigrationCompleted(uint256 totalAttempted, uint256 totalImported); - event PhaseStateMigrated(ISortitionModule.Phase phase, uint256 lastPhaseChange, uint256 disputesWithoutJurors); + event PhaseStateMigrated(Phase phase, uint256 lastPhaseChange, uint256 disputesWithoutJurors); event EmergencyReset(uint256 timestamp); // ************************************* // @@ -34,7 +45,7 @@ interface IStakeController { /// @notice Get the current phase /// @return The current phase - function getPhase() external view returns (ISortitionModule.Phase); + function getPhase() external view returns (Phase); /// @notice Execute delayed stakes during staking phase /// @param _iterations The number of delayed stakes to execute @@ -97,6 +108,16 @@ interface IStakeController { uint256 _repartition ) external view returns (bool shouldSet); + /// @notice Create dispute hook + /// @param _disputeID The dispute ID + /// @param _roundID The round ID + function createDisputeHook(uint256 _disputeID, uint256 _roundID) external; + + /// @notice Post draw hook + /// @param _disputeID The dispute ID + /// @param _roundID The round ID + function postDrawHook(uint256 _disputeID, uint256 _roundID) external; + // ************************************* // // * Sortition Delegation * // // ************************************* // @@ -113,20 +134,6 @@ interface IStakeController { /// @return The drawn juror address function draw(bytes32 _court, uint256 _coreDisputeID, uint256 _nonce) external view returns (address); - /// @notice Create dispute hook (delegated to SortitionModule) - /// @param _disputeID The dispute ID - /// @param _roundID The round ID - function createDisputeHook(uint256 _disputeID, uint256 _roundID) external; - - /// @notice Post draw hook (delegated to SortitionModule) - /// @param _disputeID The dispute ID - /// @param _roundID The round ID - function postDrawHook(uint256 _disputeID, uint256 _roundID) external; - - /// @notice Notify random number (delegated to SortitionModule) - /// @param _drawnNumber The random number - function notifyRandomNumber(uint256 _drawnNumber) external; - // ************************************* // // * View Functions * // // ************************************* // @@ -162,4 +169,8 @@ interface IStakeController { /// @param _account The account to check /// @return The deposited balance function getDepositedBalance(address _account) external view returns (uint256); + + /// @notice Get the core arbitrator contract + /// @return The core contract + function core() external view returns (KlerosCoreV2Base); } diff --git a/contracts/src/arbitration/interfaces/IPNKVault.sol b/contracts/src/arbitration/interfaces/IVault.sol similarity index 98% rename from contracts/src/arbitration/interfaces/IPNKVault.sol rename to contracts/src/arbitration/interfaces/IVault.sol index fba1cc566..8fb8d7bb9 100644 --- a/contracts/src/arbitration/interfaces/IPNKVault.sol +++ b/contracts/src/arbitration/interfaces/IVault.sol @@ -4,9 +4,9 @@ pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -/// @title IPNKVault +/// @title IVault /// @notice Interface for the PNK Vault that handles PNK deposits, withdrawals, locks, and penalties -interface IPNKVault { +interface IVault { // ************************************* // // * Events * // // ************************************* // diff --git a/contracts/src/arbitration/sortition-v2/SortitionModuleV2.sol b/contracts/src/arbitration/sortition-v2/SortitionModuleV2.sol index f5ad9178d..7a64a035e 100644 --- a/contracts/src/arbitration/sortition-v2/SortitionModuleV2.sol +++ b/contracts/src/arbitration/sortition-v2/SortitionModuleV2.sol @@ -23,10 +23,9 @@ contract SortitionModuleV2 is SortitionModuleV2Base { /// @dev Initializer (constructor equivalent for upgradable contracts). /// @param _governor The governor's address. - /// @param _core The KlerosCore contract. /// @param _stakeController The StakeController contract. - function initialize(address _governor, KlerosCore _core, IStakeController _stakeController) external initializer { - __SortitionModuleV2Base_initialize(_governor, _core, _stakeController); + function initialize(address _governor, IStakeController _stakeController) external initializer { + __SortitionModuleV2Base_initialize(_governor, _stakeController); } // ************************************* // diff --git a/contracts/src/arbitration/sortition-v2/SortitionModuleV2Base.sol b/contracts/src/arbitration/sortition-v2/SortitionModuleV2Base.sol index d4483d703..7b43770df 100644 --- a/contracts/src/arbitration/sortition-v2/SortitionModuleV2Base.sol +++ b/contracts/src/arbitration/sortition-v2/SortitionModuleV2Base.sol @@ -12,7 +12,7 @@ pragma solidity 0.8.24; import {ISortitionModuleV2} from "../interfaces/ISortitionModuleV2.sol"; import {IStakeController} from "../interfaces/IStakeController.sol"; -import {KlerosCore} from "../KlerosCore.sol"; +import {KlerosCoreV2Base} from "../core-v2/KlerosCoreV2Base.sol"; import {Initializable} from "../../proxy/Initializable.sol"; import {UUPSProxiable} from "../../proxy/UUPSProxiable.sol"; import "../../libraries/Constants.sol"; @@ -45,7 +45,6 @@ abstract contract SortitionModuleV2Base is ISortitionModuleV2, Initializable, UU // ************************************* // address public governor; // The governor of the contract. - KlerosCore public core; // The core arbitrator contract. IStakeController public stakeController; // The stake controller for coordination. mapping(bytes32 treeHash => SortitionSumTree) internal sortitionSumTrees; // The mapping trees by keys. @@ -65,22 +64,15 @@ abstract contract SortitionModuleV2Base is ISortitionModuleV2, Initializable, UU _; } - modifier onlyByCoreOrController() { - if (address(core) != msg.sender && address(stakeController) != msg.sender) revert CoreOrStakeControllerOnly(); - _; - } - // ************************************* // // * Constructor * // // ************************************* // function __SortitionModuleV2Base_initialize( address _governor, - KlerosCore _core, IStakeController _stakeController ) internal onlyInitializing { governor = _governor; - core = _core; stakeController = _stakeController; } @@ -105,7 +97,7 @@ abstract contract SortitionModuleV2Base is ISortitionModuleV2, Initializable, UU // ************************************* // /// @inheritdoc ISortitionModuleV2 - function createTree(bytes32 _key, bytes memory _extraData) external override onlyByCoreOrController { + function createTree(bytes32 _key, bytes memory _extraData) external override onlyByStakeController { SortitionSumTree storage tree = sortitionSumTrees[_key]; uint256 K = _extraDataToTreeK(_extraData); if (tree.K != 0) revert TreeAlreadyExists(); @@ -260,7 +252,7 @@ abstract contract SortitionModuleV2Base is ISortitionModuleV2, Initializable, UU bytes32 stakePathID = _accountAndCourtIDToStakePathID(_account, _courtID); bool finished = false; uint96 currentCourtID = _courtID; - + KlerosCoreV2Base core = stakeController.core(); while (!finished) { // Tokens are also implicitly staked in parent courts through sortition module to increase the chance of being drawn. _set(bytes32(uint256(currentCourtID)), _newStake, stakePathID); @@ -443,7 +435,6 @@ abstract contract SortitionModuleV2Base is ISortitionModuleV2, Initializable, UU error GovernorOnly(); error StakeControllerOnly(); - error CoreOrStakeControllerOnly(); error TreeAlreadyExists(); error InvalidTreeK(); } diff --git a/contracts/src/arbitration/sortition-v2/SortitionModuleV2Neo.sol b/contracts/src/arbitration/sortition-v2/SortitionModuleV2Neo.sol index 9128a4753..69ed92a86 100644 --- a/contracts/src/arbitration/sortition-v2/SortitionModuleV2Neo.sol +++ b/contracts/src/arbitration/sortition-v2/SortitionModuleV2Neo.sol @@ -39,18 +39,16 @@ contract SortitionModuleV2Neo is SortitionModuleV2Base { /// @dev Initializer (constructor equivalent for upgradable contracts). /// @param _governor The governor's address. - /// @param _core The KlerosCore contract. /// @param _stakeController The StakeController contract. /// @param _maxStakePerJuror Maximum stake amount per juror. /// @param _maxTotalStakedInCourt Maximum total stake per court. function initialize( address _governor, - KlerosCore _core, IStakeController _stakeController, uint256 _maxStakePerJuror, uint256 _maxTotalStakedInCourt ) external initializer { - __SortitionModuleV2Base_initialize(_governor, _core, _stakeController); + __SortitionModuleV2Base_initialize(_governor, _stakeController); maxStakePerJuror = _maxStakePerJuror; maxTotalStakedInCourt = _maxTotalStakedInCourt; diff --git a/contracts/src/arbitration/stPNK.sol b/contracts/src/arbitration/stPNK.sol index 1940af155..374b6eeff 100644 --- a/contracts/src/arbitration/stPNK.sol +++ b/contracts/src/arbitration/stPNK.sol @@ -2,24 +2,25 @@ pragma solidity 0.8.24; -import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import {Initializable} from "../proxy/Initializable.sol"; +import {UUPSProxiable} from "../proxy/UUPSProxiable.sol"; /// @title stPNK -/// @notice Non-transferable liquid staking token representing staked PNK +/// @notice Non-transferable staking token representing staked PNK /// @dev Only transferable within Kleros protocol contracts to prevent external trading /// @custom:security-contact contact@kleros.io -contract stPNK is ERC20 { +contract stPNK is ERC20Upgradeable, UUPSProxiable { + string public constant override version = "1.0.0"; + // ************************************* // // * Storage * // // ************************************* // - address public immutable vault; - - // Whitelist of protocol contracts that can receive stPNK - mapping(address => bool) public protocolContracts; - - // Track if an address is a regular user (not a protocol contract) - mapping(address => bool) public isUser; + address public governor; + address public vault; + mapping(address => bool) public protocolContracts; // Whitelist of protocol contracts that can receive stPNK + mapping(address => bool) public isUser; // Track if an address is a regular user (not a protocol contract) // ************************************* // // * Events * // @@ -27,19 +28,15 @@ contract stPNK is ERC20 { event ProtocolContractUpdated(address indexed contract_, bool allowed); - // ************************************* // - // * Errors * // - // ************************************* // - - error TransferNotAllowed(); - error OnlyVault(); - error OnlyProtocolContracts(); - error ArrayLengthMismatch(); - // ************************************* // // * Function Modifiers * // // ************************************* // + modifier onlyByGovernor() { + if (governor != msg.sender) revert GovernorOnly(); + _; + } + modifier onlyVault() { if (msg.sender != vault) revert OnlyVault(); _; @@ -54,11 +51,40 @@ contract stPNK is ERC20 { // * Constructor * // // ************************************* // - constructor(address _vault) ERC20("Staked Pinakion", "stPNK") { + constructor() { + _disableInitializers(); + } + + /// @notice Initialize the stPNK token + /// @param _governor The governor address + /// @param _vault The vault address + function initialize(address _governor, address _vault) external initializer { + __ERC20_init("Staked Pinakion", "stPNK"); + governor = _governor; vault = _vault; + protocolContracts[_vault] = true; // Automatically whitelist vault + } - // Automatically whitelist vault - protocolContracts[_vault] = true; + // ************************************* // + // * Governance * // + // ************************************* // + + /// @dev Access Control to perform implementation upgrades (UUPS Proxiable) + /// Only the governor can perform upgrades (`onlyByGovernor`) + function _authorizeUpgrade(address) internal view override onlyByGovernor { + // NOP + } + + /// @notice Change the governor address + /// @param _governor The new governor address + function changeGovernor(address _governor) external onlyByGovernor { + governor = _governor; + } + + /// @notice Change the vault address + /// @param _vault The new vault address + function changeVault(address _vault) external onlyByGovernor { + vault = _vault; } // ************************************* // @@ -90,6 +116,7 @@ contract stPNK is ERC20 { if (_contracts.length != _allowed.length) revert ArrayLengthMismatch(); for (uint256 i = 0; i < _contracts.length; i++) { + if (_contracts[i] == address(0)) continue; protocolContracts[_contracts[i]] = _allowed[i]; emit ProtocolContractUpdated(_contracts[i], _allowed[i]); } @@ -148,4 +175,14 @@ contract stPNK is ERC20 { // Block all other transfers (user-to-user, external contracts) revert TransferNotAllowed(); } + + // ************************************* // + // * Errors * // + // ************************************* // + + error GovernorOnly(); + error TransferNotAllowed(); + error OnlyVault(); + error OnlyProtocolContracts(); + error ArrayLengthMismatch(); } diff --git a/contracts/src/arbitration/stake-controller/StakeController.sol b/contracts/src/arbitration/stake-controller/StakeController.sol index 1bbaaa0de..e7993e809 100644 --- a/contracts/src/arbitration/stake-controller/StakeController.sol +++ b/contracts/src/arbitration/stake-controller/StakeController.sol @@ -3,14 +3,14 @@ pragma solidity 0.8.24; import {StakeControllerBase} from "./StakeControllerBase.sol"; -import {IPNKVault} from "../interfaces/IPNKVault.sol"; -import {ISortitionModule} from "../interfaces/ISortitionModule.sol"; -import {KlerosCore} from "../KlerosCore.sol"; +import {IVault} from "../interfaces/IVault.sol"; +import {ISortitionModuleV2} from "../interfaces/ISortitionModuleV2.sol"; +import {KlerosCoreV2Base} from "../core-v2/KlerosCoreV2Base.sol"; import {RNG} from "../../rng/RNG.sol"; /// @title StakeController /// @notice Basic implementation of the Stake Controller -/// @dev Coordinates between PNKVault and SortitionModule for the new architecture +/// @dev Coordinates between Vault and SortitionModule for the new architecture contract StakeController is StakeControllerBase { string public constant override version = "1.0.0"; @@ -26,7 +26,7 @@ contract StakeController is StakeControllerBase { /// @dev Initializer (constructor equivalent for upgradable contracts). /// @param _governor The governor's address. /// @param _core The KlerosCore contract. - /// @param _vault The PNKVault contract. + /// @param _vault The Vault contract. /// @param _sortitionModule The SortitionModule contract. /// @param _minStakingTime The minimum staking time. /// @param _maxDrawingTime The maximum drawing time. @@ -34,9 +34,9 @@ contract StakeController is StakeControllerBase { /// @param _rngLookahead The RNG lookahead time. function initialize( address _governor, - KlerosCore _core, - IPNKVault _vault, - ISortitionModule _sortitionModule, + KlerosCoreV2Base _core, + IVault _vault, + ISortitionModuleV2 _sortitionModule, uint256 _minStakingTime, uint256 _maxDrawingTime, RNG _rng, diff --git a/contracts/src/arbitration/stake-controller/StakeControllerBase.sol b/contracts/src/arbitration/stake-controller/StakeControllerBase.sol index d3d245a06..c68901caa 100644 --- a/contracts/src/arbitration/stake-controller/StakeControllerBase.sol +++ b/contracts/src/arbitration/stake-controller/StakeControllerBase.sol @@ -3,17 +3,17 @@ pragma solidity 0.8.24; import {IStakeController} from "../interfaces/IStakeController.sol"; -import {IPNKVault} from "../interfaces/IPNKVault.sol"; -import {ISortitionModule} from "../interfaces/ISortitionModule.sol"; +import {IVault} from "../interfaces/IVault.sol"; +import {ISortitionModuleV2} from "../interfaces/ISortitionModuleV2.sol"; import {IDisputeKit} from "../interfaces/IDisputeKit.sol"; -import {KlerosCore} from "../KlerosCore.sol"; +import {KlerosCoreV2Base} from "../core-v2/KlerosCoreV2Base.sol"; import {Initializable} from "../../proxy/Initializable.sol"; import {UUPSProxiable} from "../../proxy/UUPSProxiable.sol"; import {RNG} from "../../rng/RNG.sol"; import "../../libraries/Constants.sol"; /// @title StakeControllerBase -/// @notice Abstract base contract for coordinating between PNKVault and SortitionModule +/// @notice Abstract base contract for coordinating between Vault and SortitionModule /// @dev Manages phases, delayed stakes, and coordination logic abstract contract StakeControllerBase is IStakeController, Initializable, UUPSProxiable { // ************************************* // @@ -32,12 +32,12 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr // ************************************* // address public governor; // The governor of the contract. - KlerosCore public core; // The core arbitrator contract. - IPNKVault public vault; // The PNK vault for token management. - ISortitionModule public sortitionModule; // The sortition module for drawing logic. + KlerosCoreV2Base public core; // The core arbitrator contract. + IVault public vault; // The PNK vault for token management. + ISortitionModuleV2 public sortitionModule; // The sortition module for drawing logic. // Phase management (moved from SortitionModule) - ISortitionModule.Phase public phase; // The current phase. + Phase public phase; // The current phase. uint256 public minStakingTime; // The time after which the phase can be switched to Drawing if there are open disputes. uint256 public maxDrawingTime; // The time after which the phase can be switched back to Staking. uint256 public lastPhaseChange; // The last time the phase was changed. @@ -73,9 +73,9 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr function __StakeControllerBase_initialize( address _governor, - KlerosCore _core, - IPNKVault _vault, - ISortitionModule _sortitionModule, + KlerosCoreV2Base _core, + IVault _vault, + ISortitionModuleV2 _sortitionModule, uint256 _minStakingTime, uint256 _maxDrawingTime, RNG _rng, @@ -105,13 +105,13 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr /// @dev Changes the `vault` storage variable. /// @param _vault The new vault address. - function changeVault(IPNKVault _vault) external onlyByGovernor { + function changeVault(IVault _vault) external onlyByGovernor { vault = _vault; } /// @dev Changes the `sortitionModule` storage variable. /// @param _sortitionModule The new sortition module address. - function changeSortitionModule(ISortitionModule _sortitionModule) external onlyByGovernor { + function changeSortitionModule(ISortitionModuleV2 _sortitionModule) external onlyByGovernor { sortitionModule = _sortitionModule; } @@ -133,7 +133,7 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr function changeRandomNumberGenerator(RNG _rng, uint256 _rngLookahead) external onlyByGovernor { rng = _rng; rngLookahead = _rngLookahead; - if (phase == ISortitionModule.Phase.generating) { + if (phase == Phase.generating) { rng.requestRandomness(block.number + rngLookahead); randomNumberRequestBlock = block.number; } @@ -145,21 +145,21 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr /// @inheritdoc IStakeController function passPhase() external override { - if (phase == ISortitionModule.Phase.staking) { + if (phase == Phase.staking) { if (block.timestamp - lastPhaseChange < minStakingTime) revert MinStakingTimeNotPassed(); if (disputesWithoutJurors == 0) revert NoDisputesNeedingJurors(); rng.requestRandomness(block.number + rngLookahead); randomNumberRequestBlock = block.number; - phase = ISortitionModule.Phase.generating; - } else if (phase == ISortitionModule.Phase.generating) { + phase = Phase.generating; + } else if (phase == Phase.generating) { randomNumber = rng.receiveRandomness(randomNumberRequestBlock + rngLookahead); if (randomNumber == 0) revert RandomNumberNotReady(); - phase = ISortitionModule.Phase.drawing; - } else if (phase == ISortitionModule.Phase.drawing) { + phase = Phase.drawing; + } else if (phase == Phase.drawing) { if (disputesWithoutJurors > 0 && block.timestamp - lastPhaseChange < maxDrawingTime) { revert StillDrawingDisputes(); } - phase = ISortitionModule.Phase.staking; + phase = Phase.staking; } lastPhaseChange = block.timestamp; @@ -167,13 +167,13 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr } /// @inheritdoc IStakeController - function getPhase() external view override returns (ISortitionModule.Phase) { + function getPhase() external view override returns (Phase) { return phase; } /// @inheritdoc IStakeController function executeDelayedStakes(uint256 _iterations) external override { - if (phase != ISortitionModule.Phase.staking) revert NotInStakingPhase(); + if (phase != Phase.staking) revert NotInStakingPhase(); if (delayedStakeWriteIndex < delayedStakeReadIndex) revert NoDelayedStakes(); uint256 actualIterations = (delayedStakeReadIndex + _iterations) - 1 > delayedStakeWriteIndex @@ -218,18 +218,28 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr } /// @dev Internal implementation of setStake with phase-aware delayed stake logic + function _setStake( + address _account, + uint96 _courtID, + uint256 _newStake + ) internal virtual returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { + return _setStake(_account, _courtID, _newStake, false); + } + + /// @dev Internal implementation of setStake with phase-aware delayed stake logic + /// TODO: REMOVE THE INSTANT STAKING LOGIC ! function _setStake( address _account, uint96 _courtID, uint256 _newStake, bool _alreadyTransferred ) internal virtual returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { - (, , uint256 currentStake, ) = sortitionModule.getJurorBalance(_account, _courtID); + (, , uint256 currentStake, ) = sortitionModule.getJurorInfo(_account, _courtID); // Delete any existing delayed stake for this juror/court pnkWithdrawal = _deleteDelayedStake(_courtID, _account); - if (phase != ISortitionModule.Phase.staking) { + if (phase != Phase.staking) { // Store the stake change as delayed, to be applied when the phase switches back to Staking. DelayedStake storage delayedStake = delayedStakes[++delayedStakeWriteIndex]; delayedStake.account = _account; @@ -239,7 +249,7 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr if (_newStake > currentStake) { // PNK deposit: tokens are transferred now via vault coordination - delayedStake.alreadyTransferred = true; + delayedStake.alreadyTransferred = _alreadyTransferred || true; pnkDeposit = _newStake - currentStake; // Note: Actual PNK transfer is handled by KlerosCore through vault } else { @@ -250,7 +260,18 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr } // Current phase is Staking: set normal stakes through sortition module - return sortitionModule.setStake(_account, _courtID, _newStake, _alreadyTransferred); + // SortitionModule returns bool success, we need to convert to expected format + bool success = sortitionModule.setStake(_account, _courtID, _newStake); + if (success) { + if (_newStake > currentStake) { + pnkDeposit = _newStake - currentStake; + } else { + pnkWithdrawal = currentStake - _newStake; + } + return (pnkDeposit, pnkWithdrawal, StakingResult.Successful); + } else { + return (0, 0, StakingResult.StakingTransferFailed); + } } /// @inheritdoc IStakeController @@ -286,10 +307,10 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr uint96[] memory courtIds = sortitionModule.getJurorCourtIDs(_account); for (uint256 i = 0; i < courtIds.length; i++) { - (, , uint256 currentStake, ) = sortitionModule.getJurorBalance(_account, courtIds[i]); + (, , uint256 currentStake, ) = sortitionModule.getJurorInfo(_account, courtIds[i]); if (currentStake > 0) { // Set stake to 0 in sortition module to remove from trees - sortitionModule.setStake(_account, courtIds[i], 0, true); + sortitionModule.setStake(_account, courtIds[i], 0); } } @@ -325,24 +346,17 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr /// @inheritdoc IStakeController function draw(bytes32 _court, uint256 _coreDisputeID, uint256 _nonce) external view override returns (address) { - return sortitionModule.draw(_court, _coreDisputeID, _nonce); + return sortitionModule.draw(_court, _coreDisputeID, _nonce, randomNumber); } /// @inheritdoc IStakeController function createDisputeHook(uint256 _disputeID, uint256 _roundID) external override onlyByCore { disputesWithoutJurors++; - sortitionModule.createDisputeHook(_disputeID, _roundID); } /// @inheritdoc IStakeController function postDrawHook(uint256 _disputeID, uint256 _roundID) external override onlyByCore { disputesWithoutJurors--; - sortitionModule.postDrawHook(_disputeID, _roundID); - } - - /// @inheritdoc IStakeController - function notifyRandomNumber(uint256 _drawnNumber) external override { - sortitionModule.notifyRandomNumber(_drawnNumber); } // ************************************* // @@ -359,7 +373,7 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr override returns (uint256 totalStaked, uint256 totalLocked, uint256 stakedInCourt, uint256 nbCourts) { - return sortitionModule.getJurorBalance(_juror, _courtID); + return sortitionModule.getJurorInfo(_juror, _courtID); } /// @inheritdoc IStakeController @@ -369,7 +383,7 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr /// @inheritdoc IStakeController function isJurorStaked(address _juror) external view override returns (bool) { - return sortitionModule.isJurorStaked(_juror); + return sortitionModule.hasStakes(_juror); } /// @inheritdoc IStakeController @@ -432,7 +446,7 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr DelayedStake storage delayedStake = delayedStakes[latestIndex]; if (delayedStake.alreadyTransferred) { // Calculate amount to withdraw based on the difference - (, , uint256 sortitionStake, ) = sortitionModule.getJurorBalance(_juror, _courtID); + (, , uint256 sortitionStake, ) = sortitionModule.getJurorInfo(_juror, _courtID); uint256 amountToWithdraw = delayedStake.stake > sortitionStake ? delayedStake.stake - sortitionStake : 0; @@ -466,8 +480,8 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr for (uint256 i = 0; i < _accounts.length; i++) { if (_stakes[i] > 0) { // Direct stake import bypassing normal validation for migration - (, , StakingResult result) = sortitionModule.setStake(_accounts[i], _courtIDs[i], _stakes[i], true); - if (result == StakingResult.Successful) { + bool success = sortitionModule.setStake(_accounts[i], _courtIDs[i], _stakes[i]); + if (success) { totalImported++; emit StakeImported(_accounts[i], _courtIDs[i], _stakes[i]); } @@ -502,7 +516,7 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr /// @param _lastPhaseChange The last phase change timestamp /// @param _disputesWithoutJurors Number of disputes without jurors function migratePhaseState( - ISortitionModule.Phase _phase, + Phase _phase, uint256 _lastPhaseChange, uint256 _disputesWithoutJurors ) external onlyByGovernor { @@ -515,7 +529,7 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr /// @dev Emergency coordination reset for critical issues function emergencyCoordinationReset() external onlyByGovernor { - phase = ISortitionModule.Phase.staking; + phase = Phase.staking; lastPhaseChange = block.timestamp; disputesWithoutJurors = 0; randomNumber = 0; diff --git a/contracts/src/arbitration/stake-controller/StakeControllerNeo.sol b/contracts/src/arbitration/stake-controller/StakeControllerNeo.sol index a669b0b4e..d9172d2a7 100644 --- a/contracts/src/arbitration/stake-controller/StakeControllerNeo.sol +++ b/contracts/src/arbitration/stake-controller/StakeControllerNeo.sol @@ -3,15 +3,15 @@ pragma solidity 0.8.24; import {StakeControllerBase} from "./StakeControllerBase.sol"; -import {IPNKVault} from "../interfaces/IPNKVault.sol"; -import {ISortitionModule} from "../interfaces/ISortitionModule.sol"; +import {IVault} from "../interfaces/IVault.sol"; +import {ISortitionModuleV2} from "../interfaces/ISortitionModuleV2.sol"; import {IDisputeKit} from "../interfaces/IDisputeKit.sol"; -import {KlerosCore} from "../KlerosCore.sol"; +import {KlerosCoreV2Base} from "../core-v2/KlerosCoreV2Base.sol"; import {RNG} from "../../rng/RNG.sol"; /// @title StakeControllerNeo /// @notice Enhanced implementation of the Stake Controller with additional features -/// @dev Coordinates between PNKVault and SortitionModule with improved penalty logic +/// @dev Coordinates between Vault and SortitionModule with improved penalty logic contract StakeControllerNeo is StakeControllerBase { string public constant override version = "1.0.0"; @@ -50,7 +50,7 @@ contract StakeControllerNeo is StakeControllerBase { /// @dev Initializer (constructor equivalent for upgradable contracts). /// @param _governor The governor's address. /// @param _core The KlerosCore contract. - /// @param _vault The PNKVault contract. + /// @param _vault The Vault contract. /// @param _sortitionModule The SortitionModule contract. /// @param _minStakingTime The minimum staking time. /// @param _maxDrawingTime The maximum drawing time. @@ -60,9 +60,9 @@ contract StakeControllerNeo is StakeControllerBase { /// @param _penaltyCooldownBlocks Minimum blocks between penalties. function initialize( address _governor, - KlerosCore _core, - IPNKVault _vault, - ISortitionModule _sortitionModule, + KlerosCoreV2Base _core, + IVault _vault, + ISortitionModuleV2 _sortitionModule, uint256 _minStakingTime, uint256 _maxDrawingTime, RNG _rng, diff --git a/contracts/src/arbitration/vault/PNKVault.sol b/contracts/src/arbitration/vault/Vault.sol similarity index 87% rename from contracts/src/arbitration/vault/PNKVault.sol rename to contracts/src/arbitration/vault/Vault.sol index 852aec5df..e38204e46 100644 --- a/contracts/src/arbitration/vault/PNKVault.sol +++ b/contracts/src/arbitration/vault/Vault.sol @@ -2,12 +2,12 @@ pragma solidity 0.8.24; -import {PNKVaultBase, IERC20} from "./PNKVaultBase.sol"; +import {VaultBase, IERC20, stPNK} from "./VaultBase.sol"; -/// @title PNKVault +/// @title Vault /// @notice PNK Vault for handling deposits, withdrawals, locks, and penalties /// @dev Follows the same pattern as KlerosCore for upgradeable contracts -contract PNKVault is PNKVaultBase { +contract Vault is VaultBase { string public constant override version = "1.0.0"; // ************************************* // @@ -27,10 +27,11 @@ contract PNKVault is PNKVaultBase { function initialize( address _governor, IERC20 _pnk, + stPNK _stPnk, address _stakeController, address _core ) external reinitializer(1) { - __PNKVaultBase_initialize(_governor, _pnk, _stakeController, _core); + __VaultBase_initialize(_governor, _pnk, _stPnk, _stakeController, _core); } // ************************************* // diff --git a/contracts/src/arbitration/vault/PNKVaultBase.sol b/contracts/src/arbitration/vault/VaultBase.sol similarity index 86% rename from contracts/src/arbitration/vault/PNKVaultBase.sol rename to contracts/src/arbitration/vault/VaultBase.sol index d747f8079..1c2fec48d 100644 --- a/contracts/src/arbitration/vault/PNKVaultBase.sol +++ b/contracts/src/arbitration/vault/VaultBase.sol @@ -2,16 +2,16 @@ pragma solidity 0.8.24; -import {IPNKVault} from "../interfaces/IPNKVault.sol"; +import {IVault} from "../interfaces/IVault.sol"; import {stPNK} from "../stPNK.sol"; import {Initializable} from "../../proxy/Initializable.sol"; import {UUPSProxiable} from "../../proxy/UUPSProxiable.sol"; import {SafeERC20, IERC20} from "../../libraries/SafeERC20.sol"; -/// @title PNKVaultBase +/// @title VaultBase /// @notice Abstract base contract for PNK vault that handles deposits, withdrawals, locks, and penalties /// @dev Follows the same pattern as KlerosCoreBase for upgradeable contracts -abstract contract PNKVaultBase is IPNKVault, Initializable, UUPSProxiable { +abstract contract VaultBase is IVault, Initializable, UUPSProxiable { using SafeERC20 for IERC20; // ************************************* // @@ -30,7 +30,7 @@ abstract contract PNKVaultBase is IPNKVault, Initializable, UUPSProxiable { address public governor; // The governor of the contract. IERC20 public pnk; // The PNK token contract. - stPNK public stPnkToken; // The stPNK token contract. + stPNK public stPnk; // The stPNK token contract. address public stakeController; // The stake controller authorized to lock/unlock/penalize. address public core; // The KlerosCore authorized to transfer rewards. @@ -40,7 +40,7 @@ abstract contract PNKVaultBase is IPNKVault, Initializable, UUPSProxiable { // * Events * // // ************************************* // - // Events are defined in IPNKVault interface + // Events are defined in IVault interface // ************************************* // // * Function Modifiers * // @@ -65,20 +65,19 @@ abstract contract PNKVaultBase is IPNKVault, Initializable, UUPSProxiable { // * Constructor * // // ************************************* // - function __PNKVaultBase_initialize( + function __VaultBase_initialize( address _governor, IERC20 _pnk, + stPNK _stPnk, address _stakeController, address _core ) internal onlyInitializing { governor = _governor; pnk = _pnk; + stPnk = _stPnk; stakeController = _stakeController; core = _core; - // Deploy stPNK token - stPnkToken = new stPNK(address(this)); - // Add stakeController and core as protocol contracts in stPNK address[] memory contracts = new address[](2); bool[] memory allowed = new bool[](2); @@ -86,7 +85,7 @@ abstract contract PNKVaultBase is IPNKVault, Initializable, UUPSProxiable { contracts[1] = _core; allowed[0] = true; allowed[1] = true; - stPnkToken.setProtocolContracts(contracts, allowed); + stPnk.setProtocolContracts(contracts, allowed); } // ************************************* // @@ -107,7 +106,7 @@ abstract contract PNKVaultBase is IPNKVault, Initializable, UUPSProxiable { bool[] memory allowed = new bool[](1); contracts[0] = _stakeController; allowed[0] = true; - stPnkToken.setProtocolContracts(contracts, allowed); + stPnk.setProtocolContracts(contracts, allowed); } /// @dev Changes the `core` storage variable. @@ -118,14 +117,14 @@ abstract contract PNKVaultBase is IPNKVault, Initializable, UUPSProxiable { bool[] memory allowed = new bool[](1); contracts[0] = _core; allowed[0] = true; - stPnkToken.setProtocolContracts(contracts, allowed); + stPnk.setProtocolContracts(contracts, allowed); } // ************************************* // // * State Modifiers * // // ************************************* // - /// @inheritdoc IPNKVault + /// @inheritdoc IVault function deposit(uint256 _amount) external virtual override returns (uint256 stPnkAmount) { if (_amount == 0) revert InvalidAmount(); @@ -134,12 +133,12 @@ abstract contract PNKVaultBase is IPNKVault, Initializable, UUPSProxiable { // Mint 1:1 stPNK stPnkAmount = _amount; - stPnkToken.mint(msg.sender, stPnkAmount); + stPnk.mint(msg.sender, stPnkAmount); emit Deposit(msg.sender, _amount); } - /// @inheritdoc IPNKVault + /// @inheritdoc IVault function withdraw(uint256 _amount) external virtual override returns (uint256 pnkAmount) { if (_amount == 0) revert InvalidAmount(); @@ -150,10 +149,10 @@ abstract contract PNKVaultBase is IPNKVault, Initializable, UUPSProxiable { if (_amount > available) revert InsufficientAvailableBalance(); // Check stPNK balance - if (stPnkToken.balanceOf(msg.sender) < _amount) revert InsufficientStPNKBalance(); + if (stPnk.balanceOf(msg.sender) < _amount) revert InsufficientStPNKBalance(); // Burn stPNK and transfer PNK - stPnkToken.burnFrom(msg.sender, _amount); + stPnk.burnFrom(msg.sender, _amount); balance.deposited -= _amount; pnk.safeTransfer(msg.sender, _amount); @@ -161,19 +160,19 @@ abstract contract PNKVaultBase is IPNKVault, Initializable, UUPSProxiable { return _amount; } - /// @inheritdoc IPNKVault + /// @inheritdoc IVault function lockTokens(address _account, uint256 _amount) external virtual override onlyStakeController { jurorBalances[_account].locked += _amount; emit Lock(_account, _amount); } - /// @inheritdoc IPNKVault + /// @inheritdoc IVault function unlockTokens(address _account, uint256 _amount) external virtual override onlyStakeController { jurorBalances[_account].locked -= _amount; emit Unlock(_account, _amount); } - /// @inheritdoc IPNKVault + /// @inheritdoc IVault function applyPenalty( address _account, uint256 _amount @@ -188,17 +187,17 @@ abstract contract PNKVaultBase is IPNKVault, Initializable, UUPSProxiable { balance.penalties += actualPenalty; // Burn equivalent stPNK if user still holds it - uint256 userStPnkBalance = stPnkToken.balanceOf(_account); + uint256 userStPnkBalance = stPnk.balanceOf(_account); uint256 toBurn = actualPenalty > userStPnkBalance ? userStPnkBalance : actualPenalty; if (toBurn > 0) { - stPnkToken.burnFrom(_account, toBurn); + stPnk.burnFrom(_account, toBurn); } // Note: Penalized PNK stays in vault to fund rewards pool emit Penalty(_account, actualPenalty); } - /// @inheritdoc IPNKVault + /// @inheritdoc IVault function transferReward(address _account, uint256 _amount) external virtual override onlyCore { if (pnk.balanceOf(address(this)) < _amount) revert InsufficientVaultBalance(); pnk.safeTransfer(_account, _amount); @@ -209,29 +208,29 @@ abstract contract PNKVaultBase is IPNKVault, Initializable, UUPSProxiable { // * Public Views * // // ************************************* // - /// @inheritdoc IPNKVault + /// @inheritdoc IVault function getAvailableBalance(address _account) public view override returns (uint256) { JurorBalance storage balance = jurorBalances[_account]; uint256 locked = balance.locked + balance.penalties; return balance.deposited > locked ? balance.deposited - locked : 0; } - /// @inheritdoc IPNKVault + /// @inheritdoc IVault function getDepositedBalance(address _account) external view override returns (uint256) { return jurorBalances[_account].deposited; } - /// @inheritdoc IPNKVault + /// @inheritdoc IVault function getLockedBalance(address _account) external view override returns (uint256) { return jurorBalances[_account].locked; } - /// @inheritdoc IPNKVault + /// @inheritdoc IVault function getStPNKBalance(address _account) external view override returns (uint256) { - return stPnkToken.balanceOf(_account); + return stPnk.balanceOf(_account); } - /// @inheritdoc IPNKVault + /// @inheritdoc IVault function getPenaltyBalance(address _account) external view override returns (uint256) { return jurorBalances[_account].penalties; } diff --git a/contracts/src/arbitration/vault/PNKVaultNeo.sol b/contracts/src/arbitration/vault/VaultNeo.sol similarity index 67% rename from contracts/src/arbitration/vault/PNKVaultNeo.sol rename to contracts/src/arbitration/vault/VaultNeo.sol index e1294a391..f5b109ae0 100644 --- a/contracts/src/arbitration/vault/PNKVaultNeo.sol +++ b/contracts/src/arbitration/vault/VaultNeo.sol @@ -2,14 +2,14 @@ pragma solidity 0.8.24; -import {PNKVaultBase, IERC20} from "./PNKVaultBase.sol"; +import {VaultBase, IERC20, stPNK} from "./VaultBase.sol"; import {SafeERC20} from "../../libraries/SafeERC20.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; -/// @title PNKVaultNeo +/// @title VaultNeo /// @notice Enhanced PNK Vault with additional features like NFT-gated deposits /// @dev Follows the same pattern as KlerosCoreNeo for upgradeable contracts -contract PNKVaultNeo is PNKVaultBase { +contract VaultNeo is VaultBase { using SafeERC20 for IERC20; string public constant override version = "1.0.0"; @@ -51,13 +51,14 @@ contract PNKVaultNeo is PNKVaultBase { function initialize( address _governor, IERC20 _pnk, + stPNK _stPnk, address _stakeController, address _core, IERC721 _depositNft, uint256 _maxDepositPerUser, uint256 _totalDepositCap ) external reinitializer(2) { - __PNKVaultBase_initialize(_governor, _pnk, _stakeController, _core); + __VaultBase_initialize(_governor, _pnk, _stPnk, _stakeController, _core); depositNft = _depositNft; maxDepositPerUser = _maxDepositPerUser; @@ -122,77 +123,13 @@ contract PNKVaultNeo is PNKVaultBase { // Update total deposited totalDeposited += _amount; - // Execute deposit logic - if (_amount == 0) revert InvalidAmount(); - - pnk.safeTransferFrom(msg.sender, address(this), _amount); - jurorBalances[msg.sender].deposited += _amount; - - // Mint 1:1 stPNK - stPnkAmount = _amount; - stPnkToken.mint(msg.sender, stPnkAmount); - - emit Deposit(msg.sender, _amount); - return stPnkAmount; + return this.deposit(_amount); } /// @notice Withdraw PNK by burning stPNK and update total deposited function withdraw(uint256 _amount) external override returns (uint256 pnkAmount) { - if (_amount == 0) revert InvalidAmount(); - - JurorBalance storage balance = jurorBalances[msg.sender]; - - // Check available balance (deposited - locked - penalties) - uint256 available = getAvailableBalance(msg.sender); - if (_amount > available) revert InsufficientAvailableBalance(); - - // Check stPNK balance - if (stPnkToken.balanceOf(msg.sender) < _amount) revert InsufficientStPNKBalance(); - - // Burn stPNK and transfer PNK - stPnkToken.burnFrom(msg.sender, _amount); - balance.deposited -= _amount; - pnk.safeTransfer(msg.sender, _amount); - - // Update total deposited totalDeposited -= _amount; - - emit Withdraw(msg.sender, _amount); - return _amount; - } - - // ************************************* // - // * Public Views * // - // ************************************* // - - /// @notice Check if an account is eligible to deposit - /// @param _account The account to check - /// @param _amount The amount they want to deposit - /// @return eligible Whether they can deposit - /// @return reason Reason for ineligibility (empty if eligible) - function checkDepositEligibility( - address _account, - uint256 _amount - ) external view returns (bool eligible, string memory reason) { - // Check NFT requirement - if (address(depositNft) != address(0) && depositNft.balanceOf(_account) == 0) { - return (false, "NFT required for deposit"); - } - - // Check per-user limit - if (maxDepositPerUser > 0) { - uint256 currentUserDeposit = jurorBalances[_account].deposited; - if (currentUserDeposit + _amount > maxDepositPerUser) { - return (false, "Exceeds max deposit per user"); - } - } - - // Check total cap - if (totalDepositCap > 0 && totalDeposited + _amount > totalDepositCap) { - return (false, "Exceeds total deposit cap"); - } - - return (true, ""); + return this.withdraw(_amount); } // ************************************* // diff --git a/yarn.lock b/yarn.lock index 8b3ef2e5c..61ecd1a88 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5559,6 +5559,7 @@ __metadata: "@nomicfoundation/hardhat-ethers": "npm:^3.0.8" "@nomiclabs/hardhat-solhint": "npm:^4.0.1" "@openzeppelin/contracts": "npm:^5.2.0" + "@openzeppelin/contracts-upgradeable": "npm:^5.2.0" "@openzeppelin/upgrades-core": "npm:^1.42.2" "@typechain/ethers-v6": "npm:^0.5.1" "@typechain/hardhat": "npm:^9.1.0" @@ -7390,6 +7391,15 @@ __metadata: languageName: node linkType: hard +"@openzeppelin/contracts-upgradeable@npm:^5.2.0": + version: 5.3.0 + resolution: "@openzeppelin/contracts-upgradeable@npm:5.3.0" + peerDependencies: + "@openzeppelin/contracts": 5.3.0 + checksum: 10/048eb2862aa23dba6f548ba5332e1ec9282c1dd6e73125156c1d918f5e7f48b8a0e0f405d0f7e4e74794d2cea3dd9e7901a146ac315a325730dcd38f99c2a3d9 + languageName: node + linkType: hard + "@openzeppelin/contracts@npm:4.9.6": version: 4.9.6 resolution: "@openzeppelin/contracts@npm:4.9.6" From 70c173be4e3d14fd71e6cedb0fdb5e8eb35bbfe2 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Thu, 5 Jun 2025 01:08:34 +0100 Subject: [PATCH 03/24] refactor: new staking and sortition architecture (wip) --- .../src/arbitration/core-v2/KlerosCoreV2.sol | 10 +- .../arbitration/core-v2/KlerosCoreV2Base.sol | 54 +++-- .../arbitration/core-v2/KlerosCoreV2Neo.sol | 7 +- .../dispute-kits/DisputeKitClassic.sol | 4 +- .../dispute-kits/DisputeKitClassicBase.sol | 24 +-- .../dispute-kits/DisputeKitGated.sol | 4 +- .../dispute-kits/DisputeKitSybilResistant.sol | 4 +- .../interfaces/ISortitionModuleV2.sol | 3 +- .../interfaces/IStakeController.sol | 61 +++--- .../src/arbitration/interfaces/IVault.sol | 6 +- .../sortition-v2/SortitionModuleV2Base.sol | 8 +- .../stake-controller/StakeControllerBase.sol | 198 ++++++++---------- .../stake-controller/StakeControllerNeo.sol | 150 +------------ contracts/src/arbitration/vault/VaultBase.sol | 55 +++-- contracts/src/arbitration/vault/VaultNeo.sol | 12 +- 15 files changed, 233 insertions(+), 367 deletions(-) diff --git a/contracts/src/arbitration/core-v2/KlerosCoreV2.sol b/contracts/src/arbitration/core-v2/KlerosCoreV2.sol index 95ff803c7..3ae194319 100644 --- a/contracts/src/arbitration/core-v2/KlerosCoreV2.sol +++ b/contracts/src/arbitration/core-v2/KlerosCoreV2.sol @@ -22,7 +22,6 @@ contract KlerosCoreV2 is KlerosCoreV2Base { /// @notice Initialization function for UUPS proxy /// @param _governor The governor of the contract. /// @param _guardian The guardian able to pause asset withdrawals. - /// @param _pinakion The Pinakion token contract. /// @param _jurorProsecutionModule The module for juror's prosecution. /// @param _disputeKit The dispute kit responsible for the dispute logic. /// @param _hiddenVotes Whether to use commit and reveal or not. @@ -30,29 +29,30 @@ contract KlerosCoreV2 is KlerosCoreV2Base { /// @param _timesPerPeriod The timesPerPeriod array for courts /// @param _sortitionExtraData Extra data for sortition module setup /// @param _stakeController The stake controller for coordination + /// @param _vault The vault for coordination function initialize( address _governor, address _guardian, - IERC20 _pinakion, address _jurorProsecutionModule, IDisputeKit _disputeKit, bool _hiddenVotes, uint256[4] memory _courtParameters, uint256[4] memory _timesPerPeriod, bytes memory _sortitionExtraData, - IStakeController _stakeController + IStakeController _stakeController, + IVault _vault ) external initializer { __KlerosCoreV2Base_initialize( _governor, _guardian, - _pinakion, _jurorProsecutionModule, _disputeKit, _hiddenVotes, _courtParameters, _timesPerPeriod, _sortitionExtraData, - _stakeController + _stakeController, + _vault ); } diff --git a/contracts/src/arbitration/core-v2/KlerosCoreV2Base.sol b/contracts/src/arbitration/core-v2/KlerosCoreV2Base.sol index d66a6064c..64ad8d6f5 100644 --- a/contracts/src/arbitration/core-v2/KlerosCoreV2Base.sol +++ b/contracts/src/arbitration/core-v2/KlerosCoreV2Base.sol @@ -4,7 +4,8 @@ pragma solidity 0.8.24; import {IArbitrableV2, IArbitratorV2} from "../interfaces/IArbitratorV2.sol"; import {IDisputeKit} from "../interfaces/IDisputeKit.sol"; -import {IStakeController} from "../interfaces/IStakeController.sol"; +import {IStakeController, StakingResult, OnError} from "../interfaces/IStakeController.sol"; +import {IVault} from "../interfaces/IVault.sol"; import {Initializable} from "../../proxy/Initializable.sol"; import {UUPSProxiable} from "../../proxy/UUPSProxiable.sol"; import {SafeERC20, IERC20} from "../../libraries/SafeERC20.sol"; @@ -91,9 +92,9 @@ abstract contract KlerosCoreV2Base is IArbitratorV2, Initializable, UUPSProxiabl address public governor; // The governor of the contract. address public guardian; // The guardian able to pause asset withdrawals. - IERC20 public pinakion; // The Pinakion token contract. address public jurorProsecutionModule; // The module for juror's prosecution. IStakeController public stakeController; // Stake controller for coordination. + IVault public vault; // The PNK vault for atomic deposits/withdrawals. Court[] public courts; // The courts. IDisputeKit[] public disputeKits; // Array of dispute kits. Dispute[] public disputes; // The disputes. @@ -160,6 +161,7 @@ abstract contract KlerosCoreV2Base is IArbitratorV2, Initializable, UUPSProxiabl ); event Paused(); event Unpaused(); + event InactiveJurorWithdrawalFailed(address indexed _juror, uint256 _amount, bytes _reason); // ************************************* // // * Function Modifiers * // @@ -192,20 +194,20 @@ abstract contract KlerosCoreV2Base is IArbitratorV2, Initializable, UUPSProxiabl function __KlerosCoreV2Base_initialize( address _governor, address _guardian, - IERC20 _pinakion, address _jurorProsecutionModule, IDisputeKit _disputeKit, bool _hiddenVotes, uint256[4] memory _courtParameters, uint256[4] memory _timesPerPeriod, bytes memory _sortitionExtraData, - IStakeController _stakeController + IStakeController _stakeController, + IVault _vault ) internal onlyInitializing { governor = _governor; guardian = _guardian; - pinakion = _pinakion; jurorProsecutionModule = _jurorProsecutionModule; stakeController = _stakeController; + vault = _vault; // NULL_DISPUTE_KIT: an empty element at index 0 to indicate when a dispute kit is not supported. disputeKits.push(); @@ -290,12 +292,6 @@ abstract contract KlerosCoreV2Base is IArbitratorV2, Initializable, UUPSProxiabl guardian = _guardian; } - /// @dev Changes the `pinakion` storage variable. - /// @param _pinakion The new value for the `pinakion` storage variable. - function changePinakion(IERC20 _pinakion) external onlyByGovernor { - pinakion = _pinakion; - } - /// @dev Changes the `jurorProsecutionModule` storage variable. /// @param _jurorProsecutionModule The new value for the `jurorProsecutionModule` storage variable. function changeJurorProsecutionModule(address _jurorProsecutionModule) external onlyByGovernor { @@ -778,7 +774,7 @@ abstract contract KlerosCoreV2Base is IArbitratorV2, Initializable, UUPSProxiabl // Execute penalty through StakeController coordination address account = round.drawnJurors[_params.repartition]; - uint256 actualPenalty = stakeController.executeJurorPenalty(account, penalty, round.pnkAtStakePerJuror); + uint256 actualPenalty = stakeController.executeJurorPenalty(account, penalty); _params.pnkPenaltiesInRound += actualPenalty; emit TokenAndETHShift( @@ -792,8 +788,16 @@ abstract contract KlerosCoreV2Base is IArbitratorV2, Initializable, UUPSProxiabl ); // Check if juror should be set inactive - if (stakeController.shouldSetJurorInactive(account, _params.disputeID, _params.round, _params.repartition)) { - stakeController.setJurorInactive(account); + bool shouldBeInactive = !disputeKit.isVoteActive(_params.disputeID, _params.round, _params.repartition); + if (shouldBeInactive) { + uint256 pnkToWithdraw = stakeController.setJurorInactive(account); + if (pnkToWithdraw > 0) { + try vault.withdraw(account, pnkToWithdraw) { + // Successfully withdrew PNK for inactive juror + } catch (bytes memory reason) { + emit InactiveJurorWithdrawalFailed(account, pnkToWithdraw, reason); + } + } } if (_params.repartition == _params.numberOfVotesInRound - 1 && _params.coherentCount == 0) { @@ -805,7 +809,7 @@ abstract contract KlerosCoreV2Base is IArbitratorV2, Initializable, UUPSProxiabl // The dispute fees were paid in ERC20 round.feeToken.safeTransfer(governor, round.totalFeesForJurors); } - pinakion.safeTransfer(governor, _params.pnkPenaltiesInRound); + vault.transferReward(governor, _params.pnkPenaltiesInRound); emit LeftoverRewardSent( _params.disputeID, _params.round, @@ -846,7 +850,7 @@ abstract contract KlerosCoreV2Base is IArbitratorV2, Initializable, UUPSProxiabl // Give back the locked PNKs in case the juror fully unstaked earlier. if (!stakeController.isJurorStaked(account)) { - pinakion.safeTransfer(account, pnkLocked); + vault.transferReward(account, pnkLocked); } // Transfer the rewards @@ -854,7 +858,7 @@ abstract contract KlerosCoreV2Base is IArbitratorV2, Initializable, UUPSProxiabl round.sumPnkRewardPaid += pnkReward; uint256 feeReward = ((round.totalFeesForJurors / _params.coherentCount) * degreeOfCoherence) / ALPHA_DIVISOR; round.sumFeeRewardPaid += feeReward; - pinakion.safeTransfer(account, pnkReward); + vault.transferReward(account, pnkReward); if (round.feeToken == NATIVE_CURRENCY) { // The dispute fees were paid in ETH payable(account).send(feeReward); @@ -878,7 +882,7 @@ abstract contract KlerosCoreV2Base is IArbitratorV2, Initializable, UUPSProxiabl uint256 leftoverFeeReward = round.totalFeesForJurors - round.sumFeeRewardPaid; if (leftoverPnkReward != 0 || leftoverFeeReward != 0) { if (leftoverPnkReward != 0) { - pinakion.safeTransfer(governor, leftoverPnkReward); + vault.transferReward(governor, leftoverPnkReward); } if (leftoverFeeReward != 0) { if (round.feeToken == NATIVE_CURRENCY) { @@ -1103,14 +1107,20 @@ abstract contract KlerosCoreV2Base is IArbitratorV2, Initializable, UUPSProxiabl return false; } if (pnkDeposit > 0) { - if (!pinakion.safeTransferFrom(_account, address(this), pnkDeposit)) { - _stakingFailed(_onError, StakingResult.StakingTransferFailed); + try vault.deposit(_account, pnkDeposit) { + // Successfully deposited PNK and minted stPNK via Vault + } catch { + // Revert with a specific error or reuse existing one + _stakingFailed(_onError, StakingResult.StakingTransferFailed); // Indicating failure in the deposit part of staking return false; } } if (pnkWithdrawal > 0) { - if (!pinakion.safeTransfer(_account, pnkWithdrawal)) { - _stakingFailed(_onError, StakingResult.UnstakingTransferFailed); + try vault.withdraw(_account, pnkWithdrawal) { + // Successfully burned stPNK and withdrew PNK via Vault + } catch { + // Revert with a specific error or reuse existing one + _stakingFailed(_onError, StakingResult.UnstakingTransferFailed); // Indicating failure in the withdrawal part of unstaking return false; } } diff --git a/contracts/src/arbitration/core-v2/KlerosCoreV2Neo.sol b/contracts/src/arbitration/core-v2/KlerosCoreV2Neo.sol index 15155224d..3bec3e361 100644 --- a/contracts/src/arbitration/core-v2/KlerosCoreV2Neo.sol +++ b/contracts/src/arbitration/core-v2/KlerosCoreV2Neo.sol @@ -30,7 +30,6 @@ contract KlerosCoreV2Neo is KlerosCoreV2Base { /// @notice Initialization function for UUPS proxy /// @param _governor The governor of the contract. /// @param _guardian The guardian able to pause asset withdrawals. - /// @param _pinakion The Pinakion token contract. /// @param _jurorProsecutionModule The module for juror's prosecution. /// @param _disputeKit The dispute kit responsible for the dispute logic. /// @param _hiddenVotes Whether to use commit and reveal or not. @@ -42,7 +41,6 @@ contract KlerosCoreV2Neo is KlerosCoreV2Base { function initialize( address _governor, address _guardian, - IERC20 _pinakion, address _jurorProsecutionModule, IDisputeKit _disputeKit, bool _hiddenVotes, @@ -50,19 +48,20 @@ contract KlerosCoreV2Neo is KlerosCoreV2Base { uint256[4] memory _timesPerPeriod, bytes memory _sortitionExtraData, IStakeController _stakeController, + IVault _vault, IERC721 _jurorNft ) external reinitializer(2) { __KlerosCoreV2Base_initialize( _governor, _guardian, - _pinakion, _jurorProsecutionModule, _disputeKit, _hiddenVotes, _courtParameters, _timesPerPeriod, _sortitionExtraData, - _stakeController + _stakeController, + _vault ); jurorNft = _jurorNft; } diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitClassic.sol b/contracts/src/arbitration/dispute-kits/DisputeKitClassic.sol index 95b225e91..39bfac324 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitClassic.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitClassic.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.24; -import {DisputeKitClassicBase, KlerosCore} from "./DisputeKitClassicBase.sol"; +import {DisputeKitClassicBase, KlerosCoreV2} from "./DisputeKitClassicBase.sol"; /// @title DisputeKitClassic /// Dispute kit implementation of the Kleros v1 features including: @@ -25,7 +25,7 @@ contract DisputeKitClassic is DisputeKitClassicBase { /// @dev Initializer. /// @param _governor The governor's address. /// @param _core The KlerosCore arbitrator. - function initialize(address _governor, KlerosCore _core) external reinitializer(1) { + function initialize(address _governor, KlerosCoreV2 _core) external reinitializer(1) { __DisputeKitClassicBase_initialize(_governor, _core); } diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol index e3ed491eb..6d9b27756 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.24; -import {KlerosCore, KlerosCoreBase, IDisputeKit, ISortitionModule} from "../KlerosCore.sol"; +import {KlerosCoreV2, KlerosCoreV2Base, IDisputeKit, IStakeController} from "../core-v2/KlerosCoreV2.sol"; import {Initializable} from "../../proxy/Initializable.sol"; import {UUPSProxiable} from "../../proxy/UUPSProxiable.sol"; @@ -57,7 +57,7 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi 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 + KlerosCoreV2 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. @@ -141,7 +141,7 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi /// @dev Initializer. /// @param _governor The governor's address. /// @param _core The KlerosCore arbitrator. - function __DisputeKitClassicBase_initialize(address _governor, KlerosCore _core) internal onlyInitializing { + function __DisputeKitClassicBase_initialize(address _governor, KlerosCoreV2 _core) internal onlyInitializing { governor = _governor; core = _core; } @@ -172,7 +172,7 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi /// @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); + core = KlerosCoreV2(_core); } // ************************************* // @@ -221,7 +221,7 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi uint256 localRoundID = dispute.rounds.length - 1; Round storage round = dispute.rounds[localRoundID]; - ISortitionModule sortitionModule = core.sortitionModule(); + IStakeController sortitionModule = core.stakeController(); (uint96 courtID, , , , ) = core.disputes(_coreDisputeID); bytes32 key = bytes32(uint256(courtID)); // Get the ID of the tree. @@ -247,8 +247,8 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi 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."); + (, , KlerosCoreV2Base.Period period, , ) = core.disputes(_coreDisputeID); + require(period == KlerosCoreV2Base.Period.commit, "The dispute should be in Commit period."); require(_commit != bytes32(0), "Empty commit."); Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; @@ -276,8 +276,8 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi 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."); + (, , KlerosCoreV2Base.Period period, , ) = core.disputes(_coreDisputeID); + require(period == KlerosCoreV2Base.Period.vote, "The dispute should be in Vote period."); require(_voteIDs.length > 0, "No voteID provided"); Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; @@ -456,9 +456,9 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi Round storage round = dispute.rounds[dispute.rounds.length - 1]; tied = round.tied; ruling = tied ? 0 : round.winningChoice; - (, , KlerosCore.Period period, , ) = core.disputes(_coreDisputeID); + (, , KlerosCoreV2Base.Period period, , ) = core.disputes(_coreDisputeID); // Override the final ruling if only one side funded the appeals. - if (period == KlerosCoreBase.Period.execution) { + if (period == KlerosCoreV2Base.Period.execution) { uint256[] memory fundedChoices = getFundedChoices(_coreDisputeID); if (fundedChoices.length == 1) { ruling = fundedChoices[0]; @@ -625,7 +625,7 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi _coreDisputeID, core.getNumberOfRounds(_coreDisputeID) - 1 ); - (uint256 totalStaked, uint256 totalLocked, , ) = core.sortitionModule().getJurorBalance(_juror, courtID); + (, uint256 totalLocked, , uint256 totalStaked, , ) = core.stakeController().getJurorBalance(_juror, courtID); result = totalStaked >= totalLocked + lockedAmountPerJuror; if (singleDrawPerJuror) { diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitGated.sol b/contracts/src/arbitration/dispute-kits/DisputeKitGated.sol index 6c9c0ab65..4eadfa07e 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitGated.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitGated.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.24; -import {DisputeKitClassicBase, KlerosCore} from "./DisputeKitClassicBase.sol"; +import {DisputeKitClassicBase, KlerosCoreV2} from "./DisputeKitClassicBase.sol"; interface IBalanceHolder { /// @dev Returns the number of tokens in `owner` account. @@ -54,7 +54,7 @@ contract DisputeKitGated is DisputeKitClassicBase { /// @param _isERC1155 Whether the token is an ERC-1155 function initialize( address _governor, - KlerosCore _core, + KlerosCoreV2 _core, address _tokenGate, uint256 _tokenId, bool _isERC1155 diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitSybilResistant.sol b/contracts/src/arbitration/dispute-kits/DisputeKitSybilResistant.sol index 7d3b90599..8a401e9fd 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitSybilResistant.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitSybilResistant.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.24; -import {DisputeKitClassicBase, KlerosCore} from "./DisputeKitClassicBase.sol"; +import {DisputeKitClassicBase, KlerosCoreV2} from "./DisputeKitClassicBase.sol"; interface IProofOfHumanity { /// @dev Return true if the submission is registered and not expired. @@ -39,7 +39,7 @@ contract DisputeKitSybilResistant is DisputeKitClassicBase { /// @param _governor The governor's address. /// @param _core The KlerosCore arbitrator. /// @param _poh The Proof of Humanity registry. - function initialize(address _governor, KlerosCore _core, IProofOfHumanity _poh) external reinitializer(1) { + function initialize(address _governor, KlerosCoreV2 _core, IProofOfHumanity _poh) external reinitializer(1) { __DisputeKitClassicBase_initialize(_governor, _core); poh = _poh; singleDrawPerJuror = true; diff --git a/contracts/src/arbitration/interfaces/ISortitionModuleV2.sol b/contracts/src/arbitration/interfaces/ISortitionModuleV2.sol index 99f251f9d..f724781fe 100644 --- a/contracts/src/arbitration/interfaces/ISortitionModuleV2.sol +++ b/contracts/src/arbitration/interfaces/ISortitionModuleV2.sol @@ -72,13 +72,12 @@ interface ISortitionModuleV2 { /// @param _juror The juror address /// @param _courtID The court ID /// @return totalStaked Total staked amount (from external source) - /// @return totalLocked Total locked amount (from external source) /// @return stakedInCourt Amount staked in specific court /// @return nbCourts Number of courts staked in function getJurorInfo( address _juror, uint96 _courtID - ) external view returns (uint256 totalStaked, uint256 totalLocked, uint256 stakedInCourt, uint256 nbCourts); + ) external view returns (uint256 totalStaked, uint256 stakedInCourt, uint256 nbCourts); /// @notice Get court IDs where juror has stakes /// @param _juror The juror address diff --git a/contracts/src/arbitration/interfaces/IStakeController.sol b/contracts/src/arbitration/interfaces/IStakeController.sol index 616d21a27..6e255d45e 100644 --- a/contracts/src/arbitration/interfaces/IStakeController.sol +++ b/contracts/src/arbitration/interfaces/IStakeController.sol @@ -2,7 +2,6 @@ pragma solidity 0.8.24; -import {ISortitionModule} from "./ISortitionModule.sol"; import {KlerosCoreV2Base} from "../core-v2/KlerosCoreV2Base.sol"; import "../../libraries/Constants.sol"; @@ -24,17 +23,18 @@ interface IStakeController { // * Events * // // ************************************* // - event NewPhase(Phase phase); - event JurorPenaltyExecuted(address indexed account, uint256 requestedPenalty, uint256 actualPenalty); - event StakeUnlocked(address indexed account, uint256 amount); - event JurorSetInactive(address indexed account); + event NewPhase(Phase _phase); + event JurorPenaltyExecuted(address indexed _account, uint256 _penalty, uint256 _actualPenalty); + event StakeLocked(address indexed _account, uint256 _amount); + event StakeUnlocked(address indexed _account, uint256 _amount); + event JurorSetInactive(address indexed _account); // Migration events - event StakeImported(address indexed account, uint96 indexed courtID, uint256 stake); - event DelayedStakeImported(address indexed account, uint96 indexed courtID, uint256 stake, uint256 index); - event MigrationCompleted(uint256 totalAttempted, uint256 totalImported); - event PhaseStateMigrated(Phase phase, uint256 lastPhaseChange, uint256 disputesWithoutJurors); - event EmergencyReset(uint256 timestamp); + event StakeImported(address indexed _juror, uint96 indexed _courtID, uint256 _stake); + event DelayedStakeImported(address indexed _juror, uint96 indexed _courtID, uint256 _stake, uint256 _index); + event MigrationCompleted(uint256 _totalAccounts, uint256 _totalStakesImported); + event PhaseStateMigrated(Phase _phase, uint256 _lastPhaseChange, uint256 _disputesWithoutJurors); + event EmergencyReset(uint256 _timestamp); // ************************************* // // * Phase Management * // @@ -45,7 +45,7 @@ interface IStakeController { /// @notice Get the current phase /// @return The current phase - function getPhase() external view returns (Phase); + function phase() external view returns (Phase); /// @notice Execute delayed stakes during staking phase /// @param _iterations The number of delayed stakes to execute @@ -83,30 +83,13 @@ interface IStakeController { /// @notice Execute penalty on juror through vault coordination /// @param _account The account to penalize /// @param _penalty The penalty amount - /// @param _totalStake The total stake amount (for inactivity check) /// @return actualPenalty The actual penalty applied - function executeJurorPenalty( - address _account, - uint256 _penalty, - uint256 _totalStake - ) external returns (uint256 actualPenalty); + function executeJurorPenalty(address _account, uint256 _penalty) external returns (uint256 actualPenalty); /// @notice Set juror as inactive and remove from all sortition trees /// @param _account The juror to set inactive - function setJurorInactive(address _account) external; - - /// @notice Check if a juror should be set inactive after penalty - /// @param _account The juror account - /// @param _disputeID The dispute ID - /// @param _round The round number - /// @param _repartition The repartition index - /// @return shouldSet Whether the juror should be set inactive - function shouldSetJurorInactive( - address _account, - uint256 _disputeID, - uint256 _round, - uint256 _repartition - ) external view returns (bool shouldSet); + /// @return pnkToWithdraw The amount of PNK to withdraw + function setJurorInactive(address _account) external returns (uint256 pnkToWithdraw); /// @notice Create dispute hook /// @param _disputeID The dispute ID @@ -141,14 +124,26 @@ interface IStakeController { /// @notice Get juror balance information /// @param _juror The juror address /// @param _courtID The court ID + /// @return availablePnk Available PNK + /// @return lockedPnk Locked PNK + /// @return penaltyPnk Penalty PNK /// @return totalStaked Total staked amount - /// @return totalLocked Total locked amount /// @return stakedInCourt Amount staked in specific court /// @return nbCourts Number of courts staked in function getJurorBalance( address _juror, uint96 _courtID - ) external view returns (uint256 totalStaked, uint256 totalLocked, uint256 stakedInCourt, uint256 nbCourts); + ) + external + view + returns ( + uint256 availablePnk, + uint256 lockedPnk, + uint256 penaltyPnk, + uint256 totalStaked, + uint256 stakedInCourt, + uint256 nbCourts + ); /// @notice Get court IDs where juror has stakes /// @param _juror The juror address diff --git a/contracts/src/arbitration/interfaces/IVault.sol b/contracts/src/arbitration/interfaces/IVault.sol index 8fb8d7bb9..28aa6e6ae 100644 --- a/contracts/src/arbitration/interfaces/IVault.sol +++ b/contracts/src/arbitration/interfaces/IVault.sol @@ -23,14 +23,16 @@ interface IVault { // ************************************* // /// @notice Deposit PNK and mint stPNK + /// @param _from The account to deposit from /// @param _amount The amount of PNK to deposit /// @return stPnkAmount The amount of stPNK minted - function deposit(uint256 _amount) external returns (uint256 stPnkAmount); + function deposit(address _from, uint256 _amount) external returns (uint256 stPnkAmount); /// @notice Withdraw PNK by burning stPNK + /// @param _to The account to withdraw to /// @param _amount The amount to withdraw /// @return pnkAmount The amount of PNK withdrawn - function withdraw(uint256 _amount) external returns (uint256 pnkAmount); + function withdraw(address _to, uint256 _amount) external returns (uint256 pnkAmount); /// @notice Lock tokens for dispute participation (only StakeController) /// @param _account The account to lock tokens for diff --git a/contracts/src/arbitration/sortition-v2/SortitionModuleV2Base.sol b/contracts/src/arbitration/sortition-v2/SortitionModuleV2Base.sol index 7b43770df..0d34b6db8 100644 --- a/contracts/src/arbitration/sortition-v2/SortitionModuleV2Base.sol +++ b/contracts/src/arbitration/sortition-v2/SortitionModuleV2Base.sol @@ -180,17 +180,11 @@ abstract contract SortitionModuleV2Base is ISortitionModuleV2, Initializable, UU function getJurorInfo( address _juror, uint96 _courtID - ) - external - view - override - returns (uint256 totalStaked, uint256 totalLocked, uint256 stakedInCourt, uint256 nbCourts) - { + ) external view override returns (uint256 totalStaked, uint256 stakedInCourt, uint256 nbCourts) { JurorStakeInfo storage info = jurorStakeInfo[_juror]; // Get total staked and locked from stake controller totalStaked = stakeController.getDepositedBalance(_juror); - totalLocked = 0; // Will be retrieved from vault through stake controller if needed // Get stake in specific court from sortition tree stakedInCourt = this.stakeOf(_juror, _courtID); diff --git a/contracts/src/arbitration/stake-controller/StakeControllerBase.sol b/contracts/src/arbitration/stake-controller/StakeControllerBase.sol index c68901caa..6dea571ec 100644 --- a/contracts/src/arbitration/stake-controller/StakeControllerBase.sol +++ b/contracts/src/arbitration/stake-controller/StakeControllerBase.sol @@ -36,8 +36,8 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr IVault public vault; // The PNK vault for token management. ISortitionModuleV2 public sortitionModule; // The sortition module for drawing logic. - // Phase management (moved from SortitionModule) - Phase public phase; // The current phase. + // Phase management + Phase public override phase; // The current phase. Uses Phase from IStakeController. uint256 public minStakingTime; // The time after which the phase can be switched to Drawing if there are open disputes. uint256 public maxDrawingTime; // The time after which the phase can be switched back to Staking. uint256 public lastPhaseChange; // The last time the phase was changed. @@ -47,7 +47,7 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr uint256 public randomNumber; // Random number returned by RNG. uint256 public rngLookahead; // Minimal block distance between requesting and obtaining a random number. - // Delayed stakes management (moved from SortitionModule) + // Delayed stakes management uint256 public delayedStakeWriteIndex; // The index of the last `delayedStake` item that was written to the array. 0 index is skipped. uint256 public delayedStakeReadIndex; // The index of the next `delayedStake` item that should be processed. Starts at 1 because 0 index is skipped. mapping(uint256 => DelayedStake) public delayedStakes; // Stores the stakes that were changed during Drawing phase, to update them when the phase is switched to Staking. @@ -166,11 +166,6 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr emit NewPhase(phase); } - /// @inheritdoc IStakeController - function getPhase() external view override returns (Phase) { - return phase; - } - /// @inheritdoc IStakeController function executeDelayedStakes(uint256 _iterations) external override { if (phase != Phase.staking) revert NotInStakingPhase(); @@ -188,12 +183,22 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr // Nullify the index so the delayed stake won't get deleted before its own execution. delete latestDelayedStakeIndex[delayedStake.account][delayedStake.courtID]; - // Execute the delayed stake through the controller - (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) = _setStake( + // Execute the delayed stake by calling KlerosCore, which will handle Vault interactions + // core.setStakeBySortitionModule was the old way, now KlerosCore itself has _setStake + // which calls this StakeController's setStake. This creates a circular dependency if not careful. + // For delayed stakes, the StakeController should probably update SortitionModule directly + // and then tell KlerosCore to handle the deposit/withdrawal with the Vault if needed. + // OR, the KlerosCoreV2Base.setStakeBySortitionModule needs to exist and be smart enough. + + // For now, assuming KlerosCore will have a way to apply this, or this internal _setStake is used. + // This part needs careful review of how KlerosCore consumes delayed stakes. + // Let's assume _setStake here is the one that directly updates sortition and returns pnkDeposit/Withdrawal. + _setStake( delayedStake.account, delayedStake.courtID, delayedStake.stake, delayedStake.alreadyTransferred + // No direct PNK transfer here by StakeController; KlerosCore handles Vault interaction ); // Note: In delayed stake execution, we don't revert on failures to maintain batch processing @@ -212,20 +217,11 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr address _account, uint96 _courtID, uint256 _newStake, - bool _alreadyTransferred - ) external override onlyByCore returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { + bool _alreadyTransferred // Indicates if KlerosCore already handled the PNK transfer (e.g. for its direct setStake calls) + ) public override onlyByCore returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { return _setStake(_account, _courtID, _newStake, _alreadyTransferred); } - /// @dev Internal implementation of setStake with phase-aware delayed stake logic - function _setStake( - address _account, - uint96 _courtID, - uint256 _newStake - ) internal virtual returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { - return _setStake(_account, _courtID, _newStake, false); - } - /// @dev Internal implementation of setStake with phase-aware delayed stake logic /// TODO: REMOVE THE INSTANT STAKING LOGIC ! function _setStake( @@ -234,10 +230,14 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr uint256 _newStake, bool _alreadyTransferred ) internal virtual returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { - (, , uint256 currentStake, ) = sortitionModule.getJurorInfo(_account, _courtID); + (, uint256 currentStake, ) = sortitionModule.getJurorInfo(_account, _courtID); // Delete any existing delayed stake for this juror/court - pnkWithdrawal = _deleteDelayedStake(_courtID, _account); + // If a delayed stake was already funded (_alreadyTransferred = true for its deposit part), + // and now we are processing an earlier, direct setStake, _deleteDelayedStake + // should ideally signal KlerosCore to refund if the new operation effectively cancels a pre-funded deposit. + // Current _deleteDelayedStake returns the amount that *would have been withdrawn by the delayed stake itself*. + uint256 potentialWithdrawalFromDeletedDelayedStake = _deleteDelayedStake(_courtID, _account); if (phase != Phase.staking) { // Store the stake change as delayed, to be applied when the phase switches back to Staking. @@ -248,36 +248,56 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr latestDelayedStakeIndex[_account][_courtID] = delayedStakeWriteIndex; if (_newStake > currentStake) { - // PNK deposit: tokens are transferred now via vault coordination - delayedStake.alreadyTransferred = _alreadyTransferred || true; + // This is for a future deposit when phase is staking. + // If KlerosCore calls this (_alreadyTransferred=false path typically), it will use the returned pnkDeposit + // to instruct Vault.deposit(). So the delayedStake.alreadyTransferred should be set based on whether + // the funding happens now (for delayed stake path) or later (for KlerosCore direct path that gets delayed). + delayedStake.alreadyTransferred = _alreadyTransferred; // If it's a delayed execution of a pre-funded stake, it's true. + // If KlerosCore is calling and it gets delayed, _alreadyTransferred was false, so this is false. pnkDeposit = _newStake - currentStake; - // Note: Actual PNK transfer is handled by KlerosCore through vault + // No actual PNK transfer here. KlerosCore handles Vault interaction based on returned pnkDeposit. } else { - // PNK withdrawal: tokens are not transferred yet. - delayedStake.alreadyTransferred = false; + // This is for a future withdrawal. No PNK is transferred now by StakeController. + // KlerosCore will use returned pnkWithdrawal to instruct Vault.withdraw(). + delayedStake.alreadyTransferred = false; // Withdrawals are never pre-funded for a delayed op. + pnkWithdrawal = currentStake - _newStake; + if (potentialWithdrawalFromDeletedDelayedStake > pnkWithdrawal) { + // This implies the deleted delayed stake was a larger withdrawal than the current one. + // Or it was a deposit that got cancelled and replaced by a smaller one/withdrawal. + // This logic is complex if trying to reconcile pre-funded delayed stakes with new operations. + // For now, pnkWithdrawal is based on currentStake and _newStake. + } } return (pnkDeposit, pnkWithdrawal, StakingResult.Successful); } - // Current phase is Staking: set normal stakes through sortition module - // SortitionModule returns bool success, we need to convert to expected format + // Current phase is Staking: apply normally bool success = sortitionModule.setStake(_account, _courtID, _newStake); - if (success) { + if (!success) { + // This typically shouldn't fail if parameters are valid (e.g. tree exists). + // Assuming SortitionModule itself doesn't revert but returns false for logical errors not caught by KlerosCore. + return (0, 0, StakingResult.CannotStakeInThisCourt); // Or a more generic sortition error + } + + if (!_alreadyTransferred) { + // Only calculate pnkDeposit/pnkWithdrawal if KlerosCore hasn't handled it if (_newStake > currentStake) { pnkDeposit = _newStake - currentStake; } else { pnkWithdrawal = currentStake - _newStake; } - return (pnkDeposit, pnkWithdrawal, StakingResult.Successful); - } else { - return (0, 0, StakingResult.StakingTransferFailed); } + // If _alreadyTransferred is true, KlerosCore has (or will) handle the Vault interaction. + // pnkDeposit/pnkWithdrawal would be 0 in that case from this function's perspective for KlerosCore. + // However, the current KlerosCore _setStake always passes _alreadyTransferred = false. + + return (pnkDeposit, pnkWithdrawal, StakingResult.Successful); } /// @inheritdoc IStakeController function lockStake(address _account, uint256 _amount) external override onlyByCore { vault.lockTokens(_account, _amount); - emit StakeUnlocked(_account, _amount); // Note: Event name preserved for compatibility + emit StakeLocked(_account, _amount); // Event name might be misleading, should be StakeLocked. Preserved for compatibility if so. } /// @inheritdoc IStakeController @@ -289,8 +309,7 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr /// @inheritdoc IStakeController function executeJurorPenalty( address _account, - uint256 _penalty, - uint256 _totalStake + uint256 _penalty ) external virtual override onlyByCore returns (uint256 actualPenalty) { // First unlock the penalty amount vault.unlockTokens(_account, _penalty); @@ -303,36 +322,20 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr } /// @inheritdoc IStakeController - function setJurorInactive(address _account) external override onlyByCore { + function setJurorInactive(address _account) external override onlyByCore returns (uint256 pnkToWithdraw) { uint96[] memory courtIds = sortitionModule.getJurorCourtIDs(_account); for (uint256 i = 0; i < courtIds.length; i++) { - (, , uint256 currentStake, ) = sortitionModule.getJurorInfo(_account, courtIds[i]); - if (currentStake > 0) { + (, uint256 currentStakeInCourt, ) = sortitionModule.getJurorInfo(_account, courtIds[i]); + if (currentStakeInCourt > 0) { // Set stake to 0 in sortition module to remove from trees sortitionModule.setStake(_account, courtIds[i], 0); } } - emit JurorSetInactive(_account); - } + pnkToWithdraw = vault.getAvailableBalance(_account); - /// @inheritdoc IStakeController - function shouldSetJurorInactive( - address _account, - uint256 _disputeID, - uint256 _round, - uint256 _repartition - ) external view virtual override returns (bool shouldSet) { - // Check if juror has any remaining deposited balance - uint256 remainingBalance = vault.getDepositedBalance(_account); - - // Check if juror's vote is still active for this dispute - // This requires access to the dispute kit to check vote activity - // For now, we'll use a simplified check based on balance - bool isActive = remainingBalance > 0; - - return remainingBalance == 0 || !isActive; + emit JurorSetInactive(_account); } // ************************************* // @@ -346,6 +349,8 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr /// @inheritdoc IStakeController function draw(bytes32 _court, uint256 _coreDisputeID, uint256 _nonce) external view override returns (address) { + if (phase != Phase.drawing) revert NotDrawingPhase(); + if (randomNumber == 0) revert RandomNumberNotReadyYet(); return sortitionModule.draw(_court, _coreDisputeID, _nonce, randomNumber); } @@ -371,9 +376,19 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr external view override - returns (uint256 totalStaked, uint256 totalLocked, uint256 stakedInCourt, uint256 nbCourts) + returns ( + uint256 availablePnk, + uint256 lockedPnk, + uint256 penaltyPnk, + uint256 totalStaked, + uint256 stakedInCourt, + uint256 nbCourts + ) { - return sortitionModule.getJurorInfo(_juror, _courtID); + availablePnk = vault.getAvailableBalance(_juror); + lockedPnk = vault.getLockedBalance(_juror); + penaltyPnk = vault.getPenaltyBalance(_juror); + (totalStaked, stakedInCourt, nbCourts) = sortitionModule.getJurorInfo(_juror, _courtID); } /// @inheritdoc IStakeController @@ -400,59 +415,28 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr // * Internal * // // ************************************* // - /// @dev Enhanced delayed stake storage with better tracking - function _storeDelayedStakeEnhanced( - address _account, - uint96 _courtID, - uint256 _newStake - ) internal returns (bool success) { - delayedStakeWriteIndex++; - DelayedStake storage delayedStake = delayedStakes[delayedStakeWriteIndex]; - delayedStake.account = _account; - delayedStake.courtID = _courtID; - delayedStake.stake = _newStake; - delayedStake.alreadyTransferred = false; - latestDelayedStakeIndex[_account][_courtID] = delayedStakeWriteIndex; - - return true; - } - - /// @dev Enhanced inactivity check with dispute kit integration - function _shouldSetJurorInactiveEnhanced( - address _account, - uint256 _disputeID, - uint256 _round, - uint256 _repartition - ) internal view returns (bool shouldSet) { - // Check if juror has any remaining deposited balance - uint256 remainingBalance = vault.getDepositedBalance(_account); - if (remainingBalance == 0) { - return true; - } - - // Enhanced check - could be extended with dispute kit integration - // For now, simplified implementation based on balance - bool isActive = remainingBalance > 0; - - return !isActive; - } - /// @dev Checks if there is already a delayed stake. In this case consider it irrelevant and remove it. /// @param _courtID ID of the court. /// @param _juror Juror whose stake to check. - function _deleteDelayedStake(uint96 _courtID, address _juror) internal returns (uint256 actualAmountToWithdraw) { + /// @return amountToWithdrawFromDeletedDelayedStake If the deleted delayed stake was a deposit (_alreadyTransferred=true), + /// this is the PNK amount that was effectively pre-funded and now might need to be returned to the user by KlerosCore. + /// If it was a withdrawal, this is 0. + function _deleteDelayedStake( + uint96 _courtID, + address _juror + ) internal returns (uint256 amountToWithdrawFromDeletedDelayedStake) { uint256 latestIndex = latestDelayedStakeIndex[_juror][_courtID]; if (latestIndex != 0) { DelayedStake storage delayedStake = delayedStakes[latestIndex]; if (delayedStake.alreadyTransferred) { - // Calculate amount to withdraw based on the difference - (, , uint256 sortitionStake, ) = sortitionModule.getJurorInfo(_juror, _courtID); - uint256 amountToWithdraw = delayedStake.stake > sortitionStake - ? delayedStake.stake - sortitionStake - : 0; - actualAmountToWithdraw = amountToWithdraw; - - // Note: Actual token withdrawal is handled by KlerosCore through vault coordination + // This delayed stake was a deposit that was pre-funded into the system (e.g. KlerosCore took PNK). + // Now that it's being deleted (e.g., by a new setStake operation before execution), + // KlerosCore might need to refund this pre-funded amount if the new operation doesn't cover it. + (, uint256 sortitionStake, ) = sortitionModule.getJurorInfo(_juror, _courtID); + if (delayedStake.stake > sortitionStake) { + // The delayed stake was larger than current actual stake + amountToWithdrawFromDeletedDelayedStake = delayedStake.stake - sortitionStake; + } } delete delayedStakes[latestIndex]; delete latestDelayedStakeIndex[_juror][_courtID]; @@ -546,8 +530,10 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr error MinStakingTimeNotPassed(); error NoDisputesNeedingJurors(); error RandomNumberNotReady(); + error RandomNumberNotReadyYet(); error StillDrawingDisputes(); error NotInStakingPhase(); + error NotDrawingPhase(); error NoDelayedStakes(); error InvalidMigrationData(); } diff --git a/contracts/src/arbitration/stake-controller/StakeControllerNeo.sol b/contracts/src/arbitration/stake-controller/StakeControllerNeo.sol index d9172d2a7..7e30db0d4 100644 --- a/contracts/src/arbitration/stake-controller/StakeControllerNeo.sol +++ b/contracts/src/arbitration/stake-controller/StakeControllerNeo.sol @@ -2,16 +2,15 @@ pragma solidity 0.8.24; -import {StakeControllerBase} from "./StakeControllerBase.sol"; -import {IVault} from "../interfaces/IVault.sol"; +import {StakeControllerBase, IVault} from "./StakeControllerBase.sol"; import {ISortitionModuleV2} from "../interfaces/ISortitionModuleV2.sol"; import {IDisputeKit} from "../interfaces/IDisputeKit.sol"; import {KlerosCoreV2Base} from "../core-v2/KlerosCoreV2Base.sol"; import {RNG} from "../../rng/RNG.sol"; /// @title StakeControllerNeo -/// @notice Enhanced implementation of the Stake Controller with additional features -/// @dev Coordinates between Vault and SortitionModule with improved penalty logic +/// @notice ?? +/// @dev Coordinates between Vault and SortitionModule contract StakeControllerNeo is StakeControllerBase { string public constant override version = "1.0.0"; @@ -19,25 +18,12 @@ contract StakeControllerNeo is StakeControllerBase { // * Storage * // // ************************************* // - mapping(uint256 => IDisputeKit) public disputeKits; // Mapping from dispute kit ID to dispute kit contract - uint256 public disputeKitsLength; // Number of registered dispute kits - - // Enhanced penalty tracking - mapping(address => uint256) public totalPenaltiesApplied; // Track total penalties per juror - mapping(address => uint256) public lastPenaltyBlock; // Track when last penalty was applied - - // Coordination enhancement flags - bool public enableEnhancedInactivityChecks; // Whether to use enhanced inactivity detection - uint256 public penaltyCooldownBlocks; // Minimum blocks between penalties for same juror + // no storage // ************************************* // // * Events * // // ************************************* // - event DisputeKitRegistered(uint256 indexed disputeKitID, IDisputeKit indexed disputeKit); - event EnhancedInactivityChecksToggled(bool enabled); - event PenaltyCooldownChanged(uint256 newCooldown); - // ************************************* // // * Constructor * // // ************************************* // @@ -56,8 +42,6 @@ contract StakeControllerNeo is StakeControllerBase { /// @param _maxDrawingTime The maximum drawing time. /// @param _rng The random number generator. /// @param _rngLookahead The RNG lookahead time. - /// @param _enableEnhancedInactivityChecks Whether to enable enhanced inactivity checks. - /// @param _penaltyCooldownBlocks Minimum blocks between penalties. function initialize( address _governor, KlerosCoreV2Base _core, @@ -66,9 +50,7 @@ contract StakeControllerNeo is StakeControllerBase { uint256 _minStakingTime, uint256 _maxDrawingTime, RNG _rng, - uint256 _rngLookahead, - bool _enableEnhancedInactivityChecks, - uint256 _penaltyCooldownBlocks + uint256 _rngLookahead ) external reinitializer(2) { __StakeControllerBase_initialize( _governor, @@ -80,9 +62,6 @@ contract StakeControllerNeo is StakeControllerBase { _rng, _rngLookahead ); - - enableEnhancedInactivityChecks = _enableEnhancedInactivityChecks; - penaltyCooldownBlocks = _penaltyCooldownBlocks; } // ************************************* // @@ -95,124 +74,7 @@ contract StakeControllerNeo is StakeControllerBase { // NOP } - /// @dev Register a dispute kit for enhanced inactivity checks - /// @param _disputeKitID The ID of the dispute kit - /// @param _disputeKit The dispute kit contract - function registerDisputeKit(uint256 _disputeKitID, IDisputeKit _disputeKit) external onlyByGovernor { - disputeKits[_disputeKitID] = _disputeKit; - if (_disputeKitID >= disputeKitsLength) { - disputeKitsLength = _disputeKitID + 1; - } - emit DisputeKitRegistered(_disputeKitID, _disputeKit); - } - - /// @dev Toggle enhanced inactivity checks - /// @param _enabled Whether to enable enhanced checks - function toggleEnhancedInactivityChecks(bool _enabled) external onlyByGovernor { - enableEnhancedInactivityChecks = _enabled; - emit EnhancedInactivityChecksToggled(_enabled); - } - - /// @dev Change penalty cooldown blocks - /// @param _penaltyCooldownBlocks New cooldown period in blocks - function changePenaltyCooldownBlocks(uint256 _penaltyCooldownBlocks) external onlyByGovernor { - penaltyCooldownBlocks = _penaltyCooldownBlocks; - emit PenaltyCooldownChanged(_penaltyCooldownBlocks); - } - // ************************************* // - // * Enhanced Functions * // + // * State Modifiers * // // ************************************* // - - /// @dev Enhanced penalty execution with cooldown and tracking - function executeJurorPenalty( - address _account, - uint256 _penalty, - uint256 _totalStake - ) external override onlyByCore returns (uint256 actualPenalty) { - // Check penalty cooldown - if (penaltyCooldownBlocks > 0 && lastPenaltyBlock[_account] + penaltyCooldownBlocks > block.number) { - revert PenaltyCooldownActive(); - } - - // Execute penalty through vault coordination (base implementation) - vault.unlockTokens(_account, _penalty); - actualPenalty = vault.applyPenalty(_account, _penalty); - - // Update tracking - totalPenaltiesApplied[_account] += actualPenalty; - lastPenaltyBlock[_account] = block.number; - - emit JurorPenaltyExecuted(_account, _penalty, actualPenalty); - return actualPenalty; - } - - /// @dev Enhanced inactivity check with dispute kit integration - function shouldSetJurorInactive( - address _account, - uint256 _disputeID, - uint256 _round, - uint256 _repartition - ) external view override returns (bool shouldSet) { - // Check if juror has any remaining deposited balance - uint256 remainingBalance = vault.getDepositedBalance(_account); - if (remainingBalance == 0) { - return true; - } - - // If enhanced checks are disabled, use basic check - if (!enableEnhancedInactivityChecks) { - return false; - } - - // Enhanced check: query dispute kit for vote activity - // Note: This requires the dispute kit to be properly registered - // For now, we'll implement a basic version that can be extended - - // If juror has been penalized heavily, consider setting inactive - uint256 penaltyRatio = (totalPenaltiesApplied[_account] * 10000) / - (remainingBalance + totalPenaltiesApplied[_account]); - if (penaltyRatio > 5000) { - // If more than 50% of original stake was penalized - return true; - } - - return false; - } - - // ************************************* // - // * View Functions * // - // ************************************* // - - /// @notice Get penalty statistics for a juror - /// @param _account The juror address - /// @return totalPenalties Total penalties applied - /// @return lastPenaltyBlock_ Block when last penalty was applied - /// @return cooldownRemaining Blocks remaining in cooldown (0 if no cooldown) - function getPenaltyStats( - address _account - ) external view returns (uint256 totalPenalties, uint256 lastPenaltyBlock_, uint256 cooldownRemaining) { - totalPenalties = totalPenaltiesApplied[_account]; - lastPenaltyBlock_ = lastPenaltyBlock[_account]; - - if (penaltyCooldownBlocks > 0 && lastPenaltyBlock_ + penaltyCooldownBlocks > block.number) { - cooldownRemaining = (lastPenaltyBlock_ + penaltyCooldownBlocks) - block.number; - } else { - cooldownRemaining = 0; - } - } - - /// @notice Check if a juror is in penalty cooldown - /// @param _account The juror address - /// @return Whether the juror is in cooldown - function isInPenaltyCooldown(address _account) external view returns (bool) { - if (penaltyCooldownBlocks == 0) return false; - return lastPenaltyBlock[_account] + penaltyCooldownBlocks > block.number; - } - - // ************************************* // - // * Errors * // - // ************************************* // - - error PenaltyCooldownActive(); } diff --git a/contracts/src/arbitration/vault/VaultBase.sol b/contracts/src/arbitration/vault/VaultBase.sol index 1c2fec48d..b6ce4a003 100644 --- a/contracts/src/arbitration/vault/VaultBase.sol +++ b/contracts/src/arbitration/vault/VaultBase.sol @@ -125,38 +125,57 @@ abstract contract VaultBase is IVault, Initializable, UUPSProxiable { // ************************************* // /// @inheritdoc IVault - function deposit(uint256 _amount) external virtual override returns (uint256 stPnkAmount) { + function deposit(address _from, uint256 _amount) external virtual override onlyCore returns (uint256 stPnkAmount) { + return _deposit(_from, _amount); + } + + /// @dev Internal implementation of deposit. + /// @param _from The user address for the deposit. + /// @param _amount The amount of PNK to deposit. + /// @return stPnkAmount The amount of stPNK minted. + function _deposit(address _from, uint256 _amount) internal virtual returns (uint256 stPnkAmount) { if (_amount == 0) revert InvalidAmount(); - pnk.safeTransferFrom(msg.sender, address(this), _amount); - jurorBalances[msg.sender].deposited += _amount; + // Transfer PNK from the user to the vault + // The Vault must be approved by _from to transfer PNK to the vault + pnk.safeTransferFrom(_from, address(this), _amount); + jurorBalances[_from].deposited += _amount; - // Mint 1:1 stPNK + // Mint 1:1 stPNK to the user account stPnkAmount = _amount; - stPnk.mint(msg.sender, stPnkAmount); + stPnk.mint(_from, stPnkAmount); - emit Deposit(msg.sender, _amount); + emit Deposit(_from, _amount); } /// @inheritdoc IVault - function withdraw(uint256 _amount) external virtual override returns (uint256 pnkAmount) { + function withdraw(address _to, uint256 _amount) external virtual override onlyCore returns (uint256 pnkAmount) { + return _withdraw(_to, _amount); + } + + /// @dev Internal implementation of withdraw. + /// @param _to The user address for the withdrawal. + /// @param _amount The amount of stPNK to withdraw (will be burned). + /// @return pnkAmount The amount of PNK transferred back to the user. + function _withdraw(address _to, uint256 _amount) internal virtual returns (uint256 pnkAmount) { if (_amount == 0) revert InvalidAmount(); - JurorBalance storage balance = jurorBalances[msg.sender]; + JurorBalance storage balance = jurorBalances[_to]; - // Check available balance (deposited - locked - penalties) - uint256 available = getAvailableBalance(msg.sender); + // Check available balance (deposited - locked - penalties) for the user + uint256 available = getAvailableBalance(_to); if (_amount > available) revert InsufficientAvailableBalance(); - // Check stPNK balance - if (stPnk.balanceOf(msg.sender) < _amount) revert InsufficientStPNKBalance(); + // Check stPNK balance of the user + // The Vault must be approved by _to to burn their stPNK + if (stPnk.balanceOf(_to) < _amount) revert InsufficientStPNKBalance(); - // Burn stPNK and transfer PNK - stPnk.burnFrom(msg.sender, _amount); + // Burn stPNK from the user and transfer PNK to the user + stPnk.burnFrom(_to, _amount); // Vault burns user's stPNK balance.deposited -= _amount; - pnk.safeTransfer(msg.sender, _amount); + pnk.safeTransfer(_to, _amount); // Vault sends PNK to user - emit Withdraw(msg.sender, _amount); + emit Withdraw(_to, _amount); return _amount; } @@ -211,8 +230,8 @@ abstract contract VaultBase is IVault, Initializable, UUPSProxiable { /// @inheritdoc IVault function getAvailableBalance(address _account) public view override returns (uint256) { JurorBalance storage balance = jurorBalances[_account]; - uint256 locked = balance.locked + balance.penalties; - return balance.deposited > locked ? balance.deposited - locked : 0; + uint256 unavailable = balance.locked + balance.penalties; + return balance.deposited > unavailable ? balance.deposited - unavailable : 0; } /// @inheritdoc IVault diff --git a/contracts/src/arbitration/vault/VaultNeo.sol b/contracts/src/arbitration/vault/VaultNeo.sol index f5b109ae0..3f08514cd 100644 --- a/contracts/src/arbitration/vault/VaultNeo.sol +++ b/contracts/src/arbitration/vault/VaultNeo.sol @@ -101,15 +101,15 @@ contract VaultNeo is VaultBase { // ************************************* // /// @notice Deposit PNK and mint stPNK with additional validation checks - function deposit(uint256 _amount) external override returns (uint256 stPnkAmount) { + function _deposit(address _from, uint256 _amount) internal override returns (uint256 stPnkAmount) { // Check NFT requirement if set - if (address(depositNft) != address(0) && depositNft.balanceOf(msg.sender) == 0) { + if (address(depositNft) != address(0) && depositNft.balanceOf(_from) == 0) { revert DepositNftRequired(); } // Check per-user deposit limit if (maxDepositPerUser > 0) { - uint256 currentUserDeposit = jurorBalances[msg.sender].deposited; + uint256 currentUserDeposit = jurorBalances[_from].deposited; if (currentUserDeposit + _amount > maxDepositPerUser) { revert ExceedsMaxDepositPerUser(); } @@ -123,13 +123,13 @@ contract VaultNeo is VaultBase { // Update total deposited totalDeposited += _amount; - return this.deposit(_amount); + return super._deposit(_from, _amount); } /// @notice Withdraw PNK by burning stPNK and update total deposited - function withdraw(uint256 _amount) external override returns (uint256 pnkAmount) { + function _withdraw(address _to, uint256 _amount) internal override returns (uint256 pnkAmount) { totalDeposited -= _amount; - return this.withdraw(_amount); + return super._withdraw(_to, _amount); } // ************************************* // From d27e20463b1df224d1f839fa07eefcd0320ce10b Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Thu, 5 Jun 2025 01:11:23 +0100 Subject: [PATCH 04/24] refactor: new staking and sortition architecture (wip) --- .../00-home-chain-arbitration-v2-neo.ts | 133 +----------------- .../deploy/00-home-chain-arbitration-v2.ts | 2 +- 2 files changed, 2 insertions(+), 133 deletions(-) diff --git a/contracts/deploy/00-home-chain-arbitration-v2-neo.ts b/contracts/deploy/00-home-chain-arbitration-v2-neo.ts index 867a8fdd5..97c02ca4c 100644 --- a/contracts/deploy/00-home-chain-arbitration-v2-neo.ts +++ b/contracts/deploy/00-home-chain-arbitration-v2-neo.ts @@ -35,138 +35,7 @@ const deployArbitrationV2Neo: DeployFunction = async (hre: HardhatRuntimeEnviron log: true, }); - // Deploy stPNK token - const stPNK = await deploy("stPNK", { - from: deployer, - contract: "stPNK", - args: [], - log: true, - }); - - // Deploy VaultNeo - const pnkVaultNeo = await deployUpgradable(deployments, "VaultNeo", { - from: deployer, - args: [deployer, pnk.target, stPNK.address], - log: true, - }); - - // Calculate future addresses for circular dependencies - let stakeControllerNeoAddress = await deployments - .getOrNull("StakeControllerNeo") - .then((deployment) => deployment?.address); - let klerosCoreV2NeoAddress = await deployments.getOrNull("KlerosCoreV2Neo").then((deployment) => deployment?.address); - - const nonce = await ethers.provider.getTransactionCount(deployer); - let currentNonce = nonce + 2; // After SortitionModuleV2Neo impl+proxy - - if (!stakeControllerNeoAddress) { - stakeControllerNeoAddress = getContractAddress(deployer, currentNonce + 1); // proxy address - console.log( - "calculated future StakeControllerNeo address for nonce %d: %s", - currentNonce + 1, - stakeControllerNeoAddress - ); - currentNonce += 2; // impl + proxy - } else { - console.log("using existing StakeControllerNeo address: %s", stakeControllerNeoAddress); - } - - if (!klerosCoreV2NeoAddress) { - klerosCoreV2NeoAddress = getContractAddress(deployer, currentNonce + 1); // proxy address - console.log("calculated future KlerosCoreV2Neo address for nonce %d: %s", currentNonce + 1, klerosCoreV2NeoAddress); - } else { - console.log("using existing KlerosCoreV2Neo address: %s", klerosCoreV2NeoAddress); - } - - const devnet = isDevnet(hre.network); - const minStakingTime = devnet ? 180 : 1800; - const maxDrawingTime = devnet ? 600 : 1800; - const rng = (await ethers.getContract("ChainlinkRNG")) as ChainlinkRNG; - const maxStakePerJuror = PNK(2_000); - const maxTotalStaked = PNK(2_000_000); - - // Deploy SortitionModuleV2Neo - const sortitionModuleV2Neo = await deployUpgradable(deployments, "SortitionModuleV2Neo", { - from: deployer, - args: [deployer, stakeControllerNeoAddress, maxStakePerJuror, maxTotalStaked], - log: true, - }); // nonce (implementation), nonce+1 (proxy) - - // Deploy StakeControllerNeo (only if not already deployed) - const stakeControllerNeo = await getContractOrDeployUpgradable(hre, "StakeControllerNeo", { - from: deployer, - args: [ - deployer, - klerosCoreV2NeoAddress, - pnkVaultNeo.address, - sortitionModuleV2Neo.address, - rng.target, - minStakingTime, - maxDrawingTime, - RNG_LOOKAHEAD, - ], - log: true, - }); - - const minStake = PNK(200); - const alpha = 10000; - const feeForJuror = ETH(0.1); - const jurorsForCourtJump = 256; - - // Deploy KlerosCoreV2Neo (only if not already deployed) - const klerosCoreV2Neo = await getContractOrDeployUpgradable(hre, "KlerosCoreV2Neo", { - from: deployer, - args: [ - deployer, - deployer, - pnk.target, - ZeroAddress, // jurorProsecutionModule is not implemented yet - disputeKit.address, - false, - [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 - stakeControllerNeo.target, - nft.target, - ], - log: true, - }); - - // Configure cross-dependencies - console.log("Configuring cross-dependencies..."); - - // Configure stPNK token to allow VaultNeo operations - const stPNKContract = await ethers.getContractAt("stPNK", stPNK.address); - const currentVault = await stPNKContract.vault(); - if (currentVault !== pnkVaultNeo.address) { - console.log(`stPNK.changeVault(${pnkVaultNeo.address})`); - await stPNKContract.changeVault(pnkVaultNeo.address); - } - - // disputeKit.changeCore() only if necessary - const disputeKitContract = (await ethers.getContract("DisputeKitClassicV2Neo")) as DisputeKitClassic; - const currentCore = await disputeKitContract.core(); - if (currentCore !== klerosCoreV2Neo.target) { - console.log(`disputeKit.changeCore(${klerosCoreV2Neo.target})`); - await disputeKitContract.changeCore(klerosCoreV2Neo.target); - } - - // rng.changeSortitionModule() only if necessary - const rngSortitionModule = await rng.sortitionModule(); - if (rngSortitionModule !== stakeControllerNeo.target) { - console.log(`rng.changeSortitionModule(${stakeControllerNeo.target})`); - await rng.changeSortitionModule(stakeControllerNeo.target); - } - - const core = (await hre.ethers.getContract("KlerosCoreV2Neo")) as KlerosCoreV2Neo; - try { - // Manually set currency rates - console.log("Setting WETH currency rate..."); - await core.changeAcceptedFeeTokens(await weth.getAddress(), true); - await core.changeCurrencyRates(await weth.getAddress(), 1, 1); - } catch (e) { - console.error("failed to change currency rates:", e); - } + // TODO....... const disputeTemplateRegistry = await getContractOrDeployUpgradable(hre, "DisputeTemplateRegistry", { from: deployer, diff --git a/contracts/deploy/00-home-chain-arbitration-v2.ts b/contracts/deploy/00-home-chain-arbitration-v2.ts index 8785b001d..ffc833b84 100644 --- a/contracts/deploy/00-home-chain-arbitration-v2.ts +++ b/contracts/deploy/00-home-chain-arbitration-v2.ts @@ -101,7 +101,6 @@ const deployArbitrationV2: DeployFunction = async (hre: HardhatRuntimeEnvironmen args: [ deployer, deployer, - pnk.target, ZeroAddress, // JurorProsecutionModule, not implemented yet disputeKit.address, false, @@ -109,6 +108,7 @@ const deployArbitrationV2: DeployFunction = async (hre: HardhatRuntimeEnvironmen [0, 0, 0, 10], // evidencePeriod, commitPeriod, votePeriod, appealPeriod ethers.toBeHex(5), // Extra data for sortition module will return the default value of K stakeController.address, + vault.address, ], log: true, }); From 8ca5d4638bd13056549c1c9bb5eac902b24b09a1 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Thu, 5 Jun 2025 01:22:51 +0100 Subject: [PATCH 05/24] refactor: new staking and sortition architecture (wip) --- .../sortition-v2/SortitionModuleV2Neo.sol | 117 ------------------ .../stake-controller/StakeControllerNeo.sol | 80 ------------ 2 files changed, 197 deletions(-) delete mode 100644 contracts/src/arbitration/sortition-v2/SortitionModuleV2Neo.sol delete mode 100644 contracts/src/arbitration/stake-controller/StakeControllerNeo.sol diff --git a/contracts/src/arbitration/sortition-v2/SortitionModuleV2Neo.sol b/contracts/src/arbitration/sortition-v2/SortitionModuleV2Neo.sol deleted file mode 100644 index 69ed92a86..000000000 --- a/contracts/src/arbitration/sortition-v2/SortitionModuleV2Neo.sol +++ /dev/null @@ -1,117 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import {SortitionModuleV2Base} from "./SortitionModuleV2Base.sol"; -import {IStakeController} from "../interfaces/IStakeController.sol"; -import {KlerosCore} from "../KlerosCore.sol"; - -/// @title SortitionModuleV2Neo -/// @notice Enhanced implementation of the pure sortition module with stake limits -/// @dev Contains tree management and drawing logic with additional sortition constraints -contract SortitionModuleV2Neo is SortitionModuleV2Base { - string public constant override version = "2.1.0"; - - // ************************************* // - // * Storage * // - // ************************************* // - - uint256 public maxStakePerJuror; // Maximum stake amount per juror in any court - uint256 public maxTotalStakedInCourt; // Maximum total stake allowed per court - - // ************************************* // - // * Events * // - // ************************************* // - - /// @notice Emitted when stake limits are updated - /// @param maxStakePerJuror New maximum stake per juror - /// @param maxTotalStakedInCourt New maximum total stake per court - event StakeLimitsUpdated(uint256 maxStakePerJuror, uint256 maxTotalStakedInCourt); - - // ************************************* // - // * Constructor * // - // ************************************* // - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() { - _disableInitializers(); - } - - /// @dev Initializer (constructor equivalent for upgradable contracts). - /// @param _governor The governor's address. - /// @param _stakeController The StakeController contract. - /// @param _maxStakePerJuror Maximum stake amount per juror. - /// @param _maxTotalStakedInCourt Maximum total stake per court. - function initialize( - address _governor, - IStakeController _stakeController, - uint256 _maxStakePerJuror, - uint256 _maxTotalStakedInCourt - ) external initializer { - __SortitionModuleV2Base_initialize(_governor, _stakeController); - - maxStakePerJuror = _maxStakePerJuror; - maxTotalStakedInCourt = _maxTotalStakedInCourt; - - emit StakeLimitsUpdated(_maxStakePerJuror, _maxTotalStakedInCourt); - } - - // ************************************* // - // * Governance * // - // ************************************* // - - /// @dev Access Control to perform implementation upgrades (UUPS Proxiable) - /// Only the governor can perform upgrades (`onlyByGovernor`) - function _authorizeUpgrade(address) internal view override onlyByGovernor { - // NOP - } - - /// @dev Change the maximum stake per juror - /// @param _maxStakePerJuror The new maximum stake per juror - function changeMaxStakePerJuror(uint256 _maxStakePerJuror) external onlyByGovernor { - maxStakePerJuror = _maxStakePerJuror; - emit StakeLimitsUpdated(_maxStakePerJuror, maxTotalStakedInCourt); - } - - /// @dev Change the maximum total stake per court - /// @param _maxTotalStakedInCourt The new maximum total stake per court - function changeMaxTotalStakedInCourt(uint256 _maxTotalStakedInCourt) external onlyByGovernor { - maxTotalStakedInCourt = _maxTotalStakedInCourt; - emit StakeLimitsUpdated(maxStakePerJuror, _maxTotalStakedInCourt); - } - - // ************************************* // - // * Enhanced Sortition * // - // ************************************* // - - /// @dev Enhanced setStake with validation of stake limits - /// @param _account The juror address - /// @param _courtID The court ID - /// @param _newStake The new stake amount - /// @return success Whether the operation was successful - function setStake( - address _account, - uint96 _courtID, - uint256 _newStake - ) external override onlyByStakeController returns (bool success) { - // Validate stake limits before setting - if (_newStake > 0) { - // Check maximum stake per juror - if (_newStake > maxStakePerJuror) { - return false; // Above maximum stake per juror - } - - // Check maximum total stake in court - uint256 currentTotalStake = this.getTotalStakeInCourt(_courtID); - uint256 currentJurorStake = this.stakeOf(_account, _courtID); - uint256 stakeIncrease = _newStake > currentJurorStake ? _newStake - currentJurorStake : 0; - - if (currentTotalStake + stakeIncrease > maxTotalStakedInCourt) { - return false; // Would exceed court total stake limit - } - } - - // If all validations pass, use the base implementation - return _setStake(_account, _courtID, _newStake); - } -} diff --git a/contracts/src/arbitration/stake-controller/StakeControllerNeo.sol b/contracts/src/arbitration/stake-controller/StakeControllerNeo.sol deleted file mode 100644 index 7e30db0d4..000000000 --- a/contracts/src/arbitration/stake-controller/StakeControllerNeo.sol +++ /dev/null @@ -1,80 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import {StakeControllerBase, IVault} from "./StakeControllerBase.sol"; -import {ISortitionModuleV2} from "../interfaces/ISortitionModuleV2.sol"; -import {IDisputeKit} from "../interfaces/IDisputeKit.sol"; -import {KlerosCoreV2Base} from "../core-v2/KlerosCoreV2Base.sol"; -import {RNG} from "../../rng/RNG.sol"; - -/// @title StakeControllerNeo -/// @notice ?? -/// @dev Coordinates between Vault and SortitionModule -contract StakeControllerNeo is StakeControllerBase { - string public constant override version = "1.0.0"; - - // ************************************* // - // * Storage * // - // ************************************* // - - // no storage - - // ************************************* // - // * Events * // - // ************************************* // - - // ************************************* // - // * Constructor * // - // ************************************* // - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() { - _disableInitializers(); - } - - /// @dev Initializer (constructor equivalent for upgradable contracts). - /// @param _governor The governor's address. - /// @param _core The KlerosCore contract. - /// @param _vault The Vault contract. - /// @param _sortitionModule The SortitionModule contract. - /// @param _minStakingTime The minimum staking time. - /// @param _maxDrawingTime The maximum drawing time. - /// @param _rng The random number generator. - /// @param _rngLookahead The RNG lookahead time. - function initialize( - address _governor, - KlerosCoreV2Base _core, - IVault _vault, - ISortitionModuleV2 _sortitionModule, - uint256 _minStakingTime, - uint256 _maxDrawingTime, - RNG _rng, - uint256 _rngLookahead - ) external reinitializer(2) { - __StakeControllerBase_initialize( - _governor, - _core, - _vault, - _sortitionModule, - _minStakingTime, - _maxDrawingTime, - _rng, - _rngLookahead - ); - } - - // ************************************* // - // * Governance * // - // ************************************* // - - /// @dev Access Control to perform implementation upgrades (UUPS Proxiable) - /// Only the governor can perform upgrades (`onlyByGovernor`) - function _authorizeUpgrade(address) internal view override onlyByGovernor { - // NOP - } - - // ************************************* // - // * State Modifiers * // - // ************************************* // -} From 1611b4bfb383dc842bf1beae10a64681768e589a Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Thu, 5 Jun 2025 01:48:02 +0100 Subject: [PATCH 06/24] refactor: moved the new arbitration contracts around --- .../00-home-chain-arbitration-v2-neo.ts | 8 ++-- .../deploy/00-home-chain-arbitration-v2.ts | 22 +++++----- contracts/deploy/utils/klerosCoreHelper.ts | 4 +- .../KlerosCoreV2.sol => KlerosCoreX.sol} | 8 ++-- ...erosCoreV2Base.sol => KlerosCoreXBase.sol} | 24 +++++----- ...KlerosCoreV2Neo.sol => KlerosCoreXNeo.sol} | 8 ++-- ...itionModuleV2.sol => SortitionSumTree.sol} | 11 +++-- ...uleV2Base.sol => SortitionSumTreeBase.sol} | 44 +++++++++---------- .../StakeController.sol | 12 ++--- .../StakeControllerBase.sol | 30 ++++++------- .../src/arbitration/{vault => }/Vault.sol | 0 .../src/arbitration/{vault => }/VaultBase.sol | 10 ++--- .../src/arbitration/{vault => }/VaultNeo.sol | 2 +- .../dispute-kits/DisputeKitClassic.sol | 4 +- .../dispute-kits/DisputeKitClassicBase.sol | 20 ++++----- .../dispute-kits/DisputeKitGated.sol | 4 +- .../dispute-kits/DisputeKitSybilResistant.sol | 4 +- ...tionModuleV2.sol => ISortitionSumTree.sol} | 4 +- .../interfaces/IStakeController.sol | 4 +- 19 files changed, 111 insertions(+), 112 deletions(-) rename contracts/src/arbitration/{core-v2/KlerosCoreV2.sol => KlerosCoreX.sol} (94%) rename contracts/src/arbitration/{core-v2/KlerosCoreV2Base.sol => KlerosCoreXBase.sol} (98%) rename contracts/src/arbitration/{core-v2/KlerosCoreV2Neo.sol => KlerosCoreXNeo.sol} (97%) rename contracts/src/arbitration/{sortition-v2/SortitionModuleV2.sol => SortitionSumTree.sol} (78%) rename contracts/src/arbitration/{sortition-v2/SortitionModuleV2Base.sol => SortitionSumTreeBase.sol} (93%) rename contracts/src/arbitration/{stake-controller => }/StakeController.sol (86%) rename contracts/src/arbitration/{stake-controller => }/StakeControllerBase.sol (96%) rename contracts/src/arbitration/{vault => }/Vault.sol (100%) rename contracts/src/arbitration/{vault => }/VaultBase.sol (97%) rename contracts/src/arbitration/{vault => }/VaultNeo.sol (98%) rename contracts/src/arbitration/interfaces/{ISortitionModuleV2.sol => ISortitionSumTree.sol} (98%) diff --git a/contracts/deploy/00-home-chain-arbitration-v2-neo.ts b/contracts/deploy/00-home-chain-arbitration-v2-neo.ts index 97c02ca4c..358696089 100644 --- a/contracts/deploy/00-home-chain-arbitration-v2-neo.ts +++ b/contracts/deploy/00-home-chain-arbitration-v2-neo.ts @@ -5,7 +5,7 @@ import { deployUpgradable } from "./utils/deployUpgradable"; import { HomeChains, isSkipped, isDevnet, PNK, ETH } from "./utils"; import { getContractOrDeploy, getContractOrDeployUpgradable } from "./utils/getContractOrDeploy"; import { deployERC20AndFaucet, deployERC721 } from "./utils/deployTokens"; -import { ChainlinkRNG, DisputeKitClassic, KlerosCoreV2Neo, StakeControllerNeo, VaultNeo } from "../typechain-types"; +import { ChainlinkRNG, DisputeKitClassic, KlerosCoreXNeo, StakeControllerNeo, VaultNeo } from "../typechain-types"; const deployArbitrationV2Neo: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const { ethers, deployments, getNamedAccounts, getChainId } = hre; @@ -53,7 +53,7 @@ const deployArbitrationV2Neo: DeployFunction = async (hre: HardhatRuntimeEnviron console.log(`core.changeArbitrableWhitelist(${resolver.address}, true)`); await core.changeArbitrableWhitelist(resolver.address, true); - await deploy("KlerosCoreV2NeoSnapshotProxy", { + await deploy("KlerosCoreXNeoSnapshotProxy", { from: deployer, contract: "KlerosCoreSnapshotProxy", args: [deployer, core.target], @@ -63,9 +63,9 @@ const deployArbitrationV2Neo: DeployFunction = async (hre: HardhatRuntimeEnviron console.log("✅ V2 Neo Architecture deployment completed successfully!"); console.log(`📦 VaultNeo: ${pnkVaultNeo.address}`); console.log(`🎫 stPNKNeo: ${stPNK.address}`); - console.log(`🎯 SortitionModuleV2Neo: ${sortitionModuleV2Neo.address}`); + console.log(`🎯 SortitionSumTreeNeo: ${sortitionModuleV2Neo.address}`); console.log(`🎮 StakeControllerNeo: ${stakeControllerNeo.target}`); - console.log(`⚖️ KlerosCoreV2Neo: ${klerosCoreV2Neo.target}`); + console.log(`⚖️ KlerosCoreXNeo: ${klerosCoreV2Neo.target}`); console.log(`🎨 JurorNFT: ${nft.target}`); console.log(`🔐 DisputeResolver: ${resolver.address}`); }; diff --git a/contracts/deploy/00-home-chain-arbitration-v2.ts b/contracts/deploy/00-home-chain-arbitration-v2.ts index ffc833b84..64fc0e75c 100644 --- a/contracts/deploy/00-home-chain-arbitration-v2.ts +++ b/contracts/deploy/00-home-chain-arbitration-v2.ts @@ -5,7 +5,7 @@ import { deployUpgradable } from "./utils/deployUpgradable"; import { HomeChains, isSkipped, isDevnet, PNK, ETH } from "./utils"; import { getContractOrDeploy, getContractOrDeployUpgradable } from "./utils/getContractOrDeploy"; import { deployERC20AndFaucet } from "./utils/deployTokens"; -import { ChainlinkRNG, DisputeKitClassic, KlerosCoreV2, StakeController, Vault } from "../typechain-types"; +import { ChainlinkRNG, DisputeKitClassic, KlerosCoreX, StakeController, Vault } from "../typechain-types"; import { changeCurrencyRate } from "./utils/klerosCoreHelper"; const deployArbitrationV2: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { @@ -34,7 +34,7 @@ const deployArbitrationV2: DeployFunction = async (hre: HardhatRuntimeEnvironmen contract: "DisputeKitClassic", args: [ deployer, - ZeroAddress, // Placeholder for KlerosCoreV2 address, configured later + ZeroAddress, // Placeholder for KlerosCoreX address, configured later ], log: true, }); @@ -49,7 +49,7 @@ const deployArbitrationV2: DeployFunction = async (hre: HardhatRuntimeEnvironmen console.log("calculated future StakeController address for nonce %d: %s", nonce + 7, stakeControllerAddress); const klerosCoreV2Address = getContractAddress(deployer, nonce + 9); // deployed on the 10th tx (nonce+9): SortitionModule Impl tx, SortitionModule Proxy tx, StakeController Impl tx, StakeController Proxy tx, KlerosCore Impl tx, KlerosCore Proxy tx - console.log("calculated future KlerosCoreV2 address for nonce %d: %s", nonce + 9, klerosCoreV2Address); + console.log("calculated future KlerosCoreX address for nonce %d: %s", nonce + 9, klerosCoreV2Address); const stPNK = await deployUpgradable(deployments, "stPNK", { from: deployer, @@ -63,8 +63,8 @@ const deployArbitrationV2: DeployFunction = async (hre: HardhatRuntimeEnvironmen log: true, }); // nonce + 2 (implementation), nonce + 3 (proxy) - // Deploy SortitionModuleV2 - const sortitionModuleV2 = await deployUpgradable(deployments, "SortitionModuleV2", { + // Deploy SortitionSumTree + const sortitionModuleV2 = await deployUpgradable(deployments, "SortitionSumTree", { from: deployer, args: [deployer, stakeControllerAddress], log: true, @@ -95,8 +95,8 @@ const deployArbitrationV2: DeployFunction = async (hre: HardhatRuntimeEnvironmen const feeForJuror = ETH(0.1); const jurorsForCourtJump = 256; - // Deploy KlerosCoreV2 (only if not already deployed) - const klerosCoreV2 = await deployUpgradable(deployments, "KlerosCoreV2", { + // Deploy KlerosCoreX (only if not already deployed) + const klerosCoreV2 = await deployUpgradable(deployments, "KlerosCoreX", { from: deployer, args: [ deployer, @@ -132,7 +132,7 @@ const deployArbitrationV2: DeployFunction = async (hre: HardhatRuntimeEnvironmen await rng.changeSortitionModule(stakeController.address); } - const core = (await hre.ethers.getContract("KlerosCoreV2")) as KlerosCoreV2; + const core = (await hre.ethers.getContract("KlerosCoreX")) as KlerosCoreX; try { await changeCurrencyRate(core, await pnk.getAddress(), true, 12225583, 12); await changeCurrencyRate(core, await dai.getAddress(), true, 60327783, 11); @@ -141,7 +141,7 @@ const deployArbitrationV2: DeployFunction = async (hre: HardhatRuntimeEnvironmen console.error("failed to change currency rates:", e); } - await deploy("KlerosCoreV2SnapshotProxy", { + await deploy("KlerosCoreXSnapshotProxy", { from: deployer, contract: "KlerosCoreSnapshotProxy", args: [deployer, core.target], @@ -151,9 +151,9 @@ const deployArbitrationV2: DeployFunction = async (hre: HardhatRuntimeEnvironmen console.log("✅ V2 Architecture deployment completed successfully!"); console.log(`📦 Vault: ${vault.address}`); console.log(`🎫 stPNK: ${stPNK.address}`); - console.log(`🎯 SortitionModuleV2: ${sortitionModuleV2.address}`); + console.log(`🎯 SortitionSumTree: ${sortitionModuleV2.address}`); console.log(`🎮 StakeController: ${stakeController.address}`); - console.log(`⚖️ KlerosCoreV2: ${klerosCoreV2.address}`); + console.log(`⚖️ KlerosCoreX: ${klerosCoreV2.address}`); }; deployArbitrationV2.tags = ["ArbitrationV2"]; diff --git a/contracts/deploy/utils/klerosCoreHelper.ts b/contracts/deploy/utils/klerosCoreHelper.ts index 8da7d45ad..0941a8543 100644 --- a/contracts/deploy/utils/klerosCoreHelper.ts +++ b/contracts/deploy/utils/klerosCoreHelper.ts @@ -1,8 +1,8 @@ -import { KlerosCore, KlerosCoreNeo, KlerosCoreRuler, KlerosCoreUniversity, KlerosCoreV2 } from "../../typechain-types"; +import { KlerosCore, KlerosCoreNeo, KlerosCoreRuler, KlerosCoreUniversity, KlerosCoreX } from "../../typechain-types"; import { BigNumberish, toBigInt } from "ethers"; export const changeCurrencyRate = async ( - core: KlerosCore | KlerosCoreNeo | KlerosCoreRuler | KlerosCoreUniversity | KlerosCoreV2, + core: KlerosCore | KlerosCoreNeo | KlerosCoreRuler | KlerosCoreUniversity | KlerosCoreX, erc20: string, accepted: boolean, rateInEth: BigNumberish, diff --git a/contracts/src/arbitration/core-v2/KlerosCoreV2.sol b/contracts/src/arbitration/KlerosCoreX.sol similarity index 94% rename from contracts/src/arbitration/core-v2/KlerosCoreV2.sol rename to contracts/src/arbitration/KlerosCoreX.sol index 3ae194319..f820b6f38 100644 --- a/contracts/src/arbitration/core-v2/KlerosCoreV2.sol +++ b/contracts/src/arbitration/KlerosCoreX.sol @@ -2,11 +2,11 @@ pragma solidity 0.8.24; -import "./KlerosCoreV2Base.sol"; +import "./KlerosCoreXBase.sol"; -/// @title KlerosCoreV2 +/// @title KlerosCoreX /// @notice KlerosCore implementation with new StakeController architecture for testing environments -contract KlerosCoreV2 is KlerosCoreV2Base { +contract KlerosCoreX is KlerosCoreXBase { /// @notice Version of the implementation contract string public constant override version = "0.0.1"; @@ -42,7 +42,7 @@ contract KlerosCoreV2 is KlerosCoreV2Base { IStakeController _stakeController, IVault _vault ) external initializer { - __KlerosCoreV2Base_initialize( + __KlerosCoreXBase_initialize( _governor, _guardian, _jurorProsecutionModule, diff --git a/contracts/src/arbitration/core-v2/KlerosCoreV2Base.sol b/contracts/src/arbitration/KlerosCoreXBase.sol similarity index 98% rename from contracts/src/arbitration/core-v2/KlerosCoreV2Base.sol rename to contracts/src/arbitration/KlerosCoreXBase.sol index 64ad8d6f5..9c9cc9c83 100644 --- a/contracts/src/arbitration/core-v2/KlerosCoreV2Base.sol +++ b/contracts/src/arbitration/KlerosCoreXBase.sol @@ -2,19 +2,19 @@ pragma solidity 0.8.24; -import {IArbitrableV2, IArbitratorV2} from "../interfaces/IArbitratorV2.sol"; -import {IDisputeKit} from "../interfaces/IDisputeKit.sol"; -import {IStakeController, StakingResult, OnError} from "../interfaces/IStakeController.sol"; -import {IVault} from "../interfaces/IVault.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 KlerosCoreV2Base +import {IArbitrableV2, IArbitratorV2} from "./interfaces/IArbitratorV2.sol"; +import {IDisputeKit} from "./interfaces/IDisputeKit.sol"; +import {IStakeController} from "./interfaces/IStakeController.sol"; +import {IVault} from "./interfaces/IVault.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 KlerosCoreXBase /// Core arbitrator contract for Kleros v2 with new StakeController architecture. /// Note that this contract trusts the PNK token, the dispute kit and the stake controller contracts. -abstract contract KlerosCoreV2Base is IArbitratorV2, Initializable, UUPSProxiable { +abstract contract KlerosCoreXBase is IArbitratorV2, Initializable, UUPSProxiable { using SafeERC20 for IERC20; // ************************************* // @@ -191,7 +191,7 @@ abstract contract KlerosCoreV2Base is IArbitratorV2, Initializable, UUPSProxiabl // * Constructor * // // ************************************* // - function __KlerosCoreV2Base_initialize( + function __KlerosCoreXBase_initialize( address _governor, address _guardian, address _jurorProsecutionModule, diff --git a/contracts/src/arbitration/core-v2/KlerosCoreV2Neo.sol b/contracts/src/arbitration/KlerosCoreXNeo.sol similarity index 97% rename from contracts/src/arbitration/core-v2/KlerosCoreV2Neo.sol rename to contracts/src/arbitration/KlerosCoreXNeo.sol index 3bec3e361..de28de272 100644 --- a/contracts/src/arbitration/core-v2/KlerosCoreV2Neo.sol +++ b/contracts/src/arbitration/KlerosCoreXNeo.sol @@ -2,12 +2,12 @@ pragma solidity 0.8.24; -import "./KlerosCoreV2Base.sol"; +import "./KlerosCoreXBase.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; -/// @title KlerosCoreV2Neo +/// @title KlerosCoreXNeo /// @notice KlerosCore implementation with new StakeController architecture for production environments -contract KlerosCoreV2Neo is KlerosCoreV2Base { +contract KlerosCoreXNeo is KlerosCoreXBase { /// @notice Version of the implementation contract string public constant override version = "0.0.1"; @@ -51,7 +51,7 @@ contract KlerosCoreV2Neo is KlerosCoreV2Base { IVault _vault, IERC721 _jurorNft ) external reinitializer(2) { - __KlerosCoreV2Base_initialize( + __KlerosCoreXBase_initialize( _governor, _guardian, _jurorProsecutionModule, diff --git a/contracts/src/arbitration/sortition-v2/SortitionModuleV2.sol b/contracts/src/arbitration/SortitionSumTree.sol similarity index 78% rename from contracts/src/arbitration/sortition-v2/SortitionModuleV2.sol rename to contracts/src/arbitration/SortitionSumTree.sol index 7a64a035e..08beb6880 100644 --- a/contracts/src/arbitration/sortition-v2/SortitionModuleV2.sol +++ b/contracts/src/arbitration/SortitionSumTree.sol @@ -2,14 +2,13 @@ pragma solidity 0.8.24; -import {SortitionModuleV2Base} from "./SortitionModuleV2Base.sol"; -import {IStakeController} from "../interfaces/IStakeController.sol"; -import {KlerosCore} from "../KlerosCore.sol"; +import {SortitionSumTreeBase} from "./SortitionSumTreeBase.sol"; +import {IStakeController} from "./interfaces/IStakeController.sol"; -/// @title SortitionModuleV2 +/// @title SortitionSumTree /// @notice Basic implementation of the pure sortition module /// @dev Contains only tree management and drawing logic, no phase management or token operations -contract SortitionModuleV2 is SortitionModuleV2Base { +contract SortitionSumTree is SortitionSumTreeBase { string public constant override version = "2.0.0"; // ************************************* // @@ -25,7 +24,7 @@ contract SortitionModuleV2 is SortitionModuleV2Base { /// @param _governor The governor's address. /// @param _stakeController The StakeController contract. function initialize(address _governor, IStakeController _stakeController) external initializer { - __SortitionModuleV2Base_initialize(_governor, _stakeController); + __SortitionSumTreeBase_initialize(_governor, _stakeController); } // ************************************* // diff --git a/contracts/src/arbitration/sortition-v2/SortitionModuleV2Base.sol b/contracts/src/arbitration/SortitionSumTreeBase.sol similarity index 93% rename from contracts/src/arbitration/sortition-v2/SortitionModuleV2Base.sol rename to contracts/src/arbitration/SortitionSumTreeBase.sol index 0d34b6db8..62820116b 100644 --- a/contracts/src/arbitration/sortition-v2/SortitionModuleV2Base.sol +++ b/contracts/src/arbitration/SortitionSumTreeBase.sol @@ -10,17 +10,17 @@ pragma solidity 0.8.24; -import {ISortitionModuleV2} from "../interfaces/ISortitionModuleV2.sol"; -import {IStakeController} from "../interfaces/IStakeController.sol"; -import {KlerosCoreV2Base} from "../core-v2/KlerosCoreV2Base.sol"; -import {Initializable} from "../../proxy/Initializable.sol"; -import {UUPSProxiable} from "../../proxy/UUPSProxiable.sol"; -import "../../libraries/Constants.sol"; - -/// @title SortitionModuleV2Base +import {ISortitionSumTree} from "./interfaces/ISortitionSumTree.sol"; +import {IStakeController} from "./interfaces/IStakeController.sol"; +import {KlerosCoreXBase} from "./KlerosCoreXBase.sol"; +import {Initializable} from "../proxy/Initializable.sol"; +import {UUPSProxiable} from "../proxy/UUPSProxiable.sol"; +import "../libraries/Constants.sol"; + +/// @title SortitionSumTreeBase /// @notice Abstract base contract for pure sortition operations /// @dev Contains only tree management and drawing logic, no phase management or token operations -abstract contract SortitionModuleV2Base is ISortitionModuleV2, Initializable, UUPSProxiable { +abstract contract SortitionSumTreeBase is ISortitionSumTree, Initializable, UUPSProxiable { // ************************************* // // * Enums / Structs * // // ************************************* // @@ -68,7 +68,7 @@ abstract contract SortitionModuleV2Base is ISortitionModuleV2, Initializable, UU // * Constructor * // // ************************************* // - function __SortitionModuleV2Base_initialize( + function __SortitionSumTreeBase_initialize( address _governor, IStakeController _stakeController ) internal onlyInitializing { @@ -96,7 +96,7 @@ abstract contract SortitionModuleV2Base is ISortitionModuleV2, Initializable, UU // * Tree Management * // // ************************************* // - /// @inheritdoc ISortitionModuleV2 + /// @inheritdoc ISortitionSumTree function createTree(bytes32 _key, bytes memory _extraData) external override onlyByStakeController { SortitionSumTree storage tree = sortitionSumTrees[_key]; uint256 K = _extraDataToTreeK(_extraData); @@ -106,7 +106,7 @@ abstract contract SortitionModuleV2Base is ISortitionModuleV2, Initializable, UU tree.nodes.push(0); } - /// @inheritdoc ISortitionModuleV2 + /// @inheritdoc ISortitionSumTree function setStake( address _account, uint96 _courtID, @@ -119,7 +119,7 @@ abstract contract SortitionModuleV2Base is ISortitionModuleV2, Initializable, UU // * Drawing * // // ************************************* // - /// @inheritdoc ISortitionModuleV2 + /// @inheritdoc ISortitionSumTree function draw( bytes32 _court, uint256 _coreDisputeID, @@ -160,13 +160,13 @@ abstract contract SortitionModuleV2Base is ISortitionModuleV2, Initializable, UU // * View Functions * // // ************************************* // - /// @inheritdoc ISortitionModuleV2 + /// @inheritdoc ISortitionSumTree function stakeOf(address _juror, uint96 _courtID) external view override returns (uint256 value) { bytes32 stakePathID = _accountAndCourtIDToStakePathID(_juror, _courtID); return stakeOf(bytes32(uint256(_courtID)), stakePathID); } - /// @inheritdoc ISortitionModuleV2 + /// @inheritdoc ISortitionSumTree function stakeOf(bytes32 _key, bytes32 _ID) public view override returns (uint256) { SortitionSumTree storage tree = sortitionSumTrees[_key]; uint treeIndex = tree.IDsToNodeIndexes[_ID]; @@ -176,7 +176,7 @@ abstract contract SortitionModuleV2Base is ISortitionModuleV2, Initializable, UU return tree.nodes[treeIndex]; } - /// @inheritdoc ISortitionModuleV2 + /// @inheritdoc ISortitionSumTree function getJurorInfo( address _juror, uint96 _courtID @@ -193,24 +193,24 @@ abstract contract SortitionModuleV2Base is ISortitionModuleV2, Initializable, UU nbCourts = info.courtIDs.length; } - /// @inheritdoc ISortitionModuleV2 + /// @inheritdoc ISortitionSumTree function getJurorCourtIDs(address _juror) external view override returns (uint96[] memory) { return jurorStakeInfo[_juror].courtIDs; } - /// @inheritdoc ISortitionModuleV2 + /// @inheritdoc ISortitionSumTree function hasStakes(address _juror) external view override returns (bool) { return jurorStakeInfo[_juror].courtIDs.length > 0; } - /// @inheritdoc ISortitionModuleV2 + /// @inheritdoc ISortitionSumTree function getTotalStakeInCourt(uint96 _courtID) external view override returns (uint256) { SortitionSumTree storage tree = sortitionSumTrees[bytes32(uint256(_courtID))]; if (tree.nodes.length == 0) return 0; return tree.nodes[0]; // Root node contains total stake } - /// @inheritdoc ISortitionModuleV2 + /// @inheritdoc ISortitionSumTree function accountAndCourtIDToStakePathID( address _account, uint96 _courtID @@ -218,7 +218,7 @@ abstract contract SortitionModuleV2Base is ISortitionModuleV2, Initializable, UU return _accountAndCourtIDToStakePathID(_account, _courtID); } - /// @inheritdoc ISortitionModuleV2 + /// @inheritdoc ISortitionSumTree function stakePathIDToAccount(bytes32 _stakePathID) external pure override returns (address account) { return _stakePathIDToAccount(_stakePathID); } @@ -246,7 +246,7 @@ abstract contract SortitionModuleV2Base is ISortitionModuleV2, Initializable, UU bytes32 stakePathID = _accountAndCourtIDToStakePathID(_account, _courtID); bool finished = false; uint96 currentCourtID = _courtID; - KlerosCoreV2Base core = stakeController.core(); + KlerosCoreXBase core = stakeController.core(); while (!finished) { // Tokens are also implicitly staked in parent courts through sortition module to increase the chance of being drawn. _set(bytes32(uint256(currentCourtID)), _newStake, stakePathID); diff --git a/contracts/src/arbitration/stake-controller/StakeController.sol b/contracts/src/arbitration/StakeController.sol similarity index 86% rename from contracts/src/arbitration/stake-controller/StakeController.sol rename to contracts/src/arbitration/StakeController.sol index e7993e809..a5830b076 100644 --- a/contracts/src/arbitration/stake-controller/StakeController.sol +++ b/contracts/src/arbitration/StakeController.sol @@ -3,10 +3,10 @@ pragma solidity 0.8.24; import {StakeControllerBase} from "./StakeControllerBase.sol"; -import {IVault} from "../interfaces/IVault.sol"; -import {ISortitionModuleV2} from "../interfaces/ISortitionModuleV2.sol"; -import {KlerosCoreV2Base} from "../core-v2/KlerosCoreV2Base.sol"; -import {RNG} from "../../rng/RNG.sol"; +import {IVault} from "./interfaces/IVault.sol"; +import {ISortitionSumTree} from "./interfaces/ISortitionSumTree.sol"; +import {KlerosCoreXBase} from "./KlerosCoreXBase.sol"; +import {RNG} from "../rng/RNG.sol"; /// @title StakeController /// @notice Basic implementation of the Stake Controller @@ -34,9 +34,9 @@ contract StakeController is StakeControllerBase { /// @param _rngLookahead The RNG lookahead time. function initialize( address _governor, - KlerosCoreV2Base _core, + KlerosCoreXBase _core, IVault _vault, - ISortitionModuleV2 _sortitionModule, + ISortitionSumTree _sortitionModule, uint256 _minStakingTime, uint256 _maxDrawingTime, RNG _rng, diff --git a/contracts/src/arbitration/stake-controller/StakeControllerBase.sol b/contracts/src/arbitration/StakeControllerBase.sol similarity index 96% rename from contracts/src/arbitration/stake-controller/StakeControllerBase.sol rename to contracts/src/arbitration/StakeControllerBase.sol index 6dea571ec..d7d726f25 100644 --- a/contracts/src/arbitration/stake-controller/StakeControllerBase.sol +++ b/contracts/src/arbitration/StakeControllerBase.sol @@ -2,15 +2,15 @@ pragma solidity 0.8.24; -import {IStakeController} from "../interfaces/IStakeController.sol"; -import {IVault} from "../interfaces/IVault.sol"; -import {ISortitionModuleV2} from "../interfaces/ISortitionModuleV2.sol"; -import {IDisputeKit} from "../interfaces/IDisputeKit.sol"; -import {KlerosCoreV2Base} from "../core-v2/KlerosCoreV2Base.sol"; -import {Initializable} from "../../proxy/Initializable.sol"; -import {UUPSProxiable} from "../../proxy/UUPSProxiable.sol"; -import {RNG} from "../../rng/RNG.sol"; -import "../../libraries/Constants.sol"; +import {IStakeController} from "./interfaces/IStakeController.sol"; +import {IVault} from "./interfaces/IVault.sol"; +import {ISortitionSumTree} from "./interfaces/ISortitionSumTree.sol"; +import {IDisputeKit} from "./interfaces/IDisputeKit.sol"; +import {KlerosCoreXBase} from "./KlerosCoreXBase.sol"; +import {Initializable} from "../proxy/Initializable.sol"; +import {UUPSProxiable} from "../proxy/UUPSProxiable.sol"; +import {RNG} from "../rng/RNG.sol"; +import "../libraries/Constants.sol"; /// @title StakeControllerBase /// @notice Abstract base contract for coordinating between Vault and SortitionModule @@ -32,9 +32,9 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr // ************************************* // address public governor; // The governor of the contract. - KlerosCoreV2Base public core; // The core arbitrator contract. + KlerosCoreXBase public core; // The core arbitrator contract. IVault public vault; // The PNK vault for token management. - ISortitionModuleV2 public sortitionModule; // The sortition module for drawing logic. + ISortitionSumTree public sortitionModule; // The sortition module for drawing logic. // Phase management Phase public override phase; // The current phase. Uses Phase from IStakeController. @@ -73,9 +73,9 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr function __StakeControllerBase_initialize( address _governor, - KlerosCoreV2Base _core, + KlerosCoreXBase _core, IVault _vault, - ISortitionModuleV2 _sortitionModule, + ISortitionSumTree _sortitionModule, uint256 _minStakingTime, uint256 _maxDrawingTime, RNG _rng, @@ -111,7 +111,7 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr /// @dev Changes the `sortitionModule` storage variable. /// @param _sortitionModule The new sortition module address. - function changeSortitionModule(ISortitionModuleV2 _sortitionModule) external onlyByGovernor { + function changeSortitionModule(ISortitionSumTree _sortitionModule) external onlyByGovernor { sortitionModule = _sortitionModule; } @@ -188,7 +188,7 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr // which calls this StakeController's setStake. This creates a circular dependency if not careful. // For delayed stakes, the StakeController should probably update SortitionModule directly // and then tell KlerosCore to handle the deposit/withdrawal with the Vault if needed. - // OR, the KlerosCoreV2Base.setStakeBySortitionModule needs to exist and be smart enough. + // OR, the KlerosCoreXBase.setStakeBySortitionModule needs to exist and be smart enough. // For now, assuming KlerosCore will have a way to apply this, or this internal _setStake is used. // This part needs careful review of how KlerosCore consumes delayed stakes. diff --git a/contracts/src/arbitration/vault/Vault.sol b/contracts/src/arbitration/Vault.sol similarity index 100% rename from contracts/src/arbitration/vault/Vault.sol rename to contracts/src/arbitration/Vault.sol diff --git a/contracts/src/arbitration/vault/VaultBase.sol b/contracts/src/arbitration/VaultBase.sol similarity index 97% rename from contracts/src/arbitration/vault/VaultBase.sol rename to contracts/src/arbitration/VaultBase.sol index b6ce4a003..33bb47a90 100644 --- a/contracts/src/arbitration/vault/VaultBase.sol +++ b/contracts/src/arbitration/VaultBase.sol @@ -2,11 +2,11 @@ pragma solidity 0.8.24; -import {IVault} from "../interfaces/IVault.sol"; -import {stPNK} from "../stPNK.sol"; -import {Initializable} from "../../proxy/Initializable.sol"; -import {UUPSProxiable} from "../../proxy/UUPSProxiable.sol"; -import {SafeERC20, IERC20} from "../../libraries/SafeERC20.sol"; +import {IVault} from "./interfaces/IVault.sol"; +import {stPNK} from "./stPNK.sol"; +import {Initializable} from "../proxy/Initializable.sol"; +import {UUPSProxiable} from "../proxy/UUPSProxiable.sol"; +import {SafeERC20, IERC20} from "../libraries/SafeERC20.sol"; /// @title VaultBase /// @notice Abstract base contract for PNK vault that handles deposits, withdrawals, locks, and penalties diff --git a/contracts/src/arbitration/vault/VaultNeo.sol b/contracts/src/arbitration/VaultNeo.sol similarity index 98% rename from contracts/src/arbitration/vault/VaultNeo.sol rename to contracts/src/arbitration/VaultNeo.sol index 3f08514cd..5d81c184c 100644 --- a/contracts/src/arbitration/vault/VaultNeo.sol +++ b/contracts/src/arbitration/VaultNeo.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.24; import {VaultBase, IERC20, stPNK} from "./VaultBase.sol"; -import {SafeERC20} from "../../libraries/SafeERC20.sol"; +import {SafeERC20} from "../libraries/SafeERC20.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; /// @title VaultNeo diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitClassic.sol b/contracts/src/arbitration/dispute-kits/DisputeKitClassic.sol index 39bfac324..15a4188c4 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitClassic.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitClassic.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.24; -import {DisputeKitClassicBase, KlerosCoreV2} from "./DisputeKitClassicBase.sol"; +import {DisputeKitClassicBase, KlerosCoreX} from "./DisputeKitClassicBase.sol"; /// @title DisputeKitClassic /// Dispute kit implementation of the Kleros v1 features including: @@ -25,7 +25,7 @@ contract DisputeKitClassic is DisputeKitClassicBase { /// @dev Initializer. /// @param _governor The governor's address. /// @param _core The KlerosCore arbitrator. - function initialize(address _governor, KlerosCoreV2 _core) external reinitializer(1) { + function initialize(address _governor, KlerosCoreX _core) external reinitializer(1) { __DisputeKitClassicBase_initialize(_governor, _core); } diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol index 6d9b27756..1d2794b75 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.24; -import {KlerosCoreV2, KlerosCoreV2Base, IDisputeKit, IStakeController} from "../core-v2/KlerosCoreV2.sol"; +import {KlerosCoreX, KlerosCoreXBase, IDisputeKit, IStakeController} from "../KlerosCoreX.sol"; import {Initializable} from "../../proxy/Initializable.sol"; import {UUPSProxiable} from "../../proxy/UUPSProxiable.sol"; @@ -57,7 +57,7 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi uint256 public constant ONE_BASIS_POINT = 10000; // One basis point, for scaling. address public governor; // The governor of the contract. - KlerosCoreV2 public core; // The Kleros Core arbitrator + KlerosCoreX 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. @@ -141,7 +141,7 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi /// @dev Initializer. /// @param _governor The governor's address. /// @param _core The KlerosCore arbitrator. - function __DisputeKitClassicBase_initialize(address _governor, KlerosCoreV2 _core) internal onlyInitializing { + function __DisputeKitClassicBase_initialize(address _governor, KlerosCoreX _core) internal onlyInitializing { governor = _governor; core = _core; } @@ -172,7 +172,7 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi /// @dev Changes the `core` storage variable. /// @param _core The new value for the `core` storage variable. function changeCore(address _core) external onlyByGovernor { - core = KlerosCoreV2(_core); + core = KlerosCoreX(_core); } // ************************************* // @@ -247,8 +247,8 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi uint256[] calldata _voteIDs, bytes32 _commit ) external notJumped(_coreDisputeID) { - (, , KlerosCoreV2Base.Period period, , ) = core.disputes(_coreDisputeID); - require(period == KlerosCoreV2Base.Period.commit, "The dispute should be in Commit period."); + (, , KlerosCoreXBase.Period period, , ) = core.disputes(_coreDisputeID); + require(period == KlerosCoreXBase.Period.commit, "The dispute should be in Commit period."); require(_commit != bytes32(0), "Empty commit."); Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; @@ -276,8 +276,8 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi uint256 _salt, string memory _justification ) external notJumped(_coreDisputeID) { - (, , KlerosCoreV2Base.Period period, , ) = core.disputes(_coreDisputeID); - require(period == KlerosCoreV2Base.Period.vote, "The dispute should be in Vote period."); + (, , KlerosCoreXBase.Period period, , ) = core.disputes(_coreDisputeID); + require(period == KlerosCoreXBase.Period.vote, "The dispute should be in Vote period."); require(_voteIDs.length > 0, "No voteID provided"); Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; @@ -456,9 +456,9 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi Round storage round = dispute.rounds[dispute.rounds.length - 1]; tied = round.tied; ruling = tied ? 0 : round.winningChoice; - (, , KlerosCoreV2Base.Period period, , ) = core.disputes(_coreDisputeID); + (, , KlerosCoreXBase.Period period, , ) = core.disputes(_coreDisputeID); // Override the final ruling if only one side funded the appeals. - if (period == KlerosCoreV2Base.Period.execution) { + if (period == KlerosCoreXBase.Period.execution) { uint256[] memory fundedChoices = getFundedChoices(_coreDisputeID); if (fundedChoices.length == 1) { ruling = fundedChoices[0]; diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitGated.sol b/contracts/src/arbitration/dispute-kits/DisputeKitGated.sol index 4eadfa07e..732b45032 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitGated.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitGated.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.24; -import {DisputeKitClassicBase, KlerosCoreV2} from "./DisputeKitClassicBase.sol"; +import {DisputeKitClassicBase, KlerosCoreX} from "./DisputeKitClassicBase.sol"; interface IBalanceHolder { /// @dev Returns the number of tokens in `owner` account. @@ -54,7 +54,7 @@ contract DisputeKitGated is DisputeKitClassicBase { /// @param _isERC1155 Whether the token is an ERC-1155 function initialize( address _governor, - KlerosCoreV2 _core, + KlerosCoreX _core, address _tokenGate, uint256 _tokenId, bool _isERC1155 diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitSybilResistant.sol b/contracts/src/arbitration/dispute-kits/DisputeKitSybilResistant.sol index 8a401e9fd..4e3c2f61f 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitSybilResistant.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitSybilResistant.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.24; -import {DisputeKitClassicBase, KlerosCoreV2} from "./DisputeKitClassicBase.sol"; +import {DisputeKitClassicBase, KlerosCoreX} from "./DisputeKitClassicBase.sol"; interface IProofOfHumanity { /// @dev Return true if the submission is registered and not expired. @@ -39,7 +39,7 @@ contract DisputeKitSybilResistant is DisputeKitClassicBase { /// @param _governor The governor's address. /// @param _core The KlerosCore arbitrator. /// @param _poh The Proof of Humanity registry. - function initialize(address _governor, KlerosCoreV2 _core, IProofOfHumanity _poh) external reinitializer(1) { + function initialize(address _governor, KlerosCoreX _core, IProofOfHumanity _poh) external reinitializer(1) { __DisputeKitClassicBase_initialize(_governor, _core); poh = _poh; singleDrawPerJuror = true; diff --git a/contracts/src/arbitration/interfaces/ISortitionModuleV2.sol b/contracts/src/arbitration/interfaces/ISortitionSumTree.sol similarity index 98% rename from contracts/src/arbitration/interfaces/ISortitionModuleV2.sol rename to contracts/src/arbitration/interfaces/ISortitionSumTree.sol index f724781fe..84147575b 100644 --- a/contracts/src/arbitration/interfaces/ISortitionModuleV2.sol +++ b/contracts/src/arbitration/interfaces/ISortitionSumTree.sol @@ -4,10 +4,10 @@ pragma solidity 0.8.24; import "../../libraries/Constants.sol"; -/// @title ISortitionModuleV2 +/// @title ISortitionSumTree /// @notice Interface for pure sortition operations without phase management or token operations /// @dev This interface contains only tree management and drawing logic -interface ISortitionModuleV2 { +interface ISortitionSumTree { // ************************************* // // * Events * // // ************************************* // diff --git a/contracts/src/arbitration/interfaces/IStakeController.sol b/contracts/src/arbitration/interfaces/IStakeController.sol index 6e255d45e..4acba5679 100644 --- a/contracts/src/arbitration/interfaces/IStakeController.sol +++ b/contracts/src/arbitration/interfaces/IStakeController.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.24; -import {KlerosCoreV2Base} from "../core-v2/KlerosCoreV2Base.sol"; +import {KlerosCoreXBase} from "../KlerosCoreXBase.sol"; import "../../libraries/Constants.sol"; /// @title IStakeController @@ -167,5 +167,5 @@ interface IStakeController { /// @notice Get the core arbitrator contract /// @return The core contract - function core() external view returns (KlerosCoreV2Base); + function core() external view returns (KlerosCoreXBase); } From 98c60ecf61fb26117ca98c4bbb1dbf0e4405c1b9 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Thu, 5 Jun 2025 04:02:45 +0100 Subject: [PATCH 07/24] refactor: new staking and sortition architecture (wip) --- contracts/src/arbitration/KlerosCoreXBase.sol | 85 ++++++-- contracts/src/arbitration/KlerosCoreXNeo.sol | 2 +- .../src/arbitration/SortitionSumTreeBase.sol | 121 ++++++----- .../src/arbitration/StakeControllerBase.sol | 198 ++++++------------ .../interfaces/ISortitionSumTree.sol | 8 +- .../interfaces/IStakeController.sol | 20 +- .../src/arbitration/{ => old}/KlerosCore.sol | 0 .../arbitration/{ => old}/KlerosCoreBase.sol | 14 +- .../arbitration/{ => old}/KlerosCoreNeo.sol | 0 .../arbitration/{ => old}/SortitionModule.sol | 0 .../{ => old}/SortitionModuleBase.sol | 12 +- .../{ => old}/SortitionModuleNeo.sol | 0 contracts/src/libraries/Constants.sol | 1 + contracts/src/test/KlerosCoreMock.sol | 2 +- contracts/src/test/SortitionModuleMock.sol | 2 +- 15 files changed, 238 insertions(+), 227 deletions(-) rename contracts/src/arbitration/{ => old}/KlerosCore.sol (100%) rename contracts/src/arbitration/{ => old}/KlerosCoreBase.sol (99%) rename contracts/src/arbitration/{ => old}/KlerosCoreNeo.sol (100%) rename contracts/src/arbitration/{ => old}/SortitionModule.sol (100%) rename contracts/src/arbitration/{ => old}/SortitionModuleBase.sol (99%) rename contracts/src/arbitration/{ => old}/SortitionModuleNeo.sol (100%) diff --git a/contracts/src/arbitration/KlerosCoreXBase.sol b/contracts/src/arbitration/KlerosCoreXBase.sol index 9c9cc9c83..6ad15baba 100644 --- a/contracts/src/arbitration/KlerosCoreXBase.sol +++ b/contracts/src/arbitration/KlerosCoreXBase.sol @@ -177,6 +177,11 @@ abstract contract KlerosCoreXBase is IArbitratorV2, Initializable, UUPSProxiable _; } + modifier onlyStakeController() { + if (msg.sender != address(stakeController)) revert StakeControllerOnly(); + _; + } + modifier whenPaused() { if (!paused) revert WhenPausedOnly(); _; @@ -460,22 +465,21 @@ abstract contract KlerosCoreXBase is IArbitratorV2, Initializable, UUPSProxiable /// @param _newStake The new stake. /// Note that the existing delayed stake will be nullified as non-relevant. function setStake(uint96 _courtID, uint256 _newStake) external virtual whenNotPaused { - _setStake(msg.sender, _courtID, _newStake, false, OnError.Revert); + _setStake(msg.sender, _courtID, _newStake, OnError.Revert); } - /// @dev Sets the stake of a specified account in a court, typically to apply a delayed stake or unstake inactive jurors. - /// @param _account The account whose stake is being set. + /// @notice Executes a stake change initiated by the system (e.g., processing a delayed stake). + /// @dev Called by StakeControllerBase during executeDelayedStakes. Assumes KlerosCore holds pre-funded PNK if _depositPreFunded is true. + /// @param _account The juror's account. /// @param _courtID The ID of the court. - /// @param _newStake The new stake. - /// @param _alreadyTransferred Whether the PNKs have already been transferred to the contract. - function setStakeBySortitionModule( + /// @param _newStake The new stake amount for the juror in the court. + /// @return success Whether the stake was successfully set or not. + function setStakeBySystem( address _account, uint96 _courtID, - uint256 _newStake, - bool _alreadyTransferred - ) external { - if (msg.sender != address(stakeController)) revert SortitionModuleOnly(); - _setStake(_account, _courtID, _newStake, _alreadyTransferred, OnError.Return); + uint256 _newStake + ) external onlyStakeController returns (bool success) { + return _setStakeBySystem(_account, _courtID, _newStake); } /// @inheritdoc IArbitratorV2 @@ -1074,20 +1078,13 @@ abstract contract KlerosCoreXBase is IArbitratorV2, Initializable, UUPSProxiable emit DisputeKitEnabled(_courtID, _disputeKitID, _enable); } - /// @dev If called only once then set _onError to Revert, otherwise set it to Return + /// @dev If called only once then set _onError to Revert, otherwise for batch staking set it to Return /// @param _account The account to set the stake for. /// @param _courtID The ID of the court to set the stake for. /// @param _newStake The new stake. - /// @param _alreadyTransferred Whether the PNKs were already transferred to/from the staking contract. /// @param _onError Whether to revert or return false on error. /// @return Whether the stake was successfully set or not. - function _setStake( - address _account, - uint96 _courtID, - uint256 _newStake, - bool _alreadyTransferred, - OnError _onError - ) internal returns (bool) { + function _setStake(address _account, uint96 _courtID, uint256 _newStake, OnError _onError) internal returns (bool) { if (_courtID == FORKING_COURT || _courtID >= courts.length) { _stakingFailed(_onError, StakingResult.CannotStakeInThisCourt); // Staking directly into the forking court is not allowed. return false; @@ -1099,9 +1096,11 @@ abstract contract KlerosCoreXBase is IArbitratorV2, Initializable, UUPSProxiable (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) = stakeController.setStake( _account, _courtID, - _newStake, - _alreadyTransferred + _newStake ); + if (stakingResult == StakingResult.Delayed) { + return true; + } if (stakingResult != StakingResult.Successful) { _stakingFailed(_onError, stakingResult); return false; @@ -1127,6 +1126,47 @@ abstract contract KlerosCoreXBase is IArbitratorV2, Initializable, UUPSProxiable return true; } + /// @dev Internal implementation of setStakeBySystem + /// @param _account The account to set the stake for. + /// @param _courtID The ID of the court to set the stake for. + /// @param _newStake The new stake. + /// @return success Whether the stake was successfully set or not. + function _setStakeBySystem( + address _account, + uint96 _courtID, + uint256 _newStake + ) internal virtual returns (bool success) { + (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) = stakeController.setStakeBySystem( + _account, + _courtID, + _newStake + ); + OnError onError = OnError.Return; + if (stakingResult != StakingResult.Successful) { + _stakingFailed(onError, stakingResult); + return false; + } + if (pnkDeposit > 0) { + try vault.deposit(_account, pnkDeposit) { + // Successfully deposited PNK and minted stPNK via Vault + } catch { + // Revert with a specific error or reuse existing one + _stakingFailed(onError, StakingResult.StakingTransferFailed); // Indicating failure in the deposit part of staking + return false; + } + } + if (pnkWithdrawal > 0) { + try vault.withdraw(_account, pnkWithdrawal) { + // Successfully burned stPNK and withdrew PNK via Vault + } catch { + // Revert with a specific error or reuse existing one + _stakingFailed(onError, StakingResult.UnstakingTransferFailed); // Indicating failure in the withdrawal part of unstaking + return false; + } + } + return true; + } + /// @dev It may revert depending on the _onError parameter. function _stakingFailed(OnError _onError, StakingResult _result) internal pure virtual { if (_onError == OnError.Return) return; @@ -1179,6 +1219,7 @@ abstract contract KlerosCoreXBase is IArbitratorV2, Initializable, UUPSProxiable error GuardianOrGovernorOnly(); error DisputeKitOnly(); error SortitionModuleOnly(); + error StakeControllerOnly(); error UnsuccessfulCall(); error InvalidDisputKitParent(); error MinStakeLowerThanParentCourt(); diff --git a/contracts/src/arbitration/KlerosCoreXNeo.sol b/contracts/src/arbitration/KlerosCoreXNeo.sol index de28de272..f9d42b626 100644 --- a/contracts/src/arbitration/KlerosCoreXNeo.sol +++ b/contracts/src/arbitration/KlerosCoreXNeo.sol @@ -104,7 +104,7 @@ contract KlerosCoreXNeo is KlerosCoreXBase { /// Note that the existing delayed stake will be nullified as non-relevant. function setStake(uint96 _courtID, uint256 _newStake) external override whenNotPaused { if (jurorNft.balanceOf(msg.sender) == 0) revert NotEligibleForStaking(); - _setStake(msg.sender, _courtID, _newStake, false, OnError.Revert); + _setStake(msg.sender, _courtID, _newStake, OnError.Revert); } // ************************************* // diff --git a/contracts/src/arbitration/SortitionSumTreeBase.sol b/contracts/src/arbitration/SortitionSumTreeBase.sol index 62820116b..a1cc13a3f 100644 --- a/contracts/src/arbitration/SortitionSumTreeBase.sol +++ b/contracts/src/arbitration/SortitionSumTreeBase.sol @@ -1,13 +1,5 @@ // SPDX-License-Identifier: MIT -/** - * @custom:authors: [@epiqueras, @unknownunknown1, @jaybuidl, @shotaronowhere] - * @custom:reviewers: [] - * @custom:auditors: [] - * @custom:bounties: [] - * @custom:deployments: [] - */ - pragma solidity 0.8.24; import {ISortitionSumTree} from "./interfaces/ISortitionSumTree.sol"; @@ -111,7 +103,7 @@ abstract contract SortitionSumTreeBase is ISortitionSumTree, Initializable, UUPS address _account, uint96 _courtID, uint256 _newStake - ) external virtual override onlyByStakeController returns (bool success) { + ) external virtual override onlyByStakeController returns (StakingResult stakingResult) { return _setStake(_account, _courtID, _newStake); } @@ -228,18 +220,22 @@ abstract contract SortitionSumTreeBase is ISortitionSumTree, Initializable, UUPS // ************************************* // /// @dev Internal implementation of setStake with court hierarchy updates - function _setStake(address _account, uint96 _courtID, uint256 _newStake) internal returns (bool success) { + function _setStake( + address _account, + uint96 _courtID, + uint256 _newStake + ) internal returns (StakingResult stakingResult) { uint256 currentStake = this.stakeOf(_account, _courtID); if (currentStake == 0 && _newStake == 0) { - return false; // No change needed + return StakingResult.CannotStakeZeroWhenNoStake; // No change needed } JurorStakeInfo storage info = jurorStakeInfo[_account]; uint256 nbCourts = info.courtIDs.length; if (currentStake == 0 && nbCourts >= MAX_STAKE_PATHS) { - return false; // Prevent staking beyond MAX_STAKE_PATHS + return StakingResult.CannotStakeInMoreCourts; // Prevent staking beyond MAX_STAKE_PATHS but unstaking is always allowed. } // Update the sortition sum tree in court hierarchy @@ -280,10 +276,65 @@ abstract contract SortitionSumTreeBase is ISortitionSumTree, Initializable, UUPS uint256 totalStaked = stakeController.getDepositedBalance(_account); emit StakeSet(_account, _courtID, _newStake, totalStaked); - return true; + return StakingResult.Successful; + } + + /// @dev Update all the parents of a node. + /// @param _key The key of the tree to update. + /// @param _treeIndex The index of the node to start from. + /// @param _plusOrMinus Whether to add (true) or substract (false). + /// @param _value The value to add or substract. + /// `O(log_k(n))` where + /// `k` is the maximum number of children per node in the tree, + /// and `n` is the maximum number of nodes ever appended. + function _updateParents(bytes32 _key, uint256 _treeIndex, bool _plusOrMinus, uint256 _value) private { + SortitionSumTree storage tree = sortitionSumTrees[_key]; + + uint256 parentIndex = _treeIndex; + while (parentIndex != 0) { + parentIndex = (parentIndex - 1) / tree.K; + tree.nodes[parentIndex] = _plusOrMinus + ? tree.nodes[parentIndex] + _value + : tree.nodes[parentIndex] - _value; + } + } + + /// @dev Retrieves a juror's address from the stake path ID. + /// @param _stakePathID The stake path ID to unpack. + /// @return account The account. + function _stakePathIDToAccount(bytes32 _stakePathID) internal pure returns (address account) { + assembly { + // solium-disable-line security/no-inline-assembly + let ptr := mload(0x40) + for { + let i := 0x00 + } lt(i, 0x14) { + i := add(i, 0x01) + } { + mstore8(add(add(ptr, 0x0c), i), byte(i, _stakePathID)) + } + account := mload(ptr) + } + } + + function _extraDataToTreeK(bytes memory _extraData) internal pure returns (uint256 K) { + if (_extraData.length >= 32) { + assembly { + // solium-disable-line security/no-inline-assembly + K := mload(add(_extraData, 0x20)) + } + } else { + K = DEFAULT_K; + } } /// @dev Set a value in a tree. + /// @param _key The key of the tree. + /// @param _value The new value. + /// @param _ID The ID of the value. + /// `O(log_k(n))` where + /// `k` is the maximum number of children per node in the tree, + /// and `n` is the maximum number of nodes ever appended. function _set(bytes32 _key, uint256 _value, bytes32 _ID) internal { SortitionSumTree storage tree = sortitionSumTrees[_key]; uint256 treeIndex = tree.IDsToNodeIndexes[_ID]; @@ -356,48 +407,10 @@ abstract contract SortitionSumTreeBase is ISortitionSumTree, Initializable, UUPS } } - /// @dev Update all the parents of a node. - function _updateParents(bytes32 _key, uint256 _treeIndex, bool _plusOrMinus, uint256 _value) internal { - SortitionSumTree storage tree = sortitionSumTrees[_key]; - - uint256 parentIndex = _treeIndex; - while (parentIndex != 0) { - parentIndex = (parentIndex - 1) / tree.K; - tree.nodes[parentIndex] = _plusOrMinus - ? tree.nodes[parentIndex] + _value - : tree.nodes[parentIndex] - _value; - } - } - - /// @dev Retrieves a juror's address from the stake path ID. - function _stakePathIDToAccount(bytes32 _stakePathID) internal pure returns (address account) { - assembly { - // solium-disable-line security/no-inline-assembly - let ptr := mload(0x40) - for { - let i := 0x00 - } lt(i, 0x14) { - i := add(i, 0x01) - } { - mstore8(add(add(ptr, 0x0c), i), byte(i, _stakePathID)) - } - account := mload(ptr) - } - } - - /// @dev Extract tree K parameter from extra data - function _extraDataToTreeK(bytes memory _extraData) internal pure returns (uint256 K) { - if (_extraData.length >= 32) { - assembly { - // solium-disable-line security/no-inline-assembly - K := mload(add(_extraData, 0x20)) - } - } else { - K = DEFAULT_K; - } - } - /// @dev Packs an account and a court ID into a stake path ID. + /// @param _account The address of the juror to pack. + /// @param _courtID The court ID to pack. + /// @return stakePathID The stake path ID. function _accountAndCourtIDToStakePathID( address _account, uint96 _courtID diff --git a/contracts/src/arbitration/StakeControllerBase.sol b/contracts/src/arbitration/StakeControllerBase.sol index d7d726f25..2911076ca 100644 --- a/contracts/src/arbitration/StakeControllerBase.sol +++ b/contracts/src/arbitration/StakeControllerBase.sol @@ -24,7 +24,6 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr address account; // The address of the juror. uint96 courtID; // The ID of the court. uint256 stake; // The new stake. - bool alreadyTransferred; // True if tokens were already transferred before delayed stake's execution. } // ************************************* // @@ -51,7 +50,6 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr uint256 public delayedStakeWriteIndex; // The index of the last `delayedStake` item that was written to the array. 0 index is skipped. uint256 public delayedStakeReadIndex; // The index of the next `delayedStake` item that should be processed. Starts at 1 because 0 index is skipped. mapping(uint256 => DelayedStake) public delayedStakes; // Stores the stakes that were changed during Drawing phase, to update them when the phase is switched to Staking. - mapping(address jurorAccount => mapping(uint96 courtId => uint256)) public latestDelayedStakeIndex; // Maps the juror to its latest delayed stake. If there is already a delayed stake for this juror then it'll be replaced. // ************************************* // // * Function Modifiers * // @@ -178,32 +176,11 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr for (uint256 i = delayedStakeReadIndex; i < newDelayedStakeReadIndex; i++) { DelayedStake storage delayedStake = delayedStakes[i]; - // Delayed stake could've been manually removed already. In this case simply move on to the next item. - if (delayedStake.account != address(0)) { - // Nullify the index so the delayed stake won't get deleted before its own execution. - delete latestDelayedStakeIndex[delayedStake.account][delayedStake.courtID]; - - // Execute the delayed stake by calling KlerosCore, which will handle Vault interactions - // core.setStakeBySortitionModule was the old way, now KlerosCore itself has _setStake - // which calls this StakeController's setStake. This creates a circular dependency if not careful. - // For delayed stakes, the StakeController should probably update SortitionModule directly - // and then tell KlerosCore to handle the deposit/withdrawal with the Vault if needed. - // OR, the KlerosCoreXBase.setStakeBySortitionModule needs to exist and be smart enough. - - // For now, assuming KlerosCore will have a way to apply this, or this internal _setStake is used. - // This part needs careful review of how KlerosCore consumes delayed stakes. - // Let's assume _setStake here is the one that directly updates sortition and returns pnkDeposit/Withdrawal. - _setStake( - delayedStake.account, - delayedStake.courtID, - delayedStake.stake, - delayedStake.alreadyTransferred - // No direct PNK transfer here by StakeController; KlerosCore handles Vault interaction - ); + if (delayedStake.account == address(0)) continue; - // Note: In delayed stake execution, we don't revert on failures to maintain batch processing - delete delayedStakes[i]; - } + // Let KlerosCore coordinate stake update and vault deposit/withdrawal. + core.setStakeBySystem(delayedStake.account, delayedStake.courtID, delayedStake.stake); + delete delayedStakes[i]; } delayedStakeReadIndex = newDelayedStakeReadIndex; } @@ -213,85 +190,21 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr // ************************************* // /// @inheritdoc IStakeController - function setStake( + function setStakeBySystem( address _account, uint96 _courtID, - uint256 _newStake, - bool _alreadyTransferred // Indicates if KlerosCore already handled the PNK transfer (e.g. for its direct setStake calls) - ) public override onlyByCore returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { - return _setStake(_account, _courtID, _newStake, _alreadyTransferred); + uint256 _newStake + ) external override onlyByCore returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { + return _setStakeBySystem(_account, _courtID, _newStake); } - /// @dev Internal implementation of setStake with phase-aware delayed stake logic - /// TODO: REMOVE THE INSTANT STAKING LOGIC ! - function _setStake( + /// @inheritdoc IStakeController + function setStake( address _account, uint96 _courtID, - uint256 _newStake, - bool _alreadyTransferred - ) internal virtual returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { - (, uint256 currentStake, ) = sortitionModule.getJurorInfo(_account, _courtID); - - // Delete any existing delayed stake for this juror/court - // If a delayed stake was already funded (_alreadyTransferred = true for its deposit part), - // and now we are processing an earlier, direct setStake, _deleteDelayedStake - // should ideally signal KlerosCore to refund if the new operation effectively cancels a pre-funded deposit. - // Current _deleteDelayedStake returns the amount that *would have been withdrawn by the delayed stake itself*. - uint256 potentialWithdrawalFromDeletedDelayedStake = _deleteDelayedStake(_courtID, _account); - - if (phase != Phase.staking) { - // Store the stake change as delayed, to be applied when the phase switches back to Staking. - DelayedStake storage delayedStake = delayedStakes[++delayedStakeWriteIndex]; - delayedStake.account = _account; - delayedStake.courtID = _courtID; - delayedStake.stake = _newStake; - latestDelayedStakeIndex[_account][_courtID] = delayedStakeWriteIndex; - - if (_newStake > currentStake) { - // This is for a future deposit when phase is staking. - // If KlerosCore calls this (_alreadyTransferred=false path typically), it will use the returned pnkDeposit - // to instruct Vault.deposit(). So the delayedStake.alreadyTransferred should be set based on whether - // the funding happens now (for delayed stake path) or later (for KlerosCore direct path that gets delayed). - delayedStake.alreadyTransferred = _alreadyTransferred; // If it's a delayed execution of a pre-funded stake, it's true. - // If KlerosCore is calling and it gets delayed, _alreadyTransferred was false, so this is false. - pnkDeposit = _newStake - currentStake; - // No actual PNK transfer here. KlerosCore handles Vault interaction based on returned pnkDeposit. - } else { - // This is for a future withdrawal. No PNK is transferred now by StakeController. - // KlerosCore will use returned pnkWithdrawal to instruct Vault.withdraw(). - delayedStake.alreadyTransferred = false; // Withdrawals are never pre-funded for a delayed op. - pnkWithdrawal = currentStake - _newStake; - if (potentialWithdrawalFromDeletedDelayedStake > pnkWithdrawal) { - // This implies the deleted delayed stake was a larger withdrawal than the current one. - // Or it was a deposit that got cancelled and replaced by a smaller one/withdrawal. - // This logic is complex if trying to reconcile pre-funded delayed stakes with new operations. - // For now, pnkWithdrawal is based on currentStake and _newStake. - } - } - return (pnkDeposit, pnkWithdrawal, StakingResult.Successful); - } - - // Current phase is Staking: apply normally - bool success = sortitionModule.setStake(_account, _courtID, _newStake); - if (!success) { - // This typically shouldn't fail if parameters are valid (e.g. tree exists). - // Assuming SortitionModule itself doesn't revert but returns false for logical errors not caught by KlerosCore. - return (0, 0, StakingResult.CannotStakeInThisCourt); // Or a more generic sortition error - } - - if (!_alreadyTransferred) { - // Only calculate pnkDeposit/pnkWithdrawal if KlerosCore hasn't handled it - if (_newStake > currentStake) { - pnkDeposit = _newStake - currentStake; - } else { - pnkWithdrawal = currentStake - _newStake; - } - } - // If _alreadyTransferred is true, KlerosCore has (or will) handle the Vault interaction. - // pnkDeposit/pnkWithdrawal would be 0 in that case from this function's perspective for KlerosCore. - // However, the current KlerosCore _setStake always passes _alreadyTransferred = false. - - return (pnkDeposit, pnkWithdrawal, StakingResult.Successful); + uint256 _newStake + ) public override onlyByCore returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { + return _setStake(_account, _courtID, _newStake); } /// @inheritdoc IStakeController @@ -355,12 +268,12 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr } /// @inheritdoc IStakeController - function createDisputeHook(uint256 _disputeID, uint256 _roundID) external override onlyByCore { + function createDisputeHook(uint256 /*_disputeID*/, uint256 /*_roundID*/) external override onlyByCore { disputesWithoutJurors++; } /// @inheritdoc IStakeController - function postDrawHook(uint256 _disputeID, uint256 _roundID) external override onlyByCore { + function postDrawHook(uint256 /*_disputeID*/, uint256 /*_roundID*/) external override onlyByCore { disputesWithoutJurors--; } @@ -412,34 +325,62 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr } // ************************************* // - // * Internal * // + // * Internal * // // ************************************* // - /// @dev Checks if there is already a delayed stake. In this case consider it irrelevant and remove it. - /// @param _courtID ID of the court. - /// @param _juror Juror whose stake to check. - /// @return amountToWithdrawFromDeletedDelayedStake If the deleted delayed stake was a deposit (_alreadyTransferred=true), - /// this is the PNK amount that was effectively pre-funded and now might need to be returned to the user by KlerosCore. - /// If it was a withdrawal, this is 0. - function _deleteDelayedStake( + /// @dev Internal implementation of setStakeBySystem + /// @param _account The account to set the stake for. + /// @param _courtID The ID of the court to set the stake for. + /// @param _newStake The new stake. + /// @return pnkDeposit The amount of PNK to deposit. + /// @return pnkWithdrawal The amount of PNK to withdraw. + /// @return stakingResult The result of the staking operation. + function _setStakeBySystem( + address _account, uint96 _courtID, - address _juror - ) internal returns (uint256 amountToWithdrawFromDeletedDelayedStake) { - uint256 latestIndex = latestDelayedStakeIndex[_juror][_courtID]; - if (latestIndex != 0) { - DelayedStake storage delayedStake = delayedStakes[latestIndex]; - if (delayedStake.alreadyTransferred) { - // This delayed stake was a deposit that was pre-funded into the system (e.g. KlerosCore took PNK). - // Now that it's being deleted (e.g., by a new setStake operation before execution), - // KlerosCore might need to refund this pre-funded amount if the new operation doesn't cover it. - (, uint256 sortitionStake, ) = sortitionModule.getJurorInfo(_juror, _courtID); - if (delayedStake.stake > sortitionStake) { - // The delayed stake was larger than current actual stake - amountToWithdrawFromDeletedDelayedStake = delayedStake.stake - sortitionStake; - } + uint256 _newStake + ) internal virtual returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { + (, uint256 currentStake, ) = sortitionModule.getJurorInfo(_account, _courtID); + if (_newStake > currentStake) { + pnkDeposit = _newStake - currentStake; + } else if (_newStake < currentStake) { + pnkWithdrawal = currentStake - _newStake; + } + stakingResult = sortitionModule.setStake(_account, _courtID, _newStake); + if (stakingResult != StakingResult.Successful) { + return (0, 0, stakingResult); + } + } + + /// @dev Internal implementation of setStake with phase-aware delayed stake logic + /// @param _account The account to set the stake for. + /// @param _courtID The ID of the court to set the stake for. + /// @param _newStake The new stake. + /// @return pnkDeposit The amount of PNK to deposit. + /// @return pnkWithdrawal The amount of PNK to withdraw. + /// @return stakingResult The result of the staking operation. + function _setStake( + address _account, + uint96 _courtID, + uint256 _newStake + ) internal virtual returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { + (, uint256 currentStake, ) = sortitionModule.getJurorInfo(_account, _courtID); + if (_newStake > currentStake) { + pnkDeposit = _newStake - currentStake; + } else if (_newStake < currentStake) { + pnkWithdrawal = currentStake - _newStake; + } + if (phase == Phase.staking) { + stakingResult = sortitionModule.setStake(_account, _courtID, _newStake); + if (stakingResult != StakingResult.Successful) { + return (0, 0, stakingResult); } - delete delayedStakes[latestIndex]; - delete latestDelayedStakeIndex[_juror][_courtID]; + } else { + DelayedStake storage delayedStake = delayedStakes[++delayedStakeWriteIndex]; + delayedStake.account = _account; + delayedStake.courtID = _courtID; + delayedStake.stake = _newStake; + stakingResult = StakingResult.Delayed; } } @@ -464,8 +405,8 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr for (uint256 i = 0; i < _accounts.length; i++) { if (_stakes[i] > 0) { // Direct stake import bypassing normal validation for migration - bool success = sortitionModule.setStake(_accounts[i], _courtIDs[i], _stakes[i]); - if (success) { + StakingResult stakingResult = sortitionModule.setStake(_accounts[i], _courtIDs[i], _stakes[i]); + if (stakingResult == StakingResult.Successful) { totalImported++; emit StakeImported(_accounts[i], _courtIDs[i], _stakes[i]); } @@ -483,7 +424,6 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr if (delayedStake.account != address(0)) { delayedStakeWriteIndex++; delayedStakes[delayedStakeWriteIndex] = delayedStake; - latestDelayedStakeIndex[delayedStake.account][delayedStake.courtID] = delayedStakeWriteIndex; emit DelayedStakeImported( delayedStake.account, diff --git a/contracts/src/arbitration/interfaces/ISortitionSumTree.sol b/contracts/src/arbitration/interfaces/ISortitionSumTree.sol index 84147575b..114337321 100644 --- a/contracts/src/arbitration/interfaces/ISortitionSumTree.sol +++ b/contracts/src/arbitration/interfaces/ISortitionSumTree.sol @@ -32,8 +32,12 @@ interface ISortitionSumTree { /// @param _account The address of the juror /// @param _courtID The ID of the court /// @param _newStake The new stake amount - /// @return success Whether the operation was successful - function setStake(address _account, uint96 _courtID, uint256 _newStake) external returns (bool success); + /// @return stakingResult The result of the staking operation + function setStake( + address _account, + uint96 _courtID, + uint256 _newStake + ) external returns (StakingResult stakingResult); // ************************************* // // * Drawing * // diff --git a/contracts/src/arbitration/interfaces/IStakeController.sol b/contracts/src/arbitration/interfaces/IStakeController.sol index 4acba5679..de3b0eb8d 100644 --- a/contracts/src/arbitration/interfaces/IStakeController.sol +++ b/contracts/src/arbitration/interfaces/IStakeController.sol @@ -55,19 +55,31 @@ interface IStakeController { // * Stake Management * // // ************************************* // + /// @notice System-level update to a juror's stake directly in the SortitionModule. + /// @dev Called by KlerosCoreXBase for executing delayed stakes. Skips regular phase checks and delayed stake creation. + /// @param _account The juror's account + /// @param _courtID The ID of the court + /// @param _newStake The new stake amount + /// @return pnkDeposit The amount of PNK to deposit + /// @return pnkWithdrawal The amount of PNK to withdraw + /// @return stakingResult The result of the staking operation + function setStakeBySystem( + address _account, + uint96 _courtID, + uint256 _newStake + ) external returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult); + /// @notice Set stake for a juror with vault coordination - /// @param _account The address of the juror + /// @param _account The juror's account /// @param _courtID The ID of the court /// @param _newStake The new stake amount - /// @param _alreadyTransferred Whether the tokens were already transferred /// @return pnkDeposit The amount of PNK to deposit /// @return pnkWithdrawal The amount of PNK to withdraw /// @return stakingResult The result of the staking operation function setStake( address _account, uint96 _courtID, - uint256 _newStake, - bool _alreadyTransferred + uint256 _newStake ) external returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult); /// @notice Lock stake for dispute participation diff --git a/contracts/src/arbitration/KlerosCore.sol b/contracts/src/arbitration/old/KlerosCore.sol similarity index 100% rename from contracts/src/arbitration/KlerosCore.sol rename to contracts/src/arbitration/old/KlerosCore.sol diff --git a/contracts/src/arbitration/KlerosCoreBase.sol b/contracts/src/arbitration/old/KlerosCoreBase.sol similarity index 99% rename from contracts/src/arbitration/KlerosCoreBase.sol rename to contracts/src/arbitration/old/KlerosCoreBase.sol index 6a435489b..25bd126b6 100644 --- a/contracts/src/arbitration/KlerosCoreBase.sol +++ b/contracts/src/arbitration/old/KlerosCoreBase.sol @@ -2,13 +2,13 @@ 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"; +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. diff --git a/contracts/src/arbitration/KlerosCoreNeo.sol b/contracts/src/arbitration/old/KlerosCoreNeo.sol similarity index 100% rename from contracts/src/arbitration/KlerosCoreNeo.sol rename to contracts/src/arbitration/old/KlerosCoreNeo.sol diff --git a/contracts/src/arbitration/SortitionModule.sol b/contracts/src/arbitration/old/SortitionModule.sol similarity index 100% rename from contracts/src/arbitration/SortitionModule.sol rename to contracts/src/arbitration/old/SortitionModule.sol diff --git a/contracts/src/arbitration/SortitionModuleBase.sol b/contracts/src/arbitration/old/SortitionModuleBase.sol similarity index 99% rename from contracts/src/arbitration/SortitionModuleBase.sol rename to contracts/src/arbitration/old/SortitionModuleBase.sol index edb10edf1..6634fdeb6 100644 --- a/contracts/src/arbitration/SortitionModuleBase.sol +++ b/contracts/src/arbitration/old/SortitionModuleBase.sol @@ -11,12 +11,12 @@ pragma solidity 0.8.24; import {KlerosCore} from "./KlerosCore.sol"; -import {ISortitionModule} from "./interfaces/ISortitionModule.sol"; -import {IDisputeKit} from "./interfaces/IDisputeKit.sol"; -import {Initializable} from "../proxy/Initializable.sol"; -import {UUPSProxiable} from "../proxy/UUPSProxiable.sol"; -import {RNG} from "../rng/RNG.sol"; -import "../libraries/Constants.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. diff --git a/contracts/src/arbitration/SortitionModuleNeo.sol b/contracts/src/arbitration/old/SortitionModuleNeo.sol similarity index 100% rename from contracts/src/arbitration/SortitionModuleNeo.sol rename to contracts/src/arbitration/old/SortitionModuleNeo.sol diff --git a/contracts/src/libraries/Constants.sol b/contracts/src/libraries/Constants.sol index f393b4792..f2101461b 100644 --- a/contracts/src/libraries/Constants.sol +++ b/contracts/src/libraries/Constants.sol @@ -27,6 +27,7 @@ enum OnError { enum StakingResult { Successful, + Delayed, StakingTransferFailed, UnstakingTransferFailed, CannotStakeInMoreCourts, diff --git a/contracts/src/test/KlerosCoreMock.sol b/contracts/src/test/KlerosCoreMock.sol index 25eea2da3..03a907999 100644 --- a/contracts/src/test/KlerosCoreMock.sol +++ b/contracts/src/test/KlerosCoreMock.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.24; -import "../arbitration/KlerosCore.sol"; +import "../arbitration/old/KlerosCore.sol"; /// @title KlerosCoreMock /// KlerosCore with view functions to use in Foundry tests. diff --git a/contracts/src/test/SortitionModuleMock.sol b/contracts/src/test/SortitionModuleMock.sol index bfe911dfe..12ec17c39 100644 --- a/contracts/src/test/SortitionModuleMock.sol +++ b/contracts/src/test/SortitionModuleMock.sol @@ -10,7 +10,7 @@ pragma solidity 0.8.24; -import "../arbitration/SortitionModule.sol"; +import "../arbitration/old/SortitionModule.sol"; /// @title SortitionModuleMock /// @dev Adds getter functions to sortition module for Foundry tests. From 8fd11dc3a414740526da4297c0f87c284a7e04cf Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Thu, 5 Jun 2025 15:15:12 +0100 Subject: [PATCH 08/24] refactor: aligning with the staking fix PR (wip) --- contracts/src/arbitration/KlerosCoreXBase.sol | 11 +++------- .../src/arbitration/SortitionSumTreeBase.sol | 22 +++++++++---------- .../src/arbitration/StakeControllerBase.sol | 9 +++----- contracts/src/arbitration/VaultBase.sol | 3 ++- .../interfaces/IStakeController.sol | 6 ++++- .../src/arbitration/interfaces/IVault.sol | 6 ++++- 6 files changed, 28 insertions(+), 29 deletions(-) diff --git a/contracts/src/arbitration/KlerosCoreXBase.sol b/contracts/src/arbitration/KlerosCoreXBase.sol index 6ad15baba..92f8db563 100644 --- a/contracts/src/arbitration/KlerosCoreXBase.sol +++ b/contracts/src/arbitration/KlerosCoreXBase.sol @@ -778,7 +778,7 @@ abstract contract KlerosCoreXBase is IArbitratorV2, Initializable, UUPSProxiable // Execute penalty through StakeController coordination address account = round.drawnJurors[_params.repartition]; - uint256 actualPenalty = stakeController.executeJurorPenalty(account, penalty); + (uint256 pnkBalance, uint256 actualPenalty) = stakeController.setJurorPenalty(account, penalty); _params.pnkPenaltiesInRound += actualPenalty; emit TokenAndETHShift( @@ -792,8 +792,8 @@ abstract contract KlerosCoreXBase is IArbitratorV2, Initializable, UUPSProxiable ); // Check if juror should be set inactive - bool shouldBeInactive = !disputeKit.isVoteActive(_params.disputeID, _params.round, _params.repartition); - if (shouldBeInactive) { + bool inactive = !disputeKit.isVoteActive(_params.disputeID, _params.round, _params.repartition); + if (pnkBalance == 0 || inactive) { uint256 pnkToWithdraw = stakeController.setJurorInactive(account); if (pnkToWithdraw > 0) { try vault.withdraw(account, pnkToWithdraw) { @@ -852,11 +852,6 @@ abstract contract KlerosCoreXBase is IArbitratorV2, Initializable, UUPSProxiable // Release the rest of the PNKs of the juror for this round. stakeController.unlockStake(account, pnkLocked); - // Give back the locked PNKs in case the juror fully unstaked earlier. - if (!stakeController.isJurorStaked(account)) { - vault.transferReward(account, pnkLocked); - } - // Transfer the rewards uint256 pnkReward = ((_params.pnkPenaltiesInRound / _params.coherentCount) * degreeOfCoherence) / ALPHA_DIVISOR; round.sumPnkRewardPaid += pnkReward; diff --git a/contracts/src/arbitration/SortitionSumTreeBase.sol b/contracts/src/arbitration/SortitionSumTreeBase.sol index a1cc13a3f..31fa469a4 100644 --- a/contracts/src/arbitration/SortitionSumTreeBase.sol +++ b/contracts/src/arbitration/SortitionSumTreeBase.sol @@ -231,10 +231,9 @@ abstract contract SortitionSumTreeBase is ISortitionSumTree, Initializable, UUPS return StakingResult.CannotStakeZeroWhenNoStake; // No change needed } - JurorStakeInfo storage info = jurorStakeInfo[_account]; - uint256 nbCourts = info.courtIDs.length; + JurorStakeInfo storage juror = jurorStakeInfo[_account]; - if (currentStake == 0 && nbCourts >= MAX_STAKE_PATHS) { + if (currentStake == 0 && juror.courtIDs.length >= MAX_STAKE_PATHS) { return StakingResult.CannotStakeInMoreCourts; // Prevent staking beyond MAX_STAKE_PATHS but unstaking is always allowed. } @@ -253,24 +252,23 @@ abstract contract SortitionSumTreeBase is ISortitionSumTree, Initializable, UUPS } } - // Update local stake tracking (inlined from _updateJurorStakeInfo) - uint256 currentStakeInInfo = info.stakes[_courtID]; - + // Update local stake tracking + uint256 currentStakeInInfo = juror.stakes[_courtID]; if (currentStakeInInfo == 0 && _newStake > 0) { // Adding new court - info.courtIDs.push(_courtID); + juror.courtIDs.push(_courtID); } else if (currentStakeInInfo > 0 && _newStake == 0) { // Removing court - for (uint256 i = 0; i < info.courtIDs.length; i++) { - if (info.courtIDs[i] == _courtID) { - info.courtIDs[i] = info.courtIDs[info.courtIDs.length - 1]; - info.courtIDs.pop(); + for (uint256 i = 0; i < juror.courtIDs.length; i++) { + if (juror.courtIDs[i] == _courtID) { + juror.courtIDs[i] = juror.courtIDs[juror.courtIDs.length - 1]; + juror.courtIDs.pop(); break; } } } - info.stakes[_courtID] = _newStake; + juror.stakes[_courtID] = _newStake; // Get total staked amount from stake controller for event uint256 totalStaked = stakeController.getDepositedBalance(_account); diff --git a/contracts/src/arbitration/StakeControllerBase.sol b/contracts/src/arbitration/StakeControllerBase.sol index 2911076ca..585020f26 100644 --- a/contracts/src/arbitration/StakeControllerBase.sol +++ b/contracts/src/arbitration/StakeControllerBase.sol @@ -220,18 +220,15 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr } /// @inheritdoc IStakeController - function executeJurorPenalty( + function setJurorPenalty( address _account, uint256 _penalty - ) external virtual override onlyByCore returns (uint256 actualPenalty) { - // First unlock the penalty amount + ) external virtual override onlyByCore returns (uint256 pnkBalance, uint256 actualPenalty) { vault.unlockTokens(_account, _penalty); - // Then apply the penalty through vault - actualPenalty = vault.applyPenalty(_account, _penalty); + (pnkBalance, actualPenalty) = vault.applyPenalty(_account, _penalty); emit JurorPenaltyExecuted(_account, _penalty, actualPenalty); - return actualPenalty; } /// @inheritdoc IStakeController diff --git a/contracts/src/arbitration/VaultBase.sol b/contracts/src/arbitration/VaultBase.sol index 33bb47a90..fff81e9e9 100644 --- a/contracts/src/arbitration/VaultBase.sol +++ b/contracts/src/arbitration/VaultBase.sol @@ -195,11 +195,12 @@ abstract contract VaultBase is IVault, Initializable, UUPSProxiable { function applyPenalty( address _account, uint256 _amount - ) external virtual override onlyStakeController returns (uint256 actualPenalty) { + ) external virtual override onlyStakeController returns (uint256 pnkBalance, uint256 actualPenalty) { JurorBalance storage balance = jurorBalances[_account]; // Calculate actual penalty (cannot exceed deposited amount) actualPenalty = _amount > balance.deposited ? balance.deposited : _amount; + pnkBalance = balance.deposited - actualPenalty; // includes locked PNK // Update balances balance.deposited -= actualPenalty; diff --git a/contracts/src/arbitration/interfaces/IStakeController.sol b/contracts/src/arbitration/interfaces/IStakeController.sol index de3b0eb8d..128c86784 100644 --- a/contracts/src/arbitration/interfaces/IStakeController.sol +++ b/contracts/src/arbitration/interfaces/IStakeController.sol @@ -95,8 +95,12 @@ interface IStakeController { /// @notice Execute penalty on juror through vault coordination /// @param _account The account to penalize /// @param _penalty The penalty amount + /// @return pnkBalance The balance of PNK after penalty application, including locked PNK /// @return actualPenalty The actual penalty applied - function executeJurorPenalty(address _account, uint256 _penalty) external returns (uint256 actualPenalty); + function setJurorPenalty( + address _account, + uint256 _penalty + ) external returns (uint256 pnkBalance, uint256 actualPenalty); /// @notice Set juror as inactive and remove from all sortition trees /// @param _account The juror to set inactive diff --git a/contracts/src/arbitration/interfaces/IVault.sol b/contracts/src/arbitration/interfaces/IVault.sol index 28aa6e6ae..41002a155 100644 --- a/contracts/src/arbitration/interfaces/IVault.sol +++ b/contracts/src/arbitration/interfaces/IVault.sol @@ -47,8 +47,12 @@ interface IVault { /// @notice Apply penalty by reducing deposited balance (only StakeController) /// @param _account The account to penalize /// @param _amount The penalty amount + /// @return pnkBalance The balance of PNK after penalty application, including locked PNK /// @return actualPenalty The actual penalty applied - function applyPenalty(address _account, uint256 _amount) external returns (uint256 actualPenalty); + function applyPenalty( + address _account, + uint256 _amount + ) external returns (uint256 pnkBalance, uint256 actualPenalty); /// @notice Transfer PNK rewards directly to account (only KlerosCore) /// @param _account The account to receive rewards From 1604509ae309df87a7d9bdd51da3d6b3d1354003 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Thu, 5 Jun 2025 22:43:05 +0100 Subject: [PATCH 09/24] refactor: moved jurorStake mapping and struct from Sortition to Stake Controller --- .../src/arbitration/SortitionSumTreeBase.sol | 123 +++-------- .../src/arbitration/StakeControllerBase.sol | 204 ++++++++++++++---- .../interfaces/ISortitionSumTree.sol | 21 -- 3 files changed, 196 insertions(+), 152 deletions(-) diff --git a/contracts/src/arbitration/SortitionSumTreeBase.sol b/contracts/src/arbitration/SortitionSumTreeBase.sol index 31fa469a4..c3265e7ba 100644 --- a/contracts/src/arbitration/SortitionSumTreeBase.sol +++ b/contracts/src/arbitration/SortitionSumTreeBase.sol @@ -27,11 +27,6 @@ abstract contract SortitionSumTreeBase is ISortitionSumTree, Initializable, UUPS mapping(uint256 => bytes32) nodeIndexesToIDs; } - struct JurorStakeInfo { - uint96[] courtIDs; // The IDs of courts where the juror has stakes - mapping(uint96 => uint256) stakes; // Court ID -> stake amount - } - // ************************************* // // * Storage * // // ************************************* // @@ -40,7 +35,6 @@ abstract contract SortitionSumTreeBase is ISortitionSumTree, Initializable, UUPS IStakeController public stakeController; // The stake controller for coordination. mapping(bytes32 treeHash => SortitionSumTree) internal sortitionSumTrees; // The mapping trees by keys. - mapping(address account => JurorStakeInfo) internal jurorStakeInfo; // Juror stake information for sortition // ************************************* // // * Function Modifiers * // @@ -104,7 +98,37 @@ abstract contract SortitionSumTreeBase is ISortitionSumTree, Initializable, UUPS uint96 _courtID, uint256 _newStake ) external virtual override onlyByStakeController returns (StakingResult stakingResult) { - return _setStake(_account, _courtID, _newStake); + uint256 currentStake = this.stakeOf(_account, _courtID); + + if (currentStake == 0 && _newStake == 0) { + return StakingResult.CannotStakeZeroWhenNoStake; // No change needed + } + + // Update the sortition sum tree in court hierarchy + bytes32 stakePathID = _accountAndCourtIDToStakePathID(_account, _courtID); + bool finished = false; + uint96 currentCourtIDForHierarchy = _courtID; + KlerosCoreXBase core = stakeController.core(); + + while (!finished) { + _set(bytes32(uint256(currentCourtIDForHierarchy)), _newStake, stakePathID); + if (currentCourtIDForHierarchy == GENERAL_COURT) { + finished = true; + } else { + // Fetch parent court ID. Ensure core.courts() is accessible and correct. + (uint96 parentCourtID, , , , , , ) = core.courts(currentCourtIDForHierarchy); + if (parentCourtID == currentCourtIDForHierarchy) { + // Avoid infinite loop if parent is self (e.g. for general court already handled or misconfiguration) + finished = true; + } else { + currentCourtIDForHierarchy = parentCourtID; + } + } + } + + (, , , uint256 totalStaked, , ) = stakeController.getJurorBalance(_account, _courtID); + emit StakeSet(_account, _courtID, _newStake, totalStaked); + return StakingResult.Successful; } // ************************************* // @@ -168,33 +192,6 @@ abstract contract SortitionSumTreeBase is ISortitionSumTree, Initializable, UUPS return tree.nodes[treeIndex]; } - /// @inheritdoc ISortitionSumTree - function getJurorInfo( - address _juror, - uint96 _courtID - ) external view override returns (uint256 totalStaked, uint256 stakedInCourt, uint256 nbCourts) { - JurorStakeInfo storage info = jurorStakeInfo[_juror]; - - // Get total staked and locked from stake controller - totalStaked = stakeController.getDepositedBalance(_juror); - - // Get stake in specific court from sortition tree - stakedInCourt = this.stakeOf(_juror, _courtID); - - // Number of courts from local tracking - nbCourts = info.courtIDs.length; - } - - /// @inheritdoc ISortitionSumTree - function getJurorCourtIDs(address _juror) external view override returns (uint96[] memory) { - return jurorStakeInfo[_juror].courtIDs; - } - - /// @inheritdoc ISortitionSumTree - function hasStakes(address _juror) external view override returns (bool) { - return jurorStakeInfo[_juror].courtIDs.length > 0; - } - /// @inheritdoc ISortitionSumTree function getTotalStakeInCourt(uint96 _courtID) external view override returns (uint256) { SortitionSumTree storage tree = sortitionSumTrees[bytes32(uint256(_courtID))]; @@ -219,64 +216,6 @@ abstract contract SortitionSumTreeBase is ISortitionSumTree, Initializable, UUPS // * Internal * // // ************************************* // - /// @dev Internal implementation of setStake with court hierarchy updates - function _setStake( - address _account, - uint96 _courtID, - uint256 _newStake - ) internal returns (StakingResult stakingResult) { - uint256 currentStake = this.stakeOf(_account, _courtID); - - if (currentStake == 0 && _newStake == 0) { - return StakingResult.CannotStakeZeroWhenNoStake; // No change needed - } - - JurorStakeInfo storage juror = jurorStakeInfo[_account]; - - if (currentStake == 0 && juror.courtIDs.length >= MAX_STAKE_PATHS) { - return StakingResult.CannotStakeInMoreCourts; // Prevent staking beyond MAX_STAKE_PATHS but unstaking is always allowed. - } - - // Update the sortition sum tree in court hierarchy - bytes32 stakePathID = _accountAndCourtIDToStakePathID(_account, _courtID); - bool finished = false; - uint96 currentCourtID = _courtID; - KlerosCoreXBase core = stakeController.core(); - while (!finished) { - // Tokens are also implicitly staked in parent courts through sortition module to increase the chance of being drawn. - _set(bytes32(uint256(currentCourtID)), _newStake, stakePathID); - if (currentCourtID == GENERAL_COURT) { - finished = true; - } else { - (currentCourtID, , , , , , ) = core.courts(currentCourtID); // Get the parent court. - } - } - - // Update local stake tracking - uint256 currentStakeInInfo = juror.stakes[_courtID]; - if (currentStakeInInfo == 0 && _newStake > 0) { - // Adding new court - juror.courtIDs.push(_courtID); - } else if (currentStakeInInfo > 0 && _newStake == 0) { - // Removing court - for (uint256 i = 0; i < juror.courtIDs.length; i++) { - if (juror.courtIDs[i] == _courtID) { - juror.courtIDs[i] = juror.courtIDs[juror.courtIDs.length - 1]; - juror.courtIDs.pop(); - break; - } - } - } - - juror.stakes[_courtID] = _newStake; - - // Get total staked amount from stake controller for event - uint256 totalStaked = stakeController.getDepositedBalance(_account); - emit StakeSet(_account, _courtID, _newStake, totalStaked); - - return StakingResult.Successful; - } - /// @dev Update all the parents of a node. /// @param _key The key of the tree to update. /// @param _treeIndex The index of the node to start from. diff --git a/contracts/src/arbitration/StakeControllerBase.sol b/contracts/src/arbitration/StakeControllerBase.sol index 585020f26..7fab6abcf 100644 --- a/contracts/src/arbitration/StakeControllerBase.sol +++ b/contracts/src/arbitration/StakeControllerBase.sol @@ -26,6 +26,12 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr uint256 stake; // The new stake. } + struct JurorStake { + uint256 totalStake; + uint96[] stakedCourtIDs; + mapping(uint96 courtID => uint256 stake) stakes; + } + // ************************************* // // * Storage * // // ************************************* // @@ -49,7 +55,10 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr // Delayed stakes management uint256 public delayedStakeWriteIndex; // The index of the last `delayedStake` item that was written to the array. 0 index is skipped. uint256 public delayedStakeReadIndex; // The index of the next `delayedStake` item that should be processed. Starts at 1 because 0 index is skipped. - mapping(uint256 => DelayedStake) public delayedStakes; // Stores the stakes that were changed during Drawing phase, to update them when the phase is switched to Staking. + mapping(uint256 index => DelayedStake) public delayedStakes; // Stores the stakes that were changed during Drawing phase, to update them when the phase is switched to Staking. + + // Stake management + mapping(address => JurorStake) internal jurorStakes; // ************************************* // // * Function Modifiers * // @@ -225,26 +234,19 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr uint256 _penalty ) external virtual override onlyByCore returns (uint256 pnkBalance, uint256 actualPenalty) { vault.unlockTokens(_account, _penalty); - (pnkBalance, actualPenalty) = vault.applyPenalty(_account, _penalty); - emit JurorPenaltyExecuted(_account, _penalty, actualPenalty); } /// @inheritdoc IStakeController function setJurorInactive(address _account) external override onlyByCore returns (uint256 pnkToWithdraw) { - uint96[] memory courtIds = sortitionModule.getJurorCourtIDs(_account); - - for (uint256 i = 0; i < courtIds.length; i++) { - (, uint256 currentStakeInCourt, ) = sortitionModule.getJurorInfo(_account, courtIds[i]); - if (currentStakeInCourt > 0) { - // Set stake to 0 in sortition module to remove from trees - sortitionModule.setStake(_account, courtIds[i], 0); - } + uint96[] storage courtIDsForJuror = jurorStakes[_account].stakedCourtIDs; + while (courtIDsForJuror.length > 0) { + uint96 courtID = courtIDsForJuror[0]; + _setStakeBySystem(_account, courtID, 0); } - + jurorStakes[_account].totalStake = 0; pnkToWithdraw = vault.getAvailableBalance(_account); - emit JurorSetInactive(_account); } @@ -298,17 +300,19 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr availablePnk = vault.getAvailableBalance(_juror); lockedPnk = vault.getLockedBalance(_juror); penaltyPnk = vault.getPenaltyBalance(_juror); - (totalStaked, stakedInCourt, nbCourts) = sortitionModule.getJurorInfo(_juror, _courtID); + totalStaked = jurorStakes[_juror].totalStake; + stakedInCourt = jurorStakes[_juror].stakes[_courtID]; + nbCourts = jurorStakes[_juror].stakedCourtIDs.length; } /// @inheritdoc IStakeController function getJurorCourtIDs(address _juror) external view override returns (uint96[] memory) { - return sortitionModule.getJurorCourtIDs(_juror); + return jurorStakes[_juror].stakedCourtIDs; } /// @inheritdoc IStakeController function isJurorStaked(address _juror) external view override returns (bool) { - return sortitionModule.hasStakes(_juror); + return jurorStakes[_juror].totalStake > 0; } /// @inheritdoc IStakeController @@ -337,14 +341,49 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr uint96 _courtID, uint256 _newStake ) internal virtual returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { - (, uint256 currentStake, ) = sortitionModule.getJurorInfo(_account, _courtID); - if (_newStake > currentStake) { - pnkDeposit = _newStake - currentStake; - } else if (_newStake < currentStake) { - pnkWithdrawal = currentStake - _newStake; + JurorStake storage currentJurorStake = jurorStakes[_account]; + uint256 currentStakeInCourt = currentJurorStake.stakes[_courtID]; + + // Check for MAX_STAKE_PATHS before calculating deposit/withdrawal if it's a new court stake + if (currentStakeInCourt == 0 && _newStake > 0) { + if (currentJurorStake.stakedCourtIDs.length >= MAX_STAKE_PATHS) { + return (0, 0, StakingResult.CannotStakeInMoreCourts); + } + } + + uint256 previousTotalStake = currentJurorStake.totalStake; // Keep track for potential revert + if (_newStake > currentStakeInCourt) { + pnkDeposit = _newStake - currentStakeInCourt; + currentJurorStake.totalStake = previousTotalStake + pnkDeposit; + } else if (_newStake < currentStakeInCourt) { + pnkWithdrawal = currentStakeInCourt - _newStake; + currentJurorStake.totalStake = previousTotalStake - pnkWithdrawal; } + + currentJurorStake.stakes[_courtID] = _newStake; + + // Manage stakedCourtIDs + if (currentStakeInCourt == 0 && _newStake > 0) { + currentJurorStake.stakedCourtIDs.push(_courtID); + } else if (currentStakeInCourt > 0 && _newStake == 0) { + _removeCourt(currentJurorStake.stakedCourtIDs, _courtID); + } + stakingResult = sortitionModule.setStake(_account, _courtID, _newStake); if (stakingResult != StakingResult.Successful) { + // Revert local changes if sortitionModule update fails + currentJurorStake.stakes[_courtID] = currentStakeInCourt; + currentJurorStake.totalStake = previousTotalStake; + if (currentStakeInCourt == 0 && _newStake > 0) { + // revert insertion: was a push, so pop + uint96[] storage stakedCourtsRevert = currentJurorStake.stakedCourtIDs; + if (stakedCourtsRevert.length > 0 && stakedCourtsRevert[stakedCourtsRevert.length - 1] == _courtID) { + stakedCourtsRevert.pop(); + } + } else if (currentStakeInCourt > 0 && _newStake == 0) { + // revert removal: was a remove, so add it back (order might not be preserved by simple push) + currentJurorStake.stakedCourtIDs.push(_courtID); + } return (0, 0, stakingResult); } } @@ -361,23 +400,91 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr uint96 _courtID, uint256 _newStake ) internal virtual returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { - (, uint256 currentStake, ) = sortitionModule.getJurorInfo(_account, _courtID); - if (_newStake > currentStake) { - pnkDeposit = _newStake - currentStake; - } else if (_newStake < currentStake) { - pnkWithdrawal = currentStake - _newStake; + JurorStake storage currentJurorStake = jurorStakes[_account]; + uint256 currentStakeInCourt = currentJurorStake.stakes[_courtID]; + + if (_newStake > currentStakeInCourt) { + pnkDeposit = _newStake - currentStakeInCourt; + } else if (_newStake < currentStakeInCourt) { + pnkWithdrawal = currentStakeInCourt - _newStake; } - if (phase == Phase.staking) { - stakingResult = sortitionModule.setStake(_account, _courtID, _newStake); - if (stakingResult != StakingResult.Successful) { - return (0, 0, stakingResult); - } - } else { + + if (phase != Phase.staking) { + // MAX_STAKE_PATHS is checked when _setStakeBySystem() is called during executeDelayedStakes(). DelayedStake storage delayedStake = delayedStakes[++delayedStakeWriteIndex]; delayedStake.account = _account; delayedStake.courtID = _courtID; delayedStake.stake = _newStake; - stakingResult = StakingResult.Delayed; + return (pnkDeposit, pnkWithdrawal, StakingResult.Delayed); + } + + // Check for MAX_STAKE_PATHS if it's a new court stake + if (currentStakeInCourt == 0 && _newStake > 0) { + if (currentJurorStake.stakedCourtIDs.length >= MAX_STAKE_PATHS) { + return (0, 0, StakingResult.CannotStakeInMoreCourts); + } + } + + // Update local stake records first + uint256 previousTotalStake = currentJurorStake.totalStake; // Keep track for potential revert + if (_newStake > currentStakeInCourt) { + currentJurorStake.totalStake = previousTotalStake + pnkDeposit; + } else if (_newStake < currentStakeInCourt) { + currentJurorStake.totalStake = previousTotalStake - pnkWithdrawal; + } + currentJurorStake.stakes[_courtID] = _newStake; + + // Manage stakedCourtIDs + if (currentStakeInCourt == 0 && _newStake > 0) { + currentJurorStake.stakedCourtIDs.push(_courtID); + } else if (currentStakeInCourt > 0 && _newStake == 0) { + _removeCourt(currentJurorStake.stakedCourtIDs, _courtID); + } + + stakingResult = sortitionModule.setStake(_account, _courtID, _newStake); + if (stakingResult != StakingResult.Successful) { + // Revert local changes if sortitionModule update fails + currentJurorStake.stakes[_courtID] = currentStakeInCourt; + currentJurorStake.totalStake = previousTotalStake; + if (currentStakeInCourt == 0 && _newStake > 0) { + // revert insertion: was a push, so pop + uint96[] storage stakedCourtsRevert = currentJurorStake.stakedCourtIDs; + if (stakedCourtsRevert.length > 0 && stakedCourtsRevert[stakedCourtsRevert.length - 1] == _courtID) { + stakedCourtsRevert.pop(); + } + } else if (currentStakeInCourt > 0 && _newStake == 0) { + // revert removal: was a remove, so add it back (order might not be preserved by simple push) + currentJurorStake.stakedCourtIDs.push(_courtID); + } + return (0, 0, stakingResult); + } + } + + /// @dev Removes a court from a juror's list of staked courts. + /// @param _stakedCourts Storage pointer to the juror's array of staked court IDs. + /// @param _courtID The ID of the court to remove. + function _removeCourt(uint96[] storage _stakedCourts, uint96 _courtID) internal { + uint256 len = _stakedCourts.length; + if (len == 0) { + return; // Nothing to remove + } + + uint256 courtIndexToRemove = type(uint256).max; // Sentinel value indicates not found + for (uint256 i = 0; i < len; i++) { + if (_stakedCourts[i] == _courtID) { + courtIndexToRemove = i; + break; + } + } + + if (courtIndexToRemove != type(uint256).max) { + // If the courtID was found in the array + // If it's not the last element, swap the last element into its place + if (courtIndexToRemove != len - 1) { + _stakedCourts[courtIndexToRemove] = _stakedCourts[len - 1]; + } + // Remove the last element (either the original last, or the one that was swapped) + _stakedCourts.pop(); } } @@ -398,19 +505,38 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr revert InvalidMigrationData(); } - uint256 totalImported = 0; + uint256 totalImportedSuccess = 0; for (uint256 i = 0; i < _accounts.length; i++) { if (_stakes[i] > 0) { - // Direct stake import bypassing normal validation for migration - StakingResult stakingResult = sortitionModule.setStake(_accounts[i], _courtIDs[i], _stakes[i]); + address account = _accounts[i]; + uint96 courtID = _courtIDs[i]; + uint256 stakeToImport = _stakes[i]; + + // Ensure no prior stake exists for this specific account/courtID combination in this contract's state for a clean import. + // This check assumes importExistingStakes is for a fresh population or controlled append. + // If overwriting/updating was intended, this check might differ. + if (jurorStakes[account].stakes[courtID] > 0) { + // Skip or revert, depending on desired import semantics. For now, skip and log. + // emit ImportSkippedDuplicate(account, courtID, stakeToImport); + continue; + } + + // _setStakeBySystem will update local juror stake mappings (jurorStakes) + // AND call sortitionModule.setStake. + // The pnkDeposit/pnkWithdrawal are calculated but not used by this import function. + (, , StakingResult stakingResult) = _setStakeBySystem(account, courtID, stakeToImport); + if (stakingResult == StakingResult.Successful) { - totalImported++; - emit StakeImported(_accounts[i], _courtIDs[i], _stakes[i]); + totalImportedSuccess++; + emit StakeImported(account, courtID, stakeToImport); + } else { + // Log or handle failed import for a specific entry + // emit StakeImportFailed(account, courtID, stakeToImport, stakingResult); } } } - emit MigrationCompleted(_accounts.length, totalImported); + emit MigrationCompleted(_accounts.length, totalImportedSuccess); } /// @dev Import delayed stakes from old system for migration diff --git a/contracts/src/arbitration/interfaces/ISortitionSumTree.sol b/contracts/src/arbitration/interfaces/ISortitionSumTree.sol index 114337321..cb6ff81bf 100644 --- a/contracts/src/arbitration/interfaces/ISortitionSumTree.sol +++ b/contracts/src/arbitration/interfaces/ISortitionSumTree.sol @@ -72,27 +72,6 @@ interface ISortitionSumTree { /// @return value The stake of the juror in the court function stakeOf(bytes32 _key, bytes32 _ID) external view returns (uint256 value); - /// @notice Get juror information for a specific court - /// @param _juror The juror address - /// @param _courtID The court ID - /// @return totalStaked Total staked amount (from external source) - /// @return stakedInCourt Amount staked in specific court - /// @return nbCourts Number of courts staked in - function getJurorInfo( - address _juror, - uint96 _courtID - ) external view returns (uint256 totalStaked, uint256 stakedInCourt, uint256 nbCourts); - - /// @notice Get court IDs where juror has stakes - /// @param _juror The juror address - /// @return Array of court IDs - function getJurorCourtIDs(address _juror) external view returns (uint96[] memory); - - /// @notice Check if juror has any stakes in sortition trees - /// @param _juror The juror address - /// @return Whether the juror has stakes - function hasStakes(address _juror) external view returns (bool); - /// @notice Get the total stake in a court's tree /// @param _courtID The court ID /// @return Total stake in the court From fb35a86267df64dcd4d57b2c364e3667d1028534 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Thu, 5 Jun 2025 23:08:08 +0100 Subject: [PATCH 10/24] refactor: stPNK removal --- .../deploy/00-home-chain-arbitration-v2.ts | 29 +-- contracts/package.json | 1 - contracts/src/arbitration/KlerosCoreXBase.sol | 8 +- contracts/src/arbitration/Vault.sol | 5 +- contracts/src/arbitration/VaultBase.sol | 55 +---- contracts/src/arbitration/VaultNeo.sol | 13 +- .../src/arbitration/interfaces/IVault.sol | 12 +- contracts/src/arbitration/stPNK.sol | 188 ------------------ yarn.lock | 10 - 9 files changed, 30 insertions(+), 291 deletions(-) delete mode 100644 contracts/src/arbitration/stPNK.sol diff --git a/contracts/deploy/00-home-chain-arbitration-v2.ts b/contracts/deploy/00-home-chain-arbitration-v2.ts index 64fc0e75c..242d4e435 100644 --- a/contracts/deploy/00-home-chain-arbitration-v2.ts +++ b/contracts/deploy/00-home-chain-arbitration-v2.ts @@ -42,33 +42,27 @@ const deployArbitrationV2: DeployFunction = async (hre: HardhatRuntimeEnvironmen // Calculate future addresses for circular dependencies const nonce = await ethers.provider.getTransactionCount(deployer); - const vaultAddress = getContractAddress(deployer, nonce + 3); // deployed on the 4th tx (nonce+3): stPNK Impl tx, stPNK Proxy tx, Vault Impl tx, Vault Proxy tx - console.log("calculated future Vault address for nonce %d: %s", nonce + 3, vaultAddress); + const vaultAddress = getContractAddress(deployer, nonce + 1); // deployed on the 2nd tx (nonce+1): Vault Impl tx, Vault Proxy tx + console.log("calculated future Vault address for nonce %d: %s", nonce + 1, vaultAddress); - const stakeControllerAddress = getContractAddress(deployer, nonce + 7); // deployed on the 8th tx (nonce+7): StakeController Impl tx, StakeController Proxy tx - console.log("calculated future StakeController address for nonce %d: %s", nonce + 7, stakeControllerAddress); + const stakeControllerAddress = getContractAddress(deployer, nonce + 5); // deployed on the 6th tx (nonce+5): Vault Impl tx, Vault Proxy tx, SortitionModule Impl tx, SortitionModule Proxy tx,, StakeController Impl tx, StakeController Proxy tx + console.log("calculated future StakeController address for nonce %d: %s", nonce + 5, stakeControllerAddress); - const klerosCoreV2Address = getContractAddress(deployer, nonce + 9); // deployed on the 10th tx (nonce+9): SortitionModule Impl tx, SortitionModule Proxy tx, StakeController Impl tx, StakeController Proxy tx, KlerosCore Impl tx, KlerosCore Proxy tx - console.log("calculated future KlerosCoreX address for nonce %d: %s", nonce + 9, klerosCoreV2Address); - - const stPNK = await deployUpgradable(deployments, "stPNK", { - from: deployer, - args: [deployer, vaultAddress], - log: true, - }); // nonce (implementation), nonce+1 (proxy) + const klerosCoreAddress = getContractAddress(deployer, nonce + 7); // deployed on the 8th tx (nonce+7): Vault Impl tx, Vault Proxy tx, SortitionModule Impl tx, SortitionModule Proxy tx, StakeController Impl tx, StakeController Proxy tx, KlerosCore Impl tx, KlerosCore Proxy tx + console.log("calculated future KlerosCoreX address for nonce %d: %s", nonce + 7, klerosCoreAddress); const vault = await deployUpgradable(deployments, "Vault", { from: deployer, - args: [deployer, pnk.target, stPNK.address, stakeControllerAddress, klerosCoreV2Address], + args: [deployer, pnk.target, stakeControllerAddress, klerosCoreAddress], log: true, - }); // nonce + 2 (implementation), nonce + 3 (proxy) + }); // nonce (implementation), nonce + 1 (proxy) // Deploy SortitionSumTree const sortitionModuleV2 = await deployUpgradable(deployments, "SortitionSumTree", { from: deployer, args: [deployer, stakeControllerAddress], log: true, - }); // nonce + 4 (implementation), nonce + 5 (proxy) + }); // nonce + 2 (implementation), nonce + 3 (proxy) // Deploy StakeController (only if not already deployed) const devnet = isDevnet(hre.network); @@ -79,7 +73,7 @@ const deployArbitrationV2: DeployFunction = async (hre: HardhatRuntimeEnvironmen from: deployer, args: [ deployer, - klerosCoreV2Address, + klerosCoreAddress, vault.address, sortitionModuleV2.address, rng.target, @@ -88,7 +82,7 @@ const deployArbitrationV2: DeployFunction = async (hre: HardhatRuntimeEnvironmen RNG_LOOKAHEAD, ], log: true, - }); // nonce + 6 (implementation), nonce + 7 (proxy) + }); // nonce + 4 (implementation), nonce + 5 (proxy) const minStake = PNK(200); const alpha = 10000; @@ -150,7 +144,6 @@ const deployArbitrationV2: DeployFunction = async (hre: HardhatRuntimeEnvironmen console.log("✅ V2 Architecture deployment completed successfully!"); console.log(`📦 Vault: ${vault.address}`); - console.log(`🎫 stPNK: ${stPNK.address}`); console.log(`🎯 SortitionSumTree: ${sortitionModuleV2.address}`); console.log(`🎮 StakeController: ${stakeController.address}`); console.log(`⚖️ KlerosCoreX: ${klerosCoreV2.address}`); diff --git a/contracts/package.json b/contracts/package.json index e8532680e..0b4d3dd75 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -157,7 +157,6 @@ "@chainlink/contracts": "^1.3.0", "@kleros/vea-contracts": "^0.6.0", "@openzeppelin/contracts": "^5.2.0", - "@openzeppelin/contracts-upgradeable": "^5.2.0", "viem": "^2.24.1" } } diff --git a/contracts/src/arbitration/KlerosCoreXBase.sol b/contracts/src/arbitration/KlerosCoreXBase.sol index 92f8db563..441b187c5 100644 --- a/contracts/src/arbitration/KlerosCoreXBase.sol +++ b/contracts/src/arbitration/KlerosCoreXBase.sol @@ -1102,7 +1102,7 @@ abstract contract KlerosCoreXBase is IArbitratorV2, Initializable, UUPSProxiable } if (pnkDeposit > 0) { try vault.deposit(_account, pnkDeposit) { - // Successfully deposited PNK and minted stPNK via Vault + // Successfully deposited PNK } catch { // Revert with a specific error or reuse existing one _stakingFailed(_onError, StakingResult.StakingTransferFailed); // Indicating failure in the deposit part of staking @@ -1111,7 +1111,7 @@ abstract contract KlerosCoreXBase is IArbitratorV2, Initializable, UUPSProxiable } if (pnkWithdrawal > 0) { try vault.withdraw(_account, pnkWithdrawal) { - // Successfully burned stPNK and withdrew PNK via Vault + // Successfully withdrew PNK via Vault } catch { // Revert with a specific error or reuse existing one _stakingFailed(_onError, StakingResult.UnstakingTransferFailed); // Indicating failure in the withdrawal part of unstaking @@ -1143,7 +1143,7 @@ abstract contract KlerosCoreXBase is IArbitratorV2, Initializable, UUPSProxiable } if (pnkDeposit > 0) { try vault.deposit(_account, pnkDeposit) { - // Successfully deposited PNK and minted stPNK via Vault + // Successfully deposited PNK } catch { // Revert with a specific error or reuse existing one _stakingFailed(onError, StakingResult.StakingTransferFailed); // Indicating failure in the deposit part of staking @@ -1152,7 +1152,7 @@ abstract contract KlerosCoreXBase is IArbitratorV2, Initializable, UUPSProxiable } if (pnkWithdrawal > 0) { try vault.withdraw(_account, pnkWithdrawal) { - // Successfully burned stPNK and withdrew PNK via Vault + // Successfully withdrew PNK via Vault } catch { // Revert with a specific error or reuse existing one _stakingFailed(onError, StakingResult.UnstakingTransferFailed); // Indicating failure in the withdrawal part of unstaking diff --git a/contracts/src/arbitration/Vault.sol b/contracts/src/arbitration/Vault.sol index e38204e46..c00d8617f 100644 --- a/contracts/src/arbitration/Vault.sol +++ b/contracts/src/arbitration/Vault.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.24; -import {VaultBase, IERC20, stPNK} from "./VaultBase.sol"; +import {VaultBase, IERC20} from "./VaultBase.sol"; /// @title Vault /// @notice PNK Vault for handling deposits, withdrawals, locks, and penalties @@ -27,11 +27,10 @@ contract Vault is VaultBase { function initialize( address _governor, IERC20 _pnk, - stPNK _stPnk, address _stakeController, address _core ) external reinitializer(1) { - __VaultBase_initialize(_governor, _pnk, _stPnk, _stakeController, _core); + __VaultBase_initialize(_governor, _pnk, _stakeController, _core); } // ************************************* // diff --git a/contracts/src/arbitration/VaultBase.sol b/contracts/src/arbitration/VaultBase.sol index fff81e9e9..1c13fb7ab 100644 --- a/contracts/src/arbitration/VaultBase.sol +++ b/contracts/src/arbitration/VaultBase.sol @@ -3,7 +3,6 @@ pragma solidity 0.8.24; import {IVault} from "./interfaces/IVault.sol"; -import {stPNK} from "./stPNK.sol"; import {Initializable} from "../proxy/Initializable.sol"; import {UUPSProxiable} from "../proxy/UUPSProxiable.sol"; import {SafeERC20, IERC20} from "../libraries/SafeERC20.sol"; @@ -30,7 +29,6 @@ abstract contract VaultBase is IVault, Initializable, UUPSProxiable { address public governor; // The governor of the contract. IERC20 public pnk; // The PNK token contract. - stPNK public stPnk; // The stPNK token contract. address public stakeController; // The stake controller authorized to lock/unlock/penalize. address public core; // The KlerosCore authorized to transfer rewards. @@ -68,24 +66,13 @@ abstract contract VaultBase is IVault, Initializable, UUPSProxiable { function __VaultBase_initialize( address _governor, IERC20 _pnk, - stPNK _stPnk, address _stakeController, address _core ) internal onlyInitializing { governor = _governor; pnk = _pnk; - stPnk = _stPnk; stakeController = _stakeController; core = _core; - - // Add stakeController and core as protocol contracts in stPNK - address[] memory contracts = new address[](2); - bool[] memory allowed = new bool[](2); - contracts[0] = _stakeController; - contracts[1] = _core; - allowed[0] = true; - allowed[1] = true; - stPnk.setProtocolContracts(contracts, allowed); } // ************************************* // @@ -102,22 +89,12 @@ abstract contract VaultBase is IVault, Initializable, UUPSProxiable { /// @param _stakeController The new value for the `stakeController` storage variable. function changeStakeController(address _stakeController) external onlyByGovernor { stakeController = _stakeController; - address[] memory contracts = new address[](1); - bool[] memory allowed = new bool[](1); - contracts[0] = _stakeController; - allowed[0] = true; - stPnk.setProtocolContracts(contracts, allowed); } /// @dev Changes the `core` storage variable. /// @param _core The new value for the `core` storage variable. function changeCore(address _core) external onlyByGovernor { core = _core; - address[] memory contracts = new address[](1); - bool[] memory allowed = new bool[](1); - contracts[0] = _core; - allowed[0] = true; - stPnk.setProtocolContracts(contracts, allowed); } // ************************************* // @@ -125,15 +102,14 @@ abstract contract VaultBase is IVault, Initializable, UUPSProxiable { // ************************************* // /// @inheritdoc IVault - function deposit(address _from, uint256 _amount) external virtual override onlyCore returns (uint256 stPnkAmount) { - return _deposit(_from, _amount); + function deposit(address _from, uint256 _amount) external virtual override onlyCore { + _deposit(_from, _amount); } /// @dev Internal implementation of deposit. /// @param _from The user address for the deposit. /// @param _amount The amount of PNK to deposit. - /// @return stPnkAmount The amount of stPNK minted. - function _deposit(address _from, uint256 _amount) internal virtual returns (uint256 stPnkAmount) { + function _deposit(address _from, uint256 _amount) internal virtual { if (_amount == 0) revert InvalidAmount(); // Transfer PNK from the user to the vault @@ -141,10 +117,6 @@ abstract contract VaultBase is IVault, Initializable, UUPSProxiable { pnk.safeTransferFrom(_from, address(this), _amount); jurorBalances[_from].deposited += _amount; - // Mint 1:1 stPNK to the user account - stPnkAmount = _amount; - stPnk.mint(_from, stPnkAmount); - emit Deposit(_from, _amount); } @@ -155,7 +127,7 @@ abstract contract VaultBase is IVault, Initializable, UUPSProxiable { /// @dev Internal implementation of withdraw. /// @param _to The user address for the withdrawal. - /// @param _amount The amount of stPNK to withdraw (will be burned). + /// @param _amount The amount of PNK to withdraw. /// @return pnkAmount The amount of PNK transferred back to the user. function _withdraw(address _to, uint256 _amount) internal virtual returns (uint256 pnkAmount) { if (_amount == 0) revert InvalidAmount(); @@ -166,12 +138,6 @@ abstract contract VaultBase is IVault, Initializable, UUPSProxiable { uint256 available = getAvailableBalance(_to); if (_amount > available) revert InsufficientAvailableBalance(); - // Check stPNK balance of the user - // The Vault must be approved by _to to burn their stPNK - if (stPnk.balanceOf(_to) < _amount) revert InsufficientStPNKBalance(); - - // Burn stPNK from the user and transfer PNK to the user - stPnk.burnFrom(_to, _amount); // Vault burns user's stPNK balance.deposited -= _amount; pnk.safeTransfer(_to, _amount); // Vault sends PNK to user @@ -206,13 +172,6 @@ abstract contract VaultBase is IVault, Initializable, UUPSProxiable { balance.deposited -= actualPenalty; balance.penalties += actualPenalty; - // Burn equivalent stPNK if user still holds it - uint256 userStPnkBalance = stPnk.balanceOf(_account); - uint256 toBurn = actualPenalty > userStPnkBalance ? userStPnkBalance : actualPenalty; - if (toBurn > 0) { - stPnk.burnFrom(_account, toBurn); - } - // Note: Penalized PNK stays in vault to fund rewards pool emit Penalty(_account, actualPenalty); } @@ -245,11 +204,6 @@ abstract contract VaultBase is IVault, Initializable, UUPSProxiable { return jurorBalances[_account].locked; } - /// @inheritdoc IVault - function getStPNKBalance(address _account) external view override returns (uint256) { - return stPnk.balanceOf(_account); - } - /// @inheritdoc IVault function getPenaltyBalance(address _account) external view override returns (uint256) { return jurorBalances[_account].penalties; @@ -264,6 +218,5 @@ abstract contract VaultBase is IVault, Initializable, UUPSProxiable { error OnlyCore(); error InvalidAmount(); error InsufficientAvailableBalance(); - error InsufficientStPNKBalance(); error InsufficientVaultBalance(); } diff --git a/contracts/src/arbitration/VaultNeo.sol b/contracts/src/arbitration/VaultNeo.sol index 5d81c184c..02239ea33 100644 --- a/contracts/src/arbitration/VaultNeo.sol +++ b/contracts/src/arbitration/VaultNeo.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.24; -import {VaultBase, IERC20, stPNK} from "./VaultBase.sol"; +import {VaultBase, IERC20} from "./VaultBase.sol"; import {SafeERC20} from "../libraries/SafeERC20.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; @@ -51,14 +51,13 @@ contract VaultNeo is VaultBase { function initialize( address _governor, IERC20 _pnk, - stPNK _stPnk, address _stakeController, address _core, IERC721 _depositNft, uint256 _maxDepositPerUser, uint256 _totalDepositCap ) external reinitializer(2) { - __VaultBase_initialize(_governor, _pnk, _stPnk, _stakeController, _core); + __VaultBase_initialize(_governor, _pnk, _stakeController, _core); depositNft = _depositNft; maxDepositPerUser = _maxDepositPerUser; @@ -100,8 +99,8 @@ contract VaultNeo is VaultBase { // * State Modifiers * // // ************************************* // - /// @notice Deposit PNK and mint stPNK with additional validation checks - function _deposit(address _from, uint256 _amount) internal override returns (uint256 stPnkAmount) { + /// @inheritdoc VaultBase + function _deposit(address _from, uint256 _amount) internal override { // Check NFT requirement if set if (address(depositNft) != address(0) && depositNft.balanceOf(_from) == 0) { revert DepositNftRequired(); @@ -123,10 +122,10 @@ contract VaultNeo is VaultBase { // Update total deposited totalDeposited += _amount; - return super._deposit(_from, _amount); + super._deposit(_from, _amount); } - /// @notice Withdraw PNK by burning stPNK and update total deposited + /// @inheritdoc VaultBase function _withdraw(address _to, uint256 _amount) internal override returns (uint256 pnkAmount) { totalDeposited -= _amount; return super._withdraw(_to, _amount); diff --git a/contracts/src/arbitration/interfaces/IVault.sol b/contracts/src/arbitration/interfaces/IVault.sol index 41002a155..361ae6127 100644 --- a/contracts/src/arbitration/interfaces/IVault.sol +++ b/contracts/src/arbitration/interfaces/IVault.sol @@ -22,13 +22,12 @@ interface IVault { // * State Modifiers * // // ************************************* // - /// @notice Deposit PNK and mint stPNK + /// @notice Deposit PNK in the vault /// @param _from The account to deposit from /// @param _amount The amount of PNK to deposit - /// @return stPnkAmount The amount of stPNK minted - function deposit(address _from, uint256 _amount) external returns (uint256 stPnkAmount); + function deposit(address _from, uint256 _amount) external; - /// @notice Withdraw PNK by burning stPNK + /// @notice Withdraw PNK /// @param _to The account to withdraw to /// @param _amount The amount to withdraw /// @return pnkAmount The amount of PNK withdrawn @@ -78,11 +77,6 @@ interface IVault { /// @return The locked balance function getLockedBalance(address _account) external view returns (uint256); - /// @notice Get stPNK balance (same as deposited - penalties) - /// @param _account The account to check - /// @return The stPNK balance - function getStPNKBalance(address _account) external view returns (uint256); - /// @notice Get penalty balance /// @param _account The account to check /// @return The penalty balance diff --git a/contracts/src/arbitration/stPNK.sol b/contracts/src/arbitration/stPNK.sol deleted file mode 100644 index 374b6eeff..000000000 --- a/contracts/src/arbitration/stPNK.sol +++ /dev/null @@ -1,188 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; -import {Initializable} from "../proxy/Initializable.sol"; -import {UUPSProxiable} from "../proxy/UUPSProxiable.sol"; - -/// @title stPNK -/// @notice Non-transferable staking token representing staked PNK -/// @dev Only transferable within Kleros protocol contracts to prevent external trading -/// @custom:security-contact contact@kleros.io -contract stPNK is ERC20Upgradeable, UUPSProxiable { - string public constant override version = "1.0.0"; - - // ************************************* // - // * Storage * // - // ************************************* // - - address public governor; - address public vault; - mapping(address => bool) public protocolContracts; // Whitelist of protocol contracts that can receive stPNK - mapping(address => bool) public isUser; // Track if an address is a regular user (not a protocol contract) - - // ************************************* // - // * Events * // - // ************************************* // - - event ProtocolContractUpdated(address indexed contract_, bool allowed); - - // ************************************* // - // * Function Modifiers * // - // ************************************* // - - modifier onlyByGovernor() { - if (governor != msg.sender) revert GovernorOnly(); - _; - } - - modifier onlyVault() { - if (msg.sender != vault) revert OnlyVault(); - _; - } - - modifier onlyProtocolContracts() { - if (!protocolContracts[msg.sender]) revert OnlyProtocolContracts(); - _; - } - - // ************************************* // - // * Constructor * // - // ************************************* // - - constructor() { - _disableInitializers(); - } - - /// @notice Initialize the stPNK token - /// @param _governor The governor address - /// @param _vault The vault address - function initialize(address _governor, address _vault) external initializer { - __ERC20_init("Staked Pinakion", "stPNK"); - governor = _governor; - vault = _vault; - protocolContracts[_vault] = true; // Automatically whitelist vault - } - - // ************************************* // - // * Governance * // - // ************************************* // - - /// @dev Access Control to perform implementation upgrades (UUPS Proxiable) - /// Only the governor can perform upgrades (`onlyByGovernor`) - function _authorizeUpgrade(address) internal view override onlyByGovernor { - // NOP - } - - /// @notice Change the governor address - /// @param _governor The new governor address - function changeGovernor(address _governor) external onlyByGovernor { - governor = _governor; - } - - /// @notice Change the vault address - /// @param _vault The new vault address - function changeVault(address _vault) external onlyByGovernor { - vault = _vault; - } - - // ************************************* // - // * State Modifiers * // - // ************************************* // - - /// @notice Mint stPNK tokens (only vault) - /// @param _to The address to mint tokens to - /// @param _amount The amount of tokens to mint - function mint(address _to, uint256 _amount) external onlyVault { - isUser[_to] = true; - _mint(_to, _amount); - } - - /// @notice Burn stPNK tokens from account (only vault) - /// @param _from The address to burn tokens from - /// @param _amount The amount of tokens to burn - function burnFrom(address _from, uint256 _amount) external onlyVault { - _burn(_from, _amount); - } - - /// @notice Update protocol contract whitelist for multiple contracts (only via governance) - /// @param _contracts Array of contract addresses to update - /// @param _allowed Array of boolean values for each contract - function setProtocolContracts( - address[] calldata _contracts, - bool[] calldata _allowed - ) external onlyProtocolContracts { - if (_contracts.length != _allowed.length) revert ArrayLengthMismatch(); - - for (uint256 i = 0; i < _contracts.length; i++) { - if (_contracts[i] == address(0)) continue; - protocolContracts[_contracts[i]] = _allowed[i]; - emit ProtocolContractUpdated(_contracts[i], _allowed[i]); - } - } - - // ************************************* // - // * Public Views * // - // ************************************* // - - /// @notice Check if transfer is allowed (view function for frontends) - /// @param _from The sender address - /// @param _to The recipient address - /// @return Whether the transfer is allowed - function isTransferAllowed(address _from, address _to) external view returns (bool) { - if (_from == address(0) || _to == address(0)) return true; - if (protocolContracts[_from] && protocolContracts[_to]) return true; - if (protocolContracts[_from] && !protocolContracts[_to]) return true; - if (!protocolContracts[_from] && protocolContracts[_to]) return true; - return false; - } - - // ************************************* // - // * Internal * // - // ************************************* // - - /// @notice Override update to prevent external trading - /// @param _from The sender address - /// @param _to The recipient address - /// @param _value The amount to transfer - function _update(address _from, address _to, uint256 _value) internal override { - // Allow minting (_from == address(0)) and burning (_to == address(0)) - if (_from == address(0) || _to == address(0)) { - super._update(_from, _to, _value); - return; - } - - // Allow transfers between protocol contracts - if (protocolContracts[_from] && protocolContracts[_to]) { - super._update(_from, _to, _value); - return; - } - - // Allow transfers from protocol contracts to users (e.g., rewards) - if (protocolContracts[_from] && !protocolContracts[_to]) { - isUser[_to] = true; - super._update(_from, _to, _value); - return; - } - - // Allow transfers from users to protocol contracts (e.g., for operations) - if (!protocolContracts[_from] && protocolContracts[_to]) { - super._update(_from, _to, _value); - return; - } - - // Block all other transfers (user-to-user, external contracts) - revert TransferNotAllowed(); - } - - // ************************************* // - // * Errors * // - // ************************************* // - - error GovernorOnly(); - error TransferNotAllowed(); - error OnlyVault(); - error OnlyProtocolContracts(); - error ArrayLengthMismatch(); -} diff --git a/yarn.lock b/yarn.lock index 61ecd1a88..8b3ef2e5c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5559,7 +5559,6 @@ __metadata: "@nomicfoundation/hardhat-ethers": "npm:^3.0.8" "@nomiclabs/hardhat-solhint": "npm:^4.0.1" "@openzeppelin/contracts": "npm:^5.2.0" - "@openzeppelin/contracts-upgradeable": "npm:^5.2.0" "@openzeppelin/upgrades-core": "npm:^1.42.2" "@typechain/ethers-v6": "npm:^0.5.1" "@typechain/hardhat": "npm:^9.1.0" @@ -7391,15 +7390,6 @@ __metadata: languageName: node linkType: hard -"@openzeppelin/contracts-upgradeable@npm:^5.2.0": - version: 5.3.0 - resolution: "@openzeppelin/contracts-upgradeable@npm:5.3.0" - peerDependencies: - "@openzeppelin/contracts": 5.3.0 - checksum: 10/048eb2862aa23dba6f548ba5332e1ec9282c1dd6e73125156c1d918f5e7f48b8a0e0f405d0f7e4e74794d2cea3dd9e7901a146ac315a325730dcd38f99c2a3d9 - languageName: node - linkType: hard - "@openzeppelin/contracts@npm:4.9.6": version: 4.9.6 resolution: "@openzeppelin/contracts@npm:4.9.6" From 3d4832a57fef1df4acdb1a815f0bd72a1f4abea1 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Thu, 5 Jun 2025 23:17:12 +0100 Subject: [PATCH 11/24] refactor: moved StakeSet event from SortitionSumTree to StakeController --- contracts/src/arbitration/SortitionSumTreeBase.sol | 3 --- contracts/src/arbitration/StakeControllerBase.sol | 4 ++++ .../src/arbitration/interfaces/ISortitionSumTree.sol | 11 ----------- .../src/arbitration/interfaces/IStakeController.sol | 7 +++++++ 4 files changed, 11 insertions(+), 14 deletions(-) diff --git a/contracts/src/arbitration/SortitionSumTreeBase.sol b/contracts/src/arbitration/SortitionSumTreeBase.sol index c3265e7ba..d2b5644f4 100644 --- a/contracts/src/arbitration/SortitionSumTreeBase.sol +++ b/contracts/src/arbitration/SortitionSumTreeBase.sol @@ -125,9 +125,6 @@ abstract contract SortitionSumTreeBase is ISortitionSumTree, Initializable, UUPS } } } - - (, , , uint256 totalStaked, , ) = stakeController.getJurorBalance(_account, _courtID); - emit StakeSet(_account, _courtID, _newStake, totalStaked); return StakingResult.Successful; } diff --git a/contracts/src/arbitration/StakeControllerBase.sol b/contracts/src/arbitration/StakeControllerBase.sol index 7fab6abcf..5c779aabd 100644 --- a/contracts/src/arbitration/StakeControllerBase.sol +++ b/contracts/src/arbitration/StakeControllerBase.sol @@ -386,6 +386,8 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr } return (0, 0, stakingResult); } + + emit StakeSet(_account, _courtID, _newStake, currentJurorStake.totalStake); } /// @dev Internal implementation of setStake with phase-aware delayed stake logic @@ -458,6 +460,8 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr } return (0, 0, stakingResult); } + + emit StakeSet(_account, _courtID, _newStake, currentJurorStake.totalStake); } /// @dev Removes a court from a juror's list of staked courts. diff --git a/contracts/src/arbitration/interfaces/ISortitionSumTree.sol b/contracts/src/arbitration/interfaces/ISortitionSumTree.sol index cb6ff81bf..807f69c01 100644 --- a/contracts/src/arbitration/interfaces/ISortitionSumTree.sol +++ b/contracts/src/arbitration/interfaces/ISortitionSumTree.sol @@ -8,17 +8,6 @@ import "../../libraries/Constants.sol"; /// @notice Interface for pure sortition operations without phase management or token operations /// @dev This interface contains only tree management and drawing logic interface ISortitionSumTree { - // ************************************* // - // * Events * // - // ************************************* // - - /// @notice Emitted when a juror's stake is set in a court - /// @param _address The address of the juror - /// @param _courtID The ID of the court - /// @param _amount The amount of tokens staked in the court - /// @param _amountAllCourts The amount of tokens staked in all courts - event StakeSet(address indexed _address, uint256 _courtID, uint256 _amount, uint256 _amountAllCourts); - // ************************************* // // * Tree Management * // // ************************************* // diff --git a/contracts/src/arbitration/interfaces/IStakeController.sol b/contracts/src/arbitration/interfaces/IStakeController.sol index 128c86784..f2ac53f16 100644 --- a/contracts/src/arbitration/interfaces/IStakeController.sol +++ b/contracts/src/arbitration/interfaces/IStakeController.sol @@ -29,6 +29,13 @@ interface IStakeController { event StakeUnlocked(address indexed _account, uint256 _amount); event JurorSetInactive(address indexed _account); + /// @notice Emitted when a juror's stake is set in a court + /// @param _account The address of the juror + /// @param _courtID The ID of the court + /// @param _stakeInCourt The amount of tokens staked in the court + /// @param _totalStake The amount of tokens staked in all courts + event StakeSet(address indexed _account, uint96 indexed _courtID, uint256 _stakeInCourt, uint256 _totalStake); + // Migration events event StakeImported(address indexed _juror, uint96 indexed _courtID, uint256 _stake); event DelayedStakeImported(address indexed _juror, uint96 indexed _courtID, uint256 _stake, uint256 _index); From 57d5261b92cbaea00578eb573b8ed8e399a8c626 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Fri, 6 Jun 2025 13:35:08 +0100 Subject: [PATCH 12/24] feat: no more SortitionSumTree.setStake() failure to handle --- .../src/arbitration/SortitionSumTreeBase.sol | 22 ++-- .../src/arbitration/StakeControllerBase.sol | 108 +++++------------- .../interfaces/ISortitionSumTree.sol | 7 +- 3 files changed, 34 insertions(+), 103 deletions(-) diff --git a/contracts/src/arbitration/SortitionSumTreeBase.sol b/contracts/src/arbitration/SortitionSumTreeBase.sol index d2b5644f4..c5913231f 100644 --- a/contracts/src/arbitration/SortitionSumTreeBase.sol +++ b/contracts/src/arbitration/SortitionSumTreeBase.sol @@ -97,35 +97,27 @@ abstract contract SortitionSumTreeBase is ISortitionSumTree, Initializable, UUPS address _account, uint96 _courtID, uint256 _newStake - ) external virtual override onlyByStakeController returns (StakingResult stakingResult) { - uint256 currentStake = this.stakeOf(_account, _courtID); - - if (currentStake == 0 && _newStake == 0) { - return StakingResult.CannotStakeZeroWhenNoStake; // No change needed - } - - // Update the sortition sum tree in court hierarchy + ) external virtual override onlyByStakeController { bytes32 stakePathID = _accountAndCourtIDToStakePathID(_account, _courtID); bool finished = false; - uint96 currentCourtIDForHierarchy = _courtID; + uint96 currentCourtID = _courtID; KlerosCoreXBase core = stakeController.core(); while (!finished) { - _set(bytes32(uint256(currentCourtIDForHierarchy)), _newStake, stakePathID); - if (currentCourtIDForHierarchy == GENERAL_COURT) { + _set(bytes32(uint256(currentCourtID)), _newStake, stakePathID); + if (currentCourtID == GENERAL_COURT) { finished = true; } else { // Fetch parent court ID. Ensure core.courts() is accessible and correct. - (uint96 parentCourtID, , , , , , ) = core.courts(currentCourtIDForHierarchy); - if (parentCourtID == currentCourtIDForHierarchy) { + (uint96 parentCourtID, , , , , , ) = core.courts(currentCourtID); + if (parentCourtID == currentCourtID) { // Avoid infinite loop if parent is self (e.g. for general court already handled or misconfiguration) finished = true; } else { - currentCourtIDForHierarchy = parentCourtID; + currentCourtID = parentCourtID; } } } - return StakingResult.Successful; } // ************************************* // diff --git a/contracts/src/arbitration/StakeControllerBase.sol b/contracts/src/arbitration/StakeControllerBase.sol index 5c779aabd..390947d2e 100644 --- a/contracts/src/arbitration/StakeControllerBase.sol +++ b/contracts/src/arbitration/StakeControllerBase.sol @@ -344,24 +344,25 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr JurorStake storage currentJurorStake = jurorStakes[_account]; uint256 currentStakeInCourt = currentJurorStake.stakes[_courtID]; - // Check for MAX_STAKE_PATHS before calculating deposit/withdrawal if it's a new court stake - if (currentStakeInCourt == 0 && _newStake > 0) { - if (currentJurorStake.stakedCourtIDs.length >= MAX_STAKE_PATHS) { + if (currentStakeInCourt == 0) { + if (_newStake == 0) + // No change needed + return (0, 0, StakingResult.CannotStakeZeroWhenNoStake); + else if (_newStake > 0 && currentJurorStake.stakedCourtIDs.length >= MAX_STAKE_PATHS) + // Cannot stake in more courts return (0, 0, StakingResult.CannotStakeInMoreCourts); - } } - uint256 previousTotalStake = currentJurorStake.totalStake; // Keep track for potential revert + currentJurorStake.stakes[_courtID] = _newStake; + if (_newStake > currentStakeInCourt) { pnkDeposit = _newStake - currentStakeInCourt; - currentJurorStake.totalStake = previousTotalStake + pnkDeposit; + currentJurorStake.totalStake += pnkDeposit; } else if (_newStake < currentStakeInCourt) { pnkWithdrawal = currentStakeInCourt - _newStake; - currentJurorStake.totalStake = previousTotalStake - pnkWithdrawal; + currentJurorStake.totalStake -= pnkWithdrawal; } - currentJurorStake.stakes[_courtID] = _newStake; - // Manage stakedCourtIDs if (currentStakeInCourt == 0 && _newStake > 0) { currentJurorStake.stakedCourtIDs.push(_courtID); @@ -369,23 +370,7 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr _removeCourt(currentJurorStake.stakedCourtIDs, _courtID); } - stakingResult = sortitionModule.setStake(_account, _courtID, _newStake); - if (stakingResult != StakingResult.Successful) { - // Revert local changes if sortitionModule update fails - currentJurorStake.stakes[_courtID] = currentStakeInCourt; - currentJurorStake.totalStake = previousTotalStake; - if (currentStakeInCourt == 0 && _newStake > 0) { - // revert insertion: was a push, so pop - uint96[] storage stakedCourtsRevert = currentJurorStake.stakedCourtIDs; - if (stakedCourtsRevert.length > 0 && stakedCourtsRevert[stakedCourtsRevert.length - 1] == _courtID) { - stakedCourtsRevert.pop(); - } - } else if (currentStakeInCourt > 0 && _newStake == 0) { - // revert removal: was a remove, so add it back (order might not be preserved by simple push) - currentJurorStake.stakedCourtIDs.push(_courtID); - } - return (0, 0, stakingResult); - } + sortitionModule.setStake(_account, _courtID, _newStake); emit StakeSet(_account, _courtID, _newStake, currentJurorStake.totalStake); } @@ -402,6 +387,10 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr uint96 _courtID, uint256 _newStake ) internal virtual returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { + if (phase == Phase.staking) { + return _setStakeBySystem(_account, _courtID, _newStake); + } + JurorStake storage currentJurorStake = jurorStakes[_account]; uint256 currentStakeInCourt = currentJurorStake.stakes[_courtID]; @@ -411,70 +400,25 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr pnkWithdrawal = currentStakeInCourt - _newStake; } - if (phase != Phase.staking) { - // MAX_STAKE_PATHS is checked when _setStakeBySystem() is called during executeDelayedStakes(). - DelayedStake storage delayedStake = delayedStakes[++delayedStakeWriteIndex]; - delayedStake.account = _account; - delayedStake.courtID = _courtID; - delayedStake.stake = _newStake; - return (pnkDeposit, pnkWithdrawal, StakingResult.Delayed); - } - - // Check for MAX_STAKE_PATHS if it's a new court stake - if (currentStakeInCourt == 0 && _newStake > 0) { - if (currentJurorStake.stakedCourtIDs.length >= MAX_STAKE_PATHS) { - return (0, 0, StakingResult.CannotStakeInMoreCourts); - } - } - - // Update local stake records first - uint256 previousTotalStake = currentJurorStake.totalStake; // Keep track for potential revert - if (_newStake > currentStakeInCourt) { - currentJurorStake.totalStake = previousTotalStake + pnkDeposit; - } else if (_newStake < currentStakeInCourt) { - currentJurorStake.totalStake = previousTotalStake - pnkWithdrawal; - } - currentJurorStake.stakes[_courtID] = _newStake; - - // Manage stakedCourtIDs - if (currentStakeInCourt == 0 && _newStake > 0) { - currentJurorStake.stakedCourtIDs.push(_courtID); - } else if (currentStakeInCourt > 0 && _newStake == 0) { - _removeCourt(currentJurorStake.stakedCourtIDs, _courtID); - } - - stakingResult = sortitionModule.setStake(_account, _courtID, _newStake); - if (stakingResult != StakingResult.Successful) { - // Revert local changes if sortitionModule update fails - currentJurorStake.stakes[_courtID] = currentStakeInCourt; - currentJurorStake.totalStake = previousTotalStake; - if (currentStakeInCourt == 0 && _newStake > 0) { - // revert insertion: was a push, so pop - uint96[] storage stakedCourtsRevert = currentJurorStake.stakedCourtIDs; - if (stakedCourtsRevert.length > 0 && stakedCourtsRevert[stakedCourtsRevert.length - 1] == _courtID) { - stakedCourtsRevert.pop(); - } - } else if (currentStakeInCourt > 0 && _newStake == 0) { - // revert removal: was a remove, so add it back (order might not be preserved by simple push) - currentJurorStake.stakedCourtIDs.push(_courtID); - } - return (0, 0, stakingResult); - } - - emit StakeSet(_account, _courtID, _newStake, currentJurorStake.totalStake); + // MAX_STAKE_PATHS is checked by _setStakeBySystem() called during executeDelayedStakes(). + DelayedStake storage delayedStake = delayedStakes[++delayedStakeWriteIndex]; + delayedStake.account = _account; + delayedStake.courtID = _courtID; + delayedStake.stake = _newStake; + return (pnkDeposit, pnkWithdrawal, StakingResult.Delayed); } /// @dev Removes a court from a juror's list of staked courts. /// @param _stakedCourts Storage pointer to the juror's array of staked court IDs. /// @param _courtID The ID of the court to remove. function _removeCourt(uint96[] storage _stakedCourts, uint96 _courtID) internal { - uint256 len = _stakedCourts.length; - if (len == 0) { + uint256 length = _stakedCourts.length; + if (length == 0) { return; // Nothing to remove } uint256 courtIndexToRemove = type(uint256).max; // Sentinel value indicates not found - for (uint256 i = 0; i < len; i++) { + for (uint256 i = 0; i < length; i++) { if (_stakedCourts[i] == _courtID) { courtIndexToRemove = i; break; @@ -484,8 +428,8 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr if (courtIndexToRemove != type(uint256).max) { // If the courtID was found in the array // If it's not the last element, swap the last element into its place - if (courtIndexToRemove != len - 1) { - _stakedCourts[courtIndexToRemove] = _stakedCourts[len - 1]; + if (courtIndexToRemove != length - 1) { + _stakedCourts[courtIndexToRemove] = _stakedCourts[length - 1]; } // Remove the last element (either the original last, or the one that was swapped) _stakedCourts.pop(); diff --git a/contracts/src/arbitration/interfaces/ISortitionSumTree.sol b/contracts/src/arbitration/interfaces/ISortitionSumTree.sol index 807f69c01..94b415c47 100644 --- a/contracts/src/arbitration/interfaces/ISortitionSumTree.sol +++ b/contracts/src/arbitration/interfaces/ISortitionSumTree.sol @@ -21,12 +21,7 @@ interface ISortitionSumTree { /// @param _account The address of the juror /// @param _courtID The ID of the court /// @param _newStake The new stake amount - /// @return stakingResult The result of the staking operation - function setStake( - address _account, - uint96 _courtID, - uint256 _newStake - ) external returns (StakingResult stakingResult); + function setStake(address _account, uint96 _courtID, uint256 _newStake) external; // ************************************* // // * Drawing * // From 9d8737389ad63862f777bbf5830f20d7411b1003 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Mon, 9 Jun 2025 12:50:44 +0100 Subject: [PATCH 13/24] refactor: merged StakeController._setStake() and _setStakeBySystem(), removed SortitionModule refs --- contracts/src/arbitration/KlerosCoreXBase.sol | 3 +- contracts/src/arbitration/StakeController.sol | 8 +- .../src/arbitration/StakeControllerBase.sol | 108 +++++++----------- .../dispute-kits/DisputeKitClassicBase.sol | 6 +- .../interfaces/IStakeController.sol | 20 +--- 5 files changed, 52 insertions(+), 93 deletions(-) diff --git a/contracts/src/arbitration/KlerosCoreXBase.sol b/contracts/src/arbitration/KlerosCoreXBase.sol index 441b187c5..df5fe5de2 100644 --- a/contracts/src/arbitration/KlerosCoreXBase.sol +++ b/contracts/src/arbitration/KlerosCoreXBase.sol @@ -1131,7 +1131,7 @@ abstract contract KlerosCoreXBase is IArbitratorV2, Initializable, UUPSProxiable uint96 _courtID, uint256 _newStake ) internal virtual returns (bool success) { - (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) = stakeController.setStakeBySystem( + (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) = stakeController.setStake( _account, _courtID, _newStake @@ -1213,7 +1213,6 @@ abstract contract KlerosCoreXBase is IArbitratorV2, Initializable, UUPSProxiable error GovernorOnly(); error GuardianOrGovernorOnly(); error DisputeKitOnly(); - error SortitionModuleOnly(); error StakeControllerOnly(); error UnsuccessfulCall(); error InvalidDisputKitParent(); diff --git a/contracts/src/arbitration/StakeController.sol b/contracts/src/arbitration/StakeController.sol index a5830b076..88157a1f2 100644 --- a/contracts/src/arbitration/StakeController.sol +++ b/contracts/src/arbitration/StakeController.sol @@ -10,7 +10,7 @@ import {RNG} from "../rng/RNG.sol"; /// @title StakeController /// @notice Basic implementation of the Stake Controller -/// @dev Coordinates between Vault and SortitionModule for the new architecture +/// @dev Coordinates between Vault and SortitionSumTree for the new architecture contract StakeController is StakeControllerBase { string public constant override version = "1.0.0"; @@ -27,7 +27,7 @@ contract StakeController is StakeControllerBase { /// @param _governor The governor's address. /// @param _core The KlerosCore contract. /// @param _vault The Vault contract. - /// @param _sortitionModule The SortitionModule contract. + /// @param _sortition The SortitionSumTree contract. /// @param _minStakingTime The minimum staking time. /// @param _maxDrawingTime The maximum drawing time. /// @param _rng The random number generator. @@ -36,7 +36,7 @@ contract StakeController is StakeControllerBase { address _governor, KlerosCoreXBase _core, IVault _vault, - ISortitionSumTree _sortitionModule, + ISortitionSumTree _sortition, uint256 _minStakingTime, uint256 _maxDrawingTime, RNG _rng, @@ -46,7 +46,7 @@ contract StakeController is StakeControllerBase { _governor, _core, _vault, - _sortitionModule, + _sortition, _minStakingTime, _maxDrawingTime, _rng, diff --git a/contracts/src/arbitration/StakeControllerBase.sol b/contracts/src/arbitration/StakeControllerBase.sol index 390947d2e..4d0c825bf 100644 --- a/contracts/src/arbitration/StakeControllerBase.sol +++ b/contracts/src/arbitration/StakeControllerBase.sol @@ -13,7 +13,7 @@ import {RNG} from "../rng/RNG.sol"; import "../libraries/Constants.sol"; /// @title StakeControllerBase -/// @notice Abstract base contract for coordinating between Vault and SortitionModule +/// @notice Abstract base contract for coordinating between Vault and SortitionSumTree /// @dev Manages phases, delayed stakes, and coordination logic abstract contract StakeControllerBase is IStakeController, Initializable, UUPSProxiable { // ************************************* // @@ -39,7 +39,7 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr address public governor; // The governor of the contract. KlerosCoreXBase public core; // The core arbitrator contract. IVault public vault; // The PNK vault for token management. - ISortitionSumTree public sortitionModule; // The sortition module for drawing logic. + ISortitionSumTree public sortition; // The sortition sum tree for drawing. // Phase management Phase public override phase; // The current phase. Uses Phase from IStakeController. @@ -82,7 +82,7 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr address _governor, KlerosCoreXBase _core, IVault _vault, - ISortitionSumTree _sortitionModule, + ISortitionSumTree _sortition, uint256 _minStakingTime, uint256 _maxDrawingTime, RNG _rng, @@ -91,7 +91,7 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr governor = _governor; core = _core; vault = _vault; - sortitionModule = _sortitionModule; + sortition = _sortition; minStakingTime = _minStakingTime; maxDrawingTime = _maxDrawingTime; lastPhaseChange = block.timestamp; @@ -116,10 +116,10 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr vault = _vault; } - /// @dev Changes the `sortitionModule` storage variable. - /// @param _sortitionModule The new sortition module address. - function changeSortitionModule(ISortitionSumTree _sortitionModule) external onlyByGovernor { - sortitionModule = _sortitionModule; + /// @dev Changes the `sortition` storage variable. + /// @param _sortition The new sortition module address. + function changeSortitionSumTree(ISortitionSumTree _sortition) external onlyByGovernor { + sortition = _sortition; } /// @dev Changes the `minStakingTime` storage variable. @@ -198,15 +198,6 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr // * Stake Management * // // ************************************* // - /// @inheritdoc IStakeController - function setStakeBySystem( - address _account, - uint96 _courtID, - uint256 _newStake - ) external override onlyByCore returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { - return _setStakeBySystem(_account, _courtID, _newStake); - } - /// @inheritdoc IStakeController function setStake( address _account, @@ -243,7 +234,7 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr uint96[] storage courtIDsForJuror = jurorStakes[_account].stakedCourtIDs; while (courtIDsForJuror.length > 0) { uint96 courtID = courtIDsForJuror[0]; - _setStakeBySystem(_account, courtID, 0); + _setStake(_account, courtID, 0); } jurorStakes[_account].totalStake = 0; pnkToWithdraw = vault.getAvailableBalance(_account); @@ -256,14 +247,14 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr /// @inheritdoc IStakeController function createTree(bytes32 _key, bytes memory _extraData) external override onlyByCore { - sortitionModule.createTree(_key, _extraData); + sortition.createTree(_key, _extraData); } /// @inheritdoc IStakeController function draw(bytes32 _court, uint256 _coreDisputeID, uint256 _nonce) external view override returns (address) { if (phase != Phase.drawing) revert NotDrawingPhase(); if (randomNumber == 0) revert RandomNumberNotReadyYet(); - return sortitionModule.draw(_court, _coreDisputeID, _nonce, randomNumber); + return sortition.draw(_court, _coreDisputeID, _nonce, randomNumber); } /// @inheritdoc IStakeController @@ -329,14 +320,14 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr // * Internal * // // ************************************* // - /// @dev Internal implementation of setStakeBySystem + /// @dev Internal implementation of setStake with phase-aware delayed stake logic /// @param _account The account to set the stake for. /// @param _courtID The ID of the court to set the stake for. /// @param _newStake The new stake. /// @return pnkDeposit The amount of PNK to deposit. /// @return pnkWithdrawal The amount of PNK to withdraw. /// @return stakingResult The result of the staking operation. - function _setStakeBySystem( + function _setStake( address _account, uint96 _courtID, uint256 _newStake @@ -344,56 +335,39 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr JurorStake storage currentJurorStake = jurorStakes[_account]; uint256 currentStakeInCourt = currentJurorStake.stakes[_courtID]; - if (currentStakeInCourt == 0) { - if (_newStake == 0) - // No change needed - return (0, 0, StakingResult.CannotStakeZeroWhenNoStake); - else if (_newStake > 0 && currentJurorStake.stakedCourtIDs.length >= MAX_STAKE_PATHS) - // Cannot stake in more courts - return (0, 0, StakingResult.CannotStakeInMoreCourts); - } - - currentJurorStake.stakes[_courtID] = _newStake; + if (phase == Phase.staking) { + if (currentStakeInCourt == 0) { + if (_newStake == 0) + // No change needed + return (0, 0, StakingResult.CannotStakeZeroWhenNoStake); + else if (_newStake > 0 && currentJurorStake.stakedCourtIDs.length >= MAX_STAKE_PATHS) + // Cannot stake in more courts + return (0, 0, StakingResult.CannotStakeInMoreCourts); + } - if (_newStake > currentStakeInCourt) { - pnkDeposit = _newStake - currentStakeInCourt; - currentJurorStake.totalStake += pnkDeposit; - } else if (_newStake < currentStakeInCourt) { - pnkWithdrawal = currentStakeInCourt - _newStake; - currentJurorStake.totalStake -= pnkWithdrawal; - } + currentJurorStake.stakes[_courtID] = _newStake; - // Manage stakedCourtIDs - if (currentStakeInCourt == 0 && _newStake > 0) { - currentJurorStake.stakedCourtIDs.push(_courtID); - } else if (currentStakeInCourt > 0 && _newStake == 0) { - _removeCourt(currentJurorStake.stakedCourtIDs, _courtID); - } + if (_newStake > currentStakeInCourt) { + pnkDeposit = _newStake - currentStakeInCourt; + currentJurorStake.totalStake += pnkDeposit; + } else if (_newStake < currentStakeInCourt) { + pnkWithdrawal = currentStakeInCourt - _newStake; + currentJurorStake.totalStake -= pnkWithdrawal; + } - sortitionModule.setStake(_account, _courtID, _newStake); + // Manage stakedCourtIDs + if (currentStakeInCourt == 0 && _newStake > 0) { + currentJurorStake.stakedCourtIDs.push(_courtID); + } else if (currentStakeInCourt > 0 && _newStake == 0) { + _removeCourt(currentJurorStake.stakedCourtIDs, _courtID); + } - emit StakeSet(_account, _courtID, _newStake, currentJurorStake.totalStake); - } + sortition.setStake(_account, _courtID, _newStake); - /// @dev Internal implementation of setStake with phase-aware delayed stake logic - /// @param _account The account to set the stake for. - /// @param _courtID The ID of the court to set the stake for. - /// @param _newStake The new stake. - /// @return pnkDeposit The amount of PNK to deposit. - /// @return pnkWithdrawal The amount of PNK to withdraw. - /// @return stakingResult The result of the staking operation. - function _setStake( - address _account, - uint96 _courtID, - uint256 _newStake - ) internal virtual returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { - if (phase == Phase.staking) { - return _setStakeBySystem(_account, _courtID, _newStake); + emit StakeSet(_account, _courtID, _newStake, currentJurorStake.totalStake); + return (pnkDeposit, pnkWithdrawal, StakingResult.Successful); } - JurorStake storage currentJurorStake = jurorStakes[_account]; - uint256 currentStakeInCourt = currentJurorStake.stakes[_courtID]; - if (_newStake > currentStakeInCourt) { pnkDeposit = _newStake - currentStakeInCourt; } else if (_newStake < currentStakeInCourt) { @@ -470,9 +444,9 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr } // _setStakeBySystem will update local juror stake mappings (jurorStakes) - // AND call sortitionModule.setStake. + // AND call sortition.setStake. // The pnkDeposit/pnkWithdrawal are calculated but not used by this import function. - (, , StakingResult stakingResult) = _setStakeBySystem(account, courtID, stakeToImport); + (, , StakingResult stakingResult) = _setStake(account, courtID, stakeToImport); if (stakingResult == StakingResult.Successful) { totalImportedSuccess++; diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol index 1d2794b75..32d4c0219 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol @@ -207,7 +207,7 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi emit DisputeCreation(_coreDisputeID, _numberOfChoices, _extraData); } - /// @dev Draws the juror from the sortition tree. The drawn address is picked up by Kleros Core. + /// @dev Draws the juror from the Stake Controller. /// Note: Access restricted to Kleros Core only. /// @param _coreDisputeID The ID of the dispute in Kleros Core. /// @param _nonce Nonce of the drawing iteration. @@ -221,11 +221,11 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi uint256 localRoundID = dispute.rounds.length - 1; Round storage round = dispute.rounds[localRoundID]; - IStakeController sortitionModule = core.stakeController(); + IStakeController stakeController = core.stakeController(); (uint96 courtID, , , , ) = core.disputes(_coreDisputeID); bytes32 key = bytes32(uint256(courtID)); // Get the ID of the tree. - drawnAddress = sortitionModule.draw(key, _coreDisputeID, _nonce); + drawnAddress = stakeController.draw(key, _coreDisputeID, _nonce); if (_postDrawCheck(round, _coreDisputeID, drawnAddress)) { round.votes.push(Vote({account: drawnAddress, commit: bytes32(0), choice: 0, voted: false})); diff --git a/contracts/src/arbitration/interfaces/IStakeController.sol b/contracts/src/arbitration/interfaces/IStakeController.sol index f2ac53f16..af34f8e89 100644 --- a/contracts/src/arbitration/interfaces/IStakeController.sol +++ b/contracts/src/arbitration/interfaces/IStakeController.sol @@ -6,7 +6,7 @@ import {KlerosCoreXBase} from "../KlerosCoreXBase.sol"; import "../../libraries/Constants.sol"; /// @title IStakeController -/// @notice Interface for the Stake Controller that coordinates between Vault and SortitionModule +/// @notice Interface for the Stake Controller that coordinates between Vault and SortitionSumTree /// @dev Combines phase management, delayed stakes, and coordination between vault and sortition interface IStakeController { // ************************************* // @@ -62,20 +62,6 @@ interface IStakeController { // * Stake Management * // // ************************************* // - /// @notice System-level update to a juror's stake directly in the SortitionModule. - /// @dev Called by KlerosCoreXBase for executing delayed stakes. Skips regular phase checks and delayed stake creation. - /// @param _account The juror's account - /// @param _courtID The ID of the court - /// @param _newStake The new stake amount - /// @return pnkDeposit The amount of PNK to deposit - /// @return pnkWithdrawal The amount of PNK to withdraw - /// @return stakingResult The result of the staking operation - function setStakeBySystem( - address _account, - uint96 _courtID, - uint256 _newStake - ) external returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult); - /// @notice Set stake for a juror with vault coordination /// @param _account The juror's account /// @param _courtID The ID of the court @@ -128,12 +114,12 @@ interface IStakeController { // * Sortition Delegation * // // ************************************* // - /// @notice Create a sortition tree (delegated to SortitionModule) + /// @notice Create a sortition tree (delegated to SortitionSumTree) /// @param _key The key of the tree /// @param _extraData Extra data for tree configuration function createTree(bytes32 _key, bytes memory _extraData) external; - /// @notice Draw a juror for dispute (delegated to SortitionModule) + /// @notice Draw a juror for dispute (delegated to SortitionSumTree) /// @param _court The court identifier /// @param _coreDisputeID The core dispute ID /// @param _nonce The drawing nonce From 0cf0ec46d1ac3302e5e55ba7b96a9831bb1201de Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Mon, 9 Jun 2025 13:50:33 +0100 Subject: [PATCH 14/24] refactor: replaced Core._setStakeBySystem by ._setStake Neo NFT checking moved from CoreNeo to VaultNeo --- contracts/src/arbitration/KlerosCoreXBase.sol | 48 ++----------------- contracts/src/arbitration/KlerosCoreXNeo.sol | 32 ++----------- .../src/arbitration/StakeControllerBase.sol | 2 +- contracts/src/arbitration/VaultNeo.sol | 4 +- 4 files changed, 10 insertions(+), 76 deletions(-) diff --git a/contracts/src/arbitration/KlerosCoreXBase.sol b/contracts/src/arbitration/KlerosCoreXBase.sol index df5fe5de2..df72e4a7e 100644 --- a/contracts/src/arbitration/KlerosCoreXBase.sol +++ b/contracts/src/arbitration/KlerosCoreXBase.sol @@ -474,12 +474,13 @@ abstract contract KlerosCoreXBase is IArbitratorV2, Initializable, UUPSProxiable /// @param _courtID The ID of the court. /// @param _newStake The new stake amount for the juror in the court. /// @return success Whether the stake was successfully set or not. - function setStakeBySystem( + function setStakeByController( address _account, uint96 _courtID, uint256 _newStake ) external onlyStakeController returns (bool success) { - return _setStakeBySystem(_account, _courtID, _newStake); + // TODO: use TRY/CATCH ?? then delete _stakingFailed() + return _setStake(_account, _courtID, _newStake, OnError.Return); } /// @inheritdoc IArbitratorV2 @@ -1121,47 +1122,6 @@ abstract contract KlerosCoreXBase is IArbitratorV2, Initializable, UUPSProxiable return true; } - /// @dev Internal implementation of setStakeBySystem - /// @param _account The account to set the stake for. - /// @param _courtID The ID of the court to set the stake for. - /// @param _newStake The new stake. - /// @return success Whether the stake was successfully set or not. - function _setStakeBySystem( - address _account, - uint96 _courtID, - uint256 _newStake - ) internal virtual returns (bool success) { - (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) = stakeController.setStake( - _account, - _courtID, - _newStake - ); - OnError onError = OnError.Return; - if (stakingResult != StakingResult.Successful) { - _stakingFailed(onError, stakingResult); - return false; - } - if (pnkDeposit > 0) { - try vault.deposit(_account, pnkDeposit) { - // Successfully deposited PNK - } catch { - // Revert with a specific error or reuse existing one - _stakingFailed(onError, StakingResult.StakingTransferFailed); // Indicating failure in the deposit part of staking - return false; - } - } - if (pnkWithdrawal > 0) { - try vault.withdraw(_account, pnkWithdrawal) { - // Successfully withdrew PNK via Vault - } catch { - // Revert with a specific error or reuse existing one - _stakingFailed(onError, StakingResult.UnstakingTransferFailed); // Indicating failure in the withdrawal part of unstaking - return false; - } - } - return true; - } - /// @dev It may revert depending on the _onError parameter. function _stakingFailed(OnError _onError, StakingResult _result) internal pure virtual { if (_onError == OnError.Return) return; @@ -1221,6 +1181,7 @@ abstract contract KlerosCoreXBase is IArbitratorV2, Initializable, UUPSProxiable error InvalidForkingCourtAsParent(); error WrongDisputeKitIndex(); error CannotDisableClassicDK(); + error StakingZeroWhenNoStake(); error StakingInTooManyCourts(); error StakingNotPossibleInThisCourt(); error StakingLessThanCourtMinStake(); @@ -1244,5 +1205,4 @@ abstract contract KlerosCoreXBase is IArbitratorV2, Initializable, UUPSProxiable error TransferFailed(); error WhenNotPausedOnly(); error WhenPausedOnly(); - error StakingZeroWhenNoStake(); } diff --git a/contracts/src/arbitration/KlerosCoreXNeo.sol b/contracts/src/arbitration/KlerosCoreXNeo.sol index f9d42b626..f4c7aa090 100644 --- a/contracts/src/arbitration/KlerosCoreXNeo.sol +++ b/contracts/src/arbitration/KlerosCoreXNeo.sol @@ -6,9 +6,8 @@ import "./KlerosCoreXBase.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; /// @title KlerosCoreXNeo -/// @notice KlerosCore implementation with new StakeController architecture for production environments +/// @notice KlerosCore with whitelisted arbitrables contract KlerosCoreXNeo is KlerosCoreXBase { - /// @notice Version of the implementation contract string public constant override version = "0.0.1"; // ************************************* // @@ -16,7 +15,6 @@ contract KlerosCoreXNeo is KlerosCoreXBase { // ************************************* // mapping(address => bool) public arbitrableWhitelist; // Arbitrable whitelist. - IERC721 public jurorNft; // Eligible jurors NFT. // ************************************* // // * Constructor * // @@ -37,7 +35,6 @@ contract KlerosCoreXNeo is KlerosCoreXBase { /// @param _timesPerPeriod The timesPerPeriod array for courts /// @param _sortitionExtraData Extra data for sortition module setup /// @param _stakeController The stake controller for coordination - /// @param _jurorNft NFT contract to vet the jurors function initialize( address _governor, address _guardian, @@ -48,8 +45,7 @@ contract KlerosCoreXNeo is KlerosCoreXBase { uint256[4] memory _timesPerPeriod, bytes memory _sortitionExtraData, IStakeController _stakeController, - IVault _vault, - IERC721 _jurorNft + IVault _vault ) external reinitializer(2) { __KlerosCoreXBase_initialize( _governor, @@ -63,7 +59,6 @@ contract KlerosCoreXNeo is KlerosCoreXBase { _stakeController, _vault ); - jurorNft = _jurorNft; } function initialize5() external reinitializer(5) { @@ -77,13 +72,7 @@ contract KlerosCoreXNeo is KlerosCoreXBase { /// @notice Access Control to perform implementation upgrades (UUPS Proxiable) /// Only the governor can perform upgrades (`onlyByGovernor`) function _authorizeUpgrade(address) internal view override onlyByGovernor { - // Empty block: access control implemented by `onlyByGovernor` modifier - } - - /// @dev Changes the `jurorNft` storage variable. - /// @param _jurorNft The new value for the `jurorNft` storage variable. - function changeJurorNft(IERC721 _jurorNft) external onlyByGovernor { - jurorNft = _jurorNft; + // NOP } /// @dev Adds or removes an arbitrable from whitelist. @@ -93,20 +82,6 @@ contract KlerosCoreXNeo is KlerosCoreXBase { arbitrableWhitelist[_arbitrable] = _allowed; } - // ************************************* // - // * State Modifiers * // - // ************************************* // - - /// @dev Sets the caller's stake in a court. - /// Note: Staking and unstaking is forbidden during pause. - /// @param _courtID The ID of the court. - /// @param _newStake The new stake. - /// Note that the existing delayed stake will be nullified as non-relevant. - function setStake(uint96 _courtID, uint256 _newStake) external override whenNotPaused { - if (jurorNft.balanceOf(msg.sender) == 0) revert NotEligibleForStaking(); - _setStake(msg.sender, _courtID, _newStake, OnError.Revert); - } - // ************************************* // // * Internal * // // ************************************* // @@ -131,7 +106,6 @@ contract KlerosCoreXNeo is KlerosCoreXBase { // * Errors * // // ************************************* // - error NotEligibleForStaking(); error StakingMoreThanMaxStakePerJuror(); error StakingMoreThanMaxTotalStaked(); error ArbitrableNotWhitelisted(); diff --git a/contracts/src/arbitration/StakeControllerBase.sol b/contracts/src/arbitration/StakeControllerBase.sol index 4d0c825bf..d2ae8c311 100644 --- a/contracts/src/arbitration/StakeControllerBase.sol +++ b/contracts/src/arbitration/StakeControllerBase.sol @@ -188,7 +188,7 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr if (delayedStake.account == address(0)) continue; // Let KlerosCore coordinate stake update and vault deposit/withdrawal. - core.setStakeBySystem(delayedStake.account, delayedStake.courtID, delayedStake.stake); + core.setStakeByController(delayedStake.account, delayedStake.courtID, delayedStake.stake); delete delayedStakes[i]; } delayedStakeReadIndex = newDelayedStakeReadIndex; diff --git a/contracts/src/arbitration/VaultNeo.sol b/contracts/src/arbitration/VaultNeo.sol index 02239ea33..a68b34491 100644 --- a/contracts/src/arbitration/VaultNeo.sol +++ b/contracts/src/arbitration/VaultNeo.sol @@ -103,7 +103,7 @@ contract VaultNeo is VaultBase { function _deposit(address _from, uint256 _amount) internal override { // Check NFT requirement if set if (address(depositNft) != address(0) && depositNft.balanceOf(_from) == 0) { - revert DepositNftRequired(); + revert NotEligible(); } // Check per-user deposit limit @@ -135,7 +135,7 @@ contract VaultNeo is VaultBase { // * Errors * // // ************************************* // - error DepositNftRequired(); + error NotEligible(); error ExceedsMaxDepositPerUser(); error ExceedsTotalDepositCap(); } From 4c8db66fa51d712d96c1ad2c1462e14302ac2b25 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Mon, 9 Jun 2025 19:11:33 +0100 Subject: [PATCH 15/24] refactor: failed delayed stake handling now relies on a try/catch block rather a returned error enum --- contracts/src/arbitration/KlerosCoreXBase.sol | 47 +++++-------------- contracts/src/arbitration/KlerosCoreXNeo.sol | 8 ---- .../src/arbitration/StakeControllerBase.sol | 19 ++++---- .../interfaces/IStakeController.sol | 4 ++ contracts/src/libraries/Constants.sol | 17 +++---- 5 files changed, 34 insertions(+), 61 deletions(-) diff --git a/contracts/src/arbitration/KlerosCoreXBase.sol b/contracts/src/arbitration/KlerosCoreXBase.sol index df72e4a7e..1c1304268 100644 --- a/contracts/src/arbitration/KlerosCoreXBase.sol +++ b/contracts/src/arbitration/KlerosCoreXBase.sol @@ -465,7 +465,7 @@ abstract contract KlerosCoreXBase is IArbitratorV2, Initializable, UUPSProxiable /// @param _newStake The new stake. /// Note that the existing delayed stake will be nullified as non-relevant. function setStake(uint96 _courtID, uint256 _newStake) external virtual whenNotPaused { - _setStake(msg.sender, _courtID, _newStake, OnError.Revert); + _setStake(msg.sender, _courtID, _newStake); } /// @notice Executes a stake change initiated by the system (e.g., processing a delayed stake). @@ -479,8 +479,7 @@ abstract contract KlerosCoreXBase is IArbitratorV2, Initializable, UUPSProxiable uint96 _courtID, uint256 _newStake ) external onlyStakeController returns (bool success) { - // TODO: use TRY/CATCH ?? then delete _stakingFailed() - return _setStake(_account, _courtID, _newStake, OnError.Return); + return _setStake(_account, _courtID, _newStake); } /// @inheritdoc IArbitratorV2 @@ -1078,16 +1077,13 @@ abstract contract KlerosCoreXBase is IArbitratorV2, Initializable, UUPSProxiable /// @param _account The account to set the stake for. /// @param _courtID The ID of the court to set the stake for. /// @param _newStake The new stake. - /// @param _onError Whether to revert or return false on error. - /// @return Whether the stake was successfully set or not. - function _setStake(address _account, uint96 _courtID, uint256 _newStake, OnError _onError) internal returns (bool) { + /// @return success Whether the stake was successfully set or not. + function _setStake(address _account, uint96 _courtID, uint256 _newStake) internal returns (bool success) { if (_courtID == FORKING_COURT || _courtID >= courts.length) { - _stakingFailed(_onError, StakingResult.CannotStakeInThisCourt); // Staking directly into the forking court is not allowed. - return false; + revert StakingNotPossibleInThisCourt(); } if (_newStake != 0 && _newStake < courts[_courtID].minStake) { - _stakingFailed(_onError, StakingResult.CannotStakeLessThanMinStake); // Staking less than the minimum stake is not allowed. - return false; + revert StakingLessThanCourtMinStake(); } (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) = stakeController.setStake( _account, @@ -1097,40 +1093,21 @@ abstract contract KlerosCoreXBase is IArbitratorV2, Initializable, UUPSProxiable if (stakingResult == StakingResult.Delayed) { return true; } - if (stakingResult != StakingResult.Successful) { - _stakingFailed(_onError, stakingResult); - return false; - } if (pnkDeposit > 0) { try vault.deposit(_account, pnkDeposit) { - // Successfully deposited PNK + success = true; } catch { - // Revert with a specific error or reuse existing one - _stakingFailed(_onError, StakingResult.StakingTransferFailed); // Indicating failure in the deposit part of staking - return false; + success = false; } } if (pnkWithdrawal > 0) { try vault.withdraw(_account, pnkWithdrawal) { - // Successfully withdrew PNK via Vault + success = true; } catch { - // Revert with a specific error or reuse existing one - _stakingFailed(_onError, StakingResult.UnstakingTransferFailed); // Indicating failure in the withdrawal part of unstaking - return false; + success = false; } } - return true; - } - - /// @dev It may revert depending on the _onError parameter. - function _stakingFailed(OnError _onError, StakingResult _result) internal pure virtual { - if (_onError == OnError.Return) return; - if (_result == StakingResult.StakingTransferFailed) revert StakingTransferFailed(); - if (_result == StakingResult.UnstakingTransferFailed) revert UnstakingTransferFailed(); - if (_result == StakingResult.CannotStakeInMoreCourts) revert StakingInTooManyCourts(); - if (_result == StakingResult.CannotStakeInThisCourt) revert StakingNotPossibleInThisCourt(); - if (_result == StakingResult.CannotStakeLessThanMinStake) revert StakingLessThanCourtMinStake(); - if (_result == StakingResult.CannotStakeZeroWhenNoStake) revert StakingZeroWhenNoStake(); + return success; } /// @dev Gets a court ID, the minimum number of jurors and an ID of a dispute kit from a specified extra data bytes array. @@ -1181,8 +1158,6 @@ abstract contract KlerosCoreXBase is IArbitratorV2, Initializable, UUPSProxiable error InvalidForkingCourtAsParent(); error WrongDisputeKitIndex(); error CannotDisableClassicDK(); - error StakingZeroWhenNoStake(); - error StakingInTooManyCourts(); error StakingNotPossibleInThisCourt(); error StakingLessThanCourtMinStake(); error StakingTransferFailed(); diff --git a/contracts/src/arbitration/KlerosCoreXNeo.sol b/contracts/src/arbitration/KlerosCoreXNeo.sol index f4c7aa090..120e17a1c 100644 --- a/contracts/src/arbitration/KlerosCoreXNeo.sol +++ b/contracts/src/arbitration/KlerosCoreXNeo.sol @@ -96,17 +96,9 @@ contract KlerosCoreXNeo is KlerosCoreXBase { return super._createDispute(_numberOfChoices, _extraData, _feeToken, _feeAmount); } - function _stakingFailed(OnError _onError, StakingResult _result) internal pure override { - super._stakingFailed(_onError, _result); - if (_result == StakingResult.CannotStakeMoreThanMaxStakePerJuror) revert StakingMoreThanMaxStakePerJuror(); - if (_result == StakingResult.CannotStakeMoreThanMaxTotalStaked) revert StakingMoreThanMaxTotalStaked(); - } - // ************************************* // // * Errors * // // ************************************* // - error StakingMoreThanMaxStakePerJuror(); - error StakingMoreThanMaxTotalStaked(); error ArbitrableNotWhitelisted(); } diff --git a/contracts/src/arbitration/StakeControllerBase.sol b/contracts/src/arbitration/StakeControllerBase.sol index d2ae8c311..c281700b3 100644 --- a/contracts/src/arbitration/StakeControllerBase.sol +++ b/contracts/src/arbitration/StakeControllerBase.sol @@ -188,7 +188,11 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr if (delayedStake.account == address(0)) continue; // Let KlerosCore coordinate stake update and vault deposit/withdrawal. - core.setStakeByController(delayedStake.account, delayedStake.courtID, delayedStake.stake); + try core.setStakeByController(delayedStake.account, delayedStake.courtID, delayedStake.stake) { + // NOP + } catch (bytes memory data) { + emit DelayedStakeSetFailed(data); + } delete delayedStakes[i]; } delayedStakeReadIndex = newDelayedStakeReadIndex; @@ -337,12 +341,9 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr if (phase == Phase.staking) { if (currentStakeInCourt == 0) { - if (_newStake == 0) - // No change needed - return (0, 0, StakingResult.CannotStakeZeroWhenNoStake); + if (_newStake == 0) revert StakingZeroWhenNoStake(); else if (_newStake > 0 && currentJurorStake.stakedCourtIDs.length >= MAX_STAKE_PATHS) - // Cannot stake in more courts - return (0, 0, StakingResult.CannotStakeInMoreCourts); + revert StakingInTooManyCourts(); } currentJurorStake.stakes[_courtID] = _newStake; @@ -374,7 +375,6 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr pnkWithdrawal = currentStakeInCourt - _newStake; } - // MAX_STAKE_PATHS is checked by _setStakeBySystem() called during executeDelayedStakes(). DelayedStake storage delayedStake = delayedStakes[++delayedStakeWriteIndex]; delayedStake.account = _account; delayedStake.courtID = _courtID; @@ -443,8 +443,7 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr continue; } - // _setStakeBySystem will update local juror stake mappings (jurorStakes) - // AND call sortition.setStake. + // _setStake will update local juror stake mappings (jurorStakes) AND call sortition.setStake. // The pnkDeposit/pnkWithdrawal are calculated but not used by this import function. (, , StakingResult stakingResult) = _setStake(account, courtID, stakeToImport); @@ -517,6 +516,8 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr error RandomNumberNotReady(); error RandomNumberNotReadyYet(); error StillDrawingDisputes(); + error StakingZeroWhenNoStake(); + error StakingInTooManyCourts(); error NotInStakingPhase(); error NotDrawingPhase(); error NoDelayedStakes(); diff --git a/contracts/src/arbitration/interfaces/IStakeController.sol b/contracts/src/arbitration/interfaces/IStakeController.sol index af34f8e89..970debf64 100644 --- a/contracts/src/arbitration/interfaces/IStakeController.sol +++ b/contracts/src/arbitration/interfaces/IStakeController.sol @@ -36,6 +36,10 @@ interface IStakeController { /// @param _totalStake The amount of tokens staked in all courts event StakeSet(address indexed _account, uint96 indexed _courtID, uint256 _stakeInCourt, uint256 _totalStake); + /// @notice Emitted when a delayed stake execution fails + /// @param _data The data of the error defined as `abi.encodeWithSelector(CustomError.selector, /*args…*/ )` + event DelayedStakeSetFailed(bytes _data); + // Migration events event StakeImported(address indexed _juror, uint96 indexed _courtID, uint256 _stake); event DelayedStakeImported(address indexed _juror, uint96 indexed _courtID, uint256 _stake, uint256 _index); diff --git a/contracts/src/libraries/Constants.sol b/contracts/src/libraries/Constants.sol index f2101461b..cbf37d4f6 100644 --- a/contracts/src/libraries/Constants.sol +++ b/contracts/src/libraries/Constants.sol @@ -20,6 +20,7 @@ uint256 constant DEFAULT_K = 6; // Default number of children per node. uint256 constant DEFAULT_NB_OF_JURORS = 3; // The default number of jurors in a dispute. IERC20 constant NATIVE_CURRENCY = IERC20(address(0)); // The native currency, such as ETH on Arbitrum, Optimism and Ethereum L1. +// DEPRECATED enum OnError { Revert, Return @@ -28,12 +29,12 @@ enum OnError { enum StakingResult { Successful, Delayed, - StakingTransferFailed, - UnstakingTransferFailed, - CannotStakeInMoreCourts, - CannotStakeInThisCourt, - CannotStakeLessThanMinStake, - CannotStakeMoreThanMaxStakePerJuror, - CannotStakeMoreThanMaxTotalStaked, - CannotStakeZeroWhenNoStake + StakingTransferFailed, // DEPRECATED + UnstakingTransferFailed, // DEPRECATED + CannotStakeInMoreCourts, // DEPRECATED + CannotStakeInThisCourt, // DEPRECATED + CannotStakeLessThanMinStake, // DEPRECATED + CannotStakeMoreThanMaxStakePerJuror, // DEPRECATED + CannotStakeMoreThanMaxTotalStaked, // DEPRECATED + CannotStakeZeroWhenNoStake // DEPRECATED } From 8a5fbbda437884c69fdb806bd6912ffcec8e2988 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Mon, 9 Jun 2025 22:04:33 +0100 Subject: [PATCH 16/24] feat: do not update StakeController state if PNK transfer fails --- contracts/src/arbitration/KlerosCoreXBase.sol | 14 +- .../src/arbitration/StakeControllerBase.sol | 245 ++++++------------ .../interfaces/IStakeController.sol | 32 ++- 3 files changed, 115 insertions(+), 176 deletions(-) diff --git a/contracts/src/arbitration/KlerosCoreXBase.sol b/contracts/src/arbitration/KlerosCoreXBase.sol index 1c1304268..187e97f7a 100644 --- a/contracts/src/arbitration/KlerosCoreXBase.sol +++ b/contracts/src/arbitration/KlerosCoreXBase.sol @@ -1085,7 +1085,7 @@ abstract contract KlerosCoreXBase is IArbitratorV2, Initializable, UUPSProxiable if (_newStake != 0 && _newStake < courts[_courtID].minStake) { revert StakingLessThanCourtMinStake(); } - (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) = stakeController.setStake( + (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) = stakeController.validateStake( _account, _courtID, _newStake @@ -1093,20 +1093,20 @@ abstract contract KlerosCoreXBase is IArbitratorV2, Initializable, UUPSProxiable if (stakingResult == StakingResult.Delayed) { return true; } + success = true; if (pnkDeposit > 0) { - try vault.deposit(_account, pnkDeposit) { - success = true; - } catch { + try vault.deposit(_account, pnkDeposit) {} catch { success = false; } } if (pnkWithdrawal > 0) { - try vault.withdraw(_account, pnkWithdrawal) { - success = true; - } catch { + try vault.withdraw(_account, pnkWithdrawal) {} catch { success = false; } } + if (success) { + stakeController.setStake(_account, _courtID, _newStake, pnkDeposit, pnkWithdrawal); + } return success; } diff --git a/contracts/src/arbitration/StakeControllerBase.sol b/contracts/src/arbitration/StakeControllerBase.sol index c281700b3..76fc24454 100644 --- a/contracts/src/arbitration/StakeControllerBase.sol +++ b/contracts/src/arbitration/StakeControllerBase.sol @@ -203,12 +203,74 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr // ************************************* // /// @inheritdoc IStakeController - function setStake( + function validateStake( address _account, uint96 _courtID, uint256 _newStake - ) public override onlyByCore returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { - return _setStake(_account, _courtID, _newStake); + ) external view override returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { + JurorStake storage currentJurorStake = jurorStakes[_account]; + uint256 currentStakeInCourt = currentJurorStake.stakes[_courtID]; + + if (_newStake > currentStakeInCourt) { + pnkDeposit = _newStake - currentStakeInCourt; + } else if (_newStake < currentStakeInCourt) { + pnkWithdrawal = currentStakeInCourt - _newStake; + } + + if (phase != Phase.staking) { + return (pnkDeposit, pnkWithdrawal, StakingResult.Delayed); + } else { + if (currentStakeInCourt == 0) { + if (_newStake == 0) revert StakingZeroWhenNoStake(); + else if (_newStake > 0 && currentJurorStake.stakedCourtIDs.length >= MAX_STAKE_PATHS) + revert StakingInTooManyCourts(); + } + return (pnkDeposit, pnkWithdrawal, StakingResult.Successful); + } + } + + /// @inheritdoc IStakeController + function setStake( + address _account, + uint96 _courtID, + uint256 _newStake, + uint256 _pnkDeposit, + uint256 _pnkWithdrawal + ) public override onlyByCore { + JurorStake storage currentJurorStake = jurorStakes[_account]; + uint256 currentStakeInCourt = currentJurorStake.stakes[_courtID]; + + if (phase != Phase.staking) { + revert NotInStakingPhase(); + } + + // Update jurorStakes + currentJurorStake.stakes[_courtID] = _newStake; + if (_newStake > currentStakeInCourt) { + currentJurorStake.totalStake += _pnkDeposit; + } else if (_newStake < currentStakeInCourt) { + currentJurorStake.totalStake -= _pnkWithdrawal; + } + + // Manage stakedCourtIDs + if (currentStakeInCourt == 0 && _newStake > 0) { + currentJurorStake.stakedCourtIDs.push(_courtID); + } else if (currentStakeInCourt > 0 && _newStake == 0) { + _removeCourt(currentJurorStake.stakedCourtIDs, _courtID); + } + + // Update sortition tree + sortition.setStake(_account, _courtID, _newStake); + + emit StakeSet(_account, _courtID, _newStake, currentJurorStake.totalStake); + } + + /// @inheritdoc IStakeController + function setStakeDelayed(address _account, uint96 _courtID, uint256 _newStake) public override { + DelayedStake storage delayedStake = delayedStakes[++delayedStakeWriteIndex]; + delayedStake.account = _account; + delayedStake.courtID = _courtID; + delayedStake.stake = _newStake; } /// @inheritdoc IStakeController @@ -235,14 +297,24 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr /// @inheritdoc IStakeController function setJurorInactive(address _account) external override onlyByCore returns (uint256 pnkToWithdraw) { - uint96[] storage courtIDsForJuror = jurorStakes[_account].stakedCourtIDs; - while (courtIDsForJuror.length > 0) { - uint96 courtID = courtIDsForJuror[0]; - _setStake(_account, courtID, 0); + JurorStake storage currentJurorStake = jurorStakes[_account]; + uint96[] storage stakedCourtIDs = currentJurorStake.stakedCourtIDs; + while (stakedCourtIDs.length > 0) { + uint96 courtID = stakedCourtIDs[0]; + uint256 currentStakeInCourt = currentJurorStake.stakes[courtID]; + if (phase == Phase.staking) { + setStake(_account, courtID, 0, 0, currentStakeInCourt); + } else { + setStakeDelayed(_account, courtID, 0); + } + } + if (phase == Phase.staking) { + pnkToWithdraw = vault.getAvailableBalance(_account); + emit JurorSetInactive(_account, false); + } else { + pnkToWithdraw = 0; + emit JurorSetInactive(_account, true); } - jurorStakes[_account].totalStake = 0; - pnkToWithdraw = vault.getAvailableBalance(_account); - emit JurorSetInactive(_account); } // ************************************* // @@ -324,64 +396,6 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr // * Internal * // // ************************************* // - /// @dev Internal implementation of setStake with phase-aware delayed stake logic - /// @param _account The account to set the stake for. - /// @param _courtID The ID of the court to set the stake for. - /// @param _newStake The new stake. - /// @return pnkDeposit The amount of PNK to deposit. - /// @return pnkWithdrawal The amount of PNK to withdraw. - /// @return stakingResult The result of the staking operation. - function _setStake( - address _account, - uint96 _courtID, - uint256 _newStake - ) internal virtual returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { - JurorStake storage currentJurorStake = jurorStakes[_account]; - uint256 currentStakeInCourt = currentJurorStake.stakes[_courtID]; - - if (phase == Phase.staking) { - if (currentStakeInCourt == 0) { - if (_newStake == 0) revert StakingZeroWhenNoStake(); - else if (_newStake > 0 && currentJurorStake.stakedCourtIDs.length >= MAX_STAKE_PATHS) - revert StakingInTooManyCourts(); - } - - currentJurorStake.stakes[_courtID] = _newStake; - - if (_newStake > currentStakeInCourt) { - pnkDeposit = _newStake - currentStakeInCourt; - currentJurorStake.totalStake += pnkDeposit; - } else if (_newStake < currentStakeInCourt) { - pnkWithdrawal = currentStakeInCourt - _newStake; - currentJurorStake.totalStake -= pnkWithdrawal; - } - - // Manage stakedCourtIDs - if (currentStakeInCourt == 0 && _newStake > 0) { - currentJurorStake.stakedCourtIDs.push(_courtID); - } else if (currentStakeInCourt > 0 && _newStake == 0) { - _removeCourt(currentJurorStake.stakedCourtIDs, _courtID); - } - - sortition.setStake(_account, _courtID, _newStake); - - emit StakeSet(_account, _courtID, _newStake, currentJurorStake.totalStake); - return (pnkDeposit, pnkWithdrawal, StakingResult.Successful); - } - - if (_newStake > currentStakeInCourt) { - pnkDeposit = _newStake - currentStakeInCourt; - } else if (_newStake < currentStakeInCourt) { - pnkWithdrawal = currentStakeInCourt - _newStake; - } - - DelayedStake storage delayedStake = delayedStakes[++delayedStakeWriteIndex]; - delayedStake.account = _account; - delayedStake.courtID = _courtID; - delayedStake.stake = _newStake; - return (pnkDeposit, pnkWithdrawal, StakingResult.Delayed); - } - /// @dev Removes a court from a juror's list of staked courts. /// @param _stakedCourts Storage pointer to the juror's array of staked court IDs. /// @param _courtID The ID of the court to remove. @@ -410,101 +424,6 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr } } - // ************************************* // - // * Migration Utilities * // - // ************************************* // - - /// @dev Import existing stakes from old sortition module for migration - /// @param _accounts Array of juror accounts - /// @param _courtIDs Array of court IDs - /// @param _stakes Array of stake amounts - function importExistingStakes( - address[] calldata _accounts, - uint96[] calldata _courtIDs, - uint256[] calldata _stakes - ) external onlyByGovernor { - if (_accounts.length != _courtIDs.length || _accounts.length != _stakes.length) { - revert InvalidMigrationData(); - } - - uint256 totalImportedSuccess = 0; - for (uint256 i = 0; i < _accounts.length; i++) { - if (_stakes[i] > 0) { - address account = _accounts[i]; - uint96 courtID = _courtIDs[i]; - uint256 stakeToImport = _stakes[i]; - - // Ensure no prior stake exists for this specific account/courtID combination in this contract's state for a clean import. - // This check assumes importExistingStakes is for a fresh population or controlled append. - // If overwriting/updating was intended, this check might differ. - if (jurorStakes[account].stakes[courtID] > 0) { - // Skip or revert, depending on desired import semantics. For now, skip and log. - // emit ImportSkippedDuplicate(account, courtID, stakeToImport); - continue; - } - - // _setStake will update local juror stake mappings (jurorStakes) AND call sortition.setStake. - // The pnkDeposit/pnkWithdrawal are calculated but not used by this import function. - (, , StakingResult stakingResult) = _setStake(account, courtID, stakeToImport); - - if (stakingResult == StakingResult.Successful) { - totalImportedSuccess++; - emit StakeImported(account, courtID, stakeToImport); - } else { - // Log or handle failed import for a specific entry - // emit StakeImportFailed(account, courtID, stakeToImport, stakingResult); - } - } - } - - emit MigrationCompleted(_accounts.length, totalImportedSuccess); - } - - /// @dev Import delayed stakes from old system for migration - /// @param _delayedStakes Array of delayed stake data - function importDelayedStakes(DelayedStake[] calldata _delayedStakes) external onlyByGovernor { - for (uint256 i = 0; i < _delayedStakes.length; i++) { - DelayedStake memory delayedStake = _delayedStakes[i]; - if (delayedStake.account != address(0)) { - delayedStakeWriteIndex++; - delayedStakes[delayedStakeWriteIndex] = delayedStake; - - emit DelayedStakeImported( - delayedStake.account, - delayedStake.courtID, - delayedStake.stake, - delayedStakeWriteIndex - ); - } - } - } - - /// @dev Migrate phase state from old sortition module - /// @param _phase The phase to set - /// @param _lastPhaseChange The last phase change timestamp - /// @param _disputesWithoutJurors Number of disputes without jurors - function migratePhaseState( - Phase _phase, - uint256 _lastPhaseChange, - uint256 _disputesWithoutJurors - ) external onlyByGovernor { - phase = _phase; - lastPhaseChange = _lastPhaseChange; - disputesWithoutJurors = _disputesWithoutJurors; - - emit PhaseStateMigrated(_phase, _lastPhaseChange, _disputesWithoutJurors); - } - - /// @dev Emergency coordination reset for critical issues - function emergencyCoordinationReset() external onlyByGovernor { - phase = Phase.staking; - lastPhaseChange = block.timestamp; - disputesWithoutJurors = 0; - randomNumber = 0; - - emit EmergencyReset(block.timestamp); - } - // ************************************* // // * Errors * // // ************************************* // diff --git a/contracts/src/arbitration/interfaces/IStakeController.sol b/contracts/src/arbitration/interfaces/IStakeController.sol index 970debf64..badfb6e65 100644 --- a/contracts/src/arbitration/interfaces/IStakeController.sol +++ b/contracts/src/arbitration/interfaces/IStakeController.sol @@ -27,7 +27,7 @@ interface IStakeController { event JurorPenaltyExecuted(address indexed _account, uint256 _penalty, uint256 _actualPenalty); event StakeLocked(address indexed _account, uint256 _amount); event StakeUnlocked(address indexed _account, uint256 _amount); - event JurorSetInactive(address indexed _account); + event JurorSetInactive(address indexed _account, bool _delayed); /// @notice Emitted when a juror's stake is set in a court /// @param _account The address of the juror @@ -66,18 +66,38 @@ interface IStakeController { // * Stake Management * // // ************************************* // - /// @notice Set stake for a juror with vault coordination + /// @notice Validate a stake change for a juror /// @param _account The juror's account /// @param _courtID The ID of the court /// @param _newStake The new stake amount - /// @return pnkDeposit The amount of PNK to deposit - /// @return pnkWithdrawal The amount of PNK to withdraw + /// @return pnkDeposit The amount of PNK validated for deposit + /// @return pnkWithdrawal The amount of PNK validated for withdrawal /// @return stakingResult The result of the staking operation - function setStake( + function validateStake( address _account, uint96 _courtID, uint256 _newStake - ) external returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult); + ) external view returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult); + + /// @notice Set stake for a juror with vault coordination + /// @param _account The juror's account + /// @param _courtID The ID of the court + /// @param _newStake The new stake amount + /// @param _pnkDeposit The amount of PNK validated for deposit + /// @param _pnkWithdrawal The amount of PNK validated for withdrawal + function setStake( + address _account, + uint96 _courtID, + uint256 _newStake, + uint256 _pnkDeposit, + uint256 _pnkWithdrawal + ) external; + + /// @notice Set a delayed stake change for a juror to be executed in the next staking phase + /// @param _account The juror's account + /// @param _courtID The ID of the court + /// @param _newStake The new stake amount + function setStakeDelayed(address _account, uint96 _courtID, uint256 _newStake) external; /// @notice Lock stake for dispute participation /// @param _account The account to lock stake for From 799dad1c8b9491a46ca3e19e547a867762ba4adc Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Mon, 9 Jun 2025 22:14:13 +0100 Subject: [PATCH 17/24] chore: clean up --- .../00-home-chain-arbitration-v2-neo.ts | 6 +- .../deploy/00-home-chain-arbitration-v2.ts | 16 +- contracts/deploy/utils/klerosCoreHelper.ts | 4 +- .../{KlerosCoreX.sol => KlerosCore.sol} | 8 +- .../arbitration/{old => }/KlerosCoreBase.sol | 192 ++- .../{KlerosCoreXNeo.sol => KlerosCoreNeo.sol} | 8 +- contracts/src/arbitration/KlerosCoreXBase.sol | 1183 ----------------- .../src/arbitration/SortitionSumTreeBase.sol | 4 +- contracts/src/arbitration/StakeController.sol | 4 +- .../src/arbitration/StakeControllerBase.sol | 6 +- .../dispute-kits/DisputeKitClassic.sol | 4 +- .../dispute-kits/DisputeKitClassicBase.sol | 20 +- .../dispute-kits/DisputeKitGated.sol | 4 +- .../dispute-kits/DisputeKitSybilResistant.sol | 4 +- .../interfaces/IStakeController.sol | 4 +- contracts/src/arbitration/old/KlerosCore.sol | 72 - .../src/arbitration/old/KlerosCoreNeo.sol | 139 -- .../src/arbitration/old/SortitionModule.sol | 60 - .../arbitration/old/SortitionModuleBase.sol | 734 ---------- .../arbitration/old/SortitionModuleNeo.sol | 119 -- contracts/src/test/KlerosCoreMock.sol | 2 +- contracts/src/test/SortitionModuleMock.sol | 4 +- 22 files changed, 135 insertions(+), 2462 deletions(-) rename contracts/src/arbitration/{KlerosCoreX.sol => KlerosCore.sol} (94%) rename contracts/src/arbitration/{old => }/KlerosCoreBase.sol (89%) rename contracts/src/arbitration/{KlerosCoreXNeo.sol => KlerosCoreNeo.sol} (96%) delete mode 100644 contracts/src/arbitration/KlerosCoreXBase.sol delete mode 100644 contracts/src/arbitration/old/KlerosCore.sol delete mode 100644 contracts/src/arbitration/old/KlerosCoreNeo.sol delete mode 100644 contracts/src/arbitration/old/SortitionModule.sol delete mode 100644 contracts/src/arbitration/old/SortitionModuleBase.sol delete mode 100644 contracts/src/arbitration/old/SortitionModuleNeo.sol diff --git a/contracts/deploy/00-home-chain-arbitration-v2-neo.ts b/contracts/deploy/00-home-chain-arbitration-v2-neo.ts index 358696089..f453eaf98 100644 --- a/contracts/deploy/00-home-chain-arbitration-v2-neo.ts +++ b/contracts/deploy/00-home-chain-arbitration-v2-neo.ts @@ -5,7 +5,7 @@ import { deployUpgradable } from "./utils/deployUpgradable"; import { HomeChains, isSkipped, isDevnet, PNK, ETH } from "./utils"; import { getContractOrDeploy, getContractOrDeployUpgradable } from "./utils/getContractOrDeploy"; import { deployERC20AndFaucet, deployERC721 } from "./utils/deployTokens"; -import { ChainlinkRNG, DisputeKitClassic, KlerosCoreXNeo, StakeControllerNeo, VaultNeo } from "../typechain-types"; +import { ChainlinkRNG, DisputeKitClassic, KlerosCoreNeo, StakeControllerNeo, VaultNeo } from "../typechain-types"; const deployArbitrationV2Neo: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const { ethers, deployments, getNamedAccounts, getChainId } = hre; @@ -53,7 +53,7 @@ const deployArbitrationV2Neo: DeployFunction = async (hre: HardhatRuntimeEnviron console.log(`core.changeArbitrableWhitelist(${resolver.address}, true)`); await core.changeArbitrableWhitelist(resolver.address, true); - await deploy("KlerosCoreXNeoSnapshotProxy", { + await deploy("KlerosCoreNeoSnapshotProxy", { from: deployer, contract: "KlerosCoreSnapshotProxy", args: [deployer, core.target], @@ -65,7 +65,7 @@ const deployArbitrationV2Neo: DeployFunction = async (hre: HardhatRuntimeEnviron console.log(`🎫 stPNKNeo: ${stPNK.address}`); console.log(`🎯 SortitionSumTreeNeo: ${sortitionModuleV2Neo.address}`); console.log(`🎮 StakeControllerNeo: ${stakeControllerNeo.target}`); - console.log(`⚖️ KlerosCoreXNeo: ${klerosCoreV2Neo.target}`); + console.log(`⚖️ KlerosCoreNeo: ${klerosCoreV2Neo.target}`); console.log(`🎨 JurorNFT: ${nft.target}`); console.log(`🔐 DisputeResolver: ${resolver.address}`); }; diff --git a/contracts/deploy/00-home-chain-arbitration-v2.ts b/contracts/deploy/00-home-chain-arbitration-v2.ts index 242d4e435..b36e3a4ac 100644 --- a/contracts/deploy/00-home-chain-arbitration-v2.ts +++ b/contracts/deploy/00-home-chain-arbitration-v2.ts @@ -5,7 +5,7 @@ import { deployUpgradable } from "./utils/deployUpgradable"; import { HomeChains, isSkipped, isDevnet, PNK, ETH } from "./utils"; import { getContractOrDeploy, getContractOrDeployUpgradable } from "./utils/getContractOrDeploy"; import { deployERC20AndFaucet } from "./utils/deployTokens"; -import { ChainlinkRNG, DisputeKitClassic, KlerosCoreX, StakeController, Vault } from "../typechain-types"; +import { ChainlinkRNG, DisputeKitClassic, KlerosCore, StakeController, Vault } from "../typechain-types"; import { changeCurrencyRate } from "./utils/klerosCoreHelper"; const deployArbitrationV2: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { @@ -34,7 +34,7 @@ const deployArbitrationV2: DeployFunction = async (hre: HardhatRuntimeEnvironmen contract: "DisputeKitClassic", args: [ deployer, - ZeroAddress, // Placeholder for KlerosCoreX address, configured later + ZeroAddress, // Placeholder for KlerosCore address, configured later ], log: true, }); @@ -49,7 +49,7 @@ const deployArbitrationV2: DeployFunction = async (hre: HardhatRuntimeEnvironmen console.log("calculated future StakeController address for nonce %d: %s", nonce + 5, stakeControllerAddress); const klerosCoreAddress = getContractAddress(deployer, nonce + 7); // deployed on the 8th tx (nonce+7): Vault Impl tx, Vault Proxy tx, SortitionModule Impl tx, SortitionModule Proxy tx, StakeController Impl tx, StakeController Proxy tx, KlerosCore Impl tx, KlerosCore Proxy tx - console.log("calculated future KlerosCoreX address for nonce %d: %s", nonce + 7, klerosCoreAddress); + console.log("calculated future KlerosCore address for nonce %d: %s", nonce + 7, klerosCoreAddress); const vault = await deployUpgradable(deployments, "Vault", { from: deployer, @@ -89,8 +89,8 @@ const deployArbitrationV2: DeployFunction = async (hre: HardhatRuntimeEnvironmen const feeForJuror = ETH(0.1); const jurorsForCourtJump = 256; - // Deploy KlerosCoreX (only if not already deployed) - const klerosCoreV2 = await deployUpgradable(deployments, "KlerosCoreX", { + // Deploy KlerosCore (only if not already deployed) + const klerosCoreV2 = await deployUpgradable(deployments, "KlerosCore", { from: deployer, args: [ deployer, @@ -126,7 +126,7 @@ const deployArbitrationV2: DeployFunction = async (hre: HardhatRuntimeEnvironmen await rng.changeSortitionModule(stakeController.address); } - const core = (await hre.ethers.getContract("KlerosCoreX")) as KlerosCoreX; + const core = (await hre.ethers.getContract("KlerosCore")) as KlerosCore; try { await changeCurrencyRate(core, await pnk.getAddress(), true, 12225583, 12); await changeCurrencyRate(core, await dai.getAddress(), true, 60327783, 11); @@ -135,7 +135,7 @@ const deployArbitrationV2: DeployFunction = async (hre: HardhatRuntimeEnvironmen console.error("failed to change currency rates:", e); } - await deploy("KlerosCoreXSnapshotProxy", { + await deploy("KlerosCoreSnapshotProxy", { from: deployer, contract: "KlerosCoreSnapshotProxy", args: [deployer, core.target], @@ -146,7 +146,7 @@ const deployArbitrationV2: DeployFunction = async (hre: HardhatRuntimeEnvironmen console.log(`📦 Vault: ${vault.address}`); console.log(`🎯 SortitionSumTree: ${sortitionModuleV2.address}`); console.log(`🎮 StakeController: ${stakeController.address}`); - console.log(`⚖️ KlerosCoreX: ${klerosCoreV2.address}`); + console.log(`⚖️ KlerosCore: ${klerosCoreV2.address}`); }; deployArbitrationV2.tags = ["ArbitrationV2"]; diff --git a/contracts/deploy/utils/klerosCoreHelper.ts b/contracts/deploy/utils/klerosCoreHelper.ts index 0941a8543..0d4de4178 100644 --- a/contracts/deploy/utils/klerosCoreHelper.ts +++ b/contracts/deploy/utils/klerosCoreHelper.ts @@ -1,8 +1,8 @@ -import { KlerosCore, KlerosCoreNeo, KlerosCoreRuler, KlerosCoreUniversity, KlerosCoreX } from "../../typechain-types"; +import { KlerosCore, KlerosCoreNeo, KlerosCoreRuler, KlerosCoreUniversity, KlerosCore } from "../../typechain-types"; import { BigNumberish, toBigInt } from "ethers"; export const changeCurrencyRate = async ( - core: KlerosCore | KlerosCoreNeo | KlerosCoreRuler | KlerosCoreUniversity | KlerosCoreX, + core: KlerosCore | KlerosCoreNeo | KlerosCoreRuler | KlerosCoreUniversity | KlerosCore, erc20: string, accepted: boolean, rateInEth: BigNumberish, diff --git a/contracts/src/arbitration/KlerosCoreX.sol b/contracts/src/arbitration/KlerosCore.sol similarity index 94% rename from contracts/src/arbitration/KlerosCoreX.sol rename to contracts/src/arbitration/KlerosCore.sol index f820b6f38..a4f10ef2f 100644 --- a/contracts/src/arbitration/KlerosCoreX.sol +++ b/contracts/src/arbitration/KlerosCore.sol @@ -2,11 +2,11 @@ pragma solidity 0.8.24; -import "./KlerosCoreXBase.sol"; +import "./KlerosCoreBase.sol"; -/// @title KlerosCoreX +/// @title KlerosCore /// @notice KlerosCore implementation with new StakeController architecture for testing environments -contract KlerosCoreX is KlerosCoreXBase { +contract KlerosCore is KlerosCoreBase { /// @notice Version of the implementation contract string public constant override version = "0.0.1"; @@ -42,7 +42,7 @@ contract KlerosCoreX is KlerosCoreXBase { IStakeController _stakeController, IVault _vault ) external initializer { - __KlerosCoreXBase_initialize( + __KlerosCoreBase_initialize( _governor, _guardian, _jurorProsecutionModule, diff --git a/contracts/src/arbitration/old/KlerosCoreBase.sol b/contracts/src/arbitration/KlerosCoreBase.sol similarity index 89% rename from contracts/src/arbitration/old/KlerosCoreBase.sol rename to contracts/src/arbitration/KlerosCoreBase.sol index 25bd126b6..8f00caf5d 100644 --- a/contracts/src/arbitration/old/KlerosCoreBase.sol +++ b/contracts/src/arbitration/KlerosCoreBase.sol @@ -2,17 +2,18 @@ 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"; +import {IArbitrableV2, IArbitratorV2} from "./interfaces/IArbitratorV2.sol"; +import {IDisputeKit} from "./interfaces/IDisputeKit.sol"; +import {IStakeController} from "./interfaces/IStakeController.sol"; +import {IVault} from "./interfaces/IVault.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. +/// Core arbitrator contract for Kleros v2 with new StakeController architecture. +/// Note that this contract trusts the PNK token, the dispute kit and the stake controller contracts. abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable { using SafeERC20 for IERC20; @@ -91,9 +92,9 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable address public governor; // The governor of the contract. address public guardian; // The guardian able to pause asset withdrawals. - IERC20 public pinakion; // The Pinakion token contract. address public jurorProsecutionModule; // The module for juror's prosecution. - ISortitionModule public sortitionModule; // Sortition module for drawing. + IStakeController public stakeController; // Stake controller for coordination. + IVault public vault; // The PNK vault for atomic deposits/withdrawals. Court[] public courts; // The courts. IDisputeKit[] public disputeKits; // Array of dispute kits. Dispute[] public disputes; // The disputes. @@ -160,6 +161,7 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable ); event Paused(); event Unpaused(); + event InactiveJurorWithdrawalFailed(address indexed _juror, uint256 _amount, bytes _reason); // ************************************* // // * Function Modifiers * // @@ -175,6 +177,11 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable _; } + modifier onlyStakeController() { + if (msg.sender != address(stakeController)) revert StakeControllerOnly(); + _; + } + modifier whenPaused() { if (!paused) revert WhenPausedOnly(); _; @@ -192,20 +199,20 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable function __KlerosCoreBase_initialize( address _governor, address _guardian, - IERC20 _pinakion, address _jurorProsecutionModule, IDisputeKit _disputeKit, bool _hiddenVotes, uint256[4] memory _courtParameters, uint256[4] memory _timesPerPeriod, bytes memory _sortitionExtraData, - ISortitionModule _sortitionModuleAddress + IStakeController _stakeController, + IVault _vault ) internal onlyInitializing { governor = _governor; guardian = _guardian; - pinakion = _pinakion; jurorProsecutionModule = _jurorProsecutionModule; - sortitionModule = _sortitionModuleAddress; + stakeController = _stakeController; + vault = _vault; // NULL_DISPUTE_KIT: an empty element at index 0 to indicate when a dispute kit is not supported. disputeKits.push(); @@ -218,7 +225,7 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable // FORKING_COURT // TODO: Fill the properties for the Forking court, emit CourtCreated. courts.push(); - sortitionModule.createTree(bytes32(uint256(FORKING_COURT)), _sortitionExtraData); + stakeController.createTree(bytes32(uint256(FORKING_COURT)), _sortitionExtraData); // GENERAL_COURT Court storage court = courts.push(); @@ -231,7 +238,7 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable court.jurorsForCourtJump = _courtParameters[3]; court.timesPerPeriod = _timesPerPeriod; - sortitionModule.createTree(bytes32(uint256(GENERAL_COURT)), _sortitionExtraData); + stakeController.createTree(bytes32(uint256(GENERAL_COURT)), _sortitionExtraData); uint256[] memory supportedDisputeKits = new uint256[](1); supportedDisputeKits[0] = DISPUTE_KIT_CLASSIC; @@ -290,23 +297,17 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable guardian = _guardian; } - /// @dev Changes the `pinakion` storage variable. - /// @param _pinakion The new value for the `pinakion` storage variable. - function changePinakion(IERC20 _pinakion) external onlyByGovernor { - pinakion = _pinakion; - } - /// @dev Changes the `jurorProsecutionModule` storage variable. /// @param _jurorProsecutionModule The new value for the `jurorProsecutionModule` storage variable. function changeJurorProsecutionModule(address _jurorProsecutionModule) external onlyByGovernor { jurorProsecutionModule = _jurorProsecutionModule; } - /// @dev Changes the `_sortitionModule` storage variable. - /// Note that the new module should be initialized for all courts. - /// @param _sortitionModule The new value for the `sortitionModule` storage variable. - function changeSortitionModule(ISortitionModule _sortitionModule) external onlyByGovernor { - sortitionModule = _sortitionModule; + /// @dev Changes the `stakeController` storage variable. + /// Note that the new controller should be initialized for all courts. + /// @param _stakeController The new value for the `stakeController` storage variable. + function changeStakeController(IStakeController _stakeController) external onlyByGovernor { + stakeController = _stakeController; } /// @dev Add a new supported dispute kit module to the court. @@ -363,10 +364,11 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable court.jurorsForCourtJump = _jurorsForCourtJump; court.timesPerPeriod = _timesPerPeriod; - sortitionModule.createTree(bytes32(courtID), _sortitionExtraData); + stakeController.createTree(bytes32(courtID), _sortitionExtraData); + if (_parent != FORKING_COURT) { + courts[_parent].children.push(courtID); + } - // Update the parent. - courts[_parent].children.push(courtID); emit CourtCreated( uint96(courtID), _parent, @@ -463,22 +465,21 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable /// @param _newStake The new stake. /// Note that the existing delayed stake will be nullified as non-relevant. function setStake(uint96 _courtID, uint256 _newStake) external virtual whenNotPaused { - _setStake(msg.sender, _courtID, _newStake, false, OnError.Revert); + _setStake(msg.sender, _courtID, _newStake); } - /// @dev Sets the stake of a specified account in a court, typically to apply a delayed stake or unstake inactive jurors. - /// @param _account The account whose stake is being set. + /// @notice Executes a stake change initiated by the system (e.g., processing a delayed stake). + /// @dev Called by StakeControllerBase during executeDelayedStakes. Assumes KlerosCore holds pre-funded PNK if _depositPreFunded is true. + /// @param _account The juror's account. /// @param _courtID The ID of the court. - /// @param _newStake The new stake. - /// @param _alreadyTransferred Whether the PNKs have already been transferred to the contract. - function setStakeBySortitionModule( + /// @param _newStake The new stake amount for the juror in the court. + /// @return success Whether the stake was successfully set or not. + function setStakeByController( address _account, uint96 _courtID, - uint256 _newStake, - bool _alreadyTransferred - ) external { - if (msg.sender != address(sortitionModule)) revert SortitionModuleOnly(); - _setStake(_account, _courtID, _newStake, _alreadyTransferred, OnError.Return); + uint256 _newStake + ) external onlyStakeController returns (bool success) { + return _setStake(_account, _courtID, _newStake); } /// @inheritdoc IArbitratorV2 @@ -534,7 +535,7 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable round.totalFeesForJurors = _feeAmount; round.feeToken = IERC20(_feeToken); - sortitionModule.createDisputeHook(disputeID, 0); // Default round ID. + stakeController.createDisputeHook(disputeID, 0); // Default round ID. disputeKit.createDispute(disputeID, _numberOfChoices, _extraData, round.nbVotes); emit DisputeCreation(disputeID, IArbitrableV2(msg.sender)); @@ -609,11 +610,11 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable if (drawnAddress == address(0)) { continue; } - sortitionModule.lockStake(drawnAddress, round.pnkAtStakePerJuror); + stakeController.lockStake(drawnAddress, round.pnkAtStakePerJuror); emit Draw(drawnAddress, _disputeID, currentRound, round.drawnJurors.length); round.drawnJurors.push(drawnAddress); if (round.drawnJurors.length == round.nbVotes) { - sortitionModule.postDrawHook(_disputeID, currentRound); + stakeController.postDrawHook(_disputeID, currentRound); } } round.drawIterations += i; @@ -664,7 +665,7 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable extraRound.totalFeesForJurors = msg.value; extraRound.disputeKitID = newDisputeKitID; - sortitionModule.createDisputeHook(_disputeID, dispute.rounds.length - 1); + stakeController.createDisputeHook(_disputeID, dispute.rounds.length - 1); // Dispute kit was changed, so create a dispute in the new DK contract. if (extraRound.disputeKitID != round.disputeKitID) { @@ -774,28 +775,35 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable // Fully coherent jurors won't be penalized. uint256 penalty = (round.pnkAtStakePerJuror * (ALPHA_DIVISOR - degreeOfCoherence)) / ALPHA_DIVISOR; - _params.pnkPenaltiesInRound += penalty; - // Unlock the PNKs affected by the penalty + // Execute penalty through StakeController coordination address account = round.drawnJurors[_params.repartition]; - sortitionModule.unlockStake(account, penalty); + (uint256 pnkBalance, uint256 actualPenalty) = stakeController.setJurorPenalty(account, penalty); + _params.pnkPenaltiesInRound += actualPenalty; - // Apply the penalty to the staked PNKs. - sortitionModule.penalizeStake(account, penalty); emit TokenAndETHShift( account, _params.disputeID, _params.round, degreeOfCoherence, - -int256(penalty), + -int256(actualPenalty), 0, round.feeToken ); - if (!disputeKit.isVoteActive(_params.disputeID, _params.round, _params.repartition)) { - // The juror is inactive, unstake them. - sortitionModule.setJurorInactive(account); + // Check if juror should be set inactive + bool inactive = !disputeKit.isVoteActive(_params.disputeID, _params.round, _params.repartition); + if (pnkBalance == 0 || inactive) { + uint256 pnkToWithdraw = stakeController.setJurorInactive(account); + if (pnkToWithdraw > 0) { + try vault.withdraw(account, pnkToWithdraw) { + // Successfully withdrew PNK for inactive juror + } catch (bytes memory reason) { + emit InactiveJurorWithdrawalFailed(account, pnkToWithdraw, reason); + } + } } + if (_params.repartition == _params.numberOfVotesInRound - 1 && _params.coherentCount == 0) { // No one was coherent, send the rewards to the governor. if (round.feeToken == NATIVE_CURRENCY) { @@ -805,7 +813,7 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable // The dispute fees were paid in ERC20 round.feeToken.safeTransfer(governor, round.totalFeesForJurors); } - pinakion.safeTransfer(governor, _params.pnkPenaltiesInRound); + vault.transferReward(governor, _params.pnkPenaltiesInRound); emit LeftoverRewardSent( _params.disputeID, _params.round, @@ -842,19 +850,14 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable uint256 pnkLocked = (round.pnkAtStakePerJuror * degreeOfCoherence) / ALPHA_DIVISOR; // Release the rest of the PNKs of the juror for this round. - sortitionModule.unlockStake(account, pnkLocked); - - // Give back the locked PNKs in case the juror fully unstaked earlier. - if (!sortitionModule.isJurorStaked(account)) { - pinakion.safeTransfer(account, pnkLocked); - } + stakeController.unlockStake(account, pnkLocked); // Transfer the rewards uint256 pnkReward = ((_params.pnkPenaltiesInRound / _params.coherentCount) * degreeOfCoherence) / ALPHA_DIVISOR; round.sumPnkRewardPaid += pnkReward; uint256 feeReward = ((round.totalFeesForJurors / _params.coherentCount) * degreeOfCoherence) / ALPHA_DIVISOR; round.sumFeeRewardPaid += feeReward; - pinakion.safeTransfer(account, pnkReward); + vault.transferReward(account, pnkReward); if (round.feeToken == NATIVE_CURRENCY) { // The dispute fees were paid in ETH payable(account).send(feeReward); @@ -878,7 +881,7 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable uint256 leftoverFeeReward = round.totalFeesForJurors - round.sumFeeRewardPaid; if (leftoverPnkReward != 0 || leftoverFeeReward != 0) { if (leftoverPnkReward != 0) { - pinakion.safeTransfer(governor, leftoverPnkReward); + vault.transferReward(governor, leftoverPnkReward); } if (leftoverFeeReward != 0) { if (round.feeToken == NATIVE_CURRENCY) { @@ -1070,62 +1073,41 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable emit DisputeKitEnabled(_courtID, _disputeKitID, _enable); } - /// @dev If called only once then set _onError to Revert, otherwise set it to Return + /// @dev If called only once then set _onError to Revert, otherwise for batch staking set it to Return /// @param _account The account to set the stake for. /// @param _courtID The ID of the court to set the stake for. /// @param _newStake The new stake. - /// @param _alreadyTransferred Whether the PNKs were already transferred to/from the staking contract. - /// @param _onError Whether to revert or return false on error. - /// @return Whether the stake was successfully set or not. - function _setStake( - address _account, - uint96 _courtID, - uint256 _newStake, - bool _alreadyTransferred, - OnError _onError - ) internal returns (bool) { + /// @return success Whether the stake was successfully set or not. + function _setStake(address _account, uint96 _courtID, uint256 _newStake) internal returns (bool success) { if (_courtID == FORKING_COURT || _courtID >= courts.length) { - _stakingFailed(_onError, StakingResult.CannotStakeInThisCourt); // Staking directly into the forking court is not allowed. - return false; + revert StakingNotPossibleInThisCourt(); } if (_newStake != 0 && _newStake < courts[_courtID].minStake) { - _stakingFailed(_onError, StakingResult.CannotStakeLessThanMinStake); // Staking less than the minimum stake is not allowed. - return false; + revert StakingLessThanCourtMinStake(); } - (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) = sortitionModule.setStake( + (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) = stakeController.validateStake( _account, _courtID, - _newStake, - _alreadyTransferred + _newStake ); - if (stakingResult != StakingResult.Successful) { - _stakingFailed(_onError, stakingResult); - return false; + if (stakingResult == StakingResult.Delayed) { + return true; } + success = true; if (pnkDeposit > 0) { - if (!pinakion.safeTransferFrom(_account, address(this), pnkDeposit)) { - _stakingFailed(_onError, StakingResult.StakingTransferFailed); - return false; + try vault.deposit(_account, pnkDeposit) {} catch { + success = false; } } if (pnkWithdrawal > 0) { - if (!pinakion.safeTransfer(_account, pnkWithdrawal)) { - _stakingFailed(_onError, StakingResult.UnstakingTransferFailed); - return false; + try vault.withdraw(_account, pnkWithdrawal) {} catch { + success = false; } } - return true; - } - - /// @dev It may revert depending on the _onError parameter. - function _stakingFailed(OnError _onError, StakingResult _result) internal pure virtual { - if (_onError == OnError.Return) return; - if (_result == StakingResult.StakingTransferFailed) revert StakingTransferFailed(); - if (_result == StakingResult.UnstakingTransferFailed) revert UnstakingTransferFailed(); - if (_result == StakingResult.CannotStakeInMoreCourts) revert StakingInTooManyCourts(); - if (_result == StakingResult.CannotStakeInThisCourt) revert StakingNotPossibleInThisCourt(); - if (_result == StakingResult.CannotStakeLessThanMinStake) revert StakingLessThanCourtMinStake(); - if (_result == StakingResult.CannotStakeZeroWhenNoStake) revert StakingZeroWhenNoStake(); + if (success) { + stakeController.setStake(_account, _courtID, _newStake, pnkDeposit, pnkWithdrawal); + } + return success; } /// @dev Gets a court ID, the minimum number of jurors and an ID of a dispute kit from a specified extra data bytes array. @@ -1168,7 +1150,7 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable error GovernorOnly(); error GuardianOrGovernorOnly(); error DisputeKitOnly(); - error SortitionModuleOnly(); + error StakeControllerOnly(); error UnsuccessfulCall(); error InvalidDisputKitParent(); error MinStakeLowerThanParentCourt(); @@ -1176,7 +1158,6 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable error InvalidForkingCourtAsParent(); error WrongDisputeKitIndex(); error CannotDisableClassicDK(); - error StakingInTooManyCourts(); error StakingNotPossibleInThisCourt(); error StakingLessThanCourtMinStake(); error StakingTransferFailed(); @@ -1199,5 +1180,4 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable error TransferFailed(); error WhenNotPausedOnly(); error WhenPausedOnly(); - error StakingZeroWhenNoStake(); } diff --git a/contracts/src/arbitration/KlerosCoreXNeo.sol b/contracts/src/arbitration/KlerosCoreNeo.sol similarity index 96% rename from contracts/src/arbitration/KlerosCoreXNeo.sol rename to contracts/src/arbitration/KlerosCoreNeo.sol index 120e17a1c..268e023a6 100644 --- a/contracts/src/arbitration/KlerosCoreXNeo.sol +++ b/contracts/src/arbitration/KlerosCoreNeo.sol @@ -2,12 +2,12 @@ pragma solidity 0.8.24; -import "./KlerosCoreXBase.sol"; +import "./KlerosCoreBase.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; -/// @title KlerosCoreXNeo +/// @title KlerosCoreNeo /// @notice KlerosCore with whitelisted arbitrables -contract KlerosCoreXNeo is KlerosCoreXBase { +contract KlerosCoreNeo is KlerosCoreBase { string public constant override version = "0.0.1"; // ************************************* // @@ -47,7 +47,7 @@ contract KlerosCoreXNeo is KlerosCoreXBase { IStakeController _stakeController, IVault _vault ) external reinitializer(2) { - __KlerosCoreXBase_initialize( + __KlerosCoreBase_initialize( _governor, _guardian, _jurorProsecutionModule, diff --git a/contracts/src/arbitration/KlerosCoreXBase.sol b/contracts/src/arbitration/KlerosCoreXBase.sol deleted file mode 100644 index 187e97f7a..000000000 --- a/contracts/src/arbitration/KlerosCoreXBase.sol +++ /dev/null @@ -1,1183 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import {IArbitrableV2, IArbitratorV2} from "./interfaces/IArbitratorV2.sol"; -import {IDisputeKit} from "./interfaces/IDisputeKit.sol"; -import {IStakeController} from "./interfaces/IStakeController.sol"; -import {IVault} from "./interfaces/IVault.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 KlerosCoreXBase -/// Core arbitrator contract for Kleros v2 with new StakeController architecture. -/// Note that this contract trusts the PNK token, the dispute kit and the stake controller contracts. -abstract contract KlerosCoreXBase is IArbitratorV2, Initializable, UUPSProxiable { - using SafeERC20 for IERC20; - - // ************************************* // - // * Enums / Structs * // - // ************************************* // - - enum Period { - evidence, // Evidence can be submitted. This is also when drawing has to take place. - commit, // Jurors commit a hashed vote. This is skipped for courts without hidden votes. - vote, // Jurors reveal/cast their vote depending on whether the court has hidden votes or not. - appeal, // The dispute can be appealed. - execution // Tokens are redistributed and the ruling is executed. - } - - struct Court { - uint96 parent; // The parent court. - bool hiddenVotes; // Whether to use commit and reveal or not. - uint256[] children; // List of child courts. - uint256 minStake; // Minimum PNKs needed to stake in the court. - uint256 alpha; // Basis point of PNKs that are lost when incoherent. - uint256 feeForJuror; // Arbitration fee paid per juror. - uint256 jurorsForCourtJump; // The appeal after the one that reaches this number of jurors will go to the parent court if any. - uint256[4] timesPerPeriod; // The time allotted to each dispute period in the form `timesPerPeriod[period]`. - mapping(uint256 disputeKitId => bool) supportedDisputeKits; // True if DK with this ID is supported by the court. Note that each court must support classic dispute kit. - bool disabled; // True if the court is disabled. Unused for now, will be implemented later. - } - - struct Dispute { - uint96 courtID; // The ID of the court the dispute is in. - IArbitrableV2 arbitrated; // The arbitrable contract. - Period period; // The current period of the dispute. - bool ruled; // True if the ruling has been executed, false otherwise. - uint256 lastPeriodChange; // The last time the period was changed. - Round[] rounds; - } - - struct Round { - uint256 disputeKitID; // Index of the dispute kit in the array. - uint256 pnkAtStakePerJuror; // The amount of PNKs at stake for each juror in this round. - uint256 totalFeesForJurors; // The total juror fees paid in this round. - uint256 nbVotes; // The total number of votes the dispute can possibly have in the current round. Former votes[_round].length. - uint256 repartitions; // A counter of reward repartitions made in this round. - uint256 pnkPenalties; // The amount of PNKs collected from penalties in this round. - address[] drawnJurors; // Addresses of the jurors that were drawn in this round. - uint256 sumFeeRewardPaid; // Total sum of arbitration fees paid to coherent jurors as a reward in this round. - uint256 sumPnkRewardPaid; // Total sum of PNK paid to coherent jurors as a reward in this round. - IERC20 feeToken; // The token used for paying fees in this round. - uint256 drawIterations; // The number of iterations passed drawing the jurors for this round. - } - - // Workaround "stack too deep" errors - struct ExecuteParams { - uint256 disputeID; // The ID of the dispute to execute. - uint256 round; // The round to execute. - uint256 coherentCount; // The number of coherent votes in the round. - uint256 numberOfVotesInRound; // The number of votes in the round. - uint256 feePerJurorInRound; // The fee per juror in the round. - uint256 pnkAtStakePerJurorInRound; // The amount of PNKs at stake for each juror in the round. - uint256 pnkPenaltiesInRound; // The amount of PNKs collected from penalties in the round. - uint256 repartition; // The index of the repartition to execute. - } - - struct CurrencyRate { - bool feePaymentAccepted; - uint64 rateInEth; - uint8 rateDecimals; - } - - // ************************************* // - // * Storage * // - // ************************************* // - - uint256 private constant ALPHA_DIVISOR = 1e4; // The number to divide `Court.alpha` by. - uint256 private constant NON_PAYABLE_AMOUNT = (2 ** 256 - 2) / 2; // An amount higher than the supply of ETH. - - address public governor; // The governor of the contract. - address public guardian; // The guardian able to pause asset withdrawals. - address public jurorProsecutionModule; // The module for juror's prosecution. - IStakeController public stakeController; // Stake controller for coordination. - IVault public vault; // The PNK vault for atomic deposits/withdrawals. - Court[] public courts; // The courts. - IDisputeKit[] public disputeKits; // Array of dispute kits. - Dispute[] public disputes; // The disputes. - mapping(IERC20 => CurrencyRate) public currencyRates; // The price of each token in ETH. - bool public paused; // Whether asset withdrawals are paused. - - // ************************************* // - // * Events * // - // ************************************* // - - event NewPeriod(uint256 indexed _disputeID, Period _period); - event AppealPossible(uint256 indexed _disputeID, IArbitrableV2 indexed _arbitrable); - event AppealDecision(uint256 indexed _disputeID, IArbitrableV2 indexed _arbitrable); - event Draw(address indexed _address, uint256 indexed _disputeID, uint256 _roundID, uint256 _voteID); - event CourtCreated( - uint96 indexed _courtID, - uint96 indexed _parent, - bool _hiddenVotes, - uint256 _minStake, - uint256 _alpha, - uint256 _feeForJuror, - uint256 _jurorsForCourtJump, - uint256[4] _timesPerPeriod, - uint256[] _supportedDisputeKits - ); - event CourtModified( - uint96 indexed _courtID, - bool _hiddenVotes, - uint256 _minStake, - uint256 _alpha, - uint256 _feeForJuror, - uint256 _jurorsForCourtJump, - uint256[4] _timesPerPeriod - ); - event DisputeKitCreated(uint256 indexed _disputeKitID, IDisputeKit indexed _disputeKitAddress); - event DisputeKitEnabled(uint96 indexed _courtID, uint256 indexed _disputeKitID, bool indexed _enable); - event CourtJump( - uint256 indexed _disputeID, - uint256 indexed _roundID, - uint96 indexed _fromCourtID, - uint96 _toCourtID - ); - event DisputeKitJump( - uint256 indexed _disputeID, - uint256 indexed _roundID, - uint256 indexed _fromDisputeKitID, - uint256 _toDisputeKitID - ); - event TokenAndETHShift( - address indexed _account, - uint256 indexed _disputeID, - uint256 indexed _roundID, - uint256 _degreeOfCoherency, - int256 _pnkAmount, - int256 _feeAmount, - IERC20 _feeToken - ); - event LeftoverRewardSent( - uint256 indexed _disputeID, - uint256 indexed _roundID, - uint256 _pnkAmount, - uint256 _feeAmount, - IERC20 _feeToken - ); - event Paused(); - event Unpaused(); - event InactiveJurorWithdrawalFailed(address indexed _juror, uint256 _amount, bytes _reason); - - // ************************************* // - // * Function Modifiers * // - // ************************************* // - - modifier onlyByGovernor() { - if (governor != msg.sender) revert GovernorOnly(); - _; - } - - modifier onlyByGuardianOrGovernor() { - if (guardian != msg.sender && governor != msg.sender) revert GuardianOrGovernorOnly(); - _; - } - - modifier onlyStakeController() { - if (msg.sender != address(stakeController)) revert StakeControllerOnly(); - _; - } - - modifier whenPaused() { - if (!paused) revert WhenPausedOnly(); - _; - } - - modifier whenNotPaused() { - if (paused) revert WhenNotPausedOnly(); - _; - } - - // ************************************* // - // * Constructor * // - // ************************************* // - - function __KlerosCoreXBase_initialize( - address _governor, - address _guardian, - address _jurorProsecutionModule, - IDisputeKit _disputeKit, - bool _hiddenVotes, - uint256[4] memory _courtParameters, - uint256[4] memory _timesPerPeriod, - bytes memory _sortitionExtraData, - IStakeController _stakeController, - IVault _vault - ) internal onlyInitializing { - governor = _governor; - guardian = _guardian; - jurorProsecutionModule = _jurorProsecutionModule; - stakeController = _stakeController; - vault = _vault; - - // NULL_DISPUTE_KIT: an empty element at index 0 to indicate when a dispute kit is not supported. - disputeKits.push(); - - // DISPUTE_KIT_CLASSIC - disputeKits.push(_disputeKit); - - emit DisputeKitCreated(DISPUTE_KIT_CLASSIC, _disputeKit); - - // FORKING_COURT - // TODO: Fill the properties for the Forking court, emit CourtCreated. - courts.push(); - stakeController.createTree(bytes32(uint256(FORKING_COURT)), _sortitionExtraData); - - // GENERAL_COURT - Court storage court = courts.push(); - court.parent = FORKING_COURT; - court.children = new uint256[](0); - court.hiddenVotes = _hiddenVotes; - court.minStake = _courtParameters[0]; - court.alpha = _courtParameters[1]; - court.feeForJuror = _courtParameters[2]; - court.jurorsForCourtJump = _courtParameters[3]; - court.timesPerPeriod = _timesPerPeriod; - - stakeController.createTree(bytes32(uint256(GENERAL_COURT)), _sortitionExtraData); - - uint256[] memory supportedDisputeKits = new uint256[](1); - supportedDisputeKits[0] = DISPUTE_KIT_CLASSIC; - emit CourtCreated( - GENERAL_COURT, - court.parent, - _hiddenVotes, - _courtParameters[0], - _courtParameters[1], - _courtParameters[2], - _courtParameters[3], - _timesPerPeriod, - supportedDisputeKits - ); - _enableDisputeKit(GENERAL_COURT, DISPUTE_KIT_CLASSIC, true); - } - - // ************************************* // - // * Governance * // - // ************************************* // - - /// @dev Pause staking and reward execution. Can only be done by guardian or governor. - function pause() external onlyByGuardianOrGovernor whenNotPaused { - paused = true; - emit Paused(); - } - - /// @dev Unpause staking and reward execution. Can only be done by governor. - function unpause() external onlyByGovernor whenPaused { - paused = false; - emit Unpaused(); - } - - /// @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); - if (!success) revert UnsuccessfulCall(); - } - - /// @dev Changes the `governor` storage variable. - /// @param _governor The new value for the `governor` storage variable. - function changeGovernor(address payable _governor) external onlyByGovernor { - governor = _governor; - } - - /// @dev Changes the `guardian` storage variable. - /// @param _guardian The new value for the `guardian` storage variable. - function changeGuardian(address _guardian) external onlyByGovernor { - guardian = _guardian; - } - - /// @dev Changes the `jurorProsecutionModule` storage variable. - /// @param _jurorProsecutionModule The new value for the `jurorProsecutionModule` storage variable. - function changeJurorProsecutionModule(address _jurorProsecutionModule) external onlyByGovernor { - jurorProsecutionModule = _jurorProsecutionModule; - } - - /// @dev Changes the `stakeController` storage variable. - /// Note that the new controller should be initialized for all courts. - /// @param _stakeController The new value for the `stakeController` storage variable. - function changeStakeController(IStakeController _stakeController) external onlyByGovernor { - stakeController = _stakeController; - } - - /// @dev Add a new supported dispute kit module to the court. - /// @param _disputeKitAddress The address of the dispute kit contract. - function addNewDisputeKit(IDisputeKit _disputeKitAddress) external onlyByGovernor { - uint256 disputeKitID = disputeKits.length; - disputeKits.push(_disputeKitAddress); - emit DisputeKitCreated(disputeKitID, _disputeKitAddress); - } - - /// @dev Creates a court under a specified parent court. - /// @param _parent The `parent` property value of the court. - /// @param _hiddenVotes The `hiddenVotes` property value of the court. - /// @param _minStake The `minStake` property value of the court. - /// @param _alpha The `alpha` property value of the court. - /// @param _feeForJuror The `feeForJuror` property value of the court. - /// @param _jurorsForCourtJump The `jurorsForCourtJump` property value of the court. - /// @param _timesPerPeriod The `timesPerPeriod` property value of the court. - /// @param _sortitionExtraData Extra data for sortition module. - /// @param _supportedDisputeKits Indexes of dispute kits that this court will support. - function createCourt( - uint96 _parent, - bool _hiddenVotes, - uint256 _minStake, - uint256 _alpha, - uint256 _feeForJuror, - uint256 _jurorsForCourtJump, - uint256[4] memory _timesPerPeriod, - bytes memory _sortitionExtraData, - uint256[] memory _supportedDisputeKits - ) external onlyByGovernor { - if (courts[_parent].minStake > _minStake) revert MinStakeLowerThanParentCourt(); - if (_supportedDisputeKits.length == 0) revert UnsupportedDisputeKit(); - if (_parent == FORKING_COURT) revert InvalidForkingCourtAsParent(); - - uint256 courtID = courts.length; - Court storage court = courts.push(); - - for (uint256 i = 0; i < _supportedDisputeKits.length; i++) { - if (_supportedDisputeKits[i] == 0 || _supportedDisputeKits[i] >= disputeKits.length) { - revert WrongDisputeKitIndex(); - } - _enableDisputeKit(uint96(courtID), _supportedDisputeKits[i], true); - } - // Check that Classic DK support was added. - if (!court.supportedDisputeKits[DISPUTE_KIT_CLASSIC]) revert MustSupportDisputeKitClassic(); - - court.parent = _parent; - court.children = new uint256[](0); - court.hiddenVotes = _hiddenVotes; - court.minStake = _minStake; - court.alpha = _alpha; - court.feeForJuror = _feeForJuror; - court.jurorsForCourtJump = _jurorsForCourtJump; - court.timesPerPeriod = _timesPerPeriod; - - stakeController.createTree(bytes32(courtID), _sortitionExtraData); - if (_parent != FORKING_COURT) { - courts[_parent].children.push(courtID); - } - - emit CourtCreated( - uint96(courtID), - _parent, - _hiddenVotes, - _minStake, - _alpha, - _feeForJuror, - _jurorsForCourtJump, - _timesPerPeriod, - _supportedDisputeKits - ); - } - - function changeCourtParameters( - uint96 _courtID, - bool _hiddenVotes, - uint256 _minStake, - uint256 _alpha, - uint256 _feeForJuror, - uint256 _jurorsForCourtJump, - uint256[4] memory _timesPerPeriod - ) external onlyByGovernor { - Court storage court = courts[_courtID]; - if (_courtID != GENERAL_COURT && courts[court.parent].minStake > _minStake) { - revert MinStakeLowerThanParentCourt(); - } - for (uint256 i = 0; i < court.children.length; i++) { - if (courts[court.children[i]].minStake < _minStake) { - revert MinStakeLowerThanParentCourt(); - } - } - court.minStake = _minStake; - court.hiddenVotes = _hiddenVotes; - court.alpha = _alpha; - court.feeForJuror = _feeForJuror; - court.jurorsForCourtJump = _jurorsForCourtJump; - court.timesPerPeriod = _timesPerPeriod; - emit CourtModified( - _courtID, - _hiddenVotes, - _minStake, - _alpha, - _feeForJuror, - _jurorsForCourtJump, - _timesPerPeriod - ); - } - - /// @dev Adds/removes court's support for specified dispute kits. - /// @param _courtID The ID of the court. - /// @param _disputeKitIDs The IDs of dispute kits which support should be added/removed. - /// @param _enable Whether add or remove the dispute kits from the court. - function enableDisputeKits(uint96 _courtID, uint256[] memory _disputeKitIDs, bool _enable) external onlyByGovernor { - for (uint256 i = 0; i < _disputeKitIDs.length; i++) { - if (_enable) { - if (_disputeKitIDs[i] == 0 || _disputeKitIDs[i] >= disputeKits.length) { - revert WrongDisputeKitIndex(); - } - _enableDisputeKit(_courtID, _disputeKitIDs[i], true); - } else { - // Classic dispute kit must be supported by all courts. - if (_disputeKitIDs[i] == DISPUTE_KIT_CLASSIC) { - revert CannotDisableClassicDK(); - } - _enableDisputeKit(_courtID, _disputeKitIDs[i], false); - } - } - } - - /// @dev Changes the supported fee tokens. - /// @param _feeToken The fee token. - /// @param _accepted Whether the token is supported or not as a method of fee payment. - function changeAcceptedFeeTokens(IERC20 _feeToken, bool _accepted) external onlyByGovernor { - currencyRates[_feeToken].feePaymentAccepted = _accepted; - emit AcceptedFeeToken(_feeToken, _accepted); - } - - /// @dev Changes the currency rate of a fee token. - /// @param _feeToken The fee token. - /// @param _rateInEth The new rate of the fee token in ETH. - /// @param _rateDecimals The new decimals of the fee token rate. - function changeCurrencyRates(IERC20 _feeToken, uint64 _rateInEth, uint8 _rateDecimals) external onlyByGovernor { - currencyRates[_feeToken].rateInEth = _rateInEth; - currencyRates[_feeToken].rateDecimals = _rateDecimals; - emit NewCurrencyRate(_feeToken, _rateInEth, _rateDecimals); - } - - // ************************************* // - // * State Modifiers * // - // ************************************* // - - /// @dev Sets the caller's stake in a court. - /// @param _courtID The ID of the court. - /// @param _newStake The new stake. - /// Note that the existing delayed stake will be nullified as non-relevant. - function setStake(uint96 _courtID, uint256 _newStake) external virtual whenNotPaused { - _setStake(msg.sender, _courtID, _newStake); - } - - /// @notice Executes a stake change initiated by the system (e.g., processing a delayed stake). - /// @dev Called by StakeControllerBase during executeDelayedStakes. Assumes KlerosCore holds pre-funded PNK if _depositPreFunded is true. - /// @param _account The juror's account. - /// @param _courtID The ID of the court. - /// @param _newStake The new stake amount for the juror in the court. - /// @return success Whether the stake was successfully set or not. - function setStakeByController( - address _account, - uint96 _courtID, - uint256 _newStake - ) external onlyStakeController returns (bool success) { - return _setStake(_account, _courtID, _newStake); - } - - /// @inheritdoc IArbitratorV2 - function createDispute( - uint256 _numberOfChoices, - bytes memory _extraData - ) external payable override returns (uint256 disputeID) { - if (msg.value < arbitrationCost(_extraData)) revert ArbitrationFeesNotEnough(); - - return _createDispute(_numberOfChoices, _extraData, NATIVE_CURRENCY, msg.value); - } - - /// @inheritdoc IArbitratorV2 - function createDispute( - uint256 _numberOfChoices, - bytes calldata _extraData, - IERC20 _feeToken, - uint256 _feeAmount - ) external override returns (uint256 disputeID) { - if (!currencyRates[_feeToken].feePaymentAccepted) revert TokenNotAccepted(); - if (_feeAmount < arbitrationCost(_extraData, _feeToken)) revert ArbitrationFeesNotEnough(); - - if (!_feeToken.safeTransferFrom(msg.sender, address(this), _feeAmount)) revert TransferFailed(); - return _createDispute(_numberOfChoices, _extraData, _feeToken, _feeAmount); - } - - function _createDispute( - uint256 _numberOfChoices, - bytes memory _extraData, - IERC20 _feeToken, - uint256 _feeAmount - ) internal virtual returns (uint256 disputeID) { - (uint96 courtID, , uint256 disputeKitID) = _extraDataToCourtIDMinJurorsDisputeKit(_extraData); - if (!courts[courtID].supportedDisputeKits[disputeKitID]) revert DisputeKitNotSupportedByCourt(); - - disputeID = disputes.length; - Dispute storage dispute = disputes.push(); - dispute.courtID = courtID; - dispute.arbitrated = IArbitrableV2(msg.sender); - dispute.lastPeriodChange = block.timestamp; - - IDisputeKit disputeKit = disputeKits[disputeKitID]; - Court storage court = courts[courtID]; - Round storage round = dispute.rounds.push(); - - // Obtain the feeForJuror in the same currency as the _feeAmount - uint256 feeForJuror = (_feeToken == NATIVE_CURRENCY) - ? court.feeForJuror - : convertEthToTokenAmount(_feeToken, court.feeForJuror); - round.nbVotes = _feeAmount / feeForJuror; - round.disputeKitID = disputeKitID; - round.pnkAtStakePerJuror = (court.minStake * court.alpha) / ALPHA_DIVISOR; - round.totalFeesForJurors = _feeAmount; - round.feeToken = IERC20(_feeToken); - - stakeController.createDisputeHook(disputeID, 0); // Default round ID. - - disputeKit.createDispute(disputeID, _numberOfChoices, _extraData, round.nbVotes); - emit DisputeCreation(disputeID, IArbitrableV2(msg.sender)); - } - - /// @dev Passes the period of a specified dispute. - /// @param _disputeID The ID of the dispute. - function passPeriod(uint256 _disputeID) external { - Dispute storage dispute = disputes[_disputeID]; - Court storage court = courts[dispute.courtID]; - - uint256 currentRound = dispute.rounds.length - 1; - Round storage round = dispute.rounds[currentRound]; - if (dispute.period == Period.evidence) { - if ( - currentRound == 0 && - block.timestamp - dispute.lastPeriodChange < court.timesPerPeriod[uint256(dispute.period)] - ) { - revert EvidenceNotPassedAndNotAppeal(); - } - if (round.drawnJurors.length != round.nbVotes) revert DisputeStillDrawing(); - dispute.period = court.hiddenVotes ? Period.commit : Period.vote; - } else if (dispute.period == Period.commit) { - if ( - block.timestamp - dispute.lastPeriodChange < court.timesPerPeriod[uint256(dispute.period)] && - !disputeKits[round.disputeKitID].areCommitsAllCast(_disputeID) - ) { - revert CommitPeriodNotPassed(); - } - dispute.period = Period.vote; - } else if (dispute.period == Period.vote) { - if ( - block.timestamp - dispute.lastPeriodChange < court.timesPerPeriod[uint256(dispute.period)] && - !disputeKits[round.disputeKitID].areVotesAllCast(_disputeID) - ) { - revert VotePeriodNotPassed(); - } - dispute.period = Period.appeal; - emit AppealPossible(_disputeID, dispute.arbitrated); - } else if (dispute.period == Period.appeal) { - if ( - block.timestamp - dispute.lastPeriodChange < court.timesPerPeriod[uint256(dispute.period)] && - !disputeKits[round.disputeKitID].isAppealFunded(_disputeID) - ) { - revert AppealPeriodNotPassed(); - } - dispute.period = Period.execution; - } else if (dispute.period == Period.execution) { - revert DisputePeriodIsFinal(); - } - - dispute.lastPeriodChange = block.timestamp; - emit NewPeriod(_disputeID, dispute.period); - } - - /// @dev Draws jurors for the dispute. Can be called in parts. - /// @param _disputeID The ID of the dispute. - /// @param _iterations The number of iterations to run. - /// @return nbDrawnJurors The total number of jurors drawn in the round. - function draw(uint256 _disputeID, uint256 _iterations) external returns (uint256 nbDrawnJurors) { - Dispute storage dispute = disputes[_disputeID]; - uint256 currentRound = dispute.rounds.length - 1; - Round storage round = dispute.rounds[currentRound]; - if (dispute.period != Period.evidence) revert NotEvidencePeriod(); - - IDisputeKit disputeKit = disputeKits[round.disputeKitID]; - - uint256 startIndex = round.drawIterations; // for gas: less storage reads - uint256 i; - while (i < _iterations && round.drawnJurors.length < round.nbVotes) { - address drawnAddress = disputeKit.draw(_disputeID, startIndex + i++); - if (drawnAddress == address(0)) { - continue; - } - stakeController.lockStake(drawnAddress, round.pnkAtStakePerJuror); - emit Draw(drawnAddress, _disputeID, currentRound, round.drawnJurors.length); - round.drawnJurors.push(drawnAddress); - if (round.drawnJurors.length == round.nbVotes) { - stakeController.postDrawHook(_disputeID, currentRound); - } - } - round.drawIterations += i; - return round.drawnJurors.length; - } - - /// @dev Appeals the ruling of a specified dispute. - /// Note: Access restricted to the Dispute Kit for this `disputeID`. - /// @param _disputeID The ID of the dispute. - /// @param _numberOfChoices Number of choices for the dispute. Can be required during court jump. - /// @param _extraData Extradata for the dispute. Can be required during court jump. - function appeal(uint256 _disputeID, uint256 _numberOfChoices, bytes memory _extraData) external payable { - if (msg.value < appealCost(_disputeID)) revert AppealFeesNotEnough(); - - Dispute storage dispute = disputes[_disputeID]; - if (dispute.period != Period.appeal) revert DisputeNotAppealable(); - - Round storage round = dispute.rounds[dispute.rounds.length - 1]; - if (msg.sender != address(disputeKits[round.disputeKitID])) revert DisputeKitOnly(); - - uint96 newCourtID = dispute.courtID; - uint256 newDisputeKitID = round.disputeKitID; - - // Warning: the extra round must be created before calling disputeKit.createDispute() - Round storage extraRound = dispute.rounds.push(); - - if (round.nbVotes >= courts[newCourtID].jurorsForCourtJump) { - // Jump to parent court. - newCourtID = courts[newCourtID].parent; - - if (!courts[newCourtID].supportedDisputeKits[newDisputeKitID]) { - // Switch to classic dispute kit if parent court doesn't support the current one. - newDisputeKitID = DISPUTE_KIT_CLASSIC; - } - - if (newCourtID != dispute.courtID) { - emit CourtJump(_disputeID, dispute.rounds.length - 1, dispute.courtID, newCourtID); - } - } - - dispute.courtID = newCourtID; - dispute.period = Period.evidence; - dispute.lastPeriodChange = block.timestamp; - - Court storage court = courts[newCourtID]; - extraRound.nbVotes = msg.value / court.feeForJuror; // As many votes that can be afforded by the provided funds. - extraRound.pnkAtStakePerJuror = (court.minStake * court.alpha) / ALPHA_DIVISOR; - extraRound.totalFeesForJurors = msg.value; - extraRound.disputeKitID = newDisputeKitID; - - stakeController.createDisputeHook(_disputeID, dispute.rounds.length - 1); - - // Dispute kit was changed, so create a dispute in the new DK contract. - if (extraRound.disputeKitID != round.disputeKitID) { - emit DisputeKitJump(_disputeID, dispute.rounds.length - 1, round.disputeKitID, extraRound.disputeKitID); - disputeKits[extraRound.disputeKitID].createDispute( - _disputeID, - _numberOfChoices, - _extraData, - extraRound.nbVotes - ); - } - - emit AppealDecision(_disputeID, dispute.arbitrated); - emit NewPeriod(_disputeID, Period.evidence); - } - - /// @dev Distribute the PNKs at stake and the dispute fees for the specific round of the dispute. Can be called in parts. - /// Note: Reward distributions are forbidden during pause. - /// @param _disputeID The ID of the dispute. - /// @param _round The appeal round. - /// @param _iterations The number of iterations to run. - function execute(uint256 _disputeID, uint256 _round, uint256 _iterations) external whenNotPaused { - Round storage round; - { - Dispute storage dispute = disputes[_disputeID]; - if (dispute.period != Period.execution) revert NotExecutionPeriod(); - - round = dispute.rounds[_round]; - } // stack too deep workaround - - uint256 start = round.repartitions; - uint256 end = round.repartitions + _iterations; - - uint256 pnkPenaltiesInRound = round.pnkPenalties; // Keep in memory to save gas. - uint256 numberOfVotesInRound = round.drawnJurors.length; - uint256 feePerJurorInRound = round.totalFeesForJurors / numberOfVotesInRound; - uint256 pnkAtStakePerJurorInRound = round.pnkAtStakePerJuror; - uint256 coherentCount; - { - IDisputeKit disputeKit = disputeKits[round.disputeKitID]; - coherentCount = disputeKit.getCoherentCount(_disputeID, _round); // Total number of jurors that are eligible to a reward in this round. - } // stack too deep workaround - - if (coherentCount == 0) { - // We loop over the votes once as there are no rewards because it is not a tie and no one in this round is coherent with the final outcome. - if (end > numberOfVotesInRound) end = numberOfVotesInRound; - } else { - // We loop over the votes twice, first to collect the PNK penalties, and second to distribute them as rewards along with arbitration fees. - if (end > numberOfVotesInRound * 2) end = numberOfVotesInRound * 2; - } - round.repartitions = end; - - for (uint256 i = start; i < end; i++) { - if (i < numberOfVotesInRound) { - pnkPenaltiesInRound = _executePenalties( - ExecuteParams({ - disputeID: _disputeID, - round: _round, - coherentCount: coherentCount, - numberOfVotesInRound: numberOfVotesInRound, - feePerJurorInRound: feePerJurorInRound, - pnkAtStakePerJurorInRound: pnkAtStakePerJurorInRound, - pnkPenaltiesInRound: pnkPenaltiesInRound, - repartition: i - }) - ); - } else { - _executeRewards( - ExecuteParams({ - disputeID: _disputeID, - round: _round, - coherentCount: coherentCount, - numberOfVotesInRound: numberOfVotesInRound, - feePerJurorInRound: feePerJurorInRound, - pnkAtStakePerJurorInRound: pnkAtStakePerJurorInRound, - pnkPenaltiesInRound: pnkPenaltiesInRound, - repartition: i - }) - ); - } - } - if (round.pnkPenalties != pnkPenaltiesInRound) { - round.pnkPenalties = pnkPenaltiesInRound; // Reentrancy risk: breaks Check-Effect-Interact - } - } - - /// @dev Distribute the PNKs at stake and the dispute fees for the specific round of the dispute, penalties only. - /// @param _params The parameters for the execution, see `ExecuteParams`. - /// @return pnkPenaltiesInRoundCache The updated penalties in round cache. - function _executePenalties(ExecuteParams memory _params) internal returns (uint256) { - Dispute storage dispute = disputes[_params.disputeID]; - Round storage round = dispute.rounds[_params.round]; - IDisputeKit disputeKit = disputeKits[round.disputeKitID]; - - // [0, 1] value that determines how coherent the juror was in this round, in basis points. - uint256 degreeOfCoherence = disputeKit.getDegreeOfCoherence( - _params.disputeID, - _params.round, - _params.repartition, - _params.feePerJurorInRound, - _params.pnkAtStakePerJurorInRound - ); - if (degreeOfCoherence > ALPHA_DIVISOR) { - // Make sure the degree doesn't exceed 1, though it should be ensured by the dispute kit. - degreeOfCoherence = ALPHA_DIVISOR; - } - - // Fully coherent jurors won't be penalized. - uint256 penalty = (round.pnkAtStakePerJuror * (ALPHA_DIVISOR - degreeOfCoherence)) / ALPHA_DIVISOR; - - // Execute penalty through StakeController coordination - address account = round.drawnJurors[_params.repartition]; - (uint256 pnkBalance, uint256 actualPenalty) = stakeController.setJurorPenalty(account, penalty); - _params.pnkPenaltiesInRound += actualPenalty; - - emit TokenAndETHShift( - account, - _params.disputeID, - _params.round, - degreeOfCoherence, - -int256(actualPenalty), - 0, - round.feeToken - ); - - // Check if juror should be set inactive - bool inactive = !disputeKit.isVoteActive(_params.disputeID, _params.round, _params.repartition); - if (pnkBalance == 0 || inactive) { - uint256 pnkToWithdraw = stakeController.setJurorInactive(account); - if (pnkToWithdraw > 0) { - try vault.withdraw(account, pnkToWithdraw) { - // Successfully withdrew PNK for inactive juror - } catch (bytes memory reason) { - emit InactiveJurorWithdrawalFailed(account, pnkToWithdraw, reason); - } - } - } - - if (_params.repartition == _params.numberOfVotesInRound - 1 && _params.coherentCount == 0) { - // No one was coherent, send the rewards to the governor. - if (round.feeToken == NATIVE_CURRENCY) { - // The dispute fees were paid in ETH - payable(governor).send(round.totalFeesForJurors); - } else { - // The dispute fees were paid in ERC20 - round.feeToken.safeTransfer(governor, round.totalFeesForJurors); - } - vault.transferReward(governor, _params.pnkPenaltiesInRound); - emit LeftoverRewardSent( - _params.disputeID, - _params.round, - _params.pnkPenaltiesInRound, - round.totalFeesForJurors, - round.feeToken - ); - } - return _params.pnkPenaltiesInRound; - } - - /// @dev Distribute the PNKs at stake and the dispute fees for the specific round of the dispute, rewards only. - /// @param _params The parameters for the execution, see `ExecuteParams`. - function _executeRewards(ExecuteParams memory _params) internal { - Dispute storage dispute = disputes[_params.disputeID]; - Round storage round = dispute.rounds[_params.round]; - IDisputeKit disputeKit = disputeKits[round.disputeKitID]; - - // [0, 1] value that determines how coherent the juror was in this round, in basis points. - uint256 degreeOfCoherence = disputeKit.getDegreeOfCoherence( - _params.disputeID, - _params.round, - _params.repartition % _params.numberOfVotesInRound, - _params.feePerJurorInRound, - _params.pnkAtStakePerJurorInRound - ); - - // Make sure the degree doesn't exceed 1, though it should be ensured by the dispute kit. - if (degreeOfCoherence > ALPHA_DIVISOR) { - degreeOfCoherence = ALPHA_DIVISOR; - } - - address account = round.drawnJurors[_params.repartition % _params.numberOfVotesInRound]; - uint256 pnkLocked = (round.pnkAtStakePerJuror * degreeOfCoherence) / ALPHA_DIVISOR; - - // Release the rest of the PNKs of the juror for this round. - stakeController.unlockStake(account, pnkLocked); - - // Transfer the rewards - uint256 pnkReward = ((_params.pnkPenaltiesInRound / _params.coherentCount) * degreeOfCoherence) / ALPHA_DIVISOR; - round.sumPnkRewardPaid += pnkReward; - uint256 feeReward = ((round.totalFeesForJurors / _params.coherentCount) * degreeOfCoherence) / ALPHA_DIVISOR; - round.sumFeeRewardPaid += feeReward; - vault.transferReward(account, pnkReward); - if (round.feeToken == NATIVE_CURRENCY) { - // The dispute fees were paid in ETH - payable(account).send(feeReward); - } else { - // The dispute fees were paid in ERC20 - round.feeToken.safeTransfer(account, feeReward); - } - emit TokenAndETHShift( - account, - _params.disputeID, - _params.round, - degreeOfCoherence, - int256(pnkReward), - int256(feeReward), - round.feeToken - ); - - // Transfer any residual rewards to the governor. It may happen due to partial coherence of the jurors. - if (_params.repartition == _params.numberOfVotesInRound * 2 - 1) { - uint256 leftoverPnkReward = _params.pnkPenaltiesInRound - round.sumPnkRewardPaid; - uint256 leftoverFeeReward = round.totalFeesForJurors - round.sumFeeRewardPaid; - if (leftoverPnkReward != 0 || leftoverFeeReward != 0) { - if (leftoverPnkReward != 0) { - vault.transferReward(governor, leftoverPnkReward); - } - if (leftoverFeeReward != 0) { - if (round.feeToken == NATIVE_CURRENCY) { - // The dispute fees were paid in ETH - payable(governor).send(leftoverFeeReward); - } else { - // The dispute fees were paid in ERC20 - round.feeToken.safeTransfer(governor, leftoverFeeReward); - } - } - emit LeftoverRewardSent( - _params.disputeID, - _params.round, - leftoverPnkReward, - leftoverFeeReward, - round.feeToken - ); - } - } - } - - /// @dev Executes a specified dispute's ruling. - /// @param _disputeID The ID of the dispute. - function executeRuling(uint256 _disputeID) external { - Dispute storage dispute = disputes[_disputeID]; - if (dispute.period != Period.execution) revert NotExecutionPeriod(); - if (dispute.ruled) revert RulingAlreadyExecuted(); - - (uint256 winningChoice, , ) = currentRuling(_disputeID); - dispute.ruled = true; - emit Ruling(dispute.arbitrated, _disputeID, winningChoice); - dispute.arbitrated.rule(_disputeID, winningChoice); - } - - // ************************************* // - // * Public Views * // - // ************************************* // - - /// @dev Compute the cost of arbitration denominated in ETH. - /// It is recommended not to increase it often, as it can be highly time and gas consuming for the arbitrated contracts to cope with fee augmentation. - /// @param _extraData Additional info about the dispute. We use it to pass the ID of the dispute's court (first 32 bytes), the minimum number of jurors required (next 32 bytes) and the ID of the specific dispute kit (last 32 bytes). - /// @return cost The arbitration cost in ETH. - function arbitrationCost(bytes memory _extraData) public view override returns (uint256 cost) { - (uint96 courtID, uint256 minJurors, ) = _extraDataToCourtIDMinJurorsDisputeKit(_extraData); - cost = courts[courtID].feeForJuror * minJurors; - } - - /// @dev Compute the cost of arbitration denominated in `_feeToken`. - /// It is recommended not to increase it often, as it can be highly time and gas consuming for the arbitrated contracts to cope with fee augmentation. - /// @param _extraData Additional info about the dispute. We use it to pass the ID of the dispute's court (first 32 bytes), the minimum number of jurors required (next 32 bytes) and the ID of the specific dispute kit (last 32 bytes). - /// @param _feeToken The ERC20 token used to pay fees. - /// @return cost The arbitration cost in `_feeToken`. - function arbitrationCost(bytes calldata _extraData, IERC20 _feeToken) public view override returns (uint256 cost) { - cost = convertEthToTokenAmount(_feeToken, arbitrationCost(_extraData)); - } - - /// @dev Gets the cost of appealing a specified dispute. - /// @param _disputeID The ID of the dispute. - /// @return cost The appeal cost. - function appealCost(uint256 _disputeID) public view returns (uint256 cost) { - Dispute storage dispute = disputes[_disputeID]; - Round storage round = dispute.rounds[dispute.rounds.length - 1]; - Court storage court = courts[dispute.courtID]; - if (round.nbVotes >= court.jurorsForCourtJump) { - // Jump to parent court. - if (dispute.courtID == GENERAL_COURT) { - // TODO: Handle the forking when appealed in General court. - cost = NON_PAYABLE_AMOUNT; // Get the cost of the parent court. - } else { - cost = courts[court.parent].feeForJuror * ((round.nbVotes * 2) + 1); - } - } else { - // Stay in current court. - cost = court.feeForJuror * ((round.nbVotes * 2) + 1); - } - } - - /// @dev Gets the start and the end of a specified dispute's current appeal period. - /// @param _disputeID The ID of the dispute. - /// @return start The start of the appeal period. - /// @return end The end of the appeal period. - function appealPeriod(uint256 _disputeID) public view returns (uint256 start, uint256 end) { - Dispute storage dispute = disputes[_disputeID]; - if (dispute.period == Period.appeal) { - start = dispute.lastPeriodChange; - end = dispute.lastPeriodChange + courts[dispute.courtID].timesPerPeriod[uint256(Period.appeal)]; - } else { - start = 0; - end = 0; - } - } - - /// @dev Gets the current ruling of a specified dispute. - /// @param _disputeID The ID of the dispute. - /// @return ruling The current ruling. - /// @return tied Whether it's a tie or not. - /// @return overridden Whether the ruling was overridden by appeal funding or not. - function currentRuling(uint256 _disputeID) public view returns (uint256 ruling, bool tied, bool overridden) { - Dispute storage dispute = disputes[_disputeID]; - Round storage round = dispute.rounds[dispute.rounds.length - 1]; - IDisputeKit disputeKit = disputeKits[round.disputeKitID]; - (ruling, tied, overridden) = disputeKit.currentRuling(_disputeID); - } - - /// @dev Gets the round info for a specified dispute and round. - /// @dev This function must not be called from a non-view function because it returns a dynamic array which might be very large, theoretically exceeding the block gas limit. - /// @param _disputeID The ID of the dispute. - /// @param _round The round to get the info for. - /// @return round The round info. - function getRoundInfo(uint256 _disputeID, uint256 _round) external view returns (Round memory) { - return disputes[_disputeID].rounds[_round]; - } - - /// @dev Gets the PNK at stake per juror for a specified dispute and round. - /// @param _disputeID The ID of the dispute. - /// @param _round The round to get the info for. - /// @return pnkAtStakePerJuror The PNK at stake per juror. - function getPnkAtStakePerJuror(uint256 _disputeID, uint256 _round) external view returns (uint256) { - return disputes[_disputeID].rounds[_round].pnkAtStakePerJuror; - } - - /// @dev Gets the number of rounds for a specified dispute. - /// @param _disputeID The ID of the dispute. - /// @return The number of rounds. - function getNumberOfRounds(uint256 _disputeID) external view returns (uint256) { - return disputes[_disputeID].rounds.length; - } - - /// @dev Checks if a given dispute kit is supported by a given court. - /// @param _courtID The ID of the court to check the support for. - /// @param _disputeKitID The ID of the dispute kit to check the support for. - /// @return Whether the dispute kit is supported or not. - function isSupported(uint96 _courtID, uint256 _disputeKitID) external view returns (bool) { - return courts[_courtID].supportedDisputeKits[_disputeKitID]; - } - - /// @dev Gets the timesPerPeriod array for a given court. - /// @param _courtID The ID of the court to get the times from. - /// @return timesPerPeriod The timesPerPeriod array for the given court. - function getTimesPerPeriod(uint96 _courtID) external view returns (uint256[4] memory timesPerPeriod) { - timesPerPeriod = courts[_courtID].timesPerPeriod; - } - - // ************************************* // - // * Public Views for Dispute Kits * // - // ************************************* // - - /// @dev Gets the number of votes permitted for the specified dispute in the latest round. - /// @param _disputeID The ID of the dispute. - function getNumberOfVotes(uint256 _disputeID) external view returns (uint256) { - Dispute storage dispute = disputes[_disputeID]; - return dispute.rounds[dispute.rounds.length - 1].nbVotes; - } - - /// @dev Returns true if the dispute kit will be switched to a parent DK. - /// @param _disputeID The ID of the dispute. - /// @return Whether DK will be switched or not. - function isDisputeKitJumping(uint256 _disputeID) external view returns (bool) { - Dispute storage dispute = disputes[_disputeID]; - Round storage round = dispute.rounds[dispute.rounds.length - 1]; - Court storage court = courts[dispute.courtID]; - - if (round.nbVotes < court.jurorsForCourtJump) { - return false; - } - - // Jump if the parent court doesn't support the current DK. - return !courts[court.parent].supportedDisputeKits[round.disputeKitID]; - } - - function getDisputeKitsLength() external view returns (uint256) { - return disputeKits.length; - } - - function convertEthToTokenAmount(IERC20 _toToken, uint256 _amountInEth) public view returns (uint256) { - return (_amountInEth * 10 ** currencyRates[_toToken].rateDecimals) / currencyRates[_toToken].rateInEth; - } - - // ************************************* // - // * Internal * // - // ************************************* // - - /// @dev Toggles the dispute kit support for a given court. - /// @param _courtID The ID of the court to toggle the support for. - /// @param _disputeKitID The ID of the dispute kit to toggle the support for. - /// @param _enable Whether to enable or disable the support. Note that classic dispute kit should always be enabled. - function _enableDisputeKit(uint96 _courtID, uint256 _disputeKitID, bool _enable) internal { - courts[_courtID].supportedDisputeKits[_disputeKitID] = _enable; - emit DisputeKitEnabled(_courtID, _disputeKitID, _enable); - } - - /// @dev If called only once then set _onError to Revert, otherwise for batch staking set it to Return - /// @param _account The account to set the stake for. - /// @param _courtID The ID of the court to set the stake for. - /// @param _newStake The new stake. - /// @return success Whether the stake was successfully set or not. - function _setStake(address _account, uint96 _courtID, uint256 _newStake) internal returns (bool success) { - if (_courtID == FORKING_COURT || _courtID >= courts.length) { - revert StakingNotPossibleInThisCourt(); - } - if (_newStake != 0 && _newStake < courts[_courtID].minStake) { - revert StakingLessThanCourtMinStake(); - } - (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) = stakeController.validateStake( - _account, - _courtID, - _newStake - ); - if (stakingResult == StakingResult.Delayed) { - return true; - } - success = true; - if (pnkDeposit > 0) { - try vault.deposit(_account, pnkDeposit) {} catch { - success = false; - } - } - if (pnkWithdrawal > 0) { - try vault.withdraw(_account, pnkWithdrawal) {} catch { - success = false; - } - } - if (success) { - stakeController.setStake(_account, _courtID, _newStake, pnkDeposit, pnkWithdrawal); - } - return success; - } - - /// @dev Gets a court ID, the minimum number of jurors and an ID of a dispute kit from a specified extra data bytes array. - /// Note that if extradata contains an incorrect value then this value will be switched to default. - /// @param _extraData The extra data bytes array. The first 32 bytes are the court ID, the next are the minimum number of jurors and the last are the dispute kit ID. - /// @return courtID The court ID. - /// @return minJurors The minimum number of jurors required. - /// @return disputeKitID The ID of the dispute kit. - function _extraDataToCourtIDMinJurorsDisputeKit( - bytes memory _extraData - ) internal view returns (uint96 courtID, uint256 minJurors, uint256 disputeKitID) { - // Note that if the extradata doesn't contain 32 bytes for the dispute kit ID it'll return the default 0 index. - if (_extraData.length >= 64) { - assembly { - // solium-disable-line security/no-inline-assembly - courtID := mload(add(_extraData, 0x20)) - minJurors := mload(add(_extraData, 0x40)) - disputeKitID := mload(add(_extraData, 0x60)) - } - if (courtID == FORKING_COURT || courtID >= courts.length) { - courtID = GENERAL_COURT; - } - if (minJurors == 0) { - minJurors = DEFAULT_NB_OF_JURORS; - } - if (disputeKitID == NULL_DISPUTE_KIT || disputeKitID >= disputeKits.length) { - disputeKitID = DISPUTE_KIT_CLASSIC; // 0 index is not used. - } - } else { - courtID = GENERAL_COURT; - minJurors = DEFAULT_NB_OF_JURORS; - disputeKitID = DISPUTE_KIT_CLASSIC; - } - } - - // ************************************* // - // * Errors * // - // ************************************* // - - error GovernorOnly(); - error GuardianOrGovernorOnly(); - error DisputeKitOnly(); - error StakeControllerOnly(); - error UnsuccessfulCall(); - error InvalidDisputKitParent(); - error MinStakeLowerThanParentCourt(); - error UnsupportedDisputeKit(); - error InvalidForkingCourtAsParent(); - error WrongDisputeKitIndex(); - error CannotDisableClassicDK(); - error StakingNotPossibleInThisCourt(); - error StakingLessThanCourtMinStake(); - error StakingTransferFailed(); - error UnstakingTransferFailed(); - error ArbitrationFeesNotEnough(); - error DisputeKitNotSupportedByCourt(); - error MustSupportDisputeKitClassic(); - error TokenNotAccepted(); - error EvidenceNotPassedAndNotAppeal(); - error DisputeStillDrawing(); - error CommitPeriodNotPassed(); - error VotePeriodNotPassed(); - error AppealPeriodNotPassed(); - error NotEvidencePeriod(); - error AppealFeesNotEnough(); - error DisputeNotAppealable(); - error NotExecutionPeriod(); - error RulingAlreadyExecuted(); - error DisputePeriodIsFinal(); - error TransferFailed(); - error WhenNotPausedOnly(); - error WhenPausedOnly(); -} diff --git a/contracts/src/arbitration/SortitionSumTreeBase.sol b/contracts/src/arbitration/SortitionSumTreeBase.sol index c5913231f..4c34211c3 100644 --- a/contracts/src/arbitration/SortitionSumTreeBase.sol +++ b/contracts/src/arbitration/SortitionSumTreeBase.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.24; import {ISortitionSumTree} from "./interfaces/ISortitionSumTree.sol"; import {IStakeController} from "./interfaces/IStakeController.sol"; -import {KlerosCoreXBase} from "./KlerosCoreXBase.sol"; +import {KlerosCoreBase} from "./KlerosCoreBase.sol"; import {Initializable} from "../proxy/Initializable.sol"; import {UUPSProxiable} from "../proxy/UUPSProxiable.sol"; import "../libraries/Constants.sol"; @@ -101,7 +101,7 @@ abstract contract SortitionSumTreeBase is ISortitionSumTree, Initializable, UUPS bytes32 stakePathID = _accountAndCourtIDToStakePathID(_account, _courtID); bool finished = false; uint96 currentCourtID = _courtID; - KlerosCoreXBase core = stakeController.core(); + KlerosCoreBase core = stakeController.core(); while (!finished) { _set(bytes32(uint256(currentCourtID)), _newStake, stakePathID); diff --git a/contracts/src/arbitration/StakeController.sol b/contracts/src/arbitration/StakeController.sol index 88157a1f2..14b9040cd 100644 --- a/contracts/src/arbitration/StakeController.sol +++ b/contracts/src/arbitration/StakeController.sol @@ -5,7 +5,7 @@ pragma solidity 0.8.24; import {StakeControllerBase} from "./StakeControllerBase.sol"; import {IVault} from "./interfaces/IVault.sol"; import {ISortitionSumTree} from "./interfaces/ISortitionSumTree.sol"; -import {KlerosCoreXBase} from "./KlerosCoreXBase.sol"; +import {KlerosCoreBase} from "./KlerosCoreBase.sol"; import {RNG} from "../rng/RNG.sol"; /// @title StakeController @@ -34,7 +34,7 @@ contract StakeController is StakeControllerBase { /// @param _rngLookahead The RNG lookahead time. function initialize( address _governor, - KlerosCoreXBase _core, + KlerosCoreBase _core, IVault _vault, ISortitionSumTree _sortition, uint256 _minStakingTime, diff --git a/contracts/src/arbitration/StakeControllerBase.sol b/contracts/src/arbitration/StakeControllerBase.sol index 76fc24454..e593c1db2 100644 --- a/contracts/src/arbitration/StakeControllerBase.sol +++ b/contracts/src/arbitration/StakeControllerBase.sol @@ -6,7 +6,7 @@ import {IStakeController} from "./interfaces/IStakeController.sol"; import {IVault} from "./interfaces/IVault.sol"; import {ISortitionSumTree} from "./interfaces/ISortitionSumTree.sol"; import {IDisputeKit} from "./interfaces/IDisputeKit.sol"; -import {KlerosCoreXBase} from "./KlerosCoreXBase.sol"; +import {KlerosCoreBase} from "./KlerosCoreBase.sol"; import {Initializable} from "../proxy/Initializable.sol"; import {UUPSProxiable} from "../proxy/UUPSProxiable.sol"; import {RNG} from "../rng/RNG.sol"; @@ -37,7 +37,7 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr // ************************************* // address public governor; // The governor of the contract. - KlerosCoreXBase public core; // The core arbitrator contract. + KlerosCoreBase public core; // The core arbitrator contract. IVault public vault; // The PNK vault for token management. ISortitionSumTree public sortition; // The sortition sum tree for drawing. @@ -80,7 +80,7 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr function __StakeControllerBase_initialize( address _governor, - KlerosCoreXBase _core, + KlerosCoreBase _core, IVault _vault, ISortitionSumTree _sortition, uint256 _minStakingTime, diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitClassic.sol b/contracts/src/arbitration/dispute-kits/DisputeKitClassic.sol index 15a4188c4..95b225e91 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitClassic.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitClassic.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.24; -import {DisputeKitClassicBase, KlerosCoreX} from "./DisputeKitClassicBase.sol"; +import {DisputeKitClassicBase, KlerosCore} from "./DisputeKitClassicBase.sol"; /// @title DisputeKitClassic /// Dispute kit implementation of the Kleros v1 features including: @@ -25,7 +25,7 @@ contract DisputeKitClassic is DisputeKitClassicBase { /// @dev Initializer. /// @param _governor The governor's address. /// @param _core The KlerosCore arbitrator. - function initialize(address _governor, KlerosCoreX _core) external reinitializer(1) { + function initialize(address _governor, KlerosCore _core) external reinitializer(1) { __DisputeKitClassicBase_initialize(_governor, _core); } diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol index 32d4c0219..dbcf34365 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.24; -import {KlerosCoreX, KlerosCoreXBase, IDisputeKit, IStakeController} from "../KlerosCoreX.sol"; +import {KlerosCore, KlerosCoreBase, IDisputeKit, IStakeController} from "../KlerosCore.sol"; import {Initializable} from "../../proxy/Initializable.sol"; import {UUPSProxiable} from "../../proxy/UUPSProxiable.sol"; @@ -57,7 +57,7 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi uint256 public constant ONE_BASIS_POINT = 10000; // One basis point, for scaling. address public governor; // The governor of the contract. - KlerosCoreX public core; // The Kleros Core arbitrator + 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. @@ -141,7 +141,7 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi /// @dev Initializer. /// @param _governor The governor's address. /// @param _core The KlerosCore arbitrator. - function __DisputeKitClassicBase_initialize(address _governor, KlerosCoreX _core) internal onlyInitializing { + function __DisputeKitClassicBase_initialize(address _governor, KlerosCore _core) internal onlyInitializing { governor = _governor; core = _core; } @@ -172,7 +172,7 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi /// @dev Changes the `core` storage variable. /// @param _core The new value for the `core` storage variable. function changeCore(address _core) external onlyByGovernor { - core = KlerosCoreX(_core); + core = KlerosCore(_core); } // ************************************* // @@ -247,8 +247,8 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi uint256[] calldata _voteIDs, bytes32 _commit ) external notJumped(_coreDisputeID) { - (, , KlerosCoreXBase.Period period, , ) = core.disputes(_coreDisputeID); - require(period == KlerosCoreXBase.Period.commit, "The dispute should be in Commit period."); + (, , KlerosCoreBase.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]]; @@ -276,8 +276,8 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi uint256 _salt, string memory _justification ) external notJumped(_coreDisputeID) { - (, , KlerosCoreXBase.Period period, , ) = core.disputes(_coreDisputeID); - require(period == KlerosCoreXBase.Period.vote, "The dispute should be in Vote period."); + (, , KlerosCoreBase.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]]; @@ -456,9 +456,9 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi Round storage round = dispute.rounds[dispute.rounds.length - 1]; tied = round.tied; ruling = tied ? 0 : round.winningChoice; - (, , KlerosCoreXBase.Period period, , ) = core.disputes(_coreDisputeID); + (, , KlerosCoreBase.Period period, , ) = core.disputes(_coreDisputeID); // Override the final ruling if only one side funded the appeals. - if (period == KlerosCoreXBase.Period.execution) { + if (period == KlerosCoreBase.Period.execution) { uint256[] memory fundedChoices = getFundedChoices(_coreDisputeID); if (fundedChoices.length == 1) { ruling = fundedChoices[0]; diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitGated.sol b/contracts/src/arbitration/dispute-kits/DisputeKitGated.sol index 732b45032..6c9c0ab65 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitGated.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitGated.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.24; -import {DisputeKitClassicBase, KlerosCoreX} from "./DisputeKitClassicBase.sol"; +import {DisputeKitClassicBase, KlerosCore} from "./DisputeKitClassicBase.sol"; interface IBalanceHolder { /// @dev Returns the number of tokens in `owner` account. @@ -54,7 +54,7 @@ contract DisputeKitGated is DisputeKitClassicBase { /// @param _isERC1155 Whether the token is an ERC-1155 function initialize( address _governor, - KlerosCoreX _core, + KlerosCore _core, address _tokenGate, uint256 _tokenId, bool _isERC1155 diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitSybilResistant.sol b/contracts/src/arbitration/dispute-kits/DisputeKitSybilResistant.sol index 4e3c2f61f..7d3b90599 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitSybilResistant.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitSybilResistant.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.24; -import {DisputeKitClassicBase, KlerosCoreX} from "./DisputeKitClassicBase.sol"; +import {DisputeKitClassicBase, KlerosCore} from "./DisputeKitClassicBase.sol"; interface IProofOfHumanity { /// @dev Return true if the submission is registered and not expired. @@ -39,7 +39,7 @@ contract DisputeKitSybilResistant is DisputeKitClassicBase { /// @param _governor The governor's address. /// @param _core The KlerosCore arbitrator. /// @param _poh The Proof of Humanity registry. - function initialize(address _governor, KlerosCoreX _core, IProofOfHumanity _poh) external reinitializer(1) { + function initialize(address _governor, KlerosCore _core, IProofOfHumanity _poh) external reinitializer(1) { __DisputeKitClassicBase_initialize(_governor, _core); poh = _poh; singleDrawPerJuror = true; diff --git a/contracts/src/arbitration/interfaces/IStakeController.sol b/contracts/src/arbitration/interfaces/IStakeController.sol index badfb6e65..1486d8126 100644 --- a/contracts/src/arbitration/interfaces/IStakeController.sol +++ b/contracts/src/arbitration/interfaces/IStakeController.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.24; -import {KlerosCoreXBase} from "../KlerosCoreXBase.sol"; +import {KlerosCoreBase} from "../KlerosCoreBase.sol"; import "../../libraries/Constants.sol"; /// @title IStakeController @@ -200,5 +200,5 @@ interface IStakeController { /// @notice Get the core arbitrator contract /// @return The core contract - function core() external view returns (KlerosCoreXBase); + function core() external view returns (KlerosCoreBase); } diff --git a/contracts/src/arbitration/old/KlerosCore.sol b/contracts/src/arbitration/old/KlerosCore.sol deleted file mode 100644 index 287e4f685..000000000 --- a/contracts/src/arbitration/old/KlerosCore.sol +++ /dev/null @@ -1,72 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -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 { - string public constant override version = "0.9.4"; - - // ************************************* // - // * Constructor * // - // ************************************* // - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() { - _disableInitializers(); - } - - /// @dev Initializer (constructor equivalent for upgradable contracts). - /// @param _governor The governor's address. - /// @param _guardian The guardian's address. - /// @param _pinakion The address of the token contract. - /// @param _jurorProsecutionModule The address of the juror prosecution module. - /// @param _disputeKit The address of the default dispute kit. - /// @param _hiddenVotes The `hiddenVotes` property value of the general court. - /// @param _courtParameters Numeric parameters of General court (minStake, alpha, feeForJuror and jurorsForCourtJump respectively). - /// @param _timesPerPeriod The `timesPerPeriod` property value of the general court. - /// @param _sortitionExtraData The extra data for sortition module. - /// @param _sortitionModuleAddress The sortition module responsible for sortition of the jurors. - function initialize( - address _governor, - address _guardian, - IERC20 _pinakion, - address _jurorProsecutionModule, - IDisputeKit _disputeKit, - bool _hiddenVotes, - uint256[4] memory _courtParameters, - uint256[4] memory _timesPerPeriod, - bytes memory _sortitionExtraData, - ISortitionModule _sortitionModuleAddress - ) external reinitializer(1) { - __KlerosCoreBase_initialize( - _governor, - _guardian, - _pinakion, - _jurorProsecutionModule, - _disputeKit, - _hiddenVotes, - _courtParameters, - _timesPerPeriod, - _sortitionExtraData, - _sortitionModuleAddress - ); - } - - function initialize5() external reinitializer(5) { - // NOP - } - - // ************************************* // - // * Governance * // - // ************************************* // - - /// @dev Access Control to perform implementation upgrades (UUPS Proxiable) - /// Only the governor can perform upgrades (`onlyByGovernor`) - function _authorizeUpgrade(address) internal view override onlyByGovernor { - // NOP - } -} diff --git a/contracts/src/arbitration/old/KlerosCoreNeo.sol b/contracts/src/arbitration/old/KlerosCoreNeo.sol deleted file mode 100644 index 988e42a53..000000000 --- a/contracts/src/arbitration/old/KlerosCoreNeo.sol +++ /dev/null @@ -1,139 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import {KlerosCoreBase, IDisputeKit, ISortitionModule, IERC20, OnError, StakingResult} from "./KlerosCoreBase.sol"; -import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; - -/// @title KlerosCoreNeo -/// Core arbitrator contract for Kleros v2. -/// Note that this contract trusts the PNK token, the dispute kit and the sortition module contracts. -contract KlerosCoreNeo is KlerosCoreBase { - string public constant override version = "0.9.4"; - - // ************************************* // - // * Storage * // - // ************************************* // - - mapping(address => bool) public arbitrableWhitelist; // Arbitrable whitelist. - IERC721 public jurorNft; // Eligible jurors NFT. - - // ************************************* // - // * Constructor * // - // ************************************* // - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() { - _disableInitializers(); - } - - /// @dev Initializer (constructor equivalent for upgradable contracts). - /// @param _governor The governor's address. - /// @param _guardian The guardian's address. - /// @param _pinakion The address of the token contract. - /// @param _jurorProsecutionModule The address of the juror prosecution module. - /// @param _disputeKit The address of the default dispute kit. - /// @param _hiddenVotes The `hiddenVotes` property value of the general court. - /// @param _courtParameters Numeric parameters of General court (minStake, alpha, feeForJuror and jurorsForCourtJump respectively). - /// @param _timesPerPeriod The `timesPerPeriod` property value of the general court. - /// @param _sortitionExtraData The extra data for sortition module. - /// @param _sortitionModuleAddress The sortition module responsible for sortition of the jurors. - /// @param _jurorNft NFT contract to vet the jurors. - function initialize( - address _governor, - address _guardian, - IERC20 _pinakion, - address _jurorProsecutionModule, - IDisputeKit _disputeKit, - bool _hiddenVotes, - uint256[4] memory _courtParameters, - uint256[4] memory _timesPerPeriod, - bytes memory _sortitionExtraData, - ISortitionModule _sortitionModuleAddress, - IERC721 _jurorNft - ) external reinitializer(2) { - __KlerosCoreBase_initialize( - _governor, - _guardian, - _pinakion, - _jurorProsecutionModule, - _disputeKit, - _hiddenVotes, - _courtParameters, - _timesPerPeriod, - _sortitionExtraData, - _sortitionModuleAddress - ); - jurorNft = _jurorNft; - } - - function initialize5() external reinitializer(5) { - // NOP - } - - // ************************************* // - // * Governance * // - // ************************************* // - - /// @dev Access Control to perform implementation upgrades (UUPS Proxiable) - /// Only the governor can perform upgrades (`onlyByGovernor`) - function _authorizeUpgrade(address) internal view override onlyByGovernor { - // NOP - } - - /// @dev Changes the `jurorNft` storage variable. - /// @param _jurorNft The new value for the `jurorNft` storage variable. - function changeJurorNft(IERC721 _jurorNft) external onlyByGovernor { - jurorNft = _jurorNft; - } - - /// @dev Adds or removes an arbitrable from whitelist. - /// @param _arbitrable Arbitrable address. - /// @param _allowed Whether add or remove permission. - function changeArbitrableWhitelist(address _arbitrable, bool _allowed) external onlyByGovernor { - arbitrableWhitelist[_arbitrable] = _allowed; - } - - // ************************************* // - // * State Modifiers * // - // ************************************* // - - /// @dev Sets the caller's stake in a court. - /// Note: Staking and unstaking is forbidden during pause. - /// @param _courtID The ID of the court. - /// @param _newStake The new stake. - /// Note that the existing delayed stake will be nullified as non-relevant. - function setStake(uint96 _courtID, uint256 _newStake) external override whenNotPaused { - if (jurorNft.balanceOf(msg.sender) == 0) revert NotEligibleForStaking(); - super._setStake(msg.sender, _courtID, _newStake, false, OnError.Revert); - } - - // ************************************* // - // * Internal * // - // ************************************* // - - function _createDispute( - uint256 _numberOfChoices, - bytes memory _extraData, - IERC20 _feeToken, - uint256 _feeAmount - ) internal override returns (uint256 disputeID) { - if (!arbitrableWhitelist[msg.sender]) revert ArbitrableNotWhitelisted(); - return super._createDispute(_numberOfChoices, _extraData, _feeToken, _feeAmount); - } - - function _stakingFailed(OnError _onError, StakingResult _result) internal pure override { - super._stakingFailed(_onError, _result); - if (_result == StakingResult.CannotStakeMoreThanMaxStakePerJuror) revert StakingMoreThanMaxStakePerJuror(); - if (_result == StakingResult.CannotStakeMoreThanMaxTotalStaked) revert StakingMoreThanMaxTotalStaked(); - } - - // ************************************* // - // * Errors * // - // ************************************* // - - error NotEligibleForStaking(); - error StakingMoreThanMaxStakePerJuror(); - error StakingMoreThanMaxTotalStaked(); - error ArbitrableNotWhitelisted(); -} diff --git a/contracts/src/arbitration/old/SortitionModule.sol b/contracts/src/arbitration/old/SortitionModule.sol deleted file mode 100644 index 3c076791f..000000000 --- a/contracts/src/arbitration/old/SortitionModule.sol +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-License-Identifier: MIT - -/** - * @custom:authors: [@epiqueras, @unknownunknown1, @jaybuidl, @shotaronowhere] - * @custom:reviewers: [] - * @custom:auditors: [] - * @custom:bounties: [] - * @custom:deployments: [] - */ - -pragma solidity 0.8.24; - -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 { - string public constant override version = "0.8.0"; - - // ************************************* // - // * Constructor * // - // ************************************* // - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() { - _disableInitializers(); - } - - /// @dev Initializer (constructor equivalent for upgradable contracts). - /// @param _governor The governor. - /// @param _core The KlerosCore. - /// @param _minStakingTime Minimal time to stake - /// @param _maxDrawingTime Time after which the drawing phase can be switched - /// @param _rng The random number generator. - /// @param _rngLookahead Lookahead value for rng. - function initialize( - address _governor, - KlerosCore _core, - uint256 _minStakingTime, - uint256 _maxDrawingTime, - RNG _rng, - uint256 _rngLookahead - ) external reinitializer(1) { - __SortitionModuleBase_initialize(_governor, _core, _minStakingTime, _maxDrawingTime, _rng, _rngLookahead); - } - - function initialize3() external reinitializer(3) { - // NOP - } - - // ************************************* // - // * Governance * // - // ************************************* // - - /// @dev Access Control to perform implementation upgrades (UUPS Proxiable) - /// Only the governor can perform upgrades (`onlyByGovernor`) - function _authorizeUpgrade(address) internal view virtual override onlyByGovernor { - // NOP - } -} diff --git a/contracts/src/arbitration/old/SortitionModuleBase.sol b/contracts/src/arbitration/old/SortitionModuleBase.sol deleted file mode 100644 index 6634fdeb6..000000000 --- a/contracts/src/arbitration/old/SortitionModuleBase.sol +++ /dev/null @@ -1,734 +0,0 @@ -// SPDX-License-Identifier: MIT - -/** - * @custom:authors: [@epiqueras, @unknownunknown1, @jaybuidl, @shotaronowhere] - * @custom:reviewers: [] - * @custom:auditors: [] - * @custom:bounties: [] - * @custom:deployments: [] - */ - -pragma solidity 0.8.24; - -import {KlerosCore} from "./KlerosCore.sol"; -import {ISortitionModule} from "../interfaces/ISortitionModule.sol"; -import {IDisputeKit} from "../interfaces/IDisputeKit.sol"; -import {Initializable} from "../../proxy/Initializable.sol"; -import {UUPSProxiable} from "../../proxy/UUPSProxiable.sol"; -import {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, Initializable, UUPSProxiable { - // ************************************* // - // * Enums / Structs * // - // ************************************* // - - enum PreStakeHookResult { - ok, // Correct phase. All checks are passed. - stakeDelayedAlreadyTransferred, // Wrong phase but stake is increased, so transfer the tokens without updating the drawing chance. - stakeDelayedNotTransferred, // Wrong phase and stake is decreased. Delay the token transfer and drawing chance update. - failed // Checks didn't pass. Do no changes. - } - - struct SortitionSumTree { - uint256 K; // The maximum number of children per node. - // We use this to keep track of vacant positions in the tree after removing a leaf. This is for keeping the tree as balanced as possible without spending gas on moving nodes around. - uint256[] stack; - uint256[] nodes; - // Two-way mapping of IDs to node indexes. Note that node index 0 is reserved for the root node, and means the ID does not have a node. - mapping(bytes32 => uint256) IDsToNodeIndexes; - mapping(uint256 => bytes32) nodeIndexesToIDs; - } - - struct DelayedStake { - address account; // The address of the juror. - uint96 courtID; // The ID of the court. - uint256 stake; // The new stake. - bool alreadyTransferred; // True if tokens were already transferred before delayed stake's execution. - } - - struct Juror { - uint96[] courtIDs; // The IDs of courts where the juror's stake path ends. A stake path is a path from the general court to a court the juror directly staked in using `_setStake`. - uint256 stakedPnk; // The juror's total amount of tokens staked in subcourts. Reflects actual pnk balance. - uint256 lockedPnk; // The juror's total amount of tokens locked in disputes. Can reflect actual pnk balance when stakedPnk are fully withdrawn. - } - - // ************************************* // - // * Storage * // - // ************************************* // - - address public governor; // The governor of the contract. - KlerosCore public core; // The core arbitrator contract. - Phase public phase; // The current phase. - uint256 public minStakingTime; // The time after which the phase can be switched to Drawing if there are open disputes. - uint256 public maxDrawingTime; // The time after which the phase can be switched back to Staking. - uint256 public lastPhaseChange; // The last time the phase was changed. - uint256 public randomNumberRequestBlock; // Number of the block when RNG request was made. - uint256 public disputesWithoutJurors; // The number of disputes that have not finished drawing jurors. - RNG public rng; // The random number generator. - uint256 public randomNumber; // Random number returned by RNG. - uint256 public rngLookahead; // Minimal block distance between requesting and obtaining a random number. - uint256 public delayedStakeWriteIndex; // The index of the last `delayedStake` item that was written to the array. 0 index is skipped. - uint256 public delayedStakeReadIndex; // The index of the next `delayedStake` item that should be processed. Starts at 1 because 0 index is skipped. - mapping(bytes32 treeHash => SortitionSumTree) sortitionSumTrees; // The mapping trees by keys. - mapping(address account => Juror) public jurors; // The jurors. - mapping(uint256 => DelayedStake) public delayedStakes; // Stores the stakes that were changed during Drawing phase, to update them when the phase is switched to Staking. - mapping(address jurorAccount => mapping(uint96 courtId => uint256)) public latestDelayedStakeIndex; // Maps the juror to its latest delayed stake. If there is already a delayed stake for this juror then it'll be replaced. latestDelayedStakeIndex[juror][courtID]. - - // ************************************* // - // * Events * // - // ************************************* // - - /// @notice Emitted when a juror stakes in a court. - /// @param _address The address of the juror. - /// @param _courtID The ID of the court. - /// @param _amount The amount of tokens staked in the court. - /// @param _amountAllCourts The amount of tokens staked in all courts. - event StakeSet(address indexed _address, uint256 _courtID, uint256 _amount, uint256 _amountAllCourts); - - /// @notice Emitted when a juror's stake is delayed and tokens are not transferred yet. - /// @param _address The address of the juror. - /// @param _courtID The ID of the court. - /// @param _amount The amount of tokens staked in the court. - event StakeDelayedNotTransferred(address indexed _address, uint256 _courtID, uint256 _amount); - - /// @notice Emitted when a juror's stake is delayed and tokens are already deposited. - /// @param _address The address of the juror. - /// @param _courtID The ID of the court. - /// @param _amount The amount of tokens staked in the court. - event StakeDelayedAlreadyTransferredDeposited(address indexed _address, uint256 _courtID, uint256 _amount); - - /// @notice Emitted when a juror's stake is delayed and tokens are already withdrawn. - /// @param _address The address of the juror. - /// @param _courtID The ID of the court. - /// @param _amount The amount of tokens withdrawn. - event StakeDelayedAlreadyTransferredWithdrawn(address indexed _address, uint96 indexed _courtID, uint256 _amount); - - /// @notice Emitted when a juror's stake is locked. - /// @param _address The address of the juror. - /// @param _relativeAmount The amount of tokens locked. - /// @param _unlock Whether the stake is locked or unlocked. - event StakeLocked(address indexed _address, uint256 _relativeAmount, bool _unlock); - - // ************************************* // - // * Constructor * // - // ************************************* // - - function __SortitionModuleBase_initialize( - address _governor, - KlerosCore _core, - uint256 _minStakingTime, - uint256 _maxDrawingTime, - RNG _rng, - uint256 _rngLookahead - ) internal onlyInitializing { - governor = _governor; - core = _core; - minStakingTime = _minStakingTime; - maxDrawingTime = _maxDrawingTime; - lastPhaseChange = block.timestamp; - rng = _rng; - rngLookahead = _rngLookahead; - delayedStakeReadIndex = 1; - } - - // ************************************* // - // * Function Modifiers * // - // ************************************* // - - modifier onlyByGovernor() { - require(address(governor) == msg.sender, "Access not allowed: Governor only."); - _; - } - - modifier onlyByCore() { - require(address(core) == msg.sender, "Access not allowed: KlerosCore only."); - _; - } - - // ************************************* // - // * Governance * // - // ************************************* // - - /// @dev Changes the governor of the contract. - /// @param _governor The new governor. - function changeGovernor(address _governor) external onlyByGovernor { - governor = _governor; - } - - /// @dev Changes the `minStakingTime` storage variable. - /// @param _minStakingTime The new value for the `minStakingTime` storage variable. - function changeMinStakingTime(uint256 _minStakingTime) external onlyByGovernor { - minStakingTime = _minStakingTime; - } - - /// @dev Changes the `maxDrawingTime` storage variable. - /// @param _maxDrawingTime The new value for the `maxDrawingTime` storage variable. - function changeMaxDrawingTime(uint256 _maxDrawingTime) external onlyByGovernor { - maxDrawingTime = _maxDrawingTime; - } - - /// @dev Changes the `_rng` and `_rngLookahead` storage variables. - /// @param _rng The new value for the `RNGenerator` storage variable. - /// @param _rngLookahead The new value for the `rngLookahead` storage variable. - function changeRandomNumberGenerator(RNG _rng, uint256 _rngLookahead) external onlyByGovernor { - rng = _rng; - rngLookahead = _rngLookahead; - if (phase == Phase.generating) { - rng.requestRandomness(block.number + rngLookahead); - randomNumberRequestBlock = block.number; - } - } - - // ************************************* // - // * State Modifiers * // - // ************************************* // - - function passPhase() external { - if (phase == Phase.staking) { - require( - block.timestamp - lastPhaseChange >= minStakingTime, - "The minimum staking time has not passed yet." - ); - require(disputesWithoutJurors > 0, "There are no disputes that need jurors."); - rng.requestRandomness(block.number + rngLookahead); - randomNumberRequestBlock = block.number; - phase = Phase.generating; - } else if (phase == Phase.generating) { - randomNumber = rng.receiveRandomness(randomNumberRequestBlock + rngLookahead); - require(randomNumber != 0, "Random number is not ready yet"); - phase = Phase.drawing; - } else if (phase == Phase.drawing) { - require( - disputesWithoutJurors == 0 || block.timestamp - lastPhaseChange >= maxDrawingTime, - "There are still disputes without jurors and the maximum drawing time has not passed yet." - ); - phase = Phase.staking; - } - - lastPhaseChange = block.timestamp; - emit NewPhase(phase); - } - - /// @dev Create a sortition sum tree at the specified key. - /// @param _key The key of the new tree. - /// @param _extraData Extra data that contains the number of children each node in the tree should have. - function createTree(bytes32 _key, bytes memory _extraData) external override onlyByCore { - SortitionSumTree storage tree = sortitionSumTrees[_key]; - uint256 K = _extraDataToTreeK(_extraData); - require(tree.K == 0, "Tree already exists."); - require(K > 1, "K must be greater than one."); - tree.K = K; - tree.nodes.push(0); - } - - /// @dev Executes the next delayed stakes. - /// @param _iterations The number of delayed stakes to execute. - function executeDelayedStakes(uint256 _iterations) external { - require(phase == Phase.staking, "Should be in Staking phase."); - require(delayedStakeWriteIndex >= delayedStakeReadIndex, "No delayed stake to execute."); - - uint256 actualIterations = (delayedStakeReadIndex + _iterations) - 1 > delayedStakeWriteIndex - ? (delayedStakeWriteIndex - delayedStakeReadIndex) + 1 - : _iterations; - uint256 newDelayedStakeReadIndex = delayedStakeReadIndex + actualIterations; - - for (uint256 i = delayedStakeReadIndex; i < newDelayedStakeReadIndex; i++) { - DelayedStake storage delayedStake = delayedStakes[i]; - // Delayed stake could've been manually removed already. In this case simply move on to the next item. - if (delayedStake.account != address(0)) { - // Nullify the index so the delayed stake won't get deleted before its own execution. - delete latestDelayedStakeIndex[delayedStake.account][delayedStake.courtID]; - core.setStakeBySortitionModule( - delayedStake.account, - delayedStake.courtID, - delayedStake.stake, - delayedStake.alreadyTransferred - ); - delete delayedStakes[i]; - } - } - delayedStakeReadIndex = newDelayedStakeReadIndex; - } - - function createDisputeHook(uint256 /*_disputeID*/, uint256 /*_roundID*/) external override onlyByCore { - disputesWithoutJurors++; - } - - function postDrawHook(uint256 /*_disputeID*/, uint256 /*_roundID*/) external override onlyByCore { - disputesWithoutJurors--; - } - - /// @dev Saves the random number to use it in sortition. Not used by this contract because the storing of the number is inlined in passPhase(). - /// @param _randomNumber Random number returned by RNG contract. - function notifyRandomNumber(uint256 _randomNumber) public override {} - - /// @dev Sets the specified juror's stake in a court. - /// `O(n + p * log_k(j))` where - /// `n` is the number of courts the juror has staked in, - /// `p` is the depth of the court tree, - /// `k` is the minimum number of children per node of one of these courts' sortition sum tree, - /// and `j` is the maximum number of jurors that ever staked in one of these courts simultaneously. - /// @param _account The address of the juror. - /// @param _courtID The ID of the court. - /// @param _newStake The new stake. - /// @param _alreadyTransferred True if the tokens were already transferred from juror. Only relevant for delayed stakes. - /// @return pnkDeposit The amount of PNK to be deposited. - /// @return pnkWithdrawal The amount of PNK to be withdrawn. - /// @return stakingResult The result of the staking operation. - function setStake( - address _account, - uint96 _courtID, - uint256 _newStake, - bool _alreadyTransferred - ) external override onlyByCore returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { - (pnkDeposit, pnkWithdrawal, stakingResult) = _setStake(_account, _courtID, _newStake, _alreadyTransferred); - } - - /// @dev Sets the specified juror's stake in a court. - /// Note: no state changes should be made when returning `succeeded` = false, otherwise delayed stakes might break invariants. - function _setStake( - address _account, - uint96 _courtID, - uint256 _newStake, - bool _alreadyTransferred - ) internal virtual returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { - Juror storage juror = jurors[_account]; - uint256 currentStake = stakeOf(_account, _courtID); - - uint256 nbCourts = juror.courtIDs.length; - if (currentStake == 0 && nbCourts >= MAX_STAKE_PATHS) { - return (0, 0, StakingResult.CannotStakeInMoreCourts); // Prevent staking beyond MAX_STAKE_PATHS but unstaking is always allowed. - } - - if (currentStake == 0 && _newStake == 0) { - return (0, 0, StakingResult.CannotStakeZeroWhenNoStake); // Forbid staking 0 amount when current stake is 0 to avoid flaky behaviour. - } - - pnkWithdrawal = _deleteDelayedStake(_courtID, _account); - if (phase != Phase.staking) { - // Store the stake change as delayed, to be applied when the phase switches back to Staking. - DelayedStake storage delayedStake = delayedStakes[++delayedStakeWriteIndex]; - delayedStake.account = _account; - delayedStake.courtID = _courtID; - delayedStake.stake = _newStake; - latestDelayedStakeIndex[_account][_courtID] = delayedStakeWriteIndex; - if (_newStake > currentStake) { - // PNK deposit: tokens are transferred now. - delayedStake.alreadyTransferred = true; - pnkDeposit = _increaseStake(juror, _courtID, _newStake, currentStake); - emit StakeDelayedAlreadyTransferredDeposited(_account, _courtID, _newStake); - } else { - // PNK withdrawal: tokens are not transferred yet. - emit StakeDelayedNotTransferred(_account, _courtID, _newStake); - } - return (pnkDeposit, pnkWithdrawal, StakingResult.Successful); - } - - // Current phase is Staking: set normal stakes or delayed stakes (which may have been already transferred). - if (_newStake >= currentStake) { - if (!_alreadyTransferred) { - pnkDeposit = _increaseStake(juror, _courtID, _newStake, currentStake); - } - } else { - pnkWithdrawal += _decreaseStake(juror, _courtID, _newStake, currentStake); - } - - // Update the sortition sum tree. - bytes32 stakePathID = _accountAndCourtIDToStakePathID(_account, _courtID); - bool finished = false; - uint96 currenCourtID = _courtID; - while (!finished) { - // Tokens are also implicitly staked in parent courts through sortition module to increase the chance of being drawn. - _set(bytes32(uint256(currenCourtID)), _newStake, stakePathID); - if (currenCourtID == GENERAL_COURT) { - finished = true; - } else { - (currenCourtID, , , , , , ) = core.courts(currenCourtID); // Get the parent court. - } - } - emit StakeSet(_account, _courtID, _newStake, juror.stakedPnk); - return (pnkDeposit, pnkWithdrawal, StakingResult.Successful); - } - - /// @dev Checks if there is already a delayed stake. In this case consider it irrelevant and remove it. - /// @param _courtID ID of the court. - /// @param _juror Juror whose stake to check. - function _deleteDelayedStake(uint96 _courtID, address _juror) internal returns (uint256 actualAmountToWithdraw) { - uint256 latestIndex = latestDelayedStakeIndex[_juror][_courtID]; - if (latestIndex != 0) { - DelayedStake storage delayedStake = delayedStakes[latestIndex]; - if (delayedStake.alreadyTransferred) { - // Sortition stake represents the stake value that was last updated during Staking phase. - uint256 sortitionStake = stakeOf(_juror, _courtID); - - // Withdraw the tokens that were added with the latest delayed stake. - uint256 amountToWithdraw = delayedStake.stake - sortitionStake; - actualAmountToWithdraw = amountToWithdraw; - Juror storage juror = jurors[_juror]; - if (juror.stakedPnk <= actualAmountToWithdraw) { - actualAmountToWithdraw = juror.stakedPnk; - } - - // StakePnk can become lower because of penalty. - juror.stakedPnk -= actualAmountToWithdraw; - emit StakeDelayedAlreadyTransferredWithdrawn(_juror, _courtID, amountToWithdraw); - - if (sortitionStake == 0) { - // Cleanup: delete the court otherwise it will be duplicated after staking. - for (uint256 i = juror.courtIDs.length; i > 0; i--) { - if (juror.courtIDs[i - 1] == _courtID) { - juror.courtIDs[i - 1] = juror.courtIDs[juror.courtIDs.length - 1]; - juror.courtIDs.pop(); - break; - } - } - } - } - delete delayedStakes[latestIndex]; - delete latestDelayedStakeIndex[_juror][_courtID]; - } - } - - function _increaseStake( - Juror storage juror, - uint96 _courtID, - uint256 _newStake, - uint256 _currentStake - ) internal returns (uint256 transferredAmount) { - // Stake increase - // When stakedPnk becomes lower than lockedPnk count the locked tokens in when transferring tokens from juror. - // (E.g. stakedPnk = 0, lockedPnk = 150) which can happen if the juror unstaked fully while having some tokens locked. - uint256 previouslyLocked = (juror.lockedPnk >= juror.stakedPnk) ? juror.lockedPnk - juror.stakedPnk : 0; // underflow guard - transferredAmount = (_newStake >= _currentStake + previouslyLocked) // underflow guard - ? _newStake - _currentStake - previouslyLocked - : 0; - if (_currentStake == 0) { - juror.courtIDs.push(_courtID); - } - // stakedPnk can become async with _currentStake (e.g. after penalty). - juror.stakedPnk = (juror.stakedPnk >= _currentStake) ? juror.stakedPnk - _currentStake + _newStake : _newStake; - } - - function _decreaseStake( - Juror storage juror, - uint96 _courtID, - uint256 _newStake, - uint256 _currentStake - ) internal returns (uint256 transferredAmount) { - // Stakes can be partially delayed only when stake is increased. - // Stake decrease: make sure locked tokens always stay in the contract. They can only be released during Execution. - if (juror.stakedPnk >= _currentStake - _newStake + juror.lockedPnk) { - // We have enough pnk staked to afford withdrawal while keeping locked tokens. - transferredAmount = _currentStake - _newStake; - } else if (juror.stakedPnk >= juror.lockedPnk) { - // Can't afford withdrawing the current stake fully. Take whatever is available while keeping locked tokens. - transferredAmount = juror.stakedPnk - juror.lockedPnk; - } - if (_newStake == 0) { - // Cleanup - for (uint256 i = juror.courtIDs.length; i > 0; i--) { - if (juror.courtIDs[i - 1] == _courtID) { - juror.courtIDs[i - 1] = juror.courtIDs[juror.courtIDs.length - 1]; - juror.courtIDs.pop(); - break; - } - } - } - // stakedPnk can become async with _currentStake (e.g. after penalty). - juror.stakedPnk = (juror.stakedPnk >= _currentStake) ? juror.stakedPnk - _currentStake + _newStake : _newStake; - } - - function lockStake(address _account, uint256 _relativeAmount) external override onlyByCore { - jurors[_account].lockedPnk += _relativeAmount; - emit StakeLocked(_account, _relativeAmount, false); - } - - function unlockStake(address _account, uint256 _relativeAmount) external override onlyByCore { - jurors[_account].lockedPnk -= _relativeAmount; - emit StakeLocked(_account, _relativeAmount, true); - } - - function penalizeStake(address _account, uint256 _relativeAmount) external override onlyByCore { - Juror storage juror = jurors[_account]; - if (juror.stakedPnk >= _relativeAmount) { - juror.stakedPnk -= _relativeAmount; - } else { - juror.stakedPnk = 0; // stakedPnk might become lower after manual unstaking, but lockedPnk will always cover the difference. - } - } - - /// @dev Unstakes the inactive juror from all courts. - /// `O(n * (p * log_k(j)) )` where - /// `n` is the number of courts the juror has staked in, - /// `p` is the depth of the court tree, - /// `k` is the minimum number of children per node of one of these courts' sortition sum tree, - /// and `j` is the maximum number of jurors that ever staked in one of these courts simultaneously. - /// @param _account The juror to unstake. - function setJurorInactive(address _account) external override onlyByCore { - uint96[] memory courtIDs = getJurorCourtIDs(_account); - for (uint256 j = courtIDs.length; j > 0; j--) { - core.setStakeBySortitionModule(_account, courtIDs[j - 1], 0, false); - } - } - - // ************************************* // - // * Public Views * // - // ************************************* // - - /// @dev Draw an ID from a tree using a number. - /// Note that this function reverts if the sum of all values in the tree is 0. - /// @param _key The key of the tree. - /// @param _coreDisputeID Index of the dispute in Kleros Core. - /// @param _nonce Nonce to hash with random number. - /// @return drawnAddress The drawn address. - /// `O(k * log_k(n))` where - /// `k` is the maximum number of children per node in the tree, - /// and `n` is the maximum number of nodes ever appended. - function draw( - bytes32 _key, - uint256 _coreDisputeID, - uint256 _nonce - ) public view override returns (address drawnAddress) { - require(phase == Phase.drawing, "Wrong phase."); - SortitionSumTree storage tree = sortitionSumTrees[_key]; - - if (tree.nodes[0] == 0) { - return address(0); // No jurors staked. - } - - uint256 currentDrawnNumber = uint256(keccak256(abi.encodePacked(randomNumber, _coreDisputeID, _nonce))) % - tree.nodes[0]; - - // While it still has children - uint256 treeIndex = 0; - while ((tree.K * treeIndex) + 1 < tree.nodes.length) { - for (uint256 i = 1; i <= tree.K; i++) { - // Loop over children. - uint256 nodeIndex = (tree.K * treeIndex) + i; - uint256 nodeValue = tree.nodes[nodeIndex]; - - if (currentDrawnNumber >= nodeValue) { - // Go to the next child. - currentDrawnNumber -= nodeValue; - } else { - // Pick this child. - treeIndex = nodeIndex; - break; - } - } - } - drawnAddress = _stakePathIDToAccount(tree.nodeIndexesToIDs[treeIndex]); - } - - /// @dev Get the stake of a juror in a court. - /// @param _juror The address of the juror. - /// @param _courtID The ID of the court. - /// @return value The stake of the juror in the court. - function stakeOf(address _juror, uint96 _courtID) public view returns (uint256) { - bytes32 stakePathID = _accountAndCourtIDToStakePathID(_juror, _courtID); - return stakeOf(bytes32(uint256(_courtID)), stakePathID); - } - - /// @dev Get the stake of a juror in a court. - /// @param _key The key of the tree, corresponding to a court. - /// @param _ID The stake path ID, corresponding to a juror. - /// @return The stake of the juror in the court. - function stakeOf(bytes32 _key, bytes32 _ID) public view returns (uint256) { - SortitionSumTree storage tree = sortitionSumTrees[_key]; - uint treeIndex = tree.IDsToNodeIndexes[_ID]; - if (treeIndex == 0) { - return 0; - } - return tree.nodes[treeIndex]; - } - - function getJurorBalance( - address _juror, - uint96 _courtID - ) - external - view - override - returns (uint256 totalStaked, uint256 totalLocked, uint256 stakedInCourt, uint256 nbCourts) - { - Juror storage juror = jurors[_juror]; - totalStaked = juror.stakedPnk; - totalLocked = juror.lockedPnk; - stakedInCourt = stakeOf(_juror, _courtID); - nbCourts = juror.courtIDs.length; - } - - /// @dev Gets the court identifiers where a specific `_juror` has staked. - /// @param _juror The address of the juror. - function getJurorCourtIDs(address _juror) public view override returns (uint96[] memory) { - return jurors[_juror].courtIDs; - } - - function isJurorStaked(address _juror) external view override returns (bool) { - return jurors[_juror].stakedPnk > 0; - } - - // ************************************* // - // * Internal * // - // ************************************* // - - /// @dev Update all the parents of a node. - /// @param _key The key of the tree to update. - /// @param _treeIndex The index of the node to start from. - /// @param _plusOrMinus Whether to add (true) or substract (false). - /// @param _value The value to add or substract. - /// `O(log_k(n))` where - /// `k` is the maximum number of children per node in the tree, - /// and `n` is the maximum number of nodes ever appended. - function _updateParents(bytes32 _key, uint256 _treeIndex, bool _plusOrMinus, uint256 _value) private { - SortitionSumTree storage tree = sortitionSumTrees[_key]; - - uint256 parentIndex = _treeIndex; - while (parentIndex != 0) { - parentIndex = (parentIndex - 1) / tree.K; - tree.nodes[parentIndex] = _plusOrMinus - ? tree.nodes[parentIndex] + _value - : tree.nodes[parentIndex] - _value; - } - } - - /// @dev Retrieves a juror's address from the stake path ID. - /// @param _stakePathID The stake path ID to unpack. - /// @return account The account. - function _stakePathIDToAccount(bytes32 _stakePathID) internal pure returns (address account) { - assembly { - // solium-disable-line security/no-inline-assembly - let ptr := mload(0x40) - for { - let i := 0x00 - } lt(i, 0x14) { - i := add(i, 0x01) - } { - mstore8(add(add(ptr, 0x0c), i), byte(i, _stakePathID)) - } - account := mload(ptr) - } - } - - function _extraDataToTreeK(bytes memory _extraData) internal pure returns (uint256 K) { - if (_extraData.length >= 32) { - assembly { - // solium-disable-line security/no-inline-assembly - K := mload(add(_extraData, 0x20)) - } - } else { - K = DEFAULT_K; - } - } - - /// @dev Set a value in a tree. - /// @param _key The key of the tree. - /// @param _value The new value. - /// @param _ID The ID of the value. - /// `O(log_k(n))` where - /// `k` is the maximum number of children per node in the tree, - /// and `n` is the maximum number of nodes ever appended. - function _set(bytes32 _key, uint256 _value, bytes32 _ID) internal { - SortitionSumTree storage tree = sortitionSumTrees[_key]; - uint256 treeIndex = tree.IDsToNodeIndexes[_ID]; - - if (treeIndex == 0) { - // No existing node. - if (_value != 0) { - // Non zero value. - // Append. - // Add node. - if (tree.stack.length == 0) { - // No vacant spots. - // Get the index and append the value. - treeIndex = tree.nodes.length; - tree.nodes.push(_value); - - // Potentially append a new node and make the parent a sum node. - if (treeIndex != 1 && (treeIndex - 1) % tree.K == 0) { - // Is first child. - uint256 parentIndex = treeIndex / tree.K; - bytes32 parentID = tree.nodeIndexesToIDs[parentIndex]; - uint256 newIndex = treeIndex + 1; - tree.nodes.push(tree.nodes[parentIndex]); - delete tree.nodeIndexesToIDs[parentIndex]; - tree.IDsToNodeIndexes[parentID] = newIndex; - tree.nodeIndexesToIDs[newIndex] = parentID; - } - } else { - // Some vacant spot. - // Pop the stack and append the value. - treeIndex = tree.stack[tree.stack.length - 1]; - tree.stack.pop(); - tree.nodes[treeIndex] = _value; - } - - // Add label. - tree.IDsToNodeIndexes[_ID] = treeIndex; - tree.nodeIndexesToIDs[treeIndex] = _ID; - - _updateParents(_key, treeIndex, true, _value); - } - } else { - // Existing node. - if (_value == 0) { - // Zero value. - // Remove. - // Remember value and set to 0. - uint256 value = tree.nodes[treeIndex]; - tree.nodes[treeIndex] = 0; - - // Push to stack. - tree.stack.push(treeIndex); - - // Clear label. - delete tree.IDsToNodeIndexes[_ID]; - delete tree.nodeIndexesToIDs[treeIndex]; - - _updateParents(_key, treeIndex, false, value); - } else if (_value != tree.nodes[treeIndex]) { - // New, non zero value. - // Set. - bool plusOrMinus = tree.nodes[treeIndex] <= _value; - uint256 plusOrMinusValue = plusOrMinus - ? _value - tree.nodes[treeIndex] - : tree.nodes[treeIndex] - _value; - tree.nodes[treeIndex] = _value; - - _updateParents(_key, treeIndex, plusOrMinus, plusOrMinusValue); - } - } - } - - /// @dev Packs an account and a court ID into a stake path ID. - /// @param _account The address of the juror to pack. - /// @param _courtID The court ID to pack. - /// @return stakePathID The stake path ID. - function _accountAndCourtIDToStakePathID( - address _account, - uint96 _courtID - ) internal pure returns (bytes32 stakePathID) { - assembly { - // solium-disable-line security/no-inline-assembly - let ptr := mload(0x40) - for { - let i := 0x00 - } lt(i, 0x14) { - i := add(i, 0x01) - } { - mstore8(add(ptr, i), byte(add(0x0c, i), _account)) - } - for { - let i := 0x14 - } lt(i, 0x20) { - i := add(i, 0x01) - } { - mstore8(add(ptr, i), byte(i, _courtID)) - } - stakePathID := mload(ptr) - } - } -} diff --git a/contracts/src/arbitration/old/SortitionModuleNeo.sol b/contracts/src/arbitration/old/SortitionModuleNeo.sol deleted file mode 100644 index 2e60307d2..000000000 --- a/contracts/src/arbitration/old/SortitionModuleNeo.sol +++ /dev/null @@ -1,119 +0,0 @@ -// SPDX-License-Identifier: MIT - -/** - * @custom:authors: [@jaybuidl, @unknownunknown1] - * @custom:reviewers: [] - * @custom:auditors: [] - * @custom:bounties: [] - * @custom:deployments: [] - */ - -pragma solidity 0.8.24; - -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 { - string public constant override version = "0.8.0"; - - // ************************************* // - // * Storage * // - // ************************************* // - - uint256 public maxStakePerJuror; - uint256 public maxTotalStaked; - uint256 public totalStaked; - - // ************************************* // - // * Constructor * // - // ************************************* // - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() { - _disableInitializers(); - } - - /// @dev Initializer (constructor equivalent for upgradable contracts). - /// @param _governor The governor. - /// @param _core The KlerosCore. - /// @param _minStakingTime Minimal time to stake - /// @param _maxDrawingTime Time after which the drawing phase can be switched - /// @param _rng The random number generator. - /// @param _rngLookahead Lookahead value for rng. - /// @param _maxStakePerJuror The maximum amount of PNK a juror can stake in a court. - /// @param _maxTotalStaked The maximum amount of PNK that can be staked in all courts. - function initialize( - address _governor, - KlerosCore _core, - uint256 _minStakingTime, - uint256 _maxDrawingTime, - RNG _rng, - uint256 _rngLookahead, - uint256 _maxStakePerJuror, - uint256 _maxTotalStaked - ) external reinitializer(2) { - __SortitionModuleBase_initialize(_governor, _core, _minStakingTime, _maxDrawingTime, _rng, _rngLookahead); - maxStakePerJuror = _maxStakePerJuror; - maxTotalStaked = _maxTotalStaked; - } - - function initialize3() external reinitializer(3) { - // NOP - } - - // ************************************* // - // * Governance * // - // ************************************* // - - /// @dev Access Control to perform implementation upgrades (UUPS Proxiable) - /// Only the governor can perform upgrades (`onlyByGovernor`) - function _authorizeUpgrade(address) internal view override onlyByGovernor { - // NOP - } - - function changeMaxStakePerJuror(uint256 _maxStakePerJuror) external onlyByGovernor { - maxStakePerJuror = _maxStakePerJuror; - } - - function changeMaxTotalStaked(uint256 _maxTotalStaked) external onlyByGovernor { - maxTotalStaked = _maxTotalStaked; - } - - // ************************************* // - // * State Modifiers * // - // ************************************* // - - function _setStake( - address _account, - uint96 _courtID, - uint256 _newStake, - bool _alreadyTransferred - ) internal override onlyByCore returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { - uint256 currentStake = stakeOf(_account, _courtID); - bool stakeIncrease = _newStake > currentStake; - uint256 stakeChange = stakeIncrease ? _newStake - currentStake : currentStake - _newStake; - Juror storage juror = jurors[_account]; - if (stakeIncrease && !_alreadyTransferred) { - if (juror.stakedPnk + stakeChange > maxStakePerJuror) { - return (0, 0, StakingResult.CannotStakeMoreThanMaxStakePerJuror); - } - if (totalStaked + stakeChange > maxTotalStaked) { - return (0, 0, StakingResult.CannotStakeMoreThanMaxTotalStaked); - } - } - if (phase == Phase.staking) { - if (stakeIncrease) { - totalStaked += stakeChange; - } else { - totalStaked -= stakeChange; - } - } - (pnkDeposit, pnkWithdrawal, stakingResult) = super._setStake( - _account, - _courtID, - _newStake, - _alreadyTransferred - ); - } -} diff --git a/contracts/src/test/KlerosCoreMock.sol b/contracts/src/test/KlerosCoreMock.sol index 03a907999..25eea2da3 100644 --- a/contracts/src/test/KlerosCoreMock.sol +++ b/contracts/src/test/KlerosCoreMock.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.24; -import "../arbitration/old/KlerosCore.sol"; +import "../arbitration/KlerosCore.sol"; /// @title KlerosCoreMock /// KlerosCore with view functions to use in Foundry tests. diff --git a/contracts/src/test/SortitionModuleMock.sol b/contracts/src/test/SortitionModuleMock.sol index 12ec17c39..c0224b8a7 100644 --- a/contracts/src/test/SortitionModuleMock.sol +++ b/contracts/src/test/SortitionModuleMock.sol @@ -10,11 +10,11 @@ pragma solidity 0.8.24; -import "../arbitration/old/SortitionModule.sol"; +import "../arbitration/SortitionSumTree.sol"; /// @title SortitionModuleMock /// @dev Adds getter functions to sortition module for Foundry tests. -contract SortitionModuleMock is SortitionModule { +contract SortitionModuleMock is SortitionSumTree { function getSortitionProperties(bytes32 _key) external view returns (uint256 K, uint256 nodeLength) { SortitionSumTree storage tree = sortitionSumTrees[_key]; K = tree.K; From a050ca6b54b280b544214fcc6bfd68121df4cc6c Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Mon, 9 Jun 2025 22:19:17 +0100 Subject: [PATCH 18/24] fix: access control on setStakeDelayed --- contracts/src/arbitration/StakeControllerBase.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/src/arbitration/StakeControllerBase.sol b/contracts/src/arbitration/StakeControllerBase.sol index e593c1db2..ff47251f2 100644 --- a/contracts/src/arbitration/StakeControllerBase.sol +++ b/contracts/src/arbitration/StakeControllerBase.sol @@ -266,7 +266,7 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr } /// @inheritdoc IStakeController - function setStakeDelayed(address _account, uint96 _courtID, uint256 _newStake) public override { + function setStakeDelayed(address _account, uint96 _courtID, uint256 _newStake) public override onlyByCore { DelayedStake storage delayedStake = delayedStakes[++delayedStakeWriteIndex]; delayedStake.account = _account; delayedStake.courtID = _courtID; From 51d7739be447357498d5e9e86867653fbf13ef50 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Mon, 9 Jun 2025 22:23:25 +0100 Subject: [PATCH 19/24] chore: updated KlerosProxies --- contracts/src/proxy/KlerosProxies.sol | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/contracts/src/proxy/KlerosProxies.sol b/contracts/src/proxy/KlerosProxies.sol index 2a1a9381f..4d917fa66 100644 --- a/contracts/src/proxy/KlerosProxies.sol +++ b/contracts/src/proxy/KlerosProxies.sol @@ -67,14 +67,18 @@ contract RandomizerRNGProxy is UUPSProxy { constructor(address _implementation, bytes memory _data) UUPSProxy(_implementation, _data) {} } -contract SortitionModuleNeoProxy is UUPSProxy { +contract SortitionSumTree is UUPSProxy { constructor(address _implementation, bytes memory _data) UUPSProxy(_implementation, _data) {} } -contract SortitionModuleUniversityProxy is UUPSProxy { +contract StakeController is UUPSProxy { constructor(address _implementation, bytes memory _data) UUPSProxy(_implementation, _data) {} } -contract SortitionModuleProxy is UUPSProxy { +contract Vault is UUPSProxy { + constructor(address _implementation, bytes memory _data) UUPSProxy(_implementation, _data) {} +} + +contract VaultNeo is UUPSProxy { constructor(address _implementation, bytes memory _data) UUPSProxy(_implementation, _data) {} } From 0eb3292f2ad7d00d49fa279636eed6de82e145dd Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Mon, 9 Jun 2025 22:31:56 +0100 Subject: [PATCH 20/24] chore: clean up --- contracts/src/arbitration/KlerosCoreBase.sol | 2 -- contracts/src/arbitration/StakeControllerBase.sol | 6 +++--- contracts/src/libraries/Constants.sol | 4 ++-- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/contracts/src/arbitration/KlerosCoreBase.sol b/contracts/src/arbitration/KlerosCoreBase.sol index 8f00caf5d..40a975aa3 100644 --- a/contracts/src/arbitration/KlerosCoreBase.sol +++ b/contracts/src/arbitration/KlerosCoreBase.sol @@ -1160,8 +1160,6 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable error CannotDisableClassicDK(); error StakingNotPossibleInThisCourt(); error StakingLessThanCourtMinStake(); - error StakingTransferFailed(); - error UnstakingTransferFailed(); error ArbitrationFeesNotEnough(); error DisputeKitNotSupportedByCourt(); error MustSupportDisputeKitClassic(); diff --git a/contracts/src/arbitration/StakeControllerBase.sol b/contracts/src/arbitration/StakeControllerBase.sol index ff47251f2..8d2d6f389 100644 --- a/contracts/src/arbitration/StakeControllerBase.sol +++ b/contracts/src/arbitration/StakeControllerBase.sol @@ -413,10 +413,11 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr } } + // If the courtID was found in the array if (courtIndexToRemove != type(uint256).max) { - // If the courtID was found in the array - // If it's not the last element, swap the last element into its place + // If it's not the last element if (courtIndexToRemove != length - 1) { + // Swap the last element into its place _stakedCourts[courtIndexToRemove] = _stakedCourts[length - 1]; } // Remove the last element (either the original last, or the one that was swapped) @@ -440,5 +441,4 @@ abstract contract StakeControllerBase is IStakeController, Initializable, UUPSPr error NotInStakingPhase(); error NotDrawingPhase(); error NoDelayedStakes(); - error InvalidMigrationData(); } diff --git a/contracts/src/libraries/Constants.sol b/contracts/src/libraries/Constants.sol index cbf37d4f6..94bb3eb70 100644 --- a/contracts/src/libraries/Constants.sol +++ b/contracts/src/libraries/Constants.sol @@ -20,7 +20,7 @@ uint256 constant DEFAULT_K = 6; // Default number of children per node. uint256 constant DEFAULT_NB_OF_JURORS = 3; // The default number of jurors in a dispute. IERC20 constant NATIVE_CURRENCY = IERC20(address(0)); // The native currency, such as ETH on Arbitrum, Optimism and Ethereum L1. -// DEPRECATED +// DEPRECATED: still used by University contracts for now enum OnError { Revert, Return @@ -29,7 +29,7 @@ enum OnError { enum StakingResult { Successful, Delayed, - StakingTransferFailed, // DEPRECATED + StakingTransferFailed, // DEPRECATED: still used by University contracts for now UnstakingTransferFailed, // DEPRECATED CannotStakeInMoreCourts, // DEPRECATED CannotStakeInThisCourt, // DEPRECATED From d99def1801e46b5636bb8798078dbe0785251cee Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Mon, 9 Jun 2025 22:43:47 +0100 Subject: [PATCH 21/24] refactor: removed unnecessary base contract for Stake Controller and Sortition Sum Tree, deploy scripts cleanup --- .../00-home-chain-arbitration-v2-neo.ts | 79 ---- .../deploy/00-home-chain-arbitration-v2.ts | 158 ------- contracts/deploy/00-home-chain-arbitration.ts | 100 ++-- contracts/src/arbitration/KlerosCoreBase.sol | 6 +- .../src/arbitration/SortitionSumTree.sol | 359 +++++++++++++- .../src/arbitration/SortitionSumTreeBase.sol | 373 --------------- contracts/src/arbitration/StakeController.sol | 429 ++++++++++++++++- .../src/arbitration/StakeControllerBase.sol | 444 ------------------ contracts/src/test/SortitionModuleMock.sol | 2 +- 9 files changed, 845 insertions(+), 1105 deletions(-) delete mode 100644 contracts/deploy/00-home-chain-arbitration-v2-neo.ts delete mode 100644 contracts/deploy/00-home-chain-arbitration-v2.ts delete mode 100644 contracts/src/arbitration/SortitionSumTreeBase.sol delete mode 100644 contracts/src/arbitration/StakeControllerBase.sol diff --git a/contracts/deploy/00-home-chain-arbitration-v2-neo.ts b/contracts/deploy/00-home-chain-arbitration-v2-neo.ts deleted file mode 100644 index f453eaf98..000000000 --- a/contracts/deploy/00-home-chain-arbitration-v2-neo.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { HardhatRuntimeEnvironment } from "hardhat/types"; -import { DeployFunction } from "hardhat-deploy/types"; -import { getContractAddress } from "./utils/getContractAddress"; -import { deployUpgradable } from "./utils/deployUpgradable"; -import { HomeChains, isSkipped, isDevnet, PNK, ETH } from "./utils"; -import { getContractOrDeploy, getContractOrDeployUpgradable } from "./utils/getContractOrDeploy"; -import { deployERC20AndFaucet, deployERC721 } from "./utils/deployTokens"; -import { ChainlinkRNG, DisputeKitClassic, KlerosCoreNeo, StakeControllerNeo, VaultNeo } from "../typechain-types"; - -const deployArbitrationV2Neo: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { - const { ethers, deployments, getNamedAccounts, getChainId } = hre; - const { deploy } = deployments; - const { ZeroAddress } = hre.ethers; - const RNG_LOOKAHEAD = 20; - - // fallback to hardhat node signers on local network - const deployer = (await getNamedAccounts()).deployer ?? (await hre.ethers.getSigners())[0].address; - const chainId = Number(await getChainId()); - console.log("deploying to %s with deployer %s", HomeChains[chainId], deployer); - - const pnk = await deployERC20AndFaucet(hre, deployer, "PNK"); - const weth = await deployERC20AndFaucet(hre, deployer, "WETH"); - const nft = await deployERC721(hre, deployer, "Kleros V2 Neo Early User", "KlerosV2NeoEarlyUser"); - - await getContractOrDeploy(hre, "TransactionBatcher", { from: deployer, args: [], log: true }); - - await deployUpgradable(deployments, "PolicyRegistry", { from: deployer, args: [deployer], log: true }); - - await deployUpgradable(deployments, "EvidenceModule", { from: deployer, args: [deployer], log: true }); - - const disputeKit = await deployUpgradable(deployments, "DisputeKitClassicV2Neo", { - from: deployer, - contract: "DisputeKitClassic", - args: [deployer, ZeroAddress], - log: true, - }); - - // TODO....... - - const disputeTemplateRegistry = await getContractOrDeployUpgradable(hre, "DisputeTemplateRegistry", { - from: deployer, - args: [deployer], - log: true, - }); - - const resolver = await deploy("DisputeResolverV2Neo", { - from: deployer, - contract: "DisputeResolver", - args: [core.target, disputeTemplateRegistry.target], - log: true, - }); - - console.log(`core.changeArbitrableWhitelist(${resolver.address}, true)`); - await core.changeArbitrableWhitelist(resolver.address, true); - - await deploy("KlerosCoreNeoSnapshotProxy", { - from: deployer, - contract: "KlerosCoreSnapshotProxy", - args: [deployer, core.target], - log: true, - }); - - console.log("✅ V2 Neo Architecture deployment completed successfully!"); - console.log(`📦 VaultNeo: ${pnkVaultNeo.address}`); - console.log(`🎫 stPNKNeo: ${stPNK.address}`); - console.log(`🎯 SortitionSumTreeNeo: ${sortitionModuleV2Neo.address}`); - console.log(`🎮 StakeControllerNeo: ${stakeControllerNeo.target}`); - console.log(`⚖️ KlerosCoreNeo: ${klerosCoreV2Neo.target}`); - console.log(`🎨 JurorNFT: ${nft.target}`); - console.log(`🔐 DisputeResolver: ${resolver.address}`); -}; - -deployArbitrationV2Neo.tags = ["ArbitrationV2Neo"]; -deployArbitrationV2Neo.dependencies = ["ChainlinkRNG"]; -deployArbitrationV2Neo.skip = async ({ network }) => { - return isSkipped(network, !HomeChains[network.config.chainId ?? 0]); -}; - -export default deployArbitrationV2Neo; diff --git a/contracts/deploy/00-home-chain-arbitration-v2.ts b/contracts/deploy/00-home-chain-arbitration-v2.ts deleted file mode 100644 index b36e3a4ac..000000000 --- a/contracts/deploy/00-home-chain-arbitration-v2.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { HardhatRuntimeEnvironment } from "hardhat/types"; -import { DeployFunction } from "hardhat-deploy/types"; -import { getContractAddress } from "./utils/getContractAddress"; -import { deployUpgradable } from "./utils/deployUpgradable"; -import { HomeChains, isSkipped, isDevnet, PNK, ETH } from "./utils"; -import { getContractOrDeploy, getContractOrDeployUpgradable } from "./utils/getContractOrDeploy"; -import { deployERC20AndFaucet } from "./utils/deployTokens"; -import { ChainlinkRNG, DisputeKitClassic, KlerosCore, StakeController, Vault } from "../typechain-types"; -import { changeCurrencyRate } from "./utils/klerosCoreHelper"; - -const deployArbitrationV2: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { - const { ethers, deployments, getNamedAccounts, getChainId } = hre; - const { deploy } = deployments; - const { ZeroAddress } = hre.ethers; - const RNG_LOOKAHEAD = 20; - - // fallback to hardhat node signers on local network - const deployer = (await getNamedAccounts()).deployer ?? (await hre.ethers.getSigners())[0].address; - const chainId = Number(await getChainId()); - console.log("deploying to %s with deployer %s", HomeChains[chainId], deployer); - - const pnk = await deployERC20AndFaucet(hre, deployer, "PNK"); - const dai = await deployERC20AndFaucet(hre, deployer, "DAI"); - const weth = await deployERC20AndFaucet(hre, deployer, "WETH"); - - await getContractOrDeploy(hre, "TransactionBatcher", { from: deployer, args: [], log: true }); - - await getContractOrDeployUpgradable(hre, "PolicyRegistry", { from: deployer, args: [deployer], log: true }); - - await getContractOrDeployUpgradable(hre, "EvidenceModule", { from: deployer, args: [deployer], log: true }); - - const disputeKit = await deployUpgradable(deployments, "DisputeKitClassicV2", { - from: deployer, - contract: "DisputeKitClassic", - args: [ - deployer, - ZeroAddress, // Placeholder for KlerosCore address, configured later - ], - log: true, - }); - - // Calculate future addresses for circular dependencies - const nonce = await ethers.provider.getTransactionCount(deployer); - - const vaultAddress = getContractAddress(deployer, nonce + 1); // deployed on the 2nd tx (nonce+1): Vault Impl tx, Vault Proxy tx - console.log("calculated future Vault address for nonce %d: %s", nonce + 1, vaultAddress); - - const stakeControllerAddress = getContractAddress(deployer, nonce + 5); // deployed on the 6th tx (nonce+5): Vault Impl tx, Vault Proxy tx, SortitionModule Impl tx, SortitionModule Proxy tx,, StakeController Impl tx, StakeController Proxy tx - console.log("calculated future StakeController address for nonce %d: %s", nonce + 5, stakeControllerAddress); - - const klerosCoreAddress = getContractAddress(deployer, nonce + 7); // deployed on the 8th tx (nonce+7): Vault Impl tx, Vault Proxy tx, SortitionModule Impl tx, SortitionModule Proxy tx, StakeController Impl tx, StakeController Proxy tx, KlerosCore Impl tx, KlerosCore Proxy tx - console.log("calculated future KlerosCore address for nonce %d: %s", nonce + 7, klerosCoreAddress); - - const vault = await deployUpgradable(deployments, "Vault", { - from: deployer, - args: [deployer, pnk.target, stakeControllerAddress, klerosCoreAddress], - log: true, - }); // nonce (implementation), nonce + 1 (proxy) - - // Deploy SortitionSumTree - const sortitionModuleV2 = await deployUpgradable(deployments, "SortitionSumTree", { - from: deployer, - args: [deployer, stakeControllerAddress], - log: true, - }); // nonce + 2 (implementation), nonce + 3 (proxy) - - // Deploy StakeController (only if not already deployed) - const devnet = isDevnet(hre.network); - const minStakingTime = devnet ? 180 : 1800; - const maxDrawingTime = devnet ? 600 : 1800; - const rng = (await ethers.getContract("ChainlinkRNG")) as ChainlinkRNG; - const stakeController = await deployUpgradable(deployments, "StakeController", { - from: deployer, - args: [ - deployer, - klerosCoreAddress, - vault.address, - sortitionModuleV2.address, - rng.target, - minStakingTime, - maxDrawingTime, - RNG_LOOKAHEAD, - ], - log: true, - }); // nonce + 4 (implementation), nonce + 5 (proxy) - - const minStake = PNK(200); - const alpha = 10000; - const feeForJuror = ETH(0.1); - const jurorsForCourtJump = 256; - - // Deploy KlerosCore (only if not already deployed) - const klerosCoreV2 = await deployUpgradable(deployments, "KlerosCore", { - from: deployer, - args: [ - deployer, - deployer, - ZeroAddress, // JurorProsecutionModule, not implemented yet - disputeKit.address, - false, - [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 - stakeController.address, - vault.address, - ], - log: true, - }); - - // Configure cross-dependencies - console.log("Configuring cross-dependencies..."); - - // disputeKit.changeCore() only if necessary - const disputeKitContract = (await ethers.getContract("DisputeKitClassicV2")) as DisputeKitClassic; - const currentCore = await disputeKitContract.core(); - if (currentCore !== klerosCoreV2.address) { - console.log(`disputeKit.changeCore(${klerosCoreV2.address})`); - await disputeKitContract.changeCore(klerosCoreV2.address); - } - - // rng.changeSortitionModule() only if necessary - // Note: the RNG's `sortitionModule` variable is misleading, it's only for access control and should be renamed to `consumer`. - const rngSortitionModule = await rng.sortitionModule(); - if (rngSortitionModule !== stakeController.address) { - console.log(`rng.changeSortitionModule(${stakeController.address})`); - await rng.changeSortitionModule(stakeController.address); - } - - const core = (await hre.ethers.getContract("KlerosCore")) as KlerosCore; - try { - await changeCurrencyRate(core, await pnk.getAddress(), true, 12225583, 12); - await changeCurrencyRate(core, await dai.getAddress(), true, 60327783, 11); - await changeCurrencyRate(core, await weth.getAddress(), true, 1, 1); - } catch (e) { - console.error("failed to change currency rates:", e); - } - - await deploy("KlerosCoreSnapshotProxy", { - from: deployer, - contract: "KlerosCoreSnapshotProxy", - args: [deployer, core.target], - log: true, - }); - - console.log("✅ V2 Architecture deployment completed successfully!"); - console.log(`📦 Vault: ${vault.address}`); - console.log(`🎯 SortitionSumTree: ${sortitionModuleV2.address}`); - console.log(`🎮 StakeController: ${stakeController.address}`); - console.log(`⚖️ KlerosCore: ${klerosCoreV2.address}`); -}; - -deployArbitrationV2.tags = ["ArbitrationV2"]; -deployArbitrationV2.dependencies = ["ChainlinkRNG"]; -deployArbitrationV2.skip = async ({ network }) => { - return isSkipped(network, !HomeChains[network.config.chainId ?? 0]); -}; - -export default deployArbitrationV2; diff --git a/contracts/deploy/00-home-chain-arbitration.ts b/contracts/deploy/00-home-chain-arbitration.ts index e61805f31..8550d0a99 100644 --- a/contracts/deploy/00-home-chain-arbitration.ts +++ b/contracts/deploy/00-home-chain-arbitration.ts @@ -2,11 +2,11 @@ import { HardhatRuntimeEnvironment } from "hardhat/types"; import { DeployFunction } from "hardhat-deploy/types"; import { getContractAddress } from "./utils/getContractAddress"; import { deployUpgradable } from "./utils/deployUpgradable"; -import { changeCurrencyRate } from "./utils/klerosCoreHelper"; import { HomeChains, isSkipped, isDevnet, PNK, ETH } from "./utils"; import { getContractOrDeploy, getContractOrDeployUpgradable } from "./utils/getContractOrDeploy"; import { deployERC20AndFaucet } from "./utils/deployTokens"; import { ChainlinkRNG, DisputeKitClassic, KlerosCore } from "../typechain-types"; +import { changeCurrencyRate } from "./utils/klerosCoreHelper"; const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const { ethers, deployments, getNamedAccounts, getChainId } = hre; @@ -29,65 +29,104 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) await getContractOrDeployUpgradable(hre, "EvidenceModule", { from: deployer, args: [deployer], log: true }); - const disputeKit = await deployUpgradable(deployments, "DisputeKitClassic", { + const disputeKit = await deployUpgradable(deployments, "DisputeKitClassicV2", { from: deployer, - args: [deployer, ZeroAddress], + contract: "DisputeKitClassic", + args: [ + deployer, + ZeroAddress, // Placeholder for KlerosCore address, configured later + ], log: true, }); - let klerosCoreAddress = await deployments.getOrNull("KlerosCore").then((deployment) => deployment?.address); - if (!klerosCoreAddress) { - const nonce = await ethers.provider.getTransactionCount(deployer); - klerosCoreAddress = getContractAddress(deployer, nonce + 3); // deployed on the 4th tx (nonce+3): SortitionModule Impl tx, SortitionModule Proxy tx, KlerosCore Impl tx, KlerosCore Proxy tx - console.log("calculated future KlerosCore address for nonce %d: %s", nonce + 3, klerosCoreAddress); - } + // Calculate future addresses for circular dependencies + const nonce = await ethers.provider.getTransactionCount(deployer); + + const vaultAddress = getContractAddress(deployer, nonce + 1); // deployed on the 2nd tx (nonce+1): Vault Impl tx, Vault Proxy tx + console.log("calculated future Vault address for nonce %d: %s", nonce + 1, vaultAddress); + + const stakeControllerAddress = getContractAddress(deployer, nonce + 5); // deployed on the 6th tx (nonce+5): Vault Impl tx, Vault Proxy tx, SortitionModule Impl tx, SortitionModule Proxy tx,, StakeController Impl tx, StakeController Proxy tx + console.log("calculated future StakeController address for nonce %d: %s", nonce + 5, stakeControllerAddress); + + const klerosCoreAddress = getContractAddress(deployer, nonce + 7); // deployed on the 8th tx (nonce+7): Vault Impl tx, Vault Proxy tx, SortitionModule Impl tx, SortitionModule Proxy tx, StakeController Impl tx, StakeController Proxy tx, KlerosCore Impl tx, KlerosCore Proxy tx + console.log("calculated future KlerosCore address for nonce %d: %s", nonce + 7, klerosCoreAddress); + + const vault = await deployUpgradable(deployments, "Vault", { + from: deployer, + args: [deployer, pnk.target, stakeControllerAddress, klerosCoreAddress], + log: true, + }); // nonce (implementation), nonce + 1 (proxy) + + // Deploy SortitionSumTree + const sortitionModuleV2 = await deployUpgradable(deployments, "SortitionSumTree", { + from: deployer, + args: [deployer, stakeControllerAddress], + log: true, + }); // nonce + 2 (implementation), nonce + 3 (proxy) + + // Deploy StakeController const devnet = isDevnet(hre.network); const minStakingTime = devnet ? 180 : 1800; - const maxFreezingTime = devnet ? 600 : 1800; - const rng = (await ethers.getContract("ChainlinkRNG")) as ChainlinkRNG; - const sortitionModule = await deployUpgradable(deployments, "SortitionModule", { + const maxDrawingTime = devnet ? 600 : 1800; + const rng = await ethers.getContract("ChainlinkRNG"); + const stakeController = await deployUpgradable(deployments, "StakeController", { from: deployer, - args: [deployer, klerosCoreAddress, minStakingTime, maxFreezingTime, rng.target, RNG_LOOKAHEAD], + args: [ + deployer, + klerosCoreAddress, + vault.address, + sortitionModuleV2.address, + rng.target, + minStakingTime, + maxDrawingTime, + RNG_LOOKAHEAD, + ], log: true, - }); // nonce (implementation), nonce+1 (proxy) + }); // nonce + 4 (implementation), nonce + 5 (proxy) const minStake = PNK(200); const alpha = 10000; const feeForJuror = ETH(0.1); const jurorsForCourtJump = 256; - const klerosCore = await deployUpgradable(deployments, "KlerosCore", { + + // Deploy KlerosCore + const klerosCoreV2 = await deployUpgradable(deployments, "KlerosCore", { from: deployer, args: [ deployer, deployer, - pnk.target, - ZeroAddress, // KlerosCore is configured later + ZeroAddress, // JurorProsecutionModule, not implemented yet disputeKit.address, false, [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, + stakeController.address, + vault.address, ], log: true, - }); // nonce+2 (implementation), nonce+3 (proxy) + }); + + // Configure cross-dependencies + console.log("Configuring cross-dependencies..."); // disputeKit.changeCore() only if necessary - const disputeKitContract = (await ethers.getContract("DisputeKitClassic")) as DisputeKitClassic; + const disputeKitContract = await ethers.getContract("DisputeKitClassicV2"); const currentCore = await disputeKitContract.core(); - if (currentCore !== klerosCore.address) { - console.log(`disputeKit.changeCore(${klerosCore.address})`); - await disputeKitContract.changeCore(klerosCore.address); + if (currentCore !== klerosCoreV2.address) { + console.log(`disputeKit.changeCore(${klerosCoreV2.address})`); + await disputeKitContract.changeCore(klerosCoreV2.address); } // rng.changeSortitionModule() only if necessary + // Note: the RNG's `sortitionModule` variable is misleading, it's only for access control and should be renamed to `consumer`. const rngSortitionModule = await rng.sortitionModule(); - if (rngSortitionModule !== sortitionModule.address) { - console.log(`rng.changeSortitionModule(${sortitionModule.address})`); - await rng.changeSortitionModule(sortitionModule.address); + if (rngSortitionModule !== stakeController.address) { + console.log(`rng.changeSortitionModule(${stakeController.address})`); + await rng.changeSortitionModule(stakeController.address); } - const core = (await hre.ethers.getContract("KlerosCore")) as KlerosCore; + const core = await hre.ethers.getContract("KlerosCore"); try { await changeCurrencyRate(core, await pnk.getAddress(), true, 12225583, 12); await changeCurrencyRate(core, await dai.getAddress(), true, 60327783, 11); @@ -98,9 +137,16 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) await deploy("KlerosCoreSnapshotProxy", { from: deployer, + contract: "KlerosCoreSnapshotProxy", args: [deployer, core.target], log: true, }); + + console.log("✅ V2 Architecture deployment completed successfully!"); + console.log(`📦 Vault: ${vault.address}`); + console.log(`🎯 SortitionSumTree: ${sortitionModuleV2.address}`); + console.log(`🎮 StakeController: ${stakeController.address}`); + console.log(`⚖️ KlerosCore: ${klerosCoreV2.address}`); }; deployArbitration.tags = ["Arbitration"]; diff --git a/contracts/src/arbitration/KlerosCoreBase.sol b/contracts/src/arbitration/KlerosCoreBase.sol index 40a975aa3..dc1bea0e9 100644 --- a/contracts/src/arbitration/KlerosCoreBase.sol +++ b/contracts/src/arbitration/KlerosCoreBase.sol @@ -363,11 +363,9 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable court.feeForJuror = _feeForJuror; court.jurorsForCourtJump = _jurorsForCourtJump; court.timesPerPeriod = _timesPerPeriod; + courts[_parent].children.push(courtID); stakeController.createTree(bytes32(courtID), _sortitionExtraData); - if (_parent != FORKING_COURT) { - courts[_parent].children.push(courtID); - } emit CourtCreated( uint96(courtID), @@ -469,7 +467,7 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable } /// @notice Executes a stake change initiated by the system (e.g., processing a delayed stake). - /// @dev Called by StakeControllerBase during executeDelayedStakes. Assumes KlerosCore holds pre-funded PNK if _depositPreFunded is true. + /// @dev Called by StakeController during executeDelayedStakes. Assumes KlerosCore holds pre-funded PNK if _depositPreFunded is true. /// @param _account The juror's account. /// @param _courtID The ID of the court. /// @param _newStake The new stake amount for the juror in the court. diff --git a/contracts/src/arbitration/SortitionSumTree.sol b/contracts/src/arbitration/SortitionSumTree.sol index 08beb6880..833d15e56 100644 --- a/contracts/src/arbitration/SortitionSumTree.sol +++ b/contracts/src/arbitration/SortitionSumTree.sol @@ -2,15 +2,60 @@ pragma solidity 0.8.24; -import {SortitionSumTreeBase} from "./SortitionSumTreeBase.sol"; +import {ISortitionSumTree} from "./interfaces/ISortitionSumTree.sol"; import {IStakeController} from "./interfaces/IStakeController.sol"; +import {KlerosCoreBase} from "./KlerosCoreBase.sol"; +import {Initializable} from "../proxy/Initializable.sol"; +import {UUPSProxiable} from "../proxy/UUPSProxiable.sol"; +import "../libraries/Constants.sol"; /// @title SortitionSumTree -/// @notice Basic implementation of the pure sortition module +/// @notice Responsible for sortition operations /// @dev Contains only tree management and drawing logic, no phase management or token operations -contract SortitionSumTree is SortitionSumTreeBase { +contract SortitionSumTree is ISortitionSumTree, Initializable, UUPSProxiable { string public constant override version = "2.0.0"; + // ************************************* // + // * Enums / Structs * // + // ************************************* // + + struct SumTree { + uint256 K; // The maximum number of children per node. + // We use this to keep track of vacant positions in the tree after removing a leaf. This is for keeping the tree as balanced as possible without spending gas on moving nodes around. + uint256[] stack; + uint256[] nodes; + // Two-way mapping of IDs to node indexes. Note that node index 0 is reserved for the root node, and means the ID does not have a node. + mapping(bytes32 => uint256) IDsToNodeIndexes; + mapping(uint256 => bytes32) nodeIndexesToIDs; + } + + // ************************************* // + // * Storage * // + // ************************************* // + + address public governor; // The governor of the contract. + IStakeController public stakeController; // The stake controller for coordination. + + mapping(bytes32 treeHash => SumTree) internal sortitionSumTrees; // The mapping trees by keys. + + // ************************************* // + // * Function Modifiers * // + // ************************************* // + + modifier onlyByGovernor() { + if (governor != msg.sender) revert GovernorOnly(); + _; + } + + modifier onlyByStakeController() { + if (address(stakeController) != msg.sender) revert StakeControllerOnly(); + _; + } + + // ************************************* // + // * Constructor * // + // ************************************* // + // ************************************* // // * Constructor * // // ************************************* // @@ -24,7 +69,8 @@ contract SortitionSumTree is SortitionSumTreeBase { /// @param _governor The governor's address. /// @param _stakeController The StakeController contract. function initialize(address _governor, IStakeController _stakeController) external initializer { - __SortitionSumTreeBase_initialize(_governor, _stakeController); + governor = _governor; + stakeController = _stakeController; } // ************************************* // @@ -36,4 +82,309 @@ contract SortitionSumTree is SortitionSumTreeBase { function _authorizeUpgrade(address) internal view override onlyByGovernor { // NOP } + + /// @dev Changes the governor of the contract. + /// @param _governor The new governor. + function changeGovernor(address _governor) external onlyByGovernor { + governor = _governor; + } + + /// @dev Changes the `stakeController` storage variable. + /// @param _stakeController The new stake controller address. + function changeStakeController(IStakeController _stakeController) external onlyByGovernor { + stakeController = _stakeController; + } + + // ************************************* // + // * Tree Management * // + // ************************************* // + + /// @inheritdoc ISortitionSumTree + function createTree(bytes32 _key, bytes memory _extraData) external override onlyByStakeController { + SumTree storage tree = sortitionSumTrees[_key]; + uint256 K = _extraDataToTreeK(_extraData); + if (tree.K != 0) revert TreeAlreadyExists(); + if (K <= 1) revert InvalidTreeK(); + tree.K = K; + tree.nodes.push(0); + } + + /// @inheritdoc ISortitionSumTree + function setStake( + address _account, + uint96 _courtID, + uint256 _newStake + ) external virtual override onlyByStakeController { + bytes32 stakePathID = _accountAndCourtIDToStakePathID(_account, _courtID); + bool finished = false; + uint96 currentCourtID = _courtID; + KlerosCoreBase core = stakeController.core(); + + while (!finished) { + _set(bytes32(uint256(currentCourtID)), _newStake, stakePathID); + if (currentCourtID == GENERAL_COURT) { + finished = true; + } else { + // Fetch parent court ID. Ensure core.courts() is accessible and correct. + (uint96 parentCourtID, , , , , , ) = core.courts(currentCourtID); + if (parentCourtID == currentCourtID) { + // Avoid infinite loop if parent is self (e.g. for general court already handled or misconfiguration) + finished = true; + } else { + currentCourtID = parentCourtID; + } + } + } + } + + // ************************************* // + // * Drawing * // + // ************************************* // + + /// @inheritdoc ISortitionSumTree + function draw( + bytes32 _court, + uint256 _coreDisputeID, + uint256 _nonce, + uint256 _randomNumber + ) external view virtual override returns (address drawnAddress) { + SumTree storage tree = sortitionSumTrees[_court]; + + if (tree.nodes.length == 0 || tree.nodes[0] == 0) { + return address(0); // No jurors staked. + } + + uint256 currentDrawnNumber = uint256(keccak256(abi.encodePacked(_randomNumber, _coreDisputeID, _nonce))) % + tree.nodes[0]; + + // While it still has children + uint256 treeIndex = 0; + while ((tree.K * treeIndex) + 1 < tree.nodes.length) { + for (uint256 i = 1; i <= tree.K; i++) { + // Loop over children. + uint256 nodeIndex = (tree.K * treeIndex) + i; + uint256 nodeValue = tree.nodes[nodeIndex]; + + if (currentDrawnNumber >= nodeValue) { + // Go to the next child. + currentDrawnNumber -= nodeValue; + } else { + // Pick this child. + treeIndex = nodeIndex; + break; + } + } + } + drawnAddress = _stakePathIDToAccount(tree.nodeIndexesToIDs[treeIndex]); + } + + // ************************************* // + // * View Functions * // + // ************************************* // + + /// @inheritdoc ISortitionSumTree + function stakeOf(address _juror, uint96 _courtID) external view override returns (uint256 value) { + bytes32 stakePathID = _accountAndCourtIDToStakePathID(_juror, _courtID); + return stakeOf(bytes32(uint256(_courtID)), stakePathID); + } + + /// @inheritdoc ISortitionSumTree + function stakeOf(bytes32 _key, bytes32 _ID) public view override returns (uint256) { + SumTree storage tree = sortitionSumTrees[_key]; + uint treeIndex = tree.IDsToNodeIndexes[_ID]; + if (treeIndex == 0) { + return 0; + } + return tree.nodes[treeIndex]; + } + + /// @inheritdoc ISortitionSumTree + function getTotalStakeInCourt(uint96 _courtID) external view override returns (uint256) { + SumTree storage tree = sortitionSumTrees[bytes32(uint256(_courtID))]; + if (tree.nodes.length == 0) return 0; + return tree.nodes[0]; // Root node contains total stake + } + + /// @inheritdoc ISortitionSumTree + function accountAndCourtIDToStakePathID( + address _account, + uint96 _courtID + ) external pure override returns (bytes32 stakePathID) { + return _accountAndCourtIDToStakePathID(_account, _courtID); + } + + /// @inheritdoc ISortitionSumTree + function stakePathIDToAccount(bytes32 _stakePathID) external pure override returns (address account) { + return _stakePathIDToAccount(_stakePathID); + } + + // ************************************* // + // * Internal * // + // ************************************* // + + /// @dev Update all the parents of a node. + /// @param _key The key of the tree to update. + /// @param _treeIndex The index of the node to start from. + /// @param _plusOrMinus Whether to add (true) or substract (false). + /// @param _value The value to add or substract. + /// `O(log_k(n))` where + /// `k` is the maximum number of children per node in the tree, + /// and `n` is the maximum number of nodes ever appended. + function _updateParents(bytes32 _key, uint256 _treeIndex, bool _plusOrMinus, uint256 _value) private { + SumTree storage tree = sortitionSumTrees[_key]; + + uint256 parentIndex = _treeIndex; + while (parentIndex != 0) { + parentIndex = (parentIndex - 1) / tree.K; + tree.nodes[parentIndex] = _plusOrMinus + ? tree.nodes[parentIndex] + _value + : tree.nodes[parentIndex] - _value; + } + } + + /// @dev Retrieves a juror's address from the stake path ID. + /// @param _stakePathID The stake path ID to unpack. + /// @return account The account. + function _stakePathIDToAccount(bytes32 _stakePathID) internal pure returns (address account) { + assembly { + // solium-disable-line security/no-inline-assembly + let ptr := mload(0x40) + for { + let i := 0x00 + } lt(i, 0x14) { + i := add(i, 0x01) + } { + mstore8(add(add(ptr, 0x0c), i), byte(i, _stakePathID)) + } + account := mload(ptr) + } + } + + function _extraDataToTreeK(bytes memory _extraData) internal pure returns (uint256 K) { + if (_extraData.length >= 32) { + assembly { + // solium-disable-line security/no-inline-assembly + K := mload(add(_extraData, 0x20)) + } + } else { + K = DEFAULT_K; + } + } + + /// @dev Set a value in a tree. + /// @param _key The key of the tree. + /// @param _value The new value. + /// @param _ID The ID of the value. + /// `O(log_k(n))` where + /// `k` is the maximum number of children per node in the tree, + /// and `n` is the maximum number of nodes ever appended. + function _set(bytes32 _key, uint256 _value, bytes32 _ID) internal { + SumTree storage tree = sortitionSumTrees[_key]; + uint256 treeIndex = tree.IDsToNodeIndexes[_ID]; + + if (treeIndex == 0) { + // No existing node. + if (_value != 0) { + // Non zero value. + // Append. + // Add node. + if (tree.stack.length == 0) { + // No vacant spots. + // Get the index and append the value. + treeIndex = tree.nodes.length; + tree.nodes.push(_value); + + // Potentially append a new node and make the parent a sum node. + if (treeIndex != 1 && (treeIndex - 1) % tree.K == 0) { + // Is first child. + uint256 parentIndex = treeIndex / tree.K; + bytes32 parentID = tree.nodeIndexesToIDs[parentIndex]; + uint256 newIndex = treeIndex + 1; + tree.nodes.push(tree.nodes[parentIndex]); + delete tree.nodeIndexesToIDs[parentIndex]; + tree.IDsToNodeIndexes[parentID] = newIndex; + tree.nodeIndexesToIDs[newIndex] = parentID; + } + } else { + // Some vacant spot. + // Pop the stack and append the value. + treeIndex = tree.stack[tree.stack.length - 1]; + tree.stack.pop(); + tree.nodes[treeIndex] = _value; + } + + // Add label. + tree.IDsToNodeIndexes[_ID] = treeIndex; + tree.nodeIndexesToIDs[treeIndex] = _ID; + + _updateParents(_key, treeIndex, true, _value); + } + } else { + // Existing node. + if (_value == 0) { + // Zero value. + // Remove. + // Remember value and set to 0. + uint256 value = tree.nodes[treeIndex]; + tree.nodes[treeIndex] = 0; + + // Push to stack. + tree.stack.push(treeIndex); + + // Clear label. + delete tree.IDsToNodeIndexes[_ID]; + delete tree.nodeIndexesToIDs[treeIndex]; + + _updateParents(_key, treeIndex, false, value); + } else if (_value != tree.nodes[treeIndex]) { + // New, non zero value. + // Set. + bool plusOrMinus = tree.nodes[treeIndex] <= _value; + uint256 plusOrMinusValue = plusOrMinus + ? _value - tree.nodes[treeIndex] + : tree.nodes[treeIndex] - _value; + tree.nodes[treeIndex] = _value; + + _updateParents(_key, treeIndex, plusOrMinus, plusOrMinusValue); + } + } + } + + /// @dev Packs an account and a court ID into a stake path ID. + /// @param _account The address of the juror to pack. + /// @param _courtID The court ID to pack. + /// @return stakePathID The stake path ID. + function _accountAndCourtIDToStakePathID( + address _account, + uint96 _courtID + ) internal pure returns (bytes32 stakePathID) { + assembly { + // solium-disable-line security/no-inline-assembly + let ptr := mload(0x40) + for { + let i := 0x00 + } lt(i, 0x14) { + i := add(i, 0x01) + } { + mstore8(add(ptr, i), byte(add(0x0c, i), _account)) + } + for { + let i := 0x14 + } lt(i, 0x20) { + i := add(i, 0x01) + } { + mstore8(add(ptr, i), byte(i, _courtID)) + } + stakePathID := mload(ptr) + } + } + + // ************************************* // + // * Errors * // + // ************************************* // + + error GovernorOnly(); + error StakeControllerOnly(); + error TreeAlreadyExists(); + error InvalidTreeK(); } diff --git a/contracts/src/arbitration/SortitionSumTreeBase.sol b/contracts/src/arbitration/SortitionSumTreeBase.sol deleted file mode 100644 index 4c34211c3..000000000 --- a/contracts/src/arbitration/SortitionSumTreeBase.sol +++ /dev/null @@ -1,373 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import {ISortitionSumTree} from "./interfaces/ISortitionSumTree.sol"; -import {IStakeController} from "./interfaces/IStakeController.sol"; -import {KlerosCoreBase} from "./KlerosCoreBase.sol"; -import {Initializable} from "../proxy/Initializable.sol"; -import {UUPSProxiable} from "../proxy/UUPSProxiable.sol"; -import "../libraries/Constants.sol"; - -/// @title SortitionSumTreeBase -/// @notice Abstract base contract for pure sortition operations -/// @dev Contains only tree management and drawing logic, no phase management or token operations -abstract contract SortitionSumTreeBase is ISortitionSumTree, Initializable, UUPSProxiable { - // ************************************* // - // * Enums / Structs * // - // ************************************* // - - struct SortitionSumTree { - uint256 K; // The maximum number of children per node. - // We use this to keep track of vacant positions in the tree after removing a leaf. This is for keeping the tree as balanced as possible without spending gas on moving nodes around. - uint256[] stack; - uint256[] nodes; - // Two-way mapping of IDs to node indexes. Note that node index 0 is reserved for the root node, and means the ID does not have a node. - mapping(bytes32 => uint256) IDsToNodeIndexes; - mapping(uint256 => bytes32) nodeIndexesToIDs; - } - - // ************************************* // - // * Storage * // - // ************************************* // - - address public governor; // The governor of the contract. - IStakeController public stakeController; // The stake controller for coordination. - - mapping(bytes32 treeHash => SortitionSumTree) internal sortitionSumTrees; // The mapping trees by keys. - - // ************************************* // - // * Function Modifiers * // - // ************************************* // - - modifier onlyByGovernor() { - if (governor != msg.sender) revert GovernorOnly(); - _; - } - - modifier onlyByStakeController() { - if (address(stakeController) != msg.sender) revert StakeControllerOnly(); - _; - } - - // ************************************* // - // * Constructor * // - // ************************************* // - - function __SortitionSumTreeBase_initialize( - address _governor, - IStakeController _stakeController - ) internal onlyInitializing { - governor = _governor; - stakeController = _stakeController; - } - - // ************************************* // - // * Governance * // - // ************************************* // - - /// @dev Changes the governor of the contract. - /// @param _governor The new governor. - function changeGovernor(address _governor) external onlyByGovernor { - governor = _governor; - } - - /// @dev Changes the `stakeController` storage variable. - /// @param _stakeController The new stake controller address. - function changeStakeController(IStakeController _stakeController) external onlyByGovernor { - stakeController = _stakeController; - } - - // ************************************* // - // * Tree Management * // - // ************************************* // - - /// @inheritdoc ISortitionSumTree - function createTree(bytes32 _key, bytes memory _extraData) external override onlyByStakeController { - SortitionSumTree storage tree = sortitionSumTrees[_key]; - uint256 K = _extraDataToTreeK(_extraData); - if (tree.K != 0) revert TreeAlreadyExists(); - if (K <= 1) revert InvalidTreeK(); - tree.K = K; - tree.nodes.push(0); - } - - /// @inheritdoc ISortitionSumTree - function setStake( - address _account, - uint96 _courtID, - uint256 _newStake - ) external virtual override onlyByStakeController { - bytes32 stakePathID = _accountAndCourtIDToStakePathID(_account, _courtID); - bool finished = false; - uint96 currentCourtID = _courtID; - KlerosCoreBase core = stakeController.core(); - - while (!finished) { - _set(bytes32(uint256(currentCourtID)), _newStake, stakePathID); - if (currentCourtID == GENERAL_COURT) { - finished = true; - } else { - // Fetch parent court ID. Ensure core.courts() is accessible and correct. - (uint96 parentCourtID, , , , , , ) = core.courts(currentCourtID); - if (parentCourtID == currentCourtID) { - // Avoid infinite loop if parent is self (e.g. for general court already handled or misconfiguration) - finished = true; - } else { - currentCourtID = parentCourtID; - } - } - } - } - - // ************************************* // - // * Drawing * // - // ************************************* // - - /// @inheritdoc ISortitionSumTree - function draw( - bytes32 _court, - uint256 _coreDisputeID, - uint256 _nonce, - uint256 _randomNumber - ) external view virtual override returns (address drawnAddress) { - SortitionSumTree storage tree = sortitionSumTrees[_court]; - - if (tree.nodes.length == 0 || tree.nodes[0] == 0) { - return address(0); // No jurors staked. - } - - uint256 currentDrawnNumber = uint256(keccak256(abi.encodePacked(_randomNumber, _coreDisputeID, _nonce))) % - tree.nodes[0]; - - // While it still has children - uint256 treeIndex = 0; - while ((tree.K * treeIndex) + 1 < tree.nodes.length) { - for (uint256 i = 1; i <= tree.K; i++) { - // Loop over children. - uint256 nodeIndex = (tree.K * treeIndex) + i; - uint256 nodeValue = tree.nodes[nodeIndex]; - - if (currentDrawnNumber >= nodeValue) { - // Go to the next child. - currentDrawnNumber -= nodeValue; - } else { - // Pick this child. - treeIndex = nodeIndex; - break; - } - } - } - drawnAddress = _stakePathIDToAccount(tree.nodeIndexesToIDs[treeIndex]); - } - - // ************************************* // - // * View Functions * // - // ************************************* // - - /// @inheritdoc ISortitionSumTree - function stakeOf(address _juror, uint96 _courtID) external view override returns (uint256 value) { - bytes32 stakePathID = _accountAndCourtIDToStakePathID(_juror, _courtID); - return stakeOf(bytes32(uint256(_courtID)), stakePathID); - } - - /// @inheritdoc ISortitionSumTree - function stakeOf(bytes32 _key, bytes32 _ID) public view override returns (uint256) { - SortitionSumTree storage tree = sortitionSumTrees[_key]; - uint treeIndex = tree.IDsToNodeIndexes[_ID]; - if (treeIndex == 0) { - return 0; - } - return tree.nodes[treeIndex]; - } - - /// @inheritdoc ISortitionSumTree - function getTotalStakeInCourt(uint96 _courtID) external view override returns (uint256) { - SortitionSumTree storage tree = sortitionSumTrees[bytes32(uint256(_courtID))]; - if (tree.nodes.length == 0) return 0; - return tree.nodes[0]; // Root node contains total stake - } - - /// @inheritdoc ISortitionSumTree - function accountAndCourtIDToStakePathID( - address _account, - uint96 _courtID - ) external pure override returns (bytes32 stakePathID) { - return _accountAndCourtIDToStakePathID(_account, _courtID); - } - - /// @inheritdoc ISortitionSumTree - function stakePathIDToAccount(bytes32 _stakePathID) external pure override returns (address account) { - return _stakePathIDToAccount(_stakePathID); - } - - // ************************************* // - // * Internal * // - // ************************************* // - - /// @dev Update all the parents of a node. - /// @param _key The key of the tree to update. - /// @param _treeIndex The index of the node to start from. - /// @param _plusOrMinus Whether to add (true) or substract (false). - /// @param _value The value to add or substract. - /// `O(log_k(n))` where - /// `k` is the maximum number of children per node in the tree, - /// and `n` is the maximum number of nodes ever appended. - function _updateParents(bytes32 _key, uint256 _treeIndex, bool _plusOrMinus, uint256 _value) private { - SortitionSumTree storage tree = sortitionSumTrees[_key]; - - uint256 parentIndex = _treeIndex; - while (parentIndex != 0) { - parentIndex = (parentIndex - 1) / tree.K; - tree.nodes[parentIndex] = _plusOrMinus - ? tree.nodes[parentIndex] + _value - : tree.nodes[parentIndex] - _value; - } - } - - /// @dev Retrieves a juror's address from the stake path ID. - /// @param _stakePathID The stake path ID to unpack. - /// @return account The account. - function _stakePathIDToAccount(bytes32 _stakePathID) internal pure returns (address account) { - assembly { - // solium-disable-line security/no-inline-assembly - let ptr := mload(0x40) - for { - let i := 0x00 - } lt(i, 0x14) { - i := add(i, 0x01) - } { - mstore8(add(add(ptr, 0x0c), i), byte(i, _stakePathID)) - } - account := mload(ptr) - } - } - - function _extraDataToTreeK(bytes memory _extraData) internal pure returns (uint256 K) { - if (_extraData.length >= 32) { - assembly { - // solium-disable-line security/no-inline-assembly - K := mload(add(_extraData, 0x20)) - } - } else { - K = DEFAULT_K; - } - } - - /// @dev Set a value in a tree. - /// @param _key The key of the tree. - /// @param _value The new value. - /// @param _ID The ID of the value. - /// `O(log_k(n))` where - /// `k` is the maximum number of children per node in the tree, - /// and `n` is the maximum number of nodes ever appended. - function _set(bytes32 _key, uint256 _value, bytes32 _ID) internal { - SortitionSumTree storage tree = sortitionSumTrees[_key]; - uint256 treeIndex = tree.IDsToNodeIndexes[_ID]; - - if (treeIndex == 0) { - // No existing node. - if (_value != 0) { - // Non zero value. - // Append. - // Add node. - if (tree.stack.length == 0) { - // No vacant spots. - // Get the index and append the value. - treeIndex = tree.nodes.length; - tree.nodes.push(_value); - - // Potentially append a new node and make the parent a sum node. - if (treeIndex != 1 && (treeIndex - 1) % tree.K == 0) { - // Is first child. - uint256 parentIndex = treeIndex / tree.K; - bytes32 parentID = tree.nodeIndexesToIDs[parentIndex]; - uint256 newIndex = treeIndex + 1; - tree.nodes.push(tree.nodes[parentIndex]); - delete tree.nodeIndexesToIDs[parentIndex]; - tree.IDsToNodeIndexes[parentID] = newIndex; - tree.nodeIndexesToIDs[newIndex] = parentID; - } - } else { - // Some vacant spot. - // Pop the stack and append the value. - treeIndex = tree.stack[tree.stack.length - 1]; - tree.stack.pop(); - tree.nodes[treeIndex] = _value; - } - - // Add label. - tree.IDsToNodeIndexes[_ID] = treeIndex; - tree.nodeIndexesToIDs[treeIndex] = _ID; - - _updateParents(_key, treeIndex, true, _value); - } - } else { - // Existing node. - if (_value == 0) { - // Zero value. - // Remove. - // Remember value and set to 0. - uint256 value = tree.nodes[treeIndex]; - tree.nodes[treeIndex] = 0; - - // Push to stack. - tree.stack.push(treeIndex); - - // Clear label. - delete tree.IDsToNodeIndexes[_ID]; - delete tree.nodeIndexesToIDs[treeIndex]; - - _updateParents(_key, treeIndex, false, value); - } else if (_value != tree.nodes[treeIndex]) { - // New, non zero value. - // Set. - bool plusOrMinus = tree.nodes[treeIndex] <= _value; - uint256 plusOrMinusValue = plusOrMinus - ? _value - tree.nodes[treeIndex] - : tree.nodes[treeIndex] - _value; - tree.nodes[treeIndex] = _value; - - _updateParents(_key, treeIndex, plusOrMinus, plusOrMinusValue); - } - } - } - - /// @dev Packs an account and a court ID into a stake path ID. - /// @param _account The address of the juror to pack. - /// @param _courtID The court ID to pack. - /// @return stakePathID The stake path ID. - function _accountAndCourtIDToStakePathID( - address _account, - uint96 _courtID - ) internal pure returns (bytes32 stakePathID) { - assembly { - // solium-disable-line security/no-inline-assembly - let ptr := mload(0x40) - for { - let i := 0x00 - } lt(i, 0x14) { - i := add(i, 0x01) - } { - mstore8(add(ptr, i), byte(add(0x0c, i), _account)) - } - for { - let i := 0x14 - } lt(i, 0x20) { - i := add(i, 0x01) - } { - mstore8(add(ptr, i), byte(i, _courtID)) - } - stakePathID := mload(ptr) - } - } - - // ************************************* // - // * Errors * // - // ************************************* // - - error GovernorOnly(); - error StakeControllerOnly(); - error TreeAlreadyExists(); - error InvalidTreeK(); -} diff --git a/contracts/src/arbitration/StakeController.sol b/contracts/src/arbitration/StakeController.sol index 14b9040cd..06ab684da 100644 --- a/contracts/src/arbitration/StakeController.sol +++ b/contracts/src/arbitration/StakeController.sol @@ -2,22 +2,83 @@ pragma solidity 0.8.24; -import {StakeControllerBase} from "./StakeControllerBase.sol"; +import {IStakeController} from "./interfaces/IStakeController.sol"; import {IVault} from "./interfaces/IVault.sol"; import {ISortitionSumTree} from "./interfaces/ISortitionSumTree.sol"; +import {IDisputeKit} from "./interfaces/IDisputeKit.sol"; import {KlerosCoreBase} from "./KlerosCoreBase.sol"; +import {Initializable} from "../proxy/Initializable.sol"; +import {UUPSProxiable} from "../proxy/UUPSProxiable.sol"; import {RNG} from "../rng/RNG.sol"; +import "../libraries/Constants.sol"; /// @title StakeController -/// @notice Basic implementation of the Stake Controller -/// @dev Coordinates between Vault and SortitionSumTree for the new architecture -contract StakeController is StakeControllerBase { +/// @notice Responsible for coordinating between Vault and SortitionSumTree +/// @dev Manages phases, delayed stakes, and coordination logic +contract StakeController is IStakeController, Initializable, UUPSProxiable { string public constant override version = "1.0.0"; // ************************************* // - // * Constructor * // + // * Enums / Structs * // + // ************************************* // + + struct DelayedStake { + address account; // The address of the juror. + uint96 courtID; // The ID of the court. + uint256 stake; // The new stake. + } + + struct JurorStake { + uint256 totalStake; + uint96[] stakedCourtIDs; + mapping(uint96 courtID => uint256 stake) stakes; + } + + // ************************************* // + // * Storage * // + // ************************************* // + + address public governor; // The governor of the contract. + KlerosCoreBase public core; // The core arbitrator contract. + IVault public vault; // The PNK vault for token management. + ISortitionSumTree public sortition; // The sortition sum tree for drawing. + + // Phase management + Phase public override phase; // The current phase. Uses Phase from IStakeController. + uint256 public minStakingTime; // The time after which the phase can be switched to Drawing if there are open disputes. + uint256 public maxDrawingTime; // The time after which the phase can be switched back to Staking. + uint256 public lastPhaseChange; // The last time the phase was changed. + uint256 public randomNumberRequestBlock; // Number of the block when RNG request was made. + uint256 public disputesWithoutJurors; // The number of disputes that have not finished drawing jurors. + RNG public rng; // The random number generator. + uint256 public randomNumber; // Random number returned by RNG. + uint256 public rngLookahead; // Minimal block distance between requesting and obtaining a random number. + + // Delayed stakes management + uint256 public delayedStakeWriteIndex; // The index of the last `delayedStake` item that was written to the array. 0 index is skipped. + uint256 public delayedStakeReadIndex; // The index of the next `delayedStake` item that should be processed. Starts at 1 because 0 index is skipped. + mapping(uint256 index => DelayedStake) public delayedStakes; // Stores the stakes that were changed during Drawing phase, to update them when the phase is switched to Staking. + + // Stake management + mapping(address => JurorStake) internal jurorStakes; + + // ************************************* // + // * Function Modifiers * // // ************************************* // + modifier onlyByGovernor() { + if (governor != msg.sender) revert GovernorOnly(); + _; + } + + modifier onlyByCore() { + if (address(core) != msg.sender) revert CoreOnly(); + _; + } + + // ************************************* // + // * Constructor * // + // ************************************* // /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); @@ -42,16 +103,16 @@ contract StakeController is StakeControllerBase { RNG _rng, uint256 _rngLookahead ) external initializer { - __StakeControllerBase_initialize( - _governor, - _core, - _vault, - _sortition, - _minStakingTime, - _maxDrawingTime, - _rng, - _rngLookahead - ); + governor = _governor; + core = _core; + vault = _vault; + sortition = _sortition; + minStakingTime = _minStakingTime; + maxDrawingTime = _maxDrawingTime; + lastPhaseChange = block.timestamp; + rng = _rng; + rngLookahead = _rngLookahead; + delayedStakeReadIndex = 1; } // ************************************* // @@ -63,4 +124,342 @@ contract StakeController is StakeControllerBase { function _authorizeUpgrade(address) internal view override onlyByGovernor { // NOP } + + /// @dev Changes the governor of the contract. + /// @param _governor The new governor. + function changeGovernor(address _governor) external onlyByGovernor { + governor = _governor; + } + + /// @dev Changes the `vault` storage variable. + /// @param _vault The new vault address. + function changeVault(IVault _vault) external onlyByGovernor { + vault = _vault; + } + + /// @dev Changes the `sortition` storage variable. + /// @param _sortition The new sortition module address. + function changeSortitionSumTree(ISortitionSumTree _sortition) external onlyByGovernor { + sortition = _sortition; + } + + /// @dev Changes the `minStakingTime` storage variable. + /// @param _minStakingTime The new value for the `minStakingTime` storage variable. + function changeMinStakingTime(uint256 _minStakingTime) external onlyByGovernor { + minStakingTime = _minStakingTime; + } + + /// @dev Changes the `maxDrawingTime` storage variable. + /// @param _maxDrawingTime The new value for the `maxDrawingTime` storage variable. + function changeMaxDrawingTime(uint256 _maxDrawingTime) external onlyByGovernor { + maxDrawingTime = _maxDrawingTime; + } + + /// @dev Changes the `_rng` and `_rngLookahead` storage variables. + /// @param _rng The new value for the `RNGenerator` storage variable. + /// @param _rngLookahead The new value for the `rngLookahead` storage variable. + function changeRandomNumberGenerator(RNG _rng, uint256 _rngLookahead) external onlyByGovernor { + rng = _rng; + rngLookahead = _rngLookahead; + if (phase == Phase.generating) { + rng.requestRandomness(block.number + rngLookahead); + randomNumberRequestBlock = block.number; + } + } + + // ************************************* // + // * Phase Management * // + // ************************************* // + + /// @inheritdoc IStakeController + function passPhase() external override { + if (phase == Phase.staking) { + if (block.timestamp - lastPhaseChange < minStakingTime) revert MinStakingTimeNotPassed(); + if (disputesWithoutJurors == 0) revert NoDisputesNeedingJurors(); + rng.requestRandomness(block.number + rngLookahead); + randomNumberRequestBlock = block.number; + phase = Phase.generating; + } else if (phase == Phase.generating) { + randomNumber = rng.receiveRandomness(randomNumberRequestBlock + rngLookahead); + if (randomNumber == 0) revert RandomNumberNotReady(); + phase = Phase.drawing; + } else if (phase == Phase.drawing) { + if (disputesWithoutJurors > 0 && block.timestamp - lastPhaseChange < maxDrawingTime) { + revert StillDrawingDisputes(); + } + phase = Phase.staking; + } + + lastPhaseChange = block.timestamp; + emit NewPhase(phase); + } + + /// @inheritdoc IStakeController + function executeDelayedStakes(uint256 _iterations) external override { + if (phase != Phase.staking) revert NotInStakingPhase(); + if (delayedStakeWriteIndex < delayedStakeReadIndex) revert NoDelayedStakes(); + + uint256 actualIterations = (delayedStakeReadIndex + _iterations) - 1 > delayedStakeWriteIndex + ? (delayedStakeWriteIndex - delayedStakeReadIndex) + 1 + : _iterations; + uint256 newDelayedStakeReadIndex = delayedStakeReadIndex + actualIterations; + + for (uint256 i = delayedStakeReadIndex; i < newDelayedStakeReadIndex; i++) { + DelayedStake storage delayedStake = delayedStakes[i]; + if (delayedStake.account == address(0)) continue; + + // Let KlerosCore coordinate stake update and vault deposit/withdrawal. + try core.setStakeByController(delayedStake.account, delayedStake.courtID, delayedStake.stake) { + // NOP + } catch (bytes memory data) { + emit DelayedStakeSetFailed(data); + } + delete delayedStakes[i]; + } + delayedStakeReadIndex = newDelayedStakeReadIndex; + } + + // ************************************* // + // * Stake Management * // + // ************************************* // + + /// @inheritdoc IStakeController + function validateStake( + address _account, + uint96 _courtID, + uint256 _newStake + ) external view override returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { + JurorStake storage currentJurorStake = jurorStakes[_account]; + uint256 currentStakeInCourt = currentJurorStake.stakes[_courtID]; + + if (_newStake > currentStakeInCourt) { + pnkDeposit = _newStake - currentStakeInCourt; + } else if (_newStake < currentStakeInCourt) { + pnkWithdrawal = currentStakeInCourt - _newStake; + } + + if (phase != Phase.staking) { + return (pnkDeposit, pnkWithdrawal, StakingResult.Delayed); + } else { + if (currentStakeInCourt == 0) { + if (_newStake == 0) revert StakingZeroWhenNoStake(); + else if (_newStake > 0 && currentJurorStake.stakedCourtIDs.length >= MAX_STAKE_PATHS) + revert StakingInTooManyCourts(); + } + return (pnkDeposit, pnkWithdrawal, StakingResult.Successful); + } + } + + /// @inheritdoc IStakeController + function setStake( + address _account, + uint96 _courtID, + uint256 _newStake, + uint256 _pnkDeposit, + uint256 _pnkWithdrawal + ) public override onlyByCore { + JurorStake storage currentJurorStake = jurorStakes[_account]; + uint256 currentStakeInCourt = currentJurorStake.stakes[_courtID]; + + if (phase != Phase.staking) { + revert NotInStakingPhase(); + } + + // Update jurorStakes + currentJurorStake.stakes[_courtID] = _newStake; + if (_newStake > currentStakeInCourt) { + currentJurorStake.totalStake += _pnkDeposit; + } else if (_newStake < currentStakeInCourt) { + currentJurorStake.totalStake -= _pnkWithdrawal; + } + + // Manage stakedCourtIDs + if (currentStakeInCourt == 0 && _newStake > 0) { + currentJurorStake.stakedCourtIDs.push(_courtID); + } else if (currentStakeInCourt > 0 && _newStake == 0) { + _removeCourt(currentJurorStake.stakedCourtIDs, _courtID); + } + + // Update sortition tree + sortition.setStake(_account, _courtID, _newStake); + + emit StakeSet(_account, _courtID, _newStake, currentJurorStake.totalStake); + } + + /// @inheritdoc IStakeController + function setStakeDelayed(address _account, uint96 _courtID, uint256 _newStake) public override onlyByCore { + DelayedStake storage delayedStake = delayedStakes[++delayedStakeWriteIndex]; + delayedStake.account = _account; + delayedStake.courtID = _courtID; + delayedStake.stake = _newStake; + } + + /// @inheritdoc IStakeController + function lockStake(address _account, uint256 _amount) external override onlyByCore { + vault.lockTokens(_account, _amount); + emit StakeLocked(_account, _amount); // Event name might be misleading, should be StakeLocked. Preserved for compatibility if so. + } + + /// @inheritdoc IStakeController + function unlockStake(address _account, uint256 _amount) external override onlyByCore { + vault.unlockTokens(_account, _amount); + emit StakeUnlocked(_account, _amount); + } + + /// @inheritdoc IStakeController + function setJurorPenalty( + address _account, + uint256 _penalty + ) external virtual override onlyByCore returns (uint256 pnkBalance, uint256 actualPenalty) { + vault.unlockTokens(_account, _penalty); + (pnkBalance, actualPenalty) = vault.applyPenalty(_account, _penalty); + emit JurorPenaltyExecuted(_account, _penalty, actualPenalty); + } + + /// @inheritdoc IStakeController + function setJurorInactive(address _account) external override onlyByCore returns (uint256 pnkToWithdraw) { + JurorStake storage currentJurorStake = jurorStakes[_account]; + uint96[] storage stakedCourtIDs = currentJurorStake.stakedCourtIDs; + while (stakedCourtIDs.length > 0) { + uint96 courtID = stakedCourtIDs[0]; + uint256 currentStakeInCourt = currentJurorStake.stakes[courtID]; + if (phase == Phase.staking) { + setStake(_account, courtID, 0, 0, currentStakeInCourt); + } else { + setStakeDelayed(_account, courtID, 0); + } + } + if (phase == Phase.staking) { + pnkToWithdraw = vault.getAvailableBalance(_account); + emit JurorSetInactive(_account, false); + } else { + pnkToWithdraw = 0; + emit JurorSetInactive(_account, true); + } + } + + // ************************************* // + // * Sortition Delegation * // + // ************************************* // + + /// @inheritdoc IStakeController + function createTree(bytes32 _key, bytes memory _extraData) external override onlyByCore { + sortition.createTree(_key, _extraData); + } + + /// @inheritdoc IStakeController + function draw(bytes32 _court, uint256 _coreDisputeID, uint256 _nonce) external view override returns (address) { + if (phase != Phase.drawing) revert NotDrawingPhase(); + if (randomNumber == 0) revert RandomNumberNotReadyYet(); + return sortition.draw(_court, _coreDisputeID, _nonce, randomNumber); + } + + /// @inheritdoc IStakeController + function createDisputeHook(uint256 /*_disputeID*/, uint256 /*_roundID*/) external override onlyByCore { + disputesWithoutJurors++; + } + + /// @inheritdoc IStakeController + function postDrawHook(uint256 /*_disputeID*/, uint256 /*_roundID*/) external override onlyByCore { + disputesWithoutJurors--; + } + + // ************************************* // + // * View Functions * // + // ************************************* // + + /// @inheritdoc IStakeController + function getJurorBalance( + address _juror, + uint96 _courtID + ) + external + view + override + returns ( + uint256 availablePnk, + uint256 lockedPnk, + uint256 penaltyPnk, + uint256 totalStaked, + uint256 stakedInCourt, + uint256 nbCourts + ) + { + availablePnk = vault.getAvailableBalance(_juror); + lockedPnk = vault.getLockedBalance(_juror); + penaltyPnk = vault.getPenaltyBalance(_juror); + totalStaked = jurorStakes[_juror].totalStake; + stakedInCourt = jurorStakes[_juror].stakes[_courtID]; + nbCourts = jurorStakes[_juror].stakedCourtIDs.length; + } + + /// @inheritdoc IStakeController + function getJurorCourtIDs(address _juror) external view override returns (uint96[] memory) { + return jurorStakes[_juror].stakedCourtIDs; + } + + /// @inheritdoc IStakeController + function isJurorStaked(address _juror) external view override returns (bool) { + return jurorStakes[_juror].totalStake > 0; + } + + /// @inheritdoc IStakeController + function getAvailableBalance(address _account) external view override returns (uint256) { + return vault.getAvailableBalance(_account); + } + + /// @inheritdoc IStakeController + function getDepositedBalance(address _account) external view override returns (uint256) { + return vault.getDepositedBalance(_account); + } + + // ************************************* // + // * Internal * // + // ************************************* // + + /// @dev Removes a court from a juror's list of staked courts. + /// @param _stakedCourts Storage pointer to the juror's array of staked court IDs. + /// @param _courtID The ID of the court to remove. + function _removeCourt(uint96[] storage _stakedCourts, uint96 _courtID) internal { + uint256 length = _stakedCourts.length; + if (length == 0) { + return; // Nothing to remove + } + + uint256 courtIndexToRemove = type(uint256).max; // Sentinel value indicates not found + for (uint256 i = 0; i < length; i++) { + if (_stakedCourts[i] == _courtID) { + courtIndexToRemove = i; + break; + } + } + + // If the courtID was found in the array + if (courtIndexToRemove != type(uint256).max) { + // If it's not the last element + if (courtIndexToRemove != length - 1) { + // Swap the last element into its place + _stakedCourts[courtIndexToRemove] = _stakedCourts[length - 1]; + } + // Remove the last element (either the original last, or the one that was swapped) + _stakedCourts.pop(); + } + } + + // ************************************* // + // * Errors * // + // ************************************* // + + error GovernorOnly(); + error CoreOnly(); + error MinStakingTimeNotPassed(); + error NoDisputesNeedingJurors(); + error RandomNumberNotReady(); + error RandomNumberNotReadyYet(); + error StillDrawingDisputes(); + error StakingZeroWhenNoStake(); + error StakingInTooManyCourts(); + error NotInStakingPhase(); + error NotDrawingPhase(); + error NoDelayedStakes(); } diff --git a/contracts/src/arbitration/StakeControllerBase.sol b/contracts/src/arbitration/StakeControllerBase.sol deleted file mode 100644 index 8d2d6f389..000000000 --- a/contracts/src/arbitration/StakeControllerBase.sol +++ /dev/null @@ -1,444 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import {IStakeController} from "./interfaces/IStakeController.sol"; -import {IVault} from "./interfaces/IVault.sol"; -import {ISortitionSumTree} from "./interfaces/ISortitionSumTree.sol"; -import {IDisputeKit} from "./interfaces/IDisputeKit.sol"; -import {KlerosCoreBase} from "./KlerosCoreBase.sol"; -import {Initializable} from "../proxy/Initializable.sol"; -import {UUPSProxiable} from "../proxy/UUPSProxiable.sol"; -import {RNG} from "../rng/RNG.sol"; -import "../libraries/Constants.sol"; - -/// @title StakeControllerBase -/// @notice Abstract base contract for coordinating between Vault and SortitionSumTree -/// @dev Manages phases, delayed stakes, and coordination logic -abstract contract StakeControllerBase is IStakeController, Initializable, UUPSProxiable { - // ************************************* // - // * Enums / Structs * // - // ************************************* // - - struct DelayedStake { - address account; // The address of the juror. - uint96 courtID; // The ID of the court. - uint256 stake; // The new stake. - } - - struct JurorStake { - uint256 totalStake; - uint96[] stakedCourtIDs; - mapping(uint96 courtID => uint256 stake) stakes; - } - - // ************************************* // - // * Storage * // - // ************************************* // - - address public governor; // The governor of the contract. - KlerosCoreBase public core; // The core arbitrator contract. - IVault public vault; // The PNK vault for token management. - ISortitionSumTree public sortition; // The sortition sum tree for drawing. - - // Phase management - Phase public override phase; // The current phase. Uses Phase from IStakeController. - uint256 public minStakingTime; // The time after which the phase can be switched to Drawing if there are open disputes. - uint256 public maxDrawingTime; // The time after which the phase can be switched back to Staking. - uint256 public lastPhaseChange; // The last time the phase was changed. - uint256 public randomNumberRequestBlock; // Number of the block when RNG request was made. - uint256 public disputesWithoutJurors; // The number of disputes that have not finished drawing jurors. - RNG public rng; // The random number generator. - uint256 public randomNumber; // Random number returned by RNG. - uint256 public rngLookahead; // Minimal block distance between requesting and obtaining a random number. - - // Delayed stakes management - uint256 public delayedStakeWriteIndex; // The index of the last `delayedStake` item that was written to the array. 0 index is skipped. - uint256 public delayedStakeReadIndex; // The index of the next `delayedStake` item that should be processed. Starts at 1 because 0 index is skipped. - mapping(uint256 index => DelayedStake) public delayedStakes; // Stores the stakes that were changed during Drawing phase, to update them when the phase is switched to Staking. - - // Stake management - mapping(address => JurorStake) internal jurorStakes; - - // ************************************* // - // * Function Modifiers * // - // ************************************* // - - modifier onlyByGovernor() { - if (governor != msg.sender) revert GovernorOnly(); - _; - } - - modifier onlyByCore() { - if (address(core) != msg.sender) revert CoreOnly(); - _; - } - - // ************************************* // - // * Constructor * // - // ************************************* // - - function __StakeControllerBase_initialize( - address _governor, - KlerosCoreBase _core, - IVault _vault, - ISortitionSumTree _sortition, - uint256 _minStakingTime, - uint256 _maxDrawingTime, - RNG _rng, - uint256 _rngLookahead - ) internal onlyInitializing { - governor = _governor; - core = _core; - vault = _vault; - sortition = _sortition; - minStakingTime = _minStakingTime; - maxDrawingTime = _maxDrawingTime; - lastPhaseChange = block.timestamp; - rng = _rng; - rngLookahead = _rngLookahead; - delayedStakeReadIndex = 1; - } - - // ************************************* // - // * Governance * // - // ************************************* // - - /// @dev Changes the governor of the contract. - /// @param _governor The new governor. - function changeGovernor(address _governor) external onlyByGovernor { - governor = _governor; - } - - /// @dev Changes the `vault` storage variable. - /// @param _vault The new vault address. - function changeVault(IVault _vault) external onlyByGovernor { - vault = _vault; - } - - /// @dev Changes the `sortition` storage variable. - /// @param _sortition The new sortition module address. - function changeSortitionSumTree(ISortitionSumTree _sortition) external onlyByGovernor { - sortition = _sortition; - } - - /// @dev Changes the `minStakingTime` storage variable. - /// @param _minStakingTime The new value for the `minStakingTime` storage variable. - function changeMinStakingTime(uint256 _minStakingTime) external onlyByGovernor { - minStakingTime = _minStakingTime; - } - - /// @dev Changes the `maxDrawingTime` storage variable. - /// @param _maxDrawingTime The new value for the `maxDrawingTime` storage variable. - function changeMaxDrawingTime(uint256 _maxDrawingTime) external onlyByGovernor { - maxDrawingTime = _maxDrawingTime; - } - - /// @dev Changes the `_rng` and `_rngLookahead` storage variables. - /// @param _rng The new value for the `RNGenerator` storage variable. - /// @param _rngLookahead The new value for the `rngLookahead` storage variable. - function changeRandomNumberGenerator(RNG _rng, uint256 _rngLookahead) external onlyByGovernor { - rng = _rng; - rngLookahead = _rngLookahead; - if (phase == Phase.generating) { - rng.requestRandomness(block.number + rngLookahead); - randomNumberRequestBlock = block.number; - } - } - - // ************************************* // - // * Phase Management * // - // ************************************* // - - /// @inheritdoc IStakeController - function passPhase() external override { - if (phase == Phase.staking) { - if (block.timestamp - lastPhaseChange < minStakingTime) revert MinStakingTimeNotPassed(); - if (disputesWithoutJurors == 0) revert NoDisputesNeedingJurors(); - rng.requestRandomness(block.number + rngLookahead); - randomNumberRequestBlock = block.number; - phase = Phase.generating; - } else if (phase == Phase.generating) { - randomNumber = rng.receiveRandomness(randomNumberRequestBlock + rngLookahead); - if (randomNumber == 0) revert RandomNumberNotReady(); - phase = Phase.drawing; - } else if (phase == Phase.drawing) { - if (disputesWithoutJurors > 0 && block.timestamp - lastPhaseChange < maxDrawingTime) { - revert StillDrawingDisputes(); - } - phase = Phase.staking; - } - - lastPhaseChange = block.timestamp; - emit NewPhase(phase); - } - - /// @inheritdoc IStakeController - function executeDelayedStakes(uint256 _iterations) external override { - if (phase != Phase.staking) revert NotInStakingPhase(); - if (delayedStakeWriteIndex < delayedStakeReadIndex) revert NoDelayedStakes(); - - uint256 actualIterations = (delayedStakeReadIndex + _iterations) - 1 > delayedStakeWriteIndex - ? (delayedStakeWriteIndex - delayedStakeReadIndex) + 1 - : _iterations; - uint256 newDelayedStakeReadIndex = delayedStakeReadIndex + actualIterations; - - for (uint256 i = delayedStakeReadIndex; i < newDelayedStakeReadIndex; i++) { - DelayedStake storage delayedStake = delayedStakes[i]; - if (delayedStake.account == address(0)) continue; - - // Let KlerosCore coordinate stake update and vault deposit/withdrawal. - try core.setStakeByController(delayedStake.account, delayedStake.courtID, delayedStake.stake) { - // NOP - } catch (bytes memory data) { - emit DelayedStakeSetFailed(data); - } - delete delayedStakes[i]; - } - delayedStakeReadIndex = newDelayedStakeReadIndex; - } - - // ************************************* // - // * Stake Management * // - // ************************************* // - - /// @inheritdoc IStakeController - function validateStake( - address _account, - uint96 _courtID, - uint256 _newStake - ) external view override returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { - JurorStake storage currentJurorStake = jurorStakes[_account]; - uint256 currentStakeInCourt = currentJurorStake.stakes[_courtID]; - - if (_newStake > currentStakeInCourt) { - pnkDeposit = _newStake - currentStakeInCourt; - } else if (_newStake < currentStakeInCourt) { - pnkWithdrawal = currentStakeInCourt - _newStake; - } - - if (phase != Phase.staking) { - return (pnkDeposit, pnkWithdrawal, StakingResult.Delayed); - } else { - if (currentStakeInCourt == 0) { - if (_newStake == 0) revert StakingZeroWhenNoStake(); - else if (_newStake > 0 && currentJurorStake.stakedCourtIDs.length >= MAX_STAKE_PATHS) - revert StakingInTooManyCourts(); - } - return (pnkDeposit, pnkWithdrawal, StakingResult.Successful); - } - } - - /// @inheritdoc IStakeController - function setStake( - address _account, - uint96 _courtID, - uint256 _newStake, - uint256 _pnkDeposit, - uint256 _pnkWithdrawal - ) public override onlyByCore { - JurorStake storage currentJurorStake = jurorStakes[_account]; - uint256 currentStakeInCourt = currentJurorStake.stakes[_courtID]; - - if (phase != Phase.staking) { - revert NotInStakingPhase(); - } - - // Update jurorStakes - currentJurorStake.stakes[_courtID] = _newStake; - if (_newStake > currentStakeInCourt) { - currentJurorStake.totalStake += _pnkDeposit; - } else if (_newStake < currentStakeInCourt) { - currentJurorStake.totalStake -= _pnkWithdrawal; - } - - // Manage stakedCourtIDs - if (currentStakeInCourt == 0 && _newStake > 0) { - currentJurorStake.stakedCourtIDs.push(_courtID); - } else if (currentStakeInCourt > 0 && _newStake == 0) { - _removeCourt(currentJurorStake.stakedCourtIDs, _courtID); - } - - // Update sortition tree - sortition.setStake(_account, _courtID, _newStake); - - emit StakeSet(_account, _courtID, _newStake, currentJurorStake.totalStake); - } - - /// @inheritdoc IStakeController - function setStakeDelayed(address _account, uint96 _courtID, uint256 _newStake) public override onlyByCore { - DelayedStake storage delayedStake = delayedStakes[++delayedStakeWriteIndex]; - delayedStake.account = _account; - delayedStake.courtID = _courtID; - delayedStake.stake = _newStake; - } - - /// @inheritdoc IStakeController - function lockStake(address _account, uint256 _amount) external override onlyByCore { - vault.lockTokens(_account, _amount); - emit StakeLocked(_account, _amount); // Event name might be misleading, should be StakeLocked. Preserved for compatibility if so. - } - - /// @inheritdoc IStakeController - function unlockStake(address _account, uint256 _amount) external override onlyByCore { - vault.unlockTokens(_account, _amount); - emit StakeUnlocked(_account, _amount); - } - - /// @inheritdoc IStakeController - function setJurorPenalty( - address _account, - uint256 _penalty - ) external virtual override onlyByCore returns (uint256 pnkBalance, uint256 actualPenalty) { - vault.unlockTokens(_account, _penalty); - (pnkBalance, actualPenalty) = vault.applyPenalty(_account, _penalty); - emit JurorPenaltyExecuted(_account, _penalty, actualPenalty); - } - - /// @inheritdoc IStakeController - function setJurorInactive(address _account) external override onlyByCore returns (uint256 pnkToWithdraw) { - JurorStake storage currentJurorStake = jurorStakes[_account]; - uint96[] storage stakedCourtIDs = currentJurorStake.stakedCourtIDs; - while (stakedCourtIDs.length > 0) { - uint96 courtID = stakedCourtIDs[0]; - uint256 currentStakeInCourt = currentJurorStake.stakes[courtID]; - if (phase == Phase.staking) { - setStake(_account, courtID, 0, 0, currentStakeInCourt); - } else { - setStakeDelayed(_account, courtID, 0); - } - } - if (phase == Phase.staking) { - pnkToWithdraw = vault.getAvailableBalance(_account); - emit JurorSetInactive(_account, false); - } else { - pnkToWithdraw = 0; - emit JurorSetInactive(_account, true); - } - } - - // ************************************* // - // * Sortition Delegation * // - // ************************************* // - - /// @inheritdoc IStakeController - function createTree(bytes32 _key, bytes memory _extraData) external override onlyByCore { - sortition.createTree(_key, _extraData); - } - - /// @inheritdoc IStakeController - function draw(bytes32 _court, uint256 _coreDisputeID, uint256 _nonce) external view override returns (address) { - if (phase != Phase.drawing) revert NotDrawingPhase(); - if (randomNumber == 0) revert RandomNumberNotReadyYet(); - return sortition.draw(_court, _coreDisputeID, _nonce, randomNumber); - } - - /// @inheritdoc IStakeController - function createDisputeHook(uint256 /*_disputeID*/, uint256 /*_roundID*/) external override onlyByCore { - disputesWithoutJurors++; - } - - /// @inheritdoc IStakeController - function postDrawHook(uint256 /*_disputeID*/, uint256 /*_roundID*/) external override onlyByCore { - disputesWithoutJurors--; - } - - // ************************************* // - // * View Functions * // - // ************************************* // - - /// @inheritdoc IStakeController - function getJurorBalance( - address _juror, - uint96 _courtID - ) - external - view - override - returns ( - uint256 availablePnk, - uint256 lockedPnk, - uint256 penaltyPnk, - uint256 totalStaked, - uint256 stakedInCourt, - uint256 nbCourts - ) - { - availablePnk = vault.getAvailableBalance(_juror); - lockedPnk = vault.getLockedBalance(_juror); - penaltyPnk = vault.getPenaltyBalance(_juror); - totalStaked = jurorStakes[_juror].totalStake; - stakedInCourt = jurorStakes[_juror].stakes[_courtID]; - nbCourts = jurorStakes[_juror].stakedCourtIDs.length; - } - - /// @inheritdoc IStakeController - function getJurorCourtIDs(address _juror) external view override returns (uint96[] memory) { - return jurorStakes[_juror].stakedCourtIDs; - } - - /// @inheritdoc IStakeController - function isJurorStaked(address _juror) external view override returns (bool) { - return jurorStakes[_juror].totalStake > 0; - } - - /// @inheritdoc IStakeController - function getAvailableBalance(address _account) external view override returns (uint256) { - return vault.getAvailableBalance(_account); - } - - /// @inheritdoc IStakeController - function getDepositedBalance(address _account) external view override returns (uint256) { - return vault.getDepositedBalance(_account); - } - - // ************************************* // - // * Internal * // - // ************************************* // - - /// @dev Removes a court from a juror's list of staked courts. - /// @param _stakedCourts Storage pointer to the juror's array of staked court IDs. - /// @param _courtID The ID of the court to remove. - function _removeCourt(uint96[] storage _stakedCourts, uint96 _courtID) internal { - uint256 length = _stakedCourts.length; - if (length == 0) { - return; // Nothing to remove - } - - uint256 courtIndexToRemove = type(uint256).max; // Sentinel value indicates not found - for (uint256 i = 0; i < length; i++) { - if (_stakedCourts[i] == _courtID) { - courtIndexToRemove = i; - break; - } - } - - // If the courtID was found in the array - if (courtIndexToRemove != type(uint256).max) { - // If it's not the last element - if (courtIndexToRemove != length - 1) { - // Swap the last element into its place - _stakedCourts[courtIndexToRemove] = _stakedCourts[length - 1]; - } - // Remove the last element (either the original last, or the one that was swapped) - _stakedCourts.pop(); - } - } - - // ************************************* // - // * Errors * // - // ************************************* // - - error GovernorOnly(); - error CoreOnly(); - error MinStakingTimeNotPassed(); - error NoDisputesNeedingJurors(); - error RandomNumberNotReady(); - error RandomNumberNotReadyYet(); - error StillDrawingDisputes(); - error StakingZeroWhenNoStake(); - error StakingInTooManyCourts(); - error NotInStakingPhase(); - error NotDrawingPhase(); - error NoDelayedStakes(); -} diff --git a/contracts/src/test/SortitionModuleMock.sol b/contracts/src/test/SortitionModuleMock.sol index c0224b8a7..2d49aac6f 100644 --- a/contracts/src/test/SortitionModuleMock.sol +++ b/contracts/src/test/SortitionModuleMock.sol @@ -16,7 +16,7 @@ import "../arbitration/SortitionSumTree.sol"; /// @dev Adds getter functions to sortition module for Foundry tests. contract SortitionModuleMock is SortitionSumTree { function getSortitionProperties(bytes32 _key) external view returns (uint256 K, uint256 nodeLength) { - SortitionSumTree storage tree = sortitionSumTrees[_key]; + SumTree storage tree = sortitionSumTrees[_key]; K = tree.K; nodeLength = tree.nodes.length; } From cb7d9b4f5a47f4232bf302044c5164694ce892ba Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Mon, 9 Jun 2025 23:03:20 +0100 Subject: [PATCH 22/24] fix: missing setStakeDelayed --- contracts/src/arbitration/KlerosCoreBase.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/src/arbitration/KlerosCoreBase.sol b/contracts/src/arbitration/KlerosCoreBase.sol index dc1bea0e9..9a68676c9 100644 --- a/contracts/src/arbitration/KlerosCoreBase.sol +++ b/contracts/src/arbitration/KlerosCoreBase.sol @@ -1089,6 +1089,7 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable _newStake ); if (stakingResult == StakingResult.Delayed) { + stakeController.setStakeDelayed(_account, _courtID, _newStake); return true; } success = true; From 56b7b34200980355e53dba3aeca77f05879aab57 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Mon, 9 Jun 2025 23:12:01 +0100 Subject: [PATCH 23/24] fix: proxy versions, KlerosCoreSnapshotProxy --- contracts/src/arbitration/KlerosCore.sol | 2 +- contracts/src/arbitration/KlerosCoreBase.sol | 2 +- contracts/src/arbitration/KlerosCoreNeo.sol | 3 +-- contracts/src/arbitration/SortitionSumTree.sol | 2 +- contracts/src/arbitration/StakeController.sol | 2 +- contracts/src/arbitration/Vault.sol | 2 +- contracts/src/arbitration/VaultNeo.sol | 2 +- contracts/src/arbitration/view/KlerosCoreSnapshotProxy.sol | 6 +++--- 8 files changed, 10 insertions(+), 11 deletions(-) diff --git a/contracts/src/arbitration/KlerosCore.sol b/contracts/src/arbitration/KlerosCore.sol index a4f10ef2f..383d1131d 100644 --- a/contracts/src/arbitration/KlerosCore.sol +++ b/contracts/src/arbitration/KlerosCore.sol @@ -8,7 +8,7 @@ import "./KlerosCoreBase.sol"; /// @notice KlerosCore implementation with new StakeController architecture for testing environments contract KlerosCore is KlerosCoreBase { /// @notice Version of the implementation contract - string public constant override version = "0.0.1"; + string public constant override version = "0.10.0"; // ************************************* // // * Constructor * // diff --git a/contracts/src/arbitration/KlerosCoreBase.sol b/contracts/src/arbitration/KlerosCoreBase.sol index 9a68676c9..c376756d5 100644 --- a/contracts/src/arbitration/KlerosCoreBase.sol +++ b/contracts/src/arbitration/KlerosCoreBase.sol @@ -1071,7 +1071,7 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable emit DisputeKitEnabled(_courtID, _disputeKitID, _enable); } - /// @dev If called only once then set _onError to Revert, otherwise for batch staking set it to Return + /// @dev It may revert if the stake change in invalid. /// @param _account The account to set the stake for. /// @param _courtID The ID of the court to set the stake for. /// @param _newStake The new stake. diff --git a/contracts/src/arbitration/KlerosCoreNeo.sol b/contracts/src/arbitration/KlerosCoreNeo.sol index 268e023a6..557b52cba 100644 --- a/contracts/src/arbitration/KlerosCoreNeo.sol +++ b/contracts/src/arbitration/KlerosCoreNeo.sol @@ -3,12 +3,11 @@ pragma solidity 0.8.24; import "./KlerosCoreBase.sol"; -import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; /// @title KlerosCoreNeo /// @notice KlerosCore with whitelisted arbitrables contract KlerosCoreNeo is KlerosCoreBase { - string public constant override version = "0.0.1"; + string public constant override version = "0.10.0"; // ************************************* // // * Storage * // diff --git a/contracts/src/arbitration/SortitionSumTree.sol b/contracts/src/arbitration/SortitionSumTree.sol index 833d15e56..341408437 100644 --- a/contracts/src/arbitration/SortitionSumTree.sol +++ b/contracts/src/arbitration/SortitionSumTree.sol @@ -13,7 +13,7 @@ import "../libraries/Constants.sol"; /// @notice Responsible for sortition operations /// @dev Contains only tree management and drawing logic, no phase management or token operations contract SortitionSumTree is ISortitionSumTree, Initializable, UUPSProxiable { - string public constant override version = "2.0.0"; + string public constant override version = "0.9.0"; // ************************************* // // * Enums / Structs * // diff --git a/contracts/src/arbitration/StakeController.sol b/contracts/src/arbitration/StakeController.sol index 06ab684da..48f031ee3 100644 --- a/contracts/src/arbitration/StakeController.sol +++ b/contracts/src/arbitration/StakeController.sol @@ -16,7 +16,7 @@ import "../libraries/Constants.sol"; /// @notice Responsible for coordinating between Vault and SortitionSumTree /// @dev Manages phases, delayed stakes, and coordination logic contract StakeController is IStakeController, Initializable, UUPSProxiable { - string public constant override version = "1.0.0"; + string public constant override version = "0.9.0"; // ************************************* // // * Enums / Structs * // diff --git a/contracts/src/arbitration/Vault.sol b/contracts/src/arbitration/Vault.sol index c00d8617f..ac0699c84 100644 --- a/contracts/src/arbitration/Vault.sol +++ b/contracts/src/arbitration/Vault.sol @@ -8,7 +8,7 @@ import {VaultBase, IERC20} from "./VaultBase.sol"; /// @notice PNK Vault for handling deposits, withdrawals, locks, and penalties /// @dev Follows the same pattern as KlerosCore for upgradeable contracts contract Vault is VaultBase { - string public constant override version = "1.0.0"; + string public constant override version = "0.1.0"; // ************************************* // // * Constructor * // diff --git a/contracts/src/arbitration/VaultNeo.sol b/contracts/src/arbitration/VaultNeo.sol index a68b34491..d58025f08 100644 --- a/contracts/src/arbitration/VaultNeo.sol +++ b/contracts/src/arbitration/VaultNeo.sol @@ -12,7 +12,7 @@ import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; contract VaultNeo is VaultBase { using SafeERC20 for IERC20; - string public constant override version = "1.0.0"; + string public constant override version = "0.1.0"; // ************************************* // // * Storage * // diff --git a/contracts/src/arbitration/view/KlerosCoreSnapshotProxy.sol b/contracts/src/arbitration/view/KlerosCoreSnapshotProxy.sol index 633204a64..af733f2eb 100644 --- a/contracts/src/arbitration/view/KlerosCoreSnapshotProxy.sol +++ b/contracts/src/arbitration/view/KlerosCoreSnapshotProxy.sol @@ -2,10 +2,10 @@ pragma solidity 0.8.24; -import {ISortitionModule} from "../interfaces/ISortitionModule.sol"; +import {IStakeController} from "../interfaces/IStakeController.sol"; interface IKlerosCore { - function sortitionModule() external view returns (ISortitionModule); + function stakeController() external view returns (IStakeController); } /// @title KlerosCoreSnapshotProxy @@ -67,6 +67,6 @@ contract KlerosCoreSnapshotProxy { /// @param _account The address to query. /// @return totalStaked Total amount staked in V2 by the address. function balanceOf(address _account) external view returns (uint256 totalStaked) { - (totalStaked, , , ) = core.sortitionModule().getJurorBalance(_account, 0); + (, , , totalStaked, , ) = core.stakeController().getJurorBalance(_account, 0); } } From 6c28f88616438d315f0dfcc2ddebf427096a1780 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Mon, 9 Jun 2025 23:24:27 +0100 Subject: [PATCH 24/24] docs: minor things --- contracts/src/arbitration/KlerosCoreBase.sol | 4 ++-- contracts/src/arbitration/SortitionSumTree.sol | 1 + contracts/src/arbitration/Vault.sol | 3 +-- contracts/src/arbitration/VaultBase.sol | 8 -------- contracts/src/arbitration/VaultNeo.sol | 1 - 5 files changed, 4 insertions(+), 13 deletions(-) diff --git a/contracts/src/arbitration/KlerosCoreBase.sol b/contracts/src/arbitration/KlerosCoreBase.sol index c376756d5..12d027094 100644 --- a/contracts/src/arbitration/KlerosCoreBase.sol +++ b/contracts/src/arbitration/KlerosCoreBase.sol @@ -12,8 +12,8 @@ import {SafeERC20, IERC20} from "../libraries/SafeERC20.sol"; import "../libraries/Constants.sol"; /// @title KlerosCoreBase -/// Core arbitrator contract for Kleros v2 with new StakeController architecture. -/// Note that this contract trusts the PNK token, the dispute kit and the stake controller contracts. +/// Core arbitrator contract for Kleros v2. +/// Note that this contract trusts the dispute kit and the stake controller contracts. abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable { using SafeERC20 for IERC20; diff --git a/contracts/src/arbitration/SortitionSumTree.sol b/contracts/src/arbitration/SortitionSumTree.sol index 341408437..3e35950c9 100644 --- a/contracts/src/arbitration/SortitionSumTree.sol +++ b/contracts/src/arbitration/SortitionSumTree.sol @@ -121,6 +121,7 @@ contract SortitionSumTree is ISortitionSumTree, Initializable, UUPSProxiable { KlerosCoreBase core = stakeController.core(); while (!finished) { + // Tokens are also implicitly staked in parent courts via _updateParents(). _set(bytes32(uint256(currentCourtID)), _newStake, stakePathID); if (currentCourtID == GENERAL_COURT) { finished = true; diff --git a/contracts/src/arbitration/Vault.sol b/contracts/src/arbitration/Vault.sol index ac0699c84..d4bb7f8fb 100644 --- a/contracts/src/arbitration/Vault.sol +++ b/contracts/src/arbitration/Vault.sol @@ -6,9 +6,8 @@ import {VaultBase, IERC20} from "./VaultBase.sol"; /// @title Vault /// @notice PNK Vault for handling deposits, withdrawals, locks, and penalties -/// @dev Follows the same pattern as KlerosCore for upgradeable contracts contract Vault is VaultBase { - string public constant override version = "0.1.0"; + string public constant override version = "0.10.0"; // ************************************* // // * Constructor * // diff --git a/contracts/src/arbitration/VaultBase.sol b/contracts/src/arbitration/VaultBase.sol index 1c13fb7ab..fc9b56183 100644 --- a/contracts/src/arbitration/VaultBase.sol +++ b/contracts/src/arbitration/VaultBase.sol @@ -9,7 +9,6 @@ import {SafeERC20, IERC20} from "../libraries/SafeERC20.sol"; /// @title VaultBase /// @notice Abstract base contract for PNK vault that handles deposits, withdrawals, locks, and penalties -/// @dev Follows the same pattern as KlerosCoreBase for upgradeable contracts abstract contract VaultBase is IVault, Initializable, UUPSProxiable { using SafeERC20 for IERC20; @@ -31,15 +30,8 @@ abstract contract VaultBase is IVault, Initializable, UUPSProxiable { IERC20 public pnk; // The PNK token contract. address public stakeController; // The stake controller authorized to lock/unlock/penalize. address public core; // The KlerosCore authorized to transfer rewards. - mapping(address => JurorBalance) public jurorBalances; // Juror balance tracking. - // ************************************* // - // * Events * // - // ************************************* // - - // Events are defined in IVault interface - // ************************************* // // * Function Modifiers * // // ************************************* // diff --git a/contracts/src/arbitration/VaultNeo.sol b/contracts/src/arbitration/VaultNeo.sol index d58025f08..03328d58a 100644 --- a/contracts/src/arbitration/VaultNeo.sol +++ b/contracts/src/arbitration/VaultNeo.sol @@ -8,7 +8,6 @@ import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; /// @title VaultNeo /// @notice Enhanced PNK Vault with additional features like NFT-gated deposits -/// @dev Follows the same pattern as KlerosCoreNeo for upgradeable contracts contract VaultNeo is VaultBase { using SafeERC20 for IERC20;