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 all 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
100 changes: 73 additions & 27 deletions contracts/deploy/00-home-chain-arbitration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { HardhatRuntimeEnvironment } from "hardhat/types";
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 { getContractOrDeploy, getContractOrDeployUpgradable } from "./utils/getContractOrDeploy";
import { deployERC20AndFaucet } from "./utils/deployTokens";
import { ChainlinkRNG, DisputeKitClassic, KlerosCore } from "../typechain-types";
import { changeCurrencyRate } from "./utils/klerosCoreHelper";

const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { ethers, deployments, getNamedAccounts, getChainId } = hre;
Expand All @@ -29,65 +29,104 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment)

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

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

let klerosCoreAddress = await deployments.getOrNull("KlerosCore").then((deployment) => deployment?.address);
if (!klerosCoreAddress) {
const nonce = await ethers.provider.getTransactionCount(deployer);
klerosCoreAddress = getContractAddress(deployer, nonce + 3); // deployed on the 4th tx (nonce+3): SortitionModule Impl tx, SortitionModule Proxy tx, KlerosCore Impl tx, KlerosCore Proxy tx
console.log("calculated future KlerosCore address for nonce %d: %s", nonce + 3, klerosCoreAddress);
}
// 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);

Comment on lines +42 to +53
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Manual nonce arithmetic is fragile and will break on the first extra transaction
Hard-coding nonce + {1‥7} assumes that no additional tx slips in between the calculated point and the actual deployment (e.g. a library deploy, an implicit OZ admin‐upgrade deploy, or even a gas‐price retry).
Once the assumption is violated every pre-computed address is wrong, bricking the whole deployment.

Consider replacing the manual math with:

  1. predictAddress helpers available in OZ Upgrades, or
  2. deploying the dependency first (e.g. Vault impl only), fetching its actual proxy address with deployments.get and passing that concrete value to the next deploy step.

This turns the circular-dependency problem into a deterministic two-phase deploy without relying on fragile nonce bookkeeping.

🤖 Prompt for AI Agents
In contracts/deploy/00-home-chain-arbitration.ts around lines 42 to 53, the
current approach uses manual nonce arithmetic to predict future contract
addresses, which is fragile and can break if any extra transactions occur before
deployment. To fix this, replace the manual nonce calculations with a more
reliable method such as using OpenZeppelin Upgrades' `predictAddress` helpers or
deploy each dependency sequentially, retrieving the actual deployed proxy
address via `deployments.get` before passing it to the next deployment step.
This ensures deterministic address resolution without relying on fragile nonce
assumptions.

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)
Comment on lines +54 to +65
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

pnk.target may point to the implementation, not the proxy
In v6 ethers, .target is the address of the contract instance you created, in this case the implementation returned by OZ’s deployer, not the TransparentUpgradeableProxy users will interact with.

The Vault’s constructor is expected to hold a reference to the tradeable PNK token (proxy). Pass the proxy address instead:

-args: [deployer, pnk.target, stakeControllerAddress, klerosCoreAddress],
+const pnkAddress = await pnk.getAddress(); // proxy address
+args: [deployer, pnkAddress, stakeControllerAddress, klerosCoreAddress],

Same remark applies later to rng.target and core.target. Mixing .target, .address, and .getAddress() is error-prone; pick one consistently (ideally .getAddress() which always gives the proxy).

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

🤖 Prompt for AI Agents
In contracts/deploy/00-home-chain-arbitration.ts around lines 54 to 65, the code
uses pnk.target which points to the implementation contract address instead of
the proxy address required by the Vault constructor. To fix this, replace
pnk.target with the proxy address of the PNK token, ideally using
pnk.getAddress() for consistency and correctness. Apply the same change for
rng.target and core.target later in the file, ensuring all contract addresses
passed are proxy addresses obtained via .getAddress() to avoid confusion and
errors.


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

const minStake = PNK(200);
const alpha = 10000;
const feeForJuror = ETH(0.1);
const jurorsForCourtJump = 256;
const klerosCore = await deployUpgradable(deployments, "KlerosCore", {

// Deploy KlerosCore
const klerosCoreV2 = await deployUpgradable(deployments, "KlerosCore", {
from: deployer,
args: [
deployer,
deployer,
pnk.target,
ZeroAddress, // KlerosCore is configured later
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
sortitionModule.address,
stakeController.address,
vault.address,
],
log: true,
}); // nonce+2 (implementation), nonce+3 (proxy)
});

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

// disputeKit.changeCore() only if necessary
const disputeKitContract = (await ethers.getContract("DisputeKitClassic")) as DisputeKitClassic;
const disputeKitContract = await ethers.getContract<DisputeKitClassic>("DisputeKitClassicV2");
const currentCore = await disputeKitContract.core();
if (currentCore !== klerosCore.address) {
console.log(`disputeKit.changeCore(${klerosCore.address})`);
await disputeKitContract.changeCore(klerosCore.address);
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 !== sortitionModule.address) {
console.log(`rng.changeSortitionModule(${sortitionModule.address})`);
await rng.changeSortitionModule(sortitionModule.address);
if (rngSortitionModule !== stakeController.address) {
console.log(`rng.changeSortitionModule(${stakeController.address})`);
await rng.changeSortitionModule(stakeController.address);
}

const core = (await hre.ethers.getContract("KlerosCore")) as KlerosCore;
const core = await hre.ethers.getContract<KlerosCore>("KlerosCore");
try {
await changeCurrencyRate(core, await pnk.getAddress(), true, 12225583, 12);
await changeCurrencyRate(core, await dai.getAddress(), true, 60327783, 11);
Expand All @@ -98,9 +137,16 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment)

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}`);
};

deployArbitration.tags = ["Arbitration"];
Expand Down
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.10.0";

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