Skip to content

Commit e47ecfc

Browse files
committed
systemcontract: apply blacklist policy to governance election
1 parent 625bee6 commit e47ecfc

File tree

5 files changed

+203
-26
lines changed

5 files changed

+203
-26
lines changed

contracts/solidity/Governance.sol

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@ contract Governance is IGovernance, ReentrancyGuard, GovProxyUpgradeable {
1414

1515
address public constant SELF = 0x1212100000000000000000000000000000000001;
1616
// Policy contract
17-
address public constant POLICY =
18-
0x1212000000000000000000000000000000000002;
17+
address public constant POLICY = 0x1212000000000000000000000000000000000002;
1918
// GovReward contract
2019
address public constant GOV_REWARD =
2120
0x1212000000000000000000000000000000000003;
@@ -65,6 +64,8 @@ contract Governance is IGovernance, ReentrancyGuard, GovProxyUpgradeable {
6564
mapping(address => uint) public voteHeight;
6665
// candidate=>height=>number
6766
mapping(address => mapping(uint => uint)) public epochStartGasPerVote;
67+
// blacklisted candidate amount
68+
uint public blacklistedCandidates;
6869

6970
// Only for precompiled uups implementation in genesis file, need to be removed when upgrading the contract.
7071
// This override is added because "immutable __self" in UUPSUpgradeable is not avaliable in precompiled contract.
@@ -115,29 +116,21 @@ contract Governance is IGovernance, ReentrancyGuard, GovProxyUpgradeable {
115116
if (tx.origin != msg.sender) revert Errors.OnlyEOA();
116117
if (msg.value < registerFee) revert Errors.InsufficientValue();
117118
if (shareRate > 1000) revert Errors.InvalidShareRate();
118-
if (candidateList.length() >= IPolicy(POLICY).getCandidateLimit())
119-
revert Errors.RegisterDisabled();
119+
if (
120+
candidateList.length() + blacklistedCandidates >=
121+
IPolicy(POLICY).getCandidateLimit()
122+
) revert Errors.RegisterDisabled();
120123
if (exitHeightOf[msg.sender] > 0) revert Errors.LeftNotClaimed();
121-
if (!candidateList.add(msg.sender)) revert Errors.CandidateExists();
122-
if (receivedVotes[msg.sender] > 0) {
123-
totalVotes += receivedVotes[msg.sender];
124-
}
124+
if (!_activateCandidate(msg.sender)) revert Errors.CandidateExists();
125125

126126
// record share rate and balance
127127
shareRateOf[msg.sender] = shareRate;
128128
candidateBalanceOf[msg.sender] = msg.value;
129-
emit Register(msg.sender);
130129
}
131130

132131
function exitCandidate() external {
133-
if (!candidateList.remove(msg.sender))
132+
if (!_deactivateCandidate(msg.sender))
134133
revert Errors.CandidateNotExists();
135-
// remove candidate list, balance still locked
136-
exitHeightOf[msg.sender] = block.number;
137-
if (receivedVotes[msg.sender] > 0) {
138-
totalVotes -= receivedVotes[msg.sender];
139-
}
140-
emit Exit(msg.sender);
141134
}
142135

143136
function withdrawRegisterFee() external nonReentrant {
@@ -275,10 +268,42 @@ contract Governance is IGovernance, ReentrancyGuard, GovProxyUpgradeable {
275268
emit Persist(currentConsensus);
276269
}
277270

271+
function activateCandidate(address candidate) external {
272+
if (msg.sender != POLICY) revert Errors.SideCallNotAllowed();
273+
if (exitHeightOf[candidate] > 0 && _activateCandidate(candidate))
274+
blacklistedCandidates -= 1;
275+
}
276+
277+
function deactivateCandidate(address candidate) external {
278+
if (msg.sender != POLICY) revert Errors.SideCallNotAllowed();
279+
if (_deactivateCandidate(candidate)) blacklistedCandidates += 1;
280+
}
281+
278282
function getCurrentConsensus() public view returns (address[] memory) {
279283
return currentConsensus;
280284
}
281285

286+
function _activateCandidate(address candidate) internal returns (bool) {
287+
if (!candidateList.add(candidate)) return false;
288+
delete exitHeightOf[candidate];
289+
if (receivedVotes[candidate] > 0) {
290+
totalVotes += receivedVotes[candidate];
291+
}
292+
emit Activate(candidate);
293+
return true;
294+
}
295+
296+
function _deactivateCandidate(address candidate) internal returns (bool) {
297+
if (!candidateList.remove(candidate)) return false;
298+
// remove candidate list, balance still locked
299+
exitHeightOf[candidate] = block.number;
300+
if (receivedVotes[candidate] > 0) {
301+
totalVotes -= receivedVotes[candidate];
302+
}
303+
emit Deactivate(candidate);
304+
return true;
305+
}
306+
282307
function _computeReward(
283308
address voter,
284309
address candidate

contracts/solidity/Policy.sol

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ contract Policy is IPolicy, GovernanceVote, GovProxyUpgradeable {
5252
{
5353
if (isBlackListed[_addr]) revert Errors.BlacklistExists();
5454
isBlackListed[_addr] = true;
55+
IGovernance(GOV).deactivateCandidate(_addr);
5556
emit AddBlackList(_addr);
5657
}
5758

@@ -69,6 +70,7 @@ contract Policy is IPolicy, GovernanceVote, GovProxyUpgradeable {
6970
{
7071
if (!isBlackListed[_addr]) revert Errors.BlacklistNotExists();
7172
delete isBlackListed[_addr];
73+
IGovernance(GOV).activateCandidate(_addr);
7274
emit RemoveBlackList(_addr);
7375
}
7476

contracts/solidity/interfaces/IGovernance.sol

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
pragma solidity ^0.8.25;
33

44
interface IGovernance {
5-
event Register(address candidate);
6-
event Exit(address candidate);
5+
event Activate(address candidate);
6+
event Deactivate(address candidate);
77
event Vote(address indexed voter, address indexed to, uint amount);
88
event Revoke(address indexed voter, address indexed from, uint amount);
99
event VoterClaim(address indexed voter, uint reward);
@@ -40,6 +40,12 @@ interface IGovernance {
4040
// compute and update cached consensus group
4141
function onPersist() external;
4242

43+
// activate a candidate in election
44+
function activateCandidate(address candidate) external;
45+
46+
// deactivate a candidate in election
47+
function deactivateCandidate(address candidate) external;
48+
4349
// get consensus size
4450
function consensusSize() external view returns (uint);
4551
}

contracts/test/Governance.ts

Lines changed: 114 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ const CANDIDATE_LIMIT = 2000;
3535

3636
describe("Governance", function () {
3737

38-
let Governance: any;
38+
let Governance: any, Policy: any;
3939
let user: any, candidate1: any, candidate2: any;
4040

4141
beforeEach(async function () {
@@ -59,8 +59,11 @@ describe("Governance", function () {
5959

6060
const policy_code = await ethers.provider.send("eth_getCode", [policy_deploy.target]);
6161
await ethers.provider.send("hardhat_setCode", [POLICY_PROXY, policy_code]);
62-
const contract = require("../artifacts/solidity/Governance.sol/Governance.json");
63-
Governance = new ethers.Contract(GOV_PROXY, contract.abi, user);
62+
63+
const governance_contract = require("../artifacts/solidity/Governance.sol/Governance.json");
64+
Governance = new ethers.Contract(GOV_PROXY, governance_contract.abi, user);
65+
const policy_contract = require("../artifacts/solidity/Policy.sol/Policy.json");
66+
Policy = new ethers.Contract(POLICY_PROXY, policy_contract.abi, user);
6467

6568
// Write Governance config to storage
6669
await ethers.provider.send("hardhat_setStorageAt", [GOV_PROXY, "0x1", ethers.toBeHex(CONSENSUS_SIZE, 32)]);
@@ -86,7 +89,7 @@ describe("Governance", function () {
8689
await ethers.provider.send("hardhat_setStorageAt", [GOV_PROXY, "0x31ecc21a745e3968a04e9570e4425bc18fa8019c68028196b546d1669c200c6c", ethers.toBeHex(STANDBY_VALIDATORS[4], 32)]);
8790
await ethers.provider.send("hardhat_setStorageAt", [GOV_PROXY, "0x31ecc21a745e3968a04e9570e4425bc18fa8019c68028196b546d1669c200c6d", ethers.toBeHex(STANDBY_VALIDATORS[5], 32)]);
8891
await ethers.provider.send("hardhat_setStorageAt", [GOV_PROXY, "0x31ecc21a745e3968a04e9570e4425bc18fa8019c68028196b546d1669c200c6e", ethers.toBeHex(STANDBY_VALIDATORS[6], 32)]);
89-
92+
9093
// Write Policy config to storage
9194
await ethers.provider.send("hardhat_setStorageAt", [POLICY_PROXY, "0x2", ethers.toBeHex(MIN_GAS_TIP_CAP, 32)]);
9295
await ethers.provider.send("hardhat_setStorageAt", [POLICY_PROXY, "0x3", ethers.toBeHex(BASE_FEE, 32)]);
@@ -180,7 +183,7 @@ describe("Governance", function () {
180183
it("Should emit an event when a new candidate is registered", async function () {
181184
await expect(
182185
Governance.connect(candidate1).registerCandidate(500, { value: REGISTER_FEE })
183-
).emit(Governance, "Register");
186+
).emit(Governance, "Activate");
184187
});
185188
});
186189

@@ -211,7 +214,7 @@ describe("Governance", function () {
211214

212215
await expect(
213216
Governance.connect(candidate1).exitCandidate()
214-
).emit(Governance, "Exit");
217+
).emit(Governance, "Deactivate");
215218
});
216219
});
217220

@@ -276,6 +279,110 @@ describe("Governance", function () {
276279
});
277280
});
278281

282+
describe("deactivateCandidate", function () {
283+
let MockSysCall: any;
284+
let GovReward: any;
285+
286+
beforeEach(async function () {
287+
// Deploy Mock SYS_CALL
288+
const deploy_mock = await ethers.deployContract("MockSysCall");
289+
const code_mock = await ethers.provider.send("eth_getCode", [deploy_mock.target]);
290+
await ethers.provider.send("hardhat_setCode", [SYS_CALL, code_mock]);
291+
const contract_mock = require("../artifacts/solidity/test/MockSysCall.sol/MockSysCall.json");
292+
MockSysCall = new ethers.Contract(SYS_CALL, contract_mock.abi, user);
293+
// Deploy GovReward to native address
294+
const deploy_reward = await ethers.deployContract("GovReward");
295+
const code_reward = await ethers.provider.send("eth_getCode", [deploy_reward.target]);
296+
await ethers.provider.send("hardhat_setCode", [REWARD_PROXY, code_reward]);
297+
const contract_reward = require("../artifacts/solidity/GovReward.sol/GovReward.json");
298+
GovReward = new ethers.Contract(REWARD_PROXY, contract_reward.abi, user);
299+
});
300+
301+
it("Should revert if caller is not Policy", async function () {
302+
await expect(Governance.connect(user).deactivateCandidate(user.address)).to.be.revertedWithCustomError(
303+
Governance,
304+
ERRORS.SIDE_CALL_OT_ALLOWED
305+
);
306+
});
307+
308+
it("Should update storage if a candidate is deactivated", async function () {
309+
let signers = await ethers.getSigners();
310+
for (let i = 0; i < CONSENSUS_SIZE; i++) {
311+
await Governance.connect(signers[i]).registerCandidate(500, { value: REGISTER_FEE });
312+
await Governance.connect(signers[i]).vote(signers[i], { value: VOTE_TARGET_AMOUNT });
313+
}
314+
await mine(EPOCH_DURATION);
315+
await MockSysCall.call_onPersist(Governance);
316+
317+
for (let i = 0; i < 4; i++) {
318+
await expect(
319+
Policy.connect(signers[i]).addBlackList(signers[0])
320+
).not.to.be.reverted;
321+
}
322+
323+
expect(await Governance.blacklistedCandidates()).to.equal(1);
324+
expect(await Governance.exitHeightOf(signers[0].address)).to.gt(0);
325+
expect(await Governance.shareRateOf(signers[0].address)).to.equal(500);
326+
expect(await Governance.receivedVotes(signers[0].address)).to.equal(VOTE_TARGET_AMOUNT);
327+
expect(await Governance.totalVotes()).to.equal(BigInt(CONSENSUS_SIZE - 1) * VOTE_TARGET_AMOUNT);
328+
});
329+
});
330+
331+
describe("activateCandidate", function () {
332+
let MockSysCall: any;
333+
let GovReward: any;
334+
335+
beforeEach(async function () {
336+
// Deploy Mock SYS_CALL
337+
const deploy_mock = await ethers.deployContract("MockSysCall");
338+
const code_mock = await ethers.provider.send("eth_getCode", [deploy_mock.target]);
339+
await ethers.provider.send("hardhat_setCode", [SYS_CALL, code_mock]);
340+
const contract_mock = require("../artifacts/solidity/test/MockSysCall.sol/MockSysCall.json");
341+
MockSysCall = new ethers.Contract(SYS_CALL, contract_mock.abi, user);
342+
// Deploy GovReward to native address
343+
const deploy_reward = await ethers.deployContract("GovReward");
344+
const code_reward = await ethers.provider.send("eth_getCode", [deploy_reward.target]);
345+
await ethers.provider.send("hardhat_setCode", [REWARD_PROXY, code_reward]);
346+
const contract_reward = require("../artifacts/solidity/GovReward.sol/GovReward.json");
347+
GovReward = new ethers.Contract(REWARD_PROXY, contract_reward.abi, user);
348+
});
349+
350+
it("Should revert if caller is not Policy", async function () {
351+
await expect(Governance.connect(user).activateCandidate(user.address)).to.be.revertedWithCustomError(
352+
Governance,
353+
ERRORS.SIDE_CALL_OT_ALLOWED
354+
);
355+
});
356+
357+
it("Should update storage if a candidate is activated", async function () {
358+
let signers = await ethers.getSigners();
359+
for (let i = 0; i < CONSENSUS_SIZE; i++) {
360+
await Governance.connect(signers[i]).registerCandidate(500, { value: REGISTER_FEE });
361+
await Governance.connect(signers[i]).vote(signers[i], { value: VOTE_TARGET_AMOUNT });
362+
}
363+
await mine(EPOCH_DURATION);
364+
await MockSysCall.call_onPersist(Governance);
365+
366+
for (let i = 0; i < 4; i++) {
367+
await expect(
368+
Policy.connect(signers[i]).addBlackList(signers[0])
369+
).not.to.be.reverted;
370+
}
371+
372+
for (let i = 0; i < 4; i++) {
373+
await expect(
374+
Policy.connect(signers[i]).removeBlackList(signers[0])
375+
).not.to.be.reverted;
376+
}
377+
378+
expect(await Governance.blacklistedCandidates()).to.equal(0);
379+
expect(await Governance.exitHeightOf(signers[0].address)).to.equal(0);
380+
expect(await Governance.shareRateOf(signers[0].address)).to.equal(500);
381+
expect(await Governance.receivedVotes(signers[0].address)).to.equal(VOTE_TARGET_AMOUNT);
382+
expect(await Governance.totalVotes()).to.equal(BigInt(CONSENSUS_SIZE) * VOTE_TARGET_AMOUNT);
383+
});
384+
});
385+
279386
describe("getCandidates", function () {
280387
it("Should return an empty list of candidates initially", async function () {
281388
const candidates = await Governance.getCandidates();
@@ -410,7 +517,7 @@ describe("Governance", function () {
410517

411518
it("Should revert if target is not a candidate", async function () {
412519
await expect(
413-
Governance.connect(candidate1).vote(candidate1, { value: MIN_VOTE_AMOUNT})
520+
Governance.connect(candidate1).vote(candidate1, { value: MIN_VOTE_AMOUNT })
414521
).to.be.revertedWithCustomError(Governance, ERRORS.CANDIDATE_NOT_EXISTS);
415522
});
416523

contracts/test/Policy.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ethers } from "hardhat";
22
import { expect } from "chai";
3+
import { mine } from "@nomicfoundation/hardhat-network-helpers";
34
import { ERRORS } from "./helpers/errors";
45

56
// NATIVE ADDRESSES
@@ -34,7 +35,7 @@ const CANDIDATE_LIMIT = 2000;
3435

3536
describe("Policy", function () {
3637

37-
let Policy: any;
38+
let Policy: any, Governance: any;
3839
let signers: any;
3940

4041
beforeEach(async function () {
@@ -58,6 +59,9 @@ describe("Policy", function () {
5859

5960
const policy_code = await ethers.provider.send("eth_getCode", [policy_deploy.target]);
6061
await ethers.provider.send("hardhat_setCode", [POLICY_PROXY, policy_code]);
62+
63+
const governance_contract = require("../artifacts/solidity/Governance.sol/Governance.json");
64+
Governance = new ethers.Contract(GOV_PROXY, governance_contract.abi, signers[0]);
6165
const contract = require("../artifacts/solidity/Policy.sol/Policy.json");
6266
Policy = new ethers.Contract(POLICY_PROXY, contract.abi, signers[0]);
6367

@@ -142,6 +146,18 @@ describe("Policy", function () {
142146
Policy.connect(signers[3]).addBlackList(signers[0])
143147
).emit(Policy, "AddBlackList");
144148
});
149+
150+
it("Should deactivate governance if is a candidate", async function () {
151+
await Governance.connect(signers[7]).registerCandidate(500, { value: REGISTER_FEE });
152+
for (let i = 0; i < 3; i++) {
153+
await expect(
154+
Policy.connect(signers[i]).addBlackList(signers[7])
155+
).not.to.be.reverted;
156+
}
157+
await expect(
158+
Policy.connect(signers[3]).addBlackList(signers[7])
159+
).emit(Governance, "Deactivate");
160+
});
145161
});
146162

147163
describe("removeBlackList", function () {
@@ -183,6 +199,27 @@ describe("Policy", function () {
183199
Policy.connect(signers[3]).removeBlackList(signers[0])
184200
).emit(Policy, "RemoveBlackList");
185201
});
202+
203+
it("Should activate governance if is a candidate", async function () {
204+
await Governance.connect(signers[7]).registerCandidate(500, { value: REGISTER_FEE });
205+
for (let i = 0; i < 3; i++) {
206+
await expect(
207+
Policy.connect(signers[i]).addBlackList(signers[7])
208+
).not.to.be.reverted;
209+
}
210+
await expect(
211+
Policy.connect(signers[3]).addBlackList(signers[7])
212+
).emit(Governance, "Deactivate");
213+
214+
for (let i = 0; i < 3; i++) {
215+
await expect(
216+
Policy.connect(signers[i]).removeBlackList(signers[7])
217+
).not.to.be.reverted;
218+
}
219+
await expect(
220+
Policy.connect(signers[3]).removeBlackList(signers[7])
221+
).emit(Governance, "Activate");
222+
});
186223
});
187224

188225
describe("setMinGasTipCap", function () {

0 commit comments

Comments
 (0)