Skip to content

Universal Gated Dispute Kit and Shutter Gated Dispute Kit #2045

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
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
14 changes: 14 additions & 0 deletions contracts/deploy/utils/deployTokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,17 @@ export const deployERC721 = async (
log: true,
});
};

export const deployERC1155 = async (
hre: HardhatRuntimeEnvironment,
deployer: string,
name: string,
ticker: string
): Promise<Contract> => {
return getContractOrDeploy(hre, ticker, {
from: deployer,
contract: "TestERC1155",
args: [],
log: true,
});
};
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
189 changes: 189 additions & 0 deletions contracts/src/arbitration/dispute-kits/DisputeKitGatedShutter.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
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
Loading
Loading