From 63744e9f1292f83f5488ed325fa078250c6cac81 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Thu, 4 Sep 2025 18:12:38 +0100 Subject: [PATCH 01/11] feat: added a arbitrableWhitelistEnabled flag to KlerosCoreNeo --- .../deploy/00-home-chain-arbitration-neo.ts | 2 + contracts/src/arbitration/KlerosCoreNeo.sol | 8 +++- contracts/test/arbitration/staking-neo.ts | 45 +++++++++++++------ 3 files changed, 41 insertions(+), 14 deletions(-) diff --git a/contracts/deploy/00-home-chain-arbitration-neo.ts b/contracts/deploy/00-home-chain-arbitration-neo.ts index 36a895c74..d5d8a136c 100644 --- a/contracts/deploy/00-home-chain-arbitration-neo.ts +++ b/contracts/deploy/00-home-chain-arbitration-neo.ts @@ -119,6 +119,8 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) args: [core.target, disputeTemplateRegistry.target], log: true, }); + console.log(`core.changeArbitrableWhitelistEnabled(true)`); + await core.changeArbitrableWhitelistEnabled(true); console.log(`core.changeArbitrableWhitelist(${resolver.address}, true)`); await core.changeArbitrableWhitelist(resolver.address, true); diff --git a/contracts/src/arbitration/KlerosCoreNeo.sol b/contracts/src/arbitration/KlerosCoreNeo.sol index 8f0a413b3..25cef5b25 100644 --- a/contracts/src/arbitration/KlerosCoreNeo.sol +++ b/contracts/src/arbitration/KlerosCoreNeo.sol @@ -16,6 +16,7 @@ contract KlerosCoreNeo is KlerosCoreBase { // ************************************* // mapping(address => bool) public arbitrableWhitelist; // Arbitrable whitelist. + bool public arbitrableWhitelistEnabled; // Whether the arbitrable whitelist is enabled. IERC721 public jurorNft; // Eligible jurors NFT. // ************************************* // @@ -93,6 +94,11 @@ contract KlerosCoreNeo is KlerosCoreBase { arbitrableWhitelist[_arbitrable] = _allowed; } + /// @dev Enables or disables the arbitrable whitelist. + function changeArbitrableWhitelistEnabled(bool _enabled) external onlyByOwner { + arbitrableWhitelistEnabled = _enabled; + } + // ************************************* // // * State Modifiers * // // ************************************* // @@ -117,7 +123,7 @@ contract KlerosCoreNeo is KlerosCoreBase { IERC20 _feeToken, uint256 _feeAmount ) internal override returns (uint256 disputeID) { - if (!arbitrableWhitelist[msg.sender]) revert ArbitrableNotWhitelisted(); + if (arbitrableWhitelistEnabled && !arbitrableWhitelist[msg.sender]) revert ArbitrableNotWhitelisted(); return super._createDispute(_numberOfChoices, _extraData, _feeToken, _feeAmount); } diff --git a/contracts/test/arbitration/staking-neo.ts b/contracts/test/arbitration/staking-neo.ts index 5537742a6..a21bd0c7b 100644 --- a/contracts/test/arbitration/staking-neo.ts +++ b/contracts/test/arbitration/staking-neo.ts @@ -128,31 +128,50 @@ describe("Staking", async () => { SHOULD BEHAVE LIKE A NEO ARBITRATOR ************************************************************************************************/ - describe("When arbitrable is not whitelisted", () => { + describe("When arbitrable whitelist is disabled", () => { before("Setup", async () => { await deployUnhappy(); - await core.changeArbitrableWhitelist(resolver.target, false); + await core.changeArbitrableWhitelistEnabled(false); }); - it("Should fail to create a dispute", async () => { + it("Should create a dispute", async () => { const arbitrationCost = ETH(0.5); - await expect( - resolver.createDisputeForTemplate(extraData, "", "", 2, { value: arbitrationCost }) - ).to.be.revertedWithCustomError(core, "ArbitrableNotWhitelisted"); + expect(await resolver.createDisputeForTemplate(extraData, "", "", 2, { value: arbitrationCost })) + .to.emit(core, "DisputeCreation") + .withArgs(0, resolver.target); }); }); - describe("When arbitrable is whitelisted", () => { + describe("When arbitrable whitelist is enabled", () => { before("Setup", async () => { await deployUnhappy(); - await core.changeArbitrableWhitelist(resolver.target, true); + await core.changeArbitrableWhitelistEnabled(true); }); - it("Should create a dispute", async () => { - const arbitrationCost = ETH(0.5); - expect(await resolver.createDisputeForTemplate(extraData, "", "", 2, { value: arbitrationCost })) - .to.emit(core, "DisputeCreation") - .withArgs(0, resolver.target); + describe("When arbitrable is not whitelisted", () => { + before("Setup", async () => { + await core.changeArbitrableWhitelist(resolver.target, false); + }); + + it("Should fail to create a dispute", async () => { + const arbitrationCost = ETH(0.5); + await expect( + resolver.createDisputeForTemplate(extraData, "", "", 2, { value: arbitrationCost }) + ).to.be.revertedWithCustomError(core, "ArbitrableNotWhitelisted"); + }); + }); + + describe("When arbitrable is whitelisted", () => { + before("Setup", async () => { + await core.changeArbitrableWhitelist(resolver.target, true); + }); + + it("Should create a dispute", async () => { + const arbitrationCost = ETH(0.5); + expect(await resolver.createDisputeForTemplate(extraData, "", "", 2, { value: arbitrationCost })) + .to.emit(core, "DisputeCreation") + .withArgs(0, resolver.target); + }); }); }); From 262f940fcc918205f0b41aa60394c155456ec8c4 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Thu, 4 Sep 2025 18:20:39 +0100 Subject: [PATCH 02/11] feat: allow jurorNft to be address(0) which disables gating --- contracts/src/arbitration/KlerosCoreNeo.sol | 2 +- contracts/test/arbitration/staking-neo.ts | 53 ++++++++++++++------- 2 files changed, 37 insertions(+), 18 deletions(-) diff --git a/contracts/src/arbitration/KlerosCoreNeo.sol b/contracts/src/arbitration/KlerosCoreNeo.sol index 25cef5b25..6f43132d1 100644 --- a/contracts/src/arbitration/KlerosCoreNeo.sol +++ b/contracts/src/arbitration/KlerosCoreNeo.sol @@ -109,7 +109,7 @@ contract KlerosCoreNeo is KlerosCoreBase { /// @param _newStake The new stake. /// Note that the existing delayed stake will be nullified as non-relevant. function setStake(uint96 _courtID, uint256 _newStake) external override whenNotPaused { - if (jurorNft.balanceOf(msg.sender) == 0) revert NotEligibleForStaking(); + if (address(jurorNft) != address(0) && jurorNft.balanceOf(msg.sender) == 0) revert NotEligibleForStaking(); super._setStake(msg.sender, _courtID, _newStake, false, OnError.Revert); } diff --git a/contracts/test/arbitration/staking-neo.ts b/contracts/test/arbitration/staking-neo.ts index a21bd0c7b..fb36787d8 100644 --- a/contracts/test/arbitration/staking-neo.ts +++ b/contracts/test/arbitration/staking-neo.ts @@ -175,32 +175,51 @@ describe("Staking", async () => { }); }); - describe("When juror has no NFT", async () => { + describe("When juror NFT is not set", async () => { before("Setup", async () => { await deployUnhappy(); + await core.changeJurorNft(ethers.ZeroAddress); }); - it("Should not be able to stake", async () => { - await pnk.connect(juror).approve(core.target, PNK(1000)); - await expect(core.connect(juror).setStake(1, PNK(1000))).to.be.revertedWithCustomError( - core, - "NotEligibleForStaking" - ); + describe("When juror has no NFT", async () => { + it("Should be able to stake", async () => { + await pnk.connect(juror).approve(core.target, PNK(1000)); + await expect(await core.connect(juror).setStake(1, PNK(1000))) + .to.emit(sortition, "StakeSet") + .withArgs(juror.address, 1, PNK(1000), PNK(1000)); + expect(await sortition.totalStaked()).to.be.equal(PNK(1000)); + }); }); }); - describe("When juror does have a NFT", async () => { - before("Setup", async () => { - await deployUnhappy(); - await nft.safeMint(juror.address); + describe("When juror NFT is set", async () => { + describe("When juror has no NFT", async () => { + before("Setup", async () => { + await deployUnhappy(); + }); + + it("Should not be able to stake", async () => { + await pnk.connect(juror).approve(core.target, PNK(1000)); + await expect(core.connect(juror).setStake(1, PNK(1000))).to.be.revertedWithCustomError( + core, + "NotEligibleForStaking" + ); + }); }); - it("Should be able to stake", async () => { - await pnk.connect(juror).approve(core.target, PNK(1000)); - await expect(await core.connect(juror).setStake(1, PNK(1000))) - .to.emit(sortition, "StakeSet") - .withArgs(juror.address, 1, PNK(1000), PNK(1000)); - expect(await sortition.totalStaked()).to.be.equal(PNK(1000)); + describe("When juror does have a NFT", async () => { + before("Setup", async () => { + await deployUnhappy(); + await nft.safeMint(juror.address); + }); + + it("Should be able to stake", async () => { + await pnk.connect(juror).approve(core.target, PNK(1000)); + await expect(await core.connect(juror).setStake(1, PNK(1000))) + .to.emit(sortition, "StakeSet") + .withArgs(juror.address, 1, PNK(1000), PNK(1000)); + expect(await sortition.totalStaked()).to.be.equal(PNK(1000)); + }); }); }); From bd210be7bd26adfa33232606ce582b699e61fc9b Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Thu, 4 Sep 2025 18:39:43 +0100 Subject: [PATCH 03/11] feat: only KlerosCore, no more KlerosCoreBase and KlerosCoreNeo --- contracts/src/arbitration/KlerosCore.sol | 1282 ++++++++++++++++- contracts/src/arbitration/KlerosCoreBase.sol | 1275 ---------------- contracts/src/arbitration/KlerosCoreNeo.sol | 144 -- .../dispute-kits/DisputeKitClassicBase.sol | 8 +- contracts/src/proxy/KlerosProxies.sol | 12 - .../test/foundry/KlerosCore_Appeals.t.sol | 56 +- .../test/foundry/KlerosCore_Disputes.t.sol | 22 +- .../test/foundry/KlerosCore_Drawing.t.sol | 14 +- .../test/foundry/KlerosCore_Execution.t.sol | 52 +- .../test/foundry/KlerosCore_Governance.t.sol | 78 +- .../foundry/KlerosCore_Initialization.t.sol | 11 +- .../test/foundry/KlerosCore_Staking.t.sol | 20 +- .../test/foundry/KlerosCore_TestBase.sol | 7 +- .../test/foundry/KlerosCore_Voting.t.sol | 36 +- 14 files changed, 1422 insertions(+), 1595 deletions(-) delete mode 100644 contracts/src/arbitration/KlerosCoreBase.sol delete mode 100644 contracts/src/arbitration/KlerosCoreNeo.sol diff --git a/contracts/src/arbitration/KlerosCore.sol b/contracts/src/arbitration/KlerosCore.sol index 4dd30f61b..f7d6cf493 100644 --- a/contracts/src/arbitration/KlerosCore.sol +++ b/contracts/src/arbitration/KlerosCore.sol @@ -2,14 +2,198 @@ pragma solidity ^0.8.24; -import {KlerosCoreBase, IDisputeKit, ISortitionModule, IERC20} from "./KlerosCoreBase.sol"; +import {IArbitrableV2, IArbitratorV2} from "./interfaces/IArbitratorV2.sol"; +import {IDisputeKit} from "./interfaces/IDisputeKit.sol"; +import {ISortitionModule} from "./interfaces/ISortitionModule.sol"; +import {Initializable} from "../proxy/Initializable.sol"; +import {UUPSProxiable} from "../proxy/UUPSProxiable.sol"; +import {SafeERC20, IERC20} from "../libraries/SafeERC20.sol"; +import {SafeSend} from "../libraries/SafeSend.sol"; +import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import "../libraries/Constants.sol"; /// @title KlerosCore /// Core arbitrator contract for Kleros v2. /// Note that this contract trusts the PNK token, the dispute kit and the sortition module contracts. -contract KlerosCore is KlerosCoreBase { +contract KlerosCore is IArbitratorV2, Initializable, UUPSProxiable { + using SafeERC20 for IERC20; + using SafeSend for address payable; + string public constant override version = "2.0.0"; + // ************************************* // + // * Enums / Structs * // + // ************************************* // + + enum Period { + evidence, // Evidence can be submitted. This is also when drawing has to take place. + commit, // Jurors commit a hashed vote. This is skipped for courts without hidden votes. + vote, // Jurors reveal/cast their vote depending on whether the court has hidden votes or not. + appeal, // The dispute can be appealed. + execution // Tokens are redistributed and the ruling is executed. + } + + struct Court { + uint96 parent; // The parent court. + bool hiddenVotes; // Whether to use commit and reveal or not. + uint256[] children; // List of child courts. + uint256 minStake; // Minimum PNKs needed to stake in the court. + uint256 alpha; // Basis point of PNKs that are lost when incoherent. + uint256 feeForJuror; // Arbitration fee paid per juror. + uint256 jurorsForCourtJump; // The appeal after the one that reaches this number of jurors will go to the parent court if any. + uint256[4] timesPerPeriod; // The time allotted to each dispute period in the form `timesPerPeriod[period]`. + mapping(uint256 disputeKitId => bool) supportedDisputeKits; // True if DK with this ID is supported by the court. Note that each court must support classic dispute kit. + bool disabled; // True if the court is disabled. Unused for now, will be implemented later. + } + + struct Dispute { + uint96 courtID; // The ID of the court the dispute is in. + IArbitrableV2 arbitrated; // The arbitrable contract. + Period period; // The current period of the dispute. + bool ruled; // True if the ruling has been executed, false otherwise. + uint256 lastPeriodChange; // The last time the period was changed. + Round[] rounds; + } + + struct Round { + uint256 disputeKitID; // Index of the dispute kit in the array. + uint256 pnkAtStakePerJuror; // The amount of PNKs at stake for each juror in this round. + uint256 totalFeesForJurors; // The total juror fees paid in this round. + uint256 nbVotes; // The total number of votes the dispute can possibly have in the current round. Former votes[_round].length. + uint256 repartitions; // A counter of reward repartitions made in this round. + uint256 pnkPenalties; // The amount of PNKs collected from penalties in this round. + address[] drawnJurors; // Addresses of the jurors that were drawn in this round. + uint96[] drawnJurorFromCourtIDs; // The courtIDs where the juror was drawn from, possibly their stake in a subcourt. + uint256 sumFeeRewardPaid; // Total sum of arbitration fees paid to coherent jurors as a reward in this round. + uint256 sumPnkRewardPaid; // Total sum of PNK paid to coherent jurors as a reward in this round. + IERC20 feeToken; // The token used for paying fees in this round. + uint256 drawIterations; // The number of iterations passed drawing the jurors for this round. + } + + // Workaround "stack too deep" errors + struct ExecuteParams { + uint256 disputeID; // The ID of the dispute to execute. + uint256 round; // The round to execute. + uint256 coherentCount; // The number of coherent votes in the round. + uint256 numberOfVotesInRound; // The number of votes in the round. + uint256 feePerJurorInRound; // The fee per juror in the round. + uint256 pnkAtStakePerJurorInRound; // The amount of PNKs at stake for each juror in the round. + uint256 pnkPenaltiesInRound; // The amount of PNKs collected from penalties in the round. + uint256 repartition; // The index of the repartition to execute. + } + + struct CurrencyRate { + bool feePaymentAccepted; + uint64 rateInEth; + uint8 rateDecimals; + } + + // ************************************* // + // * Storage * // + // ************************************* // + + uint256 private constant NON_PAYABLE_AMOUNT = (2 ** 256 - 2) / 2; // An amount higher than the supply of ETH. + + address public owner; // The owner of the contract. + address public guardian; // The guardian able to pause asset withdrawals. + IERC20 public pinakion; // The Pinakion token contract. + address public jurorProsecutionModule; // The module for juror's prosecution. + ISortitionModule public sortitionModule; // Sortition module for drawing. + Court[] public courts; // The courts. + IDisputeKit[] public disputeKits; // Array of dispute kits. + Dispute[] public disputes; // The disputes. + mapping(IERC20 => CurrencyRate) public currencyRates; // The price of each token in ETH. + bool public paused; // Whether asset withdrawals are paused. + address public wNative; // The wrapped native token for safeSend(). + mapping(address => bool) public arbitrableWhitelist; // Arbitrable whitelist. + bool public arbitrableWhitelistEnabled; // Whether the arbitrable whitelist is enabled. + IERC721 public jurorNft; // Eligible jurors NFT. + + // ************************************* // + // * Events * // + // ************************************* // + + event NewPeriod(uint256 indexed _disputeID, Period _period); + event AppealPossible(uint256 indexed _disputeID, IArbitrableV2 indexed _arbitrable); + event AppealDecision(uint256 indexed _disputeID, IArbitrableV2 indexed _arbitrable); + event Draw(address indexed _address, uint256 indexed _disputeID, uint256 _roundID, uint256 _voteID); + event CourtCreated( + uint96 indexed _courtID, + uint96 indexed _parent, + bool _hiddenVotes, + uint256 _minStake, + uint256 _alpha, + uint256 _feeForJuror, + uint256 _jurorsForCourtJump, + uint256[4] _timesPerPeriod, + uint256[] _supportedDisputeKits + ); + event CourtModified( + uint96 indexed _courtID, + bool _hiddenVotes, + uint256 _minStake, + uint256 _alpha, + uint256 _feeForJuror, + uint256 _jurorsForCourtJump, + uint256[4] _timesPerPeriod + ); + event DisputeKitCreated(uint256 indexed _disputeKitID, IDisputeKit indexed _disputeKitAddress); + event DisputeKitEnabled(uint96 indexed _courtID, uint256 indexed _disputeKitID, bool indexed _enable); + event CourtJump( + uint256 indexed _disputeID, + uint256 indexed _roundID, + uint96 indexed _fromCourtID, + uint96 _toCourtID + ); + event DisputeKitJump( + uint256 indexed _disputeID, + uint256 indexed _roundID, + uint256 indexed _fromDisputeKitID, + uint256 _toDisputeKitID + ); + event TokenAndETHShift( + address indexed _account, + uint256 indexed _disputeID, + uint256 indexed _roundID, + uint256 _degreeOfCoherency, + int256 _pnkAmount, + int256 _feeAmount, + IERC20 _feeToken + ); + event LeftoverRewardSent( + uint256 indexed _disputeID, + uint256 indexed _roundID, + uint256 _pnkAmount, + uint256 _feeAmount, + IERC20 _feeToken + ); + event Paused(); + event Unpaused(); + + // ************************************* // + // * Function Modifiers * // + // ************************************* // + + modifier onlyByOwner() { + if (owner != msg.sender) revert OwnerOnly(); + _; + } + + modifier onlyByGuardianOrOwner() { + if (guardian != msg.sender && owner != msg.sender) revert GuardianOrOwnerOnly(); + _; + } + + modifier whenPaused() { + if (!paused) revert WhenPausedOnly(); + _; + } + + modifier whenNotPaused() { + if (paused) revert WhenNotPausedOnly(); + _; + } + // ************************************* // // * Constructor * // // ************************************* // @@ -31,6 +215,7 @@ contract KlerosCore is KlerosCoreBase { /// @param _sortitionExtraData The extra data for sortition module. /// @param _sortitionModuleAddress The sortition module responsible for sortition of the jurors. /// @param _wNative The wrapped native token address, typically wETH. + /// @param _jurorNft NFT contract to vet the jurors. function initialize( address _owner, address _guardian, @@ -42,21 +227,57 @@ contract KlerosCore is KlerosCoreBase { uint256[4] memory _timesPerPeriod, bytes memory _sortitionExtraData, ISortitionModule _sortitionModuleAddress, - address _wNative + address _wNative, + IERC721 _jurorNft ) external initializer { - __KlerosCoreBase_initialize( - _owner, - _guardian, - _pinakion, - _jurorProsecutionModule, - _disputeKit, + owner = _owner; + guardian = _guardian; + pinakion = _pinakion; + jurorProsecutionModule = _jurorProsecutionModule; + sortitionModule = _sortitionModuleAddress; + wNative = _wNative; + jurorNft = _jurorNft; + + // NULL_DISPUTE_KIT: an empty element at index 0 to indicate when a dispute kit is not supported. + disputeKits.push(); + + // DISPUTE_KIT_CLASSIC + disputeKits.push(_disputeKit); + + emit DisputeKitCreated(DISPUTE_KIT_CLASSIC, _disputeKit); + + // FORKING_COURT + // TODO: Fill the properties for the Forking court, emit CourtCreated. + courts.push(); + sortitionModule.createTree(FORKING_COURT, _sortitionExtraData); + + // GENERAL_COURT + Court storage court = courts.push(); + court.parent = FORKING_COURT; + court.children = new uint256[](0); + court.hiddenVotes = _hiddenVotes; + court.minStake = _courtParameters[0]; + court.alpha = _courtParameters[1]; + court.feeForJuror = _courtParameters[2]; + court.jurorsForCourtJump = _courtParameters[3]; + court.timesPerPeriod = _timesPerPeriod; + + sortitionModule.createTree(GENERAL_COURT, _sortitionExtraData); + + uint256[] memory supportedDisputeKits = new uint256[](1); + supportedDisputeKits[0] = DISPUTE_KIT_CLASSIC; + emit CourtCreated( + GENERAL_COURT, + court.parent, _hiddenVotes, - _courtParameters, + _courtParameters[0], + _courtParameters[1], + _courtParameters[2], + _courtParameters[3], _timesPerPeriod, - _sortitionExtraData, - _sortitionModuleAddress, - _wNative + supportedDisputeKits ); + _enableDisputeKit(GENERAL_COURT, DISPUTE_KIT_CLASSIC, true); } // ************************************* // @@ -68,4 +289,1039 @@ contract KlerosCore is KlerosCoreBase { function _authorizeUpgrade(address) internal view override onlyByOwner { // NOP } + + /// @dev Pause staking and reward execution. Can only be done by guardian or owner. + function pause() external onlyByGuardianOrOwner whenNotPaused { + paused = true; + emit Paused(); + } + + /// @dev Unpause staking and reward execution. Can only be done by owner. + function unpause() external onlyByOwner whenPaused { + paused = false; + emit Unpaused(); + } + + /// @dev Allows the owner to call anything on behalf of the contract. + /// @param _destination The destination of the call. + /// @param _amount The value sent with the call. + /// @param _data The data sent with the call. + function executeOwnerProposal(address _destination, uint256 _amount, bytes memory _data) external onlyByOwner { + (bool success, ) = _destination.call{value: _amount}(_data); + if (!success) revert UnsuccessfulCall(); + } + + /// @dev Changes the `owner` storage variable. + /// @param _owner The new value for the `owner` storage variable. + function changeOwner(address payable _owner) external onlyByOwner { + owner = _owner; + } + + /// @dev Changes the `guardian` storage variable. + /// @param _guardian The new value for the `guardian` storage variable. + function changeGuardian(address _guardian) external onlyByOwner { + guardian = _guardian; + } + + /// @dev Changes the `pinakion` storage variable. + /// @param _pinakion The new value for the `pinakion` storage variable. + function changePinakion(IERC20 _pinakion) external onlyByOwner { + pinakion = _pinakion; + } + + /// @dev Changes the `jurorProsecutionModule` storage variable. + /// @param _jurorProsecutionModule The new value for the `jurorProsecutionModule` storage variable. + function changeJurorProsecutionModule(address _jurorProsecutionModule) external onlyByOwner { + jurorProsecutionModule = _jurorProsecutionModule; + } + + /// @dev Changes the `_sortitionModule` storage variable. + /// Note that the new module should be initialized for all courts. + /// @param _sortitionModule The new value for the `sortitionModule` storage variable. + function changeSortitionModule(ISortitionModule _sortitionModule) external onlyByOwner { + sortitionModule = _sortitionModule; + } + + /// @dev Add a new supported dispute kit module to the court. + /// @param _disputeKitAddress The address of the dispute kit contract. + function addNewDisputeKit(IDisputeKit _disputeKitAddress) external onlyByOwner { + uint256 disputeKitID = disputeKits.length; + disputeKits.push(_disputeKitAddress); + emit DisputeKitCreated(disputeKitID, _disputeKitAddress); + } + + /// @dev Creates a court under a specified parent court. + /// @param _parent The `parent` property value of the court. + /// @param _hiddenVotes The `hiddenVotes` property value of the court. + /// @param _minStake The `minStake` property value of the court. + /// @param _alpha The `alpha` property value of the court. + /// @param _feeForJuror The `feeForJuror` property value of the court. + /// @param _jurorsForCourtJump The `jurorsForCourtJump` property value of the court. + /// @param _timesPerPeriod The `timesPerPeriod` property value of the court. + /// @param _sortitionExtraData Extra data for sortition module. + /// @param _supportedDisputeKits Indexes of dispute kits that this court will support. + function createCourt( + uint96 _parent, + bool _hiddenVotes, + uint256 _minStake, + uint256 _alpha, + uint256 _feeForJuror, + uint256 _jurorsForCourtJump, + uint256[4] memory _timesPerPeriod, + bytes memory _sortitionExtraData, + uint256[] memory _supportedDisputeKits + ) external onlyByOwner { + if (courts[_parent].minStake > _minStake) revert MinStakeLowerThanParentCourt(); + if (_supportedDisputeKits.length == 0) revert UnsupportedDisputeKit(); + if (_parent == FORKING_COURT) revert InvalidForkingCourtAsParent(); + + uint96 courtID = uint96(courts.length); + Court storage court = courts.push(); + + for (uint256 i = 0; i < _supportedDisputeKits.length; i++) { + if (_supportedDisputeKits[i] == 0 || _supportedDisputeKits[i] >= disputeKits.length) { + revert WrongDisputeKitIndex(); + } + _enableDisputeKit(uint96(courtID), _supportedDisputeKits[i], true); + } + // Check that Classic DK support was added. + if (!court.supportedDisputeKits[DISPUTE_KIT_CLASSIC]) revert MustSupportDisputeKitClassic(); + + court.parent = _parent; + court.children = new uint256[](0); + court.hiddenVotes = _hiddenVotes; + court.minStake = _minStake; + court.alpha = _alpha; + court.feeForJuror = _feeForJuror; + court.jurorsForCourtJump = _jurorsForCourtJump; + court.timesPerPeriod = _timesPerPeriod; + + sortitionModule.createTree(courtID, _sortitionExtraData); + + // Update the parent. + courts[_parent].children.push(courtID); + emit CourtCreated( + uint96(courtID), + _parent, + _hiddenVotes, + _minStake, + _alpha, + _feeForJuror, + _jurorsForCourtJump, + _timesPerPeriod, + _supportedDisputeKits + ); + } + + function changeCourtParameters( + uint96 _courtID, + bool _hiddenVotes, + uint256 _minStake, + uint256 _alpha, + uint256 _feeForJuror, + uint256 _jurorsForCourtJump, + uint256[4] memory _timesPerPeriod + ) external onlyByOwner { + Court storage court = courts[_courtID]; + if (_courtID != GENERAL_COURT && courts[court.parent].minStake > _minStake) { + revert MinStakeLowerThanParentCourt(); + } + for (uint256 i = 0; i < court.children.length; i++) { + if (courts[court.children[i]].minStake < _minStake) { + revert MinStakeLowerThanParentCourt(); + } + } + court.minStake = _minStake; + court.hiddenVotes = _hiddenVotes; + court.alpha = _alpha; + court.feeForJuror = _feeForJuror; + court.jurorsForCourtJump = _jurorsForCourtJump; + court.timesPerPeriod = _timesPerPeriod; + emit CourtModified( + _courtID, + _hiddenVotes, + _minStake, + _alpha, + _feeForJuror, + _jurorsForCourtJump, + _timesPerPeriod + ); + } + + /// @dev Adds/removes court's support for specified dispute kits. + /// @param _courtID The ID of the court. + /// @param _disputeKitIDs The IDs of dispute kits which support should be added/removed. + /// @param _enable Whether add or remove the dispute kits from the court. + function enableDisputeKits(uint96 _courtID, uint256[] memory _disputeKitIDs, bool _enable) external onlyByOwner { + for (uint256 i = 0; i < _disputeKitIDs.length; i++) { + if (_enable) { + if (_disputeKitIDs[i] == 0 || _disputeKitIDs[i] >= disputeKits.length) { + revert WrongDisputeKitIndex(); + } + _enableDisputeKit(_courtID, _disputeKitIDs[i], true); + } else { + // Classic dispute kit must be supported by all courts. + if (_disputeKitIDs[i] == DISPUTE_KIT_CLASSIC) { + revert CannotDisableClassicDK(); + } + _enableDisputeKit(_courtID, _disputeKitIDs[i], false); + } + } + } + + /// @dev Changes the supported fee tokens. + /// @param _feeToken The fee token. + /// @param _accepted Whether the token is supported or not as a method of fee payment. + function changeAcceptedFeeTokens(IERC20 _feeToken, bool _accepted) external onlyByOwner { + currencyRates[_feeToken].feePaymentAccepted = _accepted; + emit AcceptedFeeToken(_feeToken, _accepted); + } + + /// @dev Changes the currency rate of a fee token. + /// @param _feeToken The fee token. + /// @param _rateInEth The new rate of the fee token in ETH. + /// @param _rateDecimals The new decimals of the fee token rate. + function changeCurrencyRates(IERC20 _feeToken, uint64 _rateInEth, uint8 _rateDecimals) external onlyByOwner { + currencyRates[_feeToken].rateInEth = _rateInEth; + currencyRates[_feeToken].rateDecimals = _rateDecimals; + emit NewCurrencyRate(_feeToken, _rateInEth, _rateDecimals); + } + + /// @dev Adds or removes an arbitrable from whitelist. + /// @param _arbitrable Arbitrable address. + /// @param _allowed Whether add or remove permission. + function changeArbitrableWhitelist(address _arbitrable, bool _allowed) external onlyByOwner { + arbitrableWhitelist[_arbitrable] = _allowed; + } + + /// @dev Enables or disables the arbitrable whitelist. + function changeArbitrableWhitelistEnabled(bool _enabled) external onlyByOwner { + arbitrableWhitelistEnabled = _enabled; + } + + // ************************************* // + // * State Modifiers * // + // ************************************* // + + /// @dev Sets the caller's stake in a court. + /// @param _courtID The ID of the court. + /// @param _newStake The new stake. + /// Note that the existing delayed stake will be nullified as non-relevant. + function setStake(uint96 _courtID, uint256 _newStake) external virtual whenNotPaused { + if (address(jurorNft) != address(0) && jurorNft.balanceOf(msg.sender) == 0) revert NotEligibleForStaking(); + _setStake(msg.sender, _courtID, _newStake, false, OnError.Revert); + } + + /// @dev Sets the stake of a specified account in a court without delaying stake changes, typically to apply a delayed stake or unstake inactive jurors. + /// @param _account The account whose stake is being set. + /// @param _courtID The ID of the court. + /// @param _newStake The new stake. + function setStakeBySortitionModule(address _account, uint96 _courtID, uint256 _newStake) external { + if (msg.sender != address(sortitionModule)) revert SortitionModuleOnly(); + _setStake(_account, _courtID, _newStake, true, OnError.Return); + } + + /// @dev Transfers PNK to the juror by SortitionModule. + /// @param _account The account of the juror whose PNK to transfer. + /// @param _amount The amount to transfer. + function transferBySortitionModule(address _account, uint256 _amount) external { + if (msg.sender != address(sortitionModule)) revert SortitionModuleOnly(); + // Note eligibility is checked in SortitionModule. + pinakion.safeTransfer(_account, _amount); + } + + /// @inheritdoc IArbitratorV2 + function createDispute( + uint256 _numberOfChoices, + bytes memory _extraData + ) external payable override returns (uint256 disputeID) { + if (msg.value < arbitrationCost(_extraData)) revert ArbitrationFeesNotEnough(); + + return _createDispute(_numberOfChoices, _extraData, NATIVE_CURRENCY, msg.value); + } + + /// @inheritdoc IArbitratorV2 + function createDispute( + uint256 _numberOfChoices, + bytes calldata _extraData, + IERC20 _feeToken, + uint256 _feeAmount + ) external override returns (uint256 disputeID) { + if (!currencyRates[_feeToken].feePaymentAccepted) revert TokenNotAccepted(); + if (_feeAmount < arbitrationCost(_extraData, _feeToken)) revert ArbitrationFeesNotEnough(); + + if (!_feeToken.safeTransferFrom(msg.sender, address(this), _feeAmount)) revert TransferFailed(); + return _createDispute(_numberOfChoices, _extraData, _feeToken, _feeAmount); + } + + function _createDispute( + uint256 _numberOfChoices, + bytes memory _extraData, + IERC20 _feeToken, + uint256 _feeAmount + ) internal virtual returns (uint256 disputeID) { + if (arbitrableWhitelistEnabled && !arbitrableWhitelist[msg.sender]) revert ArbitrableNotWhitelisted(); + (uint96 courtID, , uint256 disputeKitID) = _extraDataToCourtIDMinJurorsDisputeKit(_extraData); + if (!courts[courtID].supportedDisputeKits[disputeKitID]) revert DisputeKitNotSupportedByCourt(); + + disputeID = disputes.length; + Dispute storage dispute = disputes.push(); + dispute.courtID = courtID; + dispute.arbitrated = IArbitrableV2(msg.sender); + dispute.lastPeriodChange = block.timestamp; + + IDisputeKit disputeKit = disputeKits[disputeKitID]; + Court storage court = courts[courtID]; + Round storage round = dispute.rounds.push(); + + // Obtain the feeForJuror in the same currency as the _feeAmount + uint256 feeForJuror = (_feeToken == NATIVE_CURRENCY) + ? court.feeForJuror + : convertEthToTokenAmount(_feeToken, court.feeForJuror); + round.nbVotes = _feeAmount / feeForJuror; + round.disputeKitID = disputeKitID; + round.pnkAtStakePerJuror = _calculatePnkAtStake(court.minStake, court.alpha); + round.totalFeesForJurors = _feeAmount; + round.feeToken = IERC20(_feeToken); + + sortitionModule.createDisputeHook(disputeID, 0); // Default round ID. + + disputeKit.createDispute(disputeID, _numberOfChoices, _extraData, round.nbVotes); + emit DisputeCreation(disputeID, IArbitrableV2(msg.sender)); + } + + /// @dev Passes the period of a specified dispute. + /// @param _disputeID The ID of the dispute. + function passPeriod(uint256 _disputeID) external { + Dispute storage dispute = disputes[_disputeID]; + Court storage court = courts[dispute.courtID]; + + uint256 currentRound = dispute.rounds.length - 1; + Round storage round = dispute.rounds[currentRound]; + if (dispute.period == Period.evidence) { + if ( + currentRound == 0 && + block.timestamp - dispute.lastPeriodChange < court.timesPerPeriod[uint256(dispute.period)] + ) { + revert EvidenceNotPassedAndNotAppeal(); + } + if (round.drawnJurors.length != round.nbVotes) revert DisputeStillDrawing(); + dispute.period = court.hiddenVotes ? Period.commit : Period.vote; + } else if (dispute.period == Period.commit) { + // Note that we do not want to pass to Voting period if all the commits are cast because it breaks the Shutter auto-reveal currently. + if (block.timestamp - dispute.lastPeriodChange < court.timesPerPeriod[uint256(dispute.period)]) { + revert CommitPeriodNotPassed(); + } + dispute.period = Period.vote; + } else if (dispute.period == Period.vote) { + if ( + block.timestamp - dispute.lastPeriodChange < court.timesPerPeriod[uint256(dispute.period)] && + !disputeKits[round.disputeKitID].areVotesAllCast(_disputeID) + ) { + revert VotePeriodNotPassed(); + } + dispute.period = Period.appeal; + emit AppealPossible(_disputeID, dispute.arbitrated); + } else if (dispute.period == Period.appeal) { + if ( + block.timestamp - dispute.lastPeriodChange < court.timesPerPeriod[uint256(dispute.period)] && + !disputeKits[round.disputeKitID].isAppealFunded(_disputeID) + ) { + revert AppealPeriodNotPassed(); + } + dispute.period = Period.execution; + } else if (dispute.period == Period.execution) { + revert DisputePeriodIsFinal(); + } + + dispute.lastPeriodChange = block.timestamp; + emit NewPeriod(_disputeID, dispute.period); + } + + /// @dev Draws jurors for the dispute. Can be called in parts. + /// @param _disputeID The ID of the dispute. + /// @param _iterations The number of iterations to run. + /// @return nbDrawnJurors The total number of jurors drawn in the round. + function draw(uint256 _disputeID, uint256 _iterations) external returns (uint256 nbDrawnJurors) { + Dispute storage dispute = disputes[_disputeID]; + uint256 currentRound = dispute.rounds.length - 1; + Round storage round = dispute.rounds[currentRound]; + if (dispute.period != Period.evidence) revert NotEvidencePeriod(); + + IDisputeKit disputeKit = disputeKits[round.disputeKitID]; + + uint256 startIndex = round.drawIterations; // for gas: less storage reads + uint256 i; + while (i < _iterations && round.drawnJurors.length < round.nbVotes) { + (address drawnAddress, uint96 fromSubcourtID) = disputeKit.draw(_disputeID, startIndex + i++); + if (drawnAddress == address(0)) { + continue; + } + sortitionModule.lockStake(drawnAddress, round.pnkAtStakePerJuror); + emit Draw(drawnAddress, _disputeID, currentRound, round.drawnJurors.length); + round.drawnJurors.push(drawnAddress); + round.drawnJurorFromCourtIDs.push(fromSubcourtID != 0 ? fromSubcourtID : dispute.courtID); + if (round.drawnJurors.length == round.nbVotes) { + sortitionModule.postDrawHook(_disputeID, currentRound); + } + } + round.drawIterations += i; + return round.drawnJurors.length; + } + + /// @dev Appeals the ruling of a specified dispute. + /// Note: Access restricted to the Dispute Kit for this `disputeID`. + /// @param _disputeID The ID of the dispute. + /// @param _numberOfChoices Number of choices for the dispute. Can be required during court jump. + /// @param _extraData Extradata for the dispute. Can be required during court jump. + function appeal(uint256 _disputeID, uint256 _numberOfChoices, bytes memory _extraData) external payable { + if (msg.value < appealCost(_disputeID)) revert AppealFeesNotEnough(); + + Dispute storage dispute = disputes[_disputeID]; + if (dispute.period != Period.appeal) revert DisputeNotAppealable(); + + Round storage round = dispute.rounds[dispute.rounds.length - 1]; + if (msg.sender != address(disputeKits[round.disputeKitID])) revert DisputeKitOnly(); + + // Warning: the extra round must be created before calling disputeKit.createDispute() + Round storage extraRound = dispute.rounds.push(); + + (uint96 newCourtID, uint256 newDisputeKitID, bool courtJump, ) = _getCourtAndDisputeKitJumps( + dispute, + round, + courts[dispute.courtID], + _disputeID + ); + if (courtJump) { + emit CourtJump(_disputeID, dispute.rounds.length - 1, dispute.courtID, newCourtID); + } + + dispute.courtID = newCourtID; + dispute.period = Period.evidence; + dispute.lastPeriodChange = block.timestamp; + + Court storage court = courts[newCourtID]; + extraRound.nbVotes = msg.value / court.feeForJuror; // As many votes that can be afforded by the provided funds. + extraRound.pnkAtStakePerJuror = _calculatePnkAtStake(court.minStake, court.alpha); + extraRound.totalFeesForJurors = msg.value; + extraRound.disputeKitID = newDisputeKitID; + + sortitionModule.createDisputeHook(_disputeID, dispute.rounds.length - 1); + + // Dispute kit was changed, so create a dispute in the new DK contract. + if (extraRound.disputeKitID != round.disputeKitID) { + emit DisputeKitJump(_disputeID, dispute.rounds.length - 1, round.disputeKitID, extraRound.disputeKitID); + disputeKits[extraRound.disputeKitID].createDispute( + _disputeID, + _numberOfChoices, + _extraData, + extraRound.nbVotes + ); + } + + emit AppealDecision(_disputeID, dispute.arbitrated); + emit NewPeriod(_disputeID, Period.evidence); + } + + /// @dev Distribute the PNKs at stake and the dispute fees for the specific round of the dispute. Can be called in parts. + /// Note: Reward distributions are forbidden during pause. + /// @param _disputeID The ID of the dispute. + /// @param _round The appeal round. + /// @param _iterations The number of iterations to run. + function execute(uint256 _disputeID, uint256 _round, uint256 _iterations) external whenNotPaused { + Round storage round; + { + Dispute storage dispute = disputes[_disputeID]; + if (dispute.period != Period.execution) revert NotExecutionPeriod(); + + round = dispute.rounds[_round]; + } // stack too deep workaround + + uint256 start = round.repartitions; + uint256 end = round.repartitions + _iterations; + + uint256 pnkPenaltiesInRound = round.pnkPenalties; // Keep in memory to save gas. + uint256 numberOfVotesInRound = round.drawnJurors.length; + uint256 feePerJurorInRound = round.totalFeesForJurors / numberOfVotesInRound; + uint256 pnkAtStakePerJurorInRound = round.pnkAtStakePerJuror; + uint256 coherentCount; + { + IDisputeKit disputeKit = disputeKits[round.disputeKitID]; + coherentCount = disputeKit.getCoherentCount(_disputeID, _round); // Total number of jurors that are eligible to a reward in this round. + } // stack too deep workaround + + if (coherentCount == 0) { + // We loop over the votes once as there are no rewards because it is not a tie and no one in this round is coherent with the final outcome. + if (end > numberOfVotesInRound) end = numberOfVotesInRound; + } else { + // We loop over the votes twice, first to collect the PNK penalties, and second to distribute them as rewards along with arbitration fees. + if (end > numberOfVotesInRound * 2) end = numberOfVotesInRound * 2; + } + round.repartitions = end; + + for (uint256 i = start; i < end; i++) { + if (i < numberOfVotesInRound) { + pnkPenaltiesInRound = _executePenalties( + ExecuteParams({ + disputeID: _disputeID, + round: _round, + coherentCount: coherentCount, + numberOfVotesInRound: numberOfVotesInRound, + feePerJurorInRound: feePerJurorInRound, + pnkAtStakePerJurorInRound: pnkAtStakePerJurorInRound, + pnkPenaltiesInRound: pnkPenaltiesInRound, + repartition: i + }) + ); + } else { + _executeRewards( + ExecuteParams({ + disputeID: _disputeID, + round: _round, + coherentCount: coherentCount, + numberOfVotesInRound: numberOfVotesInRound, + feePerJurorInRound: feePerJurorInRound, + pnkAtStakePerJurorInRound: pnkAtStakePerJurorInRound, + pnkPenaltiesInRound: pnkPenaltiesInRound, + repartition: i + }) + ); + } + } + if (round.pnkPenalties != pnkPenaltiesInRound) { + round.pnkPenalties = pnkPenaltiesInRound; // Reentrancy risk: breaks Check-Effect-Interact + } + } + + /// @dev Distribute the PNKs at stake and the dispute fees for the specific round of the dispute, penalties only. + /// @param _params The parameters for the execution, see `ExecuteParams`. + /// @return pnkPenaltiesInRoundCache The updated penalties in round cache. + function _executePenalties(ExecuteParams memory _params) internal returns (uint256) { + Dispute storage dispute = disputes[_params.disputeID]; + Round storage round = dispute.rounds[_params.round]; + IDisputeKit disputeKit = disputeKits[round.disputeKitID]; + + // [0, 1] value that determines how coherent the juror was in this round, in basis points. + uint256 coherence = disputeKit.getDegreeOfCoherencePenalty( + _params.disputeID, + _params.round, + _params.repartition, + _params.feePerJurorInRound, + _params.pnkAtStakePerJurorInRound + ); + + // Guard against degree exceeding 1, though it should be ensured by the dispute kit. + if (coherence > ONE_BASIS_POINT) { + coherence = ONE_BASIS_POINT; + } + + // Fully coherent jurors won't be penalized. + uint256 penalty = (round.pnkAtStakePerJuror * (ONE_BASIS_POINT - coherence)) / ONE_BASIS_POINT; + + // Unlock the PNKs affected by the penalty + address account = round.drawnJurors[_params.repartition]; + sortitionModule.unlockStake(account, penalty); + + // Apply the penalty to the staked PNKs. + uint96 penalizedInCourtID = round.drawnJurorFromCourtIDs[_params.repartition]; + (uint256 pnkBalance, uint256 newCourtStake, uint256 availablePenalty) = sortitionModule.setStakePenalty( + account, + penalizedInCourtID, + penalty + ); + _params.pnkPenaltiesInRound += availablePenalty; + emit TokenAndETHShift( + account, + _params.disputeID, + _params.round, + coherence, + -int256(availablePenalty), + 0, + round.feeToken + ); + + if (pnkBalance == 0 || !disputeKit.isVoteActive(_params.disputeID, _params.round, _params.repartition)) { + // The juror is inactive or their balance is can't cover penalties anymore, unstake them from all courts. + sortitionModule.forcedUnstakeAllCourts(account); + } else if (newCourtStake < courts[penalizedInCourtID].minStake) { + // The juror's balance fell below the court minStake, unstake them from the court. + sortitionModule.forcedUnstake(account, penalizedInCourtID); + } + + if (_params.repartition == _params.numberOfVotesInRound - 1 && _params.coherentCount == 0) { + // No one was coherent, send the rewards to the owner. + _transferFeeToken(round.feeToken, payable(owner), round.totalFeesForJurors); + pinakion.safeTransfer(owner, _params.pnkPenaltiesInRound); + emit LeftoverRewardSent( + _params.disputeID, + _params.round, + _params.pnkPenaltiesInRound, + round.totalFeesForJurors, + round.feeToken + ); + } + return _params.pnkPenaltiesInRound; + } + + /// @dev Distribute the PNKs at stake and the dispute fees for the specific round of the dispute, rewards only. + /// @param _params The parameters for the execution, see `ExecuteParams`. + function _executeRewards(ExecuteParams memory _params) internal { + Dispute storage dispute = disputes[_params.disputeID]; + Round storage round = dispute.rounds[_params.round]; + IDisputeKit disputeKit = disputeKits[round.disputeKitID]; + + // [0, 1] value that determines how coherent the juror was in this round, in basis points. + (uint256 pnkCoherence, uint256 feeCoherence) = disputeKit.getDegreeOfCoherenceReward( + _params.disputeID, + _params.round, + _params.repartition % _params.numberOfVotesInRound, + _params.feePerJurorInRound, + _params.pnkAtStakePerJurorInRound + ); + + // Guard against degree exceeding 1, though it should be ensured by the dispute kit. + if (pnkCoherence > ONE_BASIS_POINT) { + pnkCoherence = ONE_BASIS_POINT; + } + if (feeCoherence > ONE_BASIS_POINT) { + feeCoherence = ONE_BASIS_POINT; + } + + address account = round.drawnJurors[_params.repartition % _params.numberOfVotesInRound]; + uint256 pnkLocked = _applyCoherence(round.pnkAtStakePerJuror, pnkCoherence); + + // Release the rest of the PNKs of the juror for this round. + sortitionModule.unlockStake(account, pnkLocked); + + // Compute the rewards + uint256 pnkReward = _applyCoherence(_params.pnkPenaltiesInRound / _params.coherentCount, pnkCoherence); + round.sumPnkRewardPaid += pnkReward; + uint256 feeReward = _applyCoherence(round.totalFeesForJurors / _params.coherentCount, feeCoherence); + round.sumFeeRewardPaid += feeReward; + + // Transfer the fee reward + _transferFeeToken(round.feeToken, payable(account), feeReward); + + // Stake the PNK reward if possible, by-passes delayed stakes and other checks usually done by validateStake() + if (!sortitionModule.setStakeReward(account, dispute.courtID, pnkReward)) { + pinakion.safeTransfer(account, pnkReward); + } + + emit TokenAndETHShift( + account, + _params.disputeID, + _params.round, + pnkCoherence, + int256(pnkReward), + int256(feeReward), + round.feeToken + ); + + // Transfer any residual rewards to the owner. It may happen due to partial coherence of the jurors. + if (_params.repartition == _params.numberOfVotesInRound * 2 - 1) { + uint256 leftoverPnkReward = _params.pnkPenaltiesInRound - round.sumPnkRewardPaid; + uint256 leftoverFeeReward = round.totalFeesForJurors - round.sumFeeRewardPaid; + if (leftoverPnkReward != 0 || leftoverFeeReward != 0) { + if (leftoverPnkReward != 0) { + pinakion.safeTransfer(owner, leftoverPnkReward); + } + if (leftoverFeeReward != 0) { + _transferFeeToken(round.feeToken, payable(owner), leftoverFeeReward); + } + emit LeftoverRewardSent( + _params.disputeID, + _params.round, + leftoverPnkReward, + leftoverFeeReward, + round.feeToken + ); + } + } + } + + /// @dev Executes a specified dispute's ruling. + /// @param _disputeID The ID of the dispute. + function executeRuling(uint256 _disputeID) external { + Dispute storage dispute = disputes[_disputeID]; + if (dispute.period != Period.execution) revert NotExecutionPeriod(); + if (dispute.ruled) revert RulingAlreadyExecuted(); + + (uint256 winningChoice, , ) = currentRuling(_disputeID); + dispute.ruled = true; + emit Ruling(dispute.arbitrated, _disputeID, winningChoice); + dispute.arbitrated.rule(_disputeID, winningChoice); + } + + // ************************************* // + // * Public Views * // + // ************************************* // + + /// @dev Compute the cost of arbitration denominated in ETH. + /// It is recommended not to increase it often, as it can be highly time and gas consuming for the arbitrated contracts to cope with fee augmentation. + /// @param _extraData Additional info about the dispute. We use it to pass the ID of the dispute's court (first 32 bytes), the minimum number of jurors required (next 32 bytes) and the ID of the specific dispute kit (last 32 bytes). + /// @return cost The arbitration cost in ETH. + function arbitrationCost(bytes memory _extraData) public view override returns (uint256 cost) { + (uint96 courtID, uint256 minJurors, ) = _extraDataToCourtIDMinJurorsDisputeKit(_extraData); + cost = courts[courtID].feeForJuror * minJurors; + } + + /// @dev Compute the cost of arbitration denominated in `_feeToken`. + /// It is recommended not to increase it often, as it can be highly time and gas consuming for the arbitrated contracts to cope with fee augmentation. + /// @param _extraData Additional info about the dispute. We use it to pass the ID of the dispute's court (first 32 bytes), the minimum number of jurors required (next 32 bytes) and the ID of the specific dispute kit (last 32 bytes). + /// @param _feeToken The ERC20 token used to pay fees. + /// @return cost The arbitration cost in `_feeToken`. + function arbitrationCost(bytes calldata _extraData, IERC20 _feeToken) public view override returns (uint256 cost) { + cost = convertEthToTokenAmount(_feeToken, arbitrationCost(_extraData)); + } + + /// @dev Gets the cost of appealing a specified dispute. + /// @param _disputeID The ID of the dispute. + /// @return cost The appeal cost. + function appealCost(uint256 _disputeID) public view returns (uint256 cost) { + Dispute storage dispute = disputes[_disputeID]; + Round storage round = dispute.rounds[dispute.rounds.length - 1]; + Court storage court = courts[dispute.courtID]; + + (, uint256 newDisputeKitID, bool courtJump, ) = _getCourtAndDisputeKitJumps(dispute, round, court, _disputeID); + + uint256 nbVotesAfterAppeal = disputeKits[newDisputeKitID].getNbVotesAfterAppeal( + disputeKits[round.disputeKitID], + round.nbVotes + ); + + if (courtJump) { + // Jump to parent court. + if (dispute.courtID == GENERAL_COURT) { + // TODO: Handle the forking when appealed in General court. + cost = NON_PAYABLE_AMOUNT; // Get the cost of the parent court. + } else { + cost = courts[court.parent].feeForJuror * nbVotesAfterAppeal; + } + } else { + // Stay in current court. + cost = court.feeForJuror * nbVotesAfterAppeal; + } + } + + /// @dev Gets the start and the end of a specified dispute's current appeal period. + /// @param _disputeID The ID of the dispute. + /// @return start The start of the appeal period. + /// @return end The end of the appeal period. + function appealPeriod(uint256 _disputeID) external view returns (uint256 start, uint256 end) { + Dispute storage dispute = disputes[_disputeID]; + if (dispute.period == Period.appeal) { + start = dispute.lastPeriodChange; + end = dispute.lastPeriodChange + courts[dispute.courtID].timesPerPeriod[uint256(Period.appeal)]; + } else { + start = 0; + end = 0; + } + } + + /// @dev Gets the current ruling of a specified dispute. + /// @param _disputeID The ID of the dispute. + /// @return ruling The current ruling. + /// @return tied Whether it's a tie or not. + /// @return overridden Whether the ruling was overridden by appeal funding or not. + function currentRuling(uint256 _disputeID) public view returns (uint256 ruling, bool tied, bool overridden) { + Dispute storage dispute = disputes[_disputeID]; + Round storage round = dispute.rounds[dispute.rounds.length - 1]; + IDisputeKit disputeKit = disputeKits[round.disputeKitID]; + (ruling, tied, overridden) = disputeKit.currentRuling(_disputeID); + } + + /// @dev Gets the round info for a specified dispute and round. + /// @dev This function must not be called from a non-view function because it returns a dynamic array which might be very large, theoretically exceeding the block gas limit. + /// @param _disputeID The ID of the dispute. + /// @param _round The round to get the info for. + /// @return round The round info. + function getRoundInfo(uint256 _disputeID, uint256 _round) external view returns (Round memory) { + return disputes[_disputeID].rounds[_round]; + } + + /// @dev Gets the PNK at stake per juror for a specified dispute and round. + /// @param _disputeID The ID of the dispute. + /// @param _round The round to get the info for. + /// @return pnkAtStakePerJuror The PNK at stake per juror. + function getPnkAtStakePerJuror(uint256 _disputeID, uint256 _round) external view returns (uint256) { + return disputes[_disputeID].rounds[_round].pnkAtStakePerJuror; + } + + /// @dev Gets the number of rounds for a specified dispute. + /// @param _disputeID The ID of the dispute. + /// @return The number of rounds. + function getNumberOfRounds(uint256 _disputeID) external view returns (uint256) { + return disputes[_disputeID].rounds.length; + } + + /// @dev Checks if a given dispute kit is supported by a given court. + /// @param _courtID The ID of the court to check the support for. + /// @param _disputeKitID The ID of the dispute kit to check the support for. + /// @return Whether the dispute kit is supported or not. + function isSupported(uint96 _courtID, uint256 _disputeKitID) external view returns (bool) { + return courts[_courtID].supportedDisputeKits[_disputeKitID]; + } + + /// @dev Gets the timesPerPeriod array for a given court. + /// @param _courtID The ID of the court to get the times from. + /// @return timesPerPeriod The timesPerPeriod array for the given court. + function getTimesPerPeriod(uint96 _courtID) external view returns (uint256[4] memory timesPerPeriod) { + timesPerPeriod = courts[_courtID].timesPerPeriod; + } + + // ************************************* // + // * Public Views for Dispute Kits * // + // ************************************* // + + /// @dev Gets the number of votes permitted for the specified dispute in the latest round. + /// @param _disputeID The ID of the dispute. + function getNumberOfVotes(uint256 _disputeID) external view returns (uint256) { + Dispute storage dispute = disputes[_disputeID]; + return dispute.rounds[dispute.rounds.length - 1].nbVotes; + } + + /// @dev Returns true if the dispute kit will be switched to a parent DK. + /// @param _disputeID The ID of the dispute. + /// @return Whether DK will be switched or not. + function isDisputeKitJumping(uint256 _disputeID) external view returns (bool) { + Dispute storage dispute = disputes[_disputeID]; + Round storage round = dispute.rounds[dispute.rounds.length - 1]; + Court storage court = courts[dispute.courtID]; + + if (!_isCourtJumping(round, court, _disputeID)) { + return false; + } + + // Jump if the parent court doesn't support the current DK. + return !courts[court.parent].supportedDisputeKits[round.disputeKitID]; + } + + function getDisputeKitsLength() external view returns (uint256) { + return disputeKits.length; + } + + function convertEthToTokenAmount(IERC20 _toToken, uint256 _amountInEth) public view returns (uint256) { + return (_amountInEth * 10 ** currencyRates[_toToken].rateDecimals) / currencyRates[_toToken].rateInEth; + } + + // ************************************* // + // * Internal * // + // ************************************* // + + /// @dev Returns true if the round is jumping to a parent court. + /// @param _round The round to check. + /// @param _court The court to check. + /// @return Whether the round is jumping to a parent court or not. + function _isCourtJumping( + Round storage _round, + Court storage _court, + uint256 _disputeID + ) internal view returns (bool) { + return + disputeKits[_round.disputeKitID].earlyCourtJump(_disputeID) || _round.nbVotes >= _court.jurorsForCourtJump; + } + + function _getCourtAndDisputeKitJumps( + Dispute storage _dispute, + Round storage _round, + Court storage _court, + uint256 _disputeID + ) internal view returns (uint96 newCourtID, uint256 newDisputeKitID, bool courtJump, bool disputeKitJump) { + newCourtID = _dispute.courtID; + newDisputeKitID = _round.disputeKitID; + + if (!_isCourtJumping(_round, _court, _disputeID)) return (newCourtID, newDisputeKitID, false, false); + + // Jump to parent court. + newCourtID = courts[newCourtID].parent; + courtJump = true; + + if (!courts[newCourtID].supportedDisputeKits[newDisputeKitID]) { + // The current Dispute Kit is not compatible with the new court, jump to another Dispute Kit. + newDisputeKitID = disputeKits[_round.disputeKitID].getJumpDisputeKitID(); + if (newDisputeKitID == NULL_DISPUTE_KIT || !courts[newCourtID].supportedDisputeKits[newDisputeKitID]) { + // The new Dispute Kit is not defined or still not compatible, fall back to `DisputeKitClassic` which is always supported. + newDisputeKitID = DISPUTE_KIT_CLASSIC; + } + disputeKitJump = true; + } + } + + /// @dev Internal function to transfer fee tokens (ETH or ERC20) + /// @param _feeToken The token to transfer (NATIVE_CURRENCY for ETH). + /// @param _recipient The recipient address. + /// @param _amount The amount to transfer. + function _transferFeeToken(IERC20 _feeToken, address payable _recipient, uint256 _amount) internal { + if (_feeToken == NATIVE_CURRENCY) { + _recipient.safeSend(_amount, wNative); + } else { + _feeToken.safeTransfer(_recipient, _amount); + } + } + + /// @dev Applies degree of coherence to an amount + /// @param _amount The base amount to apply coherence to. + /// @param _coherence The degree of coherence in basis points. + /// @return The amount after applying the degree of coherence. + function _applyCoherence(uint256 _amount, uint256 _coherence) internal pure returns (uint256) { + return (_amount * _coherence) / ONE_BASIS_POINT; + } + + /// @dev Calculates PNK at stake per juror based on court parameters + /// @param _minStake The minimum stake for the court. + /// @param _alpha The alpha parameter for the court in basis points. + /// @return The amount of PNK at stake per juror. + function _calculatePnkAtStake(uint256 _minStake, uint256 _alpha) internal pure returns (uint256) { + return (_minStake * _alpha) / ONE_BASIS_POINT; + } + + /// @dev Toggles the dispute kit support for a given court. + /// @param _courtID The ID of the court to toggle the support for. + /// @param _disputeKitID The ID of the dispute kit to toggle the support for. + /// @param _enable Whether to enable or disable the support. Note that classic dispute kit should always be enabled. + function _enableDisputeKit(uint96 _courtID, uint256 _disputeKitID, bool _enable) internal { + courts[_courtID].supportedDisputeKits[_disputeKitID] = _enable; + emit DisputeKitEnabled(_courtID, _disputeKitID, _enable); + } + + /// @dev If called only once then set _onError to Revert, otherwise set it to Return + /// @param _account The account to set the stake for. + /// @param _courtID The ID of the court to set the stake for. + /// @param _newStake The new stake. + /// @param _noDelay True if the stake change should not be delayed. + /// @param _onError Whether to revert or return false on error. + /// @return Whether the stake was successfully set or not. + function _setStake( + address _account, + uint96 _courtID, + uint256 _newStake, + bool _noDelay, + OnError _onError + ) internal returns (bool) { + if (_courtID == FORKING_COURT || _courtID >= courts.length) { + _stakingFailed(_onError, StakingResult.CannotStakeInThisCourt); // Staking directly into the forking court is not allowed. + return false; + } + if (_newStake != 0 && _newStake < courts[_courtID].minStake) { + _stakingFailed(_onError, StakingResult.CannotStakeLessThanMinStake); // Staking less than the minimum stake is not allowed. + return false; + } + (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) = sortitionModule.validateStake( + _account, + _courtID, + _newStake, + _noDelay + ); + if (stakingResult != StakingResult.Successful && stakingResult != StakingResult.Delayed) { + _stakingFailed(_onError, stakingResult); + return false; + } else if (stakingResult == StakingResult.Delayed) { + return true; + } + if (pnkDeposit > 0) { + if (!pinakion.safeTransferFrom(_account, address(this), pnkDeposit)) { + _stakingFailed(_onError, StakingResult.StakingTransferFailed); + return false; + } + } + if (pnkWithdrawal > 0) { + if (!pinakion.safeTransfer(_account, pnkWithdrawal)) { + _stakingFailed(_onError, StakingResult.UnstakingTransferFailed); + return false; + } + } + sortitionModule.setStake(_account, _courtID, pnkDeposit, pnkWithdrawal, _newStake); + + return true; + } + + /// @dev It may revert depending on the _onError parameter. + function _stakingFailed(OnError _onError, StakingResult _result) internal pure virtual { + if (_onError == OnError.Return) return; + if (_result == StakingResult.StakingTransferFailed) revert StakingTransferFailed(); + if (_result == StakingResult.UnstakingTransferFailed) revert UnstakingTransferFailed(); + if (_result == StakingResult.CannotStakeInMoreCourts) revert StakingInTooManyCourts(); + if (_result == StakingResult.CannotStakeInThisCourt) revert StakingNotPossibleInThisCourt(); + if (_result == StakingResult.CannotStakeLessThanMinStake) revert StakingLessThanCourtMinStake(); + if (_result == StakingResult.CannotStakeZeroWhenNoStake) revert StakingZeroWhenNoStake(); + if (_result == StakingResult.CannotStakeMoreThanMaxStakePerJuror) revert StakingMoreThanMaxStakePerJuror(); + if (_result == StakingResult.CannotStakeMoreThanMaxTotalStaked) revert StakingMoreThanMaxTotalStaked(); + } + + /// @dev Gets a court ID, the minimum number of jurors and an ID of a dispute kit from a specified extra data bytes array. + /// Note that if extradata contains an incorrect value then this value will be switched to default. + /// @param _extraData The extra data bytes array. The first 32 bytes are the court ID, the next are the minimum number of jurors and the last are the dispute kit ID. + /// @return courtID The court ID. + /// @return minJurors The minimum number of jurors required. + /// @return disputeKitID The ID of the dispute kit. + function _extraDataToCourtIDMinJurorsDisputeKit( + bytes memory _extraData + ) internal view returns (uint96 courtID, uint256 minJurors, uint256 disputeKitID) { + // Note that if the extradata doesn't contain 32 bytes for the dispute kit ID it'll return the default 0 index. + if (_extraData.length >= 64) { + assembly { + // solium-disable-line security/no-inline-assembly + courtID := mload(add(_extraData, 0x20)) + minJurors := mload(add(_extraData, 0x40)) + disputeKitID := mload(add(_extraData, 0x60)) + } + if (courtID == FORKING_COURT || courtID >= courts.length) { + courtID = GENERAL_COURT; + } + if (minJurors == 0) { + minJurors = DEFAULT_NB_OF_JURORS; + } + if (disputeKitID == NULL_DISPUTE_KIT || disputeKitID >= disputeKits.length) { + disputeKitID = DISPUTE_KIT_CLASSIC; // 0 index is not used. + } + } else { + courtID = GENERAL_COURT; + minJurors = DEFAULT_NB_OF_JURORS; + disputeKitID = DISPUTE_KIT_CLASSIC; + } + } + + // ************************************* // + // * Errors * // + // ************************************* // + + error OwnerOnly(); + error GuardianOrOwnerOnly(); + error DisputeKitOnly(); + error SortitionModuleOnly(); + error UnsuccessfulCall(); + error InvalidDisputKitParent(); + error MinStakeLowerThanParentCourt(); + error UnsupportedDisputeKit(); + error InvalidForkingCourtAsParent(); + error WrongDisputeKitIndex(); + error CannotDisableClassicDK(); + error NotEligibleForStaking(); + error StakingMoreThanMaxStakePerJuror(); + error StakingMoreThanMaxTotalStaked(); + error StakingInTooManyCourts(); + error StakingNotPossibleInThisCourt(); + error StakingLessThanCourtMinStake(); + error StakingTransferFailed(); + error UnstakingTransferFailed(); + error ArbitrableNotWhitelisted(); + error ArbitrationFeesNotEnough(); + error DisputeKitNotSupportedByCourt(); + error MustSupportDisputeKitClassic(); + error TokenNotAccepted(); + error EvidenceNotPassedAndNotAppeal(); + error DisputeStillDrawing(); + error CommitPeriodNotPassed(); + error VotePeriodNotPassed(); + error AppealPeriodNotPassed(); + error NotEvidencePeriod(); + error AppealFeesNotEnough(); + error DisputeNotAppealable(); + error NotExecutionPeriod(); + error RulingAlreadyExecuted(); + error DisputePeriodIsFinal(); + error TransferFailed(); + error WhenNotPausedOnly(); + error WhenPausedOnly(); + error StakingZeroWhenNoStake(); } diff --git a/contracts/src/arbitration/KlerosCoreBase.sol b/contracts/src/arbitration/KlerosCoreBase.sol deleted file mode 100644 index fc8095715..000000000 --- a/contracts/src/arbitration/KlerosCoreBase.sol +++ /dev/null @@ -1,1275 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.24; - -import {IArbitrableV2, IArbitratorV2} from "./interfaces/IArbitratorV2.sol"; -import {IDisputeKit} from "./interfaces/IDisputeKit.sol"; -import {ISortitionModule} from "./interfaces/ISortitionModule.sol"; -import {Initializable} from "../proxy/Initializable.sol"; -import {UUPSProxiable} from "../proxy/UUPSProxiable.sol"; -import {SafeERC20, IERC20} from "../libraries/SafeERC20.sol"; -import {SafeSend} from "../libraries/SafeSend.sol"; -import "../libraries/Constants.sol"; - -/// @title KlerosCoreBase -/// Core arbitrator contract for Kleros v2. -/// Note that this contract trusts the PNK token, the dispute kit and the sortition module contracts. -abstract contract KlerosCoreBase is IArbitratorV2, Initializable, UUPSProxiable { - using SafeERC20 for IERC20; - using SafeSend for address payable; - - // ************************************* // - // * Enums / Structs * // - // ************************************* // - - enum Period { - evidence, // Evidence can be submitted. This is also when drawing has to take place. - commit, // Jurors commit a hashed vote. This is skipped for courts without hidden votes. - vote, // Jurors reveal/cast their vote depending on whether the court has hidden votes or not. - appeal, // The dispute can be appealed. - execution // Tokens are redistributed and the ruling is executed. - } - - struct Court { - uint96 parent; // The parent court. - bool hiddenVotes; // Whether to use commit and reveal or not. - uint256[] children; // List of child courts. - uint256 minStake; // Minimum PNKs needed to stake in the court. - uint256 alpha; // Basis point of PNKs that are lost when incoherent. - uint256 feeForJuror; // Arbitration fee paid per juror. - uint256 jurorsForCourtJump; // The appeal after the one that reaches this number of jurors will go to the parent court if any. - uint256[4] timesPerPeriod; // The time allotted to each dispute period in the form `timesPerPeriod[period]`. - mapping(uint256 disputeKitId => bool) supportedDisputeKits; // True if DK with this ID is supported by the court. Note that each court must support classic dispute kit. - bool disabled; // True if the court is disabled. Unused for now, will be implemented later. - } - - struct Dispute { - uint96 courtID; // The ID of the court the dispute is in. - IArbitrableV2 arbitrated; // The arbitrable contract. - Period period; // The current period of the dispute. - bool ruled; // True if the ruling has been executed, false otherwise. - uint256 lastPeriodChange; // The last time the period was changed. - Round[] rounds; - } - - struct Round { - uint256 disputeKitID; // Index of the dispute kit in the array. - uint256 pnkAtStakePerJuror; // The amount of PNKs at stake for each juror in this round. - uint256 totalFeesForJurors; // The total juror fees paid in this round. - uint256 nbVotes; // The total number of votes the dispute can possibly have in the current round. Former votes[_round].length. - uint256 repartitions; // A counter of reward repartitions made in this round. - uint256 pnkPenalties; // The amount of PNKs collected from penalties in this round. - address[] drawnJurors; // Addresses of the jurors that were drawn in this round. - uint96[] drawnJurorFromCourtIDs; // The courtIDs where the juror was drawn from, possibly their stake in a subcourt. - uint256 sumFeeRewardPaid; // Total sum of arbitration fees paid to coherent jurors as a reward in this round. - uint256 sumPnkRewardPaid; // Total sum of PNK paid to coherent jurors as a reward in this round. - IERC20 feeToken; // The token used for paying fees in this round. - uint256 drawIterations; // The number of iterations passed drawing the jurors for this round. - } - - // Workaround "stack too deep" errors - struct ExecuteParams { - uint256 disputeID; // The ID of the dispute to execute. - uint256 round; // The round to execute. - uint256 coherentCount; // The number of coherent votes in the round. - uint256 numberOfVotesInRound; // The number of votes in the round. - uint256 feePerJurorInRound; // The fee per juror in the round. - uint256 pnkAtStakePerJurorInRound; // The amount of PNKs at stake for each juror in the round. - uint256 pnkPenaltiesInRound; // The amount of PNKs collected from penalties in the round. - uint256 repartition; // The index of the repartition to execute. - } - - struct CurrencyRate { - bool feePaymentAccepted; - uint64 rateInEth; - uint8 rateDecimals; - } - - // ************************************* // - // * Storage * // - // ************************************* // - - uint256 private constant NON_PAYABLE_AMOUNT = (2 ** 256 - 2) / 2; // An amount higher than the supply of ETH. - - address public owner; // The owner of the contract. - address public guardian; // The guardian able to pause asset withdrawals. - IERC20 public pinakion; // The Pinakion token contract. - address public jurorProsecutionModule; // The module for juror's prosecution. - ISortitionModule public sortitionModule; // Sortition module for drawing. - Court[] public courts; // The courts. - IDisputeKit[] public disputeKits; // Array of dispute kits. - Dispute[] public disputes; // The disputes. - mapping(IERC20 => CurrencyRate) public currencyRates; // The price of each token in ETH. - bool public paused; // Whether asset withdrawals are paused. - address public wNative; // The wrapped native token for safeSend(). - - // ************************************* // - // * Events * // - // ************************************* // - - event NewPeriod(uint256 indexed _disputeID, Period _period); - event AppealPossible(uint256 indexed _disputeID, IArbitrableV2 indexed _arbitrable); - event AppealDecision(uint256 indexed _disputeID, IArbitrableV2 indexed _arbitrable); - event Draw(address indexed _address, uint256 indexed _disputeID, uint256 _roundID, uint256 _voteID); - event CourtCreated( - uint96 indexed _courtID, - uint96 indexed _parent, - bool _hiddenVotes, - uint256 _minStake, - uint256 _alpha, - uint256 _feeForJuror, - uint256 _jurorsForCourtJump, - uint256[4] _timesPerPeriod, - uint256[] _supportedDisputeKits - ); - event CourtModified( - uint96 indexed _courtID, - bool _hiddenVotes, - uint256 _minStake, - uint256 _alpha, - uint256 _feeForJuror, - uint256 _jurorsForCourtJump, - uint256[4] _timesPerPeriod - ); - event DisputeKitCreated(uint256 indexed _disputeKitID, IDisputeKit indexed _disputeKitAddress); - event DisputeKitEnabled(uint96 indexed _courtID, uint256 indexed _disputeKitID, bool indexed _enable); - event CourtJump( - uint256 indexed _disputeID, - uint256 indexed _roundID, - uint96 indexed _fromCourtID, - uint96 _toCourtID - ); - event DisputeKitJump( - uint256 indexed _disputeID, - uint256 indexed _roundID, - uint256 indexed _fromDisputeKitID, - uint256 _toDisputeKitID - ); - event TokenAndETHShift( - address indexed _account, - uint256 indexed _disputeID, - uint256 indexed _roundID, - uint256 _degreeOfCoherency, - int256 _pnkAmount, - int256 _feeAmount, - IERC20 _feeToken - ); - event LeftoverRewardSent( - uint256 indexed _disputeID, - uint256 indexed _roundID, - uint256 _pnkAmount, - uint256 _feeAmount, - IERC20 _feeToken - ); - event Paused(); - event Unpaused(); - - // ************************************* // - // * Function Modifiers * // - // ************************************* // - - modifier onlyByOwner() { - if (owner != msg.sender) revert OwnerOnly(); - _; - } - - modifier onlyByGuardianOrOwner() { - if (guardian != msg.sender && owner != msg.sender) revert GuardianOrOwnerOnly(); - _; - } - - modifier whenPaused() { - if (!paused) revert WhenPausedOnly(); - _; - } - - modifier whenNotPaused() { - if (paused) revert WhenNotPausedOnly(); - _; - } - - // ************************************* // - // * Constructor * // - // ************************************* // - - function __KlerosCoreBase_initialize( - address _owner, - address _guardian, - IERC20 _pinakion, - address _jurorProsecutionModule, - IDisputeKit _disputeKit, - bool _hiddenVotes, - uint256[4] memory _courtParameters, - uint256[4] memory _timesPerPeriod, - bytes memory _sortitionExtraData, - ISortitionModule _sortitionModuleAddress, - address _wNative - ) internal onlyInitializing { - owner = _owner; - guardian = _guardian; - pinakion = _pinakion; - jurorProsecutionModule = _jurorProsecutionModule; - sortitionModule = _sortitionModuleAddress; - wNative = _wNative; - - // NULL_DISPUTE_KIT: an empty element at index 0 to indicate when a dispute kit is not supported. - disputeKits.push(); - - // DISPUTE_KIT_CLASSIC - disputeKits.push(_disputeKit); - - emit DisputeKitCreated(DISPUTE_KIT_CLASSIC, _disputeKit); - - // FORKING_COURT - // TODO: Fill the properties for the Forking court, emit CourtCreated. - courts.push(); - sortitionModule.createTree(FORKING_COURT, _sortitionExtraData); - - // GENERAL_COURT - Court storage court = courts.push(); - court.parent = FORKING_COURT; - court.children = new uint256[](0); - court.hiddenVotes = _hiddenVotes; - court.minStake = _courtParameters[0]; - court.alpha = _courtParameters[1]; - court.feeForJuror = _courtParameters[2]; - court.jurorsForCourtJump = _courtParameters[3]; - court.timesPerPeriod = _timesPerPeriod; - - sortitionModule.createTree(GENERAL_COURT, _sortitionExtraData); - - uint256[] memory supportedDisputeKits = new uint256[](1); - supportedDisputeKits[0] = DISPUTE_KIT_CLASSIC; - emit CourtCreated( - GENERAL_COURT, - court.parent, - _hiddenVotes, - _courtParameters[0], - _courtParameters[1], - _courtParameters[2], - _courtParameters[3], - _timesPerPeriod, - supportedDisputeKits - ); - _enableDisputeKit(GENERAL_COURT, DISPUTE_KIT_CLASSIC, true); - } - - // ************************************* // - // * Governance * // - // ************************************* // - - /// @dev Pause staking and reward execution. Can only be done by guardian or owner. - function pause() external onlyByGuardianOrOwner whenNotPaused { - paused = true; - emit Paused(); - } - - /// @dev Unpause staking and reward execution. Can only be done by owner. - function unpause() external onlyByOwner whenPaused { - paused = false; - emit Unpaused(); - } - - /// @dev Allows the owner to call anything on behalf of the contract. - /// @param _destination The destination of the call. - /// @param _amount The value sent with the call. - /// @param _data The data sent with the call. - function executeOwnerProposal(address _destination, uint256 _amount, bytes memory _data) external onlyByOwner { - (bool success, ) = _destination.call{value: _amount}(_data); - if (!success) revert UnsuccessfulCall(); - } - - /// @dev Changes the `owner` storage variable. - /// @param _owner The new value for the `owner` storage variable. - function changeOwner(address payable _owner) external onlyByOwner { - owner = _owner; - } - - /// @dev Changes the `guardian` storage variable. - /// @param _guardian The new value for the `guardian` storage variable. - function changeGuardian(address _guardian) external onlyByOwner { - guardian = _guardian; - } - - /// @dev Changes the `pinakion` storage variable. - /// @param _pinakion The new value for the `pinakion` storage variable. - function changePinakion(IERC20 _pinakion) external onlyByOwner { - pinakion = _pinakion; - } - - /// @dev Changes the `jurorProsecutionModule` storage variable. - /// @param _jurorProsecutionModule The new value for the `jurorProsecutionModule` storage variable. - function changeJurorProsecutionModule(address _jurorProsecutionModule) external onlyByOwner { - jurorProsecutionModule = _jurorProsecutionModule; - } - - /// @dev Changes the `_sortitionModule` storage variable. - /// Note that the new module should be initialized for all courts. - /// @param _sortitionModule The new value for the `sortitionModule` storage variable. - function changeSortitionModule(ISortitionModule _sortitionModule) external onlyByOwner { - sortitionModule = _sortitionModule; - } - - /// @dev Add a new supported dispute kit module to the court. - /// @param _disputeKitAddress The address of the dispute kit contract. - function addNewDisputeKit(IDisputeKit _disputeKitAddress) external onlyByOwner { - uint256 disputeKitID = disputeKits.length; - disputeKits.push(_disputeKitAddress); - emit DisputeKitCreated(disputeKitID, _disputeKitAddress); - } - - /// @dev Creates a court under a specified parent court. - /// @param _parent The `parent` property value of the court. - /// @param _hiddenVotes The `hiddenVotes` property value of the court. - /// @param _minStake The `minStake` property value of the court. - /// @param _alpha The `alpha` property value of the court. - /// @param _feeForJuror The `feeForJuror` property value of the court. - /// @param _jurorsForCourtJump The `jurorsForCourtJump` property value of the court. - /// @param _timesPerPeriod The `timesPerPeriod` property value of the court. - /// @param _sortitionExtraData Extra data for sortition module. - /// @param _supportedDisputeKits Indexes of dispute kits that this court will support. - function createCourt( - uint96 _parent, - bool _hiddenVotes, - uint256 _minStake, - uint256 _alpha, - uint256 _feeForJuror, - uint256 _jurorsForCourtJump, - uint256[4] memory _timesPerPeriod, - bytes memory _sortitionExtraData, - uint256[] memory _supportedDisputeKits - ) external onlyByOwner { - if (courts[_parent].minStake > _minStake) revert MinStakeLowerThanParentCourt(); - if (_supportedDisputeKits.length == 0) revert UnsupportedDisputeKit(); - if (_parent == FORKING_COURT) revert InvalidForkingCourtAsParent(); - - uint96 courtID = uint96(courts.length); - Court storage court = courts.push(); - - for (uint256 i = 0; i < _supportedDisputeKits.length; i++) { - if (_supportedDisputeKits[i] == 0 || _supportedDisputeKits[i] >= disputeKits.length) { - revert WrongDisputeKitIndex(); - } - _enableDisputeKit(uint96(courtID), _supportedDisputeKits[i], true); - } - // Check that Classic DK support was added. - if (!court.supportedDisputeKits[DISPUTE_KIT_CLASSIC]) revert MustSupportDisputeKitClassic(); - - court.parent = _parent; - court.children = new uint256[](0); - court.hiddenVotes = _hiddenVotes; - court.minStake = _minStake; - court.alpha = _alpha; - court.feeForJuror = _feeForJuror; - court.jurorsForCourtJump = _jurorsForCourtJump; - court.timesPerPeriod = _timesPerPeriod; - - sortitionModule.createTree(courtID, _sortitionExtraData); - - // Update the parent. - courts[_parent].children.push(courtID); - emit CourtCreated( - uint96(courtID), - _parent, - _hiddenVotes, - _minStake, - _alpha, - _feeForJuror, - _jurorsForCourtJump, - _timesPerPeriod, - _supportedDisputeKits - ); - } - - function changeCourtParameters( - uint96 _courtID, - bool _hiddenVotes, - uint256 _minStake, - uint256 _alpha, - uint256 _feeForJuror, - uint256 _jurorsForCourtJump, - uint256[4] memory _timesPerPeriod - ) external onlyByOwner { - Court storage court = courts[_courtID]; - if (_courtID != GENERAL_COURT && courts[court.parent].minStake > _minStake) { - revert MinStakeLowerThanParentCourt(); - } - for (uint256 i = 0; i < court.children.length; i++) { - if (courts[court.children[i]].minStake < _minStake) { - revert MinStakeLowerThanParentCourt(); - } - } - court.minStake = _minStake; - court.hiddenVotes = _hiddenVotes; - court.alpha = _alpha; - court.feeForJuror = _feeForJuror; - court.jurorsForCourtJump = _jurorsForCourtJump; - court.timesPerPeriod = _timesPerPeriod; - emit CourtModified( - _courtID, - _hiddenVotes, - _minStake, - _alpha, - _feeForJuror, - _jurorsForCourtJump, - _timesPerPeriod - ); - } - - /// @dev Adds/removes court's support for specified dispute kits. - /// @param _courtID The ID of the court. - /// @param _disputeKitIDs The IDs of dispute kits which support should be added/removed. - /// @param _enable Whether add or remove the dispute kits from the court. - function enableDisputeKits(uint96 _courtID, uint256[] memory _disputeKitIDs, bool _enable) external onlyByOwner { - for (uint256 i = 0; i < _disputeKitIDs.length; i++) { - if (_enable) { - if (_disputeKitIDs[i] == 0 || _disputeKitIDs[i] >= disputeKits.length) { - revert WrongDisputeKitIndex(); - } - _enableDisputeKit(_courtID, _disputeKitIDs[i], true); - } else { - // Classic dispute kit must be supported by all courts. - if (_disputeKitIDs[i] == DISPUTE_KIT_CLASSIC) { - revert CannotDisableClassicDK(); - } - _enableDisputeKit(_courtID, _disputeKitIDs[i], false); - } - } - } - - /// @dev Changes the supported fee tokens. - /// @param _feeToken The fee token. - /// @param _accepted Whether the token is supported or not as a method of fee payment. - function changeAcceptedFeeTokens(IERC20 _feeToken, bool _accepted) external onlyByOwner { - currencyRates[_feeToken].feePaymentAccepted = _accepted; - emit AcceptedFeeToken(_feeToken, _accepted); - } - - /// @dev Changes the currency rate of a fee token. - /// @param _feeToken The fee token. - /// @param _rateInEth The new rate of the fee token in ETH. - /// @param _rateDecimals The new decimals of the fee token rate. - function changeCurrencyRates(IERC20 _feeToken, uint64 _rateInEth, uint8 _rateDecimals) external onlyByOwner { - currencyRates[_feeToken].rateInEth = _rateInEth; - currencyRates[_feeToken].rateDecimals = _rateDecimals; - emit NewCurrencyRate(_feeToken, _rateInEth, _rateDecimals); - } - - // ************************************* // - // * State Modifiers * // - // ************************************* // - - /// @dev Sets the caller's stake in a court. - /// @param _courtID The ID of the court. - /// @param _newStake The new stake. - /// Note that the existing delayed stake will be nullified as non-relevant. - function setStake(uint96 _courtID, uint256 _newStake) external virtual whenNotPaused { - _setStake(msg.sender, _courtID, _newStake, false, OnError.Revert); - } - - /// @dev Sets the stake of a specified account in a court without delaying stake changes, typically to apply a delayed stake or unstake inactive jurors. - /// @param _account The account whose stake is being set. - /// @param _courtID The ID of the court. - /// @param _newStake The new stake. - function setStakeBySortitionModule(address _account, uint96 _courtID, uint256 _newStake) external { - if (msg.sender != address(sortitionModule)) revert SortitionModuleOnly(); - _setStake(_account, _courtID, _newStake, true, OnError.Return); - } - - /// @dev Transfers PNK to the juror by SortitionModule. - /// @param _account The account of the juror whose PNK to transfer. - /// @param _amount The amount to transfer. - function transferBySortitionModule(address _account, uint256 _amount) external { - if (msg.sender != address(sortitionModule)) revert SortitionModuleOnly(); - // Note eligibility is checked in SortitionModule. - pinakion.safeTransfer(_account, _amount); - } - - /// @inheritdoc IArbitratorV2 - function createDispute( - uint256 _numberOfChoices, - bytes memory _extraData - ) external payable override returns (uint256 disputeID) { - if (msg.value < arbitrationCost(_extraData)) revert ArbitrationFeesNotEnough(); - - return _createDispute(_numberOfChoices, _extraData, NATIVE_CURRENCY, msg.value); - } - - /// @inheritdoc IArbitratorV2 - function createDispute( - uint256 _numberOfChoices, - bytes calldata _extraData, - IERC20 _feeToken, - uint256 _feeAmount - ) external override returns (uint256 disputeID) { - if (!currencyRates[_feeToken].feePaymentAccepted) revert TokenNotAccepted(); - if (_feeAmount < arbitrationCost(_extraData, _feeToken)) revert ArbitrationFeesNotEnough(); - - if (!_feeToken.safeTransferFrom(msg.sender, address(this), _feeAmount)) revert TransferFailed(); - return _createDispute(_numberOfChoices, _extraData, _feeToken, _feeAmount); - } - - function _createDispute( - uint256 _numberOfChoices, - bytes memory _extraData, - IERC20 _feeToken, - uint256 _feeAmount - ) internal virtual returns (uint256 disputeID) { - (uint96 courtID, , uint256 disputeKitID) = _extraDataToCourtIDMinJurorsDisputeKit(_extraData); - if (!courts[courtID].supportedDisputeKits[disputeKitID]) revert DisputeKitNotSupportedByCourt(); - - disputeID = disputes.length; - Dispute storage dispute = disputes.push(); - dispute.courtID = courtID; - dispute.arbitrated = IArbitrableV2(msg.sender); - dispute.lastPeriodChange = block.timestamp; - - IDisputeKit disputeKit = disputeKits[disputeKitID]; - Court storage court = courts[courtID]; - Round storage round = dispute.rounds.push(); - - // Obtain the feeForJuror in the same currency as the _feeAmount - uint256 feeForJuror = (_feeToken == NATIVE_CURRENCY) - ? court.feeForJuror - : convertEthToTokenAmount(_feeToken, court.feeForJuror); - round.nbVotes = _feeAmount / feeForJuror; - round.disputeKitID = disputeKitID; - round.pnkAtStakePerJuror = _calculatePnkAtStake(court.minStake, court.alpha); - round.totalFeesForJurors = _feeAmount; - round.feeToken = IERC20(_feeToken); - - sortitionModule.createDisputeHook(disputeID, 0); // Default round ID. - - disputeKit.createDispute(disputeID, _numberOfChoices, _extraData, round.nbVotes); - emit DisputeCreation(disputeID, IArbitrableV2(msg.sender)); - } - - /// @dev Passes the period of a specified dispute. - /// @param _disputeID The ID of the dispute. - function passPeriod(uint256 _disputeID) external { - Dispute storage dispute = disputes[_disputeID]; - Court storage court = courts[dispute.courtID]; - - uint256 currentRound = dispute.rounds.length - 1; - Round storage round = dispute.rounds[currentRound]; - if (dispute.period == Period.evidence) { - if ( - currentRound == 0 && - block.timestamp - dispute.lastPeriodChange < court.timesPerPeriod[uint256(dispute.period)] - ) { - revert EvidenceNotPassedAndNotAppeal(); - } - if (round.drawnJurors.length != round.nbVotes) revert DisputeStillDrawing(); - dispute.period = court.hiddenVotes ? Period.commit : Period.vote; - } else if (dispute.period == Period.commit) { - // Note that we do not want to pass to Voting period if all the commits are cast because it breaks the Shutter auto-reveal currently. - if (block.timestamp - dispute.lastPeriodChange < court.timesPerPeriod[uint256(dispute.period)]) { - revert CommitPeriodNotPassed(); - } - dispute.period = Period.vote; - } else if (dispute.period == Period.vote) { - if ( - block.timestamp - dispute.lastPeriodChange < court.timesPerPeriod[uint256(dispute.period)] && - !disputeKits[round.disputeKitID].areVotesAllCast(_disputeID) - ) { - revert VotePeriodNotPassed(); - } - dispute.period = Period.appeal; - emit AppealPossible(_disputeID, dispute.arbitrated); - } else if (dispute.period == Period.appeal) { - if ( - block.timestamp - dispute.lastPeriodChange < court.timesPerPeriod[uint256(dispute.period)] && - !disputeKits[round.disputeKitID].isAppealFunded(_disputeID) - ) { - revert AppealPeriodNotPassed(); - } - dispute.period = Period.execution; - } else if (dispute.period == Period.execution) { - revert DisputePeriodIsFinal(); - } - - dispute.lastPeriodChange = block.timestamp; - emit NewPeriod(_disputeID, dispute.period); - } - - /// @dev Draws jurors for the dispute. Can be called in parts. - /// @param _disputeID The ID of the dispute. - /// @param _iterations The number of iterations to run. - /// @return nbDrawnJurors The total number of jurors drawn in the round. - function draw(uint256 _disputeID, uint256 _iterations) external returns (uint256 nbDrawnJurors) { - Dispute storage dispute = disputes[_disputeID]; - uint256 currentRound = dispute.rounds.length - 1; - Round storage round = dispute.rounds[currentRound]; - if (dispute.period != Period.evidence) revert NotEvidencePeriod(); - - IDisputeKit disputeKit = disputeKits[round.disputeKitID]; - - uint256 startIndex = round.drawIterations; // for gas: less storage reads - uint256 i; - while (i < _iterations && round.drawnJurors.length < round.nbVotes) { - (address drawnAddress, uint96 fromSubcourtID) = disputeKit.draw(_disputeID, startIndex + i++); - if (drawnAddress == address(0)) { - continue; - } - sortitionModule.lockStake(drawnAddress, round.pnkAtStakePerJuror); - emit Draw(drawnAddress, _disputeID, currentRound, round.drawnJurors.length); - round.drawnJurors.push(drawnAddress); - round.drawnJurorFromCourtIDs.push(fromSubcourtID != 0 ? fromSubcourtID : dispute.courtID); - if (round.drawnJurors.length == round.nbVotes) { - sortitionModule.postDrawHook(_disputeID, currentRound); - } - } - round.drawIterations += i; - return round.drawnJurors.length; - } - - /// @dev Appeals the ruling of a specified dispute. - /// Note: Access restricted to the Dispute Kit for this `disputeID`. - /// @param _disputeID The ID of the dispute. - /// @param _numberOfChoices Number of choices for the dispute. Can be required during court jump. - /// @param _extraData Extradata for the dispute. Can be required during court jump. - function appeal(uint256 _disputeID, uint256 _numberOfChoices, bytes memory _extraData) external payable { - if (msg.value < appealCost(_disputeID)) revert AppealFeesNotEnough(); - - Dispute storage dispute = disputes[_disputeID]; - if (dispute.period != Period.appeal) revert DisputeNotAppealable(); - - Round storage round = dispute.rounds[dispute.rounds.length - 1]; - if (msg.sender != address(disputeKits[round.disputeKitID])) revert DisputeKitOnly(); - - // Warning: the extra round must be created before calling disputeKit.createDispute() - Round storage extraRound = dispute.rounds.push(); - - (uint96 newCourtID, uint256 newDisputeKitID, bool courtJump, ) = _getCourtAndDisputeKitJumps( - dispute, - round, - courts[dispute.courtID], - _disputeID - ); - if (courtJump) { - emit CourtJump(_disputeID, dispute.rounds.length - 1, dispute.courtID, newCourtID); - } - - dispute.courtID = newCourtID; - dispute.period = Period.evidence; - dispute.lastPeriodChange = block.timestamp; - - Court storage court = courts[newCourtID]; - extraRound.nbVotes = msg.value / court.feeForJuror; // As many votes that can be afforded by the provided funds. - extraRound.pnkAtStakePerJuror = _calculatePnkAtStake(court.minStake, court.alpha); - extraRound.totalFeesForJurors = msg.value; - extraRound.disputeKitID = newDisputeKitID; - - sortitionModule.createDisputeHook(_disputeID, dispute.rounds.length - 1); - - // Dispute kit was changed, so create a dispute in the new DK contract. - if (extraRound.disputeKitID != round.disputeKitID) { - emit DisputeKitJump(_disputeID, dispute.rounds.length - 1, round.disputeKitID, extraRound.disputeKitID); - disputeKits[extraRound.disputeKitID].createDispute( - _disputeID, - _numberOfChoices, - _extraData, - extraRound.nbVotes - ); - } - - emit AppealDecision(_disputeID, dispute.arbitrated); - emit NewPeriod(_disputeID, Period.evidence); - } - - /// @dev Distribute the PNKs at stake and the dispute fees for the specific round of the dispute. Can be called in parts. - /// Note: Reward distributions are forbidden during pause. - /// @param _disputeID The ID of the dispute. - /// @param _round The appeal round. - /// @param _iterations The number of iterations to run. - function execute(uint256 _disputeID, uint256 _round, uint256 _iterations) external whenNotPaused { - Round storage round; - { - Dispute storage dispute = disputes[_disputeID]; - if (dispute.period != Period.execution) revert NotExecutionPeriod(); - - round = dispute.rounds[_round]; - } // stack too deep workaround - - uint256 start = round.repartitions; - uint256 end = round.repartitions + _iterations; - - uint256 pnkPenaltiesInRound = round.pnkPenalties; // Keep in memory to save gas. - uint256 numberOfVotesInRound = round.drawnJurors.length; - uint256 feePerJurorInRound = round.totalFeesForJurors / numberOfVotesInRound; - uint256 pnkAtStakePerJurorInRound = round.pnkAtStakePerJuror; - uint256 coherentCount; - { - IDisputeKit disputeKit = disputeKits[round.disputeKitID]; - coherentCount = disputeKit.getCoherentCount(_disputeID, _round); // Total number of jurors that are eligible to a reward in this round. - } // stack too deep workaround - - if (coherentCount == 0) { - // We loop over the votes once as there are no rewards because it is not a tie and no one in this round is coherent with the final outcome. - if (end > numberOfVotesInRound) end = numberOfVotesInRound; - } else { - // We loop over the votes twice, first to collect the PNK penalties, and second to distribute them as rewards along with arbitration fees. - if (end > numberOfVotesInRound * 2) end = numberOfVotesInRound * 2; - } - round.repartitions = end; - - for (uint256 i = start; i < end; i++) { - if (i < numberOfVotesInRound) { - pnkPenaltiesInRound = _executePenalties( - ExecuteParams({ - disputeID: _disputeID, - round: _round, - coherentCount: coherentCount, - numberOfVotesInRound: numberOfVotesInRound, - feePerJurorInRound: feePerJurorInRound, - pnkAtStakePerJurorInRound: pnkAtStakePerJurorInRound, - pnkPenaltiesInRound: pnkPenaltiesInRound, - repartition: i - }) - ); - } else { - _executeRewards( - ExecuteParams({ - disputeID: _disputeID, - round: _round, - coherentCount: coherentCount, - numberOfVotesInRound: numberOfVotesInRound, - feePerJurorInRound: feePerJurorInRound, - pnkAtStakePerJurorInRound: pnkAtStakePerJurorInRound, - pnkPenaltiesInRound: pnkPenaltiesInRound, - repartition: i - }) - ); - } - } - if (round.pnkPenalties != pnkPenaltiesInRound) { - round.pnkPenalties = pnkPenaltiesInRound; // Reentrancy risk: breaks Check-Effect-Interact - } - } - - /// @dev Distribute the PNKs at stake and the dispute fees for the specific round of the dispute, penalties only. - /// @param _params The parameters for the execution, see `ExecuteParams`. - /// @return pnkPenaltiesInRoundCache The updated penalties in round cache. - function _executePenalties(ExecuteParams memory _params) internal returns (uint256) { - Dispute storage dispute = disputes[_params.disputeID]; - Round storage round = dispute.rounds[_params.round]; - IDisputeKit disputeKit = disputeKits[round.disputeKitID]; - - // [0, 1] value that determines how coherent the juror was in this round, in basis points. - uint256 coherence = disputeKit.getDegreeOfCoherencePenalty( - _params.disputeID, - _params.round, - _params.repartition, - _params.feePerJurorInRound, - _params.pnkAtStakePerJurorInRound - ); - - // Guard against degree exceeding 1, though it should be ensured by the dispute kit. - if (coherence > ONE_BASIS_POINT) { - coherence = ONE_BASIS_POINT; - } - - // Fully coherent jurors won't be penalized. - uint256 penalty = (round.pnkAtStakePerJuror * (ONE_BASIS_POINT - coherence)) / ONE_BASIS_POINT; - - // Unlock the PNKs affected by the penalty - address account = round.drawnJurors[_params.repartition]; - sortitionModule.unlockStake(account, penalty); - - // Apply the penalty to the staked PNKs. - uint96 penalizedInCourtID = round.drawnJurorFromCourtIDs[_params.repartition]; - (uint256 pnkBalance, uint256 newCourtStake, uint256 availablePenalty) = sortitionModule.setStakePenalty( - account, - penalizedInCourtID, - penalty - ); - _params.pnkPenaltiesInRound += availablePenalty; - emit TokenAndETHShift( - account, - _params.disputeID, - _params.round, - coherence, - -int256(availablePenalty), - 0, - round.feeToken - ); - - if (pnkBalance == 0 || !disputeKit.isVoteActive(_params.disputeID, _params.round, _params.repartition)) { - // The juror is inactive or their balance is can't cover penalties anymore, unstake them from all courts. - sortitionModule.forcedUnstakeAllCourts(account); - } else if (newCourtStake < courts[penalizedInCourtID].minStake) { - // The juror's balance fell below the court minStake, unstake them from the court. - sortitionModule.forcedUnstake(account, penalizedInCourtID); - } - - if (_params.repartition == _params.numberOfVotesInRound - 1 && _params.coherentCount == 0) { - // No one was coherent, send the rewards to the owner. - _transferFeeToken(round.feeToken, payable(owner), round.totalFeesForJurors); - pinakion.safeTransfer(owner, _params.pnkPenaltiesInRound); - emit LeftoverRewardSent( - _params.disputeID, - _params.round, - _params.pnkPenaltiesInRound, - round.totalFeesForJurors, - round.feeToken - ); - } - return _params.pnkPenaltiesInRound; - } - - /// @dev Distribute the PNKs at stake and the dispute fees for the specific round of the dispute, rewards only. - /// @param _params The parameters for the execution, see `ExecuteParams`. - function _executeRewards(ExecuteParams memory _params) internal { - Dispute storage dispute = disputes[_params.disputeID]; - Round storage round = dispute.rounds[_params.round]; - IDisputeKit disputeKit = disputeKits[round.disputeKitID]; - - // [0, 1] value that determines how coherent the juror was in this round, in basis points. - (uint256 pnkCoherence, uint256 feeCoherence) = disputeKit.getDegreeOfCoherenceReward( - _params.disputeID, - _params.round, - _params.repartition % _params.numberOfVotesInRound, - _params.feePerJurorInRound, - _params.pnkAtStakePerJurorInRound - ); - - // Guard against degree exceeding 1, though it should be ensured by the dispute kit. - if (pnkCoherence > ONE_BASIS_POINT) { - pnkCoherence = ONE_BASIS_POINT; - } - if (feeCoherence > ONE_BASIS_POINT) { - feeCoherence = ONE_BASIS_POINT; - } - - address account = round.drawnJurors[_params.repartition % _params.numberOfVotesInRound]; - uint256 pnkLocked = _applyCoherence(round.pnkAtStakePerJuror, pnkCoherence); - - // Release the rest of the PNKs of the juror for this round. - sortitionModule.unlockStake(account, pnkLocked); - - // Compute the rewards - uint256 pnkReward = _applyCoherence(_params.pnkPenaltiesInRound / _params.coherentCount, pnkCoherence); - round.sumPnkRewardPaid += pnkReward; - uint256 feeReward = _applyCoherence(round.totalFeesForJurors / _params.coherentCount, feeCoherence); - round.sumFeeRewardPaid += feeReward; - - // Transfer the fee reward - _transferFeeToken(round.feeToken, payable(account), feeReward); - - // Stake the PNK reward if possible, by-passes delayed stakes and other checks usually done by validateStake() - if (!sortitionModule.setStakeReward(account, dispute.courtID, pnkReward)) { - pinakion.safeTransfer(account, pnkReward); - } - - emit TokenAndETHShift( - account, - _params.disputeID, - _params.round, - pnkCoherence, - int256(pnkReward), - int256(feeReward), - round.feeToken - ); - - // Transfer any residual rewards to the owner. It may happen due to partial coherence of the jurors. - if (_params.repartition == _params.numberOfVotesInRound * 2 - 1) { - uint256 leftoverPnkReward = _params.pnkPenaltiesInRound - round.sumPnkRewardPaid; - uint256 leftoverFeeReward = round.totalFeesForJurors - round.sumFeeRewardPaid; - if (leftoverPnkReward != 0 || leftoverFeeReward != 0) { - if (leftoverPnkReward != 0) { - pinakion.safeTransfer(owner, leftoverPnkReward); - } - if (leftoverFeeReward != 0) { - _transferFeeToken(round.feeToken, payable(owner), leftoverFeeReward); - } - emit LeftoverRewardSent( - _params.disputeID, - _params.round, - leftoverPnkReward, - leftoverFeeReward, - round.feeToken - ); - } - } - } - - /// @dev Executes a specified dispute's ruling. - /// @param _disputeID The ID of the dispute. - function executeRuling(uint256 _disputeID) external { - Dispute storage dispute = disputes[_disputeID]; - if (dispute.period != Period.execution) revert NotExecutionPeriod(); - if (dispute.ruled) revert RulingAlreadyExecuted(); - - (uint256 winningChoice, , ) = currentRuling(_disputeID); - dispute.ruled = true; - emit Ruling(dispute.arbitrated, _disputeID, winningChoice); - dispute.arbitrated.rule(_disputeID, winningChoice); - } - - // ************************************* // - // * Public Views * // - // ************************************* // - - /// @dev Compute the cost of arbitration denominated in ETH. - /// It is recommended not to increase it often, as it can be highly time and gas consuming for the arbitrated contracts to cope with fee augmentation. - /// @param _extraData Additional info about the dispute. We use it to pass the ID of the dispute's court (first 32 bytes), the minimum number of jurors required (next 32 bytes) and the ID of the specific dispute kit (last 32 bytes). - /// @return cost The arbitration cost in ETH. - function arbitrationCost(bytes memory _extraData) public view override returns (uint256 cost) { - (uint96 courtID, uint256 minJurors, ) = _extraDataToCourtIDMinJurorsDisputeKit(_extraData); - cost = courts[courtID].feeForJuror * minJurors; - } - - /// @dev Compute the cost of arbitration denominated in `_feeToken`. - /// It is recommended not to increase it often, as it can be highly time and gas consuming for the arbitrated contracts to cope with fee augmentation. - /// @param _extraData Additional info about the dispute. We use it to pass the ID of the dispute's court (first 32 bytes), the minimum number of jurors required (next 32 bytes) and the ID of the specific dispute kit (last 32 bytes). - /// @param _feeToken The ERC20 token used to pay fees. - /// @return cost The arbitration cost in `_feeToken`. - function arbitrationCost(bytes calldata _extraData, IERC20 _feeToken) public view override returns (uint256 cost) { - cost = convertEthToTokenAmount(_feeToken, arbitrationCost(_extraData)); - } - - /// @dev Gets the cost of appealing a specified dispute. - /// @param _disputeID The ID of the dispute. - /// @return cost The appeal cost. - function appealCost(uint256 _disputeID) public view returns (uint256 cost) { - Dispute storage dispute = disputes[_disputeID]; - Round storage round = dispute.rounds[dispute.rounds.length - 1]; - Court storage court = courts[dispute.courtID]; - - (, uint256 newDisputeKitID, bool courtJump, ) = _getCourtAndDisputeKitJumps(dispute, round, court, _disputeID); - - uint256 nbVotesAfterAppeal = disputeKits[newDisputeKitID].getNbVotesAfterAppeal( - disputeKits[round.disputeKitID], - round.nbVotes - ); - - if (courtJump) { - // Jump to parent court. - if (dispute.courtID == GENERAL_COURT) { - // TODO: Handle the forking when appealed in General court. - cost = NON_PAYABLE_AMOUNT; // Get the cost of the parent court. - } else { - cost = courts[court.parent].feeForJuror * nbVotesAfterAppeal; - } - } else { - // Stay in current court. - cost = court.feeForJuror * nbVotesAfterAppeal; - } - } - - /// @dev Gets the start and the end of a specified dispute's current appeal period. - /// @param _disputeID The ID of the dispute. - /// @return start The start of the appeal period. - /// @return end The end of the appeal period. - function appealPeriod(uint256 _disputeID) external view returns (uint256 start, uint256 end) { - Dispute storage dispute = disputes[_disputeID]; - if (dispute.period == Period.appeal) { - start = dispute.lastPeriodChange; - end = dispute.lastPeriodChange + courts[dispute.courtID].timesPerPeriod[uint256(Period.appeal)]; - } else { - start = 0; - end = 0; - } - } - - /// @dev Gets the current ruling of a specified dispute. - /// @param _disputeID The ID of the dispute. - /// @return ruling The current ruling. - /// @return tied Whether it's a tie or not. - /// @return overridden Whether the ruling was overridden by appeal funding or not. - function currentRuling(uint256 _disputeID) public view returns (uint256 ruling, bool tied, bool overridden) { - Dispute storage dispute = disputes[_disputeID]; - Round storage round = dispute.rounds[dispute.rounds.length - 1]; - IDisputeKit disputeKit = disputeKits[round.disputeKitID]; - (ruling, tied, overridden) = disputeKit.currentRuling(_disputeID); - } - - /// @dev Gets the round info for a specified dispute and round. - /// @dev This function must not be called from a non-view function because it returns a dynamic array which might be very large, theoretically exceeding the block gas limit. - /// @param _disputeID The ID of the dispute. - /// @param _round The round to get the info for. - /// @return round The round info. - function getRoundInfo(uint256 _disputeID, uint256 _round) external view returns (Round memory) { - return disputes[_disputeID].rounds[_round]; - } - - /// @dev Gets the PNK at stake per juror for a specified dispute and round. - /// @param _disputeID The ID of the dispute. - /// @param _round The round to get the info for. - /// @return pnkAtStakePerJuror The PNK at stake per juror. - function getPnkAtStakePerJuror(uint256 _disputeID, uint256 _round) external view returns (uint256) { - return disputes[_disputeID].rounds[_round].pnkAtStakePerJuror; - } - - /// @dev Gets the number of rounds for a specified dispute. - /// @param _disputeID The ID of the dispute. - /// @return The number of rounds. - function getNumberOfRounds(uint256 _disputeID) external view returns (uint256) { - return disputes[_disputeID].rounds.length; - } - - /// @dev Checks if a given dispute kit is supported by a given court. - /// @param _courtID The ID of the court to check the support for. - /// @param _disputeKitID The ID of the dispute kit to check the support for. - /// @return Whether the dispute kit is supported or not. - function isSupported(uint96 _courtID, uint256 _disputeKitID) external view returns (bool) { - return courts[_courtID].supportedDisputeKits[_disputeKitID]; - } - - /// @dev Gets the timesPerPeriod array for a given court. - /// @param _courtID The ID of the court to get the times from. - /// @return timesPerPeriod The timesPerPeriod array for the given court. - function getTimesPerPeriod(uint96 _courtID) external view returns (uint256[4] memory timesPerPeriod) { - timesPerPeriod = courts[_courtID].timesPerPeriod; - } - - // ************************************* // - // * Public Views for Dispute Kits * // - // ************************************* // - - /// @dev Gets the number of votes permitted for the specified dispute in the latest round. - /// @param _disputeID The ID of the dispute. - function getNumberOfVotes(uint256 _disputeID) external view returns (uint256) { - Dispute storage dispute = disputes[_disputeID]; - return dispute.rounds[dispute.rounds.length - 1].nbVotes; - } - - /// @dev Returns true if the dispute kit will be switched to a parent DK. - /// @param _disputeID The ID of the dispute. - /// @return Whether DK will be switched or not. - function isDisputeKitJumping(uint256 _disputeID) external view returns (bool) { - Dispute storage dispute = disputes[_disputeID]; - Round storage round = dispute.rounds[dispute.rounds.length - 1]; - Court storage court = courts[dispute.courtID]; - - if (!_isCourtJumping(round, court, _disputeID)) { - return false; - } - - // Jump if the parent court doesn't support the current DK. - return !courts[court.parent].supportedDisputeKits[round.disputeKitID]; - } - - function getDisputeKitsLength() external view returns (uint256) { - return disputeKits.length; - } - - function convertEthToTokenAmount(IERC20 _toToken, uint256 _amountInEth) public view returns (uint256) { - return (_amountInEth * 10 ** currencyRates[_toToken].rateDecimals) / currencyRates[_toToken].rateInEth; - } - - // ************************************* // - // * Internal * // - // ************************************* // - - /// @dev Returns true if the round is jumping to a parent court. - /// @param _round The round to check. - /// @param _court The court to check. - /// @return Whether the round is jumping to a parent court or not. - function _isCourtJumping( - Round storage _round, - Court storage _court, - uint256 _disputeID - ) internal view returns (bool) { - return - disputeKits[_round.disputeKitID].earlyCourtJump(_disputeID) || _round.nbVotes >= _court.jurorsForCourtJump; - } - - function _getCourtAndDisputeKitJumps( - Dispute storage _dispute, - Round storage _round, - Court storage _court, - uint256 _disputeID - ) internal view returns (uint96 newCourtID, uint256 newDisputeKitID, bool courtJump, bool disputeKitJump) { - newCourtID = _dispute.courtID; - newDisputeKitID = _round.disputeKitID; - - if (!_isCourtJumping(_round, _court, _disputeID)) return (newCourtID, newDisputeKitID, false, false); - - // Jump to parent court. - newCourtID = courts[newCourtID].parent; - courtJump = true; - - if (!courts[newCourtID].supportedDisputeKits[newDisputeKitID]) { - // The current Dispute Kit is not compatible with the new court, jump to another Dispute Kit. - newDisputeKitID = disputeKits[_round.disputeKitID].getJumpDisputeKitID(); - if (newDisputeKitID == NULL_DISPUTE_KIT || !courts[newCourtID].supportedDisputeKits[newDisputeKitID]) { - // The new Dispute Kit is not defined or still not compatible, fall back to `DisputeKitClassic` which is always supported. - newDisputeKitID = DISPUTE_KIT_CLASSIC; - } - disputeKitJump = true; - } - } - - /// @dev Internal function to transfer fee tokens (ETH or ERC20) - /// @param _feeToken The token to transfer (NATIVE_CURRENCY for ETH). - /// @param _recipient The recipient address. - /// @param _amount The amount to transfer. - function _transferFeeToken(IERC20 _feeToken, address payable _recipient, uint256 _amount) internal { - if (_feeToken == NATIVE_CURRENCY) { - _recipient.safeSend(_amount, wNative); - } else { - _feeToken.safeTransfer(_recipient, _amount); - } - } - - /// @dev Applies degree of coherence to an amount - /// @param _amount The base amount to apply coherence to. - /// @param _coherence The degree of coherence in basis points. - /// @return The amount after applying the degree of coherence. - function _applyCoherence(uint256 _amount, uint256 _coherence) internal pure returns (uint256) { - return (_amount * _coherence) / ONE_BASIS_POINT; - } - - /// @dev Calculates PNK at stake per juror based on court parameters - /// @param _minStake The minimum stake for the court. - /// @param _alpha The alpha parameter for the court in basis points. - /// @return The amount of PNK at stake per juror. - function _calculatePnkAtStake(uint256 _minStake, uint256 _alpha) internal pure returns (uint256) { - return (_minStake * _alpha) / ONE_BASIS_POINT; - } - - /// @dev Toggles the dispute kit support for a given court. - /// @param _courtID The ID of the court to toggle the support for. - /// @param _disputeKitID The ID of the dispute kit to toggle the support for. - /// @param _enable Whether to enable or disable the support. Note that classic dispute kit should always be enabled. - function _enableDisputeKit(uint96 _courtID, uint256 _disputeKitID, bool _enable) internal { - courts[_courtID].supportedDisputeKits[_disputeKitID] = _enable; - emit DisputeKitEnabled(_courtID, _disputeKitID, _enable); - } - - /// @dev If called only once then set _onError to Revert, otherwise set it to Return - /// @param _account The account to set the stake for. - /// @param _courtID The ID of the court to set the stake for. - /// @param _newStake The new stake. - /// @param _noDelay True if the stake change should not be delayed. - /// @param _onError Whether to revert or return false on error. - /// @return Whether the stake was successfully set or not. - function _setStake( - address _account, - uint96 _courtID, - uint256 _newStake, - bool _noDelay, - OnError _onError - ) internal returns (bool) { - if (_courtID == FORKING_COURT || _courtID >= courts.length) { - _stakingFailed(_onError, StakingResult.CannotStakeInThisCourt); // Staking directly into the forking court is not allowed. - return false; - } - if (_newStake != 0 && _newStake < courts[_courtID].minStake) { - _stakingFailed(_onError, StakingResult.CannotStakeLessThanMinStake); // Staking less than the minimum stake is not allowed. - return false; - } - (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) = sortitionModule.validateStake( - _account, - _courtID, - _newStake, - _noDelay - ); - if (stakingResult != StakingResult.Successful && stakingResult != StakingResult.Delayed) { - _stakingFailed(_onError, stakingResult); - return false; - } else if (stakingResult == StakingResult.Delayed) { - return true; - } - if (pnkDeposit > 0) { - if (!pinakion.safeTransferFrom(_account, address(this), pnkDeposit)) { - _stakingFailed(_onError, StakingResult.StakingTransferFailed); - return false; - } - } - if (pnkWithdrawal > 0) { - if (!pinakion.safeTransfer(_account, pnkWithdrawal)) { - _stakingFailed(_onError, StakingResult.UnstakingTransferFailed); - return false; - } - } - sortitionModule.setStake(_account, _courtID, pnkDeposit, pnkWithdrawal, _newStake); - - return true; - } - - /// @dev It may revert depending on the _onError parameter. - function _stakingFailed(OnError _onError, StakingResult _result) internal pure virtual { - if (_onError == OnError.Return) return; - if (_result == StakingResult.StakingTransferFailed) revert StakingTransferFailed(); - if (_result == StakingResult.UnstakingTransferFailed) revert UnstakingTransferFailed(); - if (_result == StakingResult.CannotStakeInMoreCourts) revert StakingInTooManyCourts(); - if (_result == StakingResult.CannotStakeInThisCourt) revert StakingNotPossibleInThisCourt(); - if (_result == StakingResult.CannotStakeLessThanMinStake) revert StakingLessThanCourtMinStake(); - if (_result == StakingResult.CannotStakeZeroWhenNoStake) revert StakingZeroWhenNoStake(); - } - - /// @dev Gets a court ID, the minimum number of jurors and an ID of a dispute kit from a specified extra data bytes array. - /// Note that if extradata contains an incorrect value then this value will be switched to default. - /// @param _extraData The extra data bytes array. The first 32 bytes are the court ID, the next are the minimum number of jurors and the last are the dispute kit ID. - /// @return courtID The court ID. - /// @return minJurors The minimum number of jurors required. - /// @return disputeKitID The ID of the dispute kit. - function _extraDataToCourtIDMinJurorsDisputeKit( - bytes memory _extraData - ) internal view returns (uint96 courtID, uint256 minJurors, uint256 disputeKitID) { - // Note that if the extradata doesn't contain 32 bytes for the dispute kit ID it'll return the default 0 index. - if (_extraData.length >= 64) { - assembly { - // solium-disable-line security/no-inline-assembly - courtID := mload(add(_extraData, 0x20)) - minJurors := mload(add(_extraData, 0x40)) - disputeKitID := mload(add(_extraData, 0x60)) - } - if (courtID == FORKING_COURT || courtID >= courts.length) { - courtID = GENERAL_COURT; - } - if (minJurors == 0) { - minJurors = DEFAULT_NB_OF_JURORS; - } - if (disputeKitID == NULL_DISPUTE_KIT || disputeKitID >= disputeKits.length) { - disputeKitID = DISPUTE_KIT_CLASSIC; // 0 index is not used. - } - } else { - courtID = GENERAL_COURT; - minJurors = DEFAULT_NB_OF_JURORS; - disputeKitID = DISPUTE_KIT_CLASSIC; - } - } - - // ************************************* // - // * Errors * // - // ************************************* // - - error OwnerOnly(); - error GuardianOrOwnerOnly(); - error DisputeKitOnly(); - error SortitionModuleOnly(); - error UnsuccessfulCall(); - error InvalidDisputKitParent(); - error MinStakeLowerThanParentCourt(); - error UnsupportedDisputeKit(); - error InvalidForkingCourtAsParent(); - error WrongDisputeKitIndex(); - error CannotDisableClassicDK(); - error StakingInTooManyCourts(); - error StakingNotPossibleInThisCourt(); - error StakingLessThanCourtMinStake(); - error StakingTransferFailed(); - error UnstakingTransferFailed(); - error ArbitrationFeesNotEnough(); - error DisputeKitNotSupportedByCourt(); - error MustSupportDisputeKitClassic(); - error TokenNotAccepted(); - error EvidenceNotPassedAndNotAppeal(); - error DisputeStillDrawing(); - error CommitPeriodNotPassed(); - error VotePeriodNotPassed(); - error AppealPeriodNotPassed(); - error NotEvidencePeriod(); - error AppealFeesNotEnough(); - error DisputeNotAppealable(); - error NotExecutionPeriod(); - error RulingAlreadyExecuted(); - error DisputePeriodIsFinal(); - error TransferFailed(); - error WhenNotPausedOnly(); - error WhenPausedOnly(); - error StakingZeroWhenNoStake(); -} diff --git a/contracts/src/arbitration/KlerosCoreNeo.sol b/contracts/src/arbitration/KlerosCoreNeo.sol deleted file mode 100644 index 6f43132d1..000000000 --- a/contracts/src/arbitration/KlerosCoreNeo.sol +++ /dev/null @@ -1,144 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.24; - -import {KlerosCoreBase, IDisputeKit, ISortitionModule, IERC20, OnError, StakingResult} from "./KlerosCoreBase.sol"; -import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; - -/// @title KlerosCoreNeo -/// Core arbitrator contract for Kleros v2. -/// Note that this contract trusts the PNK token, the dispute kit and the sortition module contracts. -contract KlerosCoreNeo is KlerosCoreBase { - string public constant override version = "2.0.0"; - - // ************************************* // - // * Storage * // - // ************************************* // - - mapping(address => bool) public arbitrableWhitelist; // Arbitrable whitelist. - bool public arbitrableWhitelistEnabled; // Whether the arbitrable whitelist is enabled. - IERC721 public jurorNft; // Eligible jurors NFT. - - // ************************************* // - // * Constructor * // - // ************************************* // - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() { - _disableInitializers(); - } - - /// @dev Initializer (constructor equivalent for upgradable contracts). - /// @param _owner The owner's address. - /// @param _guardian The guardian's address. - /// @param _pinakion The address of the token contract. - /// @param _jurorProsecutionModule The address of the juror prosecution module. - /// @param _disputeKit The address of the default dispute kit. - /// @param _hiddenVotes The `hiddenVotes` property value of the general court. - /// @param _courtParameters Numeric parameters of General court (minStake, alpha, feeForJuror and jurorsForCourtJump respectively). - /// @param _timesPerPeriod The `timesPerPeriod` property value of the general court. - /// @param _sortitionExtraData The extra data for sortition module. - /// @param _sortitionModuleAddress The sortition module responsible for sortition of the jurors. - /// @param _jurorNft NFT contract to vet the jurors. - /// @param _wNative The wrapped native token address, typically wETH. - function initialize( - address _owner, - address _guardian, - IERC20 _pinakion, - address _jurorProsecutionModule, - IDisputeKit _disputeKit, - bool _hiddenVotes, - uint256[4] memory _courtParameters, - uint256[4] memory _timesPerPeriod, - bytes memory _sortitionExtraData, - ISortitionModule _sortitionModuleAddress, - IERC721 _jurorNft, - address _wNative - ) external reinitializer(2) { - __KlerosCoreBase_initialize( - _owner, - _guardian, - _pinakion, - _jurorProsecutionModule, - _disputeKit, - _hiddenVotes, - _courtParameters, - _timesPerPeriod, - _sortitionExtraData, - _sortitionModuleAddress, - _wNative - ); - jurorNft = _jurorNft; - } - - // ************************************* // - // * Governance * // - // ************************************* // - - /// @dev Access Control to perform implementation upgrades (UUPS Proxiable) - /// Only the owner can perform upgrades (`onlyByOwner`) - function _authorizeUpgrade(address) internal view override onlyByOwner { - // NOP - } - - /// @dev Changes the `jurorNft` storage variable. - /// @param _jurorNft The new value for the `jurorNft` storage variable. - function changeJurorNft(IERC721 _jurorNft) external onlyByOwner { - jurorNft = _jurorNft; - } - - /// @dev Adds or removes an arbitrable from whitelist. - /// @param _arbitrable Arbitrable address. - /// @param _allowed Whether add or remove permission. - function changeArbitrableWhitelist(address _arbitrable, bool _allowed) external onlyByOwner { - arbitrableWhitelist[_arbitrable] = _allowed; - } - - /// @dev Enables or disables the arbitrable whitelist. - function changeArbitrableWhitelistEnabled(bool _enabled) external onlyByOwner { - arbitrableWhitelistEnabled = _enabled; - } - - // ************************************* // - // * State Modifiers * // - // ************************************* // - - /// @dev Sets the caller's stake in a court. - /// Note: Staking and unstaking is forbidden during pause. - /// @param _courtID The ID of the court. - /// @param _newStake The new stake. - /// Note that the existing delayed stake will be nullified as non-relevant. - function setStake(uint96 _courtID, uint256 _newStake) external override whenNotPaused { - if (address(jurorNft) != address(0) && jurorNft.balanceOf(msg.sender) == 0) revert NotEligibleForStaking(); - super._setStake(msg.sender, _courtID, _newStake, false, OnError.Revert); - } - - // ************************************* // - // * Internal * // - // ************************************* // - - function _createDispute( - uint256 _numberOfChoices, - bytes memory _extraData, - IERC20 _feeToken, - uint256 _feeAmount - ) internal override returns (uint256 disputeID) { - if (arbitrableWhitelistEnabled && !arbitrableWhitelist[msg.sender]) revert ArbitrableNotWhitelisted(); - return super._createDispute(_numberOfChoices, _extraData, _feeToken, _feeAmount); - } - - function _stakingFailed(OnError _onError, StakingResult _result) internal pure override { - super._stakingFailed(_onError, _result); - if (_result == StakingResult.CannotStakeMoreThanMaxStakePerJuror) revert StakingMoreThanMaxStakePerJuror(); - if (_result == StakingResult.CannotStakeMoreThanMaxTotalStaked) revert StakingMoreThanMaxTotalStaked(); - } - - // ************************************* // - // * Errors * // - // ************************************* // - - error NotEligibleForStaking(); - error StakingMoreThanMaxStakePerJuror(); - error StakingMoreThanMaxTotalStaked(); - error ArbitrableNotWhitelisted(); -} diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol index 369789ab1..5dc1c3522 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.24; -import {KlerosCore, KlerosCoreBase, IDisputeKit, ISortitionModule} from "../KlerosCore.sol"; +import {KlerosCore, IDisputeKit, ISortitionModule} from "../KlerosCore.sol"; import {Initializable} from "../../proxy/Initializable.sol"; import {UUPSProxiable} from "../../proxy/UUPSProxiable.sol"; import {SafeSend} from "../../libraries/SafeSend.sol"; @@ -273,7 +273,7 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi bytes32 _commit ) internal notJumped(_coreDisputeID) { (, , KlerosCore.Period period, , ) = core.disputes(_coreDisputeID); - if (period != KlerosCoreBase.Period.commit) revert NotCommitPeriod(); + if (period != KlerosCore.Period.commit) revert NotCommitPeriod(); if (_commit == bytes32(0)) revert EmptyCommit(); if (!coreDisputeIDToActive[_coreDisputeID]) revert NotActiveForCoreDisputeID(); @@ -314,7 +314,7 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi address _juror ) internal notJumped(_coreDisputeID) { (, , KlerosCore.Period period, , ) = core.disputes(_coreDisputeID); - if (period != KlerosCoreBase.Period.vote) revert NotVotePeriod(); + if (period != KlerosCore.Period.vote) revert NotVotePeriod(); if (_voteIDs.length == 0) revert EmptyVoteIDs(); if (!coreDisputeIDToActive[_coreDisputeID]) revert NotActiveForCoreDisputeID(); @@ -517,7 +517,7 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi ruling = tied ? 0 : round.winningChoice; (, , KlerosCore.Period period, , ) = core.disputes(_coreDisputeID); // Override the final ruling if only one side funded the appeals. - if (period == KlerosCoreBase.Period.execution) { + if (period == KlerosCore.Period.execution) { uint256[] memory fundedChoices = getFundedChoices(_coreDisputeID); if (fundedChoices.length == 1) { ruling = fundedChoices[0]; diff --git a/contracts/src/proxy/KlerosProxies.sol b/contracts/src/proxy/KlerosProxies.sol index 57b521ffd..7db79b67c 100644 --- a/contracts/src/proxy/KlerosProxies.sol +++ b/contracts/src/proxy/KlerosProxies.sol @@ -7,10 +7,6 @@ import "./UUPSProxy.sol"; /// Workaround to get meaningful names for the proxy contracts /// Otherwise all the contracts are called `UUPSProxy` on the chain explorers -contract DisputeKitClassicNeoProxy is UUPSProxy { - constructor(address _implementation, bytes memory _data) UUPSProxy(_implementation, _data) {} -} - contract DisputeKitClassicUniversityProxy is UUPSProxy { constructor(address _implementation, bytes memory _data) UUPSProxy(_implementation, _data) {} } @@ -51,10 +47,6 @@ contract HomeGatewayToEthereumProxy is UUPSProxy { constructor(address _implementation, bytes memory _data) UUPSProxy(_implementation, _data) {} } -contract KlerosCoreNeoProxy is UUPSProxy { - constructor(address _implementation, bytes memory _data) UUPSProxy(_implementation, _data) {} -} - contract KlerosCoreRulerProxy is UUPSProxy { constructor(address _implementation, bytes memory _data) UUPSProxy(_implementation, _data) {} } @@ -75,10 +67,6 @@ contract RandomizerRNGProxy is UUPSProxy { constructor(address _implementation, bytes memory _data) UUPSProxy(_implementation, _data) {} } -contract SortitionModuleNeoProxy is UUPSProxy { - constructor(address _implementation, bytes memory _data) UUPSProxy(_implementation, _data) {} -} - contract SortitionModuleUniversityProxy is UUPSProxy { constructor(address _implementation, bytes memory _data) UUPSProxy(_implementation, _data) {} } diff --git a/contracts/test/foundry/KlerosCore_Appeals.t.sol b/contracts/test/foundry/KlerosCore_Appeals.t.sol index 6421cb119..c5016f23c 100644 --- a/contracts/test/foundry/KlerosCore_Appeals.t.sol +++ b/contracts/test/foundry/KlerosCore_Appeals.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.24; import {KlerosCore_TestBase} from "./KlerosCore_TestBase.sol"; -import {KlerosCoreBase} from "../../src/arbitration/KlerosCoreBase.sol"; +import {KlerosCore} from "../../src/arbitration/KlerosCore.sol"; import {DisputeKitClassic, DisputeKitClassicBase} from "../../src/arbitration/dispute-kits/DisputeKitClassic.sol"; import {UUPSProxy} from "../../src/proxy/UUPSProxy.sol"; import "../../src/libraries/Constants.sol"; @@ -42,34 +42,34 @@ contract KlerosCore_AppealsTest is KlerosCore_TestBase { // Simulate the call from dispute kit to check the requires unrelated to caller vm.prank(address(disputeKit)); - vm.expectRevert(KlerosCoreBase.DisputeNotAppealable.selector); + vm.expectRevert(KlerosCore.DisputeNotAppealable.selector); core.appeal{value: 0.21 ether}(disputeID, 2, arbitratorExtraData); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.AppealPossible(disputeID, arbitrable); + emit KlerosCore.AppealPossible(disputeID, arbitrable); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.appeal); + emit KlerosCore.NewPeriod(disputeID, KlerosCore.Period.appeal); core.passPeriod(disputeID); - (, , KlerosCoreBase.Period period, , uint256 lastPeriodChange) = core.disputes(disputeID); + (, , KlerosCore.Period period, , uint256 lastPeriodChange) = core.disputes(disputeID); (start, end) = core.appealPeriod(0); - assertEq(uint256(period), uint256(KlerosCoreBase.Period.appeal), "Wrong period"); + assertEq(uint256(period), uint256(KlerosCore.Period.appeal), "Wrong period"); assertEq(lastPeriodChange, block.timestamp, "Wrong lastPeriodChange"); assertEq(core.appealCost(0), 0.21 ether, "Wrong appealCost"); assertEq(start, lastPeriodChange, "Appeal period start is incorrect"); assertEq(end, lastPeriodChange + timesPerPeriod[3], "Appeal period end is incorrect"); - vm.expectRevert(KlerosCoreBase.AppealPeriodNotPassed.selector); + vm.expectRevert(KlerosCore.AppealPeriodNotPassed.selector); core.passPeriod(disputeID); // Simulate the call from dispute kit to check the requires unrelated to caller vm.prank(address(disputeKit)); - vm.expectRevert(KlerosCoreBase.AppealFeesNotEnough.selector); + vm.expectRevert(KlerosCore.AppealFeesNotEnough.selector); core.appeal{value: 0.21 ether - 1}(disputeID, 2, arbitratorExtraData); vm.deal(address(disputeKit), 0); // Nullify the balance so it doesn't get in the way. vm.prank(staker1); - vm.expectRevert(KlerosCoreBase.DisputeKitOnly.selector); + vm.expectRevert(KlerosCore.DisputeKitOnly.selector); core.appeal{value: 0.21 ether}(disputeID, 2, arbitratorExtraData); vm.prank(crowdfunder1); @@ -177,9 +177,9 @@ contract KlerosCore_AppealsTest is KlerosCore_TestBase { vm.prank(crowdfunder2); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.AppealDecision(disputeID, arbitrable); + emit KlerosCore.AppealDecision(disputeID, arbitrable); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.evidence); + emit KlerosCore.NewPeriod(disputeID, KlerosCore.Period.evidence); disputeKit.fundAppeal{value: 0.42 ether}(disputeID, 2); assertEq((disputeKit.getFundedChoices(disputeID)).length, 0, "No funded choices in the fresh round"); @@ -194,17 +194,17 @@ contract KlerosCore_AppealsTest is KlerosCore_TestBase { assertEq(sortitionModule.disputesWithoutJurors(), 1, "Wrong disputesWithoutJurors count after appeal"); assertEq(core.getNumberOfRounds(disputeID), 2, "Wrong number of rounds"); - (, , KlerosCoreBase.Period period, , uint256 lastPeriodChange) = core.disputes(disputeID); - assertEq(uint256(period), uint256(KlerosCoreBase.Period.evidence), "Wrong period"); + (, , KlerosCore.Period period, , uint256 lastPeriodChange) = core.disputes(disputeID); + assertEq(uint256(period), uint256(KlerosCore.Period.evidence), "Wrong period"); assertEq(lastPeriodChange, block.timestamp, "Wrong lastPeriodChange"); - KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 1); // Check the new round + KlerosCore.Round memory round = core.getRoundInfo(disputeID, 1); // Check the new round assertEq(round.pnkAtStakePerJuror, 1000, "Wrong pnkAtStakePerJuror"); assertEq(round.totalFeesForJurors, 0.21 ether, "Wrong totalFeesForJurors"); assertEq(round.nbVotes, 7, "Wrong nbVotes"); core.draw(disputeID, 7); - emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.vote); // Check that we don't have to wait for the timeout to pass the evidence period after appeal + emit KlerosCore.NewPeriod(disputeID, KlerosCore.Period.vote); // Check that we don't have to wait for the timeout to pass the evidence period after appeal core.passPeriod(disputeID); } @@ -263,7 +263,7 @@ contract KlerosCore_AppealsTest is KlerosCore_TestBase { vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase - KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 0); + KlerosCore.Round memory round = core.getRoundInfo(disputeID, 0); assertEq(round.disputeKitID, newDkID, "Wrong DK ID"); core.draw(disputeID, DEFAULT_NB_OF_JURORS); @@ -286,15 +286,15 @@ contract KlerosCore_AppealsTest is KlerosCore_TestBase { assertEq(core.isDisputeKitJumping(disputeID), true, "Should be jumping"); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.CourtJump(disputeID, 1, newCourtID, GENERAL_COURT); + emit KlerosCore.CourtJump(disputeID, 1, newCourtID, GENERAL_COURT); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.DisputeKitJump(disputeID, 1, newDkID, DISPUTE_KIT_CLASSIC); + emit KlerosCore.DisputeKitJump(disputeID, 1, newDkID, DISPUTE_KIT_CLASSIC); vm.expectEmit(true, true, true, true); emit DisputeKitClassicBase.DisputeCreation(disputeID, 2, newExtraData); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.AppealDecision(disputeID, arbitrable); + emit KlerosCore.AppealDecision(disputeID, arbitrable); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.evidence); + emit KlerosCore.NewPeriod(disputeID, KlerosCore.Period.evidence); vm.prank(crowdfunder2); newDisputeKit.fundAppeal{value: 0.42 ether}(disputeID, 2); @@ -322,7 +322,7 @@ contract KlerosCore_AppealsTest is KlerosCore_TestBase { // And check that draw in the new round works vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.Draw(staker1, disputeID, 1, 0); // roundID = 1 VoteID = 0 + emit KlerosCore.Draw(staker1, disputeID, 1, 0); // roundID = 1 VoteID = 0 core.draw(disputeID, 1); (address account, , , ) = disputeKit.getVoteInfo(disputeID, 1, 0); @@ -401,7 +401,7 @@ contract KlerosCore_AppealsTest is KlerosCore_TestBase { vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase - KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 0); + KlerosCore.Round memory round = core.getRoundInfo(disputeID, 0); assertEq(round.disputeKitID, dkID3, "Wrong DK ID"); core.draw(disputeID, DEFAULT_NB_OF_JURORS); @@ -424,15 +424,15 @@ contract KlerosCore_AppealsTest is KlerosCore_TestBase { assertEq(core.isDisputeKitJumping(disputeID), true, "Should be jumping"); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.CourtJump(disputeID, 1, newCourtID, GENERAL_COURT); + emit KlerosCore.CourtJump(disputeID, 1, newCourtID, GENERAL_COURT); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.DisputeKitJump(disputeID, 1, dkID3, dkID2); + emit KlerosCore.DisputeKitJump(disputeID, 1, dkID3, dkID2); vm.expectEmit(true, true, true, true); emit DisputeKitClassicBase.DisputeCreation(disputeID, 2, newExtraData); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.AppealDecision(disputeID, arbitrable); + emit KlerosCore.AppealDecision(disputeID, arbitrable); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.evidence); + emit KlerosCore.NewPeriod(disputeID, KlerosCore.Period.evidence); vm.prank(crowdfunder2); disputeKit3.fundAppeal{value: 0.42 ether}(disputeID, 2); @@ -460,7 +460,7 @@ contract KlerosCore_AppealsTest is KlerosCore_TestBase { // And check that draw in the new round works vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.Draw(staker1, disputeID, 1, 0); // roundID = 1 VoteID = 0 + emit KlerosCore.Draw(staker1, disputeID, 1, 0); // roundID = 1 VoteID = 0 core.draw(disputeID, 1); (address account, , , ) = disputeKit2.getVoteInfo(disputeID, 1, 0); @@ -497,7 +497,7 @@ contract KlerosCore_AppealsTest is KlerosCore_TestBase { // Should pass to execution period without waiting for the 2nd half of the appeal. vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.execution); + emit KlerosCore.NewPeriod(disputeID, KlerosCore.Period.execution); core.passPeriod(disputeID); } } diff --git a/contracts/test/foundry/KlerosCore_Disputes.t.sol b/contracts/test/foundry/KlerosCore_Disputes.t.sol index 954789f92..f9840014d 100644 --- a/contracts/test/foundry/KlerosCore_Disputes.t.sol +++ b/contracts/test/foundry/KlerosCore_Disputes.t.sol @@ -2,8 +2,8 @@ pragma solidity ^0.8.24; import {KlerosCore_TestBase} from "./KlerosCore_TestBase.sol"; -import {KlerosCoreBase} from "../../src/arbitration/KlerosCoreBase.sol"; -import {IArbitratorV2} from "../../src/arbitration/KlerosCoreBase.sol"; +import {KlerosCore} from "../../src/arbitration/KlerosCore.sol"; +import {IArbitratorV2} from "../../src/arbitration/KlerosCore.sol"; import {DisputeKitClassicBase} from "../../src/arbitration/dispute-kits/DisputeKitClassicBase.sol"; import {IArbitrableV2} from "../../src/arbitration/arbitrables/ArbitrableExample.sol"; import "../../src/libraries/Constants.sol"; @@ -38,11 +38,11 @@ contract KlerosCore_DisputesTest is KlerosCore_TestBase { arbitrable.changeArbitratorExtraData(newExtraData); - vm.expectRevert(KlerosCoreBase.ArbitrationFeesNotEnough.selector); + vm.expectRevert(KlerosCore.ArbitrationFeesNotEnough.selector); vm.prank(disputer); arbitrable.createDispute{value: newFee * newNbJurors - 1}("Action"); - vm.expectRevert(KlerosCoreBase.DisputeKitNotSupportedByCourt.selector); + vm.expectRevert(KlerosCore.DisputeKitNotSupportedByCourt.selector); vm.prank(disputer); arbitrable.createDispute{value: 0.04 ether}("Action"); @@ -64,18 +64,18 @@ contract KlerosCore_DisputesTest is KlerosCore_TestBase { ( uint96 courtID, IArbitrableV2 arbitrated, - KlerosCoreBase.Period period, + KlerosCore.Period period, bool ruled, uint256 lastPeriodChange ) = core.disputes(disputeID); assertEq(courtID, newCourtID, "Wrong court ID"); assertEq(address(arbitrated), address(arbitrable), "Wrong arbitrable"); - assertEq(uint256(period), uint256(KlerosCoreBase.Period.evidence), "Wrong period"); + assertEq(uint256(period), uint256(KlerosCore.Period.evidence), "Wrong period"); assertEq(ruled, false, "Should not be ruled"); assertEq(lastPeriodChange, block.timestamp, "Wrong lastPeriodChange"); - KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 0); + KlerosCore.Round memory round = core.getRoundInfo(disputeID, 0); assertEq(round.disputeKitID, newDkID, "Wrong DK ID"); assertEq(round.pnkAtStakePerJuror, 4000, "Wrong pnkAtStakePerJuror"); // minStake * alpha / divisor = 2000 * 20000/10000 assertEq(round.totalFeesForJurors, 0.04 ether, "Wrong totalFeesForJurors"); @@ -116,7 +116,7 @@ contract KlerosCore_DisputesTest is KlerosCore_TestBase { vm.prank(disputer); feeToken.approve(address(arbitrable), 1 ether); - vm.expectRevert(KlerosCoreBase.TokenNotAccepted.selector); + vm.expectRevert(KlerosCore.TokenNotAccepted.selector); vm.prank(disputer); arbitrable.createDispute("Action", 0.18 ether); @@ -125,11 +125,11 @@ contract KlerosCore_DisputesTest is KlerosCore_TestBase { vm.prank(owner); core.changeCurrencyRates(feeToken, 500, 3); - vm.expectRevert(KlerosCoreBase.ArbitrationFeesNotEnough.selector); + vm.expectRevert(KlerosCore.ArbitrationFeesNotEnough.selector); vm.prank(disputer); arbitrable.createDispute("Action", 0.18 ether - 1); - vm.expectRevert(KlerosCoreBase.TransferFailed.selector); + vm.expectRevert(KlerosCore.TransferFailed.selector); vm.prank(address(arbitrable)); // Bypass createDispute in arbitrable to avoid transfer checks there and make the arbitrable call KC directly core.createDispute(2, arbitratorExtraData, feeToken, 0.18 ether); @@ -137,7 +137,7 @@ contract KlerosCore_DisputesTest is KlerosCore_TestBase { vm.prank(disputer); arbitrable.createDispute("Action", 0.18 ether); - KlerosCoreBase.Round memory round = core.getRoundInfo(0, 0); + KlerosCore.Round memory round = core.getRoundInfo(0, 0); assertEq(round.totalFeesForJurors, 0.18 ether, "Wrong totalFeesForJurors"); assertEq(round.nbVotes, 3, "Wrong nbVotes"); assertEq(address(round.feeToken), address(feeToken), "Wrong feeToken"); diff --git a/contracts/test/foundry/KlerosCore_Drawing.t.sol b/contracts/test/foundry/KlerosCore_Drawing.t.sol index d6ffc9ea9..b8c644392 100644 --- a/contracts/test/foundry/KlerosCore_Drawing.t.sol +++ b/contracts/test/foundry/KlerosCore_Drawing.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.24; import {KlerosCore_TestBase} from "./KlerosCore_TestBase.sol"; -import {KlerosCoreBase} from "../../src/arbitration/KlerosCoreBase.sol"; +import {KlerosCore} from "../../src/arbitration/KlerosCore.sol"; import {SortitionModuleBase} from "../../src/arbitration/SortitionModuleBase.sol"; import {ISortitionModule} from "../../src/arbitration/interfaces/ISortitionModule.sol"; import "../../src/libraries/Constants.sol"; @@ -26,7 +26,7 @@ contract KlerosCore_DrawingTest is KlerosCore_TestBase { vm.expectEmit(true, true, true, true); emit SortitionModuleBase.StakeLocked(staker1, 1000, false); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.Draw(staker1, disputeID, roundID, 0); // VoteID = 0 + emit KlerosCore.Draw(staker1, disputeID, roundID, 0); // VoteID = 0 core.draw(disputeID, DEFAULT_NB_OF_JURORS); // Do 3 iterations and see that the juror will get drawn 3 times despite low stake. @@ -61,7 +61,7 @@ contract KlerosCore_DrawingTest is KlerosCore_TestBase { core.draw(disputeID, DEFAULT_NB_OF_JURORS); // No one is staked so check that the empty addresses are not drawn. - KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, roundID); + KlerosCore.Round memory round = core.getRoundInfo(disputeID, roundID); assertEq(round.drawIterations, 3, "Wrong drawIterations number"); (, , , , uint256 nbVoters, ) = disputeKit.getRoundInfo(disputeID, roundID, 0); @@ -106,16 +106,16 @@ contract KlerosCore_DrawingTest is KlerosCore_TestBase { assertEq(courtID, GENERAL_COURT, "Wrong court ID of the dispute"); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.Draw(staker1, disputeID, roundID, 0); + emit KlerosCore.Draw(staker1, disputeID, roundID, 0); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.Draw(staker1, disputeID, roundID, 1); + emit KlerosCore.Draw(staker1, disputeID, roundID, 1); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.Draw(staker1, disputeID, roundID, 2); + emit KlerosCore.Draw(staker1, disputeID, roundID, 2); core.draw(disputeID, DEFAULT_NB_OF_JURORS); assertEq(sortitionModule.disputesWithoutJurors(), 0, "Wrong disputesWithoutJurors count"); - KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, roundID); + KlerosCore.Round memory round = core.getRoundInfo(disputeID, roundID); assertEq(round.drawIterations, 3, "Wrong drawIterations number"); (, , , , uint256 nbVoters, ) = disputeKit.getRoundInfo(disputeID, roundID, 0); diff --git a/contracts/test/foundry/KlerosCore_Execution.t.sol b/contracts/test/foundry/KlerosCore_Execution.t.sol index c5763964e..33b2cda65 100644 --- a/contracts/test/foundry/KlerosCore_Execution.t.sol +++ b/contracts/test/foundry/KlerosCore_Execution.t.sol @@ -2,10 +2,10 @@ pragma solidity ^0.8.24; import {KlerosCore_TestBase} from "./KlerosCore_TestBase.sol"; -import {KlerosCoreBase} from "../../src/arbitration/KlerosCoreBase.sol"; +import {KlerosCore} from "../../src/arbitration/KlerosCore.sol"; import {SortitionModuleBase} from "../../src/arbitration/SortitionModuleBase.sol"; import {DisputeKitClassicBase} from "../../src/arbitration/dispute-kits/DisputeKitClassicBase.sol"; -import {IArbitratorV2, IArbitrableV2} from "../../src/arbitration/KlerosCoreBase.sol"; +import {IArbitratorV2, IArbitrableV2} from "../../src/arbitration/KlerosCore.sol"; import {IERC20} from "../../src/libraries/SafeERC20.sol"; import "../../src/libraries/Constants.sol"; @@ -53,7 +53,7 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); core.passPeriod(disputeID); // Appeal - vm.expectRevert(KlerosCoreBase.NotExecutionPeriod.selector); + vm.expectRevert(KlerosCore.NotExecutionPeriod.selector); core.execute(disputeID, 0, 1); vm.warp(block.timestamp + timesPerPeriod[3]); @@ -61,7 +61,7 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { vm.prank(owner); core.pause(); - vm.expectRevert(KlerosCoreBase.WhenNotPausedOnly.selector); + vm.expectRevert(KlerosCore.WhenNotPausedOnly.selector); core.execute(disputeID, 0, 1); vm.prank(owner); core.unpause(); @@ -98,16 +98,16 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { vm.expectEmit(true, true, true, true); emit SortitionModuleBase.StakeLocked(staker1, 1000, true); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.TokenAndETHShift(staker1, disputeID, 0, 0, -int256(1000), 0, IERC20(address(0))); + emit KlerosCore.TokenAndETHShift(staker1, disputeID, 0, 0, -int256(1000), 0, IERC20(address(0))); // Check iterations for the winning staker to see the shifts vm.expectEmit(true, true, true, true); emit SortitionModuleBase.StakeLocked(staker2, 0, true); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.TokenAndETHShift(staker2, disputeID, 0, 10000, 0, 0, IERC20(address(0))); + emit KlerosCore.TokenAndETHShift(staker2, disputeID, 0, 10000, 0, 0, IERC20(address(0))); vm.expectEmit(true, true, true, true); emit SortitionModuleBase.StakeLocked(staker2, 0, true); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.TokenAndETHShift(staker2, disputeID, 0, 10000, 0, 0, IERC20(address(0))); + emit KlerosCore.TokenAndETHShift(staker2, disputeID, 0, 10000, 0, 0, IERC20(address(0))); core.execute(disputeID, 0, 3); // Do 3 iterations to check penalties first (uint256 totalStaked, uint256 totalLocked, , ) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); @@ -116,23 +116,23 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { (, totalLocked, , ) = sortitionModule.getJurorBalance(staker2, GENERAL_COURT); assertEq(totalLocked, 2000, "Tokens should still be locked for staker2"); - KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 0); + KlerosCore.Round memory round = core.getRoundInfo(disputeID, 0); assertEq(round.repartitions, 3, "Wrong repartitions"); assertEq(round.pnkPenalties, 1000, "Wrong pnkPenalties"); vm.expectEmit(true, true, true, true); emit SortitionModuleBase.StakeLocked(staker1, 0, true); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.TokenAndETHShift(staker1, disputeID, 0, 0, 0, 0, IERC20(address(0))); + emit KlerosCore.TokenAndETHShift(staker1, disputeID, 0, 0, 0, 0, IERC20(address(0))); // Check iterations for the winning staker to see the shifts vm.expectEmit(true, true, true, true); emit SortitionModuleBase.StakeLocked(staker2, 1000, true); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.TokenAndETHShift(staker2, disputeID, 0, 10000, 500, 0.045 ether, IERC20(address(0))); + emit KlerosCore.TokenAndETHShift(staker2, disputeID, 0, 10000, 500, 0.045 ether, IERC20(address(0))); vm.expectEmit(true, true, true, true); emit SortitionModuleBase.StakeLocked(staker2, 1000, true); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.TokenAndETHShift(staker2, disputeID, 0, 10000, 500, 0.045 ether, IERC20(address(0))); + emit KlerosCore.TokenAndETHShift(staker2, disputeID, 0, 10000, 500, 0.045 ether, IERC20(address(0))); core.execute(disputeID, 0, 10); // Finish the iterations. We need only 3 but check that it corrects the count. (, totalLocked, , ) = sortitionModule.getJurorBalance(staker2, GENERAL_COURT); @@ -201,10 +201,10 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { uint256 ownerTokenBalance = pinakion.balanceOf(owner); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.LeftoverRewardSent(disputeID, 0, 3000, 0.09 ether, IERC20(address(0))); + emit KlerosCore.LeftoverRewardSent(disputeID, 0, 3000, 0.09 ether, IERC20(address(0))); core.execute(disputeID, 0, 3); - KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 0); + KlerosCore.Round memory round = core.getRoundInfo(disputeID, 0); assertEq(round.pnkPenalties, 3000, "Wrong pnkPenalties"); assertEq(round.sumFeeRewardPaid, 0, "Wrong sumFeeRewardPaid"); assertEq(round.sumPnkRewardPaid, 0, "Wrong sumPnkRewardPaid"); @@ -471,7 +471,7 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { assertEq(totalStaked, 1000, "Wrong amount staked"); assertEq(totalLocked, 0, "Should be fully unlocked"); - KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 0); + KlerosCore.Round memory round = core.getRoundInfo(disputeID, 0); assertEq(round.pnkPenalties, 0, "Wrong pnkPenalties"); assertEq(round.sumFeeRewardPaid, 0.09 ether, "Wrong sumFeeRewardPaid"); assertEq(round.sumPnkRewardPaid, 0, "Wrong sumPnkRewardPaid"); // No penalty so no rewards in pnk @@ -480,7 +480,7 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { assertEq(pinakion.balanceOf(address(core)), 1000, "Wrong token balance of the core"); assertEq(pinakion.balanceOf(staker1), 999999999999999000, "Wrong token balance of staker1"); - vm.expectRevert(KlerosCoreBase.SortitionModuleOnly.selector); + vm.expectRevert(KlerosCore.SortitionModuleOnly.selector); vm.prank(owner); core.transferBySortitionModule(staker1, 1000); @@ -537,12 +537,12 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { // Check only once per penalty and per reward vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.TokenAndETHShift(staker1, disputeID, 0, 10000, 0, 0, feeToken); + emit KlerosCore.TokenAndETHShift(staker1, disputeID, 0, 10000, 0, 0, feeToken); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.TokenAndETHShift(staker1, disputeID, 0, 10000, 0, 0.06 ether, feeToken); + emit KlerosCore.TokenAndETHShift(staker1, disputeID, 0, 10000, 0, 0.06 ether, feeToken); core.execute(disputeID, 0, 6); - KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 0); + KlerosCore.Round memory round = core.getRoundInfo(disputeID, 0); assertEq(round.sumFeeRewardPaid, 0.18 ether, "Wrong sumFeeRewardPaid"); assertEq(feeToken.balanceOf(address(core)), 0, "Wrong fee token balance of the core"); @@ -584,10 +584,10 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { core.passPeriod(disputeID); // Execution vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.LeftoverRewardSent(disputeID, 0, 3000, 0.18 ether, feeToken); + emit KlerosCore.LeftoverRewardSent(disputeID, 0, 3000, 0.18 ether, feeToken); core.execute(disputeID, 0, 10); // Put more iterations to check that they're capped - KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 0); + KlerosCore.Round memory round = core.getRoundInfo(disputeID, 0); assertEq(round.pnkPenalties, 3000, "Wrong pnkPenalties"); assertEq(round.sumFeeRewardPaid, 0, "Wrong sumFeeRewardPaid"); assertEq(round.sumPnkRewardPaid, 0, "Wrong sumPnkRewardPaid"); @@ -624,19 +624,19 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); core.passPeriod(disputeID); // Appeal - vm.expectRevert(KlerosCoreBase.NotExecutionPeriod.selector); + vm.expectRevert(KlerosCore.NotExecutionPeriod.selector); core.executeRuling(disputeID); vm.warp(block.timestamp + timesPerPeriod[3]); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.execution); + emit KlerosCore.NewPeriod(disputeID, KlerosCore.Period.execution); core.passPeriod(disputeID); // Execution - (, , KlerosCoreBase.Period period, , uint256 lastPeriodChange) = core.disputes(disputeID); - assertEq(uint256(period), uint256(KlerosCoreBase.Period.execution), "Wrong period"); + (, , KlerosCore.Period period, , uint256 lastPeriodChange) = core.disputes(disputeID); + assertEq(uint256(period), uint256(KlerosCore.Period.execution), "Wrong period"); assertEq(lastPeriodChange, block.timestamp, "Wrong lastPeriodChange"); - vm.expectRevert(KlerosCoreBase.DisputePeriodIsFinal.selector); + vm.expectRevert(KlerosCore.DisputePeriodIsFinal.selector); core.passPeriod(disputeID); vm.expectEmit(true, true, true, true); @@ -645,7 +645,7 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { emit IArbitrableV2.Ruling(core, disputeID, 2); core.executeRuling(disputeID); - vm.expectRevert(KlerosCoreBase.RulingAlreadyExecuted.selector); + vm.expectRevert(KlerosCore.RulingAlreadyExecuted.selector); core.executeRuling(disputeID); (, , , bool ruled, ) = core.disputes(disputeID); diff --git a/contracts/test/foundry/KlerosCore_Governance.t.sol b/contracts/test/foundry/KlerosCore_Governance.t.sol index f34d64e79..53d438c4d 100644 --- a/contracts/test/foundry/KlerosCore_Governance.t.sol +++ b/contracts/test/foundry/KlerosCore_Governance.t.sol @@ -2,8 +2,8 @@ pragma solidity ^0.8.24; import {KlerosCore_TestBase} from "./KlerosCore_TestBase.sol"; -import {KlerosCoreBase} from "../../src/arbitration/KlerosCoreBase.sol"; -import {IArbitratorV2} from "../../src/arbitration/KlerosCoreBase.sol"; +import {KlerosCore} from "../../src/arbitration/KlerosCore.sol"; +import {IArbitratorV2} from "../../src/arbitration/KlerosCore.sol"; import {DisputeKitSybilResistant} from "../../src/arbitration/dispute-kits/DisputeKitSybilResistant.sol"; import {SortitionModuleMock} from "../../src/test/SortitionModuleMock.sol"; import {PNK} from "../../src/token/PNK.sol"; @@ -13,27 +13,27 @@ import "../../src/libraries/Constants.sol"; /// @dev Tests for KlerosCore governance functions (owner/guardian operations) contract KlerosCore_GovernanceTest is KlerosCore_TestBase { function test_pause() public { - vm.expectRevert(KlerosCoreBase.GuardianOrOwnerOnly.selector); + vm.expectRevert(KlerosCore.GuardianOrOwnerOnly.selector); vm.prank(other); core.pause(); // Note that we must explicitly switch to the owner/guardian address to make the call, otherwise Foundry treats UUPS proxy as msg.sender. vm.prank(guardian); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.Paused(); + emit KlerosCore.Paused(); core.pause(); assertEq(core.paused(), true, "Wrong paused value"); // Switch between owner and guardian to test both. WhenNotPausedOnly modifier is triggered after owner's check. vm.prank(owner); - vm.expectRevert(KlerosCoreBase.WhenNotPausedOnly.selector); + vm.expectRevert(KlerosCore.WhenNotPausedOnly.selector); core.pause(); } function test_unpause() public { - vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); + vm.expectRevert(KlerosCore.OwnerOnly.selector); vm.prank(other); core.unpause(); - vm.expectRevert(KlerosCoreBase.WhenPausedOnly.selector); + vm.expectRevert(KlerosCore.WhenPausedOnly.selector); vm.prank(owner); core.unpause(); @@ -41,18 +41,18 @@ contract KlerosCore_GovernanceTest is KlerosCore_TestBase { core.pause(); vm.prank(owner); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.Unpaused(); + emit KlerosCore.Unpaused(); core.unpause(); assertEq(core.paused(), false, "Wrong paused value"); } function test_executeOwnerProposal() public { bytes memory data = abi.encodeWithSignature("changeOwner(address)", other); - vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); + vm.expectRevert(KlerosCore.OwnerOnly.selector); vm.prank(other); core.executeOwnerProposal(address(core), 0, data); - vm.expectRevert(KlerosCoreBase.UnsuccessfulCall.selector); + vm.expectRevert(KlerosCore.UnsuccessfulCall.selector); vm.prank(owner); core.executeOwnerProposal(address(core), 0, data); // It'll fail because the core is not its own owner @@ -64,7 +64,7 @@ contract KlerosCore_GovernanceTest is KlerosCore_TestBase { } function test_changeOwner() public { - vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); + vm.expectRevert(KlerosCore.OwnerOnly.selector); vm.prank(other); core.changeOwner(payable(other)); vm.prank(owner); @@ -73,7 +73,7 @@ contract KlerosCore_GovernanceTest is KlerosCore_TestBase { } function test_changeGuardian() public { - vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); + vm.expectRevert(KlerosCore.OwnerOnly.selector); vm.prank(other); core.changeGuardian(other); vm.prank(owner); @@ -83,7 +83,7 @@ contract KlerosCore_GovernanceTest is KlerosCore_TestBase { function test_changePinakion() public { PNK fakePNK = new PNK(); - vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); + vm.expectRevert(KlerosCore.OwnerOnly.selector); vm.prank(other); core.changePinakion(fakePNK); vm.prank(owner); @@ -92,7 +92,7 @@ contract KlerosCore_GovernanceTest is KlerosCore_TestBase { } function test_changeJurorProsecutionModule() public { - vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); + vm.expectRevert(KlerosCore.OwnerOnly.selector); vm.prank(other); core.changeJurorProsecutionModule(other); vm.prank(owner); @@ -102,7 +102,7 @@ contract KlerosCore_GovernanceTest is KlerosCore_TestBase { function test_changeSortitionModule() public { SortitionModuleMock fakeSM = new SortitionModuleMock(); - vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); + vm.expectRevert(KlerosCore.OwnerOnly.selector); vm.prank(other); core.changeSortitionModule(fakeSM); vm.prank(owner); @@ -112,19 +112,19 @@ contract KlerosCore_GovernanceTest is KlerosCore_TestBase { function test_addNewDisputeKit() public { DisputeKitSybilResistant newDK = new DisputeKitSybilResistant(); - vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); + vm.expectRevert(KlerosCore.OwnerOnly.selector); vm.prank(other); core.addNewDisputeKit(newDK); vm.prank(owner); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.DisputeKitCreated(2, newDK); + emit KlerosCore.DisputeKitCreated(2, newDK); core.addNewDisputeKit(newDK); assertEq(address(core.disputeKits(2)), address(newDK), "Wrong address of new DK"); assertEq(core.getDisputeKitsLength(), 3, "Wrong DK array length"); } function test_createCourt() public { - vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); + vm.expectRevert(KlerosCore.OwnerOnly.selector); vm.prank(other); uint256[] memory supportedDK = new uint256[](2); supportedDK[0] = DISPUTE_KIT_CLASSIC; @@ -141,7 +141,7 @@ contract KlerosCore_GovernanceTest is KlerosCore_TestBase { supportedDK ); - vm.expectRevert(KlerosCoreBase.MinStakeLowerThanParentCourt.selector); + vm.expectRevert(KlerosCore.MinStakeLowerThanParentCourt.selector); vm.prank(owner); core.createCourt( GENERAL_COURT, @@ -155,7 +155,7 @@ contract KlerosCore_GovernanceTest is KlerosCore_TestBase { supportedDK ); - vm.expectRevert(KlerosCoreBase.UnsupportedDisputeKit.selector); + vm.expectRevert(KlerosCore.UnsupportedDisputeKit.selector); vm.prank(owner); uint256[] memory emptySupportedDK = new uint256[](0); core.createCourt( @@ -170,7 +170,7 @@ contract KlerosCore_GovernanceTest is KlerosCore_TestBase { emptySupportedDK ); - vm.expectRevert(KlerosCoreBase.InvalidForkingCourtAsParent.selector); + vm.expectRevert(KlerosCore.InvalidForkingCourtAsParent.selector); vm.prank(owner); core.createCourt( FORKING_COURT, @@ -187,7 +187,7 @@ contract KlerosCore_GovernanceTest is KlerosCore_TestBase { uint256[] memory badSupportedDK = new uint256[](2); badSupportedDK[0] = NULL_DISPUTE_KIT; // Include NULL_DK to check that it reverts badSupportedDK[1] = DISPUTE_KIT_CLASSIC; - vm.expectRevert(KlerosCoreBase.WrongDisputeKitIndex.selector); + vm.expectRevert(KlerosCore.WrongDisputeKitIndex.selector); vm.prank(owner); core.createCourt( GENERAL_COURT, @@ -203,7 +203,7 @@ contract KlerosCore_GovernanceTest is KlerosCore_TestBase { badSupportedDK[0] = DISPUTE_KIT_CLASSIC; badSupportedDK[1] = 2; // Check out of bounds index - vm.expectRevert(KlerosCoreBase.WrongDisputeKitIndex.selector); + vm.expectRevert(KlerosCore.WrongDisputeKitIndex.selector); vm.prank(owner); core.createCourt( GENERAL_COURT, @@ -223,7 +223,7 @@ contract KlerosCore_GovernanceTest is KlerosCore_TestBase { core.addNewDisputeKit(newDK); badSupportedDK = new uint256[](1); badSupportedDK[0] = 2; // Include only sybil resistant dk - vm.expectRevert(KlerosCoreBase.MustSupportDisputeKitClassic.selector); + vm.expectRevert(KlerosCore.MustSupportDisputeKitClassic.selector); vm.prank(owner); core.createCourt( GENERAL_COURT, @@ -239,11 +239,11 @@ contract KlerosCore_GovernanceTest is KlerosCore_TestBase { vm.prank(owner); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.DisputeKitEnabled(2, DISPUTE_KIT_CLASSIC, true); + emit KlerosCore.DisputeKitEnabled(2, DISPUTE_KIT_CLASSIC, true); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.DisputeKitEnabled(2, 2, true); + emit KlerosCore.DisputeKitEnabled(2, 2, true); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.CourtCreated( + emit KlerosCore.CourtCreated( 2, GENERAL_COURT, true, @@ -299,7 +299,7 @@ contract KlerosCore_GovernanceTest is KlerosCore_TestBase { supportedDK ); - vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); + vm.expectRevert(KlerosCore.OwnerOnly.selector); vm.prank(other); core.changeCourtParameters( GENERAL_COURT, @@ -310,7 +310,7 @@ contract KlerosCore_GovernanceTest is KlerosCore_TestBase { 50, // jurors for jump [uint256(10), uint256(20), uint256(30), uint256(40)] // Times per period ); - vm.expectRevert(KlerosCoreBase.MinStakeLowerThanParentCourt.selector); + vm.expectRevert(KlerosCore.MinStakeLowerThanParentCourt.selector); vm.prank(owner); // Min stake of a parent became higher than of a child core.changeCourtParameters( @@ -323,7 +323,7 @@ contract KlerosCore_GovernanceTest is KlerosCore_TestBase { [uint256(10), uint256(20), uint256(30), uint256(40)] // Times per period ); // Min stake of a child became lower than of a parent - vm.expectRevert(KlerosCoreBase.MinStakeLowerThanParentCourt.selector); + vm.expectRevert(KlerosCore.MinStakeLowerThanParentCourt.selector); vm.prank(owner); core.changeCourtParameters( newCourtID, @@ -337,7 +337,7 @@ contract KlerosCore_GovernanceTest is KlerosCore_TestBase { vm.prank(owner); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.CourtModified( + emit KlerosCore.CourtModified( GENERAL_COURT, true, 2000, @@ -366,43 +366,43 @@ contract KlerosCore_GovernanceTest is KlerosCore_TestBase { vm.prank(owner); core.addNewDisputeKit(newDK); - vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); + vm.expectRevert(KlerosCore.OwnerOnly.selector); vm.prank(other); uint256[] memory supportedDK = new uint256[](1); supportedDK[0] = newDkID; core.enableDisputeKits(GENERAL_COURT, supportedDK, true); - vm.expectRevert(KlerosCoreBase.WrongDisputeKitIndex.selector); + vm.expectRevert(KlerosCore.WrongDisputeKitIndex.selector); vm.prank(owner); supportedDK[0] = NULL_DISPUTE_KIT; core.enableDisputeKits(GENERAL_COURT, supportedDK, true); - vm.expectRevert(KlerosCoreBase.WrongDisputeKitIndex.selector); + vm.expectRevert(KlerosCore.WrongDisputeKitIndex.selector); vm.prank(owner); supportedDK[0] = 3; // Out of bounds core.enableDisputeKits(GENERAL_COURT, supportedDK, true); - vm.expectRevert(KlerosCoreBase.CannotDisableClassicDK.selector); + vm.expectRevert(KlerosCore.CannotDisableClassicDK.selector); vm.prank(owner); supportedDK[0] = DISPUTE_KIT_CLASSIC; core.enableDisputeKits(GENERAL_COURT, supportedDK, false); vm.prank(owner); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.DisputeKitEnabled(GENERAL_COURT, newDkID, true); + emit KlerosCore.DisputeKitEnabled(GENERAL_COURT, newDkID, true); supportedDK[0] = newDkID; core.enableDisputeKits(GENERAL_COURT, supportedDK, true); assertEq(core.isSupported(GENERAL_COURT, newDkID), true, "New DK should be supported by General court"); vm.prank(owner); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.DisputeKitEnabled(GENERAL_COURT, newDkID, false); + emit KlerosCore.DisputeKitEnabled(GENERAL_COURT, newDkID, false); core.enableDisputeKits(GENERAL_COURT, supportedDK, false); assertEq(core.isSupported(GENERAL_COURT, newDkID), false, "New DK should be disabled in General court"); } function test_changeAcceptedFeeTokens() public { - vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); + vm.expectRevert(KlerosCore.OwnerOnly.selector); vm.prank(other); core.changeAcceptedFeeTokens(feeToken, true); @@ -418,7 +418,7 @@ contract KlerosCore_GovernanceTest is KlerosCore_TestBase { } function test_changeCurrencyRates() public { - vm.expectRevert(KlerosCoreBase.OwnerOnly.selector); + vm.expectRevert(KlerosCore.OwnerOnly.selector); vm.prank(other); core.changeCurrencyRates(feeToken, 100, 200); diff --git a/contracts/test/foundry/KlerosCore_Initialization.t.sol b/contracts/test/foundry/KlerosCore_Initialization.t.sol index d35190870..d9113bbb7 100644 --- a/contracts/test/foundry/KlerosCore_Initialization.t.sol +++ b/contracts/test/foundry/KlerosCore_Initialization.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.24; import {KlerosCore_TestBase} from "./KlerosCore_TestBase.sol"; -import {KlerosCoreBase} from "../../src/arbitration/KlerosCoreBase.sol"; +import {KlerosCore, IERC721} from "../../src/arbitration/KlerosCore.sol"; import {KlerosCoreMock} from "../../src/test/KlerosCoreMock.sol"; import {DisputeKitClassic} from "../../src/arbitration/dispute-kits/DisputeKitClassic.sol"; import {SortitionModuleMock} from "../../src/test/SortitionModuleMock.sol"; @@ -138,12 +138,12 @@ contract KlerosCore_InitializationTest is KlerosCore_TestBase { KlerosCoreMock newCore = KlerosCoreMock(address(proxyCore)); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.DisputeKitCreated(DISPUTE_KIT_CLASSIC, newDisputeKit); + emit KlerosCore.DisputeKitCreated(DISPUTE_KIT_CLASSIC, newDisputeKit); vm.expectEmit(true, true, true, true); uint256[] memory supportedDK = new uint256[](1); supportedDK[0] = DISPUTE_KIT_CLASSIC; - emit KlerosCoreBase.CourtCreated( + emit KlerosCore.CourtCreated( GENERAL_COURT, FORKING_COURT, false, @@ -155,7 +155,7 @@ contract KlerosCore_InitializationTest is KlerosCore_TestBase { supportedDK ); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.DisputeKitEnabled(GENERAL_COURT, DISPUTE_KIT_CLASSIC, true); + emit KlerosCore.DisputeKitEnabled(GENERAL_COURT, DISPUTE_KIT_CLASSIC, true); newCore.initialize( newOwner, newGuardian, @@ -167,7 +167,8 @@ contract KlerosCore_InitializationTest is KlerosCore_TestBase { newTimesPerPeriod, newSortitionExtraData, newSortitionModule, - address(wNative) + address(wNative), + IERC721(address(0)) ); } } diff --git a/contracts/test/foundry/KlerosCore_Staking.t.sol b/contracts/test/foundry/KlerosCore_Staking.t.sol index a315debcf..805b8ae7e 100644 --- a/contracts/test/foundry/KlerosCore_Staking.t.sol +++ b/contracts/test/foundry/KlerosCore_Staking.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.24; import {KlerosCore_TestBase} from "./KlerosCore_TestBase.sol"; -import {KlerosCoreBase} from "../../src/arbitration/KlerosCoreBase.sol"; +import {KlerosCore} from "../../src/arbitration/KlerosCore.sol"; import {SortitionModuleBase} from "../../src/arbitration/SortitionModuleBase.sol"; import {ISortitionModule} from "../../src/arbitration/interfaces/ISortitionModule.sol"; import {IKlerosCore, KlerosCoreSnapshotProxy} from "../../src/arbitration/view/KlerosCoreSnapshotProxy.sol"; @@ -14,26 +14,26 @@ contract KlerosCore_StakingTest is KlerosCore_TestBase { function test_setStake_increase() public { vm.prank(owner); core.pause(); - vm.expectRevert(KlerosCoreBase.WhenNotPausedOnly.selector); + vm.expectRevert(KlerosCore.WhenNotPausedOnly.selector); vm.prank(staker1); core.setStake(GENERAL_COURT, 1000); vm.prank(owner); core.unpause(); - vm.expectRevert(KlerosCoreBase.StakingNotPossibleInThisCourt.selector); + vm.expectRevert(KlerosCore.StakingNotPossibleInThisCourt.selector); vm.prank(staker1); core.setStake(FORKING_COURT, 1000); uint96 badCourtID = 2; - vm.expectRevert(KlerosCoreBase.StakingNotPossibleInThisCourt.selector); + vm.expectRevert(KlerosCore.StakingNotPossibleInThisCourt.selector); vm.prank(staker1); core.setStake(badCourtID, 1000); - vm.expectRevert(KlerosCoreBase.StakingLessThanCourtMinStake.selector); + vm.expectRevert(KlerosCore.StakingLessThanCourtMinStake.selector); vm.prank(staker1); core.setStake(GENERAL_COURT, 800); - vm.expectRevert(KlerosCoreBase.StakingZeroWhenNoStake.selector); + vm.expectRevert(KlerosCore.StakingZeroWhenNoStake.selector); vm.prank(staker1); core.setStake(GENERAL_COURT, 0); @@ -58,7 +58,7 @@ contract KlerosCore_StakingTest is KlerosCore_TestBase { assertEq(pinakion.balanceOf(staker1), 999999999999998999, "Wrong token balance of staker1"); // 1 eth - 1001 wei assertEq(pinakion.allowance(staker1, address(core)), 999999999999998999, "Wrong allowance for staker1"); - vm.expectRevert(KlerosCoreBase.StakingTransferFailed.selector); // This error will be caught because owner didn't approve any tokens for KlerosCore + vm.expectRevert(KlerosCore.StakingTransferFailed.selector); // This error will be caught because owner didn't approve any tokens for KlerosCore vm.prank(owner); core.setStake(GENERAL_COURT, 1000); @@ -111,7 +111,7 @@ contract KlerosCore_StakingTest is KlerosCore_TestBase { vm.prank(address(core)); pinakion.transfer(staker1, 1); // Manually send 1 token to make the withdrawal fail - vm.expectRevert(KlerosCoreBase.UnstakingTransferFailed.selector); + vm.expectRevert(KlerosCore.UnstakingTransferFailed.selector); vm.prank(staker1); core.setStake(GENERAL_COURT, 0); @@ -165,7 +165,7 @@ contract KlerosCore_StakingTest is KlerosCore_TestBase { assertEq(courts.length, 4, "Wrong courts count"); uint96 excessiveCourtID = 5; - vm.expectRevert(KlerosCoreBase.StakingInTooManyCourts.selector); + vm.expectRevert(KlerosCore.StakingInTooManyCourts.selector); vm.prank(staker1); core.setStake(excessiveCourtID, 2000); } @@ -416,7 +416,7 @@ contract KlerosCore_StakingTest is KlerosCore_TestBase { function test_setStakeBySortitionModule() public { // Note that functionality of this function was checked during delayed stakes execution - vm.expectRevert(KlerosCoreBase.SortitionModuleOnly.selector); + vm.expectRevert(KlerosCore.SortitionModuleOnly.selector); vm.prank(owner); core.setStakeBySortitionModule(staker1, GENERAL_COURT, 1000); } diff --git a/contracts/test/foundry/KlerosCore_TestBase.sol b/contracts/test/foundry/KlerosCore_TestBase.sol index 762d71db9..612db5bb7 100644 --- a/contracts/test/foundry/KlerosCore_TestBase.sol +++ b/contracts/test/foundry/KlerosCore_TestBase.sol @@ -3,8 +3,8 @@ pragma solidity ^0.8.24; import {Test} from "forge-std/Test.sol"; import {console} from "forge-std/console.sol"; // Import the console for logging -import {KlerosCoreMock, KlerosCoreBase} from "../../src/test/KlerosCoreMock.sol"; -import {IArbitratorV2} from "../../src/arbitration/KlerosCoreBase.sol"; +import {KlerosCoreMock, KlerosCore, IERC721} from "../../src/test/KlerosCoreMock.sol"; +import {IArbitratorV2} from "../../src/arbitration/KlerosCore.sol"; import {IDisputeKit} from "../../src/arbitration/interfaces/IDisputeKit.sol"; import {DisputeKitClassic, DisputeKitClassicBase} from "../../src/arbitration/dispute-kits/DisputeKitClassic.sol"; import {DisputeKitSybilResistant} from "../../src/arbitration/dispute-kits/DisputeKitSybilResistant.sol"; @@ -151,7 +151,8 @@ abstract contract KlerosCore_TestBase is Test { timesPerPeriod, sortitionExtraData, sortitionModule, - address(wNative) + address(wNative), + IERC721(address(0)) ); vm.prank(staker1); pinakion.approve(address(core), 1 ether); diff --git a/contracts/test/foundry/KlerosCore_Voting.t.sol b/contracts/test/foundry/KlerosCore_Voting.t.sol index 7d84f18e1..504c9c644 100644 --- a/contracts/test/foundry/KlerosCore_Voting.t.sol +++ b/contracts/test/foundry/KlerosCore_Voting.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.24; import {KlerosCore_TestBase} from "./KlerosCore_TestBase.sol"; -import {KlerosCoreBase} from "../../src/arbitration/KlerosCoreBase.sol"; +import {KlerosCore} from "../../src/arbitration/KlerosCore.sol"; import {DisputeKitClassic, DisputeKitClassicBase} from "../../src/arbitration/dispute-kits/DisputeKitClassic.sol"; import {IDisputeKit} from "../../src/arbitration/interfaces/IDisputeKit.sol"; import {UUPSProxy} from "../../src/proxy/UUPSProxy.sol"; @@ -44,17 +44,17 @@ contract KlerosCore_VotingTest is KlerosCore_TestBase { vm.expectRevert(DisputeKitClassicBase.NotCommitPeriod.selector); disputeKit.castCommit(disputeID, voteIDs, commit); - vm.expectRevert(KlerosCoreBase.EvidenceNotPassedAndNotAppeal.selector); + vm.expectRevert(KlerosCore.EvidenceNotPassedAndNotAppeal.selector); core.passPeriod(disputeID); vm.warp(block.timestamp + timesPerPeriod[0]); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.commit); + emit KlerosCore.NewPeriod(disputeID, KlerosCore.Period.commit); core.passPeriod(disputeID); - (, , KlerosCoreBase.Period period, , uint256 lastPeriodChange) = core.disputes(disputeID); + (, , KlerosCore.Period period, , uint256 lastPeriodChange) = core.disputes(disputeID); - assertEq(uint256(period), uint256(KlerosCoreBase.Period.commit), "Wrong period"); + assertEq(uint256(period), uint256(KlerosCore.Period.commit), "Wrong period"); assertEq(lastPeriodChange, block.timestamp, "Wrong lastPeriodChange"); vm.prank(staker1); @@ -149,12 +149,12 @@ contract KlerosCore_VotingTest is KlerosCore_TestBase { vm.warp(block.timestamp + timesPerPeriod[0]); core.passPeriod(disputeID); // Commit - vm.expectRevert(KlerosCoreBase.CommitPeriodNotPassed.selector); + vm.expectRevert(KlerosCore.CommitPeriodNotPassed.selector); core.passPeriod(disputeID); vm.warp(block.timestamp + timesPerPeriod[1]); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.vote); + emit KlerosCore.NewPeriod(disputeID, KlerosCore.Period.vote); core.passPeriod(disputeID); } @@ -178,18 +178,18 @@ contract KlerosCore_VotingTest is KlerosCore_TestBase { vm.expectRevert(DisputeKitClassicBase.NotVotePeriod.selector); disputeKit.castVote(disputeID, voteIDs, 2, 0, "XYZ"); // Leave salt empty as not needed - vm.expectRevert(KlerosCoreBase.DisputeStillDrawing.selector); + vm.expectRevert(KlerosCore.DisputeStillDrawing.selector); core.passPeriod(disputeID); core.draw(disputeID, 1); // Draw the last juror vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.vote); + emit KlerosCore.NewPeriod(disputeID, KlerosCore.Period.vote); core.passPeriod(disputeID); // Vote - (, , KlerosCoreBase.Period period, , uint256 lastPeriodChange) = core.disputes(disputeID); + (, , KlerosCore.Period period, , uint256 lastPeriodChange) = core.disputes(disputeID); - assertEq(uint256(period), uint256(KlerosCoreBase.Period.vote), "Wrong period"); + assertEq(uint256(period), uint256(KlerosCore.Period.vote), "Wrong period"); assertEq(lastPeriodChange, block.timestamp, "Wrong lastPeriodChange"); vm.prank(staker1); @@ -250,7 +250,7 @@ contract KlerosCore_VotingTest is KlerosCore_TestBase { assertEq(totalVoted, 2, "totalVoted should be 2"); assertEq(choiceCount, 1, "choiceCount should be 1 for first choice"); - vm.expectRevert(KlerosCoreBase.VotePeriodNotPassed.selector); + vm.expectRevert(KlerosCore.VotePeriodNotPassed.selector); core.passPeriod(disputeID); voteIDs = new uint256[](1); @@ -285,14 +285,14 @@ contract KlerosCore_VotingTest is KlerosCore_TestBase { vm.warp(block.timestamp + timesPerPeriod[0]); core.passPeriod(disputeID); // Votes - vm.expectRevert(KlerosCoreBase.VotePeriodNotPassed.selector); + vm.expectRevert(KlerosCore.VotePeriodNotPassed.selector); core.passPeriod(disputeID); vm.warp(block.timestamp + timesPerPeriod[2]); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.AppealPossible(disputeID, arbitrable); + emit KlerosCore.AppealPossible(disputeID, arbitrable); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.appeal); + emit KlerosCore.NewPeriod(disputeID, KlerosCore.Period.appeal); core.passPeriod(disputeID); } @@ -380,7 +380,7 @@ contract KlerosCore_VotingTest is KlerosCore_TestBase { // Should pass period by counting only committed votes. vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.NewPeriod(disputeID, KlerosCoreBase.Period.appeal); + emit KlerosCore.NewPeriod(disputeID, KlerosCore.Period.appeal); core.passPeriod(disputeID); } @@ -407,7 +407,7 @@ contract KlerosCore_VotingTest is KlerosCore_TestBase { vm.prank(owner); vm.expectEmit(true, true, true, true); - emit KlerosCoreBase.DisputeKitEnabled(GENERAL_COURT, newDkID, true); + emit KlerosCore.DisputeKitEnabled(GENERAL_COURT, newDkID, true); supportedDK[0] = newDkID; core.enableDisputeKits(GENERAL_COURT, supportedDK, true); assertEq(core.isSupported(GENERAL_COURT, newDkID), true, "New DK should be supported by General court"); @@ -434,7 +434,7 @@ contract KlerosCore_VotingTest is KlerosCore_TestBase { vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase - KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 0); + KlerosCore.Round memory round = core.getRoundInfo(disputeID, 0); assertEq(round.disputeKitID, newDkID, "Wrong DK ID"); core.draw(disputeID, DEFAULT_NB_OF_JURORS); From 40714da2b2122a34b10047f9a639b17fc6b18718 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Thu, 4 Sep 2025 18:55:39 +0100 Subject: [PATCH 04/11] feat: only SortitionModule, no more SortitionModuleBase and SortitionModuleNeo --- contracts/src/arbitration/SortitionModule.sol | 587 +++++++++++++++++- .../src/arbitration/SortitionModuleBase.sol | 572 ----------------- .../src/arbitration/SortitionModuleNeo.sol | 100 --- .../test/foundry/KlerosCore_Drawing.t.sol | 4 +- .../test/foundry/KlerosCore_Execution.t.sol | 30 +- .../foundry/KlerosCore_Initialization.t.sol | 6 +- contracts/test/foundry/KlerosCore_RNG.t.sol | 4 +- .../test/foundry/KlerosCore_Staking.t.sol | 24 +- .../test/foundry/KlerosCore_TestBase.sol | 8 +- 9 files changed, 622 insertions(+), 713 deletions(-) delete mode 100644 contracts/src/arbitration/SortitionModuleBase.sol delete mode 100644 contracts/src/arbitration/SortitionModuleNeo.sol diff --git a/contracts/src/arbitration/SortitionModule.sol b/contracts/src/arbitration/SortitionModule.sol index bd0f6e007..8c0f4de86 100644 --- a/contracts/src/arbitration/SortitionModule.sol +++ b/contracts/src/arbitration/SortitionModule.sol @@ -2,13 +2,98 @@ pragma solidity ^0.8.24; -import {SortitionModuleBase, KlerosCore, IRNG} from "./SortitionModuleBase.sol"; +import {KlerosCore} from "./KlerosCore.sol"; +import {ISortitionModule} from "./interfaces/ISortitionModule.sol"; +import {IDisputeKit} from "./interfaces/IDisputeKit.sol"; +import {Initializable} from "../proxy/Initializable.sol"; +import {UUPSProxiable} from "../proxy/UUPSProxiable.sol"; +import {SortitionTrees, TreeKey, CourtID} from "../libraries/SortitionTrees.sol"; +import {IRNG} from "../rng/IRNG.sol"; +import "../libraries/Constants.sol"; /// @title SortitionModule /// @dev A factory of trees that keeps track of staked values for sortition. -contract SortitionModule is SortitionModuleBase { +contract SortitionModule is ISortitionModule, Initializable, UUPSProxiable { + using SortitionTrees for SortitionTrees.Tree; + using SortitionTrees for mapping(TreeKey key => SortitionTrees.Tree); + string public constant override version = "2.0.0"; + // ************************************* // + // * Enums / Structs * // + // ************************************* // + + struct DelayedStake { + address account; // The address of the juror. + uint96 courtID; // The ID of the court. + uint256 stake; // The new stake. + bool alreadyTransferred; // DEPRECATED. True if tokens were already transferred before delayed stake's execution. + } + + struct Juror { + uint96[] courtIDs; // The IDs of courts where the juror's stake path ends. A stake path is a path from the general court to a court the juror directly staked in using `_setStake`. + uint256 stakedPnk; // The juror's total amount of tokens staked in subcourts. PNK balance including locked PNK and penalty deductions. + uint256 lockedPnk; // The juror's total amount of tokens locked in disputes. + } + + // ************************************* // + // * Storage * // + // ************************************* // + + address public owner; // The owner of the contract. + KlerosCore public core; // The core arbitrator contract. + Phase public phase; // The current phase. + uint256 public minStakingTime; // The time after which the phase can be switched to Drawing if there are open disputes. + uint256 public maxDrawingTime; // The time after which the phase can be switched back to Staking. + uint256 public lastPhaseChange; // The last time the phase was changed. + uint256 public randomNumberRequestBlock; // DEPRECATED: to be removed in the next redeploy + uint256 public disputesWithoutJurors; // The number of disputes that have not finished drawing jurors. + IRNG public rng; // The random number generator. + uint256 public randomNumber; // Random number returned by RNG. + uint256 public rngLookahead; // DEPRECATED: to be removed in the next redeploy + uint256 public delayedStakeWriteIndex; // The index of the last `delayedStake` item that was written to the array. 0 index is skipped. + uint256 public delayedStakeReadIndex; // The index of the next `delayedStake` item that should be processed. Starts at 1 because 0 index is skipped. + mapping(TreeKey key => SortitionTrees.Tree) sortitionSumTrees; // The mapping of sortition trees by keys. + mapping(address account => Juror) public jurors; // The jurors. + mapping(uint256 => DelayedStake) public delayedStakes; // Stores the stakes that were changed during Drawing phase, to update them when the phase is switched to Staking. + mapping(address jurorAccount => mapping(uint96 courtId => uint256)) public latestDelayedStakeIndex; // DEPRECATED. Maps the juror to its latest delayed stake. If there is already a delayed stake for this juror then it'll be replaced. latestDelayedStakeIndex[juror][courtID]. + uint256 public maxStakePerJuror; + uint256 public maxTotalStaked; + uint256 public totalStaked; + + // ************************************* // + // * Events * // + // ************************************* // + + /// @notice Emitted when a juror stakes in a court. + /// @param _address The address of the juror. + /// @param _courtID The ID of the court. + /// @param _amount The amount of tokens staked in the court. + /// @param _amountAllCourts The amount of tokens staked in all courts. + event StakeSet(address indexed _address, uint256 _courtID, uint256 _amount, uint256 _amountAllCourts); + + /// @notice Emitted when a juror's stake is delayed. + /// @param _address The address of the juror. + /// @param _courtID The ID of the court. + /// @param _amount The amount of tokens staked in the court. + event StakeDelayed(address indexed _address, uint96 indexed _courtID, uint256 _amount); + + /// @notice Emitted when a juror's stake is locked. + /// @param _address The address of the juror. + /// @param _relativeAmount The amount of tokens locked. + /// @param _unlock Whether the stake is locked or unlocked. + event StakeLocked(address indexed _address, uint256 _relativeAmount, bool _unlock); + + /// @dev Emitted when leftover PNK is available. + /// @param _account The account of the juror. + /// @param _amount The amount of PNK available. + event LeftoverPNK(address indexed _account, uint256 _amount); + + /// @dev Emitted when leftover PNK is withdrawn. + /// @param _account The account of the juror withdrawing PNK. + /// @param _amount The amount of PNK withdrawn. + event LeftoverPNKWithdrawn(address indexed _account, uint256 _amount); + // ************************************* // // * Constructor * // // ************************************* // @@ -24,14 +109,40 @@ contract SortitionModule is SortitionModuleBase { /// @param _minStakingTime Minimal time to stake /// @param _maxDrawingTime Time after which the drawing phase can be switched /// @param _rng The random number generator. + /// @param _maxStakePerJuror The maximum amount of PNK a juror can stake in a court. + /// @param _maxTotalStaked The maximum amount of PNK that can be staked in all courts. function initialize( address _owner, KlerosCore _core, uint256 _minStakingTime, uint256 _maxDrawingTime, - IRNG _rng + IRNG _rng, + uint256 _maxStakePerJuror, + uint256 _maxTotalStaked ) external initializer { - __SortitionModuleBase_initialize(_owner, _core, _minStakingTime, _maxDrawingTime, _rng); + owner = _owner; + core = _core; + minStakingTime = _minStakingTime; + maxDrawingTime = _maxDrawingTime; + lastPhaseChange = block.timestamp; + rng = _rng; + maxStakePerJuror = _maxStakePerJuror; + maxTotalStaked = _maxTotalStaked; + delayedStakeReadIndex = 1; + } + + // ************************************* // + // * Function Modifiers * // + // ************************************* // + + modifier onlyByOwner() { + if (owner != msg.sender) revert OwnerOnly(); + _; + } + + modifier onlyByCore() { + if (address(core) != msg.sender) revert KlerosCoreOnly(); + _; } // ************************************* // @@ -40,7 +151,473 @@ contract SortitionModule is SortitionModuleBase { /// @dev Access Control to perform implementation upgrades (UUPS Proxiable) /// Only the owner can perform upgrades (`onlyByOwner`) - function _authorizeUpgrade(address) internal view virtual override onlyByOwner { + function _authorizeUpgrade(address) internal view override onlyByOwner { // NOP } + + /// @dev Changes the owner of the contract. + /// @param _owner The new owner. + function changeOwner(address _owner) external onlyByOwner { + owner = _owner; + } + + /// @dev Changes the `minStakingTime` storage variable. + /// @param _minStakingTime The new value for the `minStakingTime` storage variable. + function changeMinStakingTime(uint256 _minStakingTime) external onlyByOwner { + minStakingTime = _minStakingTime; + } + + /// @dev Changes the `maxDrawingTime` storage variable. + /// @param _maxDrawingTime The new value for the `maxDrawingTime` storage variable. + function changeMaxDrawingTime(uint256 _maxDrawingTime) external onlyByOwner { + maxDrawingTime = _maxDrawingTime; + } + + /// @dev Changes the `rng` storage variable. + /// @param _rng The new random number generator. + function changeRandomNumberGenerator(IRNG _rng) external onlyByOwner { + rng = _rng; + if (phase == Phase.generating) { + rng.requestRandomness(); + } + } + + function changeMaxStakePerJuror(uint256 _maxStakePerJuror) external onlyByOwner { + maxStakePerJuror = _maxStakePerJuror; + } + + function changeMaxTotalStaked(uint256 _maxTotalStaked) external onlyByOwner { + maxTotalStaked = _maxTotalStaked; + } + + // ************************************* // + // * State Modifiers * // + // ************************************* // + + function passPhase() external { + if (phase == Phase.staking) { + if (block.timestamp - lastPhaseChange < minStakingTime) revert MinStakingTimeNotPassed(); + if (disputesWithoutJurors == 0) revert NoDisputesThatNeedJurors(); + rng.requestRandomness(); + phase = Phase.generating; + } else if (phase == Phase.generating) { + randomNumber = rng.receiveRandomness(); + if (randomNumber == 0) revert RandomNumberNotReady(); + phase = Phase.drawing; + } else if (phase == Phase.drawing) { + if (disputesWithoutJurors > 0 && block.timestamp - lastPhaseChange < maxDrawingTime) { + revert DisputesWithoutJurorsAndMaxDrawingTimeNotPassed(); + } + phase = Phase.staking; + } + + lastPhaseChange = block.timestamp; + emit NewPhase(phase); + } + + /// @dev Create a sortition sum tree at the specified key. + /// @param _courtID The ID of the court. + /// @param _extraData Extra data that contains the number of children each node in the tree should have. + function createTree(uint96 _courtID, bytes memory _extraData) external override onlyByCore { + TreeKey key = CourtID.wrap(_courtID).toTreeKey(); + uint256 K = _extraDataToTreeK(_extraData); + sortitionSumTrees.createTree(key, K); + } + + /// @dev Executes the next delayed stakes. + /// @param _iterations The number of delayed stakes to execute. + function executeDelayedStakes(uint256 _iterations) external { + if (phase != Phase.staking) revert NotStakingPhase(); + if (delayedStakeWriteIndex < delayedStakeReadIndex) revert NoDelayedStakeToExecute(); + + uint256 actualIterations = (delayedStakeReadIndex + _iterations) - 1 > delayedStakeWriteIndex + ? (delayedStakeWriteIndex - delayedStakeReadIndex) + 1 + : _iterations; + uint256 newDelayedStakeReadIndex = delayedStakeReadIndex + actualIterations; + + for (uint256 i = delayedStakeReadIndex; i < newDelayedStakeReadIndex; i++) { + DelayedStake storage delayedStake = delayedStakes[i]; + core.setStakeBySortitionModule(delayedStake.account, delayedStake.courtID, delayedStake.stake); + delete delayedStakes[i]; + } + delayedStakeReadIndex = newDelayedStakeReadIndex; + } + + function createDisputeHook(uint256 /*_disputeID*/, uint256 /*_roundID*/) external override onlyByCore { + disputesWithoutJurors++; + } + + function postDrawHook(uint256 /*_disputeID*/, uint256 /*_roundID*/) external override onlyByCore { + disputesWithoutJurors--; + } + + /// @dev Saves the random number to use it in sortition. Not used by this contract because the storing of the number is inlined in passPhase(). + /// @param _randomNumber Random number returned by RNG contract. + function notifyRandomNumber(uint256 _randomNumber) public override {} + + /// @dev Validate the specified juror's new stake for a court. + /// Note: no state changes should be made when returning stakingResult != Successful, otherwise delayed stakes might break invariants. + /// @param _account The address of the juror. + /// @param _courtID The ID of the court. + /// @param _newStake The new stake. + /// @param _noDelay True if the stake change should not be delayed. + /// @return pnkDeposit The amount of PNK to be deposited. + /// @return pnkWithdrawal The amount of PNK to be withdrawn. + /// @return stakingResult The result of the staking operation. + function validateStake( + address _account, + uint96 _courtID, + uint256 _newStake, + bool _noDelay + ) external override onlyByCore returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { + (pnkDeposit, pnkWithdrawal, stakingResult) = _validateStake(_account, _courtID, _newStake, _noDelay); + } + + function _validateStake( + address _account, + uint96 _courtID, + uint256 _newStake, + bool _noDelay + ) internal virtual returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { + Juror storage juror = jurors[_account]; + uint256 currentStake = stakeOf(_account, _courtID); + bool stakeIncrease = _newStake > currentStake; + uint256 stakeChange = stakeIncrease ? _newStake - currentStake : currentStake - _newStake; + + uint256 nbCourts = juror.courtIDs.length; + if (currentStake == 0 && nbCourts >= MAX_STAKE_PATHS) { + return (0, 0, StakingResult.CannotStakeInMoreCourts); // Prevent staking beyond MAX_STAKE_PATHS but unstaking is always allowed. + } + + if (currentStake == 0 && _newStake == 0) { + return (0, 0, StakingResult.CannotStakeZeroWhenNoStake); // Forbid staking 0 amount when current stake is 0 to avoid flaky behaviour. + } + + if (stakeIncrease) { + // Check if the stake increase is within the limits. + if (juror.stakedPnk + stakeChange > maxStakePerJuror || currentStake + stakeChange > maxStakePerJuror) { + return (0, 0, StakingResult.CannotStakeMoreThanMaxStakePerJuror); + } + if (totalStaked + stakeChange > maxTotalStaked) { + return (0, 0, StakingResult.CannotStakeMoreThanMaxTotalStaked); + } + } + + if (phase != Phase.staking && !_noDelay) { + // Store the stake change as delayed, to be applied when the phase switches back to Staking. + DelayedStake storage delayedStake = delayedStakes[++delayedStakeWriteIndex]; + delayedStake.account = _account; + delayedStake.courtID = _courtID; + delayedStake.stake = _newStake; + emit StakeDelayed(_account, _courtID, _newStake); + return (pnkDeposit, pnkWithdrawal, StakingResult.Delayed); + } + + // Current phase is Staking: set stakes. + if (stakeIncrease) { + pnkDeposit = stakeChange; + totalStaked += stakeChange; + } else { + pnkWithdrawal = stakeChange; + totalStaked -= stakeChange; + + // Ensure locked tokens remain in the contract. They can only be released during Execution. + uint256 possibleWithdrawal = juror.stakedPnk > juror.lockedPnk ? juror.stakedPnk - juror.lockedPnk : 0; + if (pnkWithdrawal > possibleWithdrawal) { + pnkWithdrawal = possibleWithdrawal; + } + } + return (pnkDeposit, pnkWithdrawal, StakingResult.Successful); + } + + /// @dev Update the state of the stakes, called by KC at the end of setStake flow. + /// `O(n + p * log_k(j))` where + /// `n` is the number of courts the juror has staked in, + /// `p` is the depth of the court tree, + /// `k` is the minimum number of children per node of one of these courts' sortition sum tree, + /// and `j` is the maximum number of jurors that ever staked in one of these courts simultaneously. + /// @param _account The address of the juror. + /// @param _courtID The ID of the court. + /// @param _pnkDeposit The amount of PNK to be deposited. + /// @param _pnkWithdrawal The amount of PNK to be withdrawn. + /// @param _newStake The new stake. + function setStake( + address _account, + uint96 _courtID, + uint256 _pnkDeposit, + uint256 _pnkWithdrawal, + uint256 _newStake + ) external override onlyByCore { + _setStake(_account, _courtID, _pnkDeposit, _pnkWithdrawal, _newStake); + } + + /// @dev Update the state of the stakes with a PNK reward deposit, called by KC during rewards execution. + /// `O(n + p * log_k(j))` where + /// `n` is the number of courts the juror has staked in, + /// `p` is the depth of the court tree, + /// `k` is the minimum number of children per node of one of these courts' sortition sum tree, + /// and `j` is the maximum number of jurors that ever staked in one of these courts simultaneously. + /// @param _account The address of the juror. + /// @param _courtID The ID of the court. + /// @param _penalty The amount of PNK to be deducted. + /// @return pnkBalance The updated total PNK balance of the juror, including the penalty. + /// @return newCourtStake The updated stake of the juror in the court. + /// @return availablePenalty The amount of PNK that was actually deducted. + function setStakePenalty( + address _account, + uint96 _courtID, + uint256 _penalty + ) external override onlyByCore returns (uint256 pnkBalance, uint256 newCourtStake, uint256 availablePenalty) { + Juror storage juror = jurors[_account]; + availablePenalty = _penalty; + newCourtStake = stakeOf(_account, _courtID); + if (juror.stakedPnk < _penalty) { + availablePenalty = juror.stakedPnk; + } + + if (availablePenalty == 0) return (juror.stakedPnk, newCourtStake, 0); // No penalty to apply. + + uint256 currentStake = stakeOf(_account, _courtID); + uint256 newStake = 0; + if (currentStake >= availablePenalty) { + newStake = currentStake - availablePenalty; + } + _setStake(_account, _courtID, 0, availablePenalty, newStake); + pnkBalance = juror.stakedPnk; // updated by _setStake() + newCourtStake = stakeOf(_account, _courtID); // updated by _setStake() + } + + /// @dev Update the state of the stakes with a PNK reward deposit, called by KC during rewards execution. + /// `O(n + p * log_k(j))` where + /// `n` is the number of courts the juror has staked in, + /// `p` is the depth of the court tree, + /// `k` is the minimum number of children per node of one of these courts' sortition sum tree, + /// and `j` is the maximum number of jurors that ever staked in one of these courts simultaneously. + /// @param _account The address of the juror. + /// @param _courtID The ID of the court. + /// @param _reward The amount of PNK to be deposited as a reward. + function setStakeReward( + address _account, + uint96 _courtID, + uint256 _reward + ) external override onlyByCore returns (bool success) { + if (_reward == 0) return true; // No reward to add. + + uint256 currentStake = stakeOf(_account, _courtID); + if (currentStake == 0) return false; // Juror has been unstaked, don't increase their stake. + + uint256 newStake = currentStake + _reward; + _setStake(_account, _courtID, _reward, 0, newStake); + return true; + } + + function _setStake( + address _account, + uint96 _courtID, + uint256 _pnkDeposit, + uint256 _pnkWithdrawal, + uint256 _newStake + ) internal virtual { + Juror storage juror = jurors[_account]; + if (_pnkDeposit > 0) { + uint256 currentStake = stakeOf(_account, _courtID); + if (currentStake == 0) { + juror.courtIDs.push(_courtID); + } + // Increase juror's balance by deposited amount. + juror.stakedPnk += _pnkDeposit; + } else { + juror.stakedPnk -= _pnkWithdrawal; + if (_newStake == 0) { + // Cleanup + for (uint256 i = juror.courtIDs.length; i > 0; i--) { + if (juror.courtIDs[i - 1] == _courtID) { + juror.courtIDs[i - 1] = juror.courtIDs[juror.courtIDs.length - 1]; + juror.courtIDs.pop(); + break; + } + } + } + } + + // Update the sortition sum tree. + bytes32 stakePathID = SortitionTrees.toStakePathID(_account, _courtID); + bool finished = false; + uint96 currentCourtID = _courtID; + while (!finished) { + // Tokens are also implicitly staked in parent courts through sortition module to increase the chance of being drawn. + TreeKey key = CourtID.wrap(currentCourtID).toTreeKey(); + sortitionSumTrees[key].set(_newStake, stakePathID); + if (currentCourtID == GENERAL_COURT) { + finished = true; + } else { + (currentCourtID, , , , , , ) = core.courts(currentCourtID); // Get the parent court. + } + } + emit StakeSet(_account, _courtID, _newStake, juror.stakedPnk); + } + + function lockStake(address _account, uint256 _relativeAmount) external override onlyByCore { + jurors[_account].lockedPnk += _relativeAmount; + emit StakeLocked(_account, _relativeAmount, false); + } + + function unlockStake(address _account, uint256 _relativeAmount) external override onlyByCore { + Juror storage juror = jurors[_account]; + juror.lockedPnk -= _relativeAmount; + emit StakeLocked(_account, _relativeAmount, true); + + uint256 amount = getJurorLeftoverPNK(_account); + if (amount > 0) { + emit LeftoverPNK(_account, amount); + } + } + + /// @dev Unstakes the inactive juror from all courts. + /// `O(n * (p * log_k(j)) )` where + /// `n` is the number of courts the juror has staked in, + /// `p` is the depth of the court tree, + /// `k` is the minimum number of children per node of one of these courts' sortition sum tree, + /// and `j` is the maximum number of jurors that ever staked in one of these courts simultaneously. + /// @param _account The juror to unstake. + function forcedUnstakeAllCourts(address _account) external override onlyByCore { + uint96[] memory courtIDs = getJurorCourtIDs(_account); + for (uint256 j = courtIDs.length; j > 0; j--) { + core.setStakeBySortitionModule(_account, courtIDs[j - 1], 0); + } + } + + /// @dev Unstakes the inactive juror from a specific court. + /// `O(n * (p * log_k(j)) )` where + /// `n` is the number of courts the juror has staked in, + /// `p` is the depth of the court tree, + /// `k` is the minimum number of children per node of one of these courts' sortition sum tree, + /// and `j` is the maximum number of jurors that ever staked in one of these courts simultaneously. + /// @param _account The juror to unstake. + /// @param _courtID The ID of the court. + function forcedUnstake(address _account, uint96 _courtID) external override onlyByCore { + core.setStakeBySortitionModule(_account, _courtID, 0); + } + + /// @dev Gives back the locked PNKs in case the juror fully unstaked earlier. + /// Note that since locked and staked PNK are async it is possible for the juror to have positive staked PNK balance + /// while having 0 stake in courts and 0 locked tokens (eg. when the juror fully unstaked during dispute and later got his tokens unlocked). + /// In this case the juror can use this function to withdraw the leftover tokens. + /// Also note that if the juror has some leftover PNK while not fully unstaked he'll have to manually unstake from all courts to trigger this function. + /// @param _account The juror whose PNK to withdraw. + function withdrawLeftoverPNK(address _account) external override { + // Can withdraw the leftover PNK if fully unstaked, has no tokens locked and has positive balance. + // This withdrawal can't be triggered by calling setStake() in KlerosCore because current stake is technically 0, thus it is done via separate function. + uint256 amount = getJurorLeftoverPNK(_account); + if (amount == 0) revert NotEligibleForWithdrawal(); + jurors[_account].stakedPnk = 0; + core.transferBySortitionModule(_account, amount); + emit LeftoverPNKWithdrawn(_account, amount); + } + + // ************************************* // + // * Public Views * // + // ************************************* // + + /// @dev Draw an ID from a tree using a number. + /// Note that this function reverts if the sum of all values in the tree is 0. + /// @param _courtID The ID of the court. + /// @param _coreDisputeID Index of the dispute in Kleros Core. + /// @param _nonce Nonce to hash with random number. + /// @return drawnAddress The drawn address. + /// `O(k * log_k(n))` where + /// `k` is the maximum number of children per node in the tree, + /// and `n` is the maximum number of nodes ever appended. + function draw( + uint96 _courtID, + uint256 _coreDisputeID, + uint256 _nonce + ) public view override returns (address drawnAddress, uint96 fromSubcourtID) { + if (phase != Phase.drawing) revert NotDrawingPhase(); + + TreeKey key = CourtID.wrap(_courtID).toTreeKey(); + (drawnAddress, fromSubcourtID) = sortitionSumTrees[key].draw(_coreDisputeID, _nonce, randomNumber); + } + + /// @dev Get the stake of a juror in a court. + /// @param _juror The address of the juror. + /// @param _courtID The ID of the court. + /// @return value The stake of the juror in the court. + function stakeOf(address _juror, uint96 _courtID) public view returns (uint256) { + bytes32 stakePathID = SortitionTrees.toStakePathID(_juror, _courtID); + TreeKey key = CourtID.wrap(_courtID).toTreeKey(); + return sortitionSumTrees[key].stakeOf(stakePathID); + } + + /// @dev Gets the balance of a juror in a court. + /// @param _juror The address of the juror. + /// @param _courtID The ID of the court. + /// @return totalStaked The total amount of tokens staked including locked tokens and penalty deductions. Equivalent to the effective stake in the General court. + /// @return totalLocked The total amount of tokens locked in disputes. + /// @return stakedInCourt The amount of tokens staked in the specified court including locked tokens and penalty deductions. + /// @return nbCourts The number of courts the juror has directly staked in. + function getJurorBalance( + address _juror, + uint96 _courtID + ) + external + view + override + returns (uint256 totalStaked, uint256 totalLocked, uint256 stakedInCourt, uint256 nbCourts) + { + Juror storage juror = jurors[_juror]; + totalStaked = juror.stakedPnk; + totalLocked = juror.lockedPnk; + stakedInCourt = stakeOf(_juror, _courtID); + nbCourts = juror.courtIDs.length; + } + + /// @dev Gets the court identifiers where a specific `_juror` has staked. + /// @param _juror The address of the juror. + function getJurorCourtIDs(address _juror) public view override returns (uint96[] memory) { + return jurors[_juror].courtIDs; + } + + function isJurorStaked(address _juror) external view override returns (bool) { + return jurors[_juror].stakedPnk > 0; + } + + function getJurorLeftoverPNK(address _juror) public view override returns (uint256) { + Juror storage juror = jurors[_juror]; + if (juror.courtIDs.length == 0 && juror.lockedPnk == 0) { + return juror.stakedPnk; + } else { + return 0; + } + } + + // ************************************* // + // * Internal * // + // ************************************* // + + function _extraDataToTreeK(bytes memory _extraData) internal pure returns (uint256 K) { + if (_extraData.length >= 32) { + assembly { + // solium-disable-line security/no-inline-assembly + K := mload(add(_extraData, 0x20)) + } + } else { + K = DEFAULT_K; + } + } + + // ************************************* // + // * Errors * // + // ************************************* // + + error OwnerOnly(); + error KlerosCoreOnly(); + error MinStakingTimeNotPassed(); + error NoDisputesThatNeedJurors(); + error RandomNumberNotReady(); + error DisputesWithoutJurorsAndMaxDrawingTimeNotPassed(); + error NotStakingPhase(); + error NoDelayedStakeToExecute(); + error NotEligibleForWithdrawal(); + error NotDrawingPhase(); } diff --git a/contracts/src/arbitration/SortitionModuleBase.sol b/contracts/src/arbitration/SortitionModuleBase.sol deleted file mode 100644 index 3c528b017..000000000 --- a/contracts/src/arbitration/SortitionModuleBase.sol +++ /dev/null @@ -1,572 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.24; - -import {KlerosCore} from "./KlerosCore.sol"; -import {ISortitionModule} from "./interfaces/ISortitionModule.sol"; -import {IDisputeKit} from "./interfaces/IDisputeKit.sol"; -import {Initializable} from "../proxy/Initializable.sol"; -import {UUPSProxiable} from "../proxy/UUPSProxiable.sol"; -import {SortitionTrees, TreeKey, CourtID} from "../libraries/SortitionTrees.sol"; -import {IRNG} from "../rng/IRNG.sol"; -import "../libraries/Constants.sol"; - -/// @title SortitionModuleBase -/// @dev A factory of trees that keeps track of staked values for sortition. -abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSProxiable { - using SortitionTrees for SortitionTrees.Tree; - using SortitionTrees for mapping(TreeKey key => SortitionTrees.Tree); - - // ************************************* // - // * Enums / Structs * // - // ************************************* // - - struct DelayedStake { - address account; // The address of the juror. - uint96 courtID; // The ID of the court. - uint256 stake; // The new stake. - bool alreadyTransferred; // DEPRECATED. True if tokens were already transferred before delayed stake's execution. - } - - struct Juror { - uint96[] courtIDs; // The IDs of courts where the juror's stake path ends. A stake path is a path from the general court to a court the juror directly staked in using `_setStake`. - uint256 stakedPnk; // The juror's total amount of tokens staked in subcourts. PNK balance including locked PNK and penalty deductions. - uint256 lockedPnk; // The juror's total amount of tokens locked in disputes. - } - - // ************************************* // - // * Storage * // - // ************************************* // - - address public owner; // The owner of the contract. - KlerosCore public core; // The core arbitrator contract. - Phase public phase; // The current phase. - uint256 public minStakingTime; // The time after which the phase can be switched to Drawing if there are open disputes. - uint256 public maxDrawingTime; // The time after which the phase can be switched back to Staking. - uint256 public lastPhaseChange; // The last time the phase was changed. - uint256 public randomNumberRequestBlock; // DEPRECATED: to be removed in the next redeploy - uint256 public disputesWithoutJurors; // The number of disputes that have not finished drawing jurors. - IRNG public rng; // The random number generator. - uint256 public randomNumber; // Random number returned by RNG. - uint256 public rngLookahead; // DEPRECATED: to be removed in the next redeploy - uint256 public delayedStakeWriteIndex; // The index of the last `delayedStake` item that was written to the array. 0 index is skipped. - uint256 public delayedStakeReadIndex; // The index of the next `delayedStake` item that should be processed. Starts at 1 because 0 index is skipped. - mapping(TreeKey key => SortitionTrees.Tree) sortitionSumTrees; // The mapping of sortition trees by keys. - mapping(address account => Juror) public jurors; // The jurors. - mapping(uint256 => DelayedStake) public delayedStakes; // Stores the stakes that were changed during Drawing phase, to update them when the phase is switched to Staking. - mapping(address jurorAccount => mapping(uint96 courtId => uint256)) public latestDelayedStakeIndex; // DEPRECATED. Maps the juror to its latest delayed stake. If there is already a delayed stake for this juror then it'll be replaced. latestDelayedStakeIndex[juror][courtID]. - - // ************************************* // - // * Events * // - // ************************************* // - - /// @notice Emitted when a juror stakes in a court. - /// @param _address The address of the juror. - /// @param _courtID The ID of the court. - /// @param _amount The amount of tokens staked in the court. - /// @param _amountAllCourts The amount of tokens staked in all courts. - event StakeSet(address indexed _address, uint256 _courtID, uint256 _amount, uint256 _amountAllCourts); - - /// @notice Emitted when a juror's stake is delayed. - /// @param _address The address of the juror. - /// @param _courtID The ID of the court. - /// @param _amount The amount of tokens staked in the court. - event StakeDelayed(address indexed _address, uint96 indexed _courtID, uint256 _amount); - - /// @notice Emitted when a juror's stake is locked. - /// @param _address The address of the juror. - /// @param _relativeAmount The amount of tokens locked. - /// @param _unlock Whether the stake is locked or unlocked. - event StakeLocked(address indexed _address, uint256 _relativeAmount, bool _unlock); - - /// @dev Emitted when leftover PNK is available. - /// @param _account The account of the juror. - /// @param _amount The amount of PNK available. - event LeftoverPNK(address indexed _account, uint256 _amount); - - /// @dev Emitted when leftover PNK is withdrawn. - /// @param _account The account of the juror withdrawing PNK. - /// @param _amount The amount of PNK withdrawn. - event LeftoverPNKWithdrawn(address indexed _account, uint256 _amount); - - // ************************************* // - // * Constructor * // - // ************************************* // - - function __SortitionModuleBase_initialize( - address _owner, - KlerosCore _core, - uint256 _minStakingTime, - uint256 _maxDrawingTime, - IRNG _rng - ) internal onlyInitializing { - owner = _owner; - core = _core; - minStakingTime = _minStakingTime; - maxDrawingTime = _maxDrawingTime; - lastPhaseChange = block.timestamp; - rng = _rng; - delayedStakeReadIndex = 1; - } - - // ************************************* // - // * Function Modifiers * // - // ************************************* // - - modifier onlyByOwner() { - if (owner != msg.sender) revert OwnerOnly(); - _; - } - - modifier onlyByCore() { - if (address(core) != msg.sender) revert KlerosCoreOnly(); - _; - } - - // ************************************* // - // * Governance * // - // ************************************* // - - /// @dev Changes the owner of the contract. - /// @param _owner The new owner. - function changeOwner(address _owner) external onlyByOwner { - owner = _owner; - } - - /// @dev Changes the `minStakingTime` storage variable. - /// @param _minStakingTime The new value for the `minStakingTime` storage variable. - function changeMinStakingTime(uint256 _minStakingTime) external onlyByOwner { - minStakingTime = _minStakingTime; - } - - /// @dev Changes the `maxDrawingTime` storage variable. - /// @param _maxDrawingTime The new value for the `maxDrawingTime` storage variable. - function changeMaxDrawingTime(uint256 _maxDrawingTime) external onlyByOwner { - maxDrawingTime = _maxDrawingTime; - } - - /// @dev Changes the `rng` storage variable. - /// @param _rng The new random number generator. - function changeRandomNumberGenerator(IRNG _rng) external onlyByOwner { - rng = _rng; - if (phase == Phase.generating) { - rng.requestRandomness(); - } - } - - // ************************************* // - // * State Modifiers * // - // ************************************* // - - function passPhase() external { - if (phase == Phase.staking) { - if (block.timestamp - lastPhaseChange < minStakingTime) revert MinStakingTimeNotPassed(); - if (disputesWithoutJurors == 0) revert NoDisputesThatNeedJurors(); - rng.requestRandomness(); - phase = Phase.generating; - } else if (phase == Phase.generating) { - randomNumber = rng.receiveRandomness(); - if (randomNumber == 0) revert RandomNumberNotReady(); - phase = Phase.drawing; - } else if (phase == Phase.drawing) { - if (disputesWithoutJurors > 0 && block.timestamp - lastPhaseChange < maxDrawingTime) { - revert DisputesWithoutJurorsAndMaxDrawingTimeNotPassed(); - } - phase = Phase.staking; - } - - lastPhaseChange = block.timestamp; - emit NewPhase(phase); - } - - /// @dev Create a sortition sum tree at the specified key. - /// @param _courtID The ID of the court. - /// @param _extraData Extra data that contains the number of children each node in the tree should have. - function createTree(uint96 _courtID, bytes memory _extraData) external override onlyByCore { - TreeKey key = CourtID.wrap(_courtID).toTreeKey(); - uint256 K = _extraDataToTreeK(_extraData); - sortitionSumTrees.createTree(key, K); - } - - /// @dev Executes the next delayed stakes. - /// @param _iterations The number of delayed stakes to execute. - function executeDelayedStakes(uint256 _iterations) external { - if (phase != Phase.staking) revert NotStakingPhase(); - if (delayedStakeWriteIndex < delayedStakeReadIndex) revert NoDelayedStakeToExecute(); - - uint256 actualIterations = (delayedStakeReadIndex + _iterations) - 1 > delayedStakeWriteIndex - ? (delayedStakeWriteIndex - delayedStakeReadIndex) + 1 - : _iterations; - uint256 newDelayedStakeReadIndex = delayedStakeReadIndex + actualIterations; - - for (uint256 i = delayedStakeReadIndex; i < newDelayedStakeReadIndex; i++) { - DelayedStake storage delayedStake = delayedStakes[i]; - core.setStakeBySortitionModule(delayedStake.account, delayedStake.courtID, delayedStake.stake); - delete delayedStakes[i]; - } - delayedStakeReadIndex = newDelayedStakeReadIndex; - } - - function createDisputeHook(uint256 /*_disputeID*/, uint256 /*_roundID*/) external override onlyByCore { - disputesWithoutJurors++; - } - - function postDrawHook(uint256 /*_disputeID*/, uint256 /*_roundID*/) external override onlyByCore { - disputesWithoutJurors--; - } - - /// @dev Saves the random number to use it in sortition. Not used by this contract because the storing of the number is inlined in passPhase(). - /// @param _randomNumber Random number returned by RNG contract. - function notifyRandomNumber(uint256 _randomNumber) public override {} - - /// @dev Validate the specified juror's new stake for a court. - /// Note: no state changes should be made when returning stakingResult != Successful, otherwise delayed stakes might break invariants. - /// @param _account The address of the juror. - /// @param _courtID The ID of the court. - /// @param _newStake The new stake. - /// @param _noDelay True if the stake change should not be delayed. - /// @return pnkDeposit The amount of PNK to be deposited. - /// @return pnkWithdrawal The amount of PNK to be withdrawn. - /// @return stakingResult The result of the staking operation. - function validateStake( - address _account, - uint96 _courtID, - uint256 _newStake, - bool _noDelay - ) external override onlyByCore returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { - (pnkDeposit, pnkWithdrawal, stakingResult) = _validateStake(_account, _courtID, _newStake, _noDelay); - } - - function _validateStake( - address _account, - uint96 _courtID, - uint256 _newStake, - bool _noDelay - ) internal virtual returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { - Juror storage juror = jurors[_account]; - uint256 currentStake = stakeOf(_account, _courtID); - - uint256 nbCourts = juror.courtIDs.length; - if (currentStake == 0 && nbCourts >= MAX_STAKE_PATHS) { - return (0, 0, StakingResult.CannotStakeInMoreCourts); // Prevent staking beyond MAX_STAKE_PATHS but unstaking is always allowed. - } - - if (currentStake == 0 && _newStake == 0) { - return (0, 0, StakingResult.CannotStakeZeroWhenNoStake); // Forbid staking 0 amount when current stake is 0 to avoid flaky behaviour. - } - - if (phase != Phase.staking && !_noDelay) { - // Store the stake change as delayed, to be applied when the phase switches back to Staking. - DelayedStake storage delayedStake = delayedStakes[++delayedStakeWriteIndex]; - delayedStake.account = _account; - delayedStake.courtID = _courtID; - delayedStake.stake = _newStake; - emit StakeDelayed(_account, _courtID, _newStake); - return (pnkDeposit, pnkWithdrawal, StakingResult.Delayed); - } - - // Current phase is Staking: set stakes. - if (_newStake >= currentStake) { - pnkDeposit = _newStake - currentStake; - } else { - pnkWithdrawal = currentStake - _newStake; - // Ensure locked tokens remain in the contract. They can only be released during Execution. - uint256 possibleWithdrawal = juror.stakedPnk > juror.lockedPnk ? juror.stakedPnk - juror.lockedPnk : 0; - if (pnkWithdrawal > possibleWithdrawal) { - pnkWithdrawal = possibleWithdrawal; - } - } - return (pnkDeposit, pnkWithdrawal, StakingResult.Successful); - } - - /// @dev Update the state of the stakes, called by KC at the end of setStake flow. - /// `O(n + p * log_k(j))` where - /// `n` is the number of courts the juror has staked in, - /// `p` is the depth of the court tree, - /// `k` is the minimum number of children per node of one of these courts' sortition sum tree, - /// and `j` is the maximum number of jurors that ever staked in one of these courts simultaneously. - /// @param _account The address of the juror. - /// @param _courtID The ID of the court. - /// @param _pnkDeposit The amount of PNK to be deposited. - /// @param _pnkWithdrawal The amount of PNK to be withdrawn. - /// @param _newStake The new stake. - function setStake( - address _account, - uint96 _courtID, - uint256 _pnkDeposit, - uint256 _pnkWithdrawal, - uint256 _newStake - ) external override onlyByCore { - _setStake(_account, _courtID, _pnkDeposit, _pnkWithdrawal, _newStake); - } - - /// @dev Update the state of the stakes with a PNK reward deposit, called by KC during rewards execution. - /// `O(n + p * log_k(j))` where - /// `n` is the number of courts the juror has staked in, - /// `p` is the depth of the court tree, - /// `k` is the minimum number of children per node of one of these courts' sortition sum tree, - /// and `j` is the maximum number of jurors that ever staked in one of these courts simultaneously. - /// @param _account The address of the juror. - /// @param _courtID The ID of the court. - /// @param _penalty The amount of PNK to be deducted. - /// @return pnkBalance The updated total PNK balance of the juror, including the penalty. - /// @return newCourtStake The updated stake of the juror in the court. - /// @return availablePenalty The amount of PNK that was actually deducted. - function setStakePenalty( - address _account, - uint96 _courtID, - uint256 _penalty - ) external override onlyByCore returns (uint256 pnkBalance, uint256 newCourtStake, uint256 availablePenalty) { - Juror storage juror = jurors[_account]; - availablePenalty = _penalty; - newCourtStake = stakeOf(_account, _courtID); - if (juror.stakedPnk < _penalty) { - availablePenalty = juror.stakedPnk; - } - - if (availablePenalty == 0) return (juror.stakedPnk, newCourtStake, 0); // No penalty to apply. - - uint256 currentStake = stakeOf(_account, _courtID); - uint256 newStake = 0; - if (currentStake >= availablePenalty) { - newStake = currentStake - availablePenalty; - } - _setStake(_account, _courtID, 0, availablePenalty, newStake); - pnkBalance = juror.stakedPnk; // updated by _setStake() - newCourtStake = stakeOf(_account, _courtID); // updated by _setStake() - } - - /// @dev Update the state of the stakes with a PNK reward deposit, called by KC during rewards execution. - /// `O(n + p * log_k(j))` where - /// `n` is the number of courts the juror has staked in, - /// `p` is the depth of the court tree, - /// `k` is the minimum number of children per node of one of these courts' sortition sum tree, - /// and `j` is the maximum number of jurors that ever staked in one of these courts simultaneously. - /// @param _account The address of the juror. - /// @param _courtID The ID of the court. - /// @param _reward The amount of PNK to be deposited as a reward. - function setStakeReward( - address _account, - uint96 _courtID, - uint256 _reward - ) external override onlyByCore returns (bool success) { - if (_reward == 0) return true; // No reward to add. - - uint256 currentStake = stakeOf(_account, _courtID); - if (currentStake == 0) return false; // Juror has been unstaked, don't increase their stake. - - uint256 newStake = currentStake + _reward; - _setStake(_account, _courtID, _reward, 0, newStake); - return true; - } - - function _setStake( - address _account, - uint96 _courtID, - uint256 _pnkDeposit, - uint256 _pnkWithdrawal, - uint256 _newStake - ) internal virtual { - Juror storage juror = jurors[_account]; - if (_pnkDeposit > 0) { - uint256 currentStake = stakeOf(_account, _courtID); - if (currentStake == 0) { - juror.courtIDs.push(_courtID); - } - // Increase juror's balance by deposited amount. - juror.stakedPnk += _pnkDeposit; - } else { - juror.stakedPnk -= _pnkWithdrawal; - if (_newStake == 0) { - // Cleanup - for (uint256 i = juror.courtIDs.length; i > 0; i--) { - if (juror.courtIDs[i - 1] == _courtID) { - juror.courtIDs[i - 1] = juror.courtIDs[juror.courtIDs.length - 1]; - juror.courtIDs.pop(); - break; - } - } - } - } - - // Update the sortition sum tree. - bytes32 stakePathID = SortitionTrees.toStakePathID(_account, _courtID); - bool finished = false; - uint96 currentCourtID = _courtID; - while (!finished) { - // Tokens are also implicitly staked in parent courts through sortition module to increase the chance of being drawn. - TreeKey key = CourtID.wrap(currentCourtID).toTreeKey(); - sortitionSumTrees[key].set(_newStake, stakePathID); - if (currentCourtID == GENERAL_COURT) { - finished = true; - } else { - (currentCourtID, , , , , , ) = core.courts(currentCourtID); // Get the parent court. - } - } - emit StakeSet(_account, _courtID, _newStake, juror.stakedPnk); - } - - function lockStake(address _account, uint256 _relativeAmount) external override onlyByCore { - jurors[_account].lockedPnk += _relativeAmount; - emit StakeLocked(_account, _relativeAmount, false); - } - - function unlockStake(address _account, uint256 _relativeAmount) external override onlyByCore { - Juror storage juror = jurors[_account]; - juror.lockedPnk -= _relativeAmount; - emit StakeLocked(_account, _relativeAmount, true); - - uint256 amount = getJurorLeftoverPNK(_account); - if (amount > 0) { - emit LeftoverPNK(_account, amount); - } - } - - /// @dev Unstakes the inactive juror from all courts. - /// `O(n * (p * log_k(j)) )` where - /// `n` is the number of courts the juror has staked in, - /// `p` is the depth of the court tree, - /// `k` is the minimum number of children per node of one of these courts' sortition sum tree, - /// and `j` is the maximum number of jurors that ever staked in one of these courts simultaneously. - /// @param _account The juror to unstake. - function forcedUnstakeAllCourts(address _account) external override onlyByCore { - uint96[] memory courtIDs = getJurorCourtIDs(_account); - for (uint256 j = courtIDs.length; j > 0; j--) { - core.setStakeBySortitionModule(_account, courtIDs[j - 1], 0); - } - } - - /// @dev Unstakes the inactive juror from a specific court. - /// `O(n * (p * log_k(j)) )` where - /// `n` is the number of courts the juror has staked in, - /// `p` is the depth of the court tree, - /// `k` is the minimum number of children per node of one of these courts' sortition sum tree, - /// and `j` is the maximum number of jurors that ever staked in one of these courts simultaneously. - /// @param _account The juror to unstake. - /// @param _courtID The ID of the court. - function forcedUnstake(address _account, uint96 _courtID) external override onlyByCore { - core.setStakeBySortitionModule(_account, _courtID, 0); - } - - /// @dev Gives back the locked PNKs in case the juror fully unstaked earlier. - /// Note that since locked and staked PNK are async it is possible for the juror to have positive staked PNK balance - /// while having 0 stake in courts and 0 locked tokens (eg. when the juror fully unstaked during dispute and later got his tokens unlocked). - /// In this case the juror can use this function to withdraw the leftover tokens. - /// Also note that if the juror has some leftover PNK while not fully unstaked he'll have to manually unstake from all courts to trigger this function. - /// @param _account The juror whose PNK to withdraw. - function withdrawLeftoverPNK(address _account) external override { - // Can withdraw the leftover PNK if fully unstaked, has no tokens locked and has positive balance. - // This withdrawal can't be triggered by calling setStake() in KlerosCore because current stake is technically 0, thus it is done via separate function. - uint256 amount = getJurorLeftoverPNK(_account); - if (amount == 0) revert NotEligibleForWithdrawal(); - jurors[_account].stakedPnk = 0; - core.transferBySortitionModule(_account, amount); - emit LeftoverPNKWithdrawn(_account, amount); - } - - // ************************************* // - // * Public Views * // - // ************************************* // - - /// @dev Draw an ID from a tree using a number. - /// Note that this function reverts if the sum of all values in the tree is 0. - /// @param _courtID The ID of the court. - /// @param _coreDisputeID Index of the dispute in Kleros Core. - /// @param _nonce Nonce to hash with random number. - /// @return drawnAddress The drawn address. - /// `O(k * log_k(n))` where - /// `k` is the maximum number of children per node in the tree, - /// and `n` is the maximum number of nodes ever appended. - function draw( - uint96 _courtID, - uint256 _coreDisputeID, - uint256 _nonce - ) public view override returns (address drawnAddress, uint96 fromSubcourtID) { - if (phase != Phase.drawing) revert NotDrawingPhase(); - - TreeKey key = CourtID.wrap(_courtID).toTreeKey(); - (drawnAddress, fromSubcourtID) = sortitionSumTrees[key].draw(_coreDisputeID, _nonce, randomNumber); - } - - /// @dev Get the stake of a juror in a court. - /// @param _juror The address of the juror. - /// @param _courtID The ID of the court. - /// @return value The stake of the juror in the court. - function stakeOf(address _juror, uint96 _courtID) public view returns (uint256) { - bytes32 stakePathID = SortitionTrees.toStakePathID(_juror, _courtID); - TreeKey key = CourtID.wrap(_courtID).toTreeKey(); - return sortitionSumTrees[key].stakeOf(stakePathID); - } - - /// @dev Gets the balance of a juror in a court. - /// @param _juror The address of the juror. - /// @param _courtID The ID of the court. - /// @return totalStaked The total amount of tokens staked including locked tokens and penalty deductions. Equivalent to the effective stake in the General court. - /// @return totalLocked The total amount of tokens locked in disputes. - /// @return stakedInCourt The amount of tokens staked in the specified court including locked tokens and penalty deductions. - /// @return nbCourts The number of courts the juror has directly staked in. - function getJurorBalance( - address _juror, - uint96 _courtID - ) - external - view - override - returns (uint256 totalStaked, uint256 totalLocked, uint256 stakedInCourt, uint256 nbCourts) - { - Juror storage juror = jurors[_juror]; - totalStaked = juror.stakedPnk; - totalLocked = juror.lockedPnk; - stakedInCourt = stakeOf(_juror, _courtID); - nbCourts = juror.courtIDs.length; - } - - /// @dev Gets the court identifiers where a specific `_juror` has staked. - /// @param _juror The address of the juror. - function getJurorCourtIDs(address _juror) public view override returns (uint96[] memory) { - return jurors[_juror].courtIDs; - } - - function isJurorStaked(address _juror) external view override returns (bool) { - return jurors[_juror].stakedPnk > 0; - } - - function getJurorLeftoverPNK(address _juror) public view override returns (uint256) { - Juror storage juror = jurors[_juror]; - if (juror.courtIDs.length == 0 && juror.lockedPnk == 0) { - return juror.stakedPnk; - } else { - return 0; - } - } - - // ************************************* // - // * Internal * // - // ************************************* // - - function _extraDataToTreeK(bytes memory _extraData) internal pure returns (uint256 K) { - if (_extraData.length >= 32) { - assembly { - // solium-disable-line security/no-inline-assembly - K := mload(add(_extraData, 0x20)) - } - } else { - K = DEFAULT_K; - } - } - - // ************************************* // - // * Errors * // - // ************************************* // - - error OwnerOnly(); - error KlerosCoreOnly(); - error MinStakingTimeNotPassed(); - error NoDisputesThatNeedJurors(); - error RandomNumberNotReady(); - error DisputesWithoutJurorsAndMaxDrawingTimeNotPassed(); - error NotStakingPhase(); - error NoDelayedStakeToExecute(); - error NotEligibleForWithdrawal(); - error NotDrawingPhase(); -} diff --git a/contracts/src/arbitration/SortitionModuleNeo.sol b/contracts/src/arbitration/SortitionModuleNeo.sol deleted file mode 100644 index 66dbba7e5..000000000 --- a/contracts/src/arbitration/SortitionModuleNeo.sol +++ /dev/null @@ -1,100 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.24; - -import {SortitionModuleBase, KlerosCore, IRNG, StakingResult} from "./SortitionModuleBase.sol"; - -/// @title SortitionModuleNeo -/// @dev A factory of trees that keeps track of staked values for sortition. -contract SortitionModuleNeo is SortitionModuleBase { - string public constant override version = "2.0.0"; - - // ************************************* // - // * Storage * // - // ************************************* // - - uint256 public maxStakePerJuror; - uint256 public maxTotalStaked; - uint256 public totalStaked; - - // ************************************* // - // * Constructor * // - // ************************************* // - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() { - _disableInitializers(); - } - - /// @dev Initializer (constructor equivalent for upgradable contracts). - /// @param _owner The owner. - /// @param _core The KlerosCore. - /// @param _minStakingTime Minimal time to stake - /// @param _maxDrawingTime Time after which the drawing phase can be switched - /// @param _rng The random number generator. - /// @param _maxStakePerJuror The maximum amount of PNK a juror can stake in a court. - /// @param _maxTotalStaked The maximum amount of PNK that can be staked in all courts. - function initialize( - address _owner, - KlerosCore _core, - uint256 _minStakingTime, - uint256 _maxDrawingTime, - IRNG _rng, - uint256 _maxStakePerJuror, - uint256 _maxTotalStaked - ) external reinitializer(2) { - __SortitionModuleBase_initialize(_owner, _core, _minStakingTime, _maxDrawingTime, _rng); - maxStakePerJuror = _maxStakePerJuror; - maxTotalStaked = _maxTotalStaked; - } - - // ************************************* // - // * Governance * // - // ************************************* // - - /// @dev Access Control to perform implementation upgrades (UUPS Proxiable) - /// Only the owner can perform upgrades (`onlyByOwner`) - function _authorizeUpgrade(address) internal view override onlyByOwner { - // NOP - } - - function changeMaxStakePerJuror(uint256 _maxStakePerJuror) external onlyByOwner { - maxStakePerJuror = _maxStakePerJuror; - } - - function changeMaxTotalStaked(uint256 _maxTotalStaked) external onlyByOwner { - maxTotalStaked = _maxTotalStaked; - } - - // ************************************* // - // * State Modifiers * // - // ************************************* // - - function _validateStake( - address _account, - uint96 _courtID, - uint256 _newStake, - bool _noDelay - ) internal override onlyByCore returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { - uint256 currentStake = stakeOf(_account, _courtID); - bool stakeIncrease = _newStake > currentStake; - uint256 stakeChange = stakeIncrease ? _newStake - currentStake : currentStake - _newStake; - Juror storage juror = jurors[_account]; - if (stakeIncrease) { - if (juror.stakedPnk + stakeChange > maxStakePerJuror || currentStake + stakeChange > maxStakePerJuror) { - return (0, 0, StakingResult.CannotStakeMoreThanMaxStakePerJuror); - } - if (totalStaked + stakeChange > maxTotalStaked) { - return (0, 0, StakingResult.CannotStakeMoreThanMaxTotalStaked); - } - } - if (phase == Phase.staking) { - if (stakeIncrease) { - totalStaked += stakeChange; - } else { - totalStaked -= stakeChange; - } - } - (pnkDeposit, pnkWithdrawal, stakingResult) = super._validateStake(_account, _courtID, _newStake, _noDelay); - } -} diff --git a/contracts/test/foundry/KlerosCore_Drawing.t.sol b/contracts/test/foundry/KlerosCore_Drawing.t.sol index b8c644392..9bffbc27d 100644 --- a/contracts/test/foundry/KlerosCore_Drawing.t.sol +++ b/contracts/test/foundry/KlerosCore_Drawing.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.24; import {KlerosCore_TestBase} from "./KlerosCore_TestBase.sol"; import {KlerosCore} from "../../src/arbitration/KlerosCore.sol"; -import {SortitionModuleBase} from "../../src/arbitration/SortitionModuleBase.sol"; +import {SortitionModule} from "../../src/arbitration/SortitionModule.sol"; import {ISortitionModule} from "../../src/arbitration/interfaces/ISortitionModule.sol"; import "../../src/libraries/Constants.sol"; @@ -24,7 +24,7 @@ contract KlerosCore_DrawingTest is KlerosCore_TestBase { sortitionModule.passPhase(); // Drawing phase vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeLocked(staker1, 1000, false); + emit SortitionModule.StakeLocked(staker1, 1000, false); vm.expectEmit(true, true, true, true); emit KlerosCore.Draw(staker1, disputeID, roundID, 0); // VoteID = 0 diff --git a/contracts/test/foundry/KlerosCore_Execution.t.sol b/contracts/test/foundry/KlerosCore_Execution.t.sol index 33b2cda65..01efd6409 100644 --- a/contracts/test/foundry/KlerosCore_Execution.t.sol +++ b/contracts/test/foundry/KlerosCore_Execution.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.24; import {KlerosCore_TestBase} from "./KlerosCore_TestBase.sol"; import {KlerosCore} from "../../src/arbitration/KlerosCore.sol"; -import {SortitionModuleBase} from "../../src/arbitration/SortitionModuleBase.sol"; +import {SortitionModule} from "../../src/arbitration/SortitionModule.sol"; import {DisputeKitClassicBase} from "../../src/arbitration/dispute-kits/DisputeKitClassicBase.sol"; import {IArbitratorV2, IArbitrableV2} from "../../src/arbitration/KlerosCore.sol"; import {IERC20} from "../../src/libraries/SafeERC20.sol"; @@ -96,16 +96,16 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { ); vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeLocked(staker1, 1000, true); + emit SortitionModule.StakeLocked(staker1, 1000, true); vm.expectEmit(true, true, true, true); emit KlerosCore.TokenAndETHShift(staker1, disputeID, 0, 0, -int256(1000), 0, IERC20(address(0))); // Check iterations for the winning staker to see the shifts vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeLocked(staker2, 0, true); + emit SortitionModule.StakeLocked(staker2, 0, true); vm.expectEmit(true, true, true, true); emit KlerosCore.TokenAndETHShift(staker2, disputeID, 0, 10000, 0, 0, IERC20(address(0))); vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeLocked(staker2, 0, true); + emit SortitionModule.StakeLocked(staker2, 0, true); vm.expectEmit(true, true, true, true); emit KlerosCore.TokenAndETHShift(staker2, disputeID, 0, 10000, 0, 0, IERC20(address(0))); core.execute(disputeID, 0, 3); // Do 3 iterations to check penalties first @@ -121,16 +121,16 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { assertEq(round.pnkPenalties, 1000, "Wrong pnkPenalties"); vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeLocked(staker1, 0, true); + emit SortitionModule.StakeLocked(staker1, 0, true); vm.expectEmit(true, true, true, true); emit KlerosCore.TokenAndETHShift(staker1, disputeID, 0, 0, 0, 0, IERC20(address(0))); // Check iterations for the winning staker to see the shifts vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeLocked(staker2, 1000, true); + emit SortitionModule.StakeLocked(staker2, 1000, true); vm.expectEmit(true, true, true, true); emit KlerosCore.TokenAndETHShift(staker2, disputeID, 0, 10000, 500, 0.045 ether, IERC20(address(0))); vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeLocked(staker2, 1000, true); + emit SortitionModule.StakeLocked(staker2, 1000, true); vm.expectEmit(true, true, true, true); emit KlerosCore.TokenAndETHShift(staker2, disputeID, 0, 10000, 500, 0.045 ether, IERC20(address(0))); core.execute(disputeID, 0, 10); // Finish the iterations. We need only 3 but check that it corrects the count. @@ -272,11 +272,11 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { // Note that these events are emitted only after the first iteration of execute() therefore the juror has been penalized only for 1000 PNK her. vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeSet(staker1, newCourtID, 19000, 39000); // 1000 PNK penalty for voteID 0 + emit SortitionModule.StakeSet(staker1, newCourtID, 19000, 39000); // 1000 PNK penalty for voteID 0 vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeSet(staker1, newCourtID, 0, 20000); // Starting with 40000 we first nullify the stake and remove 19000 and then remove penalty once since there was only first iteration (40000 - 20000 - 1000) + emit SortitionModule.StakeSet(staker1, newCourtID, 0, 20000); // Starting with 40000 we first nullify the stake and remove 19000 and then remove penalty once since there was only first iteration (40000 - 20000 - 1000) vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeSet(staker1, GENERAL_COURT, 0, 2000); // 2000 PNK should remain in balance to cover penalties since the first 1000 of locked pnk was already unlocked + emit SortitionModule.StakeSet(staker1, GENERAL_COURT, 0, 2000); // 2000 PNK should remain in balance to cover penalties since the first 1000 of locked pnk was already unlocked core.execute(disputeID, 0, 3); assertEq(pinakion.balanceOf(address(core)), 0, "Wrong token balance of the core"); @@ -335,7 +335,7 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { core.passPeriod(disputeID); // Execution vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeSet(staker1, GENERAL_COURT, 0, 0); // Juror should have no stake left and should be unstaked from the court automatically. + emit SortitionModule.StakeSet(staker1, GENERAL_COURT, 0, 0); // Juror should have no stake left and should be unstaked from the court automatically. core.execute(disputeID, 0, 6); assertEq(pinakion.balanceOf(address(core)), 0, "Wrong token balance of the core"); @@ -397,7 +397,7 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { core.passPeriod(disputeID); // Execution vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeSet(staker1, GENERAL_COURT, 0, 0); // Juror balance should be below minStake and should be unstaked from the court automatically. + emit SortitionModule.StakeSet(staker1, GENERAL_COURT, 0, 0); // Juror balance should be below minStake and should be unstaked from the court automatically. core.execute(disputeID, 0, 6); assertEq(pinakion.balanceOf(address(core)), 11000, "Wrong token balance of the core"); @@ -460,11 +460,11 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { assertEq(pinakion.balanceOf(address(core)), 1000, "Wrong token balance of the core"); assertEq(pinakion.balanceOf(staker1), 999999999999999000, "Wrong token balance of staker1"); - vm.expectRevert(SortitionModuleBase.NotEligibleForWithdrawal.selector); + vm.expectRevert(SortitionModule.NotEligibleForWithdrawal.selector); sortitionModule.withdrawLeftoverPNK(staker1); vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.LeftoverPNK(staker1, 1000); + emit SortitionModule.LeftoverPNK(staker1, 1000); core.execute(disputeID, 0, 6); (totalStaked, totalLocked, , ) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); @@ -485,7 +485,7 @@ contract KlerosCore_ExecutionTest is KlerosCore_TestBase { core.transferBySortitionModule(staker1, 1000); vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.LeftoverPNKWithdrawn(staker1, 1000); + emit SortitionModule.LeftoverPNKWithdrawn(staker1, 1000); sortitionModule.withdrawLeftoverPNK(staker1); (totalStaked, , , ) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); diff --git a/contracts/test/foundry/KlerosCore_Initialization.t.sol b/contracts/test/foundry/KlerosCore_Initialization.t.sol index d9113bbb7..d6df4a932 100644 --- a/contracts/test/foundry/KlerosCore_Initialization.t.sol +++ b/contracts/test/foundry/KlerosCore_Initialization.t.sol @@ -123,12 +123,14 @@ contract KlerosCore_InitializationTest is KlerosCore_TestBase { DisputeKitClassic newDisputeKit = DisputeKitClassic(address(proxyDk)); bytes memory initDataSm = abi.encodeWithSignature( - "initialize(address,address,uint256,uint256,address)", + "initialize(address,address,uint256,uint256,address,uint256,uint256)", newOwner, address(proxyCore), newMinStakingTime, newMaxDrawingTime, - newRng + newRng, + type(int256).max, + type(int256).max ); UUPSProxy proxySm = new UUPSProxy(address(smLogic), initDataSm); diff --git a/contracts/test/foundry/KlerosCore_RNG.t.sol b/contracts/test/foundry/KlerosCore_RNG.t.sol index da3df03ed..0afd0ba43 100644 --- a/contracts/test/foundry/KlerosCore_RNG.t.sol +++ b/contracts/test/foundry/KlerosCore_RNG.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.24; import {KlerosCore_TestBase} from "./KlerosCore_TestBase.sol"; -import {SortitionModuleBase} from "../../src/arbitration/SortitionModuleBase.sol"; +import {SortitionModule} from "../../src/arbitration/SortitionModule.sol"; import {RNGWithFallback, IRNG} from "../../src/rng/RNGWithFallback.sol"; import {RNGMock} from "../../src/test/RNGMock.sol"; import "../../src/libraries/Constants.sol"; @@ -34,7 +34,7 @@ contract KlerosCore_RNGTest is KlerosCore_TestBase { sortitionModule.passPhase(); // Generating assertEq(rngFallback.requestTimestamp(), block.timestamp, "Wrong request timestamp"); - vm.expectRevert(SortitionModuleBase.RandomNumberNotReady.selector); + vm.expectRevert(SortitionModule.RandomNumberNotReady.selector); sortitionModule.passPhase(); vm.warp(block.timestamp + fallbackTimeout + 1); diff --git a/contracts/test/foundry/KlerosCore_Staking.t.sol b/contracts/test/foundry/KlerosCore_Staking.t.sol index 805b8ae7e..070a38716 100644 --- a/contracts/test/foundry/KlerosCore_Staking.t.sol +++ b/contracts/test/foundry/KlerosCore_Staking.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.24; import {KlerosCore_TestBase} from "./KlerosCore_TestBase.sol"; import {KlerosCore} from "../../src/arbitration/KlerosCore.sol"; -import {SortitionModuleBase} from "../../src/arbitration/SortitionModuleBase.sol"; +import {SortitionModule} from "../../src/arbitration/SortitionModule.sol"; import {ISortitionModule} from "../../src/arbitration/interfaces/ISortitionModule.sol"; import {IKlerosCore, KlerosCoreSnapshotProxy} from "../../src/arbitration/view/KlerosCoreSnapshotProxy.sol"; import "../../src/libraries/Constants.sol"; @@ -39,7 +39,7 @@ contract KlerosCore_StakingTest is KlerosCore_TestBase { vm.prank(staker1); vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeSet(staker1, GENERAL_COURT, 1001, 1001); + emit SortitionModule.StakeSet(staker1, GENERAL_COURT, 1001, 1001); core.setStake(GENERAL_COURT, 1001); (uint256 totalStaked, uint256 totalLocked, uint256 stakedInCourt, uint256 nbCourts) = sortitionModule @@ -65,7 +65,7 @@ contract KlerosCore_StakingTest is KlerosCore_TestBase { // Increase stake one more time to verify the correct behavior vm.prank(staker1); vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeSet(staker1, GENERAL_COURT, 2000, 2000); + emit SortitionModule.StakeSet(staker1, GENERAL_COURT, 2000, 2000); core.setStake(GENERAL_COURT, 2000); (totalStaked, totalLocked, stakedInCourt, nbCourts) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); @@ -188,7 +188,7 @@ contract KlerosCore_StakingTest is KlerosCore_TestBase { vm.prank(staker1); vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeDelayed(staker1, GENERAL_COURT, 1500); + emit SortitionModule.StakeDelayed(staker1, GENERAL_COURT, 1500); core.setStake(GENERAL_COURT, 1500); uint256 delayedStakeId = sortitionModule.delayedStakeWriteIndex(); @@ -234,7 +234,7 @@ contract KlerosCore_StakingTest is KlerosCore_TestBase { vm.prank(staker1); vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeDelayed(staker1, GENERAL_COURT, 1800); + emit SortitionModule.StakeDelayed(staker1, GENERAL_COURT, 1800); core.setStake(GENERAL_COURT, 1800); (uint256 totalStaked, , uint256 stakedInCourt, ) = sortitionModule.getJurorBalance(staker1, GENERAL_COURT); @@ -301,7 +301,7 @@ contract KlerosCore_StakingTest is KlerosCore_TestBase { vm.prank(staker2); core.setStake(GENERAL_COURT, 10000); - vm.expectRevert(SortitionModuleBase.NoDelayedStakeToExecute.selector); + vm.expectRevert(SortitionModule.NoDelayedStakeToExecute.selector); sortitionModule.executeDelayedStakes(5); // Set the stake and create a dispute to advance the phase @@ -314,13 +314,13 @@ contract KlerosCore_StakingTest is KlerosCore_TestBase { uint256 disputeID = 0; core.draw(disputeID, DEFAULT_NB_OF_JURORS); - vm.expectRevert(SortitionModuleBase.NotStakingPhase.selector); + vm.expectRevert(SortitionModule.NotStakingPhase.selector); sortitionModule.executeDelayedStakes(5); // Create delayed stake vm.prank(staker1); vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeDelayed(staker1, GENERAL_COURT, 1500); + emit SortitionModule.StakeDelayed(staker1, GENERAL_COURT, 1500); core.setStake(GENERAL_COURT, 1500); assertEq(pinakion.balanceOf(address(core)), 10000, "Wrong token balance of the core"); // Balance should not increase because the stake was delayed @@ -329,14 +329,14 @@ contract KlerosCore_StakingTest is KlerosCore_TestBase { // Create delayed stake for another staker vm.prank(staker2); vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeDelayed(staker2, GENERAL_COURT, 0); + emit SortitionModule.StakeDelayed(staker2, GENERAL_COURT, 0); core.setStake(GENERAL_COURT, 0); assertEq(pinakion.balanceOf(staker2), 999999999999990000, "Wrong token balance of staker2"); // Balance should not change since wrong phase // Create another delayed stake for staker1 on top of it to check the execution vm.prank(staker1); vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeDelayed(staker1, GENERAL_COURT, 1800); + emit SortitionModule.StakeDelayed(staker1, GENERAL_COURT, 1800); core.setStake(GENERAL_COURT, 1800); assertEq(sortitionModule.delayedStakeWriteIndex(), 3, "Wrong delayedStakeWriteIndex"); @@ -384,9 +384,9 @@ contract KlerosCore_StakingTest is KlerosCore_TestBase { // 2 events should be emitted but the 2nd stake supersedes the first one in the end. vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeSet(staker1, GENERAL_COURT, 1500, 1500); + emit SortitionModule.StakeSet(staker1, GENERAL_COURT, 1500, 1500); vm.expectEmit(true, true, true, true); - emit SortitionModuleBase.StakeSet(staker1, GENERAL_COURT, 1800, 1800); + emit SortitionModule.StakeSet(staker1, GENERAL_COURT, 1800, 1800); sortitionModule.executeDelayedStakes(20); // Deliberately ask for more iterations than needed assertEq(sortitionModule.delayedStakeWriteIndex(), 3, "Wrong delayedStakeWriteIndex"); diff --git a/contracts/test/foundry/KlerosCore_TestBase.sol b/contracts/test/foundry/KlerosCore_TestBase.sol index 612db5bb7..17132cb28 100644 --- a/contracts/test/foundry/KlerosCore_TestBase.sol +++ b/contracts/test/foundry/KlerosCore_TestBase.sol @@ -9,7 +9,7 @@ import {IDisputeKit} from "../../src/arbitration/interfaces/IDisputeKit.sol"; import {DisputeKitClassic, DisputeKitClassicBase} from "../../src/arbitration/dispute-kits/DisputeKitClassic.sol"; import {DisputeKitSybilResistant} from "../../src/arbitration/dispute-kits/DisputeKitSybilResistant.sol"; import {ISortitionModule} from "../../src/arbitration/interfaces/ISortitionModule.sol"; -import {SortitionModuleMock, SortitionModuleBase} from "../../src/test/SortitionModuleMock.sol"; +import {SortitionModuleMock, SortitionModule} from "../../src/test/SortitionModuleMock.sol"; import {UUPSProxy} from "../../src/proxy/UUPSProxy.sol"; import {BlockHashRNG} from "../../src/rng/BlockHashRNG.sol"; import {RNGWithFallback, IRNG} from "../../src/rng/RNGWithFallback.sol"; @@ -126,12 +126,14 @@ abstract contract KlerosCore_TestBase is Test { disputeKit = DisputeKitClassic(address(proxyDk)); bytes memory initDataSm = abi.encodeWithSignature( - "initialize(address,address,uint256,uint256,address)", + "initialize(address,address,uint256,uint256,address,uint256,uint256)", owner, address(proxyCore), minStakingTime, maxDrawingTime, - rng + rng, + type(int256).max, + type(int256).max ); UUPSProxy proxySm = new UUPSProxy(address(smLogic), initDataSm); From 758a809a6237ed2d171adb214b23a6c629a64613 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Thu, 4 Sep 2025 19:17:02 +0100 Subject: [PATCH 05/11] test: extra init asserts in foundry tests --- contracts/test/foundry/KlerosCore_Initialization.t.sol | 10 ++++++++-- contracts/test/foundry/KlerosCore_TestBase.sol | 4 ++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/contracts/test/foundry/KlerosCore_Initialization.t.sol b/contracts/test/foundry/KlerosCore_Initialization.t.sol index d6df4a932..7e521063d 100644 --- a/contracts/test/foundry/KlerosCore_Initialization.t.sol +++ b/contracts/test/foundry/KlerosCore_Initialization.t.sol @@ -45,6 +45,9 @@ contract KlerosCore_InitializationTest is KlerosCore_TestBase { assertEq(core.isSupported(GENERAL_COURT, NULL_DISPUTE_KIT), false, "General court null dk should be false"); assertEq(core.isSupported(GENERAL_COURT, DISPUTE_KIT_CLASSIC), true, "General court classic dk should be true"); assertEq(core.paused(), false, "Wrong paused value"); + assertEq(core.wNative(), address(wNative), "Wrong wNative"); + assertEq(address(core.jurorNft()), address(0), "Wrong jurorNft"); + assertEq(core.arbitrableWhitelistEnabled(), false, "Wrong arbitrableWhitelistEnabled"); assertEq(pinakion.name(), "Pinakion", "Wrong token name"); assertEq(pinakion.symbol(), "PNK", "Wrong token symbol"); @@ -71,6 +74,9 @@ contract KlerosCore_InitializationTest is KlerosCore_TestBase { assertEq(sortitionModule.randomNumber(), 0, "randomNumber should be 0"); assertEq(sortitionModule.delayedStakeWriteIndex(), 0, "delayedStakeWriteIndex should be 0"); assertEq(sortitionModule.delayedStakeReadIndex(), 1, "Wrong delayedStakeReadIndex"); + assertEq(sortitionModule.maxStakePerJuror(), type(uint256).max, "Wrong maxStakePerJuror"); + assertEq(sortitionModule.maxTotalStaked(), type(uint256).max, "Wrong maxTotalStaked"); + assertEq(sortitionModule.totalStaked(), 0, "Wrong totalStaked"); (uint256 K, uint256 nodeLength) = sortitionModule.getSortitionProperties(bytes32(uint256(FORKING_COURT))); assertEq(K, 5, "Wrong tree K FORKING_COURT"); @@ -129,8 +135,8 @@ contract KlerosCore_InitializationTest is KlerosCore_TestBase { newMinStakingTime, newMaxDrawingTime, newRng, - type(int256).max, - type(int256).max + type(uint256).max, + type(uint256).max ); UUPSProxy proxySm = new UUPSProxy(address(smLogic), initDataSm); diff --git a/contracts/test/foundry/KlerosCore_TestBase.sol b/contracts/test/foundry/KlerosCore_TestBase.sol index 17132cb28..9418f2444 100644 --- a/contracts/test/foundry/KlerosCore_TestBase.sol +++ b/contracts/test/foundry/KlerosCore_TestBase.sol @@ -132,8 +132,8 @@ abstract contract KlerosCore_TestBase is Test { minStakingTime, maxDrawingTime, rng, - type(int256).max, - type(int256).max + type(uint256).max, + type(uint256).max ); UUPSProxy proxySm = new UUPSProxy(address(smLogic), initDataSm); From 7a5d08df33f6872630ba38e0b880b7aad5bf137e Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Thu, 4 Sep 2025 22:55:57 +0100 Subject: [PATCH 06/11] fix: hardhat tests, deploy scripts, unnecessary virtual keyword --- .../deploy/00-home-chain-arbitration-neo.ts | 24 +++++++++---------- contracts/deploy/00-home-chain-arbitration.ts | 11 ++++++++- contracts/src/arbitration/KlerosCore.sol | 12 +++++++--- contracts/src/arbitration/SortitionModule.sol | 9 ++++--- contracts/test/arbitration/staking-neo.ts | 16 ++++++------- 5 files changed, 42 insertions(+), 30 deletions(-) diff --git a/contracts/deploy/00-home-chain-arbitration-neo.ts b/contracts/deploy/00-home-chain-arbitration-neo.ts index d5d8a136c..879573676 100644 --- a/contracts/deploy/00-home-chain-arbitration-neo.ts +++ b/contracts/deploy/00-home-chain-arbitration-neo.ts @@ -6,7 +6,7 @@ import { changeCurrencyRate } from "./utils/klerosCoreHelper"; import { HomeChains, isSkipped, isDevnet, PNK, ETH } from "./utils"; import { getContractOrDeploy, getContractOrDeployUpgradable } from "./utils/getContractOrDeploy"; import { deployERC20AndFaucet, deployERC721 } from "./utils/deployTokens"; -import { ChainlinkRNG, DisputeKitClassic, KlerosCoreNeo, RNGWithFallback } from "../typechain-types"; +import { ChainlinkRNG, DisputeKitClassic, KlerosCore, RNGWithFallback } from "../typechain-types"; const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const { ethers, deployments, getNamedAccounts, getChainId } = hre; @@ -29,18 +29,17 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) await deployUpgradable(deployments, "EvidenceModule", { from: deployer, args: [deployer], log: true }); const classicDisputeKitID = 1; // Classic DK - const disputeKit = await deployUpgradable(deployments, "DisputeKitClassicNeo", { + const disputeKit = await deployUpgradable(deployments, "DisputeKitClassic", { from: deployer, - contract: "DisputeKitClassic", args: [deployer, ZeroAddress, weth.target, classicDisputeKitID], log: true, }); - let klerosCoreAddress = await deployments.getOrNull("KlerosCoreNeo").then((deployment) => deployment?.address); + let klerosCoreAddress = await deployments.getOrNull("KlerosCore").then((deployment) => deployment?.address); if (!klerosCoreAddress) { const nonce = await ethers.provider.getTransactionCount(deployer); klerosCoreAddress = getContractAddress(deployer, nonce + 3); // deployed on the 4th tx (nonce+3): SortitionModule Impl tx, SortitionModule Proxy tx, KlerosCore Impl tx, KlerosCore Proxy tx - console.log("calculated future KlerosCoreNeo address for nonce %d: %s", nonce + 3, klerosCoreAddress); + console.log("calculated future KlerosCore address for nonce %d: %s", nonce + 3, klerosCoreAddress); } const devnet = isDevnet(hre.network); const minStakingTime = devnet ? 180 : 1800; @@ -48,7 +47,7 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) const rngWithFallback = await ethers.getContract("RNGWithFallback"); const maxStakePerJuror = PNK(2_000); const maxTotalStaked = PNK(2_000_000); - const sortitionModule = await deployUpgradable(deployments, "SortitionModuleNeo", { + const sortitionModule = await deployUpgradable(deployments, "SortitionModule", { from: deployer, args: [ deployer, @@ -66,7 +65,7 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) const alpha = 10000; const feeForJuror = ETH(0.1); const jurorsForCourtJump = 256; - const klerosCore = await deployUpgradable(deployments, "KlerosCoreNeo", { + const klerosCore = await deployUpgradable(deployments, "KlerosCore", { from: deployer, args: [ deployer, @@ -79,14 +78,14 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) [0, 0, 0, 10], // evidencePeriod, commitPeriod, votePeriod, appealPeriod ethers.toBeHex(5), // Extra data for sortition module will return the default value of K sortitionModule.address, - nft.target, weth.target, + nft.target, ], log: true, }); // nonce+2 (implementation), nonce+3 (proxy) // disputeKit.changeCore() only if necessary - const disputeKitContract = await hre.ethers.getContract("DisputeKitClassicNeo"); + const disputeKitContract = await hre.ethers.getContract("DisputeKitClassic"); const currentCore = await disputeKitContract.core(); if (currentCore !== klerosCore.address) { console.log(`disputeKit.changeCore(${klerosCore.address})`); @@ -100,7 +99,7 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) await rngWithFallback.changeConsumer(sortitionModule.address); } - const core = await hre.ethers.getContract("KlerosCoreNeo"); + const core = await hre.ethers.getContract("KlerosCore"); try { await changeCurrencyRate(core, await weth.getAddress(), true, 1, 1); } catch (e) { @@ -113,9 +112,8 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) log: true, }); - const resolver = await deploy("DisputeResolverNeo", { + const resolver = await deploy("DisputeResolver", { from: deployer, - contract: "DisputeResolver", args: [core.target, disputeTemplateRegistry.target], log: true, }); @@ -157,7 +155,7 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) }); }; -deployArbitration.tags = ["ArbitrationNeo"]; +deployArbitration.tags = ["ArbitrationMainnet"]; deployArbitration.dependencies = ["ChainlinkRNG"]; deployArbitration.skip = async ({ network }) => { return isSkipped(network, !HomeChains[network.config.chainId ?? 0]); diff --git a/contracts/deploy/00-home-chain-arbitration.ts b/contracts/deploy/00-home-chain-arbitration.ts index 25291052d..72334eb77 100644 --- a/contracts/deploy/00-home-chain-arbitration.ts +++ b/contracts/deploy/00-home-chain-arbitration.ts @@ -53,7 +53,15 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) const rngWithFallback = await ethers.getContract("RNGWithFallback"); const sortitionModule = await deployUpgradable(deployments, "SortitionModule", { from: deployer, - args: [deployer, klerosCoreAddress, minStakingTime, maxFreezingTime, rngWithFallback.target], + args: [ + deployer, + klerosCoreAddress, + minStakingTime, + maxFreezingTime, + rngWithFallback.target, + ethers.MaxUint256, // maxStakePerJuror + ethers.MaxUint256, // maxTotalStaked + ], log: true, }); // nonce (implementation), nonce+1 (proxy) @@ -75,6 +83,7 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) ethers.toBeHex(5), // Extra data for sortition module will return the default value of K sortitionModule.address, weth.target, + ZeroAddress, // jurorNft ], log: true, }); // nonce+2 (implementation), nonce+3 (proxy) diff --git a/contracts/src/arbitration/KlerosCore.sol b/contracts/src/arbitration/KlerosCore.sol index f7d6cf493..a8c78a910 100644 --- a/contracts/src/arbitration/KlerosCore.sol +++ b/contracts/src/arbitration/KlerosCore.sol @@ -487,6 +487,12 @@ contract KlerosCore is IArbitratorV2, Initializable, UUPSProxiable { emit NewCurrencyRate(_feeToken, _rateInEth, _rateDecimals); } + /// @dev Changes the `jurorNft` storage variable. + /// @param _jurorNft The new value for the `jurorNft` storage variable. + function changeJurorNft(IERC721 _jurorNft) external onlyByOwner { + jurorNft = _jurorNft; + } + /// @dev Adds or removes an arbitrable from whitelist. /// @param _arbitrable Arbitrable address. /// @param _allowed Whether add or remove permission. @@ -507,7 +513,7 @@ contract KlerosCore is IArbitratorV2, Initializable, UUPSProxiable { /// @param _courtID The ID of the court. /// @param _newStake The new stake. /// Note that the existing delayed stake will be nullified as non-relevant. - function setStake(uint96 _courtID, uint256 _newStake) external virtual whenNotPaused { + function setStake(uint96 _courtID, uint256 _newStake) external whenNotPaused { if (address(jurorNft) != address(0) && jurorNft.balanceOf(msg.sender) == 0) revert NotEligibleForStaking(); _setStake(msg.sender, _courtID, _newStake, false, OnError.Revert); } @@ -559,7 +565,7 @@ contract KlerosCore is IArbitratorV2, Initializable, UUPSProxiable { bytes memory _extraData, IERC20 _feeToken, uint256 _feeAmount - ) internal virtual returns (uint256 disputeID) { + ) internal returns (uint256 disputeID) { if (arbitrableWhitelistEnabled && !arbitrableWhitelist[msg.sender]) revert ArbitrableNotWhitelisted(); (uint96 courtID, , uint256 disputeKitID) = _extraDataToCourtIDMinJurorsDisputeKit(_extraData); if (!courts[courtID].supportedDisputeKits[disputeKitID]) revert DisputeKitNotSupportedByCourt(); @@ -1236,7 +1242,7 @@ contract KlerosCore is IArbitratorV2, Initializable, UUPSProxiable { } /// @dev It may revert depending on the _onError parameter. - function _stakingFailed(OnError _onError, StakingResult _result) internal pure virtual { + function _stakingFailed(OnError _onError, StakingResult _result) internal pure { if (_onError == OnError.Return) return; if (_result == StakingResult.StakingTransferFailed) revert StakingTransferFailed(); if (_result == StakingResult.UnstakingTransferFailed) revert UnstakingTransferFailed(); diff --git a/contracts/src/arbitration/SortitionModule.sol b/contracts/src/arbitration/SortitionModule.sol index 8c0f4de86..bf61255ae 100644 --- a/contracts/src/arbitration/SortitionModule.sol +++ b/contracts/src/arbitration/SortitionModule.sol @@ -278,7 +278,7 @@ contract SortitionModule is ISortitionModule, Initializable, UUPSProxiable { uint96 _courtID, uint256 _newStake, bool _noDelay - ) internal virtual returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { + ) internal returns (uint256 pnkDeposit, uint256 pnkWithdrawal, StakingResult stakingResult) { Juror storage juror = jurors[_account]; uint256 currentStake = stakeOf(_account, _courtID); bool stakeIncrease = _newStake > currentStake; @@ -319,13 +319,12 @@ contract SortitionModule is ISortitionModule, Initializable, UUPSProxiable { totalStaked += stakeChange; } else { pnkWithdrawal = stakeChange; - totalStaked -= stakeChange; - - // Ensure locked tokens remain in the contract. They can only be released during Execution. uint256 possibleWithdrawal = juror.stakedPnk > juror.lockedPnk ? juror.stakedPnk - juror.lockedPnk : 0; if (pnkWithdrawal > possibleWithdrawal) { + // Ensure locked tokens remain in the contract. They can only be released during Execution. pnkWithdrawal = possibleWithdrawal; } + totalStaked -= pnkWithdrawal; } return (pnkDeposit, pnkWithdrawal, StakingResult.Successful); } @@ -417,7 +416,7 @@ contract SortitionModule is ISortitionModule, Initializable, UUPSProxiable { uint256 _pnkDeposit, uint256 _pnkWithdrawal, uint256 _newStake - ) internal virtual { + ) internal { Juror storage juror = jurors[_account]; if (_pnkDeposit > 0) { uint256 currentStake = stakeOf(_account, _courtID); diff --git a/contracts/test/arbitration/staking-neo.ts b/contracts/test/arbitration/staking-neo.ts index fb36787d8..9cd83efeb 100644 --- a/contracts/test/arbitration/staking-neo.ts +++ b/contracts/test/arbitration/staking-neo.ts @@ -3,8 +3,8 @@ import { PNK, RandomizerRNG, RandomizerMock, - SortitionModuleNeo, - KlerosCoreNeo, + SortitionModule, + KlerosCore, TestERC721, DisputeResolver, ChainlinkRNG, @@ -41,8 +41,8 @@ describe("Staking", async () => { let juror: HardhatEthersSigner; let guardian: HardhatEthersSigner; let pnk: PNK; - let core: KlerosCoreNeo; - let sortition: SortitionModuleNeo; + let core: KlerosCore; + let sortition: SortitionModule; let rng: ChainlinkRNG; let vrfCoordinator: ChainlinkVRFCoordinatorV2Mock; let nft: TestERC721; @@ -52,16 +52,16 @@ describe("Staking", async () => { const deployUnhappy = async () => { ({ deployer } = await getNamedAccounts()); - await deployments.fixture(["ArbitrationNeo"], { + await deployments.fixture(["ArbitrationMainnet"], { fallbackToGlobal: true, keepExistingDeployments: false, }); pnk = await ethers.getContract("PNK"); - core = await ethers.getContract("KlerosCoreNeo"); - sortition = await ethers.getContract("SortitionModuleNeo"); + core = await ethers.getContract("KlerosCore"); + sortition = await ethers.getContract("SortitionModule"); rng = await ethers.getContract("ChainlinkRNG"); vrfCoordinator = await ethers.getContract("ChainlinkVRFCoordinator"); - resolver = await ethers.getContract("DisputeResolverNeo"); + resolver = await ethers.getContract("DisputeResolver"); nft = await ethers.getContract("KlerosV2NeoEarlyUser"); // Juror signer setup and funding From e9f8a0a186f1cbf1ad3c5853e4035f931d7cd894 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Thu, 4 Sep 2025 22:58:16 +0100 Subject: [PATCH 07/11] refactor: renamed 00-home-chain-arbitration-neo into 00-home-chain-arbitration-mainnet --- ...in-arbitration-neo.ts => 00-home-chain-arbitration-mainnet.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename contracts/deploy/{00-home-chain-arbitration-neo.ts => 00-home-chain-arbitration-mainnet.ts} (100%) diff --git a/contracts/deploy/00-home-chain-arbitration-neo.ts b/contracts/deploy/00-home-chain-arbitration-mainnet.ts similarity index 100% rename from contracts/deploy/00-home-chain-arbitration-neo.ts rename to contracts/deploy/00-home-chain-arbitration-mainnet.ts From 923bd7bdc2f3af082c050a1d2247f55347e72edd Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Thu, 4 Sep 2025 23:14:58 +0100 Subject: [PATCH 08/11] chore: removed traces of Neo in many scripts and filenames --- ...ainnet-neo.json => courts.v2.mainnet.json} | 0 ...nnet-neo.json => policies.v2.mainnet.json} | 0 .../00-home-chain-arbitration-mainnet.ts | 2 +- contracts/deploy/00-home-chain-arbitration.ts | 2 +- .../deploy/change-sortition-module-rng.ts | 14 ++----- contracts/deploy/upgrade-all.ts | 41 ++++--------------- contracts/deploy/utils/klerosCoreHelper.ts | 4 +- contracts/package.json | 6 +-- .../scripts/generateDeploymentsMarkdown.sh | 6 +-- contracts/scripts/storage-layout.ts | 4 +- 10 files changed, 22 insertions(+), 57 deletions(-) rename contracts/config/{courts.v2.mainnet-neo.json => courts.v2.mainnet.json} (100%) rename contracts/config/{policies.v2.mainnet-neo.json => policies.v2.mainnet.json} (100%) diff --git a/contracts/config/courts.v2.mainnet-neo.json b/contracts/config/courts.v2.mainnet.json similarity index 100% rename from contracts/config/courts.v2.mainnet-neo.json rename to contracts/config/courts.v2.mainnet.json diff --git a/contracts/config/policies.v2.mainnet-neo.json b/contracts/config/policies.v2.mainnet.json similarity index 100% rename from contracts/config/policies.v2.mainnet-neo.json rename to contracts/config/policies.v2.mainnet.json diff --git a/contracts/deploy/00-home-chain-arbitration-mainnet.ts b/contracts/deploy/00-home-chain-arbitration-mainnet.ts index 879573676..a3d87f51f 100644 --- a/contracts/deploy/00-home-chain-arbitration-mainnet.ts +++ b/contracts/deploy/00-home-chain-arbitration-mainnet.ts @@ -6,7 +6,7 @@ import { changeCurrencyRate } from "./utils/klerosCoreHelper"; import { HomeChains, isSkipped, isDevnet, PNK, ETH } from "./utils"; import { getContractOrDeploy, getContractOrDeployUpgradable } from "./utils/getContractOrDeploy"; import { deployERC20AndFaucet, deployERC721 } from "./utils/deployTokens"; -import { ChainlinkRNG, DisputeKitClassic, KlerosCore, RNGWithFallback } from "../typechain-types"; +import { DisputeKitClassic, KlerosCore, RNGWithFallback } from "../typechain-types"; const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const { ethers, deployments, getNamedAccounts, getChainId } = hre; diff --git a/contracts/deploy/00-home-chain-arbitration.ts b/contracts/deploy/00-home-chain-arbitration.ts index 72334eb77..1c4c29695 100644 --- a/contracts/deploy/00-home-chain-arbitration.ts +++ b/contracts/deploy/00-home-chain-arbitration.ts @@ -6,7 +6,7 @@ import { changeCurrencyRate } from "./utils/klerosCoreHelper"; import { HomeChains, isSkipped, isDevnet, PNK, ETH, Courts } from "./utils"; import { getContractOrDeploy, getContractOrDeployUpgradable } from "./utils/getContractOrDeploy"; import { deployERC20AndFaucet } from "./utils/deployTokens"; -import { ChainlinkRNG, DisputeKitClassic, KlerosCore, RNGWithFallback } from "../typechain-types"; +import { DisputeKitClassic, KlerosCore, RNGWithFallback } from "../typechain-types"; const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const { ethers, deployments, getNamedAccounts, getChainId } = hre; diff --git a/contracts/deploy/change-sortition-module-rng.ts b/contracts/deploy/change-sortition-module-rng.ts index 2b5e72435..986e408ec 100644 --- a/contracts/deploy/change-sortition-module-rng.ts +++ b/contracts/deploy/change-sortition-module-rng.ts @@ -1,8 +1,8 @@ import { HardhatRuntimeEnvironment } from "hardhat/types"; import { DeployFunction } from "hardhat-deploy/types"; -import { HomeChains, isMainnet, isSkipped } from "./utils"; +import { HomeChains, isSkipped } from "./utils"; import { ethers } from "hardhat"; -import { ChainlinkRNG, SortitionModule, SortitionModuleNeo } from "../typechain-types"; +import { ChainlinkRNG, SortitionModule } from "../typechain-types"; const task: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const { getNamedAccounts, getChainId } = hre; @@ -13,15 +13,7 @@ const task: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { console.log("deploying to %s with deployer %s", HomeChains[chainId], deployer); const chainlinkRng = await ethers.getContract("ChainlinkRNG"); - - let sortitionModule: SortitionModule | SortitionModuleNeo; - if (isMainnet(hre.network)) { - console.log("Using SortitionModuleNeo"); - sortitionModule = await ethers.getContract("SortitionModuleNeo"); - } else { - console.log("Using SortitionModule"); - sortitionModule = await ethers.getContract("SortitionModule"); - } + const sortitionModule = await ethers.getContract("SortitionModule"); console.log(`chainlinkRng.changeConsumer(${sortitionModule.target})`); await chainlinkRng.changeConsumer(sortitionModule.target); diff --git a/contracts/deploy/upgrade-all.ts b/contracts/deploy/upgrade-all.ts index dfd0d882c..572ed7187 100644 --- a/contracts/deploy/upgrade-all.ts +++ b/contracts/deploy/upgrade-all.ts @@ -3,7 +3,7 @@ import { DeployFunction } from "hardhat-deploy/types"; import { prompt, print } from "gluegun"; import { deployUpgradable } from "./utils/deployUpgradable"; import { HomeChains, isSkipped } from "./utils"; -import { getContractNames, getContractNamesFromNetwork } from "../scripts/utils/contracts"; +import { getContractNamesFromNetwork } from "../scripts/utils/contracts"; const { bold } = print.colors; @@ -36,21 +36,7 @@ const deployUpgradeAll: DeployFunction = async (hre: HardhatRuntimeEnvironment) try { print.highlight(`🔍 Validating upgrade of ${bold(contractName)}`); - const contractNameWithoutNeo = contractName.replace(/Neo.*$/, ""); let compareStorageOptions = { contract: contractName } as any; - if (hre.network.name === "arbitrum") { - switch (contractName) { - case "KlerosCoreNeo": - case "SortitionModuleNeo": - compareStorageOptions = { deployedArtifact: `${contractName}_Implementation`, contract: contractName }; - break; - default: - compareStorageOptions = { - deployedArtifact: `${contractName}_Implementation`, - contract: contractNameWithoutNeo, - }; - } - } await hre.run("compare-storage", compareStorageOptions); print.newline(); print.highlight(`💣 Upgrading ${bold(contractName)}`); @@ -65,25 +51,12 @@ const deployUpgradeAll: DeployFunction = async (hre: HardhatRuntimeEnvironment) } print.info(`Upgrading ${contractName}...`); - switch (contractName) { - case "DisputeKitClassicNeo": - case "DisputeResolverNeo": - await deployUpgradable(deployments, contractName, { - contract: contractName, - newImplementation: contractNameWithoutNeo, - initializer, - from: deployer, - args, // Warning: do not reinitialize existing state variables, only the new ones - }); - break; - default: - await deployUpgradable(deployments, contractName, { - newImplementation: contractName, - initializer, - from: deployer, - args, // Warning: do not reinitialize existing state variables, only the new ones - }); - } + await deployUpgradable(deployments, contractName, { + newImplementation: contractName, + initializer, + from: deployer, + args, // Warning: do not reinitialize existing state variables, only the new ones + }); print.info(`Verifying ${contractName} on Etherscan...`); await hre.run("etherscan-verify", { contractName: `${contractName}_Implementation` }); } catch (err) { diff --git a/contracts/deploy/utils/klerosCoreHelper.ts b/contracts/deploy/utils/klerosCoreHelper.ts index 3419ae64e..3325652a3 100644 --- a/contracts/deploy/utils/klerosCoreHelper.ts +++ b/contracts/deploy/utils/klerosCoreHelper.ts @@ -1,8 +1,8 @@ -import { KlerosCore, KlerosCoreNeo, KlerosCoreRuler, KlerosCoreUniversity } from "../../typechain-types"; +import { KlerosCore, KlerosCoreRuler, KlerosCoreUniversity } from "../../typechain-types"; import { BigNumberish, toBigInt } from "ethers"; export const changeCurrencyRate = async ( - core: KlerosCore | KlerosCoreNeo | KlerosCoreRuler | KlerosCoreUniversity, + core: KlerosCore | KlerosCoreRuler | KlerosCoreUniversity, erc20: string, accepted: boolean, rateInEth: BigNumberish, diff --git a/contracts/package.json b/contracts/package.json index 0bca4e700..fd40b404d 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -91,11 +91,11 @@ "docbuild": "scripts/docPreprocess.sh && forge doc --build --out dist && scripts/docPostprocess.sh", "populate:courts:devnet": "hardhat populate:courts --from v2_devnet --network arbitrumSepoliaDevnet", "populate:courts:testnet": "hardhat populate:courts --from v2_testnet --network arbitrumSepolia", - "populate:courts:mainnetNeo": "hardhat populate:courts --core-type neo --from v2_mainnet_neo --network arbitrum", - "populate:policiesUris": "scripts/setPoliciesURIs.sh config/policies.v2.{devnet,testnet,mainnet-neo}.json", + "populate:courts:mainnet": "hardhat populate:courts --from v2_mainnet --network arbitrum", + "populate:policiesUris": "scripts/setPoliciesURIs.sh config/policies.v2.{devnet,testnet,mainnet}.json", "populate:policies:devnet": "hardhat populate:policy-registry --from v2_devnet --network arbitrumSepoliaDevnet", "populate:policies:testnet": "hardhat populate:policy-registry --from v2_testnet --network arbitrumSepolia", - "populate:policies:mainnetNeo": "hardhat populate:policy-registry --from v2_mainnet_neo --network arbitrum", + "populate:policies:mainnet": "hardhat populate:policy-registry --from v2_mainnet --network arbitrum", "release:patch": "scripts/publish.sh patch", "release:minor": "scripts/publish.sh minor", "release:major": "scripts/publish.sh major", diff --git a/contracts/scripts/generateDeploymentsMarkdown.sh b/contracts/scripts/generateDeploymentsMarkdown.sh index 6fa3c2932..43c3bf9b7 100755 --- a/contracts/scripts/generateDeploymentsMarkdown.sh +++ b/contracts/scripts/generateDeploymentsMarkdown.sh @@ -28,12 +28,12 @@ function generate() { #deploymentDir #explorerUrl done } -echo "### V2 Neo (prelaunch)" +echo "### V2 Mainnet" echo "#### Arbitrum One" echo generate "$SCRIPT_DIR/../deployments/arbitrum" "https://arbiscan.io/address/" | grep -v 'DAI\|WETH\|PNKFaucet' echo -echo "### Official Testnet" +echo "### V2 Testnet" echo "#### Arbitrum Sepolia" echo generate "$SCRIPT_DIR/../deployments/arbitrumSepolia" "https://sepolia.arbiscan.io/address/" @@ -47,7 +47,7 @@ echo generate "$SCRIPT_DIR/../deployments/chiado" "https://gnosis-chiado.blockscout.com/address/" echo -echo "### Devnet" +echo "### V2 Devnet (unstable)" echo "#### Arbitrum Sepolia" echo generate "$SCRIPT_DIR/../deployments/arbitrumSepoliaDevnet" "https://sepolia.arbiscan.io/address/" diff --git a/contracts/scripts/storage-layout.ts b/contracts/scripts/storage-layout.ts index 697b7d48f..70cf166e7 100644 --- a/contracts/scripts/storage-layout.ts +++ b/contracts/scripts/storage-layout.ts @@ -4,7 +4,7 @@ import { HardhatRuntimeEnvironment } from "hardhat/types"; task("storage-layout", "Prints the storage layout of a contract").setAction( async ({}, hre: HardhatRuntimeEnvironment) => { await hre.run("compile"); - const buildInfo = await hre.artifacts.getBuildInfo(`src/arbitration/KlerosCoreNeo.sol:KlerosCoreNeo`); - console.log(buildInfo.output.contracts["src/arbitration/KlerosCoreNeo.sol"]["KlerosCoreNeo"].storageLayout); + const buildInfo = await hre.artifacts.getBuildInfo(`src/arbitration/KlerosCore.sol:KlerosCore`); + console.log(buildInfo.output.contracts["src/arbitration/KlerosCore.sol"]["KlerosCore"].storageLayout); } ); From daa8bf944327075a403298a4bdf3f9a017a81db2 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Thu, 4 Sep 2025 23:46:24 +0100 Subject: [PATCH 09/11] chore: removed traces of Neo in more scripts --- contracts/deployments/contractsEthers.ts | 14 +++---- contracts/deployments/contractsViem.ts | 2 +- contracts/deployments/utils.ts | 2 +- contracts/scripts/changeOwner.ts | 4 +- .../scripts/generateDeploymentArtifact.sh | 2 +- .../scripts/generateDeploymentsMarkdown.sh | 1 + contracts/scripts/keeperBot.ts | 7 +--- contracts/scripts/keeperBotShutter.ts | 5 +-- contracts/scripts/populateCourts.ts | 18 ++++----- contracts/scripts/populatePolicyRegistry.ts | 12 +++--- contracts/scripts/utils/contracts.ts | 40 +++++-------------- .../integration/getContractsEthers.test.ts | 14 +++---- .../test/integration/getContractsViem.test.ts | 8 ++-- 13 files changed, 50 insertions(+), 79 deletions(-) diff --git a/contracts/deployments/contractsEthers.ts b/contracts/deployments/contractsEthers.ts index 49bc0552e..35e204488 100644 --- a/contracts/deployments/contractsEthers.ts +++ b/contracts/deployments/contractsEthers.ts @@ -92,10 +92,6 @@ import { KlerosCoreUniversity__factory, SortitionModuleUniversity, SortitionModuleUniversity__factory, - KlerosCoreNeo, - KlerosCoreNeo__factory, - SortitionModuleNeo, - SortitionModuleNeo__factory, } from "../typechain-types"; import { type ContractConfig, type DeploymentName, deployments, getAddress } from "./utils"; @@ -171,8 +167,8 @@ function getCommonFactories( export const getContracts = async (provider: ethers.Provider, deployment: DeploymentName) => { const { chainId } = deployments[deployment]; - let klerosCore: KlerosCore | KlerosCoreNeo | KlerosCoreUniversity; - let sortition: SortitionModule | SortitionModuleNeo | SortitionModuleUniversity; + let klerosCore: KlerosCore | KlerosCoreUniversity; + let sortition: SortitionModule | SortitionModuleUniversity; let commonFactories: CommonFactories; switch (deployment) { @@ -247,9 +243,9 @@ export const getContracts = async (provider: ethers.Provider, deployment: Deploy chainId ); break; - case "mainnetNeo": - klerosCore = KlerosCoreNeo__factory.connect(getAddress(mainnetCoreConfig, chainId), provider); - sortition = SortitionModuleNeo__factory.connect(getAddress(mainnetSortitionConfig, chainId), provider); + case "mainnet": + klerosCore = KlerosCore__factory.connect(getAddress(mainnetCoreConfig, chainId), provider); + sortition = SortitionModule__factory.connect(getAddress(mainnetSortitionConfig, chainId), provider); commonFactories = getCommonFactories( { dkClassicConfig: mainnetDkcConfig, diff --git a/contracts/deployments/contractsViem.ts b/contracts/deployments/contractsViem.ts index 792ce5b3e..9e3abc4f8 100644 --- a/contracts/deployments/contractsViem.ts +++ b/contracts/deployments/contractsViem.ts @@ -203,7 +203,7 @@ export const getConfigs = ({ deployment }: { deployment: DeploymentName }): Cont }, }); - case "mainnetNeo": + case "mainnet": return getCommonConfigs({ chainId, configs: { diff --git a/contracts/deployments/utils.ts b/contracts/deployments/utils.ts index e2711a377..a876fbd5a 100644 --- a/contracts/deployments/utils.ts +++ b/contracts/deployments/utils.ts @@ -10,7 +10,7 @@ export const deployments = { testnet: { chainId: arbitrumSepolia.id, }, - mainnetNeo: { + mainnet: { chainId: arbitrum.id, }, } as const; diff --git a/contracts/scripts/changeOwner.ts b/contracts/scripts/changeOwner.ts index 72eeaeef7..fcb64137b 100644 --- a/contracts/scripts/changeOwner.ts +++ b/contracts/scripts/changeOwner.ts @@ -7,7 +7,7 @@ const { bold } = print.colors; task("change-owner", "Changes the owner for all the contracts") .addPositionalParam("newOwner", "The address of the new owner") - .addOptionalParam("coreType", "The type of core to use between base, neo, university (default: base)", Cores.BASE) + .addOptionalParam("coreType", "The type of core to use between base, university (default: base)", Cores.BASE) .setAction(async (taskArgs, hre) => { const newOwner = taskArgs.newOwner; if (!isAddress(newOwner)) { @@ -27,7 +27,7 @@ task("change-owner", "Changes the owner for all the contracts") const coreType = Cores[taskArgs.coreType.toUpperCase() as keyof typeof Cores]; if (coreType === undefined) { - console.error("Invalid core type, must be one of base, neo, university"); + console.error("Invalid core type, must be one of base, university"); return; } console.log("Using core type %s", coreType); diff --git a/contracts/scripts/generateDeploymentArtifact.sh b/contracts/scripts/generateDeploymentArtifact.sh index dedcf1c0b..6daf2a585 100755 --- a/contracts/scripts/generateDeploymentArtifact.sh +++ b/contracts/scripts/generateDeploymentArtifact.sh @@ -2,7 +2,7 @@ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -if [[ $# < 2 ]] +if [[ $# -lt 2 ]] then echo "usage: $(basename $0) network address" exit 1 diff --git a/contracts/scripts/generateDeploymentsMarkdown.sh b/contracts/scripts/generateDeploymentsMarkdown.sh index 43c3bf9b7..5ae9dbf7f 100755 --- a/contracts/scripts/generateDeploymentsMarkdown.sh +++ b/contracts/scripts/generateDeploymentsMarkdown.sh @@ -15,6 +15,7 @@ IGNORED_ARTIFACTS=( function generate() { #deploymentDir #explorerUrl deploymentDir=$1 explorerUrl=$2 + # shellcheck disable=SC2068 for f in $(ls -1 $deploymentDir/*.json 2>/dev/null | grep -v ${IGNORED_ARTIFACTS[@]/#/-e } | sort); do contractName=$(basename $f .json) address=$(cat $f | jq -r .address) diff --git a/contracts/scripts/keeperBot.ts b/contracts/scripts/keeperBot.ts index 593d97e4a..5d1b2751f 100644 --- a/contracts/scripts/keeperBot.ts +++ b/contracts/scripts/keeperBot.ts @@ -6,7 +6,6 @@ import { DisputeKitGatedShutter, DisputeKitShutter, SortitionModule, - SortitionModuleNeo, } from "../typechain-types"; import env from "./utils/env"; import loggerFactory from "./utils/logger"; @@ -49,7 +48,7 @@ const getContracts = async () => { throw new Error("University is not supported yet"); } const contracts = await getContractsForCoreType(hre, coreType); - return { ...contracts, sortition: contracts.sortition as SortitionModule | SortitionModuleNeo }; + return { ...contracts, sortition: contracts.sortition as SortitionModule }; }; type Contribution = { @@ -277,9 +276,7 @@ const isRngReady = async () => { return true; } } else if (currentRng === blockHashRNG?.target && blockHashRNG !== null) { - const requestBlock = await sortition.randomNumberRequestBlock(); - const lookahead = await sortition.rngLookahead(); - const n = await blockHashRNG.receiveRandomness.staticCall(requestBlock + lookahead); + const n = await blockHashRNG.receiveRandomness.staticCall(); if (Number(n) === 0) { logger.info("BlockHashRNG is NOT ready yet"); return false; diff --git a/contracts/scripts/keeperBotShutter.ts b/contracts/scripts/keeperBotShutter.ts index 0b9724cae..fff463513 100644 --- a/contracts/scripts/keeperBotShutter.ts +++ b/contracts/scripts/keeperBotShutter.ts @@ -1,6 +1,6 @@ import hre from "hardhat"; import { getBytes } from "ethers"; -import { DisputeKitGatedShutter, DisputeKitShutter, SortitionModule, SortitionModuleNeo } from "../typechain-types"; +import { DisputeKitGatedShutter, DisputeKitShutter } from "../typechain-types"; import { decrypt } from "./shutter"; import env from "./utils/env"; import loggerFactory from "./utils/logger"; @@ -312,8 +312,7 @@ const getContracts = async () => { if (coreType === Cores.UNIVERSITY) { throw new Error("University is not supported yet"); } - const contracts = await getContractsForCoreType(hre, coreType); - return { ...contracts, sortition: contracts.sortition as SortitionModule | SortitionModuleNeo }; + return await getContractsForCoreType(hre, coreType); }; async function main() { diff --git a/contracts/scripts/populateCourts.ts b/contracts/scripts/populateCourts.ts index ee95dee71..cf86c9cbb 100644 --- a/contracts/scripts/populateCourts.ts +++ b/contracts/scripts/populateCourts.ts @@ -1,11 +1,11 @@ import { task, types } from "hardhat/config"; -import { KlerosCore, KlerosCoreNeo, KlerosCoreUniversity } from "../typechain-types"; +import { KlerosCore, KlerosCoreUniversity } from "../typechain-types"; import { BigNumberish, toBigInt, toNumber } from "ethers"; import courtsV1Mainnet from "../config/courts.v1.mainnet.json"; import courtsV1GnosisChain from "../config/courts.v1.gnosischain.json"; import courtsV2ArbitrumTestnet from "../config/courts.v2.testnet.json"; import courtsV2ArbitrumDevnet from "../config/courts.v2.devnet.json"; -import courtsV2MainnetNeo from "../config/courts.v2.mainnet-neo.json"; +import courtsV2Mainnet from "../config/courts.v2.mainnet.json"; import { isDevnet } from "../deploy/utils"; import { execute, writeTransactionBatch } from "./utils/execution"; import { getContracts, Cores } from "./utils/contracts"; @@ -21,7 +21,7 @@ enum Sources { V1_GNOSIS, V2_DEVNET, V2_TESTNET, - V2_MAINNET_NEO, + V2_MAINNET, } type Court = { @@ -43,7 +43,7 @@ const TEN_THOUSAND_GWEI = 10n ** 13n; task("populate:courts", "Populates the courts and their parameters") .addOptionalParam( "from", - "The source of the policies between v1_mainnet, v1_gnosis, v2_devnet, v2_testnet, v2_mainnet_neo (default: auto depending on the network)", + "The source of the policies between v1_mainnet, v1_gnosis, v2_devnet, v2_testnet, v2_mainnet (default: auto depending on the network)", undefined ) .addOptionalParam("start", "The starting index for the courts to populate (default: 0)", 0, types.int) @@ -53,7 +53,7 @@ task("populate:courts", "Populates the courts and their parameters") undefined, types.int ) - .addOptionalParam("coreType", "The type of core to use between base, neo, university (default: base)", Cores.BASE) + .addOptionalParam("coreType", "The type of core to use between base, university (default: base)", Cores.BASE) .addFlag("reverse", "Iterates the courts in reverse order, useful to increase minStake in the child courts first") .addFlag("forceV1ParametersToDev", "Use development values for the v1 courts parameters") .setAction(async (taskArgs, hre) => { @@ -74,7 +74,7 @@ task("populate:courts", "Populates the courts and their parameters") if (taskArgs.from) { from = Sources[taskArgs.from.toUpperCase() as keyof typeof Sources]; if (from === undefined) { - console.error("Invalid source, must be one of v1_mainnet, v1_gnosis, v2_devnet, v2_testnet, v2_mainnet_neo"); + console.error("Invalid source, must be one of v1_mainnet, v1_gnosis, v2_devnet, v2_testnet, v2_mainnet"); return; } } else { @@ -84,7 +84,7 @@ task("populate:courts", "Populates the courts and their parameters") const coreType = Cores[taskArgs.coreType.toUpperCase() as keyof typeof Cores]; if (coreType === undefined) { - console.error("Invalid core type, must be one of base, neo, university"); + console.error("Invalid core type, must be one of base, university"); return; } console.log("Using core type %s", coreType); @@ -133,8 +133,8 @@ task("populate:courts", "Populates the courts and their parameters") courtsV2 = courtsV2ArbitrumTestnet; break; } - case Sources.V2_MAINNET_NEO: { - courtsV2 = courtsV2MainnetNeo; + case Sources.V2_MAINNET: { + courtsV2 = courtsV2Mainnet; break; } default: diff --git a/contracts/scripts/populatePolicyRegistry.ts b/contracts/scripts/populatePolicyRegistry.ts index 6ffdacd8e..43087268a 100644 --- a/contracts/scripts/populatePolicyRegistry.ts +++ b/contracts/scripts/populatePolicyRegistry.ts @@ -4,7 +4,7 @@ import policiesV1Mainnet from "../config/policies.v1.mainnet.json"; import policiesV1GnosisChain from "../config/policies.v1.gnosischain.json"; import policiesV2ArbitrumTestnet from "../config/policies.v2.testnet.json"; import policiesV2ArbitrumDevnet from "../config/policies.v2.devnet.json"; -import policiesV2MainnetNeo from "../config/policies.v2.mainnet-neo.json"; +import policiesV2Mainnet from "../config/policies.v2.mainnet.json"; import { isDevnet } from "../deploy/utils"; import { execute, writeTransactionBatch } from "./utils/execution"; @@ -19,13 +19,13 @@ enum Sources { V1_GNOSIS, V2_DEVNET, V2_TESTNET, - V2_MAINNET_NEO, + V2_MAINNET, } task("populate:policy-registry", "Populates the policy registry for each court") .addOptionalParam( "from", - "The source of the policies between v1_mainnet, v1_gnosis, v2_devnet, v2_testnet, v2_mainnet_neo (default: auto depending on the network)", + "The source of the policies between v1_mainnet, v1_gnosis, v2_devnet, v2_testnet, v2_mainnet (default: auto depending on the network)", undefined ) .addOptionalParam("start", "The starting index for the courts to populate (default: 0)", 0, types.int) @@ -53,7 +53,7 @@ task("populate:policy-registry", "Populates the policy registry for each court") if (taskArgs.from) { from = Sources[taskArgs.from.toUpperCase() as keyof typeof Sources]; if (from === undefined) { - console.error("Invalid source, must be one of v1_mainnet, v1_gnosis, v2_devnet, v2_testnet, v2_mainnet_neo"); + console.error("Invalid source, must be one of v1_mainnet, v1_gnosis, v2_devnet, v2_testnet, v2_mainnet"); return; } } else { @@ -88,8 +88,8 @@ task("populate:policy-registry", "Populates the policy registry for each court") policiesV2 = policiesV2ArbitrumTestnet; break; } - case Sources.V2_MAINNET_NEO: { - policiesV2 = policiesV2MainnetNeo; + case Sources.V2_MAINNET: { + policiesV2 = policiesV2Mainnet; break; } default: diff --git a/contracts/scripts/utils/contracts.ts b/contracts/scripts/utils/contracts.ts index cc7bf2429..50bcac2f6 100644 --- a/contracts/scripts/utils/contracts.ts +++ b/contracts/scripts/utils/contracts.ts @@ -7,13 +7,11 @@ import { DisputeResolver, DisputeTemplateRegistry, KlerosCore, - KlerosCoreNeo, KlerosCoreUniversity, PNK, PolicyRegistry, RandomizerRNG, SortitionModule, - SortitionModuleNeo, SortitionModuleUniversity, TransactionBatcher, KlerosCoreSnapshotProxy, @@ -24,28 +22,18 @@ import { export const Cores = { BASE: "BASE", - NEO: "NEO", UNIVERSITY: "UNIVERSITY", } as const; export type Core = (typeof Cores)[keyof typeof Cores]; /** - * Get contract names by specifying the coreType (BASE, NEO, UNIVERSITY). + * Get contract names by specifying the coreType (BASE, UNIVERSITY). * @param coreType - Core type * @returns Contract names */ export const getContractNames = (coreType: Core) => { const coreSpecificNames = { - [Cores.NEO]: { - core: "KlerosCoreNeo", - sortition: "SortitionModuleNeo", - disputeKitClassic: "DisputeKitClassicNeo", - disputeKitShutter: "DisputeKitShutterNeo", - disputeKitGated: "DisputeKitGatedNeo", - disputeKitGatedShutter: "DisputeKitGatedShutterNeo", - disputeResolver: "DisputeResolverNeo", - }, [Cores.BASE]: { core: "KlerosCore", sortition: "SortitionModule", @@ -66,7 +54,7 @@ export const getContractNames = (coreType: Core) => { }, }; - if (!(coreType in coreSpecificNames)) throw new Error("Invalid core type, must be one of BASE, NEO, or UNIVERSITY"); + if (!(coreType in coreSpecificNames)) throw new Error("Invalid core type, must be one of BASE, or UNIVERSITY"); return { ...coreSpecificNames[coreType], @@ -83,20 +71,16 @@ export const getContractNames = (coreType: Core) => { }; /** - * Get contracts by specifying the coreType (BASE, NEO, UNIVERSITY). + * Get contracts by specifying the coreType (BASE, UNIVERSITY). * @param hre - Hardhat runtime environment * @param coreType - Core type * @returns Contracts */ export const getContracts = async (hre: HardhatRuntimeEnvironment, coreType: Core) => { const { ethers } = hre; - let core: KlerosCore | KlerosCoreNeo | KlerosCoreUniversity; - let sortition: SortitionModule | SortitionModuleNeo | SortitionModuleUniversity; + let core: KlerosCore | KlerosCoreUniversity; + let sortition: SortitionModule | SortitionModuleUniversity; switch (coreType) { - case Cores.NEO: - core = await ethers.getContract(getContractNames(coreType).core); - sortition = await ethers.getContract(getContractNames(coreType).sortition); - break; case Cores.BASE: core = await ethers.getContract(getContractNames(coreType).core); sortition = await ethers.getContract(getContractNames(coreType).sortition); @@ -106,7 +90,7 @@ export const getContracts = async (hre: HardhatRuntimeEnvironment, coreType: Cor sortition = await ethers.getContract(getContractNames(coreType).sortition); break; default: - throw new Error("Invalid core type, must be one of BASE, NEO, or UNIVERSITY"); + throw new Error("Invalid core type, must be one of BASE, or UNIVERSITY"); } const disputeKitClassic = await ethers.getContract(getContractNames(coreType).disputeKitClassic); const disputeKitShutter = await ethers.getContractOrNull( @@ -151,32 +135,28 @@ export const getContracts = async (hre: HardhatRuntimeEnvironment, coreType: Cor }; /** - * Get contracts by inferring the coreType (BASE, NEO, UNIVERSITY) from the network, most convenient for most cases. + * Get contracts by inferring the coreType (BASE, UNIVERSITY) from the network, most convenient for most cases. * @param hre - Hardhat runtime environment * @returns Contracts */ export const getContractsFromNetwork = async (hre: HardhatRuntimeEnvironment) => { const { network } = hre; - if (network.name === "arbitrumSepoliaDevnet" || network.name === "arbitrumSepolia") { + if (["arbitrumSepoliaDevnet", "arbitrumSepolia", "arbitrum"].includes(network.name)) { return getContracts(hre, Cores.BASE); - } else if (network.name === "arbitrum") { - return getContracts(hre, Cores.NEO); } else { throw new Error("Invalid network"); } }; /** - * Get contract names by inferring the coreType (BASE, NEO, UNIVERSITY) from the network, most convenient for most cases. + * Get contract names by inferring the coreType (BASE, UNIVERSITY) from the network, most convenient for most cases. * @param hre - Hardhat runtime environment * @returns Contract names */ export const getContractNamesFromNetwork = async (hre: HardhatRuntimeEnvironment) => { const { network } = hre; - if (network.name === "arbitrumSepoliaDevnet" || network.name === "arbitrumSepolia") { + if (["arbitrumSepoliaDevnet", "arbitrumSepolia", "arbitrum"].includes(network.name)) { return getContractNames(Cores.BASE); - } else if (network.name === "arbitrum") { - return getContractNames(Cores.NEO); } else { throw new Error("Invalid network"); } diff --git a/contracts/test/integration/getContractsEthers.test.ts b/contracts/test/integration/getContractsEthers.test.ts index 736717626..59a04dfab 100644 --- a/contracts/test/integration/getContractsEthers.test.ts +++ b/contracts/test/integration/getContractsEthers.test.ts @@ -4,10 +4,8 @@ import { arbitrum, arbitrumSepolia } from "viem/chains"; import { getContracts } from "../../deployments/contractsEthers"; import { KlerosCore__factory, - KlerosCoreNeo__factory, KlerosCoreUniversity__factory, SortitionModule__factory, - SortitionModuleNeo__factory, SortitionModuleUniversity__factory, DisputeKitClassic__factory, DisputeResolver__factory, @@ -99,7 +97,7 @@ const universityContractMapping: ContractMapping = { klerosCoreSnapshotProxy: { name: "KlerosCoreSnapshotProxy" }, }; -const neoContractMapping: ContractMapping = { +const mainnetContractMapping: ContractMapping = { klerosCore: { name: "KlerosCoreNeo" }, sortition: { name: "SortitionModuleNeo" }, disputeKitClassic: { name: "DisputeKitClassicNeo" }, @@ -291,16 +289,16 @@ describe("getContractsEthers", async () => { await verifyDeployedAddresses(contracts, NETWORKS.TESTNET, testnetContractMapping); }); - it("should return correct contract instances for mainnetNeo", async () => { - const contracts = await getContracts(arbitrumProvider, "mainnetNeo"); + it("should return correct contract instances for mainnet", async () => { + const contracts = await getContracts(arbitrumProvider, "mainnet"); // Verify chain ID const network = await arbitrumProvider.getNetwork(); expect(network.chainId).to.equal(arbitrum.id); // Verify contract instances - expect(contracts.klerosCore).to.be.instanceOf(getConstructor(KlerosCoreNeo__factory, arbitrumProvider)); - expect(contracts.sortition).to.be.instanceOf(getConstructor(SortitionModuleNeo__factory, arbitrumProvider)); + expect(contracts.klerosCore).to.be.instanceOf(getConstructor(KlerosCore__factory, arbitrumProvider)); + expect(contracts.sortition).to.be.instanceOf(getConstructor(SortitionModule__factory, arbitrumProvider)); verifyCommonContractInstances(contracts, arbitrumProvider); expect(contracts.disputeKitShutter).to.not.be.null; expect(contracts.disputeKitGated).to.not.be.null; @@ -310,7 +308,7 @@ describe("getContractsEthers", async () => { // Verify all contract addresses await verifyAllContractAddresses(contracts); - await verifyDeployedAddresses(contracts, NETWORKS.MAINNET, neoContractMapping); + await verifyDeployedAddresses(contracts, NETWORKS.MAINNET, mainnetContractMapping); }); it("should throw error for unsupported deployment", async () => { diff --git a/contracts/test/integration/getContractsViem.test.ts b/contracts/test/integration/getContractsViem.test.ts index a32db3c86..4231164ad 100644 --- a/contracts/test/integration/getContractsViem.test.ts +++ b/contracts/test/integration/getContractsViem.test.ts @@ -77,7 +77,7 @@ const universityContractMapping: ContractMapping = { klerosCoreSnapshotProxy: { name: "KlerosCoreSnapshotProxy" }, }; -const neoContractMapping: ContractMapping = { +const mainnetContractMapping: ContractMapping = { klerosCore: { name: "KlerosCoreNeo" }, sortition: { name: "SortitionModuleNeo" }, disputeKitClassic: { name: "DisputeKitClassicNeo" }, @@ -240,10 +240,10 @@ describe("getContractsViem", () => { await verifyDeployedAddresses(contracts, NETWORKS.TESTNET, testnetContractMapping); }); - it("should return correct contract instances for mainnetNeo", async () => { + it("should return correct contract instances for mainnet", async () => { const contracts = getContracts({ publicClient: arbitrumClient, - deployment: "mainnetNeo", + deployment: "mainnet", }); // Verify chain ID @@ -262,7 +262,7 @@ describe("getContractsViem", () => { expect(contracts.randomizerRng).to.not.be.undefined; // Verify deployed addresses - await verifyDeployedAddresses(contracts, NETWORKS.MAINNET, neoContractMapping); + await verifyDeployedAddresses(contracts, NETWORKS.MAINNET, mainnetContractMapping); }); it("should throw error for unsupported deployment", () => { From 4e384c90ad03532ac51ebfd7a8ef5c561ebc00fb Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Thu, 4 Sep 2025 23:59:43 +0100 Subject: [PATCH 10/11] chore: updated audit METRICS files --- contracts/audit/METRICS.html | 832 +++++++++++++++++------------------ contracts/audit/METRICS.md | 779 ++++++++++++++++---------------- 2 files changed, 789 insertions(+), 822 deletions(-) diff --git a/contracts/audit/METRICS.html b/contracts/audit/METRICS.html index df48b7b11..31a6b6e73 100644 --- a/contracts/audit/METRICS.html +++ b/contracts/audit/METRICS.html @@ -208793,25 +208793,24 @@