diff --git a/contracts/solidity/GovProxyAdmin.sol b/contracts/solidity/GovProxyAdmin.sol index fbb9cd8fcf..019ccd38cb 100644 --- a/contracts/solidity/GovProxyAdmin.sol +++ b/contracts/solidity/GovProxyAdmin.sol @@ -28,6 +28,7 @@ contract GovProxyAdmin is GovernanceVote { virtual needVote( bytes32( + // keccak256("upgradeAndCall") 0xe739b9109d83c1c6d0d640fe9ed476fc5862a6de5483b00678a3fffa7a2be2f6 ), keccak256(abi.encode(proxy, newImplementation, data)) diff --git a/contracts/solidity/Governance.sol b/contracts/solidity/Governance.sol index 2b7b706f79..500ecaf295 100644 --- a/contracts/solidity/Governance.sol +++ b/contracts/solidity/Governance.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.25; import {Errors} from "./libraries/Errors.sol"; import {IGovReward} from "./interfaces/IGovReward.sol"; import {IGovernance} from "./interfaces/IGovernance.sol"; +import {IPolicy} from "./interfaces/IPolicy.sol"; import {ERC1967Utils, GovProxyUpgradeable} from "./base/GovProxyUpgradeable.sol"; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import {ReentrancyGuard} from"@openzeppelin/contracts/utils/ReentrancyGuard.sol"; @@ -12,6 +13,9 @@ contract Governance is IGovernance, ReentrancyGuard, GovProxyUpgradeable { using EnumerableSet for EnumerableSet.AddressSet; address public constant SELF = 0x1212100000000000000000000000000000000001; + // Policy contract + address public constant POLICY = + 0x1212000000000000000000000000000000000002; // GovReward contract address public constant GOV_REWARD = 0x1212000000000000000000000000000000000003; @@ -111,6 +115,8 @@ contract Governance is IGovernance, ReentrancyGuard, GovProxyUpgradeable { if (tx.origin != msg.sender) revert Errors.OnlyEOA(); if (msg.value < registerFee) revert Errors.InsufficientValue(); if (shareRate > 1000) revert Errors.InvalidShareRate(); + if (candidateList.length() >= IPolicy(POLICY).getCandidateLimit()) + revert Errors.RegisterDisabled(); if (exitHeightOf[msg.sender] > 0) revert Errors.LeftNotClaimed(); if (!candidateList.add(msg.sender)) revert Errors.CandidateExists(); if (receivedVotes[msg.sender] > 0) { diff --git a/contracts/solidity/Policy.sol b/contracts/solidity/Policy.sol index 4d8264ddf4..8e518d2355 100644 --- a/contracts/solidity/Policy.sol +++ b/contracts/solidity/Policy.sol @@ -8,10 +8,12 @@ import {ERC1967Utils, GovProxyUpgradeable} from "./base/GovProxyUpgradeable.sol" contract Policy is IPolicy, GovernanceVote, GovProxyUpgradeable { address public constant SELF = 0x1212100000000000000000000000000000000002; + uint256 public constant DEFAULT_CANDIDATE_LIMIT = 2000; mapping(address => bool) public isBlackListed; uint256 public minGasTipCap; uint256 public baseFee; + uint256 internal candidateLimit; // Only for precompiled uups implementation in genesis file, need to be removed when upgrading the contract. // This override is added because "immutable __self" in UUPSUpgradeable is not avaliable in precompiled contract. @@ -39,6 +41,7 @@ contract Policy is IPolicy, GovernanceVote, GovProxyUpgradeable { external needVote( bytes32( + // keccak256("addBlackList") 0x4912b57f7ea75243ecaff76a75bdedbc13a6f58c1c967b0427b8aee0a276309e ), keccak256(abi.encode(_addr)) @@ -55,6 +58,7 @@ contract Policy is IPolicy, GovernanceVote, GovProxyUpgradeable { external needVote( bytes32( + // keccak256("removeBlackList") 0x310cc9bfce6443143f03d0cdc4d66afa0b3c689539eb3e65cb1820b56d672465 ), keccak256(abi.encode(_addr)) @@ -71,6 +75,7 @@ contract Policy is IPolicy, GovernanceVote, GovProxyUpgradeable { external needVote( bytes32( + // keccak256("setMinGasTipCap") 0x089197e4f35b8ada456b5531e8c1759ee3fce703602a3a957b5c9d2831082156 ), keccak256(abi.encode(_gasTipCap)) @@ -87,6 +92,7 @@ contract Policy is IPolicy, GovernanceVote, GovProxyUpgradeable { external needVote( bytes32( + // keccak256("setBaseFee") 0x83113031fe9312a872d9176bc1a087dc38ca109c517a596998332e2fb8409acc ), keccak256(abi.encode(_baseFee)) @@ -96,4 +102,27 @@ contract Policy is IPolicy, GovernanceVote, GovProxyUpgradeable { baseFee = _baseFee; emit SetBaseFee(_baseFee); } + + function setCandidateLimit( + uint256 _candidateLimit + ) + external + needVote( + bytes32( + // keccak256("setCandidateLimit") + 0x172d358b638a8ee3e962dd73800c4025c48eb0f79c479bc2cdd1f63e72779efc + ), + keccak256(abi.encode(_candidateLimit)) + ) + { + if (_candidateLimit <= 0) revert Errors.InvalidCandidateLimit(); + candidateLimit = _candidateLimit; + emit SetCandidateLimit(_candidateLimit); + } + + function getCandidateLimit() external view returns (uint256) { + uint256 limit = candidateLimit; + if (limit > 0) return limit; + else return DEFAULT_CANDIDATE_LIMIT; + } } diff --git a/contracts/solidity/Treasury.sol b/contracts/solidity/Treasury.sol index f22f0e5fde..05d752139f 100644 --- a/contracts/solidity/Treasury.sol +++ b/contracts/solidity/Treasury.sol @@ -22,6 +22,7 @@ contract Treasury is GovernanceVote, ITreasury { external needVote( bytes32( + // keccak256("fundBridge") 0xdd6d322687f552c30b168d744bbd29145a2095a3557a58387f7e7230c9449179 ), keccak256(abi.encode(_amount)) diff --git a/contracts/solidity/interfaces/IPolicy.sol b/contracts/solidity/interfaces/IPolicy.sol index d83159bf7f..f3a3d82f76 100644 --- a/contracts/solidity/interfaces/IPolicy.sol +++ b/contracts/solidity/interfaces/IPolicy.sol @@ -6,6 +6,7 @@ interface IPolicy { event RemoveBlackList(address indexed addr); event SetMinGasTipCap(uint256 gasTipCap); event SetBaseFee(uint256 baseFee); + event SetCandidateLimit(uint256 candidateLimit); // add an address to blacklist policy function addBlackList(address _addr) external; @@ -13,9 +14,24 @@ interface IPolicy { // remove an address from blacklist policy function removeBlackList(address _addr) external; + // check if an address is blacklisted by policy + function isBlackListed(address _addr) external view returns (bool); + // set a new value to minimum gas tip cap policy function setMinGasTipCap(uint256 _gasTipCap) external; + // get the value of minimum gas tip cap policy + function minGasTipCap() external view returns (uint256); + // set a new value to base fee policy function setBaseFee(uint256 _baseFee) external; + + // get the value of base fee policy + function baseFee() external view returns (uint256); + + // set candidate limit (increase only) + function setCandidateLimit(uint256 _candidateLimit) external; + + // return the value of candidate limit policy + function getCandidateLimit() external view returns (uint256); } diff --git a/contracts/solidity/libraries/Errors.sol b/contracts/solidity/libraries/Errors.sol index bb41426085..5e390526ff 100644 --- a/contracts/solidity/libraries/Errors.sol +++ b/contracts/solidity/libraries/Errors.sol @@ -15,12 +15,14 @@ library Errors { error BlacklistNotExists(); error InvalidMinGasTipCap(); error InvalidBaseFee(); + error InvalidCandidateLimit(); // Governance Errors error SideCallNotAllowed(); error OnlyEOA(); error InsufficientValue(); error InvalidShareRate(); + error RegisterDisabled(); error SameCandidate(); error CandidateExists(); error CandidateNotExists(); diff --git a/contracts/test/Governance.ts b/contracts/test/Governance.ts index 40f8f3340b..f3bea4a7db 100644 --- a/contracts/test/Governance.ts +++ b/contracts/test/Governance.ts @@ -29,6 +29,10 @@ const STANDBY_VALIDATORS = [ "0xd711da2d8c71a801fc351163337656f1321343a0" ]; +const MIN_GAS_TIP_CAP = ethers.parseUnits("1", "gwei"); +const BASE_FEE = ethers.parseUnits("1", "gwei"); +const CANDIDATE_LIMIT = 2000; + describe("Governance", function () { let Governance: any; @@ -43,10 +47,18 @@ describe("Governance", function () { // Deploy Governance contract const governance_deploy = await ethers.deployContract("Governance"); + const reward_deploy = await ethers.deployContract("GovReward"); + const policy_deploy = await ethers.deployContract("Policy"); // Copy Bytecode to native address const governance_code = await ethers.provider.send("eth_getCode", [governance_deploy.target]); await ethers.provider.send("hardhat_setCode", [GOV_PROXY, governance_code]); + + const reward_code = await ethers.provider.send("eth_getCode", [reward_deploy.target]); + await ethers.provider.send("hardhat_setCode", [REWARD_PROXY, reward_code]); + + const policy_code = await ethers.provider.send("eth_getCode", [policy_deploy.target]); + await ethers.provider.send("hardhat_setCode", [POLICY_PROXY, policy_code]); const contract = require("../artifacts/solidity/Governance.sol/Governance.json"); Governance = new ethers.Contract(GOV_PROXY, contract.abi, user); @@ -74,6 +86,11 @@ describe("Governance", function () { await ethers.provider.send("hardhat_setStorageAt", [GOV_PROXY, "0x31ecc21a745e3968a04e9570e4425bc18fa8019c68028196b546d1669c200c6c", ethers.toBeHex(STANDBY_VALIDATORS[4], 32)]); await ethers.provider.send("hardhat_setStorageAt", [GOV_PROXY, "0x31ecc21a745e3968a04e9570e4425bc18fa8019c68028196b546d1669c200c6d", ethers.toBeHex(STANDBY_VALIDATORS[5], 32)]); await ethers.provider.send("hardhat_setStorageAt", [GOV_PROXY, "0x31ecc21a745e3968a04e9570e4425bc18fa8019c68028196b546d1669c200c6e", ethers.toBeHex(STANDBY_VALIDATORS[6], 32)]); + + // Write Policy config to storage + await ethers.provider.send("hardhat_setStorageAt", [POLICY_PROXY, "0x2", ethers.toBeHex(MIN_GAS_TIP_CAP, 32)]); + await ethers.provider.send("hardhat_setStorageAt", [POLICY_PROXY, "0x3", ethers.toBeHex(BASE_FEE, 32)]); + await ethers.provider.send("hardhat_setStorageAt", [POLICY_PROXY, "0x4", ethers.toBeHex(CANDIDATE_LIMIT, 32)]); }); describe("genesis", function () { @@ -143,6 +160,13 @@ describe("Governance", function () { ).to.be.revertedWithCustomError(Governance, ERRORS.INVALID_SHARE_RATE); }); + // it("Should revert if the candidate amount exceeds limit", async function () { + // await ethers.provider.send("hardhat_setStorageAt", [POLICY_PROXY, "0x4", ethers.toBeHex(0, 32)]); + // await expect( + // Governance.connect(candidate1).registerCandidate(500, { value: REGISTER_FEE }) + // ).to.be.revertedWithCustomError(Governance, ERRORS.REGISTER_DISABLED); + // }); + it("Should register a new candidate if all conditions are met", async function () { await expect( Governance.connect(candidate1).registerCandidate(500, { value: REGISTER_FEE }) diff --git a/contracts/test/Policy.ts b/contracts/test/Policy.ts index fbc8c31cb9..bfbab7d6d3 100644 --- a/contracts/test/Policy.ts +++ b/contracts/test/Policy.ts @@ -30,6 +30,7 @@ const STANDBY_VALIDATORS = [ const MIN_GAS_TIP_CAP = ethers.parseUnits("1", "gwei"); const BASE_FEE = ethers.parseUnits("1", "gwei"); +const CANDIDATE_LIMIT = 2000; describe("Policy", function () { @@ -88,6 +89,7 @@ describe("Policy", function () { // Write Policy config to storage await ethers.provider.send("hardhat_setStorageAt", [POLICY_PROXY, "0x2", ethers.toBeHex(MIN_GAS_TIP_CAP, 32)]); await ethers.provider.send("hardhat_setStorageAt", [POLICY_PROXY, "0x3", ethers.toBeHex(BASE_FEE, 32)]); + await ethers.provider.send("hardhat_setStorageAt", [POLICY_PROXY, "0x4", ethers.toBeHex(CANDIDATE_LIMIT, 32)]); }); describe("genesis", function () { @@ -97,6 +99,9 @@ describe("Policy", function () { it("Should get base fee as expected", async function () { expect(await Policy.baseFee()).to.eq(BASE_FEE); }); + it("Should get candidate limit as expected", async function () { + expect(await Policy.getCandidateLimit()).to.eq(CANDIDATE_LIMIT); + }); }); describe("addBlackList", function () { @@ -257,4 +262,39 @@ describe("Policy", function () { ).emit(Policy, "SetBaseFee"); }); }); + + describe("setCandidateLimit", function () { + it("Should revert if the sender is not a validator", async function () { + await expect( + Policy.connect(signers[7]).setCandidateLimit(2001) + ).to.be.revertedWithCustomError(Policy, ERRORS.NOT_MINER); + }); + + it("Should change the candidate limit if meets the threshold", async function () { + for (let i = 0; i < 4; i++) { + await expect( + Policy.connect(signers[i]).setCandidateLimit(2001) + ).not.to.be.reverted; + } + expect(await Policy.getCandidateLimit()).to.eq(2001); + }); + + it("Should emit an event if meets the threshold", async function () { + for (let i = 0; i < 3; i++) { + await expect( + Policy.connect(signers[i]).setCandidateLimit(2001) + ).not.to.be.reverted; + } + await expect( + Policy.connect(signers[3]).setCandidateLimit(2001) + ).emit(Policy, "SetCandidateLimit"); + }); + }); + + describe("setCandidateLimit", function () { + it("Should return default value if not setted", async function () { + await ethers.provider.send("hardhat_setStorageAt", [POLICY_PROXY, "0x4", ethers.toBeHex(0, 32)]); + expect(await Policy.getCandidateLimit()).to.eq(CANDIDATE_LIMIT); + }); + }); }); diff --git a/contracts/test/helpers/errors.ts b/contracts/test/helpers/errors.ts index 849421369a..44c632033f 100644 --- a/contracts/test/helpers/errors.ts +++ b/contracts/test/helpers/errors.ts @@ -7,10 +7,12 @@ export const ERRORS = { BLACKLIST_NOT_EXISTS: 'BlacklistNotExists()', INVALID_MIN_GAS_TIP_CAP: 'InvalidMinGasTipCap()', INVALID_BASE_FEE: 'InvalidBaseFee()', + INVALID_CANDIDATE_LIMIT: 'InvalidCandidateLimit()', SIDE_CALL_OT_ALLOWED: 'SideCallNotAllowed()', ONLY_EOA: 'OnlyEOA()', INSUFFICIENT_VALUE: 'InsufficientValue()', INVALID_SHARE_RATE: 'InvalidShareRate()', + REGISTER_DISABLED: 'RegisterDisabled()', SAME_CANDIDATE: 'SameCandidate()', CANDIDATE_EXISTS: 'CandidateExists()', CANDIDATE_NOT_EXISTS: 'CandidateNotExists()',