Skip to content

Commit a9d6cbc

Browse files
authored
Merge pull request #146 from bane-labs/governance-v2-proposal
Governance V2
2 parents 82a8b49 + 8e80496 commit a9d6cbc

File tree

3 files changed

+394
-11
lines changed

3 files changed

+394
-11
lines changed

contracts/solidity/GovRewardV2.sol

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.0;
3+
4+
library TransferHelper {
5+
function safeTransfer(address token, address to, uint256 value) internal {
6+
// bytes4(keccak256(bytes('transfer(address,uint256)')));
7+
(bool success, bytes memory data) = token.call(
8+
abi.encodeWithSelector(0xa9059cbb, to, value)
9+
);
10+
require(
11+
success && (data.length == 0 || abi.decode(data, (bool))),
12+
"safeTransfer: transfer failed"
13+
);
14+
}
15+
16+
function safeTransferETH(address to, uint256 value) internal {
17+
(bool success, ) = to.call{value: value}(new bytes(0));
18+
require(success, "safeTransferETH: ETH transfer failed");
19+
}
20+
}
21+
22+
interface IGovernance {
23+
// get current consensus group
24+
function getCurrentConsensus() external view returns (address[] memory);
25+
}
26+
27+
interface IGovReward {
28+
function getMiners() external view returns (address[] memory);
29+
30+
function withdraw() external;
31+
}
32+
33+
contract GovReward is IGovReward {
34+
// governance contact
35+
address public constant governance =
36+
0x1212000000000000000000000000000000000001;
37+
38+
receive() external payable {}
39+
40+
modifier onlyGov() {
41+
require(msg.sender == governance, "Not governance");
42+
_;
43+
}
44+
45+
function getMiners() external view override returns (address[] memory) {
46+
return IGovernance(governance).getCurrentConsensus();
47+
}
48+
49+
function withdraw() external onlyGov {
50+
if (address(this).balance > 0) {
51+
TransferHelper.safeTransferETH(governance, address(this).balance);
52+
}
53+
}
54+
}
Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.0;
3+
4+
import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
5+
6+
interface IGovernanceV2 {
7+
event Register(address candidate);
8+
event Exit(address candidate);
9+
event Vote(address voter, address to, uint amount);
10+
event Revoke(address voter, address from, uint amount);
11+
event VoterClaim(address voter, uint reward);
12+
event CandidateWithdraw(address candidate, uint amount);
13+
event Persist(address[] validators);
14+
15+
// register to be a candidate with gas
16+
function registerCandidate(uint shareRate) external payable;
17+
18+
// exit candidates and wait for withdraw
19+
function exitCandidate() external;
20+
21+
// withdraw register fee after 2 epoch
22+
function withdrawRegisterFee() external;
23+
24+
// vote with gas, only 1 target is allowed
25+
function vote(address to) external payable;
26+
27+
// revoke votes and claim rewards
28+
function revokeVote() external;
29+
30+
// only claim rewards
31+
function claimReward() external;
32+
33+
// get consensus group members
34+
function getCurrentConsensus() external view returns (address[] memory);
35+
36+
// compute and update cached consensus group
37+
function onPersist() external;
38+
}
39+
40+
interface IGovReward {
41+
function withdraw() external;
42+
}
43+
44+
contract GovernanceV2 is IGovernanceV2 {
45+
using EnumerableSet for EnumerableSet.AddressSet;
46+
47+
// GovReward contract
48+
address public constant govReward =
49+
0x1212000000000000000000000000000000000003;
50+
address public constant sysCall =
51+
0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE;
52+
uint public constant scaleFactor = 10 ** 18;
53+
54+
uint public consensusSize;
55+
// the min balance for voting
56+
uint public minVoteAmount;
57+
// register fee
58+
uint public registerFee;
59+
// duration of an epoch (in blocks)
60+
uint public epochDuration;
61+
62+
// candidate list
63+
EnumerableSet.AddressSet internal candidateList;
64+
// settings about how much reward given to voter
65+
mapping(address => uint) public shareRateOf;
66+
// the height when exit happens
67+
mapping(address => uint) public exitHeightOf;
68+
// the left register fee to exit
69+
mapping(address => uint) public candidateBalanceOf;
70+
71+
// candidate=>amount
72+
mapping(address => uint) public receivedVotes;
73+
// voter=>candidate
74+
mapping(address => address) public votedTo;
75+
// voter=>amount
76+
mapping(address => uint) public votedAmount;
77+
78+
// the block height when current epoch starts
79+
uint public currentEpochStartHeight;
80+
// the current group of block validators
81+
address[] public currentConsensus;
82+
83+
// candidate=>total
84+
mapping(address => uint) public candidateGasPerVote;
85+
// voter=>number
86+
mapping(address => uint) public voterGasPerVote;
87+
// voter=>height
88+
mapping(address => uint) public voteHeight;
89+
// candidate=>height=>number
90+
mapping(address => mapping(uint => uint)) public epochStartGasPerVote;
91+
92+
receive() external payable {
93+
require(msg.sender == govReward, "side call not allowed");
94+
address[] memory validators = currentConsensus;
95+
uint length = validators.length;
96+
for (uint i = 0; i < length; i++) {
97+
candidateGasPerVote[validators[i]] +=
98+
(msg.value * shareRateOf[validators[i]] * scaleFactor) /
99+
consensusSize /
100+
1000 /
101+
receivedVotes[validators[i]];
102+
_safeTransferETH(
103+
validators[i],
104+
(msg.value * (1000 - shareRateOf[validators[i]])) /
105+
consensusSize /
106+
1000
107+
);
108+
}
109+
}
110+
111+
function getCandidates() public view returns (address[] memory) {
112+
return candidateList.values();
113+
}
114+
115+
function registerCandidate(uint shareRate) external payable {
116+
require(msg.value >= registerFee, "insufficient amount");
117+
require(shareRate < 1000, "invalid rate");
118+
require(!candidateList.contains(msg.sender), "candidate exists");
119+
require(exitHeightOf[msg.sender] == 0, "left not claimed");
120+
candidateList.add(msg.sender);
121+
122+
// record share rate and balance
123+
shareRateOf[msg.sender] = shareRate;
124+
candidateBalanceOf[msg.sender] = msg.value;
125+
emit Register(msg.sender);
126+
}
127+
128+
function exitCandidate() external {
129+
require(candidateList.contains(msg.sender), "candidate not exists");
130+
// remove candidate list, balance still locked
131+
candidateList.remove(msg.sender);
132+
exitHeightOf[msg.sender] = block.number;
133+
emit Exit(msg.sender);
134+
}
135+
136+
function withdrawRegisterFee() external {
137+
// require 2 epochs to exit candidate list
138+
// NOTE: suppose epoch change always happens in time
139+
require(
140+
exitHeightOf[msg.sender] > 0 &&
141+
block.number > exitHeightOf[msg.sender] + 2 * epochDuration,
142+
"withdraw not allowed"
143+
);
144+
145+
// send back balance
146+
uint amount = candidateBalanceOf[msg.sender];
147+
delete candidateBalanceOf[msg.sender];
148+
delete exitHeightOf[msg.sender];
149+
delete shareRateOf[msg.sender];
150+
_safeTransferETH(msg.sender, amount);
151+
emit CandidateWithdraw(msg.sender, amount);
152+
}
153+
154+
function vote(address candidateTo) external payable {
155+
require(msg.value >= minVoteAmount, "insufficient amount");
156+
require(candidateList.contains(candidateTo), "candidate not allowed");
157+
address votedCandidate = votedTo[msg.sender];
158+
require(
159+
votedCandidate == candidateTo || votedCandidate == address(0),
160+
"only one choice is allowed"
161+
);
162+
163+
// settle reward here
164+
if (votedCandidate != address(0)) {
165+
_settleReward(msg.sender, votedCandidate);
166+
} else {
167+
// record tag value
168+
votedTo[msg.sender] = candidateTo;
169+
voterGasPerVote[msg.sender] = candidateGasPerVote[candidateTo];
170+
}
171+
172+
// update votes
173+
votedAmount[msg.sender] += msg.value;
174+
receivedVotes[candidateTo] += msg.value;
175+
// NOTE: the left reward in the first epoch of first vote will be unclaimable.
176+
if (votedCandidate == address(0)) {
177+
voteHeight[msg.sender] = block.number;
178+
}
179+
180+
emit Vote(msg.sender, candidateTo, msg.value);
181+
}
182+
183+
function revokeVote() external {
184+
address candidateFrom = votedTo[msg.sender];
185+
uint amount = votedAmount[msg.sender];
186+
require(
187+
candidateFrom != address(0) && amount > 0,
188+
"revoke not allowed"
189+
);
190+
191+
// settle reward here
192+
_settleReward(msg.sender, candidateFrom);
193+
194+
// update votes
195+
receivedVotes[candidateFrom] -= amount;
196+
delete votedTo[msg.sender];
197+
delete votedAmount[msg.sender];
198+
199+
// delete tag value
200+
delete voterGasPerVote[msg.sender];
201+
delete voteHeight[msg.sender];
202+
203+
_safeTransferETH(msg.sender, amount);
204+
emit Revoke(msg.sender, candidateFrom, amount);
205+
}
206+
207+
function claimReward() external {
208+
address votedCandidate = votedTo[msg.sender];
209+
require(votedCandidate != address(0), "claim not allowed");
210+
_settleReward(msg.sender, votedCandidate);
211+
}
212+
213+
function onPersist() external {
214+
// NOTE: suppose onPersist always happens at the beginning of every block
215+
require(msg.sender == sysCall, "side call not allowed");
216+
// only settle validator reward if there is no epoch change
217+
IGovReward(govReward).withdraw();
218+
if (block.number < currentEpochStartHeight + epochDuration) return;
219+
220+
// update tag values
221+
address[] memory candidates = candidateList.values();
222+
uint length = candidates.length;
223+
for (uint i = 0; i < length; i++) {
224+
epochStartGasPerVote[candidates[i]][
225+
currentEpochStartHeight / epochDuration
226+
] = candidateGasPerVote[candidates[i]];
227+
}
228+
229+
// compute and update consensus
230+
currentEpochStartHeight = block.number;
231+
currentConsensus = _computeConsensus();
232+
emit Persist(currentConsensus);
233+
}
234+
235+
function getCurrentConsensus() public view returns (address[] memory) {
236+
return currentConsensus;
237+
}
238+
239+
function _settleReward(address voter, address candidate) internal {
240+
// NOTE: suppose onPersist always happens at the beginning of every block, then latestGasPerVote is always the latest
241+
uint height = voteHeight[voter];
242+
uint lastGasPerVote = voterGasPerVote[voter];
243+
uint latestGasPerVote = candidateGasPerVote[candidate];
244+
if (currentEpochStartHeight <= height) return;
245+
246+
// NOTE: suppose epoch change always happens at the beginning of a block, then vote in that block should wait another epoch to farm reward
247+
uint voteEpochEndGasPerVote = epochStartGasPerVote[candidate][
248+
(height - 1) / epochDuration + 1
249+
];
250+
if (voteEpochEndGasPerVote > lastGasPerVote) {
251+
lastGasPerVote = voteEpochEndGasPerVote;
252+
}
253+
254+
uint reward = (votedAmount[voter] *
255+
(latestGasPerVote - lastGasPerVote)) / scaleFactor;
256+
voterGasPerVote[voter] = latestGasPerVote;
257+
_safeTransferETH(voter, reward);
258+
emit VoterClaim(voter, reward);
259+
}
260+
261+
function _safeTransferETH(address to, uint value) internal {
262+
(bool success, ) = to.call{value: value}(new bytes(0));
263+
require(success, "safeTransferETH: ETH transfer failed");
264+
}
265+
266+
function _computeConsensus() internal view returns (address[] memory) {
267+
// build up a votes array
268+
address[] memory candidates = getCandidates();
269+
uint length = candidates.length;
270+
uint[] memory votes = new uint[](length);
271+
for (uint i = 0; i < length; i++) {
272+
votes[i] = receivedVotes[candidates[i]];
273+
}
274+
275+
// sort top consensusSize based on votes
276+
_topK(candidates, votes, consensusSize);
277+
278+
// return the first consensusSize candidates as consensus list
279+
address[] memory consensus = new address[](consensusSize);
280+
for (uint i = 0; i < consensusSize; i++) {
281+
consensus[i] = candidates[i];
282+
}
283+
return consensus;
284+
}
285+
286+
function _topK(
287+
address[] memory candidates,
288+
uint[] memory votes,
289+
uint k
290+
) internal pure {
291+
uint length = candidates.length;
292+
for (int j = int(k) / 2 - 1; j >= 0; j--) {
293+
_heapDown(candidates, votes, uint(j), k);
294+
}
295+
for (uint i = k; i < length; i++) {
296+
if (votes[i] > votes[0]) {
297+
votes[0] = votes[i];
298+
candidates[0] = candidates[i];
299+
_heapDown(candidates, votes, 0, k);
300+
}
301+
}
302+
}
303+
304+
function _heapDown(
305+
address[] memory candidates,
306+
uint[] memory votes,
307+
uint j,
308+
uint k
309+
) internal pure {
310+
uint i = 2 * j + 1;
311+
while (i < k) {
312+
if (i + 1 < k && votes[i] > votes[i + 1]) {
313+
i += 1;
314+
}
315+
if (votes[i] > votes[j]) {
316+
break;
317+
}
318+
(votes[i], votes[j]) = (votes[j], votes[i]);
319+
(candidates[i], candidates[j]) = (candidates[j], candidates[i]);
320+
j = i;
321+
i = i * 2 + 1;
322+
}
323+
}
324+
}

0 commit comments

Comments
 (0)