Skip to content

Commit b1e385d

Browse files
committed
feat: gated dispute kit with dynamic token gate, shutter gated dk, tests in progress
1 parent 2116239 commit b1e385d

File tree

8 files changed

+232
-65
lines changed

8 files changed

+232
-65
lines changed

contracts/deploy/00-home-chain-arbitration.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { DeployFunction } from "hardhat-deploy/types";
33
import { getContractAddress } from "./utils/getContractAddress";
44
import { deployUpgradable } from "./utils/deployUpgradable";
55
import { changeCurrencyRate } from "./utils/klerosCoreHelper";
6-
import { HomeChains, isSkipped, isDevnet, PNK, ETH } from "./utils";
6+
import { HomeChains, isSkipped, isDevnet, PNK, ETH, Courts } from "./utils";
77
import { getContractOrDeploy, getContractOrDeployUpgradable } from "./utils/getContractOrDeploy";
88
import { deployERC20AndFaucet } from "./utils/deployTokens";
99
import { ChainlinkRNG, DisputeKitClassic, KlerosCore } from "../typechain-types";
@@ -103,8 +103,25 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment)
103103
log: true,
104104
});
105105
await core.addNewDisputeKit(disputeKitShutter.address);
106-
await core.enableDisputeKits(1, [2], true); // enable disputeKitShutter on the General Court
106+
await core.enableDisputeKits(Courts.GENERAL, [2], true); // enable disputeKitShutter on the General Court
107107

