Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
4eb50fa
add v2 voting
txhsl Feb 29, 2024
830d8d4
add candidate vote counter and some other details
txhsl Feb 29, 2024
a66057c
make gas stable
txhsl Mar 1, 2024
e5d710a
use vote as round change trigger
txhsl Mar 4, 2024
3a1edba
fix bugs
txhsl Mar 5, 2024
9e3d28d
add candidate list and sort method
txhsl Mar 5, 2024
3c29ff3
add v2 GovReward
txhsl Mar 5, 2024
b7d4027
bugfix
txhsl Mar 5, 2024
14b0c95
split epoch definition
txhsl Mar 5, 2024
c2901a7
add share setting, limit vote choice
txhsl Mar 6, 2024
f9e36ef
add candidate claim
txhsl Mar 6, 2024
4b1bc9c
add cache for getting consensus
txhsl Mar 6, 2024
b50d17f
update reward distribution, bugfix
txhsl Mar 6, 2024
282c413
relate epoch i to reward i+1
txhsl Mar 6, 2024
e3cefd5
fix candidate claim
txhsl Mar 6, 2024
8ab0cc9
use topk instead of quicksort
txhsl Mar 7, 2024
5376de0
remove duplicated codes
txhsl Mar 7, 2024
e3e34e0
update initial consensus
txhsl Mar 7, 2024
31daaea
change registerTime to registerEpoch, optimize array usage
txhsl Mar 8, 2024
d875c4e
fix register
txhsl Mar 8, 2024
4bd1e18
prevent error in getConsensus()
txhsl Mar 11, 2024
3bed660
add min amout requirement for change epoch
txhsl Mar 11, 2024
b0bdc8c
add min candidates requirement for change epoch, revert 4bd1e18
txhsl Mar 11, 2024
4670ecf
use EnumerableSet
txhsl Mar 12, 2024
c8396b2
allow other size for consensus
txhsl Mar 12, 2024
3a1191b
use block height instead of timestamp
txhsl Mar 12, 2024
c018f78
fix epoch 0
txhsl Mar 12, 2024
057d307
change lazy cache to immediate update
txhsl Mar 12, 2024
8ed7867
update GovReward
txhsl Mar 13, 2024
ca4c868
add dbft specific apis
txhsl Mar 13, 2024
5c07ff2
change epochCount getter names
txhsl Mar 14, 2024
0a35b12
rm a comment
txhsl Mar 14, 2024
4308e26
fix claimRegisterFee
txhsl Mar 14, 2024
435097e
fix revokeVote
txhsl Mar 14, 2024
04d19f4
revert 4308e26
txhsl Mar 14, 2024
df5c107
fix registerCandidate
txhsl Mar 14, 2024
9d2c6a9
add rewardBase as share rate cache
txhsl Mar 14, 2024
bb52dd4
rm unused apis
txhsl Mar 15, 2024
938fb59
rename variables
txhsl Mar 15, 2024
c17b90d
update and move settings to genesis file
txhsl Mar 15, 2024
46e3f9b
rewrite v2 proposal
txhsl Mar 19, 2024
e2e4ccc
fix register value check
txhsl Mar 19, 2024
1ddc572
fix distribution
txhsl Mar 19, 2024
bfff1d9
bugfix
txhsl Mar 19, 2024
0e7b35c
fix modifier
txhsl Mar 19, 2024
694a232
fix postPersist
txhsl Mar 19, 2024
cee0cd8
update variable naming
txhsl Mar 20, 2024
5f746f3
prevent postPersist from failure
txhsl Mar 22, 2024
c556295
rename postPersist to onPersist
txhsl Mar 22, 2024
d5e6673
add side call check in onPersist
txhsl Mar 22, 2024
7018c09
remove unused methods
txhsl Mar 22, 2024
786e78b
fix onPersist
txhsl Mar 22, 2024
ed14630
update genesis file for testing
txhsl Mar 22, 2024
19ce23d
fix genesis file
txhsl Mar 22, 2024
c8ce78d
fix genesis file
txhsl Mar 22, 2024
ecbe0f7
add access limit to receive()
txhsl Mar 23, 2024
e25388d
fix withdrawRegisterFee
txhsl Mar 23, 2024
456f01c
add balance check before gov reward transfer
txhsl Mar 23, 2024
bca9d68
update onPersist
txhsl Mar 23, 2024
1ac7304
make vote() user-friendly
txhsl Mar 23, 2024
9ee869b
update genesis file
txhsl Mar 23, 2024
281ad69
change optimize configuration
txhsl Mar 23, 2024
c81008b
update govreward genesis with new configuration
txhsl Mar 23, 2024
6f8bedd
format
txhsl Mar 23, 2024
d5992c2
fix settle reward
txhsl Mar 23, 2024
8e80496
update genesis file
txhsl Mar 23, 2024
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
54 changes: 54 additions & 0 deletions contracts/solidity/GovRewardV2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

