Skip to content

Staking refactor including bug fixes #2021

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

Draft
wants to merge 24 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a255c1d
refactor: new staking and sortition architecture (wip)
jaybuidl Jun 4, 2025
c38ecba
refactor: new staking and sortition architecture (wip)
jaybuidl Jun 4, 2025
70c173b
refactor: new staking and sortition architecture (wip)
jaybuidl Jun 5, 2025
d27e204
refactor: new staking and sortition architecture (wip)
jaybuidl Jun 5, 2025
8ca5d46
refactor: new staking and sortition architecture (wip)
jaybuidl Jun 5, 2025
1611b4b
refactor: moved the new arbitration contracts around
jaybuidl Jun 5, 2025
98c60ec
refactor: new staking and sortition architecture (wip)
jaybuidl Jun 5, 2025
8fd11dc
refactor: aligning with the staking fix PR (wip)
jaybuidl Jun 5, 2025
1604509
refactor: moved jurorStake mapping and struct from Sortition to Stake…
jaybuidl Jun 5, 2025
fb35a86
refactor: stPNK removal
jaybuidl Jun 5, 2025
3d4832a
refactor: moved StakeSet event from SortitionSumTree to StakeController
jaybuidl Jun 5, 2025
57d5261
feat: no more SortitionSumTree.setStake() failure to handle
jaybuidl Jun 6, 2025
9d87373
refactor: merged StakeController._setStake() and _setStakeBySystem(),…
jaybuidl Jun 9, 2025
0cf0ec4
refactor: replaced Core._setStakeBySystem by ._setStake
jaybuidl Jun 9, 2025
4c8db66
refactor: failed delayed stake handling now relies on a try/catch blo…
jaybuidl Jun 9, 2025
8a5fbbd
feat: do not update StakeController state if PNK transfer fails
jaybuidl Jun 9, 2025
799dad1
chore: clean up
jaybuidl Jun 9, 2025
a050ca6
fix: access control on setStakeDelayed
jaybuidl Jun 9, 2025
51d7739
chore: updated KlerosProxies
jaybuidl Jun 9, 2025
0eb3292
chore: clean up
jaybuidl Jun 9, 2025
d99def1
refactor: removed unnecessary base contract for Stake Controller and …
jaybuidl Jun 9, 2025
cb7d9b4
fix: missing setStakeDelayed
jaybuidl Jun 9, 2025
56b7b34
fix: proxy versions, KlerosCoreSnapshotProxy
jaybuidl Jun 9, 2025
6c28f88
docs: minor things
jaybuidl Jun 9, 2025
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
79 changes: 79 additions & 0 deletions contracts/deploy/00-home-chain-arbitration-v2-neo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { HardhatRuntimeEnvironment } from "hardhat/types";
import { DeployFunction } from "hardhat-deploy/types";
import { getContractAddress } from "./utils/getContractAddress";
import { deployUpgradable } from "./utils/deployUpgradable";
import { HomeChains, isSkipped, isDevnet, PNK, ETH } from "./utils";
import { getContractOrDeploy, getContractOrDeployUpgradable } from "./utils/getContractOrDeploy";
import { deployERC20AndFaucet, deployERC721 } from "./utils/deployTokens";
import { ChainlinkRNG, DisputeKitClassic, KlerosCoreNeo, StakeControllerNeo, VaultNeo } from "../typechain-types";

const deployArbitrationV2Neo: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { ethers, deployments, getNamedAccounts, getChainId } = hre;
const { deploy } = deployments;
const { ZeroAddress } = hre.ethers;
const RNG_LOOKAHEAD = 20;

// fallback to hardhat node signers on local network
const deployer = (await getNamedAccounts()).deployer ?? (await hre.ethers.getSigners())[0].address;
const chainId = Number(await getChainId());
console.log("deploying to %s with deployer %s", HomeChains[chainId], deployer);

const pnk = await deployERC20AndFaucet(hre, deployer, "PNK");
const weth = await deployERC20AndFaucet(hre, deployer, "WETH");
const nft = await deployERC721(hre, deployer, "Kleros V2 Neo Early User", "KlerosV2NeoEarlyUser");

await getContractOrDeploy(hre, "TransactionBatcher", { from: deployer, args: [], log: true });

await deployUpgradable(deployments, "PolicyRegistry", { from: deployer, args: [deployer], log: true });

await deployUpgradable(deployments, "EvidenceModule", { from: deployer, args: [deployer], log: true });

