diff --git a/packages/protocol/contracts-0.8/common/EpochManager.sol b/packages/protocol/contracts-0.8/common/EpochManager.sol index b3f7d82874e..6a5dea9b221 100644 --- a/packages/protocol/contracts-0.8/common/EpochManager.sol +++ b/packages/protocol/contracts-0.8/common/EpochManager.sol @@ -196,6 +196,8 @@ contract EpochManager is epochs[currentEpochNumber].firstBlock = block.number; epochs[currentEpochNumber].startTimestamp = block.timestamp; + epochProcessing.toProcessGroups = 0; + for (uint i = 0; i < elected.length; i++) { address group = getValidators().getValidatorsGroup(elected[i]); if (!processedGroups[group].processed) { @@ -213,9 +215,7 @@ contract EpochManager is require(epochProcessing.toProcessGroups == groups.length, "number of groups does not match"); - // since we are adding values it makes sense to start from the end - for (uint ii = groups.length; ii > 0; ii--) { - uint256 i = ii - 1; + for (uint i = 0; i < groups.length; i++) { ProcessedGroup storage processedGroup = processedGroups[groups[i]]; // checks that group is actually from elected group require(processedGroup.processed, "group not processed"); @@ -226,7 +226,6 @@ contract EpochManager is greaters[i] ); - epochProcessing.toProcessGroups = 0; delete processedGroups[groups[i]]; } getCeloUnreleasedTreasure().release( diff --git a/packages/protocol/contracts-0.8/governance/Validators.sol b/packages/protocol/contracts-0.8/governance/Validators.sol index b670184250b..0b3d2d7b498 100644 --- a/packages/protocol/contracts-0.8/governance/Validators.sol +++ b/packages/protocol/contracts-0.8/governance/Validators.sol @@ -345,7 +345,6 @@ contract Validators is * @dev De-affiliates with the previously affiliated group if present. */ function affiliate(address group) external nonReentrant returns (bool) { - allowOnlyL1(); address account = getAccounts().validatorSignerToAccount(msg.sender); require(isValidator(account), "Not a validator"); require(isValidatorGroup(group), "Not a validator group"); @@ -509,7 +508,6 @@ contract Validators is * @dev Fails if the group has zero members. */ function addMember(address validator) external nonReentrant returns (bool) { - allowOnlyL1(); address account = getAccounts().validatorSignerToAccount(msg.sender); require(groups[account].members.numElements > 0, "Validator group empty"); return _addMember(account, validator, address(0), address(0)); @@ -529,7 +527,6 @@ contract Validators is address lesser, address greater ) external nonReentrant returns (bool) { - allowOnlyL1(); address account = getAccounts().validatorSignerToAccount(msg.sender); require(groups[account].members.numElements == 0, "Validator group not empty"); return _addMember(account, validator, lesser, greater); diff --git a/packages/protocol/contracts/governance/interfaces/IElection.sol b/packages/protocol/contracts/governance/interfaces/IElection.sol index a9202294729..42fd7a7225f 100644 --- a/packages/protocol/contracts/governance/interfaces/IElection.sol +++ b/packages/protocol/contracts/governance/interfaces/IElection.sol @@ -67,4 +67,5 @@ interface IElection { function hasActivatablePendingVotes(address, address) external view returns (bool); function validatorSignerAddressFromCurrentSet(uint256 index) external view returns (address); function numberValidatorsInCurrentSet() external view returns (uint256); + function owner() external view returns (address); } diff --git a/packages/protocol/contracts/governance/interfaces/ILockedCelo.sol b/packages/protocol/contracts/governance/interfaces/ILockedCelo.sol index 7db68b0ba0c..cfa19772a8d 100644 --- a/packages/protocol/contracts/governance/interfaces/ILockedCelo.sol +++ b/packages/protocol/contracts/governance/interfaces/ILockedCelo.sol @@ -20,6 +20,7 @@ interface ILockedCelo { ) external; function addSlasher(string calldata slasherIdentifier) external; + function getAccountNonvotingLockedGold(address account) external view returns (uint256); function getAccountTotalLockedCelo(address) external view returns (uint256); function getTotalLockedCelo() external view returns (uint256); function getPendingWithdrawals( diff --git a/packages/protocol/scripts/foundry/constants.sh b/packages/protocol/scripts/foundry/constants.sh index 74681b59127..ca9ad573eab 100755 --- a/packages/protocol/scripts/foundry/constants.sh +++ b/packages/protocol/scripts/foundry/constants.sh @@ -59,7 +59,8 @@ export LIBRARY_DEPENDENCIES_PATH=( "lib/openzeppelin-contracts/contracts/math/SafeMath.sol" "lib/openzeppelin-contracts8/contracts/utils/math/SafeMath.sol" "lib/openzeppelin-contracts/contracts/math/Math.sol" - "lib/openzeppelin-contracts/contracts/cryptography/ECDSA.sol" + "lib/openzeppelin-contracts/contracts/cryptography/ECDSA.sol" "lib/openzeppelin-contracts/contracts/utils/Address.sol" "lib/solidity-bytes-utils/contracts/BytesLib.sol" + "lib/celo-foundry/lib/forge-std/src/console.sol" ) diff --git a/packages/protocol/scripts/foundry/create_and_migrate_anvil_devchain.sh b/packages/protocol/scripts/foundry/create_and_migrate_anvil_devchain.sh index 865c8ac854d..a77c48f81ff 100755 --- a/packages/protocol/scripts/foundry/create_and_migrate_anvil_devchain.sh +++ b/packages/protocol/scripts/foundry/create_and_migrate_anvil_devchain.sh @@ -60,6 +60,21 @@ forge script \ $LIBRARY_FLAGS \ --rpc-url $ANVIL_RPC_URL || { echo "Migration script failed"; exit 1; } +CELO_EPOCH_REWARDS_ADDRESS=$( + cast call \ + $REGISTRY_ADDRESS \ + "getAddressForStringOrDie(string calldata identifier)(address)" \ + "EpochRewards" \ + --rpc-url $ANVIL_RPC_URL +) + +echo "Setting storage of EpochRewards start time to same value as on mainnet" +# Storage slot of start time is 2 and the value is 1587587214 which is identical to mainnet +cast rpc \ +anvil_setStorageAt \ +$CELO_EPOCH_REWARDS_ADDRESS 2 "0x000000000000000000000000000000000000000000000000000000005ea0a88e" \ +--rpc-url $ANVIL_RPC_URL + # Keeping track of the finish time to measure how long it takes to run the script entirely ELAPSED_TIME=$(($SECONDS - $START_TIME)) echo "Migration script total elapsed time: $ELAPSED_TIME seconds" diff --git a/packages/protocol/test-sol/devchain/e2e/common/EpochManager.t.sol b/packages/protocol/test-sol/devchain/e2e/common/EpochManager.t.sol index 98642ed6338..f18fd68c6f7 100644 --- a/packages/protocol/test-sol/devchain/e2e/common/EpochManager.t.sol +++ b/packages/protocol/test-sol/devchain/e2e/common/EpochManager.t.sol @@ -21,8 +21,8 @@ contract E2E_EpochManager is Test, Devchain, Utils08, ECDSAHelper08 { address[] groups; address[] validatorsArray; - uint256[] groupScore = [5e23, 7e23, 1e24]; - uint256[] validatorScore = [1e23, 1e23, 1e23, 1e23, 1e23, 1e23]; + uint256[] groupScore = [5e23, 7e23, 1e24, 4e23]; + uint256[] validatorScore = [1e23, 1e23, 1e23, 1e23, 1e23, 1e23, 1e23]; struct VoterWithPK { address voter; @@ -226,38 +226,20 @@ contract E2E_EpochManager_FinishNextEpochProcess is E2E_EpochManager { epochManager.startNextEpochProcess(); } - function test_shouldFinishNextEpochProcessing() public { - uint256[] memory groupActiveBalances = new uint256[](groups.length); - - GroupWithVotes[] memory groupWithVotes = new GroupWithVotes[](groups.length); - - (, , uint256 totalRewardsVote, , ) = epochManager.getEpochProcessingState(); - - (address[] memory groupsEligible, uint256[] memory values) = election - .getTotalVotesForEligibleValidatorGroups(); - - for (uint256 i = 0; i < groupsEligible.length; i++) { - groupActiveBalances[i] = election.getActiveVotesForGroup(groupsEligible[i]); - groupWithVotes[i] = GroupWithVotes( - groupsEligible[i], - values[i] + - election.getGroupEpochRewardsBasedOnScore( - groupsEligible[i], - totalRewardsVote, - groupScore[i] - ) - ); + function assertGroupWithVotes(GroupWithVotes[] memory groupWithVotes) internal { + for (uint256 i = 0; i < groupWithVotes.length; i++) { + uint256 expected = election.getTotalVotesForGroup(groupWithVotes[i].group); + assertEq(election.getTotalVotesForGroup(groupWithVotes[i].group), groupWithVotes[i].votes); } + } - sort(groupWithVotes); - - address[] memory lessers = new address[](groups.length); - address[] memory greaters = new address[](groups.length); - - for (uint256 i = 0; i < groups.length; i++) { - lessers[i] = i == 0 ? address(0) : groupWithVotes[i - 1].group; - greaters[i] = i == groups.length - 1 ? address(0) : groupWithVotes[i + 1].group; - } + function test_shouldFinishNextEpochProcessing() public { + address[] memory lessers; + address[] memory greaters; + address[] memory groupsEligible; + GroupWithVotes[] memory groupWithVotes; + uint256[] memory groupActiveBalances; + (lessers, greaters, groupWithVotes) = getLessersAndGreaters(groups); uint256 currentEpoch = epochManager.getCurrentEpochNumber(); address[] memory currentlyElected = epochManager.getElected(); @@ -265,6 +247,10 @@ contract E2E_EpochManager_FinishNextEpochProcess is E2E_EpochManager { originalyElected.add(currentlyElected[i]); } + // wait some time before finishing + timeTravel(vm, epochDuration / 2); + blockTravel(vm, 100); + epochManager.finishNextEpochProcess(groups, lessers, greaters); assertEq(currentEpoch + 1, epochManager.getCurrentEpochNumber()); @@ -275,14 +261,16 @@ contract E2E_EpochManager_FinishNextEpochProcess is E2E_EpochManager { assertEq(originalyElected.contains(currentlyElected[i]), true); } - for (uint256 i = 0; i < groupsEligible.length; i++) { - assertEq(election.getActiveVotesForGroup(groupsEligible[i]), groupWithVotes[i].votes); - assertGt(election.getActiveVotesForGroup(groupsEligible[i]), groupActiveBalances[i]); - } - timeTravel(vm, epochDuration + 1); epochManager.startNextEpochProcess(); + + // wait some time before finishing + timeTravel(vm, epochDuration / 2); + blockTravel(vm, 100); + + (lessers, greaters, groupWithVotes) = getLessersAndGreaters(groups); epochManager.finishNextEpochProcess(groups, lessers, greaters); + assertGroupWithVotes(groupWithVotes); assertEq(currentEpoch + 2, epochManager.getCurrentEpochNumber()); @@ -291,17 +279,223 @@ contract E2E_EpochManager_FinishNextEpochProcess is E2E_EpochManager { for (uint256 i = 0; i < currentlyElected.length; i++) { assertEq(originalyElected.contains(newlyElected2[i]), true); } + + // add new validator group and validator + (address newValidatorGroup, address newValidator) = registerNewValidatorGroupWithValidator(); + + timeTravel(vm, epochDuration + 1); + epochManager.startNextEpochProcess(); + + timeTravel(vm, epochDuration / 2); + blockTravel(vm, 100); + + (lessers, greaters, groupWithVotes) = getLessersAndGreaters(groups); + epochManager.finishNextEpochProcess(groups, lessers, greaters); + assertGroupWithVotes(groupWithVotes); + + groups.push(newValidatorGroup); + validatorsArray.push(newValidator); + + assertEq(epochManager.getElected().length, validators.getRegisteredValidators().length); + assertEq(groups.length, validators.getRegisteredValidatorGroups().length); + + timeTravel(vm, epochDuration + 1); + epochManager.startNextEpochProcess(); + (lessers, greaters, groupWithVotes) = getLessersAndGreaters(groups); + epochManager.finishNextEpochProcess(groups, lessers, greaters); + assertGroupWithVotes(groupWithVotes); + + assertEq(epochManager.getElected().length, validatorsArray.length); + + // lower the number of electable validators + vm.prank(election.owner()); + election.setElectableValidators(1, validatorsArray.length - 1); + + timeTravel(vm, epochDuration + 1); + epochManager.startNextEpochProcess(); + (lessers, greaters, groupWithVotes) = getLessersAndGreaters(groups); + epochManager.finishNextEpochProcess(groups, lessers, greaters); + assertGroupWithVotes(groupWithVotes); + + assertEq(epochManager.getElected().length, validatorsArray.length - 1); + } + + function registerNewValidatorGroupWithValidator() + internal + returns (address newValidatorGroup, address newValidator) + { + (, GroupWithVotes[] memory groupWithVotes) = getGroupsWithVotes(); + uint256 newGroupPK = uint256(keccak256(abi.encodePacked("newGroup"))); + uint256 newValidatorPK = uint256(keccak256(abi.encodePacked("newValidator"))); + + vm.deal(vm.addr(newGroupPK), 100_000_000 ether); + vm.deal(vm.addr(newValidatorPK), 100_000_000 ether); + + (uint256 validatorLockedGoldRequirement, ) = validators.getValidatorLockedGoldRequirements(); + (uint256 groupLockedGoldRequirement, ) = validators.getGroupLockedGoldRequirements(); + + newValidatorGroup = registerValidatorGroup( + "newGroup", + newGroupPK, + groupLockedGoldRequirement, + 100000000000000000000000 + ); + newValidator = registerValidator( + newValidatorPK, + validatorLockedGoldRequirement, + newValidatorGroup + ); + vm.prank(newValidatorGroup); + validators.addFirstMember(newValidator, address(0), groupWithVotes[0].group); + uint256 nonVotingLockedGold = lockedCelo.getAccountNonvotingLockedGold(newValidator); + vm.prank(newValidatorGroup); + election.vote(newValidatorGroup, nonVotingLockedGold, address(0), groupWithVotes[0].group); + + vm.startPrank(scoreManager.owner()); + scoreManager.setGroupScore(newValidatorGroup, groupScore[3]); + scoreManager.setValidatorScore(newValidator, validatorScore[6]); + vm.stopPrank(); + } + + function getGroupsWithVotes() + internal + returns (address[] memory groupsInOrder, GroupWithVotes[] memory groupWithVotes) + { + uint256[] memory votesTotal; + (groupsInOrder, votesTotal) = election.getTotalVotesForEligibleValidatorGroups(); + + groupWithVotes = new GroupWithVotes[](groupsInOrder.length); + for (uint256 i = 0; i < groupsInOrder.length; i++) { + groupWithVotes[i] = GroupWithVotes(groupsInOrder[i], votesTotal[i]); + } + } + + function getValidatorGroupsFromElected() internal returns (address[] memory) { + address[] memory elected = epochManager.getElected(); + address[] memory validatorGroups = new address[](elected.length); + for (uint256 i = 0; i < elected.length; i++) { + (, , address group, , ) = validators.getValidator(elected[i]); + validatorGroups[i] = group; + } + return validatorGroups; + } + + function getLessersAndGreaters( + address[] memory groups + ) + private + returns ( + address[] memory lessers, + address[] memory greaters, + GroupWithVotes[] memory groupWithVotes + ) + { + (, , uint256 maxTotalRewards, , ) = epochManager.getEpochProcessingState(); + uint256 totalRewards = 0; + + (, groupWithVotes) = getGroupsWithVotes(); + + lessers = new address[](groups.length); + greaters = new address[](groups.length); + + uint256[] memory rewards = new uint256[](groups.length); + + for (uint256 i = 0; i < groups.length; i++) { + uint256 groupScore = scoreManager.getGroupScore(groups[i]); + rewards[i] = election.getGroupEpochRewardsBasedOnScore( + groups[i], + maxTotalRewards, + groupScore + ); + } + for (uint256 i = 0; i < groups.length; i++) { + uint256 rewards = rewards[i]; + + for (uint256 j = 0; j < groupWithVotes.length; j++) { + if (groupWithVotes[j].group == groups[i]) { + groupWithVotes[j].votes += rewards; + break; + } + } + sort(groupWithVotes); + + address lesser = address(0); + address greater = address(0); + + for (uint256 j = 0; j < groupWithVotes.length; j++) { + if (groupWithVotes[j].group == groups[i]) { + greater = j == 0 ? address(0) : groupWithVotes[j - 1].group; + lesser = j == groupWithVotes.length - 1 ? address(0) : groupWithVotes[j + 1].group; + break; + } + } + + lessers[i] = lesser; + greaters[i] = greater; + } + } + + function registerValidatorGroup( + string memory groupName, + uint256 privateKey, + uint256 amountToLock, + uint256 commission + ) public returns (address accountAddress) { + accountAddress = vm.addr(privateKey); + vm.startPrank(accountAddress); + lockGold(amountToLock); + getAccounts().setName(groupName); + getValidators().registerValidatorGroup(commission); + vm.stopPrank(); + } + + function registerValidator( + uint256 privateKey, + uint256 amountToLock, + address groupToAffiliate + ) public returns (address) { + address accountAddress = vm.addr(privateKey); + vm.startPrank(accountAddress); + lockGold(amountToLock); + + (bytes memory ecdsaPubKey, , , ) = _generateEcdsaPubKeyWithSigner(accountAddress, privateKey); + getValidators().registerValidatorNoBls(ecdsaPubKey); + getValidators().affiliate(groupToAffiliate); + + vm.stopPrank(); + return accountAddress; + } + + function _generateEcdsaPubKeyWithSigner( + address _validator, + uint256 _signerPk + ) internal returns (bytes memory ecdsaPubKey, uint8 v, bytes32 r, bytes32 s) { + (v, r, s) = getParsedSignatureOfAddress(_validator, _signerPk); + + bytes32 addressHash = keccak256(abi.encodePacked(_validator)); + ecdsaPubKey = addressToPublicKey(addressHash, v, r, s); } - // TODO: add test when new groups are elected - // TODO: add test when groups are removed + function getParsedSignatureOfAddress( + address _address, + uint256 privateKey + ) public pure returns (uint8, bytes32, bytes32) { + bytes32 addressHash = keccak256(abi.encodePacked(_address)); + bytes32 prefixedHash = toEthSignedMessageHash(addressHash); + return vm.sign(privateKey, prefixedHash); + } + + function lockGold(uint256 value) public { + getAccounts().createAccount(); + getLockedGold().lock{ value: value }(); + } // Bubble sort algorithm since it is a small array function sort(GroupWithVotes[] memory items) public { uint length = items.length; for (uint i = 0; i < length; i++) { for (uint j = 0; j < length - 1; j++) { - if (items[j].votes > items[j + 1].votes) { + if (items[j].votes < items[j + 1].votes) { // Swap GroupWithVotes memory temp = items[j]; items[j] = items[j + 1]; diff --git a/packages/protocol/test-sol/devchain/e2e/utils.sol b/packages/protocol/test-sol/devchain/e2e/utils.sol index 48275cb8d0e..d8e8ef9c1cc 100644 --- a/packages/protocol/test-sol/devchain/e2e/utils.sol +++ b/packages/protocol/test-sol/devchain/e2e/utils.sol @@ -8,6 +8,7 @@ import { IAccounts } from "@celo-contracts/common/interfaces/IAccounts.sol"; import { IScoreManager } from "@celo-contracts-8/common/interfaces/IScoreManager.sol"; import { IValidators } from "@celo-contracts/governance/interfaces/IValidators.sol"; import { IElection } from "@celo-contracts/governance/interfaces/IElection.sol"; +import { ILockedCelo } from "@celo-contracts/governance/interfaces/ILockedCelo.sol"; // All core contracts that are expected to be in the Registry on the devchain import "@celo-contracts-8/common/FeeCurrencyDirectory.sol"; @@ -29,6 +30,7 @@ contract Devchain is UsingRegistry, TestConstants { IAccounts accounts; IScoreManager scoreManager; IElection election; + ILockedCelo lockedCelo; constructor() { // The following line is required by UsingRegistry.sol @@ -46,6 +48,7 @@ contract Devchain is UsingRegistry, TestConstants { accounts = getAccounts(); scoreManager = IScoreManager(address(getScoreReader())); election = getElection(); + lockedCelo = getLockedCelo(); // TODO: Add missing core contracts below (see list in migrations_sol/constants.sol) // TODO: Consider asserting that all contracts we expect are available in the Devchain class diff --git a/packages/protocol/test-sol/unit/governance/validators/Validators.t.sol b/packages/protocol/test-sol/unit/governance/validators/Validators.t.sol index 62306bc292d..0d3f4f21ee2 100644 --- a/packages/protocol/test-sol/unit/governance/validators/Validators.t.sol +++ b/packages/protocol/test-sol/unit/governance/validators/Validators.t.sol @@ -1255,13 +1255,6 @@ contract ValidatorsTest_Affiliate_WhenGroupAndValidatorMeetLockedGoldRequirement assertEq(affiliation, group); } - function test_Reverts_WhenL2_WhenAffiliatingWithRegisteredValidatorGroup() public { - _whenL2(); - vm.prank(validator); - vm.expectRevert("This method is no longer supported in L2."); - validators.affiliate(group); - } - function test_Emits_ValidatorAffiliatedEvent() public { vm.expectEmit(true, true, true, true); emit ValidatorAffiliated(validator, group); @@ -1334,13 +1327,6 @@ contract ValidatorsTest_Affiliate_WhenValidatorIsAlreadyAffiliatedWithValidatorG assertEq(affiliation, otherGroup); } - function test_ShouldRevert_WhenL2_WhenValidatorNotMemberOfThatValidatorGroup() public { - _whenL2(); - vm.prank(validator); - vm.expectRevert("This method is no longer supported in L2."); - validators.affiliate(otherGroup); - } - function test_Emits_ValidatorDeaffiliatedEvent_WhenValidatorNotMemberOfThatValidatorGroup() public { @@ -2134,16 +2120,6 @@ contract ValidatorsTest_AddMember is ValidatorsTest { validators.addFirstMember(validator, address(0), address(0)); } - function test_Reverts_AddMemberToTheList_WhenL2() public { - _whenL2(); - address[] memory expectedMembersList = new address[](1); - expectedMembersList[0] = validator; - - vm.prank(group); - vm.expectRevert("This method is no longer supported in L2."); - validators.addMember(validator); - } - function test_ShouldUpdateGroupSizeHistory() public { vm.prank(group); validators.addFirstMember(validator, address(0), address(0));