Skip to content

Commit baffe25

Browse files
authored
systemcontract: add transferVote method (#182)
* add transferVote method and update abi * update doc * add related ut
1 parent e1942ca commit baffe25

File tree

6 files changed

+90
-1
lines changed

6 files changed

+90
-1
lines changed

contracts/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ All GAS holders can vote and benefit from Neo X Governance, including EOA accoun
7171

7272
Neo X Governance doesn't allow voting for multiple candidates and doesn't distribute rewards to new voters until a new epoch begins. So be careful to revoke or change your vote target.
7373

74+
If it is necessary to change the vote target (e.g. the current voted candidate exits), invoke `transferVote(address candidateTo)` of `0x1212000000000000000000000000000000000001` to revote your deposited `GAS` to another candidate, and wait for the subsequent epoch to receive reward sharing.
75+
7476
At the end of every election epoch, the 7 candidates with the highest amount of votes will be selected by Governance and become consensus nodes of the next epoch. However, this consensus set recalculation has two prerequisites:
7577

7678
1. The size of candidate list is larger than `7`;

contracts/solidity/Errors.sol

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ library Errors {
2323
error OnlyEOA();
2424
error InsufficientValue();
2525
error InvalidShareRate();
26+
error SameCandidate();
2627
error CandidateExists();
2728
error CandidateNotExists();
2829
error LeftNotClaimed();

contracts/solidity/Governance.sol

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ interface IGovernance {
3030
// revoke votes and claim rewards
3131
function revokeVote() external;
3232

33+
// revoke votes, claim rewards and vote to another candidate
34+
function transferVote(address candidateTo) external;
35+
3336
// only claim rewards
3437
function claimReward() external;
3538

@@ -258,6 +261,28 @@ contract Governance is IGovernance, ReentrancyGuard, UUPSUpgradeable {
258261
_safeTransferETH(msg.sender, amount + unclaimedReward);
259262
}
260263

264+
function transferVote(address candidateTo) external nonReentrant {
265+
address candidateFrom = votedTo[msg.sender];
266+
uint amount = votedAmount[msg.sender];
267+
if (candidateFrom == address(0) || amount <= 0) revert Errors.NoVote();
268+
if (candidateFrom == candidateTo) revert Errors.SameCandidate();
269+
if (!candidateList.contains(candidateTo)) revert Errors.CandidateNotExists();
270+
271+
// settle reward here
272+
uint unclaimedReward = _settleReward(msg.sender, candidateFrom);
273+
274+
// update votes
275+
receivedVotes[candidateFrom] -= amount;
276+
receivedVotes[candidateTo] += amount;
277+
votedTo[msg.sender] = candidateTo;
278+
voterGasPerVote[msg.sender] = candidateGasPerVote[candidateTo];
279+
voteHeight[msg.sender] = block.number;
280+
281+
emit Revoke(msg.sender, candidateFrom, amount);
282+
emit Vote(msg.sender, candidateTo, amount);
283+
if (unclaimedReward > 0) _safeTransferETH(msg.sender, unclaimedReward);
284+
}
285+
261286
function claimReward() external nonReentrant {
262287
address votedCandidate = votedTo[msg.sender];
263288
if (votedCandidate == address(0)) revert Errors.NoVote();

contracts/test/Governance.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,66 @@ describe("Governance", function () {
481481
});
482482
});
483483

484+
describe("transferVote", function () {
485+
it("Should revert if the sender has not voted", async function () {
486+
await expect(
487+
Governance.connect(candidate1).transferVote(candidate1)
488+
).to.be.revertedWithCustomError(Governance, ERRORS.NO_VOTE);
489+
});
490+
491+
it("Should revert if the candidate from and to is the same", async function () {
492+
await Governance.connect(candidate1).registerCandidate(500, { value: REGISTER_FEE });
493+
await expect(
494+
Governance.connect(candidate1).vote(candidate1, { value: MIN_VOTE_AMOUNT })
495+
).not.to.be.reverted;
496+
497+
await expect(
498+
Governance.connect(candidate1).transferVote(candidate1)
499+
).to.be.revertedWithCustomError(Governance, ERRORS.SAME_CANDIDATE);
500+
});
501+
502+
it("Should revert if the candidate to is not in the candidate list", async function () {
503+
await Governance.connect(candidate1).registerCandidate(500, { value: REGISTER_FEE });
504+
await expect(
505+
Governance.connect(candidate1).vote(candidate1, { value: MIN_VOTE_AMOUNT })
506+
).not.to.be.reverted;
507+
508+
await expect(
509+
Governance.connect(candidate1).transferVote(candidate2)
510+
).to.be.revertedWithCustomError(Governance, ERRORS.CANDIDATE_NOT_EXISTS);
511+
});
512+
513+
it("Should update votes if all conditions are met", async function () {
514+
await Governance.connect(candidate1).registerCandidate(500, { value: REGISTER_FEE });
515+
await Governance.connect(candidate2).registerCandidate(500, { value: REGISTER_FEE });
516+
await expect(
517+
Governance.connect(candidate1).vote(candidate1, { value: MIN_VOTE_AMOUNT })
518+
).not.to.be.reverted;
519+
520+
await expect(
521+
Governance.connect(candidate1).transferVote(candidate2)
522+
).not.to.be.reverted;
523+
524+
expect(await Governance.receivedVotes(candidate1.address)).to.eq(0);
525+
expect(await Governance.receivedVotes(candidate2.address)).to.eq(MIN_VOTE_AMOUNT);
526+
expect(await Governance.votedTo(candidate1.address)).to.eq(candidate2.address);
527+
expect(await Governance.votedAmount(candidate1.address)).to.eq(MIN_VOTE_AMOUNT);
528+
expect(await Governance.voteHeight(candidate1.address)).to.eq(await ethers.provider.getBlockNumber());
529+
});
530+
531+
it("Should emit two events when a voter transfer votes", async function () {
532+
await Governance.connect(candidate1).registerCandidate(500, { value: REGISTER_FEE });
533+
await Governance.connect(candidate2).registerCandidate(500, { value: REGISTER_FEE });
534+
await expect(
535+
Governance.connect(candidate1).vote(candidate1, { value: MIN_VOTE_AMOUNT })
536+
).not.to.be.reverted;
537+
538+
await expect(
539+
Governance.connect(candidate1).transferVote(candidate2)
540+
).emit(Governance, "Revoke").emit(Governance, "Vote");
541+
});
542+
});
543+
484544
describe("claimReward", function () {
485545
let MockSysCall: any;
486546
let GovReward: any;

contracts/test/helpers/errors.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export const ERRORS = {
1111
ONLY_EOA: 'OnlyEOA()',
1212
INSUFFICIENT_VALUE: 'InsufficientValue()',
1313
INVALID_SHARE_RATE: 'InvalidShareRate()',
14+
SAME_CANDIDATE: 'SameCandidate()',
1415
CANDIDATE_EXISTS: 'CandidateExists()',
1516
CANDIDATE_NOT_EXISTS: 'CandidateNotExists()',
1617
LEFT_NOT_CLAIMED: 'LeftNotClaimed()',

0 commit comments

Comments
 (0)