108+
const disputeKitGated = await deployUpgradable(deployments, "DisputeKitGated", {
109+
from: deployer,
110+
args: [deployer, core.target],
111+
log: true,
112+
});
113+
await core.addNewDisputeKit(disputeKitGated.address);
114+
await core.enableDisputeKits(Courts.GENERAL, [3], true); // enable disputeKitGated on the General Court
115+
116+
const disputeKitGatedShutter = await deployUpgradable(deployments, "DisputeKitGatedShutter", {
117+
from: deployer,
118+
args: [deployer, core.target],
119+
log: true,
120+
});
121+
await core.addNewDisputeKit(disputeKitGatedShutter.address);
122+
await core.enableDisputeKits(Courts.GENERAL, [4], true); // enable disputeKitGatedShutter on the General Court
123+
124+
// Snapshot proxy
108125
await deploy("KlerosCoreSnapshotProxy", {
109126
from: deployer,
110127
args: [deployer, core.target],

contracts/hardhat.config.ts

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -105,15 +105,7 @@ const config: HardhatUserConfig = {
105105
arbitrumSepolia: {
106106
chainId: 421614,
107107
url: process.env.ARBITRUM_SEPOLIA_RPC ?? `https://arbitrum-sepolia.infura.io/v3/${process.env.INFURA_API_KEY}`,
108-
accounts:
109-
(process.env.ARB_GOERLI_PRIVATE_KEY_WALLET_1 && [
110-
process.env.ARB_GOERLI_PRIVATE_KEY_WALLET_1 as string,
111-
process.env.ARB_GOERLI_PRIVATE_KEY_WALLET_2 as string,
112-
process.env.ARB_GOERLI_PRIVATE_KEY_WALLET_3 as string,
113-
process.env.ARB_GOERLI_PRIVATE_KEY_WALLET_4 as string,
114-
process.env.ARB_GOERLI_PRIVATE_KEY_WALLET_5 as string,
115-
]) ||
116-
(process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : []),
108+
accounts: process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [],
117109
live: true,
118110
saveDeployments: true,
119111
tags: ["staging", "home", "layer2"],
@@ -131,15 +123,7 @@ const config: HardhatUserConfig = {
131123
arbitrumSepoliaDevnet: {
132124
chainId: 421614,
133125
url: process.env.ARBITRUM_SEPOLIA_RPC ?? `https://arbitrum-sepolia.infura.io/v3/${process.env.INFURA_API_KEY}`,
134-
accounts:
135-
(process.env.ARB_GOERLI_PRIVATE_KEY_WALLET_1 && [
136-
process.env.ARB_GOERLI_PRIVATE_KEY_WALLET_1 as string,
137-
process.env.ARB_GOERLI_PRIVATE_KEY_WALLET_2 as string,
138-
process.env.ARB_GOERLI_PRIVATE_KEY_WALLET_3 as string,
139-
process.env.ARB_GOERLI_PRIVATE_KEY_WALLET_4 as string,
140-
process.env.ARB_GOERLI_PRIVATE_KEY_WALLET_5 as string,
141-
]) ||
142-
(process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : []),
126+
accounts: process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [],
143127
live: true,
144128
saveDeployments: true,
145129
tags: ["staging", "home", "layer2"],

contracts/src/arbitration/dispute-kits/DisputeKitGated.sol

Lines changed: 37 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,6 @@ interface IBalanceHolderERC1155 {
2929
contract DisputeKitGated is DisputeKitClassicBase {
3030
string public constant override version = "0.10.0";
3131

32-
// ************************************* //
33-
// * Storage * //
34-
// ************************************* //
35-
36-
address public tokenGate; // The token used for gating access.
37-
uint256 public tokenId; // Only used for ERC-1155
38-
bool public isERC1155; // True if the tokenGate is an ERC-1155, false otherwise.
39-
4032
// ************************************* //
4133
// * Constructor * //
4234
// ************************************* //
@@ -49,20 +41,8 @@ contract DisputeKitGated is DisputeKitClassicBase {
4941
/// @dev Initializer.
5042
/// @param _governor The governor's address.
5143
/// @param _core The KlerosCore arbitrator.
52-
/// @param _tokenGate The token used for gating access.
53-
/// @param _tokenId The token ID for ERC-1155 (ignored for other token types)
54-
/// @param _isERC1155 Whether the token is an ERC-1155
55-
function initialize(
56-
address _governor,
57-
KlerosCore _core,
58-
address _tokenGate,
59-
uint256 _tokenId,
60-
bool _isERC1155
61-
) external reinitializer(1) {
44+
function initialize(address _governor, KlerosCore _core) external reinitializer(1) {
6245
__DisputeKitClassicBase_initialize(_governor, _core);
63-
tokenGate = _tokenGate;
64-
tokenId = _tokenId;
65-
isERC1155 = _isERC1155;
6646
}
6747

6848
// ************************ //
@@ -75,26 +55,37 @@ contract DisputeKitGated is DisputeKitClassicBase {
7555
// NOP
7656
}
7757

78-
/// @dev Changes the `tokenGate` to an ERC-20 or ERC-721 token.
79-
/// @param _tokenGate The new value for the `tokenGate` storage variable.
80-
function changeTokenGateERC20OrERC721(address _tokenGate) external onlyByGovernor {
81-
tokenGate = _tokenGate;
82-
isERC1155 = false;
83-
}
84-
85-
/// @dev Changes the `tokenGate` to an ERC-1155 token.
86-
/// @param _tokenGate The new value for the `tokenGate` storage variable.
87-
/// @param _tokenId The new value for the `tokenId` storage variable.
88-
function changeTokenGateERC1155(address _tokenGate, uint256 _tokenId) external onlyByGovernor {
89-
tokenGate = _tokenGate;
90-
tokenId = _tokenId;
91-
isERC1155 = true;
92-
}
93-
9458
// ************************************* //
9559
// * Internal * //
9660
// ************************************* //
9761

62+
/// @dev Extracts token gating information from the extra data.
63+
/// @param _extraData The extra data bytes array with the following encoding:
64+
/// - bytes 0-31: uint96 courtID, not used here
65+
/// - bytes 32-63: uint256 minJurors, not used here
66+
/// - bytes 64-95: uint256 disputeKitID, not used here
67+
/// - bytes 96-127: uint256 packedTokenGateAndFlag (address tokenGate in bits 0-159, bool isERC1155 in bit 160)
68+
/// - bytes 128-159: uint256 tokenId
69+
/// @return tokenGate The address of the token contract used for gating access.
70+
/// @return isERC1155 True if the token is an ERC-1155, false for ERC-20/ERC-721.
71+
/// @return tokenId The token ID for ERC-1155 tokens (ignored for ERC-20/ERC-721).
72+
function extraDataToTokenInfo(
73+
bytes memory _extraData
74+
) public pure returns (address tokenGate, bool isERC1155, uint256 tokenId) {
75+
// Need at least 160 bytes to safely read the parameters
76+
if (_extraData.length < 160) return (address(0), false, 0);
77+
78+
assembly {
79+
// solium-disable-line security/no-inline-assembly
80+
let packedTokenGateIsERC1155 := mload(add(_extraData, 0x80)) // 4th parameter at offset 128
81+
tokenId := mload(add(_extraData, 0xA0)) // 5th parameter at offset 160 (moved up)
82+
83+
// Unpack address from lower 160 bits and bool from bit 160
84+
tokenGate := and(packedTokenGateIsERC1155, 0xffffffffffffffffffffffffffffffffffffffff)
85+
isERC1155 := and(shr(160, packedTokenGateIsERC1155), 1)
86+
}
87+
}
88+
9889
/// @inheritdoc DisputeKitClassicBase
9990
function _postDrawCheck(
10091
Round storage _round,
@@ -103,6 +94,15 @@ contract DisputeKitGated is DisputeKitClassicBase {
10394
) internal view override returns (bool) {
10495
if (!super._postDrawCheck(_round, _coreDisputeID, _juror)) return false;
10596

97+
// Get the local dispute and extract token info from extraData
98+
uint256 localDisputeID = coreDisputeIDToLocal[_coreDisputeID];
99+
Dispute storage dispute = disputes[localDisputeID];
100+
(address tokenGate, bool isERC1155, uint256 tokenId) = extraDataToTokenInfo(dispute.extraData);
101+
102+
// If no token gate is specified, allow all jurors
103+
if (tokenGate == address(0)) return true;
104+
105+
// Check juror's token balance
106106
if (isERC1155) {
107107
return IBalanceHolderERC1155(tokenGate).balanceOf(_juror, tokenId) > 0;
108108
} else {

contracts/src/arbitration/dispute-kits/DisputeKitShutter.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ pragma solidity 0.8.24;
55
import {DisputeKitClassicBase, KlerosCore} from "./DisputeKitClassicBase.sol";
66

77
/// @title DisputeKitShutter
8+
/// Added functionality: shielded voting.
89
/// Dispute kit implementation of the Kleros v1 features including:
910
/// - a drawing system: proportional to staked PNK,
1011
/// - a vote aggregation system: plurality,
1112
/// - an incentive system: equal split between coherent votes,
1213
/// - an appeal system: fund 2 choices only, vote on any choice.
13-
/// Added functionality: an Shutter-specific event emitted when a vote is cast.
1414
contract DisputeKitShutter is DisputeKitClassicBase {
1515
string public constant override version = "0.11.1";
1616

contracts/src/proxy/KlerosProxies.sol

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ contract DisputeKitGatedProxy is UUPSProxy {
2323
constructor(address _implementation, bytes memory _data) UUPSProxy(_implementation, _data) {}
2424
}
2525

26+
contract DisputeKitGatedShutterProxy is UUPSProxy {
27+
constructor(address _implementation, bytes memory _data) UUPSProxy(_implementation, _data) {}
28+
}
29+
2630
contract DisputeKitShutterProxy is UUPSProxy {
2731
constructor(address _implementation, bytes memory _data) UUPSProxy(_implementation, _data) {}
2832
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { deployments, ethers, getNamedAccounts, network } from "hardhat";
2+
import { toBigInt, BigNumberish, Addressable } from "ethers";
3+
import { PNK, KlerosCore, SortitionModule, IncrementalNG, DisputeKitGated } from "../../typechain-types";
4+
import { expect } from "chai";
5+
import { Courts } from "../../deploy/utils";
6+
import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers";
7+
8+
/* eslint-disable no-unused-vars */
9+
/* eslint-disable no-unused-expressions */ // https://github.com/standard/standard/issues/690#issuecomment-278533482
10+
11+
describe("DisputeKitGated", async () => {
12+
const ONE_THOUSAND_PNK = 10n ** 21n;
13+
const thousandPNK = (amount: BigNumberish) => toBigInt(amount) * ONE_THOUSAND_PNK;
14+
15+
let deployer: string;
16+
let juror1: HardhatEthersSigner;
17+
let juror2: HardhatEthersSigner;
18+
let disputeKitGated: DisputeKitGated;
19+
let pnk: PNK;
20+
let core: KlerosCore;
21+
let sortitionModule: SortitionModule;
22+
let rng: IncrementalNG;
23+
const RANDOM = 424242n;
24+
25+
beforeEach("Setup", async () => {
26+
({ deployer } = await getNamedAccounts());
27+
[, juror1, juror2] = await ethers.getSigners();
28+
29+
await deployments.fixture(["Arbitration", "VeaMock"], {
30+
fallbackToGlobal: true,
31+
keepExistingDeployments: false,
32+
});
33+
disputeKitGated = (await ethers.getContract("DisputeKitGated")) as DisputeKitGated;
34+
pnk = (await ethers.getContract("PNK")) as PNK;
35+
core = (await ethers.getContract("KlerosCore")) as KlerosCore;
36+
sortitionModule = (await ethers.getContract("SortitionModule")) as SortitionModule;
37+
38+
// Make the tests more deterministic with this dummy RNG
39+
await deployments.deploy("IncrementalNG", {
40+
from: deployer,
41+
args: [RANDOM],
42+
log: true,
43+
});
44+
rng = (await ethers.getContract("IncrementalNG")) as IncrementalNG;
45+
46+
await sortitionModule.changeRandomNumberGenerator(rng.target, 20).then((tx) => tx.wait());
47+
});
48+
49+
const encodeExtraData = (
50+
courtId: number,
51+
minJurors: number,
52+
disputeKitId: number,
53+
tokenGate: string | Addressable,
54+
isERC1155: boolean,
55+
tokenId: BigNumberish
56+
) => {
57+
// Packing of tokenGate and isERC1155
58+
// uint88 (padding 11 bytes) + bool (1 byte) + address (20 bytes) = 32 bytes
59+
const packed = ethers.solidityPacked(["uint88", "bool", "address"], [0, isERC1155, tokenGate]);
60+
return ethers.AbiCoder.defaultAbiCoder().encode(
61+
["uint256", "uint256", "uint256", "bytes32", "uint256"],
62+
[courtId, minJurors, disputeKitId, packed, tokenId]
63+
);
64+
};
65+
66+
const stakeAndDraw = async (
67+
courtId: number,
68+
minJurors: number,
69+
disputeKitId: number,
70+
tokenGate: string | Addressable,
71+
isERC1155: boolean,
72+
tokenId: BigNumberish
73+
) => {
74+
// Stake jurors
75+
for (const juror of [juror1, juror2]) {
76+
await pnk.transfer(juror.address, thousandPNK(10)).then((tx) => tx.wait());
77+
expect(await pnk.balanceOf(juror.address)).to.equal(thousandPNK(10));
78+
79+
await pnk
80+
.connect(juror)
81+
.approve(core.target, thousandPNK(10), { gasLimit: 300000 })
82+
.then((tx) => tx.wait());
83+
84+
await core
85+
.connect(juror)
86+
.setStake(Courts.GENERAL, thousandPNK(10), { gasLimit: 300000 })
87+
.then((tx) => tx.wait());
88+
89+
expect(await sortitionModule.getJurorBalance(juror.address, 1)).to.deep.equal([
90+
thousandPNK(10), // totalStaked
91+
0, // totalLocked
92+
thousandPNK(10), // stakedInCourt
93+
1, // nbOfCourts
94+
]);
95+
}
96+
97+
const extraData = encodeExtraData(courtId, minJurors, disputeKitId, tokenGate, isERC1155, tokenId);
98+
console.log("extraData", extraData);
99+
100+
const tokenInfo = await disputeKitGated.extraDataToTokenInfo(extraData);
101+
expect(tokenInfo[0]).to.equal(pnk.target);
102+
expect(tokenInfo[1]).to.equal(false);
103+
expect(tokenInfo[2]).to.equal(0);
104+
105+
const arbitrationCost = await core["arbitrationCost(bytes)"](extraData);
106+
107+
// Warning: this dispute cannot be executed, in reality it should be created by an arbitrable contract, not an EOA.
108+
const tx = await core["createDispute(uint256,bytes)"](2, extraData, { value: arbitrationCost }).then((tx) =>
109+
tx.wait()
110+
);
111+
const disputeId = 0;
112+
// console.log(tx?.logs);
113+
114+
await network.provider.send("evm_increaseTime", [2000]); // Wait for minStakingTime
115+
await network.provider.send("evm_mine");
116+
await sortitionModule.passPhase().then((tx) => tx.wait()); // Staking -> Generating
117+
118+
const lookahead = await sortitionModule.rngLookahead();
119+
for (let index = 0; index < lookahead; index++) {
120+
await network.provider.send("evm_mine");
121+
}
122+
123+
await sortitionModule.passPhase().then((tx) => tx.wait()); // Generating -> Drawing
124+
return core.draw(disputeId, 20, { gasLimit: 1000000 });
125+
};
126+
127+
describe("When gating with PNK token", async () => {
128+
it("Should draw all the jurors successfully", async () => {
129+
await stakeAndDraw(Courts.GENERAL, 3, 3, pnk.target, false, 0);
130+
// TODO: expect....
131+
});
132+
});
133+
});

contracts/test/arbitration/draw.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ describe("Draw Benchmark", async () => {
150150
const [disputeId] = abiCoder.decode(["uint"], ethers.getBytes(`${trace.returnValue}`));
151151
const lastBlock = await ethers.provider.getBlock(tx.blockNumber - 1);
152152
if (lastBlock?.hash === null || lastBlock?.hash === undefined) throw new Error("lastBlock is null || undefined");
153+
153154
// Relayer tx
154155
await homeGateway
155156
.connect(await ethers.getSigner(relayer))

0 commit comments

Comments
 (0)