diff --git a/contracts/deploy/00-home-chain-arbitration.ts b/contracts/deploy/00-home-chain-arbitration.ts index d581cdc54..8aadf9ff6 100644 --- a/contracts/deploy/00-home-chain-arbitration.ts +++ b/contracts/deploy/00-home-chain-arbitration.ts @@ -3,7 +3,7 @@ import { DeployFunction } from "hardhat-deploy/types"; import { getContractAddress } from "./utils/getContractAddress"; import { deployUpgradable } from "./utils/deployUpgradable"; import { changeCurrencyRate } from "./utils/klerosCoreHelper"; -import { HomeChains, isSkipped, isDevnet, PNK, ETH } from "./utils"; +import { HomeChains, isSkipped, isDevnet, PNK, ETH, Courts } from "./utils"; import { getContractOrDeploy, getContractOrDeployUpgradable } from "./utils/getContractOrDeploy"; import { deployERC20AndFaucet } from "./utils/deployTokens"; import { ChainlinkRNG, DisputeKitClassic, KlerosCore } from "../typechain-types"; @@ -103,8 +103,25 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) log: true, }); await core.addNewDisputeKit(disputeKitShutter.address); - await core.enableDisputeKits(1, [2], true); // enable disputeKitShutter on the General Court + await core.enableDisputeKits(Courts.GENERAL, [2], true); // enable disputeKitShutter on the General Court + const disputeKitGated = await deployUpgradable(deployments, "DisputeKitGated", { + from: deployer, + args: [deployer, core.target], + log: true, + }); + await core.addNewDisputeKit(disputeKitGated.address); + await core.enableDisputeKits(Courts.GENERAL, [3], true); // enable disputeKitGated on the General Court + + const disputeKitGatedShutter = await deployUpgradable(deployments, "DisputeKitGatedShutter", { + from: deployer, + args: [deployer, core.target], + log: true, + }); + await core.addNewDisputeKit(disputeKitGatedShutter.address); + await core.enableDisputeKits(Courts.GENERAL, [4], true); // enable disputeKitGatedShutter on the General Court + + // Snapshot proxy await deploy("KlerosCoreSnapshotProxy", { from: deployer, args: [deployer, core.target], diff --git a/contracts/deploy/utils/deployTokens.ts b/contracts/deploy/utils/deployTokens.ts index 303af1ddc..3e6222f99 100644 --- a/contracts/deploy/utils/deployTokens.ts +++ b/contracts/deploy/utils/deployTokens.ts @@ -65,3 +65,17 @@ export const deployERC721 = async ( log: true, }); }; + +export const deployERC1155 = async ( + hre: HardhatRuntimeEnvironment, + deployer: string, + name: string, + ticker: string +): Promise => { + return getContractOrDeploy(hre, ticker, { + from: deployer, + contract: "TestERC1155", + args: [], + log: true, + }); +}; diff --git a/contracts/hardhat.config.ts b/contracts/hardhat.config.ts index 8ca5b5179..a108ddcf3 100644 --- a/contracts/hardhat.config.ts +++ b/contracts/hardhat.config.ts @@ -105,15 +105,7 @@ const config: HardhatUserConfig = { arbitrumSepolia: { chainId: 421614, url: process.env.ARBITRUM_SEPOLIA_RPC ?? `https://arbitrum-sepolia.infura.io/v3/${process.env.INFURA_API_KEY}`, - accounts: - (process.env.ARB_GOERLI_PRIVATE_KEY_WALLET_1 && [ - process.env.ARB_GOERLI_PRIVATE_KEY_WALLET_1 as string, - process.env.ARB_GOERLI_PRIVATE_KEY_WALLET_2 as string, - process.env.ARB_GOERLI_PRIVATE_KEY_WALLET_3 as string, - process.env.ARB_GOERLI_PRIVATE_KEY_WALLET_4 as string, - process.env.ARB_GOERLI_PRIVATE_KEY_WALLET_5 as string, - ]) || - (process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : []), + accounts: process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [], live: true, saveDeployments: true, tags: ["staging", "home", "layer2"], @@ -131,15 +123,7 @@ const config: HardhatUserConfig = { arbitrumSepoliaDevnet: { chainId: 421614, url: process.env.ARBITRUM_SEPOLIA_RPC ?? `https://arbitrum-sepolia.infura.io/v3/${process.env.INFURA_API_KEY}`, - accounts: - (process.env.ARB_GOERLI_PRIVATE_KEY_WALLET_1 && [ - process.env.ARB_GOERLI_PRIVATE_KEY_WALLET_1 as string, - process.env.ARB_GOERLI_PRIVATE_KEY_WALLET_2 as string, - process.env.ARB_GOERLI_PRIVATE_KEY_WALLET_3 as string, - process.env.ARB_GOERLI_PRIVATE_KEY_WALLET_4 as string, - process.env.ARB_GOERLI_PRIVATE_KEY_WALLET_5 as string, - ]) || - (process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : []), + accounts: process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [], live: true, saveDeployments: true, tags: ["staging", "home", "layer2"], diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitGated.sol b/contracts/src/arbitration/dispute-kits/DisputeKitGated.sol index 6c9c0ab65..804b125d5 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitGated.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitGated.sol @@ -29,14 +29,6 @@ interface IBalanceHolderERC1155 { contract DisputeKitGated is DisputeKitClassicBase { string public constant override version = "0.10.0"; - // ************************************* // - // * Storage * // - // ************************************* // - - address public tokenGate; // The token used for gating access. - uint256 public tokenId; // Only used for ERC-1155 - bool public isERC1155; // True if the tokenGate is an ERC-1155, false otherwise. - // ************************************* // // * Constructor * // // ************************************* // @@ -49,20 +41,8 @@ contract DisputeKitGated is DisputeKitClassicBase { /// @dev Initializer. /// @param _governor The governor's address. /// @param _core The KlerosCore arbitrator. - /// @param _tokenGate The token used for gating access. - /// @param _tokenId The token ID for ERC-1155 (ignored for other token types) - /// @param _isERC1155 Whether the token is an ERC-1155 - function initialize( - address _governor, - KlerosCore _core, - address _tokenGate, - uint256 _tokenId, - bool _isERC1155 - ) external reinitializer(1) { + function initialize(address _governor, KlerosCore _core) external reinitializer(1) { __DisputeKitClassicBase_initialize(_governor, _core); - tokenGate = _tokenGate; - tokenId = _tokenId; - isERC1155 = _isERC1155; } // ************************ // @@ -75,26 +55,37 @@ contract DisputeKitGated is DisputeKitClassicBase { // NOP } - /// @dev Changes the `tokenGate` to an ERC-20 or ERC-721 token. - /// @param _tokenGate The new value for the `tokenGate` storage variable. - function changeTokenGateERC20OrERC721(address _tokenGate) external onlyByGovernor { - tokenGate = _tokenGate; - isERC1155 = false; - } - - /// @dev Changes the `tokenGate` to an ERC-1155 token. - /// @param _tokenGate The new value for the `tokenGate` storage variable. - /// @param _tokenId The new value for the `tokenId` storage variable. - function changeTokenGateERC1155(address _tokenGate, uint256 _tokenId) external onlyByGovernor { - tokenGate = _tokenGate; - tokenId = _tokenId; - isERC1155 = true; - } - // ************************************* // // * Internal * // // ************************************* // + /// @dev Extracts token gating information from the extra data. + /// @param _extraData The extra data bytes array with the following encoding: + /// - bytes 0-31: uint96 courtID, not used here + /// - bytes 32-63: uint256 minJurors, not used here + /// - bytes 64-95: uint256 disputeKitID, not used here + /// - bytes 96-127: uint256 packedTokenGateAndFlag (address tokenGate in bits 0-159, bool isERC1155 in bit 160) + /// - bytes 128-159: uint256 tokenId + /// @return tokenGate The address of the token contract used for gating access. + /// @return isERC1155 True if the token is an ERC-1155, false for ERC-20/ERC-721. + /// @return tokenId The token ID for ERC-1155 tokens (ignored for ERC-20/ERC-721). + function extraDataToTokenInfo( + bytes memory _extraData + ) public pure returns (address tokenGate, bool isERC1155, uint256 tokenId) { + // Need at least 160 bytes to safely read the parameters + if (_extraData.length < 160) return (address(0), false, 0); + + assembly { + // solium-disable-line security/no-inline-assembly + let packedTokenGateIsERC1155 := mload(add(_extraData, 0x80)) // 4th parameter at offset 128 + tokenId := mload(add(_extraData, 0xA0)) // 5th parameter at offset 160 (moved up) + + // Unpack address from lower 160 bits and bool from bit 160 + tokenGate := and(packedTokenGateIsERC1155, 0xffffffffffffffffffffffffffffffffffffffff) + isERC1155 := and(shr(160, packedTokenGateIsERC1155), 1) + } + } + /// @inheritdoc DisputeKitClassicBase function _postDrawCheck( Round storage _round, @@ -103,6 +94,15 @@ contract DisputeKitGated is DisputeKitClassicBase { ) internal view override returns (bool) { if (!super._postDrawCheck(_round, _coreDisputeID, _juror)) return false; + // Get the local dispute and extract token info from extraData + uint256 localDisputeID = coreDisputeIDToLocal[_coreDisputeID]; + Dispute storage dispute = disputes[localDisputeID]; + (address tokenGate, bool isERC1155, uint256 tokenId) = extraDataToTokenInfo(dispute.extraData); + + // If no token gate is specified, allow all jurors + if (tokenGate == address(0)) return true; + + // Check juror's token balance if (isERC1155) { return IBalanceHolderERC1155(tokenGate).balanceOf(_juror, tokenId) > 0; } else { diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitGatedShutter.sol b/contracts/src/arbitration/dispute-kits/DisputeKitGatedShutter.sol new file mode 100644 index 000000000..3afda3ff2 --- /dev/null +++ b/contracts/src/arbitration/dispute-kits/DisputeKitGatedShutter.sol @@ -0,0 +1,189 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {DisputeKitClassicBase, KlerosCore} from "./DisputeKitClassicBase.sol"; + +interface IBalanceHolder { + /// @dev Returns the number of tokens in `owner` account. + /// @dev Compatible with ERC-20 and ERC-721. + /// @param owner The address of the owner. + /// @return balance The number of tokens in `owner` account. + function balanceOf(address owner) external view returns (uint256 balance); +} + +interface IBalanceHolderERC1155 { + /// @dev Returns the balance of an ERC-1155 token. + /// @param account The address of the token holder + /// @param id ID of the token + /// @return The token balance + function balanceOf(address account, uint256 id) external view returns (uint256); +} + +/// @title DisputeKitGatedShutter +/// Added functionality: shielded voting. +/// Dispute kit implementation adapted from DisputeKitClassic +/// - a drawing system: proportional to staked PNK with a non-zero balance of `tokenGate` where `tokenGate` is an ERC20, ERC721 or ERC1155 +/// - a vote aggregation system: plurality, +/// - an incentive system: equal split between coherent votes, +/// - an appeal system: fund 2 choices only, vote on any choice. +contract DisputeKitGatedShutter is DisputeKitClassicBase { + string public constant override version = "0.10.0"; + + // ************************************* // + // * Events * // + // ************************************* // + + /// @dev Emitted when a vote is cast. + /// @param _coreDisputeID The identifier of the dispute in the Arbitrator contract. + /// @param _juror The address of the juror casting the vote commitment. + /// @param _commit The commitment hash. + /// @param _identity The Shutter identity used for encryption. + /// @param _encryptedVote The Shutter encrypted vote. + event CommitCastShutter( + uint256 indexed _coreDisputeID, + address indexed _juror, + bytes32 indexed _commit, + bytes32 _identity, + bytes _encryptedVote + ); + + // ************************************* // + // * Constructor * // + // ************************************* // + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /// @dev Initializer. + /// @param _governor The governor's address. + /// @param _core The KlerosCore arbitrator. + function initialize(address _governor, KlerosCore _core) external reinitializer(1) { + __DisputeKitClassicBase_initialize(_governor, _core); + } + + // ************************ // + // * Governance * // + // ************************ // + + /// @dev Access Control to perform implementation upgrades (UUPS Proxiable) + /// Only the governor can perform upgrades (`onlyByGovernor`) + function _authorizeUpgrade(address) internal view override onlyByGovernor { + // NOP + } + + // ************************************* // + // * State Modifiers * // + // ************************************* // + + /// @dev Sets the caller's commit for the specified votes. It can be called multiple times during the + /// commit period, each call overrides the commits of the previous one. + /// `O(n)` where + /// `n` is the number of votes. + /// @param _coreDisputeID The ID of the dispute in Kleros Core. + /// @param _voteIDs The IDs of the votes. + /// @param _commit The commitment hash including the justification. + /// @param _identity The Shutter identity used for encryption. + /// @param _encryptedVote The Shutter encrypted vote. + function castCommitShutter( + uint256 _coreDisputeID, + uint256[] calldata _voteIDs, + bytes32 _commit, + bytes32 _identity, + bytes calldata _encryptedVote + ) external notJumped(_coreDisputeID) { + _castCommit(_coreDisputeID, _voteIDs, _commit); + emit CommitCastShutter(_coreDisputeID, msg.sender, _commit, _identity, _encryptedVote); + } + + function castVoteShutter( + uint256 _coreDisputeID, + uint256[] calldata _voteIDs, + uint256 _choice, + uint256 _salt, + string memory _justification + ) external { + Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]]; + address juror = dispute.rounds[dispute.rounds.length - 1].votes[_voteIDs[0]].account; + + // _castVote() ensures that all the _voteIDs do belong to `juror` + _castVote(_coreDisputeID, _voteIDs, _choice, _salt, _justification, juror); + } + + // ************************************* // + // * Public Views * // + // ************************************* // + + /** + * @dev Computes the hash of a vote using ABI encoding + * @param _choice The choice being voted for + * @param _justification The justification for the vote + * @param _salt A random salt for commitment + * @return bytes32 The hash of the encoded vote parameters + */ + function hashVote( + uint256 _choice, + uint256 _salt, + string memory _justification + ) public pure override returns (bytes32) { + bytes32 justificationHash = keccak256(bytes(_justification)); + return keccak256(abi.encode(_choice, _salt, justificationHash)); + } + + // ************************************* // + // * Internal * // + // ************************************* // + + /// @dev Extracts token gating information from the extra data. + /// @param _extraData The extra data bytes array with the following encoding: + /// - bytes 0-31: uint96 courtID, not used here + /// - bytes 32-63: uint256 minJurors, not used here + /// - bytes 64-95: uint256 disputeKitID, not used here + /// - bytes 96-127: uint256 packedTokenGateAndFlag (address tokenGate in bits 0-159, bool isERC1155 in bit 160) + /// - bytes 128-159: uint256 tokenId + /// @return tokenGate The address of the token contract used for gating access. + /// @return isERC1155 True if the token is an ERC-1155, false for ERC-20/ERC-721. + /// @return tokenId The token ID for ERC-1155 tokens (ignored for ERC-20/ERC-721). + function _extraDataToTokenInfo( + bytes memory _extraData + ) internal pure returns (address tokenGate, bool isERC1155, uint256 tokenId) { + // Need at least 160 bytes to safely read the parameters + if (_extraData.length < 160) return (address(0), false, 0); + + assembly { + // solium-disable-line security/no-inline-assembly + let packedTokenGateIsERC1155 := mload(add(_extraData, 0x80)) // 4th parameter at offset 128 + tokenId := mload(add(_extraData, 0xA0)) // 5th parameter at offset 160 (moved up) + + // Unpack address from lower 160 bits and bool from bit 160 + tokenGate := and(packedTokenGateIsERC1155, 0xffffffffffffffffffffffffffffffffffffffff) + isERC1155 := and(shr(160, packedTokenGateIsERC1155), 1) + } + } + + /// @inheritdoc DisputeKitClassicBase + function _postDrawCheck( + Round storage _round, + uint256 _coreDisputeID, + address _juror + ) internal view override returns (bool) { + if (!super._postDrawCheck(_round, _coreDisputeID, _juror)) return false; + + // Get the local dispute and extract token info from extraData + uint256 localDisputeID = coreDisputeIDToLocal[_coreDisputeID]; + Dispute storage dispute = disputes[localDisputeID]; + (address tokenGate, bool isERC1155, uint256 tokenId) = _extraDataToTokenInfo(dispute.extraData); + + // If no token gate is specified, allow all jurors + if (tokenGate == address(0)) return true; + + // Check juror's token balance + if (isERC1155) { + return IBalanceHolderERC1155(tokenGate).balanceOf(_juror, tokenId) > 0; + } else { + return IBalanceHolder(tokenGate).balanceOf(_juror) > 0; + } + } +} diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitShutter.sol b/contracts/src/arbitration/dispute-kits/DisputeKitShutter.sol index 320463722..c7efe11c1 100644 --- a/contracts/src/arbitration/dispute-kits/DisputeKitShutter.sol +++ b/contracts/src/arbitration/dispute-kits/DisputeKitShutter.sol @@ -5,12 +5,12 @@ pragma solidity 0.8.24; import {DisputeKitClassicBase, KlerosCore} from "./DisputeKitClassicBase.sol"; /// @title DisputeKitShutter +/// Added functionality: shielded voting. /// Dispute kit implementation of the Kleros v1 features including: /// - a drawing system: proportional to staked PNK, /// - a vote aggregation system: plurality, /// - an incentive system: equal split between coherent votes, /// - an appeal system: fund 2 choices only, vote on any choice. -/// Added functionality: an Shutter-specific event emitted when a vote is cast. contract DisputeKitShutter is DisputeKitClassicBase { string public constant override version = "0.11.1"; diff --git a/contracts/src/proxy/KlerosProxies.sol b/contracts/src/proxy/KlerosProxies.sol index 6490b917b..0ccd26033 100644 --- a/contracts/src/proxy/KlerosProxies.sol +++ b/contracts/src/proxy/KlerosProxies.sol @@ -23,6 +23,10 @@ contract DisputeKitGatedProxy is UUPSProxy { constructor(address _implementation, bytes memory _data) UUPSProxy(_implementation, _data) {} } +contract DisputeKitGatedShutterProxy is UUPSProxy { + constructor(address _implementation, bytes memory _data) UUPSProxy(_implementation, _data) {} +} + contract DisputeKitShutterProxy is UUPSProxy { constructor(address _implementation, bytes memory _data) UUPSProxy(_implementation, _data) {} } diff --git a/contracts/src/token/TestERC1155.sol b/contracts/src/token/TestERC1155.sol new file mode 100644 index 000000000..50a70fe03 --- /dev/null +++ b/contracts/src/token/TestERC1155.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {ERC1155} from "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; + +contract TestERC1155 is ERC1155 { + // ************************************* // + // * Storage * // + // ************************************* // + + address public owner; + uint256 private _nextTokenId; + + // ************************************* // + // * Constructor * // + // ************************************* // + + constructor() ERC1155("") { + owner = msg.sender; + } + + // ************************************* // + // * Function Modifiers * // + // ************************************* // + + modifier ownerOnly() { + require(msg.sender == owner, "Owner only"); + _; + } + + // ************************************* // + // * Governance * // + // ************************************* // + + function changeOwner(address _newOwner) external ownerOnly { + owner = _newOwner; + } + + // ************************************* // + // * State Modifiers * // + // ************************************* // + + function mint(address to, uint256 id, uint256 value, bytes memory data) external ownerOnly { + _mint(to, id, value, data); + } + + function mintBatch( + address to, + uint256[] memory ids, + uint256[] memory values, + bytes memory data + ) external ownerOnly { + _mintBatch(to, ids, values, data); + } + + function burn(address from, uint256 id, uint256 value) external ownerOnly { + _burn(from, id, value); + } + + function burnBatch(address from, uint256[] memory ids, uint256[] memory values) external ownerOnly { + _burnBatch(from, ids, values); + } +} diff --git a/contracts/test/arbitration/dispute-kit-gated.ts b/contracts/test/arbitration/dispute-kit-gated.ts new file mode 100644 index 000000000..daa78ce0c --- /dev/null +++ b/contracts/test/arbitration/dispute-kit-gated.ts @@ -0,0 +1,293 @@ +import { deployments, ethers, getNamedAccounts, network } from "hardhat"; +import { toBigInt, BigNumberish, Addressable } from "ethers"; +import { + PNK, + KlerosCore, + SortitionModule, + IncrementalNG, + DisputeKitGated, + TestERC20, + TestERC721, + TestERC1155, +} from "../../typechain-types"; +import { expect } from "chai"; +import { Courts } from "../../deploy/utils"; +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; +import { deployERC1155, deployERC721 } from "../../deploy/utils/deployTokens"; + +/* eslint-disable no-unused-vars */ +/* eslint-disable no-unused-expressions */ // https://github.com/standard/standard/issues/690#issuecomment-278533482 + +describe("DisputeKitGated", async () => { + const ONE_THOUSAND_PNK = 10n ** 21n; + const thousandPNK = (amount: BigNumberish) => toBigInt(amount) * ONE_THOUSAND_PNK; + const PNK = (amount: BigNumberish) => toBigInt(amount) * 10n ** 18n; + + let deployer: string; + let juror1: HardhatEthersSigner; + let juror2: HardhatEthersSigner; + let disputeKitGated: DisputeKitGated; + let pnk: PNK; + let dai: TestERC20; + let core: KlerosCore; + let sortitionModule: SortitionModule; + let rng: IncrementalNG; + let nft721: TestERC721; + let nft1155: TestERC1155; + const RANDOM = 424242n; + const GATED_DK_ID = 3; + const TOKEN_ID = 888; + const minStake = PNK(200); + + beforeEach("Setup", async () => { + ({ deployer } = await getNamedAccounts()); + [, juror1, juror2] = await ethers.getSigners(); + + await deployments.fixture(["Arbitration", "VeaMock"], { + fallbackToGlobal: true, + keepExistingDeployments: false, + }); + disputeKitGated = (await ethers.getContract("DisputeKitGated")) as DisputeKitGated; + pnk = (await ethers.getContract("PNK")) as PNK; + dai = (await ethers.getContract("DAI")) as TestERC20; + core = (await ethers.getContract("KlerosCore")) as KlerosCore; + sortitionModule = (await ethers.getContract("SortitionModule")) as SortitionModule; + + // Make the tests more deterministic with this dummy RNG + await deployments.deploy("IncrementalNG", { + from: deployer, + args: [RANDOM], + log: true, + }); + rng = (await ethers.getContract("IncrementalNG")) as IncrementalNG; + + await sortitionModule.changeRandomNumberGenerator(rng.target, 20).then((tx) => tx.wait()); + + const hre = require("hardhat"); + await deployERC721(hre, deployer, "TestERC721", "Nft721"); + nft721 = (await ethers.getContract("Nft721")) as TestERC721; + + await deployERC1155(hre, deployer, "TestERC1155", "Nft1155"); + nft1155 = (await ethers.getContract("Nft1155")) as TestERC1155; + await nft1155.mint(deployer, TOKEN_ID, 1, "0x00"); + }); + + const encodeExtraData = ( + courtId: number, + minJurors: BigNumberish, + disputeKitId: number, + tokenGate: string | Addressable, + isERC1155: boolean, + tokenId: BigNumberish + ) => { + // Packing of tokenGate and isERC1155 + // uint88 (padding 11 bytes) + bool (1 byte) + address (20 bytes) = 32 bytes + const packed = ethers.solidityPacked(["uint88", "bool", "address"], [0, isERC1155, tokenGate]); + return ethers.AbiCoder.defaultAbiCoder().encode( + ["uint256", "uint256", "uint256", "bytes32", "uint256"], + [courtId, minJurors, disputeKitId, packed, tokenId] + ); + }; + + const stakeAndDraw = async ( + courtId: number, + minJurors: BigNumberish, + disputeKitId: number, + tokenGate: string | Addressable, + isERC1155: boolean, + tokenId: BigNumberish + ) => { + // Stake jurors + for (const juror of [juror1, juror2]) { + await pnk.transfer(juror.address, thousandPNK(10)).then((tx) => tx.wait()); + expect(await pnk.balanceOf(juror.address)).to.equal(thousandPNK(10)); + + await pnk + .connect(juror) + .approve(core.target, thousandPNK(10), { gasLimit: 300000 }) + .then((tx) => tx.wait()); + + await core + .connect(juror) + .setStake(Courts.GENERAL, thousandPNK(10), { gasLimit: 300000 }) + .then((tx) => tx.wait()); + + expect(await sortitionModule.getJurorBalance(juror.address, 1)).to.deep.equal([ + thousandPNK(10), // totalStaked + 0, // totalLocked + thousandPNK(10), // stakedInCourt + 1, // nbOfCourts + ]); + } + + const extraData = encodeExtraData(courtId, minJurors, disputeKitId, tokenGate, isERC1155, tokenId); + // console.log("extraData", extraData); + + const tokenInfo = await disputeKitGated.extraDataToTokenInfo(extraData); + expect(tokenInfo[0]).to.equal(tokenGate); + expect(tokenInfo[1]).to.equal(isERC1155); + expect(tokenInfo[2]).to.equal(tokenId); + + const arbitrationCost = await core["arbitrationCost(bytes)"](extraData); + + // Warning: this dispute cannot be executed, in reality it should be created by an arbitrable contract, not an EOA. + const tx = await core["createDispute(uint256,bytes)"](2, extraData, { value: arbitrationCost }).then((tx) => + tx.wait() + ); + const disputeId = 0; + // console.log(tx?.logs); + + await network.provider.send("evm_increaseTime", [2000]); // Wait for minStakingTime + await network.provider.send("evm_mine"); + await sortitionModule.passPhase().then((tx) => tx.wait()); // Staking -> Generating + + const lookahead = await sortitionModule.rngLookahead(); + for (let index = 0; index < lookahead; index++) { + await network.provider.send("evm_mine"); + } + + await sortitionModule.passPhase().then((tx) => tx.wait()); // Generating -> Drawing + return core.draw(disputeId, 70, { gasLimit: 10000000 }); + }; + + describe("When gating with DAI token", async () => { + it("Should draw no juror if they don't have any DAI balance", async () => { + const nbOfJurors = 15n; + const tx = await stakeAndDraw(Courts.GENERAL, nbOfJurors, GATED_DK_ID, dai.target, false, 0).then((tx) => + tx.wait() + ); + + // Ensure that no juror is drawn + const drawLogs = + tx?.logs.filter((log: any) => log.fragment?.name === "Draw" && log.address === core.target) || []; + expect(drawLogs).to.have.length(0); + }); + + it("Should draw only the jurors who have some DAI balance", async () => { + dai.transfer(juror1.address, 1); + + const nbOfJurors = 15n; + const tx = await stakeAndDraw(Courts.GENERAL, nbOfJurors, GATED_DK_ID, dai.target, false, 0).then((tx) => + tx.wait() + ); + + // Ensure that only juror1 is drawn + const drawLogs = + tx?.logs.filter((log: any) => log.fragment?.name === "Draw" && log.address === core.target) || []; + expect(drawLogs).to.have.length(nbOfJurors); + drawLogs.forEach((log: any) => { + expect(log.args[0]).to.equal(juror1.address); + }); + + // Ensure that juror1 has PNK locked + expect(await sortitionModule.getJurorBalance(juror1.address, Courts.GENERAL)).to.deep.equal([ + thousandPNK(10), // totalStaked + minStake * nbOfJurors, // totalLocked + thousandPNK(10), // stakedInCourt + 1, // nbOfCourts + ]); + + // Ensure that juror2 has no PNK locked + expect(await sortitionModule.getJurorBalance(juror2.address, Courts.GENERAL)).to.deep.equal([ + thousandPNK(10), // totalStaked + 0, // totalLocked + thousandPNK(10), // stakedInCourt + 1, // nbOfCourts + ]); + }); + }); + + describe("When gating with ERC721 token", async () => { + it("Should draw no juror if they don't own the ERC721 token", async () => { + const nbOfJurors = 15n; + const tx = await stakeAndDraw(Courts.GENERAL, nbOfJurors, GATED_DK_ID, nft721.target, false, 0).then((tx) => + tx.wait() + ); + + // Ensure that no juror is drawn + const drawLogs = + tx?.logs.filter((log: any) => log.fragment?.name === "Draw" && log.address === core.target) || []; + expect(drawLogs).to.have.length(0); + }); + + it("Should draw only the jurors owning the ERC721 token", async () => { + await nft721.safeMint(juror2.address); + + const nbOfJurors = 15n; + const tx = await stakeAndDraw(Courts.GENERAL, nbOfJurors, GATED_DK_ID, nft721.target, false, 0).then((tx) => + tx.wait() + ); + + // Ensure that only juror2 is drawn + const drawLogs = + tx?.logs.filter((log: any) => log.fragment?.name === "Draw" && log.address === core.target) || []; + expect(drawLogs).to.have.length(nbOfJurors); + drawLogs.forEach((log: any) => { + expect(log.args[0]).to.equal(juror2.address); + }); + + // Ensure that juror1 is has no PNK locked + expect(await sortitionModule.getJurorBalance(juror1.address, Courts.GENERAL)).to.deep.equal([ + thousandPNK(10), // totalStaked + 0, // totalLocked + thousandPNK(10), // stakedInCourt + 1, // nbOfCourts + ]); + + // Ensure that juror2 has PNK locked + expect(await sortitionModule.getJurorBalance(juror2.address, Courts.GENERAL)).to.deep.equal([ + thousandPNK(10), // totalStaked + minStake * nbOfJurors, // totalLocked + thousandPNK(10), // stakedInCourt + 1, // nbOfCourts + ]); + }); + }); + + describe("When gating with ERC1155 token", async () => { + it("Should draw no juror if they don't own the ERC1155 token", async () => { + const nbOfJurors = 15n; + const tx = await stakeAndDraw(Courts.GENERAL, nbOfJurors, GATED_DK_ID, nft1155.target, true, TOKEN_ID).then( + (tx) => tx.wait() + ); + + // Ensure that no juror is drawn + const drawLogs = + tx?.logs.filter((log: any) => log.fragment?.name === "Draw" && log.address === core.target) || []; + expect(drawLogs).to.have.length(0); + }); + + it("Should draw only the jurors owning the ERC1155 token", async () => { + await nft1155.mint(juror2.address, TOKEN_ID, 1, "0x00"); + + const nbOfJurors = 15n; + const tx = await stakeAndDraw(Courts.GENERAL, nbOfJurors, GATED_DK_ID, nft1155.target, true, TOKEN_ID).then( + (tx) => tx.wait() + ); + + // Ensure that only juror2 is drawn + const drawLogs = + tx?.logs.filter((log: any) => log.fragment?.name === "Draw" && log.address === core.target) || []; + expect(drawLogs).to.have.length(nbOfJurors); + drawLogs.forEach((log: any) => { + expect(log.args[0]).to.equal(juror2.address); + }); + + // Ensure that juror1 is has no PNK locked + expect(await sortitionModule.getJurorBalance(juror1.address, Courts.GENERAL)).to.deep.equal([ + thousandPNK(10), // totalStaked + 0, // totalLocked + thousandPNK(10), // stakedInCourt + 1, // nbOfCourts + ]); + + // Ensure that juror2 has PNK locked + expect(await sortitionModule.getJurorBalance(juror2.address, Courts.GENERAL)).to.deep.equal([ + thousandPNK(10), // totalStaked + minStake * nbOfJurors, // totalLocked + thousandPNK(10), // stakedInCourt + 1, // nbOfCourts + ]); + }); + }); +}); diff --git a/contracts/test/arbitration/draw.ts b/contracts/test/arbitration/draw.ts index 1ce33bd5b..12790fdb5 100644 --- a/contracts/test/arbitration/draw.ts +++ b/contracts/test/arbitration/draw.ts @@ -150,6 +150,7 @@ describe("Draw Benchmark", async () => { const [disputeId] = abiCoder.decode(["uint"], ethers.getBytes(`${trace.returnValue}`)); const lastBlock = await ethers.provider.getBlock(tx.blockNumber - 1); if (lastBlock?.hash === null || lastBlock?.hash === undefined) throw new Error("lastBlock is null || undefined"); + // Relayer tx await homeGateway .connect(await ethers.getSigner(relayer)) diff --git a/contracts/test/arbitration/index.ts b/contracts/test/arbitration/index.ts index 78c11d0d7..d8e7ef089 100644 --- a/contracts/test/arbitration/index.ts +++ b/contracts/test/arbitration/index.ts @@ -1,25 +1,39 @@ import { expect } from "chai"; import { deployments, ethers } from "hardhat"; -import { KlerosCore, DisputeKitClassic, DisputeKitShutter } from "../../typechain-types"; +import { + KlerosCore, + DisputeKitClassic, + DisputeKitShutter, + DisputeKitGated, + DisputeKitGatedShutter, +} from "../../typechain-types"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; describe("DisputeKitClassic", async () => { // eslint-disable-next-line no-unused-vars let deployer: HardhatEthersSigner; - let core: KlerosCore, disputeKit: DisputeKitClassic, disputeKitShutter: DisputeKitShutter; + let core: KlerosCore, + disputeKit: DisputeKitClassic, + disputeKitShutter: DisputeKitShutter, + disputeKitGated: DisputeKitGated, + disputeKitGatedShutter: DisputeKitGatedShutter; before("Deploying", async () => { [deployer] = await ethers.getSigners(); - [core, disputeKit, disputeKitShutter] = await deployContracts(); + [core, disputeKit, disputeKitShutter, disputeKitGated, disputeKitGatedShutter] = await deployContracts(); }); it("Kleros Core initialization", async () => { const events = await core.queryFilter(core.filters.DisputeKitCreated()); - expect(events.length).to.equal(2); + expect(events.length).to.equal(4); expect(events[0].args._disputeKitID).to.equal(1); expect(events[0].args._disputeKitAddress).to.equal(disputeKit.target); expect(events[1].args._disputeKitID).to.equal(2); expect(events[1].args._disputeKitAddress).to.equal(disputeKitShutter.target); + expect(events[2].args._disputeKitID).to.equal(3); + expect(events[2].args._disputeKitAddress).to.equal(disputeKitGated.target); + expect(events[3].args._disputeKitID).to.equal(4); + expect(events[3].args._disputeKitAddress).to.equal(disputeKitGatedShutter.target); // Reminder: the Forking court will be added which will break these expectations. const events2 = await core.queryFilter(core.filters.CourtCreated()); @@ -35,7 +49,7 @@ describe("DisputeKitClassic", async () => { expect(events2[0].args._supportedDisputeKits).to.deep.equal([1]); const events3 = await core.queryFilter(core.filters.DisputeKitEnabled()); - expect(events3.length).to.equal(2); + expect(events3.length).to.equal(4); const classicDisputeKit = events3[0].args; expect(classicDisputeKit._courtID).to.equal(1); @@ -46,6 +60,16 @@ describe("DisputeKitClassic", async () => { expect(shutterDisputeKit._courtID).to.equal(1); expect(shutterDisputeKit._disputeKitID).to.equal(2); expect(shutterDisputeKit._enable).to.equal(true); + + const gatedDisputeKit = events3[2].args; + expect(gatedDisputeKit._courtID).to.equal(1); + expect(gatedDisputeKit._disputeKitID).to.equal(3); + expect(gatedDisputeKit._enable).to.equal(true); + + const gatedShutterDisputeKit = events3[3].args; + expect(gatedShutterDisputeKit._courtID).to.equal(1); + expect(gatedShutterDisputeKit._disputeKitID).to.equal(4); + expect(gatedShutterDisputeKit._enable).to.equal(true); }); it("Should create a dispute", async () => { @@ -69,13 +93,17 @@ describe("DisputeKitClassic", async () => { }); }); -async function deployContracts(): Promise<[KlerosCore, DisputeKitClassic, DisputeKitShutter]> { +async function deployContracts(): Promise< + [KlerosCore, DisputeKitClassic, DisputeKitShutter, DisputeKitGated, DisputeKitGatedShutter] +> { await deployments.fixture(["Arbitration", "VeaMock"], { fallbackToGlobal: true, keepExistingDeployments: false, }); const disputeKit = (await ethers.getContract("DisputeKitClassic")) as DisputeKitClassic; const disputeKitShutter = (await ethers.getContract("DisputeKitShutter")) as DisputeKitShutter; + const disputeKitGated = (await ethers.getContract("DisputeKitGated")) as DisputeKitGated; + const disputeKitGatedShutter = (await ethers.getContract("DisputeKitGatedShutter")) as DisputeKitGatedShutter; const core = (await ethers.getContract("KlerosCore")) as KlerosCore; - return [core, disputeKit, disputeKitShutter]; + return [core, disputeKit, disputeKitShutter, disputeKitGated, disputeKitGatedShutter]; }