library TransferHelper {
function safeTransfer(address token, address to, uint256 value) internal {
// bytes4(keccak256(bytes('transfer(address,uint256)')));
(bool success, bytes memory data) = token.call(
abi.encodeWithSelector(0xa9059cbb, to, value)
);
require(
success && (data.length == 0 || abi.decode(data, (bool))),
"safeTransfer: transfer failed"
);
}

function safeTransferETH(address to, uint256 value) internal {
(bool success, ) = to.call{value: value}(new bytes(0));
require(success, "safeTransferETH: ETH transfer failed");
}
}

interface IGovernance {
// get current consensus group
function getCurrentConsensus() external view returns (address[] memory);
}

interface IGovReward {
function getMiners() external view returns (address[] memory);

function withdraw() external;
}

contract GovReward is IGovReward {
// governance contact
address public constant governance =
0x1212000000000000000000000000000000000001;

receive() external payable {}

modifier onlyGov() {
require(msg.sender == governance, "Not governance");
_;
}

function getMiners() external view override returns (address[] memory) {
return IGovernance(governance).getCurrentConsensus();
}

function withdraw() external onlyGov {
if (address(this).balance > 0) {
TransferHelper.safeTransferETH(governance, address(this).balance);
}
}
}
324 changes: 324 additions & 0 deletions contracts/solidity/GovernanceV2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,324 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";

interface IGovernanceV2 {
event Register(address candidate);
event Exit(address candidate);
event Vote(address voter, address to, uint amount);
event Revoke(address voter, address from, uint amount);
event VoterClaim(address voter, uint reward);
event CandidateWithdraw(address candidate, uint amount);
event Persist(address[] validators);

// register to be a candidate with gas
function registerCandidate(uint shareRate) external payable;

// exit candidates and wait for withdraw
function exitCandidate() external;

// withdraw register fee after 2 epoch
function withdrawRegisterFee() external;

// vote with gas, only 1 target is allowed
function vote(address to) external payable;

// revoke votes and claim rewards
function revokeVote() external;

// only claim rewards
function claimReward() external;

// get consensus group members
function getCurrentConsensus() external view returns (address[] memory);

// compute and update cached consensus group
function onPersist() external;
}

interface IGovReward {
function withdraw() external;
}

