-
Notifications
You must be signed in to change notification settings - Fork 0
Governance V2 #146
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
Governance V2 #146
Changes from all commits
4eb50fa
830d8d4
a66057c
e5d710a
3a1edba
9e3d28d
3c29ff3
b7d4027
14b0c95
c2901a7
f9e36ef
4b1bc9c
b50d17f
282c413
e3cefd5
8ab0cc9
5376de0
e3e34e0
31daaea
d875c4e
4bd1e18
3bed660
b0bdc8c
4670ecf
c8396b2
3a1191b
c018f78
057d307
8ed7867
ca4c868
5c07ff2
0a35b12
4308e26
435097e
04d19f4
df5c107
9d2c6a9
bb52dd4
938fb59
c17b90d
46e3f9b
e2e4ccc
1ddc572
bfff1d9
0e7b35c
694a232
cee0cd8
5f746f3
c556295
d5e6673
7018c09
786e78b
ed14630
19ce23d
c8ce78d
ecbe0f7
e25388d
456f01c
bca9d68
1ac7304
9ee869b
281ad69
c81008b
6f8bedd
d5992c2
8e80496
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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); | ||
| } | ||
| } | ||
| } | ||
| 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"; | ||
txhsl marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| 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; | ||
txhsl marked this conversation as resolved.
Show resolved
Hide resolved
AnnaShaleva marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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++) { | ||
txhsl marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| candidateGasPerVote[validators[i]] += | ||
txhsl marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| (msg.value * shareRateOf[validators[i]] * scaleFactor) / | ||
| consensusSize / | ||
| 1000 / | ||
txhsl marked this conversation as resolved.
Show resolved
Hide resolved
Comment on lines
+98
to
+100
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
Do we need a separate issue for documentation?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"); | ||
txhsl marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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]; | ||
txhsl marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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)) { | ||
txhsl marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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][ | ||
chenquanyu marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| (height - 1) / epochDuration + 1 | ||
| ]; | ||
| if (voteEpochEndGasPerVote > lastGasPerVote) { | ||
| lastGasPerVote = voteEpochEndGasPerVote; | ||
| } | ||
AnnaShaleva marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| uint reward = (votedAmount[voter] * | ||
| (latestGasPerVote - lastGasPerVote)) / scaleFactor; | ||
| voterGasPerVote[voter] = latestGasPerVote; | ||
| _safeTransferETH(voter, reward); | ||
txhsl marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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; | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.