Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions contracts/deploy/00-home-chain-arbitration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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],
Expand Down
20 changes: 2 additions & 18 deletions contracts/hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand All @@ -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"],
Expand Down
74 changes: 37 additions & 37 deletions contracts/src/arbitration/dispute-kits/DisputeKitGated.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 * //
// ************************************* //
Expand All @@ -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;
}

// ************************ //
Expand All @@ -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,
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
4 changes: 4 additions & 0 deletions contracts/src/proxy/KlerosProxies.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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) {}
}
Expand Down
133 changes: 133 additions & 0 deletions contracts/test/arbitration/dispute-kit-gated.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { deployments, ethers, getNamedAccounts, network } from "hardhat";
import { toBigInt, BigNumberish, Addressable } from "ethers";
import { PNK, KlerosCore, SortitionModule, IncrementalNG, DisputeKitGated } from "../../typechain-types";
import { expect } from "chai";
import { Courts } from "../../deploy/utils";
import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers";

/* 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;

let deployer: string;
let juror1: HardhatEthersSigner;
let juror2: HardhatEthersSigner;
let disputeKitGated: DisputeKitGated;
let pnk: PNK;
let core: KlerosCore;
let sortitionModule: SortitionModule;
let rng: IncrementalNG;
const RANDOM = 424242n;

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;
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 encodeExtraData = (
courtId: number,
minJurors: number,
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: number,
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) =>

Check notice

Code scanning / SonarCloud

Unused assignments should be removed Low test

Remove this useless assignment to variable "tx". See more on SonarQube Cloud
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, 20, { gasLimit: 1000000 });
};

describe("When gating with PNK token", async () => {
it("Should draw all the jurors successfully", async () => {
await stakeAndDraw(Courts.GENERAL, 3, 3, pnk.target, false, 0);
// TODO: expect....
});
});
});
1 change: 1 addition & 0 deletions contracts/test/arbitration/draw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Loading
Loading