const disputeKit = await deployUpgradable(deployments, "DisputeKitClassicV2Neo", {
from: deployer,
contract: "DisputeKitClassic",
args: [deployer, ZeroAddress],
log: true,
});

// TODO.......

const disputeTemplateRegistry = await getContractOrDeployUpgradable(hre, "DisputeTemplateRegistry", {
from: deployer,
args: [deployer],
log: true,
});

const resolver = await deploy("DisputeResolverV2Neo", {
from: deployer,
contract: "DisputeResolver",
args: [core.target, disputeTemplateRegistry.target],
log: true,
});

console.log(`core.changeArbitrableWhitelist(${resolver.address}, true)`);
await core.changeArbitrableWhitelist(resolver.address, true);

await deploy("KlerosCoreNeoSnapshotProxy", {
from: deployer,
contract: "KlerosCoreSnapshotProxy",
args: [deployer, core.target],
log: true,
});

console.log("✅ V2 Neo Architecture deployment completed successfully!");
console.log(`📦 VaultNeo: ${pnkVaultNeo.address}`);
console.log(`🎫 stPNKNeo: ${stPNK.address}`);
console.log(`🎯 SortitionSumTreeNeo: ${sortitionModuleV2Neo.address}`);
console.log(`🎮 StakeControllerNeo: ${stakeControllerNeo.target}`);
console.log(`⚖️ KlerosCoreNeo: ${klerosCoreV2Neo.target}`);
console.log(`🎨 JurorNFT: ${nft.target}`);
console.log(`🔐 DisputeResolver: ${resolver.address}`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Script will crash – multiple variables are referenced before declaration.

core, pnkVaultNeo, stPNK, sortitionModuleV2Neo, stakeControllerNeo, and klerosCoreV2Neo are logged or passed as args but are never defined.
core is also used when deploying DisputeResolverV2Neo.

Minimal fix sketch:

-const disputeKit = await deployUpgradable(...);
-// TODO.......
+// Deploy core before it is referenced
+const klerosCoreV2Neo = await deployUpgradable(deployments, "KlerosCoreV2Neo", {
+  from: deployer,
+  args: [deployer, disputeKit.target /* … other args … */],
+  log: true,
+});
+const core = klerosCoreV2Neo; // alias for readability
+
+// Deploy Vault / StakeController and capture returned instances
+const pnkVaultNeo = await deployUpgradable(deployments, "VaultNeo", { /* … */ });
+const stakeControllerNeo = await deployUpgradable(deployments, "StakeControllerNeo", { /* … */ });
+const sortitionModuleV2Neo = await deployUpgradable(deployments, "SortitionSumTreeNeo", { /* … */ });
+const stPNK = await getContractOrDeploy(hre, "stPNK", { /* … */ });

Without these definitions the deployment script will throw on the first reference and break CI.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In contracts/deploy/00-home-chain-arbitration-v2-neo.ts between lines 38 and 70,
several variables such as core, pnkVaultNeo, stPNK, sortitionModuleV2Neo,
stakeControllerNeo, and klerosCoreV2Neo are used before being declared, causing
the script to crash. To fix this, ensure all these variables are properly
declared and assigned their respective contract instances or deployment results
before they are referenced or passed as arguments. This includes defining core
before deploying DisputeResolverV2Neo and initializing the other variables
before logging their addresses.

};

deployArbitrationV2Neo.tags = ["ArbitrationV2Neo"];
deployArbitrationV2Neo.dependencies = ["ChainlinkRNG"];
deployArbitrationV2Neo.skip = async ({ network }) => {
return isSkipped(network, !HomeChains[network.config.chainId ?? 0]);
};

export default deployArbitrationV2Neo;
158 changes: 158 additions & 0 deletions contracts/deploy/00-home-chain-arbitration-v2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { HardhatRuntimeEnvironment } from "hardhat/types";
import { DeployFunction } from "hardhat-deploy/types";
import { getContractAddress } from "./utils/getContractAddress";
import { deployUpgradable } from "./utils/deployUpgradable";
import { HomeChains, isSkipped, isDevnet, PNK, ETH } from "./utils";
import { getContractOrDeploy, getContractOrDeployUpgradable } from "./utils/getContractOrDeploy";
import { deployERC20AndFaucet } from "./utils/deployTokens";
import { ChainlinkRNG, DisputeKitClassic, KlerosCore, StakeController, Vault } from "../typechain-types";
import { changeCurrencyRate } from "./utils/klerosCoreHelper";

