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/deploy/utils/klerosCoreHelper.ts b/contracts/deploy/utils/klerosCoreHelper.ts index 3419ae64e..0d4de4178 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, KlerosCore } from "../../typechain-types"; import { BigNumberish, toBigInt } from "ethers"; export const changeCurrencyRate = async ( - core: KlerosCore | KlerosCoreNeo | KlerosCoreRuler | KlerosCoreUniversity, + core: KlerosCore | KlerosCoreNeo | KlerosCoreRuler | KlerosCoreUniversity | KlerosCore, erc20: string, accepted: boolean, rateInEth: BigNumberish, diff --git a/contracts/src/arbitration/KlerosCore.sol b/contracts/src/arbitration/KlerosCore.sol index 287e4f685..383d1131d 100644 --- a/contracts/src/arbitration/KlerosCore.sol +++ b/contracts/src/arbitration/KlerosCore.sol @@ -2,71 +2,67 @@ pragma solidity 0.8.24; -import {KlerosCoreBase, IDisputeKit, ISortitionModule, IERC20} from "./KlerosCoreBase.sol"; +import "./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. +/// @notice KlerosCore implementation with new StakeController architecture for testing environments contract KlerosCore is KlerosCoreBase { - string public constant override version = "0.9.4"; + /// @notice Version of the implementation contract + string public constant override version = "0.10.0"; // ************************************* // // * Constructor * // // ************************************* // - /// @custom:oz-upgrades-unsafe-allow constructor + /// @notice Constructor, initializing the implementation to reduce attack surface. 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. + /// @notice Initialization function for UUPS proxy + /// @param _governor The governor of the contract. + /// @param _guardian The guardian able to pause asset withdrawals. + /// @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 _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, - ISortitionModule _sortitionModuleAddress - ) external reinitializer(1) { + IStakeController _stakeController, + IVault _vault + ) external initializer { __KlerosCoreBase_initialize( _governor, _guardian, - _pinakion, _jurorProsecutionModule, _disputeKit, _hiddenVotes, _courtParameters, _timesPerPeriod, _sortitionExtraData, - _sortitionModuleAddress + _stakeController, + _vault ); } - function initialize5() external reinitializer(5) { - // NOP - } - // ************************************* // // * Governance * // // ************************************* // - /// @dev Access Control to perform implementation upgrades (UUPS Proxiable) - /// Only the governor can perform upgrades (`onlyByGovernor`) + /// @notice Access Control to perform implementation upgrades (UUPS Proxiable) + /// Only the governor can perform upgrades (`onlyByGovernor`) function _authorizeUpgrade(address) internal view override onlyByGovernor { - // NOP + // Empty block: access control implemented by `onlyByGovernor` modifier } } diff --git a/contracts/src/arbitration/KlerosCoreBase.sol b/contracts/src/arbitration/KlerosCoreBase.sol index 6a435489b..12d027094 100644 --- a/contracts/src/arbitration/KlerosCoreBase.sol +++ b/contracts/src/arbitration/KlerosCoreBase.sol @@ -4,7 +4,8 @@ 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 {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"; @@ -12,7 +13,7 @@ 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. +/// Note that this contract trusts 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. @@ -362,11 +363,10 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable court.feeForJuror = _feeForJuror; court.jurorsForCourtJump = _jurorsForCourtJump; court.timesPerPeriod = _timesPerPeriod; + courts[_parent].children.push(courtID); - sortitionModule.createTree(bytes32(courtID), _sortitionExtraData); + stakeController.createTree(bytes32(courtID), _sortitionExtraData); - // Update the parent. - courts[_parent].children.push(courtID); emit CourtCreated( uint96(courtID), _parent, @@ -463,22 +463,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 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. - /// @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 +533,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 +608,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 +663,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 +773,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 +811,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 +848,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 +879,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 +1071,42 @@ 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 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. - /// @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) { + stakeController.setStakeDelayed(_account, _courtID, _newStake); + 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 +1149,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,11 +1157,8 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable error InvalidForkingCourtAsParent(); error WrongDisputeKitIndex(); error CannotDisableClassicDK(); - error StakingInTooManyCourts(); error StakingNotPossibleInThisCourt(); error StakingLessThanCourtMinStake(); - error StakingTransferFailed(); - error UnstakingTransferFailed(); error ArbitrationFeesNotEnough(); error DisputeKitNotSupportedByCourt(); error MustSupportDisputeKitClassic(); @@ -1199,5 +1177,4 @@ abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable error TransferFailed(); error WhenNotPausedOnly(); error WhenPausedOnly(); - error StakingZeroWhenNoStake(); } diff --git a/contracts/src/arbitration/KlerosCoreNeo.sol b/contracts/src/arbitration/KlerosCoreNeo.sol index 988e42a53..557b52cba 100644 --- a/contracts/src/arbitration/KlerosCoreNeo.sol +++ b/contracts/src/arbitration/KlerosCoreNeo.sol @@ -2,69 +2,62 @@ pragma solidity 0.8.24; -import {KlerosCoreBase, IDisputeKit, ISortitionModule, IERC20, OnError, StakingResult} from "./KlerosCoreBase.sol"; -import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import "./KlerosCoreBase.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. +/// @notice KlerosCore with whitelisted arbitrables contract KlerosCoreNeo is KlerosCoreBase { - string public constant override version = "0.9.4"; + string public constant override version = "0.10.0"; // ************************************* // // * Storage * // // ************************************* // mapping(address => bool) public arbitrableWhitelist; // Arbitrable whitelist. - IERC721 public jurorNft; // Eligible jurors NFT. // ************************************* // // * Constructor * // // ************************************* // - /// @custom:oz-upgrades-unsafe-allow constructor + /// @notice Constructor, initializing the implementation to reduce attack surface. 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. + /// @notice Initialization function for UUPS proxy + /// @param _governor The governor of the contract. + /// @param _guardian The guardian able to pause asset withdrawals. + /// @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, - ISortitionModule _sortitionModuleAddress, - IERC721 _jurorNft + IStakeController _stakeController, + IVault _vault ) external reinitializer(2) { __KlerosCoreBase_initialize( _governor, _guardian, - _pinakion, _jurorProsecutionModule, _disputeKit, _hiddenVotes, _courtParameters, _timesPerPeriod, _sortitionExtraData, - _sortitionModuleAddress + _stakeController, + _vault ); - jurorNft = _jurorNft; } function initialize5() external reinitializer(5) { @@ -75,18 +68,12 @@ contract KlerosCoreNeo is KlerosCoreBase { // * Governance * // // ************************************* // - /// @dev Access Control to perform implementation upgrades (UUPS Proxiable) - /// Only the governor can perform upgrades (`onlyByGovernor`) + /// @notice 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. @@ -94,20 +81,6 @@ contract KlerosCoreNeo is KlerosCoreBase { 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 * // // ************************************* // @@ -122,18 +95,9 @@ contract KlerosCoreNeo is KlerosCoreBase { 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/SortitionModule.sol b/contracts/src/arbitration/SortitionModule.sol deleted file mode 100644 index 3c076791f..000000000 --- a/contracts/src/arbitration/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/SortitionModuleBase.sol b/contracts/src/arbitration/SortitionModuleBase.sol deleted file mode 100644 index edb10edf1..000000000 --- a/contracts/src/arbitration/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/SortitionModuleNeo.sol b/contracts/src/arbitration/SortitionModuleNeo.sol deleted file mode 100644 index 2e60307d2..000000000 --- a/contracts/src/arbitration/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/arbitration/SortitionSumTree.sol b/contracts/src/arbitration/SortitionSumTree.sol new file mode 100644 index 000000000..3e35950c9 --- /dev/null +++ b/contracts/src/arbitration/SortitionSumTree.sol @@ -0,0 +1,391 @@ +// 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 SortitionSumTree +/// @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 = "0.9.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 * // + // ************************************* // + + /// @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. + function initialize(address _governor, IStakeController _stakeController) external initializer { + governor = _governor; + stakeController = _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 + } + + /// @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) { + // Tokens are also implicitly staked in parent courts via _updateParents(). + _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/StakeController.sol b/contracts/src/arbitration/StakeController.sol new file mode 100644 index 000000000..48f031ee3 --- /dev/null +++ b/contracts/src/arbitration/StakeController.sol @@ -0,0 +1,465 @@ +// 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 StakeController +/// @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 = "0.9.0"; + + // ************************************* // + // * 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(); + } + + /// @dev Initializer (constructor equivalent for upgradable contracts). + /// @param _governor The governor's address. + /// @param _core The KlerosCore contract. + /// @param _vault The Vault contract. + /// @param _sortition The SortitionSumTree 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, + KlerosCoreBase _core, + IVault _vault, + ISortitionSumTree _sortition, + uint256 _minStakingTime, + uint256 _maxDrawingTime, + RNG _rng, + uint256 _rngLookahead + ) external initializer { + governor = _governor; + core = _core; + vault = _vault; + sortition = _sortition; + minStakingTime = _minStakingTime; + maxDrawingTime = _maxDrawingTime; + lastPhaseChange = block.timestamp; + rng = _rng; + rngLookahead = _rngLookahead; + delayedStakeReadIndex = 1; + } + + // ************************************* // + // * 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 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/Vault.sol b/contracts/src/arbitration/Vault.sol new file mode 100644 index 000000000..d4bb7f8fb --- /dev/null +++ b/contracts/src/arbitration/Vault.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {VaultBase, IERC20} from "./VaultBase.sol"; + +/// @title Vault +/// @notice PNK Vault for handling deposits, withdrawals, locks, and penalties +contract Vault is VaultBase { + string public constant override version = "0.10.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) { + __VaultBase_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/VaultBase.sol b/contracts/src/arbitration/VaultBase.sol new file mode 100644 index 000000000..fc9b56183 --- /dev/null +++ b/contracts/src/arbitration/VaultBase.sol @@ -0,0 +1,214 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +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"; + +/// @title VaultBase +/// @notice Abstract base contract for PNK vault that handles deposits, withdrawals, locks, and penalties +abstract contract VaultBase is IVault, 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. + 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. + + // ************************************* // + // * 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 __VaultBase_initialize( + address _governor, + IERC20 _pnk, + address _stakeController, + address _core + ) internal onlyInitializing { + governor = _governor; + pnk = _pnk; + stakeController = _stakeController; + core = _core; + } + + // ************************************* // + // * 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; + } + + /// @dev Changes the `core` storage variable. + /// @param _core The new value for the `core` storage variable. + function changeCore(address _core) external onlyByGovernor { + core = _core; + } + + // ************************************* // + // * State Modifiers * // + // ************************************* // + + /// @inheritdoc IVault + 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. + function _deposit(address _from, uint256 _amount) internal virtual { + if (_amount == 0) revert InvalidAmount(); + + // 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; + + emit Deposit(_from, _amount); + } + + /// @inheritdoc IVault + 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 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(); + + JurorBalance storage balance = jurorBalances[_to]; + + // Check available balance (deposited - locked - penalties) for the user + uint256 available = getAvailableBalance(_to); + if (_amount > available) revert InsufficientAvailableBalance(); + + balance.deposited -= _amount; + pnk.safeTransfer(_to, _amount); // Vault sends PNK to user + + emit Withdraw(_to, _amount); + return _amount; + } + + /// @inheritdoc IVault + function lockTokens(address _account, uint256 _amount) external virtual override onlyStakeController { + jurorBalances[_account].locked += _amount; + emit Lock(_account, _amount); + } + + /// @inheritdoc IVault + function unlockTokens(address _account, uint256 _amount) external virtual override onlyStakeController { + jurorBalances[_account].locked -= _amount; + emit Unlock(_account, _amount); + } + + /// @inheritdoc IVault + function applyPenalty( + address _account, + uint256 _amount + ) 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; + balance.penalties += actualPenalty; + + // Note: Penalized PNK stays in vault to fund rewards pool + emit Penalty(_account, actualPenalty); + } + + /// @inheritdoc IVault + 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 IVault + function getAvailableBalance(address _account) public view override returns (uint256) { + JurorBalance storage balance = jurorBalances[_account]; + uint256 unavailable = balance.locked + balance.penalties; + return balance.deposited > unavailable ? balance.deposited - unavailable : 0; + } + + /// @inheritdoc IVault + function getDepositedBalance(address _account) external view override returns (uint256) { + return jurorBalances[_account].deposited; + } + + /// @inheritdoc IVault + function getLockedBalance(address _account) external view override returns (uint256) { + return jurorBalances[_account].locked; + } + + /// @inheritdoc IVault + 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 InsufficientVaultBalance(); +} diff --git a/contracts/src/arbitration/VaultNeo.sol b/contracts/src/arbitration/VaultNeo.sol new file mode 100644 index 000000000..03328d58a --- /dev/null +++ b/contracts/src/arbitration/VaultNeo.sol @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {VaultBase, IERC20} from "./VaultBase.sol"; +import {SafeERC20} from "../libraries/SafeERC20.sol"; +import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; + +/// @title VaultNeo +/// @notice Enhanced PNK Vault with additional features like NFT-gated deposits +contract VaultNeo is VaultBase { + using SafeERC20 for IERC20; + + string public constant override version = "0.1.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) { + __VaultBase_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 * // + // ************************************* // + + /// @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 NotEligible(); + } + + // Check per-user deposit limit + if (maxDepositPerUser > 0) { + uint256 currentUserDeposit = jurorBalances[_from].deposited; + if (currentUserDeposit + _amount > maxDepositPerUser) { + revert ExceedsMaxDepositPerUser(); + } + } + + // Check total deposit cap + if (totalDepositCap > 0 && totalDeposited + _amount > totalDepositCap) { + revert ExceedsTotalDepositCap(); + } + + // Update total deposited + totalDeposited += _amount; + + super._deposit(_from, _amount); + } + + /// @inheritdoc VaultBase + function _withdraw(address _to, uint256 _amount) internal override returns (uint256 pnkAmount) { + totalDeposited -= _amount; + return super._withdraw(_to, _amount); + } + + // ************************************* // + // * Errors * // + // ************************************* // + + error NotEligible(); + error ExceedsMaxDepositPerUser(); + error ExceedsTotalDepositCap(); +} diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol index e3ed491eb..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 {KlerosCore, KlerosCoreBase, IDisputeKit, ISortitionModule} from "../KlerosCore.sol"; +import {KlerosCore, KlerosCoreBase, IDisputeKit, IStakeController} from "../KlerosCore.sol"; import {Initializable} from "../../proxy/Initializable.sol"; import {UUPSProxiable} from "../../proxy/UUPSProxiable.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]; - ISortitionModule sortitionModule = core.sortitionModule(); + 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})); @@ -247,7 +247,7 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi uint256[] calldata _voteIDs, bytes32 _commit ) external notJumped(_coreDisputeID) { - (, , KlerosCore.Period period, , ) = core.disputes(_coreDisputeID); + (, , KlerosCoreBase.Period period, , ) = core.disputes(_coreDisputeID); require(period == KlerosCoreBase.Period.commit, "The dispute should be in Commit period."); require(_commit != bytes32(0), "Empty commit."); @@ -276,7 +276,7 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi uint256 _salt, string memory _justification ) external notJumped(_coreDisputeID) { - (, , KlerosCore.Period period, , ) = core.disputes(_coreDisputeID); + (, , 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"); @@ -456,7 +456,7 @@ 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); + (, , KlerosCoreBase.Period period, , ) = core.disputes(_coreDisputeID); // Override the final ruling if only one side funded the appeals. if (period == KlerosCoreBase.Period.execution) { uint256[] memory fundedChoices = getFundedChoices(_coreDisputeID); @@ -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/interfaces/ISortitionSumTree.sol b/contracts/src/arbitration/interfaces/ISortitionSumTree.sol new file mode 100644 index 000000000..94b415c47 --- /dev/null +++ b/contracts/src/arbitration/interfaces/ISortitionSumTree.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import "../../libraries/Constants.sol"; + +/// @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 ISortitionSumTree { + // ************************************* // + // * 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 + function setStake(address _account, uint96 _courtID, uint256 _newStake) external; + + // ************************************* // + // * 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 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..1486d8126 --- /dev/null +++ b/contracts/src/arbitration/interfaces/IStakeController.sol @@ -0,0 +1,204 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {KlerosCoreBase} from "../KlerosCoreBase.sol"; +import "../../libraries/Constants.sol"; + +/// @title IStakeController +/// @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 { + // ************************************* // + // * 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(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, bool _delayed); + + /// @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); + + /// @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); + event MigrationCompleted(uint256 _totalAccounts, uint256 _totalStakesImported); + event PhaseStateMigrated(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 phase() external view returns (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 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 validated for deposit + /// @return pnkWithdrawal The amount of PNK validated for withdrawal + /// @return stakingResult The result of the staking operation + function validateStake( + address _account, + uint96 _courtID, + uint256 _newStake + ) 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 + /// @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 + /// @return pnkBalance The balance of PNK after penalty application, including locked PNK + /// @return actualPenalty The actual penalty applied + 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 + /// @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 + /// @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 * // + // ************************************* // + + /// @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 SortitionSumTree) + /// @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); + + // ************************************* // + // * View Functions * // + // ************************************* // + + /// @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 stakedInCourt Amount staked in specific court + /// @return nbCourts Number of courts staked in + function getJurorBalance( + address _juror, + uint96 _courtID + ) + 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 + /// @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); + + /// @notice Get the core arbitrator contract + /// @return The core contract + function core() external view returns (KlerosCoreBase); +} diff --git a/contracts/src/arbitration/interfaces/IVault.sol b/contracts/src/arbitration/interfaces/IVault.sol new file mode 100644 index 000000000..361ae6127 --- /dev/null +++ b/contracts/src/arbitration/interfaces/IVault.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/// @title IVault +/// @notice Interface for the PNK Vault that handles PNK deposits, withdrawals, locks, and penalties +interface IVault { + // ************************************* // + // * 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 in the vault + /// @param _from The account to deposit from + /// @param _amount The amount of PNK to deposit + function deposit(address _from, uint256 _amount) external; + + /// @notice Withdraw PNK + /// @param _to The account to withdraw to + /// @param _amount The amount to withdraw + /// @return pnkAmount The amount of PNK withdrawn + 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 + /// @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 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 pnkBalance, 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 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/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); } } diff --git a/contracts/src/libraries/Constants.sol b/contracts/src/libraries/Constants.sol index f393b4792..94bb3eb70 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: still used by University contracts for now enum OnError { Revert, Return @@ -27,12 +28,13 @@ enum OnError { enum StakingResult { Successful, - StakingTransferFailed, - UnstakingTransferFailed, - CannotStakeInMoreCourts, - CannotStakeInThisCourt, - CannotStakeLessThanMinStake, - CannotStakeMoreThanMaxStakePerJuror, - CannotStakeMoreThanMaxTotalStaked, - CannotStakeZeroWhenNoStake + Delayed, + StakingTransferFailed, // DEPRECATED: still used by University contracts for now + UnstakingTransferFailed, // DEPRECATED + CannotStakeInMoreCourts, // DEPRECATED + CannotStakeInThisCourt, // DEPRECATED + CannotStakeLessThanMinStake, // DEPRECATED + CannotStakeMoreThanMaxStakePerJuror, // DEPRECATED + CannotStakeMoreThanMaxTotalStaked, // DEPRECATED + CannotStakeZeroWhenNoStake // DEPRECATED } 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) {} } diff --git a/contracts/src/test/SortitionModuleMock.sol b/contracts/src/test/SortitionModuleMock.sol index bfe911dfe..2d49aac6f 100644 --- a/contracts/src/test/SortitionModuleMock.sol +++ b/contracts/src/test/SortitionModuleMock.sol @@ -10,13 +10,13 @@ pragma solidity 0.8.24; -import "../arbitration/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]; + SumTree storage tree = sortitionSumTrees[_key]; K = tree.K; nodeLength = tree.nodes.length; }