Skip to content

Commit 299ddbd

Browse files
feat(contracts): collect validator registration fee (#970)
* Add comment * Rename to validatorsCount * Remove _validatorsCount * Add _activeValidators list * Use _activeValidators for measurement * Add activeValidatorsCount * Shuffle without parameters * Use _activeValidators in calculateRoundValidators * Add fee * Collect and return fee * Fix add tests * Test registration * Check balance * Check resignation * Test fee * Check refund * Add tests * Improve _isActiveValidator * Fix reentrancy * Fee optimisation * Fee in initializer * Test fee * Improve packing * Set fee to uint128 * Update contract * Fix deployer * Set milestone * Support validatorRegistrationFee option * Calculate total supply * Set validatorRegistrationFee * Generate network * style: resolve style guide violations * regenerate networks * fixes * style: resolve style guide violations --------- Co-authored-by: oXtxNt9U <120286271+oXtxNt9U@users.noreply.github.com>
1 parent fab0392 commit 299ddbd

51 files changed

Lines changed: 3800 additions & 3339 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

contracts/src/consensus/ConsensusV1.sol

Lines changed: 136 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,9 @@ import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Own
3030

3131
contract ConsensusV1 is UUPSUpgradeable, OwnableUpgradeable {
3232
struct ValidatorData {
33-
uint256 votersCount;
3433
uint256 voteBalance;
34+
uint128 fee;
35+
uint64 votersCount;
3536
bool isResigned;
3637
bytes blsPublicKey; // 96 bits
3738
}
@@ -63,16 +64,15 @@ contract ConsensusV1 is UUPSUpgradeable, OwnableUpgradeable {
6364
address validator;
6465
}
6566

67+
event FeeUpdated(uint256 fee);
6668
event ValidatorRegistered(address addr, bytes blsPublicKey);
67-
6869
event ValidatorUpdated(address addr, bytes blsPublicKey);
69-
7070
event ValidatorResigned(address addr);
71-
7271
event Voted(address voter, address validator);
73-
7472
event Unvoted(address voter, address validator);
7573

74+
error InvalidFee();
75+
error RefundFailed();
7676
error CallerIsNotValidator();
7777
error ValidatorNotRegistered();
7878
error ValidatorAlreadyRegistered();
@@ -96,8 +96,9 @@ contract ConsensusV1 is UUPSUpgradeable, OwnableUpgradeable {
9696
mapping(address => ValidatorData) private _validatorsData;
9797
mapping(address => bool) private _hasValidator;
9898
mapping(bytes32 => bool) private _blsPublicKeys;
99-
address[] private _validators;
100-
uint256 private _validatorsCount; // Default 0
99+
address[] private _validators; // All registered validators including resigned
100+
address[] private _activeValidators; // Has valid BLS public key and is not resigned
101+
mapping(address => uint256) private _activeValidatorIndex; // Points to index in _activeValidators array
101102
uint256 private _resignedValidatorsCount; // Default 0
102103

103104
mapping(address => Vote) private _voters;
@@ -110,19 +111,26 @@ contract ConsensusV1 is UUPSUpgradeable, OwnableUpgradeable {
110111
address private _roundValidatorsHead; // Default address(0)
111112
uint256 private _roundValidatorsCount; // Default 0
112113
uint256 private _minValidators; // Default 1
114+
uint128 private _fee; // Validator registration fee: Default 0
113115

114116
RoundValidator[][] private _rounds;
115117

116118
// Initializers
117-
function initialize() public initializer {
119+
function initialize(uint128 registrationFee) public initializer {
118120
__Ownable_init(msg.sender);
119121
_minValidators = 1;
122+
_fee = registrationFee;
120123
}
121124

122125
// Overrides
123126
function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
124127

125128
// External functions
129+
function setFee(uint128 registrationFee) external onlyOwner {
130+
_fee = registrationFee;
131+
emit FeeUpdated(registrationFee);
132+
}
133+
126134
function addValidator(address addr, bytes calldata blsPublicKey, bool isResigned) external onlyOwner {
127135
if (_rounds.length > 0) {
128136
revert ImportIsNotAllowed();
@@ -142,9 +150,8 @@ contract ConsensusV1 is UUPSUpgradeable, OwnableUpgradeable {
142150
}
143151

144152
ValidatorData memory validator =
145-
ValidatorData({votersCount: 0, voteBalance: 0, isResigned: isResigned, blsPublicKey: blsPublicKey});
153+
ValidatorData({votersCount: 0, voteBalance: 0, fee: 0, isResigned: isResigned, blsPublicKey: blsPublicKey});
146154

147-
_validatorsCount++;
148155
_hasValidator[addr] = true;
149156
_validatorsData[addr] = validator;
150157
_validators.push(addr);
@@ -153,6 +160,10 @@ contract ConsensusV1 is UUPSUpgradeable, OwnableUpgradeable {
153160
_resignedValidatorsCount++;
154161
}
155162

163+
if (_canBecomeActiveValidator(addr)) {
164+
_addActiveValidator(addr);
165+
}
166+
156167
emit ValidatorRegistered(addr, blsPublicKey);
157168
}
158169

@@ -197,20 +208,29 @@ contract ConsensusV1 is UUPSUpgradeable, OwnableUpgradeable {
197208
emit Voted(voter, validator);
198209
}
199210

200-
function registerValidator(bytes calldata blsPublicKey) external {
211+
function registerValidator(bytes calldata blsPublicKey) external payable {
212+
if (msg.value != _fee) {
213+
revert InvalidFee();
214+
}
215+
201216
if (_hasValidator[msg.sender]) {
202217
revert ValidatorAlreadyRegistered();
203218
}
204219

205220
_verifyAndRegisterBlsPublicKey(blsPublicKey);
206221

207-
ValidatorData memory validator =
208-
ValidatorData({votersCount: 0, voteBalance: 0, isResigned: false, blsPublicKey: blsPublicKey});
222+
ValidatorData memory validator = ValidatorData({
223+
votersCount: 0,
224+
voteBalance: 0,
225+
fee: uint128(msg.value),
226+
isResigned: false,
227+
blsPublicKey: blsPublicKey
228+
});
209229

210-
_validatorsCount++;
211230
_hasValidator[msg.sender] = true;
212231
_validatorsData[msg.sender] = validator;
213232
_validators.push(msg.sender);
233+
_addActiveValidator(msg.sender);
214234

215235
emit ValidatorRegistered(msg.sender, blsPublicKey);
216236
}
@@ -224,6 +244,10 @@ contract ConsensusV1 is UUPSUpgradeable, OwnableUpgradeable {
224244

225245
_validatorsData[msg.sender].blsPublicKey = blsPublicKey;
226246

247+
if (_canBecomeActiveValidator(msg.sender) && !_isActiveValidator(msg.sender)) {
248+
_addActiveValidator(msg.sender);
249+
}
250+
227251
emit ValidatorUpdated(msg.sender, blsPublicKey);
228252
}
229253

@@ -237,13 +261,25 @@ contract ConsensusV1 is UUPSUpgradeable, OwnableUpgradeable {
237261
revert ValidatorAlreadyResigned();
238262
}
239263

240-
if (_validatorsCount - _resignedValidatorsCount <= _minValidators) {
264+
if (_activeValidators.length <= _minValidators) {
241265
revert BellowMinValidators();
242266
}
243267

244268
validator.isResigned = true;
245269
_resignedValidatorsCount += 1;
246270

271+
_removeActiveValidator(msg.sender);
272+
273+
// Refund the registration fee to the validator
274+
if (validator.fee > 0) {
275+
uint256 refundFee = validator.fee;
276+
validator.fee = 0;
277+
(bool success,) = payable(msg.sender).call{value: refundFee}("");
278+
if (!success) {
279+
revert RefundFailed();
280+
}
281+
}
282+
247283
emit ValidatorResigned(msg.sender);
248284
}
249285

@@ -305,18 +341,18 @@ contract ConsensusV1 is UUPSUpgradeable, OwnableUpgradeable {
305341

306342
_minValidators = n;
307343

308-
_shuffle(_validators);
344+
_shuffle();
309345
_deleteRoundValidators();
310346

311347
_roundValidatorsHead = address(0);
312-
uint8 top = uint8(_clamp(n, 0, _validatorsCount - _resignedValidatorsCount));
348+
uint8 top = uint8(_clamp(n, 0, _activeValidators.length));
313349

314350
if (top == 0) {
315351
revert NoActiveValidators();
316352
}
317353

318-
for (uint256 i = 0; i < _validators.length; i++) {
319-
address addr = _validators[i];
354+
for (uint256 i = 0; i < _activeValidators.length; i++) {
355+
address addr = _activeValidators[i];
320356

321357
ValidatorData storage data = _validatorsData[addr];
322358

@@ -375,8 +411,16 @@ contract ConsensusV1 is UUPSUpgradeable, OwnableUpgradeable {
375411
return 1;
376412
}
377413

378-
function registeredValidatorsCount() external view returns (uint256) {
379-
return _validatorsCount;
414+
function fee() external view returns (uint256) {
415+
return _fee;
416+
}
417+
418+
function validatorsCount() external view returns (uint256) {
419+
return _validators.length;
420+
}
421+
422+
function activeValidatorsCount() external view returns (uint256) {
423+
return _activeValidators.length;
380424
}
381425

382426
function resignedValidatorsCount() external view returns (uint256) {
@@ -475,8 +519,8 @@ contract ConsensusV1 is UUPSUpgradeable, OwnableUpgradeable {
475519
}
476520

477521
// Internal functions
478-
function _shuffle(address[] storage array) internal {
479-
uint256 n = array.length;
522+
function _shuffle() internal {
523+
uint256 n = _activeValidators.length;
480524
if (n == 0) {
481525
return;
482526
}
@@ -485,10 +529,35 @@ contract ConsensusV1 is UUPSUpgradeable, OwnableUpgradeable {
485529
// Get a random index between 0 and i (inclusive)
486530
uint256 j = uint256(keccak256(abi.encodePacked(block.timestamp, i))) % (i + 1);
487531

532+
if (i == j) {
533+
continue; // No need to swap if indices are the same
534+
}
535+
536+
/* Swap example
537+
i = 0; j = 2;
538+
539+
Initial state
540+
A B C
541+
A:0 B:1 C:2
542+
543+
Array SWAP
544+
C B A
545+
A:0 B:1 C:2
546+
547+
Index SWAP
548+
C B A
549+
A:2 B:1 C:0
550+
*/
551+
488552
// Swap elements at index i and j
489-
address temp = array[i];
490-
array[i] = array[j];
491-
array[j] = temp;
553+
address addrA = _activeValidators[i];
554+
address addrB = _activeValidators[j];
555+
556+
_activeValidators[i] = _activeValidators[j];
557+
_activeValidators[j] = addrA;
558+
559+
_activeValidatorIndex[addrA] = j;
560+
_activeValidatorIndex[addrB] = i;
492561
}
493562
}
494563

@@ -574,6 +643,47 @@ contract ConsensusV1 is UUPSUpgradeable, OwnableUpgradeable {
574643
_roundValidatorsCount++;
575644
}
576645

646+
function _addActiveValidator(address addr) internal {
647+
_activeValidators.push(addr);
648+
_activeValidatorIndex[addr] = _activeValidators.length - 1;
649+
}
650+
651+
function _removeActiveValidator(address addr) internal {
652+
if (!_isActiveValidator(addr)) {
653+
return;
654+
}
655+
656+
uint256 index = _activeValidatorIndex[addr];
657+
uint256 lastIndex = _activeValidators.length - 1;
658+
659+
// Copy last address to the index of the removed address. This is not swap. Last validator occurs 2 times in the array.
660+
if (index != lastIndex) {
661+
address lastValidator = _activeValidators[lastIndex];
662+
_activeValidators[index] = lastValidator;
663+
_activeValidatorIndex[lastValidator] = index;
664+
}
665+
666+
// Remove last address
667+
_activeValidators.pop();
668+
delete _activeValidatorIndex[addr];
669+
}
670+
671+
function _canBecomeActiveValidator(address addr) internal view returns (bool) {
672+
ValidatorData storage data = _validatorsData[addr];
673+
return !data.isResigned && data.blsPublicKey.length != 0;
674+
}
675+
676+
function _isActiveValidator(address addr) internal view returns (bool) {
677+
uint256 index = _activeValidatorIndex[addr];
678+
// Support for empty array
679+
if (index >= _activeValidators.length) {
680+
return false;
681+
}
682+
683+
// Check if the address at the index matches the address. Required for the case when index is 0.
684+
return _activeValidators[index] == addr;
685+
}
686+
577687
function _unvote() internal returns (address) {
578688
Vote storage voter = _voters[msg.sender];
579689
if (voter.validator == address(0)) {

contracts/test/consensus/Base.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ contract Base is Test {
1212
function test() public {}
1313

1414
function setUp() public {
15-
bytes memory data = abi.encode(ConsensusV1.initialize.selector);
15+
bytes memory data = abi.encode(ConsensusV1.initialize.selector, 0);
1616
address proxy = address(new ERC1967Proxy(address(new ConsensusV1()), data));
1717
consensus = ConsensusV1(proxy);
1818
}

contracts/test/consensus/Consensus-CalculateTop.sol

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ contract ConsensusTest is Base {
9494

9595
function test_consensus_sortedValidators_sameVoteCounts() public {
9696
vm.pauseGasMetering();
97-
assertEq(consensus.registeredValidatorsCount(), 0);
97+
assertEq(consensus.validatorsCount(), 0);
9898

9999
uint256 n = 55;
100100
uint256 balance = 50;
@@ -140,7 +140,7 @@ contract ConsensusTest is Base {
140140

141141
function test_consensus_200_topValidators() public {
142142
vm.pauseGasMetering();
143-
assertEq(consensus.registeredValidatorsCount(), 0);
143+
assertEq(consensus.validatorsCount(), 0);
144144

145145
address highest = address(0);
146146
uint256 highestBalance = 0;
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// SPDX-License-Identifier: GNU GENERAL PUBLIC LICENSE
2+
pragma solidity ^0.8.13;
3+
4+
import {ConsensusV1} from "@contracts/consensus/ConsensusV1.sol";
5+
import {Base} from "./Base.sol";
6+
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
7+
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
8+
9+
contract ConsensusTest is Base {
10+
function test_default_fee() public view {
11+
assertEq(consensus.fee(), 0);
12+
}
13+
14+
function test_default_fee_custom() public {
15+
uint256 initialFee = 10;
16+
bytes memory data = abi.encodeWithSelector(ConsensusV1.initialize.selector, initialFee);
17+
address proxy = address(new ERC1967Proxy(address(new ConsensusV1()), data));
18+
ConsensusV1 consensusCustom = ConsensusV1(proxy);
19+
20+
assertEq(consensusCustom.fee(), initialFee);
21+
}
22+
23+
function test_default_fee_should_be_adjustable() public {
24+
assertEq(consensus.fee(), 0);
25+
26+
uint128 newFee = 1000;
27+
vm.expectEmit(address(consensus));
28+
emit ConsensusV1.FeeUpdated(newFee);
29+
consensus.setFee(newFee);
30+
31+
assertEq(consensus.fee(), newFee);
32+
}
33+
}

contracts/test/consensus/Consensus-GetAllValidators.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.s
88
contract ConsensusTest is Base {
99
function test_200_validators() public {
1010
vm.pauseGasMetering();
11-
assertEq(consensus.registeredValidatorsCount(), 0);
11+
assertEq(consensus.validatorsCount(), 0);
1212

1313
uint256 n = 200;
1414
for (uint256 i = 0; i < n; i++) {

0 commit comments

Comments
 (0)