contract GovernanceV2 is IGovernanceV2 {
using EnumerableSet for EnumerableSet.AddressSet;

// GovReward contract
address public constant govReward =
0x1212000000000000000000000000000000000003;
address public constant sysCall =
0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE;
uint public constant scaleFactor = 10 ** 18;

uint public consensusSize;
// the min balance for voting
uint public minVoteAmount;
// register fee
uint public registerFee;
// duration of an epoch (in blocks)
uint public epochDuration;

// candidate list
EnumerableSet.AddressSet internal candidateList;
// settings about how much reward given to voter
mapping(address => uint) public shareRateOf;
// the height when exit happens
mapping(address => uint) public exitHeightOf;
// the left register fee to exit
mapping(address => uint) public candidateBalanceOf;

// candidate=>amount
mapping(address => uint) public receivedVotes;
// voter=>candidate
mapping(address => address) public votedTo;
// voter=>amount
mapping(address => uint) public votedAmount;

// the block height when current epoch starts
uint public currentEpochStartHeight;
// the current group of block validators
address[] public currentConsensus;

// candidate=>total
mapping(address => uint) public candidateGasPerVote;
// voter=>number
mapping(address => uint) public voterGasPerVote;
// voter=>height
mapping(address => uint) public voteHeight;
// candidate=>height=>number
mapping(address => mapping(uint => uint)) public epochStartGasPerVote;

receive() external payable {
require(msg.sender == govReward, "side call not allowed");
address[] memory validators = currentConsensus;
uint length = validators.length;
for (uint i = 0; i < length; i++) {
candidateGasPerVote[validators[i]] +=
(msg.value * shareRateOf[validators[i]] * scaleFactor) /
consensusSize /
1000 /
Comment on lines +98 to +100
Copy link
Member

Choose a reason for hiding this comment

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

We should create at least basic documentation for reward calculations to show it to users. It should be documented that:

  • shareRateOf is X/1000, otherwise it's not clear
  • register fee amount
  • minimum vote amount
  • locking period for candidate registration fee
  • etc.

Do we need a separate issue for documentation?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not yet.

receivedVotes[validators[i]];
_safeTransferETH(
validators[i],
(msg.value * (1000 - shareRateOf[validators[i]])) /
consensusSize /
1000
);
}
}

function getCandidates() public view returns (address[] memory) {
return candidateList.values();
}

function registerCandidate(uint shareRate) external payable {
require(msg.value >= registerFee, "insufficient amount");
require(shareRate < 1000, "invalid rate");
require(!candidateList.contains(msg.sender), "candidate exists");
require(exitHeightOf[msg.sender] == 0, "left not claimed");
candidateList.add(msg.sender);

// record share rate and balance
shareRateOf[msg.sender] = shareRate;
candidateBalanceOf[msg.sender] = msg.value;
emit Register(msg.sender);
}

function exitCandidate() external {
require(candidateList.contains(msg.sender), "candidate not exists");
// remove candidate list, balance still locked
candidateList.remove(msg.sender);
exitHeightOf[msg.sender] = block.number;
emit Exit(msg.sender);
}

function withdrawRegisterFee() external {
// require 2 epochs to exit candidate list
// NOTE: suppose epoch change always happens in time
require(
exitHeightOf[msg.sender] > 0 &&
block.number > exitHeightOf[msg.sender] + 2 * epochDuration,
"withdraw not allowed"
);

// send back balance
uint amount = candidateBalanceOf[msg.sender];
delete candidateBalanceOf[msg.sender];
delete exitHeightOf[msg.sender];
delete shareRateOf[msg.sender];
_safeTransferETH(msg.sender, amount);
emit CandidateWithdraw(msg.sender, amount);
}

function vote(address candidateTo) external payable {
require(msg.value >= minVoteAmount, "insufficient amount");
require(candidateList.contains(candidateTo), "candidate not allowed");
address votedCandidate = votedTo[msg.sender];
require(
votedCandidate == candidateTo || votedCandidate == address(0),
"only one choice is allowed"
);

// settle reward here
if (votedCandidate != address(0)) {
_settleReward(msg.sender, votedCandidate);
} else {
// record tag value
votedTo[msg.sender] = candidateTo;
voterGasPerVote[msg.sender] = candidateGasPerVote[candidateTo];
}

// update votes
votedAmount[msg.sender] += msg.value;
receivedVotes[candidateTo] += msg.value;
// NOTE: the left reward in the first epoch of first vote will be unclaimable.
if (votedCandidate == address(0)) {
voteHeight[msg.sender] = block.number;
}

emit Vote(msg.sender, candidateTo, msg.value);
}

function revokeVote() external {
address candidateFrom = votedTo[msg.sender];
uint amount = votedAmount[msg.sender];
require(
candidateFrom != address(0) && amount > 0,
"revoke not allowed"
);

// settle reward here
_settleReward(msg.sender, candidateFrom);

// update votes
receivedVotes[candidateFrom] -= amount;
delete votedTo[msg.sender];
delete votedAmount[msg.sender];

// delete tag value
delete voterGasPerVote[msg.sender];
delete voteHeight[msg.sender];

_safeTransferETH(msg.sender, amount);
emit Revoke(msg.sender, candidateFrom, amount);
}

function claimReward() external {
address votedCandidate = votedTo[msg.sender];
require(votedCandidate != address(0), "claim not allowed");
_settleReward(msg.sender, votedCandidate);
}

function onPersist() external {
// NOTE: suppose onPersist always happens at the beginning of every block
require(msg.sender == sysCall, "side call not allowed");
// only settle validator reward if there is no epoch change
IGovReward(govReward).withdraw();
if (block.number < currentEpochStartHeight + epochDuration) return;

// update tag values
address[] memory candidates = candidateList.values();
uint length = candidates.length;
for (uint i = 0; i < length; i++) {
epochStartGasPerVote[candidates[i]][
currentEpochStartHeight / epochDuration
] = candidateGasPerVote[candidates[i]];
}

// compute and update consensus
currentEpochStartHeight = block.number;
currentConsensus = _computeConsensus();
emit Persist(currentConsensus);
}

function getCurrentConsensus() public view returns (address[] memory) {
return currentConsensus;
}

function _settleReward(address voter, address candidate) internal {
// NOTE: suppose onPersist always happens at the beginning of every block, then latestGasPerVote is always the latest
uint height = voteHeight[voter];
uint lastGasPerVote = voterGasPerVote[voter];
uint latestGasPerVote = candidateGasPerVote[candidate];
if (currentEpochStartHeight <= height) return;

// NOTE: suppose epoch change always happens at the beginning of a block, then vote in that block should wait another epoch to farm reward
uint voteEpochEndGasPerVote = epochStartGasPerVote[candidate][
(height - 1) / epochDuration + 1
];
if (voteEpochEndGasPerVote > lastGasPerVote) {
lastGasPerVote = voteEpochEndGasPerVote;
}

uint reward = (votedAmount[voter] *
(latestGasPerVote - lastGasPerVote)) / scaleFactor;
voterGasPerVote[voter] = latestGasPerVote;
_safeTransferETH(voter, reward);
emit VoterClaim(voter, reward);
}

function _safeTransferETH(address to, uint value) internal {
(bool success, ) = to.call{value: value}(new bytes(0));
require(success, "safeTransferETH: ETH transfer failed");
}

function _computeConsensus() internal view returns (address[] memory) {
// build up a votes array
address[] memory candidates = getCandidates();
uint length = candidates.length;
uint[] memory votes = new uint[](length);
for (uint i = 0; i < length; i++) {
votes[i] = receivedVotes[candidates[i]];
}

// sort top consensusSize based on votes
_topK(candidates, votes, consensusSize);

// return the first consensusSize candidates as consensus list
address[] memory consensus = new address[](consensusSize);
for (uint i = 0; i < consensusSize; i++) {
consensus[i] = candidates[i];
}
return consensus;
}

function _topK(
address[] memory candidates,
uint[] memory votes,
uint k
) internal pure {
uint length = candidates.length;
for (int j = int(k) / 2 - 1; j >= 0; j--) {
_heapDown(candidates, votes, uint(j), k);
}
for (uint i = k; i < length; i++) {
if (votes[i] > votes[0]) {
votes[0] = votes[i];
candidates[0] = candidates[i];
_heapDown(candidates, votes, 0, k);
}
}
}

function _heapDown(
address[] memory candidates,
uint[] memory votes,
uint j,
uint k
) internal pure {
uint i = 2 * j + 1;
while (i < k) {
if (i + 1 < k && votes[i] > votes[i + 1]) {
i += 1;
}
if (votes[i] > votes[j]) {
break;
}
(votes[i], votes[j]) = (votes[j], votes[i]);
(candidates[i], candidates[j]) = (candidates[j], candidates[i]);
j = i;
i = i * 2 + 1;
}
}
}
Loading