const deployArbitrationV2: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { ethers, deployments, getNamedAccounts, getChainId } = hre;
const { deploy } = deployments;
const { ZeroAddress } = hre.ethers;
const RNG_LOOKAHEAD = 20;

// fallback to hardhat node signers on local network
const deployer = (await getNamedAccounts()).deployer ?? (await hre.ethers.getSigners())[0].address;
const chainId = Number(await getChainId());
console.log("deploying to %s with deployer %s", HomeChains[chainId], deployer);

const pnk = await deployERC20AndFaucet(hre, deployer, "PNK");
const dai = await deployERC20AndFaucet(hre, deployer, "DAI");
const weth = await deployERC20AndFaucet(hre, deployer, "WETH");

await getContractOrDeploy(hre, "TransactionBatcher", { from: deployer, args: [], log: true });

await getContractOrDeployUpgradable(hre, "PolicyRegistry", { from: deployer, args: [deployer], log: true });

await getContractOrDeployUpgradable(hre, "EvidenceModule", { from: deployer, args: [deployer], log: true });

const disputeKit = await deployUpgradable(deployments, "DisputeKitClassicV2", {
from: deployer,
contract: "DisputeKitClassic",
args: [
deployer,
ZeroAddress, // Placeholder for KlerosCore address, configured later
],
log: true,
});

// Calculate future addresses for circular dependencies
const nonce = await ethers.provider.getTransactionCount(deployer);

const vaultAddress = getContractAddress(deployer, nonce + 1); // deployed on the 2nd tx (nonce+1): Vault Impl tx, Vault Proxy tx
console.log("calculated future Vault address for nonce %d: %s", nonce + 1, vaultAddress);

const stakeControllerAddress = getContractAddress(deployer, nonce + 5); // deployed on the 6th tx (nonce+5): Vault Impl tx, Vault Proxy tx, SortitionModule Impl tx, SortitionModule Proxy tx,, StakeController Impl tx, StakeController Proxy tx
console.log("calculated future StakeController address for nonce %d: %s", nonce + 5, stakeControllerAddress);

const klerosCoreAddress = getContractAddress(deployer, nonce + 7); // deployed on the 8th tx (nonce+7): Vault Impl tx, Vault Proxy tx, SortitionModule Impl tx, SortitionModule Proxy tx, StakeController Impl tx, StakeController Proxy tx, KlerosCore Impl tx, KlerosCore Proxy tx
console.log("calculated future KlerosCore address for nonce %d: %s", nonce + 7, klerosCoreAddress);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Nonce-based address prediction is brittle

getContractAddress(deployer, nonce + X) assumes:

  1. No other transactions are sent by the deployer between nonce retrieval and the target deployment.
  2. deployUpgradable emits exactly two transactions.

If any deployment is skipped (already deployed), or deployUpgradable implementation changes (e.g. adds an admin-transfer tx), every downstream address becomes wrong and the whole deployment bricks.

Move the circular-dependency resolution inside deployUpgradable helpers (by returning the proxy address immediately) or compute the address right after each deployment with await deployments.get(name).

🤖 Prompt for AI Agents
In contracts/deploy/00-home-chain-arbitration-v2.ts around lines 42 to 53, the
current nonce-based calculation of future contract addresses is fragile because
it assumes no other transactions occur and that deployUpgradable always emits
two transactions. To fix this, avoid predicting addresses by nonce offsets;
instead, modify deployUpgradable helpers to return the deployed proxy address
immediately or retrieve the deployed contract address after each deployment
using await deployments.get(name). This ensures accurate address resolution
regardless of transaction count variations.

const vault = await deployUpgradable(deployments, "Vault", {
from: deployer,
args: [deployer, pnk.target, stakeControllerAddress, klerosCoreAddress],
log: true,
}); // nonce (implementation), nonce + 1 (proxy)

// Deploy SortitionSumTree
const sortitionModuleV2 = await deployUpgradable(deployments, "SortitionSumTree", {
from: deployer,
args: [deployer, stakeControllerAddress],
log: true,
}); // nonce + 2 (implementation), nonce + 3 (proxy)

// Deploy StakeController (only if not already deployed)
const devnet = isDevnet(hre.network);
const minStakingTime = devnet ? 180 : 1800;
const maxDrawingTime = devnet ? 600 : 1800;
const rng = (await ethers.getContract("ChainlinkRNG")) as ChainlinkRNG;
const stakeController = await deployUpgradable(deployments, "StakeController", {
from: deployer,
args: [
deployer,
klerosCoreAddress,
vault.address,
sortitionModuleV2.address,
rng.target,
minStakingTime,
maxDrawingTime,
RNG_LOOKAHEAD,
],
log: true,
}); // nonce + 4 (implementation), nonce + 5 (proxy)

const minStake = PNK(200);
const alpha = 10000;
const feeForJuror = ETH(0.1);
const jurorsForCourtJump = 256;

// Deploy KlerosCore (only if not already deployed)
const klerosCoreV2 = await deployUpgradable(deployments, "KlerosCore", {
from: deployer,
args: [
deployer,
deployer,
ZeroAddress, // JurorProsecutionModule, not implemented yet
disputeKit.address,
false,
[minStake, alpha, feeForJuror, jurorsForCourtJump],
[0, 0, 0, 10], // evidencePeriod, commitPeriod, votePeriod, appealPeriod
ethers.toBeHex(5), // Extra data for sortition module will return the default value of K
stakeController.address,
vault.address,
],
log: true,
});

// Configure cross-dependencies
console.log("Configuring cross-dependencies...");

// disputeKit.changeCore() only if necessary
const disputeKitContract = (await ethers.getContract("DisputeKitClassicV2")) as DisputeKitClassic;
const currentCore = await disputeKitContract.core();
if (currentCore !== klerosCoreV2.address) {
console.log(`disputeKit.changeCore(${klerosCoreV2.address})`);
await disputeKitContract.changeCore(klerosCoreV2.address);
}

// rng.changeSortitionModule() only if necessary
// Note: the RNG's `sortitionModule` variable is misleading, it's only for access control and should be renamed to `consumer`.
const rngSortitionModule = await rng.sortitionModule();
if (rngSortitionModule !== stakeController.address) {
console.log(`rng.changeSortitionModule(${stakeController.address})`);
await rng.changeSortitionModule(stakeController.address);
}

const core = (await hre.ethers.getContract("KlerosCore")) as KlerosCore;
try {
await changeCurrencyRate(core, await pnk.getAddress(), true, 12225583, 12);
await changeCurrencyRate(core, await dai.getAddress(), true, 60327783, 11);
await changeCurrencyRate(core, await weth.getAddress(), true, 1, 1);
} catch (e) {
console.error("failed to change currency rates:", e);
}

await deploy("KlerosCoreSnapshotProxy", {
from: deployer,
contract: "KlerosCoreSnapshotProxy",
args: [deployer, core.target],
log: true,
});

console.log("✅ V2 Architecture deployment completed successfully!");
console.log(`📦 Vault: ${vault.address}`);
console.log(`🎯 SortitionSumTree: ${sortitionModuleV2.address}`);
console.log(`🎮 StakeController: ${stakeController.address}`);
console.log(`⚖️ KlerosCore: ${klerosCoreV2.address}`);
};

deployArbitrationV2.tags = ["ArbitrationV2"];
deployArbitrationV2.dependencies = ["ChainlinkRNG"];
deployArbitrationV2.skip = async ({ network }) => {
return isSkipped(network, !HomeChains[network.config.chainId ?? 0]);
};

export default deployArbitrationV2;
4 changes: 2 additions & 2 deletions contracts/deploy/utils/klerosCoreHelper.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { KlerosCore, KlerosCoreNeo, KlerosCoreRuler, KlerosCoreUniversity } from "../../typechain-types";
import { KlerosCore, KlerosCoreNeo, KlerosCoreRuler, KlerosCoreUniversity, KlerosCore } from "../../typechain-types";
import { BigNumberish, toBigInt } from "ethers";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix duplicate import causing syntax error.

The import statement contains a duplicate KlerosCore import, which will cause a compilation error.

Apply this diff to remove the duplicate:

-import { KlerosCore, KlerosCoreNeo, KlerosCoreRuler, KlerosCoreUniversity, KlerosCore } from "../../typechain-types";
+import { KlerosCore, KlerosCoreNeo, KlerosCoreRuler, KlerosCoreUniversity } from "../../typechain-types";
🧰 Tools
🪛 Biome (1.9.4)

[error] 1-1: Declarations inside of a import declaration may not have duplicates

a second declaration of KlerosCore is not allowed

KlerosCore is first declared here

(parse)

🤖 Prompt for AI Agents
In contracts/deploy/utils/klerosCoreHelper.ts at line 1, the import statement
includes a duplicate import of KlerosCore, causing a syntax error. Remove the
repeated KlerosCore from the import list so that each imported item appears only
once.


export const changeCurrencyRate = async (
core: KlerosCore | KlerosCoreNeo | KlerosCoreRuler | KlerosCoreUniversity,
core: KlerosCore | KlerosCoreNeo | KlerosCoreRuler | KlerosCoreUniversity | KlerosCore,
erc20: string,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix duplicate type in parameter annotation.

The core parameter type annotation contains a duplicate KlerosCore type in the union.

Apply this diff to remove the duplicate:

-  core: KlerosCore | KlerosCoreNeo | KlerosCoreRuler | KlerosCoreUniversity | KlerosCore,
+  core: KlerosCore | KlerosCoreNeo | KlerosCoreRuler | KlerosCoreUniversity,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
core: KlerosCore | KlerosCoreNeo | KlerosCoreRuler | KlerosCoreUniversity | KlerosCore,
core: KlerosCore | KlerosCoreNeo | KlerosCoreRuler | KlerosCoreUniversity,
🤖 Prompt for AI Agents
In contracts/deploy/utils/klerosCoreHelper.ts at line 5, the type annotation for
the parameter 'core' includes a duplicate 'KlerosCore' in the union type. Remove
the repeated 'KlerosCore' so that each type appears only once in the union.

accepted: boolean,
rateInEth: BigNumberish,
Expand Down
52 changes: 24 additions & 28 deletions contracts/src/arbitration/KlerosCore.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,71 +2,67 @@

pragma solidity 0.8.24;

import {KlerosCoreBase, IDisputeKit, ISortitionModule, IERC20} from "./KlerosCoreBase.sol";
import "./KlerosCoreBase.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.
/// @notice KlerosCore implementation with new StakeController architecture for testing environments
contract KlerosCore is KlerosCoreBase {
string public constant override version = "0.9.4";
/// @notice Version of the implementation contract
string public constant override version = "0.0.1";

// ************************************* //
// * Constructor * //
// ************************************* //

/// @custom:oz-upgrades-unsafe-allow constructor
/// @notice Constructor, initializing the implementation to reduce attack surface.
constructor() {
_disableInitializers();
}

/// @dev Initializer (constructor equivalent for upgradable contracts).
/// @param _governor The governor'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.
/// @notice Initialization function for UUPS proxy
/// @param _governor The governor of the contract.
/// @param _guardian The guardian able to pause asset withdrawals.
/// @param _jurorProsecutionModule The module for juror's prosecution.
/// @param _disputeKit The dispute kit responsible for the dispute logic.
/// @param _hiddenVotes Whether to use commit and reveal or not.
/// @param _courtParameters [0]: minStake, [1]: alpha, [2]: feeForJuror, [3]: jurorsForCourtJump
/// @param _timesPerPeriod The timesPerPeriod array for courts
/// @param _sortitionExtraData Extra data for sortition module setup
/// @param _stakeController The stake controller for coordination
/// @param _vault The vault for coordination
function initialize(
address _governor,
address _guardian,
IERC20 _pinakion,
address _jurorProsecutionModule,
IDisputeKit _disputeKit,
bool _hiddenVotes,
uint256[4] memory _courtParameters,
uint256[4] memory _timesPerPeriod,
bytes memory _sortitionExtraData,
ISortitionModule _sortitionModuleAddress
) external reinitializer(1) {
IStakeController _stakeController,
IVault _vault
) external initializer {
__KlerosCoreBase_initialize(
_governor,
_guardian,
_pinakion,
_jurorProsecutionModule,
_disputeKit,
_hiddenVotes,
_courtParameters,
_timesPerPeriod,
_sortitionExtraData,
_sortitionModuleAddress
_stakeController,
_vault
);
}

function initialize5() external reinitializer(5) {
// NOP
}

// ************************************* //
// * Governance * //
// ************************************* //

/// @dev Access Control to perform implementation upgrades (UUPS Proxiable)
/// Only the governor can perform upgrades (`onlyByGovernor`)
/// @notice Access Control to perform implementation upgrades (UUPS Proxiable)
/// Only the governor can perform upgrades (`onlyByGovernor`)
function _authorizeUpgrade(address) internal view override onlyByGovernor {
// NOP
// Empty block: access control implemented by `onlyByGovernor` modifier
}
}
Loading
Loading