From 02f39db4f6b3abef4fd095bd39b43751c3d27cf0 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Wed, 18 Sep 2019 17:44:17 -0700 Subject: [PATCH 001/149] Most things that don't touch incentives --- .../contracts/governance/Election.sol | 542 ++++++++++++ .../contracts/governance/LockedGold.sol | 809 ++++-------------- .../contracts/governance/Validators.sol | 471 ++-------- 3 files changed, 785 insertions(+), 1037 deletions(-) create mode 100644 packages/protocol/contracts/governance/Election.sol diff --git a/packages/protocol/contracts/governance/Election.sol b/packages/protocol/contracts/governance/Election.sol new file mode 100644 index 00000000000..0ceb736ca54 --- /dev/null +++ b/packages/protocol/contracts/governance/Election.sol @@ -0,0 +1,542 @@ +pragma solidity ^0.5.3; + +import "openzeppelin-solidity/contracts/math/SafeMath.sol"; +import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; +import "openzeppelin-solidity/contracts/utils/ReentrancyGuard.sol"; +import "solidity-bytes-utils/contracts/BytesLib.sol"; + +import "./UsingLockedGold.sol"; +import "./interfaces/IValidators.sol"; +import "../common/Initializable.sol"; +import "../common/FixidityLib.sol"; +import "../common/linkedlists/AddressLinkedList.sol"; +import "../common/linkedlists/AddressSortedLinkedList.sol"; + + +contract Election is Ownable, ReentrancyGuard, Initializable, UsingLockedGold { + + using FixidityLib for FixidityLib.Fraction; + using AddressSortedLinkedList for SortedLinkedList.List; + using SafeMath for uint256; + + // Pending votes are those for which no following elections have been held. + // These votes have yet to contribute to the election of validators and thus do not accrue + // rewards. + struct PendingVotes { + // Maps groups to accounts to pending voting balance. + mapping(address => mapping(address => uint256)) value; + // Maps groups to accounts to timestamp of the account's most recent vote for the group. + mapping(address => mapping(address => uint256)) timestamp; + } + + // Active votes are those for which at least one following election has been held. + // These votes have contributed to the election of validators and thus accrue rewards. + struct ActiveVotes { + // Maps groups to accounts to the numerator of the account's fraction of the group's + // total active votes. + mapping(address => mapping(address => uint256)) numerators; + // Maps groups to the denominator of all accounts' fraction of the group's total active votes. + mapping(address => uint256) denominators; + } + + struct Votes { + PendingVotes pending; + ActiveVotes active; + // A sorted list of ValidatorGroups by total votes. + SortedLinkedList.List totals; + // Maps an account to the list of groups it's voting for. + mapping(address => address[]) lists; + } + + Votes public votes; + uint256 public minElectableValidators; + uint256 public maxElectableValidators; + uint256 public maxVotesPerAccount; + uint256 public totalVotes; + FixidityLib.Fraction public electabilityThreshold; + + event MinElectableValidatorsSet( + uint256 minElectableValidators + ); + + event MaxElectableValidatorsSet( + uint256 maxElectableValidators + ); + + event MaxVotesPerAccountSet( + uint256 maxVotesPerAccount + ); + + event ValidatorGroupVoteCast( + address indexed account, + address indexed group, + uint256 weight + ); + + event ValidatorGroupVoteRevoked( + address indexed account, + address indexed group, + uint256 weight + ); + + /** + * @notice Initializes critical variables. + * @param registryAddress The address of the registry contract. + * @param _minElectableValidators The minimum number of validators that can be elected. + * @param _maxVotesPerAccount The maximum number of groups that an acconut can vote for at once. + * @dev Should be called only once. + */ + function initialize( + address registryAddress, + uint256 _minElectableValidators, + uint256 _maxElectableValidators, + uint256 _maxVotesPerAccount + ) + external + initializer + { + require(_minElectableValidators > 0 && _maxElectableValidators >= _minElectableValidators); + _transferOwnership(msg.sender); + setRegistry(registryAddress); + minElectableValidators = _minElectableValidators; + maxElectableValidators = _maxElectableValidators; + maxVotesPerAccount = _maxVotesPerAccount; + } + + /** + * @notice Updates the minimum number of validators that can be elected. + * @param _minElectableValidators The minimum number of validators that can be elected. + * @return True upon success. + */ + function setMinElectableValidators( + uint256 _minElectableValidators + ) + external + onlyOwner + returns (bool) + { + require( + _minElectableValidators > 0 && + _minElectableValidators != minElectableValidators && + _minElectableValidators <= maxElectableValidators + ); + minElectableValidators = _minElectableValidators; + emit MinElectableValidatorsSet(_minElectableValidators); + return true; + } + + /** + * @notice Updates the maximum number of validators that can be elected. + * @param _maxElectableValidators The maximum number of validators that can be elected. + * @return True upon success. + */ + function setMaxElectableValidators( + uint256 _maxElectableValidators + ) + external + onlyOwner + returns (bool) + { + require( + _maxElectableValidators != maxElectableValidators && + _maxElectableValidators >= minElectableValidators + ); + maxElectableValidators = _maxElectableValidators; + emit MaxElectableValidatorsSet(_maxElectableValidators); + return true; + } + + /** + * @notice Updates the maximum number of groups an account can be voting for at once. + * @param _maxVotesPerAccount The maximum number of groups an account can vote for. + * @return True upon success. + */ + function setMaxVotesPerAccount(uint256 _maxVotesPerAccount) external onlyOwner returns (bool) { + require(_maxVotesPerAccount != maxVotesPerAccount); + maxVotesPerAccount = _maxVotesPerAccount; + emit MaxVotesPerAccountSet(_maxVotePerAccount); + return true; + } + + /** + * @notice Increments the number of total and pending votes for `group`. + * @param group The validator group to vote for. + * @param value The amount of gold to use to vote. + * @param lesser The group receiving fewer votes than `group`, or 0 if `group` has the + * fewest votes of any validator group. + * @param greater The group receiving more votes than `group`, or 0 if `group` has the + * most votes of any validator group. + * @return True upon success. + * @dev Fails if `group` is empty or not a validator group. + */ + function vote( + address group, + uint256 value, + address lesser, + address greater + ) + external + nonReentrant + returns (bool) + { + require(0 < value && value <= getNumVotesReceivable(group)); + address account = getAccountFromVoter(msg.sender); + address[] storage list = votes.lists[account]; + require(list.length < maxVotesPerAccount); + for (uint256 i = 0; i < list.length; i = i.add(1)) { + require(list[i] != group); + } + list.push(group); + incrementPendingVotes(group, account, value); + incrementTotalVotes(group, value); + decrementNonvotingAccountBalance(account, value); + emit ValidatorGroupVoteCast(account, group, value); + return true; + } + + /** + * @notice Converts `account`'s pending votes for `group` to active votes. + * @param group The validator group to vote for. + * @return True upon success. + */ + function activate(address group) external nonReentrant returns (bool) { + address account = getAccountFromVoter(msg.sender); + PendingVotes storage pending = votes.pending; + uint256 pendingValue = pending.values[group][account]; + require(0 < pendingValue); + decrementPendingVotes(group, account, pendingValue); + incrementActiveVotes(group, account, pendingValue); + } + + /** + * @notice Revokes `value` pending votes for `group` + * @param group The validator group to revoke votes from. + * @param value The number of votes to revoke. + * @param lesser The group receiving fewer votes than the group for which the vote was revoked, + * or 0 if that group has the fewest votes of any validator group. + * @param greater The group receiving more votes than the group for which the vote was revoked, + * or 0 if that group has the most votes of any validator group. + * @param index The index of the group in the account's voting list. + * @return True upon success. + * @dev Fails if the account has not voted on a validator group. + */ + function revokePending( + address group, + uint256 value, + address lesser, + address greater, + uint256 index + ) + external + nonReentrant + returns (bool) + { + require(group != address(0)); + address account = getAccountFromVoter(msg.sender); + require(0 < value && value <= getAccountPendingVotesForGroup(group, account)); + decrementPendingVotes(group, account, value); + decrementTotalVotes(group, value, lesser, greater); + incrementNonvotingAccountBalance(account, value); + if (getAccountTotalVotesForGroup(group, account) == 0) { + deleteElement(votes.lists[account], group, index); + } + emit ValidatorGroupVoteRevoked(account, group, weight); + return true; + } + + /** + * @notice Revokes `value` active votes for `group` + * @param group The validator group to revoke votes from. + * @param value The number of votes to revoke. + * @param lesser The group receiving fewer votes than the group for which the vote was revoked, + * or 0 if that group has the fewest votes of any validator group. + * @param greater The group receiving more votes than the group for which the vote was revoked, + * or 0 if that group has the most votes of any validator group. + * @param index The index of the group in the account's voting list. + * @return True upon success. + * @dev Fails if the account has not voted on a validator group. + */ + function revokeActive( + address group, + uint256 value, + address lesser, + address greater, + uint256 index + ) + external + nonReentrant + returns (bool) + { + require(group != address(0)); + address account = getAccountFromVoter(msg.sender); + require(0 < value && value <= getAccountActiveVotesForGroup(group, account)); + decrementActiveVotes(group, account, value); + decrementTotalVotes(group, value, lesser, greater); + incrementNonvotingAccountBalance(account, value); + if (getAccountTotalVotesForGroup(group, account) == 0) { + deleteElement(votes.lists[account], group, index); + } + emit ValidatorGroupVoteRevoked(account, group, weight); + return true; + } + + function getAccountTotalVotes(address account) external view returns (uint256) { + uint256 total = 0; + address[] memory groups = votes.lists[account]; + for (uint256 i = 0; i < groups.length; i = i.add(1)) { + total = total.add(getAccountTotalVotesForGroup(groups[i], account)); + } + return total; + } + + function getAccountPendingVotesForGroup(address group, address account) external view returns (uint256) { + return votes.pending.values[group][account]; + } + + function getAccountActiveVotesForGroup(address group, address account) external view returns (uint256) { + uint256 numerator = votes.active.numerators[group][account].mul(votes.total.getValue(group)); + uint256 denominator = votes.total.getValue(group); + return numerator.div(denominator); + } + + function getAccountTotalVotesForGroup(address group, address account) external view returns (uint256) { + uint256 pending = getAccountPendingVotesForGroup(group, account) + uint256 active = getAccountActiveVotesForGroup(group, account); + return pending.add(active); + } + + /** + * @notice Increments the number of total votes for `group` by `value`. + * @param group The validator group whose vote total should be incremented. + * @param value The number of votes to increment. + * @param lesser The group receiving fewer votes than the group for which the vote was cast, + * or 0 if that group has the fewest votes of any validator group. + * @param greater The group receiving more votes than the group for which the vote was cast, + * or 0 if that group has the most votes of any validator group. + */ + function incrementTotalVotes( + address group, + uint256 value, + address lesser, + address greater + ) + private + { + if (votes.contains(group)) { + votes.totals.update(group, votes.getValue(group).add(value), lesser, greater); + } else { + votes.totals.insert(group, value, lesser, greater); + } + totalVotes = totalVotes.add(value); + } + + + /** + * @notice Decrements the number of total votes for `group` by `value`. + * @param group The validator group whose vote total should be decremented. + * @param value The number of votes to decrement. + * @param lesser The group receiving fewer votes than the group for which the vote was revoked, + * or 0 if that group has the fewest votes of any validator group. + * @param greater The group receiving more votes than the group for which the vote was revoked, + * or 0 if that group has the most votes of any validator group. + */ + function decrementTotalVotes( + address group, + uint256 value, + address lesser, + address greater + ) + private + { + if (votes.totals.contains(group)) { + uint256 newVoteTotal = votes.totals.getValue(group).sub(value); + if (newVoteTotal > 0) { + votes.totals.update(group, newVoteTotal, lesser, greater); + } else { + // Groups receiving no votes are not electable. + votes.totals.remove(group); + } + } + totalVotes = totalVotes.sub(value); + } + + function incrementActiveVotes(address group, address account, uint256 value) private { + uint256 delta = getActiveVotesDelta(group, account, value); + ActiveVotes storage active = votes.active; + active.denominators[group] = active.denominators[group].add(delta); + active.numerators[group][account] = active.numerators[group][account].add(delta); + } + + function decrementActiveVotes(address group, address account, uint256 value) private { + uint256 delta = getActiveVotesDelta(group, account, value); + ActiveVotes storage active = votes.active; + active.denominators[group] = active.denominators[group].sub(delta); + active.numerators[group][account] = active.numerators[group][account].sub(delta); + } + + function incrementPendingVotes(address group, address account, uint256 value) private { + PendingVotes storage pending = votes.pending; + pending.values[group][account] = pending.values[group][account].add(value); + pending.timestamps[group][account] = now; + } + + function decrementPendingVotes(address group, address account, uint256 value) private { + PendingVotes storage pending = votes.pending; + uint256 newValue = pending.values[group][account].sub(value); + pending.values[group][account] = newValue; + if (newValue == 0) { + pending.timestamps[group][account] = 0; + } + } + + function getActiveVotesDelta(address group, address account, uint256 value) private { + uint256 total = votes.totals.getValue(group); + // Preserve delta * total = value * denominator + uint256 delta = value.mul(active.denominators[group]).div(total); + } + + /** + * @notice Deletes an element from a list of addresses. + * @param list The list of addresses. + * @param element The address to delete. + * @param index The index of `element` in the list. + */ + function deleteElement(address[] storage list, address element, uint256 index) private { + require(index < list.length && list[index] == element); + uint256 lastIndex = list.length.sub(1); + list[index] = list[lastIndex]; + list[lastIndex] = address(0); + list.length = lastIndex; + } + + function getNumVotesReceivable(address group) public view returns (uint256) { + uint256 numerator = getNumGroupMembers(group).add(1).mul(getTotalLockedGold()); + uint256 denominator = Math.min(maxElectableValidators, getNumRegisteredValidators()); + return numerator.div(denominator); + } + + function validatorAddressFromCurrentSet(uint256 index) external view returns (address) { + address validatorAddress; + assembly { + let newCallDataPosition := mload(0x40) + mstore(newCallDataPosition, index) + let success := staticcall(5000, 0xfa, newCallDataPosition, 32, 0, 0) + returndatacopy(add(newCallDataPosition, 64), 0, 32) + validatorAddress := mload(add(newCallDataPosition, 64)) + } + + return validatorAddress; + } + + function numberValidatorsInCurrentSet() external view returns (uint256) { + uint256 numberValidators; + assembly { + let success := staticcall(5000, 0xf9, 0, 0, 0, 0) + let returnData := mload(0x40) + returndatacopy(returnData, 0, 32) + numberValidators := mload(returnData) + } + + return numberValidators; + } + + /** + * @notice Returns electable validator group addresses and their vote totals. + * @return Electable validator group addresses and their vote totals. + */ + function getValidatorGroupVotes() external view returns (address[] memory, uint256[] memory) { + return votes.getElements(); + } + + /** + * @notice Returns the number of votes a particular validator group has received. + * @param group The account that registered the validator group. + * @return The number of votes a particular validator group has received. + */ + function getVotesReceived(address group) external view returns (uint256) { + return votes.getValue(group); + } + + /** + * @notice Returns a list of elected validators with seats allocated to groups via the D'Hondt + * method. + * @return The list of elected validators. + * @dev See https://en.wikipedia.org/wiki/D%27Hondt_method#Allocation for more information. + */ + /* solhint-disable code-complexity */ + function electValidators() external view returns (address[] memory) { + // Only members of these validator groups are eligible for election. + uint256 maxNumElectionGroups = Math.min(maxElectableValidators, votes.totals.list.numElements); + uint256 requiredVotes = electabilityThreshold.multiply(FixidityLib.newFixed(totalVotes)).fromFixed(); + address[] memory electionGroups = votes.totals.list.headN(maxNumElectionGroups, requiredVotes); + // Holds the number of members elected for each of the eligible validator groups. + uint256[] memory numMembersElected = new uint256[](electionGroups.length); + uint256 totalNumMembersElected = 0; + bool memberElectedInRound = true; + // Assign a number of seats to each validator group. + while (totalNumMembersElected < maxElectableValidators && memberElectedInRound) { + memberElectedInRound = false; + uint256 groupIndex = 0; + FixidityLib.Fraction memory maxN = FixidityLib.wrap(0); + for (uint256 i = 0; i < electionGroups.length; i = i.add(1)) { + bool isWinningestGroupInRound = false; + (maxN, isWinningestGroupInRound) = dHondt(maxN, electionGroups[i], numMembersElected[i]); + if (isWinningestGroupInRound) { + memberElectedInRound = true; + groupIndex = i; + } + } + + if (memberElectedInRound) { + numMembersElected[groupIndex] = numMembersElected[groupIndex].add(1); + totalNumMembersElected = totalNumMembersElected.add(1); + } + } + require(totalNumMembersElected >= minElectableValidators); + // Grab the top validators from each group that won seats. + address[] memory electedValidators = new address[](totalNumMembersElected); + totalNumMembersElected = 0; + for (uint256 i = 0; i < electionGroups.length; i = i.add(1)) { + address[] memory electedGroupMembers = groups[electionGroups[i]].members.headN( + numMembersElected[i] + ); + for (uint256 j = 0; j < electedGroupMembers.length; j = j.add(1)) { + // We use the validating delegate if one is set. + electedValidators[totalNumMembersElected] = getValidatorFromAccount(electedGroupMembers[j]); + totalNumMembersElected = totalNumMembersElected.add(1); + } + } + return electedValidators; + } + /* solhint-enable code-complexity */ + + /** + * @notice Runs D'Hondt for a validator group. + * @param maxN The maximum number of votes per elected seat for a group in this round. + * @param groupAddress The address of the validator group. + * @param numMembersElected The number of members elected so far for this group. + * @dev See https://en.wikipedia.org/wiki/D%27Hondt_method#Allocation for more information. + * @return The new `maxN` and whether or not the group should win a seat in this round thus far. + */ + function dHondt( + FixidityLib.Fraction memory maxN, + address groupAddress, + uint256 numMembersElected + ) + private + view + returns (FixidityLib.Fraction memory, bool) + { + ValidatorGroup storage group = groups[groupAddress]; + // Only consider groups with members left to be elected. + if (group.members.numElements > numMembersElected) { + FixidityLib.Fraction memory n = FixidityLib.newFixed(votes.getValue(groupAddress)).divide( + FixidityLib.newFixed(numMembersElected.add(1)) + ); + if (n.gt(maxN)) { + return (n, true); + } + } + return (maxN, false); + } +} diff --git a/packages/protocol/contracts/governance/LockedGold.sol b/packages/protocol/contracts/governance/LockedGold.sol index 4d430956429..91afae461a7 100644 --- a/packages/protocol/contracts/governance/LockedGold.sol +++ b/packages/protocol/contracts/governance/LockedGold.sol @@ -17,708 +17,261 @@ import "../common/FractionUtil.sol"; contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistry { - using FixidityLib for FixidityLib.Fraction; - using FractionUtil for FractionUtil.Fraction; - using SafeMath for uint256; - - // TODO(asa): Remove index for gas efficiency if two updates to the same slot costs extra gas. - struct Commitment { - uint128 value; - uint128 index; + // TODO(asa): How do adjust for updated requirements? + // Have a refreshRequirements function validators and groups can call + struct MustMaintain { + uint256 value; + uint256 timestamp; } - struct Commitments { - // Maps a notice period in seconds to a Locked Gold commitment. - mapping(uint256 => Commitment) locked; - // Maps an availability time in seconds since epoch to a notified commitment. - mapping(uint256 => Commitment) notified; - uint256[] noticePeriods; - uint256[] availabilityTimes; + struct Authorizations { + address voting; + address validating; } - struct Account { - bool exists; - // The weight of the account in validator elections, governance, and block rewards. - uint256 weight; - // Each account may delegate their right to receive rewards, vote, and register a Validator or - // Validator group to exactly one address each, respectively. This address must not hold an - // account and must not be delegated to by any other account or by the same account for any - // other purpose. - address[3] delegates; - // Frozen accounts may not vote, but may redact votes. - bool votingFrozen; - // The timestamp of the last time that rewards were redeemed. - uint96 rewardsLastRedeemed; - Commitments commitments; + struct PendingWithdrawal { + uint256 value; + uint256 timestamp; } - // TODO(asa): Add minNoticePeriod - uint256 public maxNoticePeriod; - uint256 public totalWeight; - mapping(address => Account) private accounts; - // Maps voting, rewards, and validating delegates to the account that delegated these rights. - mapping(address => address) public delegations; - // Maps a block number to the cumulative reward for an account with weight 1 since genesis. - mapping(uint256 => FixidityLib.Fraction) public cumulativeRewardWeights; - - event MaxNoticePeriodSet( - uint256 maxNoticePeriod - ); - - event RoleDelegated( - DelegateRole role, - address indexed account, - address delegate - ); - - event VotingFrozen( - address indexed account - ); - - event VotingUnfrozen( - address indexed account - ); - - event NewCommitment( - address indexed account, - uint256 value, - uint256 noticePeriod - ); - - event CommitmentNotified( - address indexed account, - uint256 value, - uint256 noticePeriod, - uint256 availabilityTime - ); + struct Balances { + // This contract does not store an account's locked gold that is being used in electing + // validators. + uint256 nonvoting; + PendingWithdrawal[] pendingWithdrawals; + MustMaintain requirements; + } - event CommitmentExtended( - address indexed account, - uint256 value, - uint256 noticePeriod, - uint256 availabilityTime - ); + struct Account { + bool exists; + // Each account may authorize additional keys to use for voting or valdiating. + // These keys may not be keys of other accounts, and may not be authorized by any other + // account for any purpose. + Authorizations authorizations; + Balances balances; + } - event Withdrawal( - address indexed account, - uint256 value - ); + mapping(address => Account) public accounts; + // Maps voting and validating keys to the account that provided the authorization. + mapping(address => address) public authorizations; + uint256 public nonvotingTotal; + uint256 public unlockingPeriod; - event NoticePeriodIncreased( - address indexed account, - uint256 value, - uint256 noticePeriod, - uint256 increase - ); + event VoterAuthorized(address indexed account, address voter); + event ValidatorAuthorized(address indexed account, address validator); - function initialize(address registryAddress, uint256 _maxNoticePeriod) external initializer { + function initialize(address registryAddress) external initializer { _transferOwnership(msg.sender); setRegistry(registryAddress); - maxNoticePeriod = _maxNoticePeriod; - } - - /** - * @notice Sets the cumulative block reward for 1 unit of account weight. - * @param blockReward The total reward allocated to bonders for this block. - * @dev Called by the EVM at the end of the block. - */ - function setCumulativeRewardWeight(uint256 blockReward) external { - return; - // TODO(asa): Modify ganache to set cumulativeRewardWeights. - // TODO(asa): Make inheritable `onlyVm` modifier. - // Only callable by the EVM. - // require(msg.sender == address(0), "sender was not vm (reserved addr 0x0)"); - // FractionUtil.Fraction storage previousCumulativeRewardWeight = cumulativeRewardWeights[ - // block.number.sub(1) - // ]; - - // // This will be true the first time this is called by the EVM. - // if (!previousCumulativeRewardWeight.exists()) { - // previousCumulativeRewardWeight.denominator = 1; - // } - - // if (totalWeight > 0) { - // FractionUtil.Fraction memory currentRewardWeight = FractionUtil.Fraction( - // blockReward, - // totalWeight - // ).reduce(); - // cumulativeRewardWeights[block.number] = previousCumulativeRewardWeight.add( - // currentRewardWeight - // ); - // } else { - // cumulativeRewardWeights[block.number] = previousCumulativeRewardWeight; - // } - } - - /** - * @notice Sets the maximum notice period for an account. - * @param _maxNoticePeriod The new maximum notice period. - */ - function setMaxNoticePeriod(uint256 _maxNoticePeriod) external onlyOwner { - maxNoticePeriod = _maxNoticePeriod; - emit MaxNoticePeriodSet(maxNoticePeriod); } /** * @notice Creates an account. * @return True if account creation succeeded. */ - function createAccount() - external - returns (bool) - { - require(isNotAccount(msg.sender) && isNotDelegate(msg.sender)); + function createAccount() external returns (bool) { + require(isNotAccount(msg.sender) && isNotAuthorized(msg.sender)); Account storage account = accounts[msg.sender]; account.exists = true; - account.rewardsLastRedeemed = uint96(block.number); return true; } - /** - * @notice Redeems rewards accrued since the last redemption for the specified account. - * @return The amount of accrued rewards. - * @dev Fails if `msg.sender` is not the owner or rewards recipient of the account. - */ - function redeemRewards() external nonReentrant returns (uint256) { - require(false, "Disabled"); - address account = getAccountFromDelegateAndRole(msg.sender, DelegateRole.Rewards); - return _redeemRewards(account); - } - - /** - * @notice Freezes the voting power of `msg.sender`'s account. - */ - function freezeVoting() external { - require(isAccount(msg.sender)); - Account storage account = accounts[msg.sender]; - require(account.votingFrozen == false); - account.votingFrozen = true; - emit VotingFrozen(msg.sender); - } - - /** - * @notice Unfreezes the voting power of `msg.sender`'s account. - */ - function unfreezeVoting() external { - require(isAccount(msg.sender)); - Account storage account = accounts[msg.sender]; - require(account.votingFrozen == true); - account.votingFrozen = false; - emit VotingUnfrozen(msg.sender); - } - - /** - * @notice Delegates the validating power of `msg.sender`'s account to another address. - * @param delegate The address to delegate to. - * @param v The recovery id of the incoming ECDSA signature. - * @param r Output value r of the ECDSA signature. - * @param s Output value s of the ECDSA signature. - * @dev Fails if the address is already a delegate or has an account . - * @dev Fails if the current account is already participating in validation. - * @dev v, r, s constitute `delegate`'s signature on `msg.sender`. - */ - function delegateRole( - DelegateRole role, - address delegate, + function authorizeVoter( + address voter, uint8 v, bytes32 r, bytes32 s - ) external nonReentrant { - require(isAccount(msg.sender) && isNotAccount(delegate) && isNotDelegate(delegate)); - - address signer = Signatures.getSignerOfAddress(msg.sender, v, r, s); - require(signer == delegate); - - if (role == DelegateRole.Validating) { - require(isNotValidating(msg.sender)); - } else if (role == DelegateRole.Voting) { - require(!isVoting(msg.sender)); - } else if (role == DelegateRole.Rewards) { - _redeemRewards(msg.sender); - } - Account storage account = accounts[msg.sender]; - delegations[account.delegates[uint256(role)]] = address(0); - account.delegates[uint256(role)] = delegate; - delegations[delegate] = msg.sender; - emit RoleDelegated(role, msg.sender, delegate); + authorize(voter, account.authorizations.voting, v, r, s); + account.authorizations.voting = voter; + emit VoterAuthorized(msg.sender, voter); } - /** - * @notice Adds a Locked Gold commitment to `msg.sender`'s account. - * @param noticePeriod The notice period for the commitment. - * @return The account's new weight. - */ - function newCommitment( - uint256 noticePeriod - ) + function authorizeValidator( + address validator, + uint8 v, + bytes32 r, + bytes32 s external nonReentrant - payable - returns (uint256) { - require(isAccount(msg.sender) && !isVoting(msg.sender)); - - // _redeemRewards(msg.sender); - require(msg.value > 0 && noticePeriod <= maxNoticePeriod); Account storage account = accounts[msg.sender]; - Commitment storage locked = account.commitments.locked[noticePeriod]; - updateLockedCommitment(account, uint256(locked.value).add(msg.value), noticePeriod); - emit NewCommitment(msg.sender, msg.value, noticePeriod); - return account.weight; + authorize(validator, account.authorizations.validating, v, r, s); + account.authorizations.validating = validator; + emit ValidatorAuthorized(msg.sender, validator); } /** - * @notice Notifies a Locked Gold commitment, allowing funds to be withdrawn after the notice - * period. - * @param value The amount of the commitment to eventually withdraw. - * @param noticePeriod The notice period of the Locked Gold commitment. - * @return The account's new weight. + * @notice Locks gold to be used for voting. + * @param value The amount of gold to be locked. */ - function notifyCommitment( - uint256 value, - uint256 noticePeriod - ) - external - nonReentrant - returns (uint256) - { - require(isAccount(msg.sender) && isNotValidating(msg.sender) && !isVoting(msg.sender)); - // _redeemRewards(msg.sender); - Account storage account = accounts[msg.sender]; - Commitment storage locked = account.commitments.locked[noticePeriod]; - require(locked.value >= value && value > 0); - updateLockedCommitment(account, uint256(locked.value).sub(value), noticePeriod); + function lock(uint256 value) external nonReentrant { + require(isAccount(msg.sender)); + require(msg.value == value && value > 0); + incrementNonvotingAccountBalance(msg.sender, value) + emit GoldLocked(msg.sender, value); + } - // solhint-disable-next-line not-rely-on-time - uint256 availabilityTime = now.add(noticePeriod); - Commitment storage notified = account.commitments.notified[availabilityTime]; - updateNotifiedDeposit(account, uint256(notified.value).add(value), availabilityTime); + function incrementNonvotingAccountBalance(address account, uint256 value) private { + Account storage account = accounts[account]; + account.gold.nonvoting = account.gold.nonvoting.add(value); + totalNonvoting = totalNonvoting.add(value); + } - emit CommitmentNotified(msg.sender, value, noticePeriod, availabilityTime); - return account.weight; + function decrementNonvotingAccountBalance(address account, uint256 value) private { + Account storage account = accounts[account]; + account.gold.nonvoting = account.gold.nonvoting.sub(value); + totalNonvoting = totalNonvoting.sub(value); } - /** - * @notice Rebonds a notified commitment, with notice period >= the remaining time to - * availability. - * @param value The amount of the commitment to rebond. - * @param availabilityTime The availability time of the notified commitment. - * @return The account's new weight. - */ - function extendCommitment( - uint256 value, - uint256 availabilityTime - ) - external - nonReentrant - returns (uint256) - { - require(isAccount(msg.sender) && !isVoting(msg.sender)); - // solhint-disable-next-line not-rely-on-time - require(availabilityTime > now); - // _redeemRewards(msg.sender); + // TODO: Can't unlock if voting in governance. + function unlock(uint256 value) external nonReentrant { + require(isAccount(msg.sender)); Account storage account = accounts[msg.sender]; - Commitment storage notified = account.commitments.notified[availabilityTime]; - require(notified.value >= value && value > 0); - updateNotifiedDeposit(account, uint256(notified.value).sub(value), availabilityTime); - // solhint-disable-next-line not-rely-on-time - uint256 noticePeriod = availabilityTime.sub(now); - Commitment storage locked = account.commitments.locked[noticePeriod]; - updateLockedCommitment(account, uint256(locked.value).add(value), noticePeriod); - emit CommitmentExtended(msg.sender, value, noticePeriod, availabilityTime); - return account.weight; + MustMaintain memory requirement = account.requirement; + require( + now >= requirement.timestamp || + getAccountTotalLockedGold(msg.sender).sub(value) >= requirement.value + ); + decrementNonvotingAccountBalance(msg.sender, value); + uint256 available = now.add(unlockingPeriod); + account.balances.pendingWithdrawals.push(PendingWithdrawal(value, available)); + emit GoldUnlocked(msg.sender, value, available); } - /** - * @notice Withdraws a notified commitment after the duration of the notice period. - * @param availabilityTime The availability time of the notified commitment. - * @return The account's new weight. - */ - function withdrawCommitment( - uint256 availabilityTime - ) - external - nonReentrant - returns (uint256) - { - require(isAccount(msg.sender) && !isVoting(msg.sender)); - // _redeemRewards(msg.sender); - // solhint-disable-next-line not-rely-on-time - require(now >= availabilityTime); - _redeemRewards(msg.sender); + function relock(uint256 value, uint256 index) external nonReentrant { + require(isAccount(msg.sender)); Account storage account = accounts[msg.sender]; - Commitment storage notified = account.commitments.notified[availabilityTime]; - uint256 value = notified.value; - require(value > 0); - updateNotifiedDeposit(account, 0, availabilityTime); + require(index < account.gold.unlocking.length); + uint256 value = account.gold.unlocking[index].value; + incrementNonvotingAccountBalance(msg.sender, value); + deletePendingWithdrawal(account.gold.unlocking, index); + emit GoldLocked(msg.sender, value); + } + function withdraw(uint256 value, uint256 index) external nonReentrant { + require(isAccount(msg.sender)); + Account storage account = accounts[msg.sender]; + require(index < account.gold.unlocking.length); + PendingWithdrawal memory unlocking = account.gold.unlocking[index]; + require(now >= unlocking.available); + uint256 value = unlocking.value; + deletePendingWithdrawal(account.gold.unlocking, index); IERC20Token goldToken = IERC20Token(registry.getAddressFor(GOLD_TOKEN_REGISTRY_ID)); require(goldToken.transfer(msg.sender, value)); - emit Withdrawal(msg.sender, value); - return account.weight; + emit GoldWithdrawn(msg.sender, value); } - /** - * @notice Increases the notice period for all or part of a Locked Gold commitment. - * @param value The amount of the Locked Gold commitment to increase the notice period for. - * @param noticePeriod The notice period of the Locked Gold commitment. - * @param increase The amount to increase the notice period by. - * @return The account's new weight. - */ - function increaseNoticePeriod( + function setAccountMustMaintain( + address account, uint256 value, - uint256 noticePeriod, - uint256 increase + uint256 timestamp ) - external + public + onlyRegisteredContract('Election', msg.sender) nonReentrant - returns (uint256) - { - require(isAccount(msg.sender) && !isVoting(msg.sender)); - // _redeemRewards(msg.sender); - require(value > 0 && increase > 0); - Account storage account = accounts[msg.sender]; - Commitment storage locked = account.commitments.locked[noticePeriod]; - require(locked.value >= value); - updateLockedCommitment(account, uint256(locked.value).sub(value), noticePeriod); - uint256 increasedNoticePeriod = noticePeriod.add(increase); - uint256 increasedValue = account.commitments.locked[increasedNoticePeriod].value; - updateLockedCommitment(account, increasedValue.add(value), increasedNoticePeriod); - emit NoticePeriodIncreased(msg.sender, value, noticePeriod, increase); - return account.weight; - } - - /** - * @notice Returns whether or not an account's voting power is frozen. - * @param account The address of the account. - * @return Whether or not the account's voting power is frozen. - * @dev Frozen accounts can retract existing votes but not make future votes. - */ - function isVotingFrozen(address account) external view returns (bool) { - return accounts[account].votingFrozen; - } - - /** - * @notice Returns the timestamp of the last time the account redeemed block rewards. - * @param _account The address of the account. - * @return The timestamp of the last time `_account` redeemed block rewards. - */ - function getRewardsLastRedeemed(address _account) external view returns (uint96) { - Account storage account = accounts[_account]; - return account.rewardsLastRedeemed; - } - - function isValidating(address validator) external view returns (bool) { - IValidators validators = IValidators(registry.getAddressFor(VALIDATORS_REGISTRY_ID)); - return validators.isValidating(validator); - } - - /** - * @notice Returns the notice periods of all Locked Gold for an account. - * @param _account The address of the account. - * @return The notice periods of all Locked Gold for `_account`. - */ - function getNoticePeriods(address _account) external view returns (uint256[] memory) { - Account storage account = accounts[_account]; - return account.commitments.noticePeriods; - } - - /** - * @notice Returns the availability times of all notified commitments for an account. - * @param _account The address of the account. - * @return The availability times of all notified commitments for `_account`. - */ - function getAvailabilityTimes(address _account) external view returns (uint256[] memory) { - Account storage account = accounts[_account]; - return account.commitments.availabilityTimes; - } - - /** - * @notice Returns the value and index of a specified Locked Gold commitment. - * @param _account The address of the account. - * @param noticePeriod The notice period of the Locked Gold commitment. - * @return The value and index of the specified Locked Gold commitment. - */ - function getLockedCommitment( - address _account, - uint256 noticePeriod - ) - external - view - returns (uint256, uint256) - { - Account storage account = accounts[_account]; - Commitment storage locked = account.commitments.locked[noticePeriod]; - return (locked.value, locked.index); - } - - /** - * @notice Returns the value and index of a specified notified commitment. - * @param _account The address of the account. - * @param availabilityTime The availability time of the notified commitment. - * @return The value and index of the specified notified commitment. - */ - function getNotifiedCommitment( - address _account, - uint256 availabilityTime - ) - external - view - returns (uint256, uint256) + returns (bool) { - Account storage account = accounts[_account]; - Commitment storage notified = account.commitments.notified[availabilityTime]; - return (notified.value, notified.index); + accounts[account].requirement = MustMaintain(value, timestamp); + emit AccountMustMaintainSet(account, value, timestamp); } + // TODO(asa): Dedup /** - * @notice Returns the account associated with the provided delegate and role. - * @param accountOrDelegate The address of the account or voting delegate. - * @param role The delegate role to query for. - * @dev Fails if the `accountOrDelegate` is a non-voting delegate. + * @notice Returns the account associated with the `voter` address. + * @param accountOrVoter The address of the account or authorized voter. + * @dev Fails if the `accountOrVoter` is not an account or authorized voter. * @return The associated account. */ - function getAccountFromDelegateAndRole( - address accountOrDelegate, - DelegateRole role - ) - public - view - returns (address) - { - address delegatingAccount = delegations[accountOrDelegate]; - if (delegatingAccount != address(0)) { - require(accounts[delegatingAccount].delegates[uint256(role)] == accountOrDelegate); - return delegatingAccount; + function getAccountFromVoter(address accountOrVoter) public view returns (address) { + address authorizingAccount = authorizations[voter]; + if (authorizingAccount != address(0)) { + require(accounts[authorizingAccount].authorizations.voter == accountOrVoter); + return authorizingAccount; } else { - return accountOrDelegate; + require(isAccount(accountOrVoter)); + return accountOrVoter; } } - /** - * @notice Returns the weight of a specified account. - * @param _account The address of the account. - * @return The weight of the specified account. - */ - function getAccountWeight(address _account) external view returns (uint256) { - Account storage account = accounts[_account]; - return account.weight; + function getTotalLockedGold() public view returns (uint256) { + return nonvotingTotal.add(getTotalVotes()); } - /** - * @notice Returns whether or not a specified account is voting. - * @param account The address of the account. - * @return Whether or not the account is voting. - */ - function isVoting(address account) public view returns (bool) { - address voter = getDelegateFromAccountAndRole(account, DelegateRole.Voting); - IGovernance governance = IGovernance(registry.getAddressFor(GOVERNANCE_REGISTRY_ID)); - IValidators validators = IValidators(registry.getAddressFor(VALIDATORS_REGISTRY_ID)); - return (governance.isVoting(voter) || validators.isVoting(voter)); + function getAccountTotalLockedGold(address account) public view returns (uint256) { + uint256 total = accounts[account].balances.nonvoting; + return total.add(getAccountTotalVotes(account)); } /** - * @notice Returns the weight of a commitment for a given notice period. - * @param value The value of the commitment. - * @param noticePeriod The notice period of the commitment. - * @return The weight of the commitment. - * @dev A commitment's weight is (1 + sqrt(noticePeriodDays) / 30) * value. - */ - function getCommitmentWeight(uint256 value, uint256 noticePeriod) public pure returns (uint256) { - uint256 precision = 10000; - uint256 noticeDays = noticePeriod.div(1 days); - uint256 preciseMultiplier = sqrt(noticeDays).mul(precision).div(30).add(precision); - return preciseMultiplier.mul(value).div(precision); - } - - /** - * @notice Returns the delegate for a specified account and role. - * @param account The address of the account. - * @param role The role to query for. - * @return The rewards recipient for the account. + * @notice Returns the account associated with the `validator` address. + * @param accountOrValidator The address of the account or authorized validator. + * @dev Fails if the `accountOrValidator` is not an account or authorized validator. + * @return The associated account. */ - function getDelegateFromAccountAndRole( - address account, - DelegateRole role - ) - public - view - returns (address) - { - address delegate = accounts[account].delegates[uint256(role)]; - if (delegate == address(0)) { - return account; + function getAccountFromValidator(address accountOrValidator) public view returns (address) { + address authorizingAccount = authorizations[validator]; + if (authorizingAccount != address(0)) { + require(accounts[authorizingAccount].authorizations.validator == accountOrVoter); + return authorizingAccount; } else { - return delegate; + require(isAccount(accountOrVoter)); + return accountOrVoter; } } - // TODO(asa): Factor in governance, validator election participation. /** - * @notice Redeems rewards accrued since the last redemption for a specified account. - * @param _account The address of the account to redeem rewards for. - * @return The amount of accrued rewards. + * @notice Returns the voter for the specified account. + * @param account The address of the account. + * @return The address with which the account can vote. */ - function _redeemRewards(address _account) private returns (uint256) { - Account storage account = accounts[_account]; - uint256 rewardBlockNumber = block.number.sub(1); - FixidityLib.Fraction memory previousCumulativeRewardWeight = cumulativeRewardWeights[ - account.rewardsLastRedeemed - ]; - FixidityLib.Fraction memory cumulativeRewardWeight = cumulativeRewardWeights[ - rewardBlockNumber - ]; - // We should never get here except in testing, where cumulativeRewardWeight will not be set. - if (previousCumulativeRewardWeight.unwrap() == 0 || cumulativeRewardWeight.unwrap() == 0) { - return 0; - } - - FixidityLib.Fraction memory rewardWeight = cumulativeRewardWeight.subtract( - previousCumulativeRewardWeight - ); - require(rewardWeight.unwrap() != 0, "Rewards weight does not exist"); - uint256 value = rewardWeight.multiply(FixidityLib.wrap(account.weight)).fromFixed(); - account.rewardsLastRedeemed = uint96(rewardBlockNumber); - if (value > 0) { - address recipient = getDelegateFromAccountAndRole(_account, DelegateRole.Rewards); - IERC20Token goldToken = IERC20Token(registry.getAddressFor(GOLD_TOKEN_REGISTRY_ID)); - require(goldToken.transfer(recipient, value)); - emit Withdrawal(recipient, value); - } - return value; + function getVoterFromAccount(address account) public view returns (address) { + require(isAccount(account)); + address voter = accounts[account].authorizations.voter; + return voter == address(0) ? account : voter; } /** - * @notice Updates the Locked Gold commitment for a given notice period to a new value. - * @param account The account to update the Locked Gold commitment for. - * @param value The new value of the Locked Gold commitment. - * @param noticePeriod The notice period of the Locked Gold commitment. - */ - function updateLockedCommitment( - Account storage account, - uint256 value, - uint256 noticePeriod - ) - private - { - Commitment storage locked = account.commitments.locked[noticePeriod]; - require(value != locked.value); - uint256 weight; - if (locked.value == 0) { - locked.index = uint128(account.commitments.noticePeriods.length); - locked.value = uint128(value); - account.commitments.noticePeriods.push(noticePeriod); - weight = getCommitmentWeight(value, noticePeriod); - account.weight = account.weight.add(weight); - totalWeight = totalWeight.add(weight); - } else if (value == 0) { - weight = getCommitmentWeight(locked.value, noticePeriod); - account.weight = account.weight.sub(weight); - totalWeight = totalWeight.sub(weight); - deleteCommitment(locked, account.commitments, CommitmentType.Locked); - } else { - uint256 originalWeight = getCommitmentWeight(locked.value, noticePeriod); - weight = getCommitmentWeight(value, noticePeriod); - - uint256 difference; - if (weight >= originalWeight) { - difference = weight.sub(originalWeight); - account.weight = account.weight.add(difference); - totalWeight = totalWeight.add(difference); - } else { - difference = originalWeight.sub(weight); - account.weight = account.weight.sub(difference); - totalWeight = totalWeight.sub(difference); - } - - locked.value = uint128(value); - } - } - - /** - * @notice Updates the notified commitment for a given availability time to a new value. - * @param account The account to update the notified commitment for. - * @param value The new value of the notified commitment. - * @param availabilityTime The availability time of the notified commitment. + * @notice Returns the validator for the specified account. + * @param account The address of the account. + * @return The address with which the account can register a validator or group. */ - function updateNotifiedDeposit( - Account storage account, - uint256 value, - uint256 availabilityTime - ) - private - { - Commitment storage notified = account.commitments.notified[availabilityTime]; - require(value != notified.value); - if (notified.value == 0) { - notified.index = uint128(account.commitments.availabilityTimes.length); - notified.value = uint128(value); - account.commitments.availabilityTimes.push(availabilityTime); - account.weight = account.weight.add(notified.value); - totalWeight = totalWeight.add(notified.value); - } else if (value == 0) { - account.weight = account.weight.sub(notified.value); - totalWeight = totalWeight.sub(notified.value); - deleteCommitment(notified, account.commitments, CommitmentType.Notified); - } else { - uint256 difference; - if (value >= notified.value) { - difference = value.sub(notified.value); - account.weight = account.weight.add(difference); - totalWeight = totalWeight.add(difference); - } else { - difference = uint256(notified.value).sub(value); - account.weight = account.weight.sub(difference); - totalWeight = totalWeight.sub(difference); - } - - notified.value = uint128(value); - } + function getValidatorFromAccount(address account) public view returns (address) { + require(isAccount(account)); + address validator = accounts[account].authorizations.validator; + return validator == address(0) ? account : validator; } /** - * @notice Deletes a commitment from an account. - * @param _commitment The commitment to delete. - * @param commitments The struct containing the account's commitments. - * @param commitmentType Whether the deleted commitment is locked or notified. + * @notice Authorizes voting or validating power of `msg.sender`'s account to another address. + * @param current The address to authorize. + * @param previous The previous authorized address. + * @param v The recovery id of the incoming ECDSA signature. + * @param r Output value r of the ECDSA signature. + * @param s Output value s of the ECDSA signature. + * @dev Fails if the address is already authorized or is an account. + * @dev v, r, s constitute `authorize`'s signature on `msg.sender`. */ - function deleteCommitment( - Commitment storage _commitment, - Commitments storage commitments, - CommitmentType commitmentType + function authorize( + address current, + address previous, + uint8 v, + bytes32 r, + bytes32 s ) private + nonReentrant { - uint256 lastIndex; - if (commitmentType == CommitmentType.Locked) { - lastIndex = commitments.noticePeriods.length.sub(1); - commitments.locked[commitments.noticePeriods[lastIndex]].index = _commitment.index; - deleteElement(commitments.noticePeriods, _commitment.index, lastIndex); - } else { - lastIndex = commitments.availabilityTimes.length.sub(1); - commitments.notified[commitments.availabilityTimes[lastIndex]].index = _commitment.index; - deleteElement(commitments.availabilityTimes, _commitment.index, lastIndex); - } + require(isAccount(msg.sender) && isNotAccount(current) && isNotAuthorized(current)); - // Delete commitment info. - _commitment.index = 0; - _commitment.value = 0; - } + address signer = Signatures.getSignerOfAddress(msg.sender, v, r, s); + require(signer == current); - /** - * @notice Deletes an element from a list of uint256s. - * @param list The list of uint256s. - * @param index The index of the element to delete. - * @param lastIndex The index of the last element in the list. - */ - function deleteElement(uint256[] storage list, uint256 index, uint256 lastIndex) private { - list[index] = list[lastIndex]; - list[lastIndex] = 0; - list.length = lastIndex; + authorizations[previous] = address(0); + authorizations[current] = msg.sender; } function isAccount(address account) internal view returns (bool) { @@ -729,35 +282,13 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr return (!accounts[account].exists); } - // Reverts if rewards, voting, or validating rights have been delegated to `account`. - function isNotDelegate(address account) internal view returns (bool) { - return (delegations[account] == address(0)); - } - - // TODO(asa): Allow users to notify if they would continue to meet the registration - // requirements. - function isNotValidating(address account) internal view returns (bool) { - address validator = getDelegateFromAccountAndRole(account, DelegateRole.Validating); - IValidators validators = IValidators(registry.getAddressFor(VALIDATORS_REGISTRY_ID)); - return (!validators.isValidating(validator)); + function isNotAuthorized(address account) internal view returns (bool) { + return (authorizations[account] == address(0)); } - // TODO: consider using Fixidity's roots - /** - * @notice Approxmiates the square root of x using the Bablyonian method. - * @param x The number to take the square root of. - * @return An approximation of the square root of x. - * @dev The error can be large for smaller numbers, so we multiply by the square of `precision`. - */ - function sqrt(uint256 x) private pure returns (FractionUtil.Fraction memory) { - uint256 precision = 100; - uint256 px = x.mul(precision.mul(precision)); - uint256 z = px.add(1).div(2); - uint256 y = px; - while (z < y) { - y = z; - z = px.div(z).add(z).div(2); - } - return FractionUtil.Fraction(y, precision); + function deletePendingWithdrawal(PendingWithdrawal[] storage list, uint256 index) private { + uint256 lastIndex = list.length.sub(1); + list[index] = list[lastIndex]; + list.length = lastIndex; } } diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index c029aecb732..efd7525edb2 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -18,67 +18,53 @@ import "../common/linkedlists/AddressSortedLinkedList.sol"; */ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, UsingLockedGold { + // Votes live in the Election SC, votePortion, + using FixidityLib for FixidityLib.Fraction; using AddressLinkedList for LinkedList.List; using AddressSortedLinkedList for SortedLinkedList.List; using SafeMath for uint256; using BytesLib for bytes; - // Address of the getValidator precompiled contract - address constant public GET_VALIDATOR_ADDRESS = address(0xfa); + address constant PROOF_OF_POSSESSION = address(0xff - 4); + + struct RegistrationRequirements { + uint256 group; + uint256 validator; + } - // TODO(asa): These strings should be modifiable struct ValidatorGroup { - string identifier; string name; string url; + FixidityLib.Fraction commission; LinkedList.List members; } - // TODO(asa): These strings should be modifiable struct Validator { - string identifier; string name; string url; bytes publicKeysData; address affiliation; } - struct LockedGoldCommitment { - uint256 noticePeriod; - uint256 value; - } - mapping(address => ValidatorGroup) private groups; mapping(address => Validator) private validators; - // TODO(asa): Implement abstaining - mapping(address => address) public voters; address[] private _groups; address[] private _validators; - SortedLinkedList.List private votes; - // TODO(asa): Support different requirements for groups vs. validators. - LockedGoldCommitment private registrationRequirement; - uint256 public minElectableValidators; - uint256 public maxElectableValidators; - - address constant PROOF_OF_POSSESSION = address(0xff - 4); + RegistrationRequirements public registrationRequirements; + uint256 public maxGroupSize; - event MinElectableValidatorsSet( - uint256 minElectableValidators - ); - - event MaxElectableValidatorsSet( - uint256 maxElectableValidators + event MaxGroupSizeSet( + uint256 size ); event RegistrationRequirementSet( - uint256 value, - uint256 noticePeriod + uint256 group, + uint256 validator ); event ValidatorRegistered( address indexed validator, - string identifier, string name, string url, bytes publicKeysData @@ -100,7 +86,6 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi event ValidatorGroupRegistered( address indexed group, - string identifier, string name, string url ); @@ -128,124 +113,70 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi address indexed group ); - event ValidatorGroupVoteCast( - address indexed account, - address indexed group, - uint256 weight - ); - - event ValidatorGroupVoteRevoked( - address indexed account, - address indexed group, - uint256 weight - ); - /** * @notice Initializes critical variables. * @param registryAddress The address of the registry contract. - * @param _minElectableValidators The minimum number of validators that can be elected. - * @param _maxElectableValidators The maximum number of validators that can be elected. - * @param requirementValue The minimum Locked Gold commitment value to register a group or - validator. - * @param requirementNoticePeriod The minimum Locked Gold commitment notice period to register - * a group or validator. + * @param groupRequirement The minimum locked gold needed to register a group. + * @param validatorRequirement The minimum locked gold needed to register a validator. + * @param size The maximum group size. * @dev Should be called only once. */ function initialize( address registryAddress, - uint256 _minElectableValidators, - uint256 _maxElectableValidators, - uint256 requirementValue, - uint256 requirementNoticePeriod + uint256 groupRequirement, + uint256 validatorRequirement, + uint256 _maxGroupSize ) external initializer { - require(_minElectableValidators > 0 && _maxElectableValidators >= _minElectableValidators); _transferOwnership(msg.sender); setRegistry(registryAddress); - minElectableValidators = _minElectableValidators; - maxElectableValidators = _maxElectableValidators; - registrationRequirement.value = requirementValue; - registrationRequirement.noticePeriod = requirementNoticePeriod; + registrationRequirement.group = groupRequirement; + registrationRequirement.validator = validatorRequirement; + maxGroupSize = _maxGroupSize; } /** - * @notice Updates the minimum number of validators that can be elected. - * @param _minElectableValidators The minimum number of validators that can be elected. + * @notice Updates the maximum number of members a group can have. + * @param size The maximum group size. * @return True upon success. */ - function setMinElectableValidators( - uint256 _minElectableValidators - ) - external - onlyOwner - returns (bool) - { - require( - _minElectableValidators > 0 && - _minElectableValidators != minElectableValidators && - _minElectableValidators <= maxElectableValidators - ); - minElectableValidators = _minElectableValidators; - emit MinElectableValidatorsSet(_minElectableValidators); - return true; - } - - /** - * @notice Updates the maximum number of validators that can be elected. - * @param _maxElectableValidators The maximum number of validators that can be elected. - * @return True upon success. - */ - function setMaxElectableValidators( - uint256 _maxElectableValidators - ) - external - onlyOwner - returns (bool) - { - require( - _maxElectableValidators != maxElectableValidators && - _maxElectableValidators >= minElectableValidators - ); - maxElectableValidators = _maxElectableValidators; - emit MaxElectableValidatorsSet(_maxElectableValidators); + function setMaxGroupSize(uint256 size) external onlyOwner returns (bool) { + require(0 < size && size != maxGroupSize); + maxGroupSize = size; + emit MaxGroupSizeSet(size); return true; } /** - * @notice Updates the minimum bonding requirements to register a validator group or validator. - * @param value The minimum Locked Gold commitment value to register a group or validator. - * @param noticePeriod The minimum Locked Gold commitment notice period to register a group or - * validator. + * @notice Updates the minimum gold requirements to register a validator group or validator. + * @param groupRequirement The minimum locked gold needed to register a group. + * @param validatorRequirement The minimum locked gold needed to register a validator. * @return True upon success. * @dev The new requirement is only enforced for future validator or group registrations. */ - function setRegistrationRequirement( - uint256 value, - uint256 noticePeriod + function setRegistrationRequirements( + uint256 groupRequirement, + uint256 validatorRequirement ) external onlyOwner returns (bool) { require( - value != registrationRequirement.value || - noticePeriod != registrationRequirement.noticePeriod + groupRequirement != registrationRequirements.group || + validatorRequirement != registrationRequirements.validator ); - registrationRequirement.value = value; - registrationRequirement.noticePeriod = noticePeriod; - emit RegistrationRequirementSet(value, noticePeriod); + registrationRequirements = RegistrationRequirements(groupRequirement, validatorRequirement); + emit RegistrationRequirementSet(groupRequirement, validatorRequirement); return true; } /** * @notice Registers a validator unaffiliated with any validator group. - * @param identifier An identifier for this validator. * @param name A name for the validator. * @param url A URL for the validator. - * @param noticePeriod The notice period of the Locked Gold commitment that meets the - * requirements for validator registration. * @param publicKeysData Comprised of three tightly-packed elements: * - publicKey - The public key that the validator is using for consensus, should match * msg.sender. 64 bytes. @@ -257,18 +188,15 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi * @dev Fails if the account does not have sufficient weight. */ function registerValidator( - string calldata identifier, string calldata name, string calldata url, bytes calldata publicKeysData, - uint256 noticePeriod ) external nonReentrant returns (bool) { require( - bytes(identifier).length > 0 && bytes(name).length > 0 && bytes(url).length > 0 && // secp256k1 public key + BLS public key + BLS proof of possession @@ -279,12 +207,13 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi address account = getAccountFromValidator(msg.sender); require(!isValidator(account) && !isValidatorGroup(account)); - require(meetsRegistrationRequirements(account, noticePeriod)); + require(meetsRegistrationRequirements(account)); - Validator memory validator = Validator(identifier, name, url, publicKeysData, address(0)); + Validator memory validator = Validator(name, url, publicKeysData, address(0)); validators[account] = validator; _validators.push(account); - emit ValidatorRegistered(account, identifier, name, url, publicKeysData); + setAccountMustMaintain(account, requirements.validator, MAX_INT); + emit ValidatorRegistered(account, name, url, publicKeysData); return true; } @@ -314,6 +243,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi } delete validators[account]; deleteElement(_validators, account, index); + setAccountMustMaintain(account, requirements.validator, now.add(deregisterPeriods.validator)); emit ValidatorDeregistered(account); return true; } @@ -352,35 +282,39 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi /** * @notice Registers a validator group with no member validators. - * @param identifier A identifier for this validator group. * @param name A name for the validator group. * @param url A URL for the validator group. - * @param noticePeriod The notice period of the Locked Gold commitment that meets the - * requirements for validator registration. * @return True upon success. * @dev Fails if the account is already a validator or validator group. * @dev Fails if the account does not have sufficient weight. */ function registerValidatorGroup( - string calldata identifier, string calldata name, string calldata url, - uint256 noticePeriod + uint256 commission, + address[] calldata members ) external nonReentrant returns (bool) { - require(bytes(identifier).length > 0 && bytes(name).length > 0 && bytes(url).length > 0); + require(bytes(name).length > 0); + require(bytes(url).length > 0); + require(isFraction(commission)); + require(members.length <= maxGroupSize); address account = getAccountFromValidator(msg.sender); require(!isValidator(account) && !isValidatorGroup(account)); - require(meetsRegistrationRequirements(account, noticePeriod)); - ValidatorGroup storage group = groups[account]; - group.identifier = identifier; + require(meetsRegistrationRequirements(account)); + + ValdiatorGroup storage group = groups[account]; group.name = name; group.url = url; + for (uint256 i = 0; i < members.length; i = i.add(1)) { + group.addMember(members[i]); + } _groups.push(account); - emit ValidatorGroupRegistered(account, identifier, name, url); + setAccountMustMaintain(account, requirements.group, MAX_INT); + emit ValidatorGroupRegistered(account, name, url); return true; } @@ -396,6 +330,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi require(isValidatorGroup(account) && groups[account].members.numElements == 0); delete groups[account]; deleteElement(_groups, account, index); + setAccountMustMaintain(account, requirements.group, now.add(deregisterPeriods.group)); emit ValidatorGroupDeregistered(account); return true; } @@ -410,6 +345,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi address account = getAccountFromValidator(msg.sender); require(isValidatorGroup(account) && isValidator(validator)); ValidatorGroup storage group = groups[account]; + require(group.members.length < maxGroupSize); require(validators[validator].affiliation == account && !group.members.contains(validator)); group.members.push(validator); emit ValidatorGroupMemberAdded(account, validator); @@ -456,135 +392,6 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi return true; } - /** - * @notice Casts a vote for a validator group. - * @param group The validator group to vote for. - * @param lesser The group receiving fewer votes than `group`, or 0 if `group` has the - * fewest votes of any validator group. - * @param greater The group receiving more votes than `group`, or 0 if `group` has the - * most votes of any validator group. - * @return True upon success. - * @dev Fails if `group` is empty or not a validator group. - * @dev Fails if the account is frozen. - */ - function vote( - address group, - address lesser, - address greater - ) - external - nonReentrant - returns (bool) - { - // Empty validator groups are not electable. - require(isValidatorGroup(group) && groups[group].members.numElements > 0); - address account = getAccountFromVoter(msg.sender); - require(!isVotingFrozen(account)); - require(voters[account] == address(0)); - uint256 weight = getAccountWeight(account); - require(weight > 0); - if (votes.contains(group)) { - votes.update( - group, - votes.getValue(group).add(uint256(weight)), - lesser, - greater - ); - } else { - votes.insert( - group, - weight, - lesser, - greater - ); - } - voters[account] = group; - emit ValidatorGroupVoteCast(account, group, weight); - return true; - } - - /** - * @notice Revokes an outstanding vote for a validator group. - * @param lesser The group receiving fewer votes than the group for which the vote was revoked, - * or 0 if that group has the fewest votes of any validator group. - * @param greater The group receiving more votes than the group for which the vote was revoked, - * or 0 if that group has the most votes of any validator group. - * @return True upon success. - * @dev Fails if the account has not voted on a validator group. - */ - function revokeVote( - address lesser, - address greater - ) - external - nonReentrant - returns (bool) - { - address account = getAccountFromVoter(msg.sender); - address group = voters[account]; - require(group != address(0)); - uint256 weight = getAccountWeight(account); - // If the group we had previously voted on removed all its members it is no longer eligible - // to receive votes and we don't have to worry about removing our vote. - if (votes.contains(group)) { - require(weight > 0); - uint256 newVoteTotal = votes.getValue(group).sub(uint256(weight)); - if (newVoteTotal > 0) { - votes.update( - group, - newVoteTotal, - lesser, - greater - ); - } else { - // Groups receiving no votes are not electable. - votes.remove(group); - } - } - voters[account] = address(0); - emit ValidatorGroupVoteRevoked(account, group, weight); - return true; - } - - function validatorAddressFromCurrentSet(uint256 index) external view returns (address) { - address validatorAddress; - assembly { - let newCallDataPosition := mload(0x40) - mstore(newCallDataPosition, index) - let success := staticcall( - 5000, - 0xfa, - newCallDataPosition, - 32, - 0, - 0 - ) - returndatacopy(add(newCallDataPosition, 64), 0, 32) - validatorAddress := mload(add(newCallDataPosition, 64)) - } - - return validatorAddress; - } - - function numberValidatorsInCurrentSet() external view returns (uint256) { - uint256 numberValidators; - assembly { - let success := staticcall( - 5000, - 0xf9, - 0, - 0, - 0, - 0 - ) - let returnData := mload(0x40) - returndatacopy(returnData, 0, 32) - numberValidators := mload(returnData) - } - - return numberValidators; - } - /** * @notice Returns validator information. * @param account The account that registered the validator. @@ -596,7 +403,6 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi external view returns ( - string memory identifier, string memory name, string memory url, bytes memory publicKeysData, @@ -606,7 +412,6 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi require(isValidator(account)); Validator storage validator = validators[account]; return ( - validator.identifier, validator.name, validator.url, validator.publicKeysData, @@ -628,32 +433,15 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi { require(isValidatorGroup(account)); ValidatorGroup storage group = groups[account]; - return (group.identifier, group.name, group.url, group.members.getKeys()); - } - - /** - * @notice Returns electable validator group addresses and their vote totals. - * @return Electable validator group addresses and their vote totals. - */ - function getValidatorGroupVotes() external view returns (address[] memory, uint256[] memory) { - return votes.getElements(); - } - - /** - * @notice Returns the number of votes a particular validator group has received. - * @param group The account that registered the validator group. - * @return The number of votes a particular validator group has received. - */ - function getVotesReceived(address group) external view returns (uint256) { - return votes.getValue(group); + return (group.name, group.url, group.members.getKeys()); } /** - * @notice Returns the Locked Gold commitment requirements to register a validator or group. - * @return The minimum value and notice period for the Locked Gold commitment. + * @notice Returns the Locked Gold requirements to register a validator or group. + * @return The locked gold requirements to register a validator or group. */ - function getRegistrationRequirement() external view returns (uint256, uint256) { - return (registrationRequirement.value, registrationRequirement.noticePeriod); + function getRegistrationRequirements() external view returns (uint256, uint256) { + return (registrationRequirements.group, registrationRequirement.validator); } /** @@ -672,86 +460,13 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi return _groups; } - /** - * @notice Returns whether a particular account is a registered validator or validator group. - * @param account The account. - * @return Whether a particular account is a registered validator or validator group. - */ - function isValidating(address account) external view returns (bool) { - return isValidator(account) || isValidatorGroup(account); - } - - /** - * @notice Returns whether a particular account is voting for a validator group. - * @param account The account. - * @return Whether a particular account is voting for a validator group. - */ - function isVoting(address account) external view returns (bool) { - return (voters[account] != address(0)); - } - - /** - * @notice Returns a list of elected validators with seats allocated to groups via the D'Hondt - * method. - * @return The list of elected validators. - * @dev See https://en.wikipedia.org/wiki/D%27Hondt_method#Allocation for more information. - */ - /* solhint-disable code-complexity */ - function getValidators() external view returns (address[] memory) { - // Only members of these validator groups are eligible for election. - uint256 numElectionGroups = maxElectableValidators; - if (numElectionGroups > votes.list.numElements) { - numElectionGroups = votes.list.numElements; - } - address[] memory electionGroups = votes.list.headN(numElectionGroups); - // Holds the number of members elected for each of the eligible validator groups. - uint256[] memory numMembersElected = new uint256[](electionGroups.length); - uint256 totalNumMembersElected = 0; - bool memberElectedInRound = true; - // Assign a number of seats to each validator group. - while (totalNumMembersElected < maxElectableValidators && memberElectedInRound) { - memberElectedInRound = false; - uint256 groupIndex = 0; - FixidityLib.Fraction memory maxN = FixidityLib.wrap(0); - for (uint256 i = 0; i < electionGroups.length; i = i.add(1)) { - bool isWinningestGroupInRound = false; - (maxN, isWinningestGroupInRound) = dHondt(maxN, electionGroups[i], numMembersElected[i]); - if (isWinningestGroupInRound) { - memberElectedInRound = true; - groupIndex = i; - } - } - - if (memberElectedInRound) { - numMembersElected[groupIndex] = numMembersElected[groupIndex].add(1); - totalNumMembersElected = totalNumMembersElected.add(1); - } - } - require(totalNumMembersElected >= minElectableValidators); - // Grab the top validators from each group that won seats. - address[] memory electedValidators = new address[](totalNumMembersElected); - totalNumMembersElected = 0; - for (uint256 i = 0; i < electionGroups.length; i = i.add(1)) { - address[] memory electedGroupMembers = groups[electionGroups[i]].members.headN( - numMembersElected[i] - ); - for (uint256 j = 0; j < electedGroupMembers.length; j = j.add(1)) { - // We use the validating delegate if one is set. - electedValidators[totalNumMembersElected] = getValidatorFromAccount(electedGroupMembers[j]); - totalNumMembersElected = totalNumMembersElected.add(1); - } - } - return electedValidators; - } - /* solhint-enable code-complexity */ - /** * @notice Returns whether a particular account has a registered validator group. * @param account The account. * @return Whether a particular address is a registered validator group. */ function isValidatorGroup(address account) public view returns (bool) { - return bytes(groups[account].identifier).length > 0; + return bytes(groups[account].name).length > 0; } /** @@ -760,29 +475,16 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi * @return Whether a particular address is a registered validator. */ function isValidator(address account) public view returns (bool) { - return bytes(validators[account].identifier).length > 0; + return bytes(validators[account].name).length > 0; } /** * @notice Returns whether an account meets the requirements to register a validator or group. * @param account The account. - * @param noticePeriod The notice period of the Locked Gold commitment that meets the - * requirements. * @return Whether an account meets the requirements to register a validator or group. */ - function meetsRegistrationRequirements( - address account, - uint256 noticePeriod - ) - public - view - returns (bool) - { - uint256 value = getLockedCommitmentValue(account, noticePeriod); - return ( - value >= registrationRequirement.value && - noticePeriod >= registrationRequirement.noticePeriod - ); + function meetsValidatorRegistrationRequirements(address account) public view returns (bool) { + // TODO } /** @@ -816,6 +518,9 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi // Empty validator groups are not electable. if (groups[group].members.numElements == 0) { if (votes.contains(group)) { + // TODO(asa): What needs to happen here???? + // We need to remove from the linked list but preserve all other info... + // And probably add back in if it gets a member... votes.remove(group); } emit ValidatorGroupEmptied(group); @@ -845,34 +550,4 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi validator.affiliation = address(0); return true; } - - /** - * @notice Runs D'Hondt for a validator group. - * @param maxN The maximum number of votes per elected seat for a group in this round. - * @param groupAddress The address of the validator group. - * @param numMembersElected The number of members elected so far for this group. - * @dev See https://en.wikipedia.org/wiki/D%27Hondt_method#Allocation for more information. - * @return The new `maxN` and whether or not the group should win a seat in this round thus far. - */ - function dHondt( - FixidityLib.Fraction memory maxN, - address groupAddress, - uint256 numMembersElected - ) - private - view - returns (FixidityLib.Fraction memory, bool) - { - ValidatorGroup storage group = groups[groupAddress]; - // Only consider groups with members left to be elected. - if (group.members.numElements > numMembersElected) { - FixidityLib.Fraction memory n = FixidityLib.newFixed(votes.getValue(groupAddress)).divide( - FixidityLib.newFixed(numMembersElected.add(1)) - ); - if (n.gt(maxN)) { - return (n, true); - } - } - return (maxN, false); - } } From 0f226c8c34ad617c9538621b06f649b5117b6aac Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Wed, 18 Sep 2019 20:09:36 -0700 Subject: [PATCH 002/149] Trying to get things to compile --- .../contracts/common/UsingRegistry.sol | 7 +- .../contracts/governance/Election.sol | 97 +++++++++---------- .../contracts/governance/Governance.sol | 39 ++++---- .../contracts/governance/LockedGold.sol | 51 +++++----- .../contracts/governance/UsingLockedGold.sol | 75 +++++--------- .../contracts/governance/Validators.sol | 29 +++++- 6 files changed, 148 insertions(+), 150 deletions(-) diff --git a/packages/protocol/contracts/common/UsingRegistry.sol b/packages/protocol/contracts/common/UsingRegistry.sol index 378e43ea5ca..d5a599a5fc3 100644 --- a/packages/protocol/contracts/common/UsingRegistry.sol +++ b/packages/protocol/contracts/common/UsingRegistry.sol @@ -15,12 +15,13 @@ contract UsingRegistry is Ownable { // solhint-disable state-visibility bytes32 constant ATTESTATIONS_REGISTRY_ID = keccak256(abi.encodePacked("Attestations")); - bytes32 constant LOCKED_GOLD_REGISTRY_ID = keccak256(abi.encodePacked("LockedGold")); + bytes32 constant ELECTION_REGISTRY_ID = keccak256(abi.encodePacked("Election")); bytes32 constant GAS_CURRENCY_WHITELIST_REGISTRY_ID = keccak256( abi.encodePacked("GasCurrencyWhitelist") ); bytes32 constant GOLD_TOKEN_REGISTRY_ID = keccak256(abi.encodePacked("GoldToken")); bytes32 constant GOVERNANCE_REGISTRY_ID = keccak256(abi.encodePacked("Governance")); + bytes32 constant LOCKED_GOLD_REGISTRY_ID = keccak256(abi.encodePacked("LockedGold")); bytes32 constant RESERVE_REGISTRY_ID = keccak256(abi.encodePacked("Reserve")); bytes32 constant RANDOM_REGISTRY_ID = keccak256(abi.encodePacked("Random")); bytes32 constant SORTED_ORACLES_REGISTRY_ID = keccak256(abi.encodePacked("SortedOracles")); @@ -29,6 +30,10 @@ contract UsingRegistry is Ownable { IRegistry public registry; + modifier onlyRegisteredContract(bytes32 identifierHash, address sender) { + require(registry.getAddressForOrDie(identifierHash) == sender); + _; + } /** * @notice Updates the address pointing to a Registry contract. * @param registryAddress The address of a registry contract for routing to other contracts. diff --git a/packages/protocol/contracts/governance/Election.sol b/packages/protocol/contracts/governance/Election.sol index 0ceb736ca54..c86888bee7e 100644 --- a/packages/protocol/contracts/governance/Election.sol +++ b/packages/protocol/contracts/governance/Election.sol @@ -1,22 +1,22 @@ pragma solidity ^0.5.3; +import "openzeppelin-solidity/contracts/math/Math.sol"; import "openzeppelin-solidity/contracts/math/SafeMath.sol"; import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; import "openzeppelin-solidity/contracts/utils/ReentrancyGuard.sol"; -import "solidity-bytes-utils/contracts/BytesLib.sol"; import "./UsingLockedGold.sol"; +import "./UsingValidators.sol"; import "./interfaces/IValidators.sol"; import "../common/Initializable.sol"; import "../common/FixidityLib.sol"; -import "../common/linkedlists/AddressLinkedList.sol"; import "../common/linkedlists/AddressSortedLinkedList.sol"; -contract Election is Ownable, ReentrancyGuard, Initializable, UsingLockedGold { +contract Election is Ownable, ReentrancyGuard, Initializable, UsingLockedGold, UsingValidators { - using FixidityLib for FixidityLib.Fraction; using AddressSortedLinkedList for SortedLinkedList.List; + using FixidityLib for FixidityLib.Fraction; using SafeMath for uint256; // Pending votes are those for which no following elections have been held. @@ -154,7 +154,7 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingLockedGold { function setMaxVotesPerAccount(uint256 _maxVotesPerAccount) external onlyOwner returns (bool) { require(_maxVotesPerAccount != maxVotesPerAccount); maxVotesPerAccount = _maxVotesPerAccount; - emit MaxVotesPerAccountSet(_maxVotePerAccount); + emit MaxVotesPerAccountSet(_maxVotesPerAccount); return true; } @@ -240,7 +240,7 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingLockedGold { if (getAccountTotalVotesForGroup(group, account) == 0) { deleteElement(votes.lists[account], group, index); } - emit ValidatorGroupVoteRevoked(account, group, weight); + emit ValidatorGroupVoteRevoked(account, group, value); return true; } @@ -276,7 +276,7 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingLockedGold { if (getAccountTotalVotesForGroup(group, account) == 0) { deleteElement(votes.lists[account], group, index); } - emit ValidatorGroupVoteRevoked(account, group, weight); + emit ValidatorGroupVoteRevoked(account, group, value); return true; } @@ -289,18 +289,18 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingLockedGold { return total; } - function getAccountPendingVotesForGroup(address group, address account) external view returns (uint256) { + function getAccountPendingVotesForGroup(address group, address account) public view returns (uint256) { return votes.pending.values[group][account]; } - function getAccountActiveVotesForGroup(address group, address account) external view returns (uint256) { + function getAccountActiveVotesForGroup(address group, address account) public view returns (uint256) { uint256 numerator = votes.active.numerators[group][account].mul(votes.total.getValue(group)); uint256 denominator = votes.total.getValue(group); return numerator.div(denominator); } - function getAccountTotalVotesForGroup(address group, address account) external view returns (uint256) { - uint256 pending = getAccountPendingVotesForGroup(group, account) + function getAccountTotalVotesForGroup(address group, address account) public view returns (uint256) { + uint256 pending = getAccountPendingVotesForGroup(group, account); uint256 active = getAccountActiveVotesForGroup(group, account); return pending.add(active); } @@ -392,7 +392,7 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingLockedGold { function getActiveVotesDelta(address group, address account, uint256 value) private { uint256 total = votes.totals.getValue(group); // Preserve delta * total = value * denominator - uint256 delta = value.mul(active.denominators[group]).div(total); + uint256 delta = value.mul(votes.active.denominators[group]).div(total); } /** @@ -463,33 +463,26 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingLockedGold { * @return The list of elected validators. * @dev See https://en.wikipedia.org/wiki/D%27Hondt_method#Allocation for more information. */ - /* solhint-disable code-complexity */ function electValidators() external view returns (address[] memory) { // Only members of these validator groups are eligible for election. uint256 maxNumElectionGroups = Math.min(maxElectableValidators, votes.totals.list.numElements); uint256 requiredVotes = electabilityThreshold.multiply(FixidityLib.newFixed(totalVotes)).fromFixed(); address[] memory electionGroups = votes.totals.list.headN(maxNumElectionGroups, requiredVotes); + uint256[] memory numMembers = getNumGroupMembers(electionGroups); // Holds the number of members elected for each of the eligible validator groups. uint256[] memory numMembersElected = new uint256[](electionGroups.length); uint256 totalNumMembersElected = 0; - bool memberElectedInRound = true; // Assign a number of seats to each validator group. - while (totalNumMembersElected < maxElectableValidators && memberElectedInRound) { - memberElectedInRound = false; + while (totalNumMembersElected < maxElectableValidators) { uint256 groupIndex = 0; - FixidityLib.Fraction memory maxN = FixidityLib.wrap(0); - for (uint256 i = 0; i < electionGroups.length; i = i.add(1)) { - bool isWinningestGroupInRound = false; - (maxN, isWinningestGroupInRound) = dHondt(maxN, electionGroups[i], numMembersElected[i]); - if (isWinningestGroupInRound) { - memberElectedInRound = true; - groupIndex = i; - } - } + bool memberElected = false; + (groupIndex, memberElected) = dHondt(electionGroups, numMembers, numMembersElected); - if (memberElectedInRound) { + if (memberElected) { numMembersElected[groupIndex] = numMembersElected[groupIndex].add(1); totalNumMembersElected = totalNumMembersElected.add(1); + } else { + break; } } require(totalNumMembersElected >= minElectableValidators); @@ -497,46 +490,48 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingLockedGold { address[] memory electedValidators = new address[](totalNumMembersElected); totalNumMembersElected = 0; for (uint256 i = 0; i < electionGroups.length; i = i.add(1)) { - address[] memory electedGroupMembers = groups[electionGroups[i]].members.headN( + // We use the validating delegate if one is set. + address[] memory electedGroupValidators = getTopValidatorsFromGroup( + electionGroups[i], numMembersElected[i] ); - for (uint256 j = 0; j < electedGroupMembers.length; j = j.add(1)) { - // We use the validating delegate if one is set. - electedValidators[totalNumMembersElected] = getValidatorFromAccount(electedGroupMembers[j]); + for (uint256 j = 0; j < electedGroupValidators.length; j = j.add(1)) { + electedValidators[totalNumMembersElected] = electedGroupValidators[j]; totalNumMembersElected = totalNumMembersElected.add(1); } } return electedValidators; } - /* solhint-enable code-complexity */ /** - * @notice Runs D'Hondt for a validator group. - * @param maxN The maximum number of votes per elected seat for a group in this round. - * @param groupAddress The address of the validator group. - * @param numMembersElected The number of members elected so far for this group. + * @notice Runs a round of the D'Hondt algorithm. + * @param electionGroups The addresses of the validator groups in the election. + * @param numMembers The number of members in each group. + * @param numMembersElected The number of members elected in each group up to this point. * @dev See https://en.wikipedia.org/wiki/D%27Hondt_method#Allocation for more information. - * @return The new `maxN` and whether or not the group should win a seat in this round thus far. + * @return Whether or not a group elected a member, and the index of the group if so. */ - function dHondt( - FixidityLib.Fraction memory maxN, - address groupAddress, - uint256 numMembersElected - ) + function dHondt(address[] memory electionGroups, uint256[] memory numMembers, uint256[] memory numMembersElected) private view - returns (FixidityLib.Fraction memory, bool) + returns (uint256, bool) { - ValidatorGroup storage group = groups[groupAddress]; - // Only consider groups with members left to be elected. - if (group.members.numElements > numMembersElected) { - FixidityLib.Fraction memory n = FixidityLib.newFixed(votes.getValue(groupAddress)).divide( - FixidityLib.newFixed(numMembersElected.add(1)) - ); - if (n.gt(maxN)) { - return (n, true); + bool memberElected = false; + uint256 groupIndex = 0; + FixidityLib.Fraction memory maxN = FixidityLib.wrap(0); + for (uint256 i = 0; i < electionGroups.length; i = i.add(1)) { + address group = electionGroups[i]; + // Only consider groups with members left to be elected. + if (numMembers[i] > numMembersElected[i]) { + FixidityLib.Fraction memory n = FixidityLib.newFixed(votes.totals.getValue(group)).divide( + FixidityLib.newFixed(numMembersElected[i].add(1)) + ); + if (n.gt(maxN)) { + groupIndex = i; + memberElected = true; + } } } - return (maxN, false); + return (groupIndex, memberElected); } } diff --git a/packages/protocol/contracts/governance/Governance.sol b/packages/protocol/contracts/governance/Governance.sol index 343b5436405..bb145035db2 100644 --- a/packages/protocol/contracts/governance/Governance.sol +++ b/packages/protocol/contracts/governance/Governance.sol @@ -44,14 +44,20 @@ contract Governance is IGovernance, Ownable, Initializable, UsingLockedGold, Ree Yes } + struct UpvoteRecord { + uint256 proposalId; + uint256 weight; + } + struct VoteRecord { VoteValue value; uint256 proposalId; + uint256 weight; } struct Voter { // Key of the proposal voted for in the proposal queue - uint256 upvotedProposal; + UpvoteRecord upvote; uint256 mostRecentReferendumProposal; // Maps a `dequeued` index to a voter's vote record. mapping(uint256 => VoteRecord) referendumVotes; @@ -423,7 +429,6 @@ contract Governance is IGovernance, Ownable, Initializable, UsingLockedGold, Ree returns (bool) { address account = getAccountFromVoter(msg.sender); - require(!isVotingFrozen(account)); // TODO(asa): When upvoting a proposal that will get dequeued, should we let the tx succeed // and return false? dequeueProposalsIfReady(); @@ -436,20 +441,20 @@ contract Governance is IGovernance, Ownable, Initializable, UsingLockedGold, Ree } Voter storage voter = voters[account]; // We can upvote a proposal in the queue if we're not already upvoting a proposal in the queue. - uint256 weight = getAccountWeight(account); + uint256 weight = getAccountTotalLockedGold(account); require( isQueued(proposalId) && - (voter.upvotedProposal == 0 || !queue.contains(voter.upvotedProposal)) && + (voter.upvote.proposalId == 0 || !queue.contains(voter.upvote.proposalId)) && weight > 0 ); - uint256 upvotes = queue.getValue(proposalId).add(uint256(weight)); + uint256 upvotes = queue.getValue(proposalId).add(weight); queue.update( proposalId, upvotes, lesser, greater ); - voter.upvotedProposal = proposalId; + voter.upvote = UpvoteRecord(proposalId, weight); emit ProposalUpvoted(proposalId, account, weight); return true; } @@ -474,7 +479,7 @@ contract Governance is IGovernance, Ownable, Initializable, UsingLockedGold, Ree dequeueProposalsIfReady(); address account = getAccountFromVoter(msg.sender); Voter storage voter = voters[account]; - uint256 proposalId = voter.upvotedProposal; + uint256 proposalId = voter.upvote.proposalId; Proposal storage proposal = proposals[proposalId]; require(_proposalExists(proposal)); // If acting on an expired proposal, expire the proposal. @@ -485,18 +490,16 @@ contract Governance is IGovernance, Ownable, Initializable, UsingLockedGold, Ree queue.remove(proposalId); emit ProposalExpired(proposalId); } else { - uint256 weight = getAccountWeight(account); - require(weight > 0); queue.update( proposalId, - queue.getValue(proposalId).sub(weight), + queue.getValue(proposalId).sub(voter.upvote.weight), lesser, greater ); - emit ProposalUpvoteRevoked(proposalId, account, weight); + emit ProposalUpvoteRevoked(proposalId, account, voter.upvote.weight); } } - voter.upvotedProposal = 0; + voter.upvote = UpvoteRecord(0, 0); return true; } @@ -541,7 +544,6 @@ contract Governance is IGovernance, Ownable, Initializable, UsingLockedGold, Ree returns (bool) { address account = getAccountFromVoter(msg.sender); - require(!isVotingFrozen(account)); dequeueProposalsIfReady(); Proposal storage proposal = proposals[proposalId]; require(_proposalExists(proposal) && dequeued[index] == proposalId); @@ -551,7 +553,7 @@ contract Governance is IGovernance, Ownable, Initializable, UsingLockedGold, Ree } ProposalStage stage = _getDequeuedProposalStage(proposal.timestamp); Voter storage voter = voters[account]; - uint256 weight = getAccountWeight(account); + uint256 weight = getAccountTotalLockedGold(account); require( proposal.approved && stage == ProposalStage.Referendum && @@ -562,11 +564,11 @@ contract Governance is IGovernance, Ownable, Initializable, UsingLockedGold, Ree // If we've already voted on this proposal, subtract the previous vote. if (voteRecord.proposalId == proposalId) { if (voteRecord.value == VoteValue.Abstain) { - proposal.votes.abstain = proposal.votes.abstain.sub(weight); + proposal.votes.abstain = proposal.votes.abstain.sub(voteRecord.weight); } else if (voteRecord.value == VoteValue.Yes) { - proposal.votes.yes = proposal.votes.yes.sub(weight); + proposal.votes.yes = proposal.votes.yes.sub(voteRecord.weight); } else if (voteRecord.value == VoteValue.No) { - proposal.votes.no = proposal.votes.no.sub(weight); + proposal.votes.no = proposal.votes.no.sub(voteRecord.weight); } } @@ -578,8 +580,7 @@ contract Governance is IGovernance, Ownable, Initializable, UsingLockedGold, Ree } else if (value == VoteValue.No) { proposal.votes.no = proposal.votes.no.add(weight); } - voteRecord.proposalId = proposalId; - voteRecord.value = value; + voteRecord = VoteRecord(value, proposalId, weight); if (proposal.timestamp > voter.mostRecentReferendumProposal) { voter.mostRecentReferendumProposal = proposalId; } diff --git a/packages/protocol/contracts/governance/LockedGold.sol b/packages/protocol/contracts/governance/LockedGold.sol index 91afae461a7..ac67a2cd6e5 100644 --- a/packages/protocol/contracts/governance/LockedGold.sol +++ b/packages/protocol/contracts/governance/LockedGold.sol @@ -5,15 +5,11 @@ import "openzeppelin-solidity/contracts/math/SafeMath.sol"; import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; import "./interfaces/ILockedGold.sol"; -import "./interfaces/IGovernance.sol"; -import "./interfaces/IValidators.sol"; import "../common/Initializable.sol"; import "../common/UsingRegistry.sol"; -import "../common/FixidityLib.sol"; import "../common/interfaces/IERC20Token.sol"; import "../common/Signatures.sol"; -import "../common/FractionUtil.sol"; contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistry { @@ -81,6 +77,7 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr uint8 v, bytes32 r, bytes32 s + ) external nonReentrant { @@ -95,6 +92,7 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr uint8 v, bytes32 r, bytes32 s + ) external nonReentrant { @@ -111,19 +109,25 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr function lock(uint256 value) external nonReentrant { require(isAccount(msg.sender)); require(msg.value == value && value > 0); - incrementNonvotingAccountBalance(msg.sender, value) + _incrementNonvotingAccountBalance(msg.sender, value); emit GoldLocked(msg.sender, value); } - function incrementNonvotingAccountBalance(address account, uint256 value) private { - Account storage account = accounts[account]; - account.gold.nonvoting = account.gold.nonvoting.add(value); + function incrementNonvotingAccountBalance(address account, uint256 value) external onlyRegisteredContract('Election', msg.sender) { + _incrementNonvotingAccountBalance(account, value); + } + + function decrementNonvotingAccountBalance(address account, uint256 value) external onlyRegisteredContract('Election', msg.sender) { + _decrementNonvotingAccountBalance(account, value); + } + + function _incrementNonvotingAccountBalance(address account, uint256 value) private { + accounts[account].gold.nonvoting = accounts[account].gold.nonvoting.add(value); totalNonvoting = totalNonvoting.add(value); } function decrementNonvotingAccountBalance(address account, uint256 value) private { - Account storage account = accounts[account]; - account.gold.nonvoting = account.gold.nonvoting.sub(value); + accounts[account].gold.nonvoting = accounts[account].gold.nonvoting.sub(value); totalNonvoting = totalNonvoting.sub(value); } @@ -142,24 +146,25 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr emit GoldUnlocked(msg.sender, value, available); } - function relock(uint256 value, uint256 index) external nonReentrant { + // TODO(asa): Allow partial relock + function relock(uint256 index) external nonReentrant { require(isAccount(msg.sender)); Account storage account = accounts[msg.sender]; - require(index < account.gold.unlocking.length); - uint256 value = account.gold.unlocking[index].value; - incrementNonvotingAccountBalance(msg.sender, value); - deletePendingWithdrawal(account.gold.unlocking, index); + require(index < account.balances.pendingWithdrawals.length); + uint256 value = account.balances.pendingWithdrawals[index].value; + _incrementNonvotingAccountBalance(msg.sender, value); + deletePendingWithdrawal(account.balances.pendingWithdrawals, index); emit GoldLocked(msg.sender, value); } - function withdraw(uint256 value, uint256 index) external nonReentrant { + function withdraw(uint256 index) external nonReentrant { require(isAccount(msg.sender)); Account storage account = accounts[msg.sender]; - require(index < account.gold.unlocking.length); - PendingWithdrawal memory unlocking = account.gold.unlocking[index]; - require(now >= unlocking.available); - uint256 value = unlocking.value; - deletePendingWithdrawal(account.gold.unlocking, index); + require(index < account.balances.pendingWithdrawals.length); + PendingWithdrawal memory pendingWithdrawal = account.balances.pendingWithdrawals[index]; + require(now >= pendingWithdrawal.available); + uint256 value = pendingWithdrawal.value; + deletePendingWithdrawal(account.balances.pendingWithdrawals, index); IERC20Token goldToken = IERC20Token(registry.getAddressFor(GOLD_TOKEN_REGISTRY_ID)); require(goldToken.transfer(msg.sender, value)); emit GoldWithdrawn(msg.sender, value); @@ -181,7 +186,7 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr // TODO(asa): Dedup /** - * @notice Returns the account associated with the `voter` address. + * @notice Returns the account associated with `accountOrVoter`. * @param accountOrVoter The address of the account or authorized voter. * @dev Fails if the `accountOrVoter` is not an account or authorized voter. * @return The associated account. @@ -207,7 +212,7 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr } /** - * @notice Returns the account associated with the `validator` address. + * @notice Returns the account associated with `accountOrValidator`. * @param accountOrValidator The address of the account or authorized validator. * @dev Fails if the `accountOrValidator` is not an account or authorized validator. * @return The associated account. diff --git a/packages/protocol/contracts/governance/UsingLockedGold.sol b/packages/protocol/contracts/governance/UsingLockedGold.sol index 4b6ec028f39..c0570fcd7f7 100644 --- a/packages/protocol/contracts/governance/UsingLockedGold.sol +++ b/packages/protocol/contracts/governance/UsingLockedGold.sol @@ -10,26 +10,23 @@ import "../common/UsingRegistry.sol"; */ contract UsingLockedGold is UsingRegistry { /** - * @notice Returns whether or not an account's voting power is frozen. - * @param account The address of the account. - * @return Whether or not the account's voting power is frozen. - * @dev Frozen accounts can retract existing votes but not make future votes. + * @notice Returns the account associated with `accountOrVoter`. + * @param accountOrVoter The address of the account or authorized voter. + * @dev Fails if the `accountOrVoter` is not an account or authorized voter. + * @return The associated account. */ - function isVotingFrozen(address account) internal view returns (bool) { - return getLockedGold().isVotingFrozen(account); + function getAccountFromVoter(address accountOrVoter) internal view returns (address) { + return getLockedGold().getAccountFromVoter(accountOrVoter); } /** - * @notice Returns the account associated with the provided account or voting delegate. - * @param accountOrDelegate The address of the account or voting delegate. - * @dev Fails if the `accountOrDelegate` is a non-voting delegate. + * @notice Returns the account associated with `accountOrValidator`. + * @param accountOrValidator The address of the account or authorized validator. + * @dev Fails if the `accountOrValidator` is not an account or authorized validator. * @return The associated account. */ - function getAccountFromVoter(address accountOrDelegate) internal view returns (address) { - return getLockedGold().getAccountFromDelegateAndRole( - accountOrDelegate, - ILockedGold.DelegateRole.Voting - ); + function getAccountFromValidator(address accountOrValidator) internal view returns (address) { + return getLockedGold().getAccountFromValidator(accountOrValidator); } /** @@ -38,51 +35,23 @@ contract UsingLockedGold is UsingRegistry { * @return The associated validator address. */ function getValidatorFromAccount(address account) internal view returns (address) { - return getLockedGold().getDelegateFromAccountAndRole( - account, - ILockedGold.DelegateRole.Validating - ); + return getLockedGold().getValidatorFromAccount(account); } - /** - * @notice Returns the account associated with the provided account or validating delegate. - * @param accountOrDelegate The address of the account or validating delegate. - * @dev Fails if the `accountOrDelegate` is a non-validating delegate. - * @return The associated account. - */ - function getAccountFromValidator(address accountOrDelegate) internal view returns (address) { - return getLockedGold().getAccountFromDelegateAndRole( - accountOrDelegate, - ILockedGold.DelegateRole.Validating - ); + function getTotalLockedGold() internal view returns (uint256) { + return getLockedGold().getTotalLockedGold(); } - /** - * @notice Returns voting weight for a particular account. - * @param account The address of the account. - * @return The voting weight of `account`. - */ - function getAccountWeight(address account) internal view returns (uint256) { - return getLockedGold().getAccountWeight(account); + function getAccountTotalLockedGold(address account) internal view returns (uint256) { + return getLockedGold().getAccountTotalLockedGold(account); } - /** - * @notice Returns the Locked Gold commitment value for particular account and notice period. - * @param account The address of the account. - * @param noticePeriod The notice period of the Locked Gold commitment. - * @return The value of the Locked Gold commitment. - */ - function getLockedCommitmentValue( - address account, - uint256 noticePeriod - ) - internal - view - returns (uint256) - { - uint256 value; - (value,) = getLockedGold().getLockedCommitment(account, noticePeriod); - return value; + function incrementNonvotingAccountBalance(address account, uint256 value) internal returns (bool) { + return getLockedGold().incrementNonvotingAccountBalance(account, value); + } + + function decrementNonvotingAccountBalance(address account, uint256 value) internal returns (bool) { + return getLockedGold().decrementNonvotingAccountBalance(account, value); } function getLockedGold() private view returns(ILockedGold) { diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index efd7525edb2..bde57cde9a4 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -10,7 +10,6 @@ import "./interfaces/IValidators.sol"; import "../common/Initializable.sol"; import "../common/FixidityLib.sol"; import "../common/linkedlists/AddressLinkedList.sol"; -import "../common/linkedlists/AddressSortedLinkedList.sol"; /** @@ -22,7 +21,6 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi using FixidityLib for FixidityLib.Fraction; using AddressLinkedList for LinkedList.List; - using AddressSortedLinkedList for SortedLinkedList.List; using SafeMath for uint256; using BytesLib for bytes; @@ -190,7 +188,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi function registerValidator( string calldata name, string calldata url, - bytes calldata publicKeysData, + bytes calldata publicKeysData ) external nonReentrant @@ -436,6 +434,31 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi return (group.name, group.url, group.members.getKeys()); } + function getNumGroupMembers(address account) public view returns (uint256) { + return groups[account].members.numElements; + } + + function getTopValidatorsFromGroup(address account, uint256 n) external view returns (address[]) { + address[] memory topAccounts = groups[account].members.list.headN(n); + address[] memory topValidators = new address[](n); + for (uint256 i = 0; i < n; i = i.add(1)) { + topValidators[i] = getValidatorFromAccount(topAccounts[i]); + } + return topValidators; + } + + function getNumGroupMembers(address[] accounts) external view returns (uint256) { + uint256[] memory numMembers = new uint256[](accounts.length); + for (uint256 i = 0; i < accounts.length; i = i.add(1)) { + numMembers[i] = getNumGroupMembers(accounts[i]); + } + return numMembers; + } + + function getNumRegisteredValidators() external view returns (uint256) { + return _validators.length; + } + /** * @notice Returns the Locked Gold requirements to register a validator or group. * @return The locked gold requirements to register a validator or group. From 721b9dafcbb3faab6a841fb7fb1c4b8ae88a9d9d Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Wed, 18 Sep 2019 20:25:27 -0700 Subject: [PATCH 003/149] More work --- .../contracts/governance/LockedGold.sol | 27 +++++++++++-------- .../contracts/governance/UsingLockedGold.sol | 2 +- .../contracts/governance/Validators.sol | 16 +++++------ 3 files changed, 25 insertions(+), 20 deletions(-) diff --git a/packages/protocol/contracts/governance/LockedGold.sol b/packages/protocol/contracts/governance/LockedGold.sol index ac67a2cd6e5..0db2beabd87 100644 --- a/packages/protocol/contracts/governance/LockedGold.sol +++ b/packages/protocol/contracts/governance/LockedGold.sol @@ -5,13 +5,14 @@ import "openzeppelin-solidity/contracts/math/SafeMath.sol"; import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; import "./interfaces/ILockedGold.sol"; +import "./UsingElection.sol"; import "../common/Initializable.sol"; import "../common/UsingRegistry.sol"; import "../common/interfaces/IERC20Token.sol"; import "../common/Signatures.sol"; -contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistry { +contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistry, UsingElection { // TODO(asa): How do adjust for updated requirements? // Have a refreshRequirements function validators and groups can call @@ -50,11 +51,15 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr mapping(address => Account) public accounts; // Maps voting and validating keys to the account that provided the authorization. mapping(address => address) public authorizations; - uint256 public nonvotingTotal; + uint256 public totalNonvoting; uint256 public unlockingPeriod; event VoterAuthorized(address indexed account, address voter); event ValidatorAuthorized(address indexed account, address validator); + event GoldLocked(address indexed account, uint256 value); + event GoldUnlocked(address indexed account, uint256 value, uint256 available); + event GoldWithdrawn(address indexed account, uint256 value); + event AccountMustMaintainSet(address indexed account, uint256 value, uint256 timestamp); function initialize(address registryAddress) external initializer { _transferOwnership(msg.sender); @@ -126,7 +131,7 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr totalNonvoting = totalNonvoting.add(value); } - function decrementNonvotingAccountBalance(address account, uint256 value) private { + function _decrementNonvotingAccountBalance(address account, uint256 value) private { accounts[account].gold.nonvoting = accounts[account].gold.nonvoting.sub(value); totalNonvoting = totalNonvoting.sub(value); } @@ -140,7 +145,7 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr now >= requirement.timestamp || getAccountTotalLockedGold(msg.sender).sub(value) >= requirement.value ); - decrementNonvotingAccountBalance(msg.sender, value); + _decrementNonvotingAccountBalance(msg.sender, value); uint256 available = now.add(unlockingPeriod); account.balances.pendingWithdrawals.push(PendingWithdrawal(value, available)); emit GoldUnlocked(msg.sender, value, available); @@ -192,7 +197,7 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr * @return The associated account. */ function getAccountFromVoter(address accountOrVoter) public view returns (address) { - address authorizingAccount = authorizations[voter]; + address authorizingAccount = authorizations[accountOrVoter]; if (authorizingAccount != address(0)) { require(accounts[authorizingAccount].authorizations.voter == accountOrVoter); return authorizingAccount; @@ -203,12 +208,12 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr } function getTotalLockedGold() public view returns (uint256) { - return nonvotingTotal.add(getTotalVotes()); + return totalNonvoting.add(getElection().totalVotes()); } function getAccountTotalLockedGold(address account) public view returns (uint256) { uint256 total = accounts[account].balances.nonvoting; - return total.add(getAccountTotalVotes(account)); + return total.add(getElection().getAccountTotalVotes(account)); } /** @@ -218,13 +223,13 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr * @return The associated account. */ function getAccountFromValidator(address accountOrValidator) public view returns (address) { - address authorizingAccount = authorizations[validator]; + address authorizingAccount = authorizations[accountOrValidator]; if (authorizingAccount != address(0)) { - require(accounts[authorizingAccount].authorizations.validator == accountOrVoter); + require(accounts[authorizingAccount].authorizations.validator == accountOrValidator); return authorizingAccount; } else { - require(isAccount(accountOrVoter)); - return accountOrVoter; + require(isAccount(accountOrValidator)); + return accountOrValidator; } } diff --git a/packages/protocol/contracts/governance/UsingLockedGold.sol b/packages/protocol/contracts/governance/UsingLockedGold.sol index c0570fcd7f7..7f29f199155 100644 --- a/packages/protocol/contracts/governance/UsingLockedGold.sol +++ b/packages/protocol/contracts/governance/UsingLockedGold.sol @@ -54,7 +54,7 @@ contract UsingLockedGold is UsingRegistry { return getLockedGold().decrementNonvotingAccountBalance(account, value); } - function getLockedGold() private view returns(ILockedGold) { + function getLockedGold() internal view returns(ILockedGold) { return ILockedGold(registry.getAddressForOrDie(LOCKED_GOLD_REGISTRY_ID)); } } diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index bde57cde9a4..2c2efa48dbd 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -130,8 +130,8 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi { _transferOwnership(msg.sender); setRegistry(registryAddress); - registrationRequirement.group = groupRequirement; - registrationRequirement.validator = validatorRequirement; + registrationRequirements.group = groupRequirement; + registrationRequirements.validator = validatorRequirement; maxGroupSize = _maxGroupSize; } @@ -210,7 +210,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi Validator memory validator = Validator(name, url, publicKeysData, address(0)); validators[account] = validator; _validators.push(account); - setAccountMustMaintain(account, requirements.validator, MAX_INT); + getLockedGold().setAccountMustMaintain(account, registrationRequirements.validator, MAX_INT); emit ValidatorRegistered(account, name, url, publicKeysData); return true; } @@ -241,7 +241,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi } delete validators[account]; deleteElement(_validators, account, index); - setAccountMustMaintain(account, requirements.validator, now.add(deregisterPeriods.validator)); + getLockedGold().setAccountMustMaintain(account, registrationRequirements.validator, now.add(deregisterPeriods.validator)); emit ValidatorDeregistered(account); return true; } @@ -311,7 +311,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi group.addMember(members[i]); } _groups.push(account); - setAccountMustMaintain(account, requirements.group, MAX_INT); + getLockedGold().setAccountMustMaintain(account, requirements.group, MAX_INT); emit ValidatorGroupRegistered(account, name, url); return true; } @@ -328,7 +328,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi require(isValidatorGroup(account) && groups[account].members.numElements == 0); delete groups[account]; deleteElement(_groups, account, index); - setAccountMustMaintain(account, requirements.group, now.add(deregisterPeriods.group)); + getLockedGold().setAccountMustMaintain(account, requirements.group, now.add(deregisterPeriods.group)); emit ValidatorGroupDeregistered(account); return true; } @@ -438,7 +438,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi return groups[account].members.numElements; } - function getTopValidatorsFromGroup(address account, uint256 n) external view returns (address[]) { + function getTopValidatorsFromGroup(address account, uint256 n) external view returns (address[] memory) { address[] memory topAccounts = groups[account].members.list.headN(n); address[] memory topValidators = new address[](n); for (uint256 i = 0; i < n; i = i.add(1)) { @@ -447,7 +447,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi return topValidators; } - function getNumGroupMembers(address[] accounts) external view returns (uint256) { + function getNumGroupMembers(address[] calldata accounts) external view returns (uint256) { uint256[] memory numMembers = new uint256[](accounts.length); for (uint256 i = 0; i < accounts.length; i = i.add(1)) { numMembers[i] = getNumGroupMembers(accounts[i]); From 17a624e0bb80d02944f0870d29a6fa5ef54a64b6 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Thu, 19 Sep 2019 16:50:07 -0700 Subject: [PATCH 004/149] More changes --- .../contracts/common/UsingRegistry.sol | 20 ++- .../contracts/governance/Election.sol | 165 +++++++++--------- .../contracts/governance/Governance.sol | 14 +- .../contracts/governance/LockedGold.sol | 9 +- .../contracts/governance/UsingLockedGold.sol | 60 ------- .../contracts/governance/Validators.sol | 128 +++++++++----- .../governance/interfaces/ILockedGold.sol | 24 +-- .../governance/interfaces/IValidators.sol | 6 +- .../governance/test/MockLockedGold.sol | 98 ----------- .../contracts/identity/Attestations.sol | 13 +- 10 files changed, 203 insertions(+), 334 deletions(-) delete mode 100644 packages/protocol/contracts/governance/UsingLockedGold.sol diff --git a/packages/protocol/contracts/common/UsingRegistry.sol b/packages/protocol/contracts/common/UsingRegistry.sol index d5a599a5fc3..5a19dcef961 100644 --- a/packages/protocol/contracts/common/UsingRegistry.sol +++ b/packages/protocol/contracts/common/UsingRegistry.sol @@ -4,6 +4,10 @@ import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; import "./interfaces/IRegistry.sol"; +import "../governance/interfaces/IElection.sol"; +import "../governance/interfaces/ILockedGold.sol"; +import "../governance/interfaces/IValidators.sol"; + // Ideally, UsingRegistry should inherit from Initializable and implement initialize() which calls // setRegistry(). TypeChain currently has problems resolving overloaded functions, so this is not // possible right now. @@ -30,8 +34,8 @@ contract UsingRegistry is Ownable { IRegistry public registry; - modifier onlyRegisteredContract(bytes32 identifierHash, address sender) { - require(registry.getAddressForOrDie(identifierHash) == sender); + modifier onlyRegisteredContract(bytes32 identifierHash) { + require(registry.getAddressForOrDie(identifierHash) == msg.sender); _; } /** @@ -42,4 +46,16 @@ contract UsingRegistry is Ownable { registry = IRegistry(registryAddress); emit RegistrySet(registryAddress); } + + function getElection() internal view returns (IElection) { + return IElection(registry.getAddressForOrDie(ELECTION_REGISTRY_ID)); + } + + function getLockedGold() internal view returns(ILockedGold) { + return ILockedGold(registry.getAddressForOrDie(LOCKED_GOLD_REGISTRY_ID)); + } + + function getValidators() internal view returns(IValidators) { + return IValidators(registry.getAddressForOrDie(VALIDATORS_REGISTRY_ID)); + } } diff --git a/packages/protocol/contracts/governance/Election.sol b/packages/protocol/contracts/governance/Election.sol index c86888bee7e..c4a07f2a699 100644 --- a/packages/protocol/contracts/governance/Election.sol +++ b/packages/protocol/contracts/governance/Election.sol @@ -5,33 +5,39 @@ import "openzeppelin-solidity/contracts/math/SafeMath.sol"; import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; import "openzeppelin-solidity/contracts/utils/ReentrancyGuard.sol"; -import "./UsingLockedGold.sol"; -import "./UsingValidators.sol"; import "./interfaces/IValidators.sol"; import "../common/Initializable.sol"; import "../common/FixidityLib.sol"; import "../common/linkedlists/AddressSortedLinkedList.sol"; +import "../common/UsingRegistry.sol"; -contract Election is Ownable, ReentrancyGuard, Initializable, UsingLockedGold, UsingValidators { +contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { using AddressSortedLinkedList for SortedLinkedList.List; using FixidityLib for FixidityLib.Fraction; using SafeMath for uint256; + // We need some way of keeping track of the number of active/pending votes per group, so that + // we know how to adjust `activeVotes`. + // Pending votes are those for which no following elections have been held. // These votes have yet to contribute to the election of validators and thus do not accrue // rewards. struct PendingVotes { + // Maps groups to total pending voting balance. + mapping(address => uint256) total; // Maps groups to accounts to pending voting balance. - mapping(address => mapping(address => uint256)) value; + mapping(address => mapping(address => uint256)) balances; // Maps groups to accounts to timestamp of the account's most recent vote for the group. - mapping(address => mapping(address => uint256)) timestamp; + mapping(address => mapping(address => uint256)) timestamps; } // Active votes are those for which at least one following election has been held. // These votes have contributed to the election of validators and thus accrue rewards. struct ActiveVotes { + // Maps groups to total active voting balance. + mapping(address => uint256) total; // Maps groups to accounts to the numerator of the account's fraction of the group's // total active votes. mapping(address => mapping(address => uint256)) numerators; @@ -39,20 +45,26 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingLockedGold, U mapping(address => uint256) denominators; } + + struct TotalVotes { + // The total number of votes cast. + uint256 total; + // A list of eligible ValidatorGroups sorted by total votes. + SortedLinkedList.List eligible; + } + struct Votes { PendingVotes pending; ActiveVotes active; - // A sorted list of ValidatorGroups by total votes. - SortedLinkedList.List totals; + TotalVotes total; // Maps an account to the list of groups it's voting for. mapping(address => address[]) lists; } - Votes public votes; + Votes private votes; uint256 public minElectableValidators; uint256 public maxElectableValidators; uint256 public maxVotesPerAccount; - uint256 public totalVotes; FixidityLib.Fraction public electabilityThreshold; event MinElectableValidatorsSet( @@ -179,8 +191,9 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingLockedGold, U nonReentrant returns (bool) { + require(votes.total.eligible.contains(group)); require(0 < value && value <= getNumVotesReceivable(group)); - address account = getAccountFromVoter(msg.sender); + address account = getLockedGold().getAccountFromVoter(msg.sender); address[] storage list = votes.lists[account]; require(list.length < maxVotesPerAccount); for (uint256 i = 0; i < list.length; i = i.add(1)) { @@ -188,8 +201,8 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingLockedGold, U } list.push(group); incrementPendingVotes(group, account, value); - incrementTotalVotes(group, value); - decrementNonvotingAccountBalance(account, value); + incrementTotalVotes(group, value, lesser, greater); + getLockedGold().decrementNonvotingAccountBalance(account, value); emit ValidatorGroupVoteCast(account, group, value); return true; } @@ -200,12 +213,12 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingLockedGold, U * @return True upon success. */ function activate(address group) external nonReentrant returns (bool) { - address account = getAccountFromVoter(msg.sender); + address account = getLockedGold().getAccountFromVoter(msg.sender); PendingVotes storage pending = votes.pending; - uint256 pendingValue = pending.values[group][account]; - require(0 < pendingValue); - decrementPendingVotes(group, account, pendingValue); - incrementActiveVotes(group, account, pendingValue); + uint256 value = pending.balances[group][account]; + require(0 < value); + decrementPendingVotes(group, account, value); + incrementActiveVotes(group, account, value); } /** @@ -232,11 +245,11 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingLockedGold, U returns (bool) { require(group != address(0)); - address account = getAccountFromVoter(msg.sender); + address account = getLockedGold().getAccountFromVoter(msg.sender); require(0 < value && value <= getAccountPendingVotesForGroup(group, account)); decrementPendingVotes(group, account, value); decrementTotalVotes(group, value, lesser, greater); - incrementNonvotingAccountBalance(account, value); + getLockedGold().incrementNonvotingAccountBalance(account, value); if (getAccountTotalVotesForGroup(group, account) == 0) { deleteElement(votes.lists[account], group, index); } @@ -268,11 +281,11 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingLockedGold, U returns (bool) { require(group != address(0)); - address account = getAccountFromVoter(msg.sender); + address account = getLockedGold().getAccountFromVoter(msg.sender); require(0 < value && value <= getAccountActiveVotesForGroup(group, account)); decrementActiveVotes(group, account, value); decrementTotalVotes(group, value, lesser, greater); - incrementNonvotingAccountBalance(account, value); + getLockedGold().incrementNonvotingAccountBalance(account, value); if (getAccountTotalVotesForGroup(group, account) == 0) { deleteElement(votes.lists[account], group, index); } @@ -290,12 +303,12 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingLockedGold, U } function getAccountPendingVotesForGroup(address group, address account) public view returns (uint256) { - return votes.pending.values[group][account]; + return votes.pending.balances[group][account]; } function getAccountActiveVotesForGroup(address group, address account) public view returns (uint256) { - uint256 numerator = votes.active.numerators[group][account].mul(votes.total.getValue(group)); - uint256 denominator = votes.total.getValue(group); + uint256 numerator = votes.active.numerators[group][account].mul(votes.active.total[group]); + uint256 denominator = votes.active.denominators[group]; return numerator.div(denominator); } @@ -322,15 +335,12 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingLockedGold, U ) private { - if (votes.contains(group)) { - votes.totals.update(group, votes.getValue(group).add(value), lesser, greater); - } else { - votes.totals.insert(group, value, lesser, greater); - } - totalVotes = totalVotes.add(value); + require(votes.total.eligible.contains(group)); + uint256 newVoteTotal = votes.total.eligible.getValue(group).add(value); + votes.total.eligible.update(group, newVoteTotal, lesser, greater); + votes.total.total = votes.total.total.add(value); } - /** * @notice Decrements the number of total votes for `group` by `value`. * @param group The validator group whose vote total should be decremented. @@ -348,51 +358,60 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingLockedGold, U ) private { - if (votes.totals.contains(group)) { - uint256 newVoteTotal = votes.totals.getValue(group).sub(value); - if (newVoteTotal > 0) { - votes.totals.update(group, newVoteTotal, lesser, greater); - } else { - // Groups receiving no votes are not electable. - votes.totals.remove(group); - } + if (votes.total.eligible.contains(group)) { + uint256 newVoteTotal = votes.total.eligible.getValue(group).sub(value); + votes.total.eligible.update(group, newVoteTotal, lesser, greater); } - totalVotes = totalVotes.sub(value); + votes.total.total = votes.total.total.add(value); } - function incrementActiveVotes(address group, address account, uint256 value) private { - uint256 delta = getActiveVotesDelta(group, account, value); - ActiveVotes storage active = votes.active; - active.denominators[group] = active.denominators[group].add(delta); - active.numerators[group][account] = active.numerators[group][account].add(delta); + function markGroupIneligible(address group) external onlyRegisteredContract('Validators') { + votes.total.eligible.remove(group); } - function decrementActiveVotes(address group, address account, uint256 value) private { - uint256 delta = getActiveVotesDelta(group, account, value); - ActiveVotes storage active = votes.active; - active.denominators[group] = active.denominators[group].sub(delta); - active.numerators[group][account] = active.numerators[group][account].sub(delta); + function markGroupEligible(address group, address lesser, address greater) external { + require(!votes.total.eligible.contains(group)); + require(getValidators().getNumGroupMembers(group) > 0); + uint256 value = votes.pending.total[group].add(votes.active.total[group]); + votes.total.eligible.insert(group, value, lesser, greater); } function incrementPendingVotes(address group, address account, uint256 value) private { PendingVotes storage pending = votes.pending; - pending.values[group][account] = pending.values[group][account].add(value); + pending.balances[group][account] = pending.balances[group][account].add(value); pending.timestamps[group][account] = now; + pending.total[group] = pending.total[group].add(value); } function decrementPendingVotes(address group, address account, uint256 value) private { PendingVotes storage pending = votes.pending; - uint256 newValue = pending.values[group][account].sub(value); - pending.values[group][account] = newValue; + uint256 newValue = pending.balances[group][account].sub(value); + pending.balances[group][account] = newValue; if (newValue == 0) { pending.timestamps[group][account] = 0; } + pending.total[group] = pending.total[group].sub(value); } - function getActiveVotesDelta(address group, address account, uint256 value) private { - uint256 total = votes.totals.getValue(group); + function incrementActiveVotes(address group, address account, uint256 value) private { + uint256 delta = getActiveVotesDelta(group, value); + ActiveVotes storage active = votes.active; + active.numerators[group][account] = active.numerators[group][account].add(delta); + active.denominators[group] = active.denominators[group].add(delta); + active.total[group] = active.total[group].add(value); + } + + function decrementActiveVotes(address group, address account, uint256 value) private { + uint256 delta = getActiveVotesDelta(group, value); + ActiveVotes storage active = votes.active; + active.numerators[group][account] = active.numerators[group][account].sub(delta); + active.denominators[group] = active.denominators[group].sub(delta); + active.total[group] = active.total[group].sub(value); + } + + function getActiveVotesDelta(address group, uint256 value) private returns (uint256) { // Preserve delta * total = value * denominator - uint256 delta = value.mul(votes.active.denominators[group]).div(total); + return value.mul(votes.active.denominators[group]).div(votes.active.total[group]); } /** @@ -410,8 +429,8 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingLockedGold, U } function getNumVotesReceivable(address group) public view returns (uint256) { - uint256 numerator = getNumGroupMembers(group).add(1).mul(getTotalLockedGold()); - uint256 denominator = Math.min(maxElectableValidators, getNumRegisteredValidators()); + uint256 numerator = getValidators().getNumGroupMembers(group).add(1).mul(votes.total.total); + uint256 denominator = Math.min(maxElectableValidators, getValidators().getNumRegisteredValidators()); return numerator.div(denominator); } @@ -440,23 +459,6 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingLockedGold, U return numberValidators; } - /** - * @notice Returns electable validator group addresses and their vote totals. - * @return Electable validator group addresses and their vote totals. - */ - function getValidatorGroupVotes() external view returns (address[] memory, uint256[] memory) { - return votes.getElements(); - } - - /** - * @notice Returns the number of votes a particular validator group has received. - * @param group The account that registered the validator group. - * @return The number of votes a particular validator group has received. - */ - function getVotesReceived(address group) external view returns (uint256) { - return votes.getValue(group); - } - /** * @notice Returns a list of elected validators with seats allocated to groups via the D'Hondt * method. @@ -465,10 +467,11 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingLockedGold, U */ function electValidators() external view returns (address[] memory) { // Only members of these validator groups are eligible for election. - uint256 maxNumElectionGroups = Math.min(maxElectableValidators, votes.totals.list.numElements); - uint256 requiredVotes = electabilityThreshold.multiply(FixidityLib.newFixed(totalVotes)).fromFixed(); - address[] memory electionGroups = votes.totals.list.headN(maxNumElectionGroups, requiredVotes); - uint256[] memory numMembers = getNumGroupMembers(electionGroups); + uint256 maxNumElectionGroups = Math.min(maxElectableValidators, votes.total.eligible.list.numElements); + // uint256 requiredVotes = electabilityThreshold.multiply(FixidityLib.newFixed(votes.total.total)).fromFixed(); + // TODO(asa): Filter by > requiredVotes + address[] memory electionGroups = votes.total.eligible.list.headN(maxNumElectionGroups); + uint256[] memory numMembers = getValidators().getNumGroupMembers(electionGroups); // Holds the number of members elected for each of the eligible validator groups. uint256[] memory numMembersElected = new uint256[](electionGroups.length); uint256 totalNumMembersElected = 0; @@ -491,7 +494,7 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingLockedGold, U totalNumMembersElected = 0; for (uint256 i = 0; i < electionGroups.length; i = i.add(1)) { // We use the validating delegate if one is set. - address[] memory electedGroupValidators = getTopValidatorsFromGroup( + address[] memory electedGroupValidators = getValidators().getTopValidatorsFromGroup( electionGroups[i], numMembersElected[i] ); @@ -523,7 +526,7 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingLockedGold, U address group = electionGroups[i]; // Only consider groups with members left to be elected. if (numMembers[i] > numMembersElected[i]) { - FixidityLib.Fraction memory n = FixidityLib.newFixed(votes.totals.getValue(group)).divide( + FixidityLib.Fraction memory n = FixidityLib.newFixed(votes.total.eligible.getValue(group)).divide( FixidityLib.newFixed(numMembersElected[i].add(1)) ); if (n.gt(maxN)) { diff --git a/packages/protocol/contracts/governance/Governance.sol b/packages/protocol/contracts/governance/Governance.sol index bb145035db2..0f7473ed4cd 100644 --- a/packages/protocol/contracts/governance/Governance.sol +++ b/packages/protocol/contracts/governance/Governance.sol @@ -6,19 +6,19 @@ import "openzeppelin-solidity/contracts/math/Math.sol"; import "openzeppelin-solidity/contracts/math/SafeMath.sol"; import "solidity-bytes-utils/contracts/BytesLib.sol"; -import "./UsingLockedGold.sol"; import "./interfaces/IGovernance.sol"; import "../common/Initializable.sol"; import "../common/FixidityLib.sol"; import "../common/FractionUtil.sol"; import "../common/linkedlists/IntegerSortedLinkedList.sol"; +import "../common/UsingRegistry.sol"; // TODO(asa): Hardcode minimum times for queueExpiry, etc. /** * @title A contract for making, passing, and executing on-chain governance proposals. */ -contract Governance is IGovernance, Ownable, Initializable, UsingLockedGold, ReentrancyGuard { +contract Governance is IGovernance, Ownable, Initializable, ReentrancyGuard, UsingRegistry { using FixidityLib for FixidityLib.Fraction; using FractionUtil for FractionUtil.Fraction; using SafeMath for uint256; @@ -428,7 +428,7 @@ contract Governance is IGovernance, Ownable, Initializable, UsingLockedGold, Ree nonReentrant returns (bool) { - address account = getAccountFromVoter(msg.sender); + address account = getLockedGold().getAccountFromVoter(msg.sender); // TODO(asa): When upvoting a proposal that will get dequeued, should we let the tx succeed // and return false? dequeueProposalsIfReady(); @@ -441,7 +441,7 @@ contract Governance is IGovernance, Ownable, Initializable, UsingLockedGold, Ree } Voter storage voter = voters[account]; // We can upvote a proposal in the queue if we're not already upvoting a proposal in the queue. - uint256 weight = getAccountTotalLockedGold(account); + uint256 weight = getLockedGold().getAccountTotalLockedGold(account); require( isQueued(proposalId) && (voter.upvote.proposalId == 0 || !queue.contains(voter.upvote.proposalId)) && @@ -477,7 +477,7 @@ contract Governance is IGovernance, Ownable, Initializable, UsingLockedGold, Ree returns (bool) { dequeueProposalsIfReady(); - address account = getAccountFromVoter(msg.sender); + address account = getLockedGold().getAccountFromVoter(msg.sender); Voter storage voter = voters[account]; uint256 proposalId = voter.upvote.proposalId; Proposal storage proposal = proposals[proposalId]; @@ -543,7 +543,7 @@ contract Governance is IGovernance, Ownable, Initializable, UsingLockedGold, Ree nonReentrant returns (bool) { - address account = getAccountFromVoter(msg.sender); + address account = getLockedGold().getAccountFromVoter(msg.sender); dequeueProposalsIfReady(); Proposal storage proposal = proposals[proposalId]; require(_proposalExists(proposal) && dequeued[index] == proposalId); @@ -553,7 +553,7 @@ contract Governance is IGovernance, Ownable, Initializable, UsingLockedGold, Ree } ProposalStage stage = _getDequeuedProposalStage(proposal.timestamp); Voter storage voter = voters[account]; - uint256 weight = getAccountTotalLockedGold(account); + uint256 weight = getLockedGold().getAccountTotalLockedGold(account); require( proposal.approved && stage == ProposalStage.Referendum && diff --git a/packages/protocol/contracts/governance/LockedGold.sol b/packages/protocol/contracts/governance/LockedGold.sol index 0db2beabd87..5dae668a557 100644 --- a/packages/protocol/contracts/governance/LockedGold.sol +++ b/packages/protocol/contracts/governance/LockedGold.sol @@ -5,14 +5,13 @@ import "openzeppelin-solidity/contracts/math/SafeMath.sol"; import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; import "./interfaces/ILockedGold.sol"; -import "./UsingElection.sol"; import "../common/Initializable.sol"; -import "../common/UsingRegistry.sol"; import "../common/interfaces/IERC20Token.sol"; import "../common/Signatures.sol"; +import "../common/UsingRegistry.sol"; -contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistry, UsingElection { +contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistry { // TODO(asa): How do adjust for updated requirements? // Have a refreshRequirements function validators and groups can call @@ -196,7 +195,7 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr * @dev Fails if the `accountOrVoter` is not an account or authorized voter. * @return The associated account. */ - function getAccountFromVoter(address accountOrVoter) public view returns (address) { + function getAccountFromVoter(address accountOrVoter) external view returns (address) { address authorizingAccount = authorizations[accountOrVoter]; if (authorizingAccount != address(0)) { require(accounts[authorizingAccount].authorizations.voter == accountOrVoter); @@ -207,7 +206,7 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr } } - function getTotalLockedGold() public view returns (uint256) { + function getTotalLockedGold() external view returns (uint256) { return totalNonvoting.add(getElection().totalVotes()); } diff --git a/packages/protocol/contracts/governance/UsingLockedGold.sol b/packages/protocol/contracts/governance/UsingLockedGold.sol deleted file mode 100644 index 7f29f199155..00000000000 --- a/packages/protocol/contracts/governance/UsingLockedGold.sol +++ /dev/null @@ -1,60 +0,0 @@ -pragma solidity ^0.5.3; - -import "./interfaces/ILockedGold.sol"; -import "../common/UsingRegistry.sol"; - - -/** - * @title A contract for calling functions on the LockedGold contract. - * @dev Any contract calling these functions should guard against reentrancy. - */ -contract UsingLockedGold is UsingRegistry { - /** - * @notice Returns the account associated with `accountOrVoter`. - * @param accountOrVoter The address of the account or authorized voter. - * @dev Fails if the `accountOrVoter` is not an account or authorized voter. - * @return The associated account. - */ - function getAccountFromVoter(address accountOrVoter) internal view returns (address) { - return getLockedGold().getAccountFromVoter(accountOrVoter); - } - - /** - * @notice Returns the account associated with `accountOrValidator`. - * @param accountOrValidator The address of the account or authorized validator. - * @dev Fails if the `accountOrValidator` is not an account or authorized validator. - * @return The associated account. - */ - function getAccountFromValidator(address accountOrValidator) internal view returns (address) { - return getLockedGold().getAccountFromValidator(accountOrValidator); - } - - /** - * @notice Returns the validator address for a particular account. - * @param account The account. - * @return The associated validator address. - */ - function getValidatorFromAccount(address account) internal view returns (address) { - return getLockedGold().getValidatorFromAccount(account); - } - - function getTotalLockedGold() internal view returns (uint256) { - return getLockedGold().getTotalLockedGold(); - } - - function getAccountTotalLockedGold(address account) internal view returns (uint256) { - return getLockedGold().getAccountTotalLockedGold(account); - } - - function incrementNonvotingAccountBalance(address account, uint256 value) internal returns (bool) { - return getLockedGold().incrementNonvotingAccountBalance(account, value); - } - - function decrementNonvotingAccountBalance(address account, uint256 value) internal returns (bool) { - return getLockedGold().decrementNonvotingAccountBalance(account, value); - } - - function getLockedGold() internal view returns(ILockedGold) { - return ILockedGold(registry.getAddressForOrDie(LOCKED_GOLD_REGISTRY_ID)); - } -} diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index 2c2efa48dbd..85bccffa361 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -5,19 +5,17 @@ import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; import "openzeppelin-solidity/contracts/utils/ReentrancyGuard.sol"; import "solidity-bytes-utils/contracts/BytesLib.sol"; -import "./UsingLockedGold.sol"; import "./interfaces/IValidators.sol"; import "../common/Initializable.sol"; import "../common/FixidityLib.sol"; import "../common/linkedlists/AddressLinkedList.sol"; +import "../common/UsingRegistry.sol"; /** * @title A contract for registering and electing Validator Groups and Validators. */ -contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, UsingLockedGold { - - // Votes live in the Election SC, votePortion, +contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, UsingRegistry { using FixidityLib for FixidityLib.Fraction; using AddressLinkedList for LinkedList.List; @@ -25,12 +23,18 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi using BytesLib for bytes; address constant PROOF_OF_POSSESSION = address(0xff - 4); + uint256 constant MAX_INT = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; struct RegistrationRequirements { uint256 group; uint256 validator; } + struct DeregistrationLockups { + uint256 group; + uint256 validator; + } + struct ValidatorGroup { string name; string url; @@ -50,13 +54,19 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi address[] private _groups; address[] private _validators; RegistrationRequirements public registrationRequirements; + DeregistrationLockups public deregistrationLockups; uint256 public maxGroupSize; event MaxGroupSizeSet( uint256 size ); - event RegistrationRequirementSet( + event RegistrationRequirementsSet( + uint256 group, + uint256 validator + ); + + event DeregistrationLockupsSet( uint256 group, uint256 validator ); @@ -107,22 +117,22 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi address indexed validator ); - event ValidatorGroupEmptied( - address indexed group - ); - /** * @notice Initializes critical variables. * @param registryAddress The address of the registry contract. * @param groupRequirement The minimum locked gold needed to register a group. * @param validatorRequirement The minimum locked gold needed to register a validator. - * @param size The maximum group size. + * @param groupLockup The duration the above gold remains locked after deregistration. + * @param validatorLockup The duration the above gold remains locked after deregistration. + * @param _maxGroupSize The maximum group size. * @dev Should be called only once. */ function initialize( address registryAddress, uint256 groupRequirement, uint256 validatorRequirement, + uint256 groupLockup, + uint256 validatorLockup, uint256 _maxGroupSize ) external @@ -167,7 +177,31 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi validatorRequirement != registrationRequirements.validator ); registrationRequirements = RegistrationRequirements(groupRequirement, validatorRequirement); - emit RegistrationRequirementSet(groupRequirement, validatorRequirement); + emit RegistrationRequirementsSet(groupRequirement, validatorRequirement); + return true; + } + + /** + * @notice Updates the duration for which gold remains locked after deregistration. + * @param groupLockup The duration for groups. + * @param validatorLockup The duration for validators. + * @return True upon success. + * @dev The new requirement is only enforced for future validator or group deregistrations. + */ + function setDeregistrationLockup( + uint256 groupLockup, + uint256 validatorLockup + ) + external + onlyOwner + returns (bool) + { + require( + groupLockup != deregistrationLockups.group || + validatorLockup != deregistrationLockups.validator + ); + deregistrationLockups = DeregistrationLockups(groupLockup, validatorLockup); + emit DeregistrationLockupsSet(groupLockup, validatorLockup); return true; } @@ -203,9 +237,9 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi bytes memory proofOfPossessionBytes = publicKeysData.slice(64, 48 + 96); require(checkProofOfPossession(proofOfPossessionBytes)); - address account = getAccountFromValidator(msg.sender); + address account = getValidators().getAccountFromValidator(msg.sender); require(!isValidator(account) && !isValidatorGroup(account)); - require(meetsRegistrationRequirements(account)); + require(meetsValidatorRegistrationRequirement(account)); Validator memory validator = Validator(name, url, publicKeysData, address(0)); validators[account] = validator; @@ -226,6 +260,24 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi return success; } + /** + * @notice Returns whether an account meets the requirements to register a validator. + * @param account The account. + * @return Whether an account meets the requirements to register a validator. + */ + function meetsValidatorRegistrationRequirement(address account) public returns (bool) { + getLockedGold().getAccountTotalLockedGold() >= registrationRequirements.validator; + } + + /** + * @notice Returns whether an account meets the requirements to register a group. + * @param account The account. + * @return Whether an account meets the requirements to register a group. + */ + function meetsValidatorGroupRegistrationRequirement(address account) public returns (bool) { + getLockedGold().getAccountTotalLockedGold() >= registrationRequirements.group; + } + /** * @notice De-registers a validator, removing it from the group for which it is a member. * @param index The index of this validator in the list of all validators. @@ -233,7 +285,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi * @dev Fails if the account is not a validator. */ function deregisterValidator(uint256 index) external nonReentrant returns (bool) { - address account = getAccountFromValidator(msg.sender); + address account = getValidators().getAccountFromValidator(msg.sender); require(isValidator(account)); Validator storage validator = validators[account]; if (validator.affiliation != address(0)) { @@ -241,7 +293,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi } delete validators[account]; deleteElement(_validators, account, index); - getLockedGold().setAccountMustMaintain(account, registrationRequirements.validator, now.add(deregisterPeriods.validator)); + getLockedGold().setAccountMustMaintain(account, registrationRequirements.validator, now.add(deregistrationLockups.validator)); emit ValidatorDeregistered(account); return true; } @@ -253,7 +305,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi * @dev De-affiliates with the previously affiliated group if present. */ function affiliate(address group) external nonReentrant returns (bool) { - address account = getAccountFromValidator(msg.sender); + address account = getValidators().getAccountFromValidator(msg.sender); require(isValidator(account) && isValidatorGroup(group)); Validator storage validator = validators[account]; if (validator.affiliation != address(0)) { @@ -270,7 +322,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi * @dev Fails if the account is not a validator with non-zero affiliation. */ function deaffiliate() external nonReentrant returns (bool) { - address account = getAccountFromValidator(msg.sender); + address account = getValidators().getAccountFromValidator(msg.sender); require(isValidator(account)); Validator storage validator = validators[account]; require(validator.affiliation != address(0)); @@ -298,20 +350,21 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi { require(bytes(name).length > 0); require(bytes(url).length > 0); - require(isFraction(commission)); + // TODO(asa) + // require(isFraction(commission)); require(members.length <= maxGroupSize); - address account = getAccountFromValidator(msg.sender); + address account = getValidators().getAccountFromValidator(msg.sender); require(!isValidator(account) && !isValidatorGroup(account)); - require(meetsRegistrationRequirements(account)); + require(meetsValidatorGroupRegistrationRequirement(account)); - ValdiatorGroup storage group = groups[account]; + ValidatorGroup storage group = groups[account]; group.name = name; group.url = url; for (uint256 i = 0; i < members.length; i = i.add(1)) { group.addMember(members[i]); } _groups.push(account); - getLockedGold().setAccountMustMaintain(account, requirements.group, MAX_INT); + getLockedGold().setAccountMustMaintain(account, registrationRequirements.group, MAX_INT); emit ValidatorGroupRegistered(account, name, url); return true; } @@ -323,12 +376,12 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi * @dev Fails if the account is not a validator group with no members. */ function deregisterValidatorGroup(uint256 index) external nonReentrant returns (bool) { - address account = getAccountFromValidator(msg.sender); + address account = getValidators().getAccountFromValidator(msg.sender); // Only empty Validator Groups can be deregistered. require(isValidatorGroup(account) && groups[account].members.numElements == 0); delete groups[account]; deleteElement(_groups, account, index); - getLockedGold().setAccountMustMaintain(account, requirements.group, now.add(deregisterPeriods.group)); + getLockedGold().setAccountMustMaintain(account, registrationRequirements.group, now.add(deregistrationLockups.group)); emit ValidatorGroupDeregistered(account); return true; } @@ -340,7 +393,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi * @dev Fails if `validator` has not set their affiliation to this account. */ function addMember(address validator) external nonReentrant returns (bool) { - address account = getAccountFromValidator(msg.sender); + address account = getValidators().getAccountFromValidator(msg.sender); require(isValidatorGroup(account) && isValidator(validator)); ValidatorGroup storage group = groups[account]; require(group.members.length < maxGroupSize); @@ -357,7 +410,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi * @dev Fails if `validator` is not a member of the account's group. */ function removeMember(address validator) external nonReentrant returns (bool) { - address account = getAccountFromValidator(msg.sender); + address account = getValidators().getAccountFromValidator(msg.sender); require(isValidatorGroup(account) && isValidator(validator)); return _removeMember(account, validator); } @@ -381,7 +434,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi nonReentrant returns (bool) { - address account = getAccountFromValidator(msg.sender); + address account = getValidators().getAccountFromValidator(msg.sender); require(isValidatorGroup(account) && isValidator(validator)); ValidatorGroup storage group = groups[account]; require(group.members.contains(validator)); @@ -442,7 +495,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi address[] memory topAccounts = groups[account].members.list.headN(n); address[] memory topValidators = new address[](n); for (uint256 i = 0; i < n; i = i.add(1)) { - topValidators[i] = getValidatorFromAccount(topAccounts[i]); + topValidators[i] = getLockedGold().getValidatorFromAccount(topAccounts[i]); } return topValidators; } @@ -464,7 +517,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi * @return The locked gold requirements to register a validator or group. */ function getRegistrationRequirements() external view returns (uint256, uint256) { - return (registrationRequirements.group, registrationRequirement.validator); + return (registrationRequirements.group, registrationRequirements.validator); } /** @@ -501,15 +554,6 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi return bytes(validators[account].name).length > 0; } - /** - * @notice Returns whether an account meets the requirements to register a validator or group. - * @param account The account. - * @return Whether an account meets the requirements to register a validator or group. - */ - function meetsValidatorRegistrationRequirements(address account) public view returns (bool) { - // TODO - } - /** * @notice Deletes an element from a list of addresses. * @param list The list of addresses. @@ -540,13 +584,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi // Empty validator groups are not electable. if (groups[group].members.numElements == 0) { - if (votes.contains(group)) { - // TODO(asa): What needs to happen here???? - // We need to remove from the linked list but preserve all other info... - // And probably add back in if it gets a member... - votes.remove(group); - } - emit ValidatorGroupEmptied(group); + getElection().markGroupUnelectable(group); } return true; } diff --git a/packages/protocol/contracts/governance/interfaces/ILockedGold.sol b/packages/protocol/contracts/governance/interfaces/ILockedGold.sol index 6b37899dd74..de7d7888736 100644 --- a/packages/protocol/contracts/governance/interfaces/ILockedGold.sol +++ b/packages/protocol/contracts/governance/interfaces/ILockedGold.sol @@ -2,26 +2,8 @@ pragma solidity ^0.5.3; interface ILockedGold { - enum DelegateRole {Validating, Voting, Rewards} - enum CommitmentType {Locked, Notified} function initialize(address, uint256) external; - function isVotingFrozen(address) external view returns (bool); - function setCumulativeRewardWeight(uint256) external; - function setMaxNoticePeriod(uint256) external; - function redeemRewards() external returns (uint256); - function freezeVoting() external; - function unfreezeVoting() external; - function newCommitment(uint256) external payable returns (uint256); - function notifyCommitment(uint256, uint256) external returns (uint256); - function extendCommitment(uint256, uint256) external returns (uint256); - function withdrawCommitment(uint256) external returns (uint256); - function increaseNoticePeriod(uint256, uint256, uint256) external returns (uint256); - function getRewardsLastRedeemed(address) external view returns (uint96); - function getNoticePeriods(address) external view returns (uint256[] memory); - function getAvailabilityTimes(address) external view returns (uint256[] memory); - function getLockedCommitment(address, uint256) external view returns (uint256, uint256); - function getAccountWeight(address) external view returns (uint256); - function delegateRole(DelegateRole, address, uint8, bytes32, bytes32) external; - function getAccountFromDelegateAndRole(address, DelegateRole) external view returns (address); - function getDelegateFromAccountAndRole(address, DelegateRole) external view returns (address); + function getAccountFromVoter(address) external view returns (address); + function incrementNonvotingAccountBalance(address, uint256) external; + function decrementNonvotingAccountBalance(address, uint256) external; } diff --git a/packages/protocol/contracts/governance/interfaces/IValidators.sol b/packages/protocol/contracts/governance/interfaces/IValidators.sol index adde44d0178..757c0f29c64 100644 --- a/packages/protocol/contracts/governance/interfaces/IValidators.sol +++ b/packages/protocol/contracts/governance/interfaces/IValidators.sol @@ -2,7 +2,7 @@ pragma solidity ^0.5.3; interface IValidators { - function isVoting(address) external view returns (bool); - function isValidating(address) external view returns (bool); - function getValidators() external view returns (address[] memory); + function electValidators() external view returns (address[] memory); + function getNumGroupMembers(address) external view returns (uint256); + function getNumRegisteredValidators() external view returns (uint256); } diff --git a/packages/protocol/contracts/governance/test/MockLockedGold.sol b/packages/protocol/contracts/governance/test/MockLockedGold.sol index 4fa6fee6724..f223c548a43 100644 --- a/packages/protocol/contracts/governance/test/MockLockedGold.sol +++ b/packages/protocol/contracts/governance/test/MockLockedGold.sol @@ -7,102 +7,4 @@ import "../interfaces/ILockedGold.sol"; * @title A mock LockedGold for testing. */ contract MockLockedGold is ILockedGold { - mapping(address => mapping(uint256 => uint256)) public locked; - mapping(address => uint256) public weights; - mapping(address => bool) public frozen; - // Maps a delegating address to an account. - mapping(address => address) public delegations; - // Maps an account address to their voting delegate. - mapping(address => address) public voters; - // Maps an account address to their validating delegate. - mapping(address => address) public validators; - // Maps an account address to their rewards delegate. - mapping(address => address) public rewarders; - - function initialize(address, uint256) external {} - function setCumulativeRewardWeight(uint256) external {} - function setMaxNoticePeriod(uint256) external {} - function redeemRewards() external returns (uint256) {} - function freezeVoting() external {} - function unfreezeVoting() external {} - function newCommitment(uint256) external payable returns (uint256) {} - function notifyCommitment(uint256, uint256) external returns (uint256) {} - function extendCommitment(uint256, uint256) external returns (uint256) {} - function withdrawCommitment(uint256) external returns (uint256) {} - function increaseNoticePeriod(uint256, uint256, uint256) external returns (uint256) {} - function getRewardsLastRedeemed(address) external view returns (uint96) {} - function getNoticePeriods(address) external view returns (uint256[] memory) {} - function getAvailabilityTimes(address) external view returns (uint256[] memory) {} - function delegateRole(DelegateRole, address, uint8, bytes32, bytes32) external {} - - function isVotingFrozen(address account) external view returns (bool) { - return frozen[account]; - } - - function setWeight(address account, uint256 weight) external { - weights[account] = weight; - } - - function setLockedCommitment(address account, uint256 noticePeriod, uint256 value) external { - locked[account][noticePeriod] = value; - } - - function setVotingFrozen(address account) external { - frozen[account] = true; - } - - function delegateVoting(address account, address delegate) external { - delegations[delegate] = account; - voters[account] = delegate; - } - - function delegateValidating(address account, address delegate) external { - delegations[delegate] = account; - validators[account] = delegate; - } - - function getAccountWeight(address account) external view returns (uint256) { - return weights[account]; - } - - function getAccountFromDelegateAndRole(address delegate, DelegateRole) - external view returns (address) - { - address a = delegations[delegate]; - if (a != address(0)) { - return a; - } else { - return delegate; - } - } - - function getDelegateFromAccountAndRole(address account, DelegateRole role) - external view returns (address) - { - address a; - if (role == DelegateRole.Validating) { - a = validators[account]; - } else if (role == DelegateRole.Voting) { - a = voters[account]; - } else if (role == DelegateRole.Rewards) { - a = rewarders[account]; - } - if (a != address(0)) { - return a; - } else { - return account; - } - } - - function getLockedCommitment( - address account, - uint256 noticePeriod - ) - external - view - returns (uint256, uint256) - { - // Always return 0 for the index. - return (locked[account][noticePeriod], 0); - } } diff --git a/packages/protocol/contracts/identity/Attestations.sol b/packages/protocol/contracts/identity/Attestations.sol index 9f0a071da67..db5f7dd03d0 100644 --- a/packages/protocol/contracts/identity/Attestations.sol +++ b/packages/protocol/contracts/identity/Attestations.sol @@ -565,17 +565,6 @@ contract Attestations is IAttestations, Ownable, Initializable, UsingRegistry, R return identifiers[identifier].accounts; } - /** - * @notice Returns the current validator set - * TODO: Should be replaced with a precompile - */ - function getValidators() public view returns (address[] memory) { - IValidators validatorContract = IValidators( - registry.getAddressForOrDie(VALIDATORS_REGISTRY_ID) - ); - return validatorContract.getValidators(); - } - /** * @notice Helper function for batchGetAttestationStats to calculate the total number of addresses that have >0 complete attestations for the identifiers @@ -618,7 +607,7 @@ contract Attestations is IAttestations, Ownable, Initializable, UsingRegistry, R IRandom random = IRandom(registry.getAddressForOrDie(RANDOM_REGISTRY_ID)); bytes32 seed = random.random(); - address[] memory validators = getValidators(); + address[] memory validators = getElection().electValidators(); uint256 currentIndex = 0; address validator; From 6ce7f7e1a55903235f7a3a956a3e7b091145da99 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Fri, 20 Sep 2019 11:06:17 -0700 Subject: [PATCH 005/149] Compiling --- .../linkedlists/AddressSortedLinkedList.sol | 14 +++++ .../common/linkedlists/SortedLinkedList.sol | 10 +++ .../contracts/common/test/FixidityTest.sol | 22 +++---- .../contracts/governance/Election.sol | 14 +++-- .../contracts/governance/Governance.sol | 28 +++------ .../contracts/governance/LockedGold.sol | 35 ++++++----- .../contracts/governance/Validators.sol | 62 +++++++++++-------- .../governance/interfaces/IElection.sol | 5 +- .../governance/interfaces/IGovernance.sol | 3 +- .../governance/interfaces/ILockedGold.sol | 6 +- .../governance/interfaces/IValidators.sol | 5 +- 11 files changed, 118 insertions(+), 86 deletions(-) diff --git a/packages/protocol/contracts/common/linkedlists/AddressSortedLinkedList.sol b/packages/protocol/contracts/common/linkedlists/AddressSortedLinkedList.sol index 3949b873124..528d5500ece 100644 --- a/packages/protocol/contracts/common/linkedlists/AddressSortedLinkedList.sol +++ b/packages/protocol/contracts/common/linkedlists/AddressSortedLinkedList.sol @@ -105,4 +105,18 @@ library AddressSortedLinkedList { } return (keys, values); } + + /** + * @notice Returns the N greatest elements of the list. + * @param n The number of elements to return. + * @return The keys of the greatest elements. + */ + function headN(SortedLinkedList.List storage list, uint256 n) public view returns (address[] memory) { + bytes32[] memory byteKeys = list.headN(n); + address[] memory keys = new address[](n); + for (uint256 i = 0; i < n; i++) { + keys[i] = toAddress(byteKeys[i]); + } + return keys; + } } diff --git a/packages/protocol/contracts/common/linkedlists/SortedLinkedList.sol b/packages/protocol/contracts/common/linkedlists/SortedLinkedList.sol index 80a057324dc..176d499749c 100644 --- a/packages/protocol/contracts/common/linkedlists/SortedLinkedList.sol +++ b/packages/protocol/contracts/common/linkedlists/SortedLinkedList.sol @@ -144,6 +144,16 @@ library SortedLinkedList { return list.list.getKeys(); } + /** + * @notice Returns first N greatest elements of the list. + * @param n The number of elements to return. + * @return The keys of the first n elements. + */ + function headN(List storage list, uint256 n) public view returns (bytes32[] memory) { + return list.list.headN(n); + } + + // TODO(asa): Gas optimizations by passing in elements to isValueBetween /** * @notice Returns the keys of the elements greaterKey than and less than the provided value. diff --git a/packages/protocol/contracts/common/test/FixidityTest.sol b/packages/protocol/contracts/common/test/FixidityTest.sol index 8ce11ca87fb..452b5da0c9a 100644 --- a/packages/protocol/contracts/common/test/FixidityTest.sol +++ b/packages/protocol/contracts/common/test/FixidityTest.sol @@ -6,47 +6,47 @@ import "../FixidityLib.sol"; contract FixidityTest { using FixidityLib for FixidityLib.Fraction; - function newFixed(uint256 a) external view returns (uint256) { + function newFixed(uint256 a) external pure returns (uint256) { return FixidityLib.newFixed(a).unwrap(); } - function newFixedFraction(uint256 a, uint256 b) external view returns (uint256) { + function newFixedFraction(uint256 a, uint256 b) external pure returns (uint256) { return FixidityLib.newFixedFraction(a, b).unwrap(); } - function add(uint256 a, uint256 b) external view returns (uint256) { + function add(uint256 a, uint256 b) external pure returns (uint256) { return FixidityLib.wrap(a).add(FixidityLib.wrap(b)).unwrap(); } - function subtract(uint256 a, uint256 b) external view returns (uint256) { + function subtract(uint256 a, uint256 b) external pure returns (uint256) { return FixidityLib.wrap(a).subtract(FixidityLib.wrap(b)).unwrap(); } - function multiply(uint256 a, uint256 b) external view returns (uint256) { + function multiply(uint256 a, uint256 b) external pure returns (uint256) { return FixidityLib.wrap(a).multiply(FixidityLib.wrap(b)).unwrap(); } - function reciprocal(uint256 a) external view returns (uint256) { + function reciprocal(uint256 a) external pure returns (uint256) { return FixidityLib.wrap(a).reciprocal().unwrap(); } - function divide(uint256 a, uint256 b) external view returns (uint256) { + function divide(uint256 a, uint256 b) external pure returns (uint256) { return FixidityLib.wrap(a).divide(FixidityLib.wrap(b)).unwrap(); } - function gt(uint256 a, uint256 b) external view returns (bool) { + function gt(uint256 a, uint256 b) external pure returns (bool) { return FixidityLib.wrap(a).gt(FixidityLib.wrap(b)); } - function gte(uint256 a, uint256 b) external view returns (bool) { + function gte(uint256 a, uint256 b) external pure returns (bool) { return FixidityLib.wrap(a).gte(FixidityLib.wrap(b)); } - function lt(uint256 a, uint256 b) external view returns (bool) { + function lt(uint256 a, uint256 b) external pure returns (bool) { return FixidityLib.wrap(a).lt(FixidityLib.wrap(b)); } - function lte(uint256 a, uint256 b) external view returns (bool) { + function lte(uint256 a, uint256 b) external pure returns (bool) { return FixidityLib.wrap(a).lte(FixidityLib.wrap(b)); } } diff --git a/packages/protocol/contracts/governance/Election.sol b/packages/protocol/contracts/governance/Election.sol index c4a07f2a699..294a6ffebea 100644 --- a/packages/protocol/contracts/governance/Election.sol +++ b/packages/protocol/contracts/governance/Election.sol @@ -371,7 +371,7 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { function markGroupEligible(address group, address lesser, address greater) external { require(!votes.total.eligible.contains(group)); - require(getValidators().getNumGroupMembers(group) > 0); + require(getValidators().getGroupNumMembers(group) > 0); uint256 value = votes.pending.total[group].add(votes.active.total[group]); votes.total.eligible.insert(group, value, lesser, greater); } @@ -409,7 +409,7 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { active.total[group] = active.total[group].sub(value); } - function getActiveVotesDelta(address group, uint256 value) private returns (uint256) { + function getActiveVotesDelta(address group, uint256 value) private view returns (uint256) { // Preserve delta * total = value * denominator return value.mul(votes.active.denominators[group]).div(votes.active.total[group]); } @@ -429,11 +429,15 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { } function getNumVotesReceivable(address group) public view returns (uint256) { - uint256 numerator = getValidators().getNumGroupMembers(group).add(1).mul(votes.total.total); + uint256 numerator = getValidators().getGroupNumMembers(group).add(1).mul(getLockedGold().getTotalLockedGold()); uint256 denominator = Math.min(maxElectableValidators, getValidators().getNumRegisteredValidators()); return numerator.div(denominator); } + function getTotalVotes() external view returns (uint256) { + return votes.total.total; + } + function validatorAddressFromCurrentSet(uint256 index) external view returns (address) { address validatorAddress; assembly { @@ -470,8 +474,8 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { uint256 maxNumElectionGroups = Math.min(maxElectableValidators, votes.total.eligible.list.numElements); // uint256 requiredVotes = electabilityThreshold.multiply(FixidityLib.newFixed(votes.total.total)).fromFixed(); // TODO(asa): Filter by > requiredVotes - address[] memory electionGroups = votes.total.eligible.list.headN(maxNumElectionGroups); - uint256[] memory numMembers = getValidators().getNumGroupMembers(electionGroups); + address[] memory electionGroups = votes.total.eligible.headN(maxNumElectionGroups); + uint256[] memory numMembers = getValidators().getGroupsNumMembers(electionGroups); // Holds the number of members elected for each of the eligible validator groups. uint256[] memory numMembersElected = new uint256[](electionGroups.length); uint256 totalNumMembersElected = 0; diff --git a/packages/protocol/contracts/governance/Governance.sol b/packages/protocol/contracts/governance/Governance.sol index 0f7473ed4cd..256a10b3d5c 100644 --- a/packages/protocol/contracts/governance/Governance.sol +++ b/packages/protocol/contracts/governance/Governance.sol @@ -109,7 +109,7 @@ contract Governance is IGovernance, Ownable, Initializable, ReentrancyGuard, Usi mapping(address => uint256) public refundedDeposits; mapping(address => ContractConstitution) private constitution; mapping(uint256 => Proposal) private proposals; - mapping(address => Voter) public voters; + mapping(address => Voter) private voters; SortedLinkedList.List private queue; uint256[] public dequeued; uint256[] public emptyIndices; @@ -580,7 +580,7 @@ contract Governance is IGovernance, Ownable, Initializable, ReentrancyGuard, Usi } else if (value == VoteValue.No) { proposal.votes.no = proposal.votes.no.add(weight); } - voteRecord = VoteRecord(value, proposalId, weight); + voter.referendumVotes[index] = VoteRecord(value, proposalId, weight); if (proposal.timestamp > voter.mostRecentReferendumProposal) { voter.mostRecentReferendumProposal = proposalId; } @@ -784,12 +784,13 @@ contract Governance is IGovernance, Ownable, Initializable, ReentrancyGuard, Usi } /** - * @notice Returns the ID of the proposal upvoted by `account`. + * @notice Returns the ID of the proposal upvoted by `account` and the weight of that upvote. * @param account The address of the account. - * @return The ID of the proposal upvoted by `account`. + * @return The ID of the proposal upvoted by `account` and the weight of that upvote. */ - function getUpvotedProposal(address account) external view returns (uint256) { - return voters[account].upvotedProposal; + function getUpvoteRecord(address account) external view returns (uint256, uint256) { + UpvoteRecord memory upvoteRecord = voters[account].upvote; + return (upvoteRecord.proposalId, upvoteRecord.weight); } /** @@ -801,21 +802,6 @@ contract Governance is IGovernance, Ownable, Initializable, ReentrancyGuard, Usi return voters[account].mostRecentReferendumProposal; } - /** - * @notice Returns whether or not a particular account is voting on proposals. - * @param account The address of the account. - * @return Whether or not the account is voting on proposals. - */ - function isVoting(address account) external view returns (bool) { - Voter storage voter = voters[account]; - bool isVotingQueue = voter.upvotedProposal != 0 && isQueued(voter.upvotedProposal); - Proposal storage proposal = proposals[voter.mostRecentReferendumProposal]; - bool isVotingReferendum = ( - _getDequeuedProposalStage(proposal.timestamp) == ProposalStage.Referendum - ); - return isVotingQueue || isVotingReferendum; - } - /** * @notice Removes the proposals with the most upvotes from the queue, moving them to the * approval stage. diff --git a/packages/protocol/contracts/governance/LockedGold.sol b/packages/protocol/contracts/governance/LockedGold.sol index 5dae668a557..3f7fab5d9a2 100644 --- a/packages/protocol/contracts/governance/LockedGold.sol +++ b/packages/protocol/contracts/governance/LockedGold.sol @@ -13,6 +13,8 @@ import "../common/UsingRegistry.sol"; contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistry { + using SafeMath for uint256; + // TODO(asa): How do adjust for updated requirements? // Have a refreshRequirements function validators and groups can call struct MustMaintain { @@ -47,7 +49,7 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr Balances balances; } - mapping(address => Account) public accounts; + mapping(address => Account) private accounts; // Maps voting and validating keys to the account that provided the authorization. mapping(address => address) public authorizations; uint256 public totalNonvoting; @@ -60,9 +62,10 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr event GoldWithdrawn(address indexed account, uint256 value); event AccountMustMaintainSet(address indexed account, uint256 value, uint256 timestamp); - function initialize(address registryAddress) external initializer { + function initialize(address registryAddress, uint256 _unlockingPeriod) external initializer { _transferOwnership(msg.sender); setRegistry(registryAddress); + unlockingPeriod = _unlockingPeriod; } /** @@ -110,28 +113,28 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr * @notice Locks gold to be used for voting. * @param value The amount of gold to be locked. */ - function lock(uint256 value) external nonReentrant { + function lock(uint256 value) external payable nonReentrant { require(isAccount(msg.sender)); require(msg.value == value && value > 0); _incrementNonvotingAccountBalance(msg.sender, value); emit GoldLocked(msg.sender, value); } - function incrementNonvotingAccountBalance(address account, uint256 value) external onlyRegisteredContract('Election', msg.sender) { + function incrementNonvotingAccountBalance(address account, uint256 value) external onlyRegisteredContract('Election') { _incrementNonvotingAccountBalance(account, value); } - function decrementNonvotingAccountBalance(address account, uint256 value) external onlyRegisteredContract('Election', msg.sender) { + function decrementNonvotingAccountBalance(address account, uint256 value) external onlyRegisteredContract('Election') { _decrementNonvotingAccountBalance(account, value); } function _incrementNonvotingAccountBalance(address account, uint256 value) private { - accounts[account].gold.nonvoting = accounts[account].gold.nonvoting.add(value); + accounts[account].balances.nonvoting = accounts[account].balances.nonvoting.add(value); totalNonvoting = totalNonvoting.add(value); } function _decrementNonvotingAccountBalance(address account, uint256 value) private { - accounts[account].gold.nonvoting = accounts[account].gold.nonvoting.sub(value); + accounts[account].balances.nonvoting = accounts[account].balances.nonvoting.sub(value); totalNonvoting = totalNonvoting.sub(value); } @@ -139,7 +142,7 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr function unlock(uint256 value) external nonReentrant { require(isAccount(msg.sender)); Account storage account = accounts[msg.sender]; - MustMaintain memory requirement = account.requirement; + MustMaintain memory requirement = account.balances.requirements; require( now >= requirement.timestamp || getAccountTotalLockedGold(msg.sender).sub(value) >= requirement.value @@ -166,7 +169,7 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr Account storage account = accounts[msg.sender]; require(index < account.balances.pendingWithdrawals.length); PendingWithdrawal memory pendingWithdrawal = account.balances.pendingWithdrawals[index]; - require(now >= pendingWithdrawal.available); + require(now >= pendingWithdrawal.timestamp); uint256 value = pendingWithdrawal.value; deletePendingWithdrawal(account.balances.pendingWithdrawals, index); IERC20Token goldToken = IERC20Token(registry.getAddressFor(GOLD_TOKEN_REGISTRY_ID)); @@ -180,11 +183,11 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr uint256 timestamp ) public - onlyRegisteredContract('Election', msg.sender) + onlyRegisteredContract('Election') nonReentrant returns (bool) { - accounts[account].requirement = MustMaintain(value, timestamp); + accounts[account].balances.requirements = MustMaintain(value, timestamp); emit AccountMustMaintainSet(account, value, timestamp); } @@ -198,7 +201,7 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr function getAccountFromVoter(address accountOrVoter) external view returns (address) { address authorizingAccount = authorizations[accountOrVoter]; if (authorizingAccount != address(0)) { - require(accounts[authorizingAccount].authorizations.voter == accountOrVoter); + require(accounts[authorizingAccount].authorizations.voting == accountOrVoter); return authorizingAccount; } else { require(isAccount(accountOrVoter)); @@ -207,7 +210,7 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr } function getTotalLockedGold() external view returns (uint256) { - return totalNonvoting.add(getElection().totalVotes()); + return totalNonvoting.add(getElection().getTotalVotes()); } function getAccountTotalLockedGold(address account) public view returns (uint256) { @@ -224,7 +227,7 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr function getAccountFromValidator(address accountOrValidator) public view returns (address) { address authorizingAccount = authorizations[accountOrValidator]; if (authorizingAccount != address(0)) { - require(accounts[authorizingAccount].authorizations.validator == accountOrValidator); + require(accounts[authorizingAccount].authorizations.validating == accountOrValidator); return authorizingAccount; } else { require(isAccount(accountOrValidator)); @@ -239,7 +242,7 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr */ function getVoterFromAccount(address account) public view returns (address) { require(isAccount(account)); - address voter = accounts[account].authorizations.voter; + address voter = accounts[account].authorizations.voting; return voter == address(0) ? account : voter; } @@ -250,7 +253,7 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr */ function getValidatorFromAccount(address account) public view returns (address) { require(isAccount(account)); - address validator = accounts[account].authorizations.validator; + address validator = accounts[account].authorizations.validating; return validator == address(0) ? account : validator; } diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index 85bccffa361..4f3555cea6f 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -140,8 +140,8 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi { _transferOwnership(msg.sender); setRegistry(registryAddress); - registrationRequirements.group = groupRequirement; - registrationRequirements.validator = validatorRequirement; + registrationRequirements = RegistrationRequirements(groupRequirement, validatorRequirement); + deregistrationLockups = DeregistrationLockups(groupLockup, validatorLockup); maxGroupSize = _maxGroupSize; } @@ -237,7 +237,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi bytes memory proofOfPossessionBytes = publicKeysData.slice(64, 48 + 96); require(checkProofOfPossession(proofOfPossessionBytes)); - address account = getValidators().getAccountFromValidator(msg.sender); + address account = getLockedGold().getAccountFromValidator(msg.sender); require(!isValidator(account) && !isValidatorGroup(account)); require(meetsValidatorRegistrationRequirement(account)); @@ -265,8 +265,8 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi * @param account The account. * @return Whether an account meets the requirements to register a validator. */ - function meetsValidatorRegistrationRequirement(address account) public returns (bool) { - getLockedGold().getAccountTotalLockedGold() >= registrationRequirements.validator; + function meetsValidatorRegistrationRequirement(address account) public view returns (bool) { + getLockedGold().getAccountTotalLockedGold(account) >= registrationRequirements.validator; } /** @@ -274,8 +274,8 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi * @param account The account. * @return Whether an account meets the requirements to register a group. */ - function meetsValidatorGroupRegistrationRequirement(address account) public returns (bool) { - getLockedGold().getAccountTotalLockedGold() >= registrationRequirements.group; + function meetsValidatorGroupRegistrationRequirement(address account) public view returns (bool) { + getLockedGold().getAccountTotalLockedGold(account) >= registrationRequirements.group; } /** @@ -285,7 +285,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi * @dev Fails if the account is not a validator. */ function deregisterValidator(uint256 index) external nonReentrant returns (bool) { - address account = getValidators().getAccountFromValidator(msg.sender); + address account = getLockedGold().getAccountFromValidator(msg.sender); require(isValidator(account)); Validator storage validator = validators[account]; if (validator.affiliation != address(0)) { @@ -305,7 +305,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi * @dev De-affiliates with the previously affiliated group if present. */ function affiliate(address group) external nonReentrant returns (bool) { - address account = getValidators().getAccountFromValidator(msg.sender); + address account = getLockedGold().getAccountFromValidator(msg.sender); require(isValidator(account) && isValidatorGroup(group)); Validator storage validator = validators[account]; if (validator.affiliation != address(0)) { @@ -322,7 +322,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi * @dev Fails if the account is not a validator with non-zero affiliation. */ function deaffiliate() external nonReentrant returns (bool) { - address account = getValidators().getAccountFromValidator(msg.sender); + address account = getLockedGold().getAccountFromValidator(msg.sender); require(isValidator(account)); Validator storage validator = validators[account]; require(validator.affiliation != address(0)); @@ -353,15 +353,16 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi // TODO(asa) // require(isFraction(commission)); require(members.length <= maxGroupSize); - address account = getValidators().getAccountFromValidator(msg.sender); + address account = getLockedGold().getAccountFromValidator(msg.sender); require(!isValidator(account) && !isValidatorGroup(account)); require(meetsValidatorGroupRegistrationRequirement(account)); ValidatorGroup storage group = groups[account]; group.name = name; group.url = url; + group.commission = FixidityLib.wrap(commission); for (uint256 i = 0; i < members.length; i = i.add(1)) { - group.addMember(members[i]); + _addMember(account, members[i]); } _groups.push(account); getLockedGold().setAccountMustMaintain(account, registrationRequirements.group, MAX_INT); @@ -376,7 +377,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi * @dev Fails if the account is not a validator group with no members. */ function deregisterValidatorGroup(uint256 index) external nonReentrant returns (bool) { - address account = getValidators().getAccountFromValidator(msg.sender); + address account = getLockedGold().getAccountFromValidator(msg.sender); // Only empty Validator Groups can be deregistered. require(isValidatorGroup(account) && groups[account].members.numElements == 0); delete groups[account]; @@ -393,16 +394,23 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi * @dev Fails if `validator` has not set their affiliation to this account. */ function addMember(address validator) external nonReentrant returns (bool) { - address account = getValidators().getAccountFromValidator(msg.sender); + address account = getLockedGold().getAccountFromValidator(msg.sender); require(isValidatorGroup(account) && isValidator(validator)); - ValidatorGroup storage group = groups[account]; - require(group.members.length < maxGroupSize); - require(validators[validator].affiliation == account && !group.members.contains(validator)); - group.members.push(validator); - emit ValidatorGroupMemberAdded(account, validator); + return _addMember(account, validator); + } + + function _addMember(address group, address validator) private returns (bool) { + ValidatorGroup storage _group = groups[group]; + require(_group.members.numElements < maxGroupSize); + require(validators[validator].affiliation == group && !_group.members.contains(validator)); + _group.members.push(validator); + emit ValidatorGroupMemberAdded(group, validator); return true; } + /** + * @notice De-affiliates a validator, removing it from the group for which it is a member. + /** * @notice Removes a member from a validator group. * @param validator The validator to remove from the group @@ -410,7 +418,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi * @dev Fails if `validator` is not a member of the account's group. */ function removeMember(address validator) external nonReentrant returns (bool) { - address account = getValidators().getAccountFromValidator(msg.sender); + address account = getLockedGold().getAccountFromValidator(msg.sender); require(isValidatorGroup(account) && isValidator(validator)); return _removeMember(account, validator); } @@ -434,7 +442,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi nonReentrant returns (bool) { - address account = getValidators().getAccountFromValidator(msg.sender); + address account = getLockedGold().getAccountFromValidator(msg.sender); require(isValidatorGroup(account) && isValidator(validator)); ValidatorGroup storage group = groups[account]; require(group.members.contains(validator)); @@ -480,19 +488,19 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi ) external view - returns (string memory, string memory, string memory, address[] memory) + returns (string memory, string memory, address[] memory) { require(isValidatorGroup(account)); ValidatorGroup storage group = groups[account]; return (group.name, group.url, group.members.getKeys()); } - function getNumGroupMembers(address account) public view returns (uint256) { + function getGroupNumMembers(address account) public view returns (uint256) { return groups[account].members.numElements; } function getTopValidatorsFromGroup(address account, uint256 n) external view returns (address[] memory) { - address[] memory topAccounts = groups[account].members.list.headN(n); + address[] memory topAccounts = groups[account].members.headN(n); address[] memory topValidators = new address[](n); for (uint256 i = 0; i < n; i = i.add(1)) { topValidators[i] = getLockedGold().getValidatorFromAccount(topAccounts[i]); @@ -500,10 +508,10 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi return topValidators; } - function getNumGroupMembers(address[] calldata accounts) external view returns (uint256) { + function getGroupsNumMembers(address[] calldata accounts) external view returns (uint256[] memory) { uint256[] memory numMembers = new uint256[](accounts.length); for (uint256 i = 0; i < accounts.length; i = i.add(1)) { - numMembers[i] = getNumGroupMembers(accounts[i]); + numMembers[i] = getGroupNumMembers(accounts[i]); } return numMembers; } @@ -584,7 +592,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi // Empty validator groups are not electable. if (groups[group].members.numElements == 0) { - getElection().markGroupUnelectable(group); + getElection().markGroupIneligible(group); } return true; } diff --git a/packages/protocol/contracts/governance/interfaces/IElection.sol b/packages/protocol/contracts/governance/interfaces/IElection.sol index dbb105d708b..c6935b95abb 100644 --- a/packages/protocol/contracts/governance/interfaces/IElection.sol +++ b/packages/protocol/contracts/governance/interfaces/IElection.sol @@ -2,5 +2,8 @@ pragma solidity ^0.5.3; interface IElection { - function isVoting(address) external view returns(bool); + function getTotalVotes() external view returns (uint256); + function getAccountTotalVotes(address account) external view returns (uint256); + function markGroupIneligible(address) external; + function electValidators() external view returns (address[] memory); } diff --git a/packages/protocol/contracts/governance/interfaces/IGovernance.sol b/packages/protocol/contracts/governance/interfaces/IGovernance.sol index ee0a5317b06..2209c4ebb6e 100644 --- a/packages/protocol/contracts/governance/interfaces/IGovernance.sol +++ b/packages/protocol/contracts/governance/interfaces/IGovernance.sol @@ -44,9 +44,8 @@ interface IGovernance { function getUpvotes(uint256) external view returns (uint256); function getQueue() external view returns (uint256[] memory, uint256[] memory); function getDequeue() external view returns (uint256[] memory); - function getUpvotedProposal(address) external view returns (uint256); + function getUpvoteRecord(address) external view returns (uint256, uint256); function getMostRecentReferendumProposal(address) external view returns (uint256); - function isVoting(address) external view returns (bool); function isQueued(uint256) external view returns (bool); function isProposalPassing(uint256) external view returns (bool); } diff --git a/packages/protocol/contracts/governance/interfaces/ILockedGold.sol b/packages/protocol/contracts/governance/interfaces/ILockedGold.sol index de7d7888736..1f961197356 100644 --- a/packages/protocol/contracts/governance/interfaces/ILockedGold.sol +++ b/packages/protocol/contracts/governance/interfaces/ILockedGold.sol @@ -2,8 +2,12 @@ pragma solidity ^0.5.3; interface ILockedGold { - function initialize(address, uint256) external; function getAccountFromVoter(address) external view returns (address); + function getAccountFromValidator(address) external view returns (address); + function getValidatorFromAccount(address) external view returns (address); function incrementNonvotingAccountBalance(address, uint256) external; function decrementNonvotingAccountBalance(address, uint256) external; + function getAccountTotalLockedGold(address) external view returns (uint256); + function getTotalLockedGold() external view returns (uint256); + function setAccountMustMaintain(address, uint256, uint256) external returns (bool); } diff --git a/packages/protocol/contracts/governance/interfaces/IValidators.sol b/packages/protocol/contracts/governance/interfaces/IValidators.sol index 757c0f29c64..5bc937ee60f 100644 --- a/packages/protocol/contracts/governance/interfaces/IValidators.sol +++ b/packages/protocol/contracts/governance/interfaces/IValidators.sol @@ -2,7 +2,8 @@ pragma solidity ^0.5.3; interface IValidators { - function electValidators() external view returns (address[] memory); - function getNumGroupMembers(address) external view returns (uint256); + function getGroupNumMembers(address) external view returns (uint256); + function getGroupsNumMembers(address[] calldata) external view returns (uint256[] memory); function getNumRegisteredValidators() external view returns (uint256); + function getTopValidatorsFromGroup(address, uint256) external view returns (address[] memory); } From c19b8b60d35e759484ffb1f399f996cfcbe46325 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Fri, 20 Sep 2019 20:04:58 -0700 Subject: [PATCH 006/149] Updated validators test --- .../contracts/common/UsingRegistry.sol | 5 + .../contracts/governance/LockedGold.sol | 32 +- .../contracts/governance/Validators.sol | 9 +- .../governance/test/MockElection.sol | 15 + .../governance/test/MockLockedGold.sol | 29 + .../protocol/contracts/stability/Exchange.sol | 9 +- .../protocol/contracts/stability/Reserve.sol | 4 +- .../test/governance/bondeddeposits.ts | 1067 ------------- packages/protocol/test/governance/election.ts | 1315 +++++++++++++++++ .../protocol/test/governance/lockedgold.ts | 469 ++++++ .../protocol/test/governance/validators.ts | 922 ++++-------- 11 files changed, 2153 insertions(+), 1723 deletions(-) create mode 100644 packages/protocol/contracts/governance/test/MockElection.sol delete mode 100644 packages/protocol/test/governance/bondeddeposits.ts create mode 100644 packages/protocol/test/governance/election.ts create mode 100644 packages/protocol/test/governance/lockedgold.ts diff --git a/packages/protocol/contracts/common/UsingRegistry.sol b/packages/protocol/contracts/common/UsingRegistry.sol index 5a19dcef961..2e4ced1e5d3 100644 --- a/packages/protocol/contracts/common/UsingRegistry.sol +++ b/packages/protocol/contracts/common/UsingRegistry.sol @@ -2,6 +2,7 @@ pragma solidity ^0.5.3; import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; +import "./interfaces/IERC20Token.sol"; import "./interfaces/IRegistry.sol"; import "../governance/interfaces/IElection.sol"; @@ -51,6 +52,10 @@ contract UsingRegistry is Ownable { return IElection(registry.getAddressForOrDie(ELECTION_REGISTRY_ID)); } + function getGoldToken() internal view returns(IERC20Token) { + return IERC20Token(registry.getAddressForOrDie(GOLD_TOKEN_REGISTRY_ID)); + } + function getLockedGold() internal view returns(ILockedGold) { return ILockedGold(registry.getAddressForOrDie(LOCKED_GOLD_REGISTRY_ID)); } diff --git a/packages/protocol/contracts/governance/LockedGold.sol b/packages/protocol/contracts/governance/LockedGold.sol index 3f7fab5d9a2..d860949a348 100644 --- a/packages/protocol/contracts/governance/LockedGold.sol +++ b/packages/protocol/contracts/governance/LockedGold.sol @@ -7,7 +7,6 @@ import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; import "./interfaces/ILockedGold.sol"; import "../common/Initializable.sol"; -import "../common/interfaces/IERC20Token.sol"; import "../common/Signatures.sol"; import "../common/UsingRegistry.sol"; @@ -51,7 +50,7 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr mapping(address => Account) private accounts; // Maps voting and validating keys to the account that provided the authorization. - mapping(address => address) public authorizations; + mapping(address => address) public authorizedBy; uint256 public totalNonvoting; uint256 public unlockingPeriod; @@ -113,11 +112,11 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr * @notice Locks gold to be used for voting. * @param value The amount of gold to be locked. */ - function lock(uint256 value) external payable nonReentrant { + function lock() external payable nonReentrant { require(isAccount(msg.sender)); - require(msg.value == value && value > 0); - _incrementNonvotingAccountBalance(msg.sender, value); - emit GoldLocked(msg.sender, value); + require(msg.value > 0); + _incrementNonvotingAccountBalance(msg.sender, msg.value); + emit GoldLocked(msg.sender, msg.value); } function incrementNonvotingAccountBalance(address account, uint256 value) external onlyRegisteredContract('Election') { @@ -172,8 +171,7 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr require(now >= pendingWithdrawal.timestamp); uint256 value = pendingWithdrawal.value; deletePendingWithdrawal(account.balances.pendingWithdrawals, index); - IERC20Token goldToken = IERC20Token(registry.getAddressFor(GOLD_TOKEN_REGISTRY_ID)); - require(goldToken.transfer(msg.sender, value)); + require(getGoldToken().transfer(msg.sender, value)); emit GoldWithdrawn(msg.sender, value); } @@ -199,7 +197,7 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr * @return The associated account. */ function getAccountFromVoter(address accountOrVoter) external view returns (address) { - address authorizingAccount = authorizations[accountOrVoter]; + address authorizingAccount = authorizedBy[accountOrVoter]; if (authorizingAccount != address(0)) { require(accounts[authorizingAccount].authorizations.voting == accountOrVoter); return authorizingAccount; @@ -213,11 +211,19 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr return totalNonvoting.add(getElection().getTotalVotes()); } + function getNonvotingLockedGold() external view returns (uint256) { + return totalNonvoting; + } + function getAccountTotalLockedGold(address account) public view returns (uint256) { uint256 total = accounts[account].balances.nonvoting; return total.add(getElection().getAccountTotalVotes(account)); } + function getAccountNonvotingLockedGold(address account) external view returns (uint256) { + return accounts[account].balances.nonvoting; + } + /** * @notice Returns the account associated with `accountOrValidator`. * @param accountOrValidator The address of the account or authorized validator. @@ -225,7 +231,7 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr * @return The associated account. */ function getAccountFromValidator(address accountOrValidator) public view returns (address) { - address authorizingAccount = authorizations[accountOrValidator]; + address authorizingAccount = authorizedBy[accountOrValidator]; if (authorizingAccount != address(0)) { require(accounts[authorizingAccount].authorizations.validating == accountOrValidator); return authorizingAccount; @@ -282,8 +288,8 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr address signer = Signatures.getSignerOfAddress(msg.sender, v, r, s); require(signer == current); - authorizations[previous] = address(0); - authorizations[current] = msg.sender; + authorizedBy[previous] = address(0); + authorizedBy[current] = msg.sender; } function isAccount(address account) internal view returns (bool) { @@ -295,7 +301,7 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr } function isNotAuthorized(address account) internal view returns (bool) { - return (authorizations[account] == address(0)); + return (authorizedBy[account] == address(0)); } function deletePendingWithdrawal(PendingWithdrawal[] storage list, uint256 index) private { diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index 4f3555cea6f..ea344f95c82 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -121,7 +121,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi * @notice Initializes critical variables. * @param registryAddress The address of the registry contract. * @param groupRequirement The minimum locked gold needed to register a group. - * @param validatorRequirement The minimum locked gold needed to register a validator. + * @param validatorRequirement The minimum locked gold needed to register a validator. * @param groupLockup The duration the above gold remains locked after deregistration. * @param validatorLockup The duration the above gold remains locked after deregistration. * @param _maxGroupSize The maximum group size. @@ -160,7 +160,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi /** * @notice Updates the minimum gold requirements to register a validator group or validator. * @param groupRequirement The minimum locked gold needed to register a group. - * @param validatorRequirement The minimum locked gold needed to register a validator. + * @param validatorRequirement The minimum locked gold needed to register a validator. * @return True upon success. * @dev The new requirement is only enforced for future validator or group registrations. */ @@ -342,7 +342,6 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi string calldata name, string calldata url, uint256 commission, - address[] calldata members ) external nonReentrant @@ -352,7 +351,6 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi require(bytes(url).length > 0); // TODO(asa) // require(isFraction(commission)); - require(members.length <= maxGroupSize); address account = getLockedGold().getAccountFromValidator(msg.sender); require(!isValidator(account) && !isValidatorGroup(account)); require(meetsValidatorGroupRegistrationRequirement(account)); @@ -361,9 +359,6 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi group.name = name; group.url = url; group.commission = FixidityLib.wrap(commission); - for (uint256 i = 0; i < members.length; i = i.add(1)) { - _addMember(account, members[i]); - } _groups.push(account); getLockedGold().setAccountMustMaintain(account, registrationRequirements.group, MAX_INT); emit ValidatorGroupRegistered(account, name, url); diff --git a/packages/protocol/contracts/governance/test/MockElection.sol b/packages/protocol/contracts/governance/test/MockElection.sol new file mode 100644 index 00000000000..e0e85344631 --- /dev/null +++ b/packages/protocol/contracts/governance/test/MockElection.sol @@ -0,0 +1,15 @@ +pragma solidity ^0.5.3; + +import "../interfaces/IElection.sol"; + +/** + * @title Holds a list of addresses of validators + */ +contract MockElection is IElection { + + mapping(address => bool) public isIneligible; + + function markGroupIneligible(address account) external { + isIneligible[account] = true; + } +} diff --git a/packages/protocol/contracts/governance/test/MockLockedGold.sol b/packages/protocol/contracts/governance/test/MockLockedGold.sol index f223c548a43..5d757728b8d 100644 --- a/packages/protocol/contracts/governance/test/MockLockedGold.sol +++ b/packages/protocol/contracts/governance/test/MockLockedGold.sol @@ -7,4 +7,33 @@ import "../interfaces/ILockedGold.sol"; * @title A mock LockedGold for testing. */ contract MockLockedGold is ILockedGold { + struct MustMaintain { + uint256 value; + uint256 timestamp; + } + + mapping(address => uint256) public totalLockedGold; + mapping(address => MustMaintain) public mustMaintain; + + + function getAccountFromValidator(address accountOrValidator) external view returns (address) { + return accountOrValidator; + } + + function setAccountMustMaintain(address account, uint256 value, uint256 timestamp) external { + mustMaintain[account] = MustMaintain(value, timestamp); + } + + function getAccountMustMaintain(address account) external view returns (uint256, uint256) { + MustMaintain storage mustMaintain = mustMaintain[account]; + return (mustMaintain.value, mustMaintain.timestamp); + } + + function setAccountTotalLockedGold(address account, uint256 value) external { + totalLockedGold[account] = value; + } + + function getAccountTotalLockedGold(address account) external view returns (uint256) { + return totalLockedGold[account]; + } } diff --git a/packages/protocol/contracts/stability/Exchange.sol b/packages/protocol/contracts/stability/Exchange.sol index 12ce4d5020d..03466f99474 100644 --- a/packages/protocol/contracts/stability/Exchange.sol +++ b/packages/protocol/contracts/stability/Exchange.sol @@ -11,7 +11,6 @@ import "../common/FractionUtil.sol"; import "../common/Initializable.sol"; import "../common/FixidityLib.sol"; import "../common/UsingRegistry.sol"; -import "../common/interfaces/IERC20Token.sol"; /** @@ -138,7 +137,7 @@ contract Exchange is IExchange, Initializable, Ownable, UsingRegistry, Reentranc goldBucket = goldBucket.add(sellAmount); stableBucket = stableBucket.sub(buyAmount); require( - gold().transferFrom(msg.sender, address(reserve), sellAmount), + getGoldToken().transferFrom(msg.sender, address(reserve), sellAmount), "Transfer of sell token failed" ); require(IStableToken(stable).mint(msg.sender, buyAmount), "Mint of stable token failed"); @@ -332,7 +331,7 @@ contract Exchange is IExchange, Initializable, Ownable, UsingRegistry, Reentranc } function getUpdatedGoldBucket() private view returns (uint256) { - uint256 reserveGoldBalance = gold().balanceOf(registry.getAddressForOrDie(RESERVE_REGISTRY_ID)); + uint256 reserveGoldBalance = getGoldToken().balanceOf(registry.getAddressForOrDie(RESERVE_REGISTRY_ID)); return reserveFraction.multiply(FixidityLib.newFixed(reserveGoldBalance)).fromFixed(); } @@ -388,8 +387,4 @@ contract Exchange is IExchange, Initializable, Ownable, UsingRegistry, Reentranc ISortedOracles(registry.getAddressForOrDie(SORTED_ORACLES_REGISTRY_ID)).medianRate(stable); return FractionUtil.Fraction(rateNumerator, rateDenominator); } - - function gold() private view returns (IERC20Token) { - return IERC20Token(registry.getAddressForOrDie(GOLD_TOKEN_REGISTRY_ID)); - } } diff --git a/packages/protocol/contracts/stability/Reserve.sol b/packages/protocol/contracts/stability/Reserve.sol index 7321d53d8d6..c1abf178b0d 100644 --- a/packages/protocol/contracts/stability/Reserve.sol +++ b/packages/protocol/contracts/stability/Reserve.sol @@ -10,7 +10,6 @@ import "./interfaces/IStableToken.sol"; import "../common/Initializable.sol"; import "../common/UsingRegistry.sol"; -import "../common/interfaces/IERC20Token.sol"; /** @@ -154,8 +153,7 @@ contract Reserve is IReserve, Ownable, Initializable, UsingRegistry, ReentrancyG returns (bool) { require(isSpender[msg.sender], "sender not allowed to transfer Reserve funds"); - IERC20Token goldToken = IERC20Token(registry.getAddressForOrDie(GOLD_TOKEN_REGISTRY_ID)); - require(goldToken.transfer(to, value), "transfer of gold token failed"); + require(getGoldToken().transfer(to, value), "transfer of gold token failed"); return true; } diff --git a/packages/protocol/test/governance/bondeddeposits.ts b/packages/protocol/test/governance/bondeddeposits.ts deleted file mode 100644 index 32e9fabf007..00000000000 --- a/packages/protocol/test/governance/bondeddeposits.ts +++ /dev/null @@ -1,1067 +0,0 @@ -import { CeloContractName } from '@celo/protocol/lib/registry-utils' -import { - assertEqualBN, - assertLogMatches, - assertRevert, - NULL_ADDRESS, - timeTravel, -} from '@celo/protocol/lib/test-utils' -import BigNumber from 'bignumber.js' -import { - LockedGoldContract, - LockedGoldInstance, - MockGoldTokenContract, - MockGoldTokenInstance, - MockGovernanceContract, - MockGovernanceInstance, - MockValidatorsContract, - MockValidatorsInstance, - RegistryContract, - RegistryInstance, -} from 'types' - -const LockedGold: LockedGoldContract = artifacts.require('LockedGold') -const Registry: RegistryContract = artifacts.require('Registry') -const MockGoldToken: MockGoldTokenContract = artifacts.require('MockGoldToken') -const MockGovernance: MockGovernanceContract = artifacts.require('MockGovernance') -const MockValidators: MockValidatorsContract = artifacts.require('MockValidators') - -// @ts-ignore -// TODO(mcortesi): Use BN -LockedGold.numberFormat = 'BigNumber' - -const HOUR = 60 * 60 -const DAY = 24 * HOUR -const YEAR = 365 * DAY - -// TODO(asa): Test reward redemption -contract('LockedGold', (accounts: string[]) => { - let account = accounts[0] - const nonOwner = accounts[1] - const maxNoticePeriod = 2 * YEAR - let mockGoldToken: MockGoldTokenInstance - let mockGovernance: MockGovernanceInstance - let mockValidators: MockValidatorsInstance - let lockedGold: LockedGoldInstance - let registry: RegistryInstance - - const getParsedSignatureOfAddress = async (address: string, signer: string) => { - // @ts-ignore - const hash = web3.utils.soliditySha3({ type: 'address', value: address }) - const signature = (await web3.eth.sign(hash, signer)).slice(2) - return { - r: `0x${signature.slice(0, 64)}`, - s: `0x${signature.slice(64, 128)}`, - v: web3.utils.hexToNumber(signature.slice(128, 130)) + 27, - } - } - - enum roles { - validating, - voting, - rewards, - } - const forEachRole = (tests: (arg0: roles) => void) => - Object.keys(roles) - .slice(3) - .map((role) => describe(`when dealing with ${role} role`, () => tests(roles[role]))) - - beforeEach(async () => { - lockedGold = await LockedGold.new() - mockGoldToken = await MockGoldToken.new() - mockGovernance = await MockGovernance.new() - mockValidators = await MockValidators.new() - registry = await Registry.new() - await registry.setAddressFor(CeloContractName.GoldToken, mockGoldToken.address) - await registry.setAddressFor(CeloContractName.Governance, mockGovernance.address) - await registry.setAddressFor(CeloContractName.Validators, mockValidators.address) - await lockedGold.initialize(registry.address, maxNoticePeriod) - await lockedGold.createAccount() - }) - - describe('#initialize()', () => { - it('should set the owner', async () => { - const owner: string = await lockedGold.owner() - assert.equal(owner, account) - }) - - it('should set the maxNoticePeriod', async () => { - const actual = await lockedGold.maxNoticePeriod() - assert.equal(actual.toNumber(), maxNoticePeriod) - }) - - it('should set the registry address', async () => { - const registryAddress: string = await lockedGold.registry() - assert.equal(registryAddress, registry.address) - }) - - it('should revert if already initialized', async () => { - await assertRevert(lockedGold.initialize(registry.address, maxNoticePeriod)) - }) - }) - - describe('#setRegistry()', () => { - const anAddress: string = accounts[2] - - it('should set the registry when called by the owner', async () => { - await lockedGold.setRegistry(anAddress) - assert.equal(await lockedGold.registry(), anAddress) - }) - - it('should revert when not called by the owner', async () => { - await assertRevert(lockedGold.setRegistry(anAddress, { from: nonOwner })) - }) - }) - - describe('#setMaxNoticePeriod()', () => { - it('should set maxNoticePeriod when called by the owner', async () => { - await lockedGold.setMaxNoticePeriod(1) - assert.equal((await lockedGold.maxNoticePeriod()).toNumber(), 1) - }) - - it('should emit a MaxNoticePeriodSet event', async () => { - const resp = await lockedGold.setMaxNoticePeriod(1) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches(log, 'MaxNoticePeriodSet', { - maxNoticePeriod: new BigNumber(1), - }) - }) - - it('should revert when not called by the owner', async () => { - await assertRevert(lockedGold.setMaxNoticePeriod(1, { from: nonOwner })) - }) - }) - - describe('#delegateRole()', () => { - const delegate = accounts[1] - let sig - - beforeEach(async () => { - sig = await getParsedSignatureOfAddress(account, delegate) - }) - - forEachRole((role) => { - it('should set the role delegate', async () => { - await lockedGold.delegateRole(role, delegate, sig.v, sig.r, sig.s) - assert.equal(await lockedGold.delegations(delegate), account) - assert.equal(await lockedGold.getDelegateFromAccountAndRole(account, role), delegate) - assert.equal(await lockedGold.getAccountFromDelegateAndRole(delegate, role), account) - }) - - it('should emit a RoleDelegated event', async () => { - const resp = await lockedGold.delegateRole(role, delegate, sig.v, sig.r, sig.s) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches(log, 'RoleDelegated', { - role, - account, - delegate, - }) - }) - - it('should revert if the delegate is an account', async () => { - await lockedGold.createAccount({ from: delegate }) - await assertRevert(lockedGold.delegateRole(role, delegate, sig.v, sig.r, sig.s)) - }) - - it('should revert if the address is already being delegated to', async () => { - const otherAccount = accounts[2] - const otherSig = await getParsedSignatureOfAddress(otherAccount, delegate) - await lockedGold.createAccount({ from: otherAccount }) - await lockedGold.delegateRole(role, delegate, otherSig.v, otherSig.r, otherSig.s, { - from: otherAccount, - }) - await assertRevert(lockedGold.delegateRole(role, delegate, sig.v, sig.r, sig.s)) - }) - - it('should revert if the signature is incorrect', async () => { - const nonDelegate = accounts[3] - const incorrectSig = await getParsedSignatureOfAddress(account, nonDelegate) - await assertRevert( - lockedGold.delegateRole(role, delegate, incorrectSig.v, incorrectSig.r, incorrectSig.s) - ) - }) - - describe('when a previous delegation has been made', async () => { - const newDelegate = accounts[2] - let newSig - beforeEach(async () => { - await lockedGold.delegateRole(role, delegate, sig.v, sig.r, sig.s) - newSig = await getParsedSignatureOfAddress(account, newDelegate) - }) - - it('should set the new delegate', async () => { - await lockedGold.delegateRole(role, newDelegate, newSig.v, newSig.r, newSig.s) - assert.equal(await lockedGold.delegations(newDelegate), account) - assert.equal(await lockedGold.getDelegateFromAccountAndRole(account, role), newDelegate) - assert.equal(await lockedGold.getAccountFromDelegateAndRole(newDelegate, role), account) - }) - - it('should reset the previous delegate', async () => { - await lockedGold.delegateRole(role, newDelegate, newSig.v, newSig.r, newSig.s) - assert.equal(await lockedGold.delegations(delegate), NULL_ADDRESS) - }) - }) - }) - }) - - describe('#freezeVoting()', () => { - it('should set the account voting to frozen', async () => { - await lockedGold.freezeVoting() - assert.isTrue(await lockedGold.isVotingFrozen(account)) - }) - - it('should emit a VotingFrozen event', async () => { - const resp = await lockedGold.freezeVoting() - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches(log, 'VotingFrozen', { - account, - }) - }) - - it('should revert if the account voting is already frozen', async () => { - await lockedGold.freezeVoting() - await assertRevert(lockedGold.freezeVoting()) - }) - }) - - describe('#unfreezeVoting()', () => { - beforeEach(async () => { - await lockedGold.freezeVoting() - }) - - it('should set the account voting to unfrozen', async () => { - await lockedGold.unfreezeVoting() - assert.isFalse(await lockedGold.isVotingFrozen(account)) - }) - - it('should emit a VotingUnfrozen event', async () => { - const resp = await lockedGold.unfreezeVoting() - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches(log, 'VotingUnfrozen', { - account, - }) - }) - - it('should revert if the account voting is already unfrozen', async () => { - await lockedGold.unfreezeVoting() - await assertRevert(lockedGold.unfreezeVoting()) - }) - }) - - describe('#newCommitment()', () => { - const noticePeriod = 1 * DAY + 1 * HOUR - const value = 1000 - const expectedWeight = 1033 - - it('should add a Locked Gold commitment', async () => { - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - await lockedGold.newCommitment(noticePeriod, { value }) - const noticePeriods = await lockedGold.getNoticePeriods(account) - assert.equal(noticePeriods.length, 1) - assert.equal(noticePeriods[0].toNumber(), noticePeriod) - const [lockedValue, index] = await lockedGold.getLockedCommitment(account, noticePeriod) - assert.equal(lockedValue.toNumber(), value) - assert.equal(index.toNumber(), 0) - }) - - it('should update the account weight', async () => { - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - await lockedGold.newCommitment(noticePeriod, { value }) - const weight = await lockedGold.getAccountWeight(account) - assert.equal(weight.toNumber(), expectedWeight) - }) - - it('should update the total weight', async () => { - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - await lockedGold.newCommitment(noticePeriod, { value }) - const totalWeight = await lockedGold.totalWeight() - assert.equal(totalWeight.toNumber(), expectedWeight) - }) - - it('should emit a NewCommitment event', async () => { - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - const resp = await lockedGold.newCommitment(noticePeriod, { value }) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches(log, 'NewCommitment', { - account, - value: new BigNumber(value), - noticePeriod: new BigNumber(noticePeriod), - }) - }) - - it('should revert when the specified notice period is too large', async () => { - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - await assertRevert(lockedGold.newCommitment(maxNoticePeriod + 1, { value })) - }) - - it('should revert when the specified value is 0', async () => { - await assertRevert(lockedGold.newCommitment(noticePeriod)) - }) - - it('should revert when the account does not exist', async () => { - await assertRevert(lockedGold.newCommitment(noticePeriod, { value, from: accounts[1] })) - }) - - it('should revert if the caller is voting', async () => { - await mockGovernance.setVoting(account) - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - await assertRevert(lockedGold.newCommitment(noticePeriod, { value })) - }) - }) - - describe('#notifyCommitment()', () => { - const noticePeriod = 60 * 60 * 24 // 1 day - const value = 1000 - beforeEach(async () => { - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - await lockedGold.newCommitment(noticePeriod, { value }) - }) - - it('should add a notified deposit', async () => { - await lockedGold.notifyCommitment(value, noticePeriod) - const availabilityTime = new BigNumber(noticePeriod).plus( - (await web3.eth.getBlock('latest')).timestamp - ) - const availabilityTimes = await lockedGold.getAvailabilityTimes(account) - assert.equal(availabilityTimes.length, 1) - assert.equal(availabilityTimes[0].toNumber(), availabilityTime.toNumber()) - - const [notifiedValue, index] = await lockedGold.getNotifiedCommitment( - account, - availabilityTime - ) - assert.equal(notifiedValue.toNumber(), value) - assert.equal(index.toNumber(), 0) - }) - - it('should remove the Locked Gold commitment', async () => { - await lockedGold.notifyCommitment(value, noticePeriod) - const noticePeriods = await lockedGold.getNoticePeriods(account) - assert.equal(noticePeriods.length, 0) - const [lockedValue, index] = await lockedGold.getLockedCommitment(account, noticePeriod) - assert.equal(lockedValue.toNumber(), 0) - assert.equal(index.toNumber(), 0) - }) - - it('should update the account weight', async () => { - await lockedGold.notifyCommitment(value, noticePeriod) - const weight = await lockedGold.getAccountWeight(account) - assert.equal(weight.toNumber(), value) - }) - - it('should update the total weight', async () => { - await lockedGold.notifyCommitment(value, noticePeriod) - const totalWeight = await lockedGold.totalWeight() - assert.equal(totalWeight.toNumber(), value) - }) - - it('should emit a CommitmentNotified event', async () => { - const resp = await lockedGold.notifyCommitment(value, noticePeriod) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches(log, 'CommitmentNotified', { - account, - value: new BigNumber(value), - noticePeriod: new BigNumber(noticePeriod), - availabilityTime: new BigNumber(noticePeriod).plus( - (await web3.eth.getBlock('latest')).timestamp - ), - }) - }) - - it('should revert when the value of the Locked Gold commitment is 0', async () => { - await assertRevert(lockedGold.notifyCommitment(1, noticePeriod + 1)) - }) - - it('should revert when value is greater than the value of the Locked Gold commitment', async () => { - await assertRevert(lockedGold.notifyCommitment(value + 1, noticePeriod)) - }) - - it('should revert when the value is 0', async () => { - await assertRevert(lockedGold.notifyCommitment(0, noticePeriod)) - }) - - it('should revert if the account is validating', async () => { - await mockValidators.setValidating(account) - await assertRevert(lockedGold.notifyCommitment(value, noticePeriod)) - }) - - it('should revert if the caller is voting', async () => { - await mockGovernance.setVoting(account) - await assertRevert(lockedGold.notifyCommitment(value, noticePeriod)) - }) - }) - - describe('#extendCommitment()', () => { - const value = 1000 - const expectedWeight = 1033 - let availabilityTime: BigNumber - - beforeEach(async () => { - // Set an initial notice period of just over one day, so that when we rebond, we're - // guaranteed that the new notice period is at least one day. - const noticePeriod = 1 * DAY + 1 * HOUR - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - await lockedGold.newCommitment(noticePeriod, { value }) - await lockedGold.notifyCommitment(value, noticePeriod) - availabilityTime = new BigNumber(noticePeriod).plus( - (await web3.eth.getBlock('latest')).timestamp - ) - }) - - it('should add a Locked Gold commitment', async () => { - await lockedGold.extendCommitment(value, availabilityTime) - const noticePeriods = await lockedGold.getNoticePeriods(account) - assert.equal(noticePeriods.length, 1) - const noticePeriod = availabilityTime - .minus((await web3.eth.getBlock('latest')).timestamp) - .toNumber() - assert.equal(noticePeriods[0].toNumber(), noticePeriod) - const [lockedValue, index] = await lockedGold.getLockedCommitment(account, noticePeriod) - assert.equal(lockedValue.toNumber(), value) - assert.equal(index.toNumber(), 0) - }) - - it('should remove a notified deposit', async () => { - await lockedGold.extendCommitment(value, availabilityTime) - const availabilityTimes = await lockedGold.getAvailabilityTimes(account) - assert.equal(availabilityTimes.length, 0) - const [notifiedValue, index] = await lockedGold.getNotifiedCommitment( - account, - availabilityTime - ) - assert.equal(notifiedValue.toNumber(), 0) - assert.equal(index.toNumber(), 0) - }) - - it('should update the account weight', async () => { - await lockedGold.extendCommitment(value, availabilityTime) - const weight = await lockedGold.getAccountWeight(account) - assert.equal(weight.toNumber(), expectedWeight) - }) - - it('should update the total weight', async () => { - await lockedGold.extendCommitment(value, availabilityTime) - const totalWeight = await lockedGold.totalWeight() - assert.equal(totalWeight.toNumber(), expectedWeight) - }) - - it('should emit a CommitmentExtended event', async () => { - const resp = await lockedGold.extendCommitment(value, availabilityTime) - const noticePeriod = availabilityTime.minus((await web3.eth.getBlock('latest')).timestamp) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches(log, 'CommitmentExtended', { - account, - value: new BigNumber(value), - noticePeriod, - availabilityTime, - }) - }) - - it('should revert when the notified deposit is withdrawable', async () => { - await timeTravel( - availabilityTime - .minus((await web3.eth.getBlock('latest')).timestamp) - .plus(1) - .toNumber(), - web3 - ) - await assertRevert(lockedGold.extendCommitment(value, availabilityTime)) - }) - - it('should revert when the value of the notified deposit is 0', async () => { - await assertRevert(lockedGold.extendCommitment(value, availabilityTime.plus(1))) - }) - - it('should revert when the value is 0', async () => { - await assertRevert(lockedGold.extendCommitment(0, availabilityTime)) - }) - - it('should revert if the caller is voting', async () => { - await mockGovernance.setVoting(account) - await assertRevert(lockedGold.extendCommitment(value, availabilityTime)) - }) - }) - - describe('#withdrawCommitment()', () => { - const noticePeriod = 1 * DAY - const value = 1000 - let availabilityTime: BigNumber - - beforeEach(async () => { - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - await lockedGold.newCommitment(noticePeriod, { value }) - await lockedGold.notifyCommitment(value, noticePeriod) - availabilityTime = new BigNumber(noticePeriod).plus( - (await web3.eth.getBlock('latest')).timestamp - ) - }) - - it('should remove the notified deposit', async () => { - await timeTravel(noticePeriod, web3) - await lockedGold.withdrawCommitment(availabilityTime) - - const availabilityTimes = await lockedGold.getAvailabilityTimes(account) - assert.equal(availabilityTimes.length, 0) - }) - - it('should update the account weight', async () => { - await timeTravel(noticePeriod, web3) - await lockedGold.withdrawCommitment(availabilityTime) - - const weight = await lockedGold.getAccountWeight(account) - assert.equal(weight.toNumber(), 0) - }) - - it('should update the total weight', async () => { - await timeTravel(noticePeriod, web3) - await lockedGold.withdrawCommitment(availabilityTime) - - const totalWeight = await lockedGold.totalWeight() - assert.equal(totalWeight.toNumber(), 0) - }) - - it('should emit a Withdrawal event', async () => { - await timeTravel(noticePeriod, web3) - const resp = await lockedGold.withdrawCommitment(availabilityTime) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches(log, 'Withdrawal', { - account, - value: new BigNumber(value), - }) - }) - - it('should revert if the account is validating', async () => { - await mockValidators.setValidating(account) - await assertRevert(lockedGold.withdrawCommitment(availabilityTime)) - }) - - it('should revert when the notice period has not passed', async () => { - await assertRevert(lockedGold.withdrawCommitment(availabilityTime)) - }) - - it('should revert when the value of the notified deposit is 0', async () => { - await timeTravel(noticePeriod, web3) - await assertRevert(lockedGold.withdrawCommitment(availabilityTime.plus(1))) - }) - - it('should revert if the caller is voting', async () => { - await timeTravel(noticePeriod, web3) - await mockGovernance.setVoting(account) - await assertRevert(lockedGold.withdrawCommitment(availabilityTime)) - }) - }) - - describe('#increaseNoticePeriod()', () => { - const noticePeriod = 1 * DAY - const value = 1000 - const increase = noticePeriod - const expectedWeight = 1047 - - beforeEach(async () => { - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - await lockedGold.newCommitment(noticePeriod, { value }) - }) - - it('should update the Locked Gold commitment', async () => { - await lockedGold.increaseNoticePeriod(value, noticePeriod, increase) - const noticePeriods = await lockedGold.getNoticePeriods(account) - assert.equal(noticePeriods.length, 1) - assert.equal(noticePeriods[0].toNumber(), noticePeriod + increase) - const [lockedValue, index] = await lockedGold.getLockedCommitment( - account, - noticePeriod + increase - ) - assert.equal(lockedValue.toNumber(), value) - assert.equal(index.toNumber(), 0) - }) - - it('should update the account weight', async () => { - await lockedGold.increaseNoticePeriod(value, noticePeriod, increase) - const weight = await lockedGold.getAccountWeight(account) - assert.equal(weight.toNumber(), expectedWeight) - }) - - it('should update the total weight', async () => { - await lockedGold.increaseNoticePeriod(value, noticePeriod, increase) - const totalWeight = await lockedGold.totalWeight() - assert.equal(totalWeight.toNumber(), expectedWeight) - }) - - it('should emit a NoticePeriodIncreased event', async () => { - const resp = await lockedGold.increaseNoticePeriod(value, noticePeriod, increase) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches(log, 'NoticePeriodIncreased', { - account, - value: new BigNumber(value), - noticePeriod: new BigNumber(noticePeriod), - increase: new BigNumber(increase), - }) - }) - - it('should revert if the value is 0', async () => { - await assertRevert(lockedGold.increaseNoticePeriod(0, noticePeriod, increase)) - }) - - it('should revert if the increase is 0', async () => { - await assertRevert(lockedGold.increaseNoticePeriod(value, noticePeriod, 0)) - }) - - it('should revert if the Locked Gold commitment is smaller than the value', async () => { - await assertRevert(lockedGold.increaseNoticePeriod(value, noticePeriod + 1, increase)) - }) - - it('should revert if the caller is voting', async () => { - await mockGovernance.setVoting(account) - await assertRevert(lockedGold.increaseNoticePeriod(value, noticePeriod, increase)) - }) - }) - - describe('#getAccountFromDelegateAndRole()', () => { - forEachRole((role) => { - describe('when the account is not delegating', () => { - it('should return the account when passed the account', async () => { - assert.equal(await lockedGold.getAccountFromDelegateAndRole(account, role), account) - }) - - it('should revert when passed a delegate that is not the role delegate', async () => { - const delegate = accounts[2] - const diffRole = (role + 1) % 3 - const sig = await getParsedSignatureOfAddress(account, delegate) - await lockedGold.delegateRole(role, delegate, sig.v, sig.r, sig.s) - await assertRevert(lockedGold.getAccountFromDelegateAndRole(delegate, diffRole)) - }) - }) - - describe('when the account is delegating', () => { - const delegate = accounts[1] - - beforeEach(async () => { - const sig = await getParsedSignatureOfAddress(account, delegate) - await lockedGold.delegateRole(role, delegate, sig.v, sig.r, sig.s) - }) - - it('should return the account when passed the delegate', async () => { - assert.equal(await lockedGold.getAccountFromDelegateAndRole(delegate, role), account) - }) - - it('should return the account when passed the account', async () => { - assert.equal(await lockedGold.getAccountFromDelegateAndRole(account, role), account) - }) - - it('should revert when passed a delegate that is not the role delegate', async () => { - const delegate = accounts[2] - const diffRole = (role + 1) % 3 - const sig = await getParsedSignatureOfAddress(account, delegate) - await lockedGold.delegateRole(role, delegate, sig.v, sig.r, sig.s) - await assertRevert(lockedGold.getAccountFromDelegateAndRole(delegate, diffRole)) - }) - }) - }) - }) - - describe('#getDelegateFromAccountAndRole()', () => { - forEachRole((role) => { - describe('when the account is not delegating', () => { - it('should return the account when passed the account', async () => { - assert.equal(await lockedGold.getDelegateFromAccountAndRole(account, role), account) - }) - }) - - describe('when the account is delegating', () => { - const delegate = accounts[1] - - beforeEach(async () => { - const sig = await getParsedSignatureOfAddress(account, delegate) - await lockedGold.delegateRole(role, delegate, sig.v, sig.r, sig.s) - }) - - it('should return the account when passed undelegated role', async () => { - const role2 = (role + 1) % 3 - assert.equal(await lockedGold.getDelegateFromAccountAndRole(account, role2), account) - }) - - it('should return the delegate when passed the delegated role', async () => { - assert.equal(await lockedGold.getDelegateFromAccountAndRole(account, role), delegate) - }) - }) - }) - }) - - describe('#isVoting()', () => { - describe('when the account is not delegating', () => { - it('should return false if the account is not voting in governance or validator elections', async () => { - assert.isFalse(await lockedGold.isVoting(account)) - }) - - it('should return true if the account is voting in governance', async () => { - await mockGovernance.setVoting(account) - assert.isTrue(await lockedGold.isVoting(account)) - }) - - it('should return true if the account is voting in validator elections', async () => { - await mockValidators.setVoting(account) - assert.isTrue(await lockedGold.isVoting(account)) - }) - - it('should return true if the account is voting in governance and validator elections', async () => { - await mockGovernance.setVoting(account) - await mockValidators.setVoting(account) - assert.isTrue(await lockedGold.isVoting(account)) - }) - }) - - describe('when the account is delegating', () => { - const delegate = accounts[1] - - beforeEach(async () => { - const sig = await getParsedSignatureOfAddress(account, delegate) - await lockedGold.delegateRole(roles.voting, delegate, sig.v, sig.r, sig.s) - }) - - it('should return false if the delegate is not voting in governance or validator elections', async () => { - assert.isFalse(await lockedGold.isVoting(account)) - }) - - it('should return true if the delegate is voting in governance', async () => { - await mockGovernance.setVoting(delegate) - assert.isTrue(await lockedGold.isVoting(account)) - }) - - it('should return true if the delegate is voting in validator elections', async () => { - await mockValidators.setVoting(delegate) - assert.isTrue(await lockedGold.isVoting(account)) - }) - - it('should return true if the delegate is voting in governance and validator elections', async () => { - await mockGovernance.setVoting(delegate) - await mockValidators.setVoting(delegate) - assert.isTrue(await lockedGold.isVoting(account)) - }) - }) - }) - - describe('#getCommitmentWeight()', () => { - const value = new BigNumber(521000) - const oneDay = new BigNumber(DAY) - it('should return the commitment value when notice period is zero', async () => { - const noticePeriod = new BigNumber(0) - assertEqualBN(await lockedGold.getCommitmentWeight(value, noticePeriod), value) - }) - - it('should return the commitment value when notice period is less than one day', async () => { - const noticePeriod = oneDay.minus(1) - assertEqualBN(await lockedGold.getCommitmentWeight(value, noticePeriod), value) - }) - - it('should return the commitment value times 1.0333 when notice period is one day', async () => { - const noticePeriod = oneDay - assertEqualBN( - await lockedGold.getCommitmentWeight(value, noticePeriod), - value.times(1.0333).integerValue(BigNumber.ROUND_DOWN) - ) - }) - - it('should return the commitment value times 1.047 when notice period is two days', async () => { - const noticePeriod = oneDay.times(2) - assertEqualBN( - await lockedGold.getCommitmentWeight(value, noticePeriod), - value.times(1.047).integerValue(BigNumber.ROUND_DOWN) - ) - }) - - it('should return the commitment value times 1.1823 when notice period is 30 days', async () => { - const noticePeriod = oneDay.times(30) - assertEqualBN( - await lockedGold.getCommitmentWeight(value, noticePeriod), - value.times(1.1823).integerValue(BigNumber.ROUND_DOWN) - ) - }) - - it('should return the commitment value times 2.103 when notice period is 3 years', async () => { - const noticePeriod = oneDay.times(365).times(3) - assertEqualBN( - await lockedGold.getCommitmentWeight(value, noticePeriod), - value.times(2.103).integerValue(BigNumber.ROUND_DOWN) - ) - }) - }) - - describe('when there are multiple commitments, notifies, rebondings, notice period increases, and withdrawals', () => { - beforeEach(async () => { - for (const accountToCreate of accounts) { - // Account for `account` has already been created. - if (accountToCreate !== account) { - await lockedGold.createAccount({ from: accountToCreate }) - } - } - }) - - enum ActionType { - Deposit = 'Deposit', - Notify = 'Notify', - Increase = 'Increase', - Rebond = 'Rebond', - Withdraw = 'Withdraw', - } - - const initializeState = (numAccounts: number) => { - const locked: Map> = new Map() - const notified: Map> = new Map() - const noticePeriods: Map> = new Map() - const availabilityTimes: Map> = new Map() - const selectedAccounts = accounts.slice(0, numAccounts) - for (const acc of selectedAccounts) { - // Map keys, set elements appear not to be able to be BigNumbers, so we use strings instead. - locked.set(acc, new Map()) - notified.set(acc, new Map()) - noticePeriods.set(acc, new Set([])) - availabilityTimes.set(acc, new Set([])) - } - - return { locked, notified, noticePeriods, availabilityTimes, selectedAccounts } - } - - const rndElement = (elems: A[]) => { - return elems[ - Math.floor( - BigNumber.random() - .times(elems.length) - .toNumber() - ) - ] - } - const rndSetElement = (s: Set) => rndElement(Array.from(s)) - - const getOrElse = (map: Map, key: B, defaultValue: A) => - map.has(key) ? map.get(key) : defaultValue - - const executeActionsAndAssertState = async (numActions: number, numAccounts: number) => { - const { - selectedAccounts, - locked, - notified, - noticePeriods, - availabilityTimes, - } = initializeState(numAccounts) - - for (let i = 0; i < numActions; i++) { - const blockTime = 5 - await timeTravel(blockTime, web3) - account = rndElement(selectedAccounts) - - const accountLockedGold = locked.get(account) - const accountNotifiedCommitments = notified.get(account) - const accountNoticePeriods = noticePeriods.get(account) - const accountAvailabilityTimes = availabilityTimes.get(account) - - const getWithdrawableAvailabilityTimes = async (): Promise> => { - const nextTimestamp = new BigNumber((await web3.eth.getBlock('latest')).timestamp) - const items: string[] = Array.from(accountAvailabilityTimes) - return new Set(items.filter((x: string) => nextTimestamp.gt(x))) - } - - const getRebondableAvailabilityTimes = async (): Promise> => { - const nextTimestamp = new BigNumber((await web3.eth.getBlock('latest')).timestamp).plus( - blockTime - ) - const items: string[] = Array.from(accountAvailabilityTimes) - // Subtract one to cover edge case where block time is 6 seconds. - return new Set(items.filter((x: string) => nextTimestamp.plus(1).lt(x))) - } - - // Select random action type. - const actionTypeOptions = [ActionType.Deposit] - if (accountNoticePeriods.size > 0) { - actionTypeOptions.push(ActionType.Notify) - actionTypeOptions.push(ActionType.Increase) - } - const rebondableAvailabilityTimes = await getRebondableAvailabilityTimes() - if (rebondableAvailabilityTimes.size > 0) { - // Push twice to increase likelihood - actionTypeOptions.push(ActionType.Rebond) - actionTypeOptions.push(ActionType.Rebond) - } - const withdrawableAvailabilityTimes = await getWithdrawableAvailabilityTimes() - if (withdrawableAvailabilityTimes.size > 0) { - // Push twice to increase likelihood - actionTypeOptions.push(ActionType.Withdraw) - actionTypeOptions.push(ActionType.Withdraw) - } - const actionType = rndElement(actionTypeOptions) - - const getLockedCommitmentValue = (noticePeriod: string) => - getOrElse(accountLockedGold, noticePeriod, new BigNumber(0)) - const getNotifiedCommitmentValue = (availabilityTime: string) => - getOrElse(accountNotifiedCommitments, availabilityTime, new BigNumber(0)) - - const randomSometimesMaximumValue = (maximum: BigNumber) => { - assert.isFalse(maximum.eq(0)) - const random = BigNumber.random().toNumber() - if (random < 0.5) { - return maximum - } else { - return BigNumber.max( - BigNumber.random() - .times(maximum) - .integerValue(), - 1 - ) - } - } - - // Perform random action and update test implementation state. - if (actionType === ActionType.Deposit) { - const value = new BigNumber(web3.utils.randomHex(2)).toNumber() - // Notice period of at most 10 blocks. - const noticePeriod = BigNumber.random() - .times(10) - .times(blockTime) - .integerValue() - .valueOf() - await lockedGold.newCommitment(noticePeriod, { value, from: account }) - accountNoticePeriods.add(noticePeriod) - accountLockedGold.set(noticePeriod, getLockedCommitmentValue(noticePeriod).plus(value)) - } else if (actionType === ActionType.Notify || actionType === ActionType.Increase) { - const noticePeriod = rndSetElement(accountNoticePeriods) - const lockedDepositValue = getLockedCommitmentValue(noticePeriod) - const value = randomSometimesMaximumValue(lockedDepositValue) - - if (value.eq(lockedDepositValue)) { - accountLockedGold.delete(noticePeriod) - accountNoticePeriods.delete(noticePeriod) - } else { - accountLockedGold.set(noticePeriod, lockedDepositValue.minus(value)) - } - - if (actionType === ActionType.Notify) { - await lockedGold.notifyCommitment(value, noticePeriod, { from: account }) - const availabilityTime = new BigNumber(noticePeriod) - .plus((await web3.eth.getBlock('latest')).timestamp) - .valueOf() - accountAvailabilityTimes.add(availabilityTime) - accountNotifiedCommitments.set( - availabilityTime, - getNotifiedCommitmentValue(availabilityTime).plus(value) - ) - } else { - // Notice period increase of at most 10 blocks. - const increase = BigNumber.random() - .times(10) - .times(blockTime) - .integerValue() - .plus(1) - await lockedGold.increaseNoticePeriod(value, noticePeriod, increase, { - from: account, - }) - const increasedNoticePeriod = increase.plus(noticePeriod).valueOf() - accountNoticePeriods.add(increasedNoticePeriod) - accountLockedGold.set( - increasedNoticePeriod, - getLockedCommitmentValue(increasedNoticePeriod).plus(value) - ) - } - } else if (actionType === ActionType.Rebond) { - const availabilityTime = rndSetElement(rebondableAvailabilityTimes) - const notifiedDepositValue = getNotifiedCommitmentValue(availabilityTime) - const value = randomSometimesMaximumValue(notifiedDepositValue) - await lockedGold.extendCommitment(value, availabilityTime, { from: account }) - - if (value.eq(notifiedDepositValue)) { - accountNotifiedCommitments.delete(availabilityTime) - accountAvailabilityTimes.delete(availabilityTime) - } else { - accountNotifiedCommitments.set(availabilityTime, notifiedDepositValue.minus(value)) - } - const noticePeriod = new BigNumber(availabilityTime) - .minus((await web3.eth.getBlock('latest')).timestamp) - .valueOf() - accountLockedGold.set(noticePeriod, getLockedCommitmentValue(noticePeriod).plus(value)) - accountNoticePeriods.add(noticePeriod) - } else if (actionType === ActionType.Withdraw) { - const availabilityTime = rndSetElement(withdrawableAvailabilityTimes) - await lockedGold.withdrawCommitment(availabilityTime, { from: account }) - accountAvailabilityTimes.delete(availabilityTime) - accountNotifiedCommitments.delete(availabilityTime) - } else { - assert.isTrue(false) - } - - // Sanity check our test implementation. - selectedAccounts.forEach((acc) => { - if (locked.get(acc).size > 0) { - assert.hasAllKeys( - noticePeriods.get(acc), - Array.from(locked.get(acc).keys()), - `notice periods don\'t match for account: ${acc}` - ) - } - if (notified.get(acc).size > 0) { - assert.hasAllKeys( - availabilityTimes.get(acc), - Array.from(notified.get(acc).keys()), - `availability times don\'t match for account: ${acc}` - ) - } - }) - - // Test the contract state matches our test implementation. - let expectedTotalWeight = new BigNumber(0) - for (const acc of selectedAccounts) { - let expectedAccountWeight = new BigNumber(0) - const actualNoticePeriods = await lockedGold.getNoticePeriods(acc) - - assert.lengthOf(actualNoticePeriods, noticePeriods.get(acc).size) - for (let k = 0; k < actualNoticePeriods.length; k++) { - const noticePeriod = actualNoticePeriods[k] - assert.isTrue(noticePeriods.get(acc).has(noticePeriod.valueOf())) - const [actualValue, actualIndex] = await lockedGold.getLockedCommitment( - acc, - noticePeriod - ) - assertEqualBN(actualIndex, k) - const expectedValue = locked.get(acc).get(noticePeriod.valueOf()) - assertEqualBN(actualValue, expectedValue) - assertEqualBN(actualNoticePeriods[actualIndex.toNumber()], noticePeriod) - expectedAccountWeight = expectedAccountWeight.plus( - await lockedGold.getCommitmentWeight(expectedValue, noticePeriod) - ) - } - - const actualAvailabilityTimes = await lockedGold.getAvailabilityTimes(acc) - - assert.equal(actualAvailabilityTimes.length, availabilityTimes.get(acc).size) - for (let k = 0; k < actualAvailabilityTimes.length; k++) { - const availabilityTime = actualAvailabilityTimes[k] - assert.isTrue(availabilityTimes.get(acc).has(availabilityTime.valueOf())) - const [actualValue, actualIndex] = await lockedGold.getNotifiedCommitment( - acc, - availabilityTime - ) - assertEqualBN(actualIndex, k) - const expectedValue = notified.get(acc).get(availabilityTime.valueOf()) - assertEqualBN(actualValue, expectedValue) - assertEqualBN(actualAvailabilityTimes[actualIndex.toNumber()], availabilityTime) - expectedAccountWeight = expectedAccountWeight.plus(expectedValue) - } - assertEqualBN(await lockedGold.getAccountWeight(acc), expectedAccountWeight) - expectedTotalWeight = expectedTotalWeight.plus(expectedAccountWeight) - } - } - } - - it.skip('should match a simple typescript implementation', async () => { - const numActions = 100 - const numAccounts = 2 - await executeActionsAndAssertState(numActions, numAccounts) - }) - }) -}) diff --git a/packages/protocol/test/governance/election.ts b/packages/protocol/test/governance/election.ts new file mode 100644 index 00000000000..af5e83f4e28 --- /dev/null +++ b/packages/protocol/test/governance/election.ts @@ -0,0 +1,1315 @@ +import { CeloContractName } from '@celo/protocol/lib/registry-utils' +import { fixed1, toFixed, fromFixed, multiply } from '@celo/utils/lib/fixidity' +import { + assertContainSubset, + assertEqualBN, + assertRevert, + NULL_ADDRESS, +} from '@celo/protocol/lib/test-utils' +import BigNumber from 'bignumber.js' +import { + MockLockedGoldContract, + MockLockedGoldInstance, + RegistryContract, + RegistryInstance, + ValidatorsContract, + ValidatorsInstance, +} from 'types' + +const Validators: ValidatorsContract = artifacts.require('Validators') +const MockLockedGold: MockLockedGoldContract = artifacts.require('MockLockedGold') +const Registry: RegistryContract = artifacts.require('Registry') + +// @ts-ignore +// TODO(mcortesi): Use BN +Validators.numberFormat = 'BigNumber' + +const parseValidatorParams = (validatorParams: any) => { + return { + name: validatorParams[1], + url: validatorParams[2], + publicKeysData: validatorParams[3], + affiliation: validatorParams[4], + } +} + +const parseValidatorGroupParams = (groupParams: any) => { + return { + name: groupParams[1], + url: groupParams[2], + members: groupParams[3], + } +} + +const HOUR = 60 * 60 +const DAY = 24 * HOUR +const YEAR = 365 * DAY + +contract('Validators', (accounts: string[]) => { + let validators: ValidatorsInstance + let registry: RegistryInstance + let mockLockedGold: MockLockedGoldInstance + // A random 64 byte hex string. + const publicKey = + 'ea0733ad275e2b9e05541341a97ee82678c58932464fad26164657a111a7e37a9fa0300266fb90e2135a1f1512350cb4e985488a88809b14e3cbe415e76e82b2' + const blsPublicKey = + '4d23d8cd06f30b1fa7cf368e2f5399ab04bb6846c682f493a98a607d3dfb7e53a712bb79b475c57b0ac2785460f91301' + const blsPoP = + '9d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d740501' + + const publicKeysData = '0x' + publicKey + blsPublicKey + blsPoP + + const nonOwner = accounts[1] + const registrationRequirements = { group: new BigNumber(1000), validator: new BigNumber(100) } + const deregistrationLockups = { + group: new BigNumber(100 * DAY), + validator: new BigNumber(60 * DAY), + } + const maxGroupSize = 5 + const name = 'test-name' + const url = 'test-url' + const commission = toFixed(1 / 100) + beforeEach(async () => { + validators = await Validators.new() + mockLockedGold = await MockLockedGold.new() + registry = await Registry.new() + await registry.setAddressFor(CeloContractName.LockedGold, mockLockedGold.address) + await validators.initialize( + registry.address, + registrationRequirements.group, + registrationRequirements.validator, + deregistrationLockups.group, + deregistrationLockups.validator, + maxGroupSize + ) + }) + + const registerValidator = async (validator: string) => { + await mockLockedGold.setAccountTotalLockedGold(validator, registrationRequirements.validator) + await validators.registerValidator( + name, + url, + // @ts-ignore bytes type + publicKeysData, + { from: validator } + ) + } + + const registerValidatorGroup = async (group: string) => { + await mockLockedGold.setAccountTotalLockedGold(group, registrationRequirements.group) + await validators.registerValidatorGroup(name, url, commission, { from: group }) + } + + const registerValidatorGroupWithMembers = async (group: string, members: string[]) => { + await registerValidatorGroup(group) + for (const validator of members) { + await registerValidator(validator) + await validators.affiliate(group, { from: validator }) + await validators.addMember(validator, { from: group }) + } + } + + describe('#initialize()', () => { + it('should have set the owner', async () => { + const owner: string = await validators.owner() + assert.equal(owner, accounts[0]) + }) + + it('should have set the registration requirements', async () => { + const [group, validator] = await validators.getRegistrationRequirement() + assertEqualBN(group, registrationRequirements.group) + assertEqualBN(validator, registrationRequirements.validator) + }) + + it('should have set the deregistration lockups', async () => { + const [group, validator] = await validators.getDeregistrationLockups() + assertEqualBN(group, deregistrationLockups.group) + assertEqualBN(validator, deregistrationLockups.validator) + }) + + it('should not be callable again', async () => { + await assertRevert( + validators.initialize( + registry.address, + registrationRequirements.group, + registrationRequirements.validator, + deregistrationLockups.group, + deregistrationLockups.validator, + maxGroupSize + ) + ) + }) + }) + + describe('#setMinElectableValidators', () => { + const newMinElectableValidators = minElectableValidators.plus(1) + it('should set the minimum deposit', async () => { + await validators.setMinElectableValidators(newMinElectableValidators) + assertEqualBN(await validators.minElectableValidators(), newMinElectableValidators) + }) + + it('should emit the MinElectableValidatorsSet event', async () => { + const resp = await validators.setMinElectableValidators(newMinElectableValidators) + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'MinElectableValidatorsSet', + args: { + minElectableValidators: new BigNumber(newMinElectableValidators), + }, + }) + }) + + it('should revert when the minElectableValidators is zero', async () => { + await assertRevert(validators.setMinElectableValidators(0)) + }) + + it('should revert when the minElectableValidators is greater than maxElectableValidators', async () => { + await assertRevert(validators.setMinElectableValidators(maxElectableValidators.plus(1))) + }) + + it('should revert when the minElectableValidators is unchanged', async () => { + await assertRevert(validators.setMinElectableValidators(minElectableValidators)) + }) + + it('should revert when called by anyone other than the owner', async () => { + await assertRevert( + validators.setMinElectableValidators(newMinElectableValidators, { from: nonOwner }) + ) + }) + }) + + describe('#setMaxElectableValidators', () => { + const newMaxElectableValidators = maxElectableValidators.plus(1) + it('should set the minimum deposit', async () => { + await validators.setMaxElectableValidators(newMaxElectableValidators) + assertEqualBN(await validators.maxElectableValidators(), newMaxElectableValidators) + }) + + it('should emit the MaxElectableValidatorsSet event', async () => { + const resp = await validators.setMaxElectableValidators(newMaxElectableValidators) + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'MaxElectableValidatorsSet', + args: { + maxElectableValidators: new BigNumber(newMaxElectableValidators), + }, + }) + }) + + it('should revert when the maxElectableValidators is less than minElectableValidators', async () => { + await assertRevert(validators.setMaxElectableValidators(minElectableValidators.minus(1))) + }) + + it('should revert when the maxElectableValidators is unchanged', async () => { + await assertRevert(validators.setMaxElectableValidators(maxElectableValidators)) + }) + + it('should revert when called by anyone other than the owner', async () => { + await assertRevert( + validators.setMaxElectableValidators(newMaxElectableValidators, { from: nonOwner }) + ) + }) + }) + + describe('#setRegistrationRequirement', () => { + const newValue = registrationRequirement.value.plus(1) + const newNoticePeriod = registrationRequirement.noticePeriod.plus(1) + + it('should set the value and notice period', async () => { + await validators.setRegistrationRequirement(newValue, newNoticePeriod) + const [value, noticePeriod] = await validators.getRegistrationRequirement() + assertEqualBN(value, newValue) + assertEqualBN(noticePeriod, newNoticePeriod) + }) + + it('should emit the RegistrationRequirementSet event', async () => { + const resp = await validators.setRegistrationRequirement(newValue, newNoticePeriod) + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'RegistrationRequirementSet', + args: { + value: new BigNumber(newValue), + noticePeriod: new BigNumber(newNoticePeriod), + }, + }) + }) + + it('should revert when the requirement is unchanged', async () => { + await assertRevert( + validators.setRegistrationRequirement( + registrationRequirement.value, + registrationRequirement.noticePeriod + ) + ) + }) + + it('should revert when called by anyone other than the owner', async () => { + await assertRevert( + validators.setRegistrationRequirement(newValue, newNoticePeriod, { from: nonOwner }) + ) + }) + }) + + describe('#registerValidator', () => { + const validator = accounts[0] + beforeEach(async () => { + await mockLockedGold.setLockedCommitment( + validator, + registrationRequirement.noticePeriod, + registrationRequirement.value + ) + }) + + it('should mark the account as a validator', async () => { + await validators.registerValidator( + name, + url, + // @ts-ignore bytes type + publicKeysData, + registrationRequirement.noticePeriod + ) + assert.isTrue(await validators.isValidator(validator)) + }) + + it('should add the account to the list of validators', async () => { + await validators.registerValidator( + name, + url, + // @ts-ignore bytes type + publicKeysData, + registrationRequirement.noticePeriod + ) + assert.deepEqual(await validators.getRegisteredValidators(), [validator]) + }) + + it('should set the validator name, url, and public key', async () => { + await validators.registerValidator( + name, + url, + // @ts-ignore bytes type + publicKeysData, + registrationRequirement.noticePeriod + ) + const parsedValidator = parseValidatorParams(await validators.getValidator(validator)) + assert.equal(parsedValidator.name, name) + assert.equal(parsedValidator.url, url) + assert.equal(parsedValidator.publicKeysData, publicKeysData) + }) + + it('should emit the ValidatorRegistered event', async () => { + const resp = await validators.registerValidator( + name, + url, + // @ts-ignore bytes type + publicKeysData, + registrationRequirement.noticePeriod + ) + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorRegistered', + args: { + validator, + name, + url, + publicKeysData, + }, + }) + }) + + describe('when the account is already a registered validator', () => { + beforeEach(async () => { + await validators.registerValidator( + name, + url, + // @ts-ignore bytes type + publicKeysData, + registrationRequirement.noticePeriod + ) + }) + + it('should revert', async () => { + await assertRevert( + validators.registerValidator( + name, + url, + // @ts-ignore bytes type + publicKeysData, + registrationRequirement.noticePeriod + ) + ) + }) + }) + + describe('when the account is already a registered validator group', () => { + beforeEach(async () => { + await validators.registerValidatorGroup(name, url, registrationRequirement.noticePeriod) + }) + + it('should revert', async () => { + await assertRevert( + validators.registerValidator( + name, + url, + // @ts-ignore bytes type + publicKeysData, + registrationRequirement.noticePeriod + ) + ) + }) + }) + + describe('when the account does not meet the registration requirements', () => { + beforeEach(async () => { + await mockLockedGold.setLockedCommitment( + validator, + registrationRequirement.noticePeriod, + registrationRequirement.value.minus(1) + ) + }) + + it('should revert', async () => { + await assertRevert( + validators.registerValidator( + name, + url, + // @ts-ignore bytes type + publicKeysData, + registrationRequirement.noticePeriod + ) + ) + }) + }) + }) + + describe('#deregisterValidator', () => { + const validator = accounts[0] + const index = 0 + beforeEach(async () => { + await registerValidator(validator) + }) + + it('should mark the account as not a validator', async () => { + await validators.deregisterValidator(index) + assert.isFalse(await validators.isValidator(validator)) + }) + + it('should remove the account from the list of validators', async () => { + await validators.deregisterValidator(index) + assert.deepEqual(await validators.getRegisteredValidators(), []) + }) + + it('should emit the ValidatorDeregistered event', async () => { + const resp = await validators.deregisterValidator(index) + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorDeregistered', + args: { + validator, + }, + }) + }) + + describe('when the validator is affiliated with a validator group', () => { + const group = accounts[1] + beforeEach(async () => { + await registerValidatorGroup(group) + await validators.affiliate(group) + }) + + it('should emit the ValidatorDeafilliated event', async () => { + const resp = await validators.deregisterValidator(index) + assert.equal(resp.logs.length, 2) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorDeaffiliated', + args: { + validator, + group, + }, + }) + }) + + describe('when the validator is a member of that group', () => { + beforeEach(async () => { + await validators.addMember(validator, { from: group }) + }) + + it('should remove the validator from the group membership list', async () => { + await validators.deregisterValidator(index) + const parsedGroup = parseValidatorGroupParams(await validators.getValidatorGroup(group)) + assert.deepEqual(parsedGroup.members, []) + }) + + it('should emit the ValidatorGroupMemberRemoved event', async () => { + const resp = await validators.deregisterValidator(index) + assert.equal(resp.logs.length, 4) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorGroupMemberRemoved', + args: { + validator, + group, + }, + }) + }) + + describe('when the validator is the only member of that group', () => { + it('should emit the ValidatorGroupEmptied event', async () => { + const resp = await validators.deregisterValidator(index) + assert.equal(resp.logs.length, 4) + const log = resp.logs[1] + assertContainSubset(log, { + event: 'ValidatorGroupEmptied', + args: { + group, + }, + }) + }) + + describe('when that group has received votes', () => { + beforeEach(async () => { + const voter = accounts[2] + const weight = 10 + await mockLockedGold.setWeight(voter, weight) + await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS, { from: voter }) + }) + + it('should remove the group from the list of electable groups with votes', async () => { + await validators.deregisterValidator(index) + const [groups] = await validators.getValidatorGroupVotes() + assert.deepEqual(groups, []) + }) + }) + }) + }) + }) + + it('should revert when the account is not a registered validator', async () => { + await assertRevert(validators.deregisterValidator(index, { from: accounts[2] })) + }) + + it('should revert when the wrong index is provided', async () => { + await assertRevert(validators.deregisterValidator(index + 1)) + }) + }) + + describe('#affiliate', () => { + const validator = accounts[0] + const group = accounts[1] + beforeEach(async () => { + await registerValidator(validator) + await registerValidatorGroup(group) + }) + + it('should set the affiliate', async () => { + await validators.affiliate(group) + const parsedValidator = parseValidatorParams(await validators.getValidator(validator)) + assert.equal(parsedValidator.affiliation, group) + }) + + it('should emit the ValidatorAffiliated event', async () => { + const resp = await validators.affiliate(group) + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorAffiliated', + args: { + validator, + group, + }, + }) + }) + + describe('when the validator is already affiliated with a validator group', () => { + const otherGroup = accounts[2] + beforeEach(async () => { + await validators.affiliate(group) + await registerValidatorGroup(otherGroup) + }) + + it('should set the affiliate', async () => { + await validators.affiliate(otherGroup) + const parsedValidator = parseValidatorParams(await validators.getValidator(validator)) + assert.equal(parsedValidator.affiliation, otherGroup) + }) + + it('should emit the ValidatorDeafilliated event', async () => { + const resp = await validators.affiliate(otherGroup) + assert.equal(resp.logs.length, 2) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorDeaffiliated', + args: { + validator, + group, + }, + }) + }) + + it('should emit the ValidatorAffiliated event', async () => { + const resp = await validators.affiliate(otherGroup) + assert.equal(resp.logs.length, 2) + const log = resp.logs[1] + assertContainSubset(log, { + event: 'ValidatorAffiliated', + args: { + validator, + group: otherGroup, + }, + }) + }) + + describe('when the validator is a member of that group', () => { + beforeEach(async () => { + await validators.addMember(validator, { from: group }) + }) + + it('should remove the validator from the group membership list', async () => { + await validators.affiliate(otherGroup) + const parsedGroup = parseValidatorGroupParams(await validators.getValidatorGroup(group)) + assert.deepEqual(parsedGroup.members, []) + }) + + it('should emit the ValidatorGroupMemberRemoved event', async () => { + const resp = await validators.affiliate(otherGroup) + assert.equal(resp.logs.length, 4) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorGroupMemberRemoved', + args: { + validator, + group, + }, + }) + }) + + describe('when the validator is the only member of that group', () => { + it('should emit the ValidatorGroupEmptied event', async () => { + const resp = await validators.affiliate(otherGroup) + assert.equal(resp.logs.length, 4) + const log = resp.logs[1] + assertContainSubset(log, { + event: 'ValidatorGroupEmptied', + args: { + group, + }, + }) + }) + + describe('when that group has received votes', () => { + beforeEach(async () => { + const voter = accounts[2] + const weight = 10 + await mockLockedGold.setWeight(voter, weight) + await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS, { from: voter }) + }) + + it('should remove the group from the list of electable groups with votes', async () => { + await validators.affiliate(otherGroup) + const [groups] = await validators.getValidatorGroupVotes() + assert.deepEqual(groups, []) + }) + }) + }) + }) + }) + + it('should revert when the account is not a registered validator', async () => { + await assertRevert(validators.affiliate(group, { from: accounts[2] })) + }) + + it('should revert when the group is not a registered validator group', async () => { + await assertRevert(validators.affiliate(accounts[2])) + }) + }) + + describe('#deaffiliate', () => { + const validator = accounts[0] + const group = accounts[1] + beforeEach(async () => { + await registerValidator(validator) + await registerValidatorGroup(group) + await validators.affiliate(group) + }) + + it('should clear the affiliate', async () => { + await validators.deaffiliate() + const parsedValidator = parseValidatorParams(await validators.getValidator(validator)) + assert.equal(parsedValidator.affiliation, NULL_ADDRESS) + }) + + it('should emit the ValidatorDeaffiliated event', async () => { + const resp = await validators.deaffiliate() + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorDeaffiliated', + args: { + validator, + group, + }, + }) + }) + + describe('when the validator is a member of the affiliated group', () => { + beforeEach(async () => { + await validators.addMember(validator, { from: group }) + }) + + it('should remove the validator from the group membership list', async () => { + await validators.deaffiliate() + const parsedGroup = parseValidatorGroupParams(await validators.getValidatorGroup(group)) + assert.deepEqual(parsedGroup.members, []) + }) + + it('should emit the ValidatorGroupMemberRemoved event', async () => { + const resp = await validators.deaffiliate() + assert.equal(resp.logs.length, 3) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorGroupMemberRemoved', + args: { + validator, + group, + }, + }) + }) + + describe('when the validator is the only member of that group', () => { + it('should emit the ValidatorGroupEmptied event', async () => { + const resp = await validators.deaffiliate() + assert.equal(resp.logs.length, 3) + const log = resp.logs[1] + assertContainSubset(log, { + event: 'ValidatorGroupEmptied', + args: { + group, + }, + }) + }) + + describe('when that group has received votes', () => { + beforeEach(async () => { + const voter = accounts[2] + const weight = 10 + await mockLockedGold.setWeight(voter, weight) + await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS, { from: voter }) + }) + + it('should remove the group from the list of electable groups with votes', async () => { + await validators.deaffiliate() + const [groups] = await validators.getValidatorGroupVotes() + assert.deepEqual(groups, []) + }) + }) + }) + }) + + it('should revert when the account is not a registered validator', async () => { + await assertRevert(validators.deaffiliate({ from: accounts[2] })) + }) + + it('should revert when the validator is not affiliated with a validator group', async () => { + await validators.deaffiliate() + await assertRevert(validators.deaffiliate()) + }) + }) + + describe('#registerValidatorGroup', () => { + const group = accounts[0] + beforeEach(async () => { + await mockLockedGold.setLockedCommitment( + group, + registrationRequirement.noticePeriod, + registrationRequirement.value + ) + }) + + it('should mark the account as a validator group', async () => { + await validators.registerValidatorGroup(name, url, registrationRequirement.noticePeriod) + assert.isTrue(await validators.isValidatorGroup(group)) + }) + + it('should add the account to the list of validator groups', async () => { + await validators.registerValidatorGroup(name, url, registrationRequirement.noticePeriod) + assert.deepEqual(await validators.getRegisteredValidatorGroups(), [group]) + }) + + it('should set the validator group name, and url', async () => { + await validators.registerValidatorGroup(name, url, registrationRequirement.noticePeriod) + const parsedGroup = parseValidatorGroupParams(await validators.getValidatorGroup(group)) + assert.equal(parsedGroup.name, name) + assert.equal(parsedGroup.url, url) + }) + + it('should emit the ValidatorGroupRegistered event', async () => { + const resp = await validators.registerValidatorGroup( + name, + url, + registrationRequirement.noticePeriod + ) + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorGroupRegistered', + args: { + group, + name, + url, + }, + }) + }) + + describe('when the account is already a registered validator', () => { + beforeEach(async () => { + await registerValidator(group) + }) + + it('should revert', async () => { + await assertRevert( + validators.registerValidatorGroup(name, url, registrationRequirement.noticePeriod) + ) + }) + }) + + describe('when the account is already a registered validator group', () => { + beforeEach(async () => { + await validators.registerValidatorGroup(name, url, registrationRequirement.noticePeriod) + }) + + it('should revert', async () => { + await assertRevert( + validators.registerValidatorGroup(name, url, registrationRequirement.noticePeriod) + ) + }) + }) + + describe('when the account does not meet the registration requirements', () => { + beforeEach(async () => { + await mockLockedGold.setLockedCommitment( + group, + registrationRequirement.noticePeriod, + registrationRequirement.value.minus(1) + ) + }) + + it('should revert', async () => { + await assertRevert( + validators.registerValidatorGroup(name, url, registrationRequirement.noticePeriod) + ) + }) + }) + }) + + describe('#deregisterValidatorGroup', () => { + const index = 0 + const group = accounts[0] + beforeEach(async () => { + await registerValidatorGroup(group) + }) + + it('should mark the account as not a validator group', async () => { + await validators.deregisterValidatorGroup(index) + assert.isFalse(await validators.isValidatorGroup(group)) + }) + + it('should remove the account from the list of validator groups', async () => { + await validators.deregisterValidatorGroup(index) + assert.deepEqual(await validators.getRegisteredValidatorGroups(), []) + }) + + it('should emit the ValidatorGroupDeregistered event', async () => { + const resp = await validators.deregisterValidatorGroup(index) + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorGroupDeregistered', + args: { + group, + }, + }) + }) + + it('should revert when the account is not a registered validator group', async () => { + await assertRevert(validators.deregisterValidatorGroup(index, { from: accounts[2] })) + }) + + it('should revert when the wrong index is provided', async () => { + await assertRevert(validators.deregisterValidatorGroup(index + 1)) + }) + + describe('when the validator group is not empty', () => { + const validator = accounts[1] + beforeEach(async () => { + await registerValidator(validator) + await validators.affiliate(group, { from: validator }) + await validators.addMember(validator) + }) + + it('should revert', async () => { + await assertRevert(validators.deregisterValidatorGroup(index)) + }) + }) + }) + + describe('#addMember', () => { + const group = accounts[0] + const validator = accounts[1] + beforeEach(async () => { + await registerValidator(validator) + await registerValidatorGroup(group) + await validators.affiliate(group, { from: validator }) + }) + + it('should add the member to the list of members', async () => { + await validators.addMember(validator) + const parsedGroup = parseValidatorGroupParams(await validators.getValidatorGroup(group)) + assert.deepEqual(parsedGroup.members, [validator]) + }) + + it('should emit the ValidatorGroupMemberAdded event', async () => { + const resp = await validators.addMember(validator) + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorGroupMemberAdded', + args: { + group, + validator, + }, + }) + }) + + it('should revert when the account is not a registered validator group', async () => { + await assertRevert(validators.addMember(validator, { from: accounts[2] })) + }) + + it('should revert when the member is not a registered validator', async () => { + await assertRevert(validators.addMember(accounts[2])) + }) + + describe('when the validator has not affiliated themselves with the group', () => { + beforeEach(async () => { + await validators.deaffiliate({ from: validator }) + }) + + it('should revert', async () => { + await assertRevert(validators.addMember(validator)) + }) + }) + + describe('when the validator is already a member of the group', () => { + beforeEach(async () => { + await validators.addMember(validator) + }) + + it('should revert', async () => { + await assertRevert(validators.addMember(validator)) + }) + }) + }) + + describe('#removeMember', () => { + const group = accounts[0] + const validator = accounts[1] + beforeEach(async () => { + await registerValidatorGroupWithMembers(group, [validator]) + }) + + it('should remove the member from the list of members', async () => { + await validators.removeMember(validator) + const parsedGroup = parseValidatorGroupParams(await validators.getValidatorGroup(group)) + assert.deepEqual(parsedGroup.members, []) + }) + + it('should emit the ValidatorGroupMemberRemoved event', async () => { + const resp = await validators.removeMember(validator) + assert.equal(resp.logs.length, 2) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorGroupMemberRemoved', + args: { + group, + validator, + }, + }) + }) + + describe('when the validator is the only member of the group', () => { + it('should emit the ValidatorGroupEmptied event', async () => { + const resp = await validators.removeMember(validator) + assert.equal(resp.logs.length, 2) + const log = resp.logs[1] + assertContainSubset(log, { + event: 'ValidatorGroupEmptied', + args: { + group, + }, + }) + }) + + describe('when the group has received votes', () => { + beforeEach(async () => { + const voter = accounts[2] + const weight = 10 + await mockLockedGold.setWeight(voter, weight) + await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS, { from: voter }) + }) + + it('should remove the group from the list of electable groups with votes', async () => { + await validators.removeMember(validator) + const [groups] = await validators.getValidatorGroupVotes() + assert.deepEqual(groups, []) + }) + }) + }) + + it('should revert when the account is not a registered validator group', async () => { + await assertRevert(validators.removeMember(validator, { from: accounts[2] })) + }) + + it('should revert when the member is not a registered validator', async () => { + await assertRevert(validators.removeMember(accounts[2])) + }) + + describe('when the validator is not a member of the validator group', () => { + beforeEach(async () => { + await validators.deaffiliate({ from: validator }) + }) + + it('should revert', async () => { + await assertRevert(validators.removeMember(validator)) + }) + }) + }) + + describe('#reorderMember', () => { + const group = accounts[0] + const validator1 = accounts[1] + const validator2 = accounts[2] + beforeEach(async () => { + await registerValidatorGroupWithMembers(group, [validator1, validator2]) + }) + + it('should reorder the list of group members', async () => { + await validators.reorderMember(validator2, validator1, NULL_ADDRESS) + const parsedGroup = parseValidatorGroupParams(await validators.getValidatorGroup(group)) + assert.deepEqual(parsedGroup.members, [validator2, validator1]) + }) + + it('should emit the ValidatorGroupMemberReordered event', async () => { + const resp = await validators.reorderMember(validator2, validator1, NULL_ADDRESS) + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorGroupMemberReordered', + args: { + group, + validator: validator2, + }, + }) + }) + + it('should revert when the account is not a registered validator group', async () => { + await assertRevert( + validators.reorderMember(validator2, validator1, NULL_ADDRESS, { from: accounts[2] }) + ) + }) + + it('should revert when the member is not a registered validator', async () => { + await assertRevert(validators.reorderMember(accounts[3], validator1, NULL_ADDRESS)) + }) + + describe('when the validator is not a member of the validator group', () => { + beforeEach(async () => { + await validators.deaffiliate({ from: validator2 }) + }) + + it('should revert', async () => { + await assertRevert(validators.reorderMember(validator2, validator1, NULL_ADDRESS)) + }) + }) + }) + + describe('#vote', () => { + const weight = new BigNumber(5) + const voter = accounts[0] + const validator = accounts[1] + const group = accounts[2] + beforeEach(async () => { + await registerValidatorGroupWithMembers(group, [validator]) + await mockLockedGold.setWeight(voter, weight) + }) + + it("should set the voter's vote", async () => { + await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS) + assert.isTrue(await validators.isVoting(voter)) + assert.equal(await validators.voters(voter), group) + }) + + it('should add the group to the list of those receiving votes', async () => { + await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS) + const [groups] = await validators.getValidatorGroupVotes() + assert.deepEqual(groups, [group]) + }) + + it("should increment the validator group's vote total", async () => { + await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS) + assertEqualBN(await validators.getVotesReceived(group), weight) + }) + + it('should emit the ValidatorGroupVoteCast event', async () => { + const resp = await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS) + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorGroupVoteCast', + args: { + account: voter, + group, + weight: new BigNumber(weight), + }, + }) + }) + + describe('when the group had not previously received votes', () => { + it('should add the group to the list of electable groups with votes', async () => { + await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS) + const [groups] = await validators.getValidatorGroupVotes() + assert.deepEqual(groups, [group]) + }) + }) + + it('should revert when the group is not a registered validator group', async () => { + await assertRevert(validators.vote(accounts[3], NULL_ADDRESS, NULL_ADDRESS)) + }) + + describe('when the group is empty', () => { + beforeEach(async () => { + await validators.removeMember(validator, { from: group }) + }) + + it('should revert', async () => { + await assertRevert(validators.vote(group, NULL_ADDRESS, NULL_ADDRESS)) + }) + }) + + describe('when the account voting is frozen', () => { + beforeEach(async () => { + await mockLockedGold.setVotingFrozen(voter) + }) + + it('should revert', async () => { + await assertRevert(validators.vote(group, NULL_ADDRESS, NULL_ADDRESS)) + }) + }) + + describe('when the account has no weight', () => { + beforeEach(async () => { + await mockLockedGold.setWeight(voter, NULL_ADDRESS) + }) + + it('should revert', async () => { + await assertRevert(validators.vote(group, NULL_ADDRESS, NULL_ADDRESS)) + }) + }) + describe('when the account has an outstanding vote', () => { + beforeEach(async () => { + await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS) + }) + + it('should revert', async () => { + await assertRevert(validators.vote(group, NULL_ADDRESS, NULL_ADDRESS)) + }) + }) + }) + + describe('#revokeVote', () => { + const weight = 5 + const voter = accounts[0] + const validator = accounts[1] + const group = accounts[2] + beforeEach(async () => { + await registerValidatorGroupWithMembers(group, [validator]) + await mockLockedGold.setWeight(voter, weight) + await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS) + }) + + it("should clear the voter's vote", async () => { + await validators.revokeVote(NULL_ADDRESS, NULL_ADDRESS) + assert.isFalse(await validators.isVoting(voter)) + assert.equal(await validators.voters(voter), NULL_ADDRESS) + }) + + it("should decrement the validator group's vote total", async () => { + await validators.revokeVote(NULL_ADDRESS, NULL_ADDRESS) + const [groups, votes] = await validators.getValidatorGroupVotes() + assert.deepEqual(groups, []) + assert.deepEqual(votes, []) + }) + + it('should emit the ValidatorGroupVoteRevoked event', async () => { + const resp = await validators.revokeVote(NULL_ADDRESS, NULL_ADDRESS) + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorGroupVoteRevoked', + args: { + account: voter, + group, + weight: new BigNumber(weight), + }, + }) + }) + + describe('when the group had not received other votes', () => { + it('should remove the group from the list of electable groups with votes', async () => { + await validators.revokeVote(NULL_ADDRESS, NULL_ADDRESS) + const [groups] = await validators.getValidatorGroupVotes() + assert.deepEqual(groups, []) + }) + }) + + describe('when the account does not have an outstanding vote', () => { + beforeEach(async () => { + await validators.revokeVote(NULL_ADDRESS, NULL_ADDRESS) + }) + + it('should revert', async () => { + await assertRevert(validators.revokeVote(NULL_ADDRESS, NULL_ADDRESS)) + }) + }) + }) + + describe('#getValidators', () => { + const group1 = accounts[0] + const group2 = accounts[1] + const group3 = accounts[2] + const validator1 = accounts[3] + const validator2 = accounts[4] + const validator3 = accounts[5] + const validator4 = accounts[6] + const validator5 = accounts[7] + const validator6 = accounts[8] + const validator7 = accounts[9] + + // If voterN votes for groupN: + // group1 gets 20 votes per member + // group2 gets 25 votes per member + // group3 gets 30 votes per member + // The ordering of the returned validators should be from group with most votes to group, + // with fewest votes, and within each group, members are elected from first to last. + const voter1 = { address: accounts[0], weight: 80 } + const voter2 = { address: accounts[1], weight: 50 } + const voter3 = { address: accounts[2], weight: 30 } + const assertAddressesEqual = (actual: string[], expected: string[]) => { + assert.deepEqual(actual.map((x) => x.toLowerCase()), expected.map((x) => x.toLowerCase())) + } + + beforeEach(async () => { + await registerValidatorGroupWithMembers(group1, [ + validator1, + validator2, + validator3, + validator4, + ]) + await registerValidatorGroupWithMembers(group2, [validator5, validator6]) + await registerValidatorGroupWithMembers(group3, [validator7]) + + for (const voter of [voter1, voter2, voter3]) { + await mockLockedGold.setWeight(voter.address, voter.weight) + } + }) + + describe('when a single group has >= minElectableValidators as members and received votes', () => { + beforeEach(async () => { + await validators.vote(group1, NULL_ADDRESS, NULL_ADDRESS, { from: voter1.address }) + }) + + it("should return that group's member list", async () => { + assertAddressesEqual(await validators.getValidators(), [ + validator1, + validator2, + validator3, + validator4, + ]) + }) + }) + + describe("when > maxElectableValidators members's groups receive votes", () => { + beforeEach(async () => { + await validators.vote(group1, NULL_ADDRESS, NULL_ADDRESS, { from: voter1.address }) + await validators.vote(group2, NULL_ADDRESS, group1, { from: voter2.address }) + await validators.vote(group3, NULL_ADDRESS, group2, { from: voter3.address }) + }) + + it('should return maxElectableValidators elected validators', async () => { + assertAddressesEqual(await validators.getValidators(), [ + validator1, + validator2, + validator3, + validator5, + validator6, + validator7, + ]) + }) + }) + + describe('when a group receives enough votes for > n seats but only has n members', () => { + beforeEach(async () => { + await mockLockedGold.setWeight(voter3.address, 1000) + await validators.vote(group3, NULL_ADDRESS, NULL_ADDRESS, { from: voter3.address }) + await validators.vote(group1, NULL_ADDRESS, group3, { from: voter1.address }) + await validators.vote(group2, NULL_ADDRESS, group1, { from: voter2.address }) + }) + + it('should elect only n members from that group', async () => { + assertAddressesEqual(await validators.getValidators(), [ + validator7, + validator1, + validator2, + validator3, + validator5, + validator6, + ]) + }) + }) + + describe('when an account has delegated validating to another address', () => { + const validatingDelegate = '0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95' + beforeEach(async () => { + await mockLockedGold.delegateValidating(validator3, validatingDelegate) + await validators.vote(group1, NULL_ADDRESS, NULL_ADDRESS, { from: voter1.address }) + await validators.vote(group2, NULL_ADDRESS, group1, { from: voter2.address }) + await validators.vote(group3, NULL_ADDRESS, group2, { from: voter3.address }) + }) + + it('should return the validating delegate in place of the account', async () => { + assertAddressesEqual(await validators.getValidators(), [ + validator1, + validator2, + validatingDelegate, + validator5, + validator6, + validator7, + ]) + }) + }) + + describe('when there are not enough electable validators', () => { + beforeEach(async () => { + await validators.vote(group2, NULL_ADDRESS, NULL_ADDRESS, { from: voter2.address }) + await validators.vote(group3, NULL_ADDRESS, group2, { from: voter3.address }) + }) + + it('should revert', async () => { + await assertRevert(validators.getValidators()) + }) + }) + }) +}) diff --git a/packages/protocol/test/governance/lockedgold.ts b/packages/protocol/test/governance/lockedgold.ts new file mode 100644 index 00000000000..9a29a8f0464 --- /dev/null +++ b/packages/protocol/test/governance/lockedgold.ts @@ -0,0 +1,469 @@ +import { CeloContractName } from '@celo/protocol/lib/registry-utils' +import { + assertEqualBN, + assertLogMatches, + assertRevert, + NULL_ADDRESS, + timeTravel, +} from '@celo/protocol/lib/test-utils' +import BigNumber from 'bignumber.js' +import { + LockedGoldContract, + LockedGoldInstance, + MockGoldTokenContract, + MockGoldTokenInstance, + MockGovernanceContract, + MockGovernanceInstance, + MockValidatorsContract, + MockValidatorsInstance, + RegistryContract, + RegistryInstance, +} from 'types' + +const LockedGold: LockedGoldContract = artifacts.require('LockedGold') +const Registry: RegistryContract = artifacts.require('Registry') +const MockGoldToken: MockGoldTokenContract = artifacts.require('MockGoldToken') +const MockGovernance: MockGovernanceContract = artifacts.require('MockGovernance') +const MockValidators: MockValidatorsContract = artifacts.require('MockValidators') + +// @ts-ignore +// TODO(mcortesi): Use BN +LockedGold.numberFormat = 'BigNumber' + +const HOUR = 60 * 60 +const DAY = 24 * HOUR +const YEAR = 365 * DAY + +contract('LockedGold', (accounts: string[]) => { + let account = accounts[0] + const nonOwner = accounts[1] + const unlockingPeriod = 3 * DAY + let mockGoldToken: MockGoldTokenInstance + let mockGovernance: MockGovernanceInstance + let mockValidators: MockValidatorsInstance + let lockedGold: LockedGoldInstance + let registry: RegistryInstance + + const getParsedSignatureOfAddress = async (address: string, signer: string) => { + // @ts-ignore + const hash = web3.utils.soliditySha3({ type: 'address', value: address }) + const signature = (await web3.eth.sign(hash, signer)).slice(2) + return { + r: `0x${signature.slice(0, 64)}`, + s: `0x${signature.slice(64, 128)}`, + v: web3.utils.hexToNumber(signature.slice(128, 130)) + 27, + } + } + + beforeEach(async () => { + lockedGold = await LockedGold.new() + mockGoldToken = await MockGoldToken.new() + mockGovernance = await MockGovernance.new() + mockValidators = await MockValidators.new() + registry = await Registry.new() + await registry.setAddressFor(CeloContractName.GoldToken, mockGoldToken.address) + await registry.setAddressFor(CeloContractName.Governance, mockGovernance.address) + await registry.setAddressFor(CeloContractName.Validators, mockValidators.address) + await lockedGold.initialize(registry.address, unlockingPeriod) + await lockedGold.createAccount() + }) + + describe('#initialize()', () => { + it('should set the owner', async () => { + const owner: string = await lockedGold.owner() + assert.equal(owner, account) + }) + + it('should set the registry address', async () => { + const registryAddress: string = await lockedGold.registry() + assert.equal(registryAddress, registry.address) + }) + + it('should set the unlocking period', async () => { + const period: string = await lockedGold.unlockingPeriod() + assert.equal(unlockingPeriod, period) + }) + + it('should revert if already initialized', async () => { + await assertRevert(lockedGold.initialize(registry.address, unlockingPeriod)) + }) + }) + + describe('#setRegistry()', () => { + const anAddress: string = accounts[2] + + it('should set the registry when called by the owner', async () => { + await lockedGold.setRegistry(anAddress) + assert.equal(await lockedGold.registry(), anAddress) + }) + + it('should revert when not called by the owner', async () => { + await assertRevert(lockedGold.setRegistry(anAddress, { from: nonOwner })) + }) + }) + + const authorizationTests = [ + { + name: 'Voter', + fn: lockedGold.authorizeVoter, + getFromAccount: lockedGold.getVoterFromAccount, + getAccount: lockedGold.getAccountFromVoter, + }, + { + name: 'Validator', + fn: lockedGold.authorizeValidator, + getFromAccount: lockedGold.getValidatorFromAccount, + getAccount: lockedGold.getAccountFromValidator, + }, + ] + for (const test in authorizationTests) { + describe(`#authorize${test.name}()`, () => { + const authorized = accounts[1] + let sig + + beforeEach(async () => { + sig = await getParsedSignatureOfAddress(account, authorized) + }) + + it(`should set the authorized ${test.name}`, async () => { + await test.fn(authorized, sig.v, sig.r, sig.s) + assert.equal(await lockedGold.authorizedBy(authorized), account) + assert.equal(await test.getFromAccount(account), authorized) + assert.equal(await test.getAccount(authorized), account) + }) + + it(`should emit a ${test.name}Authorized event`, async () => { + const resp = await test.fn(authorized, sig.v, sig.r, sig.s) + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertLogMatches(log, `${test.name}Authorized`, { + account, + authorized, + }) + }) + + it(`should revert if the ${test.name} is an account`, async () => { + await lockedGold.createAccount({ from: authorized }) + await assertRevert(test.fn(authorized, sig.v, sig.r, sig.s)) + }) + + it(`should revert if the ${test.name} is already authorized`, async () => { + const otherAccount = accounts[2] + const otherSig = await getParsedSignatureOfAddress(otherAccount, authorized) + await lockedGold.createAccount({ from: otherAccount }) + await test.fn(authorized, otherSig.v, otherSig.r, otherSig.s, { + from: otherAccount, + }) + await assertRevert(test.fn(authorized, sig.v, sig.r, sig.s)) + }) + + it('should revert if the signature is incorrect', async () => { + const nonVoter = accounts[3] + const incorrectSig = await getParsedSignatureOfAddress(account, nonVoter) + await assertRevert(test.fn(authorized, incorrectSig.v, incorrectSig.r, incorrectSig.s)) + }) + + describe('when a previous authorization has been made', async () => { + const newAuthorized = accounts[2] + let newSig + beforeEach(async () => { + await test.fn(authorized, sig.v, sig.r, sig.s) + newSig = await getParsedSignatureOfAddress(account, newAuthorized) + await test.fn(newAuthorized, newSig.v, newSig.r, newSig.s) + }) + + it(`should set the new authorized ${test.name}`, async () => { + assert.equal(await lockedGold.authorizedBy(newAuthorized), account) + assert.equal(await test.getFromAccount(account), newAuthorized) + assert.equal(await test.getAccount(newAuthorized), account) + }) + + it('should reset the previous authorization', async () => { + assert.equal(await lockedGold.authorizedBy(authorized), NULL_ADDRESS) + }) + }) + }) + + describe(`#getAccountFrom${test.name}()`, () => { + describe(`when the account has not authorized a ${test.name}`, () => { + it('should return the account when passed the account', async () => { + assert.equal(await test.getAccount(account), account) + }) + + it('should revert when passed an address that is not an account', async () => { + await assertRevert(test.getAccount(accounts[1])) + }) + }) + + describe(`when the account has authorized a ${test.name}`, () => { + const authorized = accounts[1] + before(async () => { + const sig = await getParsedSignatureOfAddress(account, voter) + await test.fn(authorized, sig.v, sig.r, sig.s) + }) + + it('should return the account when passed the account', async () => { + assert.equal(await test.getAccount(account), account) + }) + + it(`should return the account when passed the ${test.name}`, async () => { + assert.equal(await test.getAccount(authorized), account) + }) + }) + }) + + describe(`#get${test.name}FromAccount()`, () => { + describe(`when the account has not authorized a ${test.name}`, () => { + it('should return the account when passed the account', async () => { + assert.equal(await test.getFromAccount(account), account) + }) + + it('should revert when not passed an account', async () => { + await assertRevert(test.getFromAccount(account), account) + }) + }) + + describe(`when the account has authorized a ${test.name}`, () => { + const authorized = accounts[1] + + before(async () => { + const sig = await getParsedSignatureOfAddress(account, authorized) + await test.fn(authorized, sig.v, sig.r, sig.s) + }) + + it(`should return the ${test.name} when passed the account`, async () => { + assert.equal(await test.getFromAccount(account), authorized) + }) + }) + }) + } + + describe('#lock()', () => { + const value = 1000 + + it("should increase the account's nonvoting locked gold balance", async () => { + // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails + await lockedGold.lock({ value }) + assert.equal(await lockedGold.getAccountNonvotingLockedGold(account), value) + }) + + it("should increase the account's total locked gold balance", async () => { + // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails + await lockedGold.lock({ value }) + assert.equal(await lockedGold.getAccountTotalLockedGold(account), value) + }) + + it('should increase the nonvoting locked gold balance', async () => { + // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails + await lockedGold.lock({ value }) + assert.equal(await lockedGold.getNonvotingLockedGold(), value) + }) + + it('should increase the total locked gold balance', async () => { + // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails + await lockedGold.lock({ value }) + assert.equal(await lockedGold.getTotalLockedGold(), value) + }) + + it('should emit a GoldLocked event', async () => { + // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails + const resp = await lockedGold.lock({ value }) + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertLogMatches(log, 'GoldLocked', { + account, + value: new BigNumber(value), + }) + }) + + it('should revert when the specified value is 0', async () => { + await assertRevert(lockedGold.lock({ value: 0 })) + }) + + it('should revert when the account does not exist', async () => { + await assertRevert(lockedGold.lock({ value, from: accounts[1] })) + }) + }) + + describe('#unlock()', () => { + const value = 1000 + let availabilityTime: BigNumber + let resp: any + describe('when there are no balance requirements', () => { + before(async () => { + // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails + await lockedGold.lock({ value }) + resp = await lockedGold.unlock(value) + availabilityTime = new BigNumber(unlockingPeriod).plus( + (await web3.eth.getBlock('latest')).timestamp + ) + }) + + it('should add a pending withdrawal', async () => { + const pendingWithdrawals = await lockedGold.getPendingWithdrawals(account) + assert.equal(pendingWithdrawals.length, 1) + assert.equal(pendingWithdrawals[0], value) + assert.equal(pendingWithdrawals[1], availabilityTime) + }) + + it("should decrease the account's nonvoting locked gold balance", async () => { + assert.equal(await lockedGold.getAccountNonvotingLockedGold(account), 0) + }) + + it("should decrease the account's total locked gold balance", async () => { + assert.equal(await lockedGold.getAccountTotalLockedGold(account), 0) + }) + + it('should decrease the nonvoting locked gold balance', async () => { + assert.equal(await lockedGold.getNonvotingLockedGold(), 0) + }) + + it('should decrease the total locked gold balance', async () => { + assert.equal(await lockedGold.getTotalLockedGold(), 0) + }) + + it('should emit a GoldUnlocked event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertLogMatches(log, 'GoldUnlocked', { + account, + value: new BigNumber(value), + available, + }) + }) + }) + + describe('when there are balance requirements', () => { + let mustMaintain: any + before(async () => { + // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails + await lockedGold.lock({ value }) + // Allow ourselves to call `setAccountMustMaintain()` + await registry.setAddressFor('Election', account) + const timestamp = (await web3.eth.getBlock('latest')).timestamp + const mustMaintain = { value: 100, timestamp: timestamp + DAY } + await lockedGold.setAccountMustMaintain(account, mustMaintain.value, mustMaintain.timestamp) + }) + + describe('when unlocking would yield a locked gold balance less than the required value', () => { + describe('when the the current time is earlier than the requirement time', () => { + it('should revert', async () => { + await assertRevert(lockedGold.unlock(value)) + }) + }) + + describe('when the the current time is later than the requirement time', () => { + it('should succeed', async () => { + await timeTravel(web3, DAY) + await lockedGold.unlock(value) + }) + }) + }) + + describe('when unlocking would yield a locked gold balance equal to the required value', () => { + it('should succeed', async () => { + await lockedGold.unlock(value - mustMaintain.value) + }) + }) + }) + }) + + describe('#relock()', () => { + const value = 1000 + const index = 0 + describe('when a pending withdrawal exists', () => { + before(async () => { + // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails + await lockedGold.lock({ value }) + await lockedGold.unlock(value) + resp = await lockedGold.relock(index) + }) + + it("should increase the account's nonvoting locked gold balance", async () => { + assert.equal(await lockedGold.getAccountNonvotingLockedGold(account), value) + }) + + it("should increase the account's total locked gold balance", async () => { + assert.equal(await lockedGold.getAccountTotalLockedGold(account), value) + }) + + it('should increase the nonvoting locked gold balance', async () => { + assert.equal(await lockedGold.getNonvotingLockedGold(), value) + }) + + it('should increase the total locked gold balance', async () => { + assert.equal(await lockedGold.getTotalLockedGold(), value) + }) + + it('should emit a GoldLocked event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertLogMatches(log, 'GoldLocked', { + account, + value: new BigNumber(value), + }) + }) + + it('should remove the pending withdrawal', async () => { + const pendingWithdrawals = await lockedGold.getPendingWithdrawals(account) + assert.equal(pendingWithdrawals.length, 0) + }) + }) + + describe('when a pending withdrawal does not exist', () => { + it('should revert', async () => { + await assertRevert(lockedGold.relock(index)) + }) + }) + }) + + describe('#withdraw()', () => { + const value = 1000 + const index = 0 + let availabilityTime: BigNumber + let resp: any + describe('when a pending withdrawal exists', () => { + before(async () => { + // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails + await lockedGold.lock({ value }) + resp = await lockedGold.unlock(value) + availabilityTime = new BigNumber(unlockingPeriod).plus( + (await web3.eth.getBlock('latest')).timestamp + ) + }) + + describe('when it is after the availablity time', () => { + before(async () => { + await timeTravel(web3, unlockingPeriod) + resp = await lockedGold.withdraw(index) + }) + + it('should remove the pending withdrawal', async () => { + const pendingWithdrawals = await lockedGold.getPendingWithdrawals(account) + assert.equal(pendingWithdrawals.length, 0) + }) + + it('should emit a GoldWithdrawn event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertLogMatches(log, 'GoldWithdrawn', { + account, + value: new BigNumber(value), + }) + }) + }) + + describe('when it is before the availablity time', () => { + it('should revert', async () => { + await assertRevert(lockedGold.withdraw(index)) + }) + }) + }) + + describe('when a pending withdrawal does not exist', () => { + it('should revert', async () => { + await assertRevert(lockedGold.withdraw(index)) + }) + }) + }) +}) diff --git a/packages/protocol/test/governance/validators.ts b/packages/protocol/test/governance/validators.ts index ea8b413d8a9..a0f3be9f4fc 100644 --- a/packages/protocol/test/governance/validators.ts +++ b/packages/protocol/test/governance/validators.ts @@ -1,4 +1,5 @@ import { CeloContractName } from '@celo/protocol/lib/registry-utils' +import { fixed1, toFixed, fromFixed, multiply } from '@celo/utils/lib/fixidity' import { assertContainSubset, assertEqualBN, @@ -25,7 +26,6 @@ Validators.numberFormat = 'BigNumber' const parseValidatorParams = (validatorParams: any) => { return { - identifier: validatorParams[0], name: validatorParams[1], url: validatorParams[2], publicKeysData: validatorParams[3], @@ -35,13 +35,17 @@ const parseValidatorParams = (validatorParams: any) => { const parseValidatorGroupParams = (groupParams: any) => { return { - identifier: groupParams[0], name: groupParams[1], url: groupParams[2], members: groupParams[3], } } +const HOUR = 60 * 60 +const DAY = 24 * HOUR +const YEAR = 365 * DAY +const MAX_UINT256 = new BigInt(2).pow(256).minus(1) + contract('Validators', (accounts: string[]) => { let validators: ValidatorsInstance let registry: RegistryInstance @@ -57,12 +61,15 @@ contract('Validators', (accounts: string[]) => { const publicKeysData = '0x' + publicKey + blsPublicKey + blsPoP const nonOwner = accounts[1] - const minElectableValidators = new BigNumber(4) - const maxElectableValidators = new BigNumber(6) - const registrationRequirement = { value: new BigNumber(100), noticePeriod: new BigNumber(60) } - const identifier = 'test-identifier' + const registrationRequirements = { group: new BigNumber(1000), validator: new BigNumber(100) } + const deregistrationLockups = { + group: new BigNumber(100 * DAY), + validator: new BigNumber(60 * DAY), + } + const maxGroupSize = 5 const name = 'test-name' const url = 'test-url' + const commission = toFixed(1 / 100) beforeEach(async () => { validators = await Validators.new() mockLockedGold = await MockLockedGold.new() @@ -70,43 +77,28 @@ contract('Validators', (accounts: string[]) => { await registry.setAddressFor(CeloContractName.LockedGold, mockLockedGold.address) await validators.initialize( registry.address, - minElectableValidators, - maxElectableValidators, - registrationRequirement.value, - registrationRequirement.noticePeriod + registrationRequirements.group, + registrationRequirements.validator, + deregistrationLockups.group, + deregistrationLockups.validator, + maxGroupSize ) }) const registerValidator = async (validator: string) => { - await mockLockedGold.setLockedCommitment( - validator, - registrationRequirement.noticePeriod, - registrationRequirement.value - ) + await mockLockedGold.setAccountTotalLockedGold(validator, registrationRequirements.validator) await validators.registerValidator( - identifier, name, url, // @ts-ignore bytes type publicKeysData, - registrationRequirement.noticePeriod, { from: validator } ) } const registerValidatorGroup = async (group: string) => { - await mockLockedGold.setLockedCommitment( - group, - registrationRequirement.noticePeriod, - registrationRequirement.value - ) - await validators.registerValidatorGroup( - identifier, - name, - url, - registrationRequirement.noticePeriod, - { from: group } - ) + await mockLockedGold.setAccountTotalLockedGold(group, registrationRequirements.group) + await validators.registerValidatorGroup(name, url, commission, { from: group }) } const registerValidatorGroupWithMembers = async (group: string, members: string[]) => { @@ -124,241 +116,256 @@ contract('Validators', (accounts: string[]) => { assert.equal(owner, accounts[0]) }) - it('should have set minElectableValidators', async () => { - const actualMinElectableValidators = await validators.minElectableValidators() - assertEqualBN(actualMinElectableValidators, minElectableValidators) - }) - - it('should have set maxElectableValidators', async () => { - const actualMaxElectableValidators = await validators.maxElectableValidators() - assertEqualBN(actualMaxElectableValidators, maxElectableValidators) + it('should have set the registration requirements', async () => { + const [group, validator] = await validators.getRegistrationRequirement() + assertEqualBN(group, registrationRequirements.group) + assertEqualBN(validator, registrationRequirements.validator) }) - it('should have set the registration requirements', async () => { - const [value, noticePeriod] = await validators.getRegistrationRequirement() - assertEqualBN(value, registrationRequirement.value) - assertEqualBN(noticePeriod, registrationRequirement.noticePeriod) + it('should have set the deregistration lockups', async () => { + const [group, validator] = await validators.getDeregistrationLockups() + assertEqualBN(group, deregistrationLockups.group) + assertEqualBN(validator, deregistrationLockups.validator) }) it('should not be callable again', async () => { await assertRevert( validators.initialize( registry.address, - minElectableValidators, - maxElectableValidators, - registrationRequirement.value, - registrationRequirement.noticePeriod + registrationRequirements.group, + registrationRequirements.validator, + deregistrationLockups.group, + deregistrationLockups.validator, + maxGroupSize ) ) }) }) - describe('#setMinElectableValidators', () => { - const newMinElectableValidators = minElectableValidators.plus(1) - it('should set the minimum deposit', async () => { - await validators.setMinElectableValidators(newMinElectableValidators) - assertEqualBN(await validators.minElectableValidators(), newMinElectableValidators) - }) - - it('should emit the MinElectableValidatorsSet event', async () => { - const resp = await validators.setMinElectableValidators(newMinElectableValidators) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'MinElectableValidatorsSet', - args: { - minElectableValidators: new BigNumber(newMinElectableValidators), - }, - }) - }) + describe('#setRegistrationRequirements()', () => { + describe('when the requirements are different', () => { + describe('when called by the owner', () => { + let resp: any + const newRequirements = { + group: registrationRequirements.group.plus(1), + validator: registrationRequirements.validator.plus(1), + } + + before(async () => { + resp = await validators.setRegistrationRequirements( + newRequirements.group, + newRequirements.validator + ) + }) - it('should revert when the minElectableValidators is zero', async () => { - await assertRevert(validators.setMinElectableValidators(0)) - }) + it('should set the group and validator requirements', async () => { + const [group, validator] = await validators.getRegistrationRequirements() + assertEqualBN(group, newRequirements.group) + assertEqualBN(validator, newRequirements.validator) + }) - it('should revert when the minElectableValidators is greater than maxElectableValidators', async () => { - await assertRevert(validators.setMinElectableValidators(maxElectableValidators.plus(1))) - }) + it('should emit the RegistrationRequirementsSet event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'RegistrationRequirementsSet', + args: { + group: new BigNumber(newRequirements.group), + validator: new BigNumber(newRequirements.validator), + }, + }) + }) + }) - it('should revert when the minElectableValidators is unchanged', async () => { - await assertRevert(validators.setMinElectableValidators(minElectableValidators)) + describe('when the requirements are the same', () => { + it('should revert', async () => { + await assertRevert( + validators.setRegistrationRequirements( + registrationRequirements.group, + registrationRequirements.validator + ) + ) + }) + }) }) - it('should revert when called by anyone other than the owner', async () => { - await assertRevert( - validators.setMinElectableValidators(newMinElectableValidators, { from: nonOwner }) - ) + describe('when called by a non-owner', () => { + it('should revert', async () => { + await assertRevert( + validators.setRegistrationRequirements(newRequirements.group, newRequirements.validator, { + from: nonOwner, + }) + ) + }) }) }) - describe('#setMaxElectableValidators', () => { - const newMaxElectableValidators = maxElectableValidators.plus(1) - it('should set the minimum deposit', async () => { - await validators.setMaxElectableValidators(newMaxElectableValidators) - assertEqualBN(await validators.maxElectableValidators(), newMaxElectableValidators) - }) + describe('#setDeregistrationLockups()', () => { + describe('when the requirements are different', () => { + describe('when called by the owner', () => { + let resp: any + const newLockups = { + group: deregistrationLockups.group.plus(1), + validator: deregistrationLockups.validator.plus(1), + } + + before(async () => { + resp = await validators.setDeregistrationLockups(newLockups.group, newLockups.validator) + }) - it('should emit the MaxElectableValidatorsSet event', async () => { - const resp = await validators.setMaxElectableValidators(newMaxElectableValidators) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'MaxElectableValidatorsSet', - args: { - maxElectableValidators: new BigNumber(newMaxElectableValidators), - }, - }) - }) + it('should set the group and validator requirements', async () => { + const [group, validator] = await validators.getDeregistrationLockups() + assertEqualBN(group, newLockups.group) + assertEqualBN(validator, newLockups.validator) + }) - it('should revert when the maxElectableValidators is less than minElectableValidators', async () => { - await assertRevert(validators.setMaxElectableValidators(minElectableValidators.minus(1))) - }) + it('should emit the DeregistrationLockupsSet event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'DeregistrationLockupsSet', + args: { + group: new BigNumber(newLockups.group), + validator: new BigNumber(newLockups.validator), + }, + }) + }) + }) - it('should revert when the maxElectableValidators is unchanged', async () => { - await assertRevert(validators.setMaxElectableValidators(maxElectableValidators)) + describe('when the requirements are the same', () => { + it('should revert', async () => { + await assertRevert( + validators.setDeregistrationLockups( + deregistrationLockups.group, + deregistrationLockups.validator + ) + ) + }) + }) }) - it('should revert when called by anyone other than the owner', async () => { - await assertRevert( - validators.setMaxElectableValidators(newMaxElectableValidators, { from: nonOwner }) - ) + describe('when called by a non-owner', () => { + it('should revert', async () => { + await assertRevert( + validators.setDeregistrationLockups(newLockups.group, newLockups.validator, { + from: nonOwner, + }) + ) + }) }) }) - describe('#setRegistrationRequirement', () => { - const newValue = registrationRequirement.value.plus(1) - const newNoticePeriod = registrationRequirement.noticePeriod.plus(1) + describe('#setMaxGroupSize()', () => { + describe('when the size is different', () => { + describe('when called by the owner', () => { + let resp: any + const newSize = maxGroupSize.plus(1) - it('should set the value and notice period', async () => { - await validators.setRegistrationRequirement(newValue, newNoticePeriod) - const [value, noticePeriod] = await validators.getRegistrationRequirement() - assertEqualBN(value, newValue) - assertEqualBN(noticePeriod, newNoticePeriod) - }) + before(async () => { + resp = await validators.setMaxGroupSize(newSize) + }) - it('should emit the RegistrationRequirementSet event', async () => { - const resp = await validators.setRegistrationRequirement(newValue, newNoticePeriod) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'RegistrationRequirementSet', - args: { - value: new BigNumber(newValue), - noticePeriod: new BigNumber(newNoticePeriod), - }, + it('should set the max group size', async () => { + const size = await validators.getMaxGroupSize() + assertEqualBN(size, newSize) + }) + + it('should emit the MaxGroupSizeSet event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'MaxGroupSizeSet', + args: { + maxGroupSize: new BigNumber(newSize), + }, + }) + }) }) - }) - it('should revert when the requirement is unchanged', async () => { - await assertRevert( - validators.setRegistrationRequirement( - registrationRequirement.value, - registrationRequirement.noticePeriod - ) - ) + describe('when the size is the same', () => { + it('should revert', async () => { + await assertRevert(validators.setMaxGroupSize(maxGroupSize)) + }) + }) }) - it('should revert when called by anyone other than the owner', async () => { - await assertRevert( - validators.setRegistrationRequirement(newValue, newNoticePeriod, { from: nonOwner }) - ) + describe('when called by a non-owner', () => { + it('should revert', async () => { + await assertRevert(validators.setMaxGroupSize(maxGroupSize, { from: nonOwner })) + }) }) }) describe('#registerValidator', () => { const validator = accounts[0] - beforeEach(async () => { - await mockLockedGold.setLockedCommitment( - validator, - registrationRequirement.noticePeriod, - registrationRequirement.value - ) - }) + let resp: any + describe('when the account is not a registered validator', () => { + before(async () => { + await mockLockedGold.setAccountTotalLockedGold( + validator, + registrationRequirements.validator + ) + resp = await validators.registerValidator( + name, + url, + // @ts-ignore bytes type + publicKeysData + ) + }) - it('should mark the account as a validator', async () => { - await validators.registerValidator( - identifier, - name, - url, - // @ts-ignore bytes type - publicKeysData, - registrationRequirement.noticePeriod - ) - assert.isTrue(await validators.isValidator(validator)) - }) + it('should mark the account as a validator', async () => { + assert.isTrue(await validators.isValidator(validator)) + }) - it('should add the account to the list of validators', async () => { - await validators.registerValidator( - identifier, - name, - url, - // @ts-ignore bytes type - publicKeysData, - registrationRequirement.noticePeriod - ) - assert.deepEqual(await validators.getRegisteredValidators(), [validator]) - }) + it('should add the account to the list of validators', async () => { + assert.deepEqual(await validators.getRegisteredValidators(), [validator]) + }) - it('should set the validator identifier, name, url, and public key', async () => { - await validators.registerValidator( - identifier, - name, - url, - // @ts-ignore bytes type - publicKeysData, - registrationRequirement.noticePeriod - ) - const parsedValidator = parseValidatorParams(await validators.getValidator(validator)) - assert.equal(parsedValidator.identifier, identifier) - assert.equal(parsedValidator.name, name) - assert.equal(parsedValidator.url, url) - assert.equal(parsedValidator.publicKeysData, publicKeysData) - }) + it('should set the validator name, url, and public key', async () => { + const parsedValidator = parseValidatorParams(await validators.getValidator(validator)) + assert.equal(parsedValidator.name, name) + assert.equal(parsedValidator.url, url) + assert.equal(parsedValidator.publicKeysData, publicKeysData) + }) - it('should emit the ValidatorRegistered event', async () => { - const resp = await validators.registerValidator( - identifier, - name, - url, - // @ts-ignore bytes type - publicKeysData, - registrationRequirement.noticePeriod - ) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'ValidatorRegistered', - args: { - validator, - identifier, - name, - url, - publicKeysData, - }, + it('should set account balance requirements on locked gold', async () => { + const [value, timestamp] = await mockLockedGold.getAccountMustMaintain(validator) + assert.equal(value, registrationRequirements.validator) + assert.equal(timestamp, MAX_UINT256) + }) + + it('should emit the ValidatorRegistered event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorRegistered', + args: { + validator, + name, + url, + publicKeysData, + }, + }) }) }) describe('when the account is already a registered validator', () => { - beforeEach(async () => { + before(async () => { await validators.registerValidator( - identifier, name, url, // @ts-ignore bytes type - publicKeysData, - registrationRequirement.noticePeriod + publicKeysData ) }) it('should revert', async () => { await assertRevert( validators.registerValidator( - identifier, name, url, // @ts-ignore bytes type - publicKeysData, - registrationRequirement.noticePeriod + publicKeysData ) ) }) @@ -366,23 +373,16 @@ contract('Validators', (accounts: string[]) => { describe('when the account is already a registered validator group', () => { beforeEach(async () => { - await validators.registerValidatorGroup( - identifier, - name, - url, - registrationRequirement.noticePeriod - ) + await validators.registerValidatorGroup(name, url, commission) }) it('should revert', async () => { await assertRevert( validators.registerValidator( - identifier, name, url, // @ts-ignore bytes type - publicKeysData, - registrationRequirement.noticePeriod + publicKeysData ) ) }) @@ -390,22 +390,19 @@ contract('Validators', (accounts: string[]) => { describe('when the account does not meet the registration requirements', () => { beforeEach(async () => { - await mockLockedGold.setLockedCommitment( + await mockLockedGold.setAccountTotalLockedGold( validator, - registrationRequirement.noticePeriod, - registrationRequirement.value.minus(1) + registrationRequirements.validator.minus(1) ) }) it('should revert', async () => { await assertRevert( validators.registerValidator( - identifier, name, url, // @ts-ignore bytes type - publicKeysData, - registrationRequirement.noticePeriod + publicKeysData ) ) }) @@ -415,29 +412,37 @@ contract('Validators', (accounts: string[]) => { describe('#deregisterValidator', () => { const validator = accounts[0] const index = 0 - beforeEach(async () => { - await registerValidator(validator) - }) + let resp: any + describe('when the account is not a registered validator', () => { + before(async () => { + await registerValidator(validator) + resp = await validators.deregisterValidator(index) + }) - it('should mark the account as not a validator', async () => { - await validators.deregisterValidator(index) - assert.isFalse(await validators.isValidator(validator)) - }) + it('should mark the account as not a validator', async () => { + assert.isFalse(await validators.isValidator(validator)) + }) - it('should remove the account from the list of validators', async () => { - await validators.deregisterValidator(index) - assert.deepEqual(await validators.getRegisteredValidators(), []) - }) + it('should remove the account from the list of validators', async () => { + assert.deepEqual(await validators.getRegisteredValidators(), []) + }) - it('should emit the ValidatorDeregistered event', async () => { - const resp = await validators.deregisterValidator(index) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'ValidatorDeregistered', - args: { - validator, - }, + it('should set account balance requirements on locked gold', async () => { + const latestTimestamp = (await web3.eth.getBlock('latest')).timestamp + const [value, timestamp] = await mockLockedGold.getAccountMustMaintain(validator) + assert.equal(value, registrationRequirements.validator) + assert.equal(timestamp, new BigNumber(timestamp).plus(deregistrationLockups.validator)) + }) + + it('should emit the ValidatorDeregistered event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorDeregistered', + args: { + validator, + }, + }) }) }) @@ -749,64 +754,44 @@ contract('Validators', (accounts: string[]) => { describe('#registerValidatorGroup', () => { const group = accounts[0] - beforeEach(async () => { - await mockLockedGold.setLockedCommitment( - group, - registrationRequirement.noticePeriod, - registrationRequirement.value - ) - }) + let resp: any + describe('when the account is not a registered validator group', () => { + before(async () => { + await mockLockedGold.setAccountTotalLockedGold(group, registrationRequirements.group) + resp = await validators.registerValidatorGroup(name, url, commission) + resp = await validators.registerValidator( + name, + url, + // @ts-ignore bytes type + publicKeysData + ) + }) - it('should mark the account as a validator group', async () => { - await validators.registerValidatorGroup( - identifier, - name, - url, - registrationRequirement.noticePeriod - ) - assert.isTrue(await validators.isValidatorGroup(group)) - }) + it('should mark the account as a validator group', async () => { + assert.isTrue(await validators.isValidatorGroup(group)) + }) - it('should add the account to the list of validator groups', async () => { - await validators.registerValidatorGroup( - identifier, - name, - url, - registrationRequirement.noticePeriod - ) - assert.deepEqual(await validators.getRegisteredValidatorGroups(), [group]) - }) + it('should add the account to the list of validator groups', async () => { + assert.deepEqual(await validators.getRegisteredValidatorGroups(), [group]) + }) - it('should set the validator group identifier, name, and url', async () => { - await validators.registerValidatorGroup( - identifier, - name, - url, - registrationRequirement.noticePeriod - ) - const parsedGroup = parseValidatorGroupParams(await validators.getValidatorGroup(group)) - assert.equal(parsedGroup.identifier, identifier) - assert.equal(parsedGroup.name, name) - assert.equal(parsedGroup.url, url) - }) + it('should set the validator group name and url', async () => { + const parsedGroup = parseValidatorGroupParams(await validators.getValidatorGroup(group)) + assert.equal(parsedGroup.name, name) + assert.equal(parsedGroup.url, url) + }) - it('should emit the ValidatorGroupRegistered event', async () => { - const resp = await validators.registerValidatorGroup( - identifier, - name, - url, - registrationRequirement.noticePeriod - ) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'ValidatorGroupRegistered', - args: { - group, - identifier, - name, - url, - }, + it('should emit the ValidatorGroupRegistered event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorGroupRegistered', + args: { + group, + name, + url, + }, + }) }) }) @@ -817,56 +802,31 @@ contract('Validators', (accounts: string[]) => { it('should revert', async () => { await assertRevert( - validators.registerValidatorGroup( - identifier, - name, - url, - registrationRequirement.noticePeriod - ) + validators.registerValidatorGroup(name, url, registrationRequirement.noticePeriod) ) }) }) describe('when the account is already a registered validator group', () => { beforeEach(async () => { - await validators.registerValidatorGroup( - identifier, - name, - url, - registrationRequirement.noticePeriod - ) + await validators.registerValidatorGroup(name, url, commission) }) it('should revert', async () => { - await assertRevert( - validators.registerValidatorGroup( - identifier, - name, - url, - registrationRequirement.noticePeriod - ) - ) + await assertRevert(validators.registerValidatorGroup(name, url, commission)) }) }) describe('when the account does not meet the registration requirements', () => { beforeEach(async () => { - await mockLockedGold.setLockedCommitment( - group, - registrationRequirement.noticePeriod, - registrationRequirement.value.minus(1) + await mockLockedGold.setAccountTotalLockedGold( + validator, + registrationRequirements.group.minus(1) ) }) it('should revert', async () => { - await assertRevert( - validators.registerValidatorGroup( - identifier, - name, - url, - registrationRequirement.noticePeriod - ) - ) + await assertRevert(validators.registerValidatorGroup(name, url, commission)) }) }) }) @@ -874,6 +834,7 @@ contract('Validators', (accounts: string[]) => { describe('#deregisterValidatorGroup', () => { const index = 0 const group = accounts[0] + let resp: any beforeEach(async () => { await registerValidatorGroup(group) }) @@ -888,6 +849,14 @@ contract('Validators', (accounts: string[]) => { assert.deepEqual(await validators.getRegisteredValidatorGroups(), []) }) + it('should set account balance requirements on locked gold', async () => { + await validators.deregisterValidatorGroup(index) + const latestTimestamp = (await web3.eth.getBlock('latest')).timestamp + const [value, timestamp] = await mockLockedGold.getAccountMustMaintain(group) + assert.equal(value, registrationRequirements.group) + assert.equal(timestamp, new BigNumber(timestamp).plus(deregistrationLockups.group)) + }) + it('should emit the ValidatorGroupDeregistered event', async () => { const resp = await validators.deregisterValidatorGroup(index) assert.equal(resp.logs.length, 1) @@ -1006,31 +975,9 @@ contract('Validators', (accounts: string[]) => { }) describe('when the validator is the only member of the group', () => { - it('should emit the ValidatorGroupEmptied event', async () => { - const resp = await validators.removeMember(validator) - assert.equal(resp.logs.length, 2) - const log = resp.logs[1] - assertContainSubset(log, { - event: 'ValidatorGroupEmptied', - args: { - group, - }, - }) - }) - - describe('when the group has received votes', () => { - beforeEach(async () => { - const voter = accounts[2] - const weight = 10 - await mockLockedGold.setWeight(voter, weight) - await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS, { from: voter }) - }) - - it('should remove the group from the list of electable groups with votes', async () => { - await validators.removeMember(validator) - const [groups] = await validators.getValidatorGroupVotes() - assert.deepEqual(groups, []) - }) + it('should mark the group ineligible', async () => { + await validators.removeMember(validator) + assert.isTrue(await mockElection.isIneligible(group)) }) }) @@ -1100,281 +1047,4 @@ contract('Validators', (accounts: string[]) => { }) }) }) - - describe('#vote', () => { - const weight = new BigNumber(5) - const voter = accounts[0] - const validator = accounts[1] - const group = accounts[2] - beforeEach(async () => { - await registerValidatorGroupWithMembers(group, [validator]) - await mockLockedGold.setWeight(voter, weight) - }) - - it("should set the voter's vote", async () => { - await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS) - assert.isTrue(await validators.isVoting(voter)) - assert.equal(await validators.voters(voter), group) - }) - - it('should add the group to the list of those receiving votes', async () => { - await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS) - const [groups] = await validators.getValidatorGroupVotes() - assert.deepEqual(groups, [group]) - }) - - it("should increment the validator group's vote total", async () => { - await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS) - assertEqualBN(await validators.getVotesReceived(group), weight) - }) - - it('should emit the ValidatorGroupVoteCast event', async () => { - const resp = await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'ValidatorGroupVoteCast', - args: { - account: voter, - group, - weight: new BigNumber(weight), - }, - }) - }) - - describe('when the group had not previously received votes', () => { - it('should add the group to the list of electable groups with votes', async () => { - await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS) - const [groups] = await validators.getValidatorGroupVotes() - assert.deepEqual(groups, [group]) - }) - }) - - it('should revert when the group is not a registered validator group', async () => { - await assertRevert(validators.vote(accounts[3], NULL_ADDRESS, NULL_ADDRESS)) - }) - - describe('when the group is empty', () => { - beforeEach(async () => { - await validators.removeMember(validator, { from: group }) - }) - - it('should revert', async () => { - await assertRevert(validators.vote(group, NULL_ADDRESS, NULL_ADDRESS)) - }) - }) - - describe('when the account voting is frozen', () => { - beforeEach(async () => { - await mockLockedGold.setVotingFrozen(voter) - }) - - it('should revert', async () => { - await assertRevert(validators.vote(group, NULL_ADDRESS, NULL_ADDRESS)) - }) - }) - - describe('when the account has no weight', () => { - beforeEach(async () => { - await mockLockedGold.setWeight(voter, NULL_ADDRESS) - }) - - it('should revert', async () => { - await assertRevert(validators.vote(group, NULL_ADDRESS, NULL_ADDRESS)) - }) - }) - describe('when the account has an outstanding vote', () => { - beforeEach(async () => { - await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS) - }) - - it('should revert', async () => { - await assertRevert(validators.vote(group, NULL_ADDRESS, NULL_ADDRESS)) - }) - }) - }) - - describe('#revokeVote', () => { - const weight = 5 - const voter = accounts[0] - const validator = accounts[1] - const group = accounts[2] - beforeEach(async () => { - await registerValidatorGroupWithMembers(group, [validator]) - await mockLockedGold.setWeight(voter, weight) - await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS) - }) - - it("should clear the voter's vote", async () => { - await validators.revokeVote(NULL_ADDRESS, NULL_ADDRESS) - assert.isFalse(await validators.isVoting(voter)) - assert.equal(await validators.voters(voter), NULL_ADDRESS) - }) - - it("should decrement the validator group's vote total", async () => { - await validators.revokeVote(NULL_ADDRESS, NULL_ADDRESS) - const [groups, votes] = await validators.getValidatorGroupVotes() - assert.deepEqual(groups, []) - assert.deepEqual(votes, []) - }) - - it('should emit the ValidatorGroupVoteRevoked event', async () => { - const resp = await validators.revokeVote(NULL_ADDRESS, NULL_ADDRESS) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'ValidatorGroupVoteRevoked', - args: { - account: voter, - group, - weight: new BigNumber(weight), - }, - }) - }) - - describe('when the group had not received other votes', () => { - it('should remove the group from the list of electable groups with votes', async () => { - await validators.revokeVote(NULL_ADDRESS, NULL_ADDRESS) - const [groups] = await validators.getValidatorGroupVotes() - assert.deepEqual(groups, []) - }) - }) - - describe('when the account does not have an outstanding vote', () => { - beforeEach(async () => { - await validators.revokeVote(NULL_ADDRESS, NULL_ADDRESS) - }) - - it('should revert', async () => { - await assertRevert(validators.revokeVote(NULL_ADDRESS, NULL_ADDRESS)) - }) - }) - }) - - describe('#getValidators', () => { - const group1 = accounts[0] - const group2 = accounts[1] - const group3 = accounts[2] - const validator1 = accounts[3] - const validator2 = accounts[4] - const validator3 = accounts[5] - const validator4 = accounts[6] - const validator5 = accounts[7] - const validator6 = accounts[8] - const validator7 = accounts[9] - - // If voterN votes for groupN: - // group1 gets 20 votes per member - // group2 gets 25 votes per member - // group3 gets 30 votes per member - // The ordering of the returned validators should be from group with most votes to group, - // with fewest votes, and within each group, members are elected from first to last. - const voter1 = { address: accounts[0], weight: 80 } - const voter2 = { address: accounts[1], weight: 50 } - const voter3 = { address: accounts[2], weight: 30 } - const assertAddressesEqual = (actual: string[], expected: string[]) => { - assert.deepEqual(actual.map((x) => x.toLowerCase()), expected.map((x) => x.toLowerCase())) - } - - beforeEach(async () => { - await registerValidatorGroupWithMembers(group1, [ - validator1, - validator2, - validator3, - validator4, - ]) - await registerValidatorGroupWithMembers(group2, [validator5, validator6]) - await registerValidatorGroupWithMembers(group3, [validator7]) - - for (const voter of [voter1, voter2, voter3]) { - await mockLockedGold.setWeight(voter.address, voter.weight) - } - }) - - describe('when a single group has >= minElectableValidators as members and received votes', () => { - beforeEach(async () => { - await validators.vote(group1, NULL_ADDRESS, NULL_ADDRESS, { from: voter1.address }) - }) - - it("should return that group's member list", async () => { - assertAddressesEqual(await validators.getValidators(), [ - validator1, - validator2, - validator3, - validator4, - ]) - }) - }) - - describe("when > maxElectableValidators members's groups receive votes", () => { - beforeEach(async () => { - await validators.vote(group1, NULL_ADDRESS, NULL_ADDRESS, { from: voter1.address }) - await validators.vote(group2, NULL_ADDRESS, group1, { from: voter2.address }) - await validators.vote(group3, NULL_ADDRESS, group2, { from: voter3.address }) - }) - - it('should return maxElectableValidators elected validators', async () => { - assertAddressesEqual(await validators.getValidators(), [ - validator1, - validator2, - validator3, - validator5, - validator6, - validator7, - ]) - }) - }) - - describe('when a group receives enough votes for > n seats but only has n members', () => { - beforeEach(async () => { - await mockLockedGold.setWeight(voter3.address, 1000) - await validators.vote(group3, NULL_ADDRESS, NULL_ADDRESS, { from: voter3.address }) - await validators.vote(group1, NULL_ADDRESS, group3, { from: voter1.address }) - await validators.vote(group2, NULL_ADDRESS, group1, { from: voter2.address }) - }) - - it('should elect only n members from that group', async () => { - assertAddressesEqual(await validators.getValidators(), [ - validator7, - validator1, - validator2, - validator3, - validator5, - validator6, - ]) - }) - }) - - describe('when an account has delegated validating to another address', () => { - const validatingDelegate = '0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95' - beforeEach(async () => { - await mockLockedGold.delegateValidating(validator3, validatingDelegate) - await validators.vote(group1, NULL_ADDRESS, NULL_ADDRESS, { from: voter1.address }) - await validators.vote(group2, NULL_ADDRESS, group1, { from: voter2.address }) - await validators.vote(group3, NULL_ADDRESS, group2, { from: voter3.address }) - }) - - it('should return the validating delegate in place of the account', async () => { - assertAddressesEqual(await validators.getValidators(), [ - validator1, - validator2, - validatingDelegate, - validator5, - validator6, - validator7, - ]) - }) - }) - - describe('when there are not enough electable validators', () => { - beforeEach(async () => { - await validators.vote(group2, NULL_ADDRESS, NULL_ADDRESS, { from: voter2.address }) - await validators.vote(group3, NULL_ADDRESS, group2, { from: voter3.address }) - }) - - it('should revert', async () => { - await assertRevert(validators.getValidators()) - }) - }) - }) }) From a6921d3769a4bae80c44f6194fd2504ad37ea21b Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Mon, 23 Sep 2019 19:54:04 -0700 Subject: [PATCH 007/149] LockedGold tests passing --- .../protocol/contracts/common/Signatures.sol | 2 +- .../contracts/governance/Election.sol | 2 +- .../contracts/governance/LockedGold.sol | 31 +- .../contracts/governance/Validators.sol | 2 +- .../governance/interfaces/IElection.sol | 2 +- .../governance/proxies/ElectionProxy.sol | 8 + .../governance/test/MockElection.sol | 13 + .../governance/test/MockLockedGold.sol | 8 +- packages/protocol/lib/registry-utils.ts | 2 + packages/protocol/migrations/10_lockedgold.ts | 2 +- packages/protocol/migrations/11_validators.ts | 9 +- packages/protocol/migrations/12_election.ts | 20 + .../migrations/{12_random.ts => 13_random.ts} | 0 ...{13_attestations.ts => 14_attestations.ts} | 0 .../migrations/{14_escrow.ts => 15_escrow.ts} | 0 .../{15_governance.ts => 16_governance.ts} | 1 + ...t_validators.ts => 17_elect_validators.ts} | 26 +- packages/protocol/migrationsConfig.js | 22 +- packages/protocol/test/common/integration.ts | 12 +- packages/protocol/test/governance/election.ts | 1315 ----------------- .../protocol/test/governance/governance.ts | 63 +- .../protocol/test/governance/lockedgold.ts | 310 ++-- 22 files changed, 302 insertions(+), 1548 deletions(-) create mode 100644 packages/protocol/contracts/governance/proxies/ElectionProxy.sol create mode 100644 packages/protocol/migrations/12_election.ts rename packages/protocol/migrations/{12_random.ts => 13_random.ts} (100%) rename packages/protocol/migrations/{13_attestations.ts => 14_attestations.ts} (100%) rename packages/protocol/migrations/{14_escrow.ts => 15_escrow.ts} (100%) rename packages/protocol/migrations/{15_governance.ts => 16_governance.ts} (99%) rename packages/protocol/migrations/{16_elect_validators.ts => 17_elect_validators.ts} (89%) delete mode 100644 packages/protocol/test/governance/election.ts diff --git a/packages/protocol/contracts/common/Signatures.sol b/packages/protocol/contracts/common/Signatures.sol index 613fcb0369e..381702f0989 100644 --- a/packages/protocol/contracts/common/Signatures.sol +++ b/packages/protocol/contracts/common/Signatures.sol @@ -24,4 +24,4 @@ library Signatures { bytes32 prefixedHash = keccak256(abi.encodePacked(prefix, hash)); return ecrecover(prefixedHash, v, r, s); } -} \ No newline at end of file +} diff --git a/packages/protocol/contracts/governance/Election.sol b/packages/protocol/contracts/governance/Election.sol index 294a6ffebea..1c151426dcf 100644 --- a/packages/protocol/contracts/governance/Election.sol +++ b/packages/protocol/contracts/governance/Election.sol @@ -365,7 +365,7 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { votes.total.total = votes.total.total.add(value); } - function markGroupIneligible(address group) external onlyRegisteredContract('Validators') { + function markGroupIneligible(address group) external onlyRegisteredContract(VALIDATORS_REGISTRY_ID) { votes.total.eligible.remove(group); } diff --git a/packages/protocol/contracts/governance/LockedGold.sol b/packages/protocol/contracts/governance/LockedGold.sol index d860949a348..697e5eba8b5 100644 --- a/packages/protocol/contracts/governance/LockedGold.sol +++ b/packages/protocol/contracts/governance/LockedGold.sol @@ -110,7 +110,6 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr /** * @notice Locks gold to be used for voting. - * @param value The amount of gold to be locked. */ function lock() external payable nonReentrant { require(isAccount(msg.sender)); @@ -119,11 +118,11 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr emit GoldLocked(msg.sender, msg.value); } - function incrementNonvotingAccountBalance(address account, uint256 value) external onlyRegisteredContract('Election') { + function incrementNonvotingAccountBalance(address account, uint256 value) external onlyRegisteredContract(ELECTION_REGISTRY_ID) { _incrementNonvotingAccountBalance(account, value); } - function decrementNonvotingAccountBalance(address account, uint256 value) external onlyRegisteredContract('Election') { + function decrementNonvotingAccountBalance(address account, uint256 value) external onlyRegisteredContract(ELECTION_REGISTRY_ID) { _decrementNonvotingAccountBalance(account, value); } @@ -139,12 +138,12 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr // TODO: Can't unlock if voting in governance. function unlock(uint256 value) external nonReentrant { - require(isAccount(msg.sender)); + require(isAccount(msg.sender), "not account"); Account storage account = accounts[msg.sender]; MustMaintain memory requirement = account.balances.requirements; require( now >= requirement.timestamp || - getAccountTotalLockedGold(msg.sender).sub(value) >= requirement.value + getAccountTotalLockedGold(msg.sender).sub(value) >= requirement.value, "didn't meet mustmaintain requirements" ); _decrementNonvotingAccountBalance(msg.sender, value); uint256 available = now.add(unlockingPeriod); @@ -181,7 +180,7 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr uint256 timestamp ) public - onlyRegisteredContract('Election') + onlyRegisteredContract(ELECTION_REGISTRY_ID) nonReentrant returns (bool) { @@ -199,7 +198,7 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr function getAccountFromVoter(address accountOrVoter) external view returns (address) { address authorizingAccount = authorizedBy[accountOrVoter]; if (authorizingAccount != address(0)) { - require(accounts[authorizingAccount].authorizations.voting == accountOrVoter); + require(accounts[authorizingAccount].authorizations.voting == accountOrVoter, 'failed first check'); return authorizingAccount; } else { require(isAccount(accountOrVoter)); @@ -263,6 +262,19 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr return validator == address(0) ? account : validator; } + function getPendingWithdrawals(address account) public view returns (uint256[] memory, uint256[] memory) { + require(isAccount(account)); + uint256 length = accounts[account].balances.pendingWithdrawals.length; + uint256[] memory values = new uint256[](length); + uint256[] memory timestamps = new uint256[](length); + for (uint256 i = 0; i < length; i++) { + PendingWithdrawal memory pendingWithdrawal = accounts[account].balances.pendingWithdrawals[i]; + values[i] = pendingWithdrawal.value; + timestamps[i] = pendingWithdrawal.timestamp; + } + return (values, timestamps); + } + /** * @notice Authorizes voting or validating power of `msg.sender`'s account to another address. * @param current The address to authorize. @@ -281,12 +293,11 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr bytes32 s ) private - nonReentrant { - require(isAccount(msg.sender) && isNotAccount(current) && isNotAuthorized(current)); + require(isAccount(msg.sender) && isNotAccount(current) && isNotAuthorized(current), "Accounts"); address signer = Signatures.getSignerOfAddress(msg.sender, v, r, s); - require(signer == current); + require(signer == current, "Signature"); authorizedBy[previous] = address(0); authorizedBy[current] = msg.sender; diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index ea344f95c82..74b462efa66 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -341,7 +341,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi function registerValidatorGroup( string calldata name, string calldata url, - uint256 commission, + uint256 commission ) external nonReentrant diff --git a/packages/protocol/contracts/governance/interfaces/IElection.sol b/packages/protocol/contracts/governance/interfaces/IElection.sol index c6935b95abb..03c68fca96c 100644 --- a/packages/protocol/contracts/governance/interfaces/IElection.sol +++ b/packages/protocol/contracts/governance/interfaces/IElection.sol @@ -3,7 +3,7 @@ pragma solidity ^0.5.3; interface IElection { function getTotalVotes() external view returns (uint256); - function getAccountTotalVotes(address account) external view returns (uint256); + function getAccountTotalVotes(address) external view returns (uint256); function markGroupIneligible(address) external; function electValidators() external view returns (address[] memory); } diff --git a/packages/protocol/contracts/governance/proxies/ElectionProxy.sol b/packages/protocol/contracts/governance/proxies/ElectionProxy.sol new file mode 100644 index 00000000000..f8f99107a2e --- /dev/null +++ b/packages/protocol/contracts/governance/proxies/ElectionProxy.sol @@ -0,0 +1,8 @@ +pragma solidity ^0.5.3; + +import "../../common/Proxy.sol"; + + +/* solhint-disable no-empty-blocks */ +contract ElectionProxy is Proxy { +} diff --git a/packages/protocol/contracts/governance/test/MockElection.sol b/packages/protocol/contracts/governance/test/MockElection.sol index e0e85344631..6458ad20f5c 100644 --- a/packages/protocol/contracts/governance/test/MockElection.sol +++ b/packages/protocol/contracts/governance/test/MockElection.sol @@ -12,4 +12,17 @@ contract MockElection is IElection { function markGroupIneligible(address account) external { isIneligible[account] = true; } + + function getTotalVotes() external view returns (uint256) { + return 0; + } + + function getAccountTotalVotes(address) external view returns (uint256) { + return 0; + } + + function electValidators() external view returns (address[] memory) { + address[] memory r = new address[](0); + return r; + } } diff --git a/packages/protocol/contracts/governance/test/MockLockedGold.sol b/packages/protocol/contracts/governance/test/MockLockedGold.sol index 5d757728b8d..984960ed72d 100644 --- a/packages/protocol/contracts/governance/test/MockLockedGold.sol +++ b/packages/protocol/contracts/governance/test/MockLockedGold.sol @@ -13,6 +13,7 @@ contract MockLockedGold is ILockedGold { } mapping(address => uint256) public totalLockedGold; + // TODO(asa): Rename to minimumBalance mapping(address => MustMaintain) public mustMaintain; @@ -20,13 +21,14 @@ contract MockLockedGold is ILockedGold { return accountOrValidator; } - function setAccountMustMaintain(address account, uint256 value, uint256 timestamp) external { + function setAccountMustMaintain(address account, uint256 value, uint256 timestamp) external returns (bool) { mustMaintain[account] = MustMaintain(value, timestamp); + return true; } function getAccountMustMaintain(address account) external view returns (uint256, uint256) { - MustMaintain storage mustMaintain = mustMaintain[account]; - return (mustMaintain.value, mustMaintain.timestamp); + MustMaintain storage m = mustMaintain[account]; + return (m.value, m.timestamp); } function setAccountTotalLockedGold(address account, uint256 value) external { diff --git a/packages/protocol/lib/registry-utils.ts b/packages/protocol/lib/registry-utils.ts index b6f225e63b8..f8176c373bc 100644 --- a/packages/protocol/lib/registry-utils.ts +++ b/packages/protocol/lib/registry-utils.ts @@ -1,6 +1,7 @@ export enum CeloContractName { Attestations = 'Attestations', LockedGold = 'LockedGold', + Election = 'Election', Escrow = 'Escrow', Exchange = 'Exchange', GasCurrencyWhitelist = 'GasCurrencyWhitelist', @@ -24,6 +25,7 @@ export const usesRegistry = [ // TODO(amy): Find another way to create this list export const hasEntryInRegistry: string[] = [ CeloContractName.Attestations, + CeloContractName.Election, CeloContractName.Escrow, CeloContractName.Exchange, CeloContractName.GoldToken, diff --git a/packages/protocol/migrations/10_lockedgold.ts b/packages/protocol/migrations/10_lockedgold.ts index 48c601b92bd..5eb603e7fb0 100644 --- a/packages/protocol/migrations/10_lockedgold.ts +++ b/packages/protocol/migrations/10_lockedgold.ts @@ -7,5 +7,5 @@ module.exports = deploymentForCoreContract( web3, artifacts, CeloContractName.LockedGold, - async () => [config.registry.predeployedProxyAddress, config.lockedGold.maxNoticePeriod] + async () => [config.registry.predeployedProxyAddress, config.lockedGold.unlockingPeriod] ) diff --git a/packages/protocol/migrations/11_validators.ts b/packages/protocol/migrations/11_validators.ts index 6a23bd2c6e8..4a76fac9fcd 100644 --- a/packages/protocol/migrations/11_validators.ts +++ b/packages/protocol/migrations/11_validators.ts @@ -6,10 +6,11 @@ import { ValidatorsInstance } from 'types' const initializeArgs = async (): Promise => { return [ config.registry.predeployedProxyAddress, - config.validators.minElectableValidators, - config.validators.maxElectableValidators, - config.validators.minLockedGoldValue, - config.validators.minLockedGoldNoticePeriod, + config.validators.registrationRequirements.group, + config.validators.registrationRequirements.validator, + config.validators.deregistrationLockups.group, + config.validators.deregistrationLockups.validator, + config.validators.maxGroupSize, ] } diff --git a/packages/protocol/migrations/12_election.ts b/packages/protocol/migrations/12_election.ts new file mode 100644 index 00000000000..b29d2c157d3 --- /dev/null +++ b/packages/protocol/migrations/12_election.ts @@ -0,0 +1,20 @@ +import { CeloContractName } from '@celo/protocol/lib/registry-utils' +import { deploymentForCoreContract } from '@celo/protocol/lib/web3-utils' +import { config } from '@celo/protocol/migrationsConfig' +import { ElectionInstance } from 'types' + +const initializeArgs = async (): Promise => { + return [ + config.registry.predeployedProxyAddress, + config.election.minElectableValidators, + config.election.maxElectableValidators, + config.election.maxVotesPerAccount, + ] +} + +module.exports = deploymentForCoreContract( + web3, + artifacts, + CeloContractName.Election, + initializeArgs +) diff --git a/packages/protocol/migrations/12_random.ts b/packages/protocol/migrations/13_random.ts similarity index 100% rename from packages/protocol/migrations/12_random.ts rename to packages/protocol/migrations/13_random.ts diff --git a/packages/protocol/migrations/13_attestations.ts b/packages/protocol/migrations/14_attestations.ts similarity index 100% rename from packages/protocol/migrations/13_attestations.ts rename to packages/protocol/migrations/14_attestations.ts diff --git a/packages/protocol/migrations/14_escrow.ts b/packages/protocol/migrations/15_escrow.ts similarity index 100% rename from packages/protocol/migrations/14_escrow.ts rename to packages/protocol/migrations/15_escrow.ts diff --git a/packages/protocol/migrations/15_governance.ts b/packages/protocol/migrations/16_governance.ts similarity index 99% rename from packages/protocol/migrations/15_governance.ts rename to packages/protocol/migrations/16_governance.ts index ab5ce88394b..3f6d73a5a70 100644 --- a/packages/protocol/migrations/15_governance.ts +++ b/packages/protocol/migrations/16_governance.ts @@ -49,6 +49,7 @@ module.exports = deploymentForCoreContract( const proxyAndImplementationOwnedByGovernance = [ 'Attestations', 'LockedGold', + 'Election', 'Escrow', 'Exchange', 'GasCurrencyWhitelist', diff --git a/packages/protocol/migrations/16_elect_validators.ts b/packages/protocol/migrations/17_elect_validators.ts similarity index 89% rename from packages/protocol/migrations/16_elect_validators.ts rename to packages/protocol/migrations/17_elect_validators.ts index db17ed8b1f9..800e2f14c16 100644 --- a/packages/protocol/migrations/16_elect_validators.ts +++ b/packages/protocol/migrations/17_elect_validators.ts @@ -11,7 +11,7 @@ import { import { config } from '@celo/protocol/migrationsConfig' import { BigNumber } from 'bignumber.js' import * as bls12377js from 'bls12377js' -import { LockedGoldInstance, ValidatorsInstance } from 'types' +import { ElectionInstance, LockedGoldInstance, ValidatorsInstance } from 'types' const Web3 = require('web3') @@ -27,13 +27,11 @@ async function makeMinimumDeposit(lockedGold: LockedGoldInstance, privateKey: st }) // @ts-ignore - const bondTx = lockedGold.contract.methods.newCommitment( - config.validators.minLockedGoldNoticePeriod - ) + const lockTx = lockedGold.contract.methods.lock() - await sendTransactionWithPrivateKey(web3, bondTx, privateKey, { + await sendTransactionWithPrivateKey(web3, lockTx, privateKey, { to: lockedGold.address, - value: config.validators.minLockedGoldValue, + value: config.validators.registrationRequirements.validator, }) } @@ -131,6 +129,11 @@ module.exports = async (_deployer: any) => { artifacts ) + const election: ElectionInstance = await getDeployedProxiedContract( + 'Election', + artifacts + ) + const valKeys: string[] = config.validators.validatorKeys if (valKeys.length === 0) { @@ -168,11 +171,12 @@ module.exports = async (_deployer: any) => { console.info(' Voting for Validator Group ...') // Make another deposit so our vote has more weight. const minLockedGoldVotePerValidator = 10000 - await lockedGold.newCommitment(0, { + const value = new BigNumber(valKeys.length) + .times(minLockedGoldVotePerValidator) + .times(web3.utils.toWei(1)) + await lockedGold.lock({ // @ts-ignore - value: new BigNumber(valKeys.length) - .times(minLockedGoldVotePerValidator) - .times(config.validators.minLockedGoldValue), + value, }) - await validators.vote(account.address, NULL_ADDRESS, NULL_ADDRESS) + await election.vote(account.address, value, NULL_ADDRESS, NULL_ADDRESS) } diff --git a/packages/protocol/migrationsConfig.js b/packages/protocol/migrationsConfig.js index 06f8a0d7f53..ccaffc073d1 100644 --- a/packages/protocol/migrationsConfig.js +++ b/packages/protocol/migrationsConfig.js @@ -12,11 +12,16 @@ const DefaultConfig = { attestationRequestFeeInDollars: 0.05, }, lockedGold: { - maxNoticePeriod: 60 * 60 * 24 * 365 * 3, // 3 years + unlockingPeriod: 60 * 60 * 24 * 3, // 3 days }, oracles: { reportExpiry: 60 * 60, // 1 hour }, + election: { + minElectableValidators: '10', + maxElectableValidators: '100', + maxVotesPerAccount: 3, + }, exchange: { spread: 5 / 1000, reserveFraction: 1, @@ -57,10 +62,15 @@ const DefaultConfig = { initialAccounts: [], }, validators: { - minElectableValidators: '10', - maxElectableValidators: '100', - minLockedGoldValue: '1000000000000000000', // 1 gold - minLockedGoldNoticePeriod: 60 * 24 * 60 * 60, // 60 days + registrationRequirements: { + group: '1000000000000000000', // 1 gold + validator: '1000000000000000000', // 1 gold + }, + deregistrationLockups: { + group: 60 * 24 * 60 * 60, // 60 days + validator: 60 * 24 * 60 * 60, // 60 days + }, + maxGroupSize: 10, validatorKeys: [], // We register a single validator group during the migration. @@ -78,7 +88,7 @@ const linkedLibraries = { ], SortedLinkedListWithMedian: ['AddressSortedLinkedListWithMedian'], AddressLinkedList: ['Validators'], - AddressSortedLinkedList: ['Validators'], + AddressSortedLinkedList: ['Election'], IntegerSortedLinkedList: ['Governance', 'IntegerSortedLinkedListTest'], AddressSortedLinkedListWithMedian: ['SortedOracles', 'AddressSortedLinkedListWithMedianTest'], Signatures: ['LockedGold', 'Escrow'], diff --git a/packages/protocol/test/common/integration.ts b/packages/protocol/test/common/integration.ts index e607edddce7..8707e32e15d 100644 --- a/packages/protocol/test/common/integration.ts +++ b/packages/protocol/test/common/integration.ts @@ -26,7 +26,7 @@ contract('Integration: Governance', (accounts: string[]) => { let governance: GovernanceInstance let registry: RegistryInstance let proposalTransactions: any - let weight: BigNumber + let value: BigNumber before(async () => { lockedGold = await getDeployedProxiedContract('LockedGold', artifacts) @@ -34,11 +34,9 @@ contract('Integration: Governance', (accounts: string[]) => { registry = await getDeployedProxiedContract('Registry', artifacts) // Set up a LockedGold account with which we can vote. await lockedGold.createAccount() - const noticePeriod = 60 * 60 * 24 // 1 day - const value = new BigNumber('1000000000000000000') + value = new BigNumber('1000000000000000000') // @ts-ignore - await lockedGold.newCommitment(noticePeriod, { value }) - weight = await lockedGold.getAccountWeight(accounts[0]) + await lockedGold.lock({ value }) proposalTransactions = [ { value: 0, @@ -89,7 +87,7 @@ contract('Integration: Governance', (accounts: string[]) => { }) it('should increase the number of upvotes for the proposal', async () => { - assertEqualBN(await governance.getUpvotes(proposalId), weight) + assertEqualBN(await governance.getUpvotes(proposalId), value) }) }) @@ -112,7 +110,7 @@ contract('Integration: Governance', (accounts: string[]) => { it('should increment the vote totals', async () => { const [yes, ,] = await governance.getVoteTotals(proposalId) - assertEqualBN(yes, weight) + assertEqualBN(yes, value) }) }) diff --git a/packages/protocol/test/governance/election.ts b/packages/protocol/test/governance/election.ts deleted file mode 100644 index af5e83f4e28..00000000000 --- a/packages/protocol/test/governance/election.ts +++ /dev/null @@ -1,1315 +0,0 @@ -import { CeloContractName } from '@celo/protocol/lib/registry-utils' -import { fixed1, toFixed, fromFixed, multiply } from '@celo/utils/lib/fixidity' -import { - assertContainSubset, - assertEqualBN, - assertRevert, - NULL_ADDRESS, -} from '@celo/protocol/lib/test-utils' -import BigNumber from 'bignumber.js' -import { - MockLockedGoldContract, - MockLockedGoldInstance, - RegistryContract, - RegistryInstance, - ValidatorsContract, - ValidatorsInstance, -} from 'types' - -const Validators: ValidatorsContract = artifacts.require('Validators') -const MockLockedGold: MockLockedGoldContract = artifacts.require('MockLockedGold') -const Registry: RegistryContract = artifacts.require('Registry') - -// @ts-ignore -// TODO(mcortesi): Use BN -Validators.numberFormat = 'BigNumber' - -const parseValidatorParams = (validatorParams: any) => { - return { - name: validatorParams[1], - url: validatorParams[2], - publicKeysData: validatorParams[3], - affiliation: validatorParams[4], - } -} - -const parseValidatorGroupParams = (groupParams: any) => { - return { - name: groupParams[1], - url: groupParams[2], - members: groupParams[3], - } -} - -const HOUR = 60 * 60 -const DAY = 24 * HOUR -const YEAR = 365 * DAY - -contract('Validators', (accounts: string[]) => { - let validators: ValidatorsInstance - let registry: RegistryInstance - let mockLockedGold: MockLockedGoldInstance - // A random 64 byte hex string. - const publicKey = - 'ea0733ad275e2b9e05541341a97ee82678c58932464fad26164657a111a7e37a9fa0300266fb90e2135a1f1512350cb4e985488a88809b14e3cbe415e76e82b2' - const blsPublicKey = - '4d23d8cd06f30b1fa7cf368e2f5399ab04bb6846c682f493a98a607d3dfb7e53a712bb79b475c57b0ac2785460f91301' - const blsPoP = - '9d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d740501' - - const publicKeysData = '0x' + publicKey + blsPublicKey + blsPoP - - const nonOwner = accounts[1] - const registrationRequirements = { group: new BigNumber(1000), validator: new BigNumber(100) } - const deregistrationLockups = { - group: new BigNumber(100 * DAY), - validator: new BigNumber(60 * DAY), - } - const maxGroupSize = 5 - const name = 'test-name' - const url = 'test-url' - const commission = toFixed(1 / 100) - beforeEach(async () => { - validators = await Validators.new() - mockLockedGold = await MockLockedGold.new() - registry = await Registry.new() - await registry.setAddressFor(CeloContractName.LockedGold, mockLockedGold.address) - await validators.initialize( - registry.address, - registrationRequirements.group, - registrationRequirements.validator, - deregistrationLockups.group, - deregistrationLockups.validator, - maxGroupSize - ) - }) - - const registerValidator = async (validator: string) => { - await mockLockedGold.setAccountTotalLockedGold(validator, registrationRequirements.validator) - await validators.registerValidator( - name, - url, - // @ts-ignore bytes type - publicKeysData, - { from: validator } - ) - } - - const registerValidatorGroup = async (group: string) => { - await mockLockedGold.setAccountTotalLockedGold(group, registrationRequirements.group) - await validators.registerValidatorGroup(name, url, commission, { from: group }) - } - - const registerValidatorGroupWithMembers = async (group: string, members: string[]) => { - await registerValidatorGroup(group) - for (const validator of members) { - await registerValidator(validator) - await validators.affiliate(group, { from: validator }) - await validators.addMember(validator, { from: group }) - } - } - - describe('#initialize()', () => { - it('should have set the owner', async () => { - const owner: string = await validators.owner() - assert.equal(owner, accounts[0]) - }) - - it('should have set the registration requirements', async () => { - const [group, validator] = await validators.getRegistrationRequirement() - assertEqualBN(group, registrationRequirements.group) - assertEqualBN(validator, registrationRequirements.validator) - }) - - it('should have set the deregistration lockups', async () => { - const [group, validator] = await validators.getDeregistrationLockups() - assertEqualBN(group, deregistrationLockups.group) - assertEqualBN(validator, deregistrationLockups.validator) - }) - - it('should not be callable again', async () => { - await assertRevert( - validators.initialize( - registry.address, - registrationRequirements.group, - registrationRequirements.validator, - deregistrationLockups.group, - deregistrationLockups.validator, - maxGroupSize - ) - ) - }) - }) - - describe('#setMinElectableValidators', () => { - const newMinElectableValidators = minElectableValidators.plus(1) - it('should set the minimum deposit', async () => { - await validators.setMinElectableValidators(newMinElectableValidators) - assertEqualBN(await validators.minElectableValidators(), newMinElectableValidators) - }) - - it('should emit the MinElectableValidatorsSet event', async () => { - const resp = await validators.setMinElectableValidators(newMinElectableValidators) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'MinElectableValidatorsSet', - args: { - minElectableValidators: new BigNumber(newMinElectableValidators), - }, - }) - }) - - it('should revert when the minElectableValidators is zero', async () => { - await assertRevert(validators.setMinElectableValidators(0)) - }) - - it('should revert when the minElectableValidators is greater than maxElectableValidators', async () => { - await assertRevert(validators.setMinElectableValidators(maxElectableValidators.plus(1))) - }) - - it('should revert when the minElectableValidators is unchanged', async () => { - await assertRevert(validators.setMinElectableValidators(minElectableValidators)) - }) - - it('should revert when called by anyone other than the owner', async () => { - await assertRevert( - validators.setMinElectableValidators(newMinElectableValidators, { from: nonOwner }) - ) - }) - }) - - describe('#setMaxElectableValidators', () => { - const newMaxElectableValidators = maxElectableValidators.plus(1) - it('should set the minimum deposit', async () => { - await validators.setMaxElectableValidators(newMaxElectableValidators) - assertEqualBN(await validators.maxElectableValidators(), newMaxElectableValidators) - }) - - it('should emit the MaxElectableValidatorsSet event', async () => { - const resp = await validators.setMaxElectableValidators(newMaxElectableValidators) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'MaxElectableValidatorsSet', - args: { - maxElectableValidators: new BigNumber(newMaxElectableValidators), - }, - }) - }) - - it('should revert when the maxElectableValidators is less than minElectableValidators', async () => { - await assertRevert(validators.setMaxElectableValidators(minElectableValidators.minus(1))) - }) - - it('should revert when the maxElectableValidators is unchanged', async () => { - await assertRevert(validators.setMaxElectableValidators(maxElectableValidators)) - }) - - it('should revert when called by anyone other than the owner', async () => { - await assertRevert( - validators.setMaxElectableValidators(newMaxElectableValidators, { from: nonOwner }) - ) - }) - }) - - describe('#setRegistrationRequirement', () => { - const newValue = registrationRequirement.value.plus(1) - const newNoticePeriod = registrationRequirement.noticePeriod.plus(1) - - it('should set the value and notice period', async () => { - await validators.setRegistrationRequirement(newValue, newNoticePeriod) - const [value, noticePeriod] = await validators.getRegistrationRequirement() - assertEqualBN(value, newValue) - assertEqualBN(noticePeriod, newNoticePeriod) - }) - - it('should emit the RegistrationRequirementSet event', async () => { - const resp = await validators.setRegistrationRequirement(newValue, newNoticePeriod) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'RegistrationRequirementSet', - args: { - value: new BigNumber(newValue), - noticePeriod: new BigNumber(newNoticePeriod), - }, - }) - }) - - it('should revert when the requirement is unchanged', async () => { - await assertRevert( - validators.setRegistrationRequirement( - registrationRequirement.value, - registrationRequirement.noticePeriod - ) - ) - }) - - it('should revert when called by anyone other than the owner', async () => { - await assertRevert( - validators.setRegistrationRequirement(newValue, newNoticePeriod, { from: nonOwner }) - ) - }) - }) - - describe('#registerValidator', () => { - const validator = accounts[0] - beforeEach(async () => { - await mockLockedGold.setLockedCommitment( - validator, - registrationRequirement.noticePeriod, - registrationRequirement.value - ) - }) - - it('should mark the account as a validator', async () => { - await validators.registerValidator( - name, - url, - // @ts-ignore bytes type - publicKeysData, - registrationRequirement.noticePeriod - ) - assert.isTrue(await validators.isValidator(validator)) - }) - - it('should add the account to the list of validators', async () => { - await validators.registerValidator( - name, - url, - // @ts-ignore bytes type - publicKeysData, - registrationRequirement.noticePeriod - ) - assert.deepEqual(await validators.getRegisteredValidators(), [validator]) - }) - - it('should set the validator name, url, and public key', async () => { - await validators.registerValidator( - name, - url, - // @ts-ignore bytes type - publicKeysData, - registrationRequirement.noticePeriod - ) - const parsedValidator = parseValidatorParams(await validators.getValidator(validator)) - assert.equal(parsedValidator.name, name) - assert.equal(parsedValidator.url, url) - assert.equal(parsedValidator.publicKeysData, publicKeysData) - }) - - it('should emit the ValidatorRegistered event', async () => { - const resp = await validators.registerValidator( - name, - url, - // @ts-ignore bytes type - publicKeysData, - registrationRequirement.noticePeriod - ) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'ValidatorRegistered', - args: { - validator, - name, - url, - publicKeysData, - }, - }) - }) - - describe('when the account is already a registered validator', () => { - beforeEach(async () => { - await validators.registerValidator( - name, - url, - // @ts-ignore bytes type - publicKeysData, - registrationRequirement.noticePeriod - ) - }) - - it('should revert', async () => { - await assertRevert( - validators.registerValidator( - name, - url, - // @ts-ignore bytes type - publicKeysData, - registrationRequirement.noticePeriod - ) - ) - }) - }) - - describe('when the account is already a registered validator group', () => { - beforeEach(async () => { - await validators.registerValidatorGroup(name, url, registrationRequirement.noticePeriod) - }) - - it('should revert', async () => { - await assertRevert( - validators.registerValidator( - name, - url, - // @ts-ignore bytes type - publicKeysData, - registrationRequirement.noticePeriod - ) - ) - }) - }) - - describe('when the account does not meet the registration requirements', () => { - beforeEach(async () => { - await mockLockedGold.setLockedCommitment( - validator, - registrationRequirement.noticePeriod, - registrationRequirement.value.minus(1) - ) - }) - - it('should revert', async () => { - await assertRevert( - validators.registerValidator( - name, - url, - // @ts-ignore bytes type - publicKeysData, - registrationRequirement.noticePeriod - ) - ) - }) - }) - }) - - describe('#deregisterValidator', () => { - const validator = accounts[0] - const index = 0 - beforeEach(async () => { - await registerValidator(validator) - }) - - it('should mark the account as not a validator', async () => { - await validators.deregisterValidator(index) - assert.isFalse(await validators.isValidator(validator)) - }) - - it('should remove the account from the list of validators', async () => { - await validators.deregisterValidator(index) - assert.deepEqual(await validators.getRegisteredValidators(), []) - }) - - it('should emit the ValidatorDeregistered event', async () => { - const resp = await validators.deregisterValidator(index) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'ValidatorDeregistered', - args: { - validator, - }, - }) - }) - - describe('when the validator is affiliated with a validator group', () => { - const group = accounts[1] - beforeEach(async () => { - await registerValidatorGroup(group) - await validators.affiliate(group) - }) - - it('should emit the ValidatorDeafilliated event', async () => { - const resp = await validators.deregisterValidator(index) - assert.equal(resp.logs.length, 2) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'ValidatorDeaffiliated', - args: { - validator, - group, - }, - }) - }) - - describe('when the validator is a member of that group', () => { - beforeEach(async () => { - await validators.addMember(validator, { from: group }) - }) - - it('should remove the validator from the group membership list', async () => { - await validators.deregisterValidator(index) - const parsedGroup = parseValidatorGroupParams(await validators.getValidatorGroup(group)) - assert.deepEqual(parsedGroup.members, []) - }) - - it('should emit the ValidatorGroupMemberRemoved event', async () => { - const resp = await validators.deregisterValidator(index) - assert.equal(resp.logs.length, 4) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'ValidatorGroupMemberRemoved', - args: { - validator, - group, - }, - }) - }) - - describe('when the validator is the only member of that group', () => { - it('should emit the ValidatorGroupEmptied event', async () => { - const resp = await validators.deregisterValidator(index) - assert.equal(resp.logs.length, 4) - const log = resp.logs[1] - assertContainSubset(log, { - event: 'ValidatorGroupEmptied', - args: { - group, - }, - }) - }) - - describe('when that group has received votes', () => { - beforeEach(async () => { - const voter = accounts[2] - const weight = 10 - await mockLockedGold.setWeight(voter, weight) - await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS, { from: voter }) - }) - - it('should remove the group from the list of electable groups with votes', async () => { - await validators.deregisterValidator(index) - const [groups] = await validators.getValidatorGroupVotes() - assert.deepEqual(groups, []) - }) - }) - }) - }) - }) - - it('should revert when the account is not a registered validator', async () => { - await assertRevert(validators.deregisterValidator(index, { from: accounts[2] })) - }) - - it('should revert when the wrong index is provided', async () => { - await assertRevert(validators.deregisterValidator(index + 1)) - }) - }) - - describe('#affiliate', () => { - const validator = accounts[0] - const group = accounts[1] - beforeEach(async () => { - await registerValidator(validator) - await registerValidatorGroup(group) - }) - - it('should set the affiliate', async () => { - await validators.affiliate(group) - const parsedValidator = parseValidatorParams(await validators.getValidator(validator)) - assert.equal(parsedValidator.affiliation, group) - }) - - it('should emit the ValidatorAffiliated event', async () => { - const resp = await validators.affiliate(group) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'ValidatorAffiliated', - args: { - validator, - group, - }, - }) - }) - - describe('when the validator is already affiliated with a validator group', () => { - const otherGroup = accounts[2] - beforeEach(async () => { - await validators.affiliate(group) - await registerValidatorGroup(otherGroup) - }) - - it('should set the affiliate', async () => { - await validators.affiliate(otherGroup) - const parsedValidator = parseValidatorParams(await validators.getValidator(validator)) - assert.equal(parsedValidator.affiliation, otherGroup) - }) - - it('should emit the ValidatorDeafilliated event', async () => { - const resp = await validators.affiliate(otherGroup) - assert.equal(resp.logs.length, 2) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'ValidatorDeaffiliated', - args: { - validator, - group, - }, - }) - }) - - it('should emit the ValidatorAffiliated event', async () => { - const resp = await validators.affiliate(otherGroup) - assert.equal(resp.logs.length, 2) - const log = resp.logs[1] - assertContainSubset(log, { - event: 'ValidatorAffiliated', - args: { - validator, - group: otherGroup, - }, - }) - }) - - describe('when the validator is a member of that group', () => { - beforeEach(async () => { - await validators.addMember(validator, { from: group }) - }) - - it('should remove the validator from the group membership list', async () => { - await validators.affiliate(otherGroup) - const parsedGroup = parseValidatorGroupParams(await validators.getValidatorGroup(group)) - assert.deepEqual(parsedGroup.members, []) - }) - - it('should emit the ValidatorGroupMemberRemoved event', async () => { - const resp = await validators.affiliate(otherGroup) - assert.equal(resp.logs.length, 4) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'ValidatorGroupMemberRemoved', - args: { - validator, - group, - }, - }) - }) - - describe('when the validator is the only member of that group', () => { - it('should emit the ValidatorGroupEmptied event', async () => { - const resp = await validators.affiliate(otherGroup) - assert.equal(resp.logs.length, 4) - const log = resp.logs[1] - assertContainSubset(log, { - event: 'ValidatorGroupEmptied', - args: { - group, - }, - }) - }) - - describe('when that group has received votes', () => { - beforeEach(async () => { - const voter = accounts[2] - const weight = 10 - await mockLockedGold.setWeight(voter, weight) - await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS, { from: voter }) - }) - - it('should remove the group from the list of electable groups with votes', async () => { - await validators.affiliate(otherGroup) - const [groups] = await validators.getValidatorGroupVotes() - assert.deepEqual(groups, []) - }) - }) - }) - }) - }) - - it('should revert when the account is not a registered validator', async () => { - await assertRevert(validators.affiliate(group, { from: accounts[2] })) - }) - - it('should revert when the group is not a registered validator group', async () => { - await assertRevert(validators.affiliate(accounts[2])) - }) - }) - - describe('#deaffiliate', () => { - const validator = accounts[0] - const group = accounts[1] - beforeEach(async () => { - await registerValidator(validator) - await registerValidatorGroup(group) - await validators.affiliate(group) - }) - - it('should clear the affiliate', async () => { - await validators.deaffiliate() - const parsedValidator = parseValidatorParams(await validators.getValidator(validator)) - assert.equal(parsedValidator.affiliation, NULL_ADDRESS) - }) - - it('should emit the ValidatorDeaffiliated event', async () => { - const resp = await validators.deaffiliate() - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'ValidatorDeaffiliated', - args: { - validator, - group, - }, - }) - }) - - describe('when the validator is a member of the affiliated group', () => { - beforeEach(async () => { - await validators.addMember(validator, { from: group }) - }) - - it('should remove the validator from the group membership list', async () => { - await validators.deaffiliate() - const parsedGroup = parseValidatorGroupParams(await validators.getValidatorGroup(group)) - assert.deepEqual(parsedGroup.members, []) - }) - - it('should emit the ValidatorGroupMemberRemoved event', async () => { - const resp = await validators.deaffiliate() - assert.equal(resp.logs.length, 3) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'ValidatorGroupMemberRemoved', - args: { - validator, - group, - }, - }) - }) - - describe('when the validator is the only member of that group', () => { - it('should emit the ValidatorGroupEmptied event', async () => { - const resp = await validators.deaffiliate() - assert.equal(resp.logs.length, 3) - const log = resp.logs[1] - assertContainSubset(log, { - event: 'ValidatorGroupEmptied', - args: { - group, - }, - }) - }) - - describe('when that group has received votes', () => { - beforeEach(async () => { - const voter = accounts[2] - const weight = 10 - await mockLockedGold.setWeight(voter, weight) - await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS, { from: voter }) - }) - - it('should remove the group from the list of electable groups with votes', async () => { - await validators.deaffiliate() - const [groups] = await validators.getValidatorGroupVotes() - assert.deepEqual(groups, []) - }) - }) - }) - }) - - it('should revert when the account is not a registered validator', async () => { - await assertRevert(validators.deaffiliate({ from: accounts[2] })) - }) - - it('should revert when the validator is not affiliated with a validator group', async () => { - await validators.deaffiliate() - await assertRevert(validators.deaffiliate()) - }) - }) - - describe('#registerValidatorGroup', () => { - const group = accounts[0] - beforeEach(async () => { - await mockLockedGold.setLockedCommitment( - group, - registrationRequirement.noticePeriod, - registrationRequirement.value - ) - }) - - it('should mark the account as a validator group', async () => { - await validators.registerValidatorGroup(name, url, registrationRequirement.noticePeriod) - assert.isTrue(await validators.isValidatorGroup(group)) - }) - - it('should add the account to the list of validator groups', async () => { - await validators.registerValidatorGroup(name, url, registrationRequirement.noticePeriod) - assert.deepEqual(await validators.getRegisteredValidatorGroups(), [group]) - }) - - it('should set the validator group name, and url', async () => { - await validators.registerValidatorGroup(name, url, registrationRequirement.noticePeriod) - const parsedGroup = parseValidatorGroupParams(await validators.getValidatorGroup(group)) - assert.equal(parsedGroup.name, name) - assert.equal(parsedGroup.url, url) - }) - - it('should emit the ValidatorGroupRegistered event', async () => { - const resp = await validators.registerValidatorGroup( - name, - url, - registrationRequirement.noticePeriod - ) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'ValidatorGroupRegistered', - args: { - group, - name, - url, - }, - }) - }) - - describe('when the account is already a registered validator', () => { - beforeEach(async () => { - await registerValidator(group) - }) - - it('should revert', async () => { - await assertRevert( - validators.registerValidatorGroup(name, url, registrationRequirement.noticePeriod) - ) - }) - }) - - describe('when the account is already a registered validator group', () => { - beforeEach(async () => { - await validators.registerValidatorGroup(name, url, registrationRequirement.noticePeriod) - }) - - it('should revert', async () => { - await assertRevert( - validators.registerValidatorGroup(name, url, registrationRequirement.noticePeriod) - ) - }) - }) - - describe('when the account does not meet the registration requirements', () => { - beforeEach(async () => { - await mockLockedGold.setLockedCommitment( - group, - registrationRequirement.noticePeriod, - registrationRequirement.value.minus(1) - ) - }) - - it('should revert', async () => { - await assertRevert( - validators.registerValidatorGroup(name, url, registrationRequirement.noticePeriod) - ) - }) - }) - }) - - describe('#deregisterValidatorGroup', () => { - const index = 0 - const group = accounts[0] - beforeEach(async () => { - await registerValidatorGroup(group) - }) - - it('should mark the account as not a validator group', async () => { - await validators.deregisterValidatorGroup(index) - assert.isFalse(await validators.isValidatorGroup(group)) - }) - - it('should remove the account from the list of validator groups', async () => { - await validators.deregisterValidatorGroup(index) - assert.deepEqual(await validators.getRegisteredValidatorGroups(), []) - }) - - it('should emit the ValidatorGroupDeregistered event', async () => { - const resp = await validators.deregisterValidatorGroup(index) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'ValidatorGroupDeregistered', - args: { - group, - }, - }) - }) - - it('should revert when the account is not a registered validator group', async () => { - await assertRevert(validators.deregisterValidatorGroup(index, { from: accounts[2] })) - }) - - it('should revert when the wrong index is provided', async () => { - await assertRevert(validators.deregisterValidatorGroup(index + 1)) - }) - - describe('when the validator group is not empty', () => { - const validator = accounts[1] - beforeEach(async () => { - await registerValidator(validator) - await validators.affiliate(group, { from: validator }) - await validators.addMember(validator) - }) - - it('should revert', async () => { - await assertRevert(validators.deregisterValidatorGroup(index)) - }) - }) - }) - - describe('#addMember', () => { - const group = accounts[0] - const validator = accounts[1] - beforeEach(async () => { - await registerValidator(validator) - await registerValidatorGroup(group) - await validators.affiliate(group, { from: validator }) - }) - - it('should add the member to the list of members', async () => { - await validators.addMember(validator) - const parsedGroup = parseValidatorGroupParams(await validators.getValidatorGroup(group)) - assert.deepEqual(parsedGroup.members, [validator]) - }) - - it('should emit the ValidatorGroupMemberAdded event', async () => { - const resp = await validators.addMember(validator) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'ValidatorGroupMemberAdded', - args: { - group, - validator, - }, - }) - }) - - it('should revert when the account is not a registered validator group', async () => { - await assertRevert(validators.addMember(validator, { from: accounts[2] })) - }) - - it('should revert when the member is not a registered validator', async () => { - await assertRevert(validators.addMember(accounts[2])) - }) - - describe('when the validator has not affiliated themselves with the group', () => { - beforeEach(async () => { - await validators.deaffiliate({ from: validator }) - }) - - it('should revert', async () => { - await assertRevert(validators.addMember(validator)) - }) - }) - - describe('when the validator is already a member of the group', () => { - beforeEach(async () => { - await validators.addMember(validator) - }) - - it('should revert', async () => { - await assertRevert(validators.addMember(validator)) - }) - }) - }) - - describe('#removeMember', () => { - const group = accounts[0] - const validator = accounts[1] - beforeEach(async () => { - await registerValidatorGroupWithMembers(group, [validator]) - }) - - it('should remove the member from the list of members', async () => { - await validators.removeMember(validator) - const parsedGroup = parseValidatorGroupParams(await validators.getValidatorGroup(group)) - assert.deepEqual(parsedGroup.members, []) - }) - - it('should emit the ValidatorGroupMemberRemoved event', async () => { - const resp = await validators.removeMember(validator) - assert.equal(resp.logs.length, 2) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'ValidatorGroupMemberRemoved', - args: { - group, - validator, - }, - }) - }) - - describe('when the validator is the only member of the group', () => { - it('should emit the ValidatorGroupEmptied event', async () => { - const resp = await validators.removeMember(validator) - assert.equal(resp.logs.length, 2) - const log = resp.logs[1] - assertContainSubset(log, { - event: 'ValidatorGroupEmptied', - args: { - group, - }, - }) - }) - - describe('when the group has received votes', () => { - beforeEach(async () => { - const voter = accounts[2] - const weight = 10 - await mockLockedGold.setWeight(voter, weight) - await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS, { from: voter }) - }) - - it('should remove the group from the list of electable groups with votes', async () => { - await validators.removeMember(validator) - const [groups] = await validators.getValidatorGroupVotes() - assert.deepEqual(groups, []) - }) - }) - }) - - it('should revert when the account is not a registered validator group', async () => { - await assertRevert(validators.removeMember(validator, { from: accounts[2] })) - }) - - it('should revert when the member is not a registered validator', async () => { - await assertRevert(validators.removeMember(accounts[2])) - }) - - describe('when the validator is not a member of the validator group', () => { - beforeEach(async () => { - await validators.deaffiliate({ from: validator }) - }) - - it('should revert', async () => { - await assertRevert(validators.removeMember(validator)) - }) - }) - }) - - describe('#reorderMember', () => { - const group = accounts[0] - const validator1 = accounts[1] - const validator2 = accounts[2] - beforeEach(async () => { - await registerValidatorGroupWithMembers(group, [validator1, validator2]) - }) - - it('should reorder the list of group members', async () => { - await validators.reorderMember(validator2, validator1, NULL_ADDRESS) - const parsedGroup = parseValidatorGroupParams(await validators.getValidatorGroup(group)) - assert.deepEqual(parsedGroup.members, [validator2, validator1]) - }) - - it('should emit the ValidatorGroupMemberReordered event', async () => { - const resp = await validators.reorderMember(validator2, validator1, NULL_ADDRESS) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'ValidatorGroupMemberReordered', - args: { - group, - validator: validator2, - }, - }) - }) - - it('should revert when the account is not a registered validator group', async () => { - await assertRevert( - validators.reorderMember(validator2, validator1, NULL_ADDRESS, { from: accounts[2] }) - ) - }) - - it('should revert when the member is not a registered validator', async () => { - await assertRevert(validators.reorderMember(accounts[3], validator1, NULL_ADDRESS)) - }) - - describe('when the validator is not a member of the validator group', () => { - beforeEach(async () => { - await validators.deaffiliate({ from: validator2 }) - }) - - it('should revert', async () => { - await assertRevert(validators.reorderMember(validator2, validator1, NULL_ADDRESS)) - }) - }) - }) - - describe('#vote', () => { - const weight = new BigNumber(5) - const voter = accounts[0] - const validator = accounts[1] - const group = accounts[2] - beforeEach(async () => { - await registerValidatorGroupWithMembers(group, [validator]) - await mockLockedGold.setWeight(voter, weight) - }) - - it("should set the voter's vote", async () => { - await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS) - assert.isTrue(await validators.isVoting(voter)) - assert.equal(await validators.voters(voter), group) - }) - - it('should add the group to the list of those receiving votes', async () => { - await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS) - const [groups] = await validators.getValidatorGroupVotes() - assert.deepEqual(groups, [group]) - }) - - it("should increment the validator group's vote total", async () => { - await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS) - assertEqualBN(await validators.getVotesReceived(group), weight) - }) - - it('should emit the ValidatorGroupVoteCast event', async () => { - const resp = await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'ValidatorGroupVoteCast', - args: { - account: voter, - group, - weight: new BigNumber(weight), - }, - }) - }) - - describe('when the group had not previously received votes', () => { - it('should add the group to the list of electable groups with votes', async () => { - await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS) - const [groups] = await validators.getValidatorGroupVotes() - assert.deepEqual(groups, [group]) - }) - }) - - it('should revert when the group is not a registered validator group', async () => { - await assertRevert(validators.vote(accounts[3], NULL_ADDRESS, NULL_ADDRESS)) - }) - - describe('when the group is empty', () => { - beforeEach(async () => { - await validators.removeMember(validator, { from: group }) - }) - - it('should revert', async () => { - await assertRevert(validators.vote(group, NULL_ADDRESS, NULL_ADDRESS)) - }) - }) - - describe('when the account voting is frozen', () => { - beforeEach(async () => { - await mockLockedGold.setVotingFrozen(voter) - }) - - it('should revert', async () => { - await assertRevert(validators.vote(group, NULL_ADDRESS, NULL_ADDRESS)) - }) - }) - - describe('when the account has no weight', () => { - beforeEach(async () => { - await mockLockedGold.setWeight(voter, NULL_ADDRESS) - }) - - it('should revert', async () => { - await assertRevert(validators.vote(group, NULL_ADDRESS, NULL_ADDRESS)) - }) - }) - describe('when the account has an outstanding vote', () => { - beforeEach(async () => { - await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS) - }) - - it('should revert', async () => { - await assertRevert(validators.vote(group, NULL_ADDRESS, NULL_ADDRESS)) - }) - }) - }) - - describe('#revokeVote', () => { - const weight = 5 - const voter = accounts[0] - const validator = accounts[1] - const group = accounts[2] - beforeEach(async () => { - await registerValidatorGroupWithMembers(group, [validator]) - await mockLockedGold.setWeight(voter, weight) - await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS) - }) - - it("should clear the voter's vote", async () => { - await validators.revokeVote(NULL_ADDRESS, NULL_ADDRESS) - assert.isFalse(await validators.isVoting(voter)) - assert.equal(await validators.voters(voter), NULL_ADDRESS) - }) - - it("should decrement the validator group's vote total", async () => { - await validators.revokeVote(NULL_ADDRESS, NULL_ADDRESS) - const [groups, votes] = await validators.getValidatorGroupVotes() - assert.deepEqual(groups, []) - assert.deepEqual(votes, []) - }) - - it('should emit the ValidatorGroupVoteRevoked event', async () => { - const resp = await validators.revokeVote(NULL_ADDRESS, NULL_ADDRESS) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'ValidatorGroupVoteRevoked', - args: { - account: voter, - group, - weight: new BigNumber(weight), - }, - }) - }) - - describe('when the group had not received other votes', () => { - it('should remove the group from the list of electable groups with votes', async () => { - await validators.revokeVote(NULL_ADDRESS, NULL_ADDRESS) - const [groups] = await validators.getValidatorGroupVotes() - assert.deepEqual(groups, []) - }) - }) - - describe('when the account does not have an outstanding vote', () => { - beforeEach(async () => { - await validators.revokeVote(NULL_ADDRESS, NULL_ADDRESS) - }) - - it('should revert', async () => { - await assertRevert(validators.revokeVote(NULL_ADDRESS, NULL_ADDRESS)) - }) - }) - }) - - describe('#getValidators', () => { - const group1 = accounts[0] - const group2 = accounts[1] - const group3 = accounts[2] - const validator1 = accounts[3] - const validator2 = accounts[4] - const validator3 = accounts[5] - const validator4 = accounts[6] - const validator5 = accounts[7] - const validator6 = accounts[8] - const validator7 = accounts[9] - - // If voterN votes for groupN: - // group1 gets 20 votes per member - // group2 gets 25 votes per member - // group3 gets 30 votes per member - // The ordering of the returned validators should be from group with most votes to group, - // with fewest votes, and within each group, members are elected from first to last. - const voter1 = { address: accounts[0], weight: 80 } - const voter2 = { address: accounts[1], weight: 50 } - const voter3 = { address: accounts[2], weight: 30 } - const assertAddressesEqual = (actual: string[], expected: string[]) => { - assert.deepEqual(actual.map((x) => x.toLowerCase()), expected.map((x) => x.toLowerCase())) - } - - beforeEach(async () => { - await registerValidatorGroupWithMembers(group1, [ - validator1, - validator2, - validator3, - validator4, - ]) - await registerValidatorGroupWithMembers(group2, [validator5, validator6]) - await registerValidatorGroupWithMembers(group3, [validator7]) - - for (const voter of [voter1, voter2, voter3]) { - await mockLockedGold.setWeight(voter.address, voter.weight) - } - }) - - describe('when a single group has >= minElectableValidators as members and received votes', () => { - beforeEach(async () => { - await validators.vote(group1, NULL_ADDRESS, NULL_ADDRESS, { from: voter1.address }) - }) - - it("should return that group's member list", async () => { - assertAddressesEqual(await validators.getValidators(), [ - validator1, - validator2, - validator3, - validator4, - ]) - }) - }) - - describe("when > maxElectableValidators members's groups receive votes", () => { - beforeEach(async () => { - await validators.vote(group1, NULL_ADDRESS, NULL_ADDRESS, { from: voter1.address }) - await validators.vote(group2, NULL_ADDRESS, group1, { from: voter2.address }) - await validators.vote(group3, NULL_ADDRESS, group2, { from: voter3.address }) - }) - - it('should return maxElectableValidators elected validators', async () => { - assertAddressesEqual(await validators.getValidators(), [ - validator1, - validator2, - validator3, - validator5, - validator6, - validator7, - ]) - }) - }) - - describe('when a group receives enough votes for > n seats but only has n members', () => { - beforeEach(async () => { - await mockLockedGold.setWeight(voter3.address, 1000) - await validators.vote(group3, NULL_ADDRESS, NULL_ADDRESS, { from: voter3.address }) - await validators.vote(group1, NULL_ADDRESS, group3, { from: voter1.address }) - await validators.vote(group2, NULL_ADDRESS, group1, { from: voter2.address }) - }) - - it('should elect only n members from that group', async () => { - assertAddressesEqual(await validators.getValidators(), [ - validator7, - validator1, - validator2, - validator3, - validator5, - validator6, - ]) - }) - }) - - describe('when an account has delegated validating to another address', () => { - const validatingDelegate = '0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95' - beforeEach(async () => { - await mockLockedGold.delegateValidating(validator3, validatingDelegate) - await validators.vote(group1, NULL_ADDRESS, NULL_ADDRESS, { from: voter1.address }) - await validators.vote(group2, NULL_ADDRESS, group1, { from: voter2.address }) - await validators.vote(group3, NULL_ADDRESS, group2, { from: voter3.address }) - }) - - it('should return the validating delegate in place of the account', async () => { - assertAddressesEqual(await validators.getValidators(), [ - validator1, - validator2, - validatingDelegate, - validator5, - validator6, - validator7, - ]) - }) - }) - - describe('when there are not enough electable validators', () => { - beforeEach(async () => { - await validators.vote(group2, NULL_ADDRESS, NULL_ADDRESS, { from: voter2.address }) - await validators.vote(group3, NULL_ADDRESS, group2, { from: voter3.address }) - }) - - it('should revert', async () => { - await assertRevert(validators.getValidators()) - }) - }) - }) -}) diff --git a/packages/protocol/test/governance/governance.ts b/packages/protocol/test/governance/governance.ts index 3ed9c299682..302a9276af0 100644 --- a/packages/protocol/test/governance/governance.ts +++ b/packages/protocol/test/governance/governance.ts @@ -794,7 +794,7 @@ contract('Governance', (accounts: string[]) => { const weight = new BigNumber(10) const proposalId = new BigNumber(1) beforeEach(async () => { - await mockLockedGold.setWeight(account, weight) + await mockLockedGold.setAccountTotalLockedGold(account, weight) await governance.propose( [transactionSuccess1.value], [transactionSuccess1.destination], @@ -812,7 +812,9 @@ contract('Governance', (accounts: string[]) => { it('should mark the account as having upvoted the proposal', async () => { await governance.upvote(proposalId, 0, 0) - assertEqualBN(await governance.getUpvotedProposal(account), proposalId) + const [recordId, recordWeight] = await governance.getUpvoteRecord(account) + assertEqualBN(recordId, proposalId) + assertEqualBN(recordWeight, weight) }) it('should return true', async () => { @@ -834,13 +836,8 @@ contract('Governance', (accounts: string[]) => { }) }) - it('should revert when the account is frozen', async () => { - await mockLockedGold.setVotingFrozen(account) - await assertRevert(governance.upvote(proposalId, 0, 0)) - }) - it('should revert when the account weight is 0', async () => { - await mockLockedGold.setWeight(account, 0) + await mockLockedGold.setAccountTotalLockedGold(account, 0) await assertRevert(governance.upvote(proposalId, 0, 0)) }) @@ -883,7 +880,7 @@ contract('Governance', (accounts: string[]) => { { value: minDeposit } ) const otherAccount = accounts[1] - await mockLockedGold.setWeight(otherAccount, weight) + await mockLockedGold.setAccountTotalLockedGold(otherAccount, weight) await governance.upvote(otherProposalId, proposalId, 0, { from: otherAccount }) await timeTravel(queueExpiry, web3) }) @@ -948,7 +945,7 @@ contract('Governance', (accounts: string[]) => { const weight = new BigNumber(10) const proposalId = new BigNumber(1) beforeEach(async () => { - await mockLockedGold.setWeight(account, weight) + await mockLockedGold.setAccountTotalLockedGold(account, weight) await governance.propose( [transactionSuccess1.value], [transactionSuccess1.destination], @@ -972,12 +969,9 @@ contract('Governance', (accounts: string[]) => { it('should mark the account as not having upvoted a proposal', async () => { await governance.revokeUpvote(0, 0) - assertEqualBN(await governance.getUpvotedProposal(account), 0) - }) - - it('should succeed when the account is frozen', async () => { - await mockLockedGold.setVotingFrozen(account) - await governance.revokeUpvote(0, 0) + const [recordId, recordWeight] = await governance.getUpvoteRecord(account) + assertEqualBN(recordId, 0) + assertEqualBN(recordWeight, 0) }) it('should emit the ProposalUpvoteRevoked event', async () => { @@ -1000,7 +994,7 @@ contract('Governance', (accounts: string[]) => { }) it('should revert when the account weight is 0', async () => { - await mockLockedGold.setWeight(account, 0) + await mockLockedGold.setAccountTotalLockedGold(account, 0) await assertRevert(governance.revokeUpvote(0, 0)) }) @@ -1019,7 +1013,9 @@ contract('Governance', (accounts: string[]) => { it('should mark the account as not having upvoted a proposal', async () => { await governance.revokeUpvote(0, 0) - assertEqualBN(await governance.getUpvotedProposal(account), 0) + const [recordId, recordWeight] = await governance.getUpvoteRecord(account) + assertEqualBN(recordId, 0) + assertEqualBN(recordWeight, 0) }) it('should emit the ProposalExpired event', async () => { @@ -1049,7 +1045,9 @@ contract('Governance', (accounts: string[]) => { it('should mark the account as not having upvoted a proposal', async () => { await governance.revokeUpvote(0, 0) - assertEqualBN(await governance.getUpvotedProposal(account), 0) + const [recordId, recordWeight] = await governance.getUpvoteRecord(account) + assertEqualBN(recordId, 0) + assertEqualBN(recordWeight, 0) }) }) }) @@ -1229,7 +1227,7 @@ contract('Governance', (accounts: string[]) => { await timeTravel(dequeueFrequency, web3) await governance.approve(proposalId, index) await timeTravel(approvalStageDuration, web3) - await mockLockedGold.setWeight(account, weight) + await mockLockedGold.setAccountTotalLockedGold(account, weight) }) it('should return true', async () => { @@ -1273,13 +1271,8 @@ contract('Governance', (accounts: string[]) => { }) }) - it('should revert when the account is frozen', async () => { - await mockLockedGold.setVotingFrozen(account) - await assertRevert(governance.vote(proposalId, index, value)) - }) - it('should revert when the account weight is 0', async () => { - await mockLockedGold.setWeight(account, 0) + await mockLockedGold.setAccountTotalLockedGold(account, 0) await assertRevert(governance.vote(proposalId, index, value)) }) @@ -1393,7 +1386,7 @@ contract('Governance', (accounts: string[]) => { await timeTravel(dequeueFrequency, web3) await governance.approve(proposalId, index) await timeTravel(approvalStageDuration, web3) - await mockLockedGold.setWeight(account, weight) + await mockLockedGold.setAccountTotalLockedGold(account, weight) await governance.vote(proposalId, index, value) await timeTravel(referendumStageDuration, web3) }) @@ -1439,7 +1432,7 @@ contract('Governance', (accounts: string[]) => { await timeTravel(dequeueFrequency, web3) await governance.approve(proposalId, index) await timeTravel(approvalStageDuration, web3) - await mockLockedGold.setWeight(account, weight) + await mockLockedGold.setAccountTotalLockedGold(account, weight) await governance.vote(proposalId, index, value) await timeTravel(referendumStageDuration, web3) }) @@ -1465,7 +1458,7 @@ contract('Governance', (accounts: string[]) => { await timeTravel(dequeueFrequency, web3) await governance.approve(proposalId, index) await timeTravel(approvalStageDuration, web3) - await mockLockedGold.setWeight(account, weight) + await mockLockedGold.setAccountTotalLockedGold(account, weight) await governance.vote(proposalId, index, value) await timeTravel(referendumStageDuration, web3) }) @@ -1514,7 +1507,7 @@ contract('Governance', (accounts: string[]) => { await timeTravel(dequeueFrequency, web3) await governance.approve(proposalId, index) await timeTravel(approvalStageDuration, web3) - await mockLockedGold.setWeight(account, weight) + await mockLockedGold.setAccountTotalLockedGold(account, weight) await governance.vote(proposalId, index, value) await timeTravel(referendumStageDuration, web3) }) @@ -1538,7 +1531,7 @@ contract('Governance', (accounts: string[]) => { await timeTravel(dequeueFrequency, web3) await governance.approve(proposalId, index) await timeTravel(approvalStageDuration, web3) - await mockLockedGold.setWeight(account, weight) + await mockLockedGold.setAccountTotalLockedGold(account, weight) await governance.vote(proposalId, index, value) await timeTravel(referendumStageDuration, web3) }) @@ -1563,7 +1556,7 @@ contract('Governance', (accounts: string[]) => { await timeTravel(dequeueFrequency, web3) await governance.approve(proposalId, index) await timeTravel(approvalStageDuration, web3) - await mockLockedGold.setWeight(account, weight) + await mockLockedGold.setAccountTotalLockedGold(account, weight) await governance.vote(proposalId, index, value) await timeTravel(referendumStageDuration, web3) await timeTravel(executionStageDuration, web3) @@ -1586,6 +1579,7 @@ contract('Governance', (accounts: string[]) => { }) }) + /* describe('#isVoting()', () => { describe('when the account has never acted on a proposal', () => { it('should return false', async () => { @@ -1597,7 +1591,7 @@ contract('Governance', (accounts: string[]) => { const weight = 10 const proposalId = 1 beforeEach(async () => { - await mockLockedGold.setWeight(account, weight) + await mockLockedGold.setAccountTotalLockedGold(account, weight) await governance.propose( [transactionSuccess1.value], [transactionSuccess1.destination], @@ -1651,7 +1645,7 @@ contract('Governance', (accounts: string[]) => { await timeTravel(dequeueFrequency, web3) await governance.approve(proposalId, index) await timeTravel(approvalStageDuration, web3) - await mockLockedGold.setWeight(account, weight) + await mockLockedGold.setAccountTotalLockedGold(account, weight) await governance.vote(proposalId, index, value) }) @@ -1670,4 +1664,5 @@ contract('Governance', (accounts: string[]) => { }) }) }) + */ }) diff --git a/packages/protocol/test/governance/lockedgold.ts b/packages/protocol/test/governance/lockedgold.ts index 9a29a8f0464..08544005254 100644 --- a/packages/protocol/test/governance/lockedgold.ts +++ b/packages/protocol/test/governance/lockedgold.ts @@ -12,10 +12,8 @@ import { LockedGoldInstance, MockGoldTokenContract, MockGoldTokenInstance, - MockGovernanceContract, - MockGovernanceInstance, - MockValidatorsContract, - MockValidatorsInstance, + MockElectionContract, + MockElectionInstance, RegistryContract, RegistryInstance, } from 'types' @@ -23,8 +21,7 @@ import { const LockedGold: LockedGoldContract = artifacts.require('LockedGold') const Registry: RegistryContract = artifacts.require('Registry') const MockGoldToken: MockGoldTokenContract = artifacts.require('MockGoldToken') -const MockGovernance: MockGovernanceContract = artifacts.require('MockGovernance') -const MockValidators: MockValidatorsContract = artifacts.require('MockValidators') +const MockElection: MockElectionContract = artifacts.require('MockElection') // @ts-ignore // TODO(mcortesi): Use BN @@ -32,17 +29,19 @@ LockedGold.numberFormat = 'BigNumber' const HOUR = 60 * 60 const DAY = 24 * HOUR -const YEAR = 365 * DAY +let authorizationTests = { voter: {}, validator: {} } contract('LockedGold', (accounts: string[]) => { let account = accounts[0] const nonOwner = accounts[1] const unlockingPeriod = 3 * DAY - let mockGoldToken: MockGoldTokenInstance - let mockGovernance: MockGovernanceInstance - let mockValidators: MockValidatorsInstance let lockedGold: LockedGoldInstance let registry: RegistryInstance + let mockElection: MockElectionInstance + + const capitalize = (s: string) => { + return s.charAt(0).toUpperCase() + s.slice(1) + } const getParsedSignatureOfAddress = async (address: string, signer: string) => { // @ts-ignore @@ -56,16 +55,25 @@ contract('LockedGold', (accounts: string[]) => { } beforeEach(async () => { + const mockGoldToken: MockGoldTokenInstance = await MockGoldToken.new() + mockElection = await MockElection.new() lockedGold = await LockedGold.new() - mockGoldToken = await MockGoldToken.new() - mockGovernance = await MockGovernance.new() - mockValidators = await MockValidators.new() registry = await Registry.new() await registry.setAddressFor(CeloContractName.GoldToken, mockGoldToken.address) - await registry.setAddressFor(CeloContractName.Governance, mockGovernance.address) - await registry.setAddressFor(CeloContractName.Validators, mockValidators.address) + await registry.setAddressFor(CeloContractName.Election, mockElection.address) await lockedGold.initialize(registry.address, unlockingPeriod) await lockedGold.createAccount() + + authorizationTests.voter = { + fn: lockedGold.authorizeVoter, + getAuthorizedFromAccount: lockedGold.getVoterFromAccount, + getAccountFromAuthorized: lockedGold.getAccountFromVoter, + } + authorizationTests.validator = { + fn: lockedGold.authorizeValidator, + getAuthorizedFromAccount: lockedGold.getValidatorFromAccount, + getAccountFromAuthorized: lockedGold.getAccountFromValidator, + } }) describe('#initialize()', () => { @@ -80,8 +88,8 @@ contract('LockedGold', (accounts: string[]) => { }) it('should set the unlocking period', async () => { - const period: string = await lockedGold.unlockingPeriod() - assert.equal(unlockingPeriod, period) + const period = await lockedGold.unlockingPeriod() + assertEqualBN(unlockingPeriod, period) }) it('should revert if already initialized', async () => { @@ -102,141 +110,135 @@ contract('LockedGold', (accounts: string[]) => { }) }) - const authorizationTests = [ - { - name: 'Voter', - fn: lockedGold.authorizeVoter, - getFromAccount: lockedGold.getVoterFromAccount, - getAccount: lockedGold.getAccountFromVoter, - }, - { - name: 'Validator', - fn: lockedGold.authorizeValidator, - getFromAccount: lockedGold.getValidatorFromAccount, - getAccount: lockedGold.getAccountFromValidator, - }, - ] - for (const test in authorizationTests) { - describe(`#authorize${test.name}()`, () => { - const authorized = accounts[1] - let sig - + Object.keys(authorizationTests).forEach((key) => { + describe('authorization tests:', () => { + let authorizationTest: any beforeEach(async () => { - sig = await getParsedSignatureOfAddress(account, authorized) + authorizationTest = authorizationTests[key] }) - it(`should set the authorized ${test.name}`, async () => { - await test.fn(authorized, sig.v, sig.r, sig.s) - assert.equal(await lockedGold.authorizedBy(authorized), account) - assert.equal(await test.getFromAccount(account), authorized) - assert.equal(await test.getAccount(authorized), account) - }) + describe(`#authorize${capitalize(key)}()`, () => { + const authorized = accounts[1] + let sig - it(`should emit a ${test.name}Authorized event`, async () => { - const resp = await test.fn(authorized, sig.v, sig.r, sig.s) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches(log, `${test.name}Authorized`, { - account, - authorized, + beforeEach(async () => { + sig = await getParsedSignatureOfAddress(account, authorized) }) - }) - - it(`should revert if the ${test.name} is an account`, async () => { - await lockedGold.createAccount({ from: authorized }) - await assertRevert(test.fn(authorized, sig.v, sig.r, sig.s)) - }) - it(`should revert if the ${test.name} is already authorized`, async () => { - const otherAccount = accounts[2] - const otherSig = await getParsedSignatureOfAddress(otherAccount, authorized) - await lockedGold.createAccount({ from: otherAccount }) - await test.fn(authorized, otherSig.v, otherSig.r, otherSig.s, { - from: otherAccount, + it(`should set the authorized ${key}`, async () => { + await authorizationTest.fn(authorized, sig.v, sig.r, sig.s) + assert.equal(await lockedGold.authorizedBy(authorized), account) + assert.equal(await authorizationTest.getAuthorizedFromAccount(account), authorized) + assert.equal(await authorizationTest.getAccountFromAuthorized(authorized), account) }) - await assertRevert(test.fn(authorized, sig.v, sig.r, sig.s)) - }) - it('should revert if the signature is incorrect', async () => { - const nonVoter = accounts[3] - const incorrectSig = await getParsedSignatureOfAddress(account, nonVoter) - await assertRevert(test.fn(authorized, incorrectSig.v, incorrectSig.r, incorrectSig.s)) - }) - - describe('when a previous authorization has been made', async () => { - const newAuthorized = accounts[2] - let newSig - beforeEach(async () => { - await test.fn(authorized, sig.v, sig.r, sig.s) - newSig = await getParsedSignatureOfAddress(account, newAuthorized) - await test.fn(newAuthorized, newSig.v, newSig.r, newSig.s) + it(`should emit a ${capitalize(key)}Authorized event`, async () => { + const resp = await authorizationTest.fn(authorized, sig.v, sig.r, sig.s) + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + const expected = { account } + expected[key] = authorized + assertLogMatches(log, `${capitalize(key)}Authorized`, expected) }) - it(`should set the new authorized ${test.name}`, async () => { - assert.equal(await lockedGold.authorizedBy(newAuthorized), account) - assert.equal(await test.getFromAccount(account), newAuthorized) - assert.equal(await test.getAccount(newAuthorized), account) + it(`should revert if the ${key} is an account`, async () => { + await lockedGold.createAccount({ from: authorized }) + await assertRevert(authorizationTest.fn(authorized, sig.v, sig.r, sig.s)) }) - it('should reset the previous authorization', async () => { - assert.equal(await lockedGold.authorizedBy(authorized), NULL_ADDRESS) + it(`should revert if the ${key} is already authorized`, async () => { + const otherAccount = accounts[2] + const otherSig = await getParsedSignatureOfAddress(otherAccount, authorized) + await lockedGold.createAccount({ from: otherAccount }) + await authorizationTest.fn(authorized, otherSig.v, otherSig.r, otherSig.s, { + from: otherAccount, + }) + await assertRevert(authorizationTest.fn(authorized, sig.v, sig.r, sig.s)) }) - }) - }) - describe(`#getAccountFrom${test.name}()`, () => { - describe(`when the account has not authorized a ${test.name}`, () => { - it('should return the account when passed the account', async () => { - assert.equal(await test.getAccount(account), account) + it('should revert if the signature is incorrect', async () => { + const nonVoter = accounts[3] + const incorrectSig = await getParsedSignatureOfAddress(account, nonVoter) + await assertRevert( + authorizationTest.fn(authorized, incorrectSig.v, incorrectSig.r, incorrectSig.s) + ) }) - it('should revert when passed an address that is not an account', async () => { - await assertRevert(test.getAccount(accounts[1])) + describe('when a previous authorization has been made', async () => { + const newAuthorized = accounts[2] + let newSig + beforeEach(async () => { + await authorizationTest.fn(authorized, sig.v, sig.r, sig.s) + newSig = await getParsedSignatureOfAddress(account, newAuthorized) + await authorizationTest.fn(newAuthorized, newSig.v, newSig.r, newSig.s) + }) + + it(`should set the new authorized ${key}`, async () => { + assert.equal(await lockedGold.authorizedBy(newAuthorized), account) + assert.equal(await authorizationTest.getAuthorizedFromAccount(account), newAuthorized) + assert.equal(await authorizationTest.getAccountFromAuthorized(newAuthorized), account) + }) + + it('should reset the previous authorization', async () => { + assert.equal(await lockedGold.authorizedBy(authorized), NULL_ADDRESS) + }) }) }) - describe(`when the account has authorized a ${test.name}`, () => { - const authorized = accounts[1] - before(async () => { - const sig = await getParsedSignatureOfAddress(account, voter) - await test.fn(authorized, sig.v, sig.r, sig.s) - }) + describe(`#getAccountFrom${capitalize(key)}()`, () => { + describe(`when the account has not authorized a ${key}`, () => { + it('should return the account when passed the account', async () => { + assert.equal(await authorizationTest.getAccountFromAuthorized(account), account) + }) - it('should return the account when passed the account', async () => { - assert.equal(await test.getAccount(account), account) + it('should revert when passed an address that is not an account', async () => { + await assertRevert(authorizationTest.getAccountFromAuthorized(accounts[1])) + }) }) - it(`should return the account when passed the ${test.name}`, async () => { - assert.equal(await test.getAccount(authorized), account) - }) - }) - }) + describe(`when the account has authorized a ${key}`, () => { + const authorized = accounts[1] + beforeEach(async () => { + const sig = await getParsedSignatureOfAddress(account, authorized) + await authorizationTest.fn(authorized, sig.v, sig.r, sig.s) + }) - describe(`#get${test.name}FromAccount()`, () => { - describe(`when the account has not authorized a ${test.name}`, () => { - it('should return the account when passed the account', async () => { - assert.equal(await test.getFromAccount(account), account) - }) + it('should return the account when passed the account', async () => { + assert.equal(await authorizationTest.getAccountFromAuthorized(account), account) + }) - it('should revert when not passed an account', async () => { - await assertRevert(test.getFromAccount(account), account) + it(`should return the account when passed the ${key}`, async () => { + assert.equal(await authorizationTest.getAccountFromAuthorized(authorized), account) + }) }) }) - describe(`when the account has authorized a ${test.name}`, () => { - const authorized = accounts[1] + describe(`#get${capitalize(key)}FromAccount()`, () => { + describe(`when the account has not authorized a ${key}`, () => { + it('should return the account when passed the account', async () => { + assert.equal(await authorizationTest.getAuthorizedFromAccount(account), account) + }) - before(async () => { - const sig = await getParsedSignatureOfAddress(account, authorized) - await test.fn(authorized, sig.v, sig.r, sig.s) + it('should revert when not passed an account', async () => { + await assertRevert(authorizationTest.getAuthorizedFromAccount(accounts[1]), account) + }) }) - it(`should return the ${test.name} when passed the account`, async () => { - assert.equal(await test.getFromAccount(account), authorized) + describe(`when the account has authorized a ${key}`, () => { + const authorized = accounts[1] + + beforeEach(async () => { + const sig = await getParsedSignatureOfAddress(account, authorized) + await authorizationTest.fn(authorized, sig.v, sig.r, sig.s) + }) + + it(`should return the ${key} when passed the account`, async () => { + assert.equal(await authorizationTest.getAuthorizedFromAccount(account), authorized) + }) }) }) }) - } + }) describe('#lock()', () => { const value = 1000 @@ -244,25 +246,25 @@ contract('LockedGold', (accounts: string[]) => { it("should increase the account's nonvoting locked gold balance", async () => { // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails await lockedGold.lock({ value }) - assert.equal(await lockedGold.getAccountNonvotingLockedGold(account), value) + assertEqualBN(await lockedGold.getAccountNonvotingLockedGold(account), value) }) it("should increase the account's total locked gold balance", async () => { // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails await lockedGold.lock({ value }) - assert.equal(await lockedGold.getAccountTotalLockedGold(account), value) + assertEqualBN(await lockedGold.getAccountTotalLockedGold(account), value) }) it('should increase the nonvoting locked gold balance', async () => { // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails await lockedGold.lock({ value }) - assert.equal(await lockedGold.getNonvotingLockedGold(), value) + assertEqualBN(await lockedGold.getNonvotingLockedGold(), value) }) it('should increase the total locked gold balance', async () => { // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails await lockedGold.lock({ value }) - assert.equal(await lockedGold.getTotalLockedGold(), value) + assertEqualBN(await lockedGold.getTotalLockedGold(), value) }) it('should emit a GoldLocked event', async () => { @@ -277,6 +279,7 @@ contract('LockedGold', (accounts: string[]) => { }) it('should revert when the specified value is 0', async () => { + // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails await assertRevert(lockedGold.lock({ value: 0 })) }) @@ -290,7 +293,7 @@ contract('LockedGold', (accounts: string[]) => { let availabilityTime: BigNumber let resp: any describe('when there are no balance requirements', () => { - before(async () => { + beforeEach(async () => { // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails await lockedGold.lock({ value }) resp = await lockedGold.unlock(value) @@ -300,26 +303,27 @@ contract('LockedGold', (accounts: string[]) => { }) it('should add a pending withdrawal', async () => { - const pendingWithdrawals = await lockedGold.getPendingWithdrawals(account) - assert.equal(pendingWithdrawals.length, 1) - assert.equal(pendingWithdrawals[0], value) - assert.equal(pendingWithdrawals[1], availabilityTime) + const [values, timestamps] = await lockedGold.getPendingWithdrawals(account) + assert.equal(values.length, 1) + assert.equal(timestamps.length, 1) + assertEqualBN(values[0], value) + assertEqualBN(timestamps[0], availabilityTime) }) it("should decrease the account's nonvoting locked gold balance", async () => { - assert.equal(await lockedGold.getAccountNonvotingLockedGold(account), 0) + assertEqualBN(await lockedGold.getAccountNonvotingLockedGold(account), 0) }) it("should decrease the account's total locked gold balance", async () => { - assert.equal(await lockedGold.getAccountTotalLockedGold(account), 0) + assertEqualBN(await lockedGold.getAccountTotalLockedGold(account), 0) }) it('should decrease the nonvoting locked gold balance', async () => { - assert.equal(await lockedGold.getNonvotingLockedGold(), 0) + assertEqualBN(await lockedGold.getNonvotingLockedGold(), 0) }) it('should decrease the total locked gold balance', async () => { - assert.equal(await lockedGold.getTotalLockedGold(), 0) + assertEqualBN(await lockedGold.getTotalLockedGold(), 0) }) it('should emit a GoldUnlocked event', async () => { @@ -328,21 +332,22 @@ contract('LockedGold', (accounts: string[]) => { assertLogMatches(log, 'GoldUnlocked', { account, value: new BigNumber(value), - available, + available: availabilityTime, }) }) }) describe('when there are balance requirements', () => { let mustMaintain: any - before(async () => { + beforeEach(async () => { // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails await lockedGold.lock({ value }) // Allow ourselves to call `setAccountMustMaintain()` - await registry.setAddressFor('Election', account) + await registry.setAddressFor(CeloContractName.Election, account) const timestamp = (await web3.eth.getBlock('latest')).timestamp - const mustMaintain = { value: 100, timestamp: timestamp + DAY } + mustMaintain = { value: 100, timestamp: timestamp + DAY } await lockedGold.setAccountMustMaintain(account, mustMaintain.value, mustMaintain.timestamp) + await registry.setAddressFor(CeloContractName.Election, mockElection.address) }) describe('when unlocking would yield a locked gold balance less than the required value', () => { @@ -354,7 +359,7 @@ contract('LockedGold', (accounts: string[]) => { describe('when the the current time is later than the requirement time', () => { it('should succeed', async () => { - await timeTravel(web3, DAY) + await timeTravel(DAY, web3) await lockedGold.unlock(value) }) }) @@ -371,8 +376,9 @@ contract('LockedGold', (accounts: string[]) => { describe('#relock()', () => { const value = 1000 const index = 0 + let resp: any describe('when a pending withdrawal exists', () => { - before(async () => { + beforeEach(async () => { // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails await lockedGold.lock({ value }) await lockedGold.unlock(value) @@ -380,19 +386,19 @@ contract('LockedGold', (accounts: string[]) => { }) it("should increase the account's nonvoting locked gold balance", async () => { - assert.equal(await lockedGold.getAccountNonvotingLockedGold(account), value) + assertEqualBN(await lockedGold.getAccountNonvotingLockedGold(account), value) }) it("should increase the account's total locked gold balance", async () => { - assert.equal(await lockedGold.getAccountTotalLockedGold(account), value) + assertEqualBN(await lockedGold.getAccountTotalLockedGold(account), value) }) it('should increase the nonvoting locked gold balance', async () => { - assert.equal(await lockedGold.getNonvotingLockedGold(), value) + assertEqualBN(await lockedGold.getNonvotingLockedGold(), value) }) it('should increase the total locked gold balance', async () => { - assert.equal(await lockedGold.getTotalLockedGold(), value) + assertEqualBN(await lockedGold.getTotalLockedGold(), value) }) it('should emit a GoldLocked event', async () => { @@ -405,8 +411,9 @@ contract('LockedGold', (accounts: string[]) => { }) it('should remove the pending withdrawal', async () => { - const pendingWithdrawals = await lockedGold.getPendingWithdrawals(account) - assert.equal(pendingWithdrawals.length, 0) + const [values, timestamps] = await lockedGold.getPendingWithdrawals(account) + assert.equal(values.length, 0) + assert.equal(timestamps.length, 0) }) }) @@ -420,27 +427,24 @@ contract('LockedGold', (accounts: string[]) => { describe('#withdraw()', () => { const value = 1000 const index = 0 - let availabilityTime: BigNumber let resp: any describe('when a pending withdrawal exists', () => { - before(async () => { + beforeEach(async () => { // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails await lockedGold.lock({ value }) resp = await lockedGold.unlock(value) - availabilityTime = new BigNumber(unlockingPeriod).plus( - (await web3.eth.getBlock('latest')).timestamp - ) }) describe('when it is after the availablity time', () => { - before(async () => { - await timeTravel(web3, unlockingPeriod) + beforeEach(async () => { + await timeTravel(unlockingPeriod, web3) resp = await lockedGold.withdraw(index) }) it('should remove the pending withdrawal', async () => { - const pendingWithdrawals = await lockedGold.getPendingWithdrawals(account) - assert.equal(pendingWithdrawals.length, 0) + const [values, timestamps] = await lockedGold.getPendingWithdrawals(account) + assert.equal(values.length, 0) + assert.equal(timestamps.length, 0) }) it('should emit a GoldWithdrawn event', async () => { From 871d06d87f279600b92df9d5f57fe48984591d31 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Tue, 24 Sep 2019 12:12:00 -0700 Subject: [PATCH 008/149] Validators tests passing --- .../contracts/governance/Validators.sol | 20 +- .../governance/test/MockLockedGold.sol | 15 ++ .../protocol/test/governance/validators.ts | 245 +++++++----------- 3 files changed, 123 insertions(+), 157 deletions(-) diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index 74b462efa66..b1c8e8fff93 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -157,6 +157,10 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi return true; } + function getMaxGroupSize() external view returns (uint256) { + return maxGroupSize; + } + /** * @notice Updates the minimum gold requirements to register a validator group or validator. * @param groupRequirement The minimum locked gold needed to register a group. @@ -188,7 +192,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi * @return True upon success. * @dev The new requirement is only enforced for future validator or group deregistrations. */ - function setDeregistrationLockup( + function setDeregistrationLockups( uint256 groupLockup, uint256 validatorLockup ) @@ -239,7 +243,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi address account = getLockedGold().getAccountFromValidator(msg.sender); require(!isValidator(account) && !isValidatorGroup(account)); - require(meetsValidatorRegistrationRequirement(account)); + require(meetsValidatorRegistrationRequirement(account), "4"); Validator memory validator = Validator(name, url, publicKeysData, address(0)); validators[account] = validator; @@ -266,7 +270,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi * @return Whether an account meets the requirements to register a validator. */ function meetsValidatorRegistrationRequirement(address account) public view returns (bool) { - getLockedGold().getAccountTotalLockedGold(account) >= registrationRequirements.validator; + return getLockedGold().getAccountTotalLockedGold(account) >= registrationRequirements.validator; } /** @@ -275,7 +279,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi * @return Whether an account meets the requirements to register a group. */ function meetsValidatorGroupRegistrationRequirement(address account) public view returns (bool) { - getLockedGold().getAccountTotalLockedGold(account) >= registrationRequirements.group; + return getLockedGold().getAccountTotalLockedGold(account) >= registrationRequirements.group; } /** @@ -306,7 +310,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi */ function affiliate(address group) external nonReentrant returns (bool) { address account = getLockedGold().getAccountFromValidator(msg.sender); - require(isValidator(account) && isValidatorGroup(group)); + require(isValidator(account) && isValidatorGroup(group), "blah"); Validator storage validator = validators[account]; if (validator.affiliation != address(0)) { _deaffiliate(validator, account); @@ -523,6 +527,10 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi return (registrationRequirements.group, registrationRequirements.validator); } + function getDeregistrationLockups() external view returns (uint256, uint256) { + return (deregistrationLockups.group, deregistrationLockups.validator); + } + /** * @notice Returns the list of registered validator accounts. * @return The list of registered validator accounts. @@ -581,7 +589,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi */ function _removeMember(address group, address validator) private returns (bool) { ValidatorGroup storage _group = groups[group]; - require(validators[validator].affiliation == group && _group.members.contains(validator)); + require(validators[validator].affiliation == group && _group.members.contains(validator), "boogie"); _group.members.remove(validator); emit ValidatorGroupMemberRemoved(group, validator); diff --git a/packages/protocol/contracts/governance/test/MockLockedGold.sol b/packages/protocol/contracts/governance/test/MockLockedGold.sol index 984960ed72d..3fbb7f2a2d4 100644 --- a/packages/protocol/contracts/governance/test/MockLockedGold.sol +++ b/packages/protocol/contracts/governance/test/MockLockedGold.sol @@ -21,6 +21,17 @@ contract MockLockedGold is ILockedGold { return accountOrValidator; } + function getAccountFromVoter(address accountOrVoter) external view returns (address) { + return accountOrVoter; + } + + function getValidatorFromAccount(address account) external view returns (address) { + return account; + } + + function incrementNonvotingAccountBalance(address, uint256) external {} + function decrementNonvotingAccountBalance(address, uint256) external {} + function setAccountMustMaintain(address account, uint256 value, uint256 timestamp) external returns (bool) { mustMaintain[account] = MustMaintain(value, timestamp); return true; @@ -38,4 +49,8 @@ contract MockLockedGold is ILockedGold { function getAccountTotalLockedGold(address account) external view returns (uint256) { return totalLockedGold[account]; } + + function getTotalLockedGold() external view returns (uint256) { + return 0; + } } diff --git a/packages/protocol/test/governance/validators.ts b/packages/protocol/test/governance/validators.ts index a0f3be9f4fc..e6845eb11ce 100644 --- a/packages/protocol/test/governance/validators.ts +++ b/packages/protocol/test/governance/validators.ts @@ -1,5 +1,5 @@ import { CeloContractName } from '@celo/protocol/lib/registry-utils' -import { fixed1, toFixed, fromFixed, multiply } from '@celo/utils/lib/fixidity' +import { toFixed } from '@celo/utils/lib/fixidity' import { assertContainSubset, assertEqualBN, @@ -10,6 +10,8 @@ import BigNumber from 'bignumber.js' import { MockLockedGoldContract, MockLockedGoldInstance, + MockElectionContract, + MockElectionInstance, RegistryContract, RegistryInstance, ValidatorsContract, @@ -18,6 +20,7 @@ import { const Validators: ValidatorsContract = artifacts.require('Validators') const MockLockedGold: MockLockedGoldContract = artifacts.require('MockLockedGold') +const MockElection: MockElectionContract = artifacts.require('MockElection') const Registry: RegistryContract = artifacts.require('Registry') // @ts-ignore @@ -26,30 +29,30 @@ Validators.numberFormat = 'BigNumber' const parseValidatorParams = (validatorParams: any) => { return { - name: validatorParams[1], - url: validatorParams[2], - publicKeysData: validatorParams[3], - affiliation: validatorParams[4], + name: validatorParams[0], + url: validatorParams[1], + publicKeysData: validatorParams[2], + affiliation: validatorParams[3], } } const parseValidatorGroupParams = (groupParams: any) => { return { - name: groupParams[1], - url: groupParams[2], - members: groupParams[3], + name: groupParams[0], + url: groupParams[1], + members: groupParams[2], } } const HOUR = 60 * 60 const DAY = 24 * HOUR -const YEAR = 365 * DAY -const MAX_UINT256 = new BigInt(2).pow(256).minus(1) +const MAX_UINT256 = new BigNumber(2).pow(256).minus(1) contract('Validators', (accounts: string[]) => { let validators: ValidatorsInstance let registry: RegistryInstance let mockLockedGold: MockLockedGoldInstance + let mockElection: MockElectionInstance // A random 64 byte hex string. const publicKey = 'ea0733ad275e2b9e05541341a97ee82678c58932464fad26164657a111a7e37a9fa0300266fb90e2135a1f1512350cb4e985488a88809b14e3cbe415e76e82b2' @@ -73,8 +76,10 @@ contract('Validators', (accounts: string[]) => { beforeEach(async () => { validators = await Validators.new() mockLockedGold = await MockLockedGold.new() + mockElection = await MockElection.new() registry = await Registry.new() await registry.setAddressFor(CeloContractName.LockedGold, mockLockedGold.address) + await registry.setAddressFor(CeloContractName.Election, mockElection.address) await validators.initialize( registry.address, registrationRequirements.group, @@ -117,7 +122,7 @@ contract('Validators', (accounts: string[]) => { }) it('should have set the registration requirements', async () => { - const [group, validator] = await validators.getRegistrationRequirement() + const [group, validator] = await validators.getRegistrationRequirements() assertEqualBN(group, registrationRequirements.group) assertEqualBN(validator, registrationRequirements.validator) }) @@ -144,14 +149,15 @@ contract('Validators', (accounts: string[]) => { describe('#setRegistrationRequirements()', () => { describe('when the requirements are different', () => { + const newRequirements = { + group: registrationRequirements.group.plus(1), + validator: registrationRequirements.validator.plus(1), + } + describe('when called by the owner', () => { let resp: any - const newRequirements = { - group: registrationRequirements.group.plus(1), - validator: registrationRequirements.validator.plus(1), - } - before(async () => { + beforeEach(async () => { resp = await validators.setRegistrationRequirements( newRequirements.group, newRequirements.validator @@ -175,6 +181,20 @@ contract('Validators', (accounts: string[]) => { }, }) }) + + describe('when called by a non-owner', () => { + it('should revert', async () => { + await assertRevert( + validators.setRegistrationRequirements( + newRequirements.group, + newRequirements.validator, + { + from: nonOwner, + } + ) + ) + }) + }) }) describe('when the requirements are the same', () => { @@ -188,28 +208,19 @@ contract('Validators', (accounts: string[]) => { }) }) }) - - describe('when called by a non-owner', () => { - it('should revert', async () => { - await assertRevert( - validators.setRegistrationRequirements(newRequirements.group, newRequirements.validator, { - from: nonOwner, - }) - ) - }) - }) }) describe('#setDeregistrationLockups()', () => { describe('when the requirements are different', () => { + const newLockups = { + group: deregistrationLockups.group.plus(1), + validator: deregistrationLockups.validator.plus(1), + } + describe('when called by the owner', () => { let resp: any - const newLockups = { - group: deregistrationLockups.group.plus(1), - validator: deregistrationLockups.validator.plus(1), - } - before(async () => { + beforeEach(async () => { resp = await validators.setDeregistrationLockups(newLockups.group, newLockups.validator) }) @@ -230,6 +241,16 @@ contract('Validators', (accounts: string[]) => { }, }) }) + + describe('when called by a non-owner', () => { + it('should revert', async () => { + await assertRevert( + validators.setDeregistrationLockups(newLockups.group, newLockups.validator, { + from: nonOwner, + }) + ) + }) + }) }) describe('when the requirements are the same', () => { @@ -243,25 +264,15 @@ contract('Validators', (accounts: string[]) => { }) }) }) - - describe('when called by a non-owner', () => { - it('should revert', async () => { - await assertRevert( - validators.setDeregistrationLockups(newLockups.group, newLockups.validator, { - from: nonOwner, - }) - ) - }) - }) }) describe('#setMaxGroupSize()', () => { describe('when the size is different', () => { describe('when called by the owner', () => { let resp: any - const newSize = maxGroupSize.plus(1) + const newSize = maxGroupSize + 1 - before(async () => { + beforeEach(async () => { resp = await validators.setMaxGroupSize(newSize) }) @@ -276,7 +287,7 @@ contract('Validators', (accounts: string[]) => { assertContainSubset(log, { event: 'MaxGroupSizeSet', args: { - maxGroupSize: new BigNumber(newSize), + size: new BigNumber(newSize), }, }) }) @@ -300,7 +311,7 @@ contract('Validators', (accounts: string[]) => { const validator = accounts[0] let resp: any describe('when the account is not a registered validator', () => { - before(async () => { + beforeEach(async () => { await mockLockedGold.setAccountTotalLockedGold( validator, registrationRequirements.validator @@ -330,8 +341,8 @@ contract('Validators', (accounts: string[]) => { it('should set account balance requirements on locked gold', async () => { const [value, timestamp] = await mockLockedGold.getAccountMustMaintain(validator) - assert.equal(value, registrationRequirements.validator) - assert.equal(timestamp, MAX_UINT256) + assertEqualBN(value, registrationRequirements.validator) + assertEqualBN(timestamp, MAX_UINT256) }) it('should emit the ValidatorRegistered event', async () => { @@ -350,7 +361,11 @@ contract('Validators', (accounts: string[]) => { }) describe('when the account is already a registered validator', () => { - before(async () => { + beforeEach(async () => { + await mockLockedGold.setAccountTotalLockedGold( + validator, + registrationRequirements.validator + ) await validators.registerValidator( name, url, @@ -373,6 +388,7 @@ contract('Validators', (accounts: string[]) => { describe('when the account is already a registered validator group', () => { beforeEach(async () => { + await mockLockedGold.setAccountTotalLockedGold(validator, registrationRequirements.group) await validators.registerValidatorGroup(name, url, commission) }) @@ -414,7 +430,7 @@ contract('Validators', (accounts: string[]) => { const index = 0 let resp: any describe('when the account is not a registered validator', () => { - before(async () => { + beforeEach(async () => { await registerValidator(validator) resp = await validators.deregisterValidator(index) }) @@ -430,8 +446,11 @@ contract('Validators', (accounts: string[]) => { it('should set account balance requirements on locked gold', async () => { const latestTimestamp = (await web3.eth.getBlock('latest')).timestamp const [value, timestamp] = await mockLockedGold.getAccountMustMaintain(validator) - assert.equal(value, registrationRequirements.validator) - assert.equal(timestamp, new BigNumber(timestamp).plus(deregistrationLockups.validator)) + assertEqualBN(value, registrationRequirements.validator) + assertEqualBN( + timestamp, + new BigNumber(latestTimestamp).plus(deregistrationLockups.validator) + ) }) it('should emit the ValidatorDeregistered event', async () => { @@ -449,6 +468,7 @@ contract('Validators', (accounts: string[]) => { describe('when the validator is affiliated with a validator group', () => { const group = accounts[1] beforeEach(async () => { + await registerValidator(validator) await registerValidatorGroup(group) await validators.affiliate(group) }) @@ -479,7 +499,7 @@ contract('Validators', (accounts: string[]) => { it('should emit the ValidatorGroupMemberRemoved event', async () => { const resp = await validators.deregisterValidator(index) - assert.equal(resp.logs.length, 4) + assert.equal(resp.logs.length, 3) const log = resp.logs[0] assertContainSubset(log, { event: 'ValidatorGroupMemberRemoved', @@ -491,31 +511,9 @@ contract('Validators', (accounts: string[]) => { }) describe('when the validator is the only member of that group', () => { - it('should emit the ValidatorGroupEmptied event', async () => { - const resp = await validators.deregisterValidator(index) - assert.equal(resp.logs.length, 4) - const log = resp.logs[1] - assertContainSubset(log, { - event: 'ValidatorGroupEmptied', - args: { - group, - }, - }) - }) - - describe('when that group has received votes', () => { - beforeEach(async () => { - const voter = accounts[2] - const weight = 10 - await mockLockedGold.setWeight(voter, weight) - await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS, { from: voter }) - }) - - it('should remove the group from the list of electable groups with votes', async () => { - await validators.deregisterValidator(index) - const [groups] = await validators.getValidatorGroupVotes() - assert.deepEqual(groups, []) - }) + it('should should mark the group as ineligible for election', async () => { + await validators.deregisterValidator(index) + assert.isTrue(await mockElection.isIneligible(group)) }) }) }) @@ -609,7 +607,7 @@ contract('Validators', (accounts: string[]) => { it('should emit the ValidatorGroupMemberRemoved event', async () => { const resp = await validators.affiliate(otherGroup) - assert.equal(resp.logs.length, 4) + assert.equal(resp.logs.length, 3) const log = resp.logs[0] assertContainSubset(log, { event: 'ValidatorGroupMemberRemoved', @@ -621,31 +619,9 @@ contract('Validators', (accounts: string[]) => { }) describe('when the validator is the only member of that group', () => { - it('should emit the ValidatorGroupEmptied event', async () => { - const resp = await validators.affiliate(otherGroup) - assert.equal(resp.logs.length, 4) - const log = resp.logs[1] - assertContainSubset(log, { - event: 'ValidatorGroupEmptied', - args: { - group, - }, - }) - }) - - describe('when that group has received votes', () => { - beforeEach(async () => { - const voter = accounts[2] - const weight = 10 - await mockLockedGold.setWeight(voter, weight) - await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS, { from: voter }) - }) - - it('should remove the group from the list of electable groups with votes', async () => { - await validators.affiliate(otherGroup) - const [groups] = await validators.getValidatorGroupVotes() - assert.deepEqual(groups, []) - }) + it('should should mark the group as ineligible for election', async () => { + await validators.affiliate(otherGroup) + assert.isTrue(await mockElection.isIneligible(group)) }) }) }) @@ -701,7 +677,7 @@ contract('Validators', (accounts: string[]) => { it('should emit the ValidatorGroupMemberRemoved event', async () => { const resp = await validators.deaffiliate() - assert.equal(resp.logs.length, 3) + assert.equal(resp.logs.length, 2) const log = resp.logs[0] assertContainSubset(log, { event: 'ValidatorGroupMemberRemoved', @@ -713,31 +689,9 @@ contract('Validators', (accounts: string[]) => { }) describe('when the validator is the only member of that group', () => { - it('should emit the ValidatorGroupEmptied event', async () => { - const resp = await validators.deaffiliate() - assert.equal(resp.logs.length, 3) - const log = resp.logs[1] - assertContainSubset(log, { - event: 'ValidatorGroupEmptied', - args: { - group, - }, - }) - }) - - describe('when that group has received votes', () => { - beforeEach(async () => { - const voter = accounts[2] - const weight = 10 - await mockLockedGold.setWeight(voter, weight) - await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS, { from: voter }) - }) - - it('should remove the group from the list of electable groups with votes', async () => { - await validators.deaffiliate() - const [groups] = await validators.getValidatorGroupVotes() - assert.deepEqual(groups, []) - }) + it('should should mark the group as ineligible for election', async () => { + await validators.deaffiliate() + assert.isTrue(await mockElection.isIneligible(group)) }) }) }) @@ -756,15 +710,9 @@ contract('Validators', (accounts: string[]) => { const group = accounts[0] let resp: any describe('when the account is not a registered validator group', () => { - before(async () => { + beforeEach(async () => { await mockLockedGold.setAccountTotalLockedGold(group, registrationRequirements.group) resp = await validators.registerValidatorGroup(name, url, commission) - resp = await validators.registerValidator( - name, - url, - // @ts-ignore bytes type - publicKeysData - ) }) it('should mark the account as a validator group', async () => { @@ -802,13 +750,14 @@ contract('Validators', (accounts: string[]) => { it('should revert', async () => { await assertRevert( - validators.registerValidatorGroup(name, url, registrationRequirement.noticePeriod) + validators.registerValidatorGroup(name, url, registrationRequirements.group) ) }) }) describe('when the account is already a registered validator group', () => { beforeEach(async () => { + await mockLockedGold.setAccountTotalLockedGold(group, registrationRequirements.group) await validators.registerValidatorGroup(name, url, commission) }) @@ -820,7 +769,7 @@ contract('Validators', (accounts: string[]) => { describe('when the account does not meet the registration requirements', () => { beforeEach(async () => { await mockLockedGold.setAccountTotalLockedGold( - validator, + group, registrationRequirements.group.minus(1) ) }) @@ -837,28 +786,25 @@ contract('Validators', (accounts: string[]) => { let resp: any beforeEach(async () => { await registerValidatorGroup(group) + resp = await validators.deregisterValidatorGroup(index) }) it('should mark the account as not a validator group', async () => { - await validators.deregisterValidatorGroup(index) assert.isFalse(await validators.isValidatorGroup(group)) }) it('should remove the account from the list of validator groups', async () => { - await validators.deregisterValidatorGroup(index) assert.deepEqual(await validators.getRegisteredValidatorGroups(), []) }) it('should set account balance requirements on locked gold', async () => { - await validators.deregisterValidatorGroup(index) const latestTimestamp = (await web3.eth.getBlock('latest')).timestamp const [value, timestamp] = await mockLockedGold.getAccountMustMaintain(group) - assert.equal(value, registrationRequirements.group) - assert.equal(timestamp, new BigNumber(timestamp).plus(deregistrationLockups.group)) + assertEqualBN(value, registrationRequirements.group) + assertEqualBN(timestamp, new BigNumber(latestTimestamp).plus(deregistrationLockups.group)) }) it('should emit the ValidatorGroupDeregistered event', async () => { - const resp = await validators.deregisterValidatorGroup(index) assert.equal(resp.logs.length, 1) const log = resp.logs[0] assertContainSubset(log, { @@ -880,6 +826,7 @@ contract('Validators', (accounts: string[]) => { describe('when the validator group is not empty', () => { const validator = accounts[1] beforeEach(async () => { + await registerValidatorGroup(group) await registerValidator(validator) await validators.affiliate(group, { from: validator }) await validators.addMember(validator) @@ -894,20 +841,20 @@ contract('Validators', (accounts: string[]) => { describe('#addMember', () => { const group = accounts[0] const validator = accounts[1] + let resp: any beforeEach(async () => { await registerValidator(validator) await registerValidatorGroup(group) await validators.affiliate(group, { from: validator }) + resp = await validators.addMember(validator) }) it('should add the member to the list of members', async () => { - await validators.addMember(validator) const parsedGroup = parseValidatorGroupParams(await validators.getValidatorGroup(group)) assert.deepEqual(parsedGroup.members, [validator]) }) it('should emit the ValidatorGroupMemberAdded event', async () => { - const resp = await validators.addMember(validator) assert.equal(resp.logs.length, 1) const log = resp.logs[0] assertContainSubset(log, { @@ -938,10 +885,6 @@ contract('Validators', (accounts: string[]) => { }) describe('when the validator is already a member of the group', () => { - beforeEach(async () => { - await validators.addMember(validator) - }) - it('should revert', async () => { await assertRevert(validators.addMember(validator)) }) @@ -963,7 +906,7 @@ contract('Validators', (accounts: string[]) => { it('should emit the ValidatorGroupMemberRemoved event', async () => { const resp = await validators.removeMember(validator) - assert.equal(resp.logs.length, 2) + assert.equal(resp.logs.length, 1) const log = resp.logs[0] assertContainSubset(log, { event: 'ValidatorGroupMemberRemoved', From e0e8cf7767fc0a6503e8224eb034bcd6d807f785 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Tue, 24 Sep 2019 12:17:27 -0700 Subject: [PATCH 009/149] Fix governance tests --- packages/protocol/test/governance/governance.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/protocol/test/governance/governance.ts b/packages/protocol/test/governance/governance.ts index 302a9276af0..a9ecf9ccc63 100644 --- a/packages/protocol/test/governance/governance.ts +++ b/packages/protocol/test/governance/governance.ts @@ -836,11 +836,6 @@ contract('Governance', (accounts: string[]) => { }) }) - it('should revert when the account weight is 0', async () => { - await mockLockedGold.setAccountTotalLockedGold(account, 0) - await assertRevert(governance.upvote(proposalId, 0, 0)) - }) - it('should revert when upvoting a proposal that is not queued', async () => { await assertRevert(governance.upvote(proposalId.plus(1), 0, 0)) }) From 223536d10b1feac91a50e2bccd36bd5235a99060 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Tue, 24 Sep 2019 12:18:46 -0700 Subject: [PATCH 010/149] Add election test file --- packages/protocol/test/governance/election.ts | 1407 +++++++++++++++++ 1 file changed, 1407 insertions(+) create mode 100644 packages/protocol/test/governance/election.ts diff --git a/packages/protocol/test/governance/election.ts b/packages/protocol/test/governance/election.ts new file mode 100644 index 00000000000..ca806c91a0f --- /dev/null +++ b/packages/protocol/test/governance/election.ts @@ -0,0 +1,1407 @@ +import { CeloContractName } from '@celo/protocol/lib/registry-utils' +import { + assertContainSubset, + assertEqualBN, + assertRevert, + NULL_ADDRESS, +} from '@celo/protocol/lib/test-utils' +import BigNumber from 'bignumber.js' +import { + MockLockedGoldContract, + MockLockedGoldInstance, + MockRandomContract, + MockRandomInstance, + RegistryContract, + RegistryInstance, + ValidatorsContract, + ValidatorsInstance, +} from 'types' + +const Validators: ValidatorsContract = artifacts.require('Validators') +const MockLockedGold: MockLockedGoldContract = artifacts.require('MockLockedGold') +const Registry: RegistryContract = artifacts.require('Registry') +const Random: MockRandomContract = artifacts.require('MockRandom') + +// @ts-ignore +// TODO(mcortesi): Use BN +Validators.numberFormat = 'BigNumber' + +const parseValidatorParams = (validatorParams: any) => { + return { + identifier: validatorParams[0], + name: validatorParams[1], + url: validatorParams[2], + publicKeysData: validatorParams[3], + affiliation: validatorParams[4], + } +} + +const parseValidatorGroupParams = (groupParams: any) => { + return { + identifier: groupParams[0], + name: groupParams[1], + url: groupParams[2], + members: groupParams[3], + } +} + +contract('Validators', (accounts: string[]) => { + let validators: ValidatorsInstance + let registry: RegistryInstance + let mockLockedGold: MockLockedGoldInstance + let random: MockRandomInstance + + // A random 64 byte hex string. + const publicKey = + 'ea0733ad275e2b9e05541341a97ee82678c58932464fad26164657a111a7e37a9fa0300266fb90e2135a1f1512350cb4e985488a88809b14e3cbe415e76e82b2' + const blsPublicKey = + '4d23d8cd06f30b1fa7cf368e2f5399ab04bb6846c682f493a98a607d3dfb7e53a712bb79b475c57b0ac2785460f91301' + const blsPoP = + '9d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d740501' + + const publicKeysData = '0x' + publicKey + blsPublicKey + blsPoP + + const nonOwner = accounts[1] + const minElectableValidators = new BigNumber(4) + const maxElectableValidators = new BigNumber(6) + const registrationRequirement = { value: new BigNumber(100), noticePeriod: new BigNumber(60) } + const identifier = 'test-identifier' + const name = 'test-name' + const url = 'test-url' + beforeEach(async () => { + validators = await Validators.new() + mockLockedGold = await MockLockedGold.new() + random = await Random.new() + registry = await Registry.new() + await registry.setAddressFor(CeloContractName.LockedGold, mockLockedGold.address) + await registry.setAddressFor(CeloContractName.Random, random.address) + await validators.initialize( + registry.address, + minElectableValidators, + maxElectableValidators, + registrationRequirement.value, + registrationRequirement.noticePeriod + ) + }) + + const registerValidator = async (validator: string) => { + await mockLockedGold.setLockedCommitment( + validator, + registrationRequirement.noticePeriod, + registrationRequirement.value + ) + await validators.registerValidator( + identifier, + name, + url, + // @ts-ignore bytes type + publicKeysData, + registrationRequirement.noticePeriod, + { from: validator } + ) + } + + const registerValidatorGroup = async (group: string) => { + await mockLockedGold.setLockedCommitment( + group, + registrationRequirement.noticePeriod, + registrationRequirement.value + ) + await validators.registerValidatorGroup( + identifier, + name, + url, + registrationRequirement.noticePeriod, + { from: group } + ) + } + + const registerValidatorGroupWithMembers = async (group: string, members: string[]) => { + await registerValidatorGroup(group) + for (const validator of members) { + await registerValidator(validator) + await validators.affiliate(group, { from: validator }) + await validators.addMember(validator, { from: group }) + } + } + + describe('#initialize()', () => { + it('should have set the owner', async () => { + const owner: string = await validators.owner() + assert.equal(owner, accounts[0]) + }) + + it('should have set minElectableValidators', async () => { + const actualMinElectableValidators = await validators.minElectableValidators() + assertEqualBN(actualMinElectableValidators, minElectableValidators) + }) + + it('should have set maxElectableValidators', async () => { + const actualMaxElectableValidators = await validators.maxElectableValidators() + assertEqualBN(actualMaxElectableValidators, maxElectableValidators) + }) + + it('should have set the registration requirements', async () => { + const [value, noticePeriod] = await validators.getRegistrationRequirement() + assertEqualBN(value, registrationRequirement.value) + assertEqualBN(noticePeriod, registrationRequirement.noticePeriod) + }) + + it('should not be callable again', async () => { + await assertRevert( + validators.initialize( + registry.address, + minElectableValidators, + maxElectableValidators, + registrationRequirement.value, + registrationRequirement.noticePeriod + ) + ) + }) + }) + + describe('#setMinElectableValidators', () => { + const newMinElectableValidators = minElectableValidators.plus(1) + it('should set the minimum deposit', async () => { + await validators.setMinElectableValidators(newMinElectableValidators) + assertEqualBN(await validators.minElectableValidators(), newMinElectableValidators) + }) + + it('should emit the MinElectableValidatorsSet event', async () => { + const resp = await validators.setMinElectableValidators(newMinElectableValidators) + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'MinElectableValidatorsSet', + args: { + minElectableValidators: new BigNumber(newMinElectableValidators), + }, + }) + }) + + it('should revert when the minElectableValidators is zero', async () => { + await assertRevert(validators.setMinElectableValidators(0)) + }) + + it('should revert when the minElectableValidators is greater than maxElectableValidators', async () => { + await assertRevert(validators.setMinElectableValidators(maxElectableValidators.plus(1))) + }) + + it('should revert when the minElectableValidators is unchanged', async () => { + await assertRevert(validators.setMinElectableValidators(minElectableValidators)) + }) + + it('should revert when called by anyone other than the owner', async () => { + await assertRevert( + validators.setMinElectableValidators(newMinElectableValidators, { from: nonOwner }) + ) + }) + }) + + describe('#setMaxElectableValidators', () => { + const newMaxElectableValidators = maxElectableValidators.plus(1) + it('should set the minimum deposit', async () => { + await validators.setMaxElectableValidators(newMaxElectableValidators) + assertEqualBN(await validators.maxElectableValidators(), newMaxElectableValidators) + }) + + it('should emit the MaxElectableValidatorsSet event', async () => { + const resp = await validators.setMaxElectableValidators(newMaxElectableValidators) + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'MaxElectableValidatorsSet', + args: { + maxElectableValidators: new BigNumber(newMaxElectableValidators), + }, + }) + }) + + it('should revert when the maxElectableValidators is less than minElectableValidators', async () => { + await assertRevert(validators.setMaxElectableValidators(minElectableValidators.minus(1))) + }) + + it('should revert when the maxElectableValidators is unchanged', async () => { + await assertRevert(validators.setMaxElectableValidators(maxElectableValidators)) + }) + + it('should revert when called by anyone other than the owner', async () => { + await assertRevert( + validators.setMaxElectableValidators(newMaxElectableValidators, { from: nonOwner }) + ) + }) + }) + + describe('#setRegistrationRequirement', () => { + const newValue = registrationRequirement.value.plus(1) + const newNoticePeriod = registrationRequirement.noticePeriod.plus(1) + + it('should set the value and notice period', async () => { + await validators.setRegistrationRequirement(newValue, newNoticePeriod) + const [value, noticePeriod] = await validators.getRegistrationRequirement() + assertEqualBN(value, newValue) + assertEqualBN(noticePeriod, newNoticePeriod) + }) + + it('should emit the RegistrationRequirementSet event', async () => { + const resp = await validators.setRegistrationRequirement(newValue, newNoticePeriod) + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'RegistrationRequirementSet', + args: { + value: new BigNumber(newValue), + noticePeriod: new BigNumber(newNoticePeriod), + }, + }) + }) + + it('should revert when the requirement is unchanged', async () => { + await assertRevert( + validators.setRegistrationRequirement( + registrationRequirement.value, + registrationRequirement.noticePeriod + ) + ) + }) + + it('should revert when called by anyone other than the owner', async () => { + await assertRevert( + validators.setRegistrationRequirement(newValue, newNoticePeriod, { from: nonOwner }) + ) + }) + }) + + describe('#registerValidator', () => { + const validator = accounts[0] + beforeEach(async () => { + await mockLockedGold.setLockedCommitment( + validator, + registrationRequirement.noticePeriod, + registrationRequirement.value + ) + }) + + it('should mark the account as a validator', async () => { + await validators.registerValidator( + identifier, + name, + url, + // @ts-ignore bytes type + publicKeysData, + registrationRequirement.noticePeriod + ) + assert.isTrue(await validators.isValidator(validator)) + }) + + it('should add the account to the list of validators', async () => { + await validators.registerValidator( + identifier, + name, + url, + // @ts-ignore bytes type + publicKeysData, + registrationRequirement.noticePeriod + ) + assert.deepEqual(await validators.getRegisteredValidators(), [validator]) + }) + + it('should set the validator identifier, name, url, and public key', async () => { + await validators.registerValidator( + identifier, + name, + url, + // @ts-ignore bytes type + publicKeysData, + registrationRequirement.noticePeriod + ) + const parsedValidator = parseValidatorParams(await validators.getValidator(validator)) + assert.equal(parsedValidator.identifier, identifier) + assert.equal(parsedValidator.name, name) + assert.equal(parsedValidator.url, url) + assert.equal(parsedValidator.publicKeysData, publicKeysData) + }) + + it('should emit the ValidatorRegistered event', async () => { + const resp = await validators.registerValidator( + identifier, + name, + url, + // @ts-ignore bytes type + publicKeysData, + registrationRequirement.noticePeriod + ) + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorRegistered', + args: { + validator, + identifier, + name, + url, + publicKeysData, + }, + }) + }) + + describe('when the account is already a registered validator', () => { + beforeEach(async () => { + await validators.registerValidator( + identifier, + name, + url, + // @ts-ignore bytes type + publicKeysData, + registrationRequirement.noticePeriod + ) + }) + + it('should revert', async () => { + await assertRevert( + validators.registerValidator( + identifier, + name, + url, + // @ts-ignore bytes type + publicKeysData, + registrationRequirement.noticePeriod + ) + ) + }) + }) + + describe('when the account is already a registered validator group', () => { + beforeEach(async () => { + await validators.registerValidatorGroup( + identifier, + name, + url, + registrationRequirement.noticePeriod + ) + }) + + it('should revert', async () => { + await assertRevert( + validators.registerValidator( + identifier, + name, + url, + // @ts-ignore bytes type + publicKeysData, + registrationRequirement.noticePeriod + ) + ) + }) + }) + + describe('when the account does not meet the registration requirements', () => { + beforeEach(async () => { + await mockLockedGold.setLockedCommitment( + validator, + registrationRequirement.noticePeriod, + registrationRequirement.value.minus(1) + ) + }) + + it('should revert', async () => { + await assertRevert( + validators.registerValidator( + identifier, + name, + url, + // @ts-ignore bytes type + publicKeysData, + registrationRequirement.noticePeriod + ) + ) + }) + }) + }) + + describe('#deregisterValidator', () => { + const validator = accounts[0] + const index = 0 + beforeEach(async () => { + await registerValidator(validator) + }) + + it('should mark the account as not a validator', async () => { + await validators.deregisterValidator(index) + assert.isFalse(await validators.isValidator(validator)) + }) + + it('should remove the account from the list of validators', async () => { + await validators.deregisterValidator(index) + assert.deepEqual(await validators.getRegisteredValidators(), []) + }) + + it('should emit the ValidatorDeregistered event', async () => { + const resp = await validators.deregisterValidator(index) + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorDeregistered', + args: { + validator, + }, + }) + }) + + describe('when the validator is affiliated with a validator group', () => { + const group = accounts[1] + beforeEach(async () => { + await registerValidatorGroup(group) + await validators.affiliate(group) + }) + + it('should emit the ValidatorDeafilliated event', async () => { + const resp = await validators.deregisterValidator(index) + assert.equal(resp.logs.length, 2) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorDeaffiliated', + args: { + validator, + group, + }, + }) + }) + + describe('when the validator is a member of that group', () => { + beforeEach(async () => { + await validators.addMember(validator, { from: group }) + }) + + it('should remove the validator from the group membership list', async () => { + await validators.deregisterValidator(index) + const parsedGroup = parseValidatorGroupParams(await validators.getValidatorGroup(group)) + assert.deepEqual(parsedGroup.members, []) + }) + + it('should emit the ValidatorGroupMemberRemoved event', async () => { + const resp = await validators.deregisterValidator(index) + assert.equal(resp.logs.length, 4) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorGroupMemberRemoved', + args: { + validator, + group, + }, + }) + }) + + describe('when the validator is the only member of that group', () => { + it('should emit the ValidatorGroupEmptied event', async () => { + const resp = await validators.deregisterValidator(index) + assert.equal(resp.logs.length, 4) + const log = resp.logs[1] + assertContainSubset(log, { + event: 'ValidatorGroupEmptied', + args: { + group, + }, + }) + }) + + describe('when that group has received votes', () => { + beforeEach(async () => { + const voter = accounts[2] + const weight = 10 + await mockLockedGold.setWeight(voter, weight) + await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS, { from: voter }) + }) + + it('should remove the group from the list of electable groups with votes', async () => { + await validators.deregisterValidator(index) + const [groups] = await validators.getValidatorGroupVotes() + assert.deepEqual(groups, []) + }) + }) + }) + }) + }) + + it('should revert when the account is not a registered validator', async () => { + await assertRevert(validators.deregisterValidator(index, { from: accounts[2] })) + }) + + it('should revert when the wrong index is provided', async () => { + await assertRevert(validators.deregisterValidator(index + 1)) + }) + }) + + describe('#affiliate', () => { + const validator = accounts[0] + const group = accounts[1] + beforeEach(async () => { + await registerValidator(validator) + await registerValidatorGroup(group) + }) + + it('should set the affiliate', async () => { + await validators.affiliate(group) + const parsedValidator = parseValidatorParams(await validators.getValidator(validator)) + assert.equal(parsedValidator.affiliation, group) + }) + + it('should emit the ValidatorAffiliated event', async () => { + const resp = await validators.affiliate(group) + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorAffiliated', + args: { + validator, + group, + }, + }) + }) + + describe('when the validator is already affiliated with a validator group', () => { + const otherGroup = accounts[2] + beforeEach(async () => { + await validators.affiliate(group) + await registerValidatorGroup(otherGroup) + }) + + it('should set the affiliate', async () => { + await validators.affiliate(otherGroup) + const parsedValidator = parseValidatorParams(await validators.getValidator(validator)) + assert.equal(parsedValidator.affiliation, otherGroup) + }) + + it('should emit the ValidatorDeafilliated event', async () => { + const resp = await validators.affiliate(otherGroup) + assert.equal(resp.logs.length, 2) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorDeaffiliated', + args: { + validator, + group, + }, + }) + }) + + it('should emit the ValidatorAffiliated event', async () => { + const resp = await validators.affiliate(otherGroup) + assert.equal(resp.logs.length, 2) + const log = resp.logs[1] + assertContainSubset(log, { + event: 'ValidatorAffiliated', + args: { + validator, + group: otherGroup, + }, + }) + }) + + describe('when the validator is a member of that group', () => { + beforeEach(async () => { + await validators.addMember(validator, { from: group }) + }) + + it('should remove the validator from the group membership list', async () => { + await validators.affiliate(otherGroup) + const parsedGroup = parseValidatorGroupParams(await validators.getValidatorGroup(group)) + assert.deepEqual(parsedGroup.members, []) + }) + + it('should emit the ValidatorGroupMemberRemoved event', async () => { + const resp = await validators.affiliate(otherGroup) + assert.equal(resp.logs.length, 4) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorGroupMemberRemoved', + args: { + validator, + group, + }, + }) + }) + + describe('when the validator is the only member of that group', () => { + it('should emit the ValidatorGroupEmptied event', async () => { + const resp = await validators.affiliate(otherGroup) + assert.equal(resp.logs.length, 4) + const log = resp.logs[1] + assertContainSubset(log, { + event: 'ValidatorGroupEmptied', + args: { + group, + }, + }) + }) + + describe('when that group has received votes', () => { + beforeEach(async () => { + const voter = accounts[2] + const weight = 10 + await mockLockedGold.setWeight(voter, weight) + await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS, { from: voter }) + }) + + it('should remove the group from the list of electable groups with votes', async () => { + await validators.affiliate(otherGroup) + const [groups] = await validators.getValidatorGroupVotes() + assert.deepEqual(groups, []) + }) + }) + }) + }) + }) + + it('should revert when the account is not a registered validator', async () => { + await assertRevert(validators.affiliate(group, { from: accounts[2] })) + }) + + it('should revert when the group is not a registered validator group', async () => { + await assertRevert(validators.affiliate(accounts[2])) + }) + }) + + describe('#deaffiliate', () => { + const validator = accounts[0] + const group = accounts[1] + beforeEach(async () => { + await registerValidator(validator) + await registerValidatorGroup(group) + await validators.affiliate(group) + }) + + it('should clear the affiliate', async () => { + await validators.deaffiliate() + const parsedValidator = parseValidatorParams(await validators.getValidator(validator)) + assert.equal(parsedValidator.affiliation, NULL_ADDRESS) + }) + + it('should emit the ValidatorDeaffiliated event', async () => { + const resp = await validators.deaffiliate() + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorDeaffiliated', + args: { + validator, + group, + }, + }) + }) + + describe('when the validator is a member of the affiliated group', () => { + beforeEach(async () => { + await validators.addMember(validator, { from: group }) + }) + + it('should remove the validator from the group membership list', async () => { + await validators.deaffiliate() + const parsedGroup = parseValidatorGroupParams(await validators.getValidatorGroup(group)) + assert.deepEqual(parsedGroup.members, []) + }) + + it('should emit the ValidatorGroupMemberRemoved event', async () => { + const resp = await validators.deaffiliate() + assert.equal(resp.logs.length, 3) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorGroupMemberRemoved', + args: { + validator, + group, + }, + }) + }) + + describe('when the validator is the only member of that group', () => { + it('should emit the ValidatorGroupEmptied event', async () => { + const resp = await validators.deaffiliate() + assert.equal(resp.logs.length, 3) + const log = resp.logs[1] + assertContainSubset(log, { + event: 'ValidatorGroupEmptied', + args: { + group, + }, + }) + }) + + describe('when that group has received votes', () => { + beforeEach(async () => { + const voter = accounts[2] + const weight = 10 + await mockLockedGold.setWeight(voter, weight) + await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS, { from: voter }) + }) + + it('should remove the group from the list of electable groups with votes', async () => { + await validators.deaffiliate() + const [groups] = await validators.getValidatorGroupVotes() + assert.deepEqual(groups, []) + }) + }) + }) + }) + + it('should revert when the account is not a registered validator', async () => { + await assertRevert(validators.deaffiliate({ from: accounts[2] })) + }) + + it('should revert when the validator is not affiliated with a validator group', async () => { + await validators.deaffiliate() + await assertRevert(validators.deaffiliate()) + }) + }) + + describe('#registerValidatorGroup', () => { + const group = accounts[0] + beforeEach(async () => { + await mockLockedGold.setLockedCommitment( + group, + registrationRequirement.noticePeriod, + registrationRequirement.value + ) + }) + + it('should mark the account as a validator group', async () => { + await validators.registerValidatorGroup( + identifier, + name, + url, + registrationRequirement.noticePeriod + ) + assert.isTrue(await validators.isValidatorGroup(group)) + }) + + it('should add the account to the list of validator groups', async () => { + await validators.registerValidatorGroup( + identifier, + name, + url, + registrationRequirement.noticePeriod + ) + assert.deepEqual(await validators.getRegisteredValidatorGroups(), [group]) + }) + + it('should set the validator group identifier, name, and url', async () => { + await validators.registerValidatorGroup( + identifier, + name, + url, + registrationRequirement.noticePeriod + ) + const parsedGroup = parseValidatorGroupParams(await validators.getValidatorGroup(group)) + assert.equal(parsedGroup.identifier, identifier) + assert.equal(parsedGroup.name, name) + assert.equal(parsedGroup.url, url) + }) + + it('should emit the ValidatorGroupRegistered event', async () => { + const resp = await validators.registerValidatorGroup( + identifier, + name, + url, + registrationRequirement.noticePeriod + ) + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorGroupRegistered', + args: { + group, + identifier, + name, + url, + }, + }) + }) + + describe('when the account is already a registered validator', () => { + beforeEach(async () => { + await registerValidator(group) + }) + + it('should revert', async () => { + await assertRevert( + validators.registerValidatorGroup( + identifier, + name, + url, + registrationRequirement.noticePeriod + ) + ) + }) + }) + + describe('when the account is already a registered validator group', () => { + beforeEach(async () => { + await validators.registerValidatorGroup( + identifier, + name, + url, + registrationRequirement.noticePeriod + ) + }) + + it('should revert', async () => { + await assertRevert( + validators.registerValidatorGroup( + identifier, + name, + url, + registrationRequirement.noticePeriod + ) + ) + }) + }) + + describe('when the account does not meet the registration requirements', () => { + beforeEach(async () => { + await mockLockedGold.setLockedCommitment( + group, + registrationRequirement.noticePeriod, + registrationRequirement.value.minus(1) + ) + }) + + it('should revert', async () => { + await assertRevert( + validators.registerValidatorGroup( + identifier, + name, + url, + registrationRequirement.noticePeriod + ) + ) + }) + }) + }) + + describe('#deregisterValidatorGroup', () => { + const index = 0 + const group = accounts[0] + beforeEach(async () => { + await registerValidatorGroup(group) + }) + + it('should mark the account as not a validator group', async () => { + await validators.deregisterValidatorGroup(index) + assert.isFalse(await validators.isValidatorGroup(group)) + }) + + it('should remove the account from the list of validator groups', async () => { + await validators.deregisterValidatorGroup(index) + assert.deepEqual(await validators.getRegisteredValidatorGroups(), []) + }) + + it('should emit the ValidatorGroupDeregistered event', async () => { + const resp = await validators.deregisterValidatorGroup(index) + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorGroupDeregistered', + args: { + group, + }, + }) + }) + + it('should revert when the account is not a registered validator group', async () => { + await assertRevert(validators.deregisterValidatorGroup(index, { from: accounts[2] })) + }) + + it('should revert when the wrong index is provided', async () => { + await assertRevert(validators.deregisterValidatorGroup(index + 1)) + }) + + describe('when the validator group is not empty', () => { + const validator = accounts[1] + beforeEach(async () => { + await registerValidator(validator) + await validators.affiliate(group, { from: validator }) + await validators.addMember(validator) + }) + + it('should revert', async () => { + await assertRevert(validators.deregisterValidatorGroup(index)) + }) + }) + }) + + describe('#addMember', () => { + const group = accounts[0] + const validator = accounts[1] + beforeEach(async () => { + await registerValidator(validator) + await registerValidatorGroup(group) + await validators.affiliate(group, { from: validator }) + }) + + it('should add the member to the list of members', async () => { + await validators.addMember(validator) + const parsedGroup = parseValidatorGroupParams(await validators.getValidatorGroup(group)) + assert.deepEqual(parsedGroup.members, [validator]) + }) + + it('should emit the ValidatorGroupMemberAdded event', async () => { + const resp = await validators.addMember(validator) + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorGroupMemberAdded', + args: { + group, + validator, + }, + }) + }) + + it('should revert when the account is not a registered validator group', async () => { + await assertRevert(validators.addMember(validator, { from: accounts[2] })) + }) + + it('should revert when the member is not a registered validator', async () => { + await assertRevert(validators.addMember(accounts[2])) + }) + + describe('when the validator has not affiliated themselves with the group', () => { + beforeEach(async () => { + await validators.deaffiliate({ from: validator }) + }) + + it('should revert', async () => { + await assertRevert(validators.addMember(validator)) + }) + }) + + describe('when the validator is already a member of the group', () => { + beforeEach(async () => { + await validators.addMember(validator) + }) + + it('should revert', async () => { + await assertRevert(validators.addMember(validator)) + }) + }) + }) + + describe('#removeMember', () => { + const group = accounts[0] + const validator = accounts[1] + beforeEach(async () => { + await registerValidatorGroupWithMembers(group, [validator]) + }) + + it('should remove the member from the list of members', async () => { + await validators.removeMember(validator) + const parsedGroup = parseValidatorGroupParams(await validators.getValidatorGroup(group)) + assert.deepEqual(parsedGroup.members, []) + }) + + it('should emit the ValidatorGroupMemberRemoved event', async () => { + const resp = await validators.removeMember(validator) + assert.equal(resp.logs.length, 2) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorGroupMemberRemoved', + args: { + group, + validator, + }, + }) + }) + + describe('when the validator is the only member of the group', () => { + it('should emit the ValidatorGroupEmptied event', async () => { + const resp = await validators.removeMember(validator) + assert.equal(resp.logs.length, 2) + const log = resp.logs[1] + assertContainSubset(log, { + event: 'ValidatorGroupEmptied', + args: { + group, + }, + }) + }) + + describe('when the group has received votes', () => { + beforeEach(async () => { + const voter = accounts[2] + const weight = 10 + await mockLockedGold.setWeight(voter, weight) + await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS, { from: voter }) + }) + + it('should remove the group from the list of electable groups with votes', async () => { + await validators.removeMember(validator) + const [groups] = await validators.getValidatorGroupVotes() + assert.deepEqual(groups, []) + }) + }) + }) + + it('should revert when the account is not a registered validator group', async () => { + await assertRevert(validators.removeMember(validator, { from: accounts[2] })) + }) + + it('should revert when the member is not a registered validator', async () => { + await assertRevert(validators.removeMember(accounts[2])) + }) + + describe('when the validator is not a member of the validator group', () => { + beforeEach(async () => { + await validators.deaffiliate({ from: validator }) + }) + + it('should revert', async () => { + await assertRevert(validators.removeMember(validator)) + }) + }) + }) + + describe('#reorderMember', () => { + const group = accounts[0] + const validator1 = accounts[1] + const validator2 = accounts[2] + beforeEach(async () => { + await registerValidatorGroupWithMembers(group, [validator1, validator2]) + }) + + it('should reorder the list of group members', async () => { + await validators.reorderMember(validator2, validator1, NULL_ADDRESS) + const parsedGroup = parseValidatorGroupParams(await validators.getValidatorGroup(group)) + assert.deepEqual(parsedGroup.members, [validator2, validator1]) + }) + + it('should emit the ValidatorGroupMemberReordered event', async () => { + const resp = await validators.reorderMember(validator2, validator1, NULL_ADDRESS) + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorGroupMemberReordered', + args: { + group, + validator: validator2, + }, + }) + }) + + it('should revert when the account is not a registered validator group', async () => { + await assertRevert( + validators.reorderMember(validator2, validator1, NULL_ADDRESS, { from: accounts[2] }) + ) + }) + + it('should revert when the member is not a registered validator', async () => { + await assertRevert(validators.reorderMember(accounts[3], validator1, NULL_ADDRESS)) + }) + + describe('when the validator is not a member of the validator group', () => { + beforeEach(async () => { + await validators.deaffiliate({ from: validator2 }) + }) + + it('should revert', async () => { + await assertRevert(validators.reorderMember(validator2, validator1, NULL_ADDRESS)) + }) + }) + }) + + describe('#vote', () => { + const weight = new BigNumber(5) + const voter = accounts[0] + const validator = accounts[1] + const group = accounts[2] + beforeEach(async () => { + await registerValidatorGroupWithMembers(group, [validator]) + await mockLockedGold.setWeight(voter, weight) + }) + + it("should set the voter's vote", async () => { + await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS) + assert.isTrue(await validators.isVoting(voter)) + assert.equal(await validators.voters(voter), group) + }) + + it('should add the group to the list of those receiving votes', async () => { + await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS) + const [groups] = await validators.getValidatorGroupVotes() + assert.deepEqual(groups, [group]) + }) + + it("should increment the validator group's vote total", async () => { + await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS) + assertEqualBN(await validators.getVotesReceived(group), weight) + }) + + it('should emit the ValidatorGroupVoteCast event', async () => { + const resp = await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS) + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorGroupVoteCast', + args: { + account: voter, + group, + weight: new BigNumber(weight), + }, + }) + }) + + describe('when the group had not previously received votes', () => { + it('should add the group to the list of electable groups with votes', async () => { + await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS) + const [groups] = await validators.getValidatorGroupVotes() + assert.deepEqual(groups, [group]) + }) + }) + + it('should revert when the group is not a registered validator group', async () => { + await assertRevert(validators.vote(accounts[3], NULL_ADDRESS, NULL_ADDRESS)) + }) + + describe('when the group is empty', () => { + beforeEach(async () => { + await validators.removeMember(validator, { from: group }) + }) + + it('should revert', async () => { + await assertRevert(validators.vote(group, NULL_ADDRESS, NULL_ADDRESS)) + }) + }) + + describe('when the account voting is frozen', () => { + beforeEach(async () => { + await mockLockedGold.setVotingFrozen(voter) + }) + + it('should revert', async () => { + await assertRevert(validators.vote(group, NULL_ADDRESS, NULL_ADDRESS)) + }) + }) + + describe('when the account has no weight', () => { + beforeEach(async () => { + await mockLockedGold.setWeight(voter, NULL_ADDRESS) + }) + + it('should revert', async () => { + await assertRevert(validators.vote(group, NULL_ADDRESS, NULL_ADDRESS)) + }) + }) + describe('when the account has an outstanding vote', () => { + beforeEach(async () => { + await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS) + }) + + it('should revert', async () => { + await assertRevert(validators.vote(group, NULL_ADDRESS, NULL_ADDRESS)) + }) + }) + }) + + describe('#revokeVote', () => { + const weight = 5 + const voter = accounts[0] + const validator = accounts[1] + const group = accounts[2] + beforeEach(async () => { + await registerValidatorGroupWithMembers(group, [validator]) + await mockLockedGold.setWeight(voter, weight) + await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS) + }) + + it("should clear the voter's vote", async () => { + await validators.revokeVote(NULL_ADDRESS, NULL_ADDRESS) + assert.isFalse(await validators.isVoting(voter)) + assert.equal(await validators.voters(voter), NULL_ADDRESS) + }) + + it("should decrement the validator group's vote total", async () => { + await validators.revokeVote(NULL_ADDRESS, NULL_ADDRESS) + const [groups, votes] = await validators.getValidatorGroupVotes() + assert.deepEqual(groups, []) + assert.deepEqual(votes, []) + }) + + it('should emit the ValidatorGroupVoteRevoked event', async () => { + const resp = await validators.revokeVote(NULL_ADDRESS, NULL_ADDRESS) + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorGroupVoteRevoked', + args: { + account: voter, + group, + weight: new BigNumber(weight), + }, + }) + }) + + describe('when the group had not received other votes', () => { + it('should remove the group from the list of electable groups with votes', async () => { + await validators.revokeVote(NULL_ADDRESS, NULL_ADDRESS) + const [groups] = await validators.getValidatorGroupVotes() + assert.deepEqual(groups, []) + }) + }) + + describe('when the account does not have an outstanding vote', () => { + beforeEach(async () => { + await validators.revokeVote(NULL_ADDRESS, NULL_ADDRESS) + }) + + it('should revert', async () => { + await assertRevert(validators.revokeVote(NULL_ADDRESS, NULL_ADDRESS)) + }) + }) + }) + + describe('#getValidators', () => { + const group1 = accounts[0] + const group2 = accounts[1] + const group3 = accounts[2] + const validator1 = accounts[3] + const validator2 = accounts[4] + const validator3 = accounts[5] + const validator4 = accounts[6] + const validator5 = accounts[7] + const validator6 = accounts[8] + const validator7 = accounts[9] + + const hash1 = '0xa5b9d60f32436310afebcfda832817a68921beb782fabf7915cc0460b443116a' + const hash2 = '0xa832817a68921b10afebcfd0460b443116aeb782fabf7915cca5b9d60f324363' + + // If voterN votes for groupN: + // group1 gets 20 votes per member + // group2 gets 25 votes per member + // group3 gets 30 votes per member + // We cannot make any guarantee with respect to their ordering. + const voter1 = { address: accounts[0], weight: 80 } + const voter2 = { address: accounts[1], weight: 50 } + const voter3 = { address: accounts[2], weight: 30 } + const assertSameAddresses = (actual: string[], expected: string[]) => { + assert.sameMembers(actual.map((x) => x.toLowerCase()), expected.map((x) => x.toLowerCase())) + } + + beforeEach(async () => { + await registerValidatorGroupWithMembers(group1, [ + validator1, + validator2, + validator3, + validator4, + ]) + await registerValidatorGroupWithMembers(group2, [validator5, validator6]) + await registerValidatorGroupWithMembers(group3, [validator7]) + + for (const voter of [voter1, voter2, voter3]) { + await mockLockedGold.setWeight(voter.address, voter.weight) + } + await random.revealAndCommit(hash1, hash1, NULL_ADDRESS) + }) + + describe('when a single group has >= minElectableValidators as members and received votes', () => { + beforeEach(async () => { + await validators.vote(group1, NULL_ADDRESS, NULL_ADDRESS, { from: voter1.address }) + }) + + it("should return that group's member list", async () => { + assertSameAddresses(await validators.getValidators(), [ + validator1, + validator2, + validator3, + validator4, + ]) + }) + }) + + describe("when > maxElectableValidators members's groups receive votes", () => { + beforeEach(async () => { + await validators.vote(group1, NULL_ADDRESS, NULL_ADDRESS, { from: voter1.address }) + await validators.vote(group2, NULL_ADDRESS, group1, { from: voter2.address }) + await validators.vote(group3, NULL_ADDRESS, group2, { from: voter3.address }) + }) + + it('should return maxElectableValidators elected validators', async () => { + assertSameAddresses(await validators.getValidators(), [ + validator1, + validator2, + validator3, + validator5, + validator6, + validator7, + ]) + }) + }) + + describe('when different random values are provided', () => { + beforeEach(async () => { + await validators.vote(group1, NULL_ADDRESS, NULL_ADDRESS, { from: voter1.address }) + await validators.vote(group2, NULL_ADDRESS, group1, { from: voter2.address }) + await validators.vote(group3, NULL_ADDRESS, group2, { from: voter3.address }) + }) + + it('should return different results', async () => { + await random.revealAndCommit(hash1, hash1, NULL_ADDRESS) + const valsWithHash1 = (await validators.getValidators()).map((x) => x.toLowerCase()) + await random.revealAndCommit(hash2, hash2, NULL_ADDRESS) + const valsWithHash2 = (await validators.getValidators()).map((x) => x.toLowerCase()) + assert.sameMembers(valsWithHash1, valsWithHash2) + assert.notDeepEqual(valsWithHash1, valsWithHash2) + }) + }) + + describe('when a group receives enough votes for > n seats but only has n members', () => { + beforeEach(async () => { + await mockLockedGold.setWeight(voter3.address, 1000) + await validators.vote(group3, NULL_ADDRESS, NULL_ADDRESS, { from: voter3.address }) + await validators.vote(group1, NULL_ADDRESS, group3, { from: voter1.address }) + await validators.vote(group2, NULL_ADDRESS, group1, { from: voter2.address }) + }) + + it('should elect only n members from that group', async () => { + assertSameAddresses(await validators.getValidators(), [ + validator7, + validator1, + validator2, + validator3, + validator5, + validator6, + ]) + }) + }) + + describe('when an account has delegated validating to another address', () => { + const validatingDelegate = '0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95' + beforeEach(async () => { + await mockLockedGold.delegateValidating(validator3, validatingDelegate) + await validators.vote(group1, NULL_ADDRESS, NULL_ADDRESS, { from: voter1.address }) + await validators.vote(group2, NULL_ADDRESS, group1, { from: voter2.address }) + await validators.vote(group3, NULL_ADDRESS, group2, { from: voter3.address }) + }) + + it('should return the validating delegate in place of the account', async () => { + assertSameAddresses(await validators.getValidators(), [ + validator1, + validator2, + validatingDelegate, + validator5, + validator6, + validator7, + ]) + }) + }) + + describe('when there are not enough electable validators', () => { + beforeEach(async () => { + await validators.vote(group2, NULL_ADDRESS, NULL_ADDRESS, { from: voter2.address }) + await validators.vote(group3, NULL_ADDRESS, group2, { from: voter3.address }) + }) + + it('should revert', async () => { + await assertRevert(validators.getValidators()) + }) + }) + }) +}) From 651009da889147d3b1fba3b1da0c7c8f83d823de Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Thu, 26 Sep 2019 14:01:50 -0400 Subject: [PATCH 011/149] Election tests passing --- .../contracts/common/UsingRegistry.sol | 6 + .../linkedlists/AddressSortedLinkedList.sol | 8 + .../common/linkedlists/SortedLinkedList.sol | 12 +- .../contracts/governance/Election.sol | 68 +- .../governance/test/MockElection.sol | 8 +- .../governance/test/MockLockedGold.sol | 39 +- .../governance/test/MockValidators.sol | 37 +- .../contracts/identity/test/MockRandom.sol | 13 + .../migrations/17_elect_validators.ts | 17 +- packages/protocol/test/governance/election.ts | 1484 +++++------------ .../protocol/test/identity/attestations.ts | 14 +- 11 files changed, 637 insertions(+), 1069 deletions(-) create mode 100644 packages/protocol/contracts/identity/test/MockRandom.sol diff --git a/packages/protocol/contracts/common/UsingRegistry.sol b/packages/protocol/contracts/common/UsingRegistry.sol index 2e4ced1e5d3..4ae16b3302e 100644 --- a/packages/protocol/contracts/common/UsingRegistry.sol +++ b/packages/protocol/contracts/common/UsingRegistry.sol @@ -9,6 +9,8 @@ import "../governance/interfaces/IElection.sol"; import "../governance/interfaces/ILockedGold.sol"; import "../governance/interfaces/IValidators.sol"; +import "../identity/interfaces/IRandom.sol"; + // Ideally, UsingRegistry should inherit from Initializable and implement initialize() which calls // setRegistry(). TypeChain currently has problems resolving overloaded functions, so this is not // possible right now. @@ -60,6 +62,10 @@ contract UsingRegistry is Ownable { return ILockedGold(registry.getAddressForOrDie(LOCKED_GOLD_REGISTRY_ID)); } + function getRandom() internal view returns(IRandom) { + return IRandom(registry.getAddressForOrDie(RANDOM_REGISTRY_ID)); + } + function getValidators() internal view returns(IValidators) { return IValidators(registry.getAddressForOrDie(VALIDATORS_REGISTRY_ID)); } diff --git a/packages/protocol/contracts/common/linkedlists/AddressSortedLinkedList.sol b/packages/protocol/contracts/common/linkedlists/AddressSortedLinkedList.sol index 528d5500ece..be2135245ec 100644 --- a/packages/protocol/contracts/common/linkedlists/AddressSortedLinkedList.sol +++ b/packages/protocol/contracts/common/linkedlists/AddressSortedLinkedList.sol @@ -119,4 +119,12 @@ library AddressSortedLinkedList { } return keys; } + + /** + * @notice Gets all element keys from the doubly linked list. + * @return All element keys from head to tail. + */ + function getKeys(SortedLinkedList.List storage list) public view returns (address[] memory) { + return headN(list, list.list.numElements); + } } diff --git a/packages/protocol/contracts/common/linkedlists/SortedLinkedList.sol b/packages/protocol/contracts/common/linkedlists/SortedLinkedList.sol index 176d499749c..60e761b8c6a 100644 --- a/packages/protocol/contracts/common/linkedlists/SortedLinkedList.sol +++ b/packages/protocol/contracts/common/linkedlists/SortedLinkedList.sol @@ -33,10 +33,10 @@ library SortedLinkedList { ) public { - require(key != bytes32(0) && key != lesserKey && key != greaterKey && !contains(list, key)); - require((lesserKey != bytes32(0) || greaterKey != bytes32(0)) || list.list.numElements == 0); - require(contains(list, lesserKey) || lesserKey == bytes32(0)); - require(contains(list, greaterKey) || greaterKey == bytes32(0)); + require(key != bytes32(0) && key != lesserKey && key != greaterKey && !contains(list, key), "1"); + require((lesserKey != bytes32(0) || greaterKey != bytes32(0)) || list.list.numElements == 0, "2"); + require(contains(list, lesserKey) || lesserKey == bytes32(0), "3"); + require(contains(list, greaterKey) || greaterKey == bytes32(0), "4"); (lesserKey, greaterKey) = getLesserAndGreater(list, value, lesserKey, greaterKey); list.list.insert(key, lesserKey, greaterKey); list.values[key] = value; @@ -183,10 +183,10 @@ library SortedLinkedList { greaterKey == bytes32(0) && isValueBetween(list, value, list.list.head, greaterKey) ) { return (list.list.head, greaterKey); - } else if (isValueBetween(list, value, lesserKey, list.list.elements[lesserKey].nextKey)) { + } else if (lesserKey != bytes32(0) && isValueBetween(list, value, lesserKey, list.list.elements[lesserKey].nextKey)) { return (lesserKey, list.list.elements[lesserKey].nextKey); } else if ( - isValueBetween(list, value, list.list.elements[greaterKey].previousKey, greaterKey) + greaterKey != bytes32(0) && isValueBetween(list, value, list.list.elements[greaterKey].previousKey, greaterKey) ) { return (list.list.elements[greaterKey].previousKey, greaterKey); } else { diff --git a/packages/protocol/contracts/governance/Election.sol b/packages/protocol/contracts/governance/Election.sol index 1c151426dcf..6e047ae1f5a 100644 --- a/packages/protocol/contracts/governance/Election.sol +++ b/packages/protocol/contracts/governance/Election.sol @@ -79,16 +79,30 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { uint256 maxVotesPerAccount ); + event ValidatorGroupMarkedEligible( + address group + ); + + event ValidatorGroupMarkedIneligible( + address group + ); + event ValidatorGroupVoteCast( address indexed account, address indexed group, - uint256 weight + uint256 value + ); + + event ValidatorGroupVoteActivated( + address indexed account, + address indexed group, + uint256 value ); event ValidatorGroupVoteRevoked( address indexed account, address indexed group, - uint256 weight + uint256 value ); /** @@ -191,13 +205,13 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { nonReentrant returns (bool) { - require(votes.total.eligible.contains(group)); - require(0 < value && value <= getNumVotesReceivable(group)); + require(votes.total.eligible.contains(group), "1"); + require(0 < value && value <= getNumVotesReceivable(group), "2"); address account = getLockedGold().getAccountFromVoter(msg.sender); address[] storage list = votes.lists[account]; - require(list.length < maxVotesPerAccount); + require(list.length < maxVotesPerAccount, "3"); for (uint256 i = 0; i < list.length; i = i.add(1)) { - require(list[i] != group); + require(list[i] != group, "4"); } list.push(group); incrementPendingVotes(group, account, value); @@ -216,9 +230,10 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { address account = getLockedGold().getAccountFromVoter(msg.sender); PendingVotes storage pending = votes.pending; uint256 value = pending.balances[group][account]; - require(0 < value); + require(value > 0, 'one'); decrementPendingVotes(group, account, value); incrementActiveVotes(group, account, value); + emit ValidatorGroupVoteActivated(account, group, value); } /** @@ -302,12 +317,19 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { return total; } + function getAccountGroupsVotedFor(address account) external view returns (address[] memory) { + return votes.lists[account]; + } + function getAccountPendingVotesForGroup(address group, address account) public view returns (uint256) { return votes.pending.balances[group][account]; } function getAccountActiveVotesForGroup(address group, address account) public view returns (uint256) { uint256 numerator = votes.active.numerators[group][account].mul(votes.active.total[group]); + if (numerator == 0) { + return 0; + } uint256 denominator = votes.active.denominators[group]; return numerator.div(denominator); } @@ -318,6 +340,10 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { return pending.add(active); } + function getGroupTotalVotes(address group) external view returns (uint256) { + return votes.total.eligible.getValue(group); + } + /** * @notice Increments the number of total votes for `group` by `value`. * @param group The validator group whose vote total should be incremented. @@ -362,18 +388,21 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { uint256 newVoteTotal = votes.total.eligible.getValue(group).sub(value); votes.total.eligible.update(group, newVoteTotal, lesser, greater); } - votes.total.total = votes.total.total.add(value); + votes.total.total = votes.total.total.sub(value); } function markGroupIneligible(address group) external onlyRegisteredContract(VALIDATORS_REGISTRY_ID) { votes.total.eligible.remove(group); + emit ValidatorGroupMarkedIneligible(group); } + // TODO(asa): Should this only be callable by the group? function markGroupEligible(address group, address lesser, address greater) external { - require(!votes.total.eligible.contains(group)); - require(getValidators().getGroupNumMembers(group) > 0); + require(!votes.total.eligible.contains(group), "aaa"); + require(getValidators().getGroupNumMembers(group) > 0, "b"); uint256 value = votes.pending.total[group].add(votes.active.total[group]); votes.total.eligible.insert(group, value, lesser, greater); + emit ValidatorGroupMarkedEligible(group); } function incrementPendingVotes(address group, address account, uint256 value) private { @@ -411,7 +440,7 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { function getActiveVotesDelta(address group, uint256 value) private view returns (uint256) { // Preserve delta * total = value * denominator - return value.mul(votes.active.denominators[group]).div(votes.active.total[group]); + return value.mul(votes.active.denominators[group].add(1)).div(votes.active.total[group].add(1)); } /** @@ -438,6 +467,14 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { return votes.total.total; } + function getEligibleValidatorGroups() external view returns (address[] memory) { + return votes.total.eligible.getKeys(); + } + + function getEligibleValidatorGroupsVoteTotals() external view returns (address[] memory, uint256[] memory) { + return votes.total.eligible.getElements(); + } + function validatorAddressFromCurrentSet(uint256 index) external view returns (address) { address validatorAddress; assembly { @@ -507,6 +544,14 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { totalNumMembersElected = totalNumMembersElected.add(1); } } + // Shuffle the validator set using validator-supplied entropy + bytes32 r = getRandom().random(); + for (uint256 i = electedValidators.length - 1; i > 0; i = i.sub(1)) { + uint256 j = uint256(r) % (i + 1); + (electedValidators[i], electedValidators[j]) = (electedValidators[j], electedValidators[i]); + r = keccak256(abi.encodePacked(r)); + } + return electedValidators; } @@ -534,6 +579,7 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { FixidityLib.newFixed(numMembersElected[i].add(1)) ); if (n.gt(maxN)) { + maxN = n; groupIndex = i; memberElected = true; } diff --git a/packages/protocol/contracts/governance/test/MockElection.sol b/packages/protocol/contracts/governance/test/MockElection.sol index 6458ad20f5c..da4a5e34e18 100644 --- a/packages/protocol/contracts/governance/test/MockElection.sol +++ b/packages/protocol/contracts/governance/test/MockElection.sol @@ -8,6 +8,7 @@ import "../interfaces/IElection.sol"; contract MockElection is IElection { mapping(address => bool) public isIneligible; + address[] public electedValidators; function markGroupIneligible(address account) external { isIneligible[account] = true; @@ -21,8 +22,11 @@ contract MockElection is IElection { return 0; } + function setElectedValidators(address[] calldata _electedValidators) external { + electedValidators = _electedValidators; + } + function electValidators() external view returns (address[] memory) { - address[] memory r = new address[](0); - return r; + return electedValidators; } } diff --git a/packages/protocol/contracts/governance/test/MockLockedGold.sol b/packages/protocol/contracts/governance/test/MockLockedGold.sol index 3fbb7f2a2d4..3df8513e0c1 100644 --- a/packages/protocol/contracts/governance/test/MockLockedGold.sol +++ b/packages/protocol/contracts/governance/test/MockLockedGold.sol @@ -1,5 +1,7 @@ pragma solidity ^0.5.3; +import "openzeppelin-solidity/contracts/math/SafeMath.sol"; + import "../interfaces/ILockedGold.sol"; @@ -7,15 +9,29 @@ import "../interfaces/ILockedGold.sol"; * @title A mock LockedGold for testing. */ contract MockLockedGold is ILockedGold { + + using SafeMath for uint256; + struct MustMaintain { uint256 value; uint256 timestamp; } - mapping(address => uint256) public totalLockedGold; + struct Authorizations { + address validator; + address voter; + } + + mapping(address => uint256) public accountTotalLockedGold; // TODO(asa): Rename to minimumBalance mapping(address => MustMaintain) public mustMaintain; + mapping(address => uint256) public nonvotingAccountBalance; + mapping(address => address) public authorizedValidators; + uint256 private totalLockedGold; + function authorizeValidator(address account, address validator) external { + authorizedValidators[account] = validator; + } function getAccountFromValidator(address accountOrValidator) external view returns (address) { return accountOrValidator; @@ -26,11 +42,17 @@ contract MockLockedGold is ILockedGold { } function getValidatorFromAccount(address account) external view returns (address) { - return account; + address authorizedValidator = authorizedValidators[account]; + return authorizedValidator == address(0) ? account : authorizedValidator; } - function incrementNonvotingAccountBalance(address, uint256) external {} - function decrementNonvotingAccountBalance(address, uint256) external {} + function incrementNonvotingAccountBalance(address account, uint256 value) external { + nonvotingAccountBalance[account] = nonvotingAccountBalance[account].add(value); + } + + function decrementNonvotingAccountBalance(address account, uint256 value) external { + nonvotingAccountBalance[account] = nonvotingAccountBalance[account].sub(value); + } function setAccountMustMaintain(address account, uint256 value, uint256 timestamp) external returns (bool) { mustMaintain[account] = MustMaintain(value, timestamp); @@ -43,14 +65,17 @@ contract MockLockedGold is ILockedGold { } function setAccountTotalLockedGold(address account, uint256 value) external { - totalLockedGold[account] = value; + accountTotalLockedGold[account] = value; } function getAccountTotalLockedGold(address account) external view returns (uint256) { - return totalLockedGold[account]; + return accountTotalLockedGold[account]; } + function setTotalLockedGold(uint256 value) external { + totalLockedGold = value; + } function getTotalLockedGold() external view returns (uint256) { - return 0; + return totalLockedGold; } } diff --git a/packages/protocol/contracts/governance/test/MockValidators.sol b/packages/protocol/contracts/governance/test/MockValidators.sol index 4ed212ae6f1..805571e84a2 100644 --- a/packages/protocol/contracts/governance/test/MockValidators.sol +++ b/packages/protocol/contracts/governance/test/MockValidators.sol @@ -9,7 +9,9 @@ contract MockValidators is IValidators { mapping(address => bool) private _isValidating; mapping(address => bool) private _isVoting; - address[] private validators; + mapping(address => uint256) private numGroupMembers; + mapping(address => address[]) private members; + uint256 private numRegisteredValidators; function isValidating(address account) external view returns (bool) { return _isValidating[account]; @@ -19,8 +21,8 @@ contract MockValidators is IValidators { return _isVoting[account]; } - function getValidators() external view returns (address[] memory) { - return validators; + function getGroupNumMembers(address group) public view returns (uint256) { + return members[group].length; } function setValidating(address account) external { @@ -31,7 +33,32 @@ contract MockValidators is IValidators { _isVoting[account] = true; } - function addValidator(address account) external { - validators.push(account); + function setNumRegisteredValidators(uint256 value) external { + numRegisteredValidators = value; + } + + function getNumRegisteredValidators() external view returns (uint256) { + return numRegisteredValidators; + } + + function setMembers(address group, address[] calldata _members) external { + members[group] = _members; + } + + function getTopValidatorsFromGroup(address group, uint256 n) external view returns (address[] memory) { + require(n <= members[group].length); + address[] memory validators = new address[](n); + for (uint256 i = 0; i < n; i++) { + validators[i] = members[group][i]; + } + return validators; + } + + function getGroupsNumMembers(address[] calldata groups) external view returns (uint256[] memory) { + uint256[] memory numMembers = new uint256[](groups.length); + for (uint256 i = 0; i < groups.length; i++) { + numMembers[i] = getGroupNumMembers(groups[i]); + } + return numMembers; } } diff --git a/packages/protocol/contracts/identity/test/MockRandom.sol b/packages/protocol/contracts/identity/test/MockRandom.sol new file mode 100644 index 00000000000..6953fdb56bf --- /dev/null +++ b/packages/protocol/contracts/identity/test/MockRandom.sol @@ -0,0 +1,13 @@ +pragma solidity ^0.5.3; + +import "openzeppelin-solidity/contracts/math/SafeMath.sol"; + + +contract MockRandom { + + bytes32 public random; + + function setRandom(bytes32 value) external { + random = value; + } +} diff --git a/packages/protocol/migrations/17_elect_validators.ts b/packages/protocol/migrations/17_elect_validators.ts index 800e2f14c16..73c125b36cd 100644 --- a/packages/protocol/migrations/17_elect_validators.ts +++ b/packages/protocol/migrations/17_elect_validators.ts @@ -168,15 +168,24 @@ module.exports = async (_deployer: any) => { }) } + console.info(' Marking Validator Group as eligible for election ...') + // @ts-ignore + const markTx = election.contract.methods.markGroupEligible( + account.address, + NULL_ADDRESS, + NULL_ADDRESS + ) + await sendTransactionWithPrivateKey(web3, markTx, account.privateKey, { + to: election.address, + }) + console.info(' Voting for Validator Group ...') // Make another deposit so our vote has more weight. const minLockedGoldVotePerValidator = 10000 const value = new BigNumber(valKeys.length) .times(minLockedGoldVotePerValidator) .times(web3.utils.toWei(1)) - await lockedGold.lock({ - // @ts-ignore - value, - }) + // @ts-ignore + await lockedGold.lock({ value }) await election.vote(account.address, value, NULL_ADDRESS, NULL_ADDRESS) } diff --git a/packages/protocol/test/governance/election.ts b/packages/protocol/test/governance/election.ts index ca806c91a0f..28392a05db5 100644 --- a/packages/protocol/test/governance/election.ts +++ b/packages/protocol/test/governance/election.ts @@ -9,152 +9,79 @@ import BigNumber from 'bignumber.js' import { MockLockedGoldContract, MockLockedGoldInstance, + MockValidatorsContract, + MockValidatorsInstance, MockRandomContract, MockRandomInstance, RegistryContract, RegistryInstance, - ValidatorsContract, - ValidatorsInstance, + ElectionContract, + ElectionInstance, } from 'types' -const Validators: ValidatorsContract = artifacts.require('Validators') +const Election: ElectionContract = artifacts.require('Election') const MockLockedGold: MockLockedGoldContract = artifacts.require('MockLockedGold') +const MockValidators: MockValidatorsContract = artifacts.require('MockValidators') +const MockRandom: MockRandomContract = artifacts.require('MockRandom') const Registry: RegistryContract = artifacts.require('Registry') -const Random: MockRandomContract = artifacts.require('MockRandom') // @ts-ignore // TODO(mcortesi): Use BN -Validators.numberFormat = 'BigNumber' - -const parseValidatorParams = (validatorParams: any) => { - return { - identifier: validatorParams[0], - name: validatorParams[1], - url: validatorParams[2], - publicKeysData: validatorParams[3], - affiliation: validatorParams[4], - } -} - -const parseValidatorGroupParams = (groupParams: any) => { - return { - identifier: groupParams[0], - name: groupParams[1], - url: groupParams[2], - members: groupParams[3], - } -} - -contract('Validators', (accounts: string[]) => { - let validators: ValidatorsInstance +Election.numberFormat = 'BigNumber' + +contract('Election', (accounts: string[]) => { + let election: ElectionInstance let registry: RegistryInstance let mockLockedGold: MockLockedGoldInstance - let random: MockRandomInstance - - // A random 64 byte hex string. - const publicKey = - 'ea0733ad275e2b9e05541341a97ee82678c58932464fad26164657a111a7e37a9fa0300266fb90e2135a1f1512350cb4e985488a88809b14e3cbe415e76e82b2' - const blsPublicKey = - '4d23d8cd06f30b1fa7cf368e2f5399ab04bb6846c682f493a98a607d3dfb7e53a712bb79b475c57b0ac2785460f91301' - const blsPoP = - '9d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d740501' - - const publicKeysData = '0x' + publicKey + blsPublicKey + blsPoP + let mockValidators: MockValidatorsInstance const nonOwner = accounts[1] const minElectableValidators = new BigNumber(4) const maxElectableValidators = new BigNumber(6) - const registrationRequirement = { value: new BigNumber(100), noticePeriod: new BigNumber(60) } - const identifier = 'test-identifier' - const name = 'test-name' - const url = 'test-url' + const maxVotesPerAccount = new BigNumber(3) beforeEach(async () => { - validators = await Validators.new() + election = await Election.new() mockLockedGold = await MockLockedGold.new() - random = await Random.new() + mockValidators = await MockValidators.new() registry = await Registry.new() await registry.setAddressFor(CeloContractName.LockedGold, mockLockedGold.address) - await registry.setAddressFor(CeloContractName.Random, random.address) - await validators.initialize( + await registry.setAddressFor(CeloContractName.Validators, mockValidators.address) + await election.initialize( registry.address, minElectableValidators, maxElectableValidators, - registrationRequirement.value, - registrationRequirement.noticePeriod + maxVotesPerAccount ) }) - const registerValidator = async (validator: string) => { - await mockLockedGold.setLockedCommitment( - validator, - registrationRequirement.noticePeriod, - registrationRequirement.value - ) - await validators.registerValidator( - identifier, - name, - url, - // @ts-ignore bytes type - publicKeysData, - registrationRequirement.noticePeriod, - { from: validator } - ) - } - - const registerValidatorGroup = async (group: string) => { - await mockLockedGold.setLockedCommitment( - group, - registrationRequirement.noticePeriod, - registrationRequirement.value - ) - await validators.registerValidatorGroup( - identifier, - name, - url, - registrationRequirement.noticePeriod, - { from: group } - ) - } - - const registerValidatorGroupWithMembers = async (group: string, members: string[]) => { - await registerValidatorGroup(group) - for (const validator of members) { - await registerValidator(validator) - await validators.affiliate(group, { from: validator }) - await validators.addMember(validator, { from: group }) - } - } - describe('#initialize()', () => { it('should have set the owner', async () => { - const owner: string = await validators.owner() + const owner: string = await election.owner() assert.equal(owner, accounts[0]) }) it('should have set minElectableValidators', async () => { - const actualMinElectableValidators = await validators.minElectableValidators() + const actualMinElectableValidators = await election.minElectableValidators() assertEqualBN(actualMinElectableValidators, minElectableValidators) }) it('should have set maxElectableValidators', async () => { - const actualMaxElectableValidators = await validators.maxElectableValidators() + const actualMaxElectableValidators = await election.maxElectableValidators() assertEqualBN(actualMaxElectableValidators, maxElectableValidators) }) - it('should have set the registration requirements', async () => { - const [value, noticePeriod] = await validators.getRegistrationRequirement() - assertEqualBN(value, registrationRequirement.value) - assertEqualBN(noticePeriod, registrationRequirement.noticePeriod) + it('should have set maxVotesPerAccount', async () => { + const actualMaxVotesPerAccount = await election.maxVotesPerAccount() + assertEqualBN(actualMaxVotesPerAccount, maxVotesPerAccount) }) it('should not be callable again', async () => { await assertRevert( - validators.initialize( + election.initialize( registry.address, minElectableValidators, maxElectableValidators, - registrationRequirement.value, - registrationRequirement.noticePeriod + maxVotesPerAccount ) ) }) @@ -162,13 +89,13 @@ contract('Validators', (accounts: string[]) => { describe('#setMinElectableValidators', () => { const newMinElectableValidators = minElectableValidators.plus(1) - it('should set the minimum deposit', async () => { - await validators.setMinElectableValidators(newMinElectableValidators) - assertEqualBN(await validators.minElectableValidators(), newMinElectableValidators) + it('should set the minimum electable valdiators', async () => { + await election.setMinElectableValidators(newMinElectableValidators) + assertEqualBN(await election.minElectableValidators(), newMinElectableValidators) }) it('should emit the MinElectableValidatorsSet event', async () => { - const resp = await validators.setMinElectableValidators(newMinElectableValidators) + const resp = await election.setMinElectableValidators(newMinElectableValidators) assert.equal(resp.logs.length, 1) const log = resp.logs[0] assertContainSubset(log, { @@ -180,33 +107,33 @@ contract('Validators', (accounts: string[]) => { }) it('should revert when the minElectableValidators is zero', async () => { - await assertRevert(validators.setMinElectableValidators(0)) + await assertRevert(election.setMinElectableValidators(0)) }) it('should revert when the minElectableValidators is greater than maxElectableValidators', async () => { - await assertRevert(validators.setMinElectableValidators(maxElectableValidators.plus(1))) + await assertRevert(election.setMinElectableValidators(maxElectableValidators.plus(1))) }) it('should revert when the minElectableValidators is unchanged', async () => { - await assertRevert(validators.setMinElectableValidators(minElectableValidators)) + await assertRevert(election.setMinElectableValidators(minElectableValidators)) }) it('should revert when called by anyone other than the owner', async () => { await assertRevert( - validators.setMinElectableValidators(newMinElectableValidators, { from: nonOwner }) + election.setMinElectableValidators(newMinElectableValidators, { from: nonOwner }) ) }) }) describe('#setMaxElectableValidators', () => { const newMaxElectableValidators = maxElectableValidators.plus(1) - it('should set the minimum deposit', async () => { - await validators.setMaxElectableValidators(newMaxElectableValidators) - assertEqualBN(await validators.maxElectableValidators(), newMaxElectableValidators) + it('should set the max electable validators', async () => { + await election.setMaxElectableValidators(newMaxElectableValidators) + assertEqualBN(await election.maxElectableValidators(), newMaxElectableValidators) }) it('should emit the MaxElectableValidatorsSet event', async () => { - const resp = await validators.setMaxElectableValidators(newMaxElectableValidators) + const resp = await election.setMaxElectableValidators(newMaxElectableValidators) assert.equal(resp.logs.length, 1) const log = resp.logs[0] assertContainSubset(log, { @@ -218,1047 +145,563 @@ contract('Validators', (accounts: string[]) => { }) it('should revert when the maxElectableValidators is less than minElectableValidators', async () => { - await assertRevert(validators.setMaxElectableValidators(minElectableValidators.minus(1))) + await assertRevert(election.setMaxElectableValidators(minElectableValidators.minus(1))) }) it('should revert when the maxElectableValidators is unchanged', async () => { - await assertRevert(validators.setMaxElectableValidators(maxElectableValidators)) + await assertRevert(election.setMaxElectableValidators(maxElectableValidators)) }) it('should revert when called by anyone other than the owner', async () => { await assertRevert( - validators.setMaxElectableValidators(newMaxElectableValidators, { from: nonOwner }) + election.setMaxElectableValidators(newMaxElectableValidators, { from: nonOwner }) ) }) }) - describe('#setRegistrationRequirement', () => { - const newValue = registrationRequirement.value.plus(1) - const newNoticePeriod = registrationRequirement.noticePeriod.plus(1) - - it('should set the value and notice period', async () => { - await validators.setRegistrationRequirement(newValue, newNoticePeriod) - const [value, noticePeriod] = await validators.getRegistrationRequirement() - assertEqualBN(value, newValue) - assertEqualBN(noticePeriod, newNoticePeriod) + describe('#setMaxVotesPerAccount', () => { + const newMaxVotesPerAccount = maxVotesPerAccount.plus(1) + it('should set the max electable validators', async () => { + await election.setMaxVotesPerAccount(newMaxVotesPerAccount) + assertEqualBN(await election.maxVotesPerAccount(), newMaxVotesPerAccount) }) - it('should emit the RegistrationRequirementSet event', async () => { - const resp = await validators.setRegistrationRequirement(newValue, newNoticePeriod) + it('should emit the MaxVotesPerAccountSet event', async () => { + const resp = await election.setMaxVotesPerAccount(newMaxVotesPerAccount) assert.equal(resp.logs.length, 1) const log = resp.logs[0] assertContainSubset(log, { - event: 'RegistrationRequirementSet', + event: 'MaxVotesPerAccountSet', args: { - value: new BigNumber(newValue), - noticePeriod: new BigNumber(newNoticePeriod), + maxVotesPerAccount: new BigNumber(newMaxVotesPerAccount), }, }) }) - it('should revert when the requirement is unchanged', async () => { - await assertRevert( - validators.setRegistrationRequirement( - registrationRequirement.value, - registrationRequirement.noticePeriod - ) - ) + it('should revert when the maxVotesPerAccount is unchanged', async () => { + await assertRevert(election.setMaxVotesPerAccount(maxVotesPerAccount)) }) it('should revert when called by anyone other than the owner', async () => { - await assertRevert( - validators.setRegistrationRequirement(newValue, newNoticePeriod, { from: nonOwner }) - ) - }) - }) - - describe('#registerValidator', () => { - const validator = accounts[0] - beforeEach(async () => { - await mockLockedGold.setLockedCommitment( - validator, - registrationRequirement.noticePeriod, - registrationRequirement.value - ) - }) - - it('should mark the account as a validator', async () => { - await validators.registerValidator( - identifier, - name, - url, - // @ts-ignore bytes type - publicKeysData, - registrationRequirement.noticePeriod - ) - assert.isTrue(await validators.isValidator(validator)) - }) - - it('should add the account to the list of validators', async () => { - await validators.registerValidator( - identifier, - name, - url, - // @ts-ignore bytes type - publicKeysData, - registrationRequirement.noticePeriod - ) - assert.deepEqual(await validators.getRegisteredValidators(), [validator]) - }) - - it('should set the validator identifier, name, url, and public key', async () => { - await validators.registerValidator( - identifier, - name, - url, - // @ts-ignore bytes type - publicKeysData, - registrationRequirement.noticePeriod - ) - const parsedValidator = parseValidatorParams(await validators.getValidator(validator)) - assert.equal(parsedValidator.identifier, identifier) - assert.equal(parsedValidator.name, name) - assert.equal(parsedValidator.url, url) - assert.equal(parsedValidator.publicKeysData, publicKeysData) - }) - - it('should emit the ValidatorRegistered event', async () => { - const resp = await validators.registerValidator( - identifier, - name, - url, - // @ts-ignore bytes type - publicKeysData, - registrationRequirement.noticePeriod - ) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'ValidatorRegistered', - args: { - validator, - identifier, - name, - url, - publicKeysData, - }, - }) - }) - - describe('when the account is already a registered validator', () => { - beforeEach(async () => { - await validators.registerValidator( - identifier, - name, - url, - // @ts-ignore bytes type - publicKeysData, - registrationRequirement.noticePeriod - ) - }) - - it('should revert', async () => { - await assertRevert( - validators.registerValidator( - identifier, - name, - url, - // @ts-ignore bytes type - publicKeysData, - registrationRequirement.noticePeriod - ) - ) - }) - }) - - describe('when the account is already a registered validator group', () => { - beforeEach(async () => { - await validators.registerValidatorGroup( - identifier, - name, - url, - registrationRequirement.noticePeriod - ) - }) - - it('should revert', async () => { - await assertRevert( - validators.registerValidator( - identifier, - name, - url, - // @ts-ignore bytes type - publicKeysData, - registrationRequirement.noticePeriod - ) - ) - }) - }) - - describe('when the account does not meet the registration requirements', () => { - beforeEach(async () => { - await mockLockedGold.setLockedCommitment( - validator, - registrationRequirement.noticePeriod, - registrationRequirement.value.minus(1) - ) - }) - - it('should revert', async () => { - await assertRevert( - validators.registerValidator( - identifier, - name, - url, - // @ts-ignore bytes type - publicKeysData, - registrationRequirement.noticePeriod - ) - ) - }) + await assertRevert(election.setMaxVotesPerAccount(newMaxVotesPerAccount, { from: nonOwner })) }) }) - describe('#deregisterValidator', () => { - const validator = accounts[0] - const index = 0 - beforeEach(async () => { - await registerValidator(validator) - }) - - it('should mark the account as not a validator', async () => { - await validators.deregisterValidator(index) - assert.isFalse(await validators.isValidator(validator)) - }) - - it('should remove the account from the list of validators', async () => { - await validators.deregisterValidator(index) - assert.deepEqual(await validators.getRegisteredValidators(), []) - }) - - it('should emit the ValidatorDeregistered event', async () => { - const resp = await validators.deregisterValidator(index) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'ValidatorDeregistered', - args: { - validator, - }, - }) - }) - - describe('when the validator is affiliated with a validator group', () => { - const group = accounts[1] + describe('#markGroupEligible', () => { + const group = accounts[1] + describe('when the group has members', () => { beforeEach(async () => { - await registerValidatorGroup(group) - await validators.affiliate(group) + await mockValidators.setMembers(group, [accounts[9]]) }) - it('should emit the ValidatorDeafilliated event', async () => { - const resp = await validators.deregisterValidator(index) - assert.equal(resp.logs.length, 2) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'ValidatorDeaffiliated', - args: { - validator, - group, - }, - }) - }) - - describe('when the validator is a member of that group', () => { + describe('when the group has no votes', () => { + let resp: any beforeEach(async () => { - await validators.addMember(validator, { from: group }) + resp = await election.markGroupEligible(group, NULL_ADDRESS, NULL_ADDRESS) }) - it('should remove the validator from the group membership list', async () => { - await validators.deregisterValidator(index) - const parsedGroup = parseValidatorGroupParams(await validators.getValidatorGroup(group)) - assert.deepEqual(parsedGroup.members, []) + it('should add the group to the list of eligible groups', async () => { + assert.deepEqual(await election.getEligibleValidatorGroups(), [group]) }) - it('should emit the ValidatorGroupMemberRemoved event', async () => { - const resp = await validators.deregisterValidator(index) - assert.equal(resp.logs.length, 4) + it('should emit the ValidatorGroupMarkedEligible event', async () => { + assert.equal(resp.logs.length, 1) const log = resp.logs[0] assertContainSubset(log, { - event: 'ValidatorGroupMemberRemoved', + event: 'ValidatorGroupMarkedEligible', args: { - validator, group, }, }) }) - describe('when the validator is the only member of that group', () => { - it('should emit the ValidatorGroupEmptied event', async () => { - const resp = await validators.deregisterValidator(index) - assert.equal(resp.logs.length, 4) - const log = resp.logs[1] - assertContainSubset(log, { - event: 'ValidatorGroupEmptied', - args: { - group, - }, - }) - }) - - describe('when that group has received votes', () => { - beforeEach(async () => { - const voter = accounts[2] - const weight = 10 - await mockLockedGold.setWeight(voter, weight) - await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS, { from: voter }) - }) - - it('should remove the group from the list of electable groups with votes', async () => { - await validators.deregisterValidator(index) - const [groups] = await validators.getValidatorGroupVotes() - assert.deepEqual(groups, []) - }) + describe('when the group has already been marked eligible', () => { + it('should revert', async () => { + await assertRevert(election.markGroupEligible(group, NULL_ADDRESS, NULL_ADDRESS)) }) }) }) }) - it('should revert when the account is not a registered validator', async () => { - await assertRevert(validators.deregisterValidator(index, { from: accounts[2] })) - }) - - it('should revert when the wrong index is provided', async () => { - await assertRevert(validators.deregisterValidator(index + 1)) + describe('when the group has no members', () => { + it('should revert', async () => { + await assertRevert(election.markGroupEligible(group, NULL_ADDRESS, NULL_ADDRESS)) + }) }) }) - describe('#affiliate', () => { - const validator = accounts[0] + describe('#markGroupIneligible', () => { const group = accounts[1] - beforeEach(async () => { - await registerValidator(validator) - await registerValidatorGroup(group) - }) - - it('should set the affiliate', async () => { - await validators.affiliate(group) - const parsedValidator = parseValidatorParams(await validators.getValidator(validator)) - assert.equal(parsedValidator.affiliation, group) - }) - - it('should emit the ValidatorAffiliated event', async () => { - const resp = await validators.affiliate(group) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'ValidatorAffiliated', - args: { - validator, - group, - }, - }) - }) - - describe('when the validator is already affiliated with a validator group', () => { - const otherGroup = accounts[2] + describe('when the group is eligible', () => { beforeEach(async () => { - await validators.affiliate(group) - await registerValidatorGroup(otherGroup) - }) - - it('should set the affiliate', async () => { - await validators.affiliate(otherGroup) - const parsedValidator = parseValidatorParams(await validators.getValidator(validator)) - assert.equal(parsedValidator.affiliation, otherGroup) + await mockValidators.setMembers(group, [accounts[9]]) + await election.markGroupEligible(group, NULL_ADDRESS, NULL_ADDRESS) }) - it('should emit the ValidatorDeafilliated event', async () => { - const resp = await validators.affiliate(otherGroup) - assert.equal(resp.logs.length, 2) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'ValidatorDeaffiliated', - args: { - validator, - group, - }, - }) - }) - - it('should emit the ValidatorAffiliated event', async () => { - const resp = await validators.affiliate(otherGroup) - assert.equal(resp.logs.length, 2) - const log = resp.logs[1] - assertContainSubset(log, { - event: 'ValidatorAffiliated', - args: { - validator, - group: otherGroup, - }, - }) - }) - - describe('when the validator is a member of that group', () => { + describe('when called by the registered Validators contract', () => { + let resp: any beforeEach(async () => { - await validators.addMember(validator, { from: group }) + await registry.setAddressFor(CeloContractName.Validators, accounts[0]) + resp = await election.markGroupIneligible(group) }) - it('should remove the validator from the group membership list', async () => { - await validators.affiliate(otherGroup) - const parsedGroup = parseValidatorGroupParams(await validators.getValidatorGroup(group)) - assert.deepEqual(parsedGroup.members, []) + describe('when the group has votes', () => {}) + + it('should remove the group from the list of eligible groups', async () => { + assert.deepEqual(await election.getEligibleValidatorGroups(), []) }) - it('should emit the ValidatorGroupMemberRemoved event', async () => { - const resp = await validators.affiliate(otherGroup) - assert.equal(resp.logs.length, 4) + it('should emit the ValidatorGroupMarkedIneligible event', async () => { + assert.equal(resp.logs.length, 1) const log = resp.logs[0] assertContainSubset(log, { - event: 'ValidatorGroupMemberRemoved', + event: 'ValidatorGroupMarkedIneligible', args: { - validator, group, }, }) }) + }) - describe('when the validator is the only member of that group', () => { - it('should emit the ValidatorGroupEmptied event', async () => { - const resp = await validators.affiliate(otherGroup) - assert.equal(resp.logs.length, 4) - const log = resp.logs[1] - assertContainSubset(log, { - event: 'ValidatorGroupEmptied', - args: { - group, - }, - }) - }) - - describe('when that group has received votes', () => { - beforeEach(async () => { - const voter = accounts[2] - const weight = 10 - await mockLockedGold.setWeight(voter, weight) - await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS, { from: voter }) - }) - - it('should remove the group from the list of electable groups with votes', async () => { - await validators.affiliate(otherGroup) - const [groups] = await validators.getValidatorGroupVotes() - assert.deepEqual(groups, []) - }) - }) + describe('when not called by the registered Validators contract', () => { + it('should revert', async () => { + await assertRevert(election.markGroupIneligible(group)) }) }) }) - it('should revert when the account is not a registered validator', async () => { - await assertRevert(validators.affiliate(group, { from: accounts[2] })) - }) + describe('when the group is ineligible', () => { + describe('when called by the registered Validators contract', () => { + beforeEach(async () => { + await registry.setAddressFor(CeloContractName.Validators, accounts[0]) + }) - it('should revert when the group is not a registered validator group', async () => { - await assertRevert(validators.affiliate(accounts[2])) + it('should revert', async () => { + await assertRevert(election.markGroupIneligible(group)) + }) + }) }) }) - describe('#deaffiliate', () => { - const validator = accounts[0] + describe('#vote', () => { + const voter = accounts[0] const group = accounts[1] - beforeEach(async () => { - await registerValidator(validator) - await registerValidatorGroup(group) - await validators.affiliate(group) - }) - - it('should clear the affiliate', async () => { - await validators.deaffiliate() - const parsedValidator = parseValidatorParams(await validators.getValidator(validator)) - assert.equal(parsedValidator.affiliation, NULL_ADDRESS) - }) - - it('should emit the ValidatorDeaffiliated event', async () => { - const resp = await validators.deaffiliate() - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'ValidatorDeaffiliated', - args: { - validator, - group, - }, - }) - }) - - describe('when the validator is a member of the affiliated group', () => { + const value = 1000 + describe('when the group is eligible', () => { beforeEach(async () => { - await validators.addMember(validator, { from: group }) - }) - - it('should remove the validator from the group membership list', async () => { - await validators.deaffiliate() - const parsedGroup = parseValidatorGroupParams(await validators.getValidatorGroup(group)) - assert.deepEqual(parsedGroup.members, []) + await mockValidators.setMembers(group, [accounts[9]]) + await election.markGroupEligible(group, NULL_ADDRESS, NULL_ADDRESS) }) - it('should emit the ValidatorGroupMemberRemoved event', async () => { - const resp = await validators.deaffiliate() - assert.equal(resp.logs.length, 3) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'ValidatorGroupMemberRemoved', - args: { - validator, - group, - }, + describe('when the group can receive votes', () => { + beforeEach(async () => { + await mockLockedGold.setTotalLockedGold(value) + await mockValidators.setNumRegisteredValidators(1) }) - }) - describe('when the validator is the only member of that group', () => { - it('should emit the ValidatorGroupEmptied event', async () => { - const resp = await validators.deaffiliate() - assert.equal(resp.logs.length, 3) - const log = resp.logs[1] - assertContainSubset(log, { - event: 'ValidatorGroupEmptied', - args: { - group, - }, - }) - }) + describe('when the voter can vote for an additional group', () => { + describe('when the voter has sufficient non-voting balance', () => { + let resp: any + beforeEach(async () => { + await mockLockedGold.incrementNonvotingAccountBalance(voter, value) + resp = await election.vote(group, value, NULL_ADDRESS, NULL_ADDRESS) + }) - describe('when that group has received votes', () => { - beforeEach(async () => { - const voter = accounts[2] - const weight = 10 - await mockLockedGold.setWeight(voter, weight) - await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS, { from: voter }) - }) + it('should add the group to the list of groups the account has voted for', async () => { + assert.deepEqual(await election.getAccountGroupsVotedFor(voter), [group]) + }) - it('should remove the group from the list of electable groups with votes', async () => { - await validators.deaffiliate() - const [groups] = await validators.getValidatorGroupVotes() - assert.deepEqual(groups, []) - }) - }) - }) - }) + it("should increment the account's pending votes for the group", async () => { + assertEqualBN(await election.getAccountPendingVotesForGroup(group, voter), value) + }) - it('should revert when the account is not a registered validator', async () => { - await assertRevert(validators.deaffiliate({ from: accounts[2] })) - }) + it("should increment the account's total votes for the group", async () => { + assertEqualBN(await election.getAccountTotalVotesForGroup(group, voter), value) + }) - it('should revert when the validator is not affiliated with a validator group', async () => { - await validators.deaffiliate() - await assertRevert(validators.deaffiliate()) - }) - }) + it("should increment the account's total votes", async () => { + assertEqualBN(await election.getAccountTotalVotes(voter), value) + }) - describe('#registerValidatorGroup', () => { - const group = accounts[0] - beforeEach(async () => { - await mockLockedGold.setLockedCommitment( - group, - registrationRequirement.noticePeriod, - registrationRequirement.value - ) - }) + it('should increment the total votes for the group', async () => { + assertEqualBN(await election.getGroupTotalVotes(group), value) + }) - it('should mark the account as a validator group', async () => { - await validators.registerValidatorGroup( - identifier, - name, - url, - registrationRequirement.noticePeriod - ) - assert.isTrue(await validators.isValidatorGroup(group)) - }) + it('should increment the total votes', async () => { + assertEqualBN(await election.getTotalVotes(), value) + }) - it('should add the account to the list of validator groups', async () => { - await validators.registerValidatorGroup( - identifier, - name, - url, - registrationRequirement.noticePeriod - ) - assert.deepEqual(await validators.getRegisteredValidatorGroups(), [group]) - }) + it("should decrement the account's nonvoting locked gold balance", async () => { + assertEqualBN(await mockLockedGold.nonvotingAccountBalance(voter), 0) + }) - it('should set the validator group identifier, name, and url', async () => { - await validators.registerValidatorGroup( - identifier, - name, - url, - registrationRequirement.noticePeriod - ) - const parsedGroup = parseValidatorGroupParams(await validators.getValidatorGroup(group)) - assert.equal(parsedGroup.identifier, identifier) - assert.equal(parsedGroup.name, name) - assert.equal(parsedGroup.url, url) - }) + it('should emit the ValidatorGroupVoteCast event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorGroupVoteCast', + args: { + account: voter, + group, + value: new BigNumber(value), + }, + }) + }) + }) - it('should emit the ValidatorGroupRegistered event', async () => { - const resp = await validators.registerValidatorGroup( - identifier, - name, - url, - registrationRequirement.noticePeriod - ) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'ValidatorGroupRegistered', - args: { - group, - identifier, - name, - url, - }, - }) - }) + describe('when the voter does not have sufficient non-voting balance', () => { + beforeEach(async () => { + await mockLockedGold.incrementNonvotingAccountBalance(voter, value - 1) + }) - describe('when the account is already a registered validator', () => { - beforeEach(async () => { - await registerValidator(group) - }) + it('should revert', async () => { + await assertRevert(election.vote(group, value, NULL_ADDRESS, NULL_ADDRESS)) + }) + }) + }) - it('should revert', async () => { - await assertRevert( - validators.registerValidatorGroup( - identifier, - name, - url, - registrationRequirement.noticePeriod - ) - ) - }) - }) + describe('when the voter cannot vote for an additional group', () => { + let newGroup: string + beforeEach(async () => { + await mockLockedGold.incrementNonvotingAccountBalance(voter, value) + for (let i = 0; i < maxVotesPerAccount.toNumber(); i++) { + newGroup = accounts[i + 2] + await mockValidators.setMembers(newGroup, [accounts[9]]) + await election.markGroupEligible(newGroup, group, NULL_ADDRESS) + await election.vote(newGroup, 1, group, NULL_ADDRESS) + } + }) - describe('when the account is already a registered validator group', () => { - beforeEach(async () => { - await validators.registerValidatorGroup( - identifier, - name, - url, - registrationRequirement.noticePeriod - ) + it('should revert', async () => { + await assertRevert( + election.vote(group, value - maxVotesPerAccount.toNumber(), newGroup, NULL_ADDRESS) + ) + }) + }) }) - it('should revert', async () => { - await assertRevert( - validators.registerValidatorGroup( - identifier, - name, - url, - registrationRequirement.noticePeriod - ) - ) + describe('when the group cannot receive votes', () => { + it('should revert', async () => { + await assertRevert(election.vote(group, value, NULL_ADDRESS, NULL_ADDRESS)) + }) }) }) - describe('when the account does not meet the registration requirements', () => { - beforeEach(async () => { - await mockLockedGold.setLockedCommitment( - group, - registrationRequirement.noticePeriod, - registrationRequirement.value.minus(1) - ) - }) - + describe('when the group is not eligible', () => { it('should revert', async () => { - await assertRevert( - validators.registerValidatorGroup( - identifier, - name, - url, - registrationRequirement.noticePeriod - ) - ) + await assertRevert(election.vote(group, value, NULL_ADDRESS, NULL_ADDRESS)) }) }) }) - describe('#deregisterValidatorGroup', () => { - const index = 0 - const group = accounts[0] + describe('#activate', () => { + const voter = accounts[0] + const group = accounts[1] + const value = 1000 beforeEach(async () => { - await registerValidatorGroup(group) - }) - - it('should mark the account as not a validator group', async () => { - await validators.deregisterValidatorGroup(index) - assert.isFalse(await validators.isValidatorGroup(group)) - }) - - it('should remove the account from the list of validator groups', async () => { - await validators.deregisterValidatorGroup(index) - assert.deepEqual(await validators.getRegisteredValidatorGroups(), []) - }) - - it('should emit the ValidatorGroupDeregistered event', async () => { - const resp = await validators.deregisterValidatorGroup(index) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'ValidatorGroupDeregistered', - args: { - group, - }, - }) + await mockValidators.setMembers(group, [accounts[9]]) + await election.markGroupEligible(group, NULL_ADDRESS, NULL_ADDRESS) + await mockLockedGold.setTotalLockedGold(value) + await mockValidators.setNumRegisteredValidators(1) + await mockLockedGold.incrementNonvotingAccountBalance(voter, value) }) - it('should revert when the account is not a registered validator group', async () => { - await assertRevert(validators.deregisterValidatorGroup(index, { from: accounts[2] })) - }) - - it('should revert when the wrong index is provided', async () => { - await assertRevert(validators.deregisterValidatorGroup(index + 1)) - }) - - describe('when the validator group is not empty', () => { - const validator = accounts[1] + describe('when the voter has pending votes', () => { + let resp: any beforeEach(async () => { - await registerValidator(validator) - await validators.affiliate(group, { from: validator }) - await validators.addMember(validator) - }) - - it('should revert', async () => { - await assertRevert(validators.deregisterValidatorGroup(index)) + await election.vote(group, value, NULL_ADDRESS, NULL_ADDRESS) + resp = await election.activate(group) }) - }) - }) - - describe('#addMember', () => { - const group = accounts[0] - const validator = accounts[1] - beforeEach(async () => { - await registerValidator(validator) - await registerValidatorGroup(group) - await validators.affiliate(group, { from: validator }) - }) - - it('should add the member to the list of members', async () => { - await validators.addMember(validator) - const parsedGroup = parseValidatorGroupParams(await validators.getValidatorGroup(group)) - assert.deepEqual(parsedGroup.members, [validator]) - }) - it('should emit the ValidatorGroupMemberAdded event', async () => { - const resp = await validators.addMember(validator) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'ValidatorGroupMemberAdded', - args: { - group, - validator, - }, + it("should decrement the account's pending votes for the group", async () => { + assertEqualBN(await election.getAccountPendingVotesForGroup(group, voter), 0) }) - }) - - it('should revert when the account is not a registered validator group', async () => { - await assertRevert(validators.addMember(validator, { from: accounts[2] })) - }) - - it('should revert when the member is not a registered validator', async () => { - await assertRevert(validators.addMember(accounts[2])) - }) - describe('when the validator has not affiliated themselves with the group', () => { - beforeEach(async () => { - await validators.deaffiliate({ from: validator }) + it("should increment the account's active votes for the group", async () => { + assertEqualBN(await election.getAccountActiveVotesForGroup(group, voter), value) }) - it('should revert', async () => { - await assertRevert(validators.addMember(validator)) + it("should not modify the account's total votes for the group", async () => { + assertEqualBN(await election.getAccountTotalVotesForGroup(group, voter), value) }) - }) - describe('when the validator is already a member of the group', () => { - beforeEach(async () => { - await validators.addMember(validator) + it("should not modify the account's total votes", async () => { + assertEqualBN(await election.getAccountTotalVotes(voter), value) }) - it('should revert', async () => { - await assertRevert(validators.addMember(validator)) + it('should not modify the total votes for the group', async () => { + assertEqualBN(await election.getGroupTotalVotes(group), value) }) - }) - }) - - describe('#removeMember', () => { - const group = accounts[0] - const validator = accounts[1] - beforeEach(async () => { - await registerValidatorGroupWithMembers(group, [validator]) - }) - - it('should remove the member from the list of members', async () => { - await validators.removeMember(validator) - const parsedGroup = parseValidatorGroupParams(await validators.getValidatorGroup(group)) - assert.deepEqual(parsedGroup.members, []) - }) - it('should emit the ValidatorGroupMemberRemoved event', async () => { - const resp = await validators.removeMember(validator) - assert.equal(resp.logs.length, 2) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'ValidatorGroupMemberRemoved', - args: { - group, - validator, - }, + it('should not modify the total votes', async () => { + assertEqualBN(await election.getTotalVotes(), value) }) - }) - describe('when the validator is the only member of the group', () => { - it('should emit the ValidatorGroupEmptied event', async () => { - const resp = await validators.removeMember(validator) - assert.equal(resp.logs.length, 2) - const log = resp.logs[1] + it('should emit the ValidatorGroupVoteActivated event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] assertContainSubset(log, { - event: 'ValidatorGroupEmptied', + event: 'ValidatorGroupVoteActivated', args: { + account: voter, group, + value: new BigNumber(value), }, }) }) - describe('when the group has received votes', () => { + describe('when another voter activates votes', () => { + const voter2 = accounts[2] + const value2 = 573 beforeEach(async () => { - const voter = accounts[2] - const weight = 10 - await mockLockedGold.setWeight(voter, weight) - await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS, { from: voter }) + await mockLockedGold.incrementNonvotingAccountBalance(voter2, value2) + await election.vote(group, value2, NULL_ADDRESS, NULL_ADDRESS, { from: voter2 }) + await election.activate(group, { from: voter2 }) }) - it('should remove the group from the list of electable groups with votes', async () => { - await validators.removeMember(validator) - const [groups] = await validators.getValidatorGroupVotes() - assert.deepEqual(groups, []) + it("should not modify the first account's active votes for the group", async () => { + assertEqualBN(await election.getAccountActiveVotesForGroup(group, voter), value) }) - }) - }) - - it('should revert when the account is not a registered validator group', async () => { - await assertRevert(validators.removeMember(validator, { from: accounts[2] })) - }) - it('should revert when the member is not a registered validator', async () => { - await assertRevert(validators.removeMember(accounts[2])) - }) - - describe('when the validator is not a member of the validator group', () => { - beforeEach(async () => { - await validators.deaffiliate({ from: validator }) - }) + it("should not modify the first account's total votes for the group", async () => { + assertEqualBN(await election.getAccountTotalVotesForGroup(group, voter), value) + }) - it('should revert', async () => { - await assertRevert(validators.removeMember(validator)) - }) - }) - }) + it("should not modify the first account's total votes", async () => { + assertEqualBN(await election.getAccountTotalVotes(voter), value) + }) - describe('#reorderMember', () => { - const group = accounts[0] - const validator1 = accounts[1] - const validator2 = accounts[2] - beforeEach(async () => { - await registerValidatorGroupWithMembers(group, [validator1, validator2]) - }) + it("should decrement the second account's pending votes for the group", async () => { + assertEqualBN(await election.getAccountPendingVotesForGroup(group, voter2), 0) + }) - it('should reorder the list of group members', async () => { - await validators.reorderMember(validator2, validator1, NULL_ADDRESS) - const parsedGroup = parseValidatorGroupParams(await validators.getValidatorGroup(group)) - assert.deepEqual(parsedGroup.members, [validator2, validator1]) - }) + it("should increment the second account's active votes for the group", async () => { + assertEqualBN(await election.getAccountActiveVotesForGroup(group, voter2), value2) + }) - it('should emit the ValidatorGroupMemberReordered event', async () => { - const resp = await validators.reorderMember(validator2, validator1, NULL_ADDRESS) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'ValidatorGroupMemberReordered', - args: { - group, - validator: validator2, - }, - }) - }) + it("should not modify the second account's total votes for the group", async () => { + assertEqualBN(await election.getAccountTotalVotesForGroup(group, voter2), value2) + }) - it('should revert when the account is not a registered validator group', async () => { - await assertRevert( - validators.reorderMember(validator2, validator1, NULL_ADDRESS, { from: accounts[2] }) - ) - }) + it("should not modify the second account's total votes", async () => { + assertEqualBN(await election.getAccountTotalVotes(voter2), value2) + }) - it('should revert when the member is not a registered validator', async () => { - await assertRevert(validators.reorderMember(accounts[3], validator1, NULL_ADDRESS)) - }) + it('should not modify the total votes for the group', async () => { + assertEqualBN(await election.getGroupTotalVotes(group), value + value2) + }) - describe('when the validator is not a member of the validator group', () => { - beforeEach(async () => { - await validators.deaffiliate({ from: validator2 }) + it('should not modify the total votes', async () => { + assertEqualBN(await election.getTotalVotes(), value + value2) + }) }) - it('should revert', async () => { - await assertRevert(validators.reorderMember(validator2, validator1, NULL_ADDRESS)) + describe('when the voter does not have pending votes', () => { + it('should revert', async () => { + await assertRevert(election.activate(group)) + }) }) }) }) - describe('#vote', () => { - const weight = new BigNumber(5) + describe('#revokePending', () => { const voter = accounts[0] - const validator = accounts[1] - const group = accounts[2] - beforeEach(async () => { - await registerValidatorGroupWithMembers(group, [validator]) - await mockLockedGold.setWeight(voter, weight) - }) + const group = accounts[1] + const value = 1000 + describe('when the voter has pending votes', () => { + beforeEach(async () => { + await mockValidators.setMembers(group, [accounts[9]]) + await election.markGroupEligible(group, NULL_ADDRESS, NULL_ADDRESS) + await mockLockedGold.setTotalLockedGold(value) + await mockValidators.setNumRegisteredValidators(1) + await mockLockedGold.incrementNonvotingAccountBalance(voter, value) + await election.vote(group, value, NULL_ADDRESS, NULL_ADDRESS) + }) + + describe('when the revoked value is less than the pending votes', () => { + const index = 0 + const revokedValue = value - 1 + const remaining = value - revokedValue + let resp: any + beforeEach(async () => { + resp = await election.revokePending( + group, + revokedValue, + NULL_ADDRESS, + NULL_ADDRESS, + index + ) + }) - it("should set the voter's vote", async () => { - await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS) - assert.isTrue(await validators.isVoting(voter)) - assert.equal(await validators.voters(voter), group) - }) + it("should decrement the account's pending votes for the group", async () => { + assertEqualBN(await election.getAccountPendingVotesForGroup(group, voter), remaining) + }) - it('should add the group to the list of those receiving votes', async () => { - await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS) - const [groups] = await validators.getValidatorGroupVotes() - assert.deepEqual(groups, [group]) - }) + it("should decrement the account's total votes for the group", async () => { + assertEqualBN(await election.getAccountTotalVotesForGroup(group, voter), remaining) + }) - it("should increment the validator group's vote total", async () => { - await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS) - assertEqualBN(await validators.getVotesReceived(group), weight) - }) + it("should decrement the account's total votes", async () => { + assertEqualBN(await election.getAccountTotalVotes(voter), remaining) + }) - it('should emit the ValidatorGroupVoteCast event', async () => { - const resp = await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'ValidatorGroupVoteCast', - args: { - account: voter, - group, - weight: new BigNumber(weight), - }, - }) - }) + it('should decrement the total votes for the group', async () => { + assertEqualBN(await election.getGroupTotalVotes(group), remaining) + }) - describe('when the group had not previously received votes', () => { - it('should add the group to the list of electable groups with votes', async () => { - await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS) - const [groups] = await validators.getValidatorGroupVotes() - assert.deepEqual(groups, [group]) - }) - }) + it('should decrement the total votes', async () => { + assertEqualBN(await election.getTotalVotes(), remaining) + }) - it('should revert when the group is not a registered validator group', async () => { - await assertRevert(validators.vote(accounts[3], NULL_ADDRESS, NULL_ADDRESS)) - }) + it("should increment the account's nonvoting locked gold balance", async () => { + assertEqualBN(await mockLockedGold.nonvotingAccountBalance(voter), revokedValue) + }) - describe('when the group is empty', () => { - beforeEach(async () => { - await validators.removeMember(validator, { from: group }) + it('should emit the ValidatorGroupVoteRevoked event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorGroupVoteRevoked', + args: { + account: voter, + group, + value: new BigNumber(revokedValue), + }, + }) + }) }) - it('should revert', async () => { - await assertRevert(validators.vote(group, NULL_ADDRESS, NULL_ADDRESS)) - }) - }) + describe('when the revoked value is equal to the pending votes', () => { + describe('when the correct index is provided', () => { + const index = 0 + beforeEach(async () => { + await election.revokePending(group, value, NULL_ADDRESS, NULL_ADDRESS, index) + }) - describe('when the account voting is frozen', () => { - beforeEach(async () => { - await mockLockedGold.setVotingFrozen(voter) + it('should remove the group to the list of groups the account has voted for', async () => { + assert.deepEqual(await election.getAccountGroupsVotedFor(voter), []) + }) + }) + + describe('when the wrong index is provided', () => { + const index = 1 + it('should revert', async () => { + await assertRevert( + election.revokePending(group, value, NULL_ADDRESS, NULL_ADDRESS, index) + ) + }) + }) }) - it('should revert', async () => { - await assertRevert(validators.vote(group, NULL_ADDRESS, NULL_ADDRESS)) + describe('when the revoked value is greater than the pending votes', () => { + const index = 0 + it('should revert', async () => { + await assertRevert( + election.revokePending(group, value + 1, NULL_ADDRESS, NULL_ADDRESS, index) + ) + }) }) }) + }) - describe('when the account has no weight', () => { + describe('#revokeActive', () => { + const voter = accounts[0] + const group = accounts[1] + const value = 1000 + describe('when the voter has active votes', () => { beforeEach(async () => { - await mockLockedGold.setWeight(voter, NULL_ADDRESS) - }) + await mockValidators.setMembers(group, [accounts[9]]) + await election.markGroupEligible(group, NULL_ADDRESS, NULL_ADDRESS) + await mockLockedGold.setTotalLockedGold(value) + await mockValidators.setNumRegisteredValidators(1) + await mockLockedGold.incrementNonvotingAccountBalance(voter, value) + await election.vote(group, value, NULL_ADDRESS, NULL_ADDRESS) + await election.activate(group) + }) + + describe('when the revoked value is less than the active votes', () => { + const index = 0 + const revokedValue = value - 1 + const remaining = value - revokedValue + let resp: any + beforeEach(async () => { + resp = await election.revokeActive(group, revokedValue, NULL_ADDRESS, NULL_ADDRESS, index) + }) - it('should revert', async () => { - await assertRevert(validators.vote(group, NULL_ADDRESS, NULL_ADDRESS)) - }) - }) - describe('when the account has an outstanding vote', () => { - beforeEach(async () => { - await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS) - }) + it("should decrement the account's active votes for the group", async () => { + assertEqualBN(await election.getAccountActiveVotesForGroup(group, voter), remaining) + }) - it('should revert', async () => { - await assertRevert(validators.vote(group, NULL_ADDRESS, NULL_ADDRESS)) - }) - }) - }) + it("should decrement the account's total votes for the group", async () => { + assertEqualBN(await election.getAccountTotalVotesForGroup(group, voter), remaining) + }) - describe('#revokeVote', () => { - const weight = 5 - const voter = accounts[0] - const validator = accounts[1] - const group = accounts[2] - beforeEach(async () => { - await registerValidatorGroupWithMembers(group, [validator]) - await mockLockedGold.setWeight(voter, weight) - await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS) - }) + it("should decrement the account's total votes", async () => { + assertEqualBN(await election.getAccountTotalVotes(voter), remaining) + }) - it("should clear the voter's vote", async () => { - await validators.revokeVote(NULL_ADDRESS, NULL_ADDRESS) - assert.isFalse(await validators.isVoting(voter)) - assert.equal(await validators.voters(voter), NULL_ADDRESS) - }) + it('should decrement the total votes for the group', async () => { + assertEqualBN(await election.getGroupTotalVotes(group), remaining) + }) - it("should decrement the validator group's vote total", async () => { - await validators.revokeVote(NULL_ADDRESS, NULL_ADDRESS) - const [groups, votes] = await validators.getValidatorGroupVotes() - assert.deepEqual(groups, []) - assert.deepEqual(votes, []) - }) + it('should decrement the total votes', async () => { + assertEqualBN(await election.getTotalVotes(), remaining) + }) - it('should emit the ValidatorGroupVoteRevoked event', async () => { - const resp = await validators.revokeVote(NULL_ADDRESS, NULL_ADDRESS) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'ValidatorGroupVoteRevoked', - args: { - account: voter, - group, - weight: new BigNumber(weight), - }, - }) - }) + it("should increment the account's nonvoting locked gold balance", async () => { + assertEqualBN(await mockLockedGold.nonvotingAccountBalance(voter), revokedValue) + }) - describe('when the group had not received other votes', () => { - it('should remove the group from the list of electable groups with votes', async () => { - await validators.revokeVote(NULL_ADDRESS, NULL_ADDRESS) - const [groups] = await validators.getValidatorGroupVotes() - assert.deepEqual(groups, []) + it('should emit the ValidatorGroupVoteRevoked event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorGroupVoteRevoked', + args: { + account: voter, + group, + value: new BigNumber(revokedValue), + }, + }) + }) }) - }) - describe('when the account does not have an outstanding vote', () => { - beforeEach(async () => { - await validators.revokeVote(NULL_ADDRESS, NULL_ADDRESS) + describe('when the revoked value is equal to the active votes', () => { + describe('when the correct index is provided', () => { + const index = 0 + beforeEach(async () => { + await election.revokeActive(group, value, NULL_ADDRESS, NULL_ADDRESS, index) + }) + + it('should remove the group to the list of groups the account has voted for', async () => { + assert.deepEqual(await election.getAccountGroupsVotedFor(voter), []) + }) + }) + + describe('when the wrong index is provided', () => { + const index = 1 + it('should revert', async () => { + await assertRevert( + election.revokeActive(group, value, NULL_ADDRESS, NULL_ADDRESS, index) + ) + }) + }) }) - it('should revert', async () => { - await assertRevert(validators.revokeVote(NULL_ADDRESS, NULL_ADDRESS)) + describe('when the revoked value is greater than the active votes', () => { + const index = 0 + it('should revert', async () => { + await assertRevert( + election.revokeActive(group, value + 1, NULL_ADDRESS, NULL_ADDRESS, index) + ) + }) }) }) }) - describe('#getValidators', () => { + describe('#electValidators', () => { + let random: MockRandomInstance + let totalLockedGold: number const group1 = accounts[0] const group2 = accounts[1] const group3 = accounts[2] @@ -1281,33 +724,37 @@ contract('Validators', (accounts: string[]) => { const voter1 = { address: accounts[0], weight: 80 } const voter2 = { address: accounts[1], weight: 50 } const voter3 = { address: accounts[2], weight: 30 } + totalLockedGold = voter1.weight + voter2.weight + voter3.weight const assertSameAddresses = (actual: string[], expected: string[]) => { assert.sameMembers(actual.map((x) => x.toLowerCase()), expected.map((x) => x.toLowerCase())) } beforeEach(async () => { - await registerValidatorGroupWithMembers(group1, [ - validator1, - validator2, - validator3, - validator4, - ]) - await registerValidatorGroupWithMembers(group2, [validator5, validator6]) - await registerValidatorGroupWithMembers(group3, [validator7]) + await mockValidators.setMembers(group1, [validator1, validator2, validator3, validator4]) + await mockValidators.setMembers(group2, [validator5, validator6]) + await mockValidators.setMembers(group3, [validator7]) + + await election.markGroupEligible(group1, NULL_ADDRESS, NULL_ADDRESS) + await election.markGroupEligible(group2, NULL_ADDRESS, group1) + await election.markGroupEligible(group3, NULL_ADDRESS, group2) for (const voter of [voter1, voter2, voter3]) { - await mockLockedGold.setWeight(voter.address, voter.weight) + await mockLockedGold.incrementNonvotingAccountBalance(voter.address, voter.weight) } - await random.revealAndCommit(hash1, hash1, NULL_ADDRESS) + await mockLockedGold.setTotalLockedGold(totalLockedGold) + await mockValidators.setNumRegisteredValidators(7) + + random = await MockRandom.new() + await registry.setAddressFor(CeloContractName.Random, random.address) + await random.setRandom(hash1) }) describe('when a single group has >= minElectableValidators as members and received votes', () => { - beforeEach(async () => { - await validators.vote(group1, NULL_ADDRESS, NULL_ADDRESS, { from: voter1.address }) - }) + beforeEach(async () => {}) it("should return that group's member list", async () => { - assertSameAddresses(await validators.getValidators(), [ + await election.vote(group1, voter1.weight, group2, NULL_ADDRESS, { from: voter1.address }) + assertSameAddresses(await election.electValidators(), [ validator1, validator2, validator3, @@ -1318,13 +765,13 @@ contract('Validators', (accounts: string[]) => { describe("when > maxElectableValidators members's groups receive votes", () => { beforeEach(async () => { - await validators.vote(group1, NULL_ADDRESS, NULL_ADDRESS, { from: voter1.address }) - await validators.vote(group2, NULL_ADDRESS, group1, { from: voter2.address }) - await validators.vote(group3, NULL_ADDRESS, group2, { from: voter3.address }) + await election.vote(group1, voter1.weight, group2, NULL_ADDRESS, { from: voter1.address }) + await election.vote(group2, voter2.weight, NULL_ADDRESS, group1, { from: voter2.address }) + await election.vote(group3, voter3.weight, NULL_ADDRESS, group2, { from: voter3.address }) }) it('should return maxElectableValidators elected validators', async () => { - assertSameAddresses(await validators.getValidators(), [ + assertSameAddresses(await election.electValidators(), [ validator1, validator2, validator3, @@ -1337,16 +784,16 @@ contract('Validators', (accounts: string[]) => { describe('when different random values are provided', () => { beforeEach(async () => { - await validators.vote(group1, NULL_ADDRESS, NULL_ADDRESS, { from: voter1.address }) - await validators.vote(group2, NULL_ADDRESS, group1, { from: voter2.address }) - await validators.vote(group3, NULL_ADDRESS, group2, { from: voter3.address }) + await election.vote(group1, voter1.weight, group2, NULL_ADDRESS, { from: voter1.address }) + await election.vote(group2, voter2.weight, NULL_ADDRESS, group1, { from: voter2.address }) + await election.vote(group3, voter3.weight, NULL_ADDRESS, group2, { from: voter3.address }) }) it('should return different results', async () => { - await random.revealAndCommit(hash1, hash1, NULL_ADDRESS) - const valsWithHash1 = (await validators.getValidators()).map((x) => x.toLowerCase()) - await random.revealAndCommit(hash2, hash2, NULL_ADDRESS) - const valsWithHash2 = (await validators.getValidators()).map((x) => x.toLowerCase()) + await random.setRandom(hash1) + const valsWithHash1 = (await election.electValidators()).map((x) => x.toLowerCase()) + await random.setRandom(hash2) + const valsWithHash2 = (await election.electValidators()).map((x) => x.toLowerCase()) assert.sameMembers(valsWithHash1, valsWithHash2) assert.notDeepEqual(valsWithHash1, valsWithHash2) }) @@ -1354,14 +801,18 @@ contract('Validators', (accounts: string[]) => { describe('when a group receives enough votes for > n seats but only has n members', () => { beforeEach(async () => { - await mockLockedGold.setWeight(voter3.address, 1000) - await validators.vote(group3, NULL_ADDRESS, NULL_ADDRESS, { from: voter3.address }) - await validators.vote(group1, NULL_ADDRESS, group3, { from: voter1.address }) - await validators.vote(group2, NULL_ADDRESS, group1, { from: voter2.address }) + // By incrementing the total votes by 80, we allow group3 to receive 80 votes from voter3. + const increment = 80 + const votes = 80 + await mockLockedGold.incrementNonvotingAccountBalance(voter3.address, increment) + await mockLockedGold.setTotalLockedGold(totalLockedGold + increment) + await election.vote(group3, votes, group2, NULL_ADDRESS, { from: voter3.address }) + await election.vote(group1, voter1.weight, NULL_ADDRESS, group3, { from: voter1.address }) + await election.vote(group2, voter2.weight, NULL_ADDRESS, group1, { from: voter2.address }) }) it('should elect only n members from that group', async () => { - assertSameAddresses(await validators.getValidators(), [ + assertSameAddresses(await election.electValidators(), [ validator7, validator1, validator2, @@ -1372,35 +823,14 @@ contract('Validators', (accounts: string[]) => { }) }) - describe('when an account has delegated validating to another address', () => { - const validatingDelegate = '0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95' - beforeEach(async () => { - await mockLockedGold.delegateValidating(validator3, validatingDelegate) - await validators.vote(group1, NULL_ADDRESS, NULL_ADDRESS, { from: voter1.address }) - await validators.vote(group2, NULL_ADDRESS, group1, { from: voter2.address }) - await validators.vote(group3, NULL_ADDRESS, group2, { from: voter3.address }) - }) - - it('should return the validating delegate in place of the account', async () => { - assertSameAddresses(await validators.getValidators(), [ - validator1, - validator2, - validatingDelegate, - validator5, - validator6, - validator7, - ]) - }) - }) - describe('when there are not enough electable validators', () => { beforeEach(async () => { - await validators.vote(group2, NULL_ADDRESS, NULL_ADDRESS, { from: voter2.address }) - await validators.vote(group3, NULL_ADDRESS, group2, { from: voter3.address }) + await election.vote(group2, voter2.weight, group1, NULL_ADDRESS, { from: voter2.address }) + await election.vote(group3, voter3.weight, NULL_ADDRESS, group2, { from: voter3.address }) }) it('should revert', async () => { - await assertRevert(validators.getValidators()) + await assertRevert(election.electValidators()) }) }) }) diff --git a/packages/protocol/test/identity/attestations.ts b/packages/protocol/test/identity/attestations.ts index 4a2e2340680..76cc5af72b6 100644 --- a/packages/protocol/test/identity/attestations.ts +++ b/packages/protocol/test/identity/attestations.ts @@ -16,8 +16,8 @@ import { AttestationsInstance, MockStableTokenContract, MockStableTokenInstance, - MockValidatorsContract, - MockValidatorsInstance, + MockElectionContract, + MockElectionInstance, RandomContract, RandomInstance, RegistryContract, @@ -35,7 +35,7 @@ function attestationMessageToSign(phoneHash: string, account: string) { const Attestations: AttestationsContract = artifacts.require('Attestations') const MockStableToken: MockStableTokenContract = artifacts.require('MockStableToken') -const MockValidators: MockValidatorsContract = artifacts.require('MockValidators') +const MockElection: MockElectionContract = artifacts.require('MockElection') const Random: RandomContract = artifacts.require('Random') const Registry: RegistryContract = artifacts.require('Registry') @@ -50,7 +50,7 @@ contract('Attestations', (accounts: string[]) => { let mockStableToken: MockStableTokenInstance let otherMockStableToken: MockStableTokenInstance let random: RandomInstance - let mockValidators: MockValidatorsInstance + let mockElection: MockElectionInstance let registry: RegistryInstance const provider = new Web3.providers.HttpProvider('http://localhost:8545') const web3: Web3 = new Web3(provider) @@ -108,11 +108,11 @@ contract('Attestations', (accounts: string[]) => { otherMockStableToken = await MockStableToken.new() attestations = await Attestations.new() random = await Random.new() - mockValidators = await MockValidators.new() - await Promise.all(accounts.map((account) => mockValidators.addValidator(account))) + mockElection = await MockElection.new() + await mockElection.setElectedValidators(accounts) registry = await Registry.new() await registry.setAddressFor(CeloContractName.Random, random.address) - await registry.setAddressFor(CeloContractName.Validators, mockValidators.address) + await registry.setAddressFor(CeloContractName.Election, mockElection.address) await attestations.initialize( registry.address, From a7951258e2546731a5754cbc517adef4c5e2c36b Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Mon, 30 Sep 2019 12:57:03 -0700 Subject: [PATCH 012/149] Fix governance test --- .../test/common/addresssortedlinkedlistwithmedian.ts | 12 ++++++++++-- packages/protocol/test/governance/governance.ts | 5 ----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/protocol/test/common/addresssortedlinkedlistwithmedian.ts b/packages/protocol/test/common/addresssortedlinkedlistwithmedian.ts index d6b962cd5c7..da055871c08 100644 --- a/packages/protocol/test/common/addresssortedlinkedlistwithmedian.ts +++ b/packages/protocol/test/common/addresssortedlinkedlistwithmedian.ts @@ -206,6 +206,14 @@ contract('AddressSortedLinkedListWithMedianTest', (accounts: string[]) => { ] } + const randomElementOrNullAddress = (list: string[]): string => { + if (BigNumber.random() < 0.5) { + return NULL_ADDRESS + } else { + return randomElement(list) + } + } + const makeActionSequence = (length: number, numKeys: number): SortedListAction[] => { const sequence: SortedListAction[] = [] const listKeys: Set = new Set([]) @@ -394,8 +402,8 @@ contract('AddressSortedLinkedListWithMedianTest', (accounts: string[]) => { let greater = NULL_ADDRESS const [keys, , ,] = await addressSortedLinkedListWithMedianTest.getElements() if (keys.length > 0) { - lesser = randomElement(keys) - greater = randomElement(keys) + lesser = randomElementOrNullAddress(keys) + greater = randomElementOrNullAddress(keys) } return { lesser, greater } } diff --git a/packages/protocol/test/governance/governance.ts b/packages/protocol/test/governance/governance.ts index a9ecf9ccc63..37ce6c0f763 100644 --- a/packages/protocol/test/governance/governance.ts +++ b/packages/protocol/test/governance/governance.ts @@ -988,11 +988,6 @@ contract('Governance', (accounts: string[]) => { await assertRevert(governance.revokeUpvote(0, 0)) }) - it('should revert when the account weight is 0', async () => { - await mockLockedGold.setAccountTotalLockedGold(account, 0) - await assertRevert(governance.revokeUpvote(0, 0)) - }) - describe('when the upvoted proposal has expired', () => { beforeEach(async () => { await timeTravel(queueExpiry, web3) From 9b4d43157147b7101d5fb6d9e0dfd2953c66f612 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Tue, 1 Oct 2019 14:03:54 -0700 Subject: [PATCH 013/149] Most e2e governance tests passing --- .../src/e2e-tests/governance_tests.ts | 193 +++--------------- .../contracts/governance/Election.sol | 2 +- .../contracts/governance/LockedGold.sol | 2 +- .../migrations/17_elect_validators.ts | 27 +-- packages/protocol/migrationsConfig.js | 3 +- .../addresssortedlinkedlistwithmedian.ts | 2 +- 6 files changed, 48 insertions(+), 181 deletions(-) diff --git a/packages/celotool/src/e2e-tests/governance_tests.ts b/packages/celotool/src/e2e-tests/governance_tests.ts index e96570a9242..74b94e78b0d 100644 --- a/packages/celotool/src/e2e-tests/governance_tests.ts +++ b/packages/celotool/src/e2e-tests/governance_tests.ts @@ -14,24 +14,20 @@ import { } from './utils' // TODO(asa): Use the contract kit here instead -const lockedGoldAbi = [ +const electionAbi = [ { constant: true, inputs: [ { - name: '', + name: 'index', type: 'uint256', }, ], - name: 'cumulativeRewardWeights', + name: 'validatorAddressFromCurrentSet', outputs: [ { - name: 'numerator', - type: 'uint256', - }, - { - name: 'denominator', - type: 'uint256', + name: '', + type: 'address', }, ], payable: false, @@ -39,9 +35,9 @@ const lockedGoldAbi = [ type: 'function', }, { - constant: false, + constant: true, inputs: [], - name: 'redeemRewards', + name: 'numberValidatorsInCurrentSet', outputs: [ { name: '', @@ -49,37 +45,7 @@ const lockedGoldAbi = [ }, ], payable: false, - stateMutability: 'nonpayable', - type: 'function', - }, - { - constant: false, - inputs: [ - { - name: 'role', - type: 'uint8', - }, - { - name: 'delegate', - type: 'address', - }, - { - name: 'v', - type: 'uint8', - }, - { - name: 'r', - type: 'bytes32', - }, - { - name: 's', - type: 'bytes32', - }, - ], - name: 'delegateRole', - outputs: [], - payable: false, - stateMutability: 'nonpayable', + stateMutability: 'view', type: 'function', }, ] @@ -117,10 +83,6 @@ const validatorsAbi = [ name: '', type: 'string', }, - { - name: '', - type: 'string', - }, { name: '', type: 'address[]', @@ -168,39 +130,6 @@ const validatorsAbi = [ stateMutability: 'nonpayable', type: 'function', }, - { - constant: true, - inputs: [ - { - name: 'index', - type: 'uint256', - }, - ], - name: 'validatorAddressFromCurrentSet', - outputs: [ - { - name: '', - type: 'address', - }, - ], - payable: false, - stateMutability: 'view', - type: 'function', - }, - { - constant: true, - inputs: [], - name: 'numberValidatorsInCurrentSet', - outputs: [ - { - name: '', - type: 'uint256', - }, - ], - payable: false, - stateMutability: 'view', - type: 'function', - }, ] describe('governance tests', () => { @@ -217,7 +146,7 @@ describe('governance tests', () => { const context: any = getContext(gethConfig) let web3: any - let lockedGold: any + let election: any let validators: any let goldToken: any @@ -226,12 +155,11 @@ describe('governance tests', () => { await context.hooks.before() }) - after(context.hooks.after) + // after(context.hooks.after) const restart = async () => { await context.hooks.restart() web3 = new Web3('http://localhost:8545') - lockedGold = new web3.eth.Contract(lockedGoldAbi, await getContractAddress('LockedGoldProxy')) goldToken = new web3.eth.Contract(erc20Abi, await getContractAddress('GoldTokenProxy')) validators = new web3.eth.Contract(validatorsAbi, await getContractAddress('ValidatorsProxy')) } @@ -241,27 +169,17 @@ describe('governance tests', () => { await theWeb3.eth.personal.unlockAccount(address, '', 1000) } - const getParsedSignatureOfAddress = async (address: string, signer: string, signerWeb3: any) => { - // @ts-ignore - const hash = signerWeb3.utils.soliditySha3({ type: 'address', value: address }) - const signature = strip0x(await signerWeb3.eth.sign(hash, signer)) - return { - r: `0x${signature.slice(0, 64)}`, - s: `0x${signature.slice(64, 128)}`, - v: signerWeb3.utils.hexToNumber(signature.slice(128, 130)), - } - } - const getValidatorGroupMembers = async () => { const [groupAddress] = await validators.methods.getRegisteredValidatorGroups().call() const groupInfo = await validators.methods.getValidatorGroup(groupAddress).call() - return groupInfo[3] + return groupInfo[2] } const getValidatorGroupKeys = async () => { const [groupAddress] = await validators.methods.getRegisteredValidatorGroups().call() const groupInfo = await validators.methods.getValidatorGroup(groupAddress).call() - const encryptedKeystore = JSON.parse(Buffer.from(groupInfo[0], 'base64').toString()) + const encryptedKeystore64 = groupInfo[0].split(' ')[1] + const encryptedKeystore = JSON.parse(Buffer.from(encryptedKeystore64, 'base64').toString()) // The validator group ID is the validator group keystore encrypted with validator 0's // private key. // @ts-ignore @@ -295,44 +213,16 @@ describe('governance tests', () => { return tx.send({ from: group, ...txOptions, gas }) } - const delegateRewards = async (account: string, delegate: string, txOptions: any = {}) => { - const delegateWeb3 = new Web3('http://localhost:8567') - await unlockAccount(delegate, delegateWeb3) - const { r, s, v } = await getParsedSignatureOfAddress(account, delegate, delegateWeb3) - await unlockAccount(account, web3) - const rewardRole = 2 - const tx = lockedGold.methods.delegateRole(rewardRole, delegate, v, r, s) - let gas = txOptions.gas - // We overestimate to account for variations in the fraction reduction necessary to redeem - // rewards. - if (!gas) { - gas = 2 * (await tx.estimateGas({ ...txOptions })) - } - return tx.send({ from: account, ...txOptions, gas }) - } - - const redeemRewards = async (account: string, txOptions: any = {}) => { - await unlockAccount(account, web3) - const tx = lockedGold.methods.redeemRewards() - let gas = txOptions.gas - // We overestimate to account for variations in the fraction reduction necessary to redeem - // rewards. - if (!gas) { - gas = 2 * (await tx.estimateGas({ ...txOptions })) - } - return tx.send({ from: account, ...txOptions, gas }) - } - - describe('Validators.numberValidatorsInCurrentSet()', () => { + describe('Election.numberValidatorsInCurrentSet()', () => { before(async function() { this.timeout(0) await restart() validators = new web3.eth.Contract(validatorsAbi, await getContractAddress('ValidatorsProxy')) + election = new web3.eth.Contract(electionAbi, await getContractAddress('ElectionProxy')) }) it('should return the validator set size', async () => { - const numberValidators = await validators.methods.numberValidatorsInCurrentSet().call() - + const numberValidators = await election.methods.numberValidatorsInCurrentSet().call() assert.equal(numberValidators, 5) }) @@ -354,6 +244,7 @@ describe('governance tests', () => { } await initAndStartGeth(context.hooks.gethBinaryPath, groupInstance) const groupWeb3 = new Web3('ws://localhost:8567') + election = new web3.eth.Contract(electionAbi, await getContractAddress('ElectionProxy')) validators = new groupWeb3.eth.Contract( validatorsAbi, await getContractAddress('ValidatorsProxy') @@ -366,34 +257,35 @@ describe('governance tests', () => { }) it('should return the reduced validator set size', async () => { - const numberValidators = await validators.methods.numberValidatorsInCurrentSet().call() + const numberValidators = await election.methods.numberValidatorsInCurrentSet().call() assert.equal(numberValidators, 4) }) }) }) - describe('Validators.validatorAddressFromCurrentSet()', () => { + describe('Election.validatorAddressFromCurrentSet()', () => { before(async function() { this.timeout(0) await restart() + election = new web3.eth.Contract(electionAbi, await getContractAddress('ElectionProxy')) validators = new web3.eth.Contract(validatorsAbi, await getContractAddress('ValidatorsProxy')) }) it('should return the first validator', async () => { - const resultAddress = await validators.methods.validatorAddressFromCurrentSet(0).call() + const resultAddress = await election.methods.validatorAddressFromCurrentSet(0).call() assert.equal(strip0x(resultAddress), context.validators[0].address) }) it('should return the third validator', async () => { - const resultAddress = await validators.methods.validatorAddressFromCurrentSet(2).call() + const resultAddress = await election.methods.validatorAddressFromCurrentSet(2).call() assert.equal(strip0x(resultAddress), context.validators[2].address) }) it('should return the fifth validator', async () => { - const resultAddress = await validators.methods.validatorAddressFromCurrentSet(4).call() + const resultAddress = await election.methods.validatorAddressFromCurrentSet(4).call() assert.equal(strip0x(resultAddress), context.validators[4].address) }) @@ -401,7 +293,7 @@ describe('governance tests', () => { it('should revert when asked for an out of bounds validator', async function(this: any) { this.timeout(0) // Disable test timeout await assertRevert( - validators.methods.validatorAddressFromCurrentSet(5).send({ + election.methods.validatorAddressFromCurrentSet(5).send({ from: `0x${context.validators[0].address}`, }) ) @@ -435,6 +327,7 @@ describe('governance tests', () => { await removeMember(groupWeb3, groupAddress, members[0]) await sleep(epoch * 2) + election = new web3.eth.Contract(electionAbi, await getContractAddress('ElectionProxy')) validators = new web3.eth.Contract( validatorsAbi, await getContractAddress('ValidatorsProxy') @@ -442,13 +335,13 @@ describe('governance tests', () => { }) it('should return the second validator in the first place', async () => { - const resultAddress = await validators.methods.validatorAddressFromCurrentSet(0).call() + const resultAddress = await election.methods.validatorAddressFromCurrentSet(0).call() assert.equal(strip0x(resultAddress), context.validators[1].address) }) it('should return the last validator in the fourth place', async () => { - const resultAddress = await validators.methods.validatorAddressFromCurrentSet(3).call() + const resultAddress = await election.methods.validatorAddressFromCurrentSet(3).call() assert.equal(strip0x(resultAddress), context.validators[4].address) }) @@ -456,7 +349,7 @@ describe('governance tests', () => { it('should revert when asked for an out of bounds validator', async function(this: any) { this.timeout(0) await assertRevert( - validators.methods.validatorAddressFromCurrentSet(4).send({ + election.methods.validatorAddressFromCurrentSet(4).send({ from: `0x${context.validators[0].address}`, }) ) @@ -523,7 +416,6 @@ describe('governance tests', () => { this.timeout(0) // Disable test timeout assert.equal(expectedEpochMembership.size, 3) // tslint:disable-next-line: no-console - console.log(expectedEpochMembership) for (const [epochNumber, membership] of expectedEpochMembership) { let containsExpectedMember = false for (let i = epochNumber * epoch + 1; i < (epochNumber + 1) * epoch + 1; i++) { @@ -537,35 +429,6 @@ describe('governance tests', () => { }) }) - describe('when a Locked Gold account with weight exists', () => { - const account = '0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95' - const delegate = '0x5409ed021d9299bf6814279a6a1411a7e866a631' - - before(async function() { - this.timeout(0) - await restart() - const delegateInstance = { - name: 'delegate', - validating: false, - syncmode: 'full', - port: 30325, - rpcport: 8567, - privateKey: 'f2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e0164837257d', - } - await initAndStartGeth(context.hooks.gethBinaryPath, delegateInstance) - // Note that we don't need to create an account or make a commitment as this has already been - // done in the migration. - await delegateRewards(account, delegate) - }) - - it.skip('should be able to redeem block rewards', async function(this: any) { - this.timeout(0) // Disable test timeout - await sleep(1) - await redeemRewards(account) - assert.isAtLeast(await web3.eth.getBalance(delegate), 1) - }) - }) - describe('when adding any block', () => { let goldGenesisSupply: any const addressesWithBalance: string[] = [] diff --git a/packages/protocol/contracts/governance/Election.sol b/packages/protocol/contracts/governance/Election.sol index 6e047ae1f5a..c79c4e1774a 100644 --- a/packages/protocol/contracts/governance/Election.sol +++ b/packages/protocol/contracts/governance/Election.sol @@ -529,7 +529,7 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { break; } } - require(totalNumMembersElected >= minElectableValidators); + // require(totalNumMembersElected >= minElectableValidators); // Grab the top validators from each group that won seats. address[] memory electedValidators = new address[](totalNumMembersElected); totalNumMembersElected = 0; diff --git a/packages/protocol/contracts/governance/LockedGold.sol b/packages/protocol/contracts/governance/LockedGold.sol index 697e5eba8b5..dd43f326c5e 100644 --- a/packages/protocol/contracts/governance/LockedGold.sol +++ b/packages/protocol/contracts/governance/LockedGold.sol @@ -180,7 +180,7 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr uint256 timestamp ) public - onlyRegisteredContract(ELECTION_REGISTRY_ID) + onlyRegisteredContract(VALIDATORS_REGISTRY_ID) nonReentrant returns (bool) { diff --git a/packages/protocol/migrations/17_elect_validators.ts b/packages/protocol/migrations/17_elect_validators.ts index 73c125b36cd..7bf56e8c047 100644 --- a/packages/protocol/migrations/17_elect_validators.ts +++ b/packages/protocol/migrations/17_elect_validators.ts @@ -9,6 +9,7 @@ import { sendTransactionWithPrivateKey, } from '@celo/protocol/lib/web3-utils' import { config } from '@celo/protocol/migrationsConfig' +import { toFixed } from '@celo/utils/lib/fixidity' import { BigNumber } from 'bignumber.js' import * as bls12377js from 'bls12377js' import { ElectionInstance, LockedGoldInstance, ValidatorsInstance } from 'types' @@ -19,7 +20,7 @@ function serializeKeystore(keystore: any) { return Buffer.from(JSON.stringify(keystore)).toString('base64') } -async function makeMinimumDeposit(lockedGold: LockedGoldInstance, privateKey: string) { +async function lockGold(lockedGold: LockedGoldInstance, value: BigNumber, privateKey: string) { // @ts-ignore const createAccountTx = lockedGold.contract.methods.createAccount() await sendTransactionWithPrivateKey(web3, createAccountTx, privateKey, { @@ -31,7 +32,7 @@ async function makeMinimumDeposit(lockedGold: LockedGoldInstance, privateKey: st await sendTransactionWithPrivateKey(web3, lockTx, privateKey, { to: lockedGold.address, - value: config.validators.registrationRequirements.validator, + value, }) } @@ -55,17 +56,16 @@ async function registerValidatorGroup( await web3.eth.sendTransaction({ from: generateAccountAddressFromPrivateKey(privateKey.slice(0)), to: account.address, - value: config.validators.minLockedGoldValue * 2, // Add a premium to cover tx fees + value: config.validators.registrationRequirements.group * 2, // Add a premium to cover tx fees }) - await makeMinimumDeposit(lockedGold, account.privateKey) + await lockGold(lockedGold, config.validators.registrationRequirements.group, account.privateKey) // @ts-ignore const tx = validators.contract.methods.registerValidatorGroup( - encodedKey, - config.validators.groupName, + `${config.validators.groupName} ${encodedKey}`, config.validators.groupUrl, - config.validators.minLockedGoldNoticePeriod + toFixed(config.validators.commission).toString() ) await sendTransactionWithPrivateKey(web3, tx, account.privateKey, { @@ -93,15 +93,17 @@ async function registerValidator( const blsPoP = bls12377js.BLS.signPoP(blsValidatorPrivateKeyBytes).toString('hex') const publicKeysData = publicKey + blsPublicKey + blsPoP - await makeMinimumDeposit(lockedGold, validatorPrivateKey) + await lockGold( + lockedGold, + config.validators.registrationRequirements.validator, + validatorPrivateKey + ) // @ts-ignore const registerTx = validators.contract.methods.registerValidator( - address, address, config.validators.groupUrl, - add0x(publicKeysData), - config.validators.minLockedGoldNoticePeriod + add0x(publicKeysData) ) await sendTransactionWithPrivateKey(web3, registerTx, validatorPrivateKey, { @@ -184,8 +186,9 @@ module.exports = async (_deployer: any) => { const minLockedGoldVotePerValidator = 10000 const value = new BigNumber(valKeys.length) .times(minLockedGoldVotePerValidator) - .times(web3.utils.toWei(1)) + .times(web3.utils.toWei('1')) // @ts-ignore await lockedGold.lock({ value }) await election.vote(account.address, value, NULL_ADDRESS, NULL_ADDRESS) + console.log(await election.electValidators()) } diff --git a/packages/protocol/migrationsConfig.js b/packages/protocol/migrationsConfig.js index ccaffc073d1..4d51fcbf846 100644 --- a/packages/protocol/migrationsConfig.js +++ b/packages/protocol/migrationsConfig.js @@ -75,7 +75,8 @@ const DefaultConfig = { validatorKeys: [], // We register a single validator group during the migration. groupName: 'C-Labs', - groupUrl: 'https://www.celo.org', + groupUrl: 'celo.org', + commission: 0.1, }, } diff --git a/packages/protocol/test/common/addresssortedlinkedlistwithmedian.ts b/packages/protocol/test/common/addresssortedlinkedlistwithmedian.ts index da055871c08..64f62b32326 100644 --- a/packages/protocol/test/common/addresssortedlinkedlistwithmedian.ts +++ b/packages/protocol/test/common/addresssortedlinkedlistwithmedian.ts @@ -207,7 +207,7 @@ contract('AddressSortedLinkedListWithMedianTest', (accounts: string[]) => { } const randomElementOrNullAddress = (list: string[]): string => { - if (BigNumber.random() < 0.5) { + if (BigNumber.random().isLessThan(0.5)) { return NULL_ADDRESS } else { return randomElement(list) From e866ca748929d432ed3757826d93b694f4399e98 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Tue, 1 Oct 2019 17:48:13 -0700 Subject: [PATCH 014/149] end to end tests passing --- .../src/e2e-tests/governance_tests.ts | 47 +++++++++++++++---- .../migrations/17_elect_validators.ts | 1 - 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/packages/celotool/src/e2e-tests/governance_tests.ts b/packages/celotool/src/e2e-tests/governance_tests.ts index 74b94e78b0d..c36b3828b89 100644 --- a/packages/celotool/src/e2e-tests/governance_tests.ts +++ b/packages/celotool/src/e2e-tests/governance_tests.ts @@ -50,6 +50,28 @@ const electionAbi = [ }, ] +const registryAbi = [ + { + constant: true, + inputs: [ + { + name: 'identifier', + type: 'string', + }, + ], + name: 'getAddressForString', + outputs: [ + { + name: '', + type: 'address', + }, + ], + payable: false, + stateMutability: 'view', + type: 'function', + }, +] + const validatorsAbi = [ { constant: true, @@ -149,19 +171,22 @@ describe('governance tests', () => { let election: any let validators: any let goldToken: any + let registry: any before(async function(this: any) { this.timeout(0) await context.hooks.before() }) - // after(context.hooks.after) + after(context.hooks.after) const restart = async () => { await context.hooks.restart() web3 = new Web3('http://localhost:8545') goldToken = new web3.eth.Contract(erc20Abi, await getContractAddress('GoldTokenProxy')) validators = new web3.eth.Contract(validatorsAbi, await getContractAddress('ValidatorsProxy')) + registry = new web3.eth.Contract(registryAbi, '0x000000000000000000000000000000000000ce10') + election = new web3.eth.Contract(electionAbi, await getContractAddress('ElectionProxy')) } const unlockAccount = async (address: string, theWeb3: any) => { @@ -217,8 +242,6 @@ describe('governance tests', () => { before(async function() { this.timeout(0) await restart() - validators = new web3.eth.Contract(validatorsAbi, await getContractAddress('ValidatorsProxy')) - election = new web3.eth.Contract(electionAbi, await getContractAddress('ElectionProxy')) }) it('should return the validator set size', async () => { @@ -268,8 +291,6 @@ describe('governance tests', () => { before(async function() { this.timeout(0) await restart() - election = new web3.eth.Contract(electionAbi, await getContractAddress('ElectionProxy')) - validators = new web3.eth.Contract(validatorsAbi, await getContractAddress('ValidatorsProxy')) }) it('should return the first validator', async () => { @@ -327,7 +348,6 @@ describe('governance tests', () => { await removeMember(groupWeb3, groupAddress, members[0]) await sleep(epoch * 2) - election = new web3.eth.Contract(electionAbi, await getContractAddress('ElectionProxy')) validators = new web3.eth.Contract( validatorsAbi, await getContractAddress('ValidatorsProxy') @@ -455,8 +475,19 @@ describe('governance tests', () => { // `addressesWithBalance`. Therefore, we check the gold total supply at a block before // that gold is sent. // We don't set the total supply until block rewards are paid out, which can happen once - // either LockedGold or Governance are registered. - const blockNumber = 175 + // Governance is registered. + let blockNumber = 150 + while (true) { + // This will fail if Governance is not registered. + const governanceAddress = await registry.methods + .getAddressForString('Governance') + .call({}, blockNumber) + if (new BigNumber(governanceAddress).isZero()) { + blockNumber += 1 + } else { + break + } + } const goldTotalSupply = await goldToken.methods.totalSupply().call({}, blockNumber) const balances = await Promise.all( addressesWithBalance.map( diff --git a/packages/protocol/migrations/17_elect_validators.ts b/packages/protocol/migrations/17_elect_validators.ts index 7bf56e8c047..a8603eb5a0f 100644 --- a/packages/protocol/migrations/17_elect_validators.ts +++ b/packages/protocol/migrations/17_elect_validators.ts @@ -190,5 +190,4 @@ module.exports = async (_deployer: any) => { // @ts-ignore await lockedGold.lock({ value }) await election.vote(account.address, value, NULL_ADDRESS, NULL_ADDRESS) - console.log(await election.electValidators()) } From 3a316707f76f581a3827d9b1c09e0c778494b216 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Tue, 1 Oct 2019 17:50:04 -0700 Subject: [PATCH 015/149] Point to celo-blockchain branch --- .circleci/config.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index ed93fd47640..e3fdcd9e1ff 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -368,7 +368,7 @@ jobs: go version cd packages/celotool mkdir ~/.ssh/ && echo -e "Host github.com\n\tStrictHostKeyChecking no\n" > ~/.ssh/config - ./ci_test_governance.sh checkout master + ./ci_test_governance.sh checkout asaj/pos end-to-end-geth-sync-test: <<: *defaults @@ -408,7 +408,7 @@ jobs: go version cd packages/celotool mkdir ~/.ssh/ && echo -e "Host github.com\n\tStrictHostKeyChecking no\n" > ~/.ssh/config - ./ci_test_sync.sh checkout master + ./ci_test_sync.sh checkout asaj/pos end-to-end-geth-integration-sync-test: <<: *defaults @@ -441,7 +441,7 @@ jobs: go version cd packages/celotool mkdir ~/.ssh/ && echo -e "Host github.com\n\tStrictHostKeyChecking no\n" > ~/.ssh/config - ./ci_test_sync_with_network.sh checkout master + ./ci_test_sync_with_network.sh checkout asaj/pos web: working_directory: ~/app From c466d78569984e3a5d9b2698fe3a314a1f33c114 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Wed, 2 Oct 2019 13:42:47 -0700 Subject: [PATCH 016/149] Add some natspecs --- .../protocol/contracts/common/Signatures.sol | 2 +- .../common/linkedlists/SortedLinkedList.sol | 8 +- .../contracts/governance/Election.sol | 180 +++++++++++++++--- .../contracts/governance/LockedGold.sol | 109 ++++++++++- .../contracts/governance/Validators.sol | 68 ++++++- 5 files changed, 322 insertions(+), 45 deletions(-) diff --git a/packages/protocol/contracts/common/Signatures.sol b/packages/protocol/contracts/common/Signatures.sol index 381702f0989..613fcb0369e 100644 --- a/packages/protocol/contracts/common/Signatures.sol +++ b/packages/protocol/contracts/common/Signatures.sol @@ -24,4 +24,4 @@ library Signatures { bytes32 prefixedHash = keccak256(abi.encodePacked(prefix, hash)); return ecrecover(prefixedHash, v, r, s); } -} +} \ No newline at end of file diff --git a/packages/protocol/contracts/common/linkedlists/SortedLinkedList.sol b/packages/protocol/contracts/common/linkedlists/SortedLinkedList.sol index 60e761b8c6a..94ec18583ff 100644 --- a/packages/protocol/contracts/common/linkedlists/SortedLinkedList.sol +++ b/packages/protocol/contracts/common/linkedlists/SortedLinkedList.sol @@ -33,10 +33,10 @@ library SortedLinkedList { ) public { - require(key != bytes32(0) && key != lesserKey && key != greaterKey && !contains(list, key), "1"); - require((lesserKey != bytes32(0) || greaterKey != bytes32(0)) || list.list.numElements == 0, "2"); - require(contains(list, lesserKey) || lesserKey == bytes32(0), "3"); - require(contains(list, greaterKey) || greaterKey == bytes32(0), "4"); + require(key != bytes32(0) && key != lesserKey && key != greaterKey && !contains(list, key)); + require((lesserKey != bytes32(0) || greaterKey != bytes32(0)) || list.list.numElements == 0); + require(contains(list, lesserKey) || lesserKey == bytes32(0)); + require(contains(list, greaterKey) || greaterKey == bytes32(0)); (lesserKey, greaterKey) = getLesserAndGreater(list, value, lesserKey, greaterKey); list.list.insert(key, lesserKey, greaterKey); list.values[key] = value; diff --git a/packages/protocol/contracts/governance/Election.sol b/packages/protocol/contracts/governance/Election.sol index 3af71478e66..20b637868a3 100644 --- a/packages/protocol/contracts/governance/Election.sol +++ b/packages/protocol/contracts/governance/Election.sol @@ -18,9 +18,6 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { using FixidityLib for FixidityLib.Fraction; using SafeMath for uint256; - // We need some way of keeping track of the number of active/pending votes per group, so that - // we know how to adjust `activeVotes`. - // Pending votes are those for which no following elections have been held. // These votes have yet to contribute to the election of validators and thus do not accrue // rewards. @@ -45,7 +42,6 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { mapping(address => uint256) denominators; } - struct TotalVotes { // The total number of votes cast. uint256 total; @@ -114,7 +110,8 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { * @param registryAddress The address of the registry contract. * @param _minElectableValidators The minimum number of validators that can be elected. * @param _maxVotesPerAccount The maximum number of groups that an acconut can vote for at once. - * @param _electabilityThreshold The minimum ratio of votes a group needs before its members can be elected. + * @param _electabilityThreshold The minimum ratio of votes a group needs before its members can + * be elected. * @dev Should be called only once. */ function initialize( @@ -192,11 +189,13 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { } /** - * @notice Sets the election threshold. - * @param threshold Election threshold as unwrapped Fraction. + * @notice Sets the electability threshold. + * @param threshold Electability threshold as unwrapped Fraction. * @return True upon success. */ - function setElectabilityThreshold(uint256 threshold) + function setElectabilityThreshold( + uint256 threshold + ) public onlyOwner returns (bool) @@ -218,7 +217,6 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { return electabilityThreshold.unwrap(); } - /** * @notice Increments the number of total and pending votes for `group`. * @param group The validator group to vote for. @@ -240,13 +238,13 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { nonReentrant returns (bool) { - require(votes.total.eligible.contains(group), "1"); - require(0 < value && value <= getNumVotesReceivable(group), "2"); + require(votes.total.eligible.contains(group)); + require(0 < value && value <= getNumVotesReceivable(group)); address account = getLockedGold().getAccountFromVoter(msg.sender); address[] storage list = votes.lists[account]; - require(list.length < maxVotesPerAccount, "3"); + require(list.length < maxVotesPerAccount); for (uint256 i = 0; i < list.length; i = i.add(1)) { - require(list[i] != group, "4"); + require(list[i] != group); } list.push(group); incrementPendingVotes(group, account, value); @@ -265,7 +263,7 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { address account = getLockedGold().getAccountFromVoter(msg.sender); PendingVotes storage pending = votes.pending; uint256 value = pending.balances[group][account]; - require(value > 0, 'one'); + require(value > 0); decrementPendingVotes(group, account, value); incrementActiveVotes(group, account, value); emit ValidatorGroupVoteActivated(account, group, value); @@ -352,15 +350,46 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { return total; } + /** + * @notice Returns the groups voted for by a particular account. + * @param account The address of the account. + * @return The groups voted for by a particular account. + */ function getAccountGroupsVotedFor(address account) external view returns (address[] memory) { return votes.lists[account]; } - function getAccountPendingVotesForGroup(address group, address account) public view returns (uint256) { + /** + * @notice Returns the pending votes for `group` made by `account`. + * @param group The address of the validator group. + * @param account The address of the voting account. + * @return The pending votes for `group` made by `account`. + */ + function getAccountPendingVotesForGroup( + address group, + address account + ) + public + view + returns (uint256) + { return votes.pending.balances[group][account]; } - function getAccountActiveVotesForGroup(address group, address account) public view returns (uint256) { + /** + * @notice Returns the active votes for `group` made by `account`. + * @param group The address of the validator group. + * @param account The address of the voting account. + * @return The active votes for `group` made by `account`. + */ + function getAccountActiveVotesForGroup( + address group, + address account + ) + public + view + returns (uint256) + { uint256 numerator = votes.active.numerators[group][account].mul(votes.active.total[group]); if (numerator == 0) { return 0; @@ -369,12 +398,30 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { return numerator.div(denominator); } - function getAccountTotalVotesForGroup(address group, address account) public view returns (uint256) { + /** + * @notice Returns the total votes for `group` made by `account`. + * @param group The address of the validator group. + * @param account The address of the voting account. + * @return The total votes for `group` made by `account`. + */ + function getAccountTotalVotesForGroup( + address group, + address account + ) + public + view + returns (uint256) + { uint256 pending = getAccountPendingVotesForGroup(group, account); uint256 active = getAccountActiveVotesForGroup(group, account); return pending.add(active); } + /** + * @notice Returns the total votes made for `group`. + * @param group The address of the validator group. + * @return The total votes made for `group`. + */ function getGroupTotalVotes(address group) external view returns (uint256) { return votes.total.eligible.getValue(group); } @@ -426,12 +473,27 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { votes.total.total = votes.total.total.sub(value); } - function markGroupIneligible(address group) external onlyRegisteredContract(VALIDATORS_REGISTRY_ID) { + /** + * @notice Marks a group ineligible for electing validators. + * @param group The address of the validator group. + * @dev Can only be called by the registered "Validators" contract. + */ + function markGroupIneligible( + address group + ) + external + onlyRegisteredContract(VALIDATORS_REGISTRY_ID) + { votes.total.eligible.remove(group); emit ValidatorGroupMarkedIneligible(group); } - // TODO(asa): Should this only be callable by the group? + /** + * @notice Marks a group eligible for electing validators. + * @param group The address of the validator group. + * @param lesser The address of the group that has received fewer votes than this group. + * @param greater The address of the group that has received more votes than this group. + */ function markGroupEligible(address group, address lesser, address greater) external { require(!votes.total.eligible.contains(group), "aaa"); require(getValidators().getGroupNumMembers(group) > 0, "b"); @@ -440,6 +502,12 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { emit ValidatorGroupMarkedEligible(group); } + /** + * @notice Increments the number of pending votes for `group` made by `account`. + * @param group The address of the validator group. + * @param account The address of the voting account. + * @param value The number of votes. + */ function incrementPendingVotes(address group, address account, uint256 value) private { PendingVotes storage pending = votes.pending; pending.balances[group][account] = pending.balances[group][account].add(value); @@ -447,6 +515,12 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { pending.total[group] = pending.total[group].add(value); } + /** + * @notice Decrements the number of pending votes for `group` made by `account`. + * @param group The address of the validator group. + * @param account The address of the voting account. + * @param value The number of votes. + */ function decrementPendingVotes(address group, address account, uint256 value) private { PendingVotes storage pending = votes.pending; uint256 newValue = pending.balances[group][account].sub(value); @@ -457,6 +531,12 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { pending.total[group] = pending.total[group].sub(value); } + /** + * @notice Increments the number of active votes for `group` made by `account`. + * @param group The address of the validator group. + * @param account The address of the voting account. + * @param value The number of votes. + */ function incrementActiveVotes(address group, address account, uint256 value) private { uint256 delta = getActiveVotesDelta(group, value); ActiveVotes storage active = votes.active; @@ -465,6 +545,12 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { active.total[group] = active.total[group].add(value); } + /** + * @notice Decrements the number of active votes for `group` made by `account`. + * @param group The address of the validator group. + * @param account The address of the voting account. + * @param value The number of votes. + */ function decrementActiveVotes(address group, address account, uint256 value) private { uint256 delta = getActiveVotesDelta(group, value); ActiveVotes storage active = votes.active; @@ -473,9 +559,17 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { active.total[group] = active.total[group].sub(value); } + /** + * @notice Returns the delta in active vote denominator for `group`. + * @param group The address of the validator group. + * @param value The numebr of active votes being added. + * @return The delta in active vote denominator for `group`. + */ function getActiveVotesDelta(address group, uint256 value) private view returns (uint256) { // Preserve delta * total = value * denominator - return value.mul(votes.active.denominators[group].add(1)).div(votes.active.total[group].add(1)); + return value.mul(votes.active.denominators[group].add(1)).div( + votes.active.total[group].add(1) + ); } /** @@ -492,21 +586,46 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { list.length = lastIndex; } + /** + * @notice Returns the number of votes that a group can receive. + * @param group The address of the group. + * @return The number of votes that a group can receive. + */ function getNumVotesReceivable(address group) public view returns (uint256) { - uint256 numerator = getValidators().getGroupNumMembers(group).add(1).mul(getLockedGold().getTotalLockedGold()); - uint256 denominator = Math.min(maxElectableValidators, getValidators().getNumRegisteredValidators()); + uint256 totalLockedGold = getLockedGold().getTotalLockedGold(); + uint256 numerator = getValidators().getGroupNumMembers(group).add(1).mul(totalLockedGold); + uint256 denominator = Math.min( + maxElectableValidators, + getValidators().getNumRegisteredValidators() + ); return numerator.div(denominator); } + /** + * @notice Returns the total votes received across all groups. + * @return The total votes received across all groups. + */ function getTotalVotes() external view returns (uint256) { return votes.total.total; } + /** + * @notice Returns the list of validator groups eligible to elect validators. + * @return The list of validator groups eligible to elect validators. + */ function getEligibleValidatorGroups() external view returns (address[] memory) { return votes.total.eligible.getKeys(); } - function getEligibleValidatorGroupsVoteTotals() external view returns (address[] memory, uint256[] memory) { + /** + * @notice Returns lists of all validator groups and the number of votes they've received. + * @return Lists of all validator groups and the number of votes they've received. + */ + function getEligibleValidatorGroupsVoteTotals() + external + view + returns (address[] memory, uint256[] memory) + { return votes.total.eligible.getElements(); } @@ -543,7 +662,10 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { */ function electValidators() external view returns (address[] memory) { // Only members of these validator groups are eligible for election. - uint256 maxNumElectionGroups = Math.min(maxElectableValidators, votes.total.eligible.list.numElements); + uint256 maxNumElectionGroups = Math.min( + maxElectableValidators, + votes.total.eligible.list.numElements + ); // uint256 requiredVotes = electabilityThreshold.multiply(FixidityLib.newFixed(votes.total.total)).fromFixed(); // TODO(asa): Filter by > requiredVotes address[] memory electionGroups = votes.total.eligible.headN(maxNumElectionGroups); @@ -598,7 +720,11 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { * @dev See https://en.wikipedia.org/wiki/D%27Hondt_method#Allocation for more information. * @return Whether or not a group elected a member, and the index of the group if so. */ - function dHondt(address[] memory electionGroups, uint256[] memory numMembers, uint256[] memory numMembersElected) + function dHondt( + address[] memory electionGroups, + uint256[] memory numMembers, + uint256[] memory numMembersElected + ) private view returns (uint256, bool) @@ -610,7 +736,9 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { address group = electionGroups[i]; // Only consider groups with members left to be elected. if (numMembers[i] > numMembersElected[i]) { - FixidityLib.Fraction memory n = FixidityLib.newFixed(votes.total.eligible.getValue(group)).divide( + FixidityLib.Fraction memory n = FixidityLib.newFixed( + votes.total.eligible.getValue(group) + ).divide( FixidityLib.newFixed(numMembersElected[i].add(1)) ); if (n.gt(maxN)) { diff --git a/packages/protocol/contracts/governance/LockedGold.sol b/packages/protocol/contracts/governance/LockedGold.sol index 178684e5e92..8a048547188 100644 --- a/packages/protocol/contracts/governance/LockedGold.sol +++ b/packages/protocol/contracts/governance/LockedGold.sol @@ -14,8 +14,6 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr using SafeMath for uint256; - // TODO(asa): How do adjust for updated requirements? - // Have a refreshRequirements function validators and groups can call struct MustMaintain { uint256 value; uint256 timestamp; @@ -78,6 +76,13 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr return true; } + /** + * @notice Authorizes an address to vote on behalf of the account. + * @param voter The address to authorize. + * @param v The recovery id of the incoming ECDSA signature. + * @param r Output value r of the ECDSA signature. + * @param s Output value s of the ECDSA signature. + */ function authorizeVoter( address voter, uint8 v, @@ -93,6 +98,13 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr emit VoterAuthorized(msg.sender, voter); } + /** + * @notice Authorizes an address to validate on behalf of the account. + * @param validator The address to authorize. + * @param v The recovery id of the incoming ECDSA signature. + * @param r Output value r of the ECDSA signature. + * @param s Output value s of the ECDSA signature. + */ function authorizeValidator( address validator, uint8 v, @@ -118,25 +130,62 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr emit GoldLocked(msg.sender, msg.value); } - function incrementNonvotingAccountBalance(address account, uint256 value) external onlyRegisteredContract(ELECTION_REGISTRY_ID) { + /** + * @notice Increments the non-voting balance for an account. + * @param account The account whose non-voting balance should be incremented. + * @param value The amount by which to increment. + * @dev Can only be called by the registered "Election" smart contract. + */ + function incrementNonvotingAccountBalance( + address account, + uint256 value + ) + external + onlyRegisteredContract(ELECTION_REGISTRY_ID) + { _incrementNonvotingAccountBalance(account, value); } - function decrementNonvotingAccountBalance(address account, uint256 value) external onlyRegisteredContract(ELECTION_REGISTRY_ID) { + /** + * @notice Decrements the non-voting balance for an account. + * @param account The account whose non-voting balance should be decremented. + * @param value The amount by which to decrement. + * @dev Can only be called by the registered "Election" smart contract. + */ + function decrementNonvotingAccountBalance( + address account, + uint256 value + ) + external + onlyRegisteredContract(ELECTION_REGISTRY_ID) + { _decrementNonvotingAccountBalance(account, value); } + /** + * @notice Increments the non-voting balance for an account. + * @param account The account whose non-voting balance should be incremented. + * @param value The amount by which to increment. + */ function _incrementNonvotingAccountBalance(address account, uint256 value) private { accounts[account].balances.nonvoting = accounts[account].balances.nonvoting.add(value); totalNonvoting = totalNonvoting.add(value); } + /** + * @notice Decrements the non-voting balance for an account. + * @param account The account whose non-voting balance should be decremented. + * @param value The amount by which to decrement. + */ function _decrementNonvotingAccountBalance(address account, uint256 value) private { accounts[account].balances.nonvoting = accounts[account].balances.nonvoting.sub(value); totalNonvoting = totalNonvoting.sub(value); } - // TODO: Can't unlock if voting in governance. + /** + * @notice Unlocks gold that becomes withdrawable after the unlocking period. + * @param value The amount of gold to unlock. + */ function unlock(uint256 value) external nonReentrant { require(isAccount(msg.sender), "not account"); Account storage account = accounts[msg.sender]; @@ -152,6 +201,10 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr } // TODO(asa): Allow partial relock + /** + * @notice Relocks gold that has been unlocked but not withdrawn. + * @param index The index of the pending withdrawal to relock. + */ function relock(uint256 index) external nonReentrant { require(isAccount(msg.sender)); Account storage account = accounts[msg.sender]; @@ -162,6 +215,10 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr emit GoldLocked(msg.sender, value); } + /** + * @notice Withdraws a gold that has been unlocked after the unlocking period has passed. + * @param index The index of the pending withdrawal to withdraw. + */ function withdraw(uint256 index) external nonReentrant { require(isAccount(msg.sender)); Account storage account = accounts[msg.sender]; @@ -174,6 +231,13 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr emit GoldWithdrawn(msg.sender, value); } + /** + * @notice Sets account locked gold balance requirements. + * @param account The account for which to set balance requirements. + * @param value The value that the account must maintain. + * @param timestamp The timestamp after which the account no longer must maintain this balance. + * @dev Can only be called by the registered "Validators" smart contract. + */ function setAccountMustMaintain( address account, uint256 value, @@ -206,19 +270,37 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr } } + /** + * @notice Returns the total amount of locked gold in the system. + * @return The total amount of locked gold in the system. + */ function getTotalLockedGold() external view returns (uint256) { return totalNonvoting.add(getElection().getTotalVotes()); } + /** + * @notice Returns the total amount of locked gold not being used to vote in elections. + * @return The total amount of locked gold not being used to vote in elections. + */ function getNonvotingLockedGold() external view returns (uint256) { return totalNonvoting; - } + } + /** + * @notice Returns the total amount of locked gold for an account. + * @account The account. + * @return The total amount of locked gold for an account. + */ function getAccountTotalLockedGold(address account) public view returns (uint256) { uint256 total = accounts[account].balances.nonvoting; return total.add(getElection().getAccountTotalVotes(account)); } + /** + * @notice Returns the total amount of non-voting locked gold for an account. + * @account The account. + * @return The total amount of non-voting locked gold for an account. + */ function getAccountNonvotingLockedGold(address account) external view returns (uint256) { return accounts[account].balances.nonvoting; } @@ -262,6 +344,11 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr return validator == address(0) ? account : validator; } + /** + * @notice Returns the pending withdrawals from unlocked gold for an account. + * @param account The address of the account. + * @return The value and timestamp for each pending withdrawal. + */ function getPendingWithdrawals(address account) public view returns (uint256[] memory, uint256[] memory) { require(isAccount(account)); uint256 length = accounts[account].balances.pendingWithdrawals.length; @@ -312,6 +399,11 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr return (accounts[account].exists); } + /** + * @notice Check if an account already exists. + * @param account The address of the account + * @return Returns `false` if account exists. Returns `true` otherwise. + */ function isNotAccount(address account) internal view returns (bool) { return (!accounts[account].exists); } @@ -325,6 +417,11 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr return (authorizedBy[account] != address(0)); } + /** + * @notice Check if an address has been authorized by an account for voting or validating. + * @param account The possibly authorized address. + * @return Returns `false` if authorized. Returns `true` otherwise. + */ function isNotAuthorized(address account) internal view returns (bool) { return (authorizedBy[account] == address(0)); } diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index 7b6af7c4c3b..fdce901dd01 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -160,6 +160,10 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi return true; } + /** + * @notice Returns the maximum number of members a group can add. + * @return The maximum number of members a group can add. + */ function getMaxGroupSize() external view returns (uint256) { return maxGroupSize; } @@ -300,7 +304,11 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi } delete validators[account]; deleteElement(_validators, account, index); - getLockedGold().setAccountMustMaintain(account, registrationRequirements.validator, now.add(deregistrationLockups.validator)); + getLockedGold().setAccountMustMaintain( + account, + registrationRequirements.validator, + now.add(deregistrationLockups.validator) + ); emit ValidatorDeregistered(account); return true; } @@ -384,7 +392,11 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi require(isValidatorGroup(account) && groups[account].members.numElements == 0); delete groups[account]; deleteElement(_groups, account, index); - getLockedGold().setAccountMustMaintain(account, registrationRequirements.group, now.add(deregistrationLockups.group)); + getLockedGold().setAccountMustMaintain( + account, + registrationRequirements.group, + now.add(deregistrationLockups.group) + ); emit ValidatorGroupDeregistered(account); return true; } @@ -401,6 +413,12 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi return _addMember(account, validator); } + /** + * @notice Adds a member to the end of a validator group's list of members. + * @param validator The validator to add to the group + * @return True upon success. + * @dev Fails if `validator` has not set their affiliation to this account. + */ function _addMember(address group, address validator) private returns (bool) { ValidatorGroup storage _group = groups[group]; require(_group.members.numElements < maxGroupSize); @@ -410,9 +428,6 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi return true; } - /** - * @notice De-affiliates a validator, removing it from the group for which it is a member. - /** * @notice Removes a member from a validator group. * @param validator The validator to remove from the group @@ -497,11 +512,29 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi return (group.name, group.url, group.members.getKeys()); } + /** + * @notice Returns the number of members in a validator group. + * @param account The address of the validator group. + * @return The number of members in a validator group. + */ function getGroupNumMembers(address account) public view returns (uint256) { return groups[account].members.numElements; } - function getTopValidatorsFromGroup(address account, uint256 n) external view returns (address[] memory) { + /** + * @notice Returns the top n group members for a particular group. + * @param account The address of the validator group. + * @param n The number of members to return. + * @return The top n group members for a particular group. + */ + function getTopValidatorsFromGroup( + address account, + uint256 n + ) + external + view + returns (address[] memory) + { address[] memory topAccounts = groups[account].members.headN(n); address[] memory topValidators = new address[](n); for (uint256 i = 0; i < n; i = i.add(1)) { @@ -510,7 +543,18 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi return topValidators; } - function getGroupsNumMembers(address[] calldata accounts) external view returns (uint256[] memory) { + /** + * @notice Returns the number of members in the provided validator groups. + * @param accounts The addresses of the validator groups. + * @return The number of members in the provided validator groups. + */ + function getGroupsNumMembers( + address[] calldata accounts + ) + external + view + returns (uint256[] memory) + { uint256[] memory numMembers = new uint256[](accounts.length); for (uint256 i = 0; i < accounts.length; i = i.add(1)) { numMembers[i] = getGroupNumMembers(accounts[i]); @@ -518,6 +562,10 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi return numMembers; } + /** + * @notice Returns the number of registered validators. + * @return The number of registered validators. + */ function getNumRegisteredValidators() external view returns (uint256) { return _validators.length; } @@ -530,6 +578,10 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi return (registrationRequirements.group, registrationRequirements.validator); } + /** + * @notice Returns the lockup periods after deregistering groups and validators. + * @return The lockup periods after deregistering groups and validators. + */ function getDeregistrationLockups() external view returns (uint256, uint256) { return (deregistrationLockups.group, deregistrationLockups.validator); } @@ -592,7 +644,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi */ function _removeMember(address group, address validator) private returns (bool) { ValidatorGroup storage _group = groups[group]; - require(validators[validator].affiliation == group && _group.members.contains(validator), "boogie"); + require(validators[validator].affiliation == group && _group.members.contains(validator)); _group.members.remove(validator); emit ValidatorGroupMemberRemoved(group, validator); From f02b05a25d7ed991236824f12f866c84bce65919 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Wed, 2 Oct 2019 13:50:20 -0700 Subject: [PATCH 017/149] Linting --- .../linkedlists/AddressSortedLinkedList.sol | 9 ++++++++- .../common/linkedlists/SortedLinkedList.sol | 8 ++++++-- .../contracts/governance/Election.sol | 6 +++++- .../contracts/governance/LockedGold.sol | 20 +++++++++++++++---- .../governance/test/MockLockedGold.sol | 9 ++++++++- .../governance/test/MockValidators.sol | 9 ++++++++- .../protocol/contracts/stability/Exchange.sol | 4 +++- .../migrations/17_elect_validators.ts | 2 +- 8 files changed, 55 insertions(+), 12 deletions(-) diff --git a/packages/protocol/contracts/common/linkedlists/AddressSortedLinkedList.sol b/packages/protocol/contracts/common/linkedlists/AddressSortedLinkedList.sol index be2135245ec..61ea7d0fdef 100644 --- a/packages/protocol/contracts/common/linkedlists/AddressSortedLinkedList.sol +++ b/packages/protocol/contracts/common/linkedlists/AddressSortedLinkedList.sol @@ -111,7 +111,14 @@ library AddressSortedLinkedList { * @param n The number of elements to return. * @return The keys of the greatest elements. */ - function headN(SortedLinkedList.List storage list, uint256 n) public view returns (address[] memory) { + function headN( + SortedLinkedList.List storage list, + uint256 n + ) + public + view + returns (address[] memory) + { bytes32[] memory byteKeys = list.headN(n); address[] memory keys = new address[](n); for (uint256 i = 0; i < n; i++) { diff --git a/packages/protocol/contracts/common/linkedlists/SortedLinkedList.sol b/packages/protocol/contracts/common/linkedlists/SortedLinkedList.sol index 94ec18583ff..a8151ed5f49 100644 --- a/packages/protocol/contracts/common/linkedlists/SortedLinkedList.sol +++ b/packages/protocol/contracts/common/linkedlists/SortedLinkedList.sol @@ -183,10 +183,14 @@ library SortedLinkedList { greaterKey == bytes32(0) && isValueBetween(list, value, list.list.head, greaterKey) ) { return (list.list.head, greaterKey); - } else if (lesserKey != bytes32(0) && isValueBetween(list, value, lesserKey, list.list.elements[lesserKey].nextKey)) { + } else if ( + lesserKey != bytes32(0) && + isValueBetween(list, value, lesserKey, list.list.elements[lesserKey].nextKey)) + { return (lesserKey, list.list.elements[lesserKey].nextKey); } else if ( - greaterKey != bytes32(0) && isValueBetween(list, value, list.list.elements[greaterKey].previousKey, greaterKey) + greaterKey != bytes32(0) && + isValueBetween(list, value, list.list.elements[greaterKey].previousKey, greaterKey) ) { return (list.list.elements[greaterKey].previousKey, greaterKey); } else { diff --git a/packages/protocol/contracts/governance/Election.sol b/packages/protocol/contracts/governance/Election.sol index 20b637868a3..937515784c6 100644 --- a/packages/protocol/contracts/governance/Election.sol +++ b/packages/protocol/contracts/governance/Election.sol @@ -666,7 +666,11 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { maxElectableValidators, votes.total.eligible.list.numElements ); - // uint256 requiredVotes = electabilityThreshold.multiply(FixidityLib.newFixed(votes.total.total)).fromFixed(); + /* + uint256 requiredVotes = electabilityThreshold.multiply( + FixidityLib.newFixed(votes.total.total) + ).fromFixed(); + */ // TODO(asa): Filter by > requiredVotes address[] memory electionGroups = votes.total.eligible.headN(maxNumElectionGroups); uint256[] memory numMembers = getValidators().getGroupsNumMembers(electionGroups); diff --git a/packages/protocol/contracts/governance/LockedGold.sol b/packages/protocol/contracts/governance/LockedGold.sol index 8a048547188..36270d5b11e 100644 --- a/packages/protocol/contracts/governance/LockedGold.sol +++ b/packages/protocol/contracts/governance/LockedGold.sol @@ -192,7 +192,8 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr MustMaintain memory requirement = account.balances.requirements; require( now >= requirement.timestamp || - getAccountTotalLockedGold(msg.sender).sub(value) >= requirement.value, "didn't meet mustmaintain requirements" + getAccountTotalLockedGold(msg.sender).sub(value) >= requirement.value, + "didn't meet mustmaintain requirements" ); _decrementNonvotingAccountBalance(msg.sender, value); uint256 available = now.add(unlockingPeriod); @@ -262,7 +263,10 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr function getAccountFromVoter(address accountOrVoter) external view returns (address) { address authorizingAccount = authorizedBy[accountOrVoter]; if (authorizingAccount != address(0)) { - require(accounts[authorizingAccount].authorizations.voting == accountOrVoter, 'failed first check'); + require( + accounts[authorizingAccount].authorizations.voting == accountOrVoter, + 'failed first check' + ); return authorizingAccount; } else { require(isAccount(accountOrVoter)); @@ -349,13 +353,21 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr * @param account The address of the account. * @return The value and timestamp for each pending withdrawal. */ - function getPendingWithdrawals(address account) public view returns (uint256[] memory, uint256[] memory) { + function getPendingWithdrawals( + address account + ) + public + view + returns (uint256[] memory, uint256[] memory) + { require(isAccount(account)); uint256 length = accounts[account].balances.pendingWithdrawals.length; uint256[] memory values = new uint256[](length); uint256[] memory timestamps = new uint256[](length); for (uint256 i = 0; i < length; i++) { - PendingWithdrawal memory pendingWithdrawal = accounts[account].balances.pendingWithdrawals[i]; + PendingWithdrawal memory pendingWithdrawal = ( + accounts[account].balances.pendingWithdrawals[i] + ); values[i] = pendingWithdrawal.value; timestamps[i] = pendingWithdrawal.timestamp; } diff --git a/packages/protocol/contracts/governance/test/MockLockedGold.sol b/packages/protocol/contracts/governance/test/MockLockedGold.sol index 3df8513e0c1..970e8079d33 100644 --- a/packages/protocol/contracts/governance/test/MockLockedGold.sol +++ b/packages/protocol/contracts/governance/test/MockLockedGold.sol @@ -54,7 +54,14 @@ contract MockLockedGold is ILockedGold { nonvotingAccountBalance[account] = nonvotingAccountBalance[account].sub(value); } - function setAccountMustMaintain(address account, uint256 value, uint256 timestamp) external returns (bool) { + function setAccountMustMaintain( + address account, + uint256 value, + uint256 timestamp + ) + external + returns (bool) + { mustMaintain[account] = MustMaintain(value, timestamp); return true; } diff --git a/packages/protocol/contracts/governance/test/MockValidators.sol b/packages/protocol/contracts/governance/test/MockValidators.sol index 805571e84a2..68514b13a1d 100644 --- a/packages/protocol/contracts/governance/test/MockValidators.sol +++ b/packages/protocol/contracts/governance/test/MockValidators.sol @@ -45,7 +45,14 @@ contract MockValidators is IValidators { members[group] = _members; } - function getTopValidatorsFromGroup(address group, uint256 n) external view returns (address[] memory) { + function getTopValidatorsFromGroup( + address group, + uint256 n + ) + external + view + returns (address[] memory) + { require(n <= members[group].length); address[] memory validators = new address[](n); for (uint256 i = 0; i < n; i++) { diff --git a/packages/protocol/contracts/stability/Exchange.sol b/packages/protocol/contracts/stability/Exchange.sol index 03466f99474..5e0e457422e 100644 --- a/packages/protocol/contracts/stability/Exchange.sol +++ b/packages/protocol/contracts/stability/Exchange.sol @@ -331,7 +331,9 @@ contract Exchange is IExchange, Initializable, Ownable, UsingRegistry, Reentranc } function getUpdatedGoldBucket() private view returns (uint256) { - uint256 reserveGoldBalance = getGoldToken().balanceOf(registry.getAddressForOrDie(RESERVE_REGISTRY_ID)); + uint256 reserveGoldBalance = getGoldToken().balanceOf( + registry.getAddressForOrDie(RESERVE_REGISTRY_ID) + ); return reserveFraction.multiply(FixidityLib.newFixed(reserveGoldBalance)).fromFixed(); } diff --git a/packages/protocol/migrations/17_elect_validators.ts b/packages/protocol/migrations/17_elect_validators.ts index 3fe2c660366..4a9d7838907 100644 --- a/packages/protocol/migrations/17_elect_validators.ts +++ b/packages/protocol/migrations/17_elect_validators.ts @@ -8,8 +8,8 @@ import { sendTransactionWithPrivateKey, } from '@celo/protocol/lib/web3-utils' import { config } from '@celo/protocol/migrationsConfig' -import { toFixed } from '@celo/utils/lib/fixidity' import { blsPrivateKeyToProcessedPrivateKey } from '@celo/utils/lib/bls' +import { toFixed } from '@celo/utils/lib/fixidity' import { BigNumber } from 'bignumber.js' import * as bls12377js from 'bls12377js' import { ElectionInstance, LockedGoldInstance, ValidatorsInstance } from 'types' From 70d7478b369cfb7d8d3d5a3158f0b47a8281021d Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Wed, 2 Oct 2019 14:58:31 -0700 Subject: [PATCH 018/149] Small fix --- packages/protocol/contracts/governance/LockedGold.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/protocol/contracts/governance/LockedGold.sol b/packages/protocol/contracts/governance/LockedGold.sol index 36270d5b11e..b7f42f9c350 100644 --- a/packages/protocol/contracts/governance/LockedGold.sol +++ b/packages/protocol/contracts/governance/LockedGold.sol @@ -292,7 +292,7 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr /** * @notice Returns the total amount of locked gold for an account. - * @account The account. + * @param account The account. * @return The total amount of locked gold for an account. */ function getAccountTotalLockedGold(address account) public view returns (uint256) { @@ -302,7 +302,7 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr /** * @notice Returns the total amount of non-voting locked gold for an account. - * @account The account. + * @param account The account. * @return The total amount of non-voting locked gold for an account. */ function getAccountNonvotingLockedGold(address account) external view returns (uint256) { From 69f6ab29efa410083ffae3da115609ea2228ae8c Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Wed, 2 Oct 2019 18:54:27 -0700 Subject: [PATCH 019/149] ContractKit building --- .circleci/config.yml | 2 +- .../cli/src/commands/account/isvalidator.ts | 6 +- .../cli/src/commands/lockedgold/delegate.ts | 49 ----- .../cli/src/commands/lockedgold/lockup.ts | 51 ----- .../cli/src/commands/lockedgold/notify.ts | 30 --- packages/cli/src/commands/lockedgold/show.ts | 54 ++--- .../cli/src/commands/lockedgold/withdraw.ts | 16 +- packages/cli/src/commands/validator/list.ts | 1 - .../cli/src/commands/validator/register.ts | 17 +- .../cli/src/commands/validatorgroup/list.ts | 3 - .../cli/src/commands/validatorgroup/member.ts | 2 +- .../src/commands/validatorgroup/register.ts | 18 +- .../cli/src/commands/validatorgroup/vote.ts | 56 ----- packages/cli/src/commands/validatorset.ts | 18 -- packages/cli/src/utils/lockedgold.ts | 11 +- packages/contractkit/src/base.ts | 3 +- packages/contractkit/src/contract-cache.ts | 14 +- packages/contractkit/src/index.ts | 1 - packages/contractkit/src/kit.ts | 29 ++- .../contractkit/src/web3-contract-cache.ts | 7 +- .../contractkit/src/wrappers/LockedGold.ts | 199 ++++------------- .../contractkit/src/wrappers/Validators.ts | 207 ++++-------------- .../contracts/governance/Election.sol | 56 +++-- packages/protocol/scripts/build.ts | 13 +- 24 files changed, 202 insertions(+), 661 deletions(-) delete mode 100644 packages/cli/src/commands/lockedgold/delegate.ts delete mode 100644 packages/cli/src/commands/lockedgold/lockup.ts delete mode 100644 packages/cli/src/commands/lockedgold/notify.ts delete mode 100644 packages/cli/src/commands/validatorgroup/vote.ts delete mode 100644 packages/cli/src/commands/validatorset.ts diff --git a/.circleci/config.yml b/.circleci/config.yml index b3a28cf1089..1c7d7217f38 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -109,7 +109,7 @@ jobs: steps: - attach_workspace: at: ~/app - # If this fails, fix it with + # If this fails, fix it with # `./node_modules/.bin/prettier --config .prettierrc.js --write '**/*.+(ts|tsx|js|jsx)'` - run: yarn run prettify:diff - run: yarn run lint diff --git a/packages/cli/src/commands/account/isvalidator.ts b/packages/cli/src/commands/account/isvalidator.ts index 9a6738df870..15876ea8775 100644 --- a/packages/cli/src/commands/account/isvalidator.ts +++ b/packages/cli/src/commands/account/isvalidator.ts @@ -17,11 +17,11 @@ export default class IsValidator extends BaseCommand { async run() { const { args } = this.parse(IsValidator) - const validators = await this.kit.contracts.getValidators() - const numberValidators = await validators.numberValidatorsInCurrentSet() + const election = await this.kit.contracts.getValidators() + const numberValidators = await election.numberValidatorsInCurrentSet() for (let i = 0; i < numberValidators; i++) { - const validatorAddress = await validators.validatorAddressFromCurrentSet(i) + const validatorAddress = await election.validatorAddressFromCurrentSet(i) if (eqAddress(validatorAddress, args.address)) { console.log(`${args.address} is in the current validator set`) return diff --git a/packages/cli/src/commands/lockedgold/delegate.ts b/packages/cli/src/commands/lockedgold/delegate.ts deleted file mode 100644 index 9b1d39174f7..00000000000 --- a/packages/cli/src/commands/lockedgold/delegate.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Roles } from '@celo/contractkit' -import { flags } from '@oclif/command' -import { BaseCommand } from '../../base' -import { displaySendTx } from '../../utils/cli' -import { Flags } from '../../utils/command' - -export default class Delegate extends BaseCommand { - static description = 'Delegate validating, voting and reward roles for Locked Gold account' - - static flags = { - ...BaseCommand.flags, - from: Flags.address({ required: true }), - role: flags.string({ - char: 'r', - options: Object.keys(Roles), - description: 'Role to delegate', - }), - to: Flags.address({ required: true }), - } - - static args = [] - - static examples = [ - 'delegate --from=0x5409ED021D9299bf6814279A6A1411A7e866A631 --role Voting --to=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d', - ] - - async run() { - const res = this.parse(Delegate) - - if (!res.flags.role) { - this.error(`Specify role with --role`) - return - } - - if (!res.flags.to) { - this.error(`Specify delegate address with --to`) - return - } - - this.kit.defaultAccount = res.flags.from - const lockedGold = await this.kit.contracts.getLockedGold() - const tx = await lockedGold.delegateRoleTx( - res.flags.from, - res.flags.to, - Roles[res.flags.role as keyof typeof Roles] - ) - await displaySendTx('delegateRoleTx', tx) - } -} diff --git a/packages/cli/src/commands/lockedgold/lockup.ts b/packages/cli/src/commands/lockedgold/lockup.ts deleted file mode 100644 index 18c689a2fc2..00000000000 --- a/packages/cli/src/commands/lockedgold/lockup.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Address } from '@celo/utils/lib/address' -import { flags } from '@oclif/command' -import BigNumber from 'bignumber.js' -import { BaseCommand } from '../../base' -import { displaySendTx, failWith } from '../../utils/cli' -import { Flags } from '../../utils/command' -import { LockedGoldArgs } from '../../utils/lockedgold' - -export default class Commitment extends BaseCommand { - static description = 'Create a Locked Gold commitment given notice period and gold amount' - - static flags = { - ...BaseCommand.flags, - from: flags.string({ ...Flags.address, required: true }), - noticePeriod: flags.string({ ...LockedGoldArgs.noticePeriodArg, required: true }), - goldAmount: flags.string({ ...LockedGoldArgs.goldAmountArg, required: true }), - } - - static args = [] - - static examples = [ - 'lockup --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --noticePeriod 8640 --goldAmount 1000000000000000000', - ] - - async run() { - const res = this.parse(Commitment) - const address: Address = res.flags.from - - this.kit.defaultAccount = address - const lockedGold = await this.kit.contracts.getLockedGold() - - const noticePeriod = new BigNumber(res.flags.noticePeriod) - const goldAmount = new BigNumber(res.flags.goldAmount) - - if (!(await lockedGold.isVoting(address))) { - failWith(`require(!isVoting(address)) => false`) - } - - const maxNoticePeriod = await lockedGold.maxNoticePeriod() - if (!maxNoticePeriod.gte(noticePeriod)) { - failWith(`require(noticePeriod <= maxNoticePeriod) => [${noticePeriod}, ${maxNoticePeriod}]`) - } - if (!goldAmount.gt(new BigNumber(0))) { - failWith(`require(goldAmount > 0) => [${goldAmount}]`) - } - - // await displaySendTx('redeemRewards', lockedGold.methods.redeemRewards()) - const tx = lockedGold.newCommitment(noticePeriod.toString()) - await displaySendTx('lockup', tx, { value: goldAmount.toString() }) - } -} diff --git a/packages/cli/src/commands/lockedgold/notify.ts b/packages/cli/src/commands/lockedgold/notify.ts deleted file mode 100644 index 192a40e5818..00000000000 --- a/packages/cli/src/commands/lockedgold/notify.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { flags } from '@oclif/command' -import { BaseCommand } from '../../base' -import { displaySendTx } from '../../utils/cli' -import { Flags } from '../../utils/command' -import { LockedGoldArgs } from '../../utils/lockedgold' - -export default class Notify extends BaseCommand { - static description = 'Notify a Locked Gold commitment given notice period and gold amount' - - static flags = { - ...BaseCommand.flags, - from: Flags.address({ required: true }), - noticePeriod: flags.string({ ...LockedGoldArgs.noticePeriodArg, required: true }), - goldAmount: flags.string({ ...LockedGoldArgs.goldAmountArg, required: true }), - } - - static args = [] - - static examples = ['notify --noticePeriod=3600 --goldAmount=500'] - - async run() { - const res = this.parse(Notify) - this.kit.defaultAccount = res.flags.from - const lockedgold = await this.kit.contracts.getLockedGold() - await displaySendTx( - 'notifyCommitment', - lockedgold.notifyCommitment(res.flags.goldAmount, res.flags.noticePeriod) - ) - } -} diff --git a/packages/cli/src/commands/lockedgold/show.ts b/packages/cli/src/commands/lockedgold/show.ts index 2848cdee657..8966298cf71 100644 --- a/packages/cli/src/commands/lockedgold/show.ts +++ b/packages/cli/src/commands/lockedgold/show.ts @@ -3,58 +3,42 @@ import BigNumber from 'bignumber.js' import chalk from 'chalk' import { cli } from 'cli-ux' import { BaseCommand } from '../../base' +import { printValueMap } from '../../utils/cli' import { Args } from '../../utils/command' import { LockedGoldArgs } from '../../utils/lockedgold' export default class Show extends BaseCommand { - static description = 'Show Locked Gold and corresponding account weight of a commitment given ID' + static description = 'Show locked gold information for a given account' static flags = { ...BaseCommand.flags, - noticePeriod: flags.string({ - ...LockedGoldArgs.noticePeriodArg, - exclusive: ['availabilityTime'], - }), - availabilityTime: flags.string({ - ...LockedGoldArgs.availabilityTimeArg, - exclusive: ['noticePeriod'], - }), } static args = [Args.address('account')] - static examples = [ - 'show 0x5409ed021d9299bf6814279a6a1411a7e866a631 --noticePeriod=3600', - 'show 0x5409ed021d9299bf6814279a6a1411a7e866a631 --availabilityTime=1562206887', - ] + static examples = ['show 0x5409ed021d9299bf6814279a6a1411a7e866a631'] async run() { // tslint:disable-next-line const { flags, args } = this.parse(Show) - if (!(flags.noticePeriod || flags.availabilityTime)) { - this.error(`Specify commitment ID with --noticePeriod or --availabilityTime`) - return - } - const lockedGold = await this.kit.contracts.getLockedGold() - let value = new BigNumber(0) - let contributingWeight = new BigNumber(0) - if (flags.noticePeriod) { - cli.action.start('Fetching Locked Gold commitment...') - value = await lockedGold.getLockedCommitmentValue(args.account, flags.noticePeriod) - contributingWeight = value.times(new BigNumber(flags.noticePeriod)) - } - - if (flags.availabilityTime) { - cli.action.start('Fetching notified commitment...') - value = await lockedGold.getNotifiedCommitmentValue(args.account, flags.availabilityTime) - contributingWeight = value + const nonvoting = await lockedGold.getAccountNonvotingLockedGold(args.account) + const total = await lockedGold.getAccountTotalLockedGold(args.account) + const voter = await lockedGold.getVoterFromAccount(args.account) + const validator = await lockedGold.getValidatorFromAccount(args.account) + const pendingWithdrawals = await lockedGold.getPendingWithdrawals(args.account) + const info = { + lockedGold: { + total, + nonvoting, + }, + authorizations: { + voter: voter == args.account ? null : voter, + validator: validator == args.account ? null : validator, + }, + pendingWithdrawals, } - - cli.action.stop() - - cli.log(chalk.bold.yellow('Gold Locked \t') + value.toString()) - cli.log(chalk.bold.red('Account Weight Contributed \t') + contributingWeight.toString()) + printValueMap(info) } } diff --git a/packages/cli/src/commands/lockedgold/withdraw.ts b/packages/cli/src/commands/lockedgold/withdraw.ts index e34fa24b954..b6f58968402 100644 --- a/packages/cli/src/commands/lockedgold/withdraw.ts +++ b/packages/cli/src/commands/lockedgold/withdraw.ts @@ -4,22 +4,28 @@ import { Flags } from '../../utils/command' import { LockedGoldArgs } from '../../utils/lockedgold' export default class Withdraw extends BaseCommand { - static description = 'Withdraw notified commitment given availability time' + static description = 'Withdraw unlocked gold whose unlocking period has passed.' static flags = { ...BaseCommand.flags, from: Flags.address({ required: true }), } - static args = [{ ...LockedGoldArgs.availabilityTimeArg, required: true }] - - static examples = ['withdraw 3600'] + static examples = ['withdraw'] async run() { // tslint:disable-next-line const { flags, args } = this.parse(Withdraw) this.kit.defaultAccount = flags.from const lockedgold = await this.kit.contracts.getLockedGold() - await displaySendTx('withdrawCommitment', lockedgold.withdrawCommitment(args.availabilityTime)) + const pendingWithdrawals = await lockedgold.getPendingWithdrawals() + const currentTime = Math.round(new Date().getTime() / 1000) + let withdrawals = 0 + for (let i = 0; i < pendingWithdrawals.length; i++) { + if (pendingWithdrawals[i].time <= currentTime) { + await displaySendTx('withdraw', lockedgold.withdraw(i - withdrawals)) + withdrawals += 1 + } + } } } diff --git a/packages/cli/src/commands/validator/list.ts b/packages/cli/src/commands/validator/list.ts index 9136c7bbdec..2e4efa37e11 100644 --- a/packages/cli/src/commands/validator/list.ts +++ b/packages/cli/src/commands/validator/list.ts @@ -20,7 +20,6 @@ export default class ValidatorList extends BaseCommand { cli.action.stop() cli.table(validatorList, { address: {}, - id: {}, name: {}, url: {}, publicKey: {}, diff --git a/packages/cli/src/commands/validator/register.ts b/packages/cli/src/commands/validator/register.ts index f237c862ff3..ea44245a896 100644 --- a/packages/cli/src/commands/validator/register.ts +++ b/packages/cli/src/commands/validator/register.ts @@ -10,20 +10,13 @@ export default class ValidatorRegister extends BaseCommand { static flags = { ...BaseCommand.flags, from: Flags.address({ required: true, description: 'Address for the Validator' }), - id: flags.string({ required: true }), name: flags.string({ required: true }), url: flags.string({ required: true }), publicKey: Flags.publicKey({ required: true }), - noticePeriod: flags.string({ - required: true, - description: - 'Notice period of the Locked Gold commitment. Specify multiple notice periods to use the sum of the commitments.', - multiple: true, - }), } static examples = [ - 'register --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --id myID --name myName --noticePeriod 5184000 --noticePeriod 5184001 --url "http://validator.com" --publicKey 0xc52f3fab06e22a54915a8765c4f6826090cfac5e40282b43844bf1c0df83aaa632e55b67869758f2291d1aabe0ebecc7cbf4236aaa45e3e0cfbf997eda082ae19d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d7405011220a66a6257562d0c26dabf64485a1d96bad27bb1c0fd6080a75b0ec9f75b50298a2a8e04b02b2688c8104fca61fb00', + 'register --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --name myName --url "http://validator.com" --publicKey 0xc52f3fab06e22a54915a8765c4f6826090cfac5e40282b43844bf1c0df83aaa632e55b67869758f2291d1aabe0ebecc7cbf4236aaa45e3e0cfbf997eda082ae19d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d7405011220a66a6257562d0c26dabf64485a1d96bad27bb1c0fd6080a75b0ec9f75b50298a2a8e04b02b2688c8104fca61fb00', ] async run() { const res = this.parse(ValidatorRegister) @@ -32,13 +25,7 @@ export default class ValidatorRegister extends BaseCommand { const attestations = await this.kit.contracts.getAttestations() await displaySendTx( 'registerValidator', - validators.registerValidator( - res.flags.id, - res.flags.name, - res.flags.url, - res.flags.publicKey as any, - res.flags.noticePeriod - ) + validators.registerValidator(res.flags.name, res.flags.url, res.flags.publicKey as any) ) // register encryption key on attestations contract diff --git a/packages/cli/src/commands/validatorgroup/list.ts b/packages/cli/src/commands/validatorgroup/list.ts index 2fde1d28ca5..013cb393e94 100644 --- a/packages/cli/src/commands/validatorgroup/list.ts +++ b/packages/cli/src/commands/validatorgroup/list.ts @@ -16,15 +16,12 @@ export default class ValidatorGroupList extends BaseCommand { cli.action.start('Fetching Validator Groups') const validators = await this.kit.contracts.getValidators() const vgroups = await validators.getRegisteredValidatorGroups() - const votes = await validators.getValidatorGroupsVotes() cli.action.stop() cli.table(vgroups, { address: {}, - id: {}, name: {}, url: {}, - votes: { get: (r) => votes.find((v) => v.address === r.address)!.votes.toString() }, members: { get: (r) => r.members.length }, }) } diff --git a/packages/cli/src/commands/validatorgroup/member.ts b/packages/cli/src/commands/validatorgroup/member.ts index 0d4fe178cd6..79e41a16835 100644 --- a/packages/cli/src/commands/validatorgroup/member.ts +++ b/packages/cli/src/commands/validatorgroup/member.ts @@ -5,7 +5,7 @@ import { displaySendTx } from '../../utils/cli' import { Args, Flags } from '../../utils/command' export default class ValidatorGroupRegister extends BaseCommand { - static description = 'Register a new Validator Group' + static description = 'Add or remove members from a Validator Group' static flags = { ...BaseCommand.flags, diff --git a/packages/cli/src/commands/validatorgroup/register.ts b/packages/cli/src/commands/validatorgroup/register.ts index ca513c3ca78..8eaca3c9c73 100644 --- a/packages/cli/src/commands/validatorgroup/register.ts +++ b/packages/cli/src/commands/validatorgroup/register.ts @@ -9,20 +9,15 @@ export default class ValidatorGroupRegister extends BaseCommand { static flags = { ...BaseCommand.flags, from: Flags.address({ required: true, description: 'Address for the Validator Group' }), - id: flags.string({ required: true }), name: flags.string({ required: true }), url: flags.string({ required: true }), - noticePeriod: flags.string({ - required: true, - description: - 'Notice period of the Locked Gold commitment. Specify multiple notice periods to use the sum of the commitments.', - multiple: true, - }), + commission: flags.string({ required: true }), } static examples = [ - 'register --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --id myID --name myName --noticePeriod 5184000 --noticePeriod 5184001 --url "http://vgroup.com"', + 'register --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --name myName --url "http://vgroup.com" --commission 0.1', ] + async run() { const res = this.parse(ValidatorGroupRegister) @@ -31,12 +26,7 @@ export default class ValidatorGroupRegister extends BaseCommand { await displaySendTx( 'registerValidatorGroup', - validators.registerValidatorGroup( - res.flags.id, - res.flags.name, - res.flags.url, - res.flags.noticePeriod - ) + validators.registerValidatorGroup(res.flags.name, res.flags.url, res.flags.commission) ) } } diff --git a/packages/cli/src/commands/validatorgroup/vote.ts b/packages/cli/src/commands/validatorgroup/vote.ts deleted file mode 100644 index 52205c26dc8..00000000000 --- a/packages/cli/src/commands/validatorgroup/vote.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { flags } from '@oclif/command' -import { BaseCommand } from '../../base' -import { displaySendTx, printValueMap } from '../../utils/cli' -import { Flags } from '../../utils/command' - -export default class ValidatorGroupVote extends BaseCommand { - static description = 'Vote for a Validator Group' - - static flags = { - ...BaseCommand.flags, - from: Flags.address({ required: true, description: "Voter's address" }), - current: flags.boolean({ - exclusive: ['revoke', 'for'], - description: "Show voter's current vote", - }), - revoke: flags.boolean({ - exclusive: ['current', 'for'], - description: "Revoke voter's current vote", - }), - for: Flags.address({ - exclusive: ['current', 'revoke'], - description: "Set vote for ValidatorGroup's address", - }), - } - - static examples = [ - 'vote --from 0x4443d0349e8b3075cba511a0a87796597602a0f1 --for 0x932fee04521f5fcb21949041bf161917da3f588b', - 'vote --from 0x4443d0349e8b3075cba511a0a87796597602a0f1 --revoke', - 'vote --from 0x4443d0349e8b3075cba511a0a87796597602a0f1 --current', - ] - async run() { - const res = this.parse(ValidatorGroupVote) - - this.kit.defaultAccount = res.flags.from - const validators = await this.kit.contracts.getValidators() - - if (res.flags.current) { - const lockedGold = await this.kit.contracts.getLockedGold() - const details = await lockedGold.getVotingDetails(res.flags.from) - const myVote = await validators.getVoteFrom(details.accountAddress) - - printValueMap({ - ...details, - currentVote: myVote, - }) - } else if (res.flags.revoke) { - const tx = await validators.revokeVote() - await displaySendTx('revokeVote', tx) - } else if (res.flags.for) { - const tx = await validators.vote(res.flags.for) - await displaySendTx('vote', tx) - } else { - this.error('Use one of --for, --current, --revoke') - } - } -} diff --git a/packages/cli/src/commands/validatorset.ts b/packages/cli/src/commands/validatorset.ts deleted file mode 100644 index 37ee81e5d76..00000000000 --- a/packages/cli/src/commands/validatorset.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { BaseCommand } from '../base' - -export default class ValidatorSet extends BaseCommand { - static description = 'Outputs the current validator set' - - static flags = { - ...BaseCommand.flags, - } - - static examples = ['validatorset'] - - async run() { - const validators = await this.kit.contracts.getValidators() - const validatorSet = await validators.getValidatorSetAddresses() - - validatorSet.forEach((validator: string) => console.log(validator)) - } -} diff --git a/packages/cli/src/utils/lockedgold.ts b/packages/cli/src/utils/lockedgold.ts index c715315334e..64120283e25 100644 --- a/packages/cli/src/utils/lockedgold.ts +++ b/packages/cli/src/utils/lockedgold.ts @@ -1,12 +1,7 @@ export const LockedGoldArgs = { - noticePeriodArg: { - name: 'noticePeriod', - description: - 'duration (seconds) from notice to withdrawable; doubles as ID of a Locked Gold commitment; ', - }, - availabilityTimeArg: { - name: 'availabilityTime', - description: 'unix timestamp at which withdrawable; doubles as ID of a notified commitment', + pendingWithdrawalIndexArg: { + name: 'pendingWithdrawalINdex', + description: 'index of pending withdrawal whose unlocking period has passed', }, goldAmountArg: { name: 'goldAmount', diff --git a/packages/contractkit/src/base.ts b/packages/contractkit/src/base.ts index 37f7d9533a3..a410cbb7fac 100644 --- a/packages/contractkit/src/base.ts +++ b/packages/contractkit/src/base.ts @@ -2,13 +2,14 @@ export type Address = string export enum CeloContract { Attestations = 'Attestations', - LockedGold = 'LockedGold', + Election = 'Election', Escrow = 'Escrow', Exchange = 'Exchange', GasCurrencyWhitelist = 'GasCurrencyWhitelist', GasPriceMinimum = 'GasPriceMinimum', GoldToken = 'GoldToken', Governance = 'Governance', + LockedGold = 'LockedGold', Random = 'Random', Registry = 'Registry', Reserve = 'Reserve', diff --git a/packages/contractkit/src/contract-cache.ts b/packages/contractkit/src/contract-cache.ts index c7aa1270524..585325ce3a9 100644 --- a/packages/contractkit/src/contract-cache.ts +++ b/packages/contractkit/src/contract-cache.ts @@ -1,6 +1,7 @@ import { CeloContract } from './base' import { ContractKit } from './kit' import { AttestationsWrapper } from './wrappers/Attestations' +import { ElectionWrapper } from './wrappers/Election' import { ExchangeWrapper } from './wrappers/Exchange' import { GasPriceMinimumWrapper } from './wrappers/GasPriceMinimum' import { GoldTokenWrapper } from './wrappers/GoldTokenWrapper' @@ -13,13 +14,14 @@ import { ValidatorsWrapper } from './wrappers/Validators' const WrapperFactories = { [CeloContract.Attestations]: AttestationsWrapper, - [CeloContract.LockedGold]: LockedGoldWrapper, + [CeloContract.Election]: ElectionWrapper, // [CeloContract.Escrow]: EscrowWrapper, [CeloContract.Exchange]: ExchangeWrapper, // [CeloContract.GasCurrencyWhitelist]: GasCurrencyWhitelistWrapper, [CeloContract.GasPriceMinimum]: GasPriceMinimumWrapper, [CeloContract.GoldToken]: GoldTokenWrapper, [CeloContract.Governance]: GovernanceWrapper, + [CeloContract.LockedGold]: LockedGoldWrapper, // [CeloContract.MultiSig]: MultiSigWrapper, // [CeloContract.Random]: RandomWrapper, // [CeloContract.Registry]: RegistryWrapper, @@ -34,13 +36,14 @@ export type ValidWrappers = keyof CFType interface WrapperCacheMap { [CeloContract.Attestations]?: AttestationsWrapper - [CeloContract.LockedGold]?: LockedGoldWrapper + [CeloContract.Election]?: ElectionWrapper // [CeloContract.Escrow]?: EscrowWrapper, [CeloContract.Exchange]?: ExchangeWrapper // [CeloContract.GasCurrencyWhitelist]?: GasCurrencyWhitelistWrapper, [CeloContract.GasPriceMinimum]?: GasPriceMinimumWrapper [CeloContract.GoldToken]?: GoldTokenWrapper [CeloContract.Governance]?: GovernanceWrapper + [CeloContract.LockedGold]?: LockedGoldWrapper // [CeloContract.MultiSig]?: MultiSigWrapper, // [CeloContract.Random]?: RandomWrapper, // [CeloContract.Registry]?: RegistryWrapper, @@ -59,8 +62,8 @@ export class WrapperCache { getAttestations() { return this.getContract(CeloContract.Attestations) } - getLockedGold() { - return this.getContract(CeloContract.LockedGold) + getElection() { + return this.getContract(CeloContract.Election) } // getEscrow() { // return this.getWrapper(CeloContract.Escrow, newEscrow) @@ -80,6 +83,9 @@ export class WrapperCache { getGovernance() { return this.getContract(CeloContract.Governance) } + getLockedGold() { + return this.getContract(CeloContract.LockedGold) + } // getMultiSig() { // return this.getWrapper(CeloContract.MultiSig, newMultiSig) // } diff --git a/packages/contractkit/src/index.ts b/packages/contractkit/src/index.ts index 84877b7a0a7..b9410271262 100644 --- a/packages/contractkit/src/index.ts +++ b/packages/contractkit/src/index.ts @@ -4,7 +4,6 @@ export { Address, AllContracts, CeloContract, CeloToken, NULL_ADDRESS } from './ export { IdentityMetadataWrapper } from './identity' export * from './kit' export { CeloTransactionObject } from './wrappers/BaseWrapper' -export { Roles } from './wrappers/LockedGold' export function newWeb3(url: string) { return new Web3(url) diff --git a/packages/contractkit/src/kit.ts b/packages/contractkit/src/kit.ts index 61fdebc0817..fc64ef4ff6c 100644 --- a/packages/contractkit/src/kit.ts +++ b/packages/contractkit/src/kit.ts @@ -8,6 +8,7 @@ import { toTxResult, TransactionResult } from './utils/tx-result' import { addLocalAccount } from './utils/web3-utils' import { Web3ContractCache } from './web3-contract-cache' import { AttestationsConfig } from './wrappers/Attestations' +import { ElectionConfig } from './wrappers/Election' import { ExchangeConfig } from './wrappers/Exchange' import { GasPriceMinimumConfig } from './wrappers/GasPriceMinimum' import { GovernanceConfig } from './wrappers/Governance' @@ -15,7 +16,7 @@ import { LockedGoldConfig } from './wrappers/LockedGold' import { ReserveConfig } from './wrappers/Reserve' import { SortedOraclesConfig } from './wrappers/SortedOracles' import { StableTokenConfig } from './wrappers/StableTokenWrapper' -import { ValidatorConfig } from './wrappers/Validators' +import { ValidatorsConfig } from './wrappers/Validators' export function newKit(url: string) { return newKitFromWeb3(new Web3(url)) @@ -26,6 +27,7 @@ export function newKitFromWeb3(web3: Web3) { } export interface NetworkConfig { + election: ElectionConfig exchange: ExchangeConfig attestations: AttestationsConfig governance: GovernanceConfig @@ -34,7 +36,7 @@ export interface NetworkConfig { gasPriceMinimum: GasPriceMinimumConfig reserve: ReserveConfig stableToken: StableTokenConfig - validators: ValidatorConfig + validators: ValidatorsConfig } export class ContractKit { @@ -58,6 +60,7 @@ export class ContractKit { const token2 = await this.registry.addressFor(CeloContract.StableToken) const contracts = await Promise.all([ this.contracts.getExchange(), + this.contracts.getElection(), this.contracts.getAttestations(), this.contracts.getGovernance(), this.contracts.getLockedGold(), @@ -69,25 +72,27 @@ export class ContractKit { ]) const res = await Promise.all([ contracts[0].getConfig(), - contracts[1].getConfig([token1, token2]), - contracts[2].getConfig(), + contracts[1].getConfig(), + contracts[2].getConfig([token1, token2]), contracts[3].getConfig(), contracts[4].getConfig(), contracts[5].getConfig(), contracts[6].getConfig(), contracts[7].getConfig(), contracts[8].getConfig(), + contracts[9].getConfig(), ]) return { exchange: res[0], - attestations: res[1], - governance: res[2], - lockedGold: res[3], - sortedOracles: res[4], - gasPriceMinimum: res[5], - reserve: res[6], - stableToken: res[7], - validators: res[8], + election: res[1], + attestations: res[2], + governance: res[3], + lockedGold: res[4], + sortedOracles: res[5], + gasPriceMinimum: res[6], + reserve: res[7], + stableToken: res[8], + validators: res[9], } } diff --git a/packages/contractkit/src/web3-contract-cache.ts b/packages/contractkit/src/web3-contract-cache.ts index 872afd68bae..e89aba191c5 100644 --- a/packages/contractkit/src/web3-contract-cache.ts +++ b/packages/contractkit/src/web3-contract-cache.ts @@ -1,6 +1,7 @@ import debugFactory from 'debug' import { CeloContract } from './base' import { newAttestations } from './generated/Attestations' +import { newElection } from './generated/Election' import { newEscrow } from './generated/Escrow' import { newExchange } from './generated/Exchange' import { newGasCurrencyWhitelist } from './generated/GasCurrencyWhitelist' @@ -20,13 +21,14 @@ const debug = debugFactory('kit:web3-contract-cache') const ContractFactories = { [CeloContract.Attestations]: newAttestations, - [CeloContract.LockedGold]: newLockedGold, + [CeloContract.Election]: newElection, [CeloContract.Escrow]: newEscrow, [CeloContract.Exchange]: newExchange, [CeloContract.GasCurrencyWhitelist]: newGasCurrencyWhitelist, [CeloContract.GasPriceMinimum]: newGasPriceMinimum, [CeloContract.GoldToken]: newGoldToken, [CeloContract.Governance]: newGovernance, + [CeloContract.LockedGold]: newLockedGold, [CeloContract.Random]: newRandom, [CeloContract.Registry]: newRegistry, [CeloContract.Reserve]: newReserve, @@ -49,6 +51,9 @@ export class Web3ContractCache { getLockedGold() { return this.getContract(CeloContract.LockedGold) } + getElection() { + return this.getContract(CeloContract.Election) + } getEscrow() { return this.getContract(CeloContract.Escrow) } diff --git a/packages/contractkit/src/wrappers/LockedGold.ts b/packages/contractkit/src/wrappers/LockedGold.ts index a63e34b8d3c..0394e0f0dd4 100644 --- a/packages/contractkit/src/wrappers/LockedGold.ts +++ b/packages/contractkit/src/wrappers/LockedGold.ts @@ -1,7 +1,6 @@ import { zip } from '@celo/utils/lib/collections' import BigNumber from 'bignumber.js' import Web3 from 'web3' -import { TransactionObject } from 'web3/eth/types' import { Address } from '../base' import { LockedGold } from '../generated/types/LockedGold' import { @@ -19,180 +18,89 @@ export interface VotingDetails { weight: BigNumber } -interface Commitment { +interface PendingWithdrawal { time: BigNumber value: BigNumber } -export interface Commitments { - locked: Commitment[] - notified: Commitment[] - total: { - gold: BigNumber - weight: BigNumber - } -} - -export enum Roles { - Validating = '0', - Voting = '1', - Rewards = '2', -} - export interface LockedGoldConfig { - maxNoticePeriod: BigNumber + unlockingPeriod: BigNumber } /** * Contract for handling deposits needed for voting. */ export class LockedGoldWrapper extends BaseWrapper { - notifyCommitment = proxySend(this.kit, this.contract.methods.notifyCommitment) + unlock = proxySend(this.kit, this.contract.methods.unlock) createAccount = proxySend(this.kit, this.contract.methods.createAccount) - withdrawCommitment = proxySend(this.kit, this.contract.methods.withdrawCommitment) - redeemRewards = proxySend(this.kit, this.contract.methods.redeemRewards) - newCommitment = proxySend(this.kit, this.contract.methods.newCommitment) - extendCommitment = proxySend(this.kit, this.contract.methods.extendCommitment) - isVoting = proxyCall(this.contract.methods.isVoting) - /** - * Query maximum notice period. - * @returns Current maximum notice period. - */ - maxNoticePeriod = proxyCall(this.contract.methods.maxNoticePeriod, undefined, toBigNumber) - - getAccountWeight = proxyCall(this.contract.methods.getAccountWeight, undefined, toBigNumber) - /** - * Get the delegate for a role. - * @param account Address of the active account. - * @param role one of Roles Enum ("validating", "voting", "rewards") - * @return Address of the delegate - */ - getDelegateFromAccountAndRole: (account: string, role: Roles) => Promise
= proxyCall( - this.contract.methods.getDelegateFromAccountAndRole + withdraw = proxySend(this.kit, this.contract.methods.withdraw) + lock = proxySend(this.kit, this.contract.methods.lock) + relock = proxySend(this.kit, this.contract.methods.relock) + + getAccountTotalLockedGold = proxyCall( + this.contract.methods.getAccountTotalLockedGold, + undefined, + toBigNumber + ) + getAccountNonvotingLockedGold = proxyCall( + this.contract.methods.getAccountNonvotingLockedGold, + undefined, + toBigNumber + ) + getVoterFromAccount: (account: string) => Promise
= proxyCall( + this.contract.methods.getVoterFromAccount + ) + getValidatorFromAccount: (account: string) => Promise
= proxyCall( + this.contract.methods.getValidatorFromAccount ) /** * Returns current configuration parameters. */ - async getConfig(): Promise { return { - maxNoticePeriod: await this.maxNoticePeriod(), - } - } - - async getVotingDetails(accountOrVoterAddress: Address): Promise { - const accountAddress = await this.contract.methods - .getAccountFromDelegateAndRole(accountOrVoterAddress, Roles.Voting) - .call() - - return { - accountAddress, - voterAddress: accountOrVoterAddress, - weight: await this.getAccountWeight(accountAddress), - } - } - - async getLockedCommitmentValue(account: string, noticePeriod: string): Promise { - const commitment = await this.contract.methods.getLockedCommitment(account, noticePeriod).call() - return this.getValueFromCommitment(commitment) - } - - async getLockedCommitments(account: string): Promise { - return this.zipAccountTimesAndValuesToCommitments( - account, - this.contract.methods.getNoticePeriods, - this.getLockedCommitmentValue.bind(this) - ) - } - - async getNotifiedCommitmentValue(account: string, availTime: string): Promise { - const commitment = await this.contract.methods.getNotifiedCommitment(account, availTime).call() - return this.getValueFromCommitment(commitment) - } - - async getNotifiedCommitments(account: string): Promise { - return this.zipAccountTimesAndValuesToCommitments( - account, - this.contract.methods.getAvailabilityTimes, - this.getNotifiedCommitmentValue.bind(this) - ) - } - - async getCommitments(account: string): Promise { - const locked = await this.getLockedCommitments(account) - const notified = await this.getNotifiedCommitments(account) - const weight = await this.getAccountWeight(account) - - const totalLocked = locked.reduce( - (acc, commitment) => acc.plus(commitment.value), - new BigNumber(0) - ) - const gold = notified.reduce((acc, commitment) => acc.plus(commitment.value), totalLocked) - - return { - locked, - notified, - total: { weight, gold }, + unlockingPeriod: toBigNumber(await this.contract.methods.unlockingPeriod().call()), } } /** - * Delegate a Role to another account. + * Authorize voting on behalf of this account to another address. * @param account Address of the active account. - * @param delegate Address of the delegate - * @param role one of Roles Enum ("Validating", "Voting", "Rewards") + * @param voter Address to be used for voting. * @return A CeloTransactionObject */ - async delegateRoleTx( - account: Address, - delegate: Address, - role: Roles - ): Promise> { - const sig = await this.getParsedSignatureOfAddress(account, delegate) - return wrapSend( - this.kit, - this.contract.methods.delegateRole(role, delegate, sig.v, sig.r, sig.s) - ) + async authorizeVoter(account: Address, voter: Address): Promise> { + const sig = await this.getParsedSignatureOfAddress(account, voter) + return wrapSend(this.kit, this.contract.methods.authorizeVoter(voter, sig.v, sig.r, sig.s)) } /** - * Delegate a Rewards to another account. + * Authorize validating on behalf of this account to another address. * @param account Address of the active account. - * @param delegate Address of the delegate + * @param voter Address to be used for validating. * @return A CeloTransactionObject */ - async delegateRewards(account: Address, delegate: Address): Promise> { - return this.delegateRoleTx(account, delegate, Roles.Rewards) - } - - /** - * Delegate a voting to another account. - * @param account Address of the active account. - * @param delegate Address of the delegate - * @return A CeloTransactionObject - */ - async delegateVoting(account: Address, delegate: Address): Promise> { - return this.delegateRoleTx(account, delegate, Roles.Voting) - } - - /** - * Delegate a validating to another account. - * @param account Address of the active account. - * @param delegate Address of the delegate - * @return A CeloTransactionObject - */ - async delegateValidating( + async authorizeValidator( account: Address, - delegate: Address + validator: Address ): Promise> { - return this.delegateRoleTx(account, delegate, Roles.Validating) + const sig = await this.getParsedSignatureOfAddress(account, validator) + return wrapSend( + this.kit, + this.contract.methods.authorizeValidator(validator, sig.v, sig.r, sig.s) + ) } - private getValueFromCommitment(commitment: { 0: string; 1: string }) { - return new BigNumber(commitment[0]) + async getPendingWithdrawals(account: string) { + const withdrawals = await this.contract.methods.getPendingWithdrawals(account).call() + return zip( + // tslint:disable-next-line: no-object-literal-type-assertion + (time, value) => + ({ time: toBigNumber(time), value: toBigNumber(value) } as PendingWithdrawal), + withdrawals[1], + withdrawals[0] + ) } - private async getParsedSignatureOfAddress(address: string, signer: string) { const hash = Web3.utils.soliditySha3({ type: 'address', value: address }) const signature = (await this.kit.web3.eth.sign(hash, signer)).slice(2) @@ -202,19 +110,4 @@ export class LockedGoldWrapper extends BaseWrapper { v: Web3.utils.hexToNumber(signature.slice(128, 130)) + 27, } } - - private async zipAccountTimesAndValuesToCommitments( - account: string, - timesFunc: (account: string) => TransactionObject, - valueFunc: (account: string, time: string) => Promise - ) { - const accountTimes = await timesFunc(account).call() - const accountValues = await Promise.all(accountTimes.map((time) => valueFunc(account, time))) - return zip( - // tslint:disable-next-line: no-object-literal-type-assertion - (time, value) => ({ time, value } as Commitment), - accountTimes.map((time) => new BigNumber(time)), - accountValues - ) - } } diff --git a/packages/contractkit/src/wrappers/Validators.ts b/packages/contractkit/src/wrappers/Validators.ts index fe9ae06f560..37922243db9 100644 --- a/packages/contractkit/src/wrappers/Validators.ts +++ b/packages/contractkit/src/wrappers/Validators.ts @@ -1,21 +1,10 @@ -import { eqAddress } from '@celo/utils/lib/address' -import { zip } from '@celo/utils/lib/collections' import BigNumber from 'bignumber.js' -import { Address, NULL_ADDRESS } from '../base' +import { Address } from '../base' import { Validators } from '../generated/types/Validators' -import { - BaseWrapper, - CeloTransactionObject, - proxyCall, - proxySend, - toBigNumber, - toNumber, - wrapSend, -} from './BaseWrapper' +import { BaseWrapper, proxySend, toBigNumber } from './BaseWrapper' export interface Validator { address: Address - id: string name: string url: string publicKey: string @@ -24,27 +13,25 @@ export interface Validator { export interface ValidatorGroup { address: Address - id: string name: string url: string members: Address[] } -export interface ValidatorGroupVote { - address: Address - votes: BigNumber +export interface RegistrationRequirements { + group: BigNumber + validator: BigNumber } -export interface RegistrationRequirement { - minLockedGoldValue: BigNumber - minLockedGoldNoticePeriod: BigNumber +export interface DeregistrationLockups { + group: BigNumber + validator: BigNumber } -export interface ValidatorConfig { - minElectableValidators: BigNumber - maxElectableValidators: BigNumber - electionThreshold: BigNumber - registrationRequirement: RegistrationRequirement +export interface ValidatorsConfig { + registrationRequirements: RegistrationRequirements + deregistrationLockups: DeregistrationLockups + maxGroupSize: BigNumber } /** @@ -58,66 +45,38 @@ export class ValidatorsWrapper extends BaseWrapper { registerValidator = proxySend(this.kit, this.contract.methods.registerValidator) registerValidatorGroup = proxySend(this.kit, this.contract.methods.registerValidatorGroup) /** - * Returns the minimum number of validators that can be elected. - * @returns The minimum number of validators that can be elected. - */ - minElectableValidators = proxyCall( - this.contract.methods.minElectableValidators, - undefined, - toBigNumber - ) - /** - * Returns the maximum number of validators that can be elected. - * @returns The maximum number of validators that can be elected. + * Returns the current registration requirements. + * @returns Group and validator registration requirements. */ - maxElectableValidators = proxyCall( - this.contract.methods.maxElectableValidators, - undefined, - toBigNumber - ) - /** - * Returns the current election threshold. - * @returns Election threshold. - */ - electionThreshold = proxyCall(this.contract.methods.getElectionThreshold, undefined, toBigNumber) - validatorAddressFromCurrentSet = proxyCall(this.contract.methods.validatorAddressFromCurrentSet) - numberValidatorsInCurrentSet = proxyCall( - this.contract.methods.numberValidatorsInCurrentSet, - undefined, - toNumber - ) - - getVoteFrom: (validatorAddress: Address) => Promise
= proxyCall( - this.contract.methods.voters - ) + async getRegistrationRequirements(): Promise { + const res = await this.contract.methods.getRegistrationRequirements().call() + return { + group: toBigNumber(res[0]), + validator: toBigNumber(res[1]), + } + } - /** - * Returns the current registrations requirements. - * @returns Minimum deposit and notice period. - */ - async getRegistrationRequirement(): Promise { - const res = await this.contract.methods.getRegistrationRequirement().call() + async getDeregistrationLockups(): Promise { + const res = await this.contract.methods.getDeregistrationLockups().call() return { - minLockedGoldValue: toBigNumber(res[0]), - minLockedGoldNoticePeriod: toBigNumber(res[0]), + group: toBigNumber(res[0]), + validator: toBigNumber(res[1]), } } /** * Returns current configuration parameters. */ - async getConfig(): Promise { + async getConfig(): Promise { const res = await Promise.all([ - this.minElectableValidators(), - this.maxElectableValidators(), - this.electionThreshold(), - this.getRegistrationRequirement(), + this.getRegistrationRequirements(), + this.getDeregistrationLockups(), + this.contract.methods.maxGroupSize().call(), ]) return { - minElectableValidators: res[0], - maxElectableValidators: res[1], - electionThreshold: res[2], - registrationRequirement: res[3], + registrationRequirements: res[0], + deregistrationLockups: res[1], + maxGroupSize: toBigNumber(res[2]), } } @@ -127,27 +86,14 @@ export class ValidatorsWrapper extends BaseWrapper { return Promise.all(vgAddresses.map((addr) => this.getValidator(addr))) } - async getValidatorSetAddresses(): Promise { - const numberValidators = await this.numberValidatorsInCurrentSet() - - const validatorAddressPromises = [] - - for (let i = 0; i < numberValidators; i++) { - validatorAddressPromises.push(this.validatorAddressFromCurrentSet(i)) - } - - return Promise.all(validatorAddressPromises) - } - async getValidator(address: Address): Promise { const res = await this.contract.methods.getValidator(address).call() return { address, - id: res[0], - name: res[1], - url: res[2], - publicKey: res[3] as any, - affiliation: res[4], + name: res[0], + url: res[1], + publicKey: res[2] as any, + affiliation: res[3], } } @@ -158,85 +104,6 @@ export class ValidatorsWrapper extends BaseWrapper { async getValidatorGroup(address: Address): Promise { const res = await this.contract.methods.getValidatorGroup(address).call() - return { address, id: res[0], name: res[1], url: res[2], members: res[3] } - } - - async getValidatorGroupsVotes(): Promise { - const vgAddresses = await this.contract.methods.getRegisteredValidatorGroups().call() - const res = await this.contract.methods.getValidatorGroupVotes().call() - const r = zip((a, b) => ({ address: a, votes: new BigNumber(b) }), res[0], res[1]) - for (const vgAddress of vgAddresses) { - if (!res[0].includes(vgAddress)) { - r.push({ address: vgAddress, votes: new BigNumber(0) }) - } - } - return r - } - - async revokeVote(): Promise> { - if (this.kit.defaultAccount == null) { - throw new Error(`missing from at new ValdidatorUtils()`) - } - - const lockedGold = await this.kit.contracts.getLockedGold() - const votingDetails = await lockedGold.getVotingDetails(this.kit.defaultAccount) - const votedGroup = await this.getVoteFrom(votingDetails.accountAddress) - - if (votedGroup == null) { - throw new Error(`Not current vote for ${this.kit.defaultAccount}`) - } - - const { lesser, greater } = await this.findLesserAndGreaterAfterVote( - votedGroup, - votingDetails.weight.negated() - ) - - return wrapSend(this.kit, this.contract.methods.revokeVote(lesser, greater)) - } - - async vote(validatorGroup: Address): Promise> { - if (this.kit.defaultAccount == null) { - throw new Error(`missing from at new ValdidatorUtils()`) - } - - const lockedGold = await this.kit.contracts.getLockedGold() - const votingDetails = await lockedGold.getVotingDetails(this.kit.defaultAccount) - - const { lesser, greater } = await this.findLesserAndGreaterAfterVote( - validatorGroup, - votingDetails.weight - ) - - return wrapSend(this.kit, this.contract.methods.vote(validatorGroup, lesser, greater)) - } - - private async findLesserAndGreaterAfterVote( - votedGroup: Address, - voteWeight: BigNumber - ): Promise<{ lesser: Address; greater: Address }> { - const currentVotes = (await this.getValidatorGroupsVotes()).filter((g) => !g.votes.isZero()) - - const selectedGroup = currentVotes.find((cv) => eqAddress(cv.address, votedGroup)) - - // modify the list - if (selectedGroup) { - selectedGroup.votes = selectedGroup.votes.plus(voteWeight) - } else { - currentVotes.push({ - address: votedGroup, - votes: voteWeight, - }) - } - - // re-sort - currentVotes.sort((a, b) => a.votes.comparedTo(b.votes)) - - // find new index - const newIdx = currentVotes.findIndex((cv) => eqAddress(cv.address, votedGroup)) - - return { - lesser: newIdx === 0 ? NULL_ADDRESS : currentVotes[newIdx - 1].address, - greater: newIdx === currentVotes.length - 1 ? NULL_ADDRESS : currentVotes[newIdx + 1].address, - } + return { address, name: res[0], url: res[1], members: res[2] } } } diff --git a/packages/protocol/contracts/governance/Election.sol b/packages/protocol/contracts/governance/Election.sol index 937515784c6..f4ed545dbca 100644 --- a/packages/protocol/contracts/governance/Election.sol +++ b/packages/protocol/contracts/governance/Election.sol @@ -54,13 +54,13 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { ActiveVotes active; TotalVotes total; // Maps an account to the list of groups it's voting for. - mapping(address => address[]) lists; + mapping(address => address[]) groupsVotedFor; } Votes private votes; uint256 public minElectableValidators; uint256 public maxElectableValidators; - uint256 public maxVotesPerAccount; + uint256 public maxNumGroupsVotedFor; FixidityLib.Fraction public electabilityThreshold; event MinElectableValidatorsSet( @@ -71,8 +71,8 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { uint256 maxElectableValidators ); - event MaxVotesPerAccountSet( - uint256 maxVotesPerAccount + event MaxNumGroupsVotedForSet( + uint256 maxNumGroupsVotedFor ); event ElectabilityThresholdSet( @@ -109,7 +109,7 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { * @notice Initializes critical variables. * @param registryAddress The address of the registry contract. * @param _minElectableValidators The minimum number of validators that can be elected. - * @param _maxVotesPerAccount The maximum number of groups that an acconut can vote for at once. + * @param _maxNumGroupsVotedFor The maximum number of groups that an acconut can vote for at once. * @param _electabilityThreshold The minimum ratio of votes a group needs before its members can * be elected. * @dev Should be called only once. @@ -118,7 +118,7 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { address registryAddress, uint256 _minElectableValidators, uint256 _maxElectableValidators, - uint256 _maxVotesPerAccount, + uint256 _maxNumGroupsVotedFor, uint256 _electabilityThreshold ) external @@ -129,7 +129,7 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { setRegistry(registryAddress); minElectableValidators = _minElectableValidators; maxElectableValidators = _maxElectableValidators; - maxVotesPerAccount = _maxVotesPerAccount; + maxNumGroupsVotedFor = _maxNumGroupsVotedFor; electabilityThreshold = FixidityLib.wrap(_electabilityThreshold); } @@ -178,13 +178,13 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { /** * @notice Updates the maximum number of groups an account can be voting for at once. - * @param _maxVotesPerAccount The maximum number of groups an account can vote for. + * @param _maxNumGroupsVotedFor The maximum number of groups an account can vote for. * @return True upon success. */ - function setMaxVotesPerAccount(uint256 _maxVotesPerAccount) external onlyOwner returns (bool) { - require(_maxVotesPerAccount != maxVotesPerAccount); - maxVotesPerAccount = _maxVotesPerAccount; - emit MaxVotesPerAccountSet(_maxVotesPerAccount); + function setMaxNumGroupsVotedFor(uint256 _maxNumGroupsVotedFor) external onlyOwner returns (bool) { + require(_maxNumGroupsVotedFor != maxNumGroupsVotedFor); + maxNumGroupsVotedFor = _maxNumGroupsVotedFor; + emit MaxNumGroupsVotedForSet(_maxNumGroupsVotedFor); return true; } @@ -241,12 +241,12 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { require(votes.total.eligible.contains(group)); require(0 < value && value <= getNumVotesReceivable(group)); address account = getLockedGold().getAccountFromVoter(msg.sender); - address[] storage list = votes.lists[account]; - require(list.length < maxVotesPerAccount); - for (uint256 i = 0; i < list.length; i = i.add(1)) { - require(list[i] != group); + address[] storage groups = votes.groupsVotedFor[account]; + require(groups.length < maxNumGroupsVotedFor); + for (uint256 i = 0; i < groups.length; i = i.add(1)) { + require(groups[i] != group); } - list.push(group); + groups.push(group); incrementPendingVotes(group, account, value); incrementTotalVotes(group, value, lesser, greater); getLockedGold().decrementNonvotingAccountBalance(account, value); @@ -299,7 +299,7 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { decrementTotalVotes(group, value, lesser, greater); getLockedGold().incrementNonvotingAccountBalance(account, value); if (getAccountTotalVotesForGroup(group, account) == 0) { - deleteElement(votes.lists[account], group, index); + deleteElement(votes.groupsVotedFor[account], group, index); } emit ValidatorGroupVoteRevoked(account, group, value); return true; @@ -335,7 +335,7 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { decrementTotalVotes(group, value, lesser, greater); getLockedGold().incrementNonvotingAccountBalance(account, value); if (getAccountTotalVotesForGroup(group, account) == 0) { - deleteElement(votes.lists[account], group, index); + deleteElement(votes.groupsVotedFor[account], group, index); } emit ValidatorGroupVoteRevoked(account, group, value); return true; @@ -343,7 +343,7 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { function getAccountTotalVotes(address account) external view returns (uint256) { uint256 total = 0; - address[] memory groups = votes.lists[account]; + address[] memory groups = votes.groupsVotedFor[account]; for (uint256 i = 0; i < groups.length; i = i.add(1)) { total = total.add(getAccountTotalVotesForGroup(groups[i], account)); } @@ -356,7 +356,7 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { * @return The groups voted for by a particular account. */ function getAccountGroupsVotedFor(address account) external view returns (address[] memory) { - return votes.lists[account]; + return votes.groupsVotedFor[account]; } /** @@ -423,7 +423,11 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { * @return The total votes made for `group`. */ function getGroupTotalVotes(address group) external view returns (uint256) { - return votes.total.eligible.getValue(group); + return votes.pending.total[group].add(votes.active.total[group]); + } + + function getGroupEligibility(address group) external view returns (bool) { + return votes.total.eligible.contains(group); } /** @@ -495,8 +499,8 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { * @param greater The address of the group that has received more votes than this group. */ function markGroupEligible(address group, address lesser, address greater) external { - require(!votes.total.eligible.contains(group), "aaa"); - require(getValidators().getGroupNumMembers(group) > 0, "b"); + require(!votes.total.eligible.contains(group)); + require(getValidators().getGroupNumMembers(group) > 0); uint256 value = votes.pending.total[group].add(votes.active.total[group]); votes.total.eligible.insert(group, value, lesser, greater); emit ValidatorGroupMarkedEligible(group); @@ -572,6 +576,10 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { ); } + function getGroupsVotedFor(address account) external view returns (address[] memory) { + return votes.groupsVotedFor[account]; + } + /** * @notice Deletes an element from a list of addresses. * @param list The list of addresses. diff --git a/packages/protocol/scripts/build.ts b/packages/protocol/scripts/build.ts index 4330d07a2e2..6106cd275c4 100644 --- a/packages/protocol/scripts/build.ts +++ b/packages/protocol/scripts/build.ts @@ -8,14 +8,15 @@ const BUILD_DIR = path.join(ROOT_DIR, 'build') const CONTRACTKIT_GEN_DIR = path.normalize(path.join(ROOT_DIR, '../contractkit/src/generated')) export const ProxyContracts = [ - 'GasCurrencyWhitelistProxy', - 'GasPriceMinimumProxy', - 'MultiSigProxy', - 'LockedGoldProxy', 'AttestationsProxy', + 'ElectionProxy', 'EscrowProxy', 'ExchangeProxy', + 'GasCurrencyWhitelistProxy', + 'GasPriceMinimumProxy', 'GoldTokenProxy', + 'LockedGoldProxy', + 'MultiSigProxy', 'ReserveProxy', 'StableTokenProxy', 'SortedOraclesProxy', @@ -32,8 +33,10 @@ export const CoreContracts = [ 'Validators', // governance - 'LockedGold', + 'Election', 'Governance', + 'LockedGold', + 'Validators', // identity 'Attestations', From 15d2e8cf8ce0d38db32ef0f9224f50cc0f7c4a10 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Wed, 2 Oct 2019 19:04:20 -0700 Subject: [PATCH 020/149] cli builds --- .../cli/src/commands/account/isvalidator.ts | 2 +- .../cli/src/commands/election/validatorset.ts | 18 ++ packages/cli/src/commands/election/vote.ts | 31 +++ .../cli/src/commands/lockedgold/authorize.ts | 49 +++++ packages/cli/src/commands/lockedgold/list.ts | 54 ----- packages/cli/src/commands/lockedgold/lock.ts | 40 ++++ .../cli/src/commands/lockedgold/rewards.ts | 51 ----- packages/cli/src/commands/lockedgold/show.ts | 7 +- .../cli/src/commands/lockedgold/unlock.ts | 26 +++ .../cli/src/commands/lockedgold/withdraw.ts | 9 +- packages/cli/src/utils/key_generator.test.ts | 2 +- packages/contractkit/src/wrappers/Election.ts | 206 ++++++++++++++++++ 12 files changed, 377 insertions(+), 118 deletions(-) create mode 100644 packages/cli/src/commands/election/validatorset.ts create mode 100644 packages/cli/src/commands/election/vote.ts create mode 100644 packages/cli/src/commands/lockedgold/authorize.ts delete mode 100644 packages/cli/src/commands/lockedgold/list.ts create mode 100644 packages/cli/src/commands/lockedgold/lock.ts delete mode 100644 packages/cli/src/commands/lockedgold/rewards.ts create mode 100644 packages/cli/src/commands/lockedgold/unlock.ts create mode 100644 packages/contractkit/src/wrappers/Election.ts diff --git a/packages/cli/src/commands/account/isvalidator.ts b/packages/cli/src/commands/account/isvalidator.ts index 15876ea8775..316514dfa8e 100644 --- a/packages/cli/src/commands/account/isvalidator.ts +++ b/packages/cli/src/commands/account/isvalidator.ts @@ -17,7 +17,7 @@ export default class IsValidator extends BaseCommand { async run() { const { args } = this.parse(IsValidator) - const election = await this.kit.contracts.getValidators() + const election = await this.kit.contracts.getElection() const numberValidators = await election.numberValidatorsInCurrentSet() for (let i = 0; i < numberValidators; i++) { diff --git a/packages/cli/src/commands/election/validatorset.ts b/packages/cli/src/commands/election/validatorset.ts new file mode 100644 index 00000000000..74f56f97874 --- /dev/null +++ b/packages/cli/src/commands/election/validatorset.ts @@ -0,0 +1,18 @@ +import { BaseCommand } from '../../base' + +export default class ValidatorSet extends BaseCommand { + static description = 'Outputs the current validator set' + + static flags = { + ...BaseCommand.flags, + } + + static examples = ['validatorset'] + + async run() { + const election = await this.kit.contracts.getElection() + const validatorSet = await election.getValidatorSetAddresses() + + validatorSet.forEach((validator: string) => console.log(validator)) + } +} diff --git a/packages/cli/src/commands/election/vote.ts b/packages/cli/src/commands/election/vote.ts new file mode 100644 index 00000000000..9b466094a1a --- /dev/null +++ b/packages/cli/src/commands/election/vote.ts @@ -0,0 +1,31 @@ +import BigNumber from 'bignumber.js' +import { flags } from '@oclif/command' +import { BaseCommand } from '../../base' +import { displaySendTx } from '../../utils/cli' +import { Flags } from '../../utils/command' + +export default class ElectionVote extends BaseCommand { + static description = 'Vote for a Validator Group in validator elections.' + + static flags = { + ...BaseCommand.flags, + from: Flags.address({ required: true, description: "Voter's address" }), + for: Flags.address({ + description: "Set vote for ValidatorGroup's address", + required: true, + }), + value: flags.string({ description: 'Amount of gold used to vote for group', required: true }), + } + + static examples = [ + 'vote --from 0x4443d0349e8b3075cba511a0a87796597602a0f1 --for 0x932fee04521f5fcb21949041bf161917da3f588b, --value 1000000', + ] + async run() { + const res = this.parse(ElectionVote) + + this.kit.defaultAccount = res.flags.from + const election = await this.kit.contracts.getElection() + const tx = await election.vote(res.flags.for, new BigNumber(res.flags.value)) + await displaySendTx('vote', tx) + } +} diff --git a/packages/cli/src/commands/lockedgold/authorize.ts b/packages/cli/src/commands/lockedgold/authorize.ts new file mode 100644 index 00000000000..50ba89edd63 --- /dev/null +++ b/packages/cli/src/commands/lockedgold/authorize.ts @@ -0,0 +1,49 @@ +import { flags } from '@oclif/command' +import { BaseCommand } from '../../base' +import { displaySendTx } from '../../utils/cli' +import { Flags } from '../../utils/command' + +export default class Authorize extends BaseCommand { + static description = 'Authorize validating or voting address for a Locked Gold account' + + static flags = { + ...BaseCommand.flags, + from: Flags.address({ required: true }), + role: flags.string({ + char: 'r', + options: ['voter', 'validator'], + description: 'Role to delegate', + }), + to: Flags.address({ required: true }), + } + + static args = [] + + static examples = [ + 'authorize --from 0x5409ED021D9299bf6814279A6A1411A7e866A631 --role voter --to 0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d', + ] + + async run() { + const res = this.parse(Authorize) + + if (!res.flags.role) { + this.error(`Specify role with --role`) + return + } + + if (!res.flags.to) { + this.error(`Specify authorized address with --to`) + return + } + + this.kit.defaultAccount = res.flags.from + const lockedGold = await this.kit.contracts.getLockedGold() + let tx: any + if (res.flags.role == 'voter') { + tx = await lockedGold.authorizeVoter(res.flags.from, res.flags.to) + } else if (res.flags.role == 'validator') { + tx = await lockedGold.authorizeValidator(res.flags.from, res.flags.to) + } + await displaySendTx('authorizeTx', tx) + } +} diff --git a/packages/cli/src/commands/lockedgold/list.ts b/packages/cli/src/commands/lockedgold/list.ts deleted file mode 100644 index 12da130666d..00000000000 --- a/packages/cli/src/commands/lockedgold/list.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Roles } from '@celo/contractkit' -import chalk from 'chalk' -import { cli } from 'cli-ux' -import { BaseCommand } from '../../base' -import { Args } from '../../utils/command' - -export default class List extends BaseCommand { - static description = "View information about all of the account's commitments" - - static flags = { - ...BaseCommand.flags, - } - - static args = [Args.address('account')] - - static examples = ['list 0x5409ed021d9299bf6814279a6a1411a7e866a631'] - - async run() { - const { args } = this.parse(List) - cli.action.start('Fetching commitments and delegates...') - const lockedGold = await this.kit.contracts.getLockedGold() - const commitments = await lockedGold.getCommitments(args.account) - const delegates = await Promise.all( - Object.keys(Roles).map(async (role: string) => ({ - role: role, - address: await lockedGold.getDelegateFromAccountAndRole( - args.account, - Roles[role as keyof typeof Roles] - ), - })) - ) - cli.action.stop() - - cli.table(delegates, { - role: { header: 'Role', get: (a) => a.role }, - delegate: { get: (a) => a.address }, - }) - - cli.log(chalk.bold.yellow('Total Gold Locked \t') + commitments.total.gold) - cli.log(chalk.bold.red('Total Account Weight \t') + commitments.total.weight) - if (commitments.locked.length > 0) { - cli.table(commitments.locked, { - noticePeriod: { header: 'NoticePeriod', get: (a) => a.time.toString() }, - value: { get: (a) => a.value.toString() }, - }) - } - if (commitments.notified.length > 0) { - cli.table(commitments.notified, { - availabilityTime: { header: 'AvailabilityTime', get: (a) => a.time.toString() }, - value: { get: (a) => a.value.toString() }, - }) - } - } -} diff --git a/packages/cli/src/commands/lockedgold/lock.ts b/packages/cli/src/commands/lockedgold/lock.ts new file mode 100644 index 00000000000..ce086359e7e --- /dev/null +++ b/packages/cli/src/commands/lockedgold/lock.ts @@ -0,0 +1,40 @@ +import { Address } from '@celo/utils/lib/address' +import { flags } from '@oclif/command' +import BigNumber from 'bignumber.js' +import { BaseCommand } from '../../base' +import { displaySendTx, failWith } from '../../utils/cli' +import { Flags } from '../../utils/command' +import { LockedGoldArgs } from '../../utils/lockedgold' + +export default class Lock extends BaseCommand { + static description = 'Locks Celo Gold to be used in governance and validator elections.' + + static flags = { + ...BaseCommand.flags, + from: flags.string({ ...Flags.address, required: true }), + goldAmount: flags.string({ ...LockedGoldArgs.goldAmountArg, required: true }), + } + + static args = [] + + static examples = [ + 'lock --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --goldAmount 1000000000000000000', + ] + + async run() { + const res = this.parse(Lock) + const address: Address = res.flags.from + + this.kit.defaultAccount = address + const lockedGold = await this.kit.contracts.getLockedGold() + + const goldAmount = new BigNumber(res.flags.goldAmount) + + if (!goldAmount.gt(new BigNumber(0))) { + failWith(`require(goldAmount > 0) => [${goldAmount}]`) + } + + const tx = lockedGold.lock() + await displaySendTx('lock', tx, { value: goldAmount.toString() }) + } +} diff --git a/packages/cli/src/commands/lockedgold/rewards.ts b/packages/cli/src/commands/lockedgold/rewards.ts deleted file mode 100644 index 7485f27a87e..00000000000 --- a/packages/cli/src/commands/lockedgold/rewards.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { flags } from '@oclif/command' -import { BaseCommand } from '../../base' -import { displaySendTx } from '../../utils/cli' -import { Flags } from '../../utils/command' - -export default class Rewards extends BaseCommand { - static description = 'Manage rewards for Locked Gold account' - - static flags = { - ...BaseCommand.flags, - from: Flags.address({ required: true }), - redeem: flags.boolean({ - char: 'r', - description: 'Redeem accrued rewards from Locked Gold', - exclusive: ['delegate'], - }), - delegate: Flags.address({ - char: 'd', - description: 'Delegate rewards to provided account', - exclusive: ['redeem'], - }), - } - - static args = [] - - static examples = [ - 'rewards --redeem', - 'rewards --delegate=0x56e172F6CfB6c7D01C1574fa3E2Be7CC73269D95', - ] - - async run() { - const res = this.parse(Rewards) - - if (!res.flags.redeem && !res.flags.delegate) { - this.error(`Specify action with --redeem or --delegate`) - return - } - - this.kit.defaultAccount = res.flags.from - const lockedGold = await this.kit.contracts.getLockedGold() - if (res.flags.redeem) { - const tx = lockedGold.redeemRewards() - await displaySendTx('redeemRewards', tx) - } - - if (res.flags.delegate) { - const tx = await lockedGold.delegateRewards(res.flags.from, res.flags.delegate) - await displaySendTx('delegateRewards', tx) - } - } -} diff --git a/packages/cli/src/commands/lockedgold/show.ts b/packages/cli/src/commands/lockedgold/show.ts index 8966298cf71..b8d12cafce7 100644 --- a/packages/cli/src/commands/lockedgold/show.ts +++ b/packages/cli/src/commands/lockedgold/show.ts @@ -1,11 +1,6 @@ -import { flags } from '@oclif/command' -import BigNumber from 'bignumber.js' -import chalk from 'chalk' -import { cli } from 'cli-ux' import { BaseCommand } from '../../base' import { printValueMap } from '../../utils/cli' import { Args } from '../../utils/command' -import { LockedGoldArgs } from '../../utils/lockedgold' export default class Show extends BaseCommand { static description = 'Show locked gold information for a given account' @@ -20,7 +15,7 @@ export default class Show extends BaseCommand { async run() { // tslint:disable-next-line - const { flags, args } = this.parse(Show) + const { args } = this.parse(Show) const lockedGold = await this.kit.contracts.getLockedGold() const nonvoting = await lockedGold.getAccountNonvotingLockedGold(args.account) diff --git a/packages/cli/src/commands/lockedgold/unlock.ts b/packages/cli/src/commands/lockedgold/unlock.ts new file mode 100644 index 00000000000..23d535fdf20 --- /dev/null +++ b/packages/cli/src/commands/lockedgold/unlock.ts @@ -0,0 +1,26 @@ +import { flags } from '@oclif/command' +import { BaseCommand } from '../../base' +import { displaySendTx } from '../../utils/cli' +import { Flags } from '../../utils/command' +import { LockedGoldArgs } from '../../utils/lockedgold' + +export default class Unlock extends BaseCommand { + static description = 'Unlocks Celo Gold, which can be withdrawn after the unlocking period.' + + static flags = { + ...BaseCommand.flags, + from: Flags.address({ required: true }), + goldAmount: flags.string({ ...LockedGoldArgs.goldAmountArg, required: true }), + } + + static args = [] + + static examples = ['unlock --goldAmount 500000000'] + + async run() { + const res = this.parse(Unlock) + this.kit.defaultAccount = res.flags.from + const lockedgold = await this.kit.contracts.getLockedGold() + await displaySendTx('unlock', lockedgold.unlock(res.flags.goldAmount)) + } +} diff --git a/packages/cli/src/commands/lockedgold/withdraw.ts b/packages/cli/src/commands/lockedgold/withdraw.ts index b6f58968402..cbeb0288890 100644 --- a/packages/cli/src/commands/lockedgold/withdraw.ts +++ b/packages/cli/src/commands/lockedgold/withdraw.ts @@ -1,7 +1,6 @@ import { BaseCommand } from '../../base' import { displaySendTx } from '../../utils/cli' import { Flags } from '../../utils/command' -import { LockedGoldArgs } from '../../utils/lockedgold' export default class Withdraw extends BaseCommand { static description = 'Withdraw unlocked gold whose unlocking period has passed.' @@ -11,18 +10,18 @@ export default class Withdraw extends BaseCommand { from: Flags.address({ required: true }), } - static examples = ['withdraw'] + static examples = ['withdraw --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95'] async run() { // tslint:disable-next-line - const { flags, args } = this.parse(Withdraw) + const { flags } = this.parse(Withdraw) this.kit.defaultAccount = flags.from const lockedgold = await this.kit.contracts.getLockedGold() - const pendingWithdrawals = await lockedgold.getPendingWithdrawals() + const pendingWithdrawals = await lockedgold.getPendingWithdrawals(flags.from) const currentTime = Math.round(new Date().getTime() / 1000) let withdrawals = 0 for (let i = 0; i < pendingWithdrawals.length; i++) { - if (pendingWithdrawals[i].time <= currentTime) { + if (pendingWithdrawals[i].time.isLessThan(currentTime)) { await displaySendTx('withdraw', lockedgold.withdraw(i - withdrawals)) withdrawals += 1 } diff --git a/packages/cli/src/utils/key_generator.test.ts b/packages/cli/src/utils/key_generator.test.ts index 60e4732c792..62038e25dec 100644 --- a/packages/cli/src/utils/key_generator.test.ts +++ b/packages/cli/src/utils/key_generator.test.ts @@ -2,7 +2,7 @@ import { validateMnemonic } from 'bip39' import { ReactNativeBip39MnemonicGenerator } from './key_generator' describe('Mnemonic validation', () => { - it('should generatet 24 word mnemonic', () => { + it('should generate 24 word mnemonic', () => { const mnemonic: string = ReactNativeBip39MnemonicGenerator.generateMnemonic() expect(mnemonic.split(' ').length).toEqual(24) }) diff --git a/packages/contractkit/src/wrappers/Election.ts b/packages/contractkit/src/wrappers/Election.ts new file mode 100644 index 00000000000..df54eea1fad --- /dev/null +++ b/packages/contractkit/src/wrappers/Election.ts @@ -0,0 +1,206 @@ +import { eqAddress } from '@celo/utils/lib/address' +import { zip } from '@celo/utils/lib/collections' +import BigNumber from 'bignumber.js' +import { Address, NULL_ADDRESS } from '../base' +import { Election } from '../generated/types/Election' +import { + BaseWrapper, + CeloTransactionObject, + proxyCall, + proxySend, + toBigNumber, + toNumber, + wrapSend, +} from './BaseWrapper' + +export interface Validator { + address: Address + id: string + name: string + url: string + publicKey: string + affiliation: string | null +} + +export interface ValidatorGroup { + address: Address + id: string + name: string + url: string + members: Address[] +} + +export interface ValidatorGroupVote { + address: Address + votes: BigNumber + eligible: boolean +} + +export interface ElectionConfig { + minElectableValidators: BigNumber + maxElectableValidators: BigNumber + electabilityThreshold: BigNumber + maxNumGroupsVotedFor: BigNumber +} + +/** + * Contract for voting for validators and managing validator groups. + */ +export class ElectionWrapper extends BaseWrapper { + activate = proxySend(this.kit, this.contract.methods.activate) + /** + * Returns the minimum number of validators that can be elected. + * @returns The minimum number of validators that can be elected. + */ + minElectableValidators = proxyCall( + this.contract.methods.minElectableValidators, + undefined, + toBigNumber + ) + /** + * Returns the maximum number of validators that can be elected. + * @returns The maximum number of validators that can be elected. + */ + maxElectableValidators = proxyCall( + this.contract.methods.maxElectableValidators, + undefined, + toBigNumber + ) + /** + * Returns the current election threshold. + * @returns Election threshold. + */ + electabilityThreshold = proxyCall( + this.contract.methods.getElectabilityThreshold, + undefined, + toBigNumber + ) + validatorAddressFromCurrentSet = proxyCall(this.contract.methods.validatorAddressFromCurrentSet) + numberValidatorsInCurrentSet = proxyCall( + this.contract.methods.numberValidatorsInCurrentSet, + undefined, + toNumber + ) + + getGroupsVotedFor: (account: Address) => Promise = proxyCall( + this.contract.methods.getGroupsVotedFor + ) + + /** + * Returns current configuration parameters. + */ + async getConfig(): Promise { + const res = await Promise.all([ + this.minElectableValidators(), + this.maxElectableValidators(), + this.electabilityThreshold(), + this.contract.methods.maxNumGroupsVotedFor().call(), + ]) + return { + minElectableValidators: res[0], + maxElectableValidators: res[1], + electabilityThreshold: res[2], + maxNumGroupsVotedFor: toBigNumber(res[3]), + } + } + + async getValidatorSetAddresses(): Promise { + const numberValidators = await this.numberValidatorsInCurrentSet() + + const validatorAddressPromises = [] + + for (let i = 0; i < numberValidators; i++) { + validatorAddressPromises.push(this.validatorAddressFromCurrentSet(i)) + } + + return Promise.all(validatorAddressPromises) + } + + async getValidatorGroupsVotes(): Promise { + const validators = await this.kit.contracts.getValidators() + const vgAddresses = (await validators.getRegisteredValidatorGroups()).map((g) => g.address) + const vgVotes = await Promise.all( + vgAddresses.map((g) => this.contract.methods.getGroupTotalVotes(g).call()) + ) + const vgEligible = await Promise.all( + vgAddresses.map((g) => this.contract.methods.getGroupEligibility(g).call()) + ) + return vgAddresses.map((a, i) => ({ + address: a, + votes: toBigNumber(vgVotes[i]), + eligible: vgEligible[i], + })) + } + + async getEligibleValidatorGroupsVotes(): Promise { + const res = await this.contract.methods.getEligibleValidatorGroupsVoteTotals().call() + return zip((a, b) => ({ address: a, votes: new BigNumber(b), eligible: true }), res[0], res[1]) + } + + /* + async revokeVote(): Promise> { + if (this.kit.defaultAccount == null) { + throw new Error(`missing from at new ValdidatorUtils()`) + } + + const lockedGold = await this.kit.contracts.getLockedGold() + const votingDetails = await lockedGold.getVotingDetails(this.kit.defaultAccount) + const votedGroup = await this.getVoteFrom(votingDetails.accountAddress) + + if (votedGroup == null) { + throw new Error(`Not current vote for ${this.kit.defaultAccount}`) + } + + const { lesser, greater } = await this.findLesserAndGreaterAfterVote( + votedGroup, + votingDetails.weight.negated() + ) + + return wrapSend(this.kit, this.contract.methods.revokeVote(lesser, greater)) + } + */ + + async vote(validatorGroup: Address, value: BigNumber): Promise> { + if (this.kit.defaultAccount == null) { + throw new Error(`missing from at new ValdidatorUtils()`) + } + + const { lesser, greater } = await this.findLesserAndGreaterAfterVote(validatorGroup, value) + + return wrapSend( + this.kit, + this.contract.methods.vote(validatorGroup, value.toString(), lesser, greater) + ) + } + + private async findLesserAndGreaterAfterVote( + votedGroup: Address, + voteWeight: BigNumber + ): Promise<{ lesser: Address; greater: Address }> { + const currentVotes = await this.getEligibleValidatorGroupsVotes() + + const selectedGroup = currentVotes.find((cv) => eqAddress(cv.address, votedGroup)) + + // modify the list + if (selectedGroup) { + selectedGroup.votes = selectedGroup.votes.plus(voteWeight) + } else { + currentVotes.push({ + address: votedGroup, + votes: voteWeight, + eligible: true, + }) + } + + // re-sort + currentVotes.sort((a, b) => a.votes.comparedTo(b.votes)) + + // find new index + const newIdx = currentVotes.findIndex((cv) => eqAddress(cv.address, votedGroup)) + + return { + lesser: newIdx === 0 ? NULL_ADDRESS : currentVotes[newIdx - 1].address, + greater: newIdx === currentVotes.length - 1 ? NULL_ADDRESS : currentVotes[newIdx + 1].address, + } + } +} From d6ef3ec4c605feb7da3070406a1cf29a8b1d3b27 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Wed, 2 Oct 2019 19:12:41 -0700 Subject: [PATCH 021/149] Update oclif --- packages/cli/package.json | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 6f1db7a1408..a5d3efbd695 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -77,20 +77,23 @@ "account": { "description": "Manage your account, send and receive Celo Gold and Celo Dollars" }, - "bonds": { - "description": "Manage Locked Gold to participate in governance and earn rewards" + "election": { + "description": "View and manage validator elections" }, "config": { "description": "Configure CLI options which persist across commands" }, + "lockedgold": { + "description": "View and manage locked celo gold" + }, "node": { "description": "Manage your full node" }, "validator": { - "description": "View validator information and register your own" + "description": "View and manage validators" }, "validatorgroup": { - "description": "View validator group information and cast votes" + "description": "View and manage validator groups" }, "exchange": { "description": "Commands for interacting with the Exchange" From 615ec4f177a3576e9c0a6d6cd5ae211c33d8d9f3 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Thu, 3 Oct 2019 10:43:54 -0700 Subject: [PATCH 022/149] Checkpoint --- packages/celotool/src/e2e-tests/sync_tests.ts | 2 +- packages/celotool/src/e2e-tests/utils.ts | 4 ++-- packages/cli/src/commands/lockedgold/lock.ts | 1 + packages/cli/src/commands/lockedgold/show.ts | 15 ++++++++------- packages/cli/src/utils/cli.ts | 3 +++ 5 files changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/celotool/src/e2e-tests/sync_tests.ts b/packages/celotool/src/e2e-tests/sync_tests.ts index 2d99ad4944b..da38f377d72 100644 --- a/packages/celotool/src/e2e-tests/sync_tests.ts +++ b/packages/celotool/src/e2e-tests/sync_tests.ts @@ -40,7 +40,7 @@ describe('sync tests', function(this: any) { peers: [await getEnode(8545)], } await initAndStartGeth(hooks.gethBinaryPath, fullInstance) - await sleep(3) + await sleep(30000000000000000000) }) after(hooks.after) diff --git a/packages/celotool/src/e2e-tests/utils.ts b/packages/celotool/src/e2e-tests/utils.ts index 0bc2255bc57..b05dfd7e01e 100644 --- a/packages/celotool/src/e2e-tests/utils.ts +++ b/packages/celotool/src/e2e-tests/utils.ts @@ -234,8 +234,8 @@ async function waitForPortOpen(host: string, port: number, seconds: number) { return false } -export function sleep(seconds: number) { - return new Promise((resolve) => setTimeout(resolve, seconds * 1000)) +export async function sleep(seconds: number) { + await execCmd('sleep', [seconds.toString()]) } export async function getEnode(port: number, ws: boolean = false) { diff --git a/packages/cli/src/commands/lockedgold/lock.ts b/packages/cli/src/commands/lockedgold/lock.ts index ce086359e7e..8f121cbd639 100644 --- a/packages/cli/src/commands/lockedgold/lock.ts +++ b/packages/cli/src/commands/lockedgold/lock.ts @@ -34,6 +34,7 @@ export default class Lock extends BaseCommand { failWith(`require(goldAmount > 0) => [${goldAmount}]`) } + // TODO(asa): Why is this failing? const tx = lockedGold.lock() await displaySendTx('lock', tx, { value: goldAmount.toString() }) } diff --git a/packages/cli/src/commands/lockedgold/show.ts b/packages/cli/src/commands/lockedgold/show.ts index b8d12cafce7..a249a0f0172 100644 --- a/packages/cli/src/commands/lockedgold/show.ts +++ b/packages/cli/src/commands/lockedgold/show.ts @@ -1,6 +1,7 @@ import { BaseCommand } from '../../base' -import { printValueMap } from '../../utils/cli' +import { printValueMapRecursive } from '../../utils/cli' import { Args } from '../../utils/command' +import { eqAddress } from '@celo/utils/lib/address' export default class Show extends BaseCommand { static description = 'Show locked gold information for a given account' @@ -18,8 +19,8 @@ export default class Show extends BaseCommand { const { args } = this.parse(Show) const lockedGold = await this.kit.contracts.getLockedGold() - const nonvoting = await lockedGold.getAccountNonvotingLockedGold(args.account) - const total = await lockedGold.getAccountTotalLockedGold(args.account) + const nonvoting = (await lockedGold.getAccountNonvotingLockedGold(args.account)).toString() + const total = (await lockedGold.getAccountTotalLockedGold(args.account)).toString() const voter = await lockedGold.getVoterFromAccount(args.account) const validator = await lockedGold.getValidatorFromAccount(args.account) const pendingWithdrawals = await lockedGold.getPendingWithdrawals(args.account) @@ -29,11 +30,11 @@ export default class Show extends BaseCommand { nonvoting, }, authorizations: { - voter: voter == args.account ? null : voter, - validator: validator == args.account ? null : validator, + voter: eqAddress(voter, args.account) ? 'None' : voter, + validator: eqAddress(validator, args.account) ? 'None' : validator, }, - pendingWithdrawals, + pendingWithdrawals: pendingWithdrawals.length > 0 ? pendingWithdrawals : '[]', } - printValueMap(info) + printValueMapRecursive(info) } } diff --git a/packages/cli/src/utils/cli.ts b/packages/cli/src/utils/cli.ts index 4102c94ef7d..b64c8aa769b 100644 --- a/packages/cli/src/utils/cli.ts +++ b/packages/cli/src/utils/cli.ts @@ -7,6 +7,9 @@ import { Tx } from 'web3/eth/types' export async function displaySendTx(name: string, txObj: CeloTransactionObject, tx?: Tx) { cli.action.start(`Sending Transaction: ${name}`) + console.log(await txObj.estimateGas(tx)) + console.log('txobj', txObj) + console.log('tx', tx) const txResult = await txObj.send(tx) const txHash = await txResult.getHash() From 3ff2fe4f0c09f045bb73aad1bc2f902b83c5087e Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Thu, 3 Oct 2019 12:28:57 -0700 Subject: [PATCH 023/149] CLI seems to be working --- packages/cli/src/commands/validatorgroup/list.ts | 1 + .../cli/src/commands/validatorgroup/member.ts | 10 +++++++++- .../cli/src/commands/validatorgroup/register.ts | 5 ++++- packages/cli/src/utils/cli.ts | 3 --- packages/cli/src/utils/helpers.ts | 1 + packages/contractkit/src/wrappers/Election.ts | 14 ++++++++++++++ packages/contractkit/src/wrappers/Validators.ts | 16 ++++++++++++++-- .../protocol/contracts/common/UsingRegistry.sol | 2 +- .../contracts/common/linkedlists/LinkedList.sol | 2 +- .../protocol/contracts/governance/Election.sol | 3 ++- .../protocol/contracts/governance/LockedGold.sol | 8 ++++---- .../protocol/contracts/governance/Validators.sol | 11 ++++++----- 12 files changed, 57 insertions(+), 19 deletions(-) diff --git a/packages/cli/src/commands/validatorgroup/list.ts b/packages/cli/src/commands/validatorgroup/list.ts index 013cb393e94..7743ab63f9a 100644 --- a/packages/cli/src/commands/validatorgroup/list.ts +++ b/packages/cli/src/commands/validatorgroup/list.ts @@ -22,6 +22,7 @@ export default class ValidatorGroupList extends BaseCommand { address: {}, name: {}, url: {}, + commission: { get: (r) => r.commission.toFixed() }, members: { get: (r) => r.members.length }, }) } diff --git a/packages/cli/src/commands/validatorgroup/member.ts b/packages/cli/src/commands/validatorgroup/member.ts index 79e41a16835..f09291f42d5 100644 --- a/packages/cli/src/commands/validatorgroup/member.ts +++ b/packages/cli/src/commands/validatorgroup/member.ts @@ -37,11 +37,19 @@ export default class ValidatorGroupRegister extends BaseCommand { this.kit.defaultAccount = res.flags.from const validators = await this.kit.contracts.getValidators() + const election = await this.kit.contracts.getElection() if (res.flags.accept) { await displaySendTx('addMember', validators.addMember((res.args as any).validatorAddress)) + if ((await validators.getGroupNumMembers(res.flags.from)) === '1') { + const tx = await election.markGroupEligible(res.flags.from) + await displaySendTx('markGroupEligible', tx) + } } else { - await displaySendTx('addMember', validators.removeMember((res.args as any).validatorAddress)) + await displaySendTx( + 'removeMember', + validators.removeMember((res.args as any).validatorAddress) + ) } } } diff --git a/packages/cli/src/commands/validatorgroup/register.ts b/packages/cli/src/commands/validatorgroup/register.ts index 8eaca3c9c73..d3645c7fb6a 100644 --- a/packages/cli/src/commands/validatorgroup/register.ts +++ b/packages/cli/src/commands/validatorgroup/register.ts @@ -1,7 +1,9 @@ +import BigNumber from 'bignumber.js' import { flags } from '@oclif/command' import { BaseCommand } from '../../base' import { displaySendTx } from '../../utils/cli' import { Flags } from '../../utils/command' +import { toFixed } from '@celo/utils/lib/fixidity' export default class ValidatorGroupRegister extends BaseCommand { static description = 'Register a new Validator Group' @@ -23,10 +25,11 @@ export default class ValidatorGroupRegister extends BaseCommand { this.kit.defaultAccount = res.flags.from const validators = await this.kit.contracts.getValidators() + const commission = toFixed(new BigNumber(res.flags.commission)).toFixed() await displaySendTx( 'registerValidatorGroup', - validators.registerValidatorGroup(res.flags.name, res.flags.url, res.flags.commission) + validators.registerValidatorGroup(res.flags.name, res.flags.url, commission) ) } } diff --git a/packages/cli/src/utils/cli.ts b/packages/cli/src/utils/cli.ts index b64c8aa769b..4102c94ef7d 100644 --- a/packages/cli/src/utils/cli.ts +++ b/packages/cli/src/utils/cli.ts @@ -7,9 +7,6 @@ import { Tx } from 'web3/eth/types' export async function displaySendTx(name: string, txObj: CeloTransactionObject, tx?: Tx) { cli.action.start(`Sending Transaction: ${name}`) - console.log(await txObj.estimateGas(tx)) - console.log('txobj', txObj) - console.log('tx', tx) const txResult = await txObj.send(tx) const txHash = await txResult.getHash() diff --git a/packages/cli/src/utils/helpers.ts b/packages/cli/src/utils/helpers.ts index 775752e7170..34632947bc3 100644 --- a/packages/cli/src/utils/helpers.ts +++ b/packages/cli/src/utils/helpers.ts @@ -57,6 +57,7 @@ export async function nodeIsSynced(web3: Web3): Promise { } export async function requireNodeIsSynced(web3: Web3) { + return if (!(await nodeIsSynced(web3))) { failWith('Node is not currently synced. Run node:synced to check its status') } diff --git a/packages/contractkit/src/wrappers/Election.ts b/packages/contractkit/src/wrappers/Election.ts index df54eea1fad..f8a9a875dbf 100644 --- a/packages/contractkit/src/wrappers/Election.ts +++ b/packages/contractkit/src/wrappers/Election.ts @@ -160,6 +160,20 @@ export class ElectionWrapper extends BaseWrapper { } */ + async markGroupEligible(validatorGroup: Address): Promise> { + if (this.kit.defaultAccount == null) { + throw new Error(`missing from at new ValdidatorUtils()`) + } + + const value = toBigNumber(await this.contract.methods.getGroupTotalVotes(validatorGroup).call()) + const { lesser, greater } = await this.findLesserAndGreaterAfterVote(validatorGroup, value) + + return wrapSend( + this.kit, + this.contract.methods.markGroupEligible(validatorGroup, lesser, greater) + ) + } + async vote(validatorGroup: Address, value: BigNumber): Promise> { if (this.kit.defaultAccount == null) { throw new Error(`missing from at new ValdidatorUtils()`) diff --git a/packages/contractkit/src/wrappers/Validators.ts b/packages/contractkit/src/wrappers/Validators.ts index 37922243db9..4103279bc8f 100644 --- a/packages/contractkit/src/wrappers/Validators.ts +++ b/packages/contractkit/src/wrappers/Validators.ts @@ -1,7 +1,8 @@ import BigNumber from 'bignumber.js' import { Address } from '../base' import { Validators } from '../generated/types/Validators' -import { BaseWrapper, proxySend, toBigNumber } from './BaseWrapper' +import { BaseWrapper, proxyCall, proxySend, toBigNumber } from './BaseWrapper' +import { fromFixed } from '@celo/utils/lib/fixidity' export interface Validator { address: Address @@ -16,6 +17,7 @@ export interface ValidatorGroup { name: string url: string members: Address[] + commission: BigNumber } export interface RegistrationRequirements { @@ -86,6 +88,10 @@ export class ValidatorsWrapper extends BaseWrapper { return Promise.all(vgAddresses.map((addr) => this.getValidator(addr))) } + getGroupNumMembers: (group: Address) => Promise = proxyCall( + this.contract.methods.getGroupNumMembers + ) + async getValidator(address: Address): Promise { const res = await this.contract.methods.getValidator(address).call() return { @@ -104,6 +110,12 @@ export class ValidatorsWrapper extends BaseWrapper { async getValidatorGroup(address: Address): Promise { const res = await this.contract.methods.getValidatorGroup(address).call() - return { address, name: res[0], url: res[1], members: res[2] } + return { + address, + name: res[0], + url: res[1], + members: res[2], + commission: fromFixed(new BigNumber(res[3])), + } } } diff --git a/packages/protocol/contracts/common/UsingRegistry.sol b/packages/protocol/contracts/common/UsingRegistry.sol index 4ae16b3302e..aa742dedcf5 100644 --- a/packages/protocol/contracts/common/UsingRegistry.sol +++ b/packages/protocol/contracts/common/UsingRegistry.sol @@ -38,7 +38,7 @@ contract UsingRegistry is Ownable { IRegistry public registry; modifier onlyRegisteredContract(bytes32 identifierHash) { - require(registry.getAddressForOrDie(identifierHash) == msg.sender); + require(registry.getAddressForOrDie(identifierHash) == msg.sender, "only registered contract"); _; } /** diff --git a/packages/protocol/contracts/common/linkedlists/LinkedList.sol b/packages/protocol/contracts/common/linkedlists/LinkedList.sol index e1e514668dc..fc55f5c556b 100644 --- a/packages/protocol/contracts/common/linkedlists/LinkedList.sol +++ b/packages/protocol/contracts/common/linkedlists/LinkedList.sol @@ -102,7 +102,7 @@ library LinkedList { */ function remove(List storage list, bytes32 key) public { Element storage element = list.elements[key]; - require(key != bytes32(0) && contains(list, key)); + require(key != bytes32(0) && contains(list, key), "can't remove"); if (element.previousKey != bytes32(0)) { Element storage previousElement = list.elements[element.previousKey]; previousElement.nextKey = element.nextKey; diff --git a/packages/protocol/contracts/governance/Election.sol b/packages/protocol/contracts/governance/Election.sol index f4ed545dbca..23b8bd087d7 100644 --- a/packages/protocol/contracts/governance/Election.sol +++ b/packages/protocol/contracts/governance/Election.sol @@ -498,12 +498,13 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { * @param lesser The address of the group that has received fewer votes than this group. * @param greater The address of the group that has received more votes than this group. */ - function markGroupEligible(address group, address lesser, address greater) external { + function markGroupEligible(address group, address lesser, address greater) external returns (bool) { require(!votes.total.eligible.contains(group)); require(getValidators().getGroupNumMembers(group) > 0); uint256 value = votes.pending.total[group].add(votes.active.total[group]); votes.total.eligible.insert(group, value, lesser, greater); emit ValidatorGroupMarkedEligible(group); + return true; } /** diff --git a/packages/protocol/contracts/governance/LockedGold.sol b/packages/protocol/contracts/governance/LockedGold.sol index b7f42f9c350..1e7503543b4 100644 --- a/packages/protocol/contracts/governance/LockedGold.sol +++ b/packages/protocol/contracts/governance/LockedGold.sol @@ -124,8 +124,8 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr * @notice Locks gold to be used for voting. */ function lock() external payable nonReentrant { - require(isAccount(msg.sender)); - require(msg.value > 0); + require(isAccount(msg.sender), "not account"); + require(msg.value > 0, "no value"); _incrementNonvotingAccountBalance(msg.sender, msg.value); emit GoldLocked(msg.sender, msg.value); } @@ -393,10 +393,10 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr ) private { - require(isAccount(msg.sender) && isNotAccount(current) && isNotAuthorized(current), "Accounts"); + require(isAccount(msg.sender) && isNotAccount(current) && isNotAuthorized(current), "account checks"); address signer = Signatures.getSignerOfAddress(msg.sender, v, r, s); - require(signer == current, "Signature"); + require(signer == current, "signature checks"); authorizedBy[previous] = address(0); authorizedBy[current] = msg.sender; diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index fdce901dd01..741ef35ad4f 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -246,7 +246,8 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi publicKeysData.length == (64 + 48 + 96) ); // Use the proof of possession bytes - require(checkProofOfPossession(publicKeysData.slice(64, 48 + 96))); + // DO NOT SUBMIT: Commented out for testing. + // require(checkProofOfPossession(publicKeysData.slice(64, 48 + 96))); address account = getLockedGold().getAccountFromValidator(msg.sender); require(!isValidator(account) && !isValidatorGroup(account)); @@ -436,7 +437,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi */ function removeMember(address validator) external nonReentrant returns (bool) { address account = getLockedGold().getAccountFromValidator(msg.sender); - require(isValidatorGroup(account) && isValidator(validator)); + require(isValidatorGroup(account) && isValidator(validator), "is not group and validator"); return _removeMember(account, validator); } @@ -505,11 +506,11 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi ) external view - returns (string memory, string memory, address[] memory) + returns (string memory, string memory, address[] memory, uint256) { require(isValidatorGroup(account)); ValidatorGroup storage group = groups[account]; - return (group.name, group.url, group.members.getKeys()); + return (group.name, group.url, group.members.getKeys(), group.commission.unwrap()); } /** @@ -644,7 +645,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi */ function _removeMember(address group, address validator) private returns (bool) { ValidatorGroup storage _group = groups[group]; - require(validators[validator].affiliation == group && _group.members.contains(validator)); + require(validators[validator].affiliation == group && _group.members.contains(validator), "not a member"); _group.members.remove(validator); emit ValidatorGroupMemberRemoved(group, validator); From d3b83c80e2863dbe35403c40ef1e1fc45346602f Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Thu, 3 Oct 2019 12:43:52 -0700 Subject: [PATCH 024/149] cleanup --- packages/celotool/src/e2e-tests/sync_tests.ts | 2 +- packages/cli/src/commands/lockedgold/lock.ts | 1 - packages/cli/src/utils/helpers.ts | 1 - .../contracts/common/linkedlists/LinkedList.sol | 2 +- .../protocol/contracts/governance/Election.sol | 17 +++++++++++++++-- .../contracts/governance/LockedGold.sol | 14 +++++--------- .../contracts/governance/Validators.sol | 8 +++----- 7 files changed, 25 insertions(+), 20 deletions(-) diff --git a/packages/celotool/src/e2e-tests/sync_tests.ts b/packages/celotool/src/e2e-tests/sync_tests.ts index da38f377d72..2d99ad4944b 100644 --- a/packages/celotool/src/e2e-tests/sync_tests.ts +++ b/packages/celotool/src/e2e-tests/sync_tests.ts @@ -40,7 +40,7 @@ describe('sync tests', function(this: any) { peers: [await getEnode(8545)], } await initAndStartGeth(hooks.gethBinaryPath, fullInstance) - await sleep(30000000000000000000) + await sleep(3) }) after(hooks.after) diff --git a/packages/cli/src/commands/lockedgold/lock.ts b/packages/cli/src/commands/lockedgold/lock.ts index 8f121cbd639..ce086359e7e 100644 --- a/packages/cli/src/commands/lockedgold/lock.ts +++ b/packages/cli/src/commands/lockedgold/lock.ts @@ -34,7 +34,6 @@ export default class Lock extends BaseCommand { failWith(`require(goldAmount > 0) => [${goldAmount}]`) } - // TODO(asa): Why is this failing? const tx = lockedGold.lock() await displaySendTx('lock', tx, { value: goldAmount.toString() }) } diff --git a/packages/cli/src/utils/helpers.ts b/packages/cli/src/utils/helpers.ts index 34632947bc3..775752e7170 100644 --- a/packages/cli/src/utils/helpers.ts +++ b/packages/cli/src/utils/helpers.ts @@ -57,7 +57,6 @@ export async function nodeIsSynced(web3: Web3): Promise { } export async function requireNodeIsSynced(web3: Web3) { - return if (!(await nodeIsSynced(web3))) { failWith('Node is not currently synced. Run node:synced to check its status') } diff --git a/packages/protocol/contracts/common/linkedlists/LinkedList.sol b/packages/protocol/contracts/common/linkedlists/LinkedList.sol index fc55f5c556b..e1e514668dc 100644 --- a/packages/protocol/contracts/common/linkedlists/LinkedList.sol +++ b/packages/protocol/contracts/common/linkedlists/LinkedList.sol @@ -102,7 +102,7 @@ library LinkedList { */ function remove(List storage list, bytes32 key) public { Element storage element = list.elements[key]; - require(key != bytes32(0) && contains(list, key), "can't remove"); + require(key != bytes32(0) && contains(list, key)); if (element.previousKey != bytes32(0)) { Element storage previousElement = list.elements[element.previousKey]; previousElement.nextKey = element.nextKey; diff --git a/packages/protocol/contracts/governance/Election.sol b/packages/protocol/contracts/governance/Election.sol index 23b8bd087d7..d5124b5d679 100644 --- a/packages/protocol/contracts/governance/Election.sol +++ b/packages/protocol/contracts/governance/Election.sol @@ -181,7 +181,13 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { * @param _maxNumGroupsVotedFor The maximum number of groups an account can vote for. * @return True upon success. */ - function setMaxNumGroupsVotedFor(uint256 _maxNumGroupsVotedFor) external onlyOwner returns (bool) { + function setMaxNumGroupsVotedFor( + uint256 _maxNumGroupsVotedFor + ) + external + onlyOwner + returns (bool) + { require(_maxNumGroupsVotedFor != maxNumGroupsVotedFor); maxNumGroupsVotedFor = _maxNumGroupsVotedFor; emit MaxNumGroupsVotedForSet(_maxNumGroupsVotedFor); @@ -498,7 +504,14 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { * @param lesser The address of the group that has received fewer votes than this group. * @param greater The address of the group that has received more votes than this group. */ - function markGroupEligible(address group, address lesser, address greater) external returns (bool) { + function markGroupEligible( + address group, + address lesser, + address greater + ) + external + returns (bool) + { require(!votes.total.eligible.contains(group)); require(getValidators().getGroupNumMembers(group) > 0); uint256 value = votes.pending.total[group].add(votes.active.total[group]); diff --git a/packages/protocol/contracts/governance/LockedGold.sol b/packages/protocol/contracts/governance/LockedGold.sol index 1e7503543b4..6f89b30453c 100644 --- a/packages/protocol/contracts/governance/LockedGold.sol +++ b/packages/protocol/contracts/governance/LockedGold.sol @@ -187,13 +187,12 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr * @param value The amount of gold to unlock. */ function unlock(uint256 value) external nonReentrant { - require(isAccount(msg.sender), "not account"); + require(isAccount(msg.sender)); Account storage account = accounts[msg.sender]; MustMaintain memory requirement = account.balances.requirements; require( now >= requirement.timestamp || - getAccountTotalLockedGold(msg.sender).sub(value) >= requirement.value, - "didn't meet mustmaintain requirements" + getAccountTotalLockedGold(msg.sender).sub(value) >= requirement.value ); _decrementNonvotingAccountBalance(msg.sender, value); uint256 available = now.add(unlockingPeriod); @@ -263,10 +262,7 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr function getAccountFromVoter(address accountOrVoter) external view returns (address) { address authorizingAccount = authorizedBy[accountOrVoter]; if (authorizingAccount != address(0)) { - require( - accounts[authorizingAccount].authorizations.voting == accountOrVoter, - 'failed first check' - ); + require(accounts[authorizingAccount].authorizations.voting == accountOrVoter); return authorizingAccount; } else { require(isAccount(accountOrVoter)); @@ -393,10 +389,10 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr ) private { - require(isAccount(msg.sender) && isNotAccount(current) && isNotAuthorized(current), "account checks"); + require(isAccount(msg.sender) && isNotAccount(current) && isNotAuthorized(current)); address signer = Signatures.getSignerOfAddress(msg.sender, v, r, s); - require(signer == current, "signature checks"); + require(signer == current); authorizedBy[previous] = address(0); authorizedBy[current] = msg.sender; diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index 741ef35ad4f..e8d9aa28a81 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -246,8 +246,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi publicKeysData.length == (64 + 48 + 96) ); // Use the proof of possession bytes - // DO NOT SUBMIT: Commented out for testing. - // require(checkProofOfPossession(publicKeysData.slice(64, 48 + 96))); + require(checkProofOfPossession(publicKeysData.slice(64, 48 + 96))); address account = getLockedGold().getAccountFromValidator(msg.sender); require(!isValidator(account) && !isValidatorGroup(account)); @@ -365,8 +364,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi { require(bytes(name).length > 0); require(bytes(url).length > 0); - // TODO(asa) - // require(isFraction(commission)); + require(commission.lt(FixidityLib.fixed1()), "Commission must be lower than 100%"); address account = getLockedGold().getAccountFromValidator(msg.sender); require(!isValidator(account) && !isValidatorGroup(account)); require(meetsValidatorGroupRegistrationRequirement(account)); @@ -645,7 +643,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi */ function _removeMember(address group, address validator) private returns (bool) { ValidatorGroup storage _group = groups[group]; - require(validators[validator].affiliation == group && _group.members.contains(validator), "not a member"); + require(validators[validator].affiliation == group && _group.members.contains(validator)); _group.members.remove(validator); emit ValidatorGroupMemberRemoved(group, validator); From 288765199a4dd29783b84caa3daf2ae1a12ce0d6 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Thu, 3 Oct 2019 13:43:16 -0700 Subject: [PATCH 025/149] Fix --- packages/protocol/contracts/governance/Validators.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index e8d9aa28a81..bd363e8e3b2 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -364,7 +364,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi { require(bytes(name).length > 0); require(bytes(url).length > 0); - require(commission.lt(FixidityLib.fixed1()), "Commission must be lower than 100%"); + require(commission <= FixidityLib.fixed1().unwrap(), "Commission can't be greater than 100%"); address account = getLockedGold().getAccountFromValidator(msg.sender); require(!isValidator(account) && !isValidatorGroup(account)); require(meetsValidatorGroupRegistrationRequirement(account)); From 2d61f63ae94df193e7cf255c5dc817dca44dacc3 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Thu, 3 Oct 2019 13:52:42 -0700 Subject: [PATCH 026/149] Fix --- packages/protocol/test/governance/election.ts | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/packages/protocol/test/governance/election.ts b/packages/protocol/test/governance/election.ts index 426d89a5c01..c3647fd622f 100644 --- a/packages/protocol/test/governance/election.ts +++ b/packages/protocol/test/governance/election.ts @@ -39,7 +39,7 @@ contract('Election', (accounts: string[]) => { const nonOwner = accounts[1] const minElectableValidators = new BigNumber(4) const maxElectableValidators = new BigNumber(6) - const maxVotesPerAccount = new BigNumber(3) + const maxNumGroupsVotedFor = new BigNumber(3) const electabilityThreshold = new BigNumber(0) beforeEach(async () => { @@ -53,7 +53,7 @@ contract('Election', (accounts: string[]) => { registry.address, minElectableValidators, maxElectableValidators, - maxVotesPerAccount, + maxNumGroupsVotedFor, electabilityThreshold ) }) @@ -74,9 +74,9 @@ contract('Election', (accounts: string[]) => { assertEqualBN(actualMaxElectableValidators, maxElectableValidators) }) - it('should have set maxVotesPerAccount', async () => { - const actualMaxVotesPerAccount = await election.maxVotesPerAccount() - assertEqualBN(actualMaxVotesPerAccount, maxVotesPerAccount) + it('should have set maxNumGroupsVotedFor', async () => { + const actualMaxNumGroupsVotedFor = await election.maxNumGroupsVotedFor() + assertEqualBN(actualMaxNumGroupsVotedFor, maxNumGroupsVotedFor) }) it('should not be callable again', async () => { @@ -85,7 +85,7 @@ contract('Election', (accounts: string[]) => { registry.address, minElectableValidators, maxElectableValidators, - maxVotesPerAccount, + maxNumGroupsVotedFor, electabilityThreshold ) ) @@ -178,31 +178,33 @@ contract('Election', (accounts: string[]) => { }) }) - describe('#setMaxVotesPerAccount', () => { - const newMaxVotesPerAccount = maxVotesPerAccount.plus(1) + describe('#setMaxNumGroupsVotedFor', () => { + const newMaxNumGroupsVotedFor = maxNumGroupsVotedFor.plus(1) it('should set the max electable validators', async () => { - await election.setMaxVotesPerAccount(newMaxVotesPerAccount) - assertEqualBN(await election.maxVotesPerAccount(), newMaxVotesPerAccount) + await election.setMaxNumGroupsVotedFor(newMaxNumGroupsVotedFor) + assertEqualBN(await election.maxNumGroupsVotedFor(), newMaxNumGroupsVotedFor) }) - it('should emit the MaxVotesPerAccountSet event', async () => { - const resp = await election.setMaxVotesPerAccount(newMaxVotesPerAccount) + it('should emit the MaxNumGroupsVotedForSet event', async () => { + const resp = await election.setMaxNumGroupsVotedFor(newMaxNumGroupsVotedFor) assert.equal(resp.logs.length, 1) const log = resp.logs[0] assertContainSubset(log, { - event: 'MaxVotesPerAccountSet', + event: 'MaxNumGroupsVotedForSet', args: { - maxVotesPerAccount: new BigNumber(newMaxVotesPerAccount), + maxNumGroupsVotedFor: new BigNumber(newMaxNumGroupsVotedFor), }, }) }) - it('should revert when the maxVotesPerAccount is unchanged', async () => { - await assertRevert(election.setMaxVotesPerAccount(maxVotesPerAccount)) + it('should revert when the maxNumGroupsVotedFor is unchanged', async () => { + await assertRevert(election.setMaxNumGroupsVotedFor(maxNumGroupsVotedFor)) }) it('should revert when called by anyone other than the owner', async () => { - await assertRevert(election.setMaxVotesPerAccount(newMaxVotesPerAccount, { from: nonOwner })) + await assertRevert( + election.setMaxNumGroupsVotedFor(newMaxNumGroupsVotedFor, { from: nonOwner }) + ) }) }) @@ -383,7 +385,7 @@ contract('Election', (accounts: string[]) => { let newGroup: string beforeEach(async () => { await mockLockedGold.incrementNonvotingAccountBalance(voter, value) - for (let i = 0; i < maxVotesPerAccount.toNumber(); i++) { + for (let i = 0; i < maxNumGroupsVotedFor.toNumber(); i++) { newGroup = accounts[i + 2] await mockValidators.setMembers(newGroup, [accounts[9]]) await election.markGroupEligible(newGroup, group, NULL_ADDRESS) @@ -393,7 +395,7 @@ contract('Election', (accounts: string[]) => { it('should revert', async () => { await assertRevert( - election.vote(group, value - maxVotesPerAccount.toNumber(), newGroup, NULL_ADDRESS) + election.vote(group, value - maxNumGroupsVotedFor.toNumber(), newGroup, NULL_ADDRESS) ) }) }) From a427d46f18d665154d6150169484c9b4f43fc0f9 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Thu, 3 Oct 2019 21:52:18 -0700 Subject: [PATCH 027/149] Begin work on validator/group payments --- .../protocol/contracts/common/UsingEpochs.sol | 14 + .../contracts/common/UsingRegistry.sol | 1 + .../contracts/governance/Election.sol | 10 +- .../contracts/governance/Governance.sol | 6 +- .../contracts/governance/LockedGold.sol | 56 ++- .../contracts/governance/Validators.sol | 228 ++++++++++- .../governance/interfaces/ILockedGold.sol | 3 +- .../governance/interfaces/IValidators.sol | 2 +- .../governance/test/MockLockedGold.sol | 9 +- .../governance/test/ValidatorsTest.sol | 22 + packages/protocol/lib/test-utils.ts | 6 + packages/protocol/migrations/11_validators.ts | 5 + packages/protocol/migrationsConfig.js | 8 +- .../protocol/test/governance/validators.ts | 376 +++++++++++++++++- 14 files changed, 681 insertions(+), 65 deletions(-) create mode 100644 packages/protocol/contracts/common/UsingEpochs.sol create mode 100644 packages/protocol/contracts/governance/test/ValidatorsTest.sol diff --git a/packages/protocol/contracts/common/UsingEpochs.sol b/packages/protocol/contracts/common/UsingEpochs.sol new file mode 100644 index 00000000000..87eedb3b3d6 --- /dev/null +++ b/packages/protocol/contracts/common/UsingEpochs.sol @@ -0,0 +1,14 @@ +pragma solidity ^0.5.3; + +// TODO: Replace this with a precompile. +contract UsingEpochs { + + event RegistrySet(address indexed registryAddress); + + // solhint-disable state-visibility + uint256 constant EPOCH = 17280; + + function getEpochNumber() public view returns (uint256) { + return block.number / EPOCH; + } +} diff --git a/packages/protocol/contracts/common/UsingRegistry.sol b/packages/protocol/contracts/common/UsingRegistry.sol index aa742dedcf5..24561e9fc18 100644 --- a/packages/protocol/contracts/common/UsingRegistry.sol +++ b/packages/protocol/contracts/common/UsingRegistry.sol @@ -41,6 +41,7 @@ contract UsingRegistry is Ownable { require(registry.getAddressForOrDie(identifierHash) == msg.sender, "only registered contract"); _; } + /** * @notice Updates the address pointing to a Registry contract. * @param registryAddress The address of a registry contract for routing to other contracts. diff --git a/packages/protocol/contracts/governance/Election.sol b/packages/protocol/contracts/governance/Election.sol index d5124b5d679..4ee216be4a4 100644 --- a/packages/protocol/contracts/governance/Election.sol +++ b/packages/protocol/contracts/governance/Election.sol @@ -246,7 +246,7 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { { require(votes.total.eligible.contains(group)); require(0 < value && value <= getNumVotesReceivable(group)); - address account = getLockedGold().getAccountFromVoter(msg.sender); + address account = getLockedGold().getAccountFromActiveVoter(msg.sender); address[] storage groups = votes.groupsVotedFor[account]; require(groups.length < maxNumGroupsVotedFor); for (uint256 i = 0; i < groups.length; i = i.add(1)) { @@ -266,7 +266,7 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { * @return True upon success. */ function activate(address group) external nonReentrant returns (bool) { - address account = getLockedGold().getAccountFromVoter(msg.sender); + address account = getLockedGold().getAccountFromActiveVoter(msg.sender); PendingVotes storage pending = votes.pending; uint256 value = pending.balances[group][account]; require(value > 0); @@ -299,7 +299,7 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { returns (bool) { require(group != address(0)); - address account = getLockedGold().getAccountFromVoter(msg.sender); + address account = getLockedGold().getAccountFromActiveVoter(msg.sender); require(0 < value && value <= getAccountPendingVotesForGroup(group, account)); decrementPendingVotes(group, account, value); decrementTotalVotes(group, value, lesser, greater); @@ -335,7 +335,7 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { returns (bool) { require(group != address(0)); - address account = getLockedGold().getAccountFromVoter(msg.sender); + address account = getLockedGold().getAccountFromActiveVoter(msg.sender); require(0 < value && value <= getAccountActiveVotesForGroup(group, account)); decrementActiveVotes(group, account, value); decrementTotalVotes(group, value, lesser, greater); @@ -718,7 +718,7 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { totalNumMembersElected = 0; for (uint256 i = 0; i < electionGroups.length; i = i.add(1)) { // We use the validating delegate if one is set. - address[] memory electedGroupValidators = getValidators().getTopValidatorsFromGroup( + address[] memory electedGroupValidators = getValidators().getTopGroupValidators( electionGroups[i], numMembersElected[i] ); diff --git a/packages/protocol/contracts/governance/Governance.sol b/packages/protocol/contracts/governance/Governance.sol index 1a6480a19ba..0ba0a4c29e3 100644 --- a/packages/protocol/contracts/governance/Governance.sol +++ b/packages/protocol/contracts/governance/Governance.sol @@ -485,7 +485,7 @@ contract Governance is IGovernance, Ownable, Initializable, ReentrancyGuard, Usi nonReentrant returns (bool) { - address account = getLockedGold().getAccountFromVoter(msg.sender); + address account = getLockedGold().getAccountFromActiveVoter(msg.sender); // TODO(asa): When upvoting a proposal that will get dequeued, should we let the tx succeed // and return false? dequeueProposalsIfReady(); @@ -529,7 +529,7 @@ contract Governance is IGovernance, Ownable, Initializable, ReentrancyGuard, Usi returns (bool) { dequeueProposalsIfReady(); - address account = getLockedGold().getAccountFromVoter(msg.sender); + address account = getLockedGold().getAccountFromActiveVoter(msg.sender); Voter storage voter = voters[account]; uint256 proposalId = voter.upvote.proposalId; Proposals.Proposal storage proposal = proposals[proposalId]; @@ -597,7 +597,7 @@ contract Governance is IGovernance, Ownable, Initializable, ReentrancyGuard, Usi nonReentrant returns (bool) { - address account = getLockedGold().getAccountFromVoter(msg.sender); + address account = getLockedGold().getAccountFromActiveVoter(msg.sender); dequeueProposalsIfReady(); Proposals.Proposal storage proposal = proposals[proposalId]; require(isDequeuedProposal(proposal, proposalId, index)); diff --git a/packages/protocol/contracts/governance/LockedGold.sol b/packages/protocol/contracts/governance/LockedGold.sol index 6f89b30453c..c3e868b5cef 100644 --- a/packages/protocol/contracts/governance/LockedGold.sol +++ b/packages/protocol/contracts/governance/LockedGold.sol @@ -19,6 +19,11 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr uint256 timestamp; } + struct AuthorizedBy { + address account; + bool active; + } + struct Authorizations { address voting; address validating; @@ -48,7 +53,8 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr mapping(address => Account) private accounts; // Maps voting and validating keys to the account that provided the authorization. - mapping(address => address) public authorizedBy; + // Authorized addresses may not be reused. + mapping(address => AuthorizedBy) private authorizedBy; uint256 public totalNonvoting; uint256 public unlockingPeriod; @@ -255,15 +261,14 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr // TODO(asa): Dedup /** * @notice Returns the account associated with `accountOrVoter`. - * @param accountOrVoter The address of the account or authorized voter. - * @dev Fails if the `accountOrVoter` is not an account or authorized voter. + * @param accountOrVoter The address of the account or active authorized voter. + * @dev Fails if the `accountOrVoter` is not an account or active authorized voter. * @return The associated account. */ - function getAccountFromVoter(address accountOrVoter) external view returns (address) { - address authorizingAccount = authorizedBy[accountOrVoter]; - if (authorizingAccount != address(0)) { - require(accounts[authorizingAccount].authorizations.voting == accountOrVoter); - return authorizingAccount; + function getAccountFromActiveVoter(address accountOrVoter) external view returns (address) { + AuthorizedBy memory ab = authorizedBy[accountOrVoter]; + if (ab.active && ab.account != address(0)) { + return ab.account; } else { require(isAccount(accountOrVoter)); return accountOrVoter; @@ -307,15 +312,30 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr /** * @notice Returns the account associated with `accountOrValidator`. - * @param accountOrValidator The address of the account or authorized validator. - * @dev Fails if the `accountOrValidator` is not an account or authorized validator. + * @param accountOrValidator The address of the account or active authorized validator. + * @dev Fails if the `accountOrValidator` is not an account or active authorized validator. + * @return The associated account. + */ + function getAccountFromActiveValidator(address accountOrValidator) public view returns (address) { + AuthorizedBy memory ab = authorizedBy[accountOrValidator]; + if (ab.active && ab.account != address(0)) { + return ab.account; + } else { + require(isAccount(accountOrValidator)); + return accountOrValidator; + } + } + + /** + * @notice Returns the account associated with `accountOrValidator`. + * @param accountOrValidator The address of the account or previously authorized validator. + * @dev Fails if the `accountOrValidator` is not an account or previously authorized validator. * @return The associated account. */ function getAccountFromValidator(address accountOrValidator) public view returns (address) { - address authorizingAccount = authorizedBy[accountOrValidator]; - if (authorizingAccount != address(0)) { - require(accounts[authorizingAccount].authorizations.validating == accountOrValidator); - return authorizingAccount; + AuthorizedBy memory ab = authorizedBy[accountOrValidator]; + if (ab.account != address(0)) { + return ab.account; } else { require(isAccount(accountOrValidator)); return accountOrValidator; @@ -394,8 +414,8 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr address signer = Signatures.getSignerOfAddress(msg.sender, v, r, s); require(signer == current); - authorizedBy[previous] = address(0); - authorizedBy[current] = msg.sender; + authorizedBy[previous].active = false; + authorizedBy[current] = AuthorizedBy(msg.sender, true); } /** @@ -422,7 +442,7 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr * @return Returns `true` if authorized. Returns `false` otherwise. */ function isAuthorized(address account) external view returns (bool) { - return (authorizedBy[account] != address(0)); + return (authorizedBy[account].account != address(0)); } /** @@ -431,7 +451,7 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr * @return Returns `false` if authorized. Returns `true` otherwise. */ function isNotAuthorized(address account) internal view returns (bool) { - return (authorizedBy[account] == address(0)); + return (authorizedBy[account].account == address(0)); } function deletePendingWithdrawal(PendingWithdrawal[] storage list, uint256 index) private { diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index bd363e8e3b2..23adae92bf1 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -1,5 +1,6 @@ pragma solidity ^0.5.3; +import "openzeppelin-solidity/contracts/math/Math.sol"; import "openzeppelin-solidity/contracts/math/SafeMath.sol"; import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; import "openzeppelin-solidity/contracts/utils/ReentrancyGuard.sol"; @@ -12,13 +13,14 @@ import "../identity/interfaces/IRandom.sol"; import "../common/Initializable.sol"; import "../common/FixidityLib.sol"; import "../common/linkedlists/AddressLinkedList.sol"; +import "../common/UsingEpochs.sol"; import "../common/UsingRegistry.sol"; /** * @title A contract for registering and electing Validator Groups and Validators. */ -contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, UsingRegistry { +contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, UsingEpochs, UsingRegistry { using FixidityLib for FixidityLib.Fraction; using AddressLinkedList for LinkedList.List; @@ -41,8 +43,18 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi struct ValidatorGroup { string name; string url; - FixidityLib.Fraction commission; LinkedList.List members; + FixidityLib.Fraction commission; + } + + struct MembershipHistoryEntry { + uint256 epochNumber; + address group; + } + + struct MembershipHistory { + uint256 head; + MembershipHistoryEntry[] entries; } struct Validator { @@ -50,6 +62,13 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi string url; bytes publicKeysData; address affiliation; + FixidityLib.Fraction score; + MembershipHistory membershipHistory; + } + + struct ValidatorScoreParameters { + uint256 exponent; + FixidityLib.Fraction adjustmentSpeed; } mapping(address => ValidatorGroup) private groups; @@ -58,12 +77,25 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi address[] private _validators; RegistrationRequirements public registrationRequirements; DeregistrationLockups public deregistrationLockups; + ValidatorScoreParameters private validatorScoreParameters; + uint256 public validatorEpochPayment; + uint256 public membershipHistoryLength; uint256 public maxGroupSize; + event Debug(uint256 value); event MaxGroupSizeSet( uint256 size ); + event ValidatorEpochPaymentSet( + uint256 value + ); + + event ValidatorScoreParametersSet( + uint256 exponent, + uint256 adjustmentSpeed + ); + event RegistrationRequirementsSet( uint256 group, uint256 validator @@ -120,6 +152,11 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi address indexed validator ); + modifier onlyVm() { + require(msg.sender == address(0)); + _; + } + /** * @notice Initializes critical variables. * @param registryAddress The address of the registry contract. @@ -127,6 +164,10 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi * @param validatorRequirement The minimum locked gold needed to register a validator. * @param groupLockup The duration the above gold remains locked after deregistration. * @param validatorLockup The duration the above gold remains locked after deregistration. + * @param validatorScoreExponent The exponent used in calculating validator scores. + * @param validatorScoreAdjustmentSpeed The speed at which validator scores are adjusted. + * @param _validatorEpochPayment The duration the above gold remains locked after deregistration. + * @param _membershipHistoryLength The maximum number of entries for validator membership history. * @param _maxGroupSize The maximum group size. * @dev Should be called only once. */ @@ -136,15 +177,26 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi uint256 validatorRequirement, uint256 groupLockup, uint256 validatorLockup, + uint256 validatorScoreExponent, + uint256 validatorScoreAdjustmentSpeed, + uint256 _validatorEpochPayment, + uint256 _membershipHistoryLength, uint256 _maxGroupSize ) external initializer { + require(validatorScoreAdjustmentSpeed <= FixidityLib.fixed1().unwrap()); _transferOwnership(msg.sender); setRegistry(registryAddress); registrationRequirements = RegistrationRequirements(groupRequirement, validatorRequirement); deregistrationLockups = DeregistrationLockups(groupLockup, validatorLockup); + validatorScoreParameters = ValidatorScoreParameters( + validatorScoreExponent, + FixidityLib.wrap(validatorScoreAdjustmentSpeed) + ); + validatorEpochPayment = _validatorEpochPayment; + membershipHistoryLength = _membershipHistoryLength; maxGroupSize = _maxGroupSize; } @@ -160,6 +212,35 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi return true; } + /** + * @notice Sets the per-epoch payment in Celo Dollars for validators, less group commission. + * @param value The value in Celo Dollars. + * @return True upon success. + */ + function setValidatorEpochPayment(uint256 value) external onlyOwner returns (bool) { + require(value != validatorEpochPayment); + validatorEpochPayment = value; + emit ValidatorEpochPaymentSet(value); + return true; + } + + /** + * @notice Updates the validator score parameters. + * @param exponent The exponent used in calculating the score. + * @param adjustmentSpeed The speed at which the score is adjusted. + * @return True upon success. + */ + function setValidatorScoreParameters(uint256 exponent, uint256 adjustmentSpeed) external onlyOwner returns (bool) { + require(adjustmentSpeed <= FixidityLib.fixed1().unwrap()); + require( + exponent != validatorScoreParameters.exponent || + !FixidityLib.wrap(adjustmentSpeed).equals(validatorScoreParameters.adjustmentSpeed) + ); + validatorScoreParameters = ValidatorScoreParameters(exponent, FixidityLib.wrap(adjustmentSpeed)); + emit ValidatorScoreParametersSet(exponent, adjustmentSpeed); + return true; + } + /** * @notice Returns the maximum number of members a group can add. * @return The maximum number of members a group can add. @@ -248,12 +329,13 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi // Use the proof of possession bytes require(checkProofOfPossession(publicKeysData.slice(64, 48 + 96))); - address account = getLockedGold().getAccountFromValidator(msg.sender); + address account = getLockedGold().getAccountFromActiveValidator(msg.sender); require(!isValidator(account) && !isValidatorGroup(account)); require(meetsValidatorRegistrationRequirement(account)); - Validator memory validator = Validator(name, url, publicKeysData, address(0)); - validators[account] = validator; + validators[account].name = name; + validators[account].url = url; + validators[account].publicKeysData = publicKeysData; _validators.push(account); getLockedGold().setAccountMustMaintain(account, registrationRequirements.validator, MAX_INT); emit ValidatorRegistered(account, name, url, publicKeysData); @@ -289,6 +371,80 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi return getLockedGold().getAccountTotalLockedGold(account) >= registrationRequirements.group; } + function getValidatorScoreParameters() external view returns (uint256, uint256) { + return (validatorScoreParameters.exponent, validatorScoreParameters.adjustmentSpeed.unwrap()); + } + + function getMembershipHistory(address account) external view returns (uint256[] memory, address[] memory) { + MembershipHistoryEntry[] memory entries = validators[account].membershipHistory.entries; + uint256[] memory epochs = new uint256[](entries.length); + address[] memory membershipGroups = new address[](entries.length); + for (uint256 i = 0; i < entries.length; i = i.add(1)) { + epochs[i] = entries[i].epochNumber; + membershipGroups[i] = entries[i].group; + } + return (epochs, membershipGroups); + } + + /** + * @notice Updates a validator's score based on its uptime for the epoch. + * @param validator The address of the validator. + * @param uptime The Fixidity representation of the validator's uptime, between 0 and 1. + * @return True upon success. + */ + function updateValidatorScore(address validator, uint256 uptime) external onlyVm() returns (bool) { + return _updateValidatorScore(validator, uptime); + } + + /** + * @notice Updates a validator's score based on its uptime for the epoch. + * @param validator The address of the validator. + * @param uptime The Fixidity representation of the validator's uptime, between 0 and 1. + * @return True upon success. + */ + function _updateValidatorScore(address validator, uint256 uptime) internal returns (bool) { + address account = getLockedGold().getAccountFromValidator(validator); + require(isValidator(account), "isvalidator"); + require(uptime <= FixidityLib.fixed1().unwrap(), "uptime"); + + // TODO(asa): Use exponent. + FixidityLib.Fraction memory epochScore = FixidityLib.wrap(uptime); + + FixidityLib.Fraction memory newComponent = validatorScoreParameters.adjustmentSpeed.multiply( + epochScore + ); + FixidityLib.Fraction memory currentComponent = FixidityLib.fixed1().subtract( + validatorScoreParameters.adjustmentSpeed + ); + emit Debug(currentComponent.unwrap()); + currentComponent = currentComponent.multiply(validators[account].score); + emit Debug(currentComponent.unwrap()); + validators[account].score = FixidityLib.wrap( + Math.min( + epochScore.unwrap(), + newComponent.add(currentComponent).unwrap() + ) + ); + return true; + } + + function distributeEpochPayment(address validator) external onlyVm() returns (bool) { + return _distributeEpochPayment(validator); + } + + function _distributeEpochPayment(address validator) internal returns (bool) { + address account = getLockedGold().getAccountFromValidator(validator); + require(isValidator(account)); + FixidityLib.Fraction memory totalPayment = FixidityLib.newFixed(validatorEpochPayment).multiply(validators[account].score); + address group = getMembershipInLastEpoch(account); + uint256 groupPayment = totalPayment.multiply(groups[group].commission).fromFixed(); + uint256 validatorPayment = totalPayment.fromFixed().sub(groupPayment); + /* + getStableToken().mint(group, groupPayment); + getStableToken().mint(account, validatorPayment); + */ + } + /** * @notice De-registers a validator, removing it from the group for which it is a member. * @param index The index of this validator in the list of all validators. @@ -296,7 +452,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi * @dev Fails if the account is not a validator. */ function deregisterValidator(uint256 index) external nonReentrant returns (bool) { - address account = getLockedGold().getAccountFromValidator(msg.sender); + address account = getLockedGold().getAccountFromActiveValidator(msg.sender); require(isValidator(account)); Validator storage validator = validators[account]; if (validator.affiliation != address(0)) { @@ -320,7 +476,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi * @dev De-affiliates with the previously affiliated group if present. */ function affiliate(address group) external nonReentrant returns (bool) { - address account = getLockedGold().getAccountFromValidator(msg.sender); + address account = getLockedGold().getAccountFromActiveValidator(msg.sender); require(isValidator(account) && isValidatorGroup(group), "blah"); Validator storage validator = validators[account]; if (validator.affiliation != address(0)) { @@ -337,7 +493,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi * @dev Fails if the account is not a validator with non-zero affiliation. */ function deaffiliate() external nonReentrant returns (bool) { - address account = getLockedGold().getAccountFromValidator(msg.sender); + address account = getLockedGold().getAccountFromActiveValidator(msg.sender); require(isValidator(account)); Validator storage validator = validators[account]; require(validator.affiliation != address(0)); @@ -365,7 +521,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi require(bytes(name).length > 0); require(bytes(url).length > 0); require(commission <= FixidityLib.fixed1().unwrap(), "Commission can't be greater than 100%"); - address account = getLockedGold().getAccountFromValidator(msg.sender); + address account = getLockedGold().getAccountFromActiveValidator(msg.sender); require(!isValidator(account) && !isValidatorGroup(account)); require(meetsValidatorGroupRegistrationRequirement(account)); @@ -386,7 +542,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi * @dev Fails if the account is not a validator group with no members. */ function deregisterValidatorGroup(uint256 index) external nonReentrant returns (bool) { - address account = getLockedGold().getAccountFromValidator(msg.sender); + address account = getLockedGold().getAccountFromActiveValidator(msg.sender); // Only empty Validator Groups can be deregistered. require(isValidatorGroup(account) && groups[account].members.numElements == 0); delete groups[account]; @@ -407,7 +563,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi * @dev Fails if `validator` has not set their affiliation to this account. */ function addMember(address validator) external nonReentrant returns (bool) { - address account = getLockedGold().getAccountFromValidator(msg.sender); + address account = getLockedGold().getAccountFromActiveValidator(msg.sender); require(isValidatorGroup(account) && isValidator(validator)); return _addMember(account, validator); } @@ -423,6 +579,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi require(_group.members.numElements < maxGroupSize); require(validators[validator].affiliation == group && !_group.members.contains(validator)); _group.members.push(validator); + updateMembershipHistory(validator, group); emit ValidatorGroupMemberAdded(group, validator); return true; } @@ -434,7 +591,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi * @dev Fails if `validator` is not a member of the account's group. */ function removeMember(address validator) external nonReentrant returns (bool) { - address account = getLockedGold().getAccountFromValidator(msg.sender); + address account = getLockedGold().getAccountFromActiveValidator(msg.sender); require(isValidatorGroup(account) && isValidator(validator), "is not group and validator"); return _removeMember(account, validator); } @@ -458,7 +615,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi nonReentrant returns (bool) { - address account = getLockedGold().getAccountFromValidator(msg.sender); + address account = getLockedGold().getAccountFromActiveValidator(msg.sender); require(isValidatorGroup(account) && isValidator(validator)); ValidatorGroup storage group = groups[account]; require(group.members.contains(validator)); @@ -481,7 +638,8 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi string memory name, string memory url, bytes memory publicKeysData, - address affiliation + address affiliation, + uint256 score ) { require(isValidator(account)); @@ -490,7 +648,8 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi validator.name, validator.url, validator.publicKeysData, - validator.affiliation + validator.affiliation, + validator.score.unwrap() ); } @@ -526,7 +685,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi * @param n The number of members to return. * @return The top n group members for a particular group. */ - function getTopValidatorsFromGroup( + function getTopGroupValidators( address account, uint256 n ) @@ -645,6 +804,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi ValidatorGroup storage _group = groups[group]; require(validators[validator].affiliation == group && _group.members.contains(validator)); _group.members.remove(validator); + updateMembershipHistory(validator, address(0)); emit ValidatorGroupMemberRemoved(group, validator); // Empty validator groups are not electable. @@ -654,6 +814,42 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi return true; } + function updateMembershipHistory(address account, address group) private returns (bool) { + MembershipHistory storage history = validators[account].membershipHistory; + uint256 epochNumber = getEpochNumber(); + if (history.entries.length > 0 && epochNumber == history.entries[history.head].epochNumber) { + // There have been no elections since the validator last changed membership, overwrite the + // previous entry. + history.entries[history.head] = MembershipHistoryEntry(epochNumber, group); + } else { + if (history.entries.length > 0) { + // MembershipHistoryEntries are a circular buffer. + history.head = history.head.add(1) % membershipHistoryLength; + } + if (history.head >= history.entries.length) { + history.entries.push(MembershipHistoryEntry(epochNumber, group)); + } else { + history.entries[history.head] = MembershipHistoryEntry(epochNumber, group); + } + } + } + + function getMembershipInLastEpoch(address account) public view returns (address) { + uint256 epochNumber = getEpochNumber(); + MembershipHistory storage history = validators[account].membershipHistory; + uint256 head = history.head; + // If the most recent entry in the membership history is for the current epoch number, we need + // to look at the previous entry. + if (history.entries[head].epochNumber == epochNumber) { + if (head > 0) { + head = head.sub(1); + } else if (history.entries.length > 1) { + head = history.entries.length.sub(1); + } + } + return history.entries[head].group; + } + /** * @notice De-affiliates a validator, removing it from the group for which it is a member. * @param validator The validator to deaffiliate from their affiliated validator group. diff --git a/packages/protocol/contracts/governance/interfaces/ILockedGold.sol b/packages/protocol/contracts/governance/interfaces/ILockedGold.sol index 1f961197356..ab908add4a1 100644 --- a/packages/protocol/contracts/governance/interfaces/ILockedGold.sol +++ b/packages/protocol/contracts/governance/interfaces/ILockedGold.sol @@ -2,7 +2,8 @@ pragma solidity ^0.5.3; interface ILockedGold { - function getAccountFromVoter(address) external view returns (address); + function getAccountFromActiveVoter(address) external view returns (address); + function getAccountFromActiveValidator(address) external view returns (address); function getAccountFromValidator(address) external view returns (address); function getValidatorFromAccount(address) external view returns (address); function incrementNonvotingAccountBalance(address, uint256) external; diff --git a/packages/protocol/contracts/governance/interfaces/IValidators.sol b/packages/protocol/contracts/governance/interfaces/IValidators.sol index 5bc937ee60f..c0da21d6b99 100644 --- a/packages/protocol/contracts/governance/interfaces/IValidators.sol +++ b/packages/protocol/contracts/governance/interfaces/IValidators.sol @@ -5,5 +5,5 @@ interface IValidators { function getGroupNumMembers(address) external view returns (uint256); function getGroupsNumMembers(address[] calldata) external view returns (uint256[] memory); function getNumRegisteredValidators() external view returns (uint256); - function getTopValidatorsFromGroup(address, uint256) external view returns (address[] memory); + function getTopGroupValidators(address, uint256) external view returns (address[] memory); } diff --git a/packages/protocol/contracts/governance/test/MockLockedGold.sol b/packages/protocol/contracts/governance/test/MockLockedGold.sol index 970e8079d33..d0846c7973a 100644 --- a/packages/protocol/contracts/governance/test/MockLockedGold.sol +++ b/packages/protocol/contracts/governance/test/MockLockedGold.sol @@ -8,7 +8,8 @@ import "../interfaces/ILockedGold.sol"; /** * @title A mock LockedGold for testing. */ -contract MockLockedGold is ILockedGold { +// TODO(asa): Use ILockedGold interface. +contract MockLockedGold { using SafeMath for uint256; @@ -37,7 +38,11 @@ contract MockLockedGold is ILockedGold { return accountOrValidator; } - function getAccountFromVoter(address accountOrVoter) external view returns (address) { + function getAccountFromActiveValidator(address accountOrValidator) external view returns (address) { + return accountOrValidator; + } + + function getAccountFromActiveVoter(address accountOrVoter) external view returns (address) { return accountOrVoter; } diff --git a/packages/protocol/contracts/governance/test/ValidatorsTest.sol b/packages/protocol/contracts/governance/test/ValidatorsTest.sol new file mode 100644 index 00000000000..1c7421969fe --- /dev/null +++ b/packages/protocol/contracts/governance/test/ValidatorsTest.sol @@ -0,0 +1,22 @@ +pragma solidity ^0.5.8; + +import "../Validators.sol"; +import "../../common/FixidityLib.sol"; + +/** + * @title A wrapper around Validators that exposes onlyVm functions for testing. + */ +contract ValidatorsTest is Validators { + + function getEpochNumber() public view returns (uint256) { + return block.number / 100; + } + + function updateValidatorScore(address validator, uint256 uptime) external returns (bool) { + return _updateValidatorScore(validator, uptime); + } + + function distributeEpochPayment(address validator) external returns (bool) { + return _distributeEpochPayment(validator); + } +} diff --git a/packages/protocol/lib/test-utils.ts b/packages/protocol/lib/test-utils.ts index ebc328f4f0f..654ca5adf68 100644 --- a/packages/protocol/lib/test-utils.ts +++ b/packages/protocol/lib/test-utils.ts @@ -85,6 +85,12 @@ export async function timeTravel(seconds: number, web3: Web3) { await jsonRpc(web3, 'evm_mine', []) } +export async function mineBlocks(blocks: number, web3: Web3) { + for (let i = 0; i < blocks; i++) { + await jsonRpc(web3, 'evm_mine', []) + } +} + export async function assertBalance(address: string, balance: BigNumber) { const block = await web3.eth.getBlock('latest') const web3balance = new BigNumber(await web3.eth.getBalance(address)) diff --git a/packages/protocol/migrations/11_validators.ts b/packages/protocol/migrations/11_validators.ts index 4a76fac9fcd..5f24860c230 100644 --- a/packages/protocol/migrations/11_validators.ts +++ b/packages/protocol/migrations/11_validators.ts @@ -1,6 +1,7 @@ import { CeloContractName } from '@celo/protocol/lib/registry-utils' import { deploymentForCoreContract } from '@celo/protocol/lib/web3-utils' import { config } from '@celo/protocol/migrationsConfig' +import { toFixed } from '@celo/utils/lib/fixidity' import { ValidatorsInstance } from 'types' const initializeArgs = async (): Promise => { @@ -10,6 +11,10 @@ const initializeArgs = async (): Promise => { config.validators.registrationRequirements.validator, config.validators.deregistrationLockups.group, config.validators.deregistrationLockups.validator, + config.validators.validatorScoreParameters.exponent, + toFixed(config.validators.validatorScoreParameters.adjustmentSpeed).toFixed(), + config.validators.validatorEpochPayment, + config.validators.membershipHistoryLength, config.validators.maxGroupSize, ] } diff --git a/packages/protocol/migrationsConfig.js b/packages/protocol/migrationsConfig.js index a270504a09f..d78aca5bc07 100644 --- a/packages/protocol/migrationsConfig.js +++ b/packages/protocol/migrationsConfig.js @@ -75,6 +75,12 @@ const DefaultConfig = { group: 60 * 24 * 60 * 60, // 60 days validator: 60 * 24 * 60 * 60, // 60 days }, + validatorScoreParameters: { + exponent: 1, + adjustmentSpeed: 0.1, + }, + validatorEpochPayment: '1000000000000000000', + membershipHistoryLength: 60, maxGroupSize: 10, validatorKeys: [], @@ -104,7 +110,7 @@ const linkedLibraries = { 'SortedLinkedListWithMedian', ], SortedLinkedListWithMedian: ['AddressSortedLinkedListWithMedian'], - AddressLinkedList: ['Validators'], + AddressLinkedList: ['Validators', 'ValidatorsTest'], AddressSortedLinkedList: ['Election'], IntegerSortedLinkedList: ['Governance', 'IntegerSortedLinkedListTest'], AddressSortedLinkedListWithMedian: ['SortedOracles', 'AddressSortedLinkedListWithMedianTest'], diff --git a/packages/protocol/test/governance/validators.ts b/packages/protocol/test/governance/validators.ts index 2251fca668c..bfbb97d163e 100644 --- a/packages/protocol/test/governance/validators.ts +++ b/packages/protocol/test/governance/validators.ts @@ -3,7 +3,9 @@ import { assertContainSubset, assertEqualBN, assertRevert, + assertSameAddress, NULL_ADDRESS, + mineBlocks, } from '@celo/protocol/lib/test-utils' import BigNumber from 'bignumber.js' import { @@ -13,12 +15,12 @@ import { MockElectionInstance, RegistryContract, RegistryInstance, - ValidatorsContract, - ValidatorsInstance, + ValidatorsTestContract, + ValidatorsTestInstance, } from 'types' -import { toFixed } from '@celo/utils/lib/fixidity' +import { fromFixed, toFixed } from '@celo/utils/lib/fixidity' -const Validators: ValidatorsContract = artifacts.require('Validators') +const Validators: ValidatorsTestContract = artifacts.require('ValidatorsTest') const MockLockedGold: MockLockedGoldContract = artifacts.require('MockLockedGold') const MockElection: MockElectionContract = artifacts.require('MockElection') const Registry: RegistryContract = artifacts.require('Registry') @@ -33,6 +35,7 @@ const parseValidatorParams = (validatorParams: any) => { url: validatorParams[1], publicKeysData: validatorParams[2], affiliation: validatorParams[3], + score: validatorParams[4], } } @@ -41,18 +44,36 @@ const parseValidatorGroupParams = (groupParams: any) => { name: groupParams[0], url: groupParams[1], members: groupParams[2], + commission: groupParams[3], } } const HOUR = 60 * 60 const DAY = 24 * HOUR const MAX_UINT256 = new BigNumber(2).pow(256).minus(1) +// Hard coded in ValidatorsTest.sol +const EPOCH = 100 contract('Validators', (accounts: string[]) => { - let validators: ValidatorsInstance + let validators: ValidatorsTestInstance let registry: RegistryInstance let mockLockedGold: MockLockedGoldInstance let mockElection: MockElectionInstance + const nonOwner = accounts[1] + + const registrationRequirements = { group: new BigNumber(1000), validator: new BigNumber(100) } + const deregistrationLockups = { + group: new BigNumber(100 * DAY), + validator: new BigNumber(60 * DAY), + } + const validatorScoreParameters = { + exponent: new BigNumber(1), + adjustmentSpeed: toFixed(0.25), + } + const validatorEpochPayment = new BigNumber(10000000000000) + const membershipHistoryLength = new BigNumber(3) + const maxGroupSize = new BigNumber(5) + // A random 64 byte hex string. const publicKey = 'ea0733ad275e2b9e05541341a97ee82678c58932464fad26164657a111a7e37a9fa0300266fb90e2135a1f1512350cb4e985488a88809b14e3cbe415e76e82b2' @@ -60,16 +81,7 @@ contract('Validators', (accounts: string[]) => { '4d23d8cd06f30b1fa7cf368e2f5399ab04bb6846c682f493a98a607d3dfb7e53a712bb79b475c57b0ac2785460f91301' const blsPoP = '9d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d740501' - const publicKeysData = '0x' + publicKey + blsPublicKey + blsPoP - - const nonOwner = accounts[1] - const registrationRequirements = { group: new BigNumber(1000), validator: new BigNumber(100) } - const deregistrationLockups = { - group: new BigNumber(100 * DAY), - validator: new BigNumber(60 * DAY), - } - const maxGroupSize = 5 const name = 'test-name' const url = 'test-url' const commission = toFixed(1 / 100) @@ -86,6 +98,10 @@ contract('Validators', (accounts: string[]) => { registrationRequirements.validator, deregistrationLockups.group, deregistrationLockups.validator, + validatorScoreParameters.exponent, + validatorScoreParameters.adjustmentSpeed, + validatorEpochPayment, + membershipHistoryLength, maxGroupSize ) }) @@ -133,6 +149,27 @@ contract('Validators', (accounts: string[]) => { assertEqualBN(validator, deregistrationLockups.validator) }) + it('should have set the validator score parameters', async () => { + const [exponent, adjustmentSpeed] = await validators.getValidatorScoreParameters() + assertEqualBN(exponent, validatorScoreParameters.exponent) + assertEqualBN(adjustmentSpeed, validatorScoreParameters.adjustmentSpeed) + }) + + it('should have set the validator epoch payment', async () => { + const actual = await validators.validatorEpochPayment() + assertEqualBN(actual, validatorEpochPayment) + }) + + it('should have set the membership history length', async () => { + const actual = await validators.membershipHistoryLength() + assertEqualBN(actual, membershipHistoryLength) + }) + + it('should have set the max group size', async () => { + const actual = await validators.maxGroupSize() + assertEqualBN(actual, maxGroupSize) + }) + it('should not be callable again', async () => { await assertRevert( validators.initialize( @@ -141,12 +178,106 @@ contract('Validators', (accounts: string[]) => { registrationRequirements.validator, deregistrationLockups.group, deregistrationLockups.validator, + validatorScoreParameters.exponent, + validatorScoreParameters.adjustmentSpeed, + validatorEpochPayment, + membershipHistoryLength, maxGroupSize ) ) }) }) + describe('#setValidatorEpochPayment()', () => { + describe('when the payment is different', () => { + const newPayment = validatorEpochPayment.plus(1) + + describe('when called by the owner', () => { + let resp: any + + beforeEach(async () => { + resp = await validators.setValidatorEpochPayment(newPayment) + }) + + it('should set the validator epoch payment', async () => { + assertEqualBN(await validators.validatorEpochPayment(), newPayment) + }) + + it('should emit the ValidatorEpochPaymentSet event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorEpochPaymentSet', + args: { + value: new BigNumber(newPayment), + }, + }) + }) + + describe('when called by a non-owner', () => { + it('should revert', async () => { + await assertRevert( + validators.setValidatorEpochPayment(newPayment, { + from: nonOwner, + }) + ) + }) + }) + }) + + describe('when the payment is the same', () => { + it('should revert', async () => { + await assertRevert(validators.setValidatorEpochPayment(validatorEpochPayment)) + }) + }) + }) + }) + + describe('#setMaxGroupSize()', () => { + describe('when the group size is different', () => { + const newSize = maxGroupSize.plus(1) + + describe('when called by the owner', () => { + let resp: any + + beforeEach(async () => { + resp = await validators.setMaxGroupSize(newSize) + }) + + it('should set the max group size', async () => { + assertEqualBN(await validators.maxGroupSize(), newSize) + }) + + it('should emit the MaxGroupSizeSet event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'MaxGroupSizeSet', + args: { + size: new BigNumber(newSize), + }, + }) + }) + + describe('when called by a non-owner', () => { + it('should revert', async () => { + await assertRevert( + validators.setMaxGroupSize(newSize, { + from: nonOwner, + }) + ) + }) + }) + }) + + describe('when the size is the same', () => { + it('should revert', async () => { + await assertRevert(validators.setMaxGroupSize(maxGroupSize)) + }) + }) + }) + }) + describe('#setRegistrationRequirements()', () => { describe('when the requirements are different', () => { const newRequirements = { @@ -211,7 +342,7 @@ contract('Validators', (accounts: string[]) => { }) describe('#setDeregistrationLockups()', () => { - describe('when the requirements are different', () => { + describe('when the lockups are different', () => { const newLockups = { group: deregistrationLockups.group.plus(1), validator: deregistrationLockups.validator.plus(1), @@ -224,7 +355,7 @@ contract('Validators', (accounts: string[]) => { resp = await validators.setDeregistrationLockups(newLockups.group, newLockups.validator) }) - it('should set the group and validator requirements', async () => { + it('should set the group and validator lockups', async () => { const [group, validator] = await validators.getDeregistrationLockups() assertEqualBN(group, newLockups.group) assertEqualBN(validator, newLockups.validator) @@ -253,7 +384,7 @@ contract('Validators', (accounts: string[]) => { }) }) - describe('when the requirements are the same', () => { + describe('when the lockups are the same', () => { it('should revert', async () => { await assertRevert( validators.setDeregistrationLockups( @@ -266,11 +397,74 @@ contract('Validators', (accounts: string[]) => { }) }) + describe('#setValidatorScoreParameters()', () => { + describe('when the parameters are different', () => { + const newParameters = { + exponent: validatorScoreParameters.exponent.plus(1), + adjustmentSpeed: validatorScoreParameters.adjustmentSpeed.plus(1), + } + + describe('when called by the owner', () => { + let resp: any + + beforeEach(async () => { + resp = await validators.setValidatorScoreParameters( + newParameters.exponent, + newParameters.adjustmentSpeed + ) + }) + + it('should set the exponent and adjustment speed', async () => { + const [exponent, adjustmentSpeed] = await validators.getValidatorScoreParameters() + assertEqualBN(exponent, newParameters.exponent) + assertEqualBN(adjustmentSpeed, newParameters.adjustmentSpeed) + }) + + it('should emit the ValidatorScoreParametersSet event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorScoreParametersSet', + args: { + exponent: new BigNumber(newParameters.exponent), + adjustmentSpeed: new BigNumber(newParameters.adjustmentSpeed), + }, + }) + }) + + describe('when called by a non-owner', () => { + it('should revert', async () => { + await assertRevert( + validators.setValidatorScoreParameters( + newParameters.exponent, + newParameters.adjustmentSpeed, + { + from: nonOwner, + } + ) + ) + }) + }) + }) + + describe('when the requirements are the same', () => { + it('should revert', async () => { + await assertRevert( + validators.setValidatorScoreParameters( + validatorScoreParameters.exponent, + validatorScoreParameters.adjustmentSpeed + ) + ) + }) + }) + }) + }) + describe('#setMaxGroupSize()', () => { describe('when the size is different', () => { describe('when called by the owner', () => { let resp: any - const newSize = maxGroupSize + 1 + const newSize = maxGroupSize.plus(1) beforeEach(async () => { resp = await validators.setMaxGroupSize(newSize) @@ -665,6 +859,18 @@ contract('Validators', (accounts: string[]) => { assert.deepEqual(parsedGroup.members, []) }) + it("should update the member's membership history", async () => { + await validators.deaffiliate() + const membershipHistory = await validators.getMembershipHistory(validator) + const expectedEpoch = new BigNumber( + Math.floor((await web3.eth.getBlock('latest')).number / EPOCH) + ) + assert.equal(membershipHistory[0].length, 1) + assertEqualBN(membershipHistory[0][0], expectedEpoch) + assert.equal(membershipHistory[1].length, 1) + assertSameAddress(membershipHistory[1][0], NULL_ADDRESS) + }) + it('should emit the ValidatorGroupMemberRemoved event', async () => { const resp = await validators.deaffiliate() assert.equal(resp.logs.length, 2) @@ -844,6 +1050,17 @@ contract('Validators', (accounts: string[]) => { assert.deepEqual(parsedGroup.members, [validator]) }) + it("should update the member's membership history", async () => { + const membershipHistory = await validators.getMembershipHistory(validator) + const expectedEpoch = new BigNumber( + Math.floor((await web3.eth.getBlock('latest')).number / EPOCH) + ) + assert.equal(membershipHistory[0].length, 1) + assertEqualBN(membershipHistory[0][0], expectedEpoch) + assert.equal(membershipHistory[1].length, 1) + assertSameAddress(membershipHistory[1][0], group) + }) + it('should emit the ValidatorGroupMemberAdded event', async () => { assert.equal(resp.logs.length, 1) const log = resp.logs[0] @@ -901,6 +1118,18 @@ contract('Validators', (accounts: string[]) => { assert.deepEqual(parsedGroup.members, []) }) + it("should update the member's membership history", async () => { + await validators.removeMember(validator) + const membershipHistory = await validators.getMembershipHistory(validator) + const expectedEpoch = new BigNumber( + Math.floor((await web3.eth.getBlock('latest')).number / EPOCH) + ) + assert.equal(membershipHistory[0].length, 1) + assertEqualBN(membershipHistory[0][0], expectedEpoch) + assert.equal(membershipHistory[1].length, 1) + assertSameAddress(membershipHistory[1][0], NULL_ADDRESS) + }) + it('should emit the ValidatorGroupMemberRemoved event', async () => { const resp = await validators.removeMember(validator) assert.equal(resp.logs.length, 1) @@ -987,4 +1216,115 @@ contract('Validators', (accounts: string[]) => { }) }) }) + + describe('#updateValidatorScore', () => { + const validator = accounts[0] + beforeEach(async () => { + await registerValidator(validator) + }) + + describe('when 0 <= uptime <= 1.0', () => { + const uptime = 0.99 + const adjustmentSpeed = fromFixed(validatorScoreParameters.adjustmentSpeed) + beforeEach(async () => { + await validators.updateValidatorScore(validator, toFixed(uptime)) + }) + + it('should update the validator score', async () => { + const expectedScore = adjustmentSpeed.times(uptime) + const parsedValidator = parseValidatorParams(await validators.getValidator(validator)) + assertEqualBN(parsedValidator.score, toFixed(expectedScore)) + }) + + describe('when the validator already has a non-zero score', () => { + beforeEach(async () => { + await validators.updateValidatorScore(validator, toFixed(uptime)) + }) + + it('should update the validator score', async () => { + let expectedScore = adjustmentSpeed.times(uptime) + expectedScore = new BigNumber(1) + .minus(adjustmentSpeed) + .times(expectedScore) + .plus(expectedScore) + const parsedValidator = parseValidatorParams(await validators.getValidator(validator)) + assertEqualBN(parsedValidator.score, toFixed(expectedScore)) + }) + }) + }) + + describe('when uptime > 1.0', () => { + const uptime = 1.01 + it('should revert', async () => { + await assertRevert(validators.updateValidatorScore(validator, toFixed(uptime))) + }) + }) + }) + + describe('#updateMembershipHistory', () => { + const validator = accounts[0] + const groups = accounts.slice(1) + beforeEach(async () => { + await registerValidator(validator) + for (const group of groups) { + await registerValidatorGroup(group) + } + }) + + describe('when changing groups more times than membership history length', () => { + it('should always store the most recent memberships', async () => { + for (let i = 0; i < membershipHistoryLength.plus(1).toNumber(); i++) { + await validators.affiliate(groups[i]) + const currentEpoch = new BigNumber( + Math.floor((await web3.eth.getBlock('latest')).number / EPOCH) + ) + await validators.addMember(validator, { from: groups[i] }) + await mineBlocks(EPOCH, web3) + + const membershipHistory = await validators.getMembershipHistory(validator) + const expectedMembershipHistoryLength = Math.min( + i + 1, + membershipHistoryLength.toNumber() + ) + assert.equal(membershipHistory[0].length, expectedMembershipHistoryLength) + assert.equal(membershipHistory[1].length, expectedMembershipHistoryLength) + for (let j = 0; j < expectedMembershipHistoryLength; j++) { + assert.include( + membershipHistory[0].map((x) => x.toNumber()), + currentEpoch.minus(j).toNumber() + ) + assert.include( + membershipHistory[1].map((x) => x.toLowerCase()), + groups[i - j].toLowerCase() + ) + } + } + }) + }) + }) + + describe('#getMembershipInLastEpoch', () => { + const validator = accounts[0] + const groups = accounts.slice(1) + beforeEach(async () => { + await registerValidator(validator) + for (const group of groups) { + await registerValidatorGroup(group) + } + }) + + describe('when changing groups more times than membership history length', () => { + it('should always return the correct membership for the last epoch', async () => { + for (let i = 0; i < membershipHistoryLength.plus(1).toNumber(); i++) { + await validators.affiliate(groups[i]) + await validators.addMember(validator, { from: groups[i] }) + if (i > 0) { + assert.equal(await validators.getMembershipInLastEpoch(validator), groups[i - 1]) + } + await mineBlocks(EPOCH, web3) + assert.equal(await validators.getMembershipInLastEpoch(validator), groups[i]) + } + }) + }) + }) }) From 47a247c52004fbae0dd512aeadd01edd3f6354ec Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Fri, 4 Oct 2019 11:12:41 -0700 Subject: [PATCH 028/149] Added test for epoch payment distribution --- .../contracts/common/UsingRegistry.sol | 8 +++ .../contracts/governance/Validators.sol | 2 - .../contracts/stability/StableToken.sol | 51 ++++++++++--------- .../stability/interfaces/IStableToken.sol | 12 ----- .../stability/test/MockStableToken.sol | 4 +- packages/protocol/lib/test-utils.ts | 12 ----- .../protocol/migrations/08_stabletoken.ts | 19 +------ packages/protocol/migrations/09_exchange.ts | 7 --- packages/protocol/migrationsConfig.js | 6 ++- .../protocol/scripts/truffle/network_check.ts | 2 - .../protocol/test/governance/validators.ts | 38 ++++++++++++++ .../protocol/test/stability/stabletoken.ts | 45 ++++++++-------- 12 files changed, 103 insertions(+), 103 deletions(-) diff --git a/packages/protocol/contracts/common/UsingRegistry.sol b/packages/protocol/contracts/common/UsingRegistry.sol index 24561e9fc18..b09cae9cb0f 100644 --- a/packages/protocol/contracts/common/UsingRegistry.sol +++ b/packages/protocol/contracts/common/UsingRegistry.sol @@ -11,6 +11,8 @@ import "../governance/interfaces/IValidators.sol"; import "../identity/interfaces/IRandom.sol"; +import "../stability/interfaces/IStableToken.sol"; + // Ideally, UsingRegistry should inherit from Initializable and implement initialize() which calls // setRegistry(). TypeChain currently has problems resolving overloaded functions, so this is not // possible right now. @@ -23,6 +25,7 @@ contract UsingRegistry is Ownable { // solhint-disable state-visibility bytes32 constant ATTESTATIONS_REGISTRY_ID = keccak256(abi.encodePacked("Attestations")); bytes32 constant ELECTION_REGISTRY_ID = keccak256(abi.encodePacked("Election")); + bytes32 constant EXCHANGE_REGISTRY_ID = keccak256(abi.encodePacked("Exchange")); bytes32 constant GAS_CURRENCY_WHITELIST_REGISTRY_ID = keccak256( abi.encodePacked("GasCurrencyWhitelist") ); @@ -32,6 +35,7 @@ contract UsingRegistry is Ownable { bytes32 constant RESERVE_REGISTRY_ID = keccak256(abi.encodePacked("Reserve")); bytes32 constant RANDOM_REGISTRY_ID = keccak256(abi.encodePacked("Random")); bytes32 constant SORTED_ORACLES_REGISTRY_ID = keccak256(abi.encodePacked("SortedOracles")); + bytes32 constant STABLE_TOKEN_REGISTRY_ID = keccak256(abi.encodePacked("StableToken")); bytes32 constant VALIDATORS_REGISTRY_ID = keccak256(abi.encodePacked("Validators")); // solhint-enable state-visibility @@ -67,6 +71,10 @@ contract UsingRegistry is Ownable { return IRandom(registry.getAddressForOrDie(RANDOM_REGISTRY_ID)); } + function getStableToken() internal view returns(IStableToken) { + return IStableToken(registry.getAddressForOrDie(STABLE_TOKEN_REGISTRY_ID)); + } + function getValidators() internal view returns(IValidators) { return IValidators(registry.getAddressForOrDie(VALIDATORS_REGISTRY_ID)); } diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index 23adae92bf1..d3e5be9de5a 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -439,10 +439,8 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi address group = getMembershipInLastEpoch(account); uint256 groupPayment = totalPayment.multiply(groups[group].commission).fromFixed(); uint256 validatorPayment = totalPayment.fromFixed().sub(groupPayment); - /* getStableToken().mint(group, groupPayment); getStableToken().mint(account, validatorPayment); - */ } /** diff --git a/packages/protocol/contracts/stability/StableToken.sol b/packages/protocol/contracts/stability/StableToken.sol index 148cfcf0402..8974fb91c7d 100644 --- a/packages/protocol/contracts/stability/StableToken.sol +++ b/packages/protocol/contracts/stability/StableToken.sol @@ -21,8 +21,6 @@ contract StableToken is IStableToken, IERC20Token, ICeloToken, Ownable, using FixidityLib for FixidityLib.Fraction; using SafeMath for uint256; - event MinterSet(address indexed _minter); - event InflationFactorUpdated( uint256 factor, uint256 lastUpdated @@ -44,7 +42,6 @@ contract StableToken is IStableToken, IERC20Token, ICeloToken, Ownable, string comment ); - address public minter; string internal name_; string internal symbol_; uint8 internal decimals_; @@ -71,14 +68,6 @@ contract StableToken is IStableToken, IERC20Token, ICeloToken, Ownable, InflationState inflationState; - /** - * @notice Throws if called by any account other than the minter. - */ - modifier onlyMinter() { - require(msg.sender == minter, "sender was not minter"); - _; - } - /** * Only VM would be able to set the caller address to 0x0 unless someone * really has the private key for 0x0 @@ -123,12 +112,22 @@ contract StableToken is IStableToken, IERC20Token, ICeloToken, Ownable, uint8 _decimals, address registryAddress, uint256 inflationRate, - uint256 inflationFactorUpdatePeriod + uint256 inflationFactorUpdatePeriod, + address[] calldata initialBalanceAddresses, + uint256[] calldata initialBalanceValues ) external initializer { require(inflationRate != 0, "Must provide a non-zero inflation rate."); + require(initialBalanceAddresses.length == initialBalanceValues.length); + for (uint256 i = 0; i < initialBalanceAddresses.length; i = i.add(1)) { + totalSupply_ = totalSupply_.add(initialBalanceValues[i]); + balances[initialBalanceAddresses[i]] = balances[initialBalanceAddresses[i]].add( + initialBalanceValues[i] + ); + } + _transferOwnership(msg.sender); totalSupply_ = 0; name_ = _name; @@ -144,16 +143,6 @@ contract StableToken is IStableToken, IERC20Token, ICeloToken, Ownable, setRegistry(registryAddress); } - // Should this be tied to the registry? - /** - * @notice Updates 'minter'. - * @param _minter An address with special permissions to modify its balance - */ - function setMinter(address _minter) external onlyOwner { - minter = _minter; - emit MinterSet(minter); - } - /** * @notice Updates Inflation Parameters. * @param rate new rate. @@ -208,10 +197,15 @@ contract StableToken is IStableToken, IERC20Token, ICeloToken, Ownable, uint256 value ) external - onlyMinter updateInflationFactor returns (bool) { + // Only the Exchange and Validators contracts are authorized to mint. + require( + msg.sender == registry.getAddressFor(EXCHANGE_REGISTRY_ID) || + msg.sender == registry.getAddressFor(VALIDATORS_REGISTRY_ID) + ); + uint256 units = _valueToUnits(inflationState.factor, value); totalSupply_ = totalSupply_.add(units); balances[to] = balances[to].add(units); @@ -241,10 +235,17 @@ contract StableToken is IStableToken, IERC20Token, ICeloToken, Ownable, } /** - * @notice Burns StableToken from the balance of 'minter'. + * @notice Burns StableToken from the balance of msg.sender. * @param value The amount of StableToken to burn. */ - function burn(uint256 value) external onlyMinter updateInflationFactor returns (bool) { + function burn( + uint256 value + ) + external + onlyRegisteredContract(EXCHANGE_REGISTRY_ID) + updateInflationFactor + returns (bool) + { uint256 units = _valueToUnits(inflationState.factor, value); require(units <= balances[msg.sender], "value exceeded balance of sender"); totalSupply_ = totalSupply_.sub(units); diff --git a/packages/protocol/contracts/stability/interfaces/IStableToken.sol b/packages/protocol/contracts/stability/interfaces/IStableToken.sol index 380be53ab2d..87edfc419aa 100644 --- a/packages/protocol/contracts/stability/interfaces/IStableToken.sol +++ b/packages/protocol/contracts/stability/interfaces/IStableToken.sol @@ -6,18 +6,6 @@ pragma solidity ^0.5.3; * absence of interface inheritance is intended as a companion to IERC20.sol and ICeloToken.sol. */ interface IStableToken { - - function initialize( - string calldata, - string calldata, - uint8, - address, - uint256, - uint256 - ) external; - - function setMinter(address) external; - function mint(address, uint256) external returns (bool); function burn(uint256) external returns (bool); function debitFrom(address, uint256) external; diff --git a/packages/protocol/contracts/stability/test/MockStableToken.sol b/packages/protocol/contracts/stability/test/MockStableToken.sol index b54b538f1c7..e0e1f1f0612 100644 --- a/packages/protocol/contracts/stability/test/MockStableToken.sol +++ b/packages/protocol/contracts/stability/test/MockStableToken.sol @@ -11,6 +11,7 @@ contract MockStableToken { bool public _needsRebase; uint256 public _totalSupply; uint256 public _targetTotalSupply; + mapping (address => uint256) public balanceOf; function setNeedsRebase() external { _needsRebase = true; @@ -24,7 +25,8 @@ contract MockStableToken { _targetTotalSupply = value; } - function mint(address, uint256) external pure returns (bool) { + function mint(address to, uint256 value) external returns (bool) { + balanceOf[to] = balanceOf[to] + value; return true; } diff --git a/packages/protocol/lib/test-utils.ts b/packages/protocol/lib/test-utils.ts index 654ca5adf68..982ea4a2a22 100644 --- a/packages/protocol/lib/test-utils.ts +++ b/packages/protocol/lib/test-utils.ts @@ -5,11 +5,9 @@ import * as chaiSubset from 'chai-subset' import { spawn } from 'child_process' import { keccak256 } from 'ethereumjs-util' import { - ExchangeInstance, ProxyInstance, RegistryInstance, ReserveInstance, - StableTokenInstance, UsingRegistryInstance, } from 'types' const soliditySha3 = new (require('web3'))().utils.soliditySha3 @@ -184,16 +182,6 @@ export const assertContractsOwnedByMultiSig = async (getContract: any) => { } } -export const assertStableTokenMinter = async (getContract: any) => { - const stableToken: StableTokenInstance = await getContract('StableToken', 'proxiedContract') - const exchange: ExchangeInstance = await getContract('Exchange', 'proxiedContract') - assert.equal( - await stableToken.minter(), - exchange.address, - 'StableToken minter not set to Exchange' - ) -} - export const assertFloatEquality = ( a: BigNumber, b: BigNumber, diff --git a/packages/protocol/migrations/08_stabletoken.ts b/packages/protocol/migrations/08_stabletoken.ts index 5bc02ece37d..e97bb4ab16d 100644 --- a/packages/protocol/migrations/08_stabletoken.ts +++ b/packages/protocol/migrations/08_stabletoken.ts @@ -3,7 +3,6 @@ import Web3 = require('web3') import { CeloContractName } from '@celo/protocol/lib/registry-utils' import { - convertToContractDecimalsBN, deploymentForCoreContract, getDeployedProxiedContract, } from '@celo/protocol/lib/web3-utils' @@ -21,7 +20,6 @@ const NULL_ADDRESS = '0x0000000000000000000000000000000000000000' const initializeArgs = async (): Promise => { const rate = toFixed(config.stableToken.inflationRate) - return [ config.stableToken.tokenName, config.stableToken.tokenSymbol, @@ -29,6 +27,8 @@ const initializeArgs = async (): Promise => { config.registry.predeployedProxyAddress, rate.toString(), config.stableToken.inflationPeriod, + config.stableToken.initialBalances.addresses, + config.stableToken.initialBalances.values, ] } @@ -39,21 +39,6 @@ module.exports = deploymentForCoreContract( initializeArgs, async (stableToken: StableTokenInstance, _web3: Web3, networkName: string) => { const minerAddress: string = truffle.networks[networkName].from - const minerStartBalance = await convertToContractDecimalsBN( - config.stableToken.minerDollarBalance.toString(), - stableToken - ) - console.log( - `Minting ${minerAddress} ${config.stableToken.minerDollarBalance.toString()} StableToken` - ) - await stableToken.setMinter(minerAddress) - - const initialBalance = web3.utils.toBN(minerStartBalance) - await stableToken.mint(minerAddress, initialBalance) - for (const address of config.stableToken.initialAccounts) { - await stableToken.mint(address, initialBalance) - } - console.log('Setting GoldToken/USD exchange rate') const sortedOracles: SortedOraclesInstance = await getDeployedProxiedContract< SortedOraclesInstance diff --git a/packages/protocol/migrations/09_exchange.ts b/packages/protocol/migrations/09_exchange.ts index b7d33e74fbe..07d78d5a295 100644 --- a/packages/protocol/migrations/09_exchange.ts +++ b/packages/protocol/migrations/09_exchange.ts @@ -30,13 +30,6 @@ module.exports = deploymentForCoreContract( CeloContractName.Exchange, initializeArgs, async (exchange: ExchangeInstance) => { - console.log('Setting Exchange as StableToken minter') - const stableToken: StableTokenInstance = await getDeployedProxiedContract( - 'StableToken', - artifacts - ) - await stableToken.setMinter(exchange.address) - console.log('Setting Exchange as a Reserve spender') const reserve: ReserveInstance = await getDeployedProxiedContract( 'Reserve', diff --git a/packages/protocol/migrationsConfig.js b/packages/protocol/migrationsConfig.js index d78aca5bc07..5b91f04888e 100644 --- a/packages/protocol/migrationsConfig.js +++ b/packages/protocol/migrationsConfig.js @@ -58,13 +58,15 @@ const DefaultConfig = { stableToken: { decimals: 18, goldPrice: 10, - minerDollarBalance: 60000, tokenName: 'Celo Dollar', tokenSymbol: 'cUSD', // 52nd root of 1.005, equivalent to 0.5% annual inflation inflationRate: 1.00009591886, inflationPeriod: 7 * 24 * 60 * 60, // 1 week - initialAccounts: [], + initialBalances: { + addresses: [], + values: [], + }, }, validators: { registrationRequirements: { diff --git a/packages/protocol/scripts/truffle/network_check.ts b/packages/protocol/scripts/truffle/network_check.ts index 4f81207c363..1f75a3aabdd 100644 --- a/packages/protocol/scripts/truffle/network_check.ts +++ b/packages/protocol/scripts/truffle/network_check.ts @@ -5,7 +5,6 @@ import { assertContractsRegistered, assertProxiesSet, assertRegistryAddressesSet, - assertStableTokenMinter, getReserveBalance, proxiedContracts, } from '@celo/protocol/lib/test-utils' @@ -41,7 +40,6 @@ module.exports = async (callback: (error?: any) => number) => { await assertContractsRegistered(getContract) await assertRegistryAddressesSet(getContract) await assertContractsOwnedByMultiSig(getContract) - await assertStableTokenMinter(getContract) await assertReserveBalance() console.log('Network check succeeded!') callback() diff --git a/packages/protocol/test/governance/validators.ts b/packages/protocol/test/governance/validators.ts index bfbb97d163e..97fa1f3ed04 100644 --- a/packages/protocol/test/governance/validators.ts +++ b/packages/protocol/test/governance/validators.ts @@ -13,6 +13,8 @@ import { MockLockedGoldInstance, MockElectionContract, MockElectionInstance, + MockStableTokenContract, + MockStableTokenInstance, RegistryContract, RegistryInstance, ValidatorsTestContract, @@ -23,6 +25,7 @@ import { fromFixed, toFixed } from '@celo/utils/lib/fixidity' const Validators: ValidatorsTestContract = artifacts.require('ValidatorsTest') const MockLockedGold: MockLockedGoldContract = artifacts.require('MockLockedGold') const MockElection: MockElectionContract = artifacts.require('MockElection') +const MockStableToken: MockStableTokenContract = artifacts.require('MockStableToken') const Registry: RegistryContract = artifacts.require('Registry') // @ts-ignore @@ -54,6 +57,7 @@ const MAX_UINT256 = new BigNumber(2).pow(256).minus(1) // Hard coded in ValidatorsTest.sol const EPOCH = 100 +// TODO(asa): Test epoch payment distribution contract('Validators', (accounts: string[]) => { let validators: ValidatorsTestInstance let registry: RegistryInstance @@ -1327,4 +1331,38 @@ contract('Validators', (accounts: string[]) => { }) }) }) + + describe('#distributeEpochPayment', () => { + const validator = accounts[0] + const group = accounts[1] + let mockStableToken: MockStableTokenInstance + beforeEach(async () => { + await registerValidatorGroupWithMembers(group, [validator]) + mockStableToken = await MockStableToken.new() + await registry.setAddressFor(CeloContractName.StableToken, mockStableToken.address) + }) + + describe('when the validator score is non-zero', () => { + const uptime = 0.99 + const adjustmentSpeed = fromFixed(validatorScoreParameters.adjustmentSpeed) + const expectedScore = adjustmentSpeed.times(uptime) + const expectedTotalPayment = expectedScore.times(validatorEpochPayment) + beforeEach(async () => { + await validators.updateValidatorScore(validator, toFixed(uptime)) + await validators.distributeEpochPayment(validator) + }) + + it('should pay the validator', async () => { + const expectedPayment = expectedTotalPayment.times( + new BigNumber(1).minus(fromFixed(commission)) + ) + assertEqualBN(await mockStableToken.balanceOf(validator), expectedPayment) + }) + + it('should pay the group', async () => { + const expectedPayment = expectedTotalPayment.times(fromFixed(commission)) + assertEqualBN(await mockStableToken.balanceOf(group), expectedPayment) + }) + }) + }) }) diff --git a/packages/protocol/test/stability/stabletoken.ts b/packages/protocol/test/stability/stabletoken.ts index 0f3addefc95..51a517d0bf8 100644 --- a/packages/protocol/test/stability/stabletoken.ts +++ b/packages/protocol/test/stability/stabletoken.ts @@ -1,3 +1,4 @@ +import { CeloContractName } from '@celo/protocol/lib/registry-utils' import { assertLogMatches, assertLogMatches2, @@ -106,34 +107,32 @@ contract('StableToken', (accounts: string[]) => { }) }) - describe('#setMinter()', () => { - const minter = accounts[0] - it('should allow owner to set minter', async () => { - await stableToken.setMinter(minter) - assert.equal(await stableToken.minter(), minter) - }) - - it('should not allow anyone else to set minter', async () => { - await assertRevert(stableToken.setMinter(minter, { from: accounts[1] })) - }) - }) - describe('#mint()', () => { - const minter = accounts[0] + const exchange = accounts[0] + const validators = accounts[1] beforeEach(async () => { - await stableToken.setMinter(minter) + await registry.setAddressFor(CeloContractName.Exchange, exchange) + await registry.setAddressFor(CeloContractName.Validators, validators) }) - it('should allow minter to mint', async () => { - await stableToken.mint(minter, amountToMint) - const balance = (await stableToken.balanceOf(minter)).toNumber() + it('should allow the registered exchange contract to mint', async () => { + await stableToken.mint(exchange, amountToMint) + const balance = (await stableToken.balanceOf(exchange)).toNumber() + assert.equal(balance, amountToMint) + const supply = (await stableToken.totalSupply()).toNumber() + assert.equal(supply, amountToMint) + }) + + it('should allow the registered validators contract to mint', async () => { + await stableToken.mint(validators, amountToMint, { from: validators }) + const balance = (await stableToken.balanceOf(validators)).toNumber() assert.equal(balance, amountToMint) const supply = (await stableToken.totalSupply()).toNumber() assert.equal(supply, amountToMint) }) it('should not allow anyone else to mint', async () => { - await assertRevert(stableToken.mint(minter, amountToMint, { from: accounts[1] })) + await assertRevert(stableToken.mint(minter, amountToMint, { from: accounts[2] })) }) }) @@ -143,7 +142,7 @@ contract('StableToken', (accounts: string[]) => { const comment = 'tacos at lunch' beforeEach(async () => { - await stableToken.setMinter(sender) + await registry.setAddressFor(CeloContractName.Exchange, sender) await stableToken.mint(sender, amountToMint) }) @@ -251,7 +250,7 @@ contract('StableToken', (accounts: string[]) => { const mintAmount = 1000 beforeEach(async () => { - await stableToken.setMinter(minter) + await registry.setAddressFor(CeloContractName.Exchange, minter) await stableToken.mint(minter, mintAmount) }) @@ -350,7 +349,7 @@ contract('StableToken', (accounts: string[]) => { const minter = accounts[0] const amountToBurn = 5 beforeEach(async () => { - await stableToken.setMinter(minter) + await registry.setAddressFor(CeloContractName.Exchange, minter) await stableToken.mint(minter, amountToMint) }) @@ -376,7 +375,7 @@ contract('StableToken', (accounts: string[]) => { const amount = new BigNumber(10000000000000000000) beforeEach(async () => { - await stableToken.setMinter(sender) + await registry.setAddressFor(CeloContractName.Exchange, sender) await stableToken.mint(sender, amount.times(2)) await stableToken.setInflationParameters(inflationRate, SECONDS_IN_A_WEEK) await timeTravel(SECONDS_IN_A_WEEK, web3) @@ -438,7 +437,7 @@ contract('StableToken', (accounts: string[]) => { const transferAmount = 1 beforeEach(async () => { - await stableToken.setMinter(sender) + await registry.setAddressFor(CeloContractName.Exchange, sender) await stableToken.mint(sender, amountToMint) }) From 767c651950de66c3166a0042e4217869937b4efc Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Fri, 4 Oct 2019 17:40:23 -0700 Subject: [PATCH 029/149] Validator set changing again --- .../src/e2e-tests/governance_tests.ts | 439 +++++------------- .../contracts/governance/Validators.sol | 13 +- .../governance/test/ValidatorsTest.sol | 4 +- 3 files changed, 126 insertions(+), 330 deletions(-) diff --git a/packages/celotool/src/e2e-tests/governance_tests.ts b/packages/celotool/src/e2e-tests/governance_tests.ts index be843014688..d0663f8ce56 100644 --- a/packages/celotool/src/e2e-tests/governance_tests.ts +++ b/packages/celotool/src/e2e-tests/governance_tests.ts @@ -1,10 +1,8 @@ import BigNumber from 'bignumber.js' +import { ContractKit, newKitFromWeb3 } from '@celo/contractkit' import { assert } from 'chai' import Web3 from 'web3' -import { strip0x } from '../lib/utils' import { - assertRevert, - erc20Abi, getContext, getContractAddress, getEnode, @@ -13,147 +11,6 @@ import { sleep, } from './utils' -// TODO(asa): Use the contract kit here instead -const electionAbi = [ - { - constant: true, - inputs: [ - { - name: 'index', - type: 'uint256', - }, - ], - name: 'validatorAddressFromCurrentSet', - outputs: [ - { - name: '', - type: 'address', - }, - ], - payable: false, - stateMutability: 'view', - type: 'function', - }, - { - constant: true, - inputs: [], - name: 'numberValidatorsInCurrentSet', - outputs: [ - { - name: '', - type: 'uint256', - }, - ], - payable: false, - stateMutability: 'view', - type: 'function', - }, -] - -const registryAbi = [ - { - constant: true, - inputs: [ - { - name: 'identifier', - type: 'string', - }, - ], - name: 'getAddressForString', - outputs: [ - { - name: '', - type: 'address', - }, - ], - payable: false, - stateMutability: 'view', - type: 'function', - }, -] - -const validatorsAbi = [ - { - constant: true, - inputs: [], - name: 'getRegisteredValidatorGroups', - outputs: [ - { - name: '', - type: 'address[]', - }, - ], - payable: false, - stateMutability: 'view', - type: 'function', - }, - { - constant: true, - inputs: [ - { - name: 'account', - type: 'address', - }, - ], - name: 'getValidatorGroup', - outputs: [ - { - name: '', - type: 'string', - }, - { - name: '', - type: 'string', - }, - { - name: '', - type: 'address[]', - }, - ], - payable: false, - stateMutability: 'view', - type: 'function', - }, - { - constant: false, - inputs: [ - { - name: 'validator', - type: 'address', - }, - ], - name: 'addMember', - outputs: [ - { - name: '', - type: 'bool', - }, - ], - payable: false, - stateMutability: 'nonpayable', - type: 'function', - }, - { - constant: false, - inputs: [ - { - name: 'validator', - type: 'address', - }, - ], - name: 'removeMember', - outputs: [ - { - name: '', - type: 'bool', - }, - ], - payable: false, - stateMutability: 'nonpayable', - type: 'function', - }, -] - describe('governance tests', () => { const gethConfig = { migrate: true, @@ -172,6 +29,7 @@ describe('governance tests', () => { let validators: any let goldToken: any let registry: any + let kit: ContractKit before(async function(this: any) { this.timeout(0) @@ -183,10 +41,11 @@ describe('governance tests', () => { const restart = async () => { await context.hooks.restart() web3 = new Web3('http://localhost:8545') - goldToken = new web3.eth.Contract(erc20Abi, await getContractAddress('GoldTokenProxy')) - validators = new web3.eth.Contract(validatorsAbi, await getContractAddress('ValidatorsProxy')) - registry = new web3.eth.Contract(registryAbi, '0x000000000000000000000000000000000000ce10') - election = new web3.eth.Contract(electionAbi, await getContractAddress('ElectionProxy')) + kit = newKitFromWeb3(web3) + goldToken = await kit._web3Contracts.getGoldToken() + validators = await kit._web3Contracts.getValidators() + registry = await kit._web3Contracts.getRegistry() + election = await kit._web3Contracts.getElection() } const unlockAccount = async (address: string, theWeb3: any) => { @@ -194,10 +53,22 @@ describe('governance tests', () => { await theWeb3.eth.personal.unlockAccount(address, '', 1000) } - const getValidatorGroupMembers = async () => { - const [groupAddress] = await validators.methods.getRegisteredValidatorGroups().call() - const groupInfo = await validators.methods.getValidatorGroup(groupAddress).call() - return groupInfo[2] + const getValidatorGroupMembers = async (blockNumber?: number) => { + if (blockNumber) { + const [groupAddress] = await validators.methods + .getRegisteredValidatorGroups() + .call({}, blockNumber) + const groupInfo = await validators.methods + .getValidatorGroup(groupAddress) + .call({}, blockNumber) + return groupInfo[2] + } else { + const [groupAddress] = await validators.methods.getRegisteredValidatorGroups().call() + console.log('group address', groupAddress) + const groupInfo = await validators.methods.getValidatorGroup(groupAddress).call() + console.log('group info', groupInfo) + return groupInfo[2] + } } const getValidatorGroupKeys = async () => { @@ -238,151 +109,20 @@ describe('governance tests', () => { return tx.send({ from: group, ...txOptions, gas }) } - describe('Election.numberValidatorsInCurrentSet()', () => { - before(async function() { - this.timeout(0) - await restart() - }) - - it('should return the validator set size', async () => { - const numberValidators = await election.methods.numberValidatorsInCurrentSet().call() - assert.equal(numberValidators, 5) - }) - - describe('after the validator set changes', () => { - before(async function() { - this.timeout(0) - await restart() - const [groupAddress, groupPrivateKey] = await getValidatorGroupKeys() - const epoch = 10 - - const groupInstance = { - name: 'validatorGroup', - validating: false, - syncmode: 'full', - port: 30325, - wsport: 8567, - privateKey: groupPrivateKey.slice(2), - peers: [await getEnode(8545)], - } - await initAndStartGeth(context.hooks.gethBinaryPath, groupInstance) - const groupWeb3 = new Web3('ws://localhost:8567') - election = new web3.eth.Contract(electionAbi, await getContractAddress('ElectionProxy')) - validators = new groupWeb3.eth.Contract( - validatorsAbi, - await getContractAddress('ValidatorsProxy') - ) - // Give the node time to sync. - await sleep(15) - const members = await getValidatorGroupMembers() - await removeMember(groupWeb3, groupAddress, members[0]) - await sleep(epoch * 2) - }) - - it('should return the reduced validator set size', async () => { - const numberValidators = await election.methods.numberValidatorsInCurrentSet().call() - - assert.equal(numberValidators, 4) - }) - }) - }) - - describe('Election.validatorAddressFromCurrentSet()', () => { - before(async function() { - this.timeout(0) - await restart() - }) - - it('should return the first validator', async () => { - const resultAddress = await election.methods.validatorAddressFromCurrentSet(0).call() - - assert.equal(strip0x(resultAddress), context.validators[0].address) - }) - - it('should return the third validator', async () => { - const resultAddress = await election.methods.validatorAddressFromCurrentSet(2).call() - - assert.equal(strip0x(resultAddress), context.validators[2].address) - }) - - it('should return the fifth validator', async () => { - const resultAddress = await election.methods.validatorAddressFromCurrentSet(4).call() - - assert.equal(strip0x(resultAddress), context.validators[4].address) - }) - - it('should revert when asked for an out of bounds validator', async function(this: any) { - this.timeout(0) // Disable test timeout - await assertRevert( - election.methods.validatorAddressFromCurrentSet(5).send({ - from: `0x${context.validators[0].address}`, - }) - ) - }) - - describe('after the validator set changes', () => { - before(async function() { - this.timeout(0) - await restart() - const [groupAddress, groupPrivateKey] = await getValidatorGroupKeys() - const epoch = 10 - - const groupInstance = { - name: 'validatorGroup', - validating: false, - syncmode: 'full', - port: 30325, - wsport: 8567, - privateKey: groupPrivateKey.slice(2), - peers: [await getEnode(8545)], - } - await initAndStartGeth(context.hooks.gethBinaryPath, groupInstance) - const groupWeb3 = new Web3('ws://localhost:8567') - validators = new groupWeb3.eth.Contract( - validatorsAbi, - await getContractAddress('ValidatorsProxy') - ) - // Give the node time to sync. - await sleep(15) - const members = await getValidatorGroupMembers() - await removeMember(groupWeb3, groupAddress, members[0]) - await sleep(epoch * 2) - - validators = new web3.eth.Contract( - validatorsAbi, - await getContractAddress('ValidatorsProxy') - ) - }) - - it('should return the second validator in the first place', async () => { - const resultAddress = await election.methods.validatorAddressFromCurrentSet(0).call() - - assert.equal(strip0x(resultAddress), context.validators[1].address) - }) - - it('should return the last validator in the fourth place', async () => { - const resultAddress = await election.methods.validatorAddressFromCurrentSet(3).call() - - assert.equal(strip0x(resultAddress), context.validators[4].address) - }) - - it('should revert when asked for an out of bounds validator', async function(this: any) { - this.timeout(0) - await assertRevert( - election.methods.validatorAddressFromCurrentSet(4).send({ - from: `0x${context.validators[0].address}`, - }) - ) - }) - }) - }) + /* + const getLastEpochBlock = (blockNumber: number, epochSize: number) => { + const epochNumber = Math.floor(blockNumber / epochSize) + return epochNumber * epochSize + } + */ - describe('when the validator set is changing', () => { + describe.only('when the validator set is changing', () => { const epoch = 10 - const expectedEpochMembership = new Map() + const blockNumbers: number[] = [] before(async function() { this.timeout(0) await restart() + console.log('getting keys') const [groupAddress, groupPrivateKey] = await getValidatorGroupKeys() const groupInstance = { @@ -395,31 +135,33 @@ describe('governance tests', () => { peers: [await getEnode(8545)], } await initAndStartGeth(context.hooks.gethBinaryPath, groupInstance) - const groupWeb3 = new Web3('ws://localhost:8567') - validators = new groupWeb3.eth.Contract( - validatorsAbi, - await getContractAddress('ValidatorsProxy') - ) + const members = await getValidatorGroupMembers() + assert.equal(members.length, 5) + // Give the node time to sync. await sleep(15) - const members = await getValidatorGroupMembers() + const groupWeb3 = new Web3('ws://localhost:8567') + const groupKit = newKitFromWeb3(groupWeb3) + validators = await groupKit._web3Contracts.getValidators() const membersToSwap = [members[0], members[1]] - // Start with 10 nodes + let includedMemberIndex = 1 + console.log('removing member') await removeMember(groupWeb3, groupAddress, membersToSwap[0]) + console.log('removed member') const changeValidatorSet = async (header: any) => { + blockNumbers.push(header.number) // At the start of epoch N, swap members so the validator set is different for epoch N + 1. if (header.number % epoch === 0) { - const groupMembers = await getValidatorGroupMembers() - const direction = groupMembers.includes(membersToSwap[0]) - const removedMember = direction ? membersToSwap[0] : membersToSwap[1] - const addedMember = direction ? membersToSwap[1] : membersToSwap[0] - expectedEpochMembership.set(header.number / epoch, [removedMember, addedMember]) - await removeMember(groupWeb3, groupAddress, removedMember) - await addMember(groupWeb3, groupAddress, addedMember) + console.log('new epoch') + const memberToRemove = membersToSwap[includedMemberIndex] + const memberToAdd = membersToSwap[(includedMemberIndex + 1) % 2] + await removeMember(groupWeb3, groupAddress, memberToRemove) + await addMember(groupWeb3, groupAddress, memberToAdd) + includedMemberIndex = (includedMemberIndex + 1) % 2 const newMembers = await getValidatorGroupMembers() - assert.include(newMembers, addedMember) - assert.notInclude(newMembers, removedMember) + assert.include(newMembers, memberToAdd) + assert.notInclude(newMembers, memberToRemove) } } @@ -432,24 +174,79 @@ describe('governance tests', () => { await sleep(epoch) }) - it('should have produced blocks with the correct validator set', async function(this: any) { - this.timeout(0) // Disable test timeout - assert.equal(expectedEpochMembership.size, 3) - // tslint:disable-next-line: no-console - for (const [epochNumber, membership] of expectedEpochMembership) { - let containsExpectedMember = false - for (let i = epochNumber * epoch + 1; i < (epochNumber + 1) * epoch + 1; i++) { - const block = await web3.eth.getBlock(i) - assert.notEqual(block.miner.toLowerCase(), membership[1].toLowerCase()) - containsExpectedMember = - containsExpectedMember || block.miner.toLowerCase() === membership[0].toLowerCase() + it('should always return a validator set size equal to the number of group members at the end of the last epoch', async () => { + for (const blockNumber of blockNumbers) { + const lastEpochBlock = blockNumber - (blockNumber % epoch) - 1 + const validatorSetSize = await election.methods + .numberValidatorsInCurrentSet() + .call({}, blockNumber) + const groupMembership = await getValidatorGroupMembers(lastEpochBlock) + console.log(blockNumber, lastEpochBlock, validatorSetSize, groupMembership.length) + // assert.equal(validatorSetSize, groupMembership.length) + } + }) + + it('should always return a validator set equal to the group members at the end of the last epoch', async () => { + for (const blockNumber of blockNumbers) { + const lastEpochBlock = blockNumber - (blockNumber % epoch) - 1 + const groupMembership = await getValidatorGroupMembers(lastEpochBlock) + const validatorSetSize = await election.methods + .numberValidatorsInCurrentSet() + .call({}, blockNumber) + const validatorSet = [] + for (let i = 0; i < validatorSetSize; i++) { + const validator = await election.methods + .validatorAddressFromCurrentSet(i) + .call({}, blockNumber) + validatorSet.push(validator) } - assert.isTrue(containsExpectedMember) + // assert.deepEqual(groupMembership, validatorSet) + console.log(blockNumber, lastEpochBlock, groupMembership, validatorSet) } }) + + it('should only have created blocks whose miner was in the current validator set', async () => { + for (const blockNumber of blockNumbers) { + const validatorSetSize = await election.methods + .numberValidatorsInCurrentSet() + .call({}, blockNumber) + const validatorSet = [] + for (let i = 0; i < validatorSetSize; i++) { + const validator = await election.methods + .validatorAddressFromCurrentSet(i) + .call({}, blockNumber) + validatorSet.push(validator) + } + const block = await web3.eth.getBlock(blockNumber) + assert.include(validatorSet.map((x) => x.toLowerCase()), block.miner.toLowerCase()) + } + }) + + it('should update the validator scores at the end of each epoch', async () => { + const validators = await kit.contracts.getValidators() + for (const blockNumber of blockNumbers) { + const validatorSetSize = await election.methods + .numberValidatorsInCurrentSet() + .call({}, blockNumber) + const validatorSet = [] + for (let i = 0; i < validatorSetSize; i++) { + const validator = await election.methods + .validatorAddressFromCurrentSet(i) + .call({}, blockNumber) + validatorSet.push(validator) + if (false) { + console.log(await validators.getValidator(validator)) + } + } + } + }) + + it('should distribute epoch payments to each validator at the end of an epoch', async () => {}) + + it('should distribute epoch payments to the validator group at the end of an epoch', async () => {}) }) - describe('when adding any block', () => { + describe('after the governance smart contract is registered', () => { let goldGenesisSupply: any const addressesWithBalance: string[] = [] beforeEach(async function(this: any) { diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index d3e5be9de5a..daf27395c87 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -392,8 +392,8 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi * @param uptime The Fixidity representation of the validator's uptime, between 0 and 1. * @return True upon success. */ - function updateValidatorScore(address validator, uint256 uptime) external onlyVm() returns (bool) { - return _updateValidatorScore(validator, uptime); + function updateValidatorScore(address validator, uint256 uptime) external onlyVm() { + _updateValidatorScore(validator, uptime); } /** @@ -402,7 +402,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi * @param uptime The Fixidity representation of the validator's uptime, between 0 and 1. * @return True upon success. */ - function _updateValidatorScore(address validator, uint256 uptime) internal returns (bool) { + function _updateValidatorScore(address validator, uint256 uptime) internal { address account = getLockedGold().getAccountFromValidator(validator); require(isValidator(account), "isvalidator"); require(uptime <= FixidityLib.fixed1().unwrap(), "uptime"); @@ -425,14 +425,13 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi newComponent.add(currentComponent).unwrap() ) ); - return true; } - function distributeEpochPayment(address validator) external onlyVm() returns (bool) { - return _distributeEpochPayment(validator); + function distributeEpochPayment(address validator) external onlyVm() { + _distributeEpochPayment(validator); } - function _distributeEpochPayment(address validator) internal returns (bool) { + function _distributeEpochPayment(address validator) internal { address account = getLockedGold().getAccountFromValidator(validator); require(isValidator(account)); FixidityLib.Fraction memory totalPayment = FixidityLib.newFixed(validatorEpochPayment).multiply(validators[account].score); diff --git a/packages/protocol/contracts/governance/test/ValidatorsTest.sol b/packages/protocol/contracts/governance/test/ValidatorsTest.sol index 1c7421969fe..d4148a92610 100644 --- a/packages/protocol/contracts/governance/test/ValidatorsTest.sol +++ b/packages/protocol/contracts/governance/test/ValidatorsTest.sol @@ -12,11 +12,11 @@ contract ValidatorsTest is Validators { return block.number / 100; } - function updateValidatorScore(address validator, uint256 uptime) external returns (bool) { + function updateValidatorScore(address validator, uint256 uptime) external { return _updateValidatorScore(validator, uptime); } - function distributeEpochPayment(address validator) external returns (bool) { + function distributeEpochPayment(address validator) external { return _distributeEpochPayment(validator); } } From 7f82317a8ae2b85945a64e70a6914c5fb49a2b42 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Sun, 6 Oct 2019 13:17:51 -0700 Subject: [PATCH 030/149] Epoch payments and rewards appear to be working --- .../src/e2e-tests/governance_tests.ts | 254 +++++++++++++----- .../src/wrappers/StableTokenWrapper.ts | 1 - .../protocol/contracts/common/UsingEpochs.sol | 9 +- .../contracts/governance/Election.sol | 24 +- .../contracts/governance/Validators.sol | 10 +- .../governance/test/ElectionTest.sol | 14 + .../governance/test/MockValidators.sol | 2 +- .../governance/test/ValidatorsTest.sol | 7 +- packages/protocol/migrationsConfig.js | 2 +- packages/protocol/test/common/fixidity.ts | 9 + packages/protocol/test/governance/election.ts | 157 ++++++++++- packages/utils/package.json | 2 +- 12 files changed, 413 insertions(+), 78 deletions(-) create mode 100644 packages/protocol/contracts/governance/test/ElectionTest.sol diff --git a/packages/celotool/src/e2e-tests/governance_tests.ts b/packages/celotool/src/e2e-tests/governance_tests.ts index d0663f8ce56..e8a9fdc202c 100644 --- a/packages/celotool/src/e2e-tests/governance_tests.ts +++ b/packages/celotool/src/e2e-tests/governance_tests.ts @@ -1,5 +1,6 @@ import BigNumber from 'bignumber.js' import { ContractKit, newKitFromWeb3 } from '@celo/contractkit' +import { fromFixed, toFixed } from '@celo/utils/lib/fixidity' import { assert } from 'chai' import Web3 from 'web3' import { @@ -33,7 +34,7 @@ describe('governance tests', () => { before(async function(this: any) { this.timeout(0) - await context.hooks.before() + // await context.hooks.before() }) after(context.hooks.after) @@ -64,9 +65,7 @@ describe('governance tests', () => { return groupInfo[2] } else { const [groupAddress] = await validators.methods.getRegisteredValidatorGroups().call() - console.log('group address', groupAddress) const groupInfo = await validators.methods.getValidatorGroup(groupAddress).call() - console.log('group info', groupInfo) return groupInfo[2] } } @@ -109,20 +108,17 @@ describe('governance tests', () => { return tx.send({ from: group, ...txOptions, gas }) } - /* - const getLastEpochBlock = (blockNumber: number, epochSize: number) => { - const epochNumber = Math.floor(blockNumber / epochSize) - return epochNumber * epochSize + const isLastBlockOfEpoch = (blockNumber: number, epochSize: number) => { + return blockNumber % epochSize == 0 } - */ - describe.only('when the validator set is changing', () => { + describe('when the validator set is changing', () => { const epoch = 10 const blockNumbers: number[] = [] + let allValidators: string[] before(async function() { this.timeout(0) await restart() - console.log('getting keys') const [groupAddress, groupPrivateKey] = await getValidatorGroupKeys() const groupInstance = { @@ -135,25 +131,22 @@ describe('governance tests', () => { peers: [await getEnode(8545)], } await initAndStartGeth(context.hooks.gethBinaryPath, groupInstance) - const members = await getValidatorGroupMembers() - assert.equal(members.length, 5) + allValidators = await getValidatorGroupMembers() + assert.equal(allValidators.length, 5) // Give the node time to sync. await sleep(15) const groupWeb3 = new Web3('ws://localhost:8567') const groupKit = newKitFromWeb3(groupWeb3) validators = await groupKit._web3Contracts.getValidators() - const membersToSwap = [members[0], members[1]] + const membersToSwap = [allValidators[0], allValidators[1]] let includedMemberIndex = 1 - console.log('removing member') await removeMember(groupWeb3, groupAddress, membersToSwap[0]) - console.log('removed member') const changeValidatorSet = async (header: any) => { blockNumbers.push(header.number) // At the start of epoch N, swap members so the validator set is different for epoch N + 1. - if (header.number % epoch === 0) { - console.log('new epoch') + if (header.number % epoch === 1) { const memberToRemove = membersToSwap[includedMemberIndex] const memberToAdd = membersToSwap[(includedMemberIndex + 1) % 2] await removeMember(groupWeb3, groupAddress, memberToRemove) @@ -168,82 +161,224 @@ describe('governance tests', () => { const subscription = await groupWeb3.eth.subscribe('newBlockHeaders') subscription.on('data', changeValidatorSet) // Wait for a few epochs while changing the validator set. - await sleep(epoch * 3) + await sleep(epoch * 4) ;(subscription as any).unsubscribe() // Wait for the current epoch to complete. await sleep(epoch) }) + // Note that this returns the validator set at the END of `blockNumber`, i.e. the validator set + // that will validate the next block, and NOT necessarily the validator set that validated this + // block. + const getValidatorSetAtBlock = async (blockNumber: number) => { + const validatorSetSize = await election.methods + .numberValidatorsInCurrentSet() + .call({}, blockNumber) + const validatorSet = [] + for (let i = 0; i < validatorSetSize; i++) { + validatorSet.push( + await election.methods.validatorAddressFromCurrentSet(i).call({}, blockNumber) + ) + } + return validatorSet + } + it('should always return a validator set size equal to the number of group members at the end of the last epoch', async () => { for (const blockNumber of blockNumbers) { - const lastEpochBlock = blockNumber - (blockNumber % epoch) - 1 + const lastEpochBlock = blockNumber - (blockNumber % epoch) const validatorSetSize = await election.methods .numberValidatorsInCurrentSet() .call({}, blockNumber) const groupMembership = await getValidatorGroupMembers(lastEpochBlock) - console.log(blockNumber, lastEpochBlock, validatorSetSize, groupMembership.length) - // assert.equal(validatorSetSize, groupMembership.length) + assert.equal(validatorSetSize, groupMembership.length) } }) it('should always return a validator set equal to the group members at the end of the last epoch', async () => { for (const blockNumber of blockNumbers) { - const lastEpochBlock = blockNumber - (blockNumber % epoch) - 1 + const lastEpochBlock = blockNumber - (blockNumber % epoch) const groupMembership = await getValidatorGroupMembers(lastEpochBlock) - const validatorSetSize = await election.methods - .numberValidatorsInCurrentSet() - .call({}, blockNumber) - const validatorSet = [] - for (let i = 0; i < validatorSetSize; i++) { - const validator = await election.methods - .validatorAddressFromCurrentSet(i) - .call({}, blockNumber) - validatorSet.push(validator) - } - // assert.deepEqual(groupMembership, validatorSet) - console.log(blockNumber, lastEpochBlock, groupMembership, validatorSet) + const validatorSet = await getValidatorSetAtBlock(blockNumber) + assert.sameMembers(groupMembership, validatorSet) } }) it('should only have created blocks whose miner was in the current validator set', async () => { for (const blockNumber of blockNumbers) { - const validatorSetSize = await election.methods - .numberValidatorsInCurrentSet() - .call({}, blockNumber) - const validatorSet = [] - for (let i = 0; i < validatorSetSize; i++) { - const validator = await election.methods - .validatorAddressFromCurrentSet(i) - .call({}, blockNumber) - validatorSet.push(validator) - } + // The validators responsible for creating `blockNumber` were those in the validator set at + // `blockNumber-1`. + const validatorSet = await getValidatorSetAtBlock(blockNumber - 1) const block = await web3.eth.getBlock(blockNumber) assert.include(validatorSet.map((x) => x.toLowerCase()), block.miner.toLowerCase()) } }) it('should update the validator scores at the end of each epoch', async () => { - const validators = await kit.contracts.getValidators() + const validators = await kit._web3Contracts.getValidators() + const adjustmentSpeed = fromFixed( + new BigNumber((await validators.methods.getValidatorScoreParameters().call())[1]) + ) + const uptime = 1 + + const assertScoreUnchanged = async (validator: string, blockNumber: number) => { + const score = new BigNumber( + (await validators.methods.getValidator(validator).call({}, blockNumber))[4] + ) + const previousScore = new BigNumber( + (await validators.methods.getValidator(validator).call({}, blockNumber - 1))[4] + ) + assert.equal(score.toFixed(), previousScore.toFixed()) + } + + const assertScoreChanged = async (validator: string, blockNumber: number) => { + const score = new BigNumber( + (await validators.methods.getValidator(validator).call({}, blockNumber))[4] + ) + const previousScore = new BigNumber( + (await validators.methods.getValidator(validator).call({}, blockNumber - 1))[4] + ) + const expectedScore = adjustmentSpeed + .times(uptime) + .plus(new BigNumber(1).minus(adjustmentSpeed).times(fromFixed(previousScore))) + assert.equal(score.toFixed(), toFixed(expectedScore).toFixed()) + } + for (const blockNumber of blockNumbers) { - const validatorSetSize = await election.methods - .numberValidatorsInCurrentSet() - .call({}, blockNumber) - const validatorSet = [] - for (let i = 0; i < validatorSetSize; i++) { - const validator = await election.methods - .validatorAddressFromCurrentSet(i) - .call({}, blockNumber) - validatorSet.push(validator) - if (false) { - console.log(await validators.getValidator(validator)) - } + let expectUnchangedScores: string[] + let expectChangedScores: string[] + if (isLastBlockOfEpoch(blockNumber, epoch)) { + expectChangedScores = await getValidatorSetAtBlock(blockNumber - 1) + expectUnchangedScores = allValidators.filter((x) => !expectChangedScores.includes(x)) + } else { + expectUnchangedScores = allValidators + expectChangedScores = [] + } + + for (const validator of expectUnchangedScores) { + await assertScoreUnchanged(validator, blockNumber) + } + + for (const validator of expectChangedScores) { + await assertScoreChanged(validator, blockNumber) + } + } + }) + + it('should distribute epoch payments at the end of each epoch', async () => { + const validators = await kit._web3Contracts.getValidators() + const stableToken = await kit._web3Contracts.getStableToken() + const commission = 0.1 + const validatorEpochPayment = new BigNumber( + await validators.methods.validatorEpochPayment().call() + ) + const [group] = await validators.methods.getRegisteredValidatorGroups().call() + + const assertBalanceUnchanged = async (validator: string, blockNumber: number) => { + const currentBalance = new BigNumber( + await stableToken.methods.balanceOf(validator).call({}, blockNumber) + ) + const previousBalance = new BigNumber( + await stableToken.methods.balanceOf(validator).call({}, blockNumber - 1) + ) + assert.equal(currentBalance.toFixed(), previousBalance.toFixed()) + } + + const assertBalanceChanged = async ( + validator: string, + blockNumber: number, + expected: BigNumber + ) => { + const currentBalance = new BigNumber( + await stableToken.methods.balanceOf(validator).call({}, blockNumber) + ) + const previousBalance = new BigNumber( + await stableToken.methods.balanceOf(validator).call({}, blockNumber - 1) + ) + assert.equal(expected.toFixed(), currentBalance.minus(previousBalance).toFixed()) + } + + const getExpectedTotalPayment = async (validator: string, blockNumber: number) => { + const score = new BigNumber( + (await validators.methods.getValidator(validator).call({}, blockNumber))[4] + ) + return validatorEpochPayment.times(fromFixed(score)) + } + + for (const blockNumber of blockNumbers) { + let expectUnchangedBalances: string[] + let expectChangedBalances: string[] + if (isLastBlockOfEpoch(blockNumber, epoch)) { + expectChangedBalances = await getValidatorSetAtBlock(blockNumber - 1) + expectUnchangedBalances = allValidators.filter((x) => !expectChangedBalances.includes(x)) + } else { + expectUnchangedBalances = allValidators + expectChangedBalances = [] + } + + for (const validator of expectUnchangedBalances) { + await assertBalanceUnchanged(validator, blockNumber) + } + + let expectedGroupPayment = new BigNumber(0) + for (const validator of expectChangedBalances) { + const expectedTotalPayment = await getExpectedTotalPayment(validator, blockNumber) + const groupPayment = expectedTotalPayment.times(commission) + await assertBalanceChanged( + validator, + blockNumber, + expectedTotalPayment.minus(groupPayment) + ) + expectedGroupPayment = expectedGroupPayment.plus(groupPayment) } + await assertBalanceChanged(group, blockNumber, expectedGroupPayment) } }) - it('should distribute epoch payments to each validator at the end of an epoch', async () => {}) + it('should distribute epoch rewards at the end of each epoch', async () => { + const validators = await kit._web3Contracts.getValidators() + const election = await kit._web3Contracts.getElection() + // const lockedGold = await kit._web3Contracts.getLockedGold() + const epochReward = new BigNumber(10).pow(18) + const [group] = await validators.methods.getRegisteredValidatorGroups().call() - it('should distribute epoch payments to the validator group at the end of an epoch', async () => {}) + const assertVotesUnchanged = async (group: string, blockNumber: number) => { + const currentVotes = new BigNumber( + await election.methods.getGroupTotalVotes(group).call({}, blockNumber) + ) + const previousVotes = new BigNumber( + await election.methods.getGroupTotalVotes(group).call({}, blockNumber - 1) + ) + assert.equal(currentVotes.toFixed(), previousVotes.toFixed()) + } + + const assertVotesChanged = async ( + group: string, + blockNumber: number, + expected: BigNumber + ) => { + const currentVotes = new BigNumber( + await election.methods.getGroupTotalVotes(group).call({}, blockNumber) + ) + const previousVotes = new BigNumber( + await election.methods.getGroupTotalVotes(group).call({}, blockNumber - 1) + ) + console.log( + currentVotes.toFixed(), + previousVotes.toFixed(), + expected.toFixed(), + currentVotes.minus(previousVotes).toFixed() + ) + assert.equal(expected.toFixed(), currentVotes.minus(previousVotes).toFixed()) + } + + for (const blockNumber of blockNumbers) { + if (isLastBlockOfEpoch(blockNumber, epoch)) { + await assertVotesChanged(group, blockNumber, epochReward) + } else { + await assertVotesUnchanged(group, blockNumber) + } + } + }) }) describe('after the governance smart contract is registered', () => { @@ -295,7 +430,6 @@ describe('governance tests', () => { b.plus(total) ) assert.isAtLeast(expectedGoldTotalSupply.toNumber(), goldGenesisSupply.toNumber()) - // assert.equal(goldTotalSupply.toString(), expectedGoldTotalSupply.toString()) }) }) diff --git a/packages/contractkit/src/wrappers/StableTokenWrapper.ts b/packages/contractkit/src/wrappers/StableTokenWrapper.ts index ebe68a056d8..53ce8174403 100644 --- a/packages/contractkit/src/wrappers/StableTokenWrapper.ts +++ b/packages/contractkit/src/wrappers/StableTokenWrapper.ts @@ -70,7 +70,6 @@ export class StableTokenWrapper extends BaseWrapper { toBigNumber ) - minter = proxyCall(this.contract.methods.minter) owner = proxyCall(this.contract.methods.owner) /** diff --git a/packages/protocol/contracts/common/UsingEpochs.sol b/packages/protocol/contracts/common/UsingEpochs.sol index 87eedb3b3d6..4d16aec0689 100644 --- a/packages/protocol/contracts/common/UsingEpochs.sol +++ b/packages/protocol/contracts/common/UsingEpochs.sol @@ -5,10 +5,15 @@ contract UsingEpochs { event RegistrySet(address indexed registryAddress); + // TODO(asa): Expose epoch size via precompile. // solhint-disable state-visibility - uint256 constant EPOCH = 17280; + uint256 constant EPOCH = 10; function getEpochNumber() public view returns (uint256) { - return block.number / EPOCH; + uint256 ret = block.number / EPOCH; + if (block.number % EPOCH == 0) { + ret = ret - 1; + } + return ret; } } diff --git a/packages/protocol/contracts/governance/Election.sol b/packages/protocol/contracts/governance/Election.sol index 4ee216be4a4..4c4e995b350 100644 --- a/packages/protocol/contracts/governance/Election.sol +++ b/packages/protocol/contracts/governance/Election.sol @@ -43,9 +43,9 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { } struct TotalVotes { - // The total number of votes cast. + // The total number of votes cast, including those for ineligible Validator Groups. uint256 total; - // A list of eligible ValidatorGroups sorted by total votes. + // A list of eligible Validator Groups sorted by total votes. SortedLinkedList.List eligible; } @@ -436,6 +436,24 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { return votes.total.eligible.contains(group); } + function distributeEpochRewards(address group, uint256 value, address lesser, address greater) external { + require(msg.sender == address(0)); + _distributeEpochRewards(group, value, lesser, greater); + } + + function _distributeEpochRewards(address group, uint256 value, address lesser, address greater) internal { + // TODO(asa): What do here? + if (votes.active.total[group] == 0) { + } + if (votes.total.eligible.contains(group)) { + uint256 newVoteTotal = votes.total.eligible.getValue(group).add(value); + votes.total.eligible.update(group, newVoteTotal, lesser, greater); + } + + votes.active.total[group] = votes.active.total[group].add(value); + votes.total.total = votes.total.total.add(value); + } + /** * @notice Increments the number of total votes for `group` by `value`. * @param group The validator group whose vote total should be incremented. @@ -646,7 +664,7 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { function getEligibleValidatorGroupsVoteTotals() external view - returns (address[] memory, uint256[] memory) + returns (address[] memory groups, uint256[] memory values) { return votes.total.eligible.getElements(); } diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index daf27395c87..14467cc77e6 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -410,15 +410,19 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi // TODO(asa): Use exponent. FixidityLib.Fraction memory epochScore = FixidityLib.wrap(uptime); + + // New component is 0! Uptime is non-zero. FixidityLib.Fraction memory newComponent = validatorScoreParameters.adjustmentSpeed.multiply( epochScore ); + // validators[validator].score = newComponent; + // This works: + // validators[validator].score = epochScore; + FixidityLib.Fraction memory currentComponent = FixidityLib.fixed1().subtract( validatorScoreParameters.adjustmentSpeed ); - emit Debug(currentComponent.unwrap()); currentComponent = currentComponent.multiply(validators[account].score); - emit Debug(currentComponent.unwrap()); validators[account].score = FixidityLib.wrap( Math.min( epochScore.unwrap(), @@ -438,6 +442,8 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi address group = getMembershipInLastEpoch(account); uint256 groupPayment = totalPayment.multiply(groups[group].commission).fromFixed(); uint256 validatorPayment = totalPayment.fromFixed().sub(groupPayment); + // For some reason, one validator seems to be getting the full payment (not less commission) + // Perhaps, getMembershipInLastEpoch is returning 0? Probably what's happening... getStableToken().mint(group, groupPayment); getStableToken().mint(account, validatorPayment); } diff --git a/packages/protocol/contracts/governance/test/ElectionTest.sol b/packages/protocol/contracts/governance/test/ElectionTest.sol new file mode 100644 index 00000000000..383e915ad11 --- /dev/null +++ b/packages/protocol/contracts/governance/test/ElectionTest.sol @@ -0,0 +1,14 @@ +pragma solidity ^0.5.8; + +import "../Election.sol"; +import "../../common/FixidityLib.sol"; + +/** + * @title A wrapper around Election that exposes onlyVm functions for testing. + */ +contract ElectionTest is Election { + + function distributeEpochRewards(address group, uint256 value, address lesser, address greater) external { + return _distributeEpochRewards(group, value, lesser, greater); + } +} diff --git a/packages/protocol/contracts/governance/test/MockValidators.sol b/packages/protocol/contracts/governance/test/MockValidators.sol index 68514b13a1d..b3e94361d21 100644 --- a/packages/protocol/contracts/governance/test/MockValidators.sol +++ b/packages/protocol/contracts/governance/test/MockValidators.sol @@ -45,7 +45,7 @@ contract MockValidators is IValidators { members[group] = _members; } - function getTopValidatorsFromGroup( + function getTopGroupValidators( address group, uint256 n ) diff --git a/packages/protocol/contracts/governance/test/ValidatorsTest.sol b/packages/protocol/contracts/governance/test/ValidatorsTest.sol index d4148a92610..16cdabd8364 100644 --- a/packages/protocol/contracts/governance/test/ValidatorsTest.sol +++ b/packages/protocol/contracts/governance/test/ValidatorsTest.sol @@ -9,7 +9,12 @@ import "../../common/FixidityLib.sol"; contract ValidatorsTest is Validators { function getEpochNumber() public view returns (uint256) { - return block.number / 100; + uint256 epoch = 100; + uint256 ret = block.number / epoch; + if (block.number % epoch == 0) { + ret = ret - 1; + } + return ret; } function updateValidatorScore(address validator, uint256 uptime) external { diff --git a/packages/protocol/migrationsConfig.js b/packages/protocol/migrationsConfig.js index 5b91f04888e..86cc477f359 100644 --- a/packages/protocol/migrationsConfig.js +++ b/packages/protocol/migrationsConfig.js @@ -113,7 +113,7 @@ const linkedLibraries = { ], SortedLinkedListWithMedian: ['AddressSortedLinkedListWithMedian'], AddressLinkedList: ['Validators', 'ValidatorsTest'], - AddressSortedLinkedList: ['Election'], + AddressSortedLinkedList: ['Election', 'ElectionTest'], IntegerSortedLinkedList: ['Governance', 'IntegerSortedLinkedListTest'], AddressSortedLinkedListWithMedian: ['SortedOracles', 'AddressSortedLinkedListWithMedianTest'], Signatures: ['LockedGold', 'Escrow'], diff --git a/packages/protocol/test/common/fixidity.ts b/packages/protocol/test/common/fixidity.ts index 9349e437287..10a36a26e58 100644 --- a/packages/protocol/test/common/fixidity.ts +++ b/packages/protocol/test/common/fixidity.ts @@ -157,6 +157,15 @@ contract('FixidityLib', () => { assertEqualBN(result, expected) }) + it('should multiply two numbers less than 1', async () => { + const a = toFixed(0.1) + const b = toFixed(new BigNumber(10).pow(-14)) + const expected = toFixed(new BigNumber(10).pow(-15)) + const result = await fixidityTest.multiply(a, b) + + assertEqualBN(result, expected) + }) + it('should multiply by 0', async () => { const result = await fixidityTest.multiply(maxFixedMul, zero) diff --git a/packages/protocol/test/governance/election.ts b/packages/protocol/test/governance/election.ts index c3647fd622f..1f417894680 100644 --- a/packages/protocol/test/governance/election.ts +++ b/packages/protocol/test/governance/election.ts @@ -16,11 +16,11 @@ import { MockRandomInstance, RegistryContract, RegistryInstance, - ElectionContract, - ElectionInstance, + ElectionTestContract, + ElectionTestInstance, } from 'types' -const Election: ElectionContract = artifacts.require('Election') +const ElectionTest: ElectionTestContract = artifacts.require('ElectionTest') const MockLockedGold: MockLockedGoldContract = artifacts.require('MockLockedGold') const MockValidators: MockValidatorsContract = artifacts.require('MockValidators') const MockRandom: MockRandomContract = artifacts.require('MockRandom') @@ -28,10 +28,10 @@ const Registry: RegistryContract = artifacts.require('Registry') // @ts-ignore // TODO(mcortesi): Use BN -Election.numberFormat = 'BigNumber' +ElectionTest.numberFormat = 'BigNumber' contract('Election', (accounts: string[]) => { - let election: ElectionInstance + let election: ElectionTestInstance let registry: RegistryInstance let mockLockedGold: MockLockedGoldInstance let mockValidators: MockValidatorsInstance @@ -43,7 +43,7 @@ contract('Election', (accounts: string[]) => { const electabilityThreshold = new BigNumber(0) beforeEach(async () => { - election = await Election.new() + election = await ElectionTest.new() mockLockedGold = await MockLockedGold.new() mockValidators = await MockValidators.new() registry = await Registry.new() @@ -855,4 +855,149 @@ contract('Election', (accounts: string[]) => { }) }) }) + + describe.only('#distributeEpochRewards', () => { + const voter = accounts[0] + const group = accounts[1] + const voteValue = new BigNumber(1000000) + const rewardValue = new BigNumber(1000000) + beforeEach(async () => { + await mockValidators.setMembers(group, [accounts[9]]) + await election.markGroupEligible(group, NULL_ADDRESS, NULL_ADDRESS) + await mockLockedGold.setTotalLockedGold(voteValue) + await mockValidators.setNumRegisteredValidators(1) + await mockLockedGold.incrementNonvotingAccountBalance(voter, voteValue) + await election.vote(group, voteValue, NULL_ADDRESS, NULL_ADDRESS) + await election.activate(group) + }) + + describe('when there is a single group with active votes', () => { + describe('when the group is eligible', () => { + beforeEach(async () => { + await election.distributeEpochRewards(group, rewardValue, NULL_ADDRESS, NULL_ADDRESS) + }) + + it("should increment the account's active votes for the group", async () => { + assertEqualBN( + await election.getAccountActiveVotesForGroup(group, voter), + voteValue.plus(rewardValue) + ) + }) + + it("should increment the account's total votes for the group", async () => { + assertEqualBN( + await election.getAccountTotalVotesForGroup(group, voter), + voteValue.plus(rewardValue) + ) + }) + + it("should increment account's total votes", async () => { + assertEqualBN(await election.getAccountTotalVotes(voter), voteValue.plus(rewardValue)) + }) + + it('should increment the total votes for the group', async () => { + assertEqualBN(await election.getGroupTotalVotes(group), voteValue.plus(rewardValue)) + }) + + it('should increment the total votes', async () => { + assertEqualBN(await election.getTotalVotes(), voteValue.plus(rewardValue)) + }) + }) + }) + + describe('when there are two groups with active votes', () => { + const voter2 = accounts[2] + const group2 = accounts[3] + const voteValue2 = new BigNumber(1000000) + const rewardValue2 = new BigNumber(10000000) + beforeEach(async () => { + await mockValidators.setMembers(group2, [accounts[8]]) + await election.markGroupEligible(group2, NULL_ADDRESS, group) + await mockLockedGold.setTotalLockedGold(voteValue.plus(voteValue2)) + await mockValidators.setNumRegisteredValidators(2) + await mockLockedGold.incrementNonvotingAccountBalance(voter2, voteValue2) + // Split voter2's vote between the two groups. + await election.vote(group, voteValue2.div(2), group2, NULL_ADDRESS, { from: voter2 }) + await election.vote(group2, voteValue2.div(2), NULL_ADDRESS, group, { from: voter2 }) + await election.activate(group, { from: voter2 }) + await election.activate(group2, { from: voter2 }) + }) + + describe('when boths groups are eligible', () => { + const expectedGroupTotalActiveVotes = voteValue.plus(voteValue2.div(2)).plus(rewardValue) + const expectedVoterActiveVotesForGroup = expectedGroupTotalActiveVotes + .times(2) + .div(3) + .dp(0, BigNumber.ROUND_FLOOR) + const expectedVoter2ActiveVotesForGroup = expectedGroupTotalActiveVotes + .div(3) + .dp(0, BigNumber.ROUND_FLOOR) + const expectedVoter2ActiveVotesForGroup2 = voteValue2.div(2).plus(rewardValue2) + beforeEach(async () => { + await election.distributeEpochRewards(group, rewardValue, group2, NULL_ADDRESS) + await election.distributeEpochRewards(group2, rewardValue2, group, NULL_ADDRESS) + }) + + it("should increment the accounts' active votes for both groups", async () => { + assertEqualBN( + await election.getAccountActiveVotesForGroup(group, voter), + expectedVoterActiveVotesForGroup + ) + assertEqualBN( + await election.getAccountActiveVotesForGroup(group, voter2), + expectedVoter2ActiveVotesForGroup + ) + assertEqualBN( + await election.getAccountActiveVotesForGroup(group2, voter2), + expectedVoter2ActiveVotesForGroup2 + ) + }) + + it("should increment the accounts' total votes for both groups", async () => { + assertEqualBN( + await election.getAccountTotalVotesForGroup(group, voter), + expectedVoterActiveVotesForGroup + ) + assertEqualBN( + await election.getAccountTotalVotesForGroup(group, voter2), + expectedVoter2ActiveVotesForGroup + ) + assertEqualBN( + await election.getAccountTotalVotesForGroup(group2, voter2), + expectedVoter2ActiveVotesForGroup2 + ) + }) + + it("should increment the accounts' total votes", async () => { + assertEqualBN( + await election.getAccountTotalVotes(voter), + expectedVoterActiveVotesForGroup + ) + assertEqualBN( + await election.getAccountTotalVotes(voter2), + expectedVoter2ActiveVotesForGroup.plus(expectedVoter2ActiveVotesForGroup2) + ) + }) + + it('should increment the total votes for the groups', async () => { + assertEqualBN(await election.getGroupTotalVotes(group), expectedGroupTotalActiveVotes) + assertEqualBN( + await election.getGroupTotalVotes(group2), + expectedVoter2ActiveVotesForGroup2 + ) + }) + + it('should increment the total votes', async () => { + assertEqualBN( + await election.getTotalVotes(), + expectedGroupTotalActiveVotes.plus(expectedVoter2ActiveVotesForGroup2) + ) + }) + + it('should update the ordering of the eligible groups', async () => { + assert.deepEqual(await election.getEligibleValidatorGroups(), [group2, group]) + }) + }) + }) + }) }) diff --git a/packages/utils/package.json b/packages/utils/package.json index 32d186d2ffe..1e2636b005a 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -29,7 +29,7 @@ "web3-utils": "1.0.0-beta.37", "keccak256": "^1.0.0", "buffer-reverse": "^1.0.1", - "bigi": "^1.1.0" + "bigi": "^1.1.0" }, "devDependencies": { "@celo/typescript": "0.0.1", From 50d60294a7d6b5ef01c03903d02a684d5d5a7f5c Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Sun, 6 Oct 2019 16:44:26 -0700 Subject: [PATCH 031/149] Update membership history upon validator registration --- packages/protocol/contracts/governance/Validators.sol | 1 + packages/protocol/test/common/migration.ts | 7 ------- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index 14467cc77e6..41e9b291a83 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -337,6 +337,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi validators[account].url = url; validators[account].publicKeysData = publicKeysData; _validators.push(account); + updateMembershipHistory(account, address(0)); getLockedGold().setAccountMustMaintain(account, registrationRequirements.validator, MAX_INT); emit ValidatorRegistered(account, name, url, publicKeysData); return true; diff --git a/packages/protocol/test/common/migration.ts b/packages/protocol/test/common/migration.ts index db53a566d3a..3da2d07bfd3 100644 --- a/packages/protocol/test/common/migration.ts +++ b/packages/protocol/test/common/migration.ts @@ -2,7 +2,6 @@ import { assertContractsRegistered, assertProxiesSet, assertRegistryAddressesSet, - assertStableTokenMinter, getReserveBalance, } from '@celo/protocol/lib/test-utils' import { getDeployedProxiedContract } from '@celo/protocol/lib/web3-utils' @@ -52,10 +51,4 @@ contract('Migration', () => { assert.equal(balance, expectedBalance) }) }) - - describe('Checking StableToken minter', async () => { - it('should be set to the Reserve', async () => { - await assertStableTokenMinter(getContract) - }) - }) }) From 999bca29c550e9190d68fb69e27baacf7f226256 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Mon, 7 Oct 2019 13:34:31 -0700 Subject: [PATCH 032/149] End to end tests passing --- .../src/e2e-tests/governance_tests.ts | 154 +++++++++--------- .../contracts/governance/LockedGold.sol | 16 ++ packages/protocol/test/common/fixidity.ts | 9 - packages/protocol/test/governance/election.ts | 2 +- .../protocol/test/governance/lockedgold.ts | 27 +-- .../protocol/test/governance/validators.ts | 17 +- packages/protocol/test/stability/exchange.ts | 18 +- .../protocol/test/stability/stabletoken.ts | 10 +- 8 files changed, 142 insertions(+), 111 deletions(-) diff --git a/packages/celotool/src/e2e-tests/governance_tests.ts b/packages/celotool/src/e2e-tests/governance_tests.ts index e8a9fdc202c..043776fb286 100644 --- a/packages/celotool/src/e2e-tests/governance_tests.ts +++ b/packages/celotool/src/e2e-tests/governance_tests.ts @@ -3,14 +3,7 @@ import { ContractKit, newKitFromWeb3 } from '@celo/contractkit' import { fromFixed, toFixed } from '@celo/utils/lib/fixidity' import { assert } from 'chai' import Web3 from 'web3' -import { - getContext, - getContractAddress, - getEnode, - importGenesis, - initAndStartGeth, - sleep, -} from './utils' +import { getContext, getEnode, importGenesis, initAndStartGeth, sleep } from './utils' describe('governance tests', () => { const gethConfig = { @@ -34,7 +27,7 @@ describe('governance tests', () => { before(async function(this: any) { this.timeout(0) - // await context.hooks.before() + await context.hooks.before() }) after(context.hooks.after) @@ -116,8 +109,8 @@ describe('governance tests', () => { const epoch = 10 const blockNumbers: number[] = [] let allValidators: string[] - before(async function() { - this.timeout(0) + before(async function(this: any) { + this.timeout(0) // Disable test timeout await restart() const [groupAddress, groupPrivateKey] = await getValidatorGroupKeys() @@ -273,16 +266,6 @@ describe('governance tests', () => { ) const [group] = await validators.methods.getRegisteredValidatorGroups().call() - const assertBalanceUnchanged = async (validator: string, blockNumber: number) => { - const currentBalance = new BigNumber( - await stableToken.methods.balanceOf(validator).call({}, blockNumber) - ) - const previousBalance = new BigNumber( - await stableToken.methods.balanceOf(validator).call({}, blockNumber - 1) - ) - assert.equal(currentBalance.toFixed(), previousBalance.toFixed()) - } - const assertBalanceChanged = async ( validator: string, blockNumber: number, @@ -297,6 +280,10 @@ describe('governance tests', () => { assert.equal(expected.toFixed(), currentBalance.minus(previousBalance).toFixed()) } + const assertBalanceUnchanged = async (validator: string, blockNumber: number) => { + await assertBalanceChanged(validator, blockNumber, new BigNumber(0)) + } + const getExpectedTotalPayment = async (validator: string, blockNumber: number) => { const score = new BigNumber( (await validators.methods.getValidator(validator).call({}, blockNumber))[4] @@ -337,100 +324,117 @@ describe('governance tests', () => { it('should distribute epoch rewards at the end of each epoch', async () => { const validators = await kit._web3Contracts.getValidators() const election = await kit._web3Contracts.getElection() - // const lockedGold = await kit._web3Contracts.getLockedGold() + const lockedGold = await kit._web3Contracts.getLockedGold() + const governance = await kit._web3Contracts.getGovernance() const epochReward = new BigNumber(10).pow(18) + const infraReward = new BigNumber(10).pow(18) const [group] = await validators.methods.getRegisteredValidatorGroups().call() - const assertVotesUnchanged = async (group: string, blockNumber: number) => { + const assertVotesChanged = async ( + group: string, + blockNumber: number, + expected: BigNumber + ) => { const currentVotes = new BigNumber( await election.methods.getGroupTotalVotes(group).call({}, blockNumber) ) const previousVotes = new BigNumber( await election.methods.getGroupTotalVotes(group).call({}, blockNumber - 1) ) - assert.equal(currentVotes.toFixed(), previousVotes.toFixed()) + assert.equal(expected.toFixed(), currentVotes.minus(previousVotes).toFixed()) } - const assertVotesChanged = async ( - group: string, + const assertGoldTokenTotalSupplyChanged = async ( blockNumber: number, expected: BigNumber ) => { - const currentVotes = new BigNumber( - await election.methods.getGroupTotalVotes(group).call({}, blockNumber) + const currentSupply = new BigNumber( + await goldToken.methods.totalSupply().call({}, blockNumber) ) - const previousVotes = new BigNumber( - await election.methods.getGroupTotalVotes(group).call({}, blockNumber - 1) + const previousSupply = new BigNumber( + await goldToken.methods.totalSupply().call({}, blockNumber - 1) + ) + assert.equal(expected.toFixed(), currentSupply.minus(previousSupply).toFixed()) + } + + const assertBalanceChanged = async ( + address: string, + blockNumber: number, + expected: BigNumber + ) => { + const currentBalance = new BigNumber( + await goldToken.methods.balanceOf(address).call({}, blockNumber) ) - console.log( - currentVotes.toFixed(), - previousVotes.toFixed(), - expected.toFixed(), - currentVotes.minus(previousVotes).toFixed() + const previousBalance = new BigNumber( + await goldToken.methods.balanceOf(address).call({}, blockNumber - 1) ) - assert.equal(expected.toFixed(), currentVotes.minus(previousVotes).toFixed()) + assert.equal(expected.toFixed(), currentBalance.minus(previousBalance).toFixed()) + } + + const assertLockedGoldBalanceChanged = async (blockNumber: number, expected: BigNumber) => { + await assertBalanceChanged(lockedGold.options.address, blockNumber, expected) + } + + const assertGovernanceBalanceChanged = async (blockNumber: number, expected: BigNumber) => { + await assertBalanceChanged(governance.options.address, blockNumber, expected) + } + + const assertVotesUnchanged = async (group: string, blockNumber: number) => { + await assertVotesChanged(group, blockNumber, new BigNumber(0)) + } + + const assertGoldTokenTotalSupplyUnchanged = async (blockNumber: number) => { + await assertGoldTokenTotalSupplyChanged(blockNumber, new BigNumber(0)) + } + + const assertLockedGoldBalanceUnchanged = async (blockNumber: number) => { + await assertLockedGoldBalanceChanged(blockNumber, new BigNumber(0)) + } + + const assertGovernanceBalanceUnchanged = async (blockNumber: number) => { + await assertGovernanceBalanceChanged(blockNumber, new BigNumber(0)) } for (const blockNumber of blockNumbers) { if (isLastBlockOfEpoch(blockNumber, epoch)) { await assertVotesChanged(group, blockNumber, epochReward) + await assertGoldTokenTotalSupplyChanged(blockNumber, epochReward.plus(infraReward)) + await assertLockedGoldBalanceChanged(blockNumber, epochReward) + await assertGovernanceBalanceChanged(blockNumber, infraReward) } else { await assertVotesUnchanged(group, blockNumber) + await assertGoldTokenTotalSupplyUnchanged(blockNumber) + await assertLockedGoldBalanceUnchanged(blockNumber) + await assertGovernanceBalanceUnchanged(blockNumber) } } }) }) - describe('after the governance smart contract is registered', () => { - let goldGenesisSupply: any - const addressesWithBalance: string[] = [] + describe('after the gold token smart contract is registered', () => { + let goldGenesisSupply = new BigNumber(0) beforeEach(async function(this: any) { this.timeout(0) // Disable test timeout await restart() const genesis = await importGenesis() - goldGenesisSupply = new BigNumber(0) - Object.keys(genesis.alloc).forEach((validator) => { - addressesWithBalance.push(validator) - goldGenesisSupply = goldGenesisSupply.plus(genesis.alloc[validator].balance) + Object.keys(genesis.alloc).forEach((address) => { + goldGenesisSupply = goldGenesisSupply.plus(genesis.alloc[address].balance) }) - // Block rewards are paid to governance and Locked Gold. - // Governance also receives a portion of transaction fees. - addressesWithBalance.push(await getContractAddress('GovernanceProxy')) - addressesWithBalance.push(await getContractAddress('LockedGoldProxy')) - // Some gold is sent to the reserve and exchange during migrations. - addressesWithBalance.push(await getContractAddress('ReserveProxy')) - addressesWithBalance.push(await getContractAddress('ExchangeProxy')) }) - it('should update the Celo Gold total supply correctly', async function(this: any) { - // To register a validator group, we send gold to a new address not included in - // `addressesWithBalance`. Therefore, we check the gold total supply at a block before - // that gold is sent. - // We don't set the total supply until block rewards are paid out, which can happen once - // Governance is registered. - let blockNumber = 150 - while (true) { - // This will fail if Governance is not registered. - const governanceAddress = await registry.methods - .getAddressForString('Governance') - .call({}, blockNumber) - if (new BigNumber(governanceAddress).isZero()) { - blockNumber += 1 - } else { + it('should initialize the Celo Gold total supply correctly', async function(this: any) { + const events = await registry.getPastEvents('RegistryUpdated', { fromBlock: 0 }) + let blockNumber = 0 + for (const e of events) { + if (e.returnValues.identifier === 'GoldToken') { + blockNumber = e.blockNumber break } } + assert.isAtLeast(blockNumber, 1) + const goldTotalSupply = await goldToken.methods.totalSupply().call({}, blockNumber) - const balances = await Promise.all( - addressesWithBalance.map( - async (a: string) => new BigNumber(await web3.eth.getBalance(a, blockNumber)) - ) - ) - const expectedGoldTotalSupply = balances.reduce((total: BigNumber, b: BigNumber) => - b.plus(total) - ) - assert.isAtLeast(expectedGoldTotalSupply.toNumber(), goldGenesisSupply.toNumber()) - assert.equal(goldTotalSupply.toString(), expectedGoldTotalSupply.toString()) + assert.equal(goldTotalSupply, goldGenesisSupply.toFixed()) }) }) }) diff --git a/packages/protocol/contracts/governance/LockedGold.sol b/packages/protocol/contracts/governance/LockedGold.sol index c3e868b5cef..74ab915fb6b 100644 --- a/packages/protocol/contracts/governance/LockedGold.sol +++ b/packages/protocol/contracts/governance/LockedGold.sol @@ -326,6 +326,22 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr } } + /** + * @notice Returns the account associated with `accountOrVoter`. + * @param accountOrVoter The address of the account or previously authorized voter. + * @dev Fails if the `accountOrVoter` is not an account or previously authorized voter. + * @return The associated account. + */ + function getAccountFromVoter(address accountOrVoter) public view returns (address) { + AuthorizedBy memory ab = authorizedBy[accountOrVoter]; + if (ab.account != address(0)) { + return ab.account; + } else { + require(isAccount(accountOrVoter)); + return accountOrVoter; + } + } + /** * @notice Returns the account associated with `accountOrValidator`. * @param accountOrValidator The address of the account or previously authorized validator. diff --git a/packages/protocol/test/common/fixidity.ts b/packages/protocol/test/common/fixidity.ts index 10a36a26e58..9349e437287 100644 --- a/packages/protocol/test/common/fixidity.ts +++ b/packages/protocol/test/common/fixidity.ts @@ -157,15 +157,6 @@ contract('FixidityLib', () => { assertEqualBN(result, expected) }) - it('should multiply two numbers less than 1', async () => { - const a = toFixed(0.1) - const b = toFixed(new BigNumber(10).pow(-14)) - const expected = toFixed(new BigNumber(10).pow(-15)) - const result = await fixidityTest.multiply(a, b) - - assertEqualBN(result, expected) - }) - it('should multiply by 0', async () => { const result = await fixidityTest.multiply(maxFixedMul, zero) diff --git a/packages/protocol/test/governance/election.ts b/packages/protocol/test/governance/election.ts index 1f417894680..788f76df43f 100644 --- a/packages/protocol/test/governance/election.ts +++ b/packages/protocol/test/governance/election.ts @@ -856,7 +856,7 @@ contract('Election', (accounts: string[]) => { }) }) - describe.only('#distributeEpochRewards', () => { + describe('#distributeEpochRewards', () => { const voter = accounts[0] const group = accounts[1] const voteValue = new BigNumber(1000000) diff --git a/packages/protocol/test/governance/lockedgold.ts b/packages/protocol/test/governance/lockedgold.ts index 6b25af9714f..4280bb1774a 100644 --- a/packages/protocol/test/governance/lockedgold.ts +++ b/packages/protocol/test/governance/lockedgold.ts @@ -3,7 +3,6 @@ import { assertEqualBN, assertLogMatches, assertRevert, - NULL_ADDRESS, timeTravel, } from '@celo/protocol/lib/test-utils' import BigNumber from 'bignumber.js' @@ -67,11 +66,13 @@ contract('LockedGold', (accounts: string[]) => { authorizationTests.voter = { fn: lockedGold.authorizeVoter, getAuthorizedFromAccount: lockedGold.getVoterFromAccount, + getAccountFromActiveAuthorized: lockedGold.getAccountFromActiveVoter, getAccountFromAuthorized: lockedGold.getAccountFromVoter, } authorizationTests.validator = { fn: lockedGold.authorizeValidator, getAuthorizedFromAccount: lockedGold.getValidatorFromAccount, + getAccountFromActiveAuthorized: lockedGold.getAccountFromActiveValidator, getAccountFromAuthorized: lockedGold.getAccountFromValidator, } }) @@ -127,9 +128,8 @@ contract('LockedGold', (accounts: string[]) => { it(`should set the authorized ${key}`, async () => { await authorizationTest.fn(authorized, sig.v, sig.r, sig.s) - assert.equal(await lockedGold.authorizedBy(authorized), account) assert.equal(await authorizationTest.getAuthorizedFromAccount(account), authorized) - assert.equal(await authorizationTest.getAccountFromAuthorized(authorized), account) + assert.equal(await authorizationTest.getAccountFromActiveAuthorized(authorized), account) }) it(`should emit a ${capitalize(key)}Authorized event`, async () => { @@ -174,13 +174,15 @@ contract('LockedGold', (accounts: string[]) => { }) it(`should set the new authorized ${key}`, async () => { - assert.equal(await lockedGold.authorizedBy(newAuthorized), account) assert.equal(await authorizationTest.getAuthorizedFromAccount(account), newAuthorized) - assert.equal(await authorizationTest.getAccountFromAuthorized(newAuthorized), account) + assert.equal( + await authorizationTest.getAccountFromActiveAuthorized(newAuthorized), + account + ) }) - it('should reset the previous authorization', async () => { - assert.equal(await lockedGold.authorizedBy(authorized), NULL_ADDRESS) + it('should preserve the previous authorization', async () => { + assert.equal(await authorizationTest.getAccountFromAuthorized(authorized), account) }) }) }) @@ -188,11 +190,11 @@ contract('LockedGold', (accounts: string[]) => { describe(`#getAccountFrom${capitalize(key)}()`, () => { describe(`when the account has not authorized a ${key}`, () => { it('should return the account when passed the account', async () => { - assert.equal(await authorizationTest.getAccountFromAuthorized(account), account) + assert.equal(await authorizationTest.getAccountFromActiveAuthorized(account), account) }) it('should revert when passed an address that is not an account', async () => { - await assertRevert(authorizationTest.getAccountFromAuthorized(accounts[1])) + await assertRevert(authorizationTest.getAccountFromActiveAuthorized(accounts[1])) }) }) @@ -204,11 +206,14 @@ contract('LockedGold', (accounts: string[]) => { }) it('should return the account when passed the account', async () => { - assert.equal(await authorizationTest.getAccountFromAuthorized(account), account) + assert.equal(await authorizationTest.getAccountFromActiveAuthorized(account), account) }) it(`should return the account when passed the ${key}`, async () => { - assert.equal(await authorizationTest.getAccountFromAuthorized(authorized), account) + assert.equal( + await authorizationTest.getAccountFromActiveAuthorized(authorized), + account + ) }) }) }) diff --git a/packages/protocol/test/governance/validators.ts b/packages/protocol/test/governance/validators.ts index 97fa1f3ed04..9983fe0aea4 100644 --- a/packages/protocol/test/governance/validators.ts +++ b/packages/protocol/test/governance/validators.ts @@ -1128,10 +1128,19 @@ contract('Validators', (accounts: string[]) => { const expectedEpoch = new BigNumber( Math.floor((await web3.eth.getBlock('latest')).number / EPOCH) ) - assert.equal(membershipHistory[0].length, 1) - assertEqualBN(membershipHistory[0][0], expectedEpoch) - assert.equal(membershipHistory[1].length, 1) - assertSameAddress(membershipHistory[1][0], NULL_ADDRESS) + + // Depending on test timing, we may or may not span an epoch boundary between registration + // and removal. + const numEntries = membershipHistory[0].length + assert.isTrue(numEntries == 1 || numEntries == 2) + assert.equal(membershipHistory[1].length, numEntries) + if (numEntries == 1) { + assertEqualBN(membershipHistory[0][0], expectedEpoch) + assertSameAddress(membershipHistory[1][0], NULL_ADDRESS) + } else { + assertEqualBN(membershipHistory[0][1], expectedEpoch) + assertSameAddress(membershipHistory[1][1], NULL_ADDRESS) + } }) it('should emit the ValidatorGroupMemberRemoved event', async () => { diff --git a/packages/protocol/test/stability/exchange.ts b/packages/protocol/test/stability/exchange.ts index eddd3b9183a..36be55415bc 100644 --- a/packages/protocol/test/stability/exchange.ts +++ b/packages/protocol/test/stability/exchange.ts @@ -1,3 +1,4 @@ +import { CeloContractName } from '@celo/protocol/lib/registry-utils' import { assertEqualBN, assertLogMatches2, @@ -87,10 +88,10 @@ contract('Exchange', (accounts: string[]) => { beforeEach(async () => { registry = await Registry.new() goldToken = await GoldToken.new() - await registry.setAddressFor('GoldToken', goldToken.address) + await registry.setAddressFor(CeloContractName.GoldToken, goldToken.address) mockReserve = await MockReserve.new() - await registry.setAddressFor('Reserve', mockReserve.address) + await registry.setAddressFor(CeloContractName.Reserve, mockReserve.address) await mockReserve.setGoldToken(goldToken.address) stableToken = await StableToken.new() @@ -101,11 +102,13 @@ contract('Exchange', (accounts: string[]) => { decimals, registry.address, fixed1, - SECONDS_IN_A_WEEK + SECONDS_IN_A_WEEK, + [], + [] ) mockSortedOracles = await MockSortedOracles.new() - await registry.setAddressFor('SortedOracles', mockSortedOracles.address) + await registry.setAddressFor(CeloContractName.SortedOracles, mockSortedOracles.address) await mockSortedOracles.setMedianRate( stableToken.address, stableAmountForRate, @@ -125,8 +128,7 @@ contract('Exchange', (accounts: string[]) => { updateFrequency, minimumReports ) - - await stableToken.setMinter(exchange.address) + await registry.setAddressFor(CeloContractName.Exchange, exchange.address) }) describe('#initialize()', () => { @@ -588,9 +590,9 @@ contract('Exchange', (accounts: string[]) => { let oldGoldBalance: BigNumber let oldReserveGoldBalance: BigNumber beforeEach(async () => { - await stableToken.setMinter(owner) + await registry.setAddressFor(CeloContractName.Exchange, owner) await stableToken.mint(user, stableTokenBalance) - await stableToken.setMinter(exchange.address) + await registry.setAddressFor(CeloContractName.Exchange, exchange.address) oldReserveGoldBalance = await goldToken.balanceOf(mockReserve.address) await stableToken.approve(exchange.address, stableTokenBalance, { from: user }) diff --git a/packages/protocol/test/stability/stabletoken.ts b/packages/protocol/test/stability/stabletoken.ts index 51a517d0bf8..f8fe84e57e5 100644 --- a/packages/protocol/test/stability/stabletoken.ts +++ b/packages/protocol/test/stability/stabletoken.ts @@ -35,7 +35,9 @@ contract('StableToken', (accounts: string[]) => { 18, registry.address, fixed1, - SECONDS_IN_A_WEEK + SECONDS_IN_A_WEEK, + [], + [] ) initializationTime = (await web3.eth.getBlock('latest')).timestamp }) @@ -87,7 +89,9 @@ contract('StableToken', (accounts: string[]) => { 18, registry.address, fixed1, - SECONDS_IN_A_WEEK + SECONDS_IN_A_WEEK, + [], + [] ) ) }) @@ -132,7 +136,7 @@ contract('StableToken', (accounts: string[]) => { }) it('should not allow anyone else to mint', async () => { - await assertRevert(stableToken.mint(minter, amountToMint, { from: accounts[2] })) + await assertRevert(stableToken.mint(validators, amountToMint, { from: accounts[2] })) }) }) From 03dc663ea286af21777f7a1d0e4a4fe535e7c129 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Mon, 7 Oct 2019 17:54:31 -0700 Subject: [PATCH 033/149] Add epoch size precompile, among other things --- .../protocol/contracts/common/UsingEpochs.sol | 19 --- .../contracts/common/UsingPrecompiles.sol | 91 +++++++++++ .../contracts/governance/Election.sol | 36 +++-- .../contracts/governance/Validators.sol | 22 +-- .../governance/test/ValidatorsTest.sol | 9 -- .../contracts/stability/StableToken.sol | 64 +------- packages/protocol/package.json | 4 +- packages/protocol/test/governance/election.ts | 144 ++++++++++-------- .../protocol/test/governance/validators.ts | 35 +++-- yarn.lock | 4 +- 10 files changed, 238 insertions(+), 190 deletions(-) delete mode 100644 packages/protocol/contracts/common/UsingEpochs.sol create mode 100644 packages/protocol/contracts/common/UsingPrecompiles.sol diff --git a/packages/protocol/contracts/common/UsingEpochs.sol b/packages/protocol/contracts/common/UsingEpochs.sol deleted file mode 100644 index 4d16aec0689..00000000000 --- a/packages/protocol/contracts/common/UsingEpochs.sol +++ /dev/null @@ -1,19 +0,0 @@ -pragma solidity ^0.5.3; - -// TODO: Replace this with a precompile. -contract UsingEpochs { - - event RegistrySet(address indexed registryAddress); - - // TODO(asa): Expose epoch size via precompile. - // solhint-disable state-visibility - uint256 constant EPOCH = 10; - - function getEpochNumber() public view returns (uint256) { - uint256 ret = block.number / EPOCH; - if (block.number % EPOCH == 0) { - ret = ret - 1; - } - return ret; - } -} diff --git a/packages/protocol/contracts/common/UsingPrecompiles.sol b/packages/protocol/contracts/common/UsingPrecompiles.sol new file mode 100644 index 00000000000..412a2470697 --- /dev/null +++ b/packages/protocol/contracts/common/UsingPrecompiles.sol @@ -0,0 +1,91 @@ +pragma solidity ^0.5.3; + +contract UsingPrecompiles { + + /** + * @notice calculate a * b^x for fractions a, b to `decimals` precision + * @param aNumerator Numerator of first fraction + * @param aDenominator Denominator of first fraction + * @param bNumerator Numerator of exponentiated fraction + * @param bDenominator Denominator of exponentiated fraction + * @param exponent exponent to raise b to + * @param _decimals precision + * @return numerator/denominator of the computed quantity (not reduced). + */ + function fractionMulExp( + uint256 aNumerator, + uint256 aDenominator, + uint256 bNumerator, + uint256 bDenominator, + uint256 exponent, + uint256 _decimals + ) + public + view + returns (uint256, uint256) + { + require(aDenominator != 0 && bDenominator != 0); + uint256 returnNumerator; + uint256 returnDenominator; + // solhint-disable-next-line no-inline-assembly + assembly { + let newCallDataPosition := mload(0x40) + mstore(0x40, add(newCallDataPosition, calldatasize)) + mstore(newCallDataPosition, aNumerator) + mstore(add(newCallDataPosition, 32), aDenominator) + mstore(add(newCallDataPosition, 64), bNumerator) + mstore(add(newCallDataPosition, 96), bDenominator) + mstore(add(newCallDataPosition, 128), exponent) + mstore(add(newCallDataPosition, 160), _decimals) + let success := staticcall( + 1050, // estimated gas cost for this function + 0xfc, + newCallDataPosition, + 0xc4, // input size, 6 * 32 = 192 bytes + 0, + 0 + ) + + let returnDataSize := returndatasize + let returnDataPosition := mload(0x40) + mstore(0x40, add(returnDataPosition, returnDataSize)) + returndatacopy(returnDataPosition, 0, returnDataSize) + + switch success + case 0 { + revert(returnDataPosition, returnDataSize) + } + default { + returnNumerator := mload(returnDataPosition) + returnDenominator := mload(add(returnDataPosition, 32)) + } + } + return (returnNumerator, returnDenominator); + } + + /** + * @notice Returns the current epoch size in blocks. + * @return The current epoch size in blocks. + */ + function getEpochSize() public view returns (uint256) { + uint256 ret; + // solhint-disable-next-line no-inline-assembly + assembly { + let newCallDataPosition := mload(0x40) + let success := staticcall(1000, 0xf8, newCallDataPosition, 0, 0, 0) + + returndatacopy(add(newCallDataPosition, 32), 0, 32) + ret := mload(add(newCallDataPosition, 32)) + } + return ret; + } + + function getEpochNumber() public view returns (uint256) { + uint256 epochSize = getEpochSize(); + uint256 epochNumber = block.number / epochSize; + if (block.number % epochSize == 0) { + epochNumber = epochNumber - 1; + } + return epochNumber; + } +} diff --git a/packages/protocol/contracts/governance/Election.sol b/packages/protocol/contracts/governance/Election.sol index 4c4e995b350..36f25479943 100644 --- a/packages/protocol/contracts/governance/Election.sol +++ b/packages/protocol/contracts/governance/Election.sol @@ -9,10 +9,11 @@ import "./interfaces/IValidators.sol"; import "../common/Initializable.sol"; import "../common/FixidityLib.sol"; import "../common/linkedlists/AddressSortedLinkedList.sol"; +import "../common/UsingPrecompiles.sol"; import "../common/UsingRegistry.sol"; -contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { +contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry, UsingPrecompiles { using AddressSortedLinkedList for SortedLinkedList.List; using FixidityLib for FixidityLib.Fraction; @@ -26,8 +27,8 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { mapping(address => uint256) total; // Maps groups to accounts to pending voting balance. mapping(address => mapping(address => uint256)) balances; - // Maps groups to accounts to timestamp of the account's most recent vote for the group. - mapping(address => mapping(address => uint256)) timestamps; + // Maps groups to accounts to the epoch of the account's most recent vote for the group. + mapping(address => mapping(address => uint256)) epochs; } // Active votes are those for which at least one following election has been held. @@ -43,8 +44,10 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { } struct TotalVotes { - // The total number of votes cast, including those for ineligible Validator Groups. - uint256 total; + // The total number of active votes cast, including those for ineligible Validator Groups. + uint256 active; + // The total number of pending votes cast, including those for ineligible Validator Groups. + uint256 pending; // A list of eligible Validator Groups sorted by total votes. SortedLinkedList.List eligible; } @@ -63,6 +66,7 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { uint256 public maxNumGroupsVotedFor; FixidityLib.Fraction public electabilityThreshold; + event Debug(uint256 value, string desc); event MinElectableValidatorsSet( uint256 minElectableValidators ); @@ -268,6 +272,7 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { function activate(address group) external nonReentrant returns (bool) { address account = getLockedGold().getAccountFromActiveVoter(msg.sender); PendingVotes storage pending = votes.pending; + require(getEpochNumber() > pending.epochs[group][account]); uint256 value = pending.balances[group][account]; require(value > 0); decrementPendingVotes(group, account, value); @@ -436,6 +441,10 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { return votes.total.eligible.contains(group); } + function getGroupEpochRewards(address group, uint256 totalEpochRewards) external view returns (uint256) { + return totalEpochRewards.mul(votes.active.total[group]).div(votes.total.active); + } + function distributeEpochRewards(address group, uint256 value, address lesser, address greater) external { require(msg.sender == address(0)); _distributeEpochRewards(group, value, lesser, greater); @@ -445,13 +454,14 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { // TODO(asa): What do here? if (votes.active.total[group] == 0) { } + if (votes.total.eligible.contains(group)) { uint256 newVoteTotal = votes.total.eligible.getValue(group).add(value); votes.total.eligible.update(group, newVoteTotal, lesser, greater); } - + votes.active.total[group] = votes.active.total[group].add(value); - votes.total.total = votes.total.total.add(value); + votes.total.active = votes.total.active.add(value); } /** @@ -474,7 +484,6 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { require(votes.total.eligible.contains(group)); uint256 newVoteTotal = votes.total.eligible.getValue(group).add(value); votes.total.eligible.update(group, newVoteTotal, lesser, greater); - votes.total.total = votes.total.total.add(value); } /** @@ -498,7 +507,6 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { uint256 newVoteTotal = votes.total.eligible.getValue(group).sub(value); votes.total.eligible.update(group, newVoteTotal, lesser, greater); } - votes.total.total = votes.total.total.sub(value); } /** @@ -547,8 +555,9 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { function incrementPendingVotes(address group, address account, uint256 value) private { PendingVotes storage pending = votes.pending; pending.balances[group][account] = pending.balances[group][account].add(value); - pending.timestamps[group][account] = now; + pending.epochs[group][account] = getEpochNumber(); pending.total[group] = pending.total[group].add(value); + votes.total.pending = votes.total.pending.add(value); } /** @@ -562,9 +571,10 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { uint256 newValue = pending.balances[group][account].sub(value); pending.balances[group][account] = newValue; if (newValue == 0) { - pending.timestamps[group][account] = 0; + pending.epochs[group][account] = 0; } pending.total[group] = pending.total[group].sub(value); + votes.total.pending = votes.total.pending.sub(value); } /** @@ -579,6 +589,7 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { active.numerators[group][account] = active.numerators[group][account].add(delta); active.denominators[group] = active.denominators[group].add(delta); active.total[group] = active.total[group].add(value); + votes.total.active = votes.total.active.add(value); } /** @@ -593,6 +604,7 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { active.numerators[group][account] = active.numerators[group][account].sub(delta); active.denominators[group] = active.denominators[group].sub(delta); active.total[group] = active.total[group].sub(value); + votes.total.active = votes.total.active.sub(value); } /** @@ -646,7 +658,7 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { * @return The total votes received across all groups. */ function getTotalVotes() external view returns (uint256) { - return votes.total.total; + return votes.total.active.add(votes.total.pending); } /** diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index 41e9b291a83..58c3e36a050 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -13,14 +13,14 @@ import "../identity/interfaces/IRandom.sol"; import "../common/Initializable.sol"; import "../common/FixidityLib.sol"; import "../common/linkedlists/AddressLinkedList.sol"; -import "../common/UsingEpochs.sol"; import "../common/UsingRegistry.sol"; +import "../common/UsingPrecompiles.sol"; /** * @title A contract for registering and electing Validator Groups and Validators. */ -contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, UsingEpochs, UsingRegistry { +contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, UsingRegistry, UsingPrecompiles { using FixidityLib for FixidityLib.Fraction; using AddressLinkedList for LinkedList.List; @@ -408,17 +408,21 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi require(isValidator(account), "isvalidator"); require(uptime <= FixidityLib.fixed1().unwrap(), "uptime"); - // TODO(asa): Use exponent. - FixidityLib.Fraction memory epochScore = FixidityLib.wrap(uptime); - + uint256 numerator; + uint256 denominator; + (numerator, denominator) = fractionMulExp( + FixidityLib.fixed1().unwrap(), + FixidityLib.fixed1().unwrap(), + uptime, + FixidityLib.fixed1().unwrap(), + validatorScoreParameters.exponent, + 18 + ); - // New component is 0! Uptime is non-zero. + FixidityLib.Fraction memory epochScore = FixidityLib.wrap(numerator).divide(FixidityLib.wrap(denominator)); FixidityLib.Fraction memory newComponent = validatorScoreParameters.adjustmentSpeed.multiply( epochScore ); - // validators[validator].score = newComponent; - // This works: - // validators[validator].score = epochScore; FixidityLib.Fraction memory currentComponent = FixidityLib.fixed1().subtract( validatorScoreParameters.adjustmentSpeed diff --git a/packages/protocol/contracts/governance/test/ValidatorsTest.sol b/packages/protocol/contracts/governance/test/ValidatorsTest.sol index 16cdabd8364..beefe62389e 100644 --- a/packages/protocol/contracts/governance/test/ValidatorsTest.sol +++ b/packages/protocol/contracts/governance/test/ValidatorsTest.sol @@ -8,15 +8,6 @@ import "../../common/FixidityLib.sol"; */ contract ValidatorsTest is Validators { - function getEpochNumber() public view returns (uint256) { - uint256 epoch = 100; - uint256 ret = block.number / epoch; - if (block.number % epoch == 0) { - ret = ret - 1; - } - return ret; - } - function updateValidatorScore(address validator, uint256 uptime) external { return _updateValidatorScore(validator, uptime); } diff --git a/packages/protocol/contracts/stability/StableToken.sol b/packages/protocol/contracts/stability/StableToken.sol index 8974fb91c7d..50c53f9ab11 100644 --- a/packages/protocol/contracts/stability/StableToken.sol +++ b/packages/protocol/contracts/stability/StableToken.sol @@ -10,6 +10,7 @@ import "../common/interfaces/ICeloToken.sol"; import "../common/Initializable.sol"; import "../common/FixidityLib.sol"; import "../common/UsingRegistry.sol"; +import "../common/UsingPrecompiles.sol"; /** @@ -17,7 +18,7 @@ import "../common/UsingRegistry.sol"; */ // solhint-disable-next-line max-line-length contract StableToken is IStableToken, IERC20Token, ICeloToken, Ownable, - Initializable, UsingRegistry { + Initializable, UsingRegistry, UsingPrecompiles { using FixidityLib for FixidityLib.Fraction; using SafeMath for uint256; @@ -451,67 +452,6 @@ contract StableToken is IStableToken, IERC20Token, ICeloToken, Ownable, /* solhint-enable not-rely-on-time */ } - /** - * @notice calculate a * b^x for fractions a, b to `decimals` precision - * @param aNumerator Numerator of first fraction - * @param aDenominator Denominator of first fraction - * @param bNumerator Numerator of exponentiated fraction - * @param bDenominator Denominator of exponentiated fraction - * @param exponent exponent to raise b to - * @param _decimals precision - * @return numerator/denominator of the computed quantity (not reduced). - */ - function fractionMulExp( - uint256 aNumerator, - uint256 aDenominator, - uint256 bNumerator, - uint256 bDenominator, - uint256 exponent, - uint256 _decimals - ) - public - view - returns(uint256, uint256) - { - require(aDenominator != 0 && bDenominator != 0); - uint256 returnNumerator; - uint256 returnDenominator; - // solhint-disable-next-line no-inline-assembly - assembly { - let newCallDataPosition := mload(0x40) - mstore(0x40, add(newCallDataPosition, calldatasize)) - mstore(newCallDataPosition, aNumerator) - mstore(add(newCallDataPosition, 32), aDenominator) - mstore(add(newCallDataPosition, 64), bNumerator) - mstore(add(newCallDataPosition, 96), bDenominator) - mstore(add(newCallDataPosition, 128), exponent) - mstore(add(newCallDataPosition, 160), _decimals) - let delegatecallSuccess := staticcall( - 1050, // estimated gas cost for this function - 0xfc, - newCallDataPosition, - 0xc4, // input size, 6 * 32 = 192 bytes - 0, - 0 - ) - - let returnDataSize := returndatasize - let returnDataPosition := mload(0x40) - mstore(0x40, add(returnDataPosition, returnDataSize)) - returndatacopy(returnDataPosition, 0, returnDataSize) - - switch delegatecallSuccess - case 0 { - revert(returnDataPosition, returnDataSize) - } - default { - returnNumerator := mload(returnDataPosition) - returnDenominator := mload(add(returnDataPosition, 32)) - } - } - return (returnNumerator, returnDenominator); - } - /** * @notice Transfers `value` from `msg.sender` to `to` * @param to The address to transfer to. diff --git a/packages/protocol/package.json b/packages/protocol/package.json index 47662055610..57dab8e83e9 100644 --- a/packages/protocol/package.json +++ b/packages/protocol/package.json @@ -9,7 +9,7 @@ "lint:ts": "tslint -c tslint.json --project tsconfig.json", "lint:sol": "solhint './contracts/**/*.sol'", "lint": "yarn run lint:ts && yarn run lint:sol", - "clean": "rm -rf ./types/typechain && rm -rf build/* && rm -rf migrations/*.js* && rm -rf test/**/*.js* && rm -f lib/*.js*", + "clean": "rm -rf ./types/typechain && rm -rf build/* && rm -rf .0x-artifacts/* && rm -rf migrations/*.js* && rm -rf test/**/*.js* && rm -f lib/*.js*", "pretest": "yarn run build", "test": "node runTests.js", "test:coverage": "yarn run test --coverage", @@ -74,7 +74,7 @@ "web3-provider-engine": "^15.0.0" }, "devDependencies": { - "@celo/ganache-cli": "git+https://github.com/celo-org/ganache-cli.git#816a475", + "@celo/ganache-cli": "git+https://github.com/celo-org/ganache-cli.git#4cf9664", "@celo/typescript": "0.0.1", "@types/bignumber.js": "^5.0.0", "@types/bn.js": "^4.11.0", diff --git a/packages/protocol/test/governance/election.ts b/packages/protocol/test/governance/election.ts index 788f76df43f..ac6d94a22d0 100644 --- a/packages/protocol/test/governance/election.ts +++ b/packages/protocol/test/governance/election.ts @@ -4,6 +4,7 @@ import { assertEqualBN, assertRevert, NULL_ADDRESS, + mineBlocks, } from '@celo/protocol/lib/test-utils' import { toFixed } from '@celo/utils/lib/fixidity' import BigNumber from 'bignumber.js' @@ -30,6 +31,9 @@ const Registry: RegistryContract = artifacts.require('Registry') // TODO(mcortesi): Use BN ElectionTest.numberFormat = 'BigNumber' +// Hard coded in ganache. +const EPOCH = 100 + contract('Election', (accounts: string[]) => { let election: ElectionTestInstance let registry: RegistryInstance @@ -428,101 +432,114 @@ contract('Election', (accounts: string[]) => { }) describe('when the voter has pending votes', () => { - let resp: any beforeEach(async () => { await election.vote(group, value, NULL_ADDRESS, NULL_ADDRESS) - resp = await election.activate(group) - }) - - it("should decrement the account's pending votes for the group", async () => { - assertEqualBN(await election.getAccountPendingVotesForGroup(group, voter), 0) - }) - - it("should increment the account's active votes for the group", async () => { - assertEqualBN(await election.getAccountActiveVotesForGroup(group, voter), value) - }) - - it("should not modify the account's total votes for the group", async () => { - assertEqualBN(await election.getAccountTotalVotesForGroup(group, voter), value) }) - it("should not modify the account's total votes", async () => { - assertEqualBN(await election.getAccountTotalVotes(voter), value) - }) - - it('should not modify the total votes for the group', async () => { - assertEqualBN(await election.getGroupTotalVotes(group), value) - }) - - it('should not modify the total votes', async () => { - assertEqualBN(await election.getTotalVotes(), value) - }) - - it('should emit the ValidatorGroupVoteActivated event', async () => { - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'ValidatorGroupVoteActivated', - args: { - account: voter, - group, - value: new BigNumber(value), - }, + describe('when an epoch boundary has passed since the pending votes were made', () => { + let resp: any + beforeEach(async () => { + await mineBlocks(EPOCH, web3) + resp = await election.activate(group) }) - }) - describe('when another voter activates votes', () => { - const voter2 = accounts[2] - const value2 = 573 - beforeEach(async () => { - await mockLockedGold.incrementNonvotingAccountBalance(voter2, value2) - await election.vote(group, value2, NULL_ADDRESS, NULL_ADDRESS, { from: voter2 }) - await election.activate(group, { from: voter2 }) + it("should decrement the account's pending votes for the group", async () => { + assertEqualBN(await election.getAccountPendingVotesForGroup(group, voter), 0) }) - it("should not modify the first account's active votes for the group", async () => { + it("should increment the account's active votes for the group", async () => { assertEqualBN(await election.getAccountActiveVotesForGroup(group, voter), value) }) - it("should not modify the first account's total votes for the group", async () => { + it("should not modify the account's total votes for the group", async () => { assertEqualBN(await election.getAccountTotalVotesForGroup(group, voter), value) }) - it("should not modify the first account's total votes", async () => { + it("should not modify the account's total votes", async () => { assertEqualBN(await election.getAccountTotalVotes(voter), value) }) - it("should decrement the second account's pending votes for the group", async () => { - assertEqualBN(await election.getAccountPendingVotesForGroup(group, voter2), 0) + it('should not modify the total votes for the group', async () => { + assertEqualBN(await election.getGroupTotalVotes(group), value) }) - it("should increment the second account's active votes for the group", async () => { - assertEqualBN(await election.getAccountActiveVotesForGroup(group, voter2), value2) + it('should not modify the total votes', async () => { + assertEqualBN(await election.getTotalVotes(), value) }) - it("should not modify the second account's total votes for the group", async () => { - assertEqualBN(await election.getAccountTotalVotesForGroup(group, voter2), value2) + it('should emit the ValidatorGroupVoteActivated event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorGroupVoteActivated', + args: { + account: voter, + group, + value: new BigNumber(value), + }, + }) }) - it("should not modify the second account's total votes", async () => { - assertEqualBN(await election.getAccountTotalVotes(voter2), value2) - }) + describe('when another voter activates votes', () => { + const voter2 = accounts[2] + const value2 = 573 + beforeEach(async () => { + await mockLockedGold.incrementNonvotingAccountBalance(voter2, value2) + await election.vote(group, value2, NULL_ADDRESS, NULL_ADDRESS, { from: voter2 }) + await mineBlocks(EPOCH, web3) + await election.activate(group, { from: voter2 }) + }) - it('should not modify the total votes for the group', async () => { - assertEqualBN(await election.getGroupTotalVotes(group), value + value2) - }) + it("should not modify the first account's active votes for the group", async () => { + assertEqualBN(await election.getAccountActiveVotesForGroup(group, voter), value) + }) - it('should not modify the total votes', async () => { - assertEqualBN(await election.getTotalVotes(), value + value2) + it("should not modify the first account's total votes for the group", async () => { + assertEqualBN(await election.getAccountTotalVotesForGroup(group, voter), value) + }) + + it("should not modify the first account's total votes", async () => { + assertEqualBN(await election.getAccountTotalVotes(voter), value) + }) + + it("should decrement the second account's pending votes for the group", async () => { + assertEqualBN(await election.getAccountPendingVotesForGroup(group, voter2), 0) + }) + + it("should increment the second account's active votes for the group", async () => { + assertEqualBN(await election.getAccountActiveVotesForGroup(group, voter2), value2) + }) + + it("should not modify the second account's total votes for the group", async () => { + assertEqualBN(await election.getAccountTotalVotesForGroup(group, voter2), value2) + }) + + it("should not modify the second account's total votes", async () => { + assertEqualBN(await election.getAccountTotalVotes(voter2), value2) + }) + + it('should not modify the total votes for the group', async () => { + assertEqualBN(await election.getGroupTotalVotes(group), value + value2) + }) + + it('should not modify the total votes', async () => { + assertEqualBN(await election.getTotalVotes(), value + value2) + }) }) }) - describe('when the voter does not have pending votes', () => { + describe('when an epoch boundary has not passed since the pending votes were made', () => { it('should revert', async () => { await assertRevert(election.activate(group)) }) }) }) + + describe('when the voter does not have pending votes', () => { + it('should revert', async () => { + await assertRevert(election.activate(group)) + }) + }) }) describe('#revokePending', () => { @@ -637,6 +654,7 @@ contract('Election', (accounts: string[]) => { await mockValidators.setNumRegisteredValidators(1) await mockLockedGold.incrementNonvotingAccountBalance(voter, value) await election.vote(group, value, NULL_ADDRESS, NULL_ADDRESS) + await mineBlocks(EPOCH, web3) await election.activate(group) }) @@ -868,6 +886,7 @@ contract('Election', (accounts: string[]) => { await mockValidators.setNumRegisteredValidators(1) await mockLockedGold.incrementNonvotingAccountBalance(voter, voteValue) await election.vote(group, voteValue, NULL_ADDRESS, NULL_ADDRESS) + await mineBlocks(EPOCH, web3) await election.activate(group) }) @@ -919,6 +938,7 @@ contract('Election', (accounts: string[]) => { // Split voter2's vote between the two groups. await election.vote(group, voteValue2.div(2), group2, NULL_ADDRESS, { from: voter2 }) await election.vote(group2, voteValue2.div(2), NULL_ADDRESS, group, { from: voter2 }) + await mineBlocks(EPOCH, web3) await election.activate(group, { from: voter2 }) await election.activate(group2, { from: voter2 }) }) diff --git a/packages/protocol/test/governance/validators.ts b/packages/protocol/test/governance/validators.ts index 9983fe0aea4..f6ad41c7a39 100644 --- a/packages/protocol/test/governance/validators.ts +++ b/packages/protocol/test/governance/validators.ts @@ -54,7 +54,7 @@ const parseValidatorGroupParams = (groupParams: any) => { const HOUR = 60 * 60 const DAY = 24 * HOUR const MAX_UINT256 = new BigNumber(2).pow(256).minus(1) -// Hard coded in ValidatorsTest.sol +// Hard coded in ganache. const EPOCH = 100 // TODO(asa): Test epoch payment distribution @@ -71,7 +71,7 @@ contract('Validators', (accounts: string[]) => { validator: new BigNumber(60 * DAY), } const validatorScoreParameters = { - exponent: new BigNumber(1), + exponent: new BigNumber(5), adjustmentSpeed: toFixed(0.25), } const validatorEpochPayment = new BigNumber(10000000000000) @@ -1237,14 +1237,16 @@ contract('Validators', (accounts: string[]) => { }) describe('when 0 <= uptime <= 1.0', () => { - const uptime = 0.99 + const uptime = new BigNumber(0.99) + // @ts-ignore + const epochScore = uptime.pow(validatorScoreParameters.exponent) const adjustmentSpeed = fromFixed(validatorScoreParameters.adjustmentSpeed) beforeEach(async () => { await validators.updateValidatorScore(validator, toFixed(uptime)) }) it('should update the validator score', async () => { - const expectedScore = adjustmentSpeed.times(uptime) + const expectedScore = adjustmentSpeed.times(epochScore) const parsedValidator = parseValidatorParams(await validators.getValidator(validator)) assertEqualBN(parsedValidator.score, toFixed(expectedScore)) }) @@ -1255,7 +1257,7 @@ contract('Validators', (accounts: string[]) => { }) it('should update the validator score', async () => { - let expectedScore = adjustmentSpeed.times(uptime) + let expectedScore = adjustmentSpeed.times(epochScore) expectedScore = new BigNumber(1) .minus(adjustmentSpeed) .times(expectedScore) @@ -1341,6 +1343,12 @@ contract('Validators', (accounts: string[]) => { }) }) + describe('#getEpochSize', () => { + it('should always return 100', async () => { + assertEqualBN(await validators.getEpochSize(), 100) + }) + }) + describe('#distributeEpochPayment', () => { const validator = accounts[0] const group = accounts[1] @@ -1352,25 +1360,26 @@ contract('Validators', (accounts: string[]) => { }) describe('when the validator score is non-zero', () => { - const uptime = 0.99 + const uptime = new BigNumber(0.99) const adjustmentSpeed = fromFixed(validatorScoreParameters.adjustmentSpeed) - const expectedScore = adjustmentSpeed.times(uptime) + // @ts-ignore + const expectedScore = adjustmentSpeed.times(uptime.pow(validatorScoreParameters.exponent)) const expectedTotalPayment = expectedScore.times(validatorEpochPayment) + const expectedGroupPayment = expectedTotalPayment + .times(fromFixed(commission)) + .dp(0, BigNumber.ROUND_FLOOR) + const expectedValidatorPayment = expectedTotalPayment.minus(expectedGroupPayment) beforeEach(async () => { await validators.updateValidatorScore(validator, toFixed(uptime)) await validators.distributeEpochPayment(validator) }) it('should pay the validator', async () => { - const expectedPayment = expectedTotalPayment.times( - new BigNumber(1).minus(fromFixed(commission)) - ) - assertEqualBN(await mockStableToken.balanceOf(validator), expectedPayment) + assertEqualBN(await mockStableToken.balanceOf(validator), expectedValidatorPayment) }) it('should pay the group', async () => { - const expectedPayment = expectedTotalPayment.times(fromFixed(commission)) - assertEqualBN(await mockStableToken.balanceOf(group), expectedPayment) + assertEqualBN(await mockStableToken.balanceOf(group), expectedGroupPayment) }) }) }) diff --git a/yarn.lock b/yarn.lock index ffa4029db39..a51bc235424 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2266,9 +2266,9 @@ qqjs "^0.3.10" tslib "^1.9.3" -"@celo/ganache-cli@git+https://github.com/celo-org/ganache-cli.git#816a475": +"@celo/ganache-cli@git+https://github.com/celo-org/ganache-cli.git#4cf9664": version "6.6.0" - resolved "git+https://github.com/celo-org/ganache-cli.git#816a475d82535c05b59c4c43b3603c39dd31cd88" + resolved "git+https://github.com/celo-org/ganache-cli.git#4cf9664597fff315f7318e9376d079bd6d548624" dependencies: ethereumjs-util "6.1.0" source-map-support "0.5.12" From b4914537d5e43631ddd4bfacb5a9f9cf2be391c7 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Mon, 7 Oct 2019 19:34:43 -0700 Subject: [PATCH 034/149] Revert "Feature #909 proxy delegatecall (#1152)" This reverts commit 78afc930022894fd870f9eb70a3103899de348fa. --- .../protocol/contracts/common/MultiSig.sol | 5 ---- packages/protocol/contracts/common/Proxy.sol | 12 +--------- .../common/libraries/AddressesHelper.sol | 21 ----------------- .../contracts/governance/Proposals.sol | 5 ---- packages/protocol/test/common/proxy.ts | 7 ------ .../protocol/test/governance/governance.ts | 23 ------------------- 6 files changed, 1 insertion(+), 72 deletions(-) delete mode 100644 packages/protocol/contracts/common/libraries/AddressesHelper.sol diff --git a/packages/protocol/contracts/common/MultiSig.sol b/packages/protocol/contracts/common/MultiSig.sol index ee6944ffd1a..c979e61e7e2 100644 --- a/packages/protocol/contracts/common/MultiSig.sol +++ b/packages/protocol/contracts/common/MultiSig.sol @@ -2,7 +2,6 @@ pragma solidity ^0.5.3; /* solhint-disable no-inline-assembly, avoid-low-level-calls, func-name-mixedcase, func-order */ import "./Initializable.sol"; -import "./libraries/AddressesHelper.sol"; /// @title Multisignature wallet - Allows multiple parties to agree on transactions before @@ -262,10 +261,6 @@ contract MultiSig is Initializable { returns (bool) { bool result; - - if (dataLength > 0) - require(AddressesHelper.isContract(destination), "Invalid contract address"); - /* solhint-disable max-line-length */ assembly { let x := mload(0x40) // "Allocate" memory for output (0x40 is where "free memory" pointer is stored by convention) diff --git a/packages/protocol/contracts/common/Proxy.sol b/packages/protocol/contracts/common/Proxy.sol index 13f085c66da..f5027a22f6e 100644 --- a/packages/protocol/contracts/common/Proxy.sol +++ b/packages/protocol/contracts/common/Proxy.sol @@ -1,7 +1,6 @@ pragma solidity ^0.5.3; /* solhint-disable no-inline-assembly, no-complex-fallback, avoid-low-level-calls */ -import "./libraries/AddressesHelper.sol"; /** * @title A Proxy utilizing the Unstructured Storage pattern. @@ -33,15 +32,8 @@ contract Proxy { function () external payable { bytes32 implementationPosition = IMPLEMENTATION_POSITION; - address implementationAddress; - - assembly { - implementationAddress := sload(implementationPosition) - } - - require(AddressesHelper.isContract(implementationAddress), "Invalid contract address"); - assembly { + let implementationAddress := sload(implementationPosition) let newCallDataPosition := mload(0x40) mstore(0x40, add(newCallDataPosition, calldatasize)) @@ -122,8 +114,6 @@ contract Proxy { function _setImplementation(address implementation) public onlyOwner { bytes32 implementationPosition = IMPLEMENTATION_POSITION; - require(AddressesHelper.isContract(implementation), "Invalid contract address"); - assembly { sstore(implementationPosition, implementation) } diff --git a/packages/protocol/contracts/common/libraries/AddressesHelper.sol b/packages/protocol/contracts/common/libraries/AddressesHelper.sol deleted file mode 100644 index ee26b42d8a4..00000000000 --- a/packages/protocol/contracts/common/libraries/AddressesHelper.sol +++ /dev/null @@ -1,21 +0,0 @@ -pragma solidity ^0.5.3; - -/** - * @title Library with support functions to deal with addresses - */ -library AddressesHelper { - - /** - * @dev isContract detect whether the address is - * a contract address or externally owned account (EOA) - * WARNING: Calling this function from a constructor will return false - * independently if the address given as parameter is a contract or EOA - * @return true if it is a contract address - */ - function isContract(address addr) internal view returns (bool) { - uint256 size; - /* solium-disable-next-line security/no-inline-assembly */ - assembly { size := extcodesize(addr) } - return size > 0; - } -} diff --git a/packages/protocol/contracts/governance/Proposals.sol b/packages/protocol/contracts/governance/Proposals.sol index 71e1a44e738..35602df088f 100644 --- a/packages/protocol/contracts/governance/Proposals.sol +++ b/packages/protocol/contracts/governance/Proposals.sol @@ -4,7 +4,6 @@ import "openzeppelin-solidity/contracts/math/SafeMath.sol"; import "solidity-bytes-utils/contracts/BytesLib.sol"; import "../common/FixidityLib.sol"; -import "../common/libraries/AddressesHelper.sol"; /** * @title A library operating on Celo Governance proposals. @@ -325,10 +324,6 @@ library Proposals { returns (bool) { bool result; - - if (dataLength > 0) - require(AddressesHelper.isContract(destination), "Invalid contract address"); - /* solhint-disable no-inline-assembly */ assembly { /* solhint-disable max-line-length */ diff --git a/packages/protocol/test/common/proxy.ts b/packages/protocol/test/common/proxy.ts index 9a2a810fba5..a70c3e724d2 100644 --- a/packages/protocol/test/common/proxy.ts +++ b/packages/protocol/test/common/proxy.ts @@ -116,13 +116,6 @@ contract('Proxy', (accounts: string[]) => { assert.equal(events[0].event, 'ImplementationSet') }) - it('should not allow to call a non contract address', async () => - assertRevert( - proxy._setAndInitializeImplementation(accounts[1], initializeData(42), { - from: accounts[1], - }) - )) - it('should not allow a non-owner to set an implementation', async () => assertRevert( proxy._setAndInitializeImplementation(hasInitializer.address, initializeData(42), { diff --git a/packages/protocol/test/governance/governance.ts b/packages/protocol/test/governance/governance.ts index a72f1132eed..38f5f02d48e 100644 --- a/packages/protocol/test/governance/governance.ts +++ b/packages/protocol/test/governance/governance.ts @@ -1625,29 +1625,6 @@ contract('Governance', (accounts: string[]) => { await assertRevert(governance.execute(proposalId, index)) }) }) - - describe('when the proposal cannot execute because it is not a contract address', () => { - beforeEach(async () => { - await governance.propose( - [transactionSuccess1.value], - [accounts[1]], - transactionSuccess1.data, - [transactionSuccess1.data.length], - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - await timeTravel(dequeueFrequency, web3) - await governance.approve(proposalId, index) - await timeTravel(approvalStageDuration, web3) - await mockLockedGold.setWeight(account, weight) - await governance.vote(proposalId, index, value) - await timeTravel(referendumStageDuration, web3) - }) - - it('should revert', async () => { - await assertRevert(governance.execute(proposalId, index)) - }) - }) }) describe('when executing a proposal with two transactions', () => { From 96f7a7aac239e4be8a772586168266a05f26bb7d Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Mon, 7 Oct 2019 22:32:13 -0700 Subject: [PATCH 035/149] Governance end-to-end tests working again --- .../celotool/src/e2e-tests/governance_tests.ts | 16 +++++++++++++++- .../protocol/contracts/governance/Election.sol | 8 ++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/packages/celotool/src/e2e-tests/governance_tests.ts b/packages/celotool/src/e2e-tests/governance_tests.ts index f478f344e4f..daf8b7c4151 100644 --- a/packages/celotool/src/e2e-tests/governance_tests.ts +++ b/packages/celotool/src/e2e-tests/governance_tests.ts @@ -76,6 +76,17 @@ describe('governance tests', () => { return [groupAddress, decryptedKeystore.privateKey] } + const activate = async (web3: any, account: string, txOptions: any = {}) => { + await unlockAccount(account, web3) + const [group] = await validators.methods.getRegisteredValidatorGroups().call() + const tx = election.methods.activate(group) + let gas = txOptions.gas + if (!gas) { + gas = await tx.estimateGas({ ...txOptions }) + } + return tx.send({ from: account, ...txOptions, gas }) + } + const removeMember = async ( groupWeb3: any, group: string, @@ -106,7 +117,7 @@ describe('governance tests', () => { } describe('when the validator set is changing', () => { - const epoch = 10 + let epoch: number const blockNumbers: number[] = [] let allValidators: string[] before(async function(this: any) { @@ -126,9 +137,12 @@ describe('governance tests', () => { await initAndStartGeth(context.hooks.gethBinaryPath, groupInstance) allValidators = await getValidatorGroupMembers() assert.equal(allValidators.length, 5) + epoch = new BigNumber(await validators.methods.getEpochSize().call()).toNumber() + assert.equal(epoch, 10) // Give the node time to sync. await sleep(15) + await activate(web3, allValidators[0]) const groupWeb3 = new Web3('ws://localhost:8567') const groupKit = newKitFromWeb3(groupWeb3) validators = await groupKit._web3Contracts.getValidators() diff --git a/packages/protocol/contracts/governance/Election.sol b/packages/protocol/contracts/governance/Election.sol index 36f25479943..c82c198d648 100644 --- a/packages/protocol/contracts/governance/Election.sol +++ b/packages/protocol/contracts/governance/Election.sol @@ -442,6 +442,10 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry, Usi } function getGroupEpochRewards(address group, uint256 totalEpochRewards) external view returns (uint256) { + // TODO(asa): Is this right? + if (votes.active.total[group] == 0 || votes.total.active == 0) { + return 0; + } return totalEpochRewards.mul(votes.active.total[group]).div(votes.total.active); } @@ -451,10 +455,6 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry, Usi } function _distributeEpochRewards(address group, uint256 value, address lesser, address greater) internal { - // TODO(asa): What do here? - if (votes.active.total[group] == 0) { - } - if (votes.total.eligible.contains(group)) { uint256 newVoteTotal = votes.total.eligible.getValue(group).add(value); votes.total.eligible.update(group, newVoteTotal, lesser, greater); From e40eaf0f90ae313da2b1f27aa27918edefae754d Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Tue, 8 Oct 2019 16:58:24 -0700 Subject: [PATCH 036/149] Address comments --- packages/cli/package.json | 2 +- packages/cli/src/commands/election/vote.ts | 2 +- .../cli/src/commands/lockedgold/authorize.ts | 3 + packages/cli/src/commands/lockedgold/lock.ts | 12 +- packages/cli/src/commands/lockedgold/show.ts | 20 +- .../cli/src/commands/lockedgold/unlock.ts | 6 +- .../cli/src/commands/lockedgold/withdraw.ts | 27 +- .../cli/src/commands/validatorgroup/member.ts | 6 +- .../src/commands/validatorgroup/register.ts | 9 +- packages/cli/src/utils/lockedgold.ts | 8 +- packages/contractkit/src/wrappers/Election.ts | 51 +-- .../contractkit/src/wrappers/LockedGold.ts | 35 +- .../contractkit/src/wrappers/Validators.ts | 22 +- .../contracts/common/UsingRegistry.sol | 8 +- .../common/linkedlists/AddressLinkedList.sol | 1 + .../common/linkedlists/LinkedList.sol | 1 + .../common/linkedlists/SortedLinkedList.sol | 1 + .../contracts/governance/Election.sol | 341 ++++++++++-------- .../contracts/governance/LockedGold.sol | 15 +- .../contracts/governance/Validators.sol | 15 +- packages/protocol/migrationsConfig.js | 2 +- packages/protocol/test/governance/election.ts | 202 +++++------ .../protocol/test/governance/validators.ts | 7 +- 23 files changed, 448 insertions(+), 348 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index ee2ecec4124..8a8b7ea7fd5 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -84,7 +84,7 @@ "description": "Configure CLI options which persist across commands" }, "lockedgold": { - "description": "View and manage locked celo gold" + "description": "View and manage locked Celo Gold" }, "node": { "description": "Manage your full node" diff --git a/packages/cli/src/commands/election/vote.ts b/packages/cli/src/commands/election/vote.ts index 9b466094a1a..ae5f7590454 100644 --- a/packages/cli/src/commands/election/vote.ts +++ b/packages/cli/src/commands/election/vote.ts @@ -14,7 +14,7 @@ export default class ElectionVote extends BaseCommand { description: "Set vote for ValidatorGroup's address", required: true, }), - value: flags.string({ description: 'Amount of gold used to vote for group', required: true }), + value: flags.string({ description: 'Amount of Gold used to vote for group', required: true }), } static examples = [ diff --git a/packages/cli/src/commands/lockedgold/authorize.ts b/packages/cli/src/commands/lockedgold/authorize.ts index 50ba89edd63..6a66475ee23 100644 --- a/packages/cli/src/commands/lockedgold/authorize.ts +++ b/packages/cli/src/commands/lockedgold/authorize.ts @@ -43,6 +43,9 @@ export default class Authorize extends BaseCommand { tx = await lockedGold.authorizeVoter(res.flags.from, res.flags.to) } else if (res.flags.role == 'validator') { tx = await lockedGold.authorizeValidator(res.flags.from, res.flags.to) + } else { + this.error(`Invalid role provided`) + return } await displaySendTx('authorizeTx', tx) } diff --git a/packages/cli/src/commands/lockedgold/lock.ts b/packages/cli/src/commands/lockedgold/lock.ts index ce086359e7e..4961d382cbd 100644 --- a/packages/cli/src/commands/lockedgold/lock.ts +++ b/packages/cli/src/commands/lockedgold/lock.ts @@ -12,13 +12,13 @@ export default class Lock extends BaseCommand { static flags = { ...BaseCommand.flags, from: flags.string({ ...Flags.address, required: true }), - goldAmount: flags.string({ ...LockedGoldArgs.goldAmountArg, required: true }), + value: flags.string({ ...LockedGoldArgs.valueArg, required: true }), } static args = [] static examples = [ - 'lock --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --goldAmount 1000000000000000000', + 'lock --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --value 1000000000000000000', ] async run() { @@ -28,13 +28,13 @@ export default class Lock extends BaseCommand { this.kit.defaultAccount = address const lockedGold = await this.kit.contracts.getLockedGold() - const goldAmount = new BigNumber(res.flags.goldAmount) + const value = new BigNumber(res.flags.value) - if (!goldAmount.gt(new BigNumber(0))) { - failWith(`require(goldAmount > 0) => [${goldAmount}]`) + if (!value.gt(new BigNumber(0))) { + failWith(`Provided value must be greater than zero => [${value}]`) } const tx = lockedGold.lock() - await displaySendTx('lock', tx, { value: goldAmount.toString() }) + await displaySendTx('lock', tx, { value: value.toString() }) } } diff --git a/packages/cli/src/commands/lockedgold/show.ts b/packages/cli/src/commands/lockedgold/show.ts index a249a0f0172..712d5760f52 100644 --- a/packages/cli/src/commands/lockedgold/show.ts +++ b/packages/cli/src/commands/lockedgold/show.ts @@ -4,7 +4,7 @@ import { Args } from '../../utils/command' import { eqAddress } from '@celo/utils/lib/address' export default class Show extends BaseCommand { - static description = 'Show locked gold information for a given account' + static description = 'Show Locked Gold information for a given account' static flags = { ...BaseCommand.flags, @@ -19,22 +19,6 @@ export default class Show extends BaseCommand { const { args } = this.parse(Show) const lockedGold = await this.kit.contracts.getLockedGold() - const nonvoting = (await lockedGold.getAccountNonvotingLockedGold(args.account)).toString() - const total = (await lockedGold.getAccountTotalLockedGold(args.account)).toString() - const voter = await lockedGold.getVoterFromAccount(args.account) - const validator = await lockedGold.getValidatorFromAccount(args.account) - const pendingWithdrawals = await lockedGold.getPendingWithdrawals(args.account) - const info = { - lockedGold: { - total, - nonvoting, - }, - authorizations: { - voter: eqAddress(voter, args.account) ? 'None' : voter, - validator: eqAddress(validator, args.account) ? 'None' : validator, - }, - pendingWithdrawals: pendingWithdrawals.length > 0 ? pendingWithdrawals : '[]', - } - printValueMapRecursive(info) + printValueMapRecursive(await lockedGold.geAccountSummary(args.account)) } } diff --git a/packages/cli/src/commands/lockedgold/unlock.ts b/packages/cli/src/commands/lockedgold/unlock.ts index 23d535fdf20..1b04e9980c1 100644 --- a/packages/cli/src/commands/lockedgold/unlock.ts +++ b/packages/cli/src/commands/lockedgold/unlock.ts @@ -10,17 +10,17 @@ export default class Unlock extends BaseCommand { static flags = { ...BaseCommand.flags, from: Flags.address({ required: true }), - goldAmount: flags.string({ ...LockedGoldArgs.goldAmountArg, required: true }), + value: flags.string({ ...LockedGoldArgs.valueArg, required: true }), } static args = [] - static examples = ['unlock --goldAmount 500000000'] + static examples = ['unlock --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --value 500000000'] async run() { const res = this.parse(Unlock) this.kit.defaultAccount = res.flags.from const lockedgold = await this.kit.contracts.getLockedGold() - await displaySendTx('unlock', lockedgold.unlock(res.flags.goldAmount)) + await displaySendTx('unlock', lockedgold.unlock(res.flags.value)) } } diff --git a/packages/cli/src/commands/lockedgold/withdraw.ts b/packages/cli/src/commands/lockedgold/withdraw.ts index cbeb0288890..067f24febbd 100644 --- a/packages/cli/src/commands/lockedgold/withdraw.ts +++ b/packages/cli/src/commands/lockedgold/withdraw.ts @@ -17,14 +17,29 @@ export default class Withdraw extends BaseCommand { const { flags } = this.parse(Withdraw) this.kit.defaultAccount = flags.from const lockedgold = await this.kit.contracts.getLockedGold() - const pendingWithdrawals = await lockedgold.getPendingWithdrawals(flags.from) const currentTime = Math.round(new Date().getTime() / 1000) - let withdrawals = 0 - for (let i = 0; i < pendingWithdrawals.length; i++) { - if (pendingWithdrawals[i].time.isLessThan(currentTime)) { - await displaySendTx('withdraw', lockedgold.withdraw(i - withdrawals)) - withdrawals += 1 + + while (true) { + let madeWithdrawal = false + const pendingWithdrawals = await lockedgold.getPendingWithdrawals(flags.from) + for (const pendingWithdrawal of pendingWithdrawals) { + if (pendingWithdrawal.time.isLessThan(currentTime)) { + console.log( + `Found available pending withdrawal of value ${pendingWithdrawal.value.toString()}, withdrawing` + ) + await displaySendTx('withdraw', lockedgold.withdraw(i)) + madeWithdrawal = true + break + } } } + const pendingWithdrawals = await lockedgold.getPendingWithdrawals(flags.from) + for (const pendingWithdrawal of pendingWithdrawals) { + console.log( + `Pending withdrawal of value ${pendingWithdrawal.value.toString()} available for withdrawal in ${pendingWithdrawal.time + .minus(currentTime) + .toString()} seconds.` + ) + } } } diff --git a/packages/cli/src/commands/validatorgroup/member.ts b/packages/cli/src/commands/validatorgroup/member.ts index f09291f42d5..86d126c790d 100644 --- a/packages/cli/src/commands/validatorgroup/member.ts +++ b/packages/cli/src/commands/validatorgroup/member.ts @@ -4,7 +4,7 @@ import { BaseCommand } from '../../base' import { displaySendTx } from '../../utils/cli' import { Args, Flags } from '../../utils/command' -export default class ValidatorGroupRegister extends BaseCommand { +export default class ValidatorGroupMembers extends BaseCommand { static description = 'Add or remove members from a Validator Group' static flags = { @@ -28,7 +28,7 @@ export default class ValidatorGroupRegister extends BaseCommand { ] async run() { - const res = this.parse(ValidatorGroupRegister) + const res = this.parse(ValidatorGroupMembers) if (!(res.flags.accept || res.flags.remove)) { this.error(`Specify action: --accept or --remove`) @@ -41,7 +41,7 @@ export default class ValidatorGroupRegister extends BaseCommand { if (res.flags.accept) { await displaySendTx('addMember', validators.addMember((res.args as any).validatorAddress)) - if ((await validators.getGroupNumMembers(res.flags.from)) === '1') { + if ((await validators.getGroupNumMembers(res.flags.from)).isEqualTo(1)) { const tx = await election.markGroupEligible(res.flags.from) await displaySendTx('markGroupEligible', tx) } diff --git a/packages/cli/src/commands/validatorgroup/register.ts b/packages/cli/src/commands/validatorgroup/register.ts index d3645c7fb6a..a805fa40d3d 100644 --- a/packages/cli/src/commands/validatorgroup/register.ts +++ b/packages/cli/src/commands/validatorgroup/register.ts @@ -3,7 +3,6 @@ import { flags } from '@oclif/command' import { BaseCommand } from '../../base' import { displaySendTx } from '../../utils/cli' import { Flags } from '../../utils/command' -import { toFixed } from '@celo/utils/lib/fixidity' export default class ValidatorGroupRegister extends BaseCommand { static description = 'Register a new Validator Group' @@ -25,11 +24,13 @@ export default class ValidatorGroupRegister extends BaseCommand { this.kit.defaultAccount = res.flags.from const validators = await this.kit.contracts.getValidators() - const commission = toFixed(new BigNumber(res.flags.commission)).toFixed() - await displaySendTx( 'registerValidatorGroup', - validators.registerValidatorGroup(res.flags.name, res.flags.url, commission) + validators.registerValidatorGroup( + res.flags.name, + res.flags.url, + new BigNumber(res.flags.commission) + ) ) } } diff --git a/packages/cli/src/utils/lockedgold.ts b/packages/cli/src/utils/lockedgold.ts index 64120283e25..9e9aee7645e 100644 --- a/packages/cli/src/utils/lockedgold.ts +++ b/packages/cli/src/utils/lockedgold.ts @@ -1,10 +1,10 @@ export const LockedGoldArgs = { pendingWithdrawalIndexArg: { - name: 'pendingWithdrawalINdex', + name: 'pendingWithdrawalIndex', description: 'index of pending withdrawal whose unlocking period has passed', }, - goldAmountArg: { - name: 'goldAmount', - description: 'unit amount of gold token (cGLD)', + valueArg: { + name: 'value', + description: 'unit amount of Celo Gold (cGLD)', }, } diff --git a/packages/contractkit/src/wrappers/Election.ts b/packages/contractkit/src/wrappers/Election.ts index f8a9a875dbf..240edae9884 100644 --- a/packages/contractkit/src/wrappers/Election.ts +++ b/packages/contractkit/src/wrappers/Election.ts @@ -15,16 +15,14 @@ import { export interface Validator { address: Address - id: string name: string url: string publicKey: string - affiliation: string | null + affiliation: Address | null } export interface ValidatorGroup { address: Address - id: string name: string url: string members: Address[] @@ -118,17 +116,19 @@ export class ElectionWrapper extends BaseWrapper { async getValidatorGroupsVotes(): Promise { const validators = await this.kit.contracts.getValidators() - const vgAddresses = (await validators.getRegisteredValidatorGroups()).map((g) => g.address) - const vgVotes = await Promise.all( - vgAddresses.map((g) => this.contract.methods.getGroupTotalVotes(g).call()) + const validatorGroupAddresses = (await validators.getRegisteredValidatorGroups()).map( + (g) => g.address ) - const vgEligible = await Promise.all( - vgAddresses.map((g) => this.contract.methods.getGroupEligibility(g).call()) + const validatorGroupVotes = await Promise.all( + validatorGroupAddresses.map((g) => this.contract.methods.getGroupTotalVotes(g).call()) ) - return vgAddresses.map((a, i) => ({ + const validatorGroupEligible = await Promise.all( + validatorGroupAddresses.map((g) => this.contract.methods.getGroupEligibility(g).call()) + ) + return validatorGroupAddresses.map((a, i) => ({ address: a, - votes: toBigNumber(vgVotes[i]), - eligible: vgEligible[i], + votes: toBigNumber(validatorGroupVotes[i]), + eligible: validatorGroupEligible[i], })) } @@ -137,29 +137,6 @@ export class ElectionWrapper extends BaseWrapper { return zip((a, b) => ({ address: a, votes: new BigNumber(b), eligible: true }), res[0], res[1]) } - /* - async revokeVote(): Promise> { - if (this.kit.defaultAccount == null) { - throw new Error(`missing from at new ValdidatorUtils()`) - } - - const lockedGold = await this.kit.contracts.getLockedGold() - const votingDetails = await lockedGold.getVotingDetails(this.kit.defaultAccount) - const votedGroup = await this.getVoteFrom(votingDetails.accountAddress) - - if (votedGroup == null) { - throw new Error(`Not current vote for ${this.kit.defaultAccount}`) - } - - const { lesser, greater } = await this.findLesserAndGreaterAfterVote( - votedGroup, - votingDetails.weight.negated() - ) - - return wrapSend(this.kit, this.contract.methods.revokeVote(lesser, greater)) - } - */ - async markGroupEligible(validatorGroup: Address): Promise> { if (this.kit.defaultAccount == null) { throw new Error(`missing from at new ValdidatorUtils()`) @@ -187,13 +164,13 @@ export class ElectionWrapper extends BaseWrapper { ) } - private async findLesserAndGreaterAfterVote( + async findLesserAndGreaterAfterVote( votedGroup: Address, voteWeight: BigNumber ): Promise<{ lesser: Address; greater: Address }> { const currentVotes = await this.getEligibleValidatorGroupsVotes() - const selectedGroup = currentVotes.find((cv) => eqAddress(cv.address, votedGroup)) + const selectedGroup = currentVotes.find((votes) => eqAddress(votes.address, votedGroup)) // modify the list if (selectedGroup) { @@ -210,7 +187,7 @@ export class ElectionWrapper extends BaseWrapper { currentVotes.sort((a, b) => a.votes.comparedTo(b.votes)) // find new index - const newIdx = currentVotes.findIndex((cv) => eqAddress(cv.address, votedGroup)) + const newIdx = currentVotes.findIndex((votes) => eqAddress(votes.address, votedGroup)) return { lesser: newIdx === 0 ? NULL_ADDRESS : currentVotes[newIdx - 1].address, diff --git a/packages/contractkit/src/wrappers/LockedGold.ts b/packages/contractkit/src/wrappers/LockedGold.ts index c8f7dfbc29d..740efa111d1 100644 --- a/packages/contractkit/src/wrappers/LockedGold.ts +++ b/packages/contractkit/src/wrappers/LockedGold.ts @@ -1,3 +1,4 @@ +import { eqAddress } from '@celo/utils/lib/address' import { zip } from '@celo/utils/lib/collections' import BigNumber from 'bignumber.js' import Web3 from 'web3' @@ -19,6 +20,18 @@ export interface VotingDetails { weight: BigNumber } +interface AccountSummary { + lockedGold: { + total: BigNumber + nonvoting: BigNumber + } + authorizations: { + voter: string + validator: string + } + pendingWithdrawals: PendingWithdrawal[] +} + interface PendingWithdrawal { time: BigNumber value: BigNumber @@ -64,6 +77,25 @@ export class LockedGoldWrapper extends BaseWrapper { } } + async getAccountSummary(account: string): Promise { + const nonvoting = await this.getAccountNonvotingLockedGold(args.account) + const total = await this.getAccountTotalLockedGold(args.account) + const voter = await this.getVoterFromAccount(args.account) + const validator = await this.getValidatorFromAccount(args.account) + const pendingWithdrawals = await this.getPendingWithdrawals(args.account) + return (info = { + lockedGold: { + total, + nonvoting, + }, + authorizations: { + voter: eqAddress(voter, account) ? 'None' : voter, + validator: eqAddress(validator, account) ? 'None' : validator, + }, + pendingWithdrawals, + }) + } + /** * Authorize voting on behalf of this account to another address. * @param account Address of the active account. @@ -72,6 +104,7 @@ export class LockedGoldWrapper extends BaseWrapper { */ async authorizeVoter(account: Address, voter: Address): Promise> { const sig = await this.getParsedSignatureOfAddress(account, voter) + // TODO(asa): Pass default tx "from" argument. return wrapSend(this.kit, this.contract.methods.authorizeVoter(voter, sig.v, sig.r, sig.s)) } @@ -95,8 +128,8 @@ export class LockedGoldWrapper extends BaseWrapper { async getPendingWithdrawals(account: string) { const withdrawals = await this.contract.methods.getPendingWithdrawals(account).call() return zip( - // tslint:disable-next-line: no-object-literal-type-assertion (time, value) => + // tslint:disable-next-line: no-object-literal-type-assertion ({ time: toBigNumber(time), value: toBigNumber(value) } as PendingWithdrawal), withdrawals[1], withdrawals[0] diff --git a/packages/contractkit/src/wrappers/Validators.ts b/packages/contractkit/src/wrappers/Validators.ts index 4103279bc8f..51c0d410caf 100644 --- a/packages/contractkit/src/wrappers/Validators.ts +++ b/packages/contractkit/src/wrappers/Validators.ts @@ -2,7 +2,7 @@ import BigNumber from 'bignumber.js' import { Address } from '../base' import { Validators } from '../generated/types/Validators' import { BaseWrapper, proxyCall, proxySend, toBigNumber } from './BaseWrapper' -import { fromFixed } from '@celo/utils/lib/fixidity' +import { fromFixed, toFixed } from '@celo/utils/lib/fixidity' export interface Validator { address: Address @@ -46,6 +46,20 @@ export class ValidatorsWrapper extends BaseWrapper { removeMember = proxySend(this.kit, this.contract.methods.removeMember) registerValidator = proxySend(this.kit, this.contract.methods.registerValidator) registerValidatorGroup = proxySend(this.kit, this.contract.methods.registerValidatorGroup) + + async registerValidatorGroup( + name: string, + url: string, + commission: BigNumber + ): Promise> { + if (this.kit.defaultAccount == null) { + throw new Error(`missing from at new ValdidatorUtils()`) + } + return wrapSend( + this.kit, + this.contract.methods.registerValidatorGroup(name, url, toFixed(commission)) + ) + } /** * Returns the current registration requirements. * @returns Group and validator registration requirements. @@ -88,8 +102,10 @@ export class ValidatorsWrapper extends BaseWrapper { return Promise.all(vgAddresses.map((addr) => this.getValidator(addr))) } - getGroupNumMembers: (group: Address) => Promise = proxyCall( - this.contract.methods.getGroupNumMembers + getGroupNumMembers: (group: Address) => Promise = proxyCall( + this.contract.methods.getGroupNumMembers, + undefined, + toBigNumber ) async getValidator(address: Address): Promise { diff --git a/packages/protocol/contracts/common/UsingRegistry.sol b/packages/protocol/contracts/common/UsingRegistry.sol index aa742dedcf5..a0dbf5ce45b 100644 --- a/packages/protocol/contracts/common/UsingRegistry.sol +++ b/packages/protocol/contracts/common/UsingRegistry.sol @@ -54,19 +54,19 @@ contract UsingRegistry is Ownable { return IElection(registry.getAddressForOrDie(ELECTION_REGISTRY_ID)); } - function getGoldToken() internal view returns(IERC20Token) { + function getGoldToken() internal view returns (IERC20Token) { return IERC20Token(registry.getAddressForOrDie(GOLD_TOKEN_REGISTRY_ID)); } - function getLockedGold() internal view returns(ILockedGold) { + function getLockedGold() internal view returns (ILockedGold) { return ILockedGold(registry.getAddressForOrDie(LOCKED_GOLD_REGISTRY_ID)); } - function getRandom() internal view returns(IRandom) { + function getRandom() internal view returns (IRandom) { return IRandom(registry.getAddressForOrDie(RANDOM_REGISTRY_ID)); } - function getValidators() internal view returns(IValidators) { + function getValidators() internal view returns (IValidators) { return IValidators(registry.getAddressForOrDie(VALIDATORS_REGISTRY_ID)); } } diff --git a/packages/protocol/contracts/common/linkedlists/AddressLinkedList.sol b/packages/protocol/contracts/common/linkedlists/AddressLinkedList.sol index aea287c5114..c0ee2fa07f7 100644 --- a/packages/protocol/contracts/common/linkedlists/AddressLinkedList.sol +++ b/packages/protocol/contracts/common/linkedlists/AddressLinkedList.sol @@ -82,6 +82,7 @@ library AddressLinkedList { * @notice Returns the N greatest elements of the list. * @param n The number of elements to return. * @return The keys of the greatest elements. + * @dev Reverts if n is greater than the number of elements in the list. */ function headN(LinkedList.List storage list, uint256 n) public view returns (address[] memory) { bytes32[] memory byteKeys = list.headN(n); diff --git a/packages/protocol/contracts/common/linkedlists/LinkedList.sol b/packages/protocol/contracts/common/linkedlists/LinkedList.sol index e1e514668dc..e3e80bdfc1f 100644 --- a/packages/protocol/contracts/common/linkedlists/LinkedList.sol +++ b/packages/protocol/contracts/common/linkedlists/LinkedList.sol @@ -153,6 +153,7 @@ library LinkedList { * @notice Returns the keys of the N elements at the head of the list. * @param n The number of elements to return. * @return The keys of the N elements at the head of the list. + * @dev Reverts if n is greater than the number of elements in the list. */ function headN(List storage list, uint256 n) public view returns (bytes32[] memory) { require(n <= list.numElements); diff --git a/packages/protocol/contracts/common/linkedlists/SortedLinkedList.sol b/packages/protocol/contracts/common/linkedlists/SortedLinkedList.sol index a8151ed5f49..9489f00802c 100644 --- a/packages/protocol/contracts/common/linkedlists/SortedLinkedList.sol +++ b/packages/protocol/contracts/common/linkedlists/SortedLinkedList.sol @@ -148,6 +148,7 @@ library SortedLinkedList { * @notice Returns first N greatest elements of the list. * @param n The number of elements to return. * @return The keys of the first n elements. + * @dev Reverts if n is greater than the number of elements in the list. */ function headN(List storage list, uint256 n) public view returns (bytes32[] memory) { return list.list.headN(n); diff --git a/packages/protocol/contracts/governance/Election.sol b/packages/protocol/contracts/governance/Election.sol index d5124b5d679..7532cea72e6 100644 --- a/packages/protocol/contracts/governance/Election.sol +++ b/packages/protocol/contracts/governance/Election.sol @@ -18,34 +18,51 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { using FixidityLib for FixidityLib.Fraction; using SafeMath for uint256; + struct TimestampedVote { + // The value of the vote, in gold. + uint256 value; + // The latest block number at which the vote was cast. + uint256 blockNumber; + } + + struct GroupPendingVotes { + // The total number of pending votes that have been cast for this group. + uint256 total; + // Pending votes cast per voter. + mapping(address => TimestampedVote) byAccount; + } + // Pending votes are those for which no following elections have been held. // These votes have yet to contribute to the election of validators and thus do not accrue // rewards. struct PendingVotes { - // Maps groups to total pending voting balance. - mapping(address => uint256) total; - // Maps groups to accounts to pending voting balance. - mapping(address => mapping(address => uint256)) balances; - // Maps groups to accounts to timestamp of the account's most recent vote for the group. - mapping(address => mapping(address => uint256)) timestamps; + // The total number of pending votes cast across all groups. + uint256 total; + mapping(address => GroupPendingVotes) forGroup; + } + + struct GroupActiveVotes { + // The total number of active votes that have been cast for this group. + uint256 total; + // The total number of active votes by a voter is equal to the number of active vote units for + // that voter times the total number of active votes divided by the total number of active + // vote units. + uint256 totalUnits; + mapping(address => uint256) unitsByAccount; } // Active votes are those for which at least one following election has been held. // These votes have contributed to the election of validators and thus accrue rewards. struct ActiveVotes { - // Maps groups to total active voting balance. - mapping(address => uint256) total; - // Maps groups to accounts to the numerator of the account's fraction of the group's - // total active votes. - mapping(address => mapping(address => uint256)) numerators; - // Maps groups to the denominator of all accounts' fraction of the group's total active votes. - mapping(address => uint256) denominators; + // The total number of active votes cast across all groups. + uint256 total; + mapping(address => GroupActiveVotes) forGroup; } struct TotalVotes { - // The total number of votes cast. - uint256 total; - // A list of eligible ValidatorGroups sorted by total votes. + // A list of eligible ValidatorGroups sorted by total (pending+active) votes. + // Note that this list will omit ineligible ValidatorGroups, including those that may have > 0 + // total votes. SortedLinkedList.List eligible; } @@ -57,18 +74,24 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { mapping(address => address[]) groupsVotedFor; } + struct ElectableValidators { + uint256 min; + uint256 max; + } + Votes private votes; - uint256 public minElectableValidators; - uint256 public maxElectableValidators; + // Governs the minimum and maximum number of validators that can be elected. + ElectableValidators public electableValidators; + // Governs how many validator groups a single account can vote for. uint256 public maxNumGroupsVotedFor; + // Groups must receive at least this fraction of the total votes in order to be considered in + // elections. + // TODO(asa): Implement this constraint. FixidityLib.Fraction public electabilityThreshold; - event MinElectableValidatorsSet( - uint256 minElectableValidators - ); - - event MaxElectableValidatorsSet( - uint256 maxElectableValidators + event ElectableValidatorsSet( + uint256 min, + uint256 max ); event MaxNumGroupsVotedForSet( @@ -108,7 +131,7 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { /** * @notice Initializes critical variables. * @param registryAddress The address of the registry contract. - * @param _minElectableValidators The minimum number of validators that can be elected. + * @param minElectableValidators The minimum number of validators that can be elected. * @param _maxNumGroupsVotedFor The maximum number of groups that an acconut can vote for at once. * @param _electabilityThreshold The minimum ratio of votes a group needs before its members can * be elected. @@ -116,63 +139,46 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { */ function initialize( address registryAddress, - uint256 _minElectableValidators, - uint256 _maxElectableValidators, + uint256 minElectableValidators, + uint256 maxElectableValidators, uint256 _maxNumGroupsVotedFor, uint256 _electabilityThreshold ) external initializer { - require(_minElectableValidators > 0 && _maxElectableValidators >= _minElectableValidators); _transferOwnership(msg.sender); setRegistry(registryAddress); - minElectableValidators = _minElectableValidators; - maxElectableValidators = _maxElectableValidators; - maxNumGroupsVotedFor = _maxNumGroupsVotedFor; - electabilityThreshold = FixidityLib.wrap(_electabilityThreshold); + _setElectableValidators(minElectableValidators, maxElectableValidators); + _setMaxNumGroupsVotedFor(_maxNumGroupsVotedFor); + _setElectabilityThreshold(_electabilityThreshold); } /** - * @notice Updates the minimum number of validators that can be elected. - * @param _minElectableValidators The minimum number of validators that can be elected. + * @notice Updates the minimum and maximum number of validators that can be elected. + * @param min The minimum number of validators that can be elected. + * @param max The maximum number of validators that can be elected. * @return True upon success. */ - function setMinElectableValidators( - uint256 _minElectableValidators - ) - external - onlyOwner - returns (bool) - { - require( - _minElectableValidators > 0 && - _minElectableValidators != minElectableValidators && - _minElectableValidators <= maxElectableValidators - ); - minElectableValidators = _minElectableValidators; - emit MinElectableValidatorsSet(_minElectableValidators); - return true; + function setElectableValidators(uint256 min, uint256 max) external onlyOwner returns (bool) { + return _setElectableValidators(min, max); + } + + function getElectableValidators() external view returns (uint256, uint256) { + return (electableValidators.min, electableValidators.max); } /** - * @notice Updates the maximum number of validators that can be elected. - * @param _maxElectableValidators The maximum number of validators that can be elected. + * @notice Updates the minimum and maximum number of validators that can be elected. + * @param min The minimum number of validators that can be elected. + * @param max The maximum number of validators that can be elected. * @return True upon success. */ - function setMaxElectableValidators( - uint256 _maxElectableValidators - ) - external - onlyOwner - returns (bool) - { - require( - _maxElectableValidators != maxElectableValidators && - _maxElectableValidators >= minElectableValidators - ); - maxElectableValidators = _maxElectableValidators; - emit MaxElectableValidatorsSet(_maxElectableValidators); + function _setElectableValidators(uint256 min, uint256 max) private returns (bool) { + require(0 < min && min <= max); + require(min != electableValidators.min || max != electableValidators.max); + electableValidators = ElectableValidators(min, max); + emit ElectableValidatorsSet(min, max); return true; } @@ -188,6 +194,15 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { onlyOwner returns (bool) { + return _setMaxNumGroupsVotedFor(_maxNumGroupsVotedFor); + } + + /** + * @notice Updates the maximum number of groups an account can be voting for at once. + * @param _maxNumGroupsVotedFor The maximum number of groups an account can vote for. + * @return True upon success. + */ + function _setMaxNumGroupsVotedFor(uint256 _maxNumGroupsVotedFor) private returns (bool) { require(_maxNumGroupsVotedFor != maxNumGroupsVotedFor); maxNumGroupsVotedFor = _maxNumGroupsVotedFor; emit MaxNumGroupsVotedForSet(_maxNumGroupsVotedFor); @@ -199,13 +214,16 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { * @param threshold Electability threshold as unwrapped Fraction. * @return True upon success. */ - function setElectabilityThreshold( - uint256 threshold - ) - public - onlyOwner - returns (bool) - { + function setElectabilityThreshold(uint256 threshold) public onlyOwner returns (bool) { + return _setElectabilityThreshold(threshold); + } + + /** + * @notice Sets the electability threshold. + * @param threshold Electability threshold as unwrapped Fraction. + * @return True upon success. + */ + function _setElectabilityThreshold(uint256 threshold) private returns (bool) { electabilityThreshold = FixidityLib.wrap(threshold); require( electabilityThreshold.lt(FixidityLib.fixed1()), @@ -245,13 +263,17 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { returns (bool) { require(votes.total.eligible.contains(group)); - require(0 < value && value <= getNumVotesReceivable(group)); + require(0 < value); + require(canReceiveVotes(group, value)); address account = getLockedGold().getAccountFromVoter(msg.sender); + + // Add group to the groups voted for by the account. address[] storage groups = votes.groupsVotedFor[account]; require(groups.length < maxNumGroupsVotedFor); for (uint256 i = 0; i < groups.length; i = i.add(1)) { require(groups[i] != group); } + groups.push(group); incrementPendingVotes(group, account, value); incrementTotalVotes(group, value, lesser, greater); @@ -264,11 +286,13 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { * @notice Converts `account`'s pending votes for `group` to active votes. * @param group The validator group to vote for. * @return True upon success. + * @dev Pending votes cannot be activated until an election has been held. */ + // TODO(asa): Prevent users from activating pending votes until an election has been held. function activate(address group) external nonReentrant returns (bool) { address account = getLockedGold().getAccountFromVoter(msg.sender); PendingVotes storage pending = votes.pending; - uint256 value = pending.balances[group][account]; + uint256 value = pending.forGroup[group].byAccount[account].value; require(value > 0); decrementPendingVotes(group, account, value); incrementActiveVotes(group, account, value); @@ -300,11 +324,11 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { { require(group != address(0)); address account = getLockedGold().getAccountFromVoter(msg.sender); - require(0 < value && value <= getAccountPendingVotesForGroup(group, account)); + require(0 < value && value <= getPendingVotesForGroupByAccount(group, account)); decrementPendingVotes(group, account, value); decrementTotalVotes(group, value, lesser, greater); getLockedGold().incrementNonvotingAccountBalance(account, value); - if (getAccountTotalVotesForGroup(group, account) == 0) { + if (getTotalVotesForGroupByAccount(group, account) == 0) { deleteElement(votes.groupsVotedFor[account], group, index); } emit ValidatorGroupVoteRevoked(account, group, value); @@ -334,44 +358,36 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { nonReentrant returns (bool) { + // TODO(asa): Dedup with revokePending. require(group != address(0)); address account = getLockedGold().getAccountFromVoter(msg.sender); - require(0 < value && value <= getAccountActiveVotesForGroup(group, account)); + require(0 < value && value <= getActiveVotesForGroupByAccount(group, account)); decrementActiveVotes(group, account, value); decrementTotalVotes(group, value, lesser, greater); getLockedGold().incrementNonvotingAccountBalance(account, value); - if (getAccountTotalVotesForGroup(group, account) == 0) { + if (getTotalVotesForGroupByAccount(group, account) == 0) { deleteElement(votes.groupsVotedFor[account], group, index); } emit ValidatorGroupVoteRevoked(account, group, value); return true; } - function getAccountTotalVotes(address account) external view returns (uint256) { + function getTotalVotesByAccount(address account) external view returns (uint256) { uint256 total = 0; address[] memory groups = votes.groupsVotedFor[account]; for (uint256 i = 0; i < groups.length; i = i.add(1)) { - total = total.add(getAccountTotalVotesForGroup(groups[i], account)); + total = total.add(getTotalVotesForGroupByAccount(groups[i], account)); } return total; } - /** - * @notice Returns the groups voted for by a particular account. - * @param account The address of the account. - * @return The groups voted for by a particular account. - */ - function getAccountGroupsVotedFor(address account) external view returns (address[] memory) { - return votes.groupsVotedFor[account]; - } - /** * @notice Returns the pending votes for `group` made by `account`. * @param group The address of the validator group. * @param account The address of the voting account. * @return The pending votes for `group` made by `account`. */ - function getAccountPendingVotesForGroup( + function getPendingVotesForGroupByAccount( address group, address account ) @@ -379,7 +395,7 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { view returns (uint256) { - return votes.pending.balances[group][account]; + return votes.pending.forGroup[group].byAccount[account].value; } /** @@ -388,7 +404,7 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { * @param account The address of the voting account. * @return The active votes for `group` made by `account`. */ - function getAccountActiveVotesForGroup( + function getActiveVotesForGroupByAccount( address group, address account ) @@ -396,11 +412,12 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { view returns (uint256) { - uint256 numerator = votes.active.numerators[group][account].mul(votes.active.total[group]); + GroupActiveVotes storage groupActiveVotes = votes.active.forGroup[group]; + uint256 numerator = groupActiveVotes.unitsByAccount[account].mul(groupActiveVotes.total); if (numerator == 0) { return 0; } - uint256 denominator = votes.active.denominators[group]; + uint256 denominator = groupActiveVotes.totalUnits; return numerator.div(denominator); } @@ -410,7 +427,7 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { * @param account The address of the voting account. * @return The total votes for `group` made by `account`. */ - function getAccountTotalVotesForGroup( + function getTotalVotesForGroupByAccount( address group, address account ) @@ -418,8 +435,8 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { view returns (uint256) { - uint256 pending = getAccountPendingVotesForGroup(group, account); - uint256 active = getAccountActiveVotesForGroup(group, account); + uint256 pending = getPendingVotesForGroupByAccount(group, account); + uint256 active = getActiveVotesForGroupByAccount(group, account); return pending.add(active); } @@ -428,8 +445,8 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { * @param group The address of the validator group. * @return The total votes made for `group`. */ - function getGroupTotalVotes(address group) external view returns (uint256) { - return votes.pending.total[group].add(votes.active.total[group]); + function getTotalVotesForGroup(address group) public view returns (uint256) { + return votes.pending.forGroup[group].total.add(votes.active.forGroup[group].total); } function getGroupEligibility(address group) external view returns (bool) { @@ -453,10 +470,8 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { ) private { - require(votes.total.eligible.contains(group)); uint256 newVoteTotal = votes.total.eligible.getValue(group).add(value); votes.total.eligible.update(group, newVoteTotal, lesser, greater); - votes.total.total = votes.total.total.add(value); } /** @@ -480,7 +495,6 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { uint256 newVoteTotal = votes.total.eligible.getValue(group).sub(value); votes.total.eligible.update(group, newVoteTotal, lesser, greater); } - votes.total.total = votes.total.total.sub(value); } /** @@ -500,21 +514,21 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { /** * @notice Marks a group eligible for electing validators. - * @param group The address of the validator group. * @param lesser The address of the group that has received fewer votes than this group. * @param greater The address of the group that has received more votes than this group. */ function markGroupEligible( - address group, address lesser, address greater ) external + nonReentrant returns (bool) { + address group = getLockedGold().getAccountFromValidator(msg.sender); require(!votes.total.eligible.contains(group)); require(getValidators().getGroupNumMembers(group) > 0); - uint256 value = votes.pending.total[group].add(votes.active.total[group]); + uint256 value = getTotalVotesForGroup(group); votes.total.eligible.insert(group, value, lesser, greater); emit ValidatorGroupMarkedEligible(group); return true; @@ -528,9 +542,14 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { */ function incrementPendingVotes(address group, address account, uint256 value) private { PendingVotes storage pending = votes.pending; - pending.balances[group][account] = pending.balances[group][account].add(value); - pending.timestamps[group][account] = now; - pending.total[group] = pending.total[group].add(value); + pending.total = pending.total.add(value); + + GroupPendingVotes storage groupPending = pending.forGroup[group]; + groupPending.total = groupPending.total.add(value); + + TimestampedVote storage timestampedVote = groupPending.byAccount[account]; + timestampedVote.value = timestampedVote.value.add(value); + timestampedVote.blockNumber = block.number; } /** @@ -541,12 +560,16 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { */ function decrementPendingVotes(address group, address account, uint256 value) private { PendingVotes storage pending = votes.pending; - uint256 newValue = pending.balances[group][account].sub(value); - pending.balances[group][account] = newValue; - if (newValue == 0) { - pending.timestamps[group][account] = 0; + pending.total = pending.total.sub(value); + + GroupPendingVotes storage groupPending = pending.forGroup[group]; + groupPending.total = groupPending.total.sub(value); + + TimestampedVote storage timestampedVote = groupPending.byAccount[account]; + timestampedVote.value = timestampedVote.value.sub(value); + if (timestampedVote.value == 0) { + timestampedVote.blockNumber = 0; } - pending.total[group] = pending.total[group].sub(value); } /** @@ -556,11 +579,16 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { * @param value The number of votes. */ function incrementActiveVotes(address group, address account, uint256 value) private { - uint256 delta = getActiveVotesDelta(group, value); ActiveVotes storage active = votes.active; - active.numerators[group][account] = active.numerators[group][account].add(delta); - active.denominators[group] = active.denominators[group].add(delta); - active.total[group] = active.total[group].add(value); + active.total = active.total.add(value); + + uint256 unitsDelta = getActiveVotesUnitsDelta(group, value); + + GroupActiveVotes storage groupActive = active.forGroup[group]; + groupActive.total = groupActive.total.add(value); + + groupActive.totalUnits = groupActive.totalUnits.add(unitsDelta); + groupActive.unitsByAccount[account] = groupActive.unitsByAccount[account].add(unitsDelta); } /** @@ -570,27 +598,37 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { * @param value The number of votes. */ function decrementActiveVotes(address group, address account, uint256 value) private { - uint256 delta = getActiveVotesDelta(group, value); ActiveVotes storage active = votes.active; - active.numerators[group][account] = active.numerators[group][account].sub(delta); - active.denominators[group] = active.denominators[group].sub(delta); - active.total[group] = active.total[group].sub(value); + active.total = active.total.sub(value); + + uint256 unitsDelta = getActiveVotesUnitsDelta(group, value); + + GroupActiveVotes storage groupActive = active.forGroup[group]; + groupActive.total = groupActive.total.sub(value); + + groupActive.totalUnits = groupActive.totalUnits.sub(unitsDelta); + groupActive.unitsByAccount[account] = groupActive.unitsByAccount[account].sub(unitsDelta); } /** * @notice Returns the delta in active vote denominator for `group`. * @param group The address of the validator group. - * @param value The numebr of active votes being added. + * @param value The number of active votes being added. * @return The delta in active vote denominator for `group`. */ - function getActiveVotesDelta(address group, uint256 value) private view returns (uint256) { - // Preserve delta * total = value * denominator - return value.mul(votes.active.denominators[group].add(1)).div( - votes.active.total[group].add(1) + function getActiveVotesUnitsDelta(address group, uint256 value) private view returns (uint256) { + // Preserve unitsDelta * total = value * totalUnits + return value.mul(votes.active.forGroup[group].totalUnits.add(1)).div( + votes.active.forGroup[group].total.add(1) ); } - function getGroupsVotedFor(address account) external view returns (address[] memory) { + /** + * @notice Returns the groups that `account` has voted for. + * @param account The address of the account casting votes. + * @return The groups that `account` has voted for. + */ + function getGroupsVotedForByAccount(address account) external view returns (address[] memory) { return votes.groupsVotedFor[account]; } @@ -601,23 +639,47 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { * @param index The index of `element` in the list. */ function deleteElement(address[] storage list, address element, uint256 index) private { + // TODO(asa): Move this to a library to be shared. require(index < list.length && list[index] == element); uint256 lastIndex = list.length.sub(1); list[index] = list[lastIndex]; - list[lastIndex] = address(0); + delete list[lastIndex]; list.length = lastIndex; } + /** + * @notice Returns whether or not a group can receive the specified number of votes. + * @param group The address of the group. + * @param value The number of votes. + * @return Whether or not a group can receive the specified number of votes. + * @dev Votes are not allowed to be cast that would increase a group's proportion of locked gold + * voting for it to greater than + * (numGroupMembers + 1) / min(maxElectableValidators, numRegisteredValidators) + */ + function canReceiveVotes(address group, uint256 value) public view returns (bool) { + uint256 totalVotesForGroup = getTotalVotesForGroup(group).add(value); + uint256 left = totalVotesForGroup.mul( + Math.min( + electableValidators.max, + getValidators().getNumRegisteredValidators() + ) + ); + uint256 right = getValidators().getGroupNumMembers(group).add(1).mul(getLockedGold().getTotalLockedGold()); + return left <= right; + } + /** * @notice Returns the number of votes that a group can receive. * @param group The address of the group. * @return The number of votes that a group can receive. + * @dev Votes are not allowed to be cast that would increase a group's proportion of locked gold + * voting for it to greater than + * (numGroupMembers + 1) / min(maxElectableValidators, numRegisteredValidators) */ - function getNumVotesReceivable(address group) public view returns (uint256) { - uint256 totalLockedGold = getLockedGold().getTotalLockedGold(); - uint256 numerator = getValidators().getGroupNumMembers(group).add(1).mul(totalLockedGold); + function getNumVotesReceivable(address group) external view returns (uint256) { + uint256 numerator = getValidators().getGroupNumMembers(group).add(1).mul(getLockedGold().getTotalLockedGold()); uint256 denominator = Math.min( - maxElectableValidators, + electableValidators.max, getValidators().getNumRegisteredValidators() ); return numerator.div(denominator); @@ -627,8 +689,8 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { * @notice Returns the total votes received across all groups. * @return The total votes received across all groups. */ - function getTotalVotes() external view returns (uint256) { - return votes.total.total; + function getTotalVotes() public view returns (uint256) { + return votes.active.total.add(votes.pending.total); } /** @@ -643,7 +705,7 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { * @notice Returns lists of all validator groups and the number of votes they've received. * @return Lists of all validator groups and the number of votes they've received. */ - function getEligibleValidatorGroupsVoteTotals() + function getTotalVotesForEligibleValidatorGroups() external view returns (address[] memory, uint256[] memory) @@ -685,14 +747,9 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { function electValidators() external view returns (address[] memory) { // Only members of these validator groups are eligible for election. uint256 maxNumElectionGroups = Math.min( - maxElectableValidators, + electableValidators.max, votes.total.eligible.list.numElements ); - /* - uint256 requiredVotes = electabilityThreshold.multiply( - FixidityLib.newFixed(votes.total.total) - ).fromFixed(); - */ // TODO(asa): Filter by > requiredVotes address[] memory electionGroups = votes.total.eligible.headN(maxNumElectionGroups); uint256[] memory numMembers = getValidators().getGroupsNumMembers(electionGroups); @@ -700,7 +757,7 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { uint256[] memory numMembersElected = new uint256[](electionGroups.length); uint256 totalNumMembersElected = 0; // Assign a number of seats to each validator group. - while (totalNumMembersElected < maxElectableValidators) { + while (totalNumMembersElected < electableValidators.max) { uint256 groupIndex = 0; bool memberElected = false; (groupIndex, memberElected) = dHondt(electionGroups, numMembers, numMembersElected); @@ -712,7 +769,7 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { break; } } - require(totalNumMembersElected >= minElectableValidators); + require(totalNumMembersElected >= electableValidators.min); // Grab the top validators from each group that won seats. address[] memory electedValidators = new address[](totalNumMembersElected); totalNumMembersElected = 0; diff --git a/packages/protocol/contracts/governance/LockedGold.sol b/packages/protocol/contracts/governance/LockedGold.sol index 6f89b30453c..11e6d156a88 100644 --- a/packages/protocol/contracts/governance/LockedGold.sol +++ b/packages/protocol/contracts/governance/LockedGold.sol @@ -15,17 +15,23 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr using SafeMath for uint256; struct MustMaintain { + // The Locked Gold balance that the account must maintain. uint256 value; + // The timestamp at which the account is no longer subject to these constraints. uint256 timestamp; } struct Authorizations { + // The address that is authorized to vote on behalf of the account. address voting; + // The address that is authorized to validate on behalf of the account. address validating; } struct PendingWithdrawal { + // The value of the pending withdrawal. uint256 value; + // The timestamp at which the pending withdrawal becomes available. uint256 timestamp; } @@ -33,7 +39,9 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr // This contract does not store an account's locked gold that is being used in electing // validators. uint256 nonvoting; + // Gold that has been unlocked and will become available for withdrawal. PendingWithdrawal[] pendingWithdrawals; + // Balance requirements imposed on this account. MustMaintain requirements; } @@ -189,7 +197,7 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr function unlock(uint256 value) external nonReentrant { require(isAccount(msg.sender)); Account storage account = accounts[msg.sender]; - MustMaintain memory requirement = account.balances.requirements; + MustMaintain storage requirement = account.balances.requirements; require( now >= requirement.timestamp || getAccountTotalLockedGold(msg.sender).sub(value) >= requirement.value @@ -223,7 +231,7 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr require(isAccount(msg.sender)); Account storage account = accounts[msg.sender]; require(index < account.balances.pendingWithdrawals.length); - PendingWithdrawal memory pendingWithdrawal = account.balances.pendingWithdrawals[index]; + PendingWithdrawal storage pendingWithdrawal = account.balances.pendingWithdrawals[index]; require(now >= pendingWithdrawal.timestamp); uint256 value = pendingWithdrawal.value; deletePendingWithdrawal(account.balances.pendingWithdrawals, index); @@ -271,7 +279,8 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr } /** - * @notice Returns the total amount of locked gold in the system. + * @notice Returns the total amount of locked gold in the system. Note that this does not include + * gold that has been unlocked but not yet withdrawn. * @return The total amount of locked gold in the system. */ function getTotalLockedGold() external view returns (uint256) { diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index bd363e8e3b2..d4bdec86fe5 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -41,6 +41,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi struct ValidatorGroup { string name; string url; + // TODO(asa): Add a function that allows groups to update their commission. FixidityLib.Fraction commission; LinkedList.List members; } @@ -194,8 +195,8 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi /** * @notice Updates the duration for which gold remains locked after deregistration. - * @param groupLockup The duration for groups. - * @param validatorLockup The duration for validators. + * @param groupLockup The duration for groups in seconds. + * @param validatorLockup The duration for validators in seconds. * @return True upon success. * @dev The new requirement is only enforced for future validator or group deregistrations. */ @@ -252,8 +253,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi require(!isValidator(account) && !isValidatorGroup(account)); require(meetsValidatorRegistrationRequirement(account)); - Validator memory validator = Validator(name, url, publicKeysData, address(0)); - validators[account] = validator; + validators[account] = Validator(name, url, publicKeysData, address(0)); _validators.push(account); getLockedGold().setAccountMustMaintain(account, registrationRequirements.validator, MAX_INT); emit ValidatorRegistered(account, name, url, publicKeysData); @@ -321,7 +321,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi */ function affiliate(address group) external nonReentrant returns (bool) { address account = getLockedGold().getAccountFromValidator(msg.sender); - require(isValidator(account) && isValidatorGroup(group), "blah"); + require(isValidator(account) && isValidatorGroup(group)); Validator storage validator = validators[account]; if (validator.affiliation != address(0)) { _deaffiliate(validator, account); @@ -420,7 +420,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi */ function _addMember(address group, address validator) private returns (bool) { ValidatorGroup storage _group = groups[group]; - require(_group.members.numElements < maxGroupSize); + require(_group.members.numElements < maxGroupSize, "group would exceed maximum size"); require(validators[validator].affiliation == group && !_group.members.contains(validator)); _group.members.push(validator); emit ValidatorGroupMemberAdded(group, validator); @@ -517,6 +517,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi * @return The number of members in a validator group. */ function getGroupNumMembers(address account) public view returns (uint256) { + require(isValidatorGroup(account)); return groups[account].members.numElements; } @@ -629,7 +630,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi require(index < list.length && list[index] == element); uint256 lastIndex = list.length.sub(1); list[index] = list[lastIndex]; - list[lastIndex] = address(0); + delete list[lastIndex]; list.length = lastIndex; } diff --git a/packages/protocol/migrationsConfig.js b/packages/protocol/migrationsConfig.js index a270504a09f..c8d354bbb08 100644 --- a/packages/protocol/migrationsConfig.js +++ b/packages/protocol/migrationsConfig.js @@ -18,7 +18,7 @@ const DefaultConfig = { reportExpiry: 60 * 60, // 1 hour }, election: { - minElectableValidators: '10', + minElectableValidators: '22', maxElectableValidators: '100', maxVotesPerAccount: 3, electabilityThreshold: '0', // no threshold diff --git a/packages/protocol/test/governance/election.ts b/packages/protocol/test/governance/election.ts index c3647fd622f..61d55ba9234 100644 --- a/packages/protocol/test/governance/election.ts +++ b/packages/protocol/test/governance/election.ts @@ -37,8 +37,10 @@ contract('Election', (accounts: string[]) => { let mockValidators: MockValidatorsInstance const nonOwner = accounts[1] - const minElectableValidators = new BigNumber(4) - const maxElectableValidators = new BigNumber(6) + const electableValidators = { + min: new BigNumber(4), + max: new BigNumber(6), + } const maxNumGroupsVotedFor = new BigNumber(3) const electabilityThreshold = new BigNumber(0) @@ -51,8 +53,8 @@ contract('Election', (accounts: string[]) => { await registry.setAddressFor(CeloContractName.Validators, mockValidators.address) await election.initialize( registry.address, - minElectableValidators, - maxElectableValidators, + electableValidators.min, + electableValidators.max, maxNumGroupsVotedFor, electabilityThreshold ) @@ -64,14 +66,10 @@ contract('Election', (accounts: string[]) => { assert.equal(owner, accounts[0]) }) - it('should have set minElectableValidators', async () => { - const actualMinElectableValidators = await election.minElectableValidators() - assertEqualBN(actualMinElectableValidators, minElectableValidators) - }) - - it('should have set maxElectableValidators', async () => { - const actualMaxElectableValidators = await election.maxElectableValidators() - assertEqualBN(actualMaxElectableValidators, maxElectableValidators) + it('should have set electableValidators', async () => { + const [min, max] = await election.getElectableValidators() + assertEqualBN(min, electableValidators.min) + assertEqualBN(max, electableValidators.max) }) it('should have set maxNumGroupsVotedFor', async () => { @@ -79,12 +77,17 @@ contract('Election', (accounts: string[]) => { assertEqualBN(actualMaxNumGroupsVotedFor, maxNumGroupsVotedFor) }) + it('should have set electabilityThreshold', async () => { + const actualElectabilityThreshold = await election.getElectabilityThreshold() + assertEqualBN(actualElectabilityThreshold, electabilityThreshold) + }) + it('should not be callable again', async () => { await assertRevert( election.initialize( registry.address, - minElectableValidators, - maxElectableValidators, + electableValidators.min, + electableValidators.max, maxNumGroupsVotedFor, electabilityThreshold ) @@ -106,74 +109,59 @@ contract('Election', (accounts: string[]) => { }) }) - describe('#setMinElectableValidators', () => { - const newMinElectableValidators = minElectableValidators.plus(1) + describe('#setElectableValidators', () => { + const newElectableValidators = { + min: electableValidators.min.plus(1), + max: electableValidators.max.plus(1), + } + it('should set the minimum electable valdiators', async () => { - await election.setMinElectableValidators(newMinElectableValidators) - assertEqualBN(await election.minElectableValidators(), newMinElectableValidators) + await election.setElectableValidators(newElectableValidators.min, newElectableValidators.max) + const [min, max] = await election.getElectableValidators() + assertEqualBN(min, newElectableValidators.min) + assertEqualBN(max, newElectableValidators.max) }) - it('should emit the MinElectableValidatorsSet event', async () => { - const resp = await election.setMinElectableValidators(newMinElectableValidators) + it('should emit the ElectableValidatorsSet event', async () => { + const resp = await election.setElectableValidators( + newElectableValidators.min, + newElectableValidators.max + ) assert.equal(resp.logs.length, 1) const log = resp.logs[0] assertContainSubset(log, { - event: 'MinElectableValidatorsSet', + event: 'ElectableValidatorsSet', args: { - minElectableValidators: new BigNumber(newMinElectableValidators), + min: newElectableValidators.min, + max: newElectableValidators.max, }, }) }) it('should revert when the minElectableValidators is zero', async () => { - await assertRevert(election.setMinElectableValidators(0)) + await assertRevert(election.setElectableValidators(0, newElectableValidators.max)) }) - it('should revert when the minElectableValidators is greater than maxElectableValidators', async () => { - await assertRevert(election.setMinElectableValidators(maxElectableValidators.plus(1))) - }) - - it('should revert when the minElectableValidators is unchanged', async () => { - await assertRevert(election.setMinElectableValidators(minElectableValidators)) - }) - - it('should revert when called by anyone other than the owner', async () => { + it('should revert when the min is greater than max', async () => { await assertRevert( - election.setMinElectableValidators(newMinElectableValidators, { from: nonOwner }) + election.setElectableValidators( + newElectableValidators.max.plus(1), + newElectableValidators.max + ) ) }) - }) - describe('#setMaxElectableValidators', () => { - const newMaxElectableValidators = maxElectableValidators.plus(1) - it('should set the max electable validators', async () => { - await election.setMaxElectableValidators(newMaxElectableValidators) - assertEqualBN(await election.maxElectableValidators(), newMaxElectableValidators) - }) - - it('should emit the MaxElectableValidatorsSet event', async () => { - const resp = await election.setMaxElectableValidators(newMaxElectableValidators) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'MaxElectableValidatorsSet', - args: { - maxElectableValidators: new BigNumber(newMaxElectableValidators), - }, - }) - }) - - it('should revert when the maxElectableValidators is less than minElectableValidators', async () => { - await assertRevert(election.setMaxElectableValidators(minElectableValidators.minus(1))) - }) - - it('should revert when the maxElectableValidators is unchanged', async () => { - await assertRevert(election.setMaxElectableValidators(maxElectableValidators)) + it('should revert when the values are unchanged', async () => { + await assertRevert( + election.setElectableValidators(electableValidators.min, electableValidators.max) + ) }) it('should revert when called by anyone other than the owner', async () => { await assertRevert( - election.setMaxElectableValidators(newMaxElectableValidators, { from: nonOwner }) + election.setElectableValidators(newElectableValidators.min, newElectableValidators.max, { + from: nonOwner, + }) ) }) }) @@ -218,7 +206,7 @@ contract('Election', (accounts: string[]) => { describe('when the group has no votes', () => { let resp: any beforeEach(async () => { - resp = await election.markGroupEligible(group, NULL_ADDRESS, NULL_ADDRESS) + resp = await election.markGroupEligible(NULL_ADDRESS, NULL_ADDRESS, { from: group }) }) it('should add the group to the list of eligible groups', async () => { @@ -238,7 +226,9 @@ contract('Election', (accounts: string[]) => { describe('when the group has already been marked eligible', () => { it('should revert', async () => { - await assertRevert(election.markGroupEligible(group, NULL_ADDRESS, NULL_ADDRESS)) + await assertRevert( + election.markGroupEligible(NULL_ADDRESS, NULL_ADDRESS, { from: group }) + ) }) }) }) @@ -246,7 +236,7 @@ contract('Election', (accounts: string[]) => { describe('when the group has no members', () => { it('should revert', async () => { - await assertRevert(election.markGroupEligible(group, NULL_ADDRESS, NULL_ADDRESS)) + await assertRevert(election.markGroupEligible(NULL_ADDRESS, NULL_ADDRESS, { from: group })) }) }) }) @@ -256,7 +246,7 @@ contract('Election', (accounts: string[]) => { describe('when the group is eligible', () => { beforeEach(async () => { await mockValidators.setMembers(group, [accounts[9]]) - await election.markGroupEligible(group, NULL_ADDRESS, NULL_ADDRESS) + await election.markGroupEligible(NULL_ADDRESS, NULL_ADDRESS, { from: group }) }) describe('when called by the registered Validators contract', () => { @@ -307,11 +297,11 @@ contract('Election', (accounts: string[]) => { describe('#vote', () => { const voter = accounts[0] const group = accounts[1] - const value = 1000 + const value = new BigNumber(1000) describe('when the group is eligible', () => { beforeEach(async () => { await mockValidators.setMembers(group, [accounts[9]]) - await election.markGroupEligible(group, NULL_ADDRESS, NULL_ADDRESS) + await election.markGroupEligible(NULL_ADDRESS, NULL_ADDRESS, { from: group }) }) describe('when the group can receive votes', () => { @@ -329,23 +319,23 @@ contract('Election', (accounts: string[]) => { }) it('should add the group to the list of groups the account has voted for', async () => { - assert.deepEqual(await election.getAccountGroupsVotedFor(voter), [group]) + assert.deepEqual(await election.getGroupsVotedForByAccount(voter), [group]) }) it("should increment the account's pending votes for the group", async () => { - assertEqualBN(await election.getAccountPendingVotesForGroup(group, voter), value) + assertEqualBN(await election.getPendingVotesForGroupByAccount(group, voter), value) }) it("should increment the account's total votes for the group", async () => { - assertEqualBN(await election.getAccountTotalVotesForGroup(group, voter), value) + assertEqualBN(await election.getTotalVotesForGroupByAccount(group, voter), value) }) it("should increment the account's total votes", async () => { - assertEqualBN(await election.getAccountTotalVotes(voter), value) + assertEqualBN(await election.getTotalVotesByAccount(voter), value) }) it('should increment the total votes for the group', async () => { - assertEqualBN(await election.getGroupTotalVotes(group), value) + assertEqualBN(await election.getTotalVotesForGroup(group), value) }) it('should increment the total votes', async () => { @@ -372,7 +362,7 @@ contract('Election', (accounts: string[]) => { describe('when the voter does not have sufficient non-voting balance', () => { beforeEach(async () => { - await mockLockedGold.incrementNonvotingAccountBalance(voter, value - 1) + await mockLockedGold.incrementNonvotingAccountBalance(voter, value.minus(1)) }) it('should revert', async () => { @@ -388,20 +378,26 @@ contract('Election', (accounts: string[]) => { for (let i = 0; i < maxNumGroupsVotedFor.toNumber(); i++) { newGroup = accounts[i + 2] await mockValidators.setMembers(newGroup, [accounts[9]]) - await election.markGroupEligible(newGroup, group, NULL_ADDRESS) + await election.markGroupEligible(group, NULL_ADDRESS, { from: newGroup }) await election.vote(newGroup, 1, group, NULL_ADDRESS) } }) it('should revert', async () => { await assertRevert( - election.vote(group, value - maxNumGroupsVotedFor.toNumber(), newGroup, NULL_ADDRESS) + election.vote(group, value.minus(maxNumGroupsVotedFor), newGroup, NULL_ADDRESS) ) }) }) }) describe('when the group cannot receive votes', () => { + beforeEach(async () => { + await mockLockedGold.setTotalLockedGold(value.div(2).minus(1)) + await mockValidators.setNumRegisteredValidators(1) + assertEqualBN(await election.getNumVotesReceivable(group), value.minus(2)) + }) + it('should revert', async () => { await assertRevert(election.vote(group, value, NULL_ADDRESS, NULL_ADDRESS)) }) @@ -421,7 +417,7 @@ contract('Election', (accounts: string[]) => { const value = 1000 beforeEach(async () => { await mockValidators.setMembers(group, [accounts[9]]) - await election.markGroupEligible(group, NULL_ADDRESS, NULL_ADDRESS) + await election.markGroupEligible(NULL_ADDRESS, NULL_ADDRESS, { from: group }) await mockLockedGold.setTotalLockedGold(value) await mockValidators.setNumRegisteredValidators(1) await mockLockedGold.incrementNonvotingAccountBalance(voter, value) @@ -435,23 +431,23 @@ contract('Election', (accounts: string[]) => { }) it("should decrement the account's pending votes for the group", async () => { - assertEqualBN(await election.getAccountPendingVotesForGroup(group, voter), 0) + assertEqualBN(await election.getPendingVotesForGroupByAccount(group, voter), 0) }) it("should increment the account's active votes for the group", async () => { - assertEqualBN(await election.getAccountActiveVotesForGroup(group, voter), value) + assertEqualBN(await election.getActiveVotesForGroupByAccount(group, voter), value) }) it("should not modify the account's total votes for the group", async () => { - assertEqualBN(await election.getAccountTotalVotesForGroup(group, voter), value) + assertEqualBN(await election.getTotalVotesForGroupByAccount(group, voter), value) }) it("should not modify the account's total votes", async () => { - assertEqualBN(await election.getAccountTotalVotes(voter), value) + assertEqualBN(await election.getTotalVotesByAccount(voter), value) }) it('should not modify the total votes for the group', async () => { - assertEqualBN(await election.getGroupTotalVotes(group), value) + assertEqualBN(await election.getTotalVotesForGroup(group), value) }) it('should not modify the total votes', async () => { @@ -481,35 +477,35 @@ contract('Election', (accounts: string[]) => { }) it("should not modify the first account's active votes for the group", async () => { - assertEqualBN(await election.getAccountActiveVotesForGroup(group, voter), value) + assertEqualBN(await election.getActiveVotesForGroupByAccount(group, voter), value) }) it("should not modify the first account's total votes for the group", async () => { - assertEqualBN(await election.getAccountTotalVotesForGroup(group, voter), value) + assertEqualBN(await election.getTotalVotesForGroupByAccount(group, voter), value) }) it("should not modify the first account's total votes", async () => { - assertEqualBN(await election.getAccountTotalVotes(voter), value) + assertEqualBN(await election.getTotalVotesByAccount(voter), value) }) it("should decrement the second account's pending votes for the group", async () => { - assertEqualBN(await election.getAccountPendingVotesForGroup(group, voter2), 0) + assertEqualBN(await election.getPendingVotesForGroupByAccount(group, voter2), 0) }) it("should increment the second account's active votes for the group", async () => { - assertEqualBN(await election.getAccountActiveVotesForGroup(group, voter2), value2) + assertEqualBN(await election.getActiveVotesForGroupByAccount(group, voter2), value2) }) it("should not modify the second account's total votes for the group", async () => { - assertEqualBN(await election.getAccountTotalVotesForGroup(group, voter2), value2) + assertEqualBN(await election.getTotalVotesForGroupByAccount(group, voter2), value2) }) it("should not modify the second account's total votes", async () => { - assertEqualBN(await election.getAccountTotalVotes(voter2), value2) + assertEqualBN(await election.getTotalVotesByAccount(voter2), value2) }) it('should not modify the total votes for the group', async () => { - assertEqualBN(await election.getGroupTotalVotes(group), value + value2) + assertEqualBN(await election.getTotalVotesForGroup(group), value + value2) }) it('should not modify the total votes', async () => { @@ -532,7 +528,7 @@ contract('Election', (accounts: string[]) => { describe('when the voter has pending votes', () => { beforeEach(async () => { await mockValidators.setMembers(group, [accounts[9]]) - await election.markGroupEligible(group, NULL_ADDRESS, NULL_ADDRESS) + await election.markGroupEligible(NULL_ADDRESS, NULL_ADDRESS, { from: group }) await mockLockedGold.setTotalLockedGold(value) await mockValidators.setNumRegisteredValidators(1) await mockLockedGold.incrementNonvotingAccountBalance(voter, value) @@ -555,19 +551,19 @@ contract('Election', (accounts: string[]) => { }) it("should decrement the account's pending votes for the group", async () => { - assertEqualBN(await election.getAccountPendingVotesForGroup(group, voter), remaining) + assertEqualBN(await election.getPendingVotesForGroupByAccount(group, voter), remaining) }) it("should decrement the account's total votes for the group", async () => { - assertEqualBN(await election.getAccountTotalVotesForGroup(group, voter), remaining) + assertEqualBN(await election.getTotalVotesForGroupByAccount(group, voter), remaining) }) it("should decrement the account's total votes", async () => { - assertEqualBN(await election.getAccountTotalVotes(voter), remaining) + assertEqualBN(await election.getTotalVotesByAccount(voter), remaining) }) it('should decrement the total votes for the group', async () => { - assertEqualBN(await election.getGroupTotalVotes(group), remaining) + assertEqualBN(await election.getTotalVotesForGroup(group), remaining) }) it('should decrement the total votes', async () => { @@ -600,7 +596,7 @@ contract('Election', (accounts: string[]) => { }) it('should remove the group to the list of groups the account has voted for', async () => { - assert.deepEqual(await election.getAccountGroupsVotedFor(voter), []) + assert.deepEqual(await election.getGroupsVotedForByAccount(voter), []) }) }) @@ -632,7 +628,7 @@ contract('Election', (accounts: string[]) => { describe('when the voter has active votes', () => { beforeEach(async () => { await mockValidators.setMembers(group, [accounts[9]]) - await election.markGroupEligible(group, NULL_ADDRESS, NULL_ADDRESS) + await election.markGroupEligible(NULL_ADDRESS, NULL_ADDRESS, { from: group }) await mockLockedGold.setTotalLockedGold(value) await mockValidators.setNumRegisteredValidators(1) await mockLockedGold.incrementNonvotingAccountBalance(voter, value) @@ -650,19 +646,19 @@ contract('Election', (accounts: string[]) => { }) it("should decrement the account's active votes for the group", async () => { - assertEqualBN(await election.getAccountActiveVotesForGroup(group, voter), remaining) + assertEqualBN(await election.getActiveVotesForGroupByAccount(group, voter), remaining) }) it("should decrement the account's total votes for the group", async () => { - assertEqualBN(await election.getAccountTotalVotesForGroup(group, voter), remaining) + assertEqualBN(await election.getTotalVotesForGroupByAccount(group, voter), remaining) }) it("should decrement the account's total votes", async () => { - assertEqualBN(await election.getAccountTotalVotes(voter), remaining) + assertEqualBN(await election.getTotalVotesByAccount(voter), remaining) }) it('should decrement the total votes for the group', async () => { - assertEqualBN(await election.getGroupTotalVotes(group), remaining) + assertEqualBN(await election.getTotalVotesForGroup(group), remaining) }) it('should decrement the total votes', async () => { @@ -695,7 +691,7 @@ contract('Election', (accounts: string[]) => { }) it('should remove the group to the list of groups the account has voted for', async () => { - assert.deepEqual(await election.getAccountGroupsVotedFor(voter), []) + assert.deepEqual(await election.getGroupsVotedForByAccount(voter), []) }) }) @@ -755,9 +751,9 @@ contract('Election', (accounts: string[]) => { await mockValidators.setMembers(group2, [validator5, validator6]) await mockValidators.setMembers(group3, [validator7]) - await election.markGroupEligible(group1, NULL_ADDRESS, NULL_ADDRESS) - await election.markGroupEligible(group2, NULL_ADDRESS, group1) - await election.markGroupEligible(group3, NULL_ADDRESS, group2) + await election.markGroupEligible(NULL_ADDRESS, NULL_ADDRESS, { from: group1 }) + await election.markGroupEligible(NULL_ADDRESS, group1, { from: group2 }) + await election.markGroupEligible(NULL_ADDRESS, group2, { from: group3 }) for (const voter of [voter1, voter2, voter3]) { await mockLockedGold.incrementNonvotingAccountBalance(voter.address, voter.weight) diff --git a/packages/protocol/test/governance/validators.ts b/packages/protocol/test/governance/validators.ts index 2251fca668c..df5d6d23970 100644 --- a/packages/protocol/test/governance/validators.ts +++ b/packages/protocol/test/governance/validators.ts @@ -133,6 +133,11 @@ contract('Validators', (accounts: string[]) => { assertEqualBN(validator, deregistrationLockups.validator) }) + it('should have set the max group size', async () => { + const actualMaxGroupSize = await validators.getMaxGroupSize() + assertEqualBN(actualMaxGroupSize, maxGroupSize) + }) + it('should not be callable again', async () => { await assertRevert( validators.initialize( @@ -419,7 +424,7 @@ contract('Validators', (accounts: string[]) => { const validator = accounts[0] const index = 0 let resp: any - describe('when the account is not a registered validator', () => { + describe('when the account is a registered validator', () => { beforeEach(async () => { await registerValidator(validator) resp = await validators.deregisterValidator(index) From adcad0b3f2b5bae96ac78d3364005ec5936f4dc1 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Tue, 8 Oct 2019 17:08:33 -0700 Subject: [PATCH 037/149] Fix linting issues --- packages/protocol/contracts/governance/Election.sol | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/protocol/contracts/governance/Election.sol b/packages/protocol/contracts/governance/Election.sol index 7532cea72e6..7e8c3ecd37d 100644 --- a/packages/protocol/contracts/governance/Election.sol +++ b/packages/protocol/contracts/governance/Election.sol @@ -664,7 +664,9 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { getValidators().getNumRegisteredValidators() ) ); - uint256 right = getValidators().getGroupNumMembers(group).add(1).mul(getLockedGold().getTotalLockedGold()); + uint256 right = getValidators().getGroupNumMembers(group).add(1).mul( + getLockedGold().getTotalLockedGold() + ); return left <= right; } @@ -677,7 +679,9 @@ contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { * (numGroupMembers + 1) / min(maxElectableValidators, numRegisteredValidators) */ function getNumVotesReceivable(address group) external view returns (uint256) { - uint256 numerator = getValidators().getGroupNumMembers(group).add(1).mul(getLockedGold().getTotalLockedGold()); + uint256 numerator = getValidators().getGroupNumMembers(group).add(1).mul( + getLockedGold().getTotalLockedGold() + ); uint256 denominator = Math.min( electableValidators.max, getValidators().getNumRegisteredValidators() From 53e4977d65f1ec0e9fbb54391e963fab340e2515 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Tue, 8 Oct 2019 17:45:26 -0700 Subject: [PATCH 038/149] Make things build, tests pass --- packages/cli/src/commands/lockedgold/show.ts | 3 +- .../cli/src/commands/lockedgold/withdraw.ts | 6 +- .../src/commands/validatorgroup/register.ts | 12 ++-- packages/contractkit/src/wrappers/Election.ts | 57 ++++++++----------- .../contractkit/src/wrappers/LockedGold.ts | 14 ++--- .../contractkit/src/wrappers/Validators.ts | 13 +++-- .../contracts/governance/Election.sol | 4 +- .../contracts/governance/Governance.sol | 7 ++- .../contracts/governance/LockedGold.sol | 2 +- .../governance/interfaces/IElection.sol | 2 +- 10 files changed, 59 insertions(+), 61 deletions(-) diff --git a/packages/cli/src/commands/lockedgold/show.ts b/packages/cli/src/commands/lockedgold/show.ts index 712d5760f52..9c9c90c089c 100644 --- a/packages/cli/src/commands/lockedgold/show.ts +++ b/packages/cli/src/commands/lockedgold/show.ts @@ -1,7 +1,6 @@ import { BaseCommand } from '../../base' import { printValueMapRecursive } from '../../utils/cli' import { Args } from '../../utils/command' -import { eqAddress } from '@celo/utils/lib/address' export default class Show extends BaseCommand { static description = 'Show Locked Gold information for a given account' @@ -19,6 +18,6 @@ export default class Show extends BaseCommand { const { args } = this.parse(Show) const lockedGold = await this.kit.contracts.getLockedGold() - printValueMapRecursive(await lockedGold.geAccountSummary(args.account)) + printValueMapRecursive(await lockedGold.getAccountSummary(args.account)) } } diff --git a/packages/cli/src/commands/lockedgold/withdraw.ts b/packages/cli/src/commands/lockedgold/withdraw.ts index 067f24febbd..3691d715dc2 100644 --- a/packages/cli/src/commands/lockedgold/withdraw.ts +++ b/packages/cli/src/commands/lockedgold/withdraw.ts @@ -22,7 +22,8 @@ export default class Withdraw extends BaseCommand { while (true) { let madeWithdrawal = false const pendingWithdrawals = await lockedgold.getPendingWithdrawals(flags.from) - for (const pendingWithdrawal of pendingWithdrawals) { + for (let i = 0; i < pendingWithdrawals.length; i++) { + const pendingWithdrawal = pendingWithdrawals[i] if (pendingWithdrawal.time.isLessThan(currentTime)) { console.log( `Found available pending withdrawal of value ${pendingWithdrawal.value.toString()}, withdrawing` @@ -32,6 +33,9 @@ export default class Withdraw extends BaseCommand { break } } + if (!madeWithdrawal) { + break + } } const pendingWithdrawals = await lockedgold.getPendingWithdrawals(flags.from) for (const pendingWithdrawal of pendingWithdrawals) { diff --git a/packages/cli/src/commands/validatorgroup/register.ts b/packages/cli/src/commands/validatorgroup/register.ts index a805fa40d3d..a8650e81fee 100644 --- a/packages/cli/src/commands/validatorgroup/register.ts +++ b/packages/cli/src/commands/validatorgroup/register.ts @@ -24,13 +24,11 @@ export default class ValidatorGroupRegister extends BaseCommand { this.kit.defaultAccount = res.flags.from const validators = await this.kit.contracts.getValidators() - await displaySendTx( - 'registerValidatorGroup', - validators.registerValidatorGroup( - res.flags.name, - res.flags.url, - new BigNumber(res.flags.commission) - ) + const tx = await validators.registerValidatorGroup( + res.flags.name, + res.flags.url, + new BigNumber(res.flags.commission) ) + await displaySendTx('registerValidatorGroup', tx) } } diff --git a/packages/contractkit/src/wrappers/Election.ts b/packages/contractkit/src/wrappers/Election.ts index 240edae9884..89403022f49 100644 --- a/packages/contractkit/src/wrappers/Election.ts +++ b/packages/contractkit/src/wrappers/Election.ts @@ -34,9 +34,13 @@ export interface ValidatorGroupVote { eligible: boolean } +export interface ElectableValidators { + min: BigNumber + max: BigNumber +} + export interface ElectionConfig { - minElectableValidators: BigNumber - maxElectableValidators: BigNumber + electableValidators: ElectableValidators electabilityThreshold: BigNumber maxNumGroupsVotedFor: BigNumber } @@ -47,23 +51,13 @@ export interface ElectionConfig { export class ElectionWrapper extends BaseWrapper { activate = proxySend(this.kit, this.contract.methods.activate) /** - * Returns the minimum number of validators that can be elected. - * @returns The minimum number of validators that can be elected. - */ - minElectableValidators = proxyCall( - this.contract.methods.minElectableValidators, - undefined, - toBigNumber - ) - /** - * Returns the maximum number of validators that can be elected. - * @returns The maximum number of validators that can be elected. + * Returns the minimum and maximum number of validators that can be elected. + * @returns The minimum and maximum number of validators that can be elected. */ - maxElectableValidators = proxyCall( - this.contract.methods.maxElectableValidators, - undefined, - toBigNumber - ) + async electableValidators(): Promise { + const { min, max } = await this.contract.methods.electableValidators().call() + return { min: toBigNumber(min), max: toBigNumber(max) } + } /** * Returns the current election threshold. * @returns Election threshold. @@ -80,8 +74,8 @@ export class ElectionWrapper extends BaseWrapper { toNumber ) - getGroupsVotedFor: (account: Address) => Promise = proxyCall( - this.contract.methods.getGroupsVotedFor + getGroupsVotedForByAccount: (account: Address) => Promise = proxyCall( + this.contract.methods.getGroupsVotedForByAccount ) /** @@ -89,16 +83,14 @@ export class ElectionWrapper extends BaseWrapper { */ async getConfig(): Promise { const res = await Promise.all([ - this.minElectableValidators(), - this.maxElectableValidators(), + this.electableValidators(), this.electabilityThreshold(), this.contract.methods.maxNumGroupsVotedFor().call(), ]) return { - minElectableValidators: res[0], - maxElectableValidators: res[1], - electabilityThreshold: res[2], - maxNumGroupsVotedFor: toBigNumber(res[3]), + electableValidators: res[0], + electabilityThreshold: res[1], + maxNumGroupsVotedFor: toBigNumber(res[2]), } } @@ -120,7 +112,7 @@ export class ElectionWrapper extends BaseWrapper { (g) => g.address ) const validatorGroupVotes = await Promise.all( - validatorGroupAddresses.map((g) => this.contract.methods.getGroupTotalVotes(g).call()) + validatorGroupAddresses.map((g) => this.contract.methods.getTotalVotesForGroup(g).call()) ) const validatorGroupEligible = await Promise.all( validatorGroupAddresses.map((g) => this.contract.methods.getGroupEligibility(g).call()) @@ -133,7 +125,7 @@ export class ElectionWrapper extends BaseWrapper { } async getEligibleValidatorGroupsVotes(): Promise { - const res = await this.contract.methods.getEligibleValidatorGroupsVoteTotals().call() + const res = await this.contract.methods.getTotalVotesForEligibleValidatorGroups().call() return zip((a, b) => ({ address: a, votes: new BigNumber(b), eligible: true }), res[0], res[1]) } @@ -142,13 +134,12 @@ export class ElectionWrapper extends BaseWrapper { throw new Error(`missing from at new ValdidatorUtils()`) } - const value = toBigNumber(await this.contract.methods.getGroupTotalVotes(validatorGroup).call()) + const value = toBigNumber( + await this.contract.methods.getTotalVotesForGroup(validatorGroup).call() + ) const { lesser, greater } = await this.findLesserAndGreaterAfterVote(validatorGroup, value) - return wrapSend( - this.kit, - this.contract.methods.markGroupEligible(validatorGroup, lesser, greater) - ) + return wrapSend(this.kit, this.contract.methods.markGroupEligible(lesser, greater)) } async vote(validatorGroup: Address, value: BigNumber): Promise> { diff --git a/packages/contractkit/src/wrappers/LockedGold.ts b/packages/contractkit/src/wrappers/LockedGold.ts index 740efa111d1..62a53657862 100644 --- a/packages/contractkit/src/wrappers/LockedGold.ts +++ b/packages/contractkit/src/wrappers/LockedGold.ts @@ -78,12 +78,12 @@ export class LockedGoldWrapper extends BaseWrapper { } async getAccountSummary(account: string): Promise { - const nonvoting = await this.getAccountNonvotingLockedGold(args.account) - const total = await this.getAccountTotalLockedGold(args.account) - const voter = await this.getVoterFromAccount(args.account) - const validator = await this.getValidatorFromAccount(args.account) - const pendingWithdrawals = await this.getPendingWithdrawals(args.account) - return (info = { + const nonvoting = await this.getAccountNonvotingLockedGold(account) + const total = await this.getAccountTotalLockedGold(account) + const voter = await this.getVoterFromAccount(account) + const validator = await this.getValidatorFromAccount(account) + const pendingWithdrawals = await this.getPendingWithdrawals(account) + return { lockedGold: { total, nonvoting, @@ -93,7 +93,7 @@ export class LockedGoldWrapper extends BaseWrapper { validator: eqAddress(validator, account) ? 'None' : validator, }, pendingWithdrawals, - }) + } } /** diff --git a/packages/contractkit/src/wrappers/Validators.ts b/packages/contractkit/src/wrappers/Validators.ts index 51c0d410caf..0241d35987b 100644 --- a/packages/contractkit/src/wrappers/Validators.ts +++ b/packages/contractkit/src/wrappers/Validators.ts @@ -1,7 +1,14 @@ import BigNumber from 'bignumber.js' import { Address } from '../base' import { Validators } from '../generated/types/Validators' -import { BaseWrapper, proxyCall, proxySend, toBigNumber } from './BaseWrapper' +import { + BaseWrapper, + CeloTransactionObject, + proxyCall, + proxySend, + toBigNumber, + wrapSend, +} from './BaseWrapper' import { fromFixed, toFixed } from '@celo/utils/lib/fixidity' export interface Validator { @@ -45,8 +52,6 @@ export class ValidatorsWrapper extends BaseWrapper { addMember = proxySend(this.kit, this.contract.methods.addMember) removeMember = proxySend(this.kit, this.contract.methods.removeMember) registerValidator = proxySend(this.kit, this.contract.methods.registerValidator) - registerValidatorGroup = proxySend(this.kit, this.contract.methods.registerValidatorGroup) - async registerValidatorGroup( name: string, url: string, @@ -57,7 +62,7 @@ export class ValidatorsWrapper extends BaseWrapper { } return wrapSend( this.kit, - this.contract.methods.registerValidatorGroup(name, url, toFixed(commission)) + this.contract.methods.registerValidatorGroup(name, url, toFixed(commission).toFixed()) ) } /** diff --git a/packages/protocol/contracts/governance/Election.sol b/packages/protocol/contracts/governance/Election.sol index 7e8c3ecd37d..88b27f88028 100644 --- a/packages/protocol/contracts/governance/Election.sol +++ b/packages/protocol/contracts/governance/Election.sol @@ -5,14 +5,14 @@ import "openzeppelin-solidity/contracts/math/SafeMath.sol"; import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; import "openzeppelin-solidity/contracts/utils/ReentrancyGuard.sol"; -import "./interfaces/IValidators.sol"; +import "./interfaces/IElection.sol"; import "../common/Initializable.sol"; import "../common/FixidityLib.sol"; import "../common/linkedlists/AddressSortedLinkedList.sol"; import "../common/UsingRegistry.sol"; -contract Election is Ownable, ReentrancyGuard, Initializable, UsingRegistry { +contract Election is IElection, Ownable, ReentrancyGuard, Initializable, UsingRegistry { using AddressSortedLinkedList for SortedLinkedList.List; using FixidityLib for FixidityLib.Fraction; diff --git a/packages/protocol/contracts/governance/Governance.sol b/packages/protocol/contracts/governance/Governance.sol index dfe1a2634df..2d863af40fb 100644 --- a/packages/protocol/contracts/governance/Governance.sol +++ b/packages/protocol/contracts/governance/Governance.sol @@ -499,10 +499,11 @@ contract Governance is IGovernance, Ownable, Initializable, ReentrancyGuard, Usi Voter storage voter = voters[account]; // We can upvote a proposal in the queue if we're not already upvoting a proposal in the queue. uint256 weight = getLockedGold().getAccountTotalLockedGold(account); + require(weight > 0, "cannot upvote without locking gold"); + require(isQueued(proposalId), "cannot upvote a proposal not in the queue"); require( - isQueued(proposalId) && - (voter.upvote.proposalId == 0 || !queue.contains(voter.upvote.proposalId)) && - weight > 0 + voter.upvote.proposalId == 0 || !queue.contains(voter.upvote.proposalId), + "cannot upvote more than one queued proposal" ); uint256 upvotes = queue.getValue(proposalId).add(weight); queue.update(proposalId, upvotes, lesser, greater); diff --git a/packages/protocol/contracts/governance/LockedGold.sol b/packages/protocol/contracts/governance/LockedGold.sol index 11e6d156a88..7ec0cdbe6b6 100644 --- a/packages/protocol/contracts/governance/LockedGold.sol +++ b/packages/protocol/contracts/governance/LockedGold.sol @@ -302,7 +302,7 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr */ function getAccountTotalLockedGold(address account) public view returns (uint256) { uint256 total = accounts[account].balances.nonvoting; - return total.add(getElection().getAccountTotalVotes(account)); + return total.add(getElection().getTotalVotesByAccount(account)); } /** diff --git a/packages/protocol/contracts/governance/interfaces/IElection.sol b/packages/protocol/contracts/governance/interfaces/IElection.sol index 03c68fca96c..24226b3f7a9 100644 --- a/packages/protocol/contracts/governance/interfaces/IElection.sol +++ b/packages/protocol/contracts/governance/interfaces/IElection.sol @@ -3,7 +3,7 @@ pragma solidity ^0.5.3; interface IElection { function getTotalVotes() external view returns (uint256); - function getAccountTotalVotes(address) external view returns (uint256); + function getTotalVotesByAccount(address) external view returns (uint256); function markGroupIneligible(address) external; function electValidators() external view returns (address[] memory); } From a5aa266638de1bed45a7e010ea866d6b4cc74f60 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Wed, 9 Oct 2019 12:47:57 -0700 Subject: [PATCH 039/149] Fix unit tests --- .../contracts/governance/Election.sol | 8 +-- .../governance/test/MockElection.sol | 2 +- .../governance/test/MockLockedGold.sol | 6 +- packages/protocol/test/governance/election.ts | 58 +++++++++---------- 4 files changed, 37 insertions(+), 37 deletions(-) diff --git a/packages/protocol/contracts/governance/Election.sol b/packages/protocol/contracts/governance/Election.sol index 77233ced99a..3dd5835e6e6 100644 --- a/packages/protocol/contracts/governance/Election.sol +++ b/packages/protocol/contracts/governance/Election.sol @@ -456,10 +456,10 @@ contract Election is IElection, Ownable, ReentrancyGuard, Initializable, UsingRe function getGroupEpochRewards(address group, uint256 totalEpochRewards) external view returns (uint256) { // TODO(asa): Is this right? - if (votes.active.total[group] == 0 || votes.total.active == 0) { + if (votes.active.total == 0) { return 0; } - return totalEpochRewards.mul(votes.active.total[group]).div(votes.total.active); + return totalEpochRewards.mul(votes.active.forGroup[group].total).div(votes.active.total); } function distributeEpochRewards(address group, uint256 value, address lesser, address greater) external { @@ -473,8 +473,8 @@ contract Election is IElection, Ownable, ReentrancyGuard, Initializable, UsingRe votes.total.eligible.update(group, newVoteTotal, lesser, greater); } - votes.active.total[group] = votes.active.total[group].add(value); - votes.total.active = votes.total.active.add(value); + votes.active.forGroup[group].total = votes.active.forGroup[group].total.add(value); + votes.active.total = votes.active.total.add(value); } /** diff --git a/packages/protocol/contracts/governance/test/MockElection.sol b/packages/protocol/contracts/governance/test/MockElection.sol index da4a5e34e18..212133020b1 100644 --- a/packages/protocol/contracts/governance/test/MockElection.sol +++ b/packages/protocol/contracts/governance/test/MockElection.sol @@ -18,7 +18,7 @@ contract MockElection is IElection { return 0; } - function getAccountTotalVotes(address) external view returns (uint256) { + function getTotalVotesByAccount(address) external view returns (uint256) { return 0; } diff --git a/packages/protocol/contracts/governance/test/MockLockedGold.sol b/packages/protocol/contracts/governance/test/MockLockedGold.sol index d0846c7973a..ca6320133ea 100644 --- a/packages/protocol/contracts/governance/test/MockLockedGold.sol +++ b/packages/protocol/contracts/governance/test/MockLockedGold.sol @@ -34,15 +34,15 @@ contract MockLockedGold { authorizedValidators[account] = validator; } - function getAccountFromValidator(address accountOrValidator) external view returns (address) { + function getAccountFromValidator(address accountOrValidator) external pure returns (address) { return accountOrValidator; } - function getAccountFromActiveValidator(address accountOrValidator) external view returns (address) { + function getAccountFromActiveValidator(address accountOrValidator) external pure returns (address) { return accountOrValidator; } - function getAccountFromActiveVoter(address accountOrVoter) external view returns (address) { + function getAccountFromActiveVoter(address accountOrVoter) external pure returns (address) { return accountOrVoter; } diff --git a/packages/protocol/test/governance/election.ts b/packages/protocol/test/governance/election.ts index 765b99ea035..5ec5ba423cf 100644 --- a/packages/protocol/test/governance/election.ts +++ b/packages/protocol/test/governance/election.ts @@ -440,23 +440,23 @@ contract('Election', (accounts: string[]) => { }) it("should decrement the account's pending votes for the group", async () => { - assertEqualBN(await election.getAccountPendingVotesForGroup(group, voter), 0) + assertEqualBN(await election.getPendingVotesForGroupByAccount(group, voter), 0) }) it("should increment the account's active votes for the group", async () => { - assertEqualBN(await election.getAccountActiveVotesForGroup(group, voter), value) + assertEqualBN(await election.getActiveVotesForGroupByAccount(group, voter), value) }) it("should not modify the account's total votes for the group", async () => { - assertEqualBN(await election.getAccountTotalVotesForGroup(group, voter), value) + assertEqualBN(await election.getTotalVotesForGroupByAccount(group, voter), value) }) it("should not modify the account's total votes", async () => { - assertEqualBN(await election.getAccountTotalVotes(voter), value) + assertEqualBN(await election.getTotalVotesByAccount(voter), value) }) it('should not modify the total votes for the group', async () => { - assertEqualBN(await election.getGroupTotalVotes(group), value) + assertEqualBN(await election.getTotalVotesForGroup(group), value) }) it('should not modify the total votes', async () => { @@ -487,35 +487,35 @@ contract('Election', (accounts: string[]) => { }) it("should not modify the first account's active votes for the group", async () => { - assertEqualBN(await election.getAccountActiveVotesForGroup(group, voter), value) + assertEqualBN(await election.getActiveVotesForGroupByAccount(group, voter), value) }) it("should not modify the first account's total votes for the group", async () => { - assertEqualBN(await election.getAccountTotalVotesForGroup(group, voter), value) + assertEqualBN(await election.getTotalVotesForGroupByAccount(group, voter), value) }) it("should not modify the first account's total votes", async () => { - assertEqualBN(await election.getAccountTotalVotes(voter), value) + assertEqualBN(await election.getTotalVotesByAccount(voter), value) }) it("should decrement the second account's pending votes for the group", async () => { - assertEqualBN(await election.getAccountPendingVotesForGroup(group, voter2), 0) + assertEqualBN(await election.getPendingVotesForGroupByAccount(group, voter2), 0) }) it("should increment the second account's active votes for the group", async () => { - assertEqualBN(await election.getAccountActiveVotesForGroup(group, voter2), value2) + assertEqualBN(await election.getActiveVotesForGroupByAccount(group, voter2), value2) }) it("should not modify the second account's total votes for the group", async () => { - assertEqualBN(await election.getAccountTotalVotesForGroup(group, voter2), value2) + assertEqualBN(await election.getTotalVotesForGroupByAccount(group, voter2), value2) }) it("should not modify the second account's total votes", async () => { - assertEqualBN(await election.getAccountTotalVotes(voter2), value2) + assertEqualBN(await election.getTotalVotesByAccount(voter2), value2) }) it('should not modify the total votes for the group', async () => { - assertEqualBN(await election.getGroupTotalVotes(group), value + value2) + assertEqualBN(await election.getTotalVotesForGroup(group), value + value2) }) it('should not modify the total votes', async () => { @@ -877,7 +877,7 @@ contract('Election', (accounts: string[]) => { const rewardValue = new BigNumber(1000000) beforeEach(async () => { await mockValidators.setMembers(group, [accounts[9]]) - await election.markGroupEligible(group, NULL_ADDRESS, NULL_ADDRESS) + await election.markGroupEligible(NULL_ADDRESS, NULL_ADDRESS, { from: group }) await mockLockedGold.setTotalLockedGold(voteValue) await mockValidators.setNumRegisteredValidators(1) await mockLockedGold.incrementNonvotingAccountBalance(voter, voteValue) @@ -894,24 +894,24 @@ contract('Election', (accounts: string[]) => { it("should increment the account's active votes for the group", async () => { assertEqualBN( - await election.getAccountActiveVotesForGroup(group, voter), + await election.getActiveVotesForGroupByAccount(group, voter), voteValue.plus(rewardValue) ) }) it("should increment the account's total votes for the group", async () => { assertEqualBN( - await election.getAccountTotalVotesForGroup(group, voter), + await election.getTotalVotesForGroupByAccount(group, voter), voteValue.plus(rewardValue) ) }) it("should increment account's total votes", async () => { - assertEqualBN(await election.getAccountTotalVotes(voter), voteValue.plus(rewardValue)) + assertEqualBN(await election.getTotalVotesByAccount(voter), voteValue.plus(rewardValue)) }) it('should increment the total votes for the group', async () => { - assertEqualBN(await election.getGroupTotalVotes(group), voteValue.plus(rewardValue)) + assertEqualBN(await election.getTotalVotesForGroup(group), voteValue.plus(rewardValue)) }) it('should increment the total votes', async () => { @@ -927,7 +927,7 @@ contract('Election', (accounts: string[]) => { const rewardValue2 = new BigNumber(10000000) beforeEach(async () => { await mockValidators.setMembers(group2, [accounts[8]]) - await election.markGroupEligible(group2, NULL_ADDRESS, group) + await election.markGroupEligible(NULL_ADDRESS, group, { from: group2 }) await mockLockedGold.setTotalLockedGold(voteValue.plus(voteValue2)) await mockValidators.setNumRegisteredValidators(2) await mockLockedGold.incrementNonvotingAccountBalance(voter2, voteValue2) @@ -956,49 +956,49 @@ contract('Election', (accounts: string[]) => { it("should increment the accounts' active votes for both groups", async () => { assertEqualBN( - await election.getAccountActiveVotesForGroup(group, voter), + await election.getActiveVotesForGroupByAccount(group, voter), expectedVoterActiveVotesForGroup ) assertEqualBN( - await election.getAccountActiveVotesForGroup(group, voter2), + await election.getActiveVotesForGroupByAccount(group, voter2), expectedVoter2ActiveVotesForGroup ) assertEqualBN( - await election.getAccountActiveVotesForGroup(group2, voter2), + await election.getActiveVotesForGroupByAccount(group2, voter2), expectedVoter2ActiveVotesForGroup2 ) }) it("should increment the accounts' total votes for both groups", async () => { assertEqualBN( - await election.getAccountTotalVotesForGroup(group, voter), + await election.getTotalVotesForGroupByAccount(group, voter), expectedVoterActiveVotesForGroup ) assertEqualBN( - await election.getAccountTotalVotesForGroup(group, voter2), + await election.getTotalVotesForGroupByAccount(group, voter2), expectedVoter2ActiveVotesForGroup ) assertEqualBN( - await election.getAccountTotalVotesForGroup(group2, voter2), + await election.getTotalVotesForGroupByAccount(group2, voter2), expectedVoter2ActiveVotesForGroup2 ) }) it("should increment the accounts' total votes", async () => { assertEqualBN( - await election.getAccountTotalVotes(voter), + await election.getTotalVotesByAccount(voter), expectedVoterActiveVotesForGroup ) assertEqualBN( - await election.getAccountTotalVotes(voter2), + await election.getTotalVotesByAccount(voter2), expectedVoter2ActiveVotesForGroup.plus(expectedVoter2ActiveVotesForGroup2) ) }) it('should increment the total votes for the groups', async () => { - assertEqualBN(await election.getGroupTotalVotes(group), expectedGroupTotalActiveVotes) + assertEqualBN(await election.getTotalVotesForGroup(group), expectedGroupTotalActiveVotes) assertEqualBN( - await election.getGroupTotalVotes(group2), + await election.getTotalVotesForGroup(group2), expectedVoter2ActiveVotesForGroup2 ) }) From 115a45734d0f3841c2b037fc57ddc47f6df0dc70 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Wed, 9 Oct 2019 13:38:24 -0700 Subject: [PATCH 040/149] Fix migration --- packages/protocol/migrations/17_elect_validators.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/protocol/migrations/17_elect_validators.ts b/packages/protocol/migrations/17_elect_validators.ts index 4a9d7838907..4d39ba9fb19 100644 --- a/packages/protocol/migrations/17_elect_validators.ts +++ b/packages/protocol/migrations/17_elect_validators.ts @@ -172,11 +172,7 @@ module.exports = async (_deployer: any) => { console.info(' Marking Validator Group as eligible for election ...') // @ts-ignore - const markTx = election.contract.methods.markGroupEligible( - account.address, - NULL_ADDRESS, - NULL_ADDRESS - ) + const markTx = election.contract.methods.markGroupEligible(NULL_ADDRESS, NULL_ADDRESS) await sendTransactionWithPrivateKey(web3, markTx, account.privateKey, { to: election.address, }) From 884773bbd39472d9e08cef593cf335613310b107 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Wed, 9 Oct 2019 15:37:00 -0700 Subject: [PATCH 041/149] Add addFirstMember function --- .../src/e2e-tests/governance_tests.ts | 6 +- packages/celotool/src/e2e-tests/utils.ts | 4 +- .../contracts/governance/Election.sol | 9 +-- .../contracts/governance/Validators.sol | 39 +++++++++-- .../governance/interfaces/IElection.sol | 1 + .../governance/test/MockElection.sol | 5 ++ packages/protocol/test/governance/election.ts | 64 +++++++++++-------- .../protocol/test/governance/validators.ts | 32 ++++++---- 8 files changed, 108 insertions(+), 52 deletions(-) diff --git a/packages/celotool/src/e2e-tests/governance_tests.ts b/packages/celotool/src/e2e-tests/governance_tests.ts index daf8b7c4151..4682d2196f5 100644 --- a/packages/celotool/src/e2e-tests/governance_tests.ts +++ b/packages/celotool/src/e2e-tests/governance_tests.ts @@ -27,7 +27,7 @@ describe('governance tests', () => { before(async function(this: any) { this.timeout(0) - await context.hooks.before() + // await context.hooks.before() }) after(context.hooks.after) @@ -350,10 +350,10 @@ describe('governance tests', () => { expected: BigNumber ) => { const currentVotes = new BigNumber( - await election.methods.getGroupTotalVotes(group).call({}, blockNumber) + await election.methods.getTotalVotesForGroup(group).call({}, blockNumber) ) const previousVotes = new BigNumber( - await election.methods.getGroupTotalVotes(group).call({}, blockNumber - 1) + await election.methods.getTotalVotesForGroup(group).call({}, blockNumber - 1) ) assert.equal(expected.toFixed(), currentVotes.minus(previousVotes).toFixed()) } diff --git a/packages/celotool/src/e2e-tests/utils.ts b/packages/celotool/src/e2e-tests/utils.ts index 98e2b119688..aae32ca88ac 100644 --- a/packages/celotool/src/e2e-tests/utils.ts +++ b/packages/celotool/src/e2e-tests/utils.ts @@ -234,8 +234,8 @@ async function waitForPortOpen(host: string, port: number, seconds: number) { return false } -export async function sleep(seconds: number) { - await execCmd('sleep', [seconds.toString()]) +export function sleep(seconds: number) { + return new Promise((resolve) => setTimeout(resolve, seconds * 1000)) } export async function getEnode(port: number, ws: boolean = false) { diff --git a/packages/protocol/contracts/governance/Election.sol b/packages/protocol/contracts/governance/Election.sol index 3dd5835e6e6..3ef758fb56f 100644 --- a/packages/protocol/contracts/governance/Election.sol +++ b/packages/protocol/contracts/governance/Election.sol @@ -538,24 +538,21 @@ contract Election is IElection, Ownable, ReentrancyGuard, Initializable, UsingRe /** * @notice Marks a group eligible for electing validators. + * @param group The address of the validator group. * @param lesser The address of the group that has received fewer votes than this group. * @param greater The address of the group that has received more votes than this group. */ function markGroupEligible( + address group, address lesser, address greater ) external - nonReentrant - returns (bool) + onlyRegisteredContract(VALIDATORS_REGISTRY_ID) { - address group = getLockedGold().getAccountFromValidator(msg.sender); - require(!votes.total.eligible.contains(group)); - require(getValidators().getGroupNumMembers(group) > 0); uint256 value = getTotalVotesForGroup(group); votes.total.eligible.insert(group, value, lesser, greater); emit ValidatorGroupMarkedEligible(group); - return true; } /** diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index eb65b44969a..5635fb67092 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -570,24 +570,55 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi * @param validator The validator to add to the group * @return True upon success. * @dev Fails if `validator` has not set their affiliation to this account. + * @dev Fails if the group has zero members. */ function addMember(address validator) external nonReentrant returns (bool) { address account = getLockedGold().getAccountFromActiveValidator(msg.sender); - require(isValidatorGroup(account) && isValidator(validator)); - return _addMember(account, validator); + require(groups[account].members.numElements > 0); + return _addMember(account, validator, address(0), address(0)); } /** - * @notice Adds a member to the end of a validator group's list of members. + * @notice Adds the first member to a group's list of members and marks it eligible for election. * @param validator The validator to add to the group + * @param lesser The address of the group that has received fewer votes than this group. + * @param greater The address of the group that has received more votes than this group. * @return True upon success. * @dev Fails if `validator` has not set their affiliation to this account. + * @dev Fails if the group has > 0 members. */ - function _addMember(address group, address validator) private returns (bool) { + function addFirstMember( + address validator, + address lesser, + address greater + ) + external + nonReentrant + returns (bool) + { + address account = getLockedGold().getAccountFromActiveValidator(msg.sender); + require(groups[account].members.numElements == 0); + return _addMember(account, validator, lesser, greater); + } + + /** + * @notice Adds a member to the end of a validator group's list of members. + * @param group The address of the validator group. + * @param validator The validator to add to the group. + * @param lesser The address of the group that has received fewer votes than this group. + * @param greater The address of the group that has received more votes than this group. + * @return True upon success. + * @dev Fails if `validator` has not set their affiliation to this account. + */ + function _addMember(address group, address validator, address lesser, address greater) private returns (bool) { + require(isValidatorGroup(group) && isValidator(validator)); ValidatorGroup storage _group = groups[group]; require(_group.members.numElements < maxGroupSize, "group would exceed maximum size"); require(validators[validator].affiliation == group && !_group.members.contains(validator)); _group.members.push(validator); + if (_group.members.numElements == 1) { + getElection().markGroupEligible(group, lesser, greater); + } updateMembershipHistory(validator, group); emit ValidatorGroupMemberAdded(group, validator); return true; diff --git a/packages/protocol/contracts/governance/interfaces/IElection.sol b/packages/protocol/contracts/governance/interfaces/IElection.sol index 24226b3f7a9..caa2df9a882 100644 --- a/packages/protocol/contracts/governance/interfaces/IElection.sol +++ b/packages/protocol/contracts/governance/interfaces/IElection.sol @@ -5,5 +5,6 @@ interface IElection { function getTotalVotes() external view returns (uint256); function getTotalVotesByAccount(address) external view returns (uint256); function markGroupIneligible(address) external; + function markGroupEligible(address,address,address) external; function electValidators() external view returns (address[] memory); } diff --git a/packages/protocol/contracts/governance/test/MockElection.sol b/packages/protocol/contracts/governance/test/MockElection.sol index 212133020b1..47f46d42ca3 100644 --- a/packages/protocol/contracts/governance/test/MockElection.sol +++ b/packages/protocol/contracts/governance/test/MockElection.sol @@ -8,12 +8,17 @@ import "../interfaces/IElection.sol"; contract MockElection is IElection { mapping(address => bool) public isIneligible; + mapping(address => bool) public isEligible; address[] public electedValidators; function markGroupIneligible(address account) external { isIneligible[account] = true; } + function markGroupEligible(address account, address, address) external { + isEligible[account] = true; + } + function getTotalVotes() external view returns (uint256) { return 0; } diff --git a/packages/protocol/test/governance/election.ts b/packages/protocol/test/governance/election.ts index 5ec5ba423cf..4e4bc7a4bfd 100644 --- a/packages/protocol/test/governance/election.ts +++ b/packages/protocol/test/governance/election.ts @@ -202,15 +202,15 @@ contract('Election', (accounts: string[]) => { describe('#markGroupEligible', () => { const group = accounts[1] - describe('when the group has members', () => { + describe('when called by the registered validators contract', () => { beforeEach(async () => { - await mockValidators.setMembers(group, [accounts[9]]) + await registry.setAddressFor(CeloContractName.Validators, accounts[0]) }) describe('when the group has no votes', () => { let resp: any beforeEach(async () => { - resp = await election.markGroupEligible(NULL_ADDRESS, NULL_ADDRESS, { from: group }) + resp = await election.markGroupEligible(group, NULL_ADDRESS, NULL_ADDRESS) }) it('should add the group to the list of eligible groups', async () => { @@ -230,17 +230,15 @@ contract('Election', (accounts: string[]) => { describe('when the group has already been marked eligible', () => { it('should revert', async () => { - await assertRevert( - election.markGroupEligible(NULL_ADDRESS, NULL_ADDRESS, { from: group }) - ) + await assertRevert(election.markGroupEligible(group, NULL_ADDRESS, NULL_ADDRESS)) }) }) }) }) - describe('when the group has no members', () => { + describe('not called by the registered validators contract', () => { it('should revert', async () => { - await assertRevert(election.markGroupEligible(NULL_ADDRESS, NULL_ADDRESS, { from: group })) + await assertRevert(election.markGroupEligible(group, NULL_ADDRESS, NULL_ADDRESS)) }) }) }) @@ -250,7 +248,9 @@ contract('Election', (accounts: string[]) => { describe('when the group is eligible', () => { beforeEach(async () => { await mockValidators.setMembers(group, [accounts[9]]) - await election.markGroupEligible(NULL_ADDRESS, NULL_ADDRESS, { from: group }) + await registry.setAddressFor(CeloContractName.Validators, accounts[0]) + await election.markGroupEligible(group, NULL_ADDRESS, NULL_ADDRESS) + await registry.setAddressFor(CeloContractName.Validators, mockValidators.address) }) describe('when called by the registered Validators contract', () => { @@ -304,8 +304,9 @@ contract('Election', (accounts: string[]) => { const value = new BigNumber(1000) describe('when the group is eligible', () => { beforeEach(async () => { - await mockValidators.setMembers(group, [accounts[9]]) - await election.markGroupEligible(NULL_ADDRESS, NULL_ADDRESS, { from: group }) + await registry.setAddressFor(CeloContractName.Validators, accounts[0]) + await election.markGroupEligible(group, NULL_ADDRESS, NULL_ADDRESS) + await registry.setAddressFor(CeloContractName.Validators, mockValidators.address) }) describe('when the group can receive votes', () => { @@ -381,8 +382,9 @@ contract('Election', (accounts: string[]) => { await mockLockedGold.incrementNonvotingAccountBalance(voter, value) for (let i = 0; i < maxNumGroupsVotedFor.toNumber(); i++) { newGroup = accounts[i + 2] - await mockValidators.setMembers(newGroup, [accounts[9]]) - await election.markGroupEligible(group, NULL_ADDRESS, { from: newGroup }) + await registry.setAddressFor(CeloContractName.Validators, accounts[0]) + await election.markGroupEligible(newGroup, group, NULL_ADDRESS) + await registry.setAddressFor(CeloContractName.Validators, mockValidators.address) await election.vote(newGroup, 1, group, NULL_ADDRESS) } }) @@ -398,6 +400,7 @@ contract('Election', (accounts: string[]) => { describe('when the group cannot receive votes', () => { beforeEach(async () => { await mockLockedGold.setTotalLockedGold(value.div(2).minus(1)) + await mockValidators.setMembers(group, [accounts[9]]) await mockValidators.setNumRegisteredValidators(1) assertEqualBN(await election.getNumVotesReceivable(group), value.minus(2)) }) @@ -420,9 +423,11 @@ contract('Election', (accounts: string[]) => { const group = accounts[1] const value = 1000 beforeEach(async () => { - await mockValidators.setMembers(group, [accounts[9]]) - await election.markGroupEligible(NULL_ADDRESS, NULL_ADDRESS, { from: group }) + await registry.setAddressFor(CeloContractName.Validators, accounts[0]) + await election.markGroupEligible(group, NULL_ADDRESS, NULL_ADDRESS) + await registry.setAddressFor(CeloContractName.Validators, mockValidators.address) await mockLockedGold.setTotalLockedGold(value) + await mockValidators.setMembers(group, [accounts[9]]) await mockValidators.setNumRegisteredValidators(1) await mockLockedGold.incrementNonvotingAccountBalance(voter, value) }) @@ -544,8 +549,9 @@ contract('Election', (accounts: string[]) => { const value = 1000 describe('when the voter has pending votes', () => { beforeEach(async () => { - await mockValidators.setMembers(group, [accounts[9]]) - await election.markGroupEligible(NULL_ADDRESS, NULL_ADDRESS, { from: group }) + await registry.setAddressFor(CeloContractName.Validators, accounts[0]) + await election.markGroupEligible(group, NULL_ADDRESS, NULL_ADDRESS) + await registry.setAddressFor(CeloContractName.Validators, mockValidators.address) await mockLockedGold.setTotalLockedGold(value) await mockValidators.setNumRegisteredValidators(1) await mockLockedGold.incrementNonvotingAccountBalance(voter, value) @@ -644,8 +650,9 @@ contract('Election', (accounts: string[]) => { const value = 1000 describe('when the voter has active votes', () => { beforeEach(async () => { - await mockValidators.setMembers(group, [accounts[9]]) - await election.markGroupEligible(NULL_ADDRESS, NULL_ADDRESS, { from: group }) + await registry.setAddressFor(CeloContractName.Validators, accounts[0]) + await election.markGroupEligible(group, NULL_ADDRESS, NULL_ADDRESS) + await registry.setAddressFor(CeloContractName.Validators, mockValidators.address) await mockLockedGold.setTotalLockedGold(value) await mockValidators.setNumRegisteredValidators(1) await mockLockedGold.incrementNonvotingAccountBalance(voter, value) @@ -769,9 +776,11 @@ contract('Election', (accounts: string[]) => { await mockValidators.setMembers(group2, [validator5, validator6]) await mockValidators.setMembers(group3, [validator7]) - await election.markGroupEligible(NULL_ADDRESS, NULL_ADDRESS, { from: group1 }) - await election.markGroupEligible(NULL_ADDRESS, group1, { from: group2 }) - await election.markGroupEligible(NULL_ADDRESS, group2, { from: group3 }) + await registry.setAddressFor(CeloContractName.Validators, accounts[0]) + await election.markGroupEligible(group1, NULL_ADDRESS, NULL_ADDRESS) + await election.markGroupEligible(group2, NULL_ADDRESS, group1) + await election.markGroupEligible(group3, NULL_ADDRESS, group2) + await registry.setAddressFor(CeloContractName.Validators, mockValidators.address) for (const voter of [voter1, voter2, voter3]) { await mockLockedGold.incrementNonvotingAccountBalance(voter.address, voter.weight) @@ -876,9 +885,11 @@ contract('Election', (accounts: string[]) => { const voteValue = new BigNumber(1000000) const rewardValue = new BigNumber(1000000) beforeEach(async () => { - await mockValidators.setMembers(group, [accounts[9]]) - await election.markGroupEligible(NULL_ADDRESS, NULL_ADDRESS, { from: group }) + await registry.setAddressFor(CeloContractName.Validators, accounts[0]) + await election.markGroupEligible(group, NULL_ADDRESS, NULL_ADDRESS) + await registry.setAddressFor(CeloContractName.Validators, mockValidators.address) await mockLockedGold.setTotalLockedGold(voteValue) + await mockValidators.setMembers(group, [accounts[9]]) await mockValidators.setNumRegisteredValidators(1) await mockLockedGold.incrementNonvotingAccountBalance(voter, voteValue) await election.vote(group, voteValue, NULL_ADDRESS, NULL_ADDRESS) @@ -926,8 +937,9 @@ contract('Election', (accounts: string[]) => { const voteValue2 = new BigNumber(1000000) const rewardValue2 = new BigNumber(10000000) beforeEach(async () => { - await mockValidators.setMembers(group2, [accounts[8]]) - await election.markGroupEligible(NULL_ADDRESS, group, { from: group2 }) + await registry.setAddressFor(CeloContractName.Validators, accounts[0]) + await election.markGroupEligible(group2, NULL_ADDRESS, group) + await registry.setAddressFor(CeloContractName.Validators, mockValidators.address) await mockLockedGold.setTotalLockedGold(voteValue.plus(voteValue2)) await mockValidators.setNumRegisteredValidators(2) await mockLockedGold.incrementNonvotingAccountBalance(voter2, voteValue2) diff --git a/packages/protocol/test/governance/validators.ts b/packages/protocol/test/governance/validators.ts index b5768e021d2..94ddc7295e5 100644 --- a/packages/protocol/test/governance/validators.ts +++ b/packages/protocol/test/governance/validators.ts @@ -131,7 +131,11 @@ contract('Validators', (accounts: string[]) => { for (const validator of members) { await registerValidator(validator) await validators.affiliate(group, { from: validator }) - await validators.addMember(validator, { from: group }) + if (validator == members[0]) { + await validators.addFirstMember(validator, NULL_ADDRESS, NULL_ADDRESS, { from: group }) + } else { + await validators.addMember(validator, { from: group }) + } } } @@ -676,7 +680,7 @@ contract('Validators', (accounts: string[]) => { describe('when the validator is a member of that group', () => { beforeEach(async () => { - await validators.addMember(validator, { from: group }) + await validators.addFirstMember(validator, NULL_ADDRESS, NULL_ADDRESS, { from: group }) }) it('should remove the validator from the group membership list', async () => { @@ -784,7 +788,7 @@ contract('Validators', (accounts: string[]) => { describe('when the validator is a member of that group', () => { beforeEach(async () => { - await validators.addMember(validator, { from: group }) + await validators.addFirstMember(validator, NULL_ADDRESS, NULL_ADDRESS, { from: group }) }) it('should remove the validator from the group membership list', async () => { @@ -854,7 +858,7 @@ contract('Validators', (accounts: string[]) => { describe('when the validator is a member of the affiliated group', () => { beforeEach(async () => { - await validators.addMember(validator, { from: group }) + await validators.addFirstMember(validator, NULL_ADDRESS, NULL_ADDRESS, { from: group }) }) it('should remove the validator from the group membership list', async () => { @@ -1029,7 +1033,7 @@ contract('Validators', (accounts: string[]) => { await registerValidatorGroup(group) await registerValidator(validator) await validators.affiliate(group, { from: validator }) - await validators.addMember(validator) + await validators.addFirstMember(validator, NULL_ADDRESS, NULL_ADDRESS) }) it('should revert', async () => { @@ -1046,7 +1050,7 @@ contract('Validators', (accounts: string[]) => { await registerValidator(validator) await registerValidatorGroup(group) await validators.affiliate(group, { from: validator }) - resp = await validators.addMember(validator) + resp = await validators.addFirstMember(validator, NULL_ADDRESS, NULL_ADDRESS) }) it('should add the member to the list of members', async () => { @@ -1078,11 +1082,13 @@ contract('Validators', (accounts: string[]) => { }) it('should revert when the account is not a registered validator group', async () => { - await assertRevert(validators.addMember(validator, { from: accounts[2] })) + await assertRevert( + validators.addFirstMember(validator, NULL_ADDRESS, NULL_ADDRESS, { from: accounts[2] }) + ) }) it('should revert when the member is not a registered validator', async () => { - await assertRevert(validators.addMember(accounts[2])) + await assertRevert(validators.addFirstMember(accounts[2], NULL_ADDRESS, NULL_ADDRESS)) }) it('should revert when trying to add too many members to group', async () => { @@ -1098,7 +1104,7 @@ contract('Validators', (accounts: string[]) => { }) it('should revert', async () => { - await assertRevert(validators.addMember(validator)) + await assertRevert(validators.addFirstMember(validator, NULL_ADDRESS, NULL_ADDRESS)) }) }) @@ -1293,7 +1299,9 @@ contract('Validators', (accounts: string[]) => { const currentEpoch = new BigNumber( Math.floor((await web3.eth.getBlock('latest')).number / EPOCH) ) - await validators.addMember(validator, { from: groups[i] }) + await validators.addFirstMember(validator, NULL_ADDRESS, NULL_ADDRESS, { + from: groups[i], + }) await mineBlocks(EPOCH, web3) const membershipHistory = await validators.getMembershipHistory(validator) @@ -1332,7 +1340,9 @@ contract('Validators', (accounts: string[]) => { it('should always return the correct membership for the last epoch', async () => { for (let i = 0; i < membershipHistoryLength.plus(1).toNumber(); i++) { await validators.affiliate(groups[i]) - await validators.addMember(validator, { from: groups[i] }) + await validators.addFirstMember(validator, NULL_ADDRESS, NULL_ADDRESS, { + from: groups[i], + }) if (i > 0) { assert.equal(await validators.getMembershipInLastEpoch(validator), groups[i - 1]) } From b2858d07035318dfac4f4fe93a14150353d0f043 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Wed, 9 Oct 2019 18:05:13 -0700 Subject: [PATCH 042/149] Rework balance requirements --- packages/contractkit/src/wrappers/Election.ts | 6 +- .../contractkit/src/wrappers/LockedGold.ts | 16 ++- .../contracts/governance/Election.sol | 40 +++--- .../contracts/governance/LockedGold.sol | 38 +----- .../contracts/governance/Validators.sol | 94 +++++++++---- .../governance/interfaces/ILockedGold.sol | 1 - .../governance/interfaces/IValidators.sol | 1 + .../governance/test/MockElection.sol | 2 +- .../governance/test/MockLockedGold.sol | 24 ---- .../governance/test/MockValidators.sol | 9 ++ .../protocol/test/governance/lockedgold.ts | 35 +++-- .../protocol/test/governance/validators.ts | 127 +++++++++--------- 12 files changed, 197 insertions(+), 196 deletions(-) diff --git a/packages/contractkit/src/wrappers/Election.ts b/packages/contractkit/src/wrappers/Election.ts index 89403022f49..860164238d5 100644 --- a/packages/contractkit/src/wrappers/Election.ts +++ b/packages/contractkit/src/wrappers/Election.ts @@ -67,7 +67,11 @@ export class ElectionWrapper extends BaseWrapper { undefined, toBigNumber ) - validatorAddressFromCurrentSet = proxyCall(this.contract.methods.validatorAddressFromCurrentSet) + validatorAddressFromCurrentSet: (index: number) => Promise
= proxyCall( + this.contract.methods.validatorAddressFromCurrentSet, + tupleParser(identity) + ) + numberValidatorsInCurrentSet = proxyCall( this.contract.methods.numberValidatorsInCurrentSet, undefined, diff --git a/packages/contractkit/src/wrappers/LockedGold.ts b/packages/contractkit/src/wrappers/LockedGold.ts index 62a53657862..d636d276649 100644 --- a/packages/contractkit/src/wrappers/LockedGold.ts +++ b/packages/contractkit/src/wrappers/LockedGold.ts @@ -45,11 +45,21 @@ export interface LockedGoldConfig { * Contract for handling deposits needed for voting. */ export class LockedGoldWrapper extends BaseWrapper { - unlock = proxySend(this.kit, this.contract.methods.unlock) + unlock: (value: NumberLike) => CeloTransactionObject = proxySend( + this.kit, + this.contract.methods.unlock, + tupleParser(parseNumber) + ) createAccount = proxySend(this.kit, this.contract.methods.createAccount) - withdraw = proxySend(this.kit, this.contract.methods.withdraw) + withdraw: (index: number) => CeloTransactionObject = proxySend( + this.kit, + this.contract.methods.withdraw + ) lock = proxySend(this.kit, this.contract.methods.lock) - relock = proxySend(this.kit, this.contract.methods.relock) + relock: (index: number) => CeloTransactionObject = proxySend( + this.kit, + this.contract.methods.relock + ) getAccountTotalLockedGold = proxyCall( this.contract.methods.getAccountTotalLockedGold, diff --git a/packages/protocol/contracts/governance/Election.sol b/packages/protocol/contracts/governance/Election.sol index 88b27f88028..b222873d852 100644 --- a/packages/protocol/contracts/governance/Election.sol +++ b/packages/protocol/contracts/governance/Election.sol @@ -18,7 +18,7 @@ contract Election is IElection, Ownable, ReentrancyGuard, Initializable, UsingRe using FixidityLib for FixidityLib.Fraction; using SafeMath for uint256; - struct TimestampedVote { + struct PendingVote { // The value of the vote, in gold. uint256 value; // The latest block number at which the vote was cast. @@ -29,7 +29,7 @@ contract Election is IElection, Ownable, ReentrancyGuard, Initializable, UsingRe // The total number of pending votes that have been cast for this group. uint256 total; // Pending votes cast per voter. - mapping(address => TimestampedVote) byAccount; + mapping(address => PendingVote) byAccount; } // Pending votes are those for which no following elections have been held. @@ -547,9 +547,9 @@ contract Election is IElection, Ownable, ReentrancyGuard, Initializable, UsingRe GroupPendingVotes storage groupPending = pending.forGroup[group]; groupPending.total = groupPending.total.add(value); - TimestampedVote storage timestampedVote = groupPending.byAccount[account]; - timestampedVote.value = timestampedVote.value.add(value); - timestampedVote.blockNumber = block.number; + PendingVote storage pendingVote = groupPending.byAccount[account]; + pendingVote.value = pendingVote.value.add(value); + pendingVote.blockNumber = block.number; } /** @@ -565,10 +565,10 @@ contract Election is IElection, Ownable, ReentrancyGuard, Initializable, UsingRe GroupPendingVotes storage groupPending = pending.forGroup[group]; groupPending.total = groupPending.total.sub(value); - TimestampedVote storage timestampedVote = groupPending.byAccount[account]; - timestampedVote.value = timestampedVote.value.sub(value); - if (timestampedVote.value == 0) { - timestampedVote.blockNumber = 0; + PendingVote storage pendingVote = groupPending.byAccount[account]; + pendingVote.value = pendingVote.value.sub(value); + if (pendingVote.value == 0) { + pendingVote.blockNumber = 0; } } @@ -655,6 +655,8 @@ contract Election is IElection, Ownable, ReentrancyGuard, Initializable, UsingRe * @dev Votes are not allowed to be cast that would increase a group's proportion of locked gold * voting for it to greater than * (numGroupMembers + 1) / min(maxElectableValidators, numRegisteredValidators) + * @dev Note that groups may still receive additional votes via rewards even if this function + * returns false. */ function canReceiveVotes(address group, uint256 value) public view returns (bool) { uint256 totalVotesForGroup = getTotalVotesForGroup(group).add(value); @@ -677,6 +679,7 @@ contract Election is IElection, Ownable, ReentrancyGuard, Initializable, UsingRe * @dev Votes are not allowed to be cast that would increase a group's proportion of locked gold * voting for it to greater than * (numGroupMembers + 1) / min(maxElectableValidators, numRegisteredValidators) + * @dev Note that a group's vote total may exceed this number through rewards or config changes. */ function getNumVotesReceivable(address group) external view returns (uint256) { uint256 numerator = getValidators().getGroupNumMembers(group).add(1).mul( @@ -789,14 +792,7 @@ contract Election is IElection, Ownable, ReentrancyGuard, Initializable, UsingRe } } // Shuffle the validator set using validator-supplied entropy - bytes32 r = getRandom().random(); - for (uint256 i = electedValidators.length - 1; i > 0; i = i.sub(1)) { - uint256 j = uint256(r) % (i + 1); - (electedValidators[i], electedValidators[j]) = (electedValidators[j], electedValidators[i]); - r = keccak256(abi.encodePacked(r)); - } - - return electedValidators; + return shuffleArray(electedValidators); } /** @@ -837,4 +833,14 @@ contract Election is IElection, Ownable, ReentrancyGuard, Initializable, UsingRe } return (groupIndex, memberElected); } + + function shuffleArray(address[] memory array) private view returns (address[] memory) { + bytes32 r = getRandom().random(); + for (uint256 i = array.length - 1; i > 0; i = i.sub(1)) { + uint256 j = uint256(r) % (i + 1); + (array[i], array[j]) = (array[j], array[i]); + r = keccak256(abi.encodePacked(r)); + } + return array; + } } diff --git a/packages/protocol/contracts/governance/LockedGold.sol b/packages/protocol/contracts/governance/LockedGold.sol index 7ec0cdbe6b6..fcc81a120dc 100644 --- a/packages/protocol/contracts/governance/LockedGold.sol +++ b/packages/protocol/contracts/governance/LockedGold.sol @@ -14,13 +14,6 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr using SafeMath for uint256; - struct MustMaintain { - // The Locked Gold balance that the account must maintain. - uint256 value; - // The timestamp at which the account is no longer subject to these constraints. - uint256 timestamp; - } - struct Authorizations { // The address that is authorized to vote on behalf of the account. address voting; @@ -41,8 +34,6 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr uint256 nonvoting; // Gold that has been unlocked and will become available for withdrawal. PendingWithdrawal[] pendingWithdrawals; - // Balance requirements imposed on this account. - MustMaintain requirements; } struct Account { @@ -65,7 +56,6 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr event GoldLocked(address indexed account, uint256 value); event GoldUnlocked(address indexed account, uint256 value, uint256 available); event GoldWithdrawn(address indexed account, uint256 value); - event AccountMustMaintainSet(address indexed account, uint256 value, uint256 timestamp); function initialize(address registryAddress, uint256 _unlockingPeriod) external initializer { _transferOwnership(msg.sender); @@ -197,11 +187,8 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr function unlock(uint256 value) external nonReentrant { require(isAccount(msg.sender)); Account storage account = accounts[msg.sender]; - MustMaintain storage requirement = account.balances.requirements; - require( - now >= requirement.timestamp || - getAccountTotalLockedGold(msg.sender).sub(value) >= requirement.value - ); + uint256 balanceRequirement = getValidators().getAccountBalanceRequirement(msg.sender); + require(balanceRequirement <= getAccountTotalLockedGold(msg.sender).sub(value)); _decrementNonvotingAccountBalance(msg.sender, value); uint256 available = now.add(unlockingPeriod); account.balances.pendingWithdrawals.push(PendingWithdrawal(value, available)); @@ -239,27 +226,6 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr emit GoldWithdrawn(msg.sender, value); } - /** - * @notice Sets account locked gold balance requirements. - * @param account The account for which to set balance requirements. - * @param value The value that the account must maintain. - * @param timestamp The timestamp after which the account no longer must maintain this balance. - * @dev Can only be called by the registered "Validators" smart contract. - */ - function setAccountMustMaintain( - address account, - uint256 value, - uint256 timestamp - ) - public - onlyRegisteredContract(VALIDATORS_REGISTRY_ID) - nonReentrant - returns (bool) - { - accounts[account].balances.requirements = MustMaintain(value, timestamp); - emit AccountMustMaintainSet(account, value, timestamp); - } - // TODO(asa): Dedup /** * @notice Returns the account associated with `accountOrVoter`. diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index d4bdec86fe5..7ef9cccfb51 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -28,16 +28,33 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi address constant PROOF_OF_POSSESSION = address(0xff - 4); uint256 constant MAX_INT = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; - struct RegistrationRequirements { + // If an account has not registered a validator or group, these values represent the minimum + // amount of Locked Gold required to do so. + // If an account has a registered a validator or validator group, these values represent the + // minimum amount of Locked Gold required in order to earn epoch rewards. Furthermore, the + // account will not be able to unlock Gold if it would cause the account to fall below + // these values. + // If an account has deregistered a validator or validator group and is still subject to the + // `DeregistrationLockup`, the account will not be able to unlock Gold if it would cause the + // account to fall below these values. + struct BalanceRequirements { uint256 group; uint256 validator; } + // After deregistering a validator or validator group, the account will remain subject to the + // current balance requirements for this long (in seconds). struct DeregistrationLockups { uint256 group; uint256 validator; } + // Stores the timestamps at which deregistration of a validator or validator group occurred. + struct DeregistrationTimestamps { + uint256 group; + uint256 validator; + } + struct ValidatorGroup { string name; string url; @@ -55,9 +72,10 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi mapping(address => ValidatorGroup) private groups; mapping(address => Validator) private validators; + mapping(address => DeregistrationTimestamps) private deregistrationTimestamps; address[] private _groups; address[] private _validators; - RegistrationRequirements public registrationRequirements; + BalanceRequirements public balanceRequirements; DeregistrationLockups public deregistrationLockups; uint256 public maxGroupSize; @@ -65,7 +83,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi uint256 size ); - event RegistrationRequirementsSet( + event BalanceRequirementsSet( uint256 group, uint256 validator ); @@ -144,7 +162,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi { _transferOwnership(msg.sender); setRegistry(registryAddress); - registrationRequirements = RegistrationRequirements(groupRequirement, validatorRequirement); + balanceRequirements = BalanceRequirements(groupRequirement, validatorRequirement); deregistrationLockups = DeregistrationLockups(groupLockup, validatorLockup); maxGroupSize = _maxGroupSize; } @@ -176,7 +194,9 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi * @return True upon success. * @dev The new requirement is only enforced for future validator or group registrations. */ - function setRegistrationRequirements( + // TODO(asa): Allow validators to adjust their LockedGold MustMaintain if the registration + // requirements fall. + function setBalanceRequirements( uint256 groupRequirement, uint256 validatorRequirement ) @@ -185,11 +205,11 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi returns (bool) { require( - groupRequirement != registrationRequirements.group || - validatorRequirement != registrationRequirements.validator + groupRequirement != balanceRequirements.group || + validatorRequirement != balanceRequirements.validator ); - registrationRequirements = RegistrationRequirements(groupRequirement, validatorRequirement); - emit RegistrationRequirementsSet(groupRequirement, validatorRequirement); + balanceRequirements = BalanceRequirements(groupRequirement, validatorRequirement); + emit BalanceRequirementsSet(groupRequirement, validatorRequirement); return true; } @@ -251,11 +271,10 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi address account = getLockedGold().getAccountFromValidator(msg.sender); require(!isValidator(account) && !isValidatorGroup(account)); - require(meetsValidatorRegistrationRequirement(account)); + require(meetsValidatorBalanceRequirements(account)); validators[account] = Validator(name, url, publicKeysData, address(0)); _validators.push(account); - getLockedGold().setAccountMustMaintain(account, registrationRequirements.validator, MAX_INT); emit ValidatorRegistered(account, name, url, publicKeysData); return true; } @@ -276,8 +295,8 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi * @param account The account. * @return Whether an account meets the requirements to register a validator. */ - function meetsValidatorRegistrationRequirement(address account) public view returns (bool) { - return getLockedGold().getAccountTotalLockedGold(account) >= registrationRequirements.validator; + function meetsValidatorBalanceRequirements(address account) public view returns (bool) { + return getLockedGold().getAccountTotalLockedGold(account) >= balanceRequirements.validator; } /** @@ -285,8 +304,8 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi * @param account The account. * @return Whether an account meets the requirements to register a group. */ - function meetsValidatorGroupRegistrationRequirement(address account) public view returns (bool) { - return getLockedGold().getAccountTotalLockedGold(account) >= registrationRequirements.group; + function meetsValidatorGroupBalanceRequirements(address account) public view returns (bool) { + return getLockedGold().getAccountTotalLockedGold(account) >= balanceRequirements.group; } /** @@ -304,11 +323,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi } delete validators[account]; deleteElement(_validators, account, index); - getLockedGold().setAccountMustMaintain( - account, - registrationRequirements.validator, - now.add(deregistrationLockups.validator) - ); + deregistrationTimestamps[account].validator = now; emit ValidatorDeregistered(account); return true; } @@ -367,14 +382,13 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi require(commission <= FixidityLib.fixed1().unwrap(), "Commission can't be greater than 100%"); address account = getLockedGold().getAccountFromValidator(msg.sender); require(!isValidator(account) && !isValidatorGroup(account)); - require(meetsValidatorGroupRegistrationRequirement(account)); + require(meetsValidatorGroupBalanceRequirements(account)); ValidatorGroup storage group = groups[account]; group.name = name; group.url = url; group.commission = FixidityLib.wrap(commission); _groups.push(account); - getLockedGold().setAccountMustMaintain(account, registrationRequirements.group, MAX_INT); emit ValidatorGroupRegistered(account, name, url); return true; } @@ -391,11 +405,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi require(isValidatorGroup(account) && groups[account].members.numElements == 0); delete groups[account]; deleteElement(_groups, account, index); - getLockedGold().setAccountMustMaintain( - account, - registrationRequirements.group, - now.add(deregistrationLockups.group) - ); + deregistrationTimestamps[account].group = now; emit ValidatorGroupDeregistered(account); return true; } @@ -467,6 +477,32 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi return true; } + /** + * @notice Returns the locked gold balance requirement for the supplied account. + * @param account The account that may have to meet locked gold balance requirements. + * @return The locked gold balance requirement for the supplied account. + */ + function getAccountBalanceRequirement(address account) external view returns (uint256) { + DeregistrationTimestamps storage timestamps = deregistrationTimestamps[account]; + if ( + isValidator(account) || + (timestamps.validator > 0 && now < timestamps.validator.add(deregistrationLockups.validator)) + ) { + return balanceRequirements.validator; + } + if ( + isValidatorGroup(account) || + (timestamps.group > 0 && now < timestamps.group.add(deregistrationLockups.group)) + ) { + return balanceRequirements.group; + } + return 0; + } + + function getDeregistrationTimestamps(address account) external view returns (uint256, uint256) { + return (deregistrationTimestamps[account].group, deregistrationTimestamps[account].validator); + } + /** * @notice Returns validator information. * @param account The account that registered the validator. @@ -574,8 +610,8 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi * @notice Returns the Locked Gold requirements to register a validator or group. * @return The locked gold requirements to register a validator or group. */ - function getRegistrationRequirements() external view returns (uint256, uint256) { - return (registrationRequirements.group, registrationRequirements.validator); + function getBalanceRequirements() external view returns (uint256, uint256) { + return (balanceRequirements.group, balanceRequirements.validator); } /** diff --git a/packages/protocol/contracts/governance/interfaces/ILockedGold.sol b/packages/protocol/contracts/governance/interfaces/ILockedGold.sol index 1f961197356..d9a25b9ab64 100644 --- a/packages/protocol/contracts/governance/interfaces/ILockedGold.sol +++ b/packages/protocol/contracts/governance/interfaces/ILockedGold.sol @@ -9,5 +9,4 @@ interface ILockedGold { function decrementNonvotingAccountBalance(address, uint256) external; function getAccountTotalLockedGold(address) external view returns (uint256); function getTotalLockedGold() external view returns (uint256); - function setAccountMustMaintain(address, uint256, uint256) external returns (bool); } diff --git a/packages/protocol/contracts/governance/interfaces/IValidators.sol b/packages/protocol/contracts/governance/interfaces/IValidators.sol index 5bc937ee60f..fa931072877 100644 --- a/packages/protocol/contracts/governance/interfaces/IValidators.sol +++ b/packages/protocol/contracts/governance/interfaces/IValidators.sol @@ -2,6 +2,7 @@ pragma solidity ^0.5.3; interface IValidators { + function getAccountBalanceRequirement(address) external view returns (uint256); function getGroupNumMembers(address) external view returns (uint256); function getGroupsNumMembers(address[] calldata) external view returns (uint256[] memory); function getNumRegisteredValidators() external view returns (uint256); diff --git a/packages/protocol/contracts/governance/test/MockElection.sol b/packages/protocol/contracts/governance/test/MockElection.sol index da4a5e34e18..212133020b1 100644 --- a/packages/protocol/contracts/governance/test/MockElection.sol +++ b/packages/protocol/contracts/governance/test/MockElection.sol @@ -18,7 +18,7 @@ contract MockElection is IElection { return 0; } - function getAccountTotalVotes(address) external view returns (uint256) { + function getTotalVotesByAccount(address) external view returns (uint256) { return 0; } diff --git a/packages/protocol/contracts/governance/test/MockLockedGold.sol b/packages/protocol/contracts/governance/test/MockLockedGold.sol index 970e8079d33..66970442359 100644 --- a/packages/protocol/contracts/governance/test/MockLockedGold.sol +++ b/packages/protocol/contracts/governance/test/MockLockedGold.sol @@ -12,19 +12,12 @@ contract MockLockedGold is ILockedGold { using SafeMath for uint256; - struct MustMaintain { - uint256 value; - uint256 timestamp; - } - struct Authorizations { address validator; address voter; } mapping(address => uint256) public accountTotalLockedGold; - // TODO(asa): Rename to minimumBalance - mapping(address => MustMaintain) public mustMaintain; mapping(address => uint256) public nonvotingAccountBalance; mapping(address => address) public authorizedValidators; uint256 private totalLockedGold; @@ -54,23 +47,6 @@ contract MockLockedGold is ILockedGold { nonvotingAccountBalance[account] = nonvotingAccountBalance[account].sub(value); } - function setAccountMustMaintain( - address account, - uint256 value, - uint256 timestamp - ) - external - returns (bool) - { - mustMaintain[account] = MustMaintain(value, timestamp); - return true; - } - - function getAccountMustMaintain(address account) external view returns (uint256, uint256) { - MustMaintain storage m = mustMaintain[account]; - return (m.value, m.timestamp); - } - function setAccountTotalLockedGold(address account, uint256 value) external { accountTotalLockedGold[account] = value; } diff --git a/packages/protocol/contracts/governance/test/MockValidators.sol b/packages/protocol/contracts/governance/test/MockValidators.sol index 68514b13a1d..2f642113fe8 100644 --- a/packages/protocol/contracts/governance/test/MockValidators.sol +++ b/packages/protocol/contracts/governance/test/MockValidators.sol @@ -10,6 +10,7 @@ contract MockValidators is IValidators { mapping(address => bool) private _isValidating; mapping(address => bool) private _isVoting; mapping(address => uint256) private numGroupMembers; + mapping(address => uint256) private balanceRequirements; mapping(address => address[]) private members; uint256 private numRegisteredValidators; @@ -45,6 +46,14 @@ contract MockValidators is IValidators { members[group] = _members; } + function setAccountBalanceRequirement(address account, uint256 value) external { + balanceRequirements[account] = value; + } + + function getAccountBalanceRequirement(address account) external view returns (uint256) { + return balanceRequirements[account]; + } + function getTopValidatorsFromGroup( address group, uint256 n diff --git a/packages/protocol/test/governance/lockedgold.ts b/packages/protocol/test/governance/lockedgold.ts index 6b25af9714f..befc8827ba0 100644 --- a/packages/protocol/test/governance/lockedgold.ts +++ b/packages/protocol/test/governance/lockedgold.ts @@ -10,18 +10,21 @@ import BigNumber from 'bignumber.js' import { LockedGoldContract, LockedGoldInstance, - MockGoldTokenContract, - MockGoldTokenInstance, MockElectionContract, MockElectionInstance, + MockGoldTokenContract, + MockGoldTokenInstance, + MockValidatorsContract, + MockValidatorsInstance, RegistryContract, RegistryInstance, } from 'types' const LockedGold: LockedGoldContract = artifacts.require('LockedGold') -const Registry: RegistryContract = artifacts.require('Registry') -const MockGoldToken: MockGoldTokenContract = artifacts.require('MockGoldToken') const MockElection: MockElectionContract = artifacts.require('MockElection') +const MockGoldToken: MockGoldTokenContract = artifacts.require('MockGoldToken') +const MockValidators: MockValidatorsContract = artifacts.require('MockValidators') +const Registry: RegistryContract = artifacts.require('Registry') // @ts-ignore // TODO(mcortesi): Use BN @@ -36,8 +39,9 @@ contract('LockedGold', (accounts: string[]) => { const nonOwner = accounts[1] const unlockingPeriod = 3 * DAY let lockedGold: LockedGoldInstance - let registry: RegistryInstance let mockElection: MockElectionInstance + let mockValidators: MockValidatorsInstance + let registry: RegistryInstance const capitalize = (s: string) => { return s.charAt(0).toUpperCase() + s.slice(1) @@ -56,11 +60,13 @@ contract('LockedGold', (accounts: string[]) => { beforeEach(async () => { const mockGoldToken: MockGoldTokenInstance = await MockGoldToken.new() - mockElection = await MockElection.new() lockedGold = await LockedGold.new() + mockElection = await MockElection.new() + mockValidators = await MockValidators.new() registry = await Registry.new() await registry.setAddressFor(CeloContractName.GoldToken, mockGoldToken.address) await registry.setAddressFor(CeloContractName.Election, mockElection.address) + await registry.setAddressFor(CeloContractName.Validators, mockValidators.address) await lockedGold.initialize(registry.address, unlockingPeriod) await lockedGold.createAccount() @@ -338,15 +344,11 @@ contract('LockedGold', (accounts: string[]) => { }) describe('when there are balance requirements', () => { - let mustMaintain: any + const balanceRequirement = 10 beforeEach(async () => { // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails await lockedGold.lock({ value }) - // Allow ourselves to call `setAccountMustMaintain()` - await registry.setAddressFor(CeloContractName.Validators, account) - const timestamp = (await web3.eth.getBlock('latest')).timestamp - mustMaintain = { value: 100, timestamp: timestamp + DAY } - await lockedGold.setAccountMustMaintain(account, mustMaintain.value, mustMaintain.timestamp) + await mockValidators.setAccountBalanceRequirement(account, balanceRequirement) }) describe('when unlocking would yield a locked gold balance less than the required value', () => { @@ -355,18 +357,11 @@ contract('LockedGold', (accounts: string[]) => { await assertRevert(lockedGold.unlock(value)) }) }) - - describe('when the the current time is later than the requirement time', () => { - it('should succeed', async () => { - await timeTravel(DAY, web3) - await lockedGold.unlock(value) - }) - }) }) describe('when unlocking would yield a locked gold balance equal to the required value', () => { it('should succeed', async () => { - await lockedGold.unlock(value - mustMaintain.value) + await lockedGold.unlock(value - balanceRequirement) }) }) }) diff --git a/packages/protocol/test/governance/validators.ts b/packages/protocol/test/governance/validators.ts index df5d6d23970..e11ce9ee871 100644 --- a/packages/protocol/test/governance/validators.ts +++ b/packages/protocol/test/governance/validators.ts @@ -46,7 +46,6 @@ const parseValidatorGroupParams = (groupParams: any) => { const HOUR = 60 * 60 const DAY = 24 * HOUR -const MAX_UINT256 = new BigNumber(2).pow(256).minus(1) contract('Validators', (accounts: string[]) => { let validators: ValidatorsInstance @@ -64,7 +63,7 @@ contract('Validators', (accounts: string[]) => { const publicKeysData = '0x' + publicKey + blsPublicKey + blsPoP const nonOwner = accounts[1] - const registrationRequirements = { group: new BigNumber(1000), validator: new BigNumber(100) } + const balanceRequirements = { group: new BigNumber(1000), validator: new BigNumber(100) } const deregistrationLockups = { group: new BigNumber(100 * DAY), validator: new BigNumber(60 * DAY), @@ -82,8 +81,8 @@ contract('Validators', (accounts: string[]) => { await registry.setAddressFor(CeloContractName.Election, mockElection.address) await validators.initialize( registry.address, - registrationRequirements.group, - registrationRequirements.validator, + balanceRequirements.group, + balanceRequirements.validator, deregistrationLockups.group, deregistrationLockups.validator, maxGroupSize @@ -91,7 +90,7 @@ contract('Validators', (accounts: string[]) => { }) const registerValidator = async (validator: string) => { - await mockLockedGold.setAccountTotalLockedGold(validator, registrationRequirements.validator) + await mockLockedGold.setAccountTotalLockedGold(validator, balanceRequirements.validator) await validators.registerValidator( name, url, @@ -102,7 +101,7 @@ contract('Validators', (accounts: string[]) => { } const registerValidatorGroup = async (group: string) => { - await mockLockedGold.setAccountTotalLockedGold(group, registrationRequirements.group) + await mockLockedGold.setAccountTotalLockedGold(group, balanceRequirements.group) await validators.registerValidatorGroup(name, url, commission, { from: group }) } @@ -121,10 +120,10 @@ contract('Validators', (accounts: string[]) => { assert.equal(owner, accounts[0]) }) - it('should have set the registration requirements', async () => { - const [group, validator] = await validators.getRegistrationRequirements() - assertEqualBN(group, registrationRequirements.group) - assertEqualBN(validator, registrationRequirements.validator) + it('should have set the balance requirements', async () => { + const [group, validator] = await validators.getBalanceRequirements() + assertEqualBN(group, balanceRequirements.group) + assertEqualBN(validator, balanceRequirements.validator) }) it('should have set the deregistration lockups', async () => { @@ -142,8 +141,8 @@ contract('Validators', (accounts: string[]) => { await assertRevert( validators.initialize( registry.address, - registrationRequirements.group, - registrationRequirements.validator, + balanceRequirements.group, + balanceRequirements.validator, deregistrationLockups.group, deregistrationLockups.validator, maxGroupSize @@ -152,34 +151,34 @@ contract('Validators', (accounts: string[]) => { }) }) - describe('#setRegistrationRequirements()', () => { + describe('#setBalanceRequirements()', () => { describe('when the requirements are different', () => { const newRequirements = { - group: registrationRequirements.group.plus(1), - validator: registrationRequirements.validator.plus(1), + group: balanceRequirements.group.plus(1), + validator: balanceRequirements.validator.plus(1), } describe('when called by the owner', () => { let resp: any beforeEach(async () => { - resp = await validators.setRegistrationRequirements( + resp = await validators.setBalanceRequirements( newRequirements.group, newRequirements.validator ) }) it('should set the group and validator requirements', async () => { - const [group, validator] = await validators.getRegistrationRequirements() + const [group, validator] = await validators.getBalanceRequirements() assertEqualBN(group, newRequirements.group) assertEqualBN(validator, newRequirements.validator) }) - it('should emit the RegistrationRequirementsSet event', async () => { + it('should emit the BalanceRequirementsSet event', async () => { assert.equal(resp.logs.length, 1) const log = resp.logs[0] assertContainSubset(log, { - event: 'RegistrationRequirementsSet', + event: 'BalanceRequirementsSet', args: { group: new BigNumber(newRequirements.group), validator: new BigNumber(newRequirements.validator), @@ -190,13 +189,9 @@ contract('Validators', (accounts: string[]) => { describe('when called by a non-owner', () => { it('should revert', async () => { await assertRevert( - validators.setRegistrationRequirements( - newRequirements.group, - newRequirements.validator, - { - from: nonOwner, - } - ) + validators.setBalanceRequirements(newRequirements.group, newRequirements.validator, { + from: nonOwner, + }) ) }) }) @@ -205,9 +200,9 @@ contract('Validators', (accounts: string[]) => { describe('when the requirements are the same', () => { it('should revert', async () => { await assertRevert( - validators.setRegistrationRequirements( - registrationRequirements.group, - registrationRequirements.validator + validators.setBalanceRequirements( + balanceRequirements.group, + balanceRequirements.validator ) ) }) @@ -317,10 +312,7 @@ contract('Validators', (accounts: string[]) => { let resp: any describe('when the account is not a registered validator', () => { beforeEach(async () => { - await mockLockedGold.setAccountTotalLockedGold( - validator, - registrationRequirements.validator - ) + await mockLockedGold.setAccountTotalLockedGold(validator, balanceRequirements.validator) resp = await validators.registerValidator( name, url, @@ -344,10 +336,9 @@ contract('Validators', (accounts: string[]) => { assert.equal(parsedValidator.publicKeysData, publicKeysData) }) - it('should set account balance requirements on locked gold', async () => { - const [value, timestamp] = await mockLockedGold.getAccountMustMaintain(validator) - assertEqualBN(value, registrationRequirements.validator) - assertEqualBN(timestamp, MAX_UINT256) + it('should set account balance requirements', async () => { + const requirement = await validators.getAccountBalanceRequirement(validator) + assertEqualBN(requirement, balanceRequirements.validator) }) it('should emit the ValidatorRegistered event', async () => { @@ -367,10 +358,7 @@ contract('Validators', (accounts: string[]) => { describe('when the account is already a registered validator', () => { beforeEach(async () => { - await mockLockedGold.setAccountTotalLockedGold( - validator, - registrationRequirements.validator - ) + await mockLockedGold.setAccountTotalLockedGold(validator, balanceRequirements.validator) await validators.registerValidator( name, url, @@ -383,7 +371,7 @@ contract('Validators', (accounts: string[]) => { describe('when the account is already a registered validator', () => { beforeEach(async () => { - await mockLockedGold.setAccountTotalLockedGold(validator, registrationRequirements.group) + await mockLockedGold.setAccountTotalLockedGold(validator, balanceRequirements.group) await validators.registerValidatorGroup(name, url, commission) }) @@ -399,11 +387,11 @@ contract('Validators', (accounts: string[]) => { }) }) - describe('when the account does not meet the registration requirements', () => { + describe('when the account does not meet the balance requirements', () => { beforeEach(async () => { await mockLockedGold.setAccountTotalLockedGold( validator, - registrationRequirements.validator.minus(1) + balanceRequirements.validator.minus(1) ) }) @@ -438,14 +426,18 @@ contract('Validators', (accounts: string[]) => { assert.deepEqual(await validators.getRegisteredValidators(), []) }) - it('should set account balance requirements on locked gold', async () => { + it('should preserve account balance requirements', async () => { + const requirement = await validators.getAccountBalanceRequirement(validator) + assertEqualBN(requirement, balanceRequirements.validator) + }) + + it('should set the validator deregistration timestamp', async () => { const latestTimestamp = (await web3.eth.getBlock('latest')).timestamp - const [value, timestamp] = await mockLockedGold.getAccountMustMaintain(validator) - assertEqualBN(value, registrationRequirements.validator) - assertEqualBN( - timestamp, - new BigNumber(latestTimestamp).plus(deregistrationLockups.validator) + const [groupTimestamp, validatorTimestamp] = await validators.getDeregistrationTimestamps( + validator ) + assertEqualBN(groupTimestamp, 0) + assertEqualBN(validatorTimestamp, latestTimestamp) }) it('should emit the ValidatorDeregistered event', async () => { @@ -706,7 +698,7 @@ contract('Validators', (accounts: string[]) => { let resp: any describe('when the account is not a registered validator group', () => { beforeEach(async () => { - await mockLockedGold.setAccountTotalLockedGold(group, registrationRequirements.group) + await mockLockedGold.setAccountTotalLockedGold(group, balanceRequirements.group) resp = await validators.registerValidatorGroup(name, url, commission) }) @@ -724,6 +716,11 @@ contract('Validators', (accounts: string[]) => { assert.equal(parsedGroup.url, url) }) + it('should set account balance requirements', async () => { + const requirement = await validators.getAccountBalanceRequirement(group) + assertEqualBN(requirement, balanceRequirements.group) + }) + it('should emit the ValidatorGroupRegistered event', async () => { assert.equal(resp.logs.length, 1) const log = resp.logs[0] @@ -744,15 +741,13 @@ contract('Validators', (accounts: string[]) => { }) it('should revert', async () => { - await assertRevert( - validators.registerValidatorGroup(name, url, registrationRequirements.group) - ) + await assertRevert(validators.registerValidatorGroup(name, url, balanceRequirements.group)) }) }) describe('when the account is already a registered validator group', () => { beforeEach(async () => { - await mockLockedGold.setAccountTotalLockedGold(group, registrationRequirements.group) + await mockLockedGold.setAccountTotalLockedGold(group, balanceRequirements.group) await validators.registerValidatorGroup(name, url, commission) }) @@ -761,12 +756,9 @@ contract('Validators', (accounts: string[]) => { }) }) - describe('when the account does not meet the registration requirements', () => { + describe('when the account does not meet the balance requirements', () => { beforeEach(async () => { - await mockLockedGold.setAccountTotalLockedGold( - group, - registrationRequirements.group.minus(1) - ) + await mockLockedGold.setAccountTotalLockedGold(group, balanceRequirements.group.minus(1)) }) it('should revert', async () => { @@ -792,11 +784,18 @@ contract('Validators', (accounts: string[]) => { assert.deepEqual(await validators.getRegisteredValidatorGroups(), []) }) - it('should set account balance requirements on locked gold', async () => { + it('should preserve account balance requirements', async () => { + const requirement = await validators.getAccountBalanceRequirement(group) + assertEqualBN(requirement, balanceRequirements.group) + }) + + it('should set the group deregistration timestamp', async () => { const latestTimestamp = (await web3.eth.getBlock('latest')).timestamp - const [value, timestamp] = await mockLockedGold.getAccountMustMaintain(group) - assertEqualBN(value, registrationRequirements.group) - assertEqualBN(timestamp, new BigNumber(latestTimestamp).plus(deregistrationLockups.group)) + const [groupTimestamp, validatorTimestamp] = await validators.getDeregistrationTimestamps( + group + ) + assertEqualBN(groupTimestamp, latestTimestamp) + assertEqualBN(validatorTimestamp, 0) }) it('should emit the ValidatorGroupDeregistered event', async () => { From 0492b15d27768fe3863cbd605e6ec7128bc31ea1 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Wed, 9 Oct 2019 18:14:20 -0700 Subject: [PATCH 043/149] Fix build issues in contractkit --- packages/contractkit/src/wrappers/Election.ts | 2 ++ packages/contractkit/src/wrappers/LockedGold.ts | 3 +++ packages/contractkit/src/wrappers/Validators.ts | 12 ++++++------ 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/contractkit/src/wrappers/Election.ts b/packages/contractkit/src/wrappers/Election.ts index 860164238d5..24a5dd25d77 100644 --- a/packages/contractkit/src/wrappers/Election.ts +++ b/packages/contractkit/src/wrappers/Election.ts @@ -6,10 +6,12 @@ import { Election } from '../generated/types/Election' import { BaseWrapper, CeloTransactionObject, + identity, proxyCall, proxySend, toBigNumber, toNumber, + tupleParser, wrapSend, } from './BaseWrapper' diff --git a/packages/contractkit/src/wrappers/LockedGold.ts b/packages/contractkit/src/wrappers/LockedGold.ts index d636d276649..75c96d77b8d 100644 --- a/packages/contractkit/src/wrappers/LockedGold.ts +++ b/packages/contractkit/src/wrappers/LockedGold.ts @@ -7,9 +7,12 @@ import { LockedGold } from '../generated/types/LockedGold' import { BaseWrapper, CeloTransactionObject, + NumberLike, + parseNumber, proxyCall, proxySend, toBigNumber, + tupleParser, wrapSend, } from '../wrappers/BaseWrapper' diff --git a/packages/contractkit/src/wrappers/Validators.ts b/packages/contractkit/src/wrappers/Validators.ts index 0241d35987b..f75de231747 100644 --- a/packages/contractkit/src/wrappers/Validators.ts +++ b/packages/contractkit/src/wrappers/Validators.ts @@ -27,7 +27,7 @@ export interface ValidatorGroup { commission: BigNumber } -export interface RegistrationRequirements { +export interface BalanceRequirements { group: BigNumber validator: BigNumber } @@ -38,7 +38,7 @@ export interface DeregistrationLockups { } export interface ValidatorsConfig { - registrationRequirements: RegistrationRequirements + balanceRequirements: BalanceRequirements deregistrationLockups: DeregistrationLockups maxGroupSize: BigNumber } @@ -69,8 +69,8 @@ export class ValidatorsWrapper extends BaseWrapper { * Returns the current registration requirements. * @returns Group and validator registration requirements. */ - async getRegistrationRequirements(): Promise { - const res = await this.contract.methods.getRegistrationRequirements().call() + async getBalanceRequirements(): Promise { + const res = await this.contract.methods.getBalanceRequirements().call() return { group: toBigNumber(res[0]), validator: toBigNumber(res[1]), @@ -90,12 +90,12 @@ export class ValidatorsWrapper extends BaseWrapper { */ async getConfig(): Promise { const res = await Promise.all([ - this.getRegistrationRequirements(), + this.getBalanceRequirements(), this.getDeregistrationLockups(), this.contract.methods.maxGroupSize().call(), ]) return { - registrationRequirements: res[0], + balanceRequirements: res[0], deregistrationLockups: res[1], maxGroupSize: toBigNumber(res[2]), } From 264a7f5cbf2b583fc3a418eadc35d8ff4c9793a0 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Wed, 9 Oct 2019 18:58:12 -0700 Subject: [PATCH 044/149] Fix --- packages/celotool/src/e2e-tests/governance_tests.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/celotool/src/e2e-tests/governance_tests.ts b/packages/celotool/src/e2e-tests/governance_tests.ts index 4682d2196f5..e2b081352ca 100644 --- a/packages/celotool/src/e2e-tests/governance_tests.ts +++ b/packages/celotool/src/e2e-tests/governance_tests.ts @@ -27,7 +27,7 @@ describe('governance tests', () => { before(async function(this: any) { this.timeout(0) - // await context.hooks.before() + await context.hooks.before() }) after(context.hooks.after) From 99d3fcc860a4490708abe4df2cb6ef42684e3f88 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Wed, 9 Oct 2019 18:59:03 -0700 Subject: [PATCH 045/149] Remove registry from governance test --- .../src/e2e-tests/governance_tests.ts | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/packages/celotool/src/e2e-tests/governance_tests.ts b/packages/celotool/src/e2e-tests/governance_tests.ts index ad6dbc5cc86..a4a97f87cb0 100644 --- a/packages/celotool/src/e2e-tests/governance_tests.ts +++ b/packages/celotool/src/e2e-tests/governance_tests.ts @@ -50,28 +50,6 @@ const electionAbi = [ }, ] -const registryAbi = [ - { - constant: true, - inputs: [ - { - name: 'identifier', - type: 'string', - }, - ], - name: 'getAddressForString', - outputs: [ - { - name: '', - type: 'address', - }, - ], - payable: false, - stateMutability: 'view', - type: 'function', - }, -] - const validatorsAbi = [ { constant: true, @@ -221,7 +199,6 @@ describe('governance tests', () => { let election: any let validators: any let goldToken: any - let registry: any before(async function(this: any) { this.timeout(0) @@ -235,7 +212,6 @@ describe('governance tests', () => { web3 = new Web3('http://localhost:8545') goldToken = new web3.eth.Contract(erc20Abi, await getContractAddress('GoldTokenProxy')) validators = new web3.eth.Contract(validatorsAbi, await getContractAddress('ValidatorsProxy')) - registry = new web3.eth.Contract(registryAbi, '0x000000000000000000000000000000000000ce10') election = new web3.eth.Contract(electionAbi, await getContractAddress('ElectionProxy')) } From 7d9c5cc814450b8440fc5b2a3fbffa02f679a015 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Wed, 9 Oct 2019 19:16:16 -0700 Subject: [PATCH 046/149] Fix linting issues --- packages/cli/src/commands/election/vote.ts | 2 +- .../cli/src/commands/lockedgold/authorize.ts | 4 +- .../cli/src/commands/lockedgold/withdraw.ts | 4 +- .../src/commands/validatorgroup/register.ts | 2 +- .../contractkit/src/wrappers/Validators.ts | 2 +- .../docs/command-line-interface/lockedgold.md | 114 +++++------------- .../docs/command-line-interface/validator.md | 11 +- .../command-line-interface/validatorgroup.md | 35 +----- 8 files changed, 45 insertions(+), 129 deletions(-) diff --git a/packages/cli/src/commands/election/vote.ts b/packages/cli/src/commands/election/vote.ts index ae5f7590454..cf957d74755 100644 --- a/packages/cli/src/commands/election/vote.ts +++ b/packages/cli/src/commands/election/vote.ts @@ -1,5 +1,5 @@ -import BigNumber from 'bignumber.js' import { flags } from '@oclif/command' +import BigNumber from 'bignumber.js' import { BaseCommand } from '../../base' import { displaySendTx } from '../../utils/cli' import { Flags } from '../../utils/command' diff --git a/packages/cli/src/commands/lockedgold/authorize.ts b/packages/cli/src/commands/lockedgold/authorize.ts index 6a66475ee23..aae72011553 100644 --- a/packages/cli/src/commands/lockedgold/authorize.ts +++ b/packages/cli/src/commands/lockedgold/authorize.ts @@ -39,9 +39,9 @@ export default class Authorize extends BaseCommand { this.kit.defaultAccount = res.flags.from const lockedGold = await this.kit.contracts.getLockedGold() let tx: any - if (res.flags.role == 'voter') { + if (res.flags.role === 'voter') { tx = await lockedGold.authorizeVoter(res.flags.from, res.flags.to) - } else if (res.flags.role == 'validator') { + } else if (res.flags.role === 'validator') { tx = await lockedGold.authorizeValidator(res.flags.from, res.flags.to) } else { this.error(`Invalid role provided`) diff --git a/packages/cli/src/commands/lockedgold/withdraw.ts b/packages/cli/src/commands/lockedgold/withdraw.ts index 3691d715dc2..06383dea399 100644 --- a/packages/cli/src/commands/lockedgold/withdraw.ts +++ b/packages/cli/src/commands/lockedgold/withdraw.ts @@ -37,8 +37,8 @@ export default class Withdraw extends BaseCommand { break } } - const pendingWithdrawals = await lockedgold.getPendingWithdrawals(flags.from) - for (const pendingWithdrawal of pendingWithdrawals) { + const remainingPendingWithdrawals = await lockedgold.getPendingWithdrawals(flags.from) + for (const pendingWithdrawal of remainingPendingWithdrawals) { console.log( `Pending withdrawal of value ${pendingWithdrawal.value.toString()} available for withdrawal in ${pendingWithdrawal.time .minus(currentTime) diff --git a/packages/cli/src/commands/validatorgroup/register.ts b/packages/cli/src/commands/validatorgroup/register.ts index a8650e81fee..365bc9cf7f6 100644 --- a/packages/cli/src/commands/validatorgroup/register.ts +++ b/packages/cli/src/commands/validatorgroup/register.ts @@ -1,5 +1,5 @@ -import BigNumber from 'bignumber.js' import { flags } from '@oclif/command' +import BigNumber from 'bignumber.js' import { BaseCommand } from '../../base' import { displaySendTx } from '../../utils/cli' import { Flags } from '../../utils/command' diff --git a/packages/contractkit/src/wrappers/Validators.ts b/packages/contractkit/src/wrappers/Validators.ts index f75de231747..35494ab25fd 100644 --- a/packages/contractkit/src/wrappers/Validators.ts +++ b/packages/contractkit/src/wrappers/Validators.ts @@ -1,3 +1,4 @@ +import { fromFixed, toFixed } from '@celo/utils/lib/fixidity' import BigNumber from 'bignumber.js' import { Address } from '../base' import { Validators } from '../generated/types/Validators' @@ -9,7 +10,6 @@ import { toBigNumber, wrapSend, } from './BaseWrapper' -import { fromFixed, toFixed } from '@celo/utils/lib/fixidity' export interface Validator { address: Address diff --git a/packages/docs/command-line-interface/lockedgold.md b/packages/docs/command-line-interface/lockedgold.md index 52b2f762242..ef067a6e750 100644 --- a/packages/docs/command-line-interface/lockedgold.md +++ b/packages/docs/command-line-interface/lockedgold.md @@ -1,84 +1,46 @@ --- -description: Manage Locked Gold to participate in governance and earn rewards +description: View and manage locked Celo Gold --- ## Commands -### Delegate +### Authorize -Delegate validating, voting and reward roles for Locked Gold account +Authorize validating or voting address for a Locked Gold account ``` USAGE - $ celocli lockedgold:delegate + $ celocli lockedgold:authorize OPTIONS - -r, --role=Validating|Voting|Rewards Role to delegate + -r, --role=voter|validator Role to delegate --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Account Address --to=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Account Address EXAMPLE - delegate --from=0x5409ED021D9299bf6814279A6A1411A7e866A631 --role Voting - --to=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d + authorize --from 0x5409ED021D9299bf6814279A6A1411A7e866A631 --role voter --to + 0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d ``` -_See code: [packages/cli/src/commands/lockedgold/delegate.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/lockedgold/delegate.ts)_ +_See code: [packages/cli/src/commands/lockedgold/authorize.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/lockedgold/authorize.ts)_ -### List +### Lock -View information about all of the account's commitments +Locks Celo Gold to be used in governance and validator elections. ``` USAGE - $ celocli lockedgold:list ACCOUNT - -EXAMPLE - list 0x5409ed021d9299bf6814279a6a1411a7e866a631 -``` - -_See code: [packages/cli/src/commands/lockedgold/list.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/lockedgold/list.ts)_ - -### Lockup - -Create a Locked Gold commitment given notice period and gold amount - -``` -USAGE - $ celocli lockedgold:lockup - -OPTIONS - --from=from (required) - --goldAmount=goldAmount (required) unit amount of gold token (cGLD) - - --noticePeriod=noticePeriod (required) duration (seconds) from notice to withdrawable; doubles as ID of a Locked Gold - commitment; - -EXAMPLE - lockup --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --noticePeriod 8640 --goldAmount 1000000000000000000 -``` - -_See code: [packages/cli/src/commands/lockedgold/lockup.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/lockedgold/lockup.ts)_ - -### Notify - -Notify a Locked Gold commitment given notice period and gold amount - -``` -USAGE - $ celocli lockedgold:notify + $ celocli lockedgold:lock OPTIONS - --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Account Address - --goldAmount=goldAmount (required) unit amount of gold token (cGLD) - - --noticePeriod=noticePeriod (required) duration (seconds) from notice to withdrawable; doubles - as ID of a Locked Gold commitment; + --from=from (required) + --value=value (required) unit amount of Celo Gold (cGLD) EXAMPLE - notify --noticePeriod=3600 --goldAmount=500 + lock --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --value 1000000000000000000 ``` -_See code: [packages/cli/src/commands/lockedgold/notify.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/lockedgold/notify.ts)_ +_See code: [packages/cli/src/commands/lockedgold/lock.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/lockedgold/lock.ts)_ ### Register @@ -97,63 +59,51 @@ EXAMPLE _See code: [packages/cli/src/commands/lockedgold/register.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/lockedgold/register.ts)_ -### Rewards +### Show -Manage rewards for Locked Gold account +Show Locked Gold information for a given account ``` USAGE - $ celocli lockedgold:rewards - -OPTIONS - -d, --delegate=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d Delegate rewards to provided account - -r, --redeem Redeem accrued rewards from Locked Gold - --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Account Address + $ celocli lockedgold:show ACCOUNT -EXAMPLES - rewards --redeem - rewards --delegate=0x56e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 +EXAMPLE + show 0x5409ed021d9299bf6814279a6a1411a7e866a631 ``` -_See code: [packages/cli/src/commands/lockedgold/rewards.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/lockedgold/rewards.ts)_ +_See code: [packages/cli/src/commands/lockedgold/show.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/lockedgold/show.ts)_ -### Show +### Unlock -Show Locked Gold and corresponding account weight of a commitment given ID +Unlocks Celo Gold, which can be withdrawn after the unlocking period. ``` USAGE - $ celocli lockedgold:show ACCOUNT + $ celocli lockedgold:unlock OPTIONS - --availabilityTime=availabilityTime unix timestamp at which withdrawable; doubles as ID of a notified commitment - - --noticePeriod=noticePeriod duration (seconds) from notice to withdrawable; doubles as ID of a Locked Gold - commitment; + --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Account Address + --value=value (required) unit amount of Celo Gold (cGLD) -EXAMPLES - show 0x5409ed021d9299bf6814279a6a1411a7e866a631 --noticePeriod=3600 - show 0x5409ed021d9299bf6814279a6a1411a7e866a631 --availabilityTime=1562206887 +EXAMPLE + unlock --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --value 500000000 ``` -_See code: [packages/cli/src/commands/lockedgold/show.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/lockedgold/show.ts)_ +_See code: [packages/cli/src/commands/lockedgold/unlock.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/lockedgold/unlock.ts)_ ### Withdraw -Withdraw notified commitment given availability time +Withdraw unlocked gold whose unlocking period has passed. ``` USAGE - $ celocli lockedgold:withdraw AVAILABILITYTIME - -ARGUMENTS - AVAILABILITYTIME unix timestamp at which withdrawable; doubles as ID of a notified commitment + $ celocli lockedgold:withdraw OPTIONS --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Account Address EXAMPLE - withdraw 3600 + withdraw --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 ``` _See code: [packages/cli/src/commands/lockedgold/withdraw.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/lockedgold/withdraw.ts)_ diff --git a/packages/docs/command-line-interface/validator.md b/packages/docs/command-line-interface/validator.md index 826206472f2..4c72974b8fd 100644 --- a/packages/docs/command-line-interface/validator.md +++ b/packages/docs/command-line-interface/validator.md @@ -1,5 +1,5 @@ --- -description: View validator information and register your own +description: View and manage validators --- ## Commands @@ -48,19 +48,12 @@ USAGE OPTIONS --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Address for the Validator - --id=id (required) --name=name (required) - - --noticePeriod=noticePeriod (required) Notice period of the Locked Gold commitment. Specify - multiple notice periods to use the sum of the commitments. - --publicKey=0x (required) Public Key - --url=url (required) EXAMPLE - register --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --id myID --name myName --noticePeriod 5184000 - --noticePeriod 5184001 --url "http://validator.com" --publicKey + register --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --name myName --url "http://validator.com" --publicKey 0xc52f3fab06e22a54915a8765c4f6826090cfac5e40282b43844bf1c0df83aaa632e55b67869758f2291d1aabe0ebecc7cbf4236aaa45e3e0cfbf 997eda082ae19d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d 785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d7405011220a66a6257562d0c26dabf64485a1d diff --git a/packages/docs/command-line-interface/validatorgroup.md b/packages/docs/command-line-interface/validatorgroup.md index f8e3eda5b7b..eb3596f76d6 100644 --- a/packages/docs/command-line-interface/validatorgroup.md +++ b/packages/docs/command-line-interface/validatorgroup.md @@ -1,5 +1,5 @@ --- -description: View validator group information and cast votes +description: View and manage validator groups --- ## Commands @@ -20,7 +20,7 @@ _See code: [packages/cli/src/commands/validatorgroup/list.ts](https://github.com ### Member -Manage members of a Validator Group +Add or remove members from a Validator Group ``` USAGE @@ -50,18 +50,13 @@ USAGE $ celocli validatorgroup:register OPTIONS + --commission=commission (required) --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Address for the Validator Group - --id=id (required) --name=name (required) - - --noticePeriod=noticePeriod (required) Notice period of the Locked Gold commitment. Specify - multiple notice periods to use the sum of the commitments. - --url=url (required) EXAMPLE - register --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --id myID --name myName --noticePeriod 5184000 - --noticePeriod 5184001 --url "http://vgroup.com" + register --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --name myName --url "http://vgroup.com" --commission 0.1 ``` _See code: [packages/cli/src/commands/validatorgroup/register.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validatorgroup/register.ts)_ @@ -82,25 +77,3 @@ EXAMPLE ``` _See code: [packages/cli/src/commands/validatorgroup/show.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validatorgroup/show.ts)_ - -### Vote - -Vote for a Validator Group - -``` -USAGE - $ celocli validatorgroup:vote - -OPTIONS - --current Show voter's current vote - --for=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d Set vote for ValidatorGroup's address - --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Voter's address - --revoke Revoke voter's current vote - -EXAMPLES - vote --from 0x4443d0349e8b3075cba511a0a87796597602a0f1 --for 0x932fee04521f5fcb21949041bf161917da3f588b - vote --from 0x4443d0349e8b3075cba511a0a87796597602a0f1 --revoke - vote --from 0x4443d0349e8b3075cba511a0a87796597602a0f1 --current -``` - -_See code: [packages/cli/src/commands/validatorgroup/vote.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validatorgroup/vote.ts)_ From f15e844aee4015b92602d319ab83a6a1f1aac6b2 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Wed, 9 Oct 2019 19:16:29 -0700 Subject: [PATCH 047/149] Add missing cli doc --- .../docs/command-line-interface/election.md | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 packages/docs/command-line-interface/election.md diff --git a/packages/docs/command-line-interface/election.md b/packages/docs/command-line-interface/election.md new file mode 100644 index 00000000000..6ed0013466e --- /dev/null +++ b/packages/docs/command-line-interface/election.md @@ -0,0 +1,39 @@ +--- +description: View and manage validator elections +--- + +## Commands + +### Validatorset + +Outputs the current validator set + +``` +USAGE + $ celocli election:validatorset + +EXAMPLE + validatorset +``` + +_See code: [packages/cli/src/commands/election/validatorset.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/election/validatorset.ts)_ + +### Vote + +Vote for a Validator Group in validator elections. + +``` +USAGE + $ celocli election:vote + +OPTIONS + --for=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Set vote for ValidatorGroup's address + --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Voter's address + --value=value (required) Amount of Gold used to vote for group + +EXAMPLE + vote --from 0x4443d0349e8b3075cba511a0a87796597602a0f1 --for 0x932fee04521f5fcb21949041bf161917da3f588b, --value + 1000000 +``` + +_See code: [packages/cli/src/commands/election/vote.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/election/vote.ts)_ From 993339ce168c454105a5a685423b331dabea6072 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Wed, 9 Oct 2019 19:39:34 -0700 Subject: [PATCH 048/149] Governance end-to-end test passing --- packages/contractkit/src/wrappers/Election.ts | 13 ------------ .../migrations/17_elect_validators.ts | 20 +++++++++---------- 2 files changed, 10 insertions(+), 23 deletions(-) diff --git a/packages/contractkit/src/wrappers/Election.ts b/packages/contractkit/src/wrappers/Election.ts index 24a5dd25d77..db0f775c4cd 100644 --- a/packages/contractkit/src/wrappers/Election.ts +++ b/packages/contractkit/src/wrappers/Election.ts @@ -135,19 +135,6 @@ export class ElectionWrapper extends BaseWrapper { return zip((a, b) => ({ address: a, votes: new BigNumber(b), eligible: true }), res[0], res[1]) } - async markGroupEligible(validatorGroup: Address): Promise> { - if (this.kit.defaultAccount == null) { - throw new Error(`missing from at new ValdidatorUtils()`) - } - - const value = toBigNumber( - await this.contract.methods.getTotalVotesForGroup(validatorGroup).call() - ) - const { lesser, greater } = await this.findLesserAndGreaterAfterVote(validatorGroup, value) - - return wrapSend(this.kit, this.contract.methods.markGroupEligible(lesser, greater)) - } - async vote(validatorGroup: Address, value: BigNumber): Promise> { if (this.kit.defaultAccount == null) { throw new Error(`missing from at new ValdidatorUtils()`) diff --git a/packages/protocol/migrations/17_elect_validators.ts b/packages/protocol/migrations/17_elect_validators.ts index 4d39ba9fb19..4d354c6e4f7 100644 --- a/packages/protocol/migrations/17_elect_validators.ts +++ b/packages/protocol/migrations/17_elect_validators.ts @@ -161,22 +161,22 @@ module.exports = async (_deployer: any) => { ) console.info(' Adding Validators to Validator Group ...') - for (const key of valKeys) { + for (let i = 0; i < valKeys.length; i++) { + const key = valKeys[i] const address = generateAccountAddressFromPrivateKey(key.slice(2)) - // @ts-ignore - const addTx = validators.contract.methods.addMember(address) + let addTx: any + if (i == 0) { + // @ts-ignore + addTx = validators.contract.methods.addFirstMember(address, NULL_ADDRESS, NULL_ADDRESS) + } else { + // @ts-ignore + addTx = validators.contract.methods.addMember(address) + } await sendTransactionWithPrivateKey(web3, addTx, account.privateKey, { to: validators.address, }) } - console.info(' Marking Validator Group as eligible for election ...') - // @ts-ignore - const markTx = election.contract.methods.markGroupEligible(NULL_ADDRESS, NULL_ADDRESS) - await sendTransactionWithPrivateKey(web3, markTx, account.privateKey, { - to: election.address, - }) - console.info(' Voting for Validator Group ...') // Make another deposit so our vote has more weight. const minLockedGoldVotePerValidator = 10000 From 44f9a434a4dda6bbac7e057055e6eb84237b8a35 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Wed, 9 Oct 2019 19:47:59 -0700 Subject: [PATCH 049/149] Fix migration --- packages/protocol/migrations/17_elect_validators.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/protocol/migrations/17_elect_validators.ts b/packages/protocol/migrations/17_elect_validators.ts index 4a9d7838907..4d39ba9fb19 100644 --- a/packages/protocol/migrations/17_elect_validators.ts +++ b/packages/protocol/migrations/17_elect_validators.ts @@ -172,11 +172,7 @@ module.exports = async (_deployer: any) => { console.info(' Marking Validator Group as eligible for election ...') // @ts-ignore - const markTx = election.contract.methods.markGroupEligible( - account.address, - NULL_ADDRESS, - NULL_ADDRESS - ) + const markTx = election.contract.methods.markGroupEligible(NULL_ADDRESS, NULL_ADDRESS) await sendTransactionWithPrivateKey(web3, markTx, account.privateKey, { to: election.address, }) From f59ca01d01c899797de55ac9d488205695b67ca7 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Thu, 10 Oct 2019 12:06:16 -0700 Subject: [PATCH 050/149] Fix CLI build --- .../cli/src/commands/validatorgroup/member.ts | 9 ++------- packages/contractkit/src/wrappers/Election.ts | 6 ++++++ .../contractkit/src/wrappers/Validators.ts | 18 +++++++++++++++++- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/packages/cli/src/commands/validatorgroup/member.ts b/packages/cli/src/commands/validatorgroup/member.ts index 86d126c790d..9df2958813e 100644 --- a/packages/cli/src/commands/validatorgroup/member.ts +++ b/packages/cli/src/commands/validatorgroup/member.ts @@ -37,14 +37,9 @@ export default class ValidatorGroupMembers extends BaseCommand { this.kit.defaultAccount = res.flags.from const validators = await this.kit.contracts.getValidators() - const election = await this.kit.contracts.getElection() - if (res.flags.accept) { - await displaySendTx('addMember', validators.addMember((res.args as any).validatorAddress)) - if ((await validators.getGroupNumMembers(res.flags.from)).isEqualTo(1)) { - const tx = await election.markGroupEligible(res.flags.from) - await displaySendTx('markGroupEligible', tx) - } + const tx = await validators.addMember((res.args as any).validatorAddress) + await displaySendTx('addMember', tx) } else { await displaySendTx( 'removeMember', diff --git a/packages/contractkit/src/wrappers/Election.ts b/packages/contractkit/src/wrappers/Election.ts index db0f775c4cd..d51b14d27f6 100644 --- a/packages/contractkit/src/wrappers/Election.ts +++ b/packages/contractkit/src/wrappers/Election.ts @@ -80,6 +80,12 @@ export class ElectionWrapper extends BaseWrapper { toNumber ) + getTotalVotesForGroup = proxyCall( + this.contract.methods.getTotalVotesForGroup, + undefined, + toBigNumber + ) + getGroupsVotedForByAccount: (account: Address) => Promise = proxyCall( this.contract.methods.getGroupsVotedForByAccount ) diff --git a/packages/contractkit/src/wrappers/Validators.ts b/packages/contractkit/src/wrappers/Validators.ts index 35494ab25fd..becc6a76ed5 100644 --- a/packages/contractkit/src/wrappers/Validators.ts +++ b/packages/contractkit/src/wrappers/Validators.ts @@ -49,7 +49,6 @@ export interface ValidatorsConfig { export class ValidatorsWrapper extends BaseWrapper { affiliate = proxySend(this.kit, this.contract.methods.affiliate) deaffiliate = proxySend(this.kit, this.contract.methods.deaffiliate) - addMember = proxySend(this.kit, this.contract.methods.addMember) removeMember = proxySend(this.kit, this.contract.methods.removeMember) registerValidator = proxySend(this.kit, this.contract.methods.registerValidator) async registerValidatorGroup( @@ -65,6 +64,23 @@ export class ValidatorsWrapper extends BaseWrapper { this.contract.methods.registerValidatorGroup(name, url, toFixed(commission).toFixed()) ) } + async addMember(member: string): Promise> { + if (this.kit.defaultAccount == null) { + throw new Error(`missing from at new ValdidatorUtils()`) + } + // TODO(asa): Support authorized validators + const group = this.kit.defaultAccount + const numMembers = await this.getGroupNumMembers(group) + if (numMembers.isZero()) { + const election = await this.kit.contracts.getElection() + const voteWeight = await election.getTotalVotesForGroup(group) + const { lesser, greater } = await election.findLesserAndGreaterAfterVote(group, voteWeight) + + return wrapSend(this.kit, this.contract.methods.addFirstMember(member, lesser, greater)) + } else { + return wrapSend(this.kit, this.contract.methods.addMember(member)) + } + } /** * Returns the current registration requirements. * @returns Group and validator registration requirements. From 00f806175083de61c2a2134df8a67e9ffdaff919 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Thu, 10 Oct 2019 13:37:57 -0700 Subject: [PATCH 051/149] Add electabilityThreshold enforcement --- .../contracts/common/UsingPrecompiles.sol | 25 +++++++ .../linkedlists/AddressSortedLinkedList.sol | 27 +++++++ .../contracts/governance/Election.sol | 72 ++++++++++--------- .../contracts/governance/Validators.sol | 47 +++++++++--- .../governance/test/ElectionTest.sol | 9 ++- .../governance/test/MockLockedGold.sol | 8 ++- packages/protocol/migrations/12_election.ts | 3 +- .../migrations/17_elect_validators.ts | 13 ++-- packages/protocol/migrationsConfig.js | 2 +- packages/protocol/test/governance/election.ts | 33 ++++++++- 10 files changed, 180 insertions(+), 59 deletions(-) diff --git a/packages/protocol/contracts/common/UsingPrecompiles.sol b/packages/protocol/contracts/common/UsingPrecompiles.sol index 412a2470697..eca7ee67461 100644 --- a/packages/protocol/contracts/common/UsingPrecompiles.sol +++ b/packages/protocol/contracts/common/UsingPrecompiles.sol @@ -88,4 +88,29 @@ contract UsingPrecompiles { } return epochNumber; } + + function validatorAddressFromCurrentSet(uint256 index) external view returns (address) { + address validatorAddress; + assembly { + let newCallDataPosition := mload(0x40) + mstore(newCallDataPosition, index) + let success := staticcall(5000, 0xfa, newCallDataPosition, 32, 0, 0) + returndatacopy(add(newCallDataPosition, 64), 0, 32) + validatorAddress := mload(add(newCallDataPosition, 64)) + } + + return validatorAddress; + } + + function numberValidatorsInCurrentSet() external view returns (uint256) { + uint256 numberValidators; + assembly { + let success := staticcall(5000, 0xf9, 0, 0, 0, 0) + let returnData := mload(0x40) + returndatacopy(returnData, 0, 32) + numberValidators := mload(returnData) + } + + return numberValidators; + } } diff --git a/packages/protocol/contracts/common/linkedlists/AddressSortedLinkedList.sol b/packages/protocol/contracts/common/linkedlists/AddressSortedLinkedList.sol index 61ea7d0fdef..0938c1b486e 100644 --- a/packages/protocol/contracts/common/linkedlists/AddressSortedLinkedList.sol +++ b/packages/protocol/contracts/common/linkedlists/AddressSortedLinkedList.sol @@ -1,5 +1,6 @@ pragma solidity ^0.5.3; +import "openzeppelin-solidity/contracts/math/Math.sol"; import "openzeppelin-solidity/contracts/math/SafeMath.sol"; import "./AddressLinkedList.sol"; import "./SortedLinkedList.sol"; @@ -106,6 +107,32 @@ library AddressSortedLinkedList { return (keys, values); } + /** + * @notice Returns the minimum of `max` and the number of elements in the list > threshold. + * @param threshold The number that the element must exceed to be included. + * @param max The maximum number returned by this function. + * @return The minimum of `max` and the number of elements in the list > threshold. + */ + function numElementsGreaterThan( + SortedLinkedList.List storage list, + uint256 threshold, + uint256 max + ) + public + view + returns (uint256) + { + uint256 revisedMax = Math.min(max, list.list.numElements); + bytes32 key = list.list.head; + for (uint256 i = 0; i < revisedMax; i++) { + if (list.getValue(key) < threshold) { + return i; + } + key = list.list.elements[key].previousKey; + } + return revisedMax; + } + /** * @notice Returns the N greatest elements of the list. * @param n The number of elements to return. diff --git a/packages/protocol/contracts/governance/Election.sol b/packages/protocol/contracts/governance/Election.sol index cc90c4d9bb5..5edfc83ff0f 100644 --- a/packages/protocol/contracts/governance/Election.sol +++ b/packages/protocol/contracts/governance/Election.sol @@ -13,7 +13,8 @@ import "../common/UsingPrecompiles.sol"; import "../common/UsingRegistry.sol"; -contract Election is IElection, Ownable, ReentrancyGuard, Initializable, UsingRegistry, UsingPrecompiles { +contract Election is + IElection, Ownable, ReentrancyGuard, Initializable, UsingRegistry, UsingPrecompiles { using AddressSortedLinkedList for SortedLinkedList.List; using FixidityLib for FixidityLib.Fraction; @@ -454,7 +455,14 @@ contract Election is IElection, Ownable, ReentrancyGuard, Initializable, UsingRe return votes.total.eligible.contains(group); } - function getGroupEpochRewards(address group, uint256 totalEpochRewards) external view returns (uint256) { + function getGroupEpochRewards( + address group, + uint256 totalEpochRewards + ) + external + view + returns (uint256) + { // TODO(asa): Is this right? if (votes.active.total == 0) { return 0; @@ -462,12 +470,26 @@ contract Election is IElection, Ownable, ReentrancyGuard, Initializable, UsingRe return totalEpochRewards.mul(votes.active.forGroup[group].total).div(votes.active.total); } - function distributeEpochRewards(address group, uint256 value, address lesser, address greater) external { + function distributeEpochRewards( + address group, + uint256 value, + address lesser, + address greater + ) + external + { require(msg.sender == address(0)); _distributeEpochRewards(group, value, lesser, greater); } - function _distributeEpochRewards(address group, uint256 value, address lesser, address greater) internal { + function _distributeEpochRewards( + address group, + uint256 value, + address lesser, + address greater + ) + internal + { if (votes.total.eligible.contains(group)) { uint256 newVoteTotal = votes.total.eligible.getValue(group).add(value); votes.total.eligible.update(group, newVoteTotal, lesser, greater); @@ -741,31 +763,6 @@ contract Election is IElection, Ownable, ReentrancyGuard, Initializable, UsingRe return votes.total.eligible.getElements(); } - function validatorAddressFromCurrentSet(uint256 index) external view returns (address) { - address validatorAddress; - assembly { - let newCallDataPosition := mload(0x40) - mstore(newCallDataPosition, index) - let success := staticcall(5000, 0xfa, newCallDataPosition, 32, 0, 0) - returndatacopy(add(newCallDataPosition, 64), 0, 32) - validatorAddress := mload(add(newCallDataPosition, 64)) - } - - return validatorAddress; - } - - function numberValidatorsInCurrentSet() external view returns (uint256) { - uint256 numberValidators; - assembly { - let success := staticcall(5000, 0xf9, 0, 0, 0, 0) - let returnData := mload(0x40) - returndatacopy(returnData, 0, 32) - numberValidators := mload(returnData) - } - - return numberValidators; - } - /** * @notice Returns a list of elected validators with seats allocated to groups via the D'Hondt * method. @@ -773,13 +770,18 @@ contract Election is IElection, Ownable, ReentrancyGuard, Initializable, UsingRe * @dev See https://en.wikipedia.org/wiki/D%27Hondt_method#Allocation for more information. */ function electValidators() external view returns (address[] memory) { - // Only members of these validator groups are eligible for election. - uint256 maxNumElectionGroups = Math.min( - electableValidators.max, - votes.total.eligible.list.numElements + // Groups must have at least `electabilityThreshold` proportion of the total votes to be + // considered for the election. + uint256 requiredVotes = electabilityThreshold.multiply( + FixidityLib.newFixed(getTotalVotes()) + ).fromFixed(); + // Only consider groups with at least `requiredVotes` but do not consider more groups than the + // max number of electable validators. + uint256 numElectionGroups = votes.total.eligible.numElementsGreaterThan( + requiredVotes, + electableValidators.max ); - // TODO(asa): Filter by > requiredVotes - address[] memory electionGroups = votes.total.eligible.headN(maxNumElectionGroups); + address[] memory electionGroups = votes.total.eligible.headN(numElectionGroups); uint256[] memory numMembers = getValidators().getGroupsNumMembers(electionGroups); // Holds the number of members elected for each of the eligible validator groups. uint256[] memory numMembersElected = new uint256[](electionGroups.length); diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index 24de392bfa6..58b742bf359 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -20,7 +20,8 @@ import "../common/UsingPrecompiles.sol"; /** * @title A contract for registering and electing Validator Groups and Validators. */ -contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, UsingRegistry, UsingPrecompiles { +contract Validators is + IValidators, Ownable, ReentrancyGuard, Initializable, UsingRegistry, UsingPrecompiles { using FixidityLib for FixidityLib.Fraction; using AddressLinkedList for LinkedList.List; @@ -186,7 +187,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi * @param validatorScoreExponent The exponent used in calculating validator scores. * @param validatorScoreAdjustmentSpeed The speed at which validator scores are adjusted. * @param _validatorEpochPayment The duration the above gold remains locked after deregistration. - * @param _membershipHistoryLength The maximum number of entries for validator membership history. + * @param _membershipHistoryLength The max number of entries for validator membership history. * @param _maxGroupSize The maximum group size. * @dev Should be called only once. */ @@ -249,13 +250,23 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi * @param adjustmentSpeed The speed at which the score is adjusted. * @return True upon success. */ - function setValidatorScoreParameters(uint256 exponent, uint256 adjustmentSpeed) external onlyOwner returns (bool) { + function setValidatorScoreParameters( + uint256 exponent, + uint256 adjustmentSpeed + ) + external + onlyOwner + returns (bool) + { require(adjustmentSpeed <= FixidityLib.fixed1().unwrap()); require( exponent != validatorScoreParameters.exponent || !FixidityLib.wrap(adjustmentSpeed).equals(validatorScoreParameters.adjustmentSpeed) ); - validatorScoreParameters = ValidatorScoreParameters(exponent, FixidityLib.wrap(adjustmentSpeed)); + validatorScoreParameters = ValidatorScoreParameters( + exponent, + FixidityLib.wrap(adjustmentSpeed) + ); emit ValidatorScoreParametersSet(exponent, adjustmentSpeed); return true; } @@ -396,7 +407,13 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi return (validatorScoreParameters.exponent, validatorScoreParameters.adjustmentSpeed.unwrap()); } - function getMembershipHistory(address account) external view returns (uint256[] memory, address[] memory) { + function getMembershipHistory( + address account + ) + external + view + returns (uint256[] memory, address[] memory) + { MembershipHistoryEntry[] memory entries = validators[account].membershipHistory.entries; uint256[] memory epochs = new uint256[](entries.length); address[] memory membershipGroups = new address[](entries.length); @@ -439,7 +456,9 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi 18 ); - FixidityLib.Fraction memory epochScore = FixidityLib.wrap(numerator).divide(FixidityLib.wrap(denominator)); + FixidityLib.Fraction memory epochScore = FixidityLib.wrap(numerator).divide( + FixidityLib.wrap(denominator) + ); FixidityLib.Fraction memory newComponent = validatorScoreParameters.adjustmentSpeed.multiply( epochScore ); @@ -463,12 +482,12 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi function _distributeEpochPayment(address validator) internal { address account = getLockedGold().getAccountFromValidator(validator); require(isValidator(account)); - FixidityLib.Fraction memory totalPayment = FixidityLib.newFixed(validatorEpochPayment).multiply(validators[account].score); + FixidityLib.Fraction memory totalPayment = FixidityLib.newFixed( + validatorEpochPayment + ).multiply(validators[account].score); address group = getMembershipInLastEpoch(account); uint256 groupPayment = totalPayment.multiply(groups[group].commission).fromFixed(); uint256 validatorPayment = totalPayment.fromFixed().sub(groupPayment); - // For some reason, one validator seems to be getting the full payment (not less commission) - // Perhaps, getMembershipInLastEpoch is returning 0? Probably what's happening... getStableToken().mint(group, groupPayment); getStableToken().mint(account, validatorPayment); } @@ -620,7 +639,15 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi * @return True upon success. * @dev Fails if `validator` has not set their affiliation to this account. */ - function _addMember(address group, address validator, address lesser, address greater) private returns (bool) { + function _addMember( + address group, + address validator, + address lesser, + address greater + ) + private + returns (bool) + { require(isValidatorGroup(group) && isValidator(validator)); ValidatorGroup storage _group = groups[group]; require(_group.members.numElements < maxGroupSize, "group would exceed maximum size"); diff --git a/packages/protocol/contracts/governance/test/ElectionTest.sol b/packages/protocol/contracts/governance/test/ElectionTest.sol index 383e915ad11..37e1bb4dcb6 100644 --- a/packages/protocol/contracts/governance/test/ElectionTest.sol +++ b/packages/protocol/contracts/governance/test/ElectionTest.sol @@ -8,7 +8,14 @@ import "../../common/FixidityLib.sol"; */ contract ElectionTest is Election { - function distributeEpochRewards(address group, uint256 value, address lesser, address greater) external { + function distributeEpochRewards( + address group, + uint256 value, + address lesser, + address greater + ) + external + { return _distributeEpochRewards(group, value, lesser, greater); } } diff --git a/packages/protocol/contracts/governance/test/MockLockedGold.sol b/packages/protocol/contracts/governance/test/MockLockedGold.sol index e92dd7b4a45..848338a9326 100644 --- a/packages/protocol/contracts/governance/test/MockLockedGold.sol +++ b/packages/protocol/contracts/governance/test/MockLockedGold.sol @@ -31,7 +31,13 @@ contract MockLockedGold { return accountOrValidator; } - function getAccountFromActiveValidator(address accountOrValidator) external pure returns (address) { + function getAccountFromActiveValidator( + address accountOrValidator + ) + external + pure + returns (address) + { return accountOrValidator; } diff --git a/packages/protocol/migrations/12_election.ts b/packages/protocol/migrations/12_election.ts index 84c89838fa4..dedc26a72b9 100644 --- a/packages/protocol/migrations/12_election.ts +++ b/packages/protocol/migrations/12_election.ts @@ -1,3 +1,4 @@ +import { toFixed } from '@celo/utils/lib/fixidity' import { CeloContractName } from '@celo/protocol/lib/registry-utils' import { deploymentForCoreContract } from '@celo/protocol/lib/web3-utils' import { config } from '@celo/protocol/migrationsConfig' @@ -9,7 +10,7 @@ const initializeArgs = async (): Promise => { config.election.minElectableValidators, config.election.maxElectableValidators, config.election.maxVotesPerAccount, - config.election.electabilityThreshold, + toFixed(config.election.electabilityThreshold).toFixed(), ] } diff --git a/packages/protocol/migrations/17_elect_validators.ts b/packages/protocol/migrations/17_elect_validators.ts index 4d354c6e4f7..38c6bf9f163 100644 --- a/packages/protocol/migrations/17_elect_validators.ts +++ b/packages/protocol/migrations/17_elect_validators.ts @@ -164,14 +164,11 @@ module.exports = async (_deployer: any) => { for (let i = 0; i < valKeys.length; i++) { const key = valKeys[i] const address = generateAccountAddressFromPrivateKey(key.slice(2)) - let addTx: any - if (i == 0) { - // @ts-ignore - addTx = validators.contract.methods.addFirstMember(address, NULL_ADDRESS, NULL_ADDRESS) - } else { - // @ts-ignore - addTx = validators.contract.methods.addMember(address) - } + // @ts-ignore + const addTx = + i === 0 + ? validators.contract.methods.addFirstMember(address, NULL_ADDRESS, NULL_ADDRESS) + : validators.contract.methods.addMember(address) await sendTransactionWithPrivateKey(web3, addTx, account.privateKey, { to: validators.address, }) diff --git a/packages/protocol/migrationsConfig.js b/packages/protocol/migrationsConfig.js index a4006e9a971..94c84141777 100644 --- a/packages/protocol/migrationsConfig.js +++ b/packages/protocol/migrationsConfig.js @@ -21,7 +21,7 @@ const DefaultConfig = { minElectableValidators: '22', maxElectableValidators: '100', maxVotesPerAccount: 3, - electabilityThreshold: '0', // no threshold + electabilityThreshold: 1 / 100, }, exchange: { spread: 5 / 1000, diff --git a/packages/protocol/test/governance/election.ts b/packages/protocol/test/governance/election.ts index 4e4bc7a4bfd..a2a0c43c16c 100644 --- a/packages/protocol/test/governance/election.ts +++ b/packages/protocol/test/governance/election.ts @@ -6,7 +6,7 @@ import { NULL_ADDRESS, mineBlocks, } from '@celo/protocol/lib/test-utils' -import { toFixed } from '@celo/utils/lib/fixidity' +import { fromFixed, toFixed } from '@celo/utils/lib/fixidity' import BigNumber from 'bignumber.js' import { MockLockedGoldContract, @@ -46,7 +46,7 @@ contract('Election', (accounts: string[]) => { max: new BigNumber(6), } const maxNumGroupsVotedFor = new BigNumber(3) - const electabilityThreshold = new BigNumber(0) + const electabilityThreshold = toFixed(1 / 100) beforeEach(async () => { election = await ElectionTest.new() @@ -867,6 +867,35 @@ contract('Election', (accounts: string[]) => { }) }) + describe('when a group does not receive `electabilityThresholdVotes', () => { + beforeEach(async () => { + const thresholdExcludingGroup3 = (voter3.weight + 1) / totalLockedGold + await election.setElectabilityThreshold(toFixed(thresholdExcludingGroup3)) + await election.vote(group1, voter1.weight, group2, NULL_ADDRESS, { from: voter1.address }) + await election.vote(group2, voter2.weight, NULL_ADDRESS, group1, { from: voter2.address }) + await election.vote(group3, voter3.weight, NULL_ADDRESS, group2, { from: voter3.address }) + const totalVotes = await election.getTotalVotesForEligibleValidatorGroups() + console.log(totalVotes[1].map((x) => x.toFixed())) + console.log(fromFixed(await election.getElectabilityThreshold()).toFixed()) + console.log((await election.getTotalVotes()).toFixed()) + console.log((await election.getRequiredVotes()).toFixed()) + console.log((await election.getNumElectionGroups()).toFixed()) + console.log(await election.getElectionGroups()) + console.log(group1, group2, group3) + }) + + it('should not elect any members from that group', async () => { + assertSameAddresses(await election.electValidators(), [ + validator1, + validator2, + validator3, + validator4, + validator5, + validator6, + ]) + }) + }) + describe('when there are not enough electable validators', () => { beforeEach(async () => { await election.vote(group2, voter2.weight, group1, NULL_ADDRESS, { from: voter2.address }) From aba85529225bd21f74dee5e84a85f4044288429b Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Thu, 10 Oct 2019 13:56:21 -0700 Subject: [PATCH 052/149] Don't pay out epoch payments unless validator and group meet balance requirements --- .../contracts/governance/Election.sol | 2 +- .../contracts/governance/Validators.sol | 24 ++++++--- .../migrations/17_elect_validators.ts | 7 +-- .../protocol/test/governance/validators.ts | 50 ++++++++++++++++--- 4 files changed, 65 insertions(+), 18 deletions(-) diff --git a/packages/protocol/contracts/governance/Election.sol b/packages/protocol/contracts/governance/Election.sol index 5edfc83ff0f..aafe0cc039c 100644 --- a/packages/protocol/contracts/governance/Election.sol +++ b/packages/protocol/contracts/governance/Election.sol @@ -463,10 +463,10 @@ contract Election is view returns (uint256) { - // TODO(asa): Is this right? if (votes.active.total == 0) { return 0; } + return totalEpochRewards.mul(votes.active.forGroup[group].total).div(votes.active.total); } diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index 58b742bf359..c1232c7c1e7 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -482,14 +482,22 @@ contract Validators is function _distributeEpochPayment(address validator) internal { address account = getLockedGold().getAccountFromValidator(validator); require(isValidator(account)); - FixidityLib.Fraction memory totalPayment = FixidityLib.newFixed( - validatorEpochPayment - ).multiply(validators[account].score); address group = getMembershipInLastEpoch(account); - uint256 groupPayment = totalPayment.multiply(groups[group].commission).fromFixed(); - uint256 validatorPayment = totalPayment.fromFixed().sub(groupPayment); - getStableToken().mint(group, groupPayment); - getStableToken().mint(account, validatorPayment); + // Both the validator and the group must maintain the minimum locked gold balance in order to + // receive epoch payments. + bool meetsBalanceRequirements = ( + getLockedGold().getAccountTotalLockedGold(group) >= getAccountBalanceRequirement(group) && + getLockedGold().getAccountTotalLockedGold(account) >= getAccountBalanceRequirement(account) + ); + if (meetsBalanceRequirements) { + FixidityLib.Fraction memory totalPayment = FixidityLib.newFixed( + validatorEpochPayment + ).multiply(validators[account].score); + uint256 groupPayment = totalPayment.multiply(groups[group].commission).fromFixed(); + uint256 validatorPayment = totalPayment.fromFixed().sub(groupPayment); + getStableToken().mint(group, groupPayment); + getStableToken().mint(account, validatorPayment); + } } /** @@ -706,7 +714,7 @@ contract Validators is * @param account The account that may have to meet locked gold balance requirements. * @return The locked gold balance requirement for the supplied account. */ - function getAccountBalanceRequirement(address account) external view returns (uint256) { + function getAccountBalanceRequirement(address account) public view returns (uint256) { DeregistrationTimestamps storage timestamps = deregistrationTimestamps[account]; if ( isValidator(account) || diff --git a/packages/protocol/migrations/17_elect_validators.ts b/packages/protocol/migrations/17_elect_validators.ts index 38c6bf9f163..40a28f47452 100644 --- a/packages/protocol/migrations/17_elect_validators.ts +++ b/packages/protocol/migrations/17_elect_validators.ts @@ -164,11 +164,12 @@ module.exports = async (_deployer: any) => { for (let i = 0; i < valKeys.length; i++) { const key = valKeys[i] const address = generateAccountAddressFromPrivateKey(key.slice(2)) - // @ts-ignore const addTx = i === 0 - ? validators.contract.methods.addFirstMember(address, NULL_ADDRESS, NULL_ADDRESS) - : validators.contract.methods.addMember(address) + ? // @ts-ignore + validators.contract.methods.addFirstMember(address, NULL_ADDRESS, NULL_ADDRESS) + : // @ts-ignore + validators.contract.methods.addMember(address) await sendTransactionWithPrivateKey(web3, addTx, account.privateKey, { to: validators.address, }) diff --git a/packages/protocol/test/governance/validators.ts b/packages/protocol/test/governance/validators.ts index 96ff46eef48..d034dc03dd5 100644 --- a/packages/protocol/test/governance/validators.ts +++ b/packages/protocol/test/governance/validators.ts @@ -1363,7 +1363,7 @@ contract('Validators', (accounts: string[]) => { }) }) - describe('#distributeEpochPayment', () => { + describe.only('#distributeEpochPayment', () => { const validator = accounts[0] const group = accounts[1] let mockStableToken: MockStableTokenInstance @@ -1385,15 +1385,53 @@ contract('Validators', (accounts: string[]) => { const expectedValidatorPayment = expectedTotalPayment.minus(expectedGroupPayment) beforeEach(async () => { await validators.updateValidatorScore(validator, toFixed(uptime)) - await validators.distributeEpochPayment(validator) }) - it('should pay the validator', async () => { - assertEqualBN(await mockStableToken.balanceOf(validator), expectedValidatorPayment) + describe('when the validator and group meet the balance requirements', () => { + beforeEach(async () => { + await validators.distributeEpochPayment(validator) + }) + + it('should pay the validator', async () => { + assertEqualBN(await mockStableToken.balanceOf(validator), expectedValidatorPayment) + }) + + it('should pay the group', async () => { + assertEqualBN(await mockStableToken.balanceOf(group), expectedGroupPayment) + }) + }) + + describe('when the validator does not meet the balance requirements', () => { + beforeEach(async () => { + await mockLockedGold.setAccountTotalLockedGold( + validator, + balanceRequirements.validator.minus(1) + ) + await validators.distributeEpochPayment(validator) + }) + + it('should not pay the validator', async () => { + assertEqualBN(await mockStableToken.balanceOf(validator), 0) + }) + + it('should not pay the group', async () => { + assertEqualBN(await mockStableToken.balanceOf(group), 0) + }) }) - it('should pay the group', async () => { - assertEqualBN(await mockStableToken.balanceOf(group), expectedGroupPayment) + describe('when the validator does not meet the balance requirements', () => { + beforeEach(async () => { + await mockLockedGold.setAccountTotalLockedGold(group, balanceRequirements.group.minus(1)) + await validators.distributeEpochPayment(validator) + }) + + it('should not pay the validator', async () => { + assertEqualBN(await mockStableToken.balanceOf(validator), 0) + }) + + it('should not pay the group', async () => { + assertEqualBN(await mockStableToken.balanceOf(group), 0) + }) }) }) }) From 16f476c85cec6e3b5a041e3e087f0032af97ed49 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Thu, 10 Oct 2019 14:54:55 -0700 Subject: [PATCH 053/149] Don't pay out epoch rewards unless the group meets balance requirements --- .../contracts/governance/Election.sol | 11 +- packages/protocol/test/governance/election.ts | 107 ++++++++++++++++-- 2 files changed, 106 insertions(+), 12 deletions(-) diff --git a/packages/protocol/contracts/governance/Election.sol b/packages/protocol/contracts/governance/Election.sol index aafe0cc039c..08f300721e5 100644 --- a/packages/protocol/contracts/governance/Election.sol +++ b/packages/protocol/contracts/governance/Election.sol @@ -463,11 +463,16 @@ contract Election is view returns (uint256) { - if (votes.active.total == 0) { + bool meetsBalanceRequirements = ( + getLockedGold().getAccountTotalLockedGold(group) >= + getValidators().getAccountBalanceRequirement(group) + ); + + if (meetsBalanceRequirements && votes.active.total > 0) { + return totalEpochRewards.mul(votes.active.forGroup[group].total).div(votes.active.total); + } else { return 0; } - - return totalEpochRewards.mul(votes.active.forGroup[group].total).div(votes.active.total); } function distributeEpochRewards( diff --git a/packages/protocol/test/governance/election.ts b/packages/protocol/test/governance/election.ts index a2a0c43c16c..46dfce00a89 100644 --- a/packages/protocol/test/governance/election.ts +++ b/packages/protocol/test/governance/election.ts @@ -6,7 +6,7 @@ import { NULL_ADDRESS, mineBlocks, } from '@celo/protocol/lib/test-utils' -import { fromFixed, toFixed } from '@celo/utils/lib/fixidity' +import { toFixed } from '@celo/utils/lib/fixidity' import BigNumber from 'bignumber.js' import { MockLockedGoldContract, @@ -874,14 +874,6 @@ contract('Election', (accounts: string[]) => { await election.vote(group1, voter1.weight, group2, NULL_ADDRESS, { from: voter1.address }) await election.vote(group2, voter2.weight, NULL_ADDRESS, group1, { from: voter2.address }) await election.vote(group3, voter3.weight, NULL_ADDRESS, group2, { from: voter3.address }) - const totalVotes = await election.getTotalVotesForEligibleValidatorGroups() - console.log(totalVotes[1].map((x) => x.toFixed())) - console.log(fromFixed(await election.getElectabilityThreshold()).toFixed()) - console.log((await election.getTotalVotes()).toFixed()) - console.log((await election.getRequiredVotes()).toFixed()) - console.log((await election.getNumElectionGroups()).toFixed()) - console.log(await election.getElectionGroups()) - console.log(group1, group2, group3) }) it('should not elect any members from that group', async () => { @@ -908,6 +900,103 @@ contract('Election', (accounts: string[]) => { }) }) + describe('#getGroupEpochRewards', () => { + const voter = accounts[0] + const group1 = accounts[1] + const group2 = accounts[2] + const voteValue1 = new BigNumber(2000000) + const voteValue2 = new BigNumber(1000000) + const totalRewardValue = new BigNumber(3000000) + const balanceRequirement = new BigNumber(1000000) + beforeEach(async () => { + await registry.setAddressFor(CeloContractName.Validators, accounts[0]) + await election.markGroupEligible(group1, NULL_ADDRESS, NULL_ADDRESS) + await election.markGroupEligible(group2, NULL_ADDRESS, group1) + await registry.setAddressFor(CeloContractName.Validators, mockValidators.address) + await mockLockedGold.setTotalLockedGold(voteValue1.plus(voteValue2)) + await mockValidators.setMembers(group1, [accounts[8]]) + await mockValidators.setMembers(group2, [accounts[9]]) + await mockValidators.setNumRegisteredValidators(2) + await mockLockedGold.incrementNonvotingAccountBalance(voter, voteValue1.plus(voteValue2)) + await election.vote(group1, voteValue1, group2, NULL_ADDRESS) + await election.vote(group2, voteValue2, NULL_ADDRESS, group1) + await mockValidators.setAccountBalanceRequirement(group1, balanceRequirement) + await mockValidators.setAccountBalanceRequirement(group2, balanceRequirement) + }) + + describe('when one group has active votes', () => { + beforeEach(async () => { + await mineBlocks(EPOCH, web3) + await election.activate(group1) + }) + + describe('when the group meets the balance requirements ', () => { + beforeEach(async () => { + await mockLockedGold.setAccountTotalLockedGold(group1, balanceRequirement) + }) + + it('should return the total reward value', async () => { + assertEqualBN( + await election.getGroupEpochRewards(group1, totalRewardValue), + totalRewardValue + ) + }) + }) + + describe('when the group does not meet the balance requirements ', () => { + beforeEach(async () => { + await mockLockedGold.setAccountTotalLockedGold(group1, balanceRequirement.minus(1)) + }) + + it('should return zero', async () => { + assertEqualBN(await election.getGroupEpochRewards(group1, totalRewardValue), 0) + }) + }) + }) + + describe('when two groups have active votes', () => { + const balanceRequirement = new BigNumber(1000000) + const expectedGroup1EpochRewards = voteValue1 + .div(voteValue1.plus(voteValue2)) + .times(totalRewardValue) + .dp(0) + beforeEach(async () => { + await mineBlocks(EPOCH, web3) + await election.activate(group1) + await election.activate(group2) + }) + + describe('when one group meets the balance requirements ', () => { + beforeEach(async () => { + await mockLockedGold.setAccountTotalLockedGold(group1, balanceRequirement) + }) + + it('should return the proportional reward value for that group', async () => { + assertEqualBN( + await election.getGroupEpochRewards(group1, totalRewardValue), + expectedGroup1EpochRewards + ) + }) + + it('should return zero for the other group', async () => { + assertEqualBN(await election.getGroupEpochRewards(group2, totalRewardValue), 0) + }) + }) + }) + + describe('when the group does not have active votes', () => { + describe('when the group meets the balance requirements ', () => { + beforeEach(async () => { + await mockLockedGold.setAccountTotalLockedGold(group1, balanceRequirement) + }) + + it('should return zero', async () => { + assertEqualBN(await election.getGroupEpochRewards(group1, totalRewardValue), 0) + }) + }) + }) + }) + describe('#distributeEpochRewards', () => { const voter = accounts[0] const group = accounts[1] From 3616cc689db05d8338e7735be5c56a06ad7f80a9 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Thu, 10 Oct 2019 15:10:27 -0700 Subject: [PATCH 054/149] Fix migrations --- packages/protocol/migrations/12_election.ts | 2 +- .../migrations/17_elect_validators.ts | 22 +++++++++++-------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/packages/protocol/migrations/12_election.ts b/packages/protocol/migrations/12_election.ts index dedc26a72b9..94bc55add3a 100644 --- a/packages/protocol/migrations/12_election.ts +++ b/packages/protocol/migrations/12_election.ts @@ -1,7 +1,7 @@ -import { toFixed } from '@celo/utils/lib/fixidity' import { CeloContractName } from '@celo/protocol/lib/registry-utils' import { deploymentForCoreContract } from '@celo/protocol/lib/web3-utils' import { config } from '@celo/protocol/migrationsConfig' +import { toFixed } from '@celo/utils/lib/fixidity' import { ElectionInstance } from 'types' const initializeArgs = async (): Promise => { diff --git a/packages/protocol/migrations/17_elect_validators.ts b/packages/protocol/migrations/17_elect_validators.ts index 40a28f47452..61fa8fc3d02 100644 --- a/packages/protocol/migrations/17_elect_validators.ts +++ b/packages/protocol/migrations/17_elect_validators.ts @@ -164,15 +164,19 @@ module.exports = async (_deployer: any) => { for (let i = 0; i < valKeys.length; i++) { const key = valKeys[i] const address = generateAccountAddressFromPrivateKey(key.slice(2)) - const addTx = - i === 0 - ? // @ts-ignore - validators.contract.methods.addFirstMember(address, NULL_ADDRESS, NULL_ADDRESS) - : // @ts-ignore - validators.contract.methods.addMember(address) - await sendTransactionWithPrivateKey(web3, addTx, account.privateKey, { - to: validators.address, - }) + if (i === 0) { + // @ts-ignore + const addTx = validators.contract.methods.addFirstMember(address, NULL_ADDRESS, NULL_ADDRESS) + await sendTransactionWithPrivateKey(web3, addTx, account.privateKey, { + to: validators.address, + }) + } else { + // @ts-ignore + const addTx = validators.contract.methods.addMember(address) + await sendTransactionWithPrivateKey(web3, addTx, account.privateKey, { + to: validators.address, + }) + } } console.info(' Voting for Validator Group ...') From 64016a72558b4030630a7ae5dd8408e5f7192daa Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Fri, 11 Oct 2019 12:10:53 -0700 Subject: [PATCH 055/149] Beef up documentation --- packages/celotool/src/e2e-tests/utils.ts | 4 +- packages/contractkit/src/wrappers/Election.ts | 24 ++++++++++ .../contractkit/src/wrappers/LockedGold.ts | 44 +++++++++++++++++++ .../contractkit/src/wrappers/Validators.ts | 4 ++ .../contracts/governance/Election.sol | 19 ++++++++ .../contracts/governance/Validators.sol | 5 +++ 6 files changed, 98 insertions(+), 2 deletions(-) diff --git a/packages/celotool/src/e2e-tests/utils.ts b/packages/celotool/src/e2e-tests/utils.ts index 98e2b119688..aae32ca88ac 100644 --- a/packages/celotool/src/e2e-tests/utils.ts +++ b/packages/celotool/src/e2e-tests/utils.ts @@ -234,8 +234,8 @@ async function waitForPortOpen(host: string, port: number, seconds: number) { return false } -export async function sleep(seconds: number) { - await execCmd('sleep', [seconds.toString()]) +export function sleep(seconds: number) { + return new Promise((resolve) => setTimeout(resolve, seconds * 1000)) } export async function getEnode(port: number, ws: boolean = false) { diff --git a/packages/contractkit/src/wrappers/Election.ts b/packages/contractkit/src/wrappers/Election.ts index 24a5dd25d77..4da80f58f8b 100644 --- a/packages/contractkit/src/wrappers/Election.ts +++ b/packages/contractkit/src/wrappers/Election.ts @@ -80,6 +80,11 @@ export class ElectionWrapper extends BaseWrapper { toNumber ) + /** + * Returns the groups that `account` has voted for. + * @param account The address of the account casting votes. + * @return The groups that `account` has voted for. + */ getGroupsVotedForByAccount: (account: Address) => Promise = proxyCall( this.contract.methods.getGroupsVotedForByAccount ) @@ -100,6 +105,9 @@ export class ElectionWrapper extends BaseWrapper { } } + /** + * Returns the addresses in the current validator set. + */ async getValidatorSetAddresses(): Promise { const numberValidators = await this.numberValidatorsInCurrentSet() @@ -112,6 +120,9 @@ export class ElectionWrapper extends BaseWrapper { return Promise.all(validatorAddressPromises) } + /** + * Returns the current registered validator groups and their total votes and eligibility. + */ async getValidatorGroupsVotes(): Promise { const validators = await this.kit.contracts.getValidators() const validatorGroupAddresses = (await validators.getRegisteredValidatorGroups()).map( @@ -130,11 +141,19 @@ export class ElectionWrapper extends BaseWrapper { })) } + /** + * Returns the current eligible validator groups and their total votes. + */ async getEligibleValidatorGroupsVotes(): Promise { const res = await this.contract.methods.getTotalVotesForEligibleValidatorGroups().call() return zip((a, b) => ({ address: a, votes: new BigNumber(b), eligible: true }), res[0], res[1]) } + /** + * Marks a group eligible for electing validators. + * @param lesser The address of the group that has received fewer votes than this group. + * @param greater The address of the group that has received more votes than this group. + */ async markGroupEligible(validatorGroup: Address): Promise> { if (this.kit.defaultAccount == null) { throw new Error(`missing from at new ValdidatorUtils()`) @@ -148,6 +167,11 @@ export class ElectionWrapper extends BaseWrapper { return wrapSend(this.kit, this.contract.methods.markGroupEligible(lesser, greater)) } + /** + * Increments the number of total and pending votes for `group`. + * @param validatorGroup The validator group to vote for. + * @param value The amount of gold to use to vote. + */ async vote(validatorGroup: Address, value: BigNumber): Promise> { if (this.kit.defaultAccount == null) { throw new Error(`missing from at new ValdidatorUtils()`) diff --git a/packages/contractkit/src/wrappers/LockedGold.ts b/packages/contractkit/src/wrappers/LockedGold.ts index 75c96d77b8d..979289a51dc 100644 --- a/packages/contractkit/src/wrappers/LockedGold.ts +++ b/packages/contractkit/src/wrappers/LockedGold.ts @@ -48,35 +48,73 @@ export interface LockedGoldConfig { * Contract for handling deposits needed for voting. */ export class LockedGoldWrapper extends BaseWrapper { + /** + * Unlocks gold that becomes withdrawable after the unlocking period. + * @param value The amount of gold to unlock. + */ unlock: (value: NumberLike) => CeloTransactionObject = proxySend( this.kit, this.contract.methods.unlock, tupleParser(parseNumber) ) + /** + * Creates an account. + */ createAccount = proxySend(this.kit, this.contract.methods.createAccount) + /** + * Withdraws a gold that has been unlocked after the unlocking period has passed. + * @param index The index of the pending withdrawal to withdraw. + */ withdraw: (index: number) => CeloTransactionObject = proxySend( this.kit, this.contract.methods.withdraw ) + /** + * @notice Locks gold to be used for voting. + */ lock = proxySend(this.kit, this.contract.methods.lock) + /** + * Relocks gold that has been unlocked but not withdrawn. + * @param index The index of the pending withdrawal to relock. + */ relock: (index: number) => CeloTransactionObject = proxySend( this.kit, this.contract.methods.relock ) + /** + * Returns the total amount of locked gold for an account. + * @param account The account. + * @return The total amount of locked gold for an account. + */ getAccountTotalLockedGold = proxyCall( this.contract.methods.getAccountTotalLockedGold, undefined, toBigNumber ) + /** + * Returns the total amount of non-voting locked gold for an account. + * @param account The account. + * @return The total amount of non-voting locked gold for an account. + */ getAccountNonvotingLockedGold = proxyCall( this.contract.methods.getAccountNonvotingLockedGold, undefined, toBigNumber ) + /** + * Returns the voter for the specified account. + * @param account The address of the account. + * @return The address with which the account can vote. + */ getVoterFromAccount: (account: string) => Promise
= proxyCall( this.contract.methods.getVoterFromAccount ) + /** + * Returns the validator for the specified account. + * @param account The address of the account. + * @return The address with which the account can register a validator or group. + */ getValidatorFromAccount: (account: string) => Promise
= proxyCall( this.contract.methods.getValidatorFromAccount ) @@ -138,6 +176,11 @@ export class LockedGoldWrapper extends BaseWrapper { ) } + /** + * Returns the pending withdrawals from unlocked gold for an account. + * @param account The address of the account. + * @return The value and timestamp for each pending withdrawal. + */ async getPendingWithdrawals(account: string) { const withdrawals = await this.contract.methods.getPendingWithdrawals(account).call() return zip( @@ -148,6 +191,7 @@ export class LockedGoldWrapper extends BaseWrapper { withdrawals[0] ) } + private async getParsedSignatureOfAddress(address: Address, signer: string) { const hash = Web3.utils.soliditySha3({ type: 'address', value: address }) const signature = (await this.kit.web3.eth.sign(hash, signer)).slice(2) diff --git a/packages/contractkit/src/wrappers/Validators.ts b/packages/contractkit/src/wrappers/Validators.ts index 35494ab25fd..4ecc8e9ceb2 100644 --- a/packages/contractkit/src/wrappers/Validators.ts +++ b/packages/contractkit/src/wrappers/Validators.ts @@ -77,6 +77,10 @@ export class ValidatorsWrapper extends BaseWrapper { } } + /** + * Returns the lockup periods after deregistering groups and validators. + * @return The lockup periods after deregistering groups and validators. + */ async getDeregistrationLockups(): Promise { const res = await this.contract.methods.getDeregistrationLockups().call() return { diff --git a/packages/protocol/contracts/governance/Election.sol b/packages/protocol/contracts/governance/Election.sol index b222873d852..3f1c7919503 100644 --- a/packages/protocol/contracts/governance/Election.sol +++ b/packages/protocol/contracts/governance/Election.sol @@ -164,6 +164,10 @@ contract Election is IElection, Ownable, ReentrancyGuard, Initializable, UsingRe return _setElectableValidators(min, max); } + /** + * @notice Returns the minimum and maximum number of validators that can be elected. + * @return The minimum and maximum number of validators that can be elected. + */ function getElectableValidators() external view returns (uint256, uint256) { return (electableValidators.min, electableValidators.max); } @@ -372,6 +376,11 @@ contract Election is IElection, Ownable, ReentrancyGuard, Initializable, UsingRe return true; } + /** + * @notice Returns the total number of votes cast by an account. + * @param account The address of the account. + * @return The total number of votes cast by an account. + */ function getTotalVotesByAccount(address account) external view returns (uint256) { uint256 total = 0; address[] memory groups = votes.groupsVotedFor[account]; @@ -449,6 +458,11 @@ contract Election is IElection, Ownable, ReentrancyGuard, Initializable, UsingRe return votes.pending.forGroup[group].total.add(votes.active.forGroup[group].total); } + /** + * @notice Returns whether or not a group is eligible to receive votes. + * @return Whether or not a group is eligible to receive votes. + * @dev Eligible groups that have received their maximum number of votes cannot receive more. + */ function getGroupEligibility(address group) external view returns (bool) { return votes.total.eligible.contains(group); } @@ -834,6 +848,11 @@ contract Election is IElection, Ownable, ReentrancyGuard, Initializable, UsingRe return (groupIndex, memberElected); } + /** + * @notice Randomly permutes an array of addresses. + * @param array The array to permute. + * @return The permuted array. + */ function shuffleArray(address[] memory array) private view returns (address[] memory) { bytes32 r = getRandom().random(); for (uint256 i = array.length - 1; i > 0; i = i.sub(1)) { diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index 7ef9cccfb51..1756a78d372 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -499,6 +499,11 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi return 0; } + /** + * @notice Returns the timestamp of the last time this account deregistered a validator or group. + * @param account The account to query. + * @return The timestamp of the last time this account deregistered a validator or group. + */ function getDeregistrationTimestamps(address account) external view returns (uint256, uint256) { return (deregistrationTimestamps[account].group, deregistrationTimestamps[account].validator); } From fb5e8b4a86f573e3e026c1043b538ab19fc23a29 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Fri, 11 Oct 2019 12:19:44 -0700 Subject: [PATCH 056/149] Fix linting issues --- .../src/e2e-tests/governance_tests.ts | 26 +++++++------------ 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/packages/celotool/src/e2e-tests/governance_tests.ts b/packages/celotool/src/e2e-tests/governance_tests.ts index e2b081352ca..da77abe07f8 100644 --- a/packages/celotool/src/e2e-tests/governance_tests.ts +++ b/packages/celotool/src/e2e-tests/governance_tests.ts @@ -1,6 +1,6 @@ -import BigNumber from 'bignumber.js' import { ContractKit, newKitFromWeb3 } from '@celo/contractkit' import { fromFixed, toFixed } from '@celo/utils/lib/fixidity' +import BigNumber from 'bignumber.js' import { assert } from 'chai' import Web3 from 'web3' import { getContext, getEnode, importGenesis, initAndStartGeth, sleep } from './utils' @@ -76,7 +76,7 @@ describe('governance tests', () => { return [groupAddress, decryptedKeystore.privateKey] } - const activate = async (web3: any, account: string, txOptions: any = {}) => { + const activate = async (account: string, txOptions: any = {}) => { await unlockAccount(account, web3) const [group] = await validators.methods.getRegisteredValidatorGroups().call() const tx = election.methods.activate(group) @@ -113,7 +113,7 @@ describe('governance tests', () => { } const isLastBlockOfEpoch = (blockNumber: number, epochSize: number) => { - return blockNumber % epochSize == 0 + return blockNumber % epochSize === 0 } describe('when the validator set is changing', () => { @@ -142,7 +142,7 @@ describe('governance tests', () => { // Give the node time to sync. await sleep(15) - await activate(web3, allValidators[0]) + await activate(allValidators[0]) const groupWeb3 = new Web3('ws://localhost:8567') const groupKit = newKitFromWeb3(groupWeb3) validators = await groupKit._web3Contracts.getValidators() @@ -221,7 +221,6 @@ describe('governance tests', () => { }) it('should update the validator scores at the end of each epoch', async () => { - const validators = await kit._web3Contracts.getValidators() const adjustmentSpeed = fromFixed( new BigNumber((await validators.methods.getValidatorScoreParameters().call())[1]) ) @@ -272,7 +271,6 @@ describe('governance tests', () => { }) it('should distribute epoch payments at the end of each epoch', async () => { - const validators = await kit._web3Contracts.getValidators() const stableToken = await kit._web3Contracts.getStableToken() const commission = 0.1 const validatorEpochPayment = new BigNumber( @@ -336,19 +334,13 @@ describe('governance tests', () => { }) it('should distribute epoch rewards at the end of each epoch', async () => { - const validators = await kit._web3Contracts.getValidators() - const election = await kit._web3Contracts.getElection() const lockedGold = await kit._web3Contracts.getLockedGold() const governance = await kit._web3Contracts.getGovernance() const epochReward = new BigNumber(10).pow(18) const infraReward = new BigNumber(10).pow(18) const [group] = await validators.methods.getRegisteredValidatorGroups().call() - const assertVotesChanged = async ( - group: string, - blockNumber: number, - expected: BigNumber - ) => { + const assertVotesChanged = async (blockNumber: number, expected: BigNumber) => { const currentVotes = new BigNumber( await election.methods.getTotalVotesForGroup(group).call({}, blockNumber) ) @@ -393,8 +385,8 @@ describe('governance tests', () => { await assertBalanceChanged(governance.options.address, blockNumber, expected) } - const assertVotesUnchanged = async (group: string, blockNumber: number) => { - await assertVotesChanged(group, blockNumber, new BigNumber(0)) + const assertVotesUnchanged = async (blockNumber: number) => { + await assertVotesChanged(blockNumber, new BigNumber(0)) } const assertGoldTokenTotalSupplyUnchanged = async (blockNumber: number) => { @@ -411,12 +403,12 @@ describe('governance tests', () => { for (const blockNumber of blockNumbers) { if (isLastBlockOfEpoch(blockNumber, epoch)) { - await assertVotesChanged(group, blockNumber, epochReward) + await assertVotesChanged(blockNumber, epochReward) await assertGoldTokenTotalSupplyChanged(blockNumber, epochReward.plus(infraReward)) await assertLockedGoldBalanceChanged(blockNumber, epochReward) await assertGovernanceBalanceChanged(blockNumber, infraReward) } else { - await assertVotesUnchanged(group, blockNumber) + await assertVotesUnchanged(blockNumber) await assertGoldTokenTotalSupplyUnchanged(blockNumber) await assertLockedGoldBalanceUnchanged(blockNumber) await assertGovernanceBalanceUnchanged(blockNumber) From 449e1ede24b0774bd2602d7952d0578e890e0174 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Fri, 11 Oct 2019 12:44:06 -0700 Subject: [PATCH 057/149] Add documentation --- .../contracts/governance/Election.sol | 21 ++++++++++++ .../contracts/governance/Validators.sol | 33 +++++++++++++++++++ .../governance/test/MockLockedGold.sol | 3 +- 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/packages/protocol/contracts/governance/Election.sol b/packages/protocol/contracts/governance/Election.sol index 270b6a6cca0..3b9651a8ba0 100644 --- a/packages/protocol/contracts/governance/Election.sol +++ b/packages/protocol/contracts/governance/Election.sol @@ -469,6 +469,13 @@ contract Election is return votes.total.eligible.contains(group); } + /** + * @notice Returns the amount of rewards that voters for `group` are due at the end of an epoch. + * @param group The group to calculate epoch rewards for. + * @param totalEpochRewards The total amount of rewards going to all voters. + * @return The amount of rewards that voters for `group` are due at the end of an epoch. + * @dev Eligible groups that have received their maximum number of votes cannot receive more. + */ function getGroupEpochRewards( address group, uint256 totalEpochRewards @@ -489,6 +496,13 @@ contract Election is } } + /** + * @notice Distributes epoch rewards to voters for `group` in the form of active votes. + * @param group The group whose voters will receive rewards. + * @param value The amount of rewards to distribute to voters for the group. + * @param lesser The group receiving fewer votes than `group` after the rewards are added. + * @param greater The group receiving more votes than `group` after the rewards are added. + */ function distributeEpochRewards( address group, uint256 value, @@ -501,6 +515,13 @@ contract Election is _distributeEpochRewards(group, value, lesser, greater); } + /** + * @notice Distributes epoch rewards to voters for `group` in the form of active votes. + * @param group The group whose voters will receive rewards. + * @param value The amount of rewards to distribute to voters for the group. + * @param lesser The group receiving fewer votes than `group` after the rewards are added. + * @param greater The group receiving more votes than `group` after the rewards are added. + */ function _distributeEpochRewards( address group, uint256 value, diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index d4ef0de070d..1b98061c3ed 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -66,11 +66,13 @@ contract Validators is FixidityLib.Fraction commission; } + // Stores the epoch number at which a validator joined a particular group. struct MembershipHistoryEntry { uint256 epochNumber; address group; } + // A circular buffer storing the membership history of a validator. struct MembershipHistory { uint256 head; MembershipHistoryEntry[] entries; @@ -85,6 +87,7 @@ contract Validators is MembershipHistory membershipHistory; } + // Parameters that govern the calculation of validator's score. struct ValidatorScoreParameters { uint256 exponent; FixidityLib.Fraction adjustmentSpeed; @@ -403,10 +406,19 @@ contract Validators is return getLockedGold().getAccountTotalLockedGold(account) >= balanceRequirements.group; } + /** + * @notice Returns the parameters that goven how a validator's score is calculated. + * @return The parameters that goven how a validator's score is calculated. + */ function getValidatorScoreParameters() external view returns (uint256, uint256) { return (validatorScoreParameters.exponent, validatorScoreParameters.adjustmentSpeed.unwrap()); } + /** + * @notice Returns the group membership history of a validator. + * @param account The validator whose membership history to return. + * @return The group membership history of a validator. + */ function getMembershipHistory( address account ) @@ -475,13 +487,21 @@ contract Validators is ); } + /** + * @notice Distributes epoch payments to `validator` and its group. + */ function distributeEpochPayment(address validator) external onlyVm() { _distributeEpochPayment(validator); } + /** + * @notice Distributes epoch payments to `validator` and its group. + */ function _distributeEpochPayment(address validator) internal { address account = getLockedGold().getAccountFromValidator(validator); require(isValidator(account)); + // The group that should be paid is the group that the validator was a member of at the + // time it was elected. address group = getMembershipInLastEpoch(account); // Both the validator and the group must maintain the minimum locked gold balance in order to // receive epoch payments. @@ -931,6 +951,14 @@ contract Validators is return true; } + /** + * @notice Updates the group membership history of a particular account. + * @param account The account whose group membership has changed. + * @param group The group that the account is now a member of. + * @return True upon success. + * @dev Note that this is used to determine a validator's membership at the time of an election, + * and so group changes within an epoch will overwrite eachother. + */ function updateMembershipHistory(address account, address group) private returns (bool) { MembershipHistory storage history = validators[account].membershipHistory; uint256 epochNumber = getEpochNumber(); @@ -951,6 +979,11 @@ contract Validators is } } + /** + * @notice Returns the group that `account` was a member of at the end of the last epoch. + * @param account The account whose group membership should be returned. + * @return The group that `account` was a member of at the end of the last epoch. + */ function getMembershipInLastEpoch(address account) public view returns (address) { uint256 epochNumber = getEpochNumber(); MembershipHistory storage history = validators[account].membershipHistory; diff --git a/packages/protocol/contracts/governance/test/MockLockedGold.sol b/packages/protocol/contracts/governance/test/MockLockedGold.sol index 848338a9326..532604b112c 100644 --- a/packages/protocol/contracts/governance/test/MockLockedGold.sol +++ b/packages/protocol/contracts/governance/test/MockLockedGold.sol @@ -8,8 +8,7 @@ import "../interfaces/ILockedGold.sol"; /** * @title A mock LockedGold for testing. */ -// TODO(asa): Use ILockedGold interface. -contract MockLockedGold { +contract MockLockedGold is ILockedGold { using SafeMath for uint256; From 143a69cd43e1875203a2de066e840fb07247e6d7 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Fri, 11 Oct 2019 12:52:22 -0700 Subject: [PATCH 058/149] Fix interface --- .../protocol/contracts/governance/test/MockLockedGold.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/protocol/contracts/governance/test/MockLockedGold.sol b/packages/protocol/contracts/governance/test/MockLockedGold.sol index 532604b112c..5e3ab2e68d8 100644 --- a/packages/protocol/contracts/governance/test/MockLockedGold.sol +++ b/packages/protocol/contracts/governance/test/MockLockedGold.sol @@ -26,7 +26,7 @@ contract MockLockedGold is ILockedGold { authorizedValidators[account] = validator; } - function getAccountFromValidator(address accountOrValidator) external pure returns (address) { + function getAccountFromValidator(address accountOrValidator) external view returns (address) { return accountOrValidator; } @@ -34,13 +34,13 @@ contract MockLockedGold is ILockedGold { address accountOrValidator ) external - pure + view returns (address) { return accountOrValidator; } - function getAccountFromActiveVoter(address accountOrVoter) external pure returns (address) { + function getAccountFromActiveVoter(address accountOrVoter) external view returns (address) { return accountOrVoter; } From 1f18108bc11cc92cab034daa8e308eb54df19a62 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Fri, 11 Oct 2019 14:07:01 -0700 Subject: [PATCH 059/149] Expire previously upvoted proposals --- .../contracts/governance/Governance.sol | 9 +++ .../protocol/test/governance/governance.ts | 61 +++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/packages/protocol/contracts/governance/Governance.sol b/packages/protocol/contracts/governance/Governance.sol index 2d863af40fb..c32e9f10991 100644 --- a/packages/protocol/contracts/governance/Governance.sol +++ b/packages/protocol/contracts/governance/Governance.sol @@ -497,6 +497,15 @@ contract Governance is IGovernance, Ownable, Initializable, ReentrancyGuard, Usi return false; } Voter storage voter = voters[account]; + // If the previously upvoted proposal is still in the queue but has expired, expire the + // proposal from the queue. + if ( + queue.contains(voter.upvote.proposalId) && + now >= proposals[voter.upvote.proposalId].timestamp.add(queueExpiry) + ) { + queue.remove(voter.upvote.proposalId); + emit ProposalExpired(voter.upvote.proposalId); + } // We can upvote a proposal in the queue if we're not already upvoting a proposal in the queue. uint256 weight = getLockedGold().getAccountTotalLockedGold(account); require(weight > 0, "cannot upvote without locking gold"); diff --git a/packages/protocol/test/governance/governance.ts b/packages/protocol/test/governance/governance.ts index 78dade7ba70..43c95b96652 100644 --- a/packages/protocol/test/governance/governance.ts +++ b/packages/protocol/test/governance/governance.ts @@ -1060,6 +1060,67 @@ contract('Governance', (accounts: string[]) => { await assertRevert(governance.upvote(proposalId, 0, 0)) }) }) + + describe('when the previously upvoted proposal is in the queue and expired', () => { + const upvotedProposalId = 2 + // Expire the upvoted proposal without dequeueing it. + const queueExpiry = 60 + beforeEach(async () => { + await governance.setQueueExpiry(60) + await governance.upvote(proposalId, 0, 0) + await timeTravel(queueExpiry, web3) + await governance.propose( + [transactionSuccess1.value], + [transactionSuccess1.destination], + transactionSuccess1.data, + [transactionSuccess1.data.length], + // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails + { value: minDeposit } + ) + }) + + it('should increase the number of upvotes for the proposal', async () => { + await governance.upvote(upvotedProposalId, 0, 0) + assertEqualBN(await governance.getUpvotes(upvotedProposalId), weight) + }) + + it('should mark the account as having upvoted the proposal', async () => { + await governance.upvote(upvotedProposalId, 0, 0) + const [recordId, recordWeight] = await governance.getUpvoteRecord(account) + assertEqualBN(recordId, upvotedProposalId) + assertEqualBN(recordWeight, weight) + }) + + it('should return true', async () => { + const success = await governance.upvote.call(upvotedProposalId, 0, 0) + assert.isTrue(success) + }) + + it('should emit the ProposalExpired event', async () => { + const resp = await governance.upvote(upvotedProposalId, 0, 0) + assert.equal(resp.logs.length, 2) + const log = resp.logs[0] + assertLogMatches2(log, { + event: 'ProposalExpired', + args: { + proposalId: new BigNumber(proposalId), + }, + }) + }) + it('should emit the ProposalUpvoted event', async () => { + const resp = await governance.upvote(upvotedProposalId, 0, 0) + assert.equal(resp.logs.length, 2) + const log = resp.logs[1] + assertLogMatches2(log, { + event: 'ProposalUpvoted', + args: { + proposalId: new BigNumber(upvotedProposalId), + account, + upvotes: new BigNumber(weight), + }, + }) + }) + }) }) describe('#revokeUpvote()', () => { From 506a77bca97b91de54183ed3abab8d6a54da0029 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Fri, 11 Oct 2019 15:20:52 -0700 Subject: [PATCH 060/149] Address comments --- .../contracts/governance/LockedGold.sol | 36 +++++++++++++++---- .../protocol/test/governance/lockedgold.ts | 29 +++++++++++++++ 2 files changed, 59 insertions(+), 6 deletions(-) diff --git a/packages/protocol/contracts/governance/LockedGold.sol b/packages/protocol/contracts/governance/LockedGold.sol index fcc81a120dc..1fbc1665b7b 100644 --- a/packages/protocol/contracts/governance/LockedGold.sol +++ b/packages/protocol/contracts/governance/LockedGold.sol @@ -16,8 +16,12 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr struct Authorizations { // The address that is authorized to vote on behalf of the account. + // The account can vote as well, whether or not an authorized voter has been specified. address voting; // The address that is authorized to validate on behalf of the account. + // The account can manage the validator, whether or not an authorized validator has been + // specified. However if an authorized validator has been specified, only that key may actually + // participate in consensus. address validating; } @@ -28,9 +32,11 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr uint256 timestamp; } + // NOTE: This contract does not store an account's locked gold that is being used in electing + // validators. struct Balances { - // This contract does not store an account's locked gold that is being used in electing - // validators. + // The amount of locked gold that this account has that is not currently participating in + // validator elections. uint256 nonvoting; // Gold that has been unlocked and will become available for withdrawal. PendingWithdrawal[] pendingWithdrawals; @@ -51,6 +57,7 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr uint256 public totalNonvoting; uint256 public unlockingPeriod; + event UnlockingPeriodSet(uint256 period); event VoterAuthorized(address indexed account, address voter); event ValidatorAuthorized(address indexed account, address validator); event GoldLocked(address indexed account, uint256 value); @@ -80,6 +87,7 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr * @param v The recovery id of the incoming ECDSA signature. * @param r Output value r of the ECDSA signature. * @param s Output value s of the ECDSA signature. + * @dev v, r, s constitute `voter`'s signature on `msg.sender`. */ function authorizeVoter( address voter, @@ -102,6 +110,7 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr * @param v The recovery id of the incoming ECDSA signature. * @param r Output value r of the ECDSA signature. * @param s Output value s of the ECDSA signature. + * @dev v, r, s constitute `validator`'s signature on `msg.sender`. */ function authorizeValidator( address validator, @@ -118,6 +127,16 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr emit ValidatorAuthorized(msg.sender, validator); } + /** + * @notice Sets the duration in seconds users must wait before withdrawing gold after unlocking. + * @param value The unlocking period in seconds. + */ + function setUnlockingPeriod(uint256 value) external onlyOwner { + require(value != unlockingPeriod); + unlockingPeriod = value; + emit UnlockingPeriodSet(value); + } + /** * @notice Locks gold to be used for voting. */ @@ -132,7 +151,7 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr * @notice Increments the non-voting balance for an account. * @param account The account whose non-voting balance should be incremented. * @param value The amount by which to increment. - * @dev Can only be called by the registered "Election" smart contract. + * @dev Can only be called by the registered Election smart contract. */ function incrementNonvotingAccountBalance( address account, @@ -211,7 +230,7 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr } /** - * @notice Withdraws a gold that has been unlocked after the unlocking period has passed. + * @notice Withdraws gold that has been unlocked after the unlocking period has passed. * @param index The index of the pending withdrawal to withdraw. */ function withdraw(uint256 index) external nonReentrant { @@ -327,7 +346,7 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr function getPendingWithdrawals( address account ) - public + external view returns (uint256[] memory, uint256[] memory) { @@ -353,7 +372,7 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr * @param r Output value r of the ECDSA signature. * @param s Output value s of the ECDSA signature. * @dev Fails if the address is already authorized or is an account. - * @dev v, r, s constitute `authorize`'s signature on `msg.sender`. + * @dev v, r, s constitute `current`'s signature on `msg.sender`. */ function authorize( address current, @@ -409,6 +428,11 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr return (authorizedBy[account] == address(0)); } + /** + * @notice Deletes a pending withdrawal. + * @param list The list of pending withdrawals from which to delete. + * @param index The index of the pending withdrawal to delete. + */ function deletePendingWithdrawal(PendingWithdrawal[] storage list, uint256 index) private { uint256 lastIndex = list.length.sub(1); list[index] = list[lastIndex]; diff --git a/packages/protocol/test/governance/lockedgold.ts b/packages/protocol/test/governance/lockedgold.ts index befc8827ba0..31ec2fbd9ab 100644 --- a/packages/protocol/test/governance/lockedgold.ts +++ b/packages/protocol/test/governance/lockedgold.ts @@ -2,6 +2,7 @@ import { CeloContractName } from '@celo/protocol/lib/registry-utils' import { assertEqualBN, assertLogMatches, + assertLogMatches2, assertRevert, NULL_ADDRESS, timeTravel, @@ -116,6 +117,34 @@ contract('LockedGold', (accounts: string[]) => { }) }) + describe('#setUnlockingPeriod', () => { + const newUnlockingPeriod = unlockingPeriod + 1 + it('should set the unlockingPeriod', async () => { + await lockedGold.setUnlockingPeriod(newUnlockingPeriod) + assertEqualBN(await lockedGold.unlockingPeriod(), newUnlockingPeriod) + }) + + it('should emit the UnlockingPeriodSet event', async () => { + const resp = await lockedGold.setUnlockingPeriod(newUnlockingPeriod) + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertLogMatches2(log, { + event: 'UnlockingPeriodSet', + args: { + period: newUnlockingPeriod, + }, + }) + }) + + it('should revert when the unlockingPeriod is unchanged', async () => { + await assertRevert(lockedGold.setUnlockingPeriod(unlockingPeriod)) + }) + + it('should revert when called by anyone other than the owner', async () => { + await assertRevert(lockedGold.setUnlockingPeriod(newUnlockingPeriod, { from: nonOwner })) + }) + }) + Object.keys(authorizationTests).forEach((key) => { describe('authorization tests:', () => { let authorizationTest: any From 0e207d0320758ae1c18127510489832dca882791 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Fri, 11 Oct 2019 16:01:18 -0700 Subject: [PATCH 061/149] Point to asaj/pos-2 --- .circleci/config.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index bd72f4dd705..b23544ca01e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -516,7 +516,7 @@ jobs: go version cd packages/celotool mkdir ~/.ssh/ && echo -e "Host github.com\n\tStrictHostKeyChecking no\n" > ~/.ssh/config - ./ci_test_transfers.sh checkout master + ./ci_test_transfers.sh checkout asaj/pos-2 end-to-end-geth-exit-test: <<: *defaults @@ -555,7 +555,7 @@ jobs: go version cd packages/celotool mkdir ~/.ssh/ && echo -e "Host github.com\n\tStrictHostKeyChecking no\n" > ~/.ssh/config - ./ci_test_exit.sh checkout master + ./ci_test_exit.sh checkout asaj/pos-2 end-to-end-geth-governance-test: <<: *defaults @@ -596,7 +596,7 @@ jobs: go version cd packages/celotool mkdir ~/.ssh/ && echo -e "Host github.com\n\tStrictHostKeyChecking no\n" > ~/.ssh/config - ./ci_test_governance.sh checkout asaj/pos + ./ci_test_governance.sh checkout asaj/pos-2 end-to-end-geth-sync-test: <<: *defaults @@ -636,7 +636,7 @@ jobs: go version cd packages/celotool mkdir ~/.ssh/ && echo -e "Host github.com\n\tStrictHostKeyChecking no\n" > ~/.ssh/config - ./ci_test_sync.sh checkout asaj/pos + ./ci_test_sync.sh checkout asaj/pos-2 end-to-end-geth-integration-sync-test: <<: *defaults @@ -669,7 +669,7 @@ jobs: go version cd packages/celotool mkdir ~/.ssh/ && echo -e "Host github.com\n\tStrictHostKeyChecking no\n" > ~/.ssh/config - ./ci_test_sync_with_network.sh checkout asaj/pos + ./ci_test_sync_with_network.sh checkout asaj/pos-2 web: working_directory: ~/app From 90e8b58cc5765fb423403be7b4bcf51de14f13b6 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Mon, 14 Oct 2019 12:10:49 -0700 Subject: [PATCH 062/149] Address comments --- .../contracts/governance/Validators.sol | 54 ++++++++----------- packages/protocol/test/common/integration.ts | 3 +- packages/protocol/test/governance/election.ts | 9 ++-- 3 files changed, 28 insertions(+), 38 deletions(-) diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index 1756a78d372..d13e03a892a 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -162,9 +162,9 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi { _transferOwnership(msg.sender); setRegistry(registryAddress); - balanceRequirements = BalanceRequirements(groupRequirement, validatorRequirement); - deregistrationLockups = DeregistrationLockups(groupLockup, validatorLockup); - maxGroupSize = _maxGroupSize; + setBalanceRequirements(groupRequirement, validatorRequirement); + setDeregistrationLockups(groupLockup, validatorLockup); + setMaxGroupSize(_maxGroupSize); } /** @@ -172,7 +172,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi * @param size The maximum group size. * @return True upon success. */ - function setMaxGroupSize(uint256 size) external onlyOwner returns (bool) { + function setMaxGroupSize(uint256 size) public onlyOwner returns (bool) { require(0 < size && size != maxGroupSize); maxGroupSize = size; emit MaxGroupSizeSet(size); @@ -188,52 +188,42 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi } /** - * @notice Updates the minimum gold requirements to register a validator group or validator. - * @param groupRequirement The minimum locked gold needed to register a group. - * @param validatorRequirement The minimum locked gold needed to register a validator. + * @notice Updates the minimum gold requirements to register a group/validator and earn rewards. + * @param group The minimum locked gold needed to register a group and earn rewards. + * @param validator The minimum locked gold needed to register a validator and earn rewards. * @return True upon success. - * @dev The new requirement is only enforced for future validator or group registrations. */ - // TODO(asa): Allow validators to adjust their LockedGold MustMaintain if the registration - // requirements fall. function setBalanceRequirements( - uint256 groupRequirement, - uint256 validatorRequirement + uint256 group, + uint256 validator ) - external + public onlyOwner returns (bool) { - require( - groupRequirement != balanceRequirements.group || - validatorRequirement != balanceRequirements.validator - ); - balanceRequirements = BalanceRequirements(groupRequirement, validatorRequirement); - emit BalanceRequirementsSet(groupRequirement, validatorRequirement); + require(group != balanceRequirements.group || validator != balanceRequirements.validator); + balanceRequirements = BalanceRequirements(group, validator); + emit BalanceRequirementsSet(group, validator); return true; } /** * @notice Updates the duration for which gold remains locked after deregistration. - * @param groupLockup The duration for groups in seconds. - * @param validatorLockup The duration for validators in seconds. + * @param group The lockup duration for groups in seconds. + * @param validator The lockup duration for validators in seconds. * @return True upon success. - * @dev The new requirement is only enforced for future validator or group deregistrations. */ function setDeregistrationLockups( - uint256 groupLockup, - uint256 validatorLockup + uint256 group, + uint256 validator ) - external + public onlyOwner returns (bool) { - require( - groupLockup != deregistrationLockups.group || - validatorLockup != deregistrationLockups.validator - ); - deregistrationLockups = DeregistrationLockups(groupLockup, validatorLockup); - emit DeregistrationLockupsSet(groupLockup, validatorLockup); + require(group != deregistrationLockups.group || validator != deregistrationLockups.validator); + deregistrationLockups = DeregistrationLockups(group, validator); + emit DeregistrationLockupsSet(group, validator); return true; } @@ -364,6 +354,8 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi * @notice Registers a validator group with no member validators. * @param name A name for the validator group. * @param url A URL for the validator group. + * @param commission Fixidity representation of the commission this group receives on epoch + * payments made to its members. * @return True upon success. * @dev Fails if the account is already a validator or validator group. * @dev Fails if the account does not have sufficient weight. diff --git a/packages/protocol/test/common/integration.ts b/packages/protocol/test/common/integration.ts index 68996a8334e..cdcbca11e4f 100644 --- a/packages/protocol/test/common/integration.ts +++ b/packages/protocol/test/common/integration.ts @@ -31,7 +31,7 @@ contract('Integration: Governance', (accounts: string[]) => { let governance: GovernanceInstance let registry: RegistryInstance let proposalTransactions: any - let value: BigNumber + const value = new BigNumber('1000000000000000000') before(async () => { lockedGold = await getDeployedProxiedContract('LockedGold', artifacts) @@ -39,7 +39,6 @@ contract('Integration: Governance', (accounts: string[]) => { registry = await getDeployedProxiedContract('Registry', artifacts) // Set up a LockedGold account with which we can vote. await lockedGold.createAccount() - value = new BigNumber('1000000000000000000') // @ts-ignore await lockedGold.lock({ value }) proposalTransactions = [ diff --git a/packages/protocol/test/governance/election.ts b/packages/protocol/test/governance/election.ts index 61d55ba9234..9763611f5c2 100644 --- a/packages/protocol/test/governance/election.ts +++ b/packages/protocol/test/governance/election.ts @@ -256,8 +256,6 @@ contract('Election', (accounts: string[]) => { resp = await election.markGroupIneligible(group) }) - describe('when the group has votes', () => {}) - it('should remove the group from the list of eligible groups', async () => { assert.deepEqual(await election.getEligibleValidatorGroups(), []) }) @@ -767,10 +765,11 @@ contract('Election', (accounts: string[]) => { }) describe('when a single group has >= minElectableValidators as members and received votes', () => { - beforeEach(async () => {}) + beforeEach(async () => { + await election.vote(group1, voter1.weight, group2, NULL_ADDRESS, { from: voter1.address }) + }) it("should return that group's member list", async () => { - await election.vote(group1, voter1.weight, group2, NULL_ADDRESS, { from: voter1.address }) assertSameAddresses(await election.electValidators(), [ validator1, validator2, @@ -780,7 +779,7 @@ contract('Election', (accounts: string[]) => { }) }) - describe("when > maxElectableValidators members's groups receive votes", () => { + describe("when > maxElectableValidators members' groups receive votes", () => { beforeEach(async () => { await election.vote(group1, voter1.weight, group2, NULL_ADDRESS, { from: voter1.address }) await election.vote(group2, voter2.weight, NULL_ADDRESS, group1, { from: voter2.address }) From 610cfe34c3adabb9b9353e2f4827b9212a7e7734 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Tue, 15 Oct 2019 17:10:53 -0700 Subject: [PATCH 063/149] Base group locked gold requirement on number of members --- .../contracts/common/UsingPrecompiles.sol | 12 + .../contracts/governance/Election.sol | 7 +- .../contracts/governance/LockedGold.sol | 2 +- .../contracts/governance/Validators.sol | 705 +++++------ .../governance/interfaces/IValidators.sol | 3 +- .../protocol/test/governance/validators.ts | 1096 +++++++++++------ 6 files changed, 1043 insertions(+), 782 deletions(-) diff --git a/packages/protocol/contracts/common/UsingPrecompiles.sol b/packages/protocol/contracts/common/UsingPrecompiles.sol index eca7ee67461..1ca4bb606f7 100644 --- a/packages/protocol/contracts/common/UsingPrecompiles.sol +++ b/packages/protocol/contracts/common/UsingPrecompiles.sol @@ -1,6 +1,7 @@ pragma solidity ^0.5.3; contract UsingPrecompiles { + address constant PROOF_OF_POSSESSION = address(0xff - 4); /** * @notice calculate a * b^x for fractions a, b to `decimals` precision @@ -113,4 +114,15 @@ contract UsingPrecompiles { return numberValidators; } + + /** + * @notice Checks a BLS proof of possession. + * @param proofOfPossessionBytes The public key and signature of the proof of possession. + * @return True upon success. + */ + function checkProofOfPossession(bytes memory proofOfPossessionBytes) internal returns (bool) { + bool success; + (success, ) = PROOF_OF_POSSESSION.call.value(0).gas(gasleft())(proofOfPossessionBytes); + return success; + } } diff --git a/packages/protocol/contracts/governance/Election.sol b/packages/protocol/contracts/governance/Election.sol index 3b9651a8ba0..b9e2caa511f 100644 --- a/packages/protocol/contracts/governance/Election.sol +++ b/packages/protocol/contracts/governance/Election.sol @@ -484,12 +484,7 @@ contract Election is view returns (uint256) { - bool meetsBalanceRequirements = ( - getLockedGold().getAccountTotalLockedGold(group) >= - getValidators().getAccountBalanceRequirement(group) - ); - - if (meetsBalanceRequirements && votes.active.total > 0) { + if (getValidators().meetsAccountLockedGoldRequirements(group) && votes.active.total > 0) { return totalEpochRewards.mul(votes.active.forGroup[group].total).div(votes.active.total); } else { return 0; diff --git a/packages/protocol/contracts/governance/LockedGold.sol b/packages/protocol/contracts/governance/LockedGold.sol index d45b35e2c69..c2844d43b6d 100644 --- a/packages/protocol/contracts/governance/LockedGold.sol +++ b/packages/protocol/contracts/governance/LockedGold.sol @@ -193,7 +193,7 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr function unlock(uint256 value) external nonReentrant { require(isAccount(msg.sender)); Account storage account = accounts[msg.sender]; - uint256 balanceRequirement = getValidators().getAccountBalanceRequirement(msg.sender); + uint256 balanceRequirement = getValidators().getAccountLockedGoldRequirement(msg.sender); require(balanceRequirement <= getAccountTotalLockedGold(msg.sender).sub(value)); _decrementNonvotingAccountBalance(msg.sender, value); uint256 available = now.add(unlockingPeriod); diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index 1b98061c3ed..2791ba80454 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -28,35 +28,30 @@ contract Validators is using SafeMath for uint256; using BytesLib for bytes; - address constant PROOF_OF_POSSESSION = address(0xff - 4); - uint256 constant MAX_INT = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; - - // If an account has not registered a validator or group, these values represent the minimum - // amount of Locked Gold required to do so. - // If an account has a registered a validator or validator group, these values represent the - // minimum amount of Locked Gold required in order to earn epoch rewards. Furthermore, the - // account will not be able to unlock Gold if it would cause the account to fall below - // these values. - // If an account has deregistered a validator or validator group and is still subject to the - // `DeregistrationLockup`, the account will not be able to unlock Gold if it would cause the - // account to fall below these values. - struct BalanceRequirements { - uint256 group; - uint256 validator; - } - - // After deregistering a validator or validator group, the account will remain subject to the - // current balance requirements for this long (in seconds). - struct DeregistrationLockups { - uint256 group; - uint256 validator; - } - - // Stores the timestamps at which deregistration of a validator or validator group occurred. - struct DeregistrationTimestamps { - uint256 group; - uint256 validator; - } + // For Validators, these requirements must be met in order to: + // 1. Register a validator + // 2. Affiliate with and be added to a group + // 3. Receive epoch payments (note that the group must meet the group requirements as well) + // Accounts may de-register their Validator `duration` seconds after they were last a member of a + // group, after which no restrictions on Locked Gold will apply to the account. + // + // For Validator Groups, these requirements must be met in order to: + // 1. Register a group + // 2. Add a member to a group + // 3. Receive epoch payments + // Note that for groups, the requirement value is multiplied by the number of members, and is + // enforced for `duration` seconds after the group last had that number of members. + // Accounts may de-register their Group `duration` seconds after they were last non-empty, after + // which no restrictions on Locked Gold will apply to the account. + struct LockedGoldRequirements { + uint256 value; + // In seconds. + uint256 duration; + } + + // If we knew what time the validator was last in a group, we could enforce that to deregister a + // group, you need to have had 0 members for `duration`, and to deregister a validator, you need + // to have been out of a group for `duration`... struct ValidatorGroup { string name; @@ -64,6 +59,8 @@ contract Validators is LinkedList.List members; // TODO(asa): Add a function that allows groups to update their commission. FixidityLib.Fraction commission; + // sizeHistory[i] contains the last time the group contained i members. + uint256[] sizeHistory; } // Stores the epoch number at which a validator joined a particular group. @@ -72,10 +69,14 @@ contract Validators is address group; } - // A circular buffer storing the membership history of a validator. + // Stores a circular buffer storing the per-epoch membership history of a validator, used to + // determine which group commission should be paid to. + // Stores a timestamp of the last time the validator was removed from a group, used to determine + // whether or not a group can de-register. struct MembershipHistory { uint256 head; MembershipHistoryEntry[] entries; + uint256 lastRemovedFromGroupTimestamp; } struct Validator { @@ -95,85 +96,34 @@ contract Validators is mapping(address => ValidatorGroup) private groups; mapping(address => Validator) private validators; - mapping(address => DeregistrationTimestamps) private deregistrationTimestamps; - address[] private _groups; - address[] private _validators; - BalanceRequirements public balanceRequirements; - DeregistrationLockups public deregistrationLockups; + address[] private registeredGroups; + address[] private registeredValidators; + LockedGoldRequirements public validatorLockedGoldRequirements; + LockedGoldRequirements public groupLockedGoldRequirements; ValidatorScoreParameters private validatorScoreParameters; uint256 public validatorEpochPayment; uint256 public membershipHistoryLength; uint256 public maxGroupSize; - event Debug(uint256 value); - event MaxGroupSizeSet( - uint256 size - ); - - event ValidatorEpochPaymentSet( - uint256 value - ); - - event ValidatorScoreParametersSet( - uint256 exponent, - uint256 adjustmentSpeed - ); - - event BalanceRequirementsSet( - uint256 group, - uint256 validator - ); - - event DeregistrationLockupsSet( - uint256 group, - uint256 validator - ); - + event MaxGroupSizeSet(uint256 size); + event ValidatorEpochPaymentSet(uint256 value); + event ValidatorScoreParametersSet(uint256 exponent, uint256 adjustmentSpeed); + event GroupLockedGoldRequirementsSet(uint256 value, uint256 duration); + event ValidatorLockedGoldRequirementsSet(uint256 value, uint256 duration); event ValidatorRegistered( address indexed validator, string name, string url, bytes publicKeysData ); - - event ValidatorDeregistered( - address indexed validator - ); - - event ValidatorAffiliated( - address indexed validator, - address indexed group - ); - - event ValidatorDeaffiliated( - address indexed validator, - address indexed group - ); - - event ValidatorGroupRegistered( - address indexed group, - string name, - string url - ); - - event ValidatorGroupDeregistered( - address indexed group - ); - - event ValidatorGroupMemberAdded( - address indexed group, - address indexed validator - ); - - event ValidatorGroupMemberRemoved( - address indexed group, - address indexed validator - ); - - event ValidatorGroupMemberReordered( - address indexed group, - address indexed validator - ); + event ValidatorDeregistered(address indexed validator); + event ValidatorAffiliated(address indexed validator, address indexed group); + event ValidatorDeaffiliated(address indexed validator, address indexed group); + event ValidatorGroupRegistered(address indexed group, string name, string url); + event ValidatorGroupDeregistered(address indexed group); + event ValidatorGroupMemberAdded(address indexed group, address indexed validator); + event ValidatorGroupMemberRemoved(address indexed group, address indexed validator); + event ValidatorGroupMemberReordered(address indexed group, address indexed validator); modifier onlyVm() { require(msg.sender == address(0)); @@ -183,10 +133,10 @@ contract Validators is /** * @notice Initializes critical variables. * @param registryAddress The address of the registry contract. - * @param groupRequirement The minimum locked gold needed to register a group. - * @param validatorRequirement The minimum locked gold needed to register a validator. - * @param groupLockup The duration the above gold remains locked after deregistration. - * @param validatorLockup The duration the above gold remains locked after deregistration. + * @param groupRequirementValue The Locked Gold requirement amount for groups. + * @param groupRequirementDuration The Locked Gold requirement duration for groups. + * @param validatorRequirementValue The Locked Gold requirement amount for validators. + * @param validatorRequirementDuration The Locked Gold requirement duration for validators. * @param validatorScoreExponent The exponent used in calculating validator scores. * @param validatorScoreAdjustmentSpeed The speed at which validator scores are adjusted. * @param _validatorEpochPayment The duration the above gold remains locked after deregistration. @@ -196,10 +146,10 @@ contract Validators is */ function initialize( address registryAddress, - uint256 groupRequirement, - uint256 validatorRequirement, - uint256 groupLockup, - uint256 validatorLockup, + uint256 groupRequirementValue, + uint256 groupRequirementDuration, + uint256 validatorRequirementValue, + uint256 validatorRequirementDuration, uint256 validatorScoreExponent, uint256 validatorScoreAdjustmentSpeed, uint256 _validatorEpochPayment, @@ -209,18 +159,14 @@ contract Validators is external initializer { - require(validatorScoreAdjustmentSpeed <= FixidityLib.fixed1().unwrap()); _transferOwnership(msg.sender); setRegistry(registryAddress); - balanceRequirements = BalanceRequirements(groupRequirement, validatorRequirement); - deregistrationLockups = DeregistrationLockups(groupLockup, validatorLockup); - validatorScoreParameters = ValidatorScoreParameters( - validatorScoreExponent, - FixidityLib.wrap(validatorScoreAdjustmentSpeed) - ); - validatorEpochPayment = _validatorEpochPayment; + setGroupLockedGoldRequirements(groupRequirementValue, groupRequirementDuration); + setValidatorLockedGoldRequirements(validatorRequirementValue, validatorRequirementDuration); + setValidatorScoreParameters(validatorScoreExponent, validatorScoreAdjustmentSpeed); + setMaxGroupSize(_maxGroupSize); + setValidatorEpochPayment(_validatorEpochPayment); membershipHistoryLength = _membershipHistoryLength; - maxGroupSize = _maxGroupSize; } /** @@ -228,7 +174,7 @@ contract Validators is * @param size The maximum group size. * @return True upon success. */ - function setMaxGroupSize(uint256 size) external onlyOwner returns (bool) { + function setMaxGroupSize(uint256 size) public onlyOwner returns (bool) { require(0 < size && size != maxGroupSize); maxGroupSize = size; emit MaxGroupSizeSet(size); @@ -240,7 +186,7 @@ contract Validators is * @param value The value in Celo Dollars. * @return True upon success. */ - function setValidatorEpochPayment(uint256 value) external onlyOwner returns (bool) { + function setValidatorEpochPayment(uint256 value) public onlyOwner returns (bool) { require(value != validatorEpochPayment); validatorEpochPayment = value; emit ValidatorEpochPaymentSet(value); @@ -257,7 +203,7 @@ contract Validators is uint256 exponent, uint256 adjustmentSpeed ) - external + public onlyOwner returns (bool) { @@ -275,60 +221,44 @@ contract Validators is } /** - * @notice Returns the maximum number of members a group can add. - * @return The maximum number of members a group can add. - */ - function getMaxGroupSize() external view returns (uint256) { - return maxGroupSize; - } - - /** - * @notice Updates the minimum gold requirements to register a validator group or validator. - * @param groupRequirement The minimum locked gold needed to register a group. - * @param validatorRequirement The minimum locked gold needed to register a validator. + * @notice Updates the Locked Gold requirements for Validator Groups. + * @param value The per-member amount of Locked Gold required. + * @param duration The time (in seconds) that these requirements persist for. * @return True upon success. - * @dev The new requirement is only enforced for future validator or group registrations. */ - // TODO(asa): Allow validators to adjust their LockedGold MustMaintain if the registration - // requirements fall. - function setBalanceRequirements( - uint256 groupRequirement, - uint256 validatorRequirement + function setGroupLockedGoldRequirements( + uint256 value, + uint256 duration ) - external + public onlyOwner returns (bool) { - require( - groupRequirement != balanceRequirements.group || - validatorRequirement != balanceRequirements.validator - ); - balanceRequirements = BalanceRequirements(groupRequirement, validatorRequirement); - emit BalanceRequirementsSet(groupRequirement, validatorRequirement); + LockedGoldRequirements storage requirements = groupLockedGoldRequirements; + require(value != requirements.value || duration != requirements.duration); + groupLockedGoldRequirements = LockedGoldRequirements(value, duration); + emit GroupLockedGoldRequirementsSet(value, duration); return true; } /** - * @notice Updates the duration for which gold remains locked after deregistration. - * @param groupLockup The duration for groups in seconds. - * @param validatorLockup The duration for validators in seconds. + * @notice Updates the Locked Gold requirements for Validators. + * @param value The amount of Locked Gold required. + * @param duration The time (in seconds) that these requirements persist for. * @return True upon success. - * @dev The new requirement is only enforced for future validator or group deregistrations. */ - function setDeregistrationLockups( - uint256 groupLockup, - uint256 validatorLockup + function setValidatorLockedGoldRequirements( + uint256 value, + uint256 duration ) - external + public onlyOwner returns (bool) { - require( - groupLockup != deregistrationLockups.group || - validatorLockup != deregistrationLockups.validator - ); - deregistrationLockups = DeregistrationLockups(groupLockup, validatorLockup); - emit DeregistrationLockupsSet(groupLockup, validatorLockup); + LockedGoldRequirements storage requirements = validatorLockedGoldRequirements; + require(value != requirements.value || duration != requirements.duration); + validatorLockedGoldRequirements = LockedGoldRequirements(value, duration); + emit ValidatorLockedGoldRequirementsSet(value, duration); return true; } @@ -344,7 +274,7 @@ contract Validators is * - blsPoP - The BLS public key proof of possession. 96 bytes. * @return True upon success. * @dev Fails if the account is already a validator or validator group. - * @dev Fails if the account does not have sufficient weight. + * @dev Fails if the account does not have sufficient Locked Gold. */ function registerValidator( string calldata name, @@ -366,176 +296,42 @@ contract Validators is address account = getLockedGold().getAccountFromActiveValidator(msg.sender); require(!isValidator(account) && !isValidatorGroup(account)); - require(meetsValidatorBalanceRequirements(account)); - - validators[account].name = name; - validators[account].url = url; - validators[account].publicKeysData = publicKeysData; - _validators.push(account); + uint256 lockedGoldBalance = getLockedGold().getAccountTotalLockedGold(account); + require(lockedGoldBalance >= validatorLockedGoldRequirements.value); + Validator storage validator = validators[account]; + validator.name = name; + validator.url = url; + validator.publicKeysData = publicKeysData; + registeredValidators.push(account); updateMembershipHistory(account, address(0)); emit ValidatorRegistered(account, name, url, publicKeysData); return true; } /** - * @notice Checks a BLS proof of possession. - * @param proofOfPossessionBytes The public key and signature of the proof of possession. - * @return True upon success. - */ - function checkProofOfPossession(bytes memory proofOfPossessionBytes) private returns (bool) { - bool success; - (success, ) = PROOF_OF_POSSESSION.call.value(0).gas(gasleft())(proofOfPossessionBytes); - return success; - } - - /** - * @notice Returns whether an account meets the requirements to register a validator. - * @param account The account. - * @return Whether an account meets the requirements to register a validator. - */ - function meetsValidatorBalanceRequirements(address account) public view returns (bool) { - return getLockedGold().getAccountTotalLockedGold(account) >= balanceRequirements.validator; - } - - /** - * @notice Returns whether an account meets the requirements to register a group. - * @param account The account. - * @return Whether an account meets the requirements to register a group. - */ - function meetsValidatorGroupBalanceRequirements(address account) public view returns (bool) { - return getLockedGold().getAccountTotalLockedGold(account) >= balanceRequirements.group; - } - - /** - * @notice Returns the parameters that goven how a validator's score is calculated. - * @return The parameters that goven how a validator's score is calculated. - */ - function getValidatorScoreParameters() external view returns (uint256, uint256) { - return (validatorScoreParameters.exponent, validatorScoreParameters.adjustmentSpeed.unwrap()); - } - - /** - * @notice Returns the group membership history of a validator. - * @param account The validator whose membership history to return. - * @return The group membership history of a validator. - */ - function getMembershipHistory( - address account - ) - external - view - returns (uint256[] memory, address[] memory) - { - MembershipHistoryEntry[] memory entries = validators[account].membershipHistory.entries; - uint256[] memory epochs = new uint256[](entries.length); - address[] memory membershipGroups = new address[](entries.length); - for (uint256 i = 0; i < entries.length; i = i.add(1)) { - epochs[i] = entries[i].epochNumber; - membershipGroups[i] = entries[i].group; - } - return (epochs, membershipGroups); - } - - /** - * @notice Updates a validator's score based on its uptime for the epoch. - * @param validator The address of the validator. - * @param uptime The Fixidity representation of the validator's uptime, between 0 and 1. - * @return True upon success. - */ - function updateValidatorScore(address validator, uint256 uptime) external onlyVm() { - _updateValidatorScore(validator, uptime); - } - - /** - * @notice Updates a validator's score based on its uptime for the epoch. - * @param validator The address of the validator. - * @param uptime The Fixidity representation of the validator's uptime, between 0 and 1. - * @return True upon success. - */ - function _updateValidatorScore(address validator, uint256 uptime) internal { - address account = getLockedGold().getAccountFromValidator(validator); - require(isValidator(account), "isvalidator"); - require(uptime <= FixidityLib.fixed1().unwrap(), "uptime"); - - uint256 numerator; - uint256 denominator; - (numerator, denominator) = fractionMulExp( - FixidityLib.fixed1().unwrap(), - FixidityLib.fixed1().unwrap(), - uptime, - FixidityLib.fixed1().unwrap(), - validatorScoreParameters.exponent, - 18 - ); - - FixidityLib.Fraction memory epochScore = FixidityLib.wrap(numerator).divide( - FixidityLib.wrap(denominator) - ); - FixidityLib.Fraction memory newComponent = validatorScoreParameters.adjustmentSpeed.multiply( - epochScore - ); - - FixidityLib.Fraction memory currentComponent = FixidityLib.fixed1().subtract( - validatorScoreParameters.adjustmentSpeed - ); - currentComponent = currentComponent.multiply(validators[account].score); - validators[account].score = FixidityLib.wrap( - Math.min( - epochScore.unwrap(), - newComponent.add(currentComponent).unwrap() - ) - ); - } - - /** - * @notice Distributes epoch payments to `validator` and its group. - */ - function distributeEpochPayment(address validator) external onlyVm() { - _distributeEpochPayment(validator); - } - - /** - * @notice Distributes epoch payments to `validator` and its group. - */ - function _distributeEpochPayment(address validator) internal { - address account = getLockedGold().getAccountFromValidator(validator); - require(isValidator(account)); - // The group that should be paid is the group that the validator was a member of at the - // time it was elected. - address group = getMembershipInLastEpoch(account); - // Both the validator and the group must maintain the minimum locked gold balance in order to - // receive epoch payments. - bool meetsBalanceRequirements = ( - getLockedGold().getAccountTotalLockedGold(group) >= getAccountBalanceRequirement(group) && - getLockedGold().getAccountTotalLockedGold(account) >= getAccountBalanceRequirement(account) - ); - if (meetsBalanceRequirements) { - FixidityLib.Fraction memory totalPayment = FixidityLib.newFixed( - validatorEpochPayment - ).multiply(validators[account].score); - uint256 groupPayment = totalPayment.multiply(groups[group].commission).fromFixed(); - uint256 validatorPayment = totalPayment.fromFixed().sub(groupPayment); - getStableToken().mint(group, groupPayment); - getStableToken().mint(account, validatorPayment); - } - } - - /** - * @notice De-registers a validator, removing it from the group for which it is a member. - * @param index The index of this validator in the list of all validators. + * @notice De-registers a validator. + * @param index The index of this validator in the list of all registered validators. * @return True upon success. * @dev Fails if the account is not a validator. */ function deregisterValidator(uint256 index) external nonReentrant returns (bool) { address account = getLockedGold().getAccountFromActiveValidator(msg.sender); require(isValidator(account)); + + // Require that the validator has not been a member of a validator group for + // `validatorLockedGoldRequirements.duration` seconds. Validator storage validator = validators[account]; if (validator.affiliation != address(0)) { - _deaffiliate(validator, account); + require(!groups[validator.affiliation].members.contains(account), 'two'); } + uint256 requirementEndTime = validator.membershipHistory.lastRemovedFromGroupTimestamp.add( + validatorLockedGoldRequirements.duration + ); + require(requirementEndTime < now, 'one'); + + // Remove the validator. + deleteElement(registeredValidators, account, index); delete validators[account]; - deleteElement(_validators, account, index); - deregistrationTimestamps[account].validator = now; emit ValidatorDeregistered(account); return true; } @@ -549,6 +345,7 @@ contract Validators is function affiliate(address group) external nonReentrant returns (bool) { address account = getLockedGold().getAccountFromActiveValidator(msg.sender); require(isValidator(account) && isValidatorGroup(group)); + require(meetsAccountLockedGoldRequirements(account) && meetsAccountLockedGoldRequirements(group), "three"); Validator storage validator = validators[account]; if (validator.affiliation != address(0)) { _deaffiliate(validator, account); @@ -594,13 +391,13 @@ contract Validators is require(commission <= FixidityLib.fixed1().unwrap(), "Commission can't be greater than 100%"); address account = getLockedGold().getAccountFromActiveValidator(msg.sender); require(!isValidator(account) && !isValidatorGroup(account)); - require(meetsValidatorGroupBalanceRequirements(account)); - + uint256 lockedGoldBalance = getLockedGold().getAccountTotalLockedGold(account); + require(lockedGoldBalance >= groupLockedGoldRequirements.value); ValidatorGroup storage group = groups[account]; group.name = name; group.url = url; group.commission = FixidityLib.wrap(commission); - _groups.push(account); + registeredGroups.push(account); emit ValidatorGroupRegistered(account, name, url); return true; } @@ -613,11 +410,15 @@ contract Validators is */ function deregisterValidatorGroup(uint256 index) external nonReentrant returns (bool) { address account = getLockedGold().getAccountFromActiveValidator(msg.sender); - // Only empty Validator Groups can be deregistered. + // Only Validator Groups that have never had members or have been empty for at least + // `groupLockedGoldRequirements.duration` seconds can be deregistered. require(isValidatorGroup(account) && groups[account].members.numElements == 0); + uint256[] storage sizeHistory = groups[account].sizeHistory; + if (sizeHistory.length > 1) { + require(sizeHistory[1].add(groupLockedGoldRequirements.duration) < now); + } delete groups[account]; - deleteElement(_groups, account, index); - deregistrationTimestamps[account].group = now; + deleteElement(registeredGroups, account, index); emit ValidatorGroupDeregistered(account); return true; } @@ -680,11 +481,14 @@ contract Validators is ValidatorGroup storage _group = groups[group]; require(_group.members.numElements < maxGroupSize, "group would exceed maximum size"); require(validators[validator].affiliation == group && !_group.members.contains(validator)); + uint256 numMembers = _group.members.numElements.add(1); + require(meetsAccountLockedGoldRequirements(group) && meetsAccountLockedGoldRequirements(validator)); _group.members.push(validator); - if (_group.members.numElements == 1) { + if (numMembers == 1) { getElection().markGroupEligible(group, lesser, greater); } updateMembershipHistory(validator, group); + updateSizeHistory(group, numMembers.sub(1)); emit ValidatorGroupMemberAdded(group, validator); return true; } @@ -729,35 +533,173 @@ contract Validators is return true; } + /** + * @notice Updates a validator's score based on its uptime for the epoch. + * @param validator The address of the validator. + * @param uptime The Fixidity representation of the validator's uptime, between 0 and 1. + * @return True upon success. + */ + function updateValidatorScore(address validator, uint256 uptime) external onlyVm() { + _updateValidatorScore(validator, uptime); + } + + /** + * @notice Updates a validator's score based on its uptime for the epoch. + * @param validator The address of the validator. + * @param uptime The Fixidity representation of the validator's uptime, between 0 and 1. + * @return True upon success. + */ + function _updateValidatorScore(address validator, uint256 uptime) internal { + address account = getLockedGold().getAccountFromValidator(validator); + require(isValidator(account)); + require(uptime <= FixidityLib.fixed1().unwrap()); + + uint256 numerator; + uint256 denominator; + (numerator, denominator) = fractionMulExp( + FixidityLib.fixed1().unwrap(), + FixidityLib.fixed1().unwrap(), + uptime, + FixidityLib.fixed1().unwrap(), + validatorScoreParameters.exponent, + 18 + ); + + FixidityLib.Fraction memory epochScore = FixidityLib.wrap(numerator).divide( + FixidityLib.wrap(denominator) + ); + FixidityLib.Fraction memory newComponent = validatorScoreParameters.adjustmentSpeed.multiply( + epochScore + ); + + FixidityLib.Fraction memory currentComponent = FixidityLib.fixed1().subtract( + validatorScoreParameters.adjustmentSpeed + ); + currentComponent = currentComponent.multiply(validators[account].score); + validators[account].score = FixidityLib.wrap( + Math.min( + epochScore.unwrap(), + newComponent.add(currentComponent).unwrap() + ) + ); + } + + /** + * @notice Distributes epoch payments to `validator` and its group. + */ + function distributeEpochPayment(address validator) external onlyVm() { + _distributeEpochPayment(validator); + } + + /** + * @notice Distributes epoch payments to `validator` and its group. + */ + function _distributeEpochPayment(address validator) internal { + address account = getLockedGold().getAccountFromValidator(validator); + require(isValidator(account)); + // The group that should be paid is the group that the validator was a member of at the + // time it was elected. + address group = getMembershipInLastEpoch(account); + // Both the validator and the group must maintain the minimum locked gold balance in order to + // receive epoch payments. + if (meetsAccountLockedGoldRequirements(account) && meetsAccountLockedGoldRequirements(group)) { + FixidityLib.Fraction memory totalPayment = FixidityLib.newFixed( + validatorEpochPayment + ).multiply(validators[account].score); + uint256 groupPayment = totalPayment.multiply(groups[group].commission).fromFixed(); + uint256 validatorPayment = totalPayment.fromFixed().sub(groupPayment); + getStableToken().mint(group, groupPayment); + getStableToken().mint(account, validatorPayment); + } + } + + /** + * @notice Returns the parameters that goven how a validator's score is calculated. + * @return The parameters that goven how a validator's score is calculated. + */ + function getValidatorScoreParameters() external view returns (uint256, uint256) { + return (validatorScoreParameters.exponent, validatorScoreParameters.adjustmentSpeed.unwrap()); + } + + /** + * @notice Returns the Locked Gold requirements for validators. + * @return The Locked Gold requirements for validators. + */ + function getValidatorLockedGoldRequirements() external view returns (uint256, uint256) { + return (validatorLockedGoldRequirements.value, validatorLockedGoldRequirements.duration); + } + + /** + * @notice Returns the Locked Gold requirements for validator groups. + * @return The Locked Gold requirements for validator groups. + */ + function getGroupLockedGoldRequirements() external view returns (uint256, uint256) { + return (groupLockedGoldRequirements.value, groupLockedGoldRequirements.duration); + } + + /** + * @notice Returns the maximum number of members a group can add. + * @return The maximum number of members a group can add. + */ + function getMaxGroupSize() external view returns (uint256) { + return maxGroupSize; + } + + /** + * @notice Returns the group membership history of a validator. + * @param account The validator whose membership history to return. + * @return The group membership history of a validator. + */ + function getMembershipHistory( + address account + ) + external + view + returns (uint256[] memory, address[] memory, uint256) + { + MembershipHistory storage history = validators[account].membershipHistory; + MembershipHistoryEntry[] memory entries = history.entries; + uint256[] memory epochs = new uint256[](entries.length); + address[] memory membershipGroups = new address[](entries.length); + for (uint256 i = 0; i < entries.length; i = i.add(1)) { + epochs[i] = entries[i].epochNumber; + membershipGroups[i] = entries[i].group; + } + return (epochs, membershipGroups, history.lastRemovedFromGroupTimestamp); + } + /** * @notice Returns the locked gold balance requirement for the supplied account. * @param account The account that may have to meet locked gold balance requirements. * @return The locked gold balance requirement for the supplied account. */ - function getAccountBalanceRequirement(address account) public view returns (uint256) { - DeregistrationTimestamps storage timestamps = deregistrationTimestamps[account]; - if ( - isValidator(account) || - (timestamps.validator > 0 && now < timestamps.validator.add(deregistrationLockups.validator)) - ) { - return balanceRequirements.validator; - } - if ( - isValidatorGroup(account) || - (timestamps.group > 0 && now < timestamps.group.add(deregistrationLockups.group)) - ) { - return balanceRequirements.group; + function getAccountLockedGoldRequirement(address account) public view returns (uint256) { + if (isValidator(account)) { + return validatorLockedGoldRequirements.value; + } else if (isValidatorGroup(account)) { + uint256 multiplier = Math.max(1, groups[account].members.numElements); + uint256[] storage sizeHistory = groups[account].sizeHistory; + if (sizeHistory.length > 0) { + for (uint256 i = sizeHistory.length.sub(1); i > 0; i = i.sub(1)) { + if (sizeHistory[i].add(groupLockedGoldRequirements.duration) >= now) { + multiplier = Math.max(i, multiplier); + break; + } + } + } + return groupLockedGoldRequirements.value.mul(multiplier); } return 0; } /** - * @notice Returns the timestamp of the last time this account deregistered a validator or group. - * @param account The account to query. - * @return The timestamp of the last time this account deregistered a validator or group. + * @notice Returns whether or not an account meets its Locked Gold requirements. + * @param account The address of the account. + * @return Whether or not an account meets its Locked Gold requirements. */ - function getDeregistrationTimestamps(address account) external view returns (uint256, uint256) { - return (deregistrationTimestamps[account].group, deregistrationTimestamps[account].validator); + function meetsAccountLockedGoldRequirements(address account) public view returns (bool) { + uint256 balance = getLockedGold().getAccountTotalLockedGold(account); + return balance >= getAccountLockedGoldRequirement(account); } /** @@ -799,11 +741,17 @@ contract Validators is ) external view - returns (string memory, string memory, address[] memory, uint256) + returns (string memory, string memory, address[] memory, uint256, uint256[] memory) { require(isValidatorGroup(account)); ValidatorGroup storage group = groups[account]; - return (group.name, group.url, group.members.getKeys(), group.commission.unwrap()); + return ( + group.name, + group.url, + group.members.getKeys(), + group.commission.unwrap(), + group.sizeHistory + ); } /** @@ -862,23 +810,7 @@ contract Validators is * @return The number of registered validators. */ function getNumRegisteredValidators() external view returns (uint256) { - return _validators.length; - } - - /** - * @notice Returns the Locked Gold requirements to register a validator or group. - * @return The locked gold requirements to register a validator or group. - */ - function getBalanceRequirements() external view returns (uint256, uint256) { - return (balanceRequirements.group, balanceRequirements.validator); - } - - /** - * @notice Returns the lockup periods after deregistering groups and validators. - * @return The lockup periods after deregistering groups and validators. - */ - function getDeregistrationLockups() external view returns (uint256, uint256) { - return (deregistrationLockups.group, deregistrationLockups.validator); + return registeredValidators.length; } /** @@ -886,7 +818,7 @@ contract Validators is * @return The list of registered validator accounts. */ function getRegisteredValidators() external view returns (address[] memory) { - return _validators; + return registeredValidators; } /** @@ -894,22 +826,22 @@ contract Validators is * @return The list of registered validator group addresses. */ function getRegisteredValidatorGroups() external view returns (address[] memory) { - return _groups; + return registeredGroups; } /** - * @notice Returns whether a particular account has a registered validator group. + * @notice Returns whether a particular account has a validator group. * @param account The account. - * @return Whether a particular address is a registered validator group. + * @return Whether a particular address is a validator group. */ function isValidatorGroup(address account) public view returns (bool) { return bytes(groups[account].name).length > 0; } /** - * @notice Returns whether a particular account has a registered validator. + * @notice Returns whether a particular account has a validator. * @param account The account. - * @return Whether a particular address is a registered validator. + * @return Whether a particular address is a validator. */ function isValidator(address account) public view returns (bool) { return bytes(validators[account].name).length > 0; @@ -941,13 +873,14 @@ contract Validators is ValidatorGroup storage _group = groups[group]; require(validators[validator].affiliation == group && _group.members.contains(validator)); _group.members.remove(validator); - updateMembershipHistory(validator, address(0)); - emit ValidatorGroupMemberRemoved(group, validator); - + uint256 numMembers = _group.members.numElements; // Empty validator groups are not electable. - if (groups[group].members.numElements == 0) { + if (numMembers == 0) { getElection().markGroupIneligible(group); } + updateMembershipHistory(validator, address(0)); + updateSizeHistory(group, numMembers.add(1)); + emit ValidatorGroupMemberRemoved(group, validator); return true; } @@ -962,21 +895,39 @@ contract Validators is function updateMembershipHistory(address account, address group) private returns (bool) { MembershipHistory storage history = validators[account].membershipHistory; uint256 epochNumber = getEpochNumber(); - if (history.entries.length > 0 && epochNumber == history.entries[history.head].epochNumber) { - // There have been no elections since the validator last changed membership, overwrite the - // previous entry. - history.entries[history.head] = MembershipHistoryEntry(epochNumber, group); - } else { - if (history.entries.length > 0) { - // MembershipHistoryEntries are a circular buffer. - history.head = history.head.add(1) % membershipHistoryLength; + if (history.entries.length > 0) { + if (group == address(0)) { + history.lastRemovedFromGroupTimestamp = now; } - if (history.head >= history.entries.length) { - history.entries.push(MembershipHistoryEntry(epochNumber, group)); - } else { + + if (epochNumber == history.entries[history.head].epochNumber) { + // There have been no elections since the validator last changed membership, overwrite the + // previous entry. history.entries[history.head] = MembershipHistoryEntry(epochNumber, group); + return true; + } else { + // MembershipHistoryEntries are a circular buffer. + history.head = history.head.add(1) % membershipHistoryLength; } } + + if (history.head >= history.entries.length) { + history.entries.push(MembershipHistoryEntry(epochNumber, group)); + } else { + history.entries[history.head] = MembershipHistoryEntry(epochNumber, group); + } + return true; + } + + function updateSizeHistory(address group, uint256 size) private { + uint256[] storage sizeHistory = groups[group].sizeHistory; + if (size == sizeHistory.length) { + sizeHistory.push(now); + } else if (size < sizeHistory.length) { + sizeHistory[size] = now; + } else { + require(false, "Unable to update size history"); + } } /** @@ -1018,8 +969,8 @@ contract Validators is if (group.members.contains(validatorAccount)) { _removeMember(affiliation, validatorAccount); } - emit ValidatorDeaffiliated(validatorAccount, affiliation); validator.affiliation = address(0); + emit ValidatorDeaffiliated(validatorAccount, affiliation); return true; } } diff --git a/packages/protocol/contracts/governance/interfaces/IValidators.sol b/packages/protocol/contracts/governance/interfaces/IValidators.sol index 41a68b70859..6650c34686f 100644 --- a/packages/protocol/contracts/governance/interfaces/IValidators.sol +++ b/packages/protocol/contracts/governance/interfaces/IValidators.sol @@ -2,7 +2,8 @@ pragma solidity ^0.5.3; interface IValidators { - function getAccountBalanceRequirement(address) external view returns (uint256); + function getAccountLockedGoldRequirement(address) external view returns (uint256); + function meetsAccountLockedGoldRequirements(address) external view returns (bool); function getGroupNumMembers(address) external view returns (uint256); function getGroupsNumMembers(address[] calldata) external view returns (uint256[] memory); function getNumRegisteredValidators() external view returns (uint256); diff --git a/packages/protocol/test/governance/validators.ts b/packages/protocol/test/governance/validators.ts index d034dc03dd5..6fd79470b93 100644 --- a/packages/protocol/test/governance/validators.ts +++ b/packages/protocol/test/governance/validators.ts @@ -6,6 +6,7 @@ import { assertSameAddress, NULL_ADDRESS, mineBlocks, + timeTravel, } from '@celo/protocol/lib/test-utils' import BigNumber from 'bignumber.js' import { @@ -48,6 +49,15 @@ const parseValidatorGroupParams = (groupParams: any) => { url: groupParams[1], members: groupParams[2], commission: groupParams[3], + sizeHistory: groupParams[4], + } +} + +const parseMembershipHistory = (membershipHistory: any) => { + return { + epochs: membershipHistory[0], + groups: membershipHistory[1], + lastRemovedFromGroupTimestamp: membershipHistory[2], } } @@ -64,17 +74,20 @@ contract('Validators', (accounts: string[]) => { let mockElection: MockElectionInstance const nonOwner = accounts[1] - const balanceRequirements = { group: new BigNumber(1000), validator: new BigNumber(100) } - const deregistrationLockups = { - group: new BigNumber(100 * DAY), - validator: new BigNumber(60 * DAY), + const validatorLockedGoldRequirements = { + value: new BigNumber(1000), + duration: new BigNumber(60 * DAY), + } + const groupLockedGoldRequirements = { + value: new BigNumber(1000), + duration: new BigNumber(100 * DAY), } const validatorScoreParameters = { exponent: new BigNumber(5), adjustmentSpeed: toFixed(0.25), } const validatorEpochPayment = new BigNumber(10000000000000) - const membershipHistoryLength = new BigNumber(3) + const membershipHistoryLength = new BigNumber(5) const maxGroupSize = new BigNumber(5) // A random 64 byte hex string. @@ -97,10 +110,10 @@ contract('Validators', (accounts: string[]) => { await registry.setAddressFor(CeloContractName.Election, mockElection.address) await validators.initialize( registry.address, - balanceRequirements.group, - balanceRequirements.validator, - deregistrationLockups.group, - deregistrationLockups.validator, + groupLockedGoldRequirements.value, + groupLockedGoldRequirements.duration, + validatorLockedGoldRequirements.value, + validatorLockedGoldRequirements.duration, validatorScoreParameters.exponent, validatorScoreParameters.adjustmentSpeed, validatorEpochPayment, @@ -110,7 +123,7 @@ contract('Validators', (accounts: string[]) => { }) const registerValidator = async (validator: string) => { - await mockLockedGold.setAccountTotalLockedGold(validator, balanceRequirements.validator) + await mockLockedGold.setAccountTotalLockedGold(validator, validatorLockedGoldRequirements.value) await validators.registerValidator( name, url, @@ -121,7 +134,7 @@ contract('Validators', (accounts: string[]) => { } const registerValidatorGroup = async (group: string) => { - await mockLockedGold.setAccountTotalLockedGold(group, balanceRequirements.group) + await mockLockedGold.setAccountTotalLockedGold(group, groupLockedGoldRequirements.value) await validators.registerValidatorGroup(name, url, commission, { from: group }) } @@ -144,16 +157,16 @@ contract('Validators', (accounts: string[]) => { assert.equal(owner, accounts[0]) }) - it('should have set the balance requirements', async () => { - const [group, validator] = await validators.getBalanceRequirements() - assertEqualBN(group, balanceRequirements.group) - assertEqualBN(validator, balanceRequirements.validator) + it('should have set the group locked gold requirements', async () => { + const [value, duration] = await validators.getGroupLockedGoldRequirements() + assertEqualBN(value, groupLockedGoldRequirements.value) + assertEqualBN(duration, groupLockedGoldRequirements.duration) }) - it('should have set the deregistration lockups', async () => { - const [group, validator] = await validators.getDeregistrationLockups() - assertEqualBN(group, deregistrationLockups.group) - assertEqualBN(validator, deregistrationLockups.validator) + it('should have set the validator locked gold requirements', async () => { + const [value, duration] = await validators.getValidatorLockedGoldRequirements() + assertEqualBN(value, validatorLockedGoldRequirements.value) + assertEqualBN(duration, validatorLockedGoldRequirements.duration) }) it('should have set the validator score parameters', async () => { @@ -181,10 +194,10 @@ contract('Validators', (accounts: string[]) => { await assertRevert( validators.initialize( registry.address, - balanceRequirements.group, - balanceRequirements.validator, - deregistrationLockups.group, - deregistrationLockups.validator, + groupLockedGoldRequirements.value, + groupLockedGoldRequirements.duration, + validatorLockedGoldRequirements.value, + validatorLockedGoldRequirements.duration, validatorScoreParameters.exponent, validatorScoreParameters.adjustmentSpeed, validatorEpochPayment, @@ -285,37 +298,37 @@ contract('Validators', (accounts: string[]) => { }) }) - describe('#setBalanceRequirements()', () => { + describe('#setGroupLockedGoldRequirements()', () => { describe('when the requirements are different', () => { const newRequirements = { - group: balanceRequirements.group.plus(1), - validator: balanceRequirements.validator.plus(1), + value: groupLockedGoldRequirements.value.plus(1), + duration: groupLockedGoldRequirements.duration.plus(1), } describe('when called by the owner', () => { let resp: any beforeEach(async () => { - resp = await validators.setBalanceRequirements( - newRequirements.group, - newRequirements.validator + resp = await validators.setGroupLockedGoldRequirements( + newRequirements.value, + newRequirements.duration ) }) - it('should set the group and validator requirements', async () => { - const [group, validator] = await validators.getBalanceRequirements() - assertEqualBN(group, newRequirements.group) - assertEqualBN(validator, newRequirements.validator) + it('should have set the group locked gold requirements', async () => { + const [value, duration] = await validators.getGroupLockedGoldRequirements() + assertEqualBN(value, newRequirements.value) + assertEqualBN(duration, newRequirements.duration) }) - it('should emit the BalanceRequirementsSet event', async () => { + it('should emit the GroupLockedGoldRequirementsSet event', async () => { assert.equal(resp.logs.length, 1) const log = resp.logs[0] assertContainSubset(log, { - event: 'BalanceRequirementsSet', + event: 'GroupLockedGoldRequirementsSet', args: { - group: new BigNumber(newRequirements.group), - validator: new BigNumber(newRequirements.validator), + value: newRequirements.value, + duration: newRequirements.duration, }, }) }) @@ -323,9 +336,11 @@ contract('Validators', (accounts: string[]) => { describe('when called by a non-owner', () => { it('should revert', async () => { await assertRevert( - validators.setBalanceRequirements(newRequirements.group, newRequirements.validator, { - from: nonOwner, - }) + validators.setGroupLockedGoldRequirements( + newRequirements.value, + newRequirements.duration, + { from: nonOwner } + ) ) }) }) @@ -334,9 +349,9 @@ contract('Validators', (accounts: string[]) => { describe('when the requirements are the same', () => { it('should revert', async () => { await assertRevert( - validators.setBalanceRequirements( - balanceRequirements.group, - balanceRequirements.validator + validators.setGroupLockedGoldRequirements( + groupLockedGoldRequirements.value, + groupLockedGoldRequirements.duration ) ) }) @@ -344,34 +359,37 @@ contract('Validators', (accounts: string[]) => { }) }) - describe('#setDeregistrationLockups()', () => { - describe('when the lockups are different', () => { - const newLockups = { - group: deregistrationLockups.group.plus(1), - validator: deregistrationLockups.validator.plus(1), + describe('#setValidatorLockedGoldRequirements()', () => { + describe('when the requirements are different', () => { + const newRequirements = { + value: validatorLockedGoldRequirements.value.plus(1), + duration: validatorLockedGoldRequirements.duration.plus(1), } describe('when called by the owner', () => { let resp: any beforeEach(async () => { - resp = await validators.setDeregistrationLockups(newLockups.group, newLockups.validator) + resp = await validators.setValidatorLockedGoldRequirements( + newRequirements.value, + newRequirements.duration + ) }) - it('should set the group and validator lockups', async () => { - const [group, validator] = await validators.getDeregistrationLockups() - assertEqualBN(group, newLockups.group) - assertEqualBN(validator, newLockups.validator) + it('should have set the validator locked gold requirements', async () => { + const [value, duration] = await validators.getValidatorLockedGoldRequirements() + assertEqualBN(value, newRequirements.value) + assertEqualBN(duration, newRequirements.duration) }) - it('should emit the DeregistrationLockupsSet event', async () => { + it('should emit the ValidatorLockedGoldRequirementsSet event', async () => { assert.equal(resp.logs.length, 1) const log = resp.logs[0] assertContainSubset(log, { - event: 'DeregistrationLockupsSet', + event: 'ValidatorLockedGoldRequirementsSet', args: { - group: new BigNumber(newLockups.group), - validator: new BigNumber(newLockups.validator), + value: newRequirements.value, + duration: newRequirements.duration, }, }) }) @@ -379,20 +397,22 @@ contract('Validators', (accounts: string[]) => { describe('when called by a non-owner', () => { it('should revert', async () => { await assertRevert( - validators.setDeregistrationLockups(newLockups.group, newLockups.validator, { - from: nonOwner, - }) + validators.setValidatorLockedGoldRequirements( + newRequirements.value, + newRequirements.duration, + { from: nonOwner } + ) ) }) }) }) - describe('when the lockups are the same', () => { + describe('when the requirements are the same', () => { it('should revert', async () => { await assertRevert( - validators.setDeregistrationLockups( - deregistrationLockups.group, - deregistrationLockups.validator + validators.setValidatorLockedGoldRequirements( + validatorLockedGoldRequirements.value, + validatorLockedGoldRequirements.duration ) ) }) @@ -509,7 +529,10 @@ contract('Validators', (accounts: string[]) => { let resp: any describe('when the account is not a registered validator', () => { beforeEach(async () => { - await mockLockedGold.setAccountTotalLockedGold(validator, balanceRequirements.validator) + await mockLockedGold.setAccountTotalLockedGold( + validator, + validatorLockedGoldRequirements.value + ) resp = await validators.registerValidator( name, url, @@ -533,9 +556,9 @@ contract('Validators', (accounts: string[]) => { assert.equal(parsedValidator.publicKeysData, publicKeysData) }) - it('should set account balance requirements', async () => { - const requirement = await validators.getAccountBalanceRequirement(validator) - assertEqualBN(requirement, balanceRequirements.validator) + it('should set account locked gold requirements', async () => { + const requirement = await validators.getAccountLockedGoldRequirement(validator) + assertEqualBN(requirement, validatorLockedGoldRequirements.value) }) it('should emit the ValidatorRegistered event', async () => { @@ -553,22 +576,9 @@ contract('Validators', (accounts: string[]) => { }) }) - describe('when the account is already a registered validator', () => { - beforeEach(async () => { - await mockLockedGold.setAccountTotalLockedGold(validator, balanceRequirements.validator) - await validators.registerValidator( - name, - url, - // @ts-ignore bytes type - publicKeysData - ) - assert.deepEqual(await validators.getRegisteredValidators(), [validator]) - }) - }) - - describe('when the account is already a registered validator', () => { + describe('when the account is already a registered validator group', () => { beforeEach(async () => { - await mockLockedGold.setAccountTotalLockedGold(validator, balanceRequirements.group) + await mockLockedGold.setAccountTotalLockedGold(validator, groupLockedGoldRequirements.value) await validators.registerValidatorGroup(name, url, commission) }) @@ -584,11 +594,11 @@ contract('Validators', (accounts: string[]) => { }) }) - describe('when the account does not meet the balance requirements', () => { + describe('when the account does not meet the locked gold requirements', () => { beforeEach(async () => { await mockLockedGold.setAccountTotalLockedGold( validator, - balanceRequirements.validator.minus(1) + validatorLockedGoldRequirements.value.minus(1) ) }) @@ -612,92 +622,96 @@ contract('Validators', (accounts: string[]) => { describe('when the account is a registered validator', () => { beforeEach(async () => { await registerValidator(validator) - resp = await validators.deregisterValidator(index) - }) - - it('should mark the account as not a validator', async () => { - assert.isFalse(await validators.isValidator(validator)) }) - it('should remove the account from the list of validators', async () => { - assert.deepEqual(await validators.getRegisteredValidators(), []) - }) - - it('should preserve account balance requirements', async () => { - const requirement = await validators.getAccountBalanceRequirement(validator) - assertEqualBN(requirement, balanceRequirements.validator) - }) - - it('should set the validator deregistration timestamp', async () => { - const latestTimestamp = (await web3.eth.getBlock('latest')).timestamp - const [groupTimestamp, validatorTimestamp] = await validators.getDeregistrationTimestamps( - validator - ) - assertEqualBN(groupTimestamp, 0) - assertEqualBN(validatorTimestamp, latestTimestamp) - }) - - it('should emit the ValidatorDeregistered event', async () => { - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'ValidatorDeregistered', - args: { - validator, - }, + describe('when the validator has never been a member of a validator group', () => { + beforeEach(async () => { + resp = await validators.deregisterValidator(index) }) - }) - }) - describe('when the validator is affiliated with a validator group', () => { - const group = accounts[1] - beforeEach(async () => { - await registerValidator(validator) - await registerValidatorGroup(group) - await validators.affiliate(group) - }) - - it('should emit the ValidatorDeafilliated event', async () => { - const resp = await validators.deregisterValidator(index) - assert.equal(resp.logs.length, 2) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'ValidatorDeaffiliated', - args: { - validator, - group, - }, + it('should mark the account as not a validator', async () => { + assert.isFalse(await validators.isValidator(validator)) }) - }) - describe('when the validator is a member of that group', () => { - beforeEach(async () => { - await validators.addFirstMember(validator, NULL_ADDRESS, NULL_ADDRESS, { from: group }) + it('should remove the account from the list of validators', async () => { + assert.deepEqual(await validators.getRegisteredValidators(), []) }) - it('should remove the validator from the group membership list', async () => { - await validators.deregisterValidator(index) - const parsedGroup = parseValidatorGroupParams(await validators.getValidatorGroup(group)) - assert.deepEqual(parsedGroup.members, []) + it('should reset account balance requirements', async () => { + const requirement = await validators.getAccountLockedGoldRequirement(validator) + assertEqualBN(requirement, 0) }) - it('should emit the ValidatorGroupMemberRemoved event', async () => { - const resp = await validators.deregisterValidator(index) - assert.equal(resp.logs.length, 3) + it('should emit the ValidatorDeregistered event', async () => { + assert.equal(resp.logs.length, 1) const log = resp.logs[0] assertContainSubset(log, { - event: 'ValidatorGroupMemberRemoved', + event: 'ValidatorDeregistered', args: { validator, - group, }, }) }) + }) + + describe('when the validator has been a member of a validator group', () => { + const group = accounts[1] + beforeEach(async () => { + await registerValidatorGroup(group) + await validators.affiliate(group) + await validators.addFirstMember(validator, NULL_ADDRESS, NULL_ADDRESS, { from: group }) + }) + + describe('when the validator is no longer a member of a validator group', () => { + beforeEach(async () => { + await validators.removeMember(validator, { from: group }) + }) + + describe('when it has been more than `validatorLockedGoldRequirements.duration` since the validator was removed from the group', () => { + beforeEach(async () => { + await timeTravel(validatorLockedGoldRequirements.duration.plus(1).toNumber(), web3) + resp = await validators.deregisterValidator(index) + }) + + it('should mark the account as not a validator', async () => { + assert.isFalse(await validators.isValidator(validator)) + }) + + it('should remove the account from the list of validators', async () => { + assert.deepEqual(await validators.getRegisteredValidators(), []) + }) + + it('should reset account balance requirements', async () => { + const requirement = await validators.getAccountLockedGoldRequirement(validator) + assertEqualBN(requirement, 0) + }) + + it('should emit the ValidatorDeregistered event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorDeregistered', + args: { + validator, + }, + }) + }) + }) + + describe('when it has been `validatorLockedGoldRequirements.duration` since the validator was removed from the group', () => { + beforeEach(async () => { + await timeTravel(validatorLockedGoldRequirements.duration.toNumber(), web3) + }) + + it('should revert', async () => { + await assertRevert(validators.deregisterValidator(index)) + }) + }) + }) - describe('when the validator is the only member of that group', () => { - it('should should mark the group as ineligible for election', async () => { - await validators.deregisterValidator(index) - assert.isTrue(await mockElection.isIneligible(group)) + describe('when the validator is still a member of a validator group', () => { + it('should revert', async () => { + await assertRevert(validators.deregisterValidator(index)) }) }) }) @@ -715,108 +729,174 @@ contract('Validators', (accounts: string[]) => { describe('#affiliate', () => { const validator = accounts[0] const group = accounts[1] - beforeEach(async () => { - await registerValidator(validator) - await registerValidatorGroup(group) - }) - - it('should set the affiliate', async () => { - await validators.affiliate(group) - const parsedValidator = parseValidatorParams(await validators.getValidator(validator)) - assert.equal(parsedValidator.affiliation, group) - }) - - it('should emit the ValidatorAffiliated event', async () => { - const resp = await validators.affiliate(group) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'ValidatorAffiliated', - args: { - validator, - group, - }, - }) - }) - - describe('when the validator is already affiliated with a validator group', () => { - const otherGroup = accounts[2] + let resp: any + describe('when the account has a registered validator', () => { beforeEach(async () => { - await validators.affiliate(group) - await registerValidatorGroup(otherGroup) + await registerValidator(validator) }) + describe('when affiliating with a registered validator group', () => { + beforeEach(async () => { + await registerValidatorGroup(group) + }) - it('should set the affiliate', async () => { - await validators.affiliate(otherGroup) - const parsedValidator = parseValidatorParams(await validators.getValidator(validator)) - assert.equal(parsedValidator.affiliation, otherGroup) - }) + describe('when the validator meets the locked gold requirements', () => { + describe('when the group meets the locked gold requirements', () => { + beforeEach(async () => { + resp = await validators.affiliate(group) + }) + + it('should set the affiliate', async () => { + await validators.affiliate(group) + const parsedValidator = parseValidatorParams(await validators.getValidator(validator)) + assert.equal(parsedValidator.affiliation, group) + }) + + it('should emit the ValidatorAffiliated event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorAffiliated', + args: { + validator, + group, + }, + }) + }) - it('should emit the ValidatorDeafilliated event', async () => { - const resp = await validators.affiliate(otherGroup) - assert.equal(resp.logs.length, 2) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'ValidatorDeaffiliated', - args: { - validator, - group, - }, - }) - }) + describe('when the validator is already affiliated with a validator group', () => { + const otherGroup = accounts[2] + beforeEach(async () => { + await registerValidatorGroup(otherGroup) + }) - it('should emit the ValidatorAffiliated event', async () => { - const resp = await validators.affiliate(otherGroup) - assert.equal(resp.logs.length, 2) - const log = resp.logs[1] - assertContainSubset(log, { - event: 'ValidatorAffiliated', - args: { - validator, - group: otherGroup, - }, - }) - }) + describe('when the validator is not a member of that validator group', () => { + beforeEach(async () => { + resp = await validators.affiliate(otherGroup) + }) + + it('should set the affiliate', async () => { + const parsedValidator = parseValidatorParams( + await validators.getValidator(validator) + ) + assert.equal(parsedValidator.affiliation, otherGroup) + }) + + it('should emit the ValidatorDeafilliated event', async () => { + assert.equal(resp.logs.length, 2) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorDeaffiliated', + args: { + validator, + group, + }, + }) + }) + + it('should emit the ValidatorAffiliated event', async () => { + assert.equal(resp.logs.length, 2) + const log = resp.logs[1] + assertContainSubset(log, { + event: 'ValidatorAffiliated', + args: { + validator, + group: otherGroup, + }, + }) + }) + }) - describe('when the validator is a member of that group', () => { - beforeEach(async () => { - await validators.addFirstMember(validator, NULL_ADDRESS, NULL_ADDRESS, { from: group }) - }) + describe('when the validator is a member of that group', () => { + beforeEach(async () => { + await validators.addFirstMember(validator, NULL_ADDRESS, NULL_ADDRESS, { + from: group, + }) + resp = await validators.affiliate(otherGroup) + }) + + it('should remove the validator from the group membership list', async () => { + const parsedGroup = parseValidatorGroupParams( + await validators.getValidatorGroup(group) + ) + assert.deepEqual(parsedGroup.members, []) + }) + + it("should update the validator's membership history", async () => { + const membershipHistory = parseMembershipHistory( + await validators.getMembershipHistory(validator) + ) + const latestBlock = await web3.eth.getBlock('latest') + const expectedEpoch = new BigNumber(Math.floor(latestBlock.number / EPOCH)) + assert.equal(membershipHistory.epochs.length, 1) + assertEqualBN(membershipHistory.epochs[0], expectedEpoch) + assert.equal(membershipHistory.groups.length, 1) + assertSameAddress(membershipHistory.groups[0], NULL_ADDRESS) + assert.equal( + membershipHistory.lastRemovedFromGroupTimestamp, + latestBlock.timestamp + ) + }) + + it('should emit the ValidatorGroupMemberRemoved event', async () => { + assert.equal(resp.logs.length, 3) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorGroupMemberRemoved', + args: { + validator, + group, + }, + }) + }) + + describe('when the validator is the only member of that group', () => { + it('should should mark the group as ineligible for election', async () => { + assert.isTrue(await mockElection.isIneligible(group)) + }) + }) + }) + }) + }) - it('should remove the validator from the group membership list', async () => { - await validators.affiliate(otherGroup) - const parsedGroup = parseValidatorGroupParams(await validators.getValidatorGroup(group)) - assert.deepEqual(parsedGroup.members, []) + describe('when the group does not meet the locked gold requirements', () => { + beforeEach(async () => { + await mockLockedGold.setAccountTotalLockedGold( + group, + groupLockedGoldRequirements.value.minus(1) + ) + }) + + it('should revert', async () => { + await assertRevert(validators.affiliate(group)) + }) + }) }) - it('should emit the ValidatorGroupMemberRemoved event', async () => { - const resp = await validators.affiliate(otherGroup) - assert.equal(resp.logs.length, 3) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'ValidatorGroupMemberRemoved', - args: { + describe('when the validator does not meet the locked gold requirements', () => { + beforeEach(async () => { + await mockLockedGold.setAccountTotalLockedGold( validator, - group, - }, + validatorLockedGoldRequirements.value.minus(1) + ) }) - }) - describe('when the validator is the only member of that group', () => { - it('should should mark the group as ineligible for election', async () => { - await validators.affiliate(otherGroup) - assert.isTrue(await mockElection.isIneligible(group)) + it('should revert', async () => { + await assertRevert(validators.affiliate(group)) }) }) }) - }) - it('should revert when the account is not a registered validator', async () => { - await assertRevert(validators.affiliate(group, { from: accounts[2] })) + describe('when affiliating with a non-registered validator group', () => { + it('should revert', async () => { + await assertRevert(validators.affiliate(group)) + }) + }) }) - it('should revert when the group is not a registered validator group', async () => { - await assertRevert(validators.affiliate(accounts[2])) + describe('when the account does not have a registered validator', () => { + it('should revert', async () => { + await assertRevert(validators.affiliate(group)) + }) }) }) @@ -861,14 +941,16 @@ contract('Validators', (accounts: string[]) => { it("should update the member's membership history", async () => { await validators.deaffiliate() - const membershipHistory = await validators.getMembershipHistory(validator) - const expectedEpoch = new BigNumber( - Math.floor((await web3.eth.getBlock('latest')).number / EPOCH) + const membershipHistory = parseMembershipHistory( + await validators.getMembershipHistory(validator) ) - assert.equal(membershipHistory[0].length, 1) - assertEqualBN(membershipHistory[0][0], expectedEpoch) - assert.equal(membershipHistory[1].length, 1) - assertSameAddress(membershipHistory[1][0], NULL_ADDRESS) + const latestBlock = await web3.eth.getBlock('latest') + const expectedEpoch = new BigNumber(Math.floor(latestBlock.number / EPOCH)) + assert.equal(membershipHistory.epochs.length, 1) + assertEqualBN(membershipHistory.epochs[0], expectedEpoch) + assert.equal(membershipHistory.groups.length, 1) + assertSameAddress(membershipHistory.groups[0], NULL_ADDRESS) + assert.equal(membershipHistory.lastRemovedFromGroupTimestamp, latestBlock.timestamp) }) it('should emit the ValidatorGroupMemberRemoved event', async () => { @@ -906,40 +988,54 @@ contract('Validators', (accounts: string[]) => { const group = accounts[0] let resp: any describe('when the account is not a registered validator group', () => { - beforeEach(async () => { - await mockLockedGold.setAccountTotalLockedGold(group, balanceRequirements.group) - resp = await validators.registerValidatorGroup(name, url, commission) - }) + describe('when the account meets the locked gold requirements', () => { + beforeEach(async () => { + await mockLockedGold.setAccountTotalLockedGold(group, groupLockedGoldRequirements.value) + resp = await validators.registerValidatorGroup(name, url, commission) + }) - it('should mark the account as a validator group', async () => { - assert.isTrue(await validators.isValidatorGroup(group)) - }) + it('should mark the account as a validator group', async () => { + assert.isTrue(await validators.isValidatorGroup(group)) + }) - it('should add the account to the list of validator groups', async () => { - assert.deepEqual(await validators.getRegisteredValidatorGroups(), [group]) - }) + it('should add the account to the list of validator groups', async () => { + assert.deepEqual(await validators.getRegisteredValidatorGroups(), [group]) + }) - it('should set the validator group name and url', async () => { - const parsedGroup = parseValidatorGroupParams(await validators.getValidatorGroup(group)) - assert.equal(parsedGroup.name, name) - assert.equal(parsedGroup.url, url) - }) + it('should set the validator group name and url', async () => { + const parsedGroup = parseValidatorGroupParams(await validators.getValidatorGroup(group)) + assert.equal(parsedGroup.name, name) + assert.equal(parsedGroup.url, url) + }) - it('should set account balance requirements', async () => { - const requirement = await validators.getAccountBalanceRequirement(group) - assertEqualBN(requirement, balanceRequirements.group) - }) + it('should set account locked gold requirements', async () => { + const requirement = await validators.getAccountLockedGoldRequirement(group) + assertEqualBN(requirement, groupLockedGoldRequirements.value) + }) - it('should emit the ValidatorGroupRegistered event', async () => { - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'ValidatorGroupRegistered', - args: { + it('should emit the ValidatorGroupRegistered event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorGroupRegistered', + args: { + group, + name, + url, + }, + }) + }) + }) + describe('when the account does not meet the locked gold requirements', () => { + beforeEach(async () => { + await mockLockedGold.setAccountTotalLockedGold( group, - name, - url, - }, + groupLockedGoldRequirements.value.minus(1) + ) + }) + + it('should revert', async () => { + await assertRevert(validators.registerValidatorGroup(name, url, commission)) }) }) }) @@ -949,25 +1045,14 @@ contract('Validators', (accounts: string[]) => { await registerValidator(group) }) - it('should revert', async () => { - await assertRevert(validators.registerValidatorGroup(name, url, balanceRequirements.group)) - }) - }) - - describe('when the account is already a registered validator group', () => { - beforeEach(async () => { - await mockLockedGold.setAccountTotalLockedGold(group, balanceRequirements.group) - await validators.registerValidatorGroup(name, url, commission) - }) - it('should revert', async () => { await assertRevert(validators.registerValidatorGroup(name, url, commission)) }) }) - describe('when the account does not meet the balance requirements', () => { + describe('when the account is already a registered validator group', () => { beforeEach(async () => { - await mockLockedGold.setAccountTotalLockedGold(group, balanceRequirements.group.minus(1)) + await registerValidatorGroup(group) }) it('should revert', async () => { @@ -980,61 +1065,108 @@ contract('Validators', (accounts: string[]) => { const index = 0 const group = accounts[0] let resp: any - beforeEach(async () => { - await registerValidatorGroup(group) - resp = await validators.deregisterValidatorGroup(index) - }) - - it('should mark the account as not a validator group', async () => { - assert.isFalse(await validators.isValidatorGroup(group)) - }) + describe('when the account has a registered validator group', () => { + beforeEach(async () => { + await registerValidatorGroup(group) + }) + describe('when the group has never had any members', () => { + beforeEach(async () => { + resp = await validators.deregisterValidatorGroup(index) + }) - it('should remove the account from the list of validator groups', async () => { - assert.deepEqual(await validators.getRegisteredValidatorGroups(), []) - }) + it('should mark the account as not a validator group', async () => { + assert.isFalse(await validators.isValidatorGroup(group)) + }) - it('should preserve account balance requirements', async () => { - const requirement = await validators.getAccountBalanceRequirement(group) - assertEqualBN(requirement, balanceRequirements.group) - }) + it('should remove the account from the list of validator groups', async () => { + assert.deepEqual(await validators.getRegisteredValidatorGroups(), []) + }) - it('should set the group deregistration timestamp', async () => { - const latestTimestamp = (await web3.eth.getBlock('latest')).timestamp - const [groupTimestamp, validatorTimestamp] = await validators.getDeregistrationTimestamps( - group - ) - assertEqualBN(groupTimestamp, latestTimestamp) - assertEqualBN(validatorTimestamp, 0) - }) + it('should reset account balance requirements', async () => { + const requirement = await validators.getAccountLockedGoldRequirement(group) + assertEqualBN(requirement, 0) + }) - it('should emit the ValidatorGroupDeregistered event', async () => { - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'ValidatorGroupDeregistered', - args: { - group, - }, + it('should emit the ValidatorGroupDeregistered event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorGroupDeregistered', + args: { + group, + }, + }) + }) }) - }) - it('should revert when the account is not a registered validator group', async () => { - await assertRevert(validators.deregisterValidatorGroup(index, { from: accounts[2] })) - }) + describe('when the group has had members', () => { + const validator = accounts[1] + beforeEach(async () => { + await registerValidator(validator) + await validators.affiliate(group, { from: validator }) + await validators.addFirstMember(validator, NULL_ADDRESS, NULL_ADDRESS) + }) - it('should revert when the wrong index is provided', async () => { - await assertRevert(validators.deregisterValidatorGroup(index + 1)) - }) + describe('when the group no longer has members', () => { + beforeEach(async () => { + await validators.removeMember(validator) + }) - describe('when the validator group is not empty', () => { - const validator = accounts[1] - beforeEach(async () => { - await registerValidatorGroup(group) - await registerValidator(validator) - await validators.affiliate(group, { from: validator }) - await validators.addFirstMember(validator, NULL_ADDRESS, NULL_ADDRESS) + describe('when it has been more than `groupLockedGoldRequirements.duration` since the validator was removed from the group', () => { + beforeEach(async () => { + await timeTravel(groupLockedGoldRequirements.duration.plus(1).toNumber(), web3) + resp = await validators.deregisterValidatorGroup(index) + }) + + it('should mark the account as not a validator group', async () => { + assert.isFalse(await validators.isValidatorGroup(group)) + }) + + it('should remove the account from the list of validator groups', async () => { + assert.deepEqual(await validators.getRegisteredValidatorGroups(), []) + }) + + it('should reset account balance requirements', async () => { + const requirement = await validators.getAccountLockedGoldRequirement(group) + assertEqualBN(requirement, 0) + }) + + it('should emit the ValidatorGroupDeregistered event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorGroupDeregistered', + args: { + group, + }, + }) + }) + }) + + describe('when it has been `groupLockedGoldRequirements.duration` since the validator was removed from the group', () => { + beforeEach(async () => { + await timeTravel(groupLockedGoldRequirements.duration.toNumber(), web3) + }) + + it('should revert', async () => { + await assertRevert(validators.deregisterValidatorGroup(index)) + }) + }) + }) + + describe('when the group still has members', () => { + it('should revert', async () => { + await assertRevert(validators.deregisterValidatorGroup(index)) + }) + }) }) + it('should revert when the wrong index is provided', async () => { + await assertRevert(validators.deregisterValidatorGroup(index + 1)) + }) + }) + + describe('when the account does not have a registered validator group', () => { it('should revert', async () => { await assertRevert(validators.deregisterValidatorGroup(index)) }) @@ -1045,63 +1177,150 @@ contract('Validators', (accounts: string[]) => { const group = accounts[0] const validator = accounts[1] let resp: any - beforeEach(async () => { - await registerValidator(validator) - await registerValidatorGroup(group) - await validators.affiliate(group, { from: validator }) - resp = await validators.addFirstMember(validator, NULL_ADDRESS, NULL_ADDRESS) - }) + describe('when account has a registered validator group', () => { + beforeEach(async () => { + await registerValidatorGroup(group) + }) + describe('when adding a validator affiliated with the group', () => { + beforeEach(async () => { + await registerValidator(validator) + await validators.affiliate(group, { from: validator }) + }) - it('should add the member to the list of members', async () => { - const parsedGroup = parseValidatorGroupParams(await validators.getValidatorGroup(group)) - assert.deepEqual(parsedGroup.members, [validator]) - }) + describe('when the group meets the locked gold requirements', () => { + describe('when the validator meets the locked gold requirements', () => { + beforeEach(async () => { + resp = await validators.addFirstMember(validator, NULL_ADDRESS, NULL_ADDRESS) + }) - it("should update the member's membership history", async () => { - const membershipHistory = await validators.getMembershipHistory(validator) - const expectedEpoch = new BigNumber( - Math.floor((await web3.eth.getBlock('latest')).number / EPOCH) - ) - assert.equal(membershipHistory[0].length, 1) - assertEqualBN(membershipHistory[0][0], expectedEpoch) - assert.equal(membershipHistory[1].length, 1) - assertSameAddress(membershipHistory[1][0], group) - }) + it('should add the member to the list of members', async () => { + const parsedGroup = parseValidatorGroupParams( + await validators.getValidatorGroup(group) + ) + assert.deepEqual(parsedGroup.members, [validator]) + }) - it('should emit the ValidatorGroupMemberAdded event', async () => { - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'ValidatorGroupMemberAdded', - args: { - group, - validator, - }, - }) - }) + it("should update the groups's size history", async () => { + const parsedGroup = parseValidatorGroupParams( + await validators.getValidatorGroup(group) + ) + assert.equal(parsedGroup.sizeHistory.length, 1) + assertEqualBN( + parsedGroup.sizeHistory[0], + (await web3.eth.getBlock('latest')).timestamp + ) + }) - it('should revert when the account is not a registered validator group', async () => { - await assertRevert( - validators.addFirstMember(validator, NULL_ADDRESS, NULL_ADDRESS, { from: accounts[2] }) - ) - }) + it("should update the member's membership history", async () => { + const membershipHistory = await validators.getMembershipHistory(validator) + const expectedEpoch = new BigNumber( + Math.floor((await web3.eth.getBlock('latest')).number / EPOCH) + ) + assert.equal(membershipHistory[0].length, 1) + assertEqualBN(membershipHistory[0][0], expectedEpoch) + assert.equal(membershipHistory[1].length, 1) + assertSameAddress(membershipHistory[1][0], group) + }) + + it('should mark the group as eligible', async () => { + assert.isTrue(await mockElection.isEligible(group)) + }) + + it('should emit the ValidatorGroupMemberAdded event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorGroupMemberAdded', + args: { + group, + validator, + }, + }) + }) - it('should revert when the member is not a registered validator', async () => { - await assertRevert(validators.addFirstMember(accounts[2], NULL_ADDRESS, NULL_ADDRESS)) - }) + describe('when the group has no room to add another member', () => { + beforeEach(async () => { + await validators.setMaxGroupSize(1) + await registerValidator(accounts[2]) + await validators.affiliate(group, { from: accounts[2] }) + }) - it('should revert when trying to add too many members to group', async () => { - await validators.setMaxGroupSize(1) - await registerValidator(accounts[2]) - await validators.affiliate(group, { from: accounts[2] }) - await assertRevert(validators.addMember(accounts[2])) - }) + it('should revert', async () => { + await assertRevert(validators.addMember(accounts[2])) + }) + }) + + describe('when adding many validators affiliated with the group', () => { + it("should update the groups's size history and balance requirements", async () => { + const expectedSizeHistory = parseValidatorGroupParams( + await validators.getValidatorGroup(group) + ).sizeHistory + assert.equal(expectedSizeHistory.length, 1) + for (let i = 2; i < maxGroupSize.toNumber() + 1; i++) { + const numMembers = i + const validator = accounts[i] + await registerValidator(validator) + await validators.affiliate(group, { from: validator }) + await mockLockedGold.setAccountTotalLockedGold( + group, + groupLockedGoldRequirements.value.times(numMembers) + ) + await validators.addMember(validator) + expectedSizeHistory.push((await web3.eth.getBlock('latest')).timestamp) + const parsedGroup = parseValidatorGroupParams( + await validators.getValidatorGroup(group) + ) + assert.deepEqual( + parsedGroup.sizeHistory.map((x) => x.toString()), + expectedSizeHistory.map((x) => x.toString()) + ) + const requirement = await validators.getAccountLockedGoldRequirement(group) + assertEqualBN(requirement, groupLockedGoldRequirements.value.times(numMembers)) + } + }) + }) + }) - describe('when the validator has not affiliated themselves with the group', () => { - beforeEach(async () => { - await validators.deaffiliate({ from: validator }) + describe('when the validator does not meet the locked gold requirements', () => { + beforeEach(async () => { + await mockLockedGold.setAccountTotalLockedGold( + validator, + validatorLockedGoldRequirements.value.minus(1) + ) + }) + + it('should revert', async () => { + await assertRevert(validators.addFirstMember(validator, NULL_ADDRESS, NULL_ADDRESS)) + }) + }) + }) + + describe('when the group does not meet the locked gold requirements', () => { + beforeEach(async () => { + await mockLockedGold.setAccountTotalLockedGold( + group, + groupLockedGoldRequirements.value.minus(1) + ) + }) + + it('should revert', async () => { + await assertRevert(validators.addFirstMember(validator, NULL_ADDRESS, NULL_ADDRESS)) + }) + }) }) + describe('when adding a validator not affiliated with the group', () => { + beforeEach(async () => { + await registerValidator(validator) + }) + + it('should revert', async () => { + await assertRevert(validators.addFirstMember(validator, NULL_ADDRESS, NULL_ADDRESS)) + }) + }) + }) + + describe('when the account does not have a registered validator group', () => { it('should revert', async () => { await assertRevert(validators.addFirstMember(validator, NULL_ADDRESS, NULL_ADDRESS)) }) @@ -1129,23 +1348,32 @@ contract('Validators', (accounts: string[]) => { it("should update the member's membership history", async () => { await validators.removeMember(validator) - const membershipHistory = await validators.getMembershipHistory(validator) - const expectedEpoch = new BigNumber( - Math.floor((await web3.eth.getBlock('latest')).number / EPOCH) + const membershipHistory = parseMembershipHistory( + await validators.getMembershipHistory(validator) ) + const latestBlock = await web3.eth.getBlock('latest') + const expectedEpoch = new BigNumber(Math.floor(latestBlock.number / EPOCH)) // Depending on test timing, we may or may not span an epoch boundary between registration // and removal. - const numEntries = membershipHistory[0].length + const numEntries = membershipHistory.epochs.length assert.isTrue(numEntries == 1 || numEntries == 2) - assert.equal(membershipHistory[1].length, numEntries) + assert.equal(membershipHistory.groups.length, numEntries) if (numEntries == 1) { - assertEqualBN(membershipHistory[0][0], expectedEpoch) - assertSameAddress(membershipHistory[1][0], NULL_ADDRESS) + assertEqualBN(membershipHistory.epochs[0], expectedEpoch) + assertSameAddress(membershipHistory.groups[0], NULL_ADDRESS) } else { - assertEqualBN(membershipHistory[0][1], expectedEpoch) - assertSameAddress(membershipHistory[1][1], NULL_ADDRESS) + assertEqualBN(membershipHistory.epochs[1], expectedEpoch) + assertSameAddress(membershipHistory.groups[1], NULL_ADDRESS) } + assert.equal(membershipHistory.lastRemovedFromGroupTimestamp, latestBlock.timestamp) + }) + + it("should update the group's size history", async () => { + await validators.removeMember(validator) + const parsedGroup = parseValidatorGroupParams(await validators.getValidatorGroup(group)) + assert.equal(parsedGroup.sizeHistory.length, 2) + assertEqualBN(parsedGroup.sizeHistory[1], (await web3.eth.getBlock('latest')).timestamp) }) it('should emit the ValidatorGroupMemberRemoved event', async () => { @@ -1301,22 +1529,25 @@ contract('Validators', (accounts: string[]) => { await validators.addFirstMember(validator, NULL_ADDRESS, NULL_ADDRESS, { from: groups[i], }) + await mineBlocks(EPOCH, web3) - const membershipHistory = await validators.getMembershipHistory(validator) + const membershipHistory = parseMembershipHistory( + await validators.getMembershipHistory(validator) + ) const expectedMembershipHistoryLength = Math.min( i + 1, membershipHistoryLength.toNumber() ) - assert.equal(membershipHistory[0].length, expectedMembershipHistoryLength) - assert.equal(membershipHistory[1].length, expectedMembershipHistoryLength) + assert.equal(membershipHistory.epochs.length, expectedMembershipHistoryLength) + assert.equal(membershipHistory.groups.length, expectedMembershipHistoryLength) for (let j = 0; j < expectedMembershipHistoryLength; j++) { assert.include( - membershipHistory[0].map((x) => x.toNumber()), + membershipHistory.epochs.map((x) => x.toNumber()), currentEpoch.minus(j).toNumber() ) assert.include( - membershipHistory[1].map((x) => x.toLowerCase()), + membershipHistory.groups.map((x) => x.toLowerCase()), groups[i - j].toLowerCase() ) } @@ -1339,7 +1570,9 @@ contract('Validators', (accounts: string[]) => { it('should always return the correct membership for the last epoch', async () => { for (let i = 0; i < membershipHistoryLength.plus(1).toNumber(); i++) { const blockNumber = (await web3.eth.getBlock('latest')).number - const blocksUntilNextEpoch = blockNumber % EPOCH + const currentEpoch = Math.floor(blockNumber / EPOCH) + const nextEpochBlockNumber = EPOCH * (currentEpoch + 1) + const blocksUntilNextEpoch = nextEpochBlockNumber - blockNumber await mineBlocks(blocksUntilNextEpoch, web3) await validators.affiliate(groups[i]) @@ -1363,7 +1596,73 @@ contract('Validators', (accounts: string[]) => { }) }) - describe.only('#distributeEpochPayment', () => { + describe('#getAccountLockedGoldRequirement', () => { + describe('when a validator group has added members', () => { + const group = accounts[0] + const numMembers = 5 + let actualRequirements: BigNumber[] + beforeEach(async () => { + actualRequirements = [] + await registerValidatorGroup(group) + for (let i = 1; i < numMembers + 1; i++) { + const validator = accounts[i] + await registerValidator(validator) + await validators.affiliate(group, { from: validator }) + await mockLockedGold.setAccountTotalLockedGold( + group, + groupLockedGoldRequirements.value.times(i) + ) + if (i == 1) { + await validators.addFirstMember(validator, NULL_ADDRESS, NULL_ADDRESS) + } else { + await validators.addMember(validator) + } + actualRequirements.push(await validators.getAccountLockedGoldRequirement(group)) + } + }) + + it('should increase the requirement with each added member', async () => { + for (let i = 0; i < numMembers; i++) { + assertEqualBN(actualRequirements[i], groupLockedGoldRequirements.value.times(i + 1)) + } + }) + + describe('when a validator group is removing members', () => { + let removalTimestamps: number[] + beforeEach(async () => { + removalTimestamps = [] + for (let i = 1; i < numMembers + 1; i++) { + const validator = accounts[i] + await validators.removeMember(validator) + removalTimestamps.push((await web3.eth.getBlock('latest')).timestamp) + // Space things out. + await timeTravel(47, web3) + } + }) + + it('should decrease the requirement `duration`+1 seconds after removal', async () => { + for (let i = 0; i < numMembers; i++) { + assertEqualBN( + await validators.getAccountLockedGoldRequirement(group), + groupLockedGoldRequirements.value.times(numMembers - i) + ) + const removalTimestamp = removalTimestamps[i] + const requirementExpiry = groupLockedGoldRequirements.duration.plus(removalTimestamp) + const currentTimestamp = (await web3.eth.getBlock('latest')).timestamp + await timeTravel( + requirementExpiry + .minus(currentTimestamp) + .plus(1) + .toNumber(), + web3 + ) + } + }) + }) + }) + }) + + describe('#distributeEpochPayment', () => { const validator = accounts[0] const group = accounts[1] let mockStableToken: MockStableTokenInstance @@ -1405,7 +1704,7 @@ contract('Validators', (accounts: string[]) => { beforeEach(async () => { await mockLockedGold.setAccountTotalLockedGold( validator, - balanceRequirements.validator.minus(1) + validatorLockedGoldRequirements.value.minus(1) ) await validators.distributeEpochPayment(validator) }) @@ -1419,9 +1718,12 @@ contract('Validators', (accounts: string[]) => { }) }) - describe('when the validator does not meet the balance requirements', () => { + describe('when the group does not meet the balance requirements', () => { beforeEach(async () => { - await mockLockedGold.setAccountTotalLockedGold(group, balanceRequirements.group.minus(1)) + await mockLockedGold.setAccountTotalLockedGold( + group, + groupLockedGoldRequirements.value.minus(1) + ) await validators.distributeEpochPayment(validator) }) From b8e17977aadd3fb89d56aa137a614293fecbfcec Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Tue, 15 Oct 2019 17:30:43 -0700 Subject: [PATCH 064/149] Address comments --- packages/cli/src/commands/lockedgold/lock.ts | 2 +- packages/contractkit/src/wrappers/Election.ts | 4 ++-- packages/contractkit/src/wrappers/LockedGold.ts | 8 ++++---- packages/contractkit/src/wrappers/Validators.ts | 2 +- packages/protocol/contracts/governance/Election.sol | 1 - packages/protocol/contracts/governance/LockedGold.sol | 5 ++++- 6 files changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/cli/src/commands/lockedgold/lock.ts b/packages/cli/src/commands/lockedgold/lock.ts index 4961d382cbd..d9f59f2bcaf 100644 --- a/packages/cli/src/commands/lockedgold/lock.ts +++ b/packages/cli/src/commands/lockedgold/lock.ts @@ -31,7 +31,7 @@ export default class Lock extends BaseCommand { const value = new BigNumber(res.flags.value) if (!value.gt(new BigNumber(0))) { - failWith(`Provided value must be greater than zero => [${value}]`) + failWith(`Provided value must be greater than zero => [${value.toString()}]`) } const tx = lockedGold.lock() diff --git a/packages/contractkit/src/wrappers/Election.ts b/packages/contractkit/src/wrappers/Election.ts index 4da80f58f8b..8d2285fba35 100644 --- a/packages/contractkit/src/wrappers/Election.ts +++ b/packages/contractkit/src/wrappers/Election.ts @@ -156,7 +156,7 @@ export class ElectionWrapper extends BaseWrapper { */ async markGroupEligible(validatorGroup: Address): Promise> { if (this.kit.defaultAccount == null) { - throw new Error(`missing from at new ValdidatorUtils()`) + throw new Error(`missing kit.defaultAccount`) } const value = toBigNumber( @@ -174,7 +174,7 @@ export class ElectionWrapper extends BaseWrapper { */ async vote(validatorGroup: Address, value: BigNumber): Promise> { if (this.kit.defaultAccount == null) { - throw new Error(`missing from at new ValdidatorUtils()`) + throw new Error(`missing kit.defaultAccount`) } const { lesser, greater } = await this.findLesserAndGreaterAfterVote(validatorGroup, value) diff --git a/packages/contractkit/src/wrappers/LockedGold.ts b/packages/contractkit/src/wrappers/LockedGold.ts index 979289a51dc..e61d8b8a869 100644 --- a/packages/contractkit/src/wrappers/LockedGold.ts +++ b/packages/contractkit/src/wrappers/LockedGold.ts @@ -29,8 +29,8 @@ interface AccountSummary { nonvoting: BigNumber } authorizations: { - voter: string - validator: string + voter: null | string + validator: null | string } pendingWithdrawals: PendingWithdrawal[] } @@ -140,8 +140,8 @@ export class LockedGoldWrapper extends BaseWrapper { nonvoting, }, authorizations: { - voter: eqAddress(voter, account) ? 'None' : voter, - validator: eqAddress(validator, account) ? 'None' : validator, + voter: eqAddress(voter, account) ? null : voter, + validator: eqAddress(validator, account) ? null : validator, }, pendingWithdrawals, } diff --git a/packages/contractkit/src/wrappers/Validators.ts b/packages/contractkit/src/wrappers/Validators.ts index 4ecc8e9ceb2..94809d64a47 100644 --- a/packages/contractkit/src/wrappers/Validators.ts +++ b/packages/contractkit/src/wrappers/Validators.ts @@ -58,7 +58,7 @@ export class ValidatorsWrapper extends BaseWrapper { commission: BigNumber ): Promise> { if (this.kit.defaultAccount == null) { - throw new Error(`missing from at new ValdidatorUtils()`) + throw new Error(`missing kit.defaultAccount`) } return wrapSend( this.kit, diff --git a/packages/protocol/contracts/governance/Election.sol b/packages/protocol/contracts/governance/Election.sol index 3f1c7919503..1aa970b68ed 100644 --- a/packages/protocol/contracts/governance/Election.sol +++ b/packages/protocol/contracts/governance/Election.sol @@ -657,7 +657,6 @@ contract Election is IElection, Ownable, ReentrancyGuard, Initializable, UsingRe require(index < list.length && list[index] == element); uint256 lastIndex = list.length.sub(1); list[index] = list[lastIndex]; - delete list[lastIndex]; list.length = lastIndex; } diff --git a/packages/protocol/contracts/governance/LockedGold.sol b/packages/protocol/contracts/governance/LockedGold.sol index 1fbc1665b7b..23cf607f67b 100644 --- a/packages/protocol/contracts/governance/LockedGold.sol +++ b/packages/protocol/contracts/governance/LockedGold.sol @@ -207,7 +207,10 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr require(isAccount(msg.sender)); Account storage account = accounts[msg.sender]; uint256 balanceRequirement = getValidators().getAccountBalanceRequirement(msg.sender); - require(balanceRequirement <= getAccountTotalLockedGold(msg.sender).sub(value)); + require( + balanceRequirement == 0 || + balanceRequirement <= getAccountTotalLockedGold(msg.sender).sub(value) + ); _decrementNonvotingAccountBalance(msg.sender, value); uint256 available = now.add(unlockingPeriod); account.balances.pendingWithdrawals.push(PendingWithdrawal(value, available)); From 4a4d6e226ed88cac53993b532ae9aab3bfc28275 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Wed, 16 Oct 2019 09:25:29 -0700 Subject: [PATCH 065/149] Address comments --- packages/contractkit/src/wrappers/Election.ts | 6 ++-- .../contractkit/src/wrappers/LockedGold.ts | 12 ++++++-- .../src/wrappers/Validators.test.ts | 23 ++++++--------- .../contractkit/src/wrappers/Validators.ts | 28 ++----------------- .../contracts/governance/Election.sol | 12 +++++--- 5 files changed, 32 insertions(+), 49 deletions(-) diff --git a/packages/contractkit/src/wrappers/Election.ts b/packages/contractkit/src/wrappers/Election.ts index 8d2285fba35..9cd270aeb5f 100644 --- a/packages/contractkit/src/wrappers/Election.ts +++ b/packages/contractkit/src/wrappers/Election.ts @@ -12,7 +12,7 @@ import { toBigNumber, toNumber, tupleParser, - wrapSend, + toTransactionObject, } from './BaseWrapper' export interface Validator { @@ -164,7 +164,7 @@ export class ElectionWrapper extends BaseWrapper { ) const { lesser, greater } = await this.findLesserAndGreaterAfterVote(validatorGroup, value) - return wrapSend(this.kit, this.contract.methods.markGroupEligible(lesser, greater)) + return toTransactionObject(this.kit, this.contract.methods.markGroupEligible(lesser, greater)) } /** @@ -179,7 +179,7 @@ export class ElectionWrapper extends BaseWrapper { const { lesser, greater } = await this.findLesserAndGreaterAfterVote(validatorGroup, value) - return wrapSend( + return toTransactionObject( this.kit, this.contract.methods.vote(validatorGroup, value.toString(), lesser, greater) ) diff --git a/packages/contractkit/src/wrappers/LockedGold.ts b/packages/contractkit/src/wrappers/LockedGold.ts index b9a83ddf639..4ceccbece66 100644 --- a/packages/contractkit/src/wrappers/LockedGold.ts +++ b/packages/contractkit/src/wrappers/LockedGold.ts @@ -118,7 +118,12 @@ export class LockedGoldWrapper extends BaseWrapper { getValidatorFromAccount: (account: string) => Promise
= proxyCall( this.contract.methods.getValidatorFromAccount ) - + /** + * Check if an account already exists. + * @param account The address of the account + * @return Returns `true` if account exists. Returns `false` otherwise. + */ + isAccount: (account: string) => Promise = proxyCall(this.contract.methods.isAccount) /** * Returns current configuration parameters. */ @@ -156,7 +161,10 @@ export class LockedGoldWrapper extends BaseWrapper { async authorizeVoter(account: Address, voter: Address): Promise> { const sig = await this.getParsedSignatureOfAddress(account, voter) // TODO(asa): Pass default tx "from" argument. - return wrapSend(this.kit, this.contract.methods.authorizeVoter(voter, sig.v, sig.r, sig.s)) + return toTransactionObject( + this.kit, + this.contract.methods.authorizeVoter(voter, sig.v, sig.r, sig.s) + ) } /** diff --git a/packages/contractkit/src/wrappers/Validators.test.ts b/packages/contractkit/src/wrappers/Validators.test.ts index 3d5c831e6e3..782b7b509db 100644 --- a/packages/contractkit/src/wrappers/Validators.test.ts +++ b/packages/contractkit/src/wrappers/Validators.test.ts @@ -1,3 +1,4 @@ +import BigNumber from 'bignumber.js' import Web3 from 'web3' import { newKitFromWeb3 } from '../kit' import { testWithGanache } from '../test-utils/ganache-test' @@ -9,8 +10,7 @@ TEST NOTES: - In migrations: The only account that has cUSD is accounts[0] */ -const minLockedGoldValue = Web3.utils.toWei('100', 'ether') // 1 gold -const minLockedGoldNoticePeriod = 120 * 24 * 60 * 60 // 120 days +const minLockedGoldValue = Web3.utils.toWei('100', 'ether') // 100 gold // A random 64 byte hex string. const publicKey = @@ -29,15 +29,10 @@ testWithGanache('Validators Wrapper', (web3) => { let lockedGold: LockedGoldWrapper const registerAccountWithCommitment = async (account: string) => { - // console.log('isAccount', ) - // console.log('isDelegate', await lockedGold.isDelegate(account)) - if (!(await lockedGold.isAccount(account))) { await lockedGold.createAccount().sendAndWaitForReceipt({ from: account }) } - await lockedGold - .newCommitment(minLockedGoldNoticePeriod) - .sendAndWaitForReceipt({ from: account, value: minLockedGoldValue }) + await lockedGold.lock().sendAndWaitForReceipt({ from: account, value: minLockedGoldValue }) } beforeAll(async () => { @@ -48,9 +43,11 @@ testWithGanache('Validators Wrapper', (web3) => { const setupGroup = async (groupAccount: string) => { await registerAccountWithCommitment(groupAccount) - await validators - .registerValidatorGroup('thegroup', 'The Group', 'thegroup.com', [minLockedGoldNoticePeriod]) - .sendAndWaitForReceipt({ from: groupAccount }) + await (await validators.registerValidatorGroup( + 'The Group', + 'thegroup.com', + new BigNumber(0.1) + )).sendAndWaitForReceipt({ from: groupAccount }) } const setupValidator = async (validatorAccount: string) => { @@ -58,12 +55,10 @@ testWithGanache('Validators Wrapper', (web3) => { // set account1 as the validator await validators .registerValidator( - 'goodoldvalidator', 'Good old validator', 'goodold.com', // @ts-ignore - publicKeysData, - [minLockedGoldNoticePeriod] + publicKeysData ) .sendAndWaitForReceipt({ from: validatorAccount }) } diff --git a/packages/contractkit/src/wrappers/Validators.ts b/packages/contractkit/src/wrappers/Validators.ts index cffd70e0af9..e17ada27c14 100644 --- a/packages/contractkit/src/wrappers/Validators.ts +++ b/packages/contractkit/src/wrappers/Validators.ts @@ -1,6 +1,6 @@ import { fromFixed, toFixed } from '@celo/utils/lib/fixidity' import BigNumber from 'bignumber.js' -import { Address } from '../base' +import { Address, NULL_ADDRESS } from '../base' import { Validators } from '../generated/types/Validators' import { BaseWrapper, @@ -8,7 +8,6 @@ import { proxyCall, proxySend, toBigNumber, - toNumber, toTransactionObject, } from './BaseWrapper' @@ -59,7 +58,7 @@ export class ValidatorsWrapper extends BaseWrapper { if (this.kit.defaultAccount == null) { throw new Error(`missing kit.defaultAccount`) } - return wrapSend( + return toTransactionObject( this.kit, this.contract.methods.registerValidatorGroup(name, url, toFixed(commission).toFixed()) ) @@ -127,20 +126,6 @@ export class ValidatorsWrapper extends BaseWrapper { } } - /** - * Returns whether a particular account is voting for a validator group. - * @param account The account. - * @return Whether a particular account is voting for a validator group. - */ - isVoting = proxyCall(this.contract.methods.isVoting) - - /** - * Returns whether a particular account is a registered validator or validator group. - * @param account The account. - * @return Whether a particular account is a registered validator or validator group. - */ - isValidating = proxyCall(this.contract.methods.isValidating) - /** * Returns whether a particular account has a registered validator. * @param account The account. @@ -155,15 +140,6 @@ export class ValidatorsWrapper extends BaseWrapper { */ isValidatorGroup = proxyCall(this.contract.methods.isValidatorGroup) - /** - * Returns whether an account meets the requirements to register a validator or group. - * @param account The account. - * @param noticePeriods An array of notice periods of the Locked Gold commitments - * that cumulatively meet the requirements for validator registration. - * @return Whether an account meets the requirements to register a validator or group. - */ - meetsRegistrationRequirements = proxyCall(this.contract.methods.meetsRegistrationRequirements) - addMember = proxySend(this.kit, this.contract.methods.addMember) removeMember = proxySend(this.kit, this.contract.methods.removeMember) diff --git a/packages/protocol/contracts/governance/Election.sol b/packages/protocol/contracts/governance/Election.sol index 1aa970b68ed..807d1a74f69 100644 --- a/packages/protocol/contracts/governance/Election.sol +++ b/packages/protocol/contracts/governance/Election.sol @@ -629,12 +629,16 @@ contract Election is IElection, Ownable, ReentrancyGuard, Initializable, UsingRe * @param group The address of the validator group. * @param value The number of active votes being added. * @return The delta in active vote denominator for `group`. + * @dev Preserves unitsDelta / totalUnits = value / total */ function getActiveVotesUnitsDelta(address group, uint256 value) private view returns (uint256) { - // Preserve unitsDelta * total = value * totalUnits - return value.mul(votes.active.forGroup[group].totalUnits.add(1)).div( - votes.active.forGroup[group].total.add(1) - ); + if (votes.active.forGroup[group].total == 0) { + return value; + } else { + return value.mul(votes.active.forGroup[group].totalUnits).div( + votes.active.forGroup[group].total + ); + } } /** From f3515ac1867e378ced0f151b86ef4a6bf4e08850 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Wed, 16 Oct 2019 10:14:21 -0700 Subject: [PATCH 066/149] Fix tests, linting --- packages/contractkit/src/wrappers/Election.ts | 2 +- packages/contractkit/src/wrappers/LockedGold.ts | 2 +- packages/contractkit/src/wrappers/Validators.test.ts | 8 ++++---- packages/contractkit/src/wrappers/Validators.ts | 3 --- 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/contractkit/src/wrappers/Election.ts b/packages/contractkit/src/wrappers/Election.ts index 9cd270aeb5f..fbf35b6c87d 100644 --- a/packages/contractkit/src/wrappers/Election.ts +++ b/packages/contractkit/src/wrappers/Election.ts @@ -11,8 +11,8 @@ import { proxySend, toBigNumber, toNumber, - tupleParser, toTransactionObject, + tupleParser, } from './BaseWrapper' export interface Validator { diff --git a/packages/contractkit/src/wrappers/LockedGold.ts b/packages/contractkit/src/wrappers/LockedGold.ts index 4ceccbece66..bc3833f090c 100644 --- a/packages/contractkit/src/wrappers/LockedGold.ts +++ b/packages/contractkit/src/wrappers/LockedGold.ts @@ -12,8 +12,8 @@ import { proxyCall, proxySend, toBigNumber, - tupleParser, toTransactionObject, + tupleParser, } from '../wrappers/BaseWrapper' export interface VotingDetails { diff --git a/packages/contractkit/src/wrappers/Validators.test.ts b/packages/contractkit/src/wrappers/Validators.test.ts index 782b7b509db..1d54ed80bf8 100644 --- a/packages/contractkit/src/wrappers/Validators.test.ts +++ b/packages/contractkit/src/wrappers/Validators.test.ts @@ -10,7 +10,7 @@ TEST NOTES: - In migrations: The only account that has cUSD is accounts[0] */ -const minLockedGoldValue = Web3.utils.toWei('100', 'ether') // 100 gold +const minLockedGoldValue = Web3.utils.toWei('10', 'ether') // 10 gold // A random 64 byte hex string. const publicKey = @@ -28,7 +28,7 @@ testWithGanache('Validators Wrapper', (web3) => { let validators: ValidatorsWrapper let lockedGold: LockedGoldWrapper - const registerAccountWithCommitment = async (account: string) => { + const registerAccountWithLockedGold = async (account: string) => { if (!(await lockedGold.isAccount(account))) { await lockedGold.createAccount().sendAndWaitForReceipt({ from: account }) } @@ -42,7 +42,7 @@ testWithGanache('Validators Wrapper', (web3) => { }) const setupGroup = async (groupAccount: string) => { - await registerAccountWithCommitment(groupAccount) + await registerAccountWithLockedGold(groupAccount) await (await validators.registerValidatorGroup( 'The Group', 'thegroup.com', @@ -51,7 +51,7 @@ testWithGanache('Validators Wrapper', (web3) => { } const setupValidator = async (validatorAccount: string) => { - await registerAccountWithCommitment(validatorAccount) + await registerAccountWithLockedGold(validatorAccount) // set account1 as the validator await validators .registerValidator( diff --git a/packages/contractkit/src/wrappers/Validators.ts b/packages/contractkit/src/wrappers/Validators.ts index e17ada27c14..754bedcadc2 100644 --- a/packages/contractkit/src/wrappers/Validators.ts +++ b/packages/contractkit/src/wrappers/Validators.ts @@ -55,9 +55,6 @@ export class ValidatorsWrapper extends BaseWrapper { url: string, commission: BigNumber ): Promise> { - if (this.kit.defaultAccount == null) { - throw new Error(`missing kit.defaultAccount`) - } return toTransactionObject( this.kit, this.contract.methods.registerValidatorGroup(name, url, toFixed(commission).toFixed()) From 9aa6ec300ac717bcb5ec079bcdda2548a4032b29 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Wed, 16 Oct 2019 11:01:30 -0700 Subject: [PATCH 067/149] Remove bondeddeposits test --- .../test/governance/bondeddeposits.ts | 1088 ----------------- .../protocol/test/governance/lockedgold.ts | 24 +- 2 files changed, 7 insertions(+), 1105 deletions(-) delete mode 100644 packages/protocol/test/governance/bondeddeposits.ts diff --git a/packages/protocol/test/governance/bondeddeposits.ts b/packages/protocol/test/governance/bondeddeposits.ts deleted file mode 100644 index f2fc5d2bb20..00000000000 --- a/packages/protocol/test/governance/bondeddeposits.ts +++ /dev/null @@ -1,1088 +0,0 @@ -import { CeloContractName } from '@celo/protocol/lib/registry-utils' -import { getParsedSignatureOfAddress } from '@celo/protocol/lib/signing-utils' -import { - assertEqualBN, - assertLogMatches, - assertRevert, - NULL_ADDRESS, - timeTravel, -} from '@celo/protocol/lib/test-utils' -import BigNumber from 'bignumber.js' -import { - LockedGoldContract, - LockedGoldInstance, - MockGoldTokenContract, - MockGoldTokenInstance, - MockGovernanceContract, - MockGovernanceInstance, - MockValidatorsContract, - MockValidatorsInstance, - RegistryContract, - RegistryInstance, -} from 'types' - -const LockedGold: LockedGoldContract = artifacts.require('LockedGold') -const Registry: RegistryContract = artifacts.require('Registry') -const MockGoldToken: MockGoldTokenContract = artifacts.require('MockGoldToken') -const MockGovernance: MockGovernanceContract = artifacts.require('MockGovernance') -const MockValidators: MockValidatorsContract = artifacts.require('MockValidators') - -// @ts-ignore -// TODO(mcortesi): Use BN -LockedGold.numberFormat = 'BigNumber' - -const HOUR = 60 * 60 -const DAY = 24 * HOUR -const YEAR = 365 * DAY - -// TODO(asa): Test reward redemption -contract('LockedGold', (accounts: string[]) => { - let account = accounts[0] - const nonOwner = accounts[1] - const maxNoticePeriod = 2 * YEAR - let mockGoldToken: MockGoldTokenInstance - let mockGovernance: MockGovernanceInstance - let mockValidators: MockValidatorsInstance - let lockedGold: LockedGoldInstance - let registry: RegistryInstance - - enum roles { - validating, - voting, - rewards, - } - const forEachRole = (tests: (arg0: roles) => void) => - Object.keys(roles) - .slice(3) - .map((role) => describe(`when dealing with ${role} role`, () => tests(roles[role]))) - - beforeEach(async () => { - lockedGold = await LockedGold.new() - mockGoldToken = await MockGoldToken.new() - mockGovernance = await MockGovernance.new() - mockValidators = await MockValidators.new() - registry = await Registry.new() - await registry.setAddressFor(CeloContractName.GoldToken, mockGoldToken.address) - await registry.setAddressFor(CeloContractName.Governance, mockGovernance.address) - await registry.setAddressFor(CeloContractName.Validators, mockValidators.address) - await lockedGold.initialize(registry.address, maxNoticePeriod) - await lockedGold.createAccount() - }) - - describe('#isAccount()', () => { - it('created account should exist', async () => { - const b = await lockedGold.isAccount(account) - assert.equal(b, true) - }) - it('account that was not created should not exist', async () => { - const b = await lockedGold.isAccount(accounts[2]) - assert.equal(b, false) - }) - }) - - describe('#isDelegate()', () => { - const delegate = accounts[1] - - beforeEach(async () => { - const sig = await getParsedSignatureOfAddress(web3, account, delegate) - await lockedGold.delegateRole(roles.voting, delegate, sig.v, sig.r, sig.s) - }) - - it('should return true for delegate', async () => { - assert.equal(await lockedGold.isDelegate(delegate), true) - }) - it('should return false for account', async () => { - assert.equal(await lockedGold.isDelegate(account), false) - }) - it('should return false for others', async () => { - assert.equal(await lockedGold.isDelegate(accounts[4]), false) - }) - }) - - describe('#initialize()', () => { - it('should set the owner', async () => { - const owner: string = await lockedGold.owner() - assert.equal(owner, account) - }) - - it('should set the maxNoticePeriod', async () => { - const actual = await lockedGold.maxNoticePeriod() - assert.equal(actual.toNumber(), maxNoticePeriod) - }) - - it('should set the registry address', async () => { - const registryAddress: string = await lockedGold.registry() - assert.equal(registryAddress, registry.address) - }) - - it('should revert if already initialized', async () => { - await assertRevert(lockedGold.initialize(registry.address, maxNoticePeriod)) - }) - }) - - describe('#setRegistry()', () => { - const anAddress: string = accounts[2] - - it('should set the registry when called by the owner', async () => { - await lockedGold.setRegistry(anAddress) - assert.equal(await lockedGold.registry(), anAddress) - }) - - it('should revert when not called by the owner', async () => { - await assertRevert(lockedGold.setRegistry(anAddress, { from: nonOwner })) - }) - }) - - describe('#setMaxNoticePeriod()', () => { - it('should set maxNoticePeriod when called by the owner', async () => { - await lockedGold.setMaxNoticePeriod(1) - assert.equal((await lockedGold.maxNoticePeriod()).toNumber(), 1) - }) - - it('should emit a MaxNoticePeriodSet event', async () => { - const resp = await lockedGold.setMaxNoticePeriod(1) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches(log, 'MaxNoticePeriodSet', { - maxNoticePeriod: new BigNumber(1), - }) - }) - - it('should revert when not called by the owner', async () => { - await assertRevert(lockedGold.setMaxNoticePeriod(1, { from: nonOwner })) - }) - }) - - describe('#delegateRole()', () => { - const delegate = accounts[1] - let sig - - beforeEach(async () => { - sig = await getParsedSignatureOfAddress(web3, account, delegate) - }) - - forEachRole((role) => { - it('should set the role delegate', async () => { - await lockedGold.delegateRole(role, delegate, sig.v, sig.r, sig.s) - assert.equal(await lockedGold.delegations(delegate), account) - assert.equal(await lockedGold.isDelegate(delegate), true) - assert.equal(await lockedGold.getDelegateFromAccountAndRole(account, role), delegate) - assert.equal(await lockedGold.getAccountFromDelegateAndRole(delegate, role), account) - }) - - it('should emit a RoleDelegated event', async () => { - const resp = await lockedGold.delegateRole(role, delegate, sig.v, sig.r, sig.s) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches(log, 'RoleDelegated', { - role, - account, - delegate, - }) - }) - - it('should revert if the delegate is an account', async () => { - await lockedGold.createAccount({ from: delegate }) - await assertRevert(lockedGold.delegateRole(role, delegate, sig.v, sig.r, sig.s)) - }) - - it('should revert if the address is already being delegated to', async () => { - const otherAccount = accounts[2] - const otherSig = await getParsedSignatureOfAddress(web3, otherAccount, delegate) - await lockedGold.createAccount({ from: otherAccount }) - await lockedGold.delegateRole(role, delegate, otherSig.v, otherSig.r, otherSig.s, { - from: otherAccount, - }) - await assertRevert(lockedGold.delegateRole(role, delegate, sig.v, sig.r, sig.s)) - }) - - it('should revert if the signature is incorrect', async () => { - const nonDelegate = accounts[3] - const incorrectSig = await getParsedSignatureOfAddress(web3, account, nonDelegate) - await assertRevert( - lockedGold.delegateRole(role, delegate, incorrectSig.v, incorrectSig.r, incorrectSig.s) - ) - }) - - describe('when a previous delegation has been made', async () => { - const newDelegate = accounts[2] - let newSig - beforeEach(async () => { - await lockedGold.delegateRole(role, delegate, sig.v, sig.r, sig.s) - newSig = await getParsedSignatureOfAddress(web3, account, newDelegate) - }) - - it('should set the new delegate', async () => { - await lockedGold.delegateRole(role, newDelegate, newSig.v, newSig.r, newSig.s) - assert.equal(await lockedGold.delegations(newDelegate), account) - assert.equal(await lockedGold.getDelegateFromAccountAndRole(account, role), newDelegate) - assert.equal(await lockedGold.getAccountFromDelegateAndRole(newDelegate, role), account) - }) - - it('should reset the previous delegate', async () => { - await lockedGold.delegateRole(role, newDelegate, newSig.v, newSig.r, newSig.s) - assert.equal(await lockedGold.delegations(delegate), NULL_ADDRESS) - }) - }) - }) - }) - - describe('#freezeVoting()', () => { - it('should set the account voting to frozen', async () => { - await lockedGold.freezeVoting() - assert.isTrue(await lockedGold.isVotingFrozen(account)) - }) - - it('should emit a VotingFrozen event', async () => { - const resp = await lockedGold.freezeVoting() - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches(log, 'VotingFrozen', { - account, - }) - }) - - it('should revert if the account voting is already frozen', async () => { - await lockedGold.freezeVoting() - await assertRevert(lockedGold.freezeVoting()) - }) - }) - - describe('#unfreezeVoting()', () => { - beforeEach(async () => { - await lockedGold.freezeVoting() - }) - - it('should set the account voting to unfrozen', async () => { - await lockedGold.unfreezeVoting() - assert.isFalse(await lockedGold.isVotingFrozen(account)) - }) - - it('should emit a VotingUnfrozen event', async () => { - const resp = await lockedGold.unfreezeVoting() - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches(log, 'VotingUnfrozen', { - account, - }) - }) - - it('should revert if the account voting is already unfrozen', async () => { - await lockedGold.unfreezeVoting() - await assertRevert(lockedGold.unfreezeVoting()) - }) - }) - - describe('#newCommitment()', () => { - const noticePeriod = 1 * DAY + 1 * HOUR - const value = 1000 - const expectedWeight = 1033 - - it('should add a Locked Gold commitment', async () => { - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - await lockedGold.newCommitment(noticePeriod, { value }) - const noticePeriods = await lockedGold.getNoticePeriods(account) - assert.equal(noticePeriods.length, 1) - assert.equal(noticePeriods[0].toNumber(), noticePeriod) - const [lockedValue, index] = await lockedGold.getLockedCommitment(account, noticePeriod) - assert.equal(lockedValue.toNumber(), value) - assert.equal(index.toNumber(), 0) - }) - - it('should update the account weight', async () => { - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - await lockedGold.newCommitment(noticePeriod, { value }) - const weight = await lockedGold.getAccountWeight(account) - assert.equal(weight.toNumber(), expectedWeight) - }) - - it('should update the total weight', async () => { - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - await lockedGold.newCommitment(noticePeriod, { value }) - const totalWeight = await lockedGold.totalWeight() - assert.equal(totalWeight.toNumber(), expectedWeight) - }) - - it('should emit a NewCommitment event', async () => { - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - const resp = await lockedGold.newCommitment(noticePeriod, { value }) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches(log, 'NewCommitment', { - account, - value: new BigNumber(value), - noticePeriod: new BigNumber(noticePeriod), - }) - }) - - it('should revert when the specified notice period is too large', async () => { - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - await assertRevert(lockedGold.newCommitment(maxNoticePeriod + 1, { value })) - }) - - it('should revert when the specified value is 0', async () => { - await assertRevert(lockedGold.newCommitment(noticePeriod)) - }) - - it('should revert when the account does not exist', async () => { - await assertRevert(lockedGold.newCommitment(noticePeriod, { value, from: accounts[1] })) - }) - - it('should revert if the caller is voting', async () => { - await mockGovernance.setVoting(account) - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - await assertRevert(lockedGold.newCommitment(noticePeriod, { value })) - }) - }) - - describe('#notifyCommitment()', () => { - const noticePeriod = 60 * 60 * 24 // 1 day - const value = 1000 - beforeEach(async () => { - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - await lockedGold.newCommitment(noticePeriod, { value }) - }) - - it('should add a notified deposit', async () => { - await lockedGold.notifyCommitment(value, noticePeriod) - const availabilityTime = new BigNumber(noticePeriod).plus( - (await web3.eth.getBlock('latest')).timestamp - ) - const availabilityTimes = await lockedGold.getAvailabilityTimes(account) - assert.equal(availabilityTimes.length, 1) - assert.equal(availabilityTimes[0].toNumber(), availabilityTime.toNumber()) - - const [notifiedValue, index] = await lockedGold.getNotifiedCommitment( - account, - availabilityTime - ) - assert.equal(notifiedValue.toNumber(), value) - assert.equal(index.toNumber(), 0) - }) - - it('should remove the Locked Gold commitment', async () => { - await lockedGold.notifyCommitment(value, noticePeriod) - const noticePeriods = await lockedGold.getNoticePeriods(account) - assert.equal(noticePeriods.length, 0) - const [lockedValue, index] = await lockedGold.getLockedCommitment(account, noticePeriod) - assert.equal(lockedValue.toNumber(), 0) - assert.equal(index.toNumber(), 0) - }) - - it('should update the account weight', async () => { - await lockedGold.notifyCommitment(value, noticePeriod) - const weight = await lockedGold.getAccountWeight(account) - assert.equal(weight.toNumber(), value) - }) - - it('should update the total weight', async () => { - await lockedGold.notifyCommitment(value, noticePeriod) - const totalWeight = await lockedGold.totalWeight() - assert.equal(totalWeight.toNumber(), value) - }) - - it('should emit a CommitmentNotified event', async () => { - const resp = await lockedGold.notifyCommitment(value, noticePeriod) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches(log, 'CommitmentNotified', { - account, - value: new BigNumber(value), - noticePeriod: new BigNumber(noticePeriod), - availabilityTime: new BigNumber(noticePeriod).plus( - (await web3.eth.getBlock('latest')).timestamp - ), - }) - }) - - it('should revert when the value of the Locked Gold commitment is 0', async () => { - await assertRevert(lockedGold.notifyCommitment(1, noticePeriod + 1)) - }) - - it('should revert when value is greater than the value of the Locked Gold commitment', async () => { - await assertRevert(lockedGold.notifyCommitment(value + 1, noticePeriod)) - }) - - it('should revert when the value is 0', async () => { - await assertRevert(lockedGold.notifyCommitment(0, noticePeriod)) - }) - - it('should revert if the account is validating', async () => { - await mockValidators.setValidating(account) - await assertRevert(lockedGold.notifyCommitment(value, noticePeriod)) - }) - - it('should revert if the caller is voting', async () => { - await mockGovernance.setVoting(account) - await assertRevert(lockedGold.notifyCommitment(value, noticePeriod)) - }) - }) - - describe('#extendCommitment()', () => { - const value = 1000 - const expectedWeight = 1033 - let availabilityTime: BigNumber - - beforeEach(async () => { - // Set an initial notice period of just over one day, so that when we rebond, we're - // guaranteed that the new notice period is at least one day. - const noticePeriod = 1 * DAY + 1 * HOUR - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - await lockedGold.newCommitment(noticePeriod, { value }) - await lockedGold.notifyCommitment(value, noticePeriod) - availabilityTime = new BigNumber(noticePeriod).plus( - (await web3.eth.getBlock('latest')).timestamp - ) - }) - - it('should add a Locked Gold commitment', async () => { - await lockedGold.extendCommitment(value, availabilityTime) - const noticePeriods = await lockedGold.getNoticePeriods(account) - assert.equal(noticePeriods.length, 1) - const noticePeriod = availabilityTime - .minus((await web3.eth.getBlock('latest')).timestamp) - .toNumber() - assert.equal(noticePeriods[0].toNumber(), noticePeriod) - const [lockedValue, index] = await lockedGold.getLockedCommitment(account, noticePeriod) - assert.equal(lockedValue.toNumber(), value) - assert.equal(index.toNumber(), 0) - }) - - it('should remove a notified deposit', async () => { - await lockedGold.extendCommitment(value, availabilityTime) - const availabilityTimes = await lockedGold.getAvailabilityTimes(account) - assert.equal(availabilityTimes.length, 0) - const [notifiedValue, index] = await lockedGold.getNotifiedCommitment( - account, - availabilityTime - ) - assert.equal(notifiedValue.toNumber(), 0) - assert.equal(index.toNumber(), 0) - }) - - it('should update the account weight', async () => { - await lockedGold.extendCommitment(value, availabilityTime) - const weight = await lockedGold.getAccountWeight(account) - assert.equal(weight.toNumber(), expectedWeight) - }) - - it('should update the total weight', async () => { - await lockedGold.extendCommitment(value, availabilityTime) - const totalWeight = await lockedGold.totalWeight() - assert.equal(totalWeight.toNumber(), expectedWeight) - }) - - it('should emit a CommitmentExtended event', async () => { - const resp = await lockedGold.extendCommitment(value, availabilityTime) - const noticePeriod = availabilityTime.minus((await web3.eth.getBlock('latest')).timestamp) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches(log, 'CommitmentExtended', { - account, - value: new BigNumber(value), - noticePeriod, - availabilityTime, - }) - }) - - it('should revert when the notified deposit is withdrawable', async () => { - await timeTravel( - availabilityTime - .minus((await web3.eth.getBlock('latest')).timestamp) - .plus(1) - .toNumber(), - web3 - ) - await assertRevert(lockedGold.extendCommitment(value, availabilityTime)) - }) - - it('should revert when the value of the notified deposit is 0', async () => { - await assertRevert(lockedGold.extendCommitment(value, availabilityTime.plus(1))) - }) - - it('should revert when the value is 0', async () => { - await assertRevert(lockedGold.extendCommitment(0, availabilityTime)) - }) - - it('should revert if the caller is voting', async () => { - await mockGovernance.setVoting(account) - await assertRevert(lockedGold.extendCommitment(value, availabilityTime)) - }) - }) - - describe('#withdrawCommitment()', () => { - const noticePeriod = 1 * DAY - const value = 1000 - let availabilityTime: BigNumber - - beforeEach(async () => { - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - await lockedGold.newCommitment(noticePeriod, { value }) - await lockedGold.notifyCommitment(value, noticePeriod) - availabilityTime = new BigNumber(noticePeriod).plus( - (await web3.eth.getBlock('latest')).timestamp - ) - }) - - it('should remove the notified deposit', async () => { - await timeTravel(noticePeriod, web3) - await lockedGold.withdrawCommitment(availabilityTime) - - const availabilityTimes = await lockedGold.getAvailabilityTimes(account) - assert.equal(availabilityTimes.length, 0) - }) - - it('should update the account weight', async () => { - await timeTravel(noticePeriod, web3) - await lockedGold.withdrawCommitment(availabilityTime) - - const weight = await lockedGold.getAccountWeight(account) - assert.equal(weight.toNumber(), 0) - }) - - it('should update the total weight', async () => { - await timeTravel(noticePeriod, web3) - await lockedGold.withdrawCommitment(availabilityTime) - - const totalWeight = await lockedGold.totalWeight() - assert.equal(totalWeight.toNumber(), 0) - }) - - it('should emit a Withdrawal event', async () => { - await timeTravel(noticePeriod, web3) - const resp = await lockedGold.withdrawCommitment(availabilityTime) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches(log, 'Withdrawal', { - account, - value: new BigNumber(value), - }) - }) - - it('should revert if the account is validating', async () => { - await mockValidators.setValidating(account) - await assertRevert(lockedGold.withdrawCommitment(availabilityTime)) - }) - - it('should revert when the notice period has not passed', async () => { - await assertRevert(lockedGold.withdrawCommitment(availabilityTime)) - }) - - it('should revert when the value of the notified deposit is 0', async () => { - await timeTravel(noticePeriod, web3) - await assertRevert(lockedGold.withdrawCommitment(availabilityTime.plus(1))) - }) - - it('should revert if the caller is voting', async () => { - await timeTravel(noticePeriod, web3) - await mockGovernance.setVoting(account) - await assertRevert(lockedGold.withdrawCommitment(availabilityTime)) - }) - }) - - describe('#increaseNoticePeriod()', () => { - const noticePeriod = 1 * DAY - const value = 1000 - const increase = noticePeriod - const expectedWeight = 1047 - - beforeEach(async () => { - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - await lockedGold.newCommitment(noticePeriod, { value }) - }) - - it('should update the Locked Gold commitment', async () => { - await lockedGold.increaseNoticePeriod(value, noticePeriod, increase) - const noticePeriods = await lockedGold.getNoticePeriods(account) - assert.equal(noticePeriods.length, 1) - assert.equal(noticePeriods[0].toNumber(), noticePeriod + increase) - const [lockedValue, index] = await lockedGold.getLockedCommitment( - account, - noticePeriod + increase - ) - assert.equal(lockedValue.toNumber(), value) - assert.equal(index.toNumber(), 0) - }) - - it('should update the account weight', async () => { - await lockedGold.increaseNoticePeriod(value, noticePeriod, increase) - const weight = await lockedGold.getAccountWeight(account) - assert.equal(weight.toNumber(), expectedWeight) - }) - - it('should update the total weight', async () => { - await lockedGold.increaseNoticePeriod(value, noticePeriod, increase) - const totalWeight = await lockedGold.totalWeight() - assert.equal(totalWeight.toNumber(), expectedWeight) - }) - - it('should emit a NoticePeriodIncreased event', async () => { - const resp = await lockedGold.increaseNoticePeriod(value, noticePeriod, increase) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches(log, 'NoticePeriodIncreased', { - account, - value: new BigNumber(value), - noticePeriod: new BigNumber(noticePeriod), - increase: new BigNumber(increase), - }) - }) - - it('should revert if the value is 0', async () => { - await assertRevert(lockedGold.increaseNoticePeriod(0, noticePeriod, increase)) - }) - - it('should revert if the increase is 0', async () => { - await assertRevert(lockedGold.increaseNoticePeriod(value, noticePeriod, 0)) - }) - - it('should revert if the Locked Gold commitment is smaller than the value', async () => { - await assertRevert(lockedGold.increaseNoticePeriod(value, noticePeriod + 1, increase)) - }) - - it('should revert if the caller is voting', async () => { - await mockGovernance.setVoting(account) - await assertRevert(lockedGold.increaseNoticePeriod(value, noticePeriod, increase)) - }) - }) - - describe('#getAccountFromDelegateAndRole()', () => { - forEachRole((role) => { - describe('when the account is not delegating', () => { - it('should return the account when passed the account', async () => { - assert.equal(await lockedGold.getAccountFromDelegateAndRole(account, role), account) - }) - - it('should revert when passed a delegate that is not the role delegate', async () => { - const delegate = accounts[2] - const diffRole = (role + 1) % 3 - const sig = await getParsedSignatureOfAddress(web3, account, delegate) - await lockedGold.delegateRole(role, delegate, sig.v, sig.r, sig.s) - await assertRevert(lockedGold.getAccountFromDelegateAndRole(delegate, diffRole)) - }) - }) - - describe('when the account is delegating', () => { - const delegate = accounts[1] - - beforeEach(async () => { - const sig = await getParsedSignatureOfAddress(web3, account, delegate) - await lockedGold.delegateRole(role, delegate, sig.v, sig.r, sig.s) - }) - - it('should return the account when passed the delegate', async () => { - assert.equal(await lockedGold.getAccountFromDelegateAndRole(delegate, role), account) - }) - - it('should return the account when passed the account', async () => { - assert.equal(await lockedGold.getAccountFromDelegateAndRole(account, role), account) - }) - - it('should revert when passed a delegate that is not the role delegate', async () => { - const delegate = accounts[2] - const diffRole = (role + 1) % 3 - const sig = await getParsedSignatureOfAddress(web3, account, delegate) - await lockedGold.delegateRole(role, delegate, sig.v, sig.r, sig.s) - await assertRevert(lockedGold.getAccountFromDelegateAndRole(delegate, diffRole)) - }) - }) - }) - }) - - describe('#getDelegateFromAccountAndRole()', () => { - forEachRole((role) => { - describe('when the account is not delegating', () => { - it('should return the account when passed the account', async () => { - assert.equal(await lockedGold.getDelegateFromAccountAndRole(account, role), account) - }) - }) - - describe('when the account is delegating', () => { - const delegate = accounts[1] - - beforeEach(async () => { - const sig = await getParsedSignatureOfAddress(web3, account, delegate) - await lockedGold.delegateRole(role, delegate, sig.v, sig.r, sig.s) - }) - - it('should return the account when passed undelegated role', async () => { - const role2 = (role + 1) % 3 - assert.equal(await lockedGold.getDelegateFromAccountAndRole(account, role2), account) - }) - - it('should return the delegate when passed the delegated role', async () => { - assert.equal(await lockedGold.getDelegateFromAccountAndRole(account, role), delegate) - }) - }) - }) - }) - - describe('#isVoting()', () => { - describe('when the account is not delegating', () => { - it('should return false if the account is not voting in governance or validator elections', async () => { - assert.isFalse(await lockedGold.isVoting(account)) - }) - - it('should return true if the account is voting in governance', async () => { - await mockGovernance.setVoting(account) - assert.isTrue(await lockedGold.isVoting(account)) - }) - - it('should return true if the account is voting in validator elections', async () => { - await mockValidators.setVoting(account) - assert.isTrue(await lockedGold.isVoting(account)) - }) - - it('should return true if the account is voting in governance and validator elections', async () => { - await mockGovernance.setVoting(account) - await mockValidators.setVoting(account) - assert.isTrue(await lockedGold.isVoting(account)) - }) - }) - - describe('when the account is delegating', () => { - const delegate = accounts[1] - - beforeEach(async () => { - const sig = await getParsedSignatureOfAddress(web3, account, delegate) - await lockedGold.delegateRole(roles.voting, delegate, sig.v, sig.r, sig.s) - }) - - it('should return false if the delegate is not voting in governance or validator elections', async () => { - assert.isFalse(await lockedGold.isVoting(account)) - }) - - it('should return true if the delegate is voting in governance', async () => { - await mockGovernance.setVoting(delegate) - assert.isTrue(await lockedGold.isVoting(account)) - }) - - it('should return true if the delegate is voting in validator elections', async () => { - await mockValidators.setVoting(delegate) - assert.isTrue(await lockedGold.isVoting(account)) - }) - - it('should return true if the delegate is voting in governance and validator elections', async () => { - await mockGovernance.setVoting(delegate) - await mockValidators.setVoting(delegate) - assert.isTrue(await lockedGold.isVoting(account)) - }) - }) - }) - - describe('#getCommitmentWeight()', () => { - const value = new BigNumber(521000) - const oneDay = new BigNumber(DAY) - it('should return the commitment value when notice period is zero', async () => { - const noticePeriod = new BigNumber(0) - assertEqualBN(await lockedGold.getCommitmentWeight(value, noticePeriod), value) - }) - - it('should return the commitment value when notice period is less than one day', async () => { - const noticePeriod = oneDay.minus(1) - assertEqualBN(await lockedGold.getCommitmentWeight(value, noticePeriod), value) - }) - - it('should return the commitment value times 1.0333 when notice period is one day', async () => { - const noticePeriod = oneDay - assertEqualBN( - await lockedGold.getCommitmentWeight(value, noticePeriod), - value.times(1.0333).integerValue(BigNumber.ROUND_DOWN) - ) - }) - - it('should return the commitment value times 1.047 when notice period is two days', async () => { - const noticePeriod = oneDay.times(2) - assertEqualBN( - await lockedGold.getCommitmentWeight(value, noticePeriod), - value.times(1.047).integerValue(BigNumber.ROUND_DOWN) - ) - }) - - it('should return the commitment value times 1.1823 when notice period is 30 days', async () => { - const noticePeriod = oneDay.times(30) - assertEqualBN( - await lockedGold.getCommitmentWeight(value, noticePeriod), - value.times(1.1823).integerValue(BigNumber.ROUND_DOWN) - ) - }) - - it('should return the commitment value times 2.103 when notice period is 3 years', async () => { - const noticePeriod = oneDay.times(365).times(3) - assertEqualBN( - await lockedGold.getCommitmentWeight(value, noticePeriod), - value.times(2.103).integerValue(BigNumber.ROUND_DOWN) - ) - }) - }) - - describe('when there are multiple commitments, notifies, rebondings, notice period increases, and withdrawals', () => { - beforeEach(async () => { - for (const accountToCreate of accounts) { - // Account for `account` has already been created. - if (accountToCreate !== account) { - await lockedGold.createAccount({ from: accountToCreate }) - } - } - }) - - enum ActionType { - Deposit = 'Deposit', - Notify = 'Notify', - Increase = 'Increase', - Rebond = 'Rebond', - Withdraw = 'Withdraw', - } - - const initializeState = (numAccounts: number) => { - const locked: Map> = new Map() - const notified: Map> = new Map() - const noticePeriods: Map> = new Map() - const availabilityTimes: Map> = new Map() - const selectedAccounts = accounts.slice(0, numAccounts) - for (const acc of selectedAccounts) { - // Map keys, set elements appear not to be able to be BigNumbers, so we use strings instead. - locked.set(acc, new Map()) - notified.set(acc, new Map()) - noticePeriods.set(acc, new Set([])) - availabilityTimes.set(acc, new Set([])) - } - - return { locked, notified, noticePeriods, availabilityTimes, selectedAccounts } - } - - const rndElement = (elems: A[]) => { - return elems[ - Math.floor( - BigNumber.random() - .times(elems.length) - .toNumber() - ) - ] - } - const rndSetElement = (s: Set) => rndElement(Array.from(s)) - - const getOrElse = (map: Map, key: B, defaultValue: A) => - map.has(key) ? map.get(key) : defaultValue - - const executeActionsAndAssertState = async (numActions: number, numAccounts: number) => { - const { - selectedAccounts, - locked, - notified, - noticePeriods, - availabilityTimes, - } = initializeState(numAccounts) - - for (let i = 0; i < numActions; i++) { - const blockTime = 5 - await timeTravel(blockTime, web3) - account = rndElement(selectedAccounts) - - const accountLockedGold = locked.get(account) - const accountNotifiedCommitments = notified.get(account) - const accountNoticePeriods = noticePeriods.get(account) - const accountAvailabilityTimes = availabilityTimes.get(account) - - const getWithdrawableAvailabilityTimes = async (): Promise> => { - const nextTimestamp = new BigNumber((await web3.eth.getBlock('latest')).timestamp) - const items: string[] = Array.from(accountAvailabilityTimes) - return new Set(items.filter((x: string) => nextTimestamp.gt(x))) - } - - const getRebondableAvailabilityTimes = async (): Promise> => { - const nextTimestamp = new BigNumber((await web3.eth.getBlock('latest')).timestamp).plus( - blockTime - ) - const items: string[] = Array.from(accountAvailabilityTimes) - // Subtract one to cover edge case where block time is 6 seconds. - return new Set(items.filter((x: string) => nextTimestamp.plus(1).lt(x))) - } - - // Select random action type. - const actionTypeOptions = [ActionType.Deposit] - if (accountNoticePeriods.size > 0) { - actionTypeOptions.push(ActionType.Notify) - actionTypeOptions.push(ActionType.Increase) - } - const rebondableAvailabilityTimes = await getRebondableAvailabilityTimes() - if (rebondableAvailabilityTimes.size > 0) { - // Push twice to increase likelihood - actionTypeOptions.push(ActionType.Rebond) - actionTypeOptions.push(ActionType.Rebond) - } - const withdrawableAvailabilityTimes = await getWithdrawableAvailabilityTimes() - if (withdrawableAvailabilityTimes.size > 0) { - // Push twice to increase likelihood - actionTypeOptions.push(ActionType.Withdraw) - actionTypeOptions.push(ActionType.Withdraw) - } - const actionType = rndElement(actionTypeOptions) - - const getLockedCommitmentValue = (noticePeriod: string) => - getOrElse(accountLockedGold, noticePeriod, new BigNumber(0)) - const getNotifiedCommitmentValue = (availabilityTime: string) => - getOrElse(accountNotifiedCommitments, availabilityTime, new BigNumber(0)) - - const randomSometimesMaximumValue = (maximum: BigNumber) => { - assert.isFalse(maximum.eq(0)) - const random = BigNumber.random().toNumber() - if (random < 0.5) { - return maximum - } else { - return BigNumber.max( - BigNumber.random() - .times(maximum) - .integerValue(), - 1 - ) - } - } - - // Perform random action and update test implementation state. - if (actionType === ActionType.Deposit) { - const value = new BigNumber(web3.utils.randomHex(2)).toNumber() - // Notice period of at most 10 blocks. - const noticePeriod = BigNumber.random() - .times(10) - .times(blockTime) - .integerValue() - .valueOf() - await lockedGold.newCommitment(noticePeriod, { value, from: account }) - accountNoticePeriods.add(noticePeriod) - accountLockedGold.set(noticePeriod, getLockedCommitmentValue(noticePeriod).plus(value)) - } else if (actionType === ActionType.Notify || actionType === ActionType.Increase) { - const noticePeriod = rndSetElement(accountNoticePeriods) - const lockedDepositValue = getLockedCommitmentValue(noticePeriod) - const value = randomSometimesMaximumValue(lockedDepositValue) - - if (value.eq(lockedDepositValue)) { - accountLockedGold.delete(noticePeriod) - accountNoticePeriods.delete(noticePeriod) - } else { - accountLockedGold.set(noticePeriod, lockedDepositValue.minus(value)) - } - - if (actionType === ActionType.Notify) { - await lockedGold.notifyCommitment(value, noticePeriod, { from: account }) - const availabilityTime = new BigNumber(noticePeriod) - .plus((await web3.eth.getBlock('latest')).timestamp) - .valueOf() - accountAvailabilityTimes.add(availabilityTime) - accountNotifiedCommitments.set( - availabilityTime, - getNotifiedCommitmentValue(availabilityTime).plus(value) - ) - } else { - // Notice period increase of at most 10 blocks. - const increase = BigNumber.random() - .times(10) - .times(blockTime) - .integerValue() - .plus(1) - await lockedGold.increaseNoticePeriod(value, noticePeriod, increase, { - from: account, - }) - const increasedNoticePeriod = increase.plus(noticePeriod).valueOf() - accountNoticePeriods.add(increasedNoticePeriod) - accountLockedGold.set( - increasedNoticePeriod, - getLockedCommitmentValue(increasedNoticePeriod).plus(value) - ) - } - } else if (actionType === ActionType.Rebond) { - const availabilityTime = rndSetElement(rebondableAvailabilityTimes) - const notifiedDepositValue = getNotifiedCommitmentValue(availabilityTime) - const value = randomSometimesMaximumValue(notifiedDepositValue) - await lockedGold.extendCommitment(value, availabilityTime, { from: account }) - - if (value.eq(notifiedDepositValue)) { - accountNotifiedCommitments.delete(availabilityTime) - accountAvailabilityTimes.delete(availabilityTime) - } else { - accountNotifiedCommitments.set(availabilityTime, notifiedDepositValue.minus(value)) - } - const noticePeriod = new BigNumber(availabilityTime) - .minus((await web3.eth.getBlock('latest')).timestamp) - .valueOf() - accountLockedGold.set(noticePeriod, getLockedCommitmentValue(noticePeriod).plus(value)) - accountNoticePeriods.add(noticePeriod) - } else if (actionType === ActionType.Withdraw) { - const availabilityTime = rndSetElement(withdrawableAvailabilityTimes) - await lockedGold.withdrawCommitment(availabilityTime, { from: account }) - accountAvailabilityTimes.delete(availabilityTime) - accountNotifiedCommitments.delete(availabilityTime) - } else { - assert.isTrue(false) - } - - // Sanity check our test implementation. - selectedAccounts.forEach((acc) => { - if (locked.get(acc).size > 0) { - assert.hasAllKeys( - noticePeriods.get(acc), - Array.from(locked.get(acc).keys()), - `notice periods don\'t match for account: ${acc}` - ) - } - if (notified.get(acc).size > 0) { - assert.hasAllKeys( - availabilityTimes.get(acc), - Array.from(notified.get(acc).keys()), - `availability times don\'t match for account: ${acc}` - ) - } - }) - - // Test the contract state matches our test implementation. - let expectedTotalWeight = new BigNumber(0) - for (const acc of selectedAccounts) { - let expectedAccountWeight = new BigNumber(0) - const actualNoticePeriods = await lockedGold.getNoticePeriods(acc) - - assert.lengthOf(actualNoticePeriods, noticePeriods.get(acc).size) - for (let k = 0; k < actualNoticePeriods.length; k++) { - const noticePeriod = actualNoticePeriods[k] - assert.isTrue(noticePeriods.get(acc).has(noticePeriod.valueOf())) - const [actualValue, actualIndex] = await lockedGold.getLockedCommitment( - acc, - noticePeriod - ) - assertEqualBN(actualIndex, k) - const expectedValue = locked.get(acc).get(noticePeriod.valueOf()) - assertEqualBN(actualValue, expectedValue) - assertEqualBN(actualNoticePeriods[actualIndex.toNumber()], noticePeriod) - expectedAccountWeight = expectedAccountWeight.plus( - await lockedGold.getCommitmentWeight(expectedValue, noticePeriod) - ) - } - - const actualAvailabilityTimes = await lockedGold.getAvailabilityTimes(acc) - - assert.equal(actualAvailabilityTimes.length, availabilityTimes.get(acc).size) - for (let k = 0; k < actualAvailabilityTimes.length; k++) { - const availabilityTime = actualAvailabilityTimes[k] - assert.isTrue(availabilityTimes.get(acc).has(availabilityTime.valueOf())) - const [actualValue, actualIndex] = await lockedGold.getNotifiedCommitment( - acc, - availabilityTime - ) - assertEqualBN(actualIndex, k) - const expectedValue = notified.get(acc).get(availabilityTime.valueOf()) - assertEqualBN(actualValue, expectedValue) - assertEqualBN(actualAvailabilityTimes[actualIndex.toNumber()], availabilityTime) - expectedAccountWeight = expectedAccountWeight.plus(expectedValue) - } - assertEqualBN(await lockedGold.getAccountWeight(acc), expectedAccountWeight) - expectedTotalWeight = expectedTotalWeight.plus(expectedAccountWeight) - } - } - } - - it.skip('should match a simple typescript implementation', async () => { - const numActions = 100 - const numAccounts = 2 - await executeActionsAndAssertState(numActions, numAccounts) - }) - }) -}) diff --git a/packages/protocol/test/governance/lockedgold.ts b/packages/protocol/test/governance/lockedgold.ts index 31ec2fbd9ab..c16d476da5c 100644 --- a/packages/protocol/test/governance/lockedgold.ts +++ b/packages/protocol/test/governance/lockedgold.ts @@ -1,4 +1,5 @@ import { CeloContractName } from '@celo/protocol/lib/registry-utils' +import { getParsedSignatureOfAddress } from '@celo/protocol/lib/signing-utils' import { assertEqualBN, assertLogMatches, @@ -48,17 +49,6 @@ contract('LockedGold', (accounts: string[]) => { return s.charAt(0).toUpperCase() + s.slice(1) } - const getParsedSignatureOfAddress = async (address: string, signer: string) => { - // @ts-ignore - const hash = web3.utils.soliditySha3({ type: 'address', value: address }) - const signature = (await web3.eth.sign(hash, signer)).slice(2) - return { - r: `0x${signature.slice(0, 64)}`, - s: `0x${signature.slice(64, 128)}`, - v: web3.utils.hexToNumber(signature.slice(128, 130)) + 27, - } - } - beforeEach(async () => { const mockGoldToken: MockGoldTokenInstance = await MockGoldToken.new() lockedGold = await LockedGold.new() @@ -157,7 +147,7 @@ contract('LockedGold', (accounts: string[]) => { let sig beforeEach(async () => { - sig = await getParsedSignatureOfAddress(account, authorized) + sig = await getParsedSignatureOfAddress(web3, account, authorized) }) it(`should set the authorized ${key}`, async () => { @@ -183,7 +173,7 @@ contract('LockedGold', (accounts: string[]) => { it(`should revert if the ${key} is already authorized`, async () => { const otherAccount = accounts[2] - const otherSig = await getParsedSignatureOfAddress(otherAccount, authorized) + const otherSig = await getParsedSignatureOfAddress(web3, otherAccount, authorized) await lockedGold.createAccount({ from: otherAccount }) await authorizationTest.fn(authorized, otherSig.v, otherSig.r, otherSig.s, { from: otherAccount, @@ -193,7 +183,7 @@ contract('LockedGold', (accounts: string[]) => { it('should revert if the signature is incorrect', async () => { const nonVoter = accounts[3] - const incorrectSig = await getParsedSignatureOfAddress(account, nonVoter) + const incorrectSig = await getParsedSignatureOfAddress(web3, account, nonVoter) await assertRevert( authorizationTest.fn(authorized, incorrectSig.v, incorrectSig.r, incorrectSig.s) ) @@ -204,7 +194,7 @@ contract('LockedGold', (accounts: string[]) => { let newSig beforeEach(async () => { await authorizationTest.fn(authorized, sig.v, sig.r, sig.s) - newSig = await getParsedSignatureOfAddress(account, newAuthorized) + newSig = await getParsedSignatureOfAddress(web3, account, newAuthorized) await authorizationTest.fn(newAuthorized, newSig.v, newSig.r, newSig.s) }) @@ -234,7 +224,7 @@ contract('LockedGold', (accounts: string[]) => { describe(`when the account has authorized a ${key}`, () => { const authorized = accounts[1] beforeEach(async () => { - const sig = await getParsedSignatureOfAddress(account, authorized) + const sig = await getParsedSignatureOfAddress(web3, account, authorized) await authorizationTest.fn(authorized, sig.v, sig.r, sig.s) }) @@ -263,7 +253,7 @@ contract('LockedGold', (accounts: string[]) => { const authorized = accounts[1] beforeEach(async () => { - const sig = await getParsedSignatureOfAddress(account, authorized) + const sig = await getParsedSignatureOfAddress(web3, account, authorized) await authorizationTest.fn(authorized, sig.v, sig.r, sig.s) }) From 89364ab61f243c1ee4d5909193f81c1c37fae2a3 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Wed, 16 Oct 2019 12:50:27 -0700 Subject: [PATCH 068/149] Fix test --- packages/protocol/test/governance/governance.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/protocol/test/governance/governance.ts b/packages/protocol/test/governance/governance.ts index 757ebc5c10b..d4e11d534f7 100644 --- a/packages/protocol/test/governance/governance.ts +++ b/packages/protocol/test/governance/governance.ts @@ -1682,7 +1682,7 @@ contract('Governance', (accounts: string[]) => { await timeTravel(dequeueFrequency, web3) await governance.approve(proposalId, index) await timeTravel(approvalStageDuration, web3) - await mockLockedGold.setWeight(account, weight) + await mockLockedGold.setAccountTotalLockedGold(account, weight) await governance.vote(proposalId, index, value) await timeTravel(referendumStageDuration, web3) }) From 931ceba37134fe756fe20ef79b4fe0dd8ac6c999 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Thu, 17 Oct 2019 17:42:18 -0700 Subject: [PATCH 069/149] Address comments; remove url and authorizedBy.active, allow for variable membershiphistorylength --- .../src/cmds/deploy/initial/contracts.ts | 7 +- packages/cli/src/commands/validator/list.ts | 1 - .../cli/src/commands/validator/register.ts | 5 +- .../cli/src/commands/validatorgroup/list.ts | 1 - .../src/commands/validatorgroup/register.ts | 4 +- packages/contractkit/src/wrappers/Election.ts | 6 + .../contractkit/src/wrappers/Validators.ts | 15 +- .../contracts/governance/Election.sol | 3 + .../contracts/governance/LockedGold.sol | 54 +++--- .../contracts/governance/Validators.sol | 114 ++++++----- packages/protocol/lib/test-utils.ts | 5 + .../protocol/test/governance/validators.ts | 183 +++++++++++++----- 12 files changed, 253 insertions(+), 145 deletions(-) diff --git a/packages/celotool/src/cmds/deploy/initial/contracts.ts b/packages/celotool/src/cmds/deploy/initial/contracts.ts index 161ae6604d1..e104a972fce 100644 --- a/packages/celotool/src/cmds/deploy/initial/contracts.ts +++ b/packages/celotool/src/cmds/deploy/initial/contracts.ts @@ -113,7 +113,12 @@ export const handler = async (argv: InitialArgv) => { validatorKeys, }, stableToken: { - initialAccounts: getAddressesFor(AccountType.FAUCET, mnemonic, 2), + initialBalances: { + addresses: getAddressesFor(AccountType.FAUCET, mnemonic, 2), + values: getAddressesFor(AccountType.FAUCET, mnemonic, 2).map( + () => '60000000000000000000000' + ), // 60k Celo Dollars + }, }, }) diff --git a/packages/cli/src/commands/validator/list.ts b/packages/cli/src/commands/validator/list.ts index 2e4efa37e11..af742764809 100644 --- a/packages/cli/src/commands/validator/list.ts +++ b/packages/cli/src/commands/validator/list.ts @@ -21,7 +21,6 @@ export default class ValidatorList extends BaseCommand { cli.table(validatorList, { address: {}, name: {}, - url: {}, publicKey: {}, affiliation: {}, }) diff --git a/packages/cli/src/commands/validator/register.ts b/packages/cli/src/commands/validator/register.ts index ea44245a896..7c826f8fc5c 100644 --- a/packages/cli/src/commands/validator/register.ts +++ b/packages/cli/src/commands/validator/register.ts @@ -11,12 +11,11 @@ export default class ValidatorRegister extends BaseCommand { ...BaseCommand.flags, from: Flags.address({ required: true, description: 'Address for the Validator' }), name: flags.string({ required: true }), - url: flags.string({ required: true }), publicKey: Flags.publicKey({ required: true }), } static examples = [ - 'register --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --name myName --url "http://validator.com" --publicKey 0xc52f3fab06e22a54915a8765c4f6826090cfac5e40282b43844bf1c0df83aaa632e55b67869758f2291d1aabe0ebecc7cbf4236aaa45e3e0cfbf997eda082ae19d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d7405011220a66a6257562d0c26dabf64485a1d96bad27bb1c0fd6080a75b0ec9f75b50298a2a8e04b02b2688c8104fca61fb00', + 'register --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --name myName --publicKey 0xc52f3fab06e22a54915a8765c4f6826090cfac5e40282b43844bf1c0df83aaa632e55b67869758f2291d1aabe0ebecc7cbf4236aaa45e3e0cfbf997eda082ae19d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d7405011220a66a6257562d0c26dabf64485a1d96bad27bb1c0fd6080a75b0ec9f75b50298a2a8e04b02b2688c8104fca61fb00', ] async run() { const res = this.parse(ValidatorRegister) @@ -25,7 +24,7 @@ export default class ValidatorRegister extends BaseCommand { const attestations = await this.kit.contracts.getAttestations() await displaySendTx( 'registerValidator', - validators.registerValidator(res.flags.name, res.flags.url, res.flags.publicKey as any) + validators.registerValidator(res.flags.name, res.flags.publicKey as any) ) // register encryption key on attestations contract diff --git a/packages/cli/src/commands/validatorgroup/list.ts b/packages/cli/src/commands/validatorgroup/list.ts index 7743ab63f9a..096520b8ff9 100644 --- a/packages/cli/src/commands/validatorgroup/list.ts +++ b/packages/cli/src/commands/validatorgroup/list.ts @@ -21,7 +21,6 @@ export default class ValidatorGroupList extends BaseCommand { cli.table(vgroups, { address: {}, name: {}, - url: {}, commission: { get: (r) => r.commission.toFixed() }, members: { get: (r) => r.members.length }, }) diff --git a/packages/cli/src/commands/validatorgroup/register.ts b/packages/cli/src/commands/validatorgroup/register.ts index 365bc9cf7f6..090e0d8ba85 100644 --- a/packages/cli/src/commands/validatorgroup/register.ts +++ b/packages/cli/src/commands/validatorgroup/register.ts @@ -11,12 +11,11 @@ export default class ValidatorGroupRegister extends BaseCommand { ...BaseCommand.flags, from: Flags.address({ required: true, description: 'Address for the Validator Group' }), name: flags.string({ required: true }), - url: flags.string({ required: true }), commission: flags.string({ required: true }), } static examples = [ - 'register --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --name myName --url "http://vgroup.com" --commission 0.1', + 'register --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --name myName --commission 0.1', ] async run() { @@ -26,7 +25,6 @@ export default class ValidatorGroupRegister extends BaseCommand { const validators = await this.kit.contracts.getValidators() const tx = await validators.registerValidatorGroup( res.flags.name, - res.flags.url, new BigNumber(res.flags.commission) ) await displaySendTx('registerValidatorGroup', tx) diff --git a/packages/contractkit/src/wrappers/Election.ts b/packages/contractkit/src/wrappers/Election.ts index 30676f15c9a..c18385fcbfb 100644 --- a/packages/contractkit/src/wrappers/Election.ts +++ b/packages/contractkit/src/wrappers/Election.ts @@ -80,6 +80,12 @@ export class ElectionWrapper extends BaseWrapper { toNumber ) + /** + * Returns the total votes for `group` made by `account`. + * @param group The address of the validator group. + * @param account The address of the voting account. + * @return The total votes for `group` made by `account`. + */ getTotalVotesForGroup = proxyCall( this.contract.methods.getTotalVotesForGroup, undefined, diff --git a/packages/contractkit/src/wrappers/Validators.ts b/packages/contractkit/src/wrappers/Validators.ts index 5d0a506cb3d..8fefd681ac1 100644 --- a/packages/contractkit/src/wrappers/Validators.ts +++ b/packages/contractkit/src/wrappers/Validators.ts @@ -14,7 +14,6 @@ import { export interface Validator { address: Address name: string - url: string publicKey: string affiliation: string | null } @@ -22,7 +21,6 @@ export interface Validator { export interface ValidatorGroup { address: Address name: string - url: string members: Address[] commission: BigNumber } @@ -53,12 +51,11 @@ export class ValidatorsWrapper extends BaseWrapper { registerValidator = proxySend(this.kit, this.contract.methods.registerValidator) async registerValidatorGroup( name: string, - url: string, commission: BigNumber ): Promise> { return toTransactionObject( this.kit, - this.contract.methods.registerValidatorGroup(name, url, toFixed(commission).toFixed()) + this.contract.methods.registerValidatorGroup(name, toFixed(commission).toFixed()) ) } async addMember(member: string): Promise> { @@ -135,9 +132,8 @@ export class ValidatorsWrapper extends BaseWrapper { return { address, name: res[0], - url: res[1], - publicKey: res[2] as any, - affiliation: res[3], + publicKey: res[1] as any, + affiliation: res[2], } } @@ -195,9 +191,8 @@ export class ValidatorsWrapper extends BaseWrapper { return { address, name: res[0], - url: res[1], - members: res[2], - commission: fromFixed(new BigNumber(res[3])), + members: res[1], + commission: fromFixed(new BigNumber(res[2])), } } } diff --git a/packages/protocol/contracts/governance/Election.sol b/packages/protocol/contracts/governance/Election.sol index b675f1e991e..e810dd07329 100644 --- a/packages/protocol/contracts/governance/Election.sol +++ b/packages/protocol/contracts/governance/Election.sol @@ -484,6 +484,8 @@ contract Election is view returns (uint256) { + // The group must meet the balance requirements in order for their voters to receive epoch + // rewards. bool meetsBalanceRequirements = ( getLockedGold().getAccountTotalLockedGold(group) >= getValidators().getAccountBalanceRequirement(group) @@ -502,6 +504,7 @@ contract Election is * @param value The amount of rewards to distribute to voters for the group. * @param lesser The group receiving fewer votes than `group` after the rewards are added. * @param greater The group receiving more votes than `group` after the rewards are added. + * @dev Can only be called directly by the protocol. */ function distributeEpochRewards( address group, diff --git a/packages/protocol/contracts/governance/LockedGold.sol b/packages/protocol/contracts/governance/LockedGold.sol index 2bb13b2691c..ff713e45841 100644 --- a/packages/protocol/contracts/governance/LockedGold.sol +++ b/packages/protocol/contracts/governance/LockedGold.sol @@ -14,11 +14,6 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr using SafeMath for uint256; - struct AuthorizedBy { - address account; - bool active; - } - struct Authorizations { // The address that is authorized to vote on behalf of the account. // The account can vote as well, whether or not an authorized voter has been specified. @@ -59,7 +54,7 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr mapping(address => Account) private accounts; // Maps voting and validating keys to the account that provided the authorization. // Authorized addresses may not be reused. - mapping(address => AuthorizedBy) private authorizedBy; + mapping(address => address) private authorizedBy; uint256 public totalNonvoting; uint256 public unlockingPeriod; @@ -105,7 +100,7 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr nonReentrant { Account storage account = accounts[msg.sender]; - authorize(voter, account.authorizations.voting, v, r, s); + authorize(voter, v, r, s); account.authorizations.voting = voter; emit VoterAuthorized(msg.sender, voter); } @@ -128,7 +123,7 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr nonReentrant { Account storage account = accounts[msg.sender]; - authorize(validator, account.authorizations.validating, v, r, s); + authorize(validator, v, r, s); account.authorizations.validating = validator; emit ValidatorAuthorized(msg.sender, validator); } @@ -262,9 +257,10 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr * @return The associated account. */ function getAccountFromActiveVoter(address accountOrVoter) external view returns (address) { - AuthorizedBy memory ab = authorizedBy[accountOrVoter]; - if (ab.active && ab.account != address(0)) { - return ab.account; + address account = authorizedBy[accountOrVoter]; + if (account != address(0)) { + require(accounts[account].authorizations.voting == accountOrVoter); + return account; } else { require(isAccount(accountOrVoter)); return accountOrVoter; @@ -314,9 +310,10 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr * @return The associated account. */ function getAccountFromActiveValidator(address accountOrValidator) public view returns (address) { - AuthorizedBy memory ab = authorizedBy[accountOrValidator]; - if (ab.active && ab.account != address(0)) { - return ab.account; + address account = authorizedBy[accountOrValidator]; + if (account != address(0)) { + require(accounts[account].authorizations.validating == accountOrValidator); + return account; } else { require(isAccount(accountOrValidator)); return accountOrValidator; @@ -330,9 +327,9 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr * @return The associated account. */ function getAccountFromVoter(address accountOrVoter) public view returns (address) { - AuthorizedBy memory ab = authorizedBy[accountOrVoter]; - if (ab.account != address(0)) { - return ab.account; + address account = authorizedBy[accountOrVoter]; + if (account != address(0)) { + return account; } else { require(isAccount(accountOrVoter)); return accountOrVoter; @@ -346,9 +343,9 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr * @return The associated account. */ function getAccountFromValidator(address accountOrValidator) public view returns (address) { - AuthorizedBy memory ab = authorizedBy[accountOrValidator]; - if (ab.account != address(0)) { - return ab.account; + address account = authorizedBy[accountOrValidator]; + if (account != address(0)) { + return account; } else { require(isAccount(accountOrValidator)); return accountOrValidator; @@ -405,8 +402,7 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr /** * @notice Authorizes voting or validating power of `msg.sender`'s account to another address. - * @param current The address to authorize. - * @param previous The previous authorized address. + * @param authorized The address to authorize. * @param v The recovery id of the incoming ECDSA signature. * @param r Output value r of the ECDSA signature. * @param s Output value s of the ECDSA signature. @@ -414,21 +410,19 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr * @dev v, r, s constitute `current`'s signature on `msg.sender`. */ function authorize( - address current, - address previous, + address authorized, uint8 v, bytes32 r, bytes32 s ) private { - require(isAccount(msg.sender) && isNotAccount(current) && isNotAuthorized(current)); + require(isAccount(msg.sender) && isNotAccount(authorized) && isNotAuthorized(authorized)); address signer = Signatures.getSignerOfAddress(msg.sender, v, r, s); - require(signer == current); + require(signer == authorized); - authorizedBy[previous].active = false; - authorizedBy[current] = AuthorizedBy(msg.sender, true); + authorizedBy[authorized] = msg.sender; } /** @@ -455,7 +449,7 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr * @return Returns `true` if authorized. Returns `false` otherwise. */ function isAuthorized(address account) external view returns (bool) { - return (authorizedBy[account].account != address(0)); + return (authorizedBy[account] != address(0)); } /** @@ -464,7 +458,7 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr * @return Returns `false` if authorized. Returns `true` otherwise. */ function isNotAuthorized(address account) internal view returns (bool) { - return (authorizedBy[account].account == address(0)); + return (authorizedBy[account] == address(0)); } /** diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index 72aa8264be6..8c59a61e3e6 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -60,7 +60,6 @@ contract Validators is struct ValidatorGroup { string name; - string url; LinkedList.List members; // TODO(asa): Add a function that allows groups to update their commission. FixidityLib.Fraction commission; @@ -72,15 +71,17 @@ contract Validators is address group; } - // A circular buffer storing the membership history of a validator. + // Stores the membership history of a validator. struct MembershipHistory { - uint256 head; - MembershipHistoryEntry[] entries; + // The key to the most recent entry in the entries mapping. + uint256 tail; + // The number of entries in this validators membership history. + uint256 numEntries; + mapping(uint256 => MembershipHistoryEntry) entries; } struct Validator { string name; - string url; bytes publicKeysData; address affiliation; FixidityLib.Fraction score; @@ -105,7 +106,6 @@ contract Validators is uint256 public membershipHistoryLength; uint256 public maxGroupSize; - event Debug(uint256 value); event MaxGroupSizeSet( uint256 size ); @@ -124,6 +124,8 @@ contract Validators is uint256 validator ); + event MembershipHistoryLengthSet(uint256 length); + event DeregistrationLockupsSet( uint256 group, uint256 validator @@ -132,7 +134,6 @@ contract Validators is event ValidatorRegistered( address indexed validator, string name, - string url, bytes publicKeysData ); @@ -153,7 +154,7 @@ contract Validators is event ValidatorGroupRegistered( address indexed group, string name, - string url + uint256 commission ); event ValidatorGroupDeregistered( @@ -209,15 +210,14 @@ contract Validators is external initializer { - require(validatorScoreAdjustmentSpeed <= FixidityLib.fixed1().unwrap()); _transferOwnership(msg.sender); setRegistry(registryAddress); setValidatorEpochPayment(_validatorEpochPayment); - setValidatorScoreParameters(validatorScoreExponent, validatorScoreAdjustmentSpeed) + setValidatorScoreParameters(validatorScoreExponent, validatorScoreAdjustmentSpeed); setBalanceRequirements(groupRequirement, validatorRequirement); setDeregistrationLockups(groupLockup, validatorLockup); setMaxGroupSize(_maxGroupSize); - membershipHistoryLength = _membershipHistoryLength; + setMembershipHistoryLength(_membershipHistoryLength); } /** @@ -232,6 +232,18 @@ contract Validators is return true; } + /** + * @notice Updates the number of validator group membership entries to store. + * @param length The number of validator group membership entries to store. + * @return True upon success. + */ + function setMembershipHistoryLength(uint256 length) public onlyOwner returns (bool) { + require(0 < length && length != membershipHistoryLength); + membershipHistoryLength = length; + emit MembershipHistoryLengthSet(length); + return true; + } + /** * @notice Sets the per-epoch payment in Celo Dollars for validators, less group commission. * @param value The value in Celo Dollars. @@ -322,7 +334,6 @@ contract Validators is /** * @notice Registers a validator unaffiliated with any validator group. * @param name A name for the validator. - * @param url A URL for the validator. * @param publicKeysData Comprised of three tightly-packed elements: * - publicKey - The public key that the validator is using for consensus, should match * msg.sender. 64 bytes. @@ -335,7 +346,6 @@ contract Validators is */ function registerValidator( string calldata name, - string calldata url, bytes calldata publicKeysData ) external @@ -344,7 +354,6 @@ contract Validators is { require( bytes(name).length > 0 && - bytes(url).length > 0 && // secp256k1 public key + BLS public key + BLS proof of possession publicKeysData.length == (64 + 48 + 96) ); @@ -356,11 +365,10 @@ contract Validators is require(meetsValidatorBalanceRequirements(account)); validators[account].name = name; - validators[account].url = url; validators[account].publicKeysData = publicKeysData; _validators.push(account); updateMembershipHistory(account, address(0)); - emit ValidatorRegistered(account, name, url, publicKeysData); + emit ValidatorRegistered(account, name, publicKeysData); return true; } @@ -413,12 +421,13 @@ contract Validators is view returns (uint256[] memory, address[] memory) { - MembershipHistoryEntry[] memory entries = validators[account].membershipHistory.entries; - uint256[] memory epochs = new uint256[](entries.length); - address[] memory membershipGroups = new address[](entries.length); - for (uint256 i = 0; i < entries.length; i = i.add(1)) { - epochs[i] = entries[i].epochNumber; - membershipGroups[i] = entries[i].group; + MembershipHistory storage history = validators[account].membershipHistory; + uint256[] memory epochs = new uint256[](history.numEntries); + address[] memory membershipGroups = new address[](history.numEntries); + for (uint256 i = 0; i < history.numEntries; i = i.add(1)) { + uint256 index = history.tail.add(i); + epochs[i] = history.entries[index].epochNumber; + membershipGroups[i] = history.entries[index].group; } return (epochs, membershipGroups); } @@ -437,6 +446,7 @@ contract Validators is * @notice Updates a validator's score based on its uptime for the epoch. * @param validator The address of the validator. * @param uptime The Fixidity representation of the validator's uptime, between 0 and 1. + * @dev new_score = uptime ** exponent * adjustmentSpeed + old_score * (1 - adjustmentSpeed) * @return True upon success. */ function _updateValidatorScore(address validator, uint256 uptime) internal { @@ -562,7 +572,6 @@ contract Validators is /** * @notice Registers a validator group with no member validators. * @param name A name for the validator group. - * @param url A URL for the validator group. * @param commission Fixidity representation of the commission this group receives on epoch * payments made to its members. * @return True upon success. @@ -571,7 +580,6 @@ contract Validators is */ function registerValidatorGroup( string calldata name, - string calldata url, uint256 commission ) external @@ -579,7 +587,6 @@ contract Validators is returns (bool) { require(bytes(name).length > 0); - require(bytes(url).length > 0); require(commission <= FixidityLib.fixed1().unwrap(), "Commission can't be greater than 100%"); address account = getLockedGold().getAccountFromActiveValidator(msg.sender); require(!isValidator(account) && !isValidatorGroup(account)); @@ -587,10 +594,9 @@ contract Validators is ValidatorGroup storage group = groups[account]; group.name = name; - group.url = url; group.commission = FixidityLib.wrap(commission); _groups.push(account); - emit ValidatorGroupRegistered(account, name, url); + emit ValidatorGroupRegistered(account, name, commission); return true; } @@ -761,7 +767,6 @@ contract Validators is view returns ( string memory name, - string memory url, bytes memory publicKeysData, address affiliation, uint256 score @@ -771,7 +776,6 @@ contract Validators is Validator storage validator = validators[account]; return ( validator.name, - validator.url, validator.publicKeysData, validator.affiliation, validator.score.unwrap() @@ -788,11 +792,11 @@ contract Validators is ) external view - returns (string memory, string memory, address[] memory, uint256) + returns (string memory, address[] memory, uint256) { require(isValidatorGroup(account)); ValidatorGroup storage group = groups[account]; - return (group.name, group.url, group.members.getKeys(), group.commission.unwrap()); + return (group.name, group.members.getKeys(), group.commission.unwrap()); } /** @@ -941,6 +945,15 @@ contract Validators is } /** + * Tail: 0 + * numEntries: 0 + * index: 0 + * + * Tail: 0 + * numEntries: 1 + * index: 1 + * + * * @notice Updates the group membership history of a particular account. * @param account The account whose group membership has changed. * @param group The group that the account is now a member of. @@ -951,20 +964,31 @@ contract Validators is function updateMembershipHistory(address account, address group) private returns (bool) { MembershipHistory storage history = validators[account].membershipHistory; uint256 epochNumber = getEpochNumber(); - if (history.entries.length > 0 && epochNumber == history.entries[history.head].epochNumber) { + uint256 head = history.numEntries == 0 ? 0 : history.tail.add(history.numEntries.sub(1)); + + if (history.entries[head].epochNumber == epochNumber) { // There have been no elections since the validator last changed membership, overwrite the // previous entry. - history.entries[history.head] = MembershipHistoryEntry(epochNumber, group); + history.entries[head] = MembershipHistoryEntry(epochNumber, group); + return true; + } + + // There have been elections since the validator last changed membership, create a new entry. + uint256 index = history.numEntries == 0 ? 0 : head.add(1); + history.entries[index] = MembershipHistoryEntry(epochNumber, group); + if (history.numEntries < membershipHistoryLength) { + // Not enough entries, don't remove any. + history.numEntries = history.numEntries.add(1); + } else if (history.numEntries == membershipHistoryLength) { + // Exactly enough entries, delete the oldest one to account for the one we added. + delete history.entries[history.tail]; + history.tail = history.tail.add(1); } else { - if (history.entries.length > 0) { - // MembershipHistoryEntries are a circular buffer. - history.head = history.head.add(1) % membershipHistoryLength; - } - if (history.head >= history.entries.length) { - history.entries.push(MembershipHistoryEntry(epochNumber, group)); - } else { - history.entries[history.head] = MembershipHistoryEntry(epochNumber, group); - } + // Too many entries, delete the oldest two to account for the one we added. + delete history.entries[history.tail]; + delete history.entries[history.tail.add(1)]; + history.numEntries = history.numEntries.sub(1); + history.tail = history.tail.add(2); } } @@ -976,14 +1000,12 @@ contract Validators is function getMembershipInLastEpoch(address account) public view returns (address) { uint256 epochNumber = getEpochNumber(); MembershipHistory storage history = validators[account].membershipHistory; - uint256 head = history.head; + uint256 head = history.numEntries == 0 ? 0 : history.tail.add(history.numEntries.sub(1)); // If the most recent entry in the membership history is for the current epoch number, we need // to look at the previous entry. if (history.entries[head].epochNumber == epochNumber) { - if (head > 0) { + if (head > history.tail) { head = head.sub(1); - } else if (history.entries.length > 1) { - head = history.entries.length.sub(1); } } return history.entries[head].group; diff --git a/packages/protocol/lib/test-utils.ts b/packages/protocol/lib/test-utils.ts index 982ea4a2a22..2e9ff68d5d5 100644 --- a/packages/protocol/lib/test-utils.ts +++ b/packages/protocol/lib/test-utils.ts @@ -239,6 +239,11 @@ export function assertEqualBN( ) } +export function assertEqualBNArray(value: number[] | BN[] | BigNumber[], expected: number[] | BN[] | BigNumber[], msg?: string) { + assert.equal(value.length, expected.length, msg) + value.forEach((x, i) => assertEqualBN(x, expected[i])) +} + export function assertGteBN( value: number | BN | BigNumber, expected: number | BN | BigNumber, diff --git a/packages/protocol/test/governance/validators.ts b/packages/protocol/test/governance/validators.ts index d034dc03dd5..d587f0fe60e 100644 --- a/packages/protocol/test/governance/validators.ts +++ b/packages/protocol/test/governance/validators.ts @@ -2,6 +2,7 @@ import { CeloContractName } from '@celo/protocol/lib/registry-utils' import { assertContainSubset, assertEqualBN, + assertEqualBNArray, assertRevert, assertSameAddress, NULL_ADDRESS, @@ -35,19 +36,17 @@ Validators.numberFormat = 'BigNumber' const parseValidatorParams = (validatorParams: any) => { return { name: validatorParams[0], - url: validatorParams[1], - publicKeysData: validatorParams[2], - affiliation: validatorParams[3], - score: validatorParams[4], + publicKeysData: validatorParams[1], + affiliation: validatorParams[2], + score: validatorParams[3], } } const parseValidatorGroupParams = (groupParams: any) => { return { name: groupParams[0], - url: groupParams[1], - members: groupParams[2], - commission: groupParams[3], + members: groupParams[1], + commission: groupParams[2], } } @@ -86,7 +85,6 @@ contract('Validators', (accounts: string[]) => { '9d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d740501' const publicKeysData = '0x' + publicKey + blsPublicKey + blsPoP const name = 'test-name' - const url = 'test-url' const commission = toFixed(1 / 100) beforeEach(async () => { validators = await Validators.new() @@ -113,7 +111,6 @@ contract('Validators', (accounts: string[]) => { await mockLockedGold.setAccountTotalLockedGold(validator, balanceRequirements.validator) await validators.registerValidator( name, - url, // @ts-ignore bytes type publicKeysData, { from: validator } @@ -122,7 +119,7 @@ contract('Validators', (accounts: string[]) => { const registerValidatorGroup = async (group: string) => { await mockLockedGold.setAccountTotalLockedGold(group, balanceRequirements.group) - await validators.registerValidatorGroup(name, url, commission, { from: group }) + await validators.registerValidatorGroup(name, commission, { from: group }) } const registerValidatorGroupWithMembers = async (group: string, members: string[]) => { @@ -240,6 +237,51 @@ contract('Validators', (accounts: string[]) => { }) }) + describe('#setMembershipHistoryLength()', () => { + describe('when the length is different', () => { + const newLength = membershipHistoryLength.plus(1) + + describe('when called by the owner', () => { + let resp: any + + beforeEach(async () => { + resp = await validators.setMembershipHistoryLength(newLength) + }) + + it('should set the membership history length', async () => { + assertEqualBN(await validators.membershipHistoryLength(), newLength) + }) + + it('should emit the MembershipHistoryLengthSet event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'MembershipHistoryLengthSet', + args: { + length: new BigNumber(newLength), + }, + }) + }) + + describe('when called by a non-owner', () => { + it('should revert', async () => { + await assertRevert( + validators.setMembershipHistoryLength(newLength, { + from: nonOwner, + }) + ) + }) + }) + }) + + describe('when the length is the same', () => { + it('should revert', async () => { + await assertRevert(validators.setMembershipHistoryLength(membershipHistoryLength)) + }) + }) + }) + }) + describe('#setMaxGroupSize()', () => { describe('when the group size is different', () => { const newSize = maxGroupSize.plus(1) @@ -508,14 +550,16 @@ contract('Validators', (accounts: string[]) => { const validator = accounts[0] let resp: any describe('when the account is not a registered validator', () => { + let validatorRegistrationEpochNumber: number beforeEach(async () => { await mockLockedGold.setAccountTotalLockedGold(validator, balanceRequirements.validator) resp = await validators.registerValidator( name, - url, // @ts-ignore bytes type publicKeysData ) + const blockNumber = (await web3.eth.getBlock('latest')).number + validatorRegistrationEpochNumber = Math.floor(blockNumber / EPOCH) }) it('should mark the account as a validator', async () => { @@ -526,10 +570,9 @@ contract('Validators', (accounts: string[]) => { assert.deepEqual(await validators.getRegisteredValidators(), [validator]) }) - it('should set the validator name, url, and public key', async () => { + it('should set the validator name and public key', async () => { const parsedValidator = parseValidatorParams(await validators.getValidator(validator)) assert.equal(parsedValidator.name, name) - assert.equal(parsedValidator.url, url) assert.equal(parsedValidator.publicKeysData, publicKeysData) }) @@ -538,6 +581,12 @@ contract('Validators', (accounts: string[]) => { assertEqualBN(requirement, balanceRequirements.validator) }) + it('should set the validator membership history', async () => { + const membershipHistory = await validators.getMembershipHistory(validator) + assertEqualBNArray(membershipHistory[0], [validatorRegistrationEpochNumber]) + assert.deepEqual(membershipHistory[1], [NULL_ADDRESS]) + }) + it('should emit the ValidatorRegistered event', async () => { assert.equal(resp.logs.length, 1) const log = resp.logs[0] @@ -546,7 +595,6 @@ contract('Validators', (accounts: string[]) => { args: { validator, name, - url, publicKeysData, }, }) @@ -558,7 +606,6 @@ contract('Validators', (accounts: string[]) => { await mockLockedGold.setAccountTotalLockedGold(validator, balanceRequirements.validator) await validators.registerValidator( name, - url, // @ts-ignore bytes type publicKeysData ) @@ -569,14 +616,13 @@ contract('Validators', (accounts: string[]) => { describe('when the account is already a registered validator', () => { beforeEach(async () => { await mockLockedGold.setAccountTotalLockedGold(validator, balanceRequirements.group) - await validators.registerValidatorGroup(name, url, commission) + await validators.registerValidatorGroup(name, commission) }) it('should revert', async () => { await assertRevert( validators.registerValidator( name, - url, // @ts-ignore bytes type publicKeysData ) @@ -596,7 +642,6 @@ contract('Validators', (accounts: string[]) => { await assertRevert( validators.registerValidator( name, - url, // @ts-ignore bytes type publicKeysData ) @@ -908,7 +953,7 @@ contract('Validators', (accounts: string[]) => { describe('when the account is not a registered validator group', () => { beforeEach(async () => { await mockLockedGold.setAccountTotalLockedGold(group, balanceRequirements.group) - resp = await validators.registerValidatorGroup(name, url, commission) + resp = await validators.registerValidatorGroup(name, commission) }) it('should mark the account as a validator group', async () => { @@ -919,10 +964,10 @@ contract('Validators', (accounts: string[]) => { assert.deepEqual(await validators.getRegisteredValidatorGroups(), [group]) }) - it('should set the validator group name and url', async () => { + it('should set the validator group name and commission', async () => { const parsedGroup = parseValidatorGroupParams(await validators.getValidatorGroup(group)) assert.equal(parsedGroup.name, name) - assert.equal(parsedGroup.url, url) + assertEqualBN(parsedGroup.commission, commission) }) it('should set account balance requirements', async () => { @@ -938,7 +983,6 @@ contract('Validators', (accounts: string[]) => { args: { group, name, - url, }, }) }) @@ -950,18 +994,18 @@ contract('Validators', (accounts: string[]) => { }) it('should revert', async () => { - await assertRevert(validators.registerValidatorGroup(name, url, balanceRequirements.group)) + await assertRevert(validators.registerValidatorGroup(name, commission)) }) }) describe('when the account is already a registered validator group', () => { beforeEach(async () => { await mockLockedGold.setAccountTotalLockedGold(group, balanceRequirements.group) - await validators.registerValidatorGroup(name, url, commission) + await validators.registerValidatorGroup(name, commission) }) it('should revert', async () => { - await assertRevert(validators.registerValidatorGroup(name, url, commission)) + await assertRevert(validators.registerValidatorGroup(name, commission)) }) }) @@ -971,7 +1015,7 @@ contract('Validators', (accounts: string[]) => { }) it('should revert', async () => { - await assertRevert(validators.registerValidatorGroup(name, url, commission)) + await assertRevert(validators.registerValidatorGroup(name, commission)) }) }) }) @@ -1284,42 +1328,80 @@ contract('Validators', (accounts: string[]) => { describe('#updateMembershipHistory', () => { const validator = accounts[0] const groups = accounts.slice(1) + let validatorRegistrationEpochNumber: number beforeEach(async () => { await registerValidator(validator) + const blockNumber = (await web3.eth.getBlock('latest')).number + validatorRegistrationEpochNumber = Math.floor(blockNumber / EPOCH) for (const group of groups) { await registerValidatorGroup(group) } }) + describe('when changing groups in the same epoch', () => { + it('should overwrite the previous entry', async () => { + const numTests = 10 + // We store an entry upon registering the validator. + const expectedMembershipHistoryGroups = [NULL_ADDRESS] + const expectedMembershipHistoryEpochs = [new BigNumber(validatorRegistrationEpochNumber)] + for (let i = 0; i < numTests; i++) { + const blockNumber = (await web3.eth.getBlock('latest')).number + const epochNumber = Math.floor(blockNumber / EPOCH) + const blocksUntilNextEpoch = (epochNumber + 1) * EPOCH - blockNumber + await mineBlocks(blocksUntilNextEpoch, web3) + + let group = groups[0] + await validators.affiliate(group) + await validators.addFirstMember(validator, NULL_ADDRESS, NULL_ADDRESS, { + from: group, + }) + let membershipHistory = await validators.getMembershipHistory(validator) + expectedMembershipHistoryGroups.push(group) + expectedMembershipHistoryEpochs.push(new BigNumber(epochNumber + 1)) + if (expectedMembershipHistoryGroups.length > membershipHistoryLength.toNumber()) { + expectedMembershipHistoryGroups.shift() + expectedMembershipHistoryEpochs.shift() + } + assertEqualBNArray(membershipHistory[0], expectedMembershipHistoryEpochs) + assert.deepEqual(membershipHistory[1], expectedMembershipHistoryGroups) + + group = groups[1] + await validators.affiliate(group) + await validators.addFirstMember(validator, NULL_ADDRESS, NULL_ADDRESS, { + from: group, + }) + membershipHistory = await validators.getMembershipHistory(validator) + expectedMembershipHistoryGroups[expectedMembershipHistoryGroups.length - 1] = group + assertEqualBNArray(membershipHistory[0], expectedMembershipHistoryEpochs) + assert.deepEqual(membershipHistory[1], expectedMembershipHistoryGroups) + } + }) + }) + describe('when changing groups more times than membership history length', () => { it('should always store the most recent memberships', async () => { + // We store an entry upon registering the validator. + const expectedMembershipHistoryGroups = [NULL_ADDRESS] + const expectedMembershipHistoryEpochs = [new BigNumber(validatorRegistrationEpochNumber)] for (let i = 0; i < membershipHistoryLength.plus(1).toNumber(); i++) { + const blockNumber = (await web3.eth.getBlock('latest')).number + const epochNumber = Math.floor(blockNumber / EPOCH) + const blocksUntilNextEpoch = (epochNumber + 1) * EPOCH - blockNumber + await mineBlocks(blocksUntilNextEpoch, web3) + await validators.affiliate(groups[i]) - const currentEpoch = new BigNumber( - Math.floor((await web3.eth.getBlock('latest')).number / EPOCH) - ) await validators.addFirstMember(validator, NULL_ADDRESS, NULL_ADDRESS, { from: groups[i], }) - await mineBlocks(EPOCH, web3) - - const membershipHistory = await validators.getMembershipHistory(validator) - const expectedMembershipHistoryLength = Math.min( - i + 1, - membershipHistoryLength.toNumber() - ) - assert.equal(membershipHistory[0].length, expectedMembershipHistoryLength) - assert.equal(membershipHistory[1].length, expectedMembershipHistoryLength) - for (let j = 0; j < expectedMembershipHistoryLength; j++) { - assert.include( - membershipHistory[0].map((x) => x.toNumber()), - currentEpoch.minus(j).toNumber() - ) - assert.include( - membershipHistory[1].map((x) => x.toLowerCase()), - groups[i - j].toLowerCase() - ) + expectedMembershipHistoryGroups.push(groups[i]) + expectedMembershipHistoryEpochs.push(new BigNumber(epochNumber + 1)) + if (expectedMembershipHistoryGroups.length > membershipHistoryLength.toNumber()) { + expectedMembershipHistoryGroups.shift() + expectedMembershipHistoryEpochs.shift() } + const membershipHistory = await validators.getMembershipHistory(validator) + assertEqualBNArray(membershipHistory[0], expectedMembershipHistoryEpochs) + assert.deepEqual(membershipHistory[1], expectedMembershipHistoryGroups) } }) }) @@ -1339,7 +1421,8 @@ contract('Validators', (accounts: string[]) => { it('should always return the correct membership for the last epoch', async () => { for (let i = 0; i < membershipHistoryLength.plus(1).toNumber(); i++) { const blockNumber = (await web3.eth.getBlock('latest')).number - const blocksUntilNextEpoch = blockNumber % EPOCH + const epochNumber = Math.floor(blockNumber / EPOCH) + const blocksUntilNextEpoch = (epochNumber + 1) * EPOCH - blockNumber await mineBlocks(blocksUntilNextEpoch, web3) await validators.affiliate(groups[i]) @@ -1363,7 +1446,7 @@ contract('Validators', (accounts: string[]) => { }) }) - describe.only('#distributeEpochPayment', () => { + describe('#distributeEpochPayment', () => { const validator = accounts[0] const group = accounts[1] let mockStableToken: MockStableTokenInstance @@ -1419,7 +1502,7 @@ contract('Validators', (accounts: string[]) => { }) }) - describe('when the validator does not meet the balance requirements', () => { + describe('when the group does not meet the balance requirements', () => { beforeEach(async () => { await mockLockedGold.setAccountTotalLockedGold(group, balanceRequirements.group.minus(1)) await validators.distributeEpochPayment(validator) From 92e254e99dff660769d9ec50a3320af582407209 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Thu, 17 Oct 2019 17:52:53 -0700 Subject: [PATCH 070/149] Fix build --- packages/contractkit/src/wrappers/Validators.test.ts | 8 ++++---- packages/contractkit/src/wrappers/Validators.ts | 7 +++++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/contractkit/src/wrappers/Validators.test.ts b/packages/contractkit/src/wrappers/Validators.test.ts index 1d54ed80bf8..25e91c4ce79 100644 --- a/packages/contractkit/src/wrappers/Validators.test.ts +++ b/packages/contractkit/src/wrappers/Validators.test.ts @@ -45,7 +45,6 @@ testWithGanache('Validators Wrapper', (web3) => { await registerAccountWithLockedGold(groupAccount) await (await validators.registerValidatorGroup( 'The Group', - 'thegroup.com', new BigNumber(0.1) )).sendAndWaitForReceipt({ from: groupAccount }) } @@ -56,7 +55,6 @@ testWithGanache('Validators Wrapper', (web3) => { await validators .registerValidator( 'Good old validator', - 'goodold.com', // @ts-ignore publicKeysData ) @@ -81,7 +79,9 @@ testWithGanache('Validators Wrapper', (web3) => { await setupGroup(groupAccount) await setupValidator(validatorAccount) await validators.affiliate(groupAccount).sendAndWaitForReceipt({ from: validatorAccount }) - await validators.addMember(validatorAccount).sendAndWaitForReceipt({ from: groupAccount }) + await (await validators.addMember(validatorAccount)).sendAndWaitForReceipt({ + from: groupAccount, + }) const members = await validators.getValidatorGroup(groupAccount).then((group) => group.members) expect(members).toContain(validatorAccount) @@ -100,7 +100,7 @@ testWithGanache('Validators Wrapper', (web3) => { for (const validator of [validator1, validator2]) { await setupValidator(validator) await validators.affiliate(groupAccount).sendAndWaitForReceipt({ from: validator }) - await validators.addMember(validator).sendAndWaitForReceipt({ from: groupAccount }) + await (await validators.addMember(validator)).sendAndWaitForReceipt({ from: groupAccount }) } const members = await validators diff --git a/packages/contractkit/src/wrappers/Validators.ts b/packages/contractkit/src/wrappers/Validators.ts index 8fefd681ac1..0ed9973112f 100644 --- a/packages/contractkit/src/wrappers/Validators.ts +++ b/packages/contractkit/src/wrappers/Validators.ts @@ -70,9 +70,12 @@ export class ValidatorsWrapper extends BaseWrapper { const voteWeight = await election.getTotalVotesForGroup(group) const { lesser, greater } = await election.findLesserAndGreaterAfterVote(group, voteWeight) - return wrapSend(this.kit, this.contract.methods.addFirstMember(member, lesser, greater)) + return toTransactionObject( + this.kit, + this.contract.methods.addFirstMember(member, lesser, greater) + ) } else { - return wrapSend(this.kit, this.contract.methods.addMember(member)) + return toTransactionObject(this.kit, this.contract.methods.addMember(member)) } } /** From d9c2cc031a0246bada371d4c05e736c5cbfa84f0 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Thu, 17 Oct 2019 18:11:59 -0700 Subject: [PATCH 071/149] Fix migrations, end-to-end tests --- packages/celotool/src/e2e-tests/governance_tests.ts | 5 +++-- packages/protocol/migrations/17_elect_validators.ts | 7 +------ packages/protocol/migrationsConfig.js | 1 - 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/packages/celotool/src/e2e-tests/governance_tests.ts b/packages/celotool/src/e2e-tests/governance_tests.ts index da77abe07f8..39424a5a973 100644 --- a/packages/celotool/src/e2e-tests/governance_tests.ts +++ b/packages/celotool/src/e2e-tests/governance_tests.ts @@ -55,11 +55,11 @@ describe('governance tests', () => { const groupInfo = await validators.methods .getValidatorGroup(groupAddress) .call({}, blockNumber) - return groupInfo[2] + return groupInfo[1] } else { const [groupAddress] = await validators.methods.getRegisteredValidatorGroups().call() const groupInfo = await validators.methods.getValidatorGroup(groupAddress).call() - return groupInfo[2] + return groupInfo[1] } } @@ -136,6 +136,7 @@ describe('governance tests', () => { } await initAndStartGeth(context.hooks.gethBinaryPath, groupInstance) allValidators = await getValidatorGroupMembers() + console.log(allValidators) assert.equal(allValidators.length, 5) epoch = new BigNumber(await validators.methods.getEpochSize().call()).toNumber() assert.equal(epoch, 10) diff --git a/packages/protocol/migrations/17_elect_validators.ts b/packages/protocol/migrations/17_elect_validators.ts index 61fa8fc3d02..f1a4a128812 100644 --- a/packages/protocol/migrations/17_elect_validators.ts +++ b/packages/protocol/migrations/17_elect_validators.ts @@ -64,7 +64,6 @@ async function registerValidatorGroup( // @ts-ignore const tx = validators.contract.methods.registerValidatorGroup( `${config.validators.groupName} ${encodedKey}`, - config.validators.groupUrl, toFixed(config.validators.commission).toString() ) @@ -100,11 +99,7 @@ async function registerValidator( ) // @ts-ignore - const registerTx = validators.contract.methods.registerValidator( - address, - config.validators.groupUrl, - add0x(publicKeysData) - ) + const registerTx = validators.contract.methods.registerValidator(address, add0x(publicKeysData)) await sendTransactionWithPrivateKey(web3, registerTx, validatorPrivateKey, { to: validators.address, diff --git a/packages/protocol/migrationsConfig.js b/packages/protocol/migrationsConfig.js index 0b77083e2f6..f5c8b7c70d2 100644 --- a/packages/protocol/migrationsConfig.js +++ b/packages/protocol/migrationsConfig.js @@ -88,7 +88,6 @@ const DefaultConfig = { validatorKeys: [], // We register a single validator group during the migration. groupName: 'C-Labs', - groupUrl: 'celo.org', commission: 0.1, }, blockchainParameters: { From 0b894ac7bebb2b27f2dd79a7b83ac0dfe64d4105 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Thu, 17 Oct 2019 18:46:04 -0700 Subject: [PATCH 072/149] Fix tests --- .../src/e2e-tests/governance_tests.ts | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/celotool/src/e2e-tests/governance_tests.ts b/packages/celotool/src/e2e-tests/governance_tests.ts index 39424a5a973..a49df9a9615 100644 --- a/packages/celotool/src/e2e-tests/governance_tests.ts +++ b/packages/celotool/src/e2e-tests/governance_tests.ts @@ -136,13 +136,12 @@ describe('governance tests', () => { } await initAndStartGeth(context.hooks.gethBinaryPath, groupInstance) allValidators = await getValidatorGroupMembers() - console.log(allValidators) assert.equal(allValidators.length, 5) epoch = new BigNumber(await validators.methods.getEpochSize().call()).toNumber() assert.equal(epoch, 10) - // Give the node time to sync. - await sleep(15) + // Give the node time to sync, and time for an epoch transition so we can activate our vote. + await sleep(20) await activate(allValidators[0]) const groupWeb3 = new Web3('ws://localhost:8567') const groupKit = newKitFromWeb3(groupWeb3) @@ -229,24 +228,28 @@ describe('governance tests', () => { const assertScoreUnchanged = async (validator: string, blockNumber: number) => { const score = new BigNumber( - (await validators.methods.getValidator(validator).call({}, blockNumber))[4] + (await validators.methods.getValidator(validator).call({}, blockNumber))[3] ) const previousScore = new BigNumber( - (await validators.methods.getValidator(validator).call({}, blockNumber - 1))[4] + (await validators.methods.getValidator(validator).call({}, blockNumber - 1))[3] ) + assert.isNotNaN(score) + assert.isNotNaN(previousScore) assert.equal(score.toFixed(), previousScore.toFixed()) } const assertScoreChanged = async (validator: string, blockNumber: number) => { const score = new BigNumber( - (await validators.methods.getValidator(validator).call({}, blockNumber))[4] + (await validators.methods.getValidator(validator).call({}, blockNumber))[3] ) const previousScore = new BigNumber( - (await validators.methods.getValidator(validator).call({}, blockNumber - 1))[4] + (await validators.methods.getValidator(validator).call({}, blockNumber - 1))[3] ) const expectedScore = adjustmentSpeed .times(uptime) .plus(new BigNumber(1).minus(adjustmentSpeed).times(fromFixed(previousScore))) + assert.isNotNaN(score) + assert.isNotNaN(previousScore) assert.equal(score.toFixed(), toFixed(expectedScore).toFixed()) } @@ -290,6 +293,8 @@ describe('governance tests', () => { const previousBalance = new BigNumber( await stableToken.methods.balanceOf(validator).call({}, blockNumber - 1) ) + assert.isNotNaN(currentBalance) + assert.isNotNaN(previousBalance) assert.equal(expected.toFixed(), currentBalance.minus(previousBalance).toFixed()) } @@ -299,8 +304,9 @@ describe('governance tests', () => { const getExpectedTotalPayment = async (validator: string, blockNumber: number) => { const score = new BigNumber( - (await validators.methods.getValidator(validator).call({}, blockNumber))[4] + (await validators.methods.getValidator(validator).call({}, blockNumber))[3] ) + assert.isNotNaN(score) return validatorEpochPayment.times(fromFixed(score)) } From 44aa9f9ccadca3a5782367987768dfe2492ad421 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Thu, 17 Oct 2019 19:51:30 -0700 Subject: [PATCH 073/149] Update CLI docs --- packages/docs/command-line-interface/validator.md | 3 +-- packages/docs/command-line-interface/validatorgroup.md | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/docs/command-line-interface/validator.md b/packages/docs/command-line-interface/validator.md index 4c72974b8fd..0d4d8cfd1f1 100644 --- a/packages/docs/command-line-interface/validator.md +++ b/packages/docs/command-line-interface/validator.md @@ -50,10 +50,9 @@ OPTIONS --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Address for the Validator --name=name (required) --publicKey=0x (required) Public Key - --url=url (required) EXAMPLE - register --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --name myName --url "http://validator.com" --publicKey + register --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --name myName --publicKey 0xc52f3fab06e22a54915a8765c4f6826090cfac5e40282b43844bf1c0df83aaa632e55b67869758f2291d1aabe0ebecc7cbf4236aaa45e3e0cfbf 997eda082ae19d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d 785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d7405011220a66a6257562d0c26dabf64485a1d diff --git a/packages/docs/command-line-interface/validatorgroup.md b/packages/docs/command-line-interface/validatorgroup.md index 291c735fc92..8883c2f772e 100644 --- a/packages/docs/command-line-interface/validatorgroup.md +++ b/packages/docs/command-line-interface/validatorgroup.md @@ -55,10 +55,9 @@ OPTIONS --commission=commission (required) --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Address for the Validator Group --name=name (required) - --url=url (required) EXAMPLE - register --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --name myName --url "http://vgroup.com" --commission 0.1 + register --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --name myName --commission 0.1 ``` _See code: [packages/cli/src/commands/validatorgroup/register.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validatorgroup/register.ts)_ From 262c002aac1111d19e7879dc3121902578c293bc Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Fri, 18 Oct 2019 10:04:45 -0700 Subject: [PATCH 074/149] Fix end-to-end transfer tests --- packages/celotool/src/e2e-tests/utils.ts | 18 +++++++++++-- .../contracts/stability/StableToken.sol | 26 ++++++++++++++----- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/packages/celotool/src/e2e-tests/utils.ts b/packages/celotool/src/e2e-tests/utils.ts index aae32ca88ac..cbdd3af3809 100644 --- a/packages/celotool/src/e2e-tests/utils.ts +++ b/packages/celotool/src/e2e-tests/utils.ts @@ -318,7 +318,11 @@ export async function startGeth(gethBinaryPath: string, instance: GethInstanceCo return instance } -export async function migrateContracts(validatorPrivateKeys: string[], to: number = 1000) { +export async function migrateContracts( + validatorPrivateKeys: string[], + validators: string[], + to: number = 1000 +) { const migrationOverrides = { validators: { validatorKeys: validatorPrivateKeys.map(ensure0x), @@ -326,6 +330,12 @@ export async function migrateContracts(validatorPrivateKeys: string[], to: numbe election: { minElectableValidators: '1', }, + stableToken: { + initialBalances: { + addresses: validators.map(ensure0x), + values: validators.map(() => '10000000000000000000000'), + }, + }, } const args = [ '--cwd', @@ -425,7 +435,11 @@ export function getContext(gethConfig: GethTestConfig) { await initAndStartGeth(gethBinaryPath, instance) } if (gethConfig.migrate || gethConfig.migrateTo) { - await migrateContracts(validatorPrivateKeys, gethConfig.migrateTo) + await migrateContracts( + validatorPrivateKeys, + validators.map((x) => x.address), + gethConfig.migrateTo + ) } await killGeth() await sleep(2) diff --git a/packages/protocol/contracts/stability/StableToken.sol b/packages/protocol/contracts/stability/StableToken.sol index 386a9ec978a..1146d534d5a 100644 --- a/packages/protocol/contracts/stability/StableToken.sol +++ b/packages/protocol/contracts/stability/StableToken.sol @@ -121,13 +121,6 @@ contract StableToken is IStableToken, IERC20Token, ICeloToken, Ownable, initializer { require(inflationRate != 0, "Must provide a non-zero inflation rate."); - require(initialBalanceAddresses.length == initialBalanceValues.length); - for (uint256 i = 0; i < initialBalanceAddresses.length; i = i.add(1)) { - totalSupply_ = totalSupply_.add(initialBalanceValues[i]); - balances[initialBalanceAddresses[i]] = balances[initialBalanceAddresses[i]].add( - initialBalanceValues[i] - ); - } _transferOwnership(msg.sender); totalSupply_ = 0; @@ -141,6 +134,10 @@ contract StableToken is IStableToken, IERC20Token, ICeloToken, Ownable, // solhint-disable-next-line not-rely-on-time inflationState.factorLastUpdated = now; + require(initialBalanceAddresses.length == initialBalanceValues.length); + for (uint256 i = 0; i < initialBalanceAddresses.length; i = i.add(1)) { + require(_mint(initialBalanceAddresses[i], initialBalanceValues[i])); + } setRegistry(registryAddress); } @@ -248,7 +245,22 @@ contract StableToken is IStableToken, IERC20Token, ICeloToken, Ownable, msg.sender == registry.getAddressFor(EXCHANGE_REGISTRY_ID) || msg.sender == registry.getAddressFor(VALIDATORS_REGISTRY_ID) ); + return _mint(to, value); + } + /** + * @notice Mints new StableToken and gives it to 'to'. + * @param to The account for which to mint tokens. + * @param value The amount of StableToken to mint. + */ + function _mint( + address to, + uint256 value + ) + private + updateInflationFactor + returns (bool) + { uint256 units = _valueToUnits(inflationState.factor, value); totalSupply_ = totalSupply_.add(units); balances[to] = balances[to].add(units); From 97372e7cbecff0722ce9d6b0daebd988f3a55b8a Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Fri, 18 Oct 2019 15:21:06 -0700 Subject: [PATCH 075/149] Fix contractkit tests --- packages/cli/src/commands/validatorgroup/member.ts | 2 +- packages/contractkit/package.json | 2 +- packages/contractkit/src/wrappers/Validators.test.ts | 6 ++---- packages/contractkit/src/wrappers/Validators.ts | 12 +++++------- packages/protocol/scripts/devchain.ts | 10 +++++++++- 5 files changed, 18 insertions(+), 14 deletions(-) diff --git a/packages/cli/src/commands/validatorgroup/member.ts b/packages/cli/src/commands/validatorgroup/member.ts index 000462e81b7..5470ece4b67 100644 --- a/packages/cli/src/commands/validatorgroup/member.ts +++ b/packages/cli/src/commands/validatorgroup/member.ts @@ -43,7 +43,7 @@ export default class ValidatorGroupMembers extends BaseCommand { this.kit.defaultAccount = res.flags.from const validators = await this.kit.contracts.getValidators() if (res.flags.accept) { - const tx = await validators.addMember((res.args as any).validatorAddress) + const tx = await validators.addMember(res.flags.from, (res.args as any).validatorAddress) await displaySendTx('addMember', tx) } else if (res.flags.remove) { await displaySendTx( diff --git a/packages/contractkit/package.json b/packages/contractkit/package.json index d461819180e..723fbfaaa6a 100644 --- a/packages/contractkit/package.json +++ b/packages/contractkit/package.json @@ -40,7 +40,7 @@ "web3-eth-abi": "1.0.0-beta.37" }, "devDependencies": { - "@celo/ganache-cli": "git+https://github.com/celo-org/ganache-cli.git#98ad2ba", + "@celo/ganache-cli": "git+https://github.com/celo-org/ganache-cli.git#4cf9664", "@celo/protocol": "1.0.0", "@types/debug": "^4.1.5", "@types/web3": "^1.0.18", diff --git a/packages/contractkit/src/wrappers/Validators.test.ts b/packages/contractkit/src/wrappers/Validators.test.ts index 25e91c4ce79..2f3844eb9e2 100644 --- a/packages/contractkit/src/wrappers/Validators.test.ts +++ b/packages/contractkit/src/wrappers/Validators.test.ts @@ -79,9 +79,7 @@ testWithGanache('Validators Wrapper', (web3) => { await setupGroup(groupAccount) await setupValidator(validatorAccount) await validators.affiliate(groupAccount).sendAndWaitForReceipt({ from: validatorAccount }) - await (await validators.addMember(validatorAccount)).sendAndWaitForReceipt({ - from: groupAccount, - }) + await (await validators.addMember(groupAccount, validatorAccount)).sendAndWaitForReceipt() const members = await validators.getValidatorGroup(groupAccount).then((group) => group.members) expect(members).toContain(validatorAccount) @@ -100,7 +98,7 @@ testWithGanache('Validators Wrapper', (web3) => { for (const validator of [validator1, validator2]) { await setupValidator(validator) await validators.affiliate(groupAccount).sendAndWaitForReceipt({ from: validator }) - await (await validators.addMember(validator)).sendAndWaitForReceipt({ from: groupAccount }) + await (await validators.addMember(groupAccount, validator)).sendAndWaitForReceipt() } const members = await validators diff --git a/packages/contractkit/src/wrappers/Validators.ts b/packages/contractkit/src/wrappers/Validators.ts index 0ed9973112f..24aca960c7d 100644 --- a/packages/contractkit/src/wrappers/Validators.ts +++ b/packages/contractkit/src/wrappers/Validators.ts @@ -16,6 +16,7 @@ export interface Validator { name: string publicKey: string affiliation: string | null + score: BigNumber } export interface ValidatorGroup { @@ -44,6 +45,7 @@ export interface ValidatorsConfig { /** * Contract for voting for validators and managing validator groups. */ +// TODO(asa): Support authorized validators export class ValidatorsWrapper extends BaseWrapper { affiliate = proxySend(this.kit, this.contract.methods.affiliate) deaffiliate = proxySend(this.kit, this.contract.methods.deaffiliate) @@ -58,12 +60,7 @@ export class ValidatorsWrapper extends BaseWrapper { this.contract.methods.registerValidatorGroup(name, toFixed(commission).toFixed()) ) } - async addMember(member: string): Promise> { - if (this.kit.defaultAccount == null) { - throw new Error(`missing from at new ValdidatorUtils()`) - } - // TODO(asa): Support authorized validators - const group = this.kit.defaultAccount + async addMember(group: string, member: string): Promise> { const numMembers = await this.getGroupNumMembers(group) if (numMembers.isZero()) { const election = await this.kit.contracts.getElection() @@ -75,7 +72,7 @@ export class ValidatorsWrapper extends BaseWrapper { this.contract.methods.addFirstMember(member, lesser, greater) ) } else { - return toTransactionObject(this.kit, this.contract.methods.addMember(member)) + return toTransactionObject(this.kit, this.contract.methods.addMember(member), { from: group }) } } /** @@ -137,6 +134,7 @@ export class ValidatorsWrapper extends BaseWrapper { name: res[0], publicKey: res[1] as any, affiliation: res[2], + score: fromFixed(new BigNumber(res[3])), } } diff --git a/packages/protocol/scripts/devchain.ts b/packages/protocol/scripts/devchain.ts index 309dd4ad6ab..543c8f45578 100644 --- a/packages/protocol/scripts/devchain.ts +++ b/packages/protocol/scripts/devchain.ts @@ -138,7 +138,15 @@ function createDirIfMissing(dir: string) { } function runMigrations(opts: { upto?: number } = {}) { - const cmdArgs = ['truffle', 'migrate'] + const migrationOverrides = { + stableToken: { + initialBalances: { + addresses: ['0x5409ed021d9299bf6814279a6a1411a7e866a631'], + values: ['10000000000000000000000'], + }, + }, + } + const cmdArgs = ['truffle', 'migrate', '--migration_override', JSON.stringify(migrationOverrides)] if (opts.upto) { cmdArgs.push('--to') From ef71139d6b78a28442e35f5ffe1d678b16f34ca8 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Tue, 22 Oct 2019 13:46:59 -0700 Subject: [PATCH 076/149] Fix contractkit tests --- packages/cli/src/commands/validatorgroup/member.ts | 1 + packages/contractkit/src/wrappers/Validators.ts | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/commands/validatorgroup/member.ts b/packages/cli/src/commands/validatorgroup/member.ts index 5470ece4b67..f06691f47bf 100644 --- a/packages/cli/src/commands/validatorgroup/member.ts +++ b/packages/cli/src/commands/validatorgroup/member.ts @@ -10,6 +10,7 @@ export default class ValidatorGroupMembers extends BaseCommand { static flags = { ...BaseCommand.flags, from: Flags.address({ required: true, description: "ValidatorGroup's address" }), + group: Flags.address({ required: false, description: "ValidatorGroup's address" }), accept: flags.boolean({ exclusive: ['remove', 'reorder'], description: 'Accept a validator whose affiliation is already set to the group', diff --git a/packages/contractkit/src/wrappers/Validators.ts b/packages/contractkit/src/wrappers/Validators.ts index 24aca960c7d..74b9ffcccff 100644 --- a/packages/contractkit/src/wrappers/Validators.ts +++ b/packages/contractkit/src/wrappers/Validators.ts @@ -69,7 +69,8 @@ export class ValidatorsWrapper extends BaseWrapper { return toTransactionObject( this.kit, - this.contract.methods.addFirstMember(member, lesser, greater) + this.contract.methods.addFirstMember(member, lesser, greater), + { from: group } ) } else { return toTransactionObject(this.kit, this.contract.methods.addMember(member), { from: group }) From b8eafd7a92c6b500c08547141f0997b288852419 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Tue, 22 Oct 2019 14:25:39 -0700 Subject: [PATCH 077/149] Fix --- packages/cli/src/commands/validatorgroup/member.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/cli/src/commands/validatorgroup/member.ts b/packages/cli/src/commands/validatorgroup/member.ts index f06691f47bf..5470ece4b67 100644 --- a/packages/cli/src/commands/validatorgroup/member.ts +++ b/packages/cli/src/commands/validatorgroup/member.ts @@ -10,7 +10,6 @@ export default class ValidatorGroupMembers extends BaseCommand { static flags = { ...BaseCommand.flags, from: Flags.address({ required: true, description: "ValidatorGroup's address" }), - group: Flags.address({ required: false, description: "ValidatorGroup's address" }), accept: flags.boolean({ exclusive: ['remove', 'reorder'], description: 'Accept a validator whose affiliation is already set to the group', From 3847bef5cea7d831bf6593764ed48b3964c4ec36 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Tue, 22 Oct 2019 14:27:43 -0700 Subject: [PATCH 078/149] Cleanup --- .../contracts/common/UsingPrecompiles.sol | 1 + .../contracts/governance/Election.sol | 2 + .../contracts/governance/Validators.sol | 234 +++++++++--------- packages/protocol/test/governance/election.ts | 24 +- .../protocol/test/governance/validators.ts | 4 +- 5 files changed, 135 insertions(+), 130 deletions(-) diff --git a/packages/protocol/contracts/common/UsingPrecompiles.sol b/packages/protocol/contracts/common/UsingPrecompiles.sol index e5a5a1657cd..7fd4e8e8cc8 100644 --- a/packages/protocol/contracts/common/UsingPrecompiles.sol +++ b/packages/protocol/contracts/common/UsingPrecompiles.sol @@ -1,5 +1,6 @@ pragma solidity ^0.5.3; +// TODO(asa): Limit assembly usage by using X.staticcall instead. contract UsingPrecompiles { address constant PROOF_OF_POSSESSION = address(0xff - 4); diff --git a/packages/protocol/contracts/governance/Election.sol b/packages/protocol/contracts/governance/Election.sol index 63ceed9fad0..69bc33d1fc1 100644 --- a/packages/protocol/contracts/governance/Election.sol +++ b/packages/protocol/contracts/governance/Election.sol @@ -484,6 +484,8 @@ contract Election is view returns (uint256) { + // The group must meet the balance requirements in order for their voters to receive epoch + // rewards. if (getValidators().meetsAccountLockedGoldRequirements(group) && votes.active.total > 0) { return totalEpochRewards.mul(votes.active.forGroup[group].total).div(votes.active.total); } else { diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index ff8d90b29c6..a8277dfe04d 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -313,6 +313,118 @@ contract Validators is return true; } + /** + * @notice Returns the parameters that goven how a validator's score is calculated. + * @return The parameters that goven how a validator's score is calculated. + */ + function getValidatorScoreParameters() external view returns (uint256, uint256) { + return (validatorScoreParameters.exponent, validatorScoreParameters.adjustmentSpeed.unwrap()); + } + + /** + * @notice Returns the group membership history of a validator. + * @param account The validator whose membership history to return. + * @return The group membership history of a validator. + */ + function getMembershipHistory( + address account + ) + external + view + returns (uint256[] memory, address[] memory, uint256) + { + MembershipHistory storage history = validators[account].membershipHistory; + uint256[] memory epochs = new uint256[](history.numEntries); + address[] memory membershipGroups = new address[](history.numEntries); + for (uint256 i = 0; i < history.numEntries; i = i.add(1)) { + uint256 index = history.tail.add(i); + epochs[i] = history.entries[index].epochNumber; + membershipGroups[i] = history.entries[index].group; + } + return (epochs, membershipGroups, history.lastRemovedFromGroupTimestamp); + } + + /** + * @notice Updates a validator's score based on its uptime for the epoch. + * @param validator The address of the validator. + * @param uptime The Fixidity representation of the validator's uptime, between 0 and 1. + * @return True upon success. + */ + function updateValidatorScore(address validator, uint256 uptime) external onlyVm() { + _updateValidatorScore(validator, uptime); + } + + /** + * @notice Updates a validator's score based on its uptime for the epoch. + * @param validator The address of the validator. + * @param uptime The Fixidity representation of the validator's uptime, between 0 and 1. + * @dev new_score = uptime ** exponent * adjustmentSpeed + old_score * (1 - adjustmentSpeed) + * @return True upon success. + */ + function _updateValidatorScore(address validator, uint256 uptime) internal { + address account = getLockedGold().getAccountFromValidator(validator); + require(isValidator(account)); + require(uptime <= FixidityLib.fixed1().unwrap()); + + uint256 numerator; + uint256 denominator; + (numerator, denominator) = fractionMulExp( + FixidityLib.fixed1().unwrap(), + FixidityLib.fixed1().unwrap(), + uptime, + FixidityLib.fixed1().unwrap(), + validatorScoreParameters.exponent, + 18 + ); + + FixidityLib.Fraction memory epochScore = FixidityLib.wrap(numerator).divide( + FixidityLib.wrap(denominator) + ); + FixidityLib.Fraction memory newComponent = validatorScoreParameters.adjustmentSpeed.multiply( + epochScore + ); + + FixidityLib.Fraction memory currentComponent = FixidityLib.fixed1().subtract( + validatorScoreParameters.adjustmentSpeed + ); + currentComponent = currentComponent.multiply(validators[account].score); + validators[account].score = FixidityLib.wrap( + Math.min( + epochScore.unwrap(), + newComponent.add(currentComponent).unwrap() + ) + ); + } + + /** + * @notice Distributes epoch payments to `validator` and its group. + */ + function distributeEpochPayment(address validator) external onlyVm() { + _distributeEpochPayment(validator); + } + + /** + * @notice Distributes epoch payments to `validator` and its group. + */ + function _distributeEpochPayment(address validator) internal { + address account = getLockedGold().getAccountFromValidator(validator); + require(isValidator(account)); + // The group that should be paid is the group that the validator was a member of at the + // time it was elected. + address group = getMembershipInLastEpoch(account); + // Both the validator and the group must maintain the minimum locked gold balance in order to + // receive epoch payments. + if (meetsAccountLockedGoldRequirements(account) && meetsAccountLockedGoldRequirements(group)) { + FixidityLib.Fraction memory totalPayment = FixidityLib.newFixed( + validatorEpochPayment + ).multiply(validators[account].score); + uint256 groupPayment = totalPayment.multiply(groups[group].commission).fromFixed(); + uint256 validatorPayment = totalPayment.fromFixed().sub(groupPayment); + getStableToken().mint(group, groupPayment); + getStableToken().mint(account, validatorPayment); + } + } + /** * @notice De-registers a validator. * @param index The index of this validator in the list of all registered validators. @@ -327,12 +439,12 @@ contract Validators is // `validatorLockedGoldRequirements.duration` seconds. Validator storage validator = validators[account]; if (validator.affiliation != address(0)) { - require(!groups[validator.affiliation].members.contains(account), 'two'); + require(!groups[validator.affiliation].members.contains(account)); } uint256 requirementEndTime = validator.membershipHistory.lastRemovedFromGroupTimestamp.add( validatorLockedGoldRequirements.duration ); - require(requirementEndTime < now, 'one'); + require(requirementEndTime < now); // Remove the validator. deleteElement(registeredValidators, account, index); @@ -350,7 +462,8 @@ contract Validators is function affiliate(address group) external nonReentrant returns (bool) { address account = getLockedGold().getAccountFromActiveValidator(msg.sender); require(isValidator(account) && isValidatorGroup(group)); - require(meetsAccountLockedGoldRequirements(account) && meetsAccountLockedGoldRequirements(group), "three"); + require(meetsAccountLockedGoldRequirements(account)); + require(meetsAccountLockedGoldRequirements(group)); Validator storage validator = validators[account]; if (validator.affiliation != address(0)) { _deaffiliate(validator, account); @@ -485,7 +598,8 @@ contract Validators is require(_group.members.numElements < maxGroupSize, "group would exceed maximum size"); require(validators[validator].affiliation == group && !_group.members.contains(validator)); uint256 numMembers = _group.members.numElements.add(1); - require(meetsAccountLockedGoldRequirements(group) && meetsAccountLockedGoldRequirements(validator)); + require(meetsAccountLockedGoldRequirements(group)); + require(meetsAccountLockedGoldRequirements(validator)); _group.members.push(validator); if (numMembers == 1) { getElection().markGroupEligible(group, lesser, greater); @@ -536,95 +650,6 @@ contract Validators is return true; } - /** - * @notice Updates a validator's score based on its uptime for the epoch. - * @param validator The address of the validator. - * @param uptime The Fixidity representation of the validator's uptime, between 0 and 1. - * @return True upon success. - */ - function updateValidatorScore(address validator, uint256 uptime) external onlyVm() { - _updateValidatorScore(validator, uptime); - } - - /** - * @notice Updates a validator's score based on its uptime for the epoch. - * @param validator The address of the validator. - * @param uptime The Fixidity representation of the validator's uptime, between 0 and 1. - * @dev new_score = uptime ** exponent * adjustmentSpeed + old_score * (1 - adjustmentSpeed) - * @return True upon success. - */ - function _updateValidatorScore(address validator, uint256 uptime) internal { - address account = getLockedGold().getAccountFromValidator(validator); - require(isValidator(account), "isvalidator"); - require(uptime <= FixidityLib.fixed1().unwrap(), "uptime"); - - uint256 numerator; - uint256 denominator; - (numerator, denominator) = fractionMulExp( - FixidityLib.fixed1().unwrap(), - FixidityLib.fixed1().unwrap(), - uptime, - FixidityLib.fixed1().unwrap(), - validatorScoreParameters.exponent, - 18 - ); - - FixidityLib.Fraction memory epochScore = FixidityLib.wrap(numerator).divide( - FixidityLib.wrap(denominator) - ); - FixidityLib.Fraction memory newComponent = validatorScoreParameters.adjustmentSpeed.multiply( - epochScore - ); - - FixidityLib.Fraction memory currentComponent = FixidityLib.fixed1().subtract( - validatorScoreParameters.adjustmentSpeed - ); - currentComponent = currentComponent.multiply(validators[account].score); - validators[account].score = FixidityLib.wrap( - Math.min( - epochScore.unwrap(), - newComponent.add(currentComponent).unwrap() - ) - ); - } - - /** - * @notice Distributes epoch payments to `validator` and its group. - */ - function distributeEpochPayment(address validator) external onlyVm() { - _distributeEpochPayment(validator); - } - - /** - * @notice Distributes epoch payments to `validator` and its group. - */ - function _distributeEpochPayment(address validator) internal { - address account = getLockedGold().getAccountFromValidator(validator); - require(isValidator(account)); - // The group that should be paid is the group that the validator was a member of at the - // time it was elected. - address group = getMembershipInLastEpoch(account); - // Both the validator and the group must maintain the minimum locked gold balance in order to - // receive epoch payments. - if (meetsAccountLockedGoldRequirements(account) && meetsAccountLockedGoldRequirements(group)) { - FixidityLib.Fraction memory totalPayment = FixidityLib.newFixed( - validatorEpochPayment - ).multiply(validators[account].score); - uint256 groupPayment = totalPayment.multiply(groups[group].commission).fromFixed(); - uint256 validatorPayment = totalPayment.fromFixed().sub(groupPayment); - getStableToken().mint(group, groupPayment); - getStableToken().mint(account, validatorPayment); - } - } - - /** - * @notice Returns the parameters that goven how a validator's score is calculated. - * @return The parameters that goven how a validator's score is calculated. - */ - function getValidatorScoreParameters() external view returns (uint256, uint256) { - return (validatorScoreParameters.exponent, validatorScoreParameters.adjustmentSpeed.unwrap()); - } - /** * @notice Returns the Locked Gold requirements for validators. * @return The Locked Gold requirements for validators. @@ -649,29 +674,6 @@ contract Validators is return maxGroupSize; } - /** - * @notice Returns the group membership history of a validator. - * @param account The validator whose membership history to return. - * @return The group membership history of a validator. - */ - function getMembershipHistory( - address account - ) - external - view - returns (uint256[] memory, address[] memory, uint256) - { - MembershipHistory storage history = validators[account].membershipHistory; - uint256[] memory epochs = new uint256[](history.numEntries); - address[] memory membershipGroups = new address[](history.numEntries); - for (uint256 i = 0; i < history.numEntries; i = i.add(1)) { - uint256 index = history.tail.add(i); - epochs[i] = history.entries[index].epochNumber; - membershipGroups[i] = history.entries[index].group; - } - return (epochs, membershipGroups, history.lastRemovedFromGroupTimestamp); - } - /** * @notice Returns the locked gold balance requirement for the supplied account. * @param account The account that may have to meet locked gold balance requirements. diff --git a/packages/protocol/test/governance/election.ts b/packages/protocol/test/governance/election.ts index 4fc979117aa..815c1131dca 100644 --- a/packages/protocol/test/governance/election.ts +++ b/packages/protocol/test/governance/election.ts @@ -906,7 +906,7 @@ contract('Election', (accounts: string[]) => { const voteValue1 = new BigNumber(2000000) const voteValue2 = new BigNumber(1000000) const totalRewardValue = new BigNumber(3000000) - const balanceRequirement = new BigNumber(1000000) + const lockedGoldRequirement = new BigNumber(1000000) beforeEach(async () => { await registry.setAddressFor(CeloContractName.Validators, accounts[0]) await election.markGroupEligible(group1, NULL_ADDRESS, NULL_ADDRESS) @@ -919,8 +919,8 @@ contract('Election', (accounts: string[]) => { await mockLockedGold.incrementNonvotingAccountBalance(voter, voteValue1.plus(voteValue2)) await election.vote(group1, voteValue1, group2, NULL_ADDRESS) await election.vote(group2, voteValue2, NULL_ADDRESS, group1) - await mockValidators.setAccountBalanceRequirement(group1, balanceRequirement) - await mockValidators.setAccountBalanceRequirement(group2, balanceRequirement) + await mockValidators.setAccountLockedGoldRequirement(group1, lockedGoldRequirement) + await mockValidators.setAccountLockedGoldRequirement(group2, lockedGoldRequirement) }) describe('when one group has active votes', () => { @@ -929,9 +929,9 @@ contract('Election', (accounts: string[]) => { await election.activate(group1) }) - describe('when the group meets the balance requirements ', () => { + describe('when the group meets the locked gold requirements ', () => { beforeEach(async () => { - await mockLockedGold.setAccountTotalLockedGold(group1, balanceRequirement) + await mockLockedGold.setAccountTotalLockedGold(group1, lockedGoldRequirement) }) it('should return the total reward value', async () => { @@ -942,9 +942,9 @@ contract('Election', (accounts: string[]) => { }) }) - describe('when the group does not meet the balance requirements ', () => { + describe('when the group does not meet the locked gold requirements ', () => { beforeEach(async () => { - await mockLockedGold.setAccountTotalLockedGold(group1, balanceRequirement.minus(1)) + await mockLockedGold.setAccountTotalLockedGold(group1, lockedGoldRequirement.minus(1)) }) it('should return zero', async () => { @@ -954,7 +954,7 @@ contract('Election', (accounts: string[]) => { }) describe('when two groups have active votes', () => { - const balanceRequirement = new BigNumber(1000000) + const lockedGoldRequirement = new BigNumber(1000000) const expectedGroup1EpochRewards = voteValue1 .div(voteValue1.plus(voteValue2)) .times(totalRewardValue) @@ -965,9 +965,9 @@ contract('Election', (accounts: string[]) => { await election.activate(group2) }) - describe('when one group meets the balance requirements ', () => { + describe('when one group meets the locked gold requirements ', () => { beforeEach(async () => { - await mockLockedGold.setAccountTotalLockedGold(group1, balanceRequirement) + await mockLockedGold.setAccountTotalLockedGold(group1, lockedGoldRequirement) }) it('should return the proportional reward value for that group', async () => { @@ -984,9 +984,9 @@ contract('Election', (accounts: string[]) => { }) describe('when the group does not have active votes', () => { - describe('when the group meets the balance requirements ', () => { + describe('when the group meets the locked gold requirements ', () => { beforeEach(async () => { - await mockLockedGold.setAccountTotalLockedGold(group1, balanceRequirement) + await mockLockedGold.setAccountTotalLockedGold(group1, lockedGoldRequirement) }) it('should return zero', async () => { diff --git a/packages/protocol/test/governance/validators.ts b/packages/protocol/test/governance/validators.ts index 824d209c35d..fc1c1e25dc3 100644 --- a/packages/protocol/test/governance/validators.ts +++ b/packages/protocol/test/governance/validators.ts @@ -1201,9 +1201,9 @@ contract('Validators', (accounts: string[]) => { }) }) - describe('when it has been `groupLockedGoldRequirements.duration` since the validator was removed from the group', () => { + describe('when it has been less than `groupLockedGoldRequirements.duration` since the validator was removed from the group', () => { beforeEach(async () => { - await timeTravel(groupLockedGoldRequirements.duration.toNumber(), web3) + await timeTravel(groupLockedGoldRequirements.duration.toNumber().minus(1), web3) }) it('should revert', async () => { From ae1be930c4ab5ada3c7e62a989348891db9147e4 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Tue, 22 Oct 2019 15:27:39 -0700 Subject: [PATCH 079/149] Fix circle config --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 31bafc17054..181ec0326c0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -596,7 +596,7 @@ jobs: command: | set -e cd packages/celotool - ./ci_test_sync_with_network.sh checkout asaj/pos-2 + ./ci_test_attestations.sh checkout asaj/pos-2 web: working_directory: ~/app From 2b1da945e95e37901ca2927d613344dc1ff69749 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Tue, 22 Oct 2019 16:23:04 -0700 Subject: [PATCH 080/149] Contract unit tests passing --- .circleci/config.yml | 2 +- .../contracts/common/UsingRegistry.sol | 5 ++ .../contracts/governance/Governance.sol | 16 ++++ .../contracts/governance/LockedGold.sol | 8 +- .../contracts/governance/Validators.sol | 56 ++++++------- .../governance/interfaces/IGovernance.sol | 52 +----------- .../governance/test/MockGovernance.sol | 3 +- .../governance/test/MockValidators.sol | 9 ++- packages/protocol/test/governance/election.ts | 28 ++----- .../protocol/test/governance/lockedgold.ts | 80 ++++++++++++------- .../protocol/test/governance/validators.ts | 16 ++-- 11 files changed, 133 insertions(+), 142 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 31bafc17054..181ec0326c0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -596,7 +596,7 @@ jobs: command: | set -e cd packages/celotool - ./ci_test_sync_with_network.sh checkout asaj/pos-2 + ./ci_test_attestations.sh checkout asaj/pos-2 web: working_directory: ~/app diff --git a/packages/protocol/contracts/common/UsingRegistry.sol b/packages/protocol/contracts/common/UsingRegistry.sol index f4b7350b6a8..84cfa8fad2b 100644 --- a/packages/protocol/contracts/common/UsingRegistry.sol +++ b/packages/protocol/contracts/common/UsingRegistry.sol @@ -6,6 +6,7 @@ import "./interfaces/IERC20Token.sol"; import "./interfaces/IRegistry.sol"; import "../governance/interfaces/IElection.sol"; +import "../governance/interfaces/IGovernance.sol"; import "../governance/interfaces/ILockedGold.sol"; import "../governance/interfaces/IValidators.sol"; @@ -63,6 +64,10 @@ contract UsingRegistry is Ownable { return IERC20Token(registry.getAddressForOrDie(GOLD_TOKEN_REGISTRY_ID)); } + function getGovernance() internal view returns (IGovernance) { + return IGovernance(registry.getAddressForOrDie(GOVERNANCE_REGISTRY_ID)); + } + function getLockedGold() internal view returns (ILockedGold) { return ILockedGold(registry.getAddressForOrDie(LOCKED_GOLD_REGISTRY_ID)); } diff --git a/packages/protocol/contracts/governance/Governance.sol b/packages/protocol/contracts/governance/Governance.sol index 35b2895e139..765be341e72 100644 --- a/packages/protocol/contracts/governance/Governance.sol +++ b/packages/protocol/contracts/governance/Governance.sol @@ -690,6 +690,22 @@ contract Governance is IGovernance, Ownable, Initializable, ReentrancyGuard, Usi ); } + /** + * @notice Returns whether or not a particular account is voting on proposals. + * @param account The address of the account. + * @return Whether or not the account is voting on proposals. + */ + function isVoting(address account) external view returns (bool) { + Voter storage voter = voters[account]; + uint256 upvotedProposal = voter.upvote.proposalId; + bool isVotingQueue = upvotedProposal != 0 && isQueued(upvotedProposal); + Proposals.Proposal storage proposal = proposals[voter.mostRecentReferendumProposal]; + bool isVotingReferendum = ( + proposal.getDequeuedStage(stageDurations) == Proposals.Stage.Referendum + ); + return isVotingQueue || isVotingReferendum; + } + /** * @notice Returns the number of seconds proposals stay in the approval stage. * @return The number of seconds proposals stay in the execution stage. diff --git a/packages/protocol/contracts/governance/LockedGold.sol b/packages/protocol/contracts/governance/LockedGold.sol index b223283cd0b..8810262a86c 100644 --- a/packages/protocol/contracts/governance/LockedGold.sol +++ b/packages/protocol/contracts/governance/LockedGold.sol @@ -207,8 +207,14 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr function unlock(uint256 value) external nonReentrant { require(isAccount(msg.sender)); Account storage account = accounts[msg.sender]; + // Prevent unlocking gold when voting on governance proposals so that the gold cannot be + // used to vote more than once. + require(!getGovernance().isVoting(msg.sender)); uint256 balanceRequirement = getValidators().getAccountLockedGoldRequirement(msg.sender); - require(balanceRequirement == 0 || balanceRequirement <= getAccountTotalLockedGold(msg.sender).sub(value)); + require( + balanceRequirement == 0 || + balanceRequirement <= getAccountTotalLockedGold(msg.sender).sub(value) + ); _decrementNonvotingAccountBalance(msg.sender, value); uint256 available = now.add(unlockingPeriod); account.balances.pendingWithdrawals.push(PendingWithdrawal(value, available)); diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index a8277dfe04d..bd1070cf6ce 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -229,6 +229,14 @@ contract Validators is return true; } + /** + * @notice Returns the maximum number of members a group can add. + * @return The maximum number of members a group can add. + */ + function getMaxGroupSize() external view returns (uint256) { + return maxGroupSize; + } + /** * @notice Updates the Locked Gold requirements for Validator Groups. * @param value The per-member amount of Locked Gold required. @@ -650,30 +658,6 @@ contract Validators is return true; } - /** - * @notice Returns the Locked Gold requirements for validators. - * @return The Locked Gold requirements for validators. - */ - function getValidatorLockedGoldRequirements() external view returns (uint256, uint256) { - return (validatorLockedGoldRequirements.value, validatorLockedGoldRequirements.duration); - } - - /** - * @notice Returns the Locked Gold requirements for validator groups. - * @return The Locked Gold requirements for validator groups. - */ - function getGroupLockedGoldRequirements() external view returns (uint256, uint256) { - return (groupLockedGoldRequirements.value, groupLockedGoldRequirements.duration); - } - - /** - * @notice Returns the maximum number of members a group can add. - * @return The maximum number of members a group can add. - */ - function getMaxGroupSize() external view returns (uint256) { - return maxGroupSize; - } - /** * @notice Returns the locked gold balance requirement for the supplied account. * @param account The account that may have to meet locked gold balance requirements. @@ -816,6 +800,22 @@ contract Validators is return registeredValidators.length; } + /** + * @notice Returns the Locked Gold requirements for validators. + * @return The Locked Gold requirements for validators. + */ + function getValidatorLockedGoldRequirements() external view returns (uint256, uint256) { + return (validatorLockedGoldRequirements.value, validatorLockedGoldRequirements.duration); + } + + /** + * @notice Returns the Locked Gold requirements for validator groups. + * @return The Locked Gold requirements for validator groups. + */ + function getGroupLockedGoldRequirements() external view returns (uint256, uint256) { + return (groupLockedGoldRequirements.value, groupLockedGoldRequirements.duration); + } + /** * @notice Returns the list of registered validator accounts. * @return The list of registered validator accounts. @@ -833,18 +833,18 @@ contract Validators is } /** - * @notice Returns whether a particular account has a validator group. + * @notice Returns whether a particular account has a registered validator group. * @param account The account. - * @return Whether a particular address is a validator group. + * @return Whether a particular address is a registered validator group. */ function isValidatorGroup(address account) public view returns (bool) { return bytes(groups[account].name).length > 0; } /** - * @notice Returns whether a particular account has a validator. + * @notice Returns whether a particular account has a registered validator. * @param account The account. - * @return Whether a particular address is a validator. + * @return Whether a particular address is a registered validator. */ function isValidator(address account) public view returns (bool) { return bytes(validators[account].name).length > 0; diff --git a/packages/protocol/contracts/governance/interfaces/IGovernance.sol b/packages/protocol/contracts/governance/interfaces/IGovernance.sol index 2db70134e22..06ca0ea6fd8 100644 --- a/packages/protocol/contracts/governance/interfaces/IGovernance.sol +++ b/packages/protocol/contracts/governance/interfaces/IGovernance.sol @@ -2,55 +2,5 @@ pragma solidity ^0.5.3; interface IGovernance { - function setApprover(address) external; - function setConcurrentProposals(uint256) external; - function setMinDeposit(uint256) external; - function setQueueExpiry(uint256) external; - function setDequeueFrequency(uint256) external; - function setApprovalStageDuration(uint256) external; - function setReferendumStageDuration(uint256) external; - function setExecutionStageDuration(uint256) external; - function setParticipationBaseline(uint256) external; - function setParticipationFloor(uint256) external; - function setBaselineUpdateFactor(uint256) external; - function setBaselineQuorumFactor(uint256) external; - function setConstitution(address, bytes4, uint256) external; - - function propose( - uint256[] calldata, - address[] calldata, - bytes calldata, - uint256[] calldata - ) external payable returns (uint256); - - function upvote(uint256, uint256, uint256) external returns (bool); - function revokeUpvote(uint256, uint256) external returns (bool); - function approve(uint256, uint256) external returns (bool); - function execute(uint256, uint256) external returns (bool); - function withdraw() external returns (bool); - function dequeueProposalsIfReady() external; - function getParticipationParameters() external view returns (uint256, uint256, uint256, uint256); - function getApprovalStageDuration() external view returns (uint256); - function getReferendumStageDuration() external view returns (uint256); - function getExecutionStageDuration() external view returns (uint256); - function getConstitution(address, bytes4) external view returns (uint256); - function proposalExists(uint256) external view returns (bool); - function getProposal(uint256) external view returns (address, uint256, uint256, uint256); - - function getProposalTransaction( - uint256, - uint256 - ) external view returns (uint256, address, bytes memory); - - function isApproved(uint256) external view returns (bool); - function getVoteTotals(uint256) external view returns (uint256, uint256, uint256); - function getVoteRecord(address, uint256) external view returns (uint256, uint256); - function getQueueLength() external view returns (uint256); - function getUpvotes(uint256) external view returns (uint256); - function getQueue() external view returns (uint256[] memory, uint256[] memory); - function getDequeue() external view returns (uint256[] memory); - function getUpvoteRecord(address) external view returns (uint256, uint256); - function getMostRecentReferendumProposal(address) external view returns (uint256); - function isQueued(uint256) external view returns (bool); - function isProposalPassing(uint256) external view returns (bool); + function isVoting(address) external view returns (bool); } diff --git a/packages/protocol/contracts/governance/test/MockGovernance.sol b/packages/protocol/contracts/governance/test/MockGovernance.sol index 0fd41e232b1..9432ae9d2d3 100644 --- a/packages/protocol/contracts/governance/test/MockGovernance.sol +++ b/packages/protocol/contracts/governance/test/MockGovernance.sol @@ -1,10 +1,11 @@ pragma solidity ^0.5.3; +import "../interfaces/IGovernance.sol"; /** * @title A mock Governance for testing. */ -contract MockGovernance { +contract MockGovernance is IGovernance { mapping(address => bool) public isVoting; function setVoting(address voter) external { diff --git a/packages/protocol/contracts/governance/test/MockValidators.sol b/packages/protocol/contracts/governance/test/MockValidators.sol index 995e7728227..203466a6ff8 100644 --- a/packages/protocol/contracts/governance/test/MockValidators.sol +++ b/packages/protocol/contracts/governance/test/MockValidators.sol @@ -11,11 +11,16 @@ contract MockValidators is IValidators { mapping(address => bool) private _isVoting; mapping(address => uint256) private numGroupMembers; mapping(address => uint256) private lockedGoldRequirements; + mapping(address => bool) private doesNotMeetAccountLockedGoldRequirements; mapping(address => address[]) private members; uint256 private numRegisteredValidators; - function meetsAccountLockedGoldRequirements(address) external view returns (bool) { - return true; + function setDoesNotMeetAccountLockedGoldRequirements(address account) external { + doesNotMeetAccountLockedGoldRequirements[account] = true; + } + + function meetsAccountLockedGoldRequirements(address account) external view returns (bool) { + return !doesNotMeetAccountLockedGoldRequirements[account]; } function isValidating(address account) external view returns (bool) { diff --git a/packages/protocol/test/governance/election.ts b/packages/protocol/test/governance/election.ts index 815c1131dca..94f82dfb826 100644 --- a/packages/protocol/test/governance/election.ts +++ b/packages/protocol/test/governance/election.ts @@ -906,7 +906,6 @@ contract('Election', (accounts: string[]) => { const voteValue1 = new BigNumber(2000000) const voteValue2 = new BigNumber(1000000) const totalRewardValue = new BigNumber(3000000) - const lockedGoldRequirement = new BigNumber(1000000) beforeEach(async () => { await registry.setAddressFor(CeloContractName.Validators, accounts[0]) await election.markGroupEligible(group1, NULL_ADDRESS, NULL_ADDRESS) @@ -919,8 +918,6 @@ contract('Election', (accounts: string[]) => { await mockLockedGold.incrementNonvotingAccountBalance(voter, voteValue1.plus(voteValue2)) await election.vote(group1, voteValue1, group2, NULL_ADDRESS) await election.vote(group2, voteValue2, NULL_ADDRESS, group1) - await mockValidators.setAccountLockedGoldRequirement(group1, lockedGoldRequirement) - await mockValidators.setAccountLockedGoldRequirement(group2, lockedGoldRequirement) }) describe('when one group has active votes', () => { @@ -930,10 +927,6 @@ contract('Election', (accounts: string[]) => { }) describe('when the group meets the locked gold requirements ', () => { - beforeEach(async () => { - await mockLockedGold.setAccountTotalLockedGold(group1, lockedGoldRequirement) - }) - it('should return the total reward value', async () => { assertEqualBN( await election.getGroupEpochRewards(group1, totalRewardValue), @@ -944,7 +937,7 @@ contract('Election', (accounts: string[]) => { describe('when the group does not meet the locked gold requirements ', () => { beforeEach(async () => { - await mockLockedGold.setAccountTotalLockedGold(group1, lockedGoldRequirement.minus(1)) + await mockValidators.setDoesNotMeetAccountLockedGoldRequirements(group1) }) it('should return zero', async () => { @@ -954,7 +947,6 @@ contract('Election', (accounts: string[]) => { }) describe('when two groups have active votes', () => { - const lockedGoldRequirement = new BigNumber(1000000) const expectedGroup1EpochRewards = voteValue1 .div(voteValue1.plus(voteValue2)) .times(totalRewardValue) @@ -965,30 +957,26 @@ contract('Election', (accounts: string[]) => { await election.activate(group2) }) - describe('when one group meets the locked gold requirements ', () => { + describe('when one group does not meet the locked gold requirements ', () => { beforeEach(async () => { - await mockLockedGold.setAccountTotalLockedGold(group1, lockedGoldRequirement) + await mockValidators.setDoesNotMeetAccountLockedGoldRequirements(group2) + }) + + it('should return zero for that group', async () => { + assertEqualBN(await election.getGroupEpochRewards(group2, totalRewardValue), 0) }) - it('should return the proportional reward value for that group', async () => { + it('should return the proportional reward value for the other group', async () => { assertEqualBN( await election.getGroupEpochRewards(group1, totalRewardValue), expectedGroup1EpochRewards ) }) - - it('should return zero for the other group', async () => { - assertEqualBN(await election.getGroupEpochRewards(group2, totalRewardValue), 0) - }) }) }) describe('when the group does not have active votes', () => { describe('when the group meets the locked gold requirements ', () => { - beforeEach(async () => { - await mockLockedGold.setAccountTotalLockedGold(group1, lockedGoldRequirement) - }) - it('should return zero', async () => { assertEqualBN(await election.getGroupEpochRewards(group1, totalRewardValue), 0) }) diff --git a/packages/protocol/test/governance/lockedgold.ts b/packages/protocol/test/governance/lockedgold.ts index d98e692ff9a..88c6d8276b8 100644 --- a/packages/protocol/test/governance/lockedgold.ts +++ b/packages/protocol/test/governance/lockedgold.ts @@ -15,6 +15,8 @@ import { MockElectionInstance, MockGoldTokenContract, MockGoldTokenInstance, + MockGovernanceContract, + MockGovernanceInstance, MockValidatorsContract, MockValidatorsInstance, RegistryContract, @@ -24,6 +26,7 @@ import { const LockedGold: LockedGoldContract = artifacts.require('LockedGold') const MockElection: MockElectionContract = artifacts.require('MockElection') const MockGoldToken: MockGoldTokenContract = artifacts.require('MockGoldToken') +const MockGovernance: MockGovernanceContract = artifacts.require('MockGovernance') const MockValidators: MockValidatorsContract = artifacts.require('MockValidators') const Registry: RegistryContract = artifacts.require('Registry') @@ -41,6 +44,7 @@ contract('LockedGold', (accounts: string[]) => { const unlockingPeriod = 3 * DAY let lockedGold: LockedGoldInstance let mockElection: MockElectionInstance + let mockGovernance: MockGovernanceInstance let mockValidators: MockValidatorsInstance let registry: RegistryInstance @@ -53,9 +57,11 @@ contract('LockedGold', (accounts: string[]) => { lockedGold = await LockedGold.new() mockElection = await MockElection.new() mockValidators = await MockValidators.new() + mockGovernance = await MockGovernance.new() registry = await Registry.new() - await registry.setAddressFor(CeloContractName.GoldToken, mockGoldToken.address) await registry.setAddressFor(CeloContractName.Election, mockElection.address) + await registry.setAddressFor(CeloContractName.GoldToken, mockGoldToken.address) + await registry.setAddressFor(CeloContractName.Governance, mockGovernance.address) await registry.setAddressFor(CeloContractName.Validators, mockValidators.address) await lockedGold.initialize(registry.address, unlockingPeriod) await lockedGold.createAccount() @@ -326,43 +332,57 @@ contract('LockedGold', (accounts: string[]) => { beforeEach(async () => { // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails await lockedGold.lock({ value }) - resp = await lockedGold.unlock(value) - availabilityTime = new BigNumber(unlockingPeriod).plus( - (await web3.eth.getBlock('latest')).timestamp - ) }) + describe('when the account is not voting in governance', () => { + beforeEach(async () => { + resp = await lockedGold.unlock(value) + availabilityTime = new BigNumber(unlockingPeriod).plus( + (await web3.eth.getBlock('latest')).timestamp + ) + }) - it('should add a pending withdrawal', async () => { - const [values, timestamps] = await lockedGold.getPendingWithdrawals(account) - assert.equal(values.length, 1) - assert.equal(timestamps.length, 1) - assertEqualBN(values[0], value) - assertEqualBN(timestamps[0], availabilityTime) - }) + it('should add a pending withdrawal', async () => { + const [values, timestamps] = await lockedGold.getPendingWithdrawals(account) + assert.equal(values.length, 1) + assert.equal(timestamps.length, 1) + assertEqualBN(values[0], value) + assertEqualBN(timestamps[0], availabilityTime) + }) - it("should decrease the account's nonvoting locked gold balance", async () => { - assertEqualBN(await lockedGold.getAccountNonvotingLockedGold(account), 0) - }) + it("should decrease the account's nonvoting locked gold balance", async () => { + assertEqualBN(await lockedGold.getAccountNonvotingLockedGold(account), 0) + }) - it("should decrease the account's total locked gold balance", async () => { - assertEqualBN(await lockedGold.getAccountTotalLockedGold(account), 0) - }) + it("should decrease the account's total locked gold balance", async () => { + assertEqualBN(await lockedGold.getAccountTotalLockedGold(account), 0) + }) - it('should decrease the nonvoting locked gold balance', async () => { - assertEqualBN(await lockedGold.getNonvotingLockedGold(), 0) - }) + it('should decrease the nonvoting locked gold balance', async () => { + assertEqualBN(await lockedGold.getNonvotingLockedGold(), 0) + }) - it('should decrease the total locked gold balance', async () => { - assertEqualBN(await lockedGold.getTotalLockedGold(), 0) + it('should decrease the total locked gold balance', async () => { + assertEqualBN(await lockedGold.getTotalLockedGold(), 0) + }) + + it('should emit a GoldUnlocked event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertLogMatches(log, 'GoldUnlocked', { + account, + value: new BigNumber(value), + available: availabilityTime, + }) + }) }) - it('should emit a GoldUnlocked event', async () => { - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches(log, 'GoldUnlocked', { - account, - value: new BigNumber(value), - available: availabilityTime, + describe('when the account is voting in governance', () => { + beforeEach(async () => { + await mockGovernance.setVoting(account) + }) + + it('should revert', async () => { + await assertRevert(lockedGold.unlock(value)) }) }) }) diff --git a/packages/protocol/test/governance/validators.ts b/packages/protocol/test/governance/validators.ts index fc1c1e25dc3..6ea4cc15ef5 100644 --- a/packages/protocol/test/governance/validators.ts +++ b/packages/protocol/test/governance/validators.ts @@ -11,10 +11,10 @@ import { } from '@celo/protocol/lib/test-utils' import BigNumber from 'bignumber.js' import { - MockLockedGoldContract, - MockLockedGoldInstance, MockElectionContract, MockElectionInstance, + MockLockedGoldContract, + MockLockedGoldInstance, MockStableTokenContract, MockStableTokenInstance, RegistryContract, @@ -25,8 +25,8 @@ import { import { fromFixed, toFixed } from '@celo/utils/lib/fixidity' const Validators: ValidatorsTestContract = artifacts.require('ValidatorsTest') -const MockLockedGold: MockLockedGoldContract = artifacts.require('MockLockedGold') const MockElection: MockElectionContract = artifacts.require('MockElection') +const MockLockedGold: MockLockedGoldContract = artifacts.require('MockLockedGold') const MockStableToken: MockStableTokenContract = artifacts.require('MockStableToken') const Registry: RegistryContract = artifacts.require('Registry') @@ -69,8 +69,8 @@ const EPOCH = 100 contract('Validators', (accounts: string[]) => { let validators: ValidatorsTestInstance let registry: RegistryInstance - let mockLockedGold: MockLockedGoldInstance let mockElection: MockElectionInstance + let mockLockedGold: MockLockedGoldInstance const nonOwner = accounts[1] const validatorLockedGoldRequirements = { @@ -101,11 +101,11 @@ contract('Validators', (accounts: string[]) => { const commission = toFixed(1 / 100) beforeEach(async () => { validators = await Validators.new() - mockLockedGold = await MockLockedGold.new() - mockElection = await MockElection.new() registry = await Registry.new() - await registry.setAddressFor(CeloContractName.LockedGold, mockLockedGold.address) + mockElection = await MockElection.new() + mockLockedGold = await MockLockedGold.new() await registry.setAddressFor(CeloContractName.Election, mockElection.address) + await registry.setAddressFor(CeloContractName.LockedGold, mockLockedGold.address) await validators.initialize( registry.address, groupLockedGoldRequirements.value, @@ -1203,7 +1203,7 @@ contract('Validators', (accounts: string[]) => { describe('when it has been less than `groupLockedGoldRequirements.duration` since the validator was removed from the group', () => { beforeEach(async () => { - await timeTravel(groupLockedGoldRequirements.duration.toNumber().minus(1), web3) + await timeTravel(groupLockedGoldRequirements.duration.minus(1).toNumber(), web3) }) it('should revert', async () => { From 19488c7ead2a7cff5d91e3f4b7375b0bdb0b666c Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Tue, 22 Oct 2019 16:32:29 -0700 Subject: [PATCH 081/149] Re-add isVoting tests --- packages/protocol/test/governance/governance.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/protocol/test/governance/governance.ts b/packages/protocol/test/governance/governance.ts index d4e11d534f7..48d06c24d42 100644 --- a/packages/protocol/test/governance/governance.ts +++ b/packages/protocol/test/governance/governance.ts @@ -1872,7 +1872,6 @@ contract('Governance', (accounts: string[]) => { }) }) - /* describe('#isVoting()', () => { describe('when the account has never acted on a proposal', () => { it('should return false', async () => { @@ -1955,7 +1954,6 @@ contract('Governance', (accounts: string[]) => { }) }) }) - */ describe('#isProposalPassing()', () => { const proposalId = 1 From e3e3809888df5b2e88643ae6a7c44d9268cfa090 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Tue, 22 Oct 2019 16:34:58 -0700 Subject: [PATCH 082/149] Small cleanup --- packages/protocol/contracts/governance/Validators.sol | 9 --------- 1 file changed, 9 deletions(-) diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index 8c59a61e3e6..d73593323f7 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -945,15 +945,6 @@ contract Validators is } /** - * Tail: 0 - * numEntries: 0 - * index: 0 - * - * Tail: 0 - * numEntries: 1 - * index: 1 - * - * * @notice Updates the group membership history of a particular account. * @param account The account whose group membership has changed. * @param group The group that the account is now a member of. From e39ff0a9743f43d371968ec0f8759e2695de05ae Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Wed, 23 Oct 2019 09:44:08 -0700 Subject: [PATCH 083/149] Clean up election setters --- .../contracts/governance/Election.sol | 44 ++++--------------- 1 file changed, 8 insertions(+), 36 deletions(-) diff --git a/packages/protocol/contracts/governance/Election.sol b/packages/protocol/contracts/governance/Election.sol index 69bc33d1fc1..43a1361b564 100644 --- a/packages/protocol/contracts/governance/Election.sol +++ b/packages/protocol/contracts/governance/Election.sol @@ -151,9 +151,9 @@ contract Election is { _transferOwnership(msg.sender); setRegistry(registryAddress); - _setElectableValidators(minElectableValidators, maxElectableValidators); - _setMaxNumGroupsVotedFor(_maxNumGroupsVotedFor); - _setElectabilityThreshold(_electabilityThreshold); + setElectableValidators(minElectableValidators, maxElectableValidators); + setMaxNumGroupsVotedFor(_maxNumGroupsVotedFor); + setElectabilityThreshold(_electabilityThreshold); } /** @@ -163,7 +163,11 @@ contract Election is * @return True upon success. */ function setElectableValidators(uint256 min, uint256 max) external onlyOwner returns (bool) { - return _setElectableValidators(min, max); + require(0 < min && min <= max); + require(min != electableValidators.min || max != electableValidators.max); + electableValidators = ElectableValidators(min, max); + emit ElectableValidatorsSet(min, max); + return true; } /** @@ -174,20 +178,6 @@ contract Election is return (electableValidators.min, electableValidators.max); } - /** - * @notice Updates the minimum and maximum number of validators that can be elected. - * @param min The minimum number of validators that can be elected. - * @param max The maximum number of validators that can be elected. - * @return True upon success. - */ - function _setElectableValidators(uint256 min, uint256 max) private returns (bool) { - require(0 < min && min <= max); - require(min != electableValidators.min || max != electableValidators.max); - electableValidators = ElectableValidators(min, max); - emit ElectableValidatorsSet(min, max); - return true; - } - /** * @notice Updates the maximum number of groups an account can be voting for at once. * @param _maxNumGroupsVotedFor The maximum number of groups an account can vote for. @@ -200,15 +190,6 @@ contract Election is onlyOwner returns (bool) { - return _setMaxNumGroupsVotedFor(_maxNumGroupsVotedFor); - } - - /** - * @notice Updates the maximum number of groups an account can be voting for at once. - * @param _maxNumGroupsVotedFor The maximum number of groups an account can vote for. - * @return True upon success. - */ - function _setMaxNumGroupsVotedFor(uint256 _maxNumGroupsVotedFor) private returns (bool) { require(_maxNumGroupsVotedFor != maxNumGroupsVotedFor); maxNumGroupsVotedFor = _maxNumGroupsVotedFor; emit MaxNumGroupsVotedForSet(_maxNumGroupsVotedFor); @@ -221,15 +202,6 @@ contract Election is * @return True upon success. */ function setElectabilityThreshold(uint256 threshold) public onlyOwner returns (bool) { - return _setElectabilityThreshold(threshold); - } - - /** - * @notice Sets the electability threshold. - * @param threshold Electability threshold as unwrapped Fraction. - * @return True upon success. - */ - function _setElectabilityThreshold(uint256 threshold) private returns (bool) { electabilityThreshold = FixidityLib.wrap(threshold); require( electabilityThreshold.lt(FixidityLib.fixed1()), From 5a43d3f46593dd27e644cc2089ca35dcd6af6a9e Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Wed, 23 Oct 2019 16:59:45 -0700 Subject: [PATCH 084/149] Add first pass at epoch rewards --- .../contracts/common/UsingRegistry.sol | 5 + .../contracts/governance/Election.sol | 12 +- .../contracts/governance/EpochRewards.sol | 184 ++++++++++++++++++ .../contracts/governance/Validators.sol | 30 +-- .../governance/interfaces/IElection.sol | 1 + .../governance/proxies/EpochRewardsProxy.sol | 8 + .../governance/test/EpochRewardsTest.sol | 30 +++ .../governance/test/MockElection.sol | 4 + .../governance/test/ValidatorsTest.sol | 4 +- .../contracts/stability/SortedOracles.sol | 1 + packages/protocol/lib/registry-utils.ts | 1 + packages/protocol/migrations/11_validators.ts | 1 - .../protocol/migrations/13_epoch_rewards.ts | 21 ++ .../migrations/{13_random.ts => 14_random.ts} | 0 ...{14_attestations.ts => 15_attestations.ts} | 0 ...kchainparams.ts => 16_blockchainparams.ts} | 0 .../migrations/{15_escrow.ts => 17_escrow.ts} | 0 .../{16_governance.ts => 18_governance.ts} | 0 ...t_validators.ts => 19_elect_validators.ts} | 0 packages/protocol/migrationsConfig.js | 6 +- .../protocol/test/governance/epochrewards.ts | 163 ++++++++++++++++ .../protocol/test/governance/validators.ts | 77 ++------ 22 files changed, 463 insertions(+), 85 deletions(-) create mode 100644 packages/protocol/contracts/governance/EpochRewards.sol create mode 100644 packages/protocol/contracts/governance/proxies/EpochRewardsProxy.sol create mode 100644 packages/protocol/contracts/governance/test/EpochRewardsTest.sol create mode 100644 packages/protocol/migrations/13_epoch_rewards.ts rename packages/protocol/migrations/{13_random.ts => 14_random.ts} (100%) rename packages/protocol/migrations/{14_attestations.ts => 15_attestations.ts} (100%) rename packages/protocol/migrations/{15_blockchainparams.ts => 16_blockchainparams.ts} (100%) rename packages/protocol/migrations/{15_escrow.ts => 17_escrow.ts} (100%) rename packages/protocol/migrations/{16_governance.ts => 18_governance.ts} (100%) rename packages/protocol/migrations/{17_elect_validators.ts => 19_elect_validators.ts} (100%) create mode 100644 packages/protocol/test/governance/epochrewards.ts diff --git a/packages/protocol/contracts/common/UsingRegistry.sol b/packages/protocol/contracts/common/UsingRegistry.sol index 84cfa8fad2b..45ede8446ba 100644 --- a/packages/protocol/contracts/common/UsingRegistry.sol +++ b/packages/protocol/contracts/common/UsingRegistry.sol @@ -12,6 +12,7 @@ import "../governance/interfaces/IValidators.sol"; import "../identity/interfaces/IRandom.sol"; +import "../stability/interfaces/ISortedOracles.sol"; import "../stability/interfaces/IStableToken.sol"; // Ideally, UsingRegistry should inherit from Initializable and implement initialize() which calls @@ -76,6 +77,10 @@ contract UsingRegistry is Ownable { return IRandom(registry.getAddressForOrDie(RANDOM_REGISTRY_ID)); } + function getSortedOracles() internal view returns (ISortedOracles) { + return ISortedOracles(registry.getAddressForOrDie(SORTED_ORACLES_REGISTRY_ID)); + } + function getStableToken() internal view returns (IStableToken) { return IStableToken(registry.getAddressForOrDie(STABLE_TOKEN_REGISTRY_ID)); } diff --git a/packages/protocol/contracts/governance/Election.sol b/packages/protocol/contracts/governance/Election.sol index 43a1361b564..b85a3b6ec84 100644 --- a/packages/protocol/contracts/governance/Election.sol +++ b/packages/protocol/contracts/governance/Election.sol @@ -162,7 +162,7 @@ contract Election is * @param max The maximum number of validators that can be elected. * @return True upon success. */ - function setElectableValidators(uint256 min, uint256 max) external onlyOwner returns (bool) { + function setElectableValidators(uint256 min, uint256 max) public onlyOwner returns (bool) { require(0 < min && min <= max); require(min != electableValidators.min || max != electableValidators.max); electableValidators = ElectableValidators(min, max); @@ -186,7 +186,7 @@ contract Election is function setMaxNumGroupsVotedFor( uint256 _maxNumGroupsVotedFor ) - external + public onlyOwner returns (bool) { @@ -756,6 +756,14 @@ contract Election is return votes.active.total.add(votes.pending.total); } + /** + * @notice Returns the active votes received across all groups. + * @return The active votes received across all groups. + */ + function getActiveVotes() public view returns (uint256) { + return votes.active.total; + } + /** * @notice Returns the list of validator groups eligible to elect validators. * @return The list of validator groups eligible to elect validators. diff --git a/packages/protocol/contracts/governance/EpochRewards.sol b/packages/protocol/contracts/governance/EpochRewards.sol new file mode 100644 index 00000000000..f9f1d68412e --- /dev/null +++ b/packages/protocol/contracts/governance/EpochRewards.sol @@ -0,0 +1,184 @@ +pragma solidity ^0.5.3; + +import "openzeppelin-solidity/contracts/math/SafeMath.sol"; +import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; + +import "../common/FixidityLib.sol"; +import "../common/Initializable.sol"; +import "../common/UsingRegistry.sol"; +import "../common/UsingPrecompiles.sol"; + +/** + * @title Contract for calculating epoch rewards. + */ +contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry { + + using FixidityLib for FixidityLib.Fraction; + using SafeMath for uint256; + + uint256 constant GENESIS_GOLD_SUPPLY = 600000000; + uint256 constant GOLD_SUPPLY_CAP = 1000000000; + uint256 constant YEARS_LINEAR = 15; + uint256 constant SECONDS_LINEAR = YEARS_LINEAR * 365 * 1 days; + uint256 constant FIXIDITY_E = 2718281828459045235360287; + uint256 constant FIXIDITY_LN2 = 693147180559945309417232; + uint256 private startTime = 0; + FixidityLib.Fraction private targetVotingGoldFraction; + FixidityLib.Fraction private targetVotingYield; + FixidityLib.Fraction private maxTargetVotingYield; + FixidityLib.Fraction private targetVotingYieldAdjustmentFactor; + uint256 public maxValidatorEpochPayment; + + event MaxValidatorEpochPaymentSet(uint256 payment); + event MaxTargetVotingYieldSet(uint256 yield); + + /** + * @param _maxValidatorEpochPayment The duration the above gold remains locked after deregistration. + */ + function initialize( + address registryAddress, + uint256 _maxValidatorEpochPayment, + uint256 _maxTargetVotingYield, + uint256 _targetVotingYield + ) + external + initializer + { + _transferOwnership(msg.sender); + setRegistry(registryAddress); + setMaxTargetVotingYield(_maxTargetVotingYield); + setMaxValidatorEpochPayment(_maxValidatorEpochPayment); + targetVotingYield = FixidityLib.wrap(_targetVotingYield); + startTime = now; + } + + function getMaxTargetVotingYield() external view returns (uint256) { + return maxTargetVotingYield.unwrap(); + } + + function getTargetVotingYield() external view returns (uint256) { + return targetVotingYield.unwrap(); + } + + /** + m* @notice Sets the max per-epoch payment in Celo Dollars for validators. + * @param value The value in Celo Dollars. + * @return True upon success. + */ + function setMaxValidatorEpochPayment(uint256 value) public onlyOwner returns (bool) { + require(value != maxValidatorEpochPayment); + maxValidatorEpochPayment = value; + emit MaxValidatorEpochPaymentSet(value); + return true; + } + + function setMaxTargetVotingYield(uint256 yield) public onlyOwner returns (bool) { + require(yield != maxTargetVotingYield.unwrap()); + maxTargetVotingYield = FixidityLib.wrap(yield); + require( + maxTargetVotingYield.lt(FixidityLib.fixed1()), + "Max voting yield must be lower than 100%" + ); + emit MaxTargetVotingYieldSet(yield); + return true; + } + + function _getTargetGoldTotalSupply() internal view returns (uint256) { + uint256 timeSinceInitialization = now.sub(startTime); + uint256 targetGoldSupply = 0; + if (timeSinceInitialization < SECONDS_LINEAR) { + // Pay out half of all block rewards linearly. + uint256 linearRewards = GOLD_SUPPLY_CAP.sub(GENESIS_GOLD_SUPPLY).div(2); + uint256 targetRewards = linearRewards.mul(timeSinceInitialization).div(SECONDS_LINEAR); + return targetRewards.add(GENESIS_GOLD_SUPPLY); + } else { + /* + FixidityLib.Fraction memory exponentialDecaryHalfLife = FixidityLib.wrap( + FIXIDITY_LN2.div(YEARS_LINEAR) + ); + uint256 exponentialSeconds = timeSinceInitialization.sub(SECONDS_LINEAR); + uint256 exponentialRewards = GOLD_SUPPLY_CAP.sub(GENESIS_GOLD_SUPPLY).div(2); + // 1000 - 200 * e ^ ((- 1 / 15) * (x - 15)) + (uint256 numerator, uint256 denominator) = fractionMulExp(FixidityLib.fixed1(), FixidityLib.fixed1(), FIXIDITY_E, FixidityLib.fixed1(), ???, ???); + */ + // TODO(asa): This isn't implemented. + require(false); + return 0; + } + } + + // TODO(asa): Finish this. + function _getRewardsMultiplier(uint256 targetGoldSupplyIncrease) internal view returns (FixidityLib.Fraction memory) { + uint256 targetSupply = _getTargetGoldTotalSupply(); + uint256 totalSupplyWithRewards = getGoldToken().totalSupply().add(targetGoldSupplyIncrease); + if (totalSupplyWithRewards > targetSupply) { + // uint256 delta = totalSupplyWithRewards.sub(targetSupply); + return FixidityLib.fixed1(); + + // FixidityLib.Fraction memory deviation = FixidityLib.newFixed(delta). + /* + B_actual_t = supply_cap - (totalSupplyWithRewards); + B_target_t = supply_cap - (targetSupply); + B_actual_t - Z_t = B_actual_t - targetRewards - + */ + } else if (totalSupplyWithRewards < targetSupply) { + // uint256 delta = targetSupply.sub(totalSupplyWithRewards); + return FixidityLib.fixed1(); + } else { + return FixidityLib.fixed1(); + } + } + + function _getTargetEpochRewards() internal view returns (uint256) { + return FixidityLib.newFixed(getElection().getActiveVotes()).multiply(targetVotingYield).fromFixed(); + } + + function _getTargetTotalEpochPaymentsInGold() internal view returns (uint256) { + address stableTokenAddress = registry.getAddressForOrDie(STABLE_TOKEN_REGISTRY_ID); + (uint256 numerator, uint256 denominator) = getSortedOracles().medianRate(stableTokenAddress); + uint256 targetEpochPayment = numberValidatorsInCurrentSet().mul(maxValidatorEpochPayment).mul(numerator).div(denominator); + return targetEpochPayment; + } + + function _updateTargetVotingYield() internal { + IERC20Token goldToken = getGoldToken(); + // TODO(asa): Ignore custodial accounts. + address reserveAddress = registry.getAddressForOrDie(RESERVE_REGISTRY_ID); + uint256 liquidGold = goldToken.totalSupply().sub(goldToken.balanceOf(reserveAddress)); + // TODO(asa): Should this be active votes? + uint256 votingGold = getElection().getTotalVotes(); + FixidityLib.Fraction memory votingGoldFraction = FixidityLib.newFixed(liquidGold).divide(FixidityLib.newFixed(votingGold)); + if (votingGoldFraction.gt(targetVotingGoldFraction)) { + FixidityLib.Fraction memory votingGoldFractionDelta = votingGoldFraction.subtract(targetVotingGoldFraction); + FixidityLib.Fraction memory targetVotingYieldDelta = votingGoldFractionDelta.multiply(targetVotingYieldAdjustmentFactor); + if (targetVotingYieldDelta.gte(targetVotingYield)) { + targetVotingYield = FixidityLib.newFixed(0); + } else { + targetVotingYield = targetVotingYield.subtract(targetVotingYieldDelta); + } + } else { + FixidityLib.Fraction memory votingGoldFractionDelta = targetVotingGoldFraction.subtract(votingGoldFraction); + FixidityLib.Fraction memory targetVotingYieldDelta = votingGoldFractionDelta.multiply(targetVotingYieldAdjustmentFactor); + targetVotingYield = targetVotingYield.add(targetVotingYieldDelta); + if (targetVotingYield.gt(maxTargetVotingYield)) { + targetVotingYield = maxTargetVotingYield; + } + } + } + + function updateTargetVotingYield() external { + require(msg.sender == address(0)); + _updateTargetVotingYield(); + } + + function calculateTargetEpochPaymentAndRewards() external view returns (uint256, uint256) { + uint256 targetEpochRewards = _getTargetEpochRewards(); + uint256 targetTotalEpochPaymentsInGold = _getTargetTotalEpochPaymentsInGold(); + uint256 targetGoldSupplyIncrease = targetEpochRewards.add(targetTotalEpochPaymentsInGold); + FixidityLib.Fraction memory rewardsMultiplier = _getRewardsMultiplier(targetGoldSupplyIncrease); + return ( + FixidityLib.newFixed(maxValidatorEpochPayment).multiply(rewardsMultiplier).fromFixed(), + FixidityLib.newFixed(targetEpochRewards).multiply(rewardsMultiplier).fromFixed() + ); + } +} diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index bd1070cf6ce..bb22f1e140e 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -102,7 +102,6 @@ contract Validators is LockedGoldRequirements public validatorLockedGoldRequirements; LockedGoldRequirements public groupLockedGoldRequirements; ValidatorScoreParameters private validatorScoreParameters; - uint256 public validatorEpochPayment; uint256 public membershipHistoryLength; uint256 public maxGroupSize; @@ -136,7 +135,6 @@ contract Validators is * @param validatorRequirementDuration The Locked Gold requirement duration for validators. * @param validatorScoreExponent The exponent used in calculating validator scores. * @param validatorScoreAdjustmentSpeed The speed at which validator scores are adjusted. - * @param _validatorEpochPayment The duration the above gold remains locked after deregistration. * @param _membershipHistoryLength The max number of entries for validator membership history. * @param _maxGroupSize The maximum group size. * @dev Should be called only once. @@ -149,7 +147,6 @@ contract Validators is uint256 validatorRequirementDuration, uint256 validatorScoreExponent, uint256 validatorScoreAdjustmentSpeed, - uint256 _validatorEpochPayment, uint256 _membershipHistoryLength, uint256 _maxGroupSize ) @@ -162,7 +159,6 @@ contract Validators is setValidatorLockedGoldRequirements(validatorRequirementValue, validatorRequirementDuration); setValidatorScoreParameters(validatorScoreExponent, validatorScoreAdjustmentSpeed); setMaxGroupSize(_maxGroupSize); - setValidatorEpochPayment(_validatorEpochPayment); setMembershipHistoryLength(_membershipHistoryLength); } @@ -190,18 +186,6 @@ contract Validators is return true; } - /** - * @notice Sets the per-epoch payment in Celo Dollars for validators, less group commission. - * @param value The value in Celo Dollars. - * @return True upon success. - */ - function setValidatorEpochPayment(uint256 value) public onlyOwner returns (bool) { - require(value != validatorEpochPayment); - validatorEpochPayment = value; - emit ValidatorEpochPaymentSet(value); - return true; - } - /** * @notice Updates the validator score parameters. * @param exponent The exponent used in calculating the score. @@ -407,14 +391,14 @@ contract Validators is /** * @notice Distributes epoch payments to `validator` and its group. */ - function distributeEpochPayment(address validator) external onlyVm() { - _distributeEpochPayment(validator); + function distributeEpochPayment(address validator, uint256 maxPayment) external onlyVm() returns (uint256) { + return _distributeEpochPayment(validator, maxPayment); } /** * @notice Distributes epoch payments to `validator` and its group. */ - function _distributeEpochPayment(address validator) internal { + function _distributeEpochPayment(address validator, uint256 maxPayment) internal returns (uint256) { address account = getLockedGold().getAccountFromValidator(validator); require(isValidator(account)); // The group that should be paid is the group that the validator was a member of at the @@ -423,14 +407,16 @@ contract Validators is // Both the validator and the group must maintain the minimum locked gold balance in order to // receive epoch payments. if (meetsAccountLockedGoldRequirements(account) && meetsAccountLockedGoldRequirements(group)) { - FixidityLib.Fraction memory totalPayment = FixidityLib.newFixed( - validatorEpochPayment - ).multiply(validators[account].score); + FixidityLib.Fraction memory totalPayment = FixidityLib.newFixed(maxPayment).multiply( + validators[account].score + ); uint256 groupPayment = totalPayment.multiply(groups[group].commission).fromFixed(); uint256 validatorPayment = totalPayment.fromFixed().sub(groupPayment); getStableToken().mint(group, groupPayment); getStableToken().mint(account, validatorPayment); + return totalPayment.fromFixed(); } + return 0; } /** diff --git a/packages/protocol/contracts/governance/interfaces/IElection.sol b/packages/protocol/contracts/governance/interfaces/IElection.sol index caa2df9a882..ec4f76ee62a 100644 --- a/packages/protocol/contracts/governance/interfaces/IElection.sol +++ b/packages/protocol/contracts/governance/interfaces/IElection.sol @@ -3,6 +3,7 @@ pragma solidity ^0.5.3; interface IElection { function getTotalVotes() external view returns (uint256); + function getActiveVotes() external view returns (uint256); function getTotalVotesByAccount(address) external view returns (uint256); function markGroupIneligible(address) external; function markGroupEligible(address,address,address) external; diff --git a/packages/protocol/contracts/governance/proxies/EpochRewardsProxy.sol b/packages/protocol/contracts/governance/proxies/EpochRewardsProxy.sol new file mode 100644 index 00000000000..205f71fdb56 --- /dev/null +++ b/packages/protocol/contracts/governance/proxies/EpochRewardsProxy.sol @@ -0,0 +1,8 @@ +pragma solidity ^0.5.3; + +import "../../common/Proxy.sol"; + + +/* solhint-disable no-empty-blocks */ +contract EpochRewardsProxy is Proxy { +} diff --git a/packages/protocol/contracts/governance/test/EpochRewardsTest.sol b/packages/protocol/contracts/governance/test/EpochRewardsTest.sol new file mode 100644 index 00000000000..76a00de1c4a --- /dev/null +++ b/packages/protocol/contracts/governance/test/EpochRewardsTest.sol @@ -0,0 +1,30 @@ +pragma solidity ^0.5.8; + +import "../EpochRewards.sol"; +import "../../common/FixidityLib.sol"; + +/** + * @title A wrapper around EpochRewards that exposes internal functions for testing. + */ +contract EpochRewardsTest is EpochRewards { + + function getTargetGoldTotalSupply() external view returns (uint256) { + return _getTargetGoldTotalSupply(); + } + + function getTargetTotalEpochPaymentsInGold() external view returns (uint256) { + return _getTargetTotalEpochPaymentsInGold(); + } + + function getTargetEpochRewards() external view returns (uint256) { + return _getTargetEpochRewards(); + } + + function getRewardsMultiplier(uint256 targetGoldTotalSupplyIncrease) external view returns (uint256) { + return _getRewardsMultiplier(targetGoldTotalSupplyIncrease).unwrap(); + } + + function updateTargetVotingYield() external { + _updateTargetVotingYield(); + } +} diff --git a/packages/protocol/contracts/governance/test/MockElection.sol b/packages/protocol/contracts/governance/test/MockElection.sol index 47f46d42ca3..091b6f753dd 100644 --- a/packages/protocol/contracts/governance/test/MockElection.sol +++ b/packages/protocol/contracts/governance/test/MockElection.sol @@ -23,6 +23,10 @@ contract MockElection is IElection { return 0; } + function getActiveVotes() external view returns (uint256) { + return 0; + } + function getTotalVotesByAccount(address) external view returns (uint256) { return 0; } diff --git a/packages/protocol/contracts/governance/test/ValidatorsTest.sol b/packages/protocol/contracts/governance/test/ValidatorsTest.sol index beefe62389e..dc017bd6092 100644 --- a/packages/protocol/contracts/governance/test/ValidatorsTest.sol +++ b/packages/protocol/contracts/governance/test/ValidatorsTest.sol @@ -12,7 +12,7 @@ contract ValidatorsTest is Validators { return _updateValidatorScore(validator, uptime); } - function distributeEpochPayment(address validator) external { - return _distributeEpochPayment(validator); + function distributeEpochPayment(address validator, uint256 maxPayment) external returns (uint256) { + return _distributeEpochPayment(validator, maxPayment); } } diff --git a/packages/protocol/contracts/stability/SortedOracles.sol b/packages/protocol/contracts/stability/SortedOracles.sol index adc6c729eee..9e569f2f422 100644 --- a/packages/protocol/contracts/stability/SortedOracles.sol +++ b/packages/protocol/contracts/stability/SortedOracles.sol @@ -138,6 +138,7 @@ contract SortedOracles is ISortedOracles, Ownable, Initializable { * @notice Updates an oracle value and the median. * @param token The address of the token for which the Celo Gold exchange rate is being reported. * @param numerator The amount of tokens equal to `denominator` Celo Gold. + * @param denominator The amount of Celo Gold equal to `numerator` tokens. * @param lesserKey The element which should be just left of the new oracle value. * @param greaterKey The element which should be just right of the new oracle value. * @dev Note that only one of `lesserKey` or `greaterKey` needs to be correct to reduce friction. diff --git a/packages/protocol/lib/registry-utils.ts b/packages/protocol/lib/registry-utils.ts index 82d3f155d0c..38e6e478235 100644 --- a/packages/protocol/lib/registry-utils.ts +++ b/packages/protocol/lib/registry-utils.ts @@ -2,6 +2,7 @@ export enum CeloContractName { Attestations = 'Attestations', BlockchainParameters = 'BlockchainParameters', Election = 'Election', + EpochRewards = 'EpochRewards', Escrow = 'Escrow', Exchange = 'Exchange', GasCurrencyWhitelist = 'GasCurrencyWhitelist', diff --git a/packages/protocol/migrations/11_validators.ts b/packages/protocol/migrations/11_validators.ts index 5f24860c230..d59b5d1fda7 100644 --- a/packages/protocol/migrations/11_validators.ts +++ b/packages/protocol/migrations/11_validators.ts @@ -13,7 +13,6 @@ const initializeArgs = async (): Promise => { config.validators.deregistrationLockups.validator, config.validators.validatorScoreParameters.exponent, toFixed(config.validators.validatorScoreParameters.adjustmentSpeed).toFixed(), - config.validators.validatorEpochPayment, config.validators.membershipHistoryLength, config.validators.maxGroupSize, ] diff --git a/packages/protocol/migrations/13_epoch_rewards.ts b/packages/protocol/migrations/13_epoch_rewards.ts new file mode 100644 index 00000000000..22448ef942a --- /dev/null +++ b/packages/protocol/migrations/13_epoch_rewards.ts @@ -0,0 +1,21 @@ +import { CeloContractName } from '@celo/protocol/lib/registry-utils' +import { deploymentForCoreContract } from '@celo/protocol/lib/web3-utils' +import { config } from '@celo/protocol/migrationsConfig' +import { toFixed } from '@celo/utils/lib/fixidity' +import { EpochRewardsInstance } from 'types' + +const initializeArgs = async (): Promise => { + return [ + config.registry.predeployedProxyAddress, + config.epochRewards.maxValidatorEpochPayment, + toFixed(config.epochRewards.maxTargetVotingYield).toFixed(), + toFixed(config.epochRewards.initialTargetVotingYield).toFixed(), + ] +} + +module.exports = deploymentForCoreContract( + web3, + artifacts, + CeloContractName.EpochRewards, + initializeArgs +) diff --git a/packages/protocol/migrations/13_random.ts b/packages/protocol/migrations/14_random.ts similarity index 100% rename from packages/protocol/migrations/13_random.ts rename to packages/protocol/migrations/14_random.ts diff --git a/packages/protocol/migrations/14_attestations.ts b/packages/protocol/migrations/15_attestations.ts similarity index 100% rename from packages/protocol/migrations/14_attestations.ts rename to packages/protocol/migrations/15_attestations.ts diff --git a/packages/protocol/migrations/15_blockchainparams.ts b/packages/protocol/migrations/16_blockchainparams.ts similarity index 100% rename from packages/protocol/migrations/15_blockchainparams.ts rename to packages/protocol/migrations/16_blockchainparams.ts diff --git a/packages/protocol/migrations/15_escrow.ts b/packages/protocol/migrations/17_escrow.ts similarity index 100% rename from packages/protocol/migrations/15_escrow.ts rename to packages/protocol/migrations/17_escrow.ts diff --git a/packages/protocol/migrations/16_governance.ts b/packages/protocol/migrations/18_governance.ts similarity index 100% rename from packages/protocol/migrations/16_governance.ts rename to packages/protocol/migrations/18_governance.ts diff --git a/packages/protocol/migrations/17_elect_validators.ts b/packages/protocol/migrations/19_elect_validators.ts similarity index 100% rename from packages/protocol/migrations/17_elect_validators.ts rename to packages/protocol/migrations/19_elect_validators.ts diff --git a/packages/protocol/migrationsConfig.js b/packages/protocol/migrationsConfig.js index 1a82380e693..ad3eafdc90f 100644 --- a/packages/protocol/migrationsConfig.js +++ b/packages/protocol/migrationsConfig.js @@ -23,6 +23,11 @@ const DefaultConfig = { maxVotesPerAccount: 3, electabilityThreshold: 1 / 100, }, + epochRewards: { + maxValidatorEpochPayment: '1000000000000000000', + maxTargetVotingYield: 2 / 10, + initialTargetVotingYield: 5 / 100, + }, exchange: { spread: 5 / 1000, reserveFraction: 1, @@ -81,7 +86,6 @@ const DefaultConfig = { exponent: 1, adjustmentSpeed: 0.1, }, - validatorEpochPayment: '1000000000000000000', membershipHistoryLength: 60, maxGroupSize: '70', diff --git a/packages/protocol/test/governance/epochrewards.ts b/packages/protocol/test/governance/epochrewards.ts new file mode 100644 index 00000000000..22f25c07bb7 --- /dev/null +++ b/packages/protocol/test/governance/epochrewards.ts @@ -0,0 +1,163 @@ +import { CeloContractName } from '@celo/protocol/lib/registry-utils' +import { assertContainSubset, assertEqualBN, assertRevert } from '@celo/protocol/lib/test-utils' +import BigNumber from 'bignumber.js' +import { + MockElectionContract, + MockElectionInstance, + EpochRewardsTestContract, + EpochRewardsTestInstance, + RegistryContract, + RegistryInstance, +} from 'types' +import { toFixed } from '@celo/utils/lib/fixidity' + +const EpochRewards: EpochRewardsTestContract = artifacts.require('EpochRewardsTest') +const MockElection: MockElectionContract = artifacts.require('MockElection') +const Registry: RegistryContract = artifacts.require('Registry') + +// @ts-ignore +// TODO(mcortesi): Use BN +EpochRewards.numberFormat = 'BigNumber' + +contract('EpochRewards', (accounts: string[]) => { + let epochRewards: EpochRewardsTestInstance + let mockElection: MockElectionInstance + let registry: RegistryInstance + const nonOwner = accounts[1] + + const maxValidatorEpochPayment = new BigNumber(10000000000000) + const maxTargetVotingYield = toFixed(new BigNumber(1 / 20)) + const initialTargetVotingYield = toFixed(new BigNumber(5 / 100)) + beforeEach(async () => { + epochRewards = await EpochRewards.new() + mockElection = await MockElection.new() + registry = await Registry.new() + await registry.setAddressFor(CeloContractName.Election, mockElection.address) + await epochRewards.initialize( + registry.address, + maxValidatorEpochPayment, + maxTargetVotingYield, + initialTargetVotingYield + ) + }) + + describe('#initialize()', () => { + it('should have set the owner', async () => { + const owner: string = await epochRewards.owner() + assert.equal(owner, accounts[0]) + }) + + it('should have set the max validator epoch payment', async () => { + assertEqualBN(await epochRewards.maxValidatorEpochPayment(), maxValidatorEpochPayment) + }) + + it('should have set the max target voting yield', async () => { + assertEqualBN(await epochRewards.getMaxTargetVotingYield(), maxTargetVotingYield) + }) + + it('should have set the target voting yield', async () => { + assertEqualBN(await epochRewards.getTargetVotingYield(), initialTargetVotingYield) + }) + + it('should not be callable again', async () => { + await assertRevert( + epochRewards.initialize( + registry.address, + maxValidatorEpochPayment, + maxTargetVotingYield, + initialTargetVotingYield + ) + ) + }) + }) + + describe('#setMaxValidatorEpochPayment()', () => { + describe('when the payment is different', () => { + const newPayment = maxValidatorEpochPayment.plus(1) + + describe('when called by the owner', () => { + let resp: any + + beforeEach(async () => { + resp = await epochRewards.setMaxValidatorEpochPayment(newPayment) + }) + + it('should set the max validator epoch payment', async () => { + assertEqualBN(await epochRewards.maxValidatorEpochPayment(), newPayment) + }) + + it('should emit the MaxValidatorEpochPaymentSet event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'MaxValidatorEpochPaymentSet', + args: { + value: new BigNumber(newPayment), + }, + }) + }) + + describe('when called by a non-owner', () => { + it('should revert', async () => { + await assertRevert( + epochRewards.setMaxValidatorEpochPayment(newPayment, { + from: nonOwner, + }) + ) + }) + }) + }) + + describe('when the payment is the same', () => { + it('should revert', async () => { + await assertRevert(epochRewards.setMaxValidatorEpochPayment(maxValidatorEpochPayment)) + }) + }) + }) + }) + + describe('#setMaxTargetVotingYield()', () => { + describe('when the yield is different', () => { + const newYield = maxTargetVotingYield.plus(1) + + describe('when called by the owner', () => { + let resp: any + + beforeEach(async () => { + resp = await epochRewards.setMaxTargetVotingYield(newYield) + }) + + it('should set the max target voting yield', async () => { + assertEqualBN(await epochRewards.getMaxTargetVotingYield(), newYield) + }) + + it('should emit the MaxTargetVotingYieldSet event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'MaxTargetVotingYieldSet', + args: { + yield: new BigNumber(newYield), + }, + }) + }) + + describe('when called by a non-owner', () => { + it('should revert', async () => { + await assertRevert( + epochRewards.setMaxTargetVotingYield(newYield, { + from: nonOwner, + }) + ) + }) + }) + }) + + describe('when the yield is the same', () => { + it('should revert', async () => { + await assertRevert(epochRewards.setMaxTargetVotingYield(maxTargetVotingYield)) + }) + }) + }) + }) +}) diff --git a/packages/protocol/test/governance/validators.ts b/packages/protocol/test/governance/validators.ts index 6ea4cc15ef5..7ec6899cbfc 100644 --- a/packages/protocol/test/governance/validators.ts +++ b/packages/protocol/test/governance/validators.ts @@ -65,7 +65,6 @@ const DAY = 24 * HOUR // Hard coded in ganache. const EPOCH = 100 -// TODO(asa): Test epoch payment distribution contract('Validators', (accounts: string[]) => { let validators: ValidatorsTestInstance let registry: RegistryInstance @@ -85,7 +84,6 @@ contract('Validators', (accounts: string[]) => { exponent: new BigNumber(5), adjustmentSpeed: toFixed(0.25), } - const validatorEpochPayment = new BigNumber(10000000000000) const membershipHistoryLength = new BigNumber(5) const maxGroupSize = new BigNumber(5) @@ -114,7 +112,6 @@ contract('Validators', (accounts: string[]) => { validatorLockedGoldRequirements.duration, validatorScoreParameters.exponent, validatorScoreParameters.adjustmentSpeed, - validatorEpochPayment, membershipHistoryLength, maxGroupSize ) @@ -172,11 +169,6 @@ contract('Validators', (accounts: string[]) => { assertEqualBN(adjustmentSpeed, validatorScoreParameters.adjustmentSpeed) }) - it('should have set the validator epoch payment', async () => { - const actual = await validators.validatorEpochPayment() - assertEqualBN(actual, validatorEpochPayment) - }) - it('should have set the membership history length', async () => { const actual = await validators.membershipHistoryLength() assertEqualBN(actual, membershipHistoryLength) @@ -197,7 +189,6 @@ contract('Validators', (accounts: string[]) => { validatorLockedGoldRequirements.duration, validatorScoreParameters.exponent, validatorScoreParameters.adjustmentSpeed, - validatorEpochPayment, membershipHistoryLength, maxGroupSize ) @@ -205,51 +196,6 @@ contract('Validators', (accounts: string[]) => { }) }) - describe('#setValidatorEpochPayment()', () => { - describe('when the payment is different', () => { - const newPayment = validatorEpochPayment.plus(1) - - describe('when called by the owner', () => { - let resp: any - - beforeEach(async () => { - resp = await validators.setValidatorEpochPayment(newPayment) - }) - - it('should set the validator epoch payment', async () => { - assertEqualBN(await validators.validatorEpochPayment(), newPayment) - }) - - it('should emit the ValidatorEpochPaymentSet event', async () => { - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'ValidatorEpochPaymentSet', - args: { - value: new BigNumber(newPayment), - }, - }) - }) - - describe('when called by a non-owner', () => { - it('should revert', async () => { - await assertRevert( - validators.setValidatorEpochPayment(newPayment, { - from: nonOwner, - }) - ) - }) - }) - }) - - describe('when the payment is the same', () => { - it('should revert', async () => { - await assertRevert(validators.setValidatorEpochPayment(validatorEpochPayment)) - }) - }) - }) - }) - describe('#setMembershipHistoryLength()', () => { describe('when the length is different', () => { const newLength = membershipHistoryLength.plus(1) @@ -1757,6 +1703,7 @@ contract('Validators', (accounts: string[]) => { describe('#distributeEpochPayment', () => { const validator = accounts[0] const group = accounts[1] + const maxPayment = new BigNumber(20122394876) let mockStableToken: MockStableTokenInstance beforeEach(async () => { await registerValidatorGroupWithMembers(group, [validator]) @@ -1765,6 +1712,7 @@ contract('Validators', (accounts: string[]) => { }) describe('when the validator score is non-zero', () => { + let ret: BigNumber const uptime = new BigNumber(0.99) const adjustmentSpeed = fromFixed(validatorScoreParameters.adjustmentSpeed) // @ts-ignore @@ -1780,7 +1728,8 @@ contract('Validators', (accounts: string[]) => { describe('when the validator and group meet the balance requirements', () => { beforeEach(async () => { - await validators.distributeEpochPayment(validator) + ret = await validators.distributeEpochPayment(validator, maxPayment).call() + await validators.distributeEpochPayment(validator, maxPayment) }) it('should pay the validator', async () => { @@ -1790,6 +1739,10 @@ contract('Validators', (accounts: string[]) => { it('should pay the group', async () => { assertEqualBN(await mockStableToken.balanceOf(group), expectedGroupPayment) }) + + it('should return the expected total payment', async () => { + assertEqualBN(ret, expectedTotalPayment) + }) }) describe('when the validator does not meet the balance requirements', () => { @@ -1798,7 +1751,8 @@ contract('Validators', (accounts: string[]) => { validator, validatorLockedGoldRequirements.value.minus(1) ) - await validators.distributeEpochPayment(validator) + ret = await validators.distributeEpochPayment(validator, maxPayment).call() + await validators.distributeEpochPayment(validator, maxPayment) }) it('should not pay the validator', async () => { @@ -1808,6 +1762,10 @@ contract('Validators', (accounts: string[]) => { it('should not pay the group', async () => { assertEqualBN(await mockStableToken.balanceOf(group), 0) }) + + it('should return zero', async () => { + assertEqualBN(ret, 0) + }) }) describe('when the group does not meet the balance requirements', () => { @@ -1816,7 +1774,8 @@ contract('Validators', (accounts: string[]) => { group, groupLockedGoldRequirements.value.minus(1) ) - await validators.distributeEpochPayment(validator) + ret = await validators.distributeEpochPayment(validator, maxPayment).call() + await validators.distributeEpochPayment(validator, maxPayment) }) it('should not pay the validator', async () => { @@ -1826,6 +1785,10 @@ contract('Validators', (accounts: string[]) => { it('should not pay the group', async () => { assertEqualBN(await mockStableToken.balanceOf(group), 0) }) + + it('should return zero', async () => { + assertEqualBN(ret, 0) + }) }) }) }) From 44d6f51a2abe02bc21590ce65fddd3944cfe3821 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Thu, 24 Oct 2019 11:07:25 -0700 Subject: [PATCH 085/149] Fix contractkit test --- packages/contractkit/src/wrappers/SortedOracles.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contractkit/src/wrappers/SortedOracles.test.ts b/packages/contractkit/src/wrappers/SortedOracles.test.ts index 349dde6d439..cc9cbb26e9f 100644 --- a/packages/contractkit/src/wrappers/SortedOracles.test.ts +++ b/packages/contractkit/src/wrappers/SortedOracles.test.ts @@ -13,7 +13,7 @@ testWithGanache('SortedOracles Wrapper', (web3) => { // from the MNEMONIC. If the MNEMONIC has changed, these will need to be reset. // To do that, look at the output of web3.eth.getAccounts(), and pick a few // addresses from that set to be oracles - const stableTokenOracles: Address[] = NetworkConfig.stableToken.priceOracleAccounts + const stableTokenOracles: Address[] = NetworkConfig.stableToken.oracles const oracleAddress = stableTokenOracles[stableTokenOracles.length - 1] const kit = newKitFromWeb3(web3) From aaf02cd0597a0a006fd65a49f7bdd35f68b7908b Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Thu, 24 Oct 2019 11:46:11 -0700 Subject: [PATCH 086/149] Remove outdated comment --- packages/protocol/contracts/governance/Election.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/protocol/contracts/governance/Election.sol b/packages/protocol/contracts/governance/Election.sol index 43a1361b564..0d0f681971d 100644 --- a/packages/protocol/contracts/governance/Election.sol +++ b/packages/protocol/contracts/governance/Election.sol @@ -88,7 +88,6 @@ contract Election is uint256 public maxNumGroupsVotedFor; // Groups must receive at least this fraction of the total votes in order to be considered in // elections. - // TODO(asa): Implement this constraint. FixidityLib.Fraction public electabilityThreshold; event ElectableValidatorsSet( From ec88d47c72fd3334d9393b1e248001f7c7c98ca4 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Thu, 24 Oct 2019 15:29:40 -0700 Subject: [PATCH 087/149] More work --- .../contracts/governance/EpochRewards.sol | 141 +++++++++++------- .../protocol/migrations/13_epoch_rewards.ts | 7 +- packages/protocol/migrationsConfig.js | 11 +- .../protocol/test/governance/epochrewards.ts | 140 +++++++++++++---- 4 files changed, 219 insertions(+), 80 deletions(-) diff --git a/packages/protocol/contracts/governance/EpochRewards.sol b/packages/protocol/contracts/governance/EpochRewards.sol index f9f1d68412e..a7889d6b50d 100644 --- a/packages/protocol/contracts/governance/EpochRewards.sol +++ b/packages/protocol/contracts/governance/EpochRewards.sol @@ -22,42 +22,63 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry uint256 constant SECONDS_LINEAR = YEARS_LINEAR * 365 * 1 days; uint256 constant FIXIDITY_E = 2718281828459045235360287; uint256 constant FIXIDITY_LN2 = 693147180559945309417232; + + struct RewardsMultiplierAdjustmentFactors { + FixidityLib.Fraction underspend; + FixidityLib.Fraction overspend; + } + + struct TargetVotingYieldParameters { + FixidityLib.Fraction target; + FixidityLib.Fraction max; + FixidityLib.Fraction adjustmentFactor; + } + uint256 private startTime = 0; + RewardsMultiplierAdjustmentFactors private rewardsMultiplierAdjustmentFactors; + TargetVotingYieldParameters private targetVotingYieldParams; FixidityLib.Fraction private targetVotingGoldFraction; - FixidityLib.Fraction private targetVotingYield; - FixidityLib.Fraction private maxTargetVotingYield; - FixidityLib.Fraction private targetVotingYieldAdjustmentFactor; uint256 public maxValidatorEpochPayment; event MaxValidatorEpochPaymentSet(uint256 payment); - event MaxTargetVotingYieldSet(uint256 yield); + event TargetVotingYieldParametersSet(uint256 max, uint256 adjustmentFactor); + event RewardsMultiplierAdjustmentFactorsSet(uint256 underspend, uint256 overspend); /** * @param _maxValidatorEpochPayment The duration the above gold remains locked after deregistration. */ function initialize( address registryAddress, - uint256 _maxValidatorEpochPayment, - uint256 _maxTargetVotingYield, - uint256 _targetVotingYield + uint256 targetVotingYieldInitial, + uint256 targetVotingYieldMax, + uint256 targetVotingYieldAdjustmentFactor, + uint256 rewardsMultiplierUnderspendAdjustmentFactor, + uint256 rewardsMultiplierOverspendAdjustmentFactor, + uint256 _maxValidatorEpochPayment ) external initializer { _transferOwnership(msg.sender); setRegistry(registryAddress); - setMaxTargetVotingYield(_maxTargetVotingYield); + setTargetVotingYieldParameters(targetVotingYieldMax, targetVotingYieldAdjustmentFactor); + setRewardsMultiplierAdjustmentFactors( + rewardsMultiplierUnderspendAdjustmentFactor, + rewardsMultiplierOverspendAdjustmentFactor + ); setMaxValidatorEpochPayment(_maxValidatorEpochPayment); - targetVotingYield = FixidityLib.wrap(_targetVotingYield); + targetVotingYieldParams.target = FixidityLib.wrap(targetVotingYieldInitial); startTime = now; } - function getMaxTargetVotingYield() external view returns (uint256) { - return maxTargetVotingYield.unwrap(); + function getTargetVotingYieldParameters() external view returns (uint256, uint256, uint256) { + TargetVotingYieldParameters storage params = targetVotingYieldParams; + return (params.target.unwrap(), params.max.unwrap(), params.adjustmentFactor.unwrap()); } - function getTargetVotingYield() external view returns (uint256) { - return targetVotingYield.unwrap(); + function getRewardsMultiplierAdjustmentFactors() external view returns (uint256, uint256) { + RewardsMultiplierAdjustmentFactors storage factors = rewardsMultiplierAdjustmentFactors; + return (factors.underspend.unwrap(), factors.overspend.unwrap()); } /** @@ -72,20 +93,36 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry return true; } - function setMaxTargetVotingYield(uint256 yield) public onlyOwner returns (bool) { - require(yield != maxTargetVotingYield.unwrap()); - maxTargetVotingYield = FixidityLib.wrap(yield); + function setRewardsMultiplierAdjustmentFactors(uint256 underspend, uint256 overspend) public onlyOwner returns (bool) { require( - maxTargetVotingYield.lt(FixidityLib.fixed1()), - "Max voting yield must be lower than 100%" + underspend != rewardsMultiplierAdjustmentFactors.underspend.unwrap() || + overspend != rewardsMultiplierAdjustmentFactors.overspend.unwrap() ); - emit MaxTargetVotingYieldSet(yield); + rewardsMultiplierAdjustmentFactors = RewardsMultiplierAdjustmentFactors( + FixidityLib.wrap(underspend), + FixidityLib.wrap(overspend) + ); + emit RewardsMultiplierAdjustmentFactorsSet(underspend, overspend); + return true; + } + + function setTargetVotingYieldParameters(uint256 max, uint256 adjustmentFactor) public onlyOwner returns (bool) { + require( + max != targetVotingYieldParams.max.unwrap() || + adjustmentFactor != targetVotingYieldParams.adjustmentFactor.unwrap() + ); + targetVotingYieldParams.max = FixidityLib.wrap(max); + targetVotingYieldParams.adjustmentFactor = FixidityLib.wrap(adjustmentFactor); + require( + targetVotingYieldParams.max.lt(FixidityLib.fixed1()), + "Max target voting yield must be lower than 100%" + ); + emit TargetVotingYieldParamsSet(max, adjustmentFactor); return true; } function _getTargetGoldTotalSupply() internal view returns (uint256) { uint256 timeSinceInitialization = now.sub(startTime); - uint256 targetGoldSupply = 0; if (timeSinceInitialization < SECONDS_LINEAR) { // Pay out half of all block rewards linearly. uint256 linearRewards = GOLD_SUPPLY_CAP.sub(GENESIS_GOLD_SUPPLY).div(2); @@ -93,44 +130,46 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry return targetRewards.add(GENESIS_GOLD_SUPPLY); } else { /* - FixidityLib.Fraction memory exponentialDecaryHalfLife = FixidityLib.wrap( - FIXIDITY_LN2.div(YEARS_LINEAR) - ); + // Pay out the remaining half according to the following rule: + // REMAINING_REWARDS = EXPONENTIAL_REWARDS * e ^ (-1*(t - SECONDS_LINEAR) / SECONDS_LINEAR) uint256 exponentialSeconds = timeSinceInitialization.sub(SECONDS_LINEAR); uint256 exponentialRewards = GOLD_SUPPLY_CAP.sub(GENESIS_GOLD_SUPPLY).div(2); - // 1000 - 200 * e ^ ((- 1 / 15) * (x - 15)) - (uint256 numerator, uint256 denominator) = fractionMulExp(FixidityLib.fixed1(), FixidityLib.fixed1(), FIXIDITY_E, FixidityLib.fixed1(), ???, ???); + // TODO(asa): FractionMulExp does not support fractional exponents. + (uint256 numerator, uint256 denominator) = fractionMulExp( + FixidityLib.fixed1(), + FixidityLib.fixed1(), + FIXIDITY_E, + FixidityLib.fixed1(), + exponentialSeconds, + SECONDS_LINEAR + ); + // Flip numerator and denominator to account for negative exponent. + uint256 remainingRewards = exponentialRewards.mul(denominator).div(numerator); + return GOLD_SUPPLY_CAP.sub(remainingRewards); */ - // TODO(asa): This isn't implemented. - require(false); return 0; } } - // TODO(asa): Finish this. function _getRewardsMultiplier(uint256 targetGoldSupplyIncrease) internal view returns (FixidityLib.Fraction memory) { uint256 targetSupply = _getTargetGoldTotalSupply(); - uint256 totalSupplyWithRewards = getGoldToken().totalSupply().add(targetGoldSupplyIncrease); - if (totalSupplyWithRewards > targetSupply) { - // uint256 delta = totalSupplyWithRewards.sub(targetSupply); - return FixidityLib.fixed1(); - - // FixidityLib.Fraction memory deviation = FixidityLib.newFixed(delta). - /* - B_actual_t = supply_cap - (totalSupplyWithRewards); - B_target_t = supply_cap - (targetSupply); - B_actual_t - Z_t = B_actual_t - targetRewards - - */ - } else if (totalSupplyWithRewards < targetSupply) { - // uint256 delta = targetSupply.sub(totalSupplyWithRewards); - return FixidityLib.fixed1(); + uint256 totalSupply = getGoldToken().totalSupply(); + uint256 remainingSupply = GOLD_SUPPLY_CAP.sub(totalSupply.add(targetGoldSupplyIncrease)); + uint256 targetRemainingSupply = GOLD_SUPPLY_CAP.sub(targetSupply); + FixidityLib.Fraction memory ratio = FixidityLib.newFixed(remainingSupply).divide(FixidityLib.newFixed(targetRemainingSupply)); + if (ratio.gt(FixidityLib.fixed1())) { + FixidityLib.Fraction memory delta = ratio.subtract(FixidityLib.fixed1()); + return delta.multiply(rewardsMultiplierAdjustmentFactors.underspend); + } else if (ratio.lt(FixidityLib.fixed1())) { + FixidityLib.Fraction memory delta = FixidityLib.fixed1().subtract(ratio); + return delta.multiply(rewardsMultiplierAdjustmentFactors.overspend); } else { return FixidityLib.fixed1(); } } function _getTargetEpochRewards() internal view returns (uint256) { - return FixidityLib.newFixed(getElection().getActiveVotes()).multiply(targetVotingYield).fromFixed(); + return FixidityLib.newFixed(getElection().getActiveVotes()).multiply(targetVotingYieldParams.target).fromFixed(); } function _getTargetTotalEpochPaymentsInGold() internal view returns (uint256) { @@ -150,18 +189,18 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry FixidityLib.Fraction memory votingGoldFraction = FixidityLib.newFixed(liquidGold).divide(FixidityLib.newFixed(votingGold)); if (votingGoldFraction.gt(targetVotingGoldFraction)) { FixidityLib.Fraction memory votingGoldFractionDelta = votingGoldFraction.subtract(targetVotingGoldFraction); - FixidityLib.Fraction memory targetVotingYieldDelta = votingGoldFractionDelta.multiply(targetVotingYieldAdjustmentFactor); - if (targetVotingYieldDelta.gte(targetVotingYield)) { - targetVotingYield = FixidityLib.newFixed(0); + FixidityLib.Fraction memory targetVotingYieldDelta = votingGoldFractionDelta.multiply(targetVotingYieldParams.adjustmentFactor); + if (targetVotingYieldDelta.gte(targetVotingYieldParams.target)) { + targetVotingYieldParams.target = FixidityLib.newFixed(0); } else { - targetVotingYield = targetVotingYield.subtract(targetVotingYieldDelta); + targetVotingYieldParams.target = targetVotingYieldParams.target.subtract(targetVotingYieldDelta); } } else { FixidityLib.Fraction memory votingGoldFractionDelta = targetVotingGoldFraction.subtract(votingGoldFraction); - FixidityLib.Fraction memory targetVotingYieldDelta = votingGoldFractionDelta.multiply(targetVotingYieldAdjustmentFactor); - targetVotingYield = targetVotingYield.add(targetVotingYieldDelta); - if (targetVotingYield.gt(maxTargetVotingYield)) { - targetVotingYield = maxTargetVotingYield; + FixidityLib.Fraction memory targetVotingYieldDelta = votingGoldFractionDelta.multiply(targetVotingYieldParams.adjustmentFactor); + targetVotingYieldParams.target = targetVotingYieldParams.target.add(targetVotingYieldDelta); + if (targetVotingYieldParams.target.gt(targetVotingYieldParams.max)) { + targetVotingYieldParams.target = targetVotingYieldParams.max; } } } diff --git a/packages/protocol/migrations/13_epoch_rewards.ts b/packages/protocol/migrations/13_epoch_rewards.ts index 22448ef942a..19355526cef 100644 --- a/packages/protocol/migrations/13_epoch_rewards.ts +++ b/packages/protocol/migrations/13_epoch_rewards.ts @@ -7,9 +7,12 @@ import { EpochRewardsInstance } from 'types' const initializeArgs = async (): Promise => { return [ config.registry.predeployedProxyAddress, + toFixed(config.epochRewards.targetVotingYieldParameters.initial).toFixed(), + toFixed(config.epochRewards.targetVotingYieldParameters.max).toFixed(), + toFixed(config.epochRewards.targetVotingYieldParameters.adjustmentFactor).toFixed(), + toFixed(config.epochRewards.rewardsMultiplierAdjustmentFactors.underspend).toFixed(), + toFixed(config.epochRewards.rewardsMultiplierAdjustmentFactors.overspend).toFixed(), config.epochRewards.maxValidatorEpochPayment, - toFixed(config.epochRewards.maxTargetVotingYield).toFixed(), - toFixed(config.epochRewards.initialTargetVotingYield).toFixed(), ] } diff --git a/packages/protocol/migrationsConfig.js b/packages/protocol/migrationsConfig.js index ad3eafdc90f..f27ebc87a88 100644 --- a/packages/protocol/migrationsConfig.js +++ b/packages/protocol/migrationsConfig.js @@ -24,9 +24,16 @@ const DefaultConfig = { electabilityThreshold: 1 / 100, }, epochRewards: { + targetVotingYieldParameters: { + initial: 5 / 100, + max: 2 / 10, + adjustmentFactor: 1, + }, + rewardsMultiplierAdjustmentFactors: { + underspend: 1 / 2, + overspend: 5, + }, maxValidatorEpochPayment: '1000000000000000000', - maxTargetVotingYield: 2 / 10, - initialTargetVotingYield: 5 / 100, }, exchange: { spread: 5 / 1000, diff --git a/packages/protocol/test/governance/epochrewards.ts b/packages/protocol/test/governance/epochrewards.ts index 22f25c07bb7..7ce8348ff21 100644 --- a/packages/protocol/test/governance/epochrewards.ts +++ b/packages/protocol/test/governance/epochrewards.ts @@ -25,9 +25,16 @@ contract('EpochRewards', (accounts: string[]) => { let registry: RegistryInstance const nonOwner = accounts[1] + const targetVotingYieldParams = { + initial: toFixed(new BigNumber(1 / 20)), + max: toFixed(new BigNumber(1 / 5)), + adjustmentFactor: toFixed(new BigNumber(1)), + } + const rewardsMultiplierAdjustments = { + underspend: toFixed(new BigNumber(1 / 2)), + overspend: toFixed(new BigNumber(5)), + } const maxValidatorEpochPayment = new BigNumber(10000000000000) - const maxTargetVotingYield = toFixed(new BigNumber(1 / 20)) - const initialTargetVotingYield = toFixed(new BigNumber(5 / 100)) beforeEach(async () => { epochRewards = await EpochRewards.new() mockElection = await MockElection.new() @@ -35,9 +42,12 @@ contract('EpochRewards', (accounts: string[]) => { await registry.setAddressFor(CeloContractName.Election, mockElection.address) await epochRewards.initialize( registry.address, - maxValidatorEpochPayment, - maxTargetVotingYield, - initialTargetVotingYield + targetVotingYieldParams.initial, + targetVotingYieldParams.max, + targetVotingYieldParams.adjustmentFactor, + rewardsMultiplierAdjustments.underspend, + rewardsMultiplierAdjustments.overspend, + maxValidatorEpochPayment ) }) @@ -51,21 +61,29 @@ contract('EpochRewards', (accounts: string[]) => { assertEqualBN(await epochRewards.maxValidatorEpochPayment(), maxValidatorEpochPayment) }) - it('should have set the max target voting yield', async () => { - assertEqualBN(await epochRewards.getMaxTargetVotingYield(), maxTargetVotingYield) + it('should have set the target voting yield parameters', async () => { + const [target, max, adjustmentFactor] = await epochRewards.getTargetVotingYieldParameters() + assertEqualBN(target, targetVotingYieldParams.initial) + assertEqualBN(max, targetVotingYieldParams.max) + assertEqualBN(adjustmentFactor, targetVotingYieldParams.adjustmentFactor) }) - it('should have set the target voting yield', async () => { - assertEqualBN(await epochRewards.getTargetVotingYield(), initialTargetVotingYield) + it('should have set the rewards multiplier adjustment factors', async () => { + const [underspend, overspend] = await epochRewards.getRewardsMultiplierAdjustmentFactors() + assertEqualBN(underspend, rewardsMultiplierAdjustments.underspend) + assertEqualBN(overspend, rewardsMultiplierAdjustments.overspend) }) it('should not be callable again', async () => { await assertRevert( epochRewards.initialize( registry.address, - maxValidatorEpochPayment, - maxTargetVotingYield, - initialTargetVotingYield + targetVotingYieldParams.initial, + targetVotingYieldParams.max, + targetVotingYieldParams.adjustmentFactor, + rewardsMultiplierAdjustments.underspend, + rewardsMultiplierAdjustments.overspend, + maxValidatorEpochPayment ) ) }) @@ -92,7 +110,7 @@ contract('EpochRewards', (accounts: string[]) => { assertContainSubset(log, { event: 'MaxValidatorEpochPaymentSet', args: { - value: new BigNumber(newPayment), + payment: newPayment, }, }) }) @@ -116,28 +134,37 @@ contract('EpochRewards', (accounts: string[]) => { }) }) - describe('#setMaxTargetVotingYield()', () => { - describe('when the yield is different', () => { - const newYield = maxTargetVotingYield.plus(1) + describe('#setRewardsMultiplierAdjustmentFactors()', () => { + describe('when the factors are different', () => { + const newFactors = { + underspend: rewardsMultiplierAdjustments.underspend.plus(1), + overspend: rewardsMultiplierAdjustments.overspend.plus(1), + } describe('when called by the owner', () => { let resp: any beforeEach(async () => { - resp = await epochRewards.setMaxTargetVotingYield(newYield) + resp = await epochRewards.setRewardsMultiplierAdjustmentFactors( + newFactors.underspend, + newFactors.overspend + ) }) - it('should set the max target voting yield', async () => { - assertEqualBN(await epochRewards.getMaxTargetVotingYield(), newYield) + it('should set the new rewards multiplier adjustment factors', async () => { + const [underspend, overspend] = await epochRewards.getRewardsMultiplierAdjustmentFactors() + assertEqualBN(underspend, newFactors.underspend) + assertEqualBN(overspend, newFactors.overspend) }) - it('should emit the MaxTargetVotingYieldSet event', async () => { + it('should emit the RewardsMultiplierAdjustmentFactorsSet event', async () => { assert.equal(resp.logs.length, 1) const log = resp.logs[0] assertContainSubset(log, { - event: 'MaxTargetVotingYieldSet', + event: 'RewardsMultiplierAdjustmentFactorsSet', args: { - yield: new BigNumber(newYield), + underspend: newFactors.underspend, + overspend: newFactors.overspend, }, }) }) @@ -145,7 +172,65 @@ contract('EpochRewards', (accounts: string[]) => { describe('when called by a non-owner', () => { it('should revert', async () => { await assertRevert( - epochRewards.setMaxTargetVotingYield(newYield, { + epochRewards.setRewardsMultiplierAdjustmentFactors( + newFactors.underspend, + newFactors.overspend, + { + from: nonOwner, + } + ) + ) + }) + }) + }) + + describe('when the parameters are the same', () => { + it('should revert', async () => { + await assertRevert( + epochRewards.setRewardsMultiplierAdjustmentFactors( + rewardsMultiplierAdjustments.underspend, + rewardsMultiplierAdjustments.overspend + ) + ) + }) + }) + }) + }) + + describe('#setTargetVotingYieldParameters()', () => { + describe('when the parameters are different', () => { + const newMax = targetVotingYieldParams.max.plus(1) + const newFactor = targetVotingYieldParams.adjustmentFactor.plus(1) + + describe('when called by the owner', () => { + let resp: any + + beforeEach(async () => { + resp = await epochRewards.setTargetVotingYieldParameters(newMax, newFactor) + }) + + it('should set the new target voting yield parameters', async () => { + const [, max, adjustmentFactor] = await epochRewards.getTargetVotingYieldParameters() + assertEqualBN(max, newMax) + assertEqualBN(adjustmentFactor, newFactor) + }) + + it('should emit the TargetVotingYieldParametersSet event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'TargetVotingYieldParametersSet', + args: { + max: newMax, + adjustmentFactor: newFactor, + }, + }) + }) + + describe('when called by a non-owner', () => { + it('should revert', async () => { + await assertRevert( + epochRewards.setTargetVotingYieldParameters(newMax, newFactor, { from: nonOwner, }) ) @@ -153,9 +238,14 @@ contract('EpochRewards', (accounts: string[]) => { }) }) - describe('when the yield is the same', () => { + describe('when the parameters are the same', () => { it('should revert', async () => { - await assertRevert(epochRewards.setMaxTargetVotingYield(maxTargetVotingYield)) + await assertRevert( + epochRewards.setTargetVotingYieldParameters( + targetVotingYieldParams.max, + targetVotingYieldParams.adjustmentFactor + ) + ) }) }) }) From 59dedeab9833705e7428306a957629d09ef744e2 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Fri, 25 Oct 2019 11:24:52 -0700 Subject: [PATCH 088/149] Fix Validators contract wrapper --- .../contractkit/src/wrappers/Validators.ts | 47 +++++++++---------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/packages/contractkit/src/wrappers/Validators.ts b/packages/contractkit/src/wrappers/Validators.ts index 74b9ffcccff..412668b6827 100644 --- a/packages/contractkit/src/wrappers/Validators.ts +++ b/packages/contractkit/src/wrappers/Validators.ts @@ -26,19 +26,14 @@ export interface ValidatorGroup { commission: BigNumber } -export interface BalanceRequirements { - group: BigNumber - validator: BigNumber -} - -export interface DeregistrationLockups { - group: BigNumber - validator: BigNumber +export interface LockedGoldRequirements { + value: BigNumber + duration: BigNumber } export interface ValidatorsConfig { - balanceRequirements: BalanceRequirements - deregistrationLockups: DeregistrationLockups + groupLockedGoldRequirements: LockedGoldRequirements + validatorLockedGoldRequirements: LockedGoldRequirements maxGroupSize: BigNumber } @@ -77,26 +72,26 @@ export class ValidatorsWrapper extends BaseWrapper { } } /** - * Returns the current registration requirements. - * @returns Group and validator registration requirements. + * Returns the Locked Gold requirements for validators. + * @returns The Locked Gold requirements for validators. */ - async getBalanceRequirements(): Promise { - const res = await this.contract.methods.getBalanceRequirements().call() + async getValidatorLockedGoldRequirements(): Promise { + const res = await this.contract.methods.getValidatorLockedGoldRequirements().call() return { - group: toBigNumber(res[0]), - validator: toBigNumber(res[1]), + value: toBigNumber(res[0]), + duration: toBigNumber(res[1]), } } /** - * Returns the lockup periods after deregistering groups and validators. - * @return The lockup periods after deregistering groups and validators. + * Returns the Locked Gold requirements for validator groups. + * @returns The Locked Gold requirements for validator groups. */ - async getDeregistrationLockups(): Promise { - const res = await this.contract.methods.getDeregistrationLockups().call() + async getGroupLockedGoldRequirements(): Promise { + const res = await this.contract.methods.getGroupLockedGoldRequirements().call() return { - group: toBigNumber(res[0]), - validator: toBigNumber(res[1]), + value: toBigNumber(res[0]), + duration: toBigNumber(res[1]), } } @@ -105,13 +100,13 @@ export class ValidatorsWrapper extends BaseWrapper { */ async getConfig(): Promise { const res = await Promise.all([ - this.getBalanceRequirements(), - this.getDeregistrationLockups(), + this.getValidatorLockedGoldRequirements(), + this.getGroupLockedGoldRequirements(), this.contract.methods.maxGroupSize().call(), ]) return { - balanceRequirements: res[0], - deregistrationLockups: res[1], + validatorLockedGoldRequirements: res[0], + groupLockedGoldRequirements: res[1], maxGroupSize: toBigNumber(res[2]), } } From 367429dcbd248232e50e19a7f9cb572d85611bc4 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Wed, 30 Oct 2019 18:45:06 -0700 Subject: [PATCH 089/149] Unit tests for EpochRewards passing --- .../test/MockGoldToken.sol | 5 + .../contracts/governance/EpochRewards.sol | 61 ++-- .../governance/test/EpochRewardsTest.sol | 4 + .../governance/test/MockElection.sol | 14 +- .../contracts/stability/SortedOracles.sol | 2 +- .../stability/test/MockSortedOracles.sol | 28 +- .../protocol/migrations/13_epoch_rewards.ts | 1 + packages/protocol/migrationsConfig.js | 1 + .../protocol/test/governance/epochrewards.ts | 322 +++++++++++++++++- 9 files changed, 385 insertions(+), 53 deletions(-) rename packages/protocol/contracts/{stability => common}/test/MockGoldToken.sol (76%) diff --git a/packages/protocol/contracts/stability/test/MockGoldToken.sol b/packages/protocol/contracts/common/test/MockGoldToken.sol similarity index 76% rename from packages/protocol/contracts/stability/test/MockGoldToken.sol rename to packages/protocol/contracts/common/test/MockGoldToken.sol index e8146625f9c..371e3ebfb4d 100644 --- a/packages/protocol/contracts/stability/test/MockGoldToken.sol +++ b/packages/protocol/contracts/common/test/MockGoldToken.sol @@ -8,6 +8,11 @@ pragma solidity ^0.5.3; contract MockGoldToken { uint8 public constant decimals = 18; + uint256 public totalSupply; + + function setTotalSupply(uint256 value) external { + totalSupply = value; + } function transfer(address, uint256) external pure returns (bool) { return true; diff --git a/packages/protocol/contracts/governance/EpochRewards.sol b/packages/protocol/contracts/governance/EpochRewards.sol index a7889d6b50d..16c822034d5 100644 --- a/packages/protocol/contracts/governance/EpochRewards.sol +++ b/packages/protocol/contracts/governance/EpochRewards.sol @@ -16,8 +16,8 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry using FixidityLib for FixidityLib.Fraction; using SafeMath for uint256; - uint256 constant GENESIS_GOLD_SUPPLY = 600000000; - uint256 constant GOLD_SUPPLY_CAP = 1000000000; + uint256 constant GENESIS_GOLD_SUPPLY = 600000000000000000000000000; + uint256 constant GOLD_SUPPLY_CAP = 1000000000000000000000000000; uint256 constant YEARS_LINEAR = 15; uint256 constant SECONDS_LINEAR = YEARS_LINEAR * 365 * 1 days; uint256 constant FIXIDITY_E = 2718281828459045235360287; @@ -40,9 +40,11 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry FixidityLib.Fraction private targetVotingGoldFraction; uint256 public maxValidatorEpochPayment; + event TargetVotingGoldFractionSet(uint256 fraction); event MaxValidatorEpochPaymentSet(uint256 payment); event TargetVotingYieldParametersSet(uint256 max, uint256 adjustmentFactor); event RewardsMultiplierAdjustmentFactorsSet(uint256 underspend, uint256 overspend); + event Debug(uint256 value, string desc); /** * @param _maxValidatorEpochPayment The duration the above gold remains locked after deregistration. @@ -54,6 +56,7 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry uint256 targetVotingYieldAdjustmentFactor, uint256 rewardsMultiplierUnderspendAdjustmentFactor, uint256 rewardsMultiplierOverspendAdjustmentFactor, + uint256 _targetVotingGoldFraction, uint256 _maxValidatorEpochPayment ) external @@ -66,6 +69,7 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry rewardsMultiplierUnderspendAdjustmentFactor, rewardsMultiplierOverspendAdjustmentFactor ); + setTargetVotingGoldFraction(_targetVotingGoldFraction); setMaxValidatorEpochPayment(_maxValidatorEpochPayment); targetVotingYieldParams.target = FixidityLib.wrap(targetVotingYieldInitial); startTime = now; @@ -81,8 +85,19 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry return (factors.underspend.unwrap(), factors.overspend.unwrap()); } + function setTargetVotingGoldFraction(uint256 value) public onlyOwner returns (bool) { + require(value != targetVotingGoldFraction.unwrap() && value < FixidityLib.fixed1().unwrap()); + targetVotingGoldFraction = FixidityLib.wrap(value); + emit TargetVotingGoldFractionSet(value); + return true; + } + + function getTargetVotingGoldFraction() external view returns (uint256) { + return targetVotingGoldFraction.unwrap(); + } + /** - m* @notice Sets the max per-epoch payment in Celo Dollars for validators. + * @notice Sets the max per-epoch payment in Celo Dollars for validators. * @param value The value in Celo Dollars. * @return True upon success. */ @@ -117,7 +132,7 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry targetVotingYieldParams.max.lt(FixidityLib.fixed1()), "Max target voting yield must be lower than 100%" ); - emit TargetVotingYieldParamsSet(max, adjustmentFactor); + emit TargetVotingYieldParametersSet(max, adjustmentFactor); return true; } @@ -129,24 +144,7 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry uint256 targetRewards = linearRewards.mul(timeSinceInitialization).div(SECONDS_LINEAR); return targetRewards.add(GENESIS_GOLD_SUPPLY); } else { - /* - // Pay out the remaining half according to the following rule: - // REMAINING_REWARDS = EXPONENTIAL_REWARDS * e ^ (-1*(t - SECONDS_LINEAR) / SECONDS_LINEAR) - uint256 exponentialSeconds = timeSinceInitialization.sub(SECONDS_LINEAR); - uint256 exponentialRewards = GOLD_SUPPLY_CAP.sub(GENESIS_GOLD_SUPPLY).div(2); - // TODO(asa): FractionMulExp does not support fractional exponents. - (uint256 numerator, uint256 denominator) = fractionMulExp( - FixidityLib.fixed1(), - FixidityLib.fixed1(), - FIXIDITY_E, - FixidityLib.fixed1(), - exponentialSeconds, - SECONDS_LINEAR - ); - // Flip numerator and denominator to account for negative exponent. - uint256 remainingRewards = exponentialRewards.mul(denominator).div(numerator); - return GOLD_SUPPLY_CAP.sub(remainingRewards); - */ + // TODO(asa): Implement block reward calculation for years 15-30. return 0; } } @@ -159,10 +157,10 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry FixidityLib.Fraction memory ratio = FixidityLib.newFixed(remainingSupply).divide(FixidityLib.newFixed(targetRemainingSupply)); if (ratio.gt(FixidityLib.fixed1())) { FixidityLib.Fraction memory delta = ratio.subtract(FixidityLib.fixed1()); - return delta.multiply(rewardsMultiplierAdjustmentFactors.underspend); + return delta.multiply(rewardsMultiplierAdjustmentFactors.underspend).add(FixidityLib.fixed1()); } else if (ratio.lt(FixidityLib.fixed1())) { FixidityLib.Fraction memory delta = FixidityLib.fixed1().subtract(ratio); - return delta.multiply(rewardsMultiplierAdjustmentFactors.overspend); + return FixidityLib.fixed1().subtract(delta.multiply(rewardsMultiplierAdjustmentFactors.overspend)); } else { return FixidityLib.fixed1(); } @@ -175,29 +173,34 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry function _getTargetTotalEpochPaymentsInGold() internal view returns (uint256) { address stableTokenAddress = registry.getAddressForOrDie(STABLE_TOKEN_REGISTRY_ID); (uint256 numerator, uint256 denominator) = getSortedOracles().medianRate(stableTokenAddress); - uint256 targetEpochPayment = numberValidatorsInCurrentSet().mul(maxValidatorEpochPayment).mul(numerator).div(denominator); + uint256 targetEpochPayment = numberValidatorsInCurrentSet().mul(maxValidatorEpochPayment).mul(denominator).div(numerator); return targetEpochPayment; } function _updateTargetVotingYield() internal { - IERC20Token goldToken = getGoldToken(); // TODO(asa): Ignore custodial accounts. address reserveAddress = registry.getAddressForOrDie(RESERVE_REGISTRY_ID); - uint256 liquidGold = goldToken.totalSupply().sub(goldToken.balanceOf(reserveAddress)); + uint256 liquidGold = getGoldToken().totalSupply().sub(reserveAddress.balance); // TODO(asa): Should this be active votes? uint256 votingGold = getElection().getTotalVotes(); - FixidityLib.Fraction memory votingGoldFraction = FixidityLib.newFixed(liquidGold).divide(FixidityLib.newFixed(votingGold)); + FixidityLib.Fraction memory votingGoldFraction = FixidityLib.newFixed(votingGold).divide(FixidityLib.newFixed(liquidGold)); + emit Debug(votingGoldFraction.unwrap(), "voting gold fraction"); + emit Debug(targetVotingGoldFraction.unwrap(), "target voting gold fraction"); if (votingGoldFraction.gt(targetVotingGoldFraction)) { FixidityLib.Fraction memory votingGoldFractionDelta = votingGoldFraction.subtract(targetVotingGoldFraction); + emit Debug(votingGoldFractionDelta.unwrap(), "voting gold fraction delta"); FixidityLib.Fraction memory targetVotingYieldDelta = votingGoldFractionDelta.multiply(targetVotingYieldParams.adjustmentFactor); + emit Debug(targetVotingYieldDelta.unwrap(), "target voting yield delta"); if (targetVotingYieldDelta.gte(targetVotingYieldParams.target)) { targetVotingYieldParams.target = FixidityLib.newFixed(0); } else { targetVotingYieldParams.target = targetVotingYieldParams.target.subtract(targetVotingYieldDelta); } - } else { + } else if (votingGoldFraction.lt(targetVotingGoldFraction)) { FixidityLib.Fraction memory votingGoldFractionDelta = targetVotingGoldFraction.subtract(votingGoldFraction); + emit Debug(votingGoldFractionDelta.unwrap(), "voting gold fraction delta"); FixidityLib.Fraction memory targetVotingYieldDelta = votingGoldFractionDelta.multiply(targetVotingYieldParams.adjustmentFactor); + emit Debug(targetVotingYieldDelta.unwrap(), "target voting yield delta"); targetVotingYieldParams.target = targetVotingYieldParams.target.add(targetVotingYieldDelta); if (targetVotingYieldParams.target.gt(targetVotingYieldParams.max)) { targetVotingYieldParams.target = targetVotingYieldParams.max; diff --git a/packages/protocol/contracts/governance/test/EpochRewardsTest.sol b/packages/protocol/contracts/governance/test/EpochRewardsTest.sol index 76a00de1c4a..d2b7f82a17f 100644 --- a/packages/protocol/contracts/governance/test/EpochRewardsTest.sol +++ b/packages/protocol/contracts/governance/test/EpochRewardsTest.sol @@ -27,4 +27,8 @@ contract EpochRewardsTest is EpochRewards { function updateTargetVotingYield() external { _updateTargetVotingYield(); } + + function numberValidatorsInCurrentSet() public view returns (uint256) { + return 100; + } } diff --git a/packages/protocol/contracts/governance/test/MockElection.sol b/packages/protocol/contracts/governance/test/MockElection.sol index 091b6f753dd..e4065c5c2ba 100644 --- a/packages/protocol/contracts/governance/test/MockElection.sol +++ b/packages/protocol/contracts/governance/test/MockElection.sol @@ -10,6 +10,8 @@ contract MockElection is IElection { mapping(address => bool) public isIneligible; mapping(address => bool) public isEligible; address[] public electedValidators; + uint256 active; + uint256 total; function markGroupIneligible(address account) external { isIneligible[account] = true; @@ -20,17 +22,25 @@ contract MockElection is IElection { } function getTotalVotes() external view returns (uint256) { - return 0; + return total; } function getActiveVotes() external view returns (uint256) { - return 0; + return active; } function getTotalVotesByAccount(address) external view returns (uint256) { return 0; } + function setActiveVotes(uint256 value) external { + active = value; + } + + function setTotalVotes(uint256 value) external { + total = value; + } + function setElectedValidators(address[] calldata _electedValidators) external { electedValidators = _electedValidators; } diff --git a/packages/protocol/contracts/stability/SortedOracles.sol b/packages/protocol/contracts/stability/SortedOracles.sol index 9e569f2f422..f44e1abaca7 100644 --- a/packages/protocol/contracts/stability/SortedOracles.sol +++ b/packages/protocol/contracts/stability/SortedOracles.sol @@ -8,7 +8,7 @@ import "../common/linkedlists/AddressSortedLinkedListWithMedian.sol"; import "../common/linkedlists/SortedLinkedListWithMedian.sol"; -// TODO: don't treat timestamps as Fixidity values +// TODO: Move SortedOracles to Fixidity /** * @title Maintains a sorted list of oracle exchange rates between Celo Gold and other currencies. */ diff --git a/packages/protocol/contracts/stability/test/MockSortedOracles.sol b/packages/protocol/contracts/stability/test/MockSortedOracles.sol index e09ec291f2c..c519ebddeea 100644 --- a/packages/protocol/contracts/stability/test/MockSortedOracles.sol +++ b/packages/protocol/contracts/stability/test/MockSortedOracles.sol @@ -6,25 +6,17 @@ pragma solidity ^0.5.3; */ contract MockSortedOracles { - mapping(address => uint128) public numerators; - mapping(address => uint128) public denominators; - mapping(address => uint128) public medianTimestamp; - mapping(address => uint128) public numRates; - - function setMedianRate( - address token, - uint128 numerator, - uint128 denominator - ) - external - returns (bool) - { + uint256 public constant DENOMINATOR = 0x10000000000000000; + mapping(address => uint256) public numerators; + mapping(address => uint256) public medianTimestamp; + mapping(address => uint256) public numRates; + + function setMedianRate(address token, uint256 numerator) external returns (bool) { numerators[token] = numerator; - denominators[token] = denominator; return true; } - function setMedianTimestamp(address token, uint128 timestamp) external { + function setMedianTimestamp(address token, uint256 timestamp) external { medianTimestamp[token] = timestamp; } @@ -33,11 +25,11 @@ contract MockSortedOracles { medianTimestamp[token] = uint128(now); } - function setNumRates(address token, uint128 rate) external { + function setNumRates(address token, uint256 rate) external { numRates[token] = rate; } - function medianRate(address token) external view returns (uint128, uint128) { - return (numerators[token], denominators[token]); + function medianRate(address token) external view returns (uint256, uint256) { + return (numerators[token], DENOMINATOR); } } diff --git a/packages/protocol/migrations/13_epoch_rewards.ts b/packages/protocol/migrations/13_epoch_rewards.ts index 19355526cef..6562c70cd41 100644 --- a/packages/protocol/migrations/13_epoch_rewards.ts +++ b/packages/protocol/migrations/13_epoch_rewards.ts @@ -12,6 +12,7 @@ const initializeArgs = async (): Promise => { toFixed(config.epochRewards.targetVotingYieldParameters.adjustmentFactor).toFixed(), toFixed(config.epochRewards.rewardsMultiplierAdjustmentFactors.underspend).toFixed(), toFixed(config.epochRewards.rewardsMultiplierAdjustmentFactors.overspend).toFixed(), + toFixed(config.epochRewards.targetVotingGoldFraction).toFixed(), config.epochRewards.maxValidatorEpochPayment, ] } diff --git a/packages/protocol/migrationsConfig.js b/packages/protocol/migrationsConfig.js index 424bf148d05..4a3b97d092b 100644 --- a/packages/protocol/migrationsConfig.js +++ b/packages/protocol/migrationsConfig.js @@ -34,6 +34,7 @@ const DefaultConfig = { underspend: 1 / 2, overspend: 5, }, + targetVotingGoldFraction: 2 / 3, maxValidatorEpochPayment: '1000000000000000000', }, exchange: { diff --git a/packages/protocol/test/governance/epochrewards.ts b/packages/protocol/test/governance/epochrewards.ts index 7ce8348ff21..3818d86d083 100644 --- a/packages/protocol/test/governance/epochrewards.ts +++ b/packages/protocol/test/governance/epochrewards.ts @@ -1,45 +1,74 @@ import { CeloContractName } from '@celo/protocol/lib/registry-utils' -import { assertContainSubset, assertEqualBN, assertRevert } from '@celo/protocol/lib/test-utils' +import { + assertContainSubset, + assertEqualBN, + assertRevert, + timeTravel, +} from '@celo/protocol/lib/test-utils' import BigNumber from 'bignumber.js' import { MockElectionContract, MockElectionInstance, + MockGoldTokenContract, + MockGoldTokenInstance, + MockSortedOraclesContract, + MockSortedOraclesInstance, EpochRewardsTestContract, EpochRewardsTestInstance, RegistryContract, RegistryInstance, } from 'types' -import { toFixed } from '@celo/utils/lib/fixidity' +import { fromFixed, toFixed } from '@celo/utils/lib/fixidity' const EpochRewards: EpochRewardsTestContract = artifacts.require('EpochRewardsTest') const MockElection: MockElectionContract = artifacts.require('MockElection') +const MockGoldToken: MockGoldTokenContract = artifacts.require('MockGoldToken') +const MockSortedOracles: MockSortedOraclesContract = artifacts.require('MockSortedOracles') const Registry: RegistryContract = artifacts.require('Registry') // @ts-ignore // TODO(mcortesi): Use BN EpochRewards.numberFormat = 'BigNumber' +const YEAR = new BigNumber(365 * 24 * 60 * 60) +const SUPPLY_CAP = new BigNumber(web3.utils.toWei('1000000000')) + +const getExpectedTargetTotalSupply = (timeDelta: BigNumber) => { + const genesisSupply = new BigNumber(web3.utils.toWei('600000000')) + const linearRewards = new BigNumber(web3.utils.toWei('200000000')) + return genesisSupply + .plus(timeDelta.times(linearRewards).div(YEAR.times(15))) + .integerValue(BigNumber.ROUND_FLOOR) +} + contract('EpochRewards', (accounts: string[]) => { let epochRewards: EpochRewardsTestInstance let mockElection: MockElectionInstance + let mockGoldToken: MockGoldTokenInstance + let mockSortedOracles: MockSortedOraclesInstance let registry: RegistryInstance const nonOwner = accounts[1] const targetVotingYieldParams = { initial: toFixed(new BigNumber(1 / 20)), max: toFixed(new BigNumber(1 / 5)), - adjustmentFactor: toFixed(new BigNumber(1)), + adjustmentFactor: toFixed(new BigNumber(1 / 365)), } const rewardsMultiplierAdjustments = { underspend: toFixed(new BigNumber(1 / 2)), overspend: toFixed(new BigNumber(5)), } + const targetVotingGoldFraction = toFixed(new BigNumber(2 / 3)) const maxValidatorEpochPayment = new BigNumber(10000000000000) beforeEach(async () => { epochRewards = await EpochRewards.new() mockElection = await MockElection.new() + mockGoldToken = await MockGoldToken.new() + mockSortedOracles = await MockSortedOracles.new() registry = await Registry.new() await registry.setAddressFor(CeloContractName.Election, mockElection.address) + await registry.setAddressFor(CeloContractName.GoldToken, mockGoldToken.address) + await registry.setAddressFor(CeloContractName.SortedOracles, mockSortedOracles.address) await epochRewards.initialize( registry.address, targetVotingYieldParams.initial, @@ -47,6 +76,7 @@ contract('EpochRewards', (accounts: string[]) => { targetVotingYieldParams.adjustmentFactor, rewardsMultiplierAdjustments.underspend, rewardsMultiplierAdjustments.overspend, + targetVotingGoldFraction, maxValidatorEpochPayment ) }) @@ -83,12 +113,58 @@ contract('EpochRewards', (accounts: string[]) => { targetVotingYieldParams.adjustmentFactor, rewardsMultiplierAdjustments.underspend, rewardsMultiplierAdjustments.overspend, + targetVotingGoldFraction, maxValidatorEpochPayment ) ) }) }) + describe('#setTargetVotingGoldFraction()', () => { + describe('when the fraction is different', () => { + const newFraction = targetVotingGoldFraction.plus(1) + + describe('when called by the owner', () => { + let resp: any + + beforeEach(async () => { + resp = await epochRewards.setTargetVotingGoldFraction(newFraction) + }) + + it('should set the target voting gold fraction', async () => { + assertEqualBN(await epochRewards.getTargetVotingGoldFraction(), newFraction) + }) + + it('should emit the TargetVotingGoldFractionSet event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'TargetVotingGoldFractionSet', + args: { + fraction: newFraction, + }, + }) + }) + + describe('when called by a non-owner', () => { + it('should revert', async () => { + await assertRevert( + epochRewards.setTargetVotingGoldFraction(newFraction, { + from: nonOwner, + }) + ) + }) + }) + }) + + describe('when the fraction is the same', () => { + it('should revert', async () => { + await assertRevert(epochRewards.setTargetVotingGoldFraction(targetVotingGoldFraction)) + }) + }) + }) + }) + describe('#setMaxValidatorEpochPayment()', () => { describe('when the payment is different', () => { const newPayment = maxValidatorEpochPayment.plus(1) @@ -250,4 +326,244 @@ contract('EpochRewards', (accounts: string[]) => { }) }) }) + + describe('#getTargetGoldTotalSupply()', () => { + describe('when it has been fewer than 15 years since genesis', () => { + const timeDelta = YEAR.times(10) + beforeEach(async () => { + await timeTravel(timeDelta.toNumber(), web3) + }) + + it('should return 600MM + 200MM * t / 15', async () => { + assertEqualBN( + await epochRewards.getTargetGoldTotalSupply(), + getExpectedTargetTotalSupply(timeDelta) + ) + }) + }) + }) + + describe('#getTargetEpochRewards()', () => { + describe('when there are active votes', () => { + const activeVotes = 1000000 + beforeEach(async () => { + await mockElection.setActiveVotes(activeVotes) + }) + + it('should return a percentage of the active votes', async () => { + const expected = fromFixed(targetVotingYieldParams.initial).times(activeVotes) + assertEqualBN(await epochRewards.getTargetEpochRewards(), expected) + }) + }) + }) + + describe.only('#getTargetTotalEpochPaymentsInGold()', () => { + describe('when a StableToken exchange rate is set', () => { + // 7 StableToken to one Celo Gold + const exchangeRate = 7 + const sortedOraclesDenominator = new BigNumber('0x10000000000000000') + const randomAddress = web3.utils.randomHex(20) + // Hard coded in EpochRewardsTest.sol + const numValidators = 100 + beforeEach(async () => { + await registry.setAddressFor(CeloContractName.StableToken, randomAddress) + await mockSortedOracles.setMedianRate( + randomAddress, + sortedOraclesDenominator.times(exchangeRate) + ) + }) + + it('should return the number of validators times the max payment divided by the exchange rate', async () => { + const expected = maxValidatorEpochPayment + .times(numValidators) + .div(exchangeRate) + .integerValue(BigNumber.ROUND_FLOOR) + assertEqualBN(await epochRewards.getTargetTotalEpochPaymentsInGold(), expected) + }) + }) + }) + + describe.only('#getRewardsMultiplier()', () => { + const timeDelta = YEAR.times(10) + const expectedTargetTotalSupply = getExpectedTargetTotalSupply(timeDelta) + const expectedTargetRemainingSupply = SUPPLY_CAP.minus(expectedTargetTotalSupply) + const targetEpochReward = new BigNumber(120397694238746) + beforeEach(async () => { + await timeTravel(timeDelta.toNumber(), web3) + }) + + describe('when the target supply is equal to the actual supply after rewards', () => { + beforeEach(async () => { + await mockGoldToken.setTotalSupply(expectedTargetTotalSupply.minus(targetEpochReward)) + }) + + it('should return one', async () => { + assertEqualBN(await epochRewards.getRewardsMultiplier(targetEpochReward), toFixed(1)) + }) + }) + + describe('when the actual remaining supply is 10% more than the target remaining supply after rewards', () => { + beforeEach(async () => { + const actualRemainingSupply = expectedTargetRemainingSupply.times(1.1) + const totalSupply = SUPPLY_CAP.minus(actualRemainingSupply) + .minus(targetEpochReward) + .integerValue(BigNumber.ROUND_FLOOR) + await mockGoldToken.setTotalSupply(totalSupply) + }) + + it('should return one plus 10% times the underspend adjustment', async () => { + const actual = fromFixed(await epochRewards.getRewardsMultiplier(targetEpochReward)) + const expected = new BigNumber(1).plus( + fromFixed(rewardsMultiplierAdjustments.underspend).times(0.1) + ) + // Assert equal to 9 decimal places due to fixidity imprecision. + assert.equal(expected.dp(9).toFixed(), actual.dp(9).toFixed()) + }) + }) + + describe('when the actual remaining supply is 10% less than the target remaining supply after rewards', () => { + beforeEach(async () => { + const actualRemainingSupply = expectedTargetRemainingSupply.times(0.9) + const totalSupply = SUPPLY_CAP.minus(actualRemainingSupply) + .minus(targetEpochReward) + .integerValue(BigNumber.ROUND_FLOOR) + await mockGoldToken.setTotalSupply(totalSupply) + }) + + it('should return one minus 10% times the underspend adjustment', async () => { + const actual = fromFixed(await epochRewards.getRewardsMultiplier(targetEpochReward)) + const expected = new BigNumber(1).minus( + fromFixed(rewardsMultiplierAdjustments.overspend).times(0.1) + ) + // Assert equal to 9 decimal places due to fixidity imprecision. + assert.equal(expected.dp(9).toFixed(), actual.dp(9).toFixed()) + }) + }) + }) + + describe.only('#updateTargetVotingYield()', () => { + const randomAddress = web3.utils.randomHex(20) + const totalSupply = new BigNumber(129762987346298761037469283746) + const reserveBalance = new BigNumber(2397846127684712867321) + const floatingSupply = totalSupply.minus(reserveBalance) + beforeEach(async () => { + await mockGoldToken.setTotalSupply(totalSupply) + await web3.eth.sendTransaction({ + from: accounts[9], + to: randomAddress, + value: reserveBalance.toString(), + }) + await registry.setAddressFor(CeloContractName.Reserve, randomAddress) + }) + + describe('when the percentage of voting gold is equal to the target', () => { + beforeEach(async () => { + const totalVotes = floatingSupply + .times(fromFixed(targetVotingGoldFraction)) + .integerValue(BigNumber.ROUND_FLOOR) + await mockElection.setTotalVotes(totalVotes) + await epochRewards.updateTargetVotingYield() + }) + + it('should not change the target voting yield', async () => { + assertEqualBN( + (await epochRewards.getTargetVotingYieldParameters())[0], + targetVotingYieldParams.initial + ) + }) + }) + + describe('when the percentage of voting gold is 10% less than the target', () => { + beforeEach(async () => { + const totalVotes = floatingSupply + .times(fromFixed(targetVotingGoldFraction).minus(0.1)) + .integerValue(BigNumber.ROUND_FLOOR) + await mockElection.setTotalVotes(totalVotes) + await epochRewards.updateTargetVotingYield() + }) + + it('should increase the target voting yield by 10% times the adjustment factor', async () => { + const expected = fromFixed( + targetVotingYieldParams.initial.plus(targetVotingYieldParams.adjustmentFactor.times(0.1)) + ) + const actual = fromFixed((await epochRewards.getTargetVotingYieldParameters())[0]) + // Assert equal to 9 decimal places due to fixidity imprecision. + assert.equal(expected.dp(9).toFixed(), actual.dp(9).toFixed()) + }) + }) + + describe('when the percentage of voting gold is 10% more than the target', () => { + beforeEach(async () => { + const totalVotes = floatingSupply + .times(fromFixed(targetVotingGoldFraction).plus(0.1)) + .integerValue(BigNumber.ROUND_FLOOR) + await mockElection.setTotalVotes(totalVotes) + await epochRewards.updateTargetVotingYield() + }) + + it('should decrease the target voting yield by 10% times the adjustment factor', async () => { + const expected = fromFixed( + targetVotingYieldParams.initial.minus(targetVotingYieldParams.adjustmentFactor.times(0.1)) + ) + const actual = fromFixed((await epochRewards.getTargetVotingYieldParameters())[0]) + // Assert equal to 9 decimal places due to fixidity imprecision. + assert.equal(expected.dp(9).toFixed(), actual.dp(9).toFixed()) + }) + }) + }) + + describe.only('#calculateTargetEpochPaymentAndRewards()', () => { + describe('when there are active votes, a stable token exchange rate is set and the actual remaining supply is 10% more than the target remaining supply after rewards', () => { + const activeVotes = 1000000 + const sortedOraclesDenominator = new BigNumber('0x10000000000000000') + const randomAddress = web3.utils.randomHex(20) + const timeDelta = YEAR.times(10) + // 7 StableToken to one Celo Gold + const exchangeRate = 7 + // Hard coded in EpochRewardsTest.sol + const numValidators = 100 + let expectedMultiplier: BigNumber + beforeEach(async () => { + await mockElection.setActiveVotes(activeVotes) + await registry.setAddressFor(CeloContractName.StableToken, randomAddress) + await mockSortedOracles.setMedianRate( + randomAddress, + sortedOraclesDenominator.times(exchangeRate) + ) + await timeTravel(timeDelta.toNumber(), web3) + const expectedTargetTotalEpochPaymentsInGold = maxValidatorEpochPayment + .times(numValidators) + .div(exchangeRate) + .integerValue(BigNumber.ROUND_FLOOR) + const expectedTargetEpochRewards = fromFixed(targetVotingYieldParams.initial).times( + activeVotes + ) + const expectedTargetGoldSupplyIncrease = expectedTargetEpochRewards.plus( + expectedTargetTotalEpochPaymentsInGold + ) + const expectedTargetTotalSupply = getExpectedTargetTotalSupply(timeDelta) + const expectedTargetRemainingSupply = SUPPLY_CAP.minus(expectedTargetTotalSupply) + const actualRemainingSupply = expectedTargetRemainingSupply.times(1.1) + const totalSupply = SUPPLY_CAP.minus(actualRemainingSupply) + .minus(expectedTargetGoldSupplyIncrease) + .integerValue(BigNumber.ROUND_FLOOR) + await mockGoldToken.setTotalSupply(totalSupply) + expectedMultiplier = new BigNumber(1).plus( + fromFixed(rewardsMultiplierAdjustments.underspend).times(0.1) + ) + }) + + it('should return the max validator epoch payment times the rewards multiplier', async () => { + const expected = maxValidatorEpochPayment.times(expectedMultiplier) + assertEqualBN((await epochRewards.calculateTargetEpochPaymentAndRewards())[0], expected) + }) + + it('should return the target yield times the number of active votes times the rewards multiplier', async () => { + const expected = fromFixed(targetVotingYieldParams.initial) + .times(activeVotes) + .times(expectedMultiplier) + assertEqualBN((await epochRewards.calculateTargetEpochPaymentAndRewards())[1], expected) + }) + }) + }) }) From 9281b4c7429ef9d2d73615af4a2f5e7d77357aa4 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Thu, 31 Oct 2019 14:11:05 -0700 Subject: [PATCH 090/149] Make precompile public --- packages/protocol/contracts/common/UsingPrecompiles.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/protocol/contracts/common/UsingPrecompiles.sol b/packages/protocol/contracts/common/UsingPrecompiles.sol index c2add1fc0d6..1bdb26152c8 100644 --- a/packages/protocol/contracts/common/UsingPrecompiles.sol +++ b/packages/protocol/contracts/common/UsingPrecompiles.sol @@ -134,7 +134,7 @@ contract UsingPrecompiles { address sender, bytes memory proofOfPossessionBytes ) - private + public returns (bool) { bool success; From 95c77d975049f0d48d8a5aef9f156346179806e4 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Thu, 31 Oct 2019 14:52:12 -0700 Subject: [PATCH 091/149] Fix migrations --- packages/protocol/migrations/11_validators.ts | 8 +++---- .../migrations/17_elect_validators.ts | 21 ++++++++++++++----- packages/protocol/migrationsConfig.js | 12 +++++------ 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/packages/protocol/migrations/11_validators.ts b/packages/protocol/migrations/11_validators.ts index 5f24860c230..2d692d15040 100644 --- a/packages/protocol/migrations/11_validators.ts +++ b/packages/protocol/migrations/11_validators.ts @@ -7,10 +7,10 @@ import { ValidatorsInstance } from 'types' const initializeArgs = async (): Promise => { return [ config.registry.predeployedProxyAddress, - config.validators.registrationRequirements.group, - config.validators.registrationRequirements.validator, - config.validators.deregistrationLockups.group, - config.validators.deregistrationLockups.validator, + config.validators.groupLockedGoldRequirements.value, + config.validators.groupLockedGoldRequirements.duration, + config.validators.validatorLockedGoldRequirements.value, + config.validators.validatorLockedGoldRequirements.duration, config.validators.validatorScoreParameters.exponent, toFixed(config.validators.validatorScoreParameters.adjustmentSpeed).toFixed(), config.validators.validatorEpochPayment, diff --git a/packages/protocol/migrations/17_elect_validators.ts b/packages/protocol/migrations/17_elect_validators.ts index 8314f288495..84dbc7019aa 100644 --- a/packages/protocol/migrations/17_elect_validators.ts +++ b/packages/protocol/migrations/17_elect_validators.ts @@ -39,7 +39,8 @@ async function lockGold(lockedGold: LockedGoldInstance, value: BigNumber, privat async function registerValidatorGroup( lockedGold: LockedGoldInstance, validators: ValidatorsInstance, - privateKey: string + privateKey: string, + numMembers: number ) { // Validators can't also be validator groups, so we create a new account to register the // validator group with, and set the group identifier to the private key of this account @@ -53,13 +54,18 @@ async function registerValidatorGroup( const encryptedPrivateKey = encryptionWeb3.eth.accounts.encrypt(account.privateKey, privateKey) const encodedKey = serializeKeystore(encryptedPrivateKey) + // Value is per-validator. + const lockedGoldValue = new BigNumber(config.validators.groupLockedGoldRequirements.value).times( + numMembers + ) + await web3.eth.sendTransaction({ from: generateAccountAddressFromPrivateKey(privateKey.slice(0)), to: account.address, - value: config.validators.registrationRequirements.group * 2, // Add a premium to cover tx fees + value: lockedGoldValue.times(1.01).toFixed(), // Add a premium to cover tx fees }) - await lockGold(lockedGold, config.validators.registrationRequirements.group, account.privateKey) + await lockGold(lockedGold, lockedGoldValue, account.privateKey) // @ts-ignore const tx = validators.contract.methods.registerValidatorGroup( @@ -97,7 +103,7 @@ async function registerValidator( await lockGold( lockedGold, - config.validators.registrationRequirements.validator, + config.validators.validatorLockedGoldRequirements.value, validatorPrivateKey ) @@ -151,7 +157,12 @@ module.exports = async (_deployer: any) => { console.info(' Registering ValidatorGroup ...') const firstPrivateKey = valKeys[0] - const account = await registerValidatorGroup(lockedGold, validators, firstPrivateKey) + const account = await registerValidatorGroup( + lockedGold, + validators, + firstPrivateKey, + valKeys.length + ) console.info(' Registering Validators ...') await Promise.all( diff --git a/packages/protocol/migrationsConfig.js b/packages/protocol/migrationsConfig.js index c5747fa40b3..eaf26bfd86d 100644 --- a/packages/protocol/migrationsConfig.js +++ b/packages/protocol/migrationsConfig.js @@ -80,13 +80,13 @@ const DefaultConfig = { oracles: [], }, validators: { - registrationRequirements: { - group: '1000000000000000000', // 1 gold - validator: '1000000000000000000', // 1 gold + groupLockedGoldRequirements: { + value: '10000000000000000000000', // 10000 gold + duration: 60 * 24 * 60 * 60, // 60 days }, - deregistrationLockups: { - group: 60 * 24 * 60 * 60, // 60 days - validator: 60 * 24 * 60 * 60, // 60 days + validatorLockedGoldRequirements: { + value: '10000000000000000000000', // 10000 gold + duration: 60 * 24 * 60 * 60, // 60 days }, validatorScoreParameters: { exponent: 1, From e2eec9b55d48a2ac9307e0e84ee2ced00867278a Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Fri, 1 Nov 2019 12:08:25 -0700 Subject: [PATCH 092/149] end-to-end tests passing, not accounting for voting yield and multiplier --- .../src/e2e-tests/governance_tests.ts | 101 +++++++++++++++--- packages/contractkit/src/base.ts | 1 + packages/contractkit/src/contract-cache.ts | 6 ++ packages/contractkit/src/kit.ts | 3 + .../contractkit/src/web3-contract-cache.ts | 5 + .../contracts/governance/EpochRewards.sol | 7 ++ packages/protocol/lib/web3-utils.ts | 2 +- .../migrations/19_elect_validators.ts | 5 + packages/protocol/migrationsConfig.js | 2 +- packages/protocol/scripts/build.ts | 5 +- 10 files changed, 117 insertions(+), 20 deletions(-) diff --git a/packages/celotool/src/e2e-tests/governance_tests.ts b/packages/celotool/src/e2e-tests/governance_tests.ts index b026ba5ff24..e301731ead1 100644 --- a/packages/celotool/src/e2e-tests/governance_tests.ts +++ b/packages/celotool/src/e2e-tests/governance_tests.ts @@ -20,9 +20,12 @@ describe('governance tests', () => { const context: any = getContext(gethConfig) let web3: any let election: any - let validators: any + let stableToken: any + let sortedOracles: any + let epochRewards: any let goldToken: any let registry: any + let validators: any let kit: ContractKit before(async function(this: any) { @@ -37,9 +40,12 @@ describe('governance tests', () => { web3 = new Web3('http://localhost:8545') kit = newKitFromWeb3(web3) goldToken = await kit._web3Contracts.getGoldToken() + stableToken = await kit._web3Contracts.getStableToken() + sortedOracles = await kit._web3Contracts.getSortedOracles() validators = await kit._web3Contracts.getValidators() registry = await kit._web3Contracts.getRegistry() election = await kit._web3Contracts.getElection() + epochRewards = await kit._web3Contracts.getEpochRewards() } const unlockAccount = async (address: string, theWeb3: any) => { @@ -116,7 +122,7 @@ describe('governance tests', () => { return blockNumber % epochSize === 0 } - describe('when the validator set is changing', () => { + describe.only('when the validator set is changing', () => { let epoch: number const blockNumbers: number[] = [] let allValidators: string[] @@ -275,10 +281,9 @@ describe('governance tests', () => { }) it('should distribute epoch payments at the end of each epoch', async () => { - const stableToken = await kit._web3Contracts.getStableToken() const commission = 0.1 - const validatorEpochPayment = new BigNumber( - await validators.methods.validatorEpochPayment().call() + const maxValidatorEpochPayment = new BigNumber( + await epochRewards.methods.maxValidatorEpochPayment().call() ) const [group] = await validators.methods.getRegisteredValidatorGroups().call() @@ -295,7 +300,7 @@ describe('governance tests', () => { ) assert.isNotNaN(currentBalance) assert.isNotNaN(previousBalance) - assert.equal(expected.toFixed(), currentBalance.minus(previousBalance).toFixed()) + assert.equal(currentBalance.minus(previousBalance).toFixed(), expected.toFixed()) } const assertBalanceUnchanged = async (validator: string, blockNumber: number) => { @@ -307,7 +312,7 @@ describe('governance tests', () => { (await validators.methods.getValidator(validator).call({}, blockNumber))[3] ) assert.isNotNaN(score) - return validatorEpochPayment.times(fromFixed(score)) + return maxValidatorEpochPayment.times(fromFixed(score)) } for (const blockNumber of blockNumbers) { @@ -343,8 +348,6 @@ describe('governance tests', () => { it('should distribute epoch rewards at the end of each epoch', async () => { const lockedGold = await kit._web3Contracts.getLockedGold() const governance = await kit._web3Contracts.getGovernance() - const epochReward = new BigNumber(10).pow(18) - const infraReward = new BigNumber(10).pow(18) const [group] = await validators.methods.getRegisteredValidatorGroups().call() const assertVotesChanged = async (blockNumber: number, expected: BigNumber) => { @@ -354,7 +357,7 @@ describe('governance tests', () => { const previousVotes = new BigNumber( await election.methods.getTotalVotesForGroup(group).call({}, blockNumber - 1) ) - assert.equal(expected.toFixed(), currentVotes.minus(previousVotes).toFixed()) + assert.equal(currentVotes.minus(previousVotes).toFixed(), expected.toFixed()) } const assertGoldTokenTotalSupplyChanged = async ( @@ -367,7 +370,7 @@ describe('governance tests', () => { const previousSupply = new BigNumber( await goldToken.methods.totalSupply().call({}, blockNumber - 1) ) - assert.equal(expected.toFixed(), currentSupply.minus(previousSupply).toFixed()) + assert.equal(currentSupply.minus(previousSupply).toFixed(), expected.toFixed()) } const assertBalanceChanged = async ( @@ -381,7 +384,7 @@ describe('governance tests', () => { const previousBalance = new BigNumber( await goldToken.methods.balanceOf(address).call({}, blockNumber - 1) ) - assert.equal(expected.toFixed(), currentBalance.minus(previousBalance).toFixed()) + assert.equal(currentBalance.minus(previousBalance).toFixed(), expected.toFixed()) } const assertLockedGoldBalanceChanged = async (blockNumber: number, expected: BigNumber) => { @@ -408,12 +411,45 @@ describe('governance tests', () => { await assertGovernanceBalanceChanged(blockNumber, new BigNumber(0)) } + const getStableTokenSupplyChange = async (blockNumber: number) => { + const currentSupply = new BigNumber( + await stableToken.methods.totalSupply().call({}, blockNumber) + ) + const previousSupply = new BigNumber( + await stableToken.methods.totalSupply().call({}, blockNumber - 1) + ) + return currentSupply.minus(previousSupply) + } + + const getStableTokenExchangeRate = async (blockNumber: number) => { + const rate = await sortedOracles.methods + .medianRate(stableToken.options.address) + .call({}, blockNumber) + return new BigNumber(rate[0]).div(rate[1]) + } + for (const blockNumber of blockNumbers) { if (isLastBlockOfEpoch(blockNumber, epoch)) { - await assertVotesChanged(blockNumber, epochReward) - await assertGoldTokenTotalSupplyChanged(blockNumber, epochReward.plus(infraReward)) - await assertLockedGoldBalanceChanged(blockNumber, epochReward) - await assertGovernanceBalanceChanged(blockNumber, infraReward) + // We use the number of active votes from the previous block to calculate the expected + // epoch reward as the number of active votes for the current block will include the + // epoch reward. + const activeVotes = new BigNumber( + await election.methods.getActiveVotes().call({}, blockNumber - 1) + ) + const targetVotingYield = new BigNumber( + (await epochRewards.methods.getTargetVotingYieldParameters().call({}, blockNumber))[0] + ) + const expectedEpochReward = activeVotes.times(fromFixed(targetVotingYield)) + const expectedInfraReward = new BigNumber(10).pow(18) + const stableTokenSupplyChange = await getStableTokenSupplyChange(blockNumber) + const exchangeRate = await getStableTokenExchangeRate(blockNumber) + const expectedGoldTotalSupplyChange = expectedInfraReward + .plus(expectedEpochReward) + .plus(stableTokenSupplyChange.div(exchangeRate)) + await assertVotesChanged(blockNumber, expectedEpochReward) + await assertLockedGoldBalanceChanged(blockNumber, expectedEpochReward) + await assertGovernanceBalanceChanged(blockNumber, expectedInfraReward) + await assertGoldTokenTotalSupplyChanged(blockNumber, expectedGoldTotalSupplyChange) } else { await assertVotesUnchanged(blockNumber) await assertGoldTokenTotalSupplyUnchanged(blockNumber) @@ -422,6 +458,39 @@ describe('governance tests', () => { } } }) + + it('should update the target voting yield', async () => { + const assertTargetVotingYieldChanged = async (blockNumber: number, expected: BigNumber) => { + const currentTarget = new BigNumber( + (await epochRewards.methods.getTargetVotingYieldParameters().call({}, blockNumber))[0] + ) + const previousTarget = new BigNumber( + (await epochRewards.methods.getTargetVotingYieldParameters().call({}, blockNumber - 1))[0] + ) + assert.equal(currentTarget.minus(previousTarget).toFixed(), expected.toFixed()) + } + + const assertTargetVotingYieldUnchanged = async (blockNumber: number) => { + await assertTargetVotingYieldChanged(blockNumber, new BigNumber(0)) + } + + for (const blockNumber of blockNumbers) { + if (isLastBlockOfEpoch(blockNumber, epoch)) { + const actualVotingPercentage = toFixed(new BigNumber(1)) + const targetVotingGoldPercentage = new BigNumber( + await epochRewards.methods.getTargetVotingGoldFraction().call({}, blockNumber) + ) + const difference = actualVotingPercentage.minus(targetVotingGoldPercentage) + const adjustmentFactor = new BigNumber( + (await epochRewards.methods.getTargetVotingYieldParameters().call({}, blockNumber))[1] + ) + const delta = difference.times(adjustmentFactor) + await assertTargetVotingYieldChanged(blockNumber, fromFixed(delta)) + } else { + await assertTargetVotingYieldUnchanged(blockNumber) + } + } + }) }) describe('after the gold token smart contract is registered', () => { diff --git a/packages/contractkit/src/base.ts b/packages/contractkit/src/base.ts index a410cbb7fac..156e6342fa7 100644 --- a/packages/contractkit/src/base.ts +++ b/packages/contractkit/src/base.ts @@ -3,6 +3,7 @@ export type Address = string export enum CeloContract { Attestations = 'Attestations', Election = 'Election', + EpochRewards = 'EpochRewards', Escrow = 'Escrow', Exchange = 'Exchange', GasCurrencyWhitelist = 'GasCurrencyWhitelist', diff --git a/packages/contractkit/src/contract-cache.ts b/packages/contractkit/src/contract-cache.ts index c93a5cf7713..b975a430bca 100644 --- a/packages/contractkit/src/contract-cache.ts +++ b/packages/contractkit/src/contract-cache.ts @@ -2,6 +2,7 @@ import { CeloContract } from './base' import { ContractKit } from './kit' import { AttestationsWrapper } from './wrappers/Attestations' import { ElectionWrapper } from './wrappers/Election' +// import { EpochRewardsWrapper } from './wrappers/EpochRewards' import { ExchangeWrapper } from './wrappers/Exchange' import { GasPriceMinimumWrapper } from './wrappers/GasPriceMinimum' import { GoldTokenWrapper } from './wrappers/GoldTokenWrapper' @@ -15,6 +16,7 @@ import { ValidatorsWrapper } from './wrappers/Validators' const WrapperFactories = { [CeloContract.Attestations]: AttestationsWrapper, [CeloContract.Election]: ElectionWrapper, + // [CeloContract.EpochRewards]?: EpochRewardsWrapper, // [CeloContract.Escrow]: EscrowWrapper, [CeloContract.Exchange]: ExchangeWrapper, // [CeloContract.GasCurrencyWhitelist]: GasCurrencyWhitelistWrapper, @@ -37,6 +39,7 @@ export type ValidWrappers = keyof CFType interface WrapperCacheMap { [CeloContract.Attestations]?: AttestationsWrapper [CeloContract.Election]?: ElectionWrapper + // [CeloContract.EpochRewards]?: EpochRewardsWrapper // [CeloContract.Escrow]?: EscrowWrapper, [CeloContract.Exchange]?: ExchangeWrapper // [CeloContract.GasCurrencyWhitelist]?: GasCurrencyWhitelistWrapper, @@ -70,6 +73,9 @@ export class WrapperCache { getElection() { return this.getContract(CeloContract.Election) } + // getEpochRewards() { + // return this.getContract(CeloContract.EpochRewards) + // } // getEscrow() { // return this.getWrapper(CeloContract.Escrow, newEscrow) // } diff --git a/packages/contractkit/src/kit.ts b/packages/contractkit/src/kit.ts index a997ff88e06..9fb45094880 100644 --- a/packages/contractkit/src/kit.ts +++ b/packages/contractkit/src/kit.ts @@ -89,6 +89,7 @@ export class ContractKit { this.contracts.getReserve(), this.contracts.getStableToken(), this.contracts.getValidators(), + // this.contracts.getEpochRewards(), ]) const res = await Promise.all([ contracts[0].getConfig(), @@ -101,6 +102,7 @@ export class ContractKit { contracts[7].getConfig(), contracts[8].getConfig(), contracts[9].getConfig(), + // contracts[10].getConfig(), ]) return { exchange: res[0], @@ -113,6 +115,7 @@ export class ContractKit { reserve: res[7], stableToken: res[8], validators: res[9], + // epochRewards: res[10], } } diff --git a/packages/contractkit/src/web3-contract-cache.ts b/packages/contractkit/src/web3-contract-cache.ts index 35d705170eb..f4d65e3cec9 100644 --- a/packages/contractkit/src/web3-contract-cache.ts +++ b/packages/contractkit/src/web3-contract-cache.ts @@ -2,6 +2,7 @@ import debugFactory from 'debug' import { CeloContract } from './base' import { newAttestations } from './generated/Attestations' import { newElection } from './generated/Election' +import { newEpochRewards } from './generated/EpochRewards' import { newEscrow } from './generated/Escrow' import { newExchange } from './generated/Exchange' import { newGasCurrencyWhitelist } from './generated/GasCurrencyWhitelist' @@ -22,6 +23,7 @@ const debug = debugFactory('kit:web3-contract-cache') const ContractFactories = { [CeloContract.Attestations]: newAttestations, [CeloContract.Election]: newElection, + [CeloContract.EpochRewards]: newEpochRewards, [CeloContract.Escrow]: newEscrow, [CeloContract.Exchange]: newExchange, [CeloContract.GasCurrencyWhitelist]: newGasCurrencyWhitelist, @@ -62,6 +64,9 @@ export class Web3ContractCache { getElection() { return this.getContract(CeloContract.Election) } + getEpochRewards() { + return this.getContract(CeloContract.EpochRewards) + } getEscrow() { return this.getContract(CeloContract.Escrow) } diff --git a/packages/protocol/contracts/governance/EpochRewards.sol b/packages/protocol/contracts/governance/EpochRewards.sol index 16c822034d5..860c5252ea6 100644 --- a/packages/protocol/contracts/governance/EpochRewards.sol +++ b/packages/protocol/contracts/governance/EpochRewards.sol @@ -155,6 +155,7 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry uint256 remainingSupply = GOLD_SUPPLY_CAP.sub(totalSupply.add(targetGoldSupplyIncrease)); uint256 targetRemainingSupply = GOLD_SUPPLY_CAP.sub(targetSupply); FixidityLib.Fraction memory ratio = FixidityLib.newFixed(remainingSupply).divide(FixidityLib.newFixed(targetRemainingSupply)); + /* if (ratio.gt(FixidityLib.fixed1())) { FixidityLib.Fraction memory delta = ratio.subtract(FixidityLib.fixed1()); return delta.multiply(rewardsMultiplierAdjustmentFactors.underspend).add(FixidityLib.fixed1()); @@ -164,6 +165,8 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry } else { return FixidityLib.fixed1(); } + */ + return FixidityLib.fixed1(); } function _getTargetEpochRewards() internal view returns (uint256) { @@ -219,8 +222,12 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry uint256 targetGoldSupplyIncrease = targetEpochRewards.add(targetTotalEpochPaymentsInGold); FixidityLib.Fraction memory rewardsMultiplier = _getRewardsMultiplier(targetGoldSupplyIncrease); return ( + /* FixidityLib.newFixed(maxValidatorEpochPayment).multiply(rewardsMultiplier).fromFixed(), FixidityLib.newFixed(targetEpochRewards).multiply(rewardsMultiplier).fromFixed() + */ + maxValidatorEpochPayment, + targetEpochRewards ); } } diff --git a/packages/protocol/lib/web3-utils.ts b/packages/protocol/lib/web3-utils.ts index d1d38057d4f..224d42b4d1f 100644 --- a/packages/protocol/lib/web3-utils.ts +++ b/packages/protocol/lib/web3-utils.ts @@ -58,7 +58,7 @@ export async function sendTransactionWithPrivateKey( ...txArgs, data: encodedTxData, from: address, - gas: estimatedGas * 2, + gas: estimatedGas * 10, }, privateKey ) diff --git a/packages/protocol/migrations/19_elect_validators.ts b/packages/protocol/migrations/19_elect_validators.ts index 84dbc7019aa..7c028eac28d 100644 --- a/packages/protocol/migrations/19_elect_validators.ts +++ b/packages/protocol/migrations/19_elect_validators.ts @@ -101,12 +101,14 @@ async function registerValidator( ).toString('hex') const publicKeysData = publicKey + blsPublicKey + blsPoP + console.log('locking gold for validator registration') await lockGold( lockedGold, config.validators.validatorLockedGoldRequirements.value, validatorPrivateKey ) + console.log('registering validator') // @ts-ignore const registerTx = validators.contract.methods.registerValidator(address, add0x(publicKeysData)) @@ -114,6 +116,7 @@ async function registerValidator( to: validators.address, }) + console.log('affiliating') // @ts-ignore const affiliateTx = validators.contract.methods.affiliate(groupAddress) @@ -121,6 +124,8 @@ async function registerValidator( to: validators.address, }) + console.log('done registering validator') + return } diff --git a/packages/protocol/migrationsConfig.js b/packages/protocol/migrationsConfig.js index 95873e1cee3..de3ba5e4146 100644 --- a/packages/protocol/migrationsConfig.js +++ b/packages/protocol/migrationsConfig.js @@ -28,7 +28,7 @@ const DefaultConfig = { targetVotingYieldParameters: { initial: 5 / 100, max: 2 / 10, - adjustmentFactor: 1, + adjustmentFactor: 1 / 365, }, rewardsMultiplierAdjustmentFactors: { underspend: 1 / 2, diff --git a/packages/protocol/scripts/build.ts b/packages/protocol/scripts/build.ts index 6106cd275c4..16ab976217b 100644 --- a/packages/protocol/scripts/build.ts +++ b/packages/protocol/scripts/build.ts @@ -10,6 +10,7 @@ const CONTRACTKIT_GEN_DIR = path.normalize(path.join(ROOT_DIR, '../contractkit/s export const ProxyContracts = [ 'AttestationsProxy', 'ElectionProxy', + 'EpochRewardsProxy', 'EscrowProxy', 'ExchangeProxy', 'GasCurrencyWhitelistProxy', @@ -25,15 +26,16 @@ export const ProxyContracts = [ ] export const CoreContracts = [ // common - 'GasPriceMinimum', 'GasCurrencyWhitelist', + 'GoldToken', 'MultiSig', 'Registry', 'Validators', // governance 'Election', + 'EpochRewards', 'Governance', 'LockedGold', 'Validators', @@ -45,7 +47,6 @@ export const CoreContracts = [ // stability 'Exchange', - 'GoldToken', 'Reserve', 'StableToken', 'SortedOracles', From 2bc75cc35d6d913921a9fe758c19f64527e9953c Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Fri, 1 Nov 2019 13:14:37 -0700 Subject: [PATCH 093/149] Fix typo --- packages/protocol/contracts/governance/Validators.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index 8f4ec311a9d..d6eb07fb6ce 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -68,7 +68,7 @@ contract Validators is address group; } - // Stores a the per-epoch membership history of a validator, used to determine which group + // Stores the per-epoch membership history of a validator, used to determine which group // commission should be paid to at the end of an epoch. // Stores a timestamp of the last time the validator was removed from a group, used to determine // whether or not a group can de-register. From 72ff0ba6fee0cbab2933f26cade7516dfade064b Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Fri, 1 Nov 2019 14:44:58 -0700 Subject: [PATCH 094/149] end-to-end tests adjusted for rewards multiplier --- .../src/e2e-tests/governance_tests.ts | 58 ++++++++++-- .../contracts/governance/EpochRewards.sol | 88 ++++++++++++------- .../governance/test/EpochRewardsTest.sol | 12 --- .../protocol/migrations/13_epoch_rewards.ts | 5 +- packages/protocol/migrationsConfig.js | 9 +- 5 files changed, 117 insertions(+), 55 deletions(-) diff --git a/packages/celotool/src/e2e-tests/governance_tests.ts b/packages/celotool/src/e2e-tests/governance_tests.ts index e301731ead1..cf5ce53a7cf 100644 --- a/packages/celotool/src/e2e-tests/governance_tests.ts +++ b/packages/celotool/src/e2e-tests/governance_tests.ts @@ -122,6 +122,20 @@ describe('governance tests', () => { return blockNumber % epochSize === 0 } + const assertDifferenceCloseTo = ( + current: BigNumber, + previous: BigNumber, + expected: BigNumber, + delta: BigNumber + ) => { + const difference = current.minus(previous) + if (expected.isZero()) { + assert.equal(difference.toFixed(), expected.toFixed()) + } else { + assert.closeTo(difference.toNumber(), expected.toNumber(), delta.toNumber()) + } + } + describe.only('when the validator set is changing', () => { let epoch: number const blockNumbers: number[] = [] @@ -300,7 +314,12 @@ describe('governance tests', () => { ) assert.isNotNaN(currentBalance) assert.isNotNaN(previousBalance) - assert.equal(currentBalance.minus(previousBalance).toFixed(), expected.toFixed()) + assertDifferenceCloseTo( + currentBalance, + previousBalance, + expected, + new BigNumber(10).pow(12).times(5) + ) } const assertBalanceUnchanged = async (validator: string, blockNumber: number) => { @@ -312,7 +331,12 @@ describe('governance tests', () => { (await validators.methods.getValidator(validator).call({}, blockNumber))[3] ) assert.isNotNaN(score) - return maxValidatorEpochPayment.times(fromFixed(score)) + // We need to calculate the rewards multiplier for the previous block, before + // the rewards actually are awarded. + const rewardsMultiplier = new BigNumber( + await epochRewards.methods.getRewardsMultiplier().call({}, blockNumber - 1) + ) + return maxValidatorEpochPayment.times(fromFixed(score)).times(fromFixed(rewardsMultiplier)) } for (const blockNumber of blockNumbers) { @@ -357,7 +381,12 @@ describe('governance tests', () => { const previousVotes = new BigNumber( await election.methods.getTotalVotesForGroup(group).call({}, blockNumber - 1) ) - assert.equal(currentVotes.minus(previousVotes).toFixed(), expected.toFixed()) + assertDifferenceCloseTo( + currentVotes, + previousVotes, + expected, + new BigNumber(10).pow(12).times(5) + ) } const assertGoldTokenTotalSupplyChanged = async ( @@ -370,7 +399,12 @@ describe('governance tests', () => { const previousSupply = new BigNumber( await goldToken.methods.totalSupply().call({}, blockNumber - 1) ) - assert.equal(currentSupply.minus(previousSupply).toFixed(), expected.toFixed()) + assertDifferenceCloseTo( + currentSupply, + previousSupply, + expected, + new BigNumber(10).pow(12).times(5) + ) } const assertBalanceChanged = async ( @@ -384,7 +418,12 @@ describe('governance tests', () => { const previousBalance = new BigNumber( await goldToken.methods.balanceOf(address).call({}, blockNumber - 1) ) - assert.equal(currentBalance.minus(previousBalance).toFixed(), expected.toFixed()) + assertDifferenceCloseTo( + currentBalance, + previousBalance, + expected, + new BigNumber(10).pow(12).times(5) + ) } const assertLockedGoldBalanceChanged = async (blockNumber: number, expected: BigNumber) => { @@ -439,7 +478,14 @@ describe('governance tests', () => { const targetVotingYield = new BigNumber( (await epochRewards.methods.getTargetVotingYieldParameters().call({}, blockNumber))[0] ) - const expectedEpochReward = activeVotes.times(fromFixed(targetVotingYield)) + // We need to calculate the rewards multiplier for the previous block, before + // the rewards actually are awarded. + const rewardsMultiplier = new BigNumber( + await epochRewards.methods.getRewardsMultiplier().call({}, blockNumber - 1) + ) + const expectedEpochReward = activeVotes + .times(fromFixed(targetVotingYield)) + .times(fromFixed(rewardsMultiplier)) const expectedInfraReward = new BigNumber(10).pow(18) const stableTokenSupplyChange = await getStableTokenSupplyChange(blockNumber) const exchangeRate = await getStableTokenExchangeRate(blockNumber) diff --git a/packages/protocol/contracts/governance/EpochRewards.sol b/packages/protocol/contracts/governance/EpochRewards.sol index 860c5252ea6..dc58ee8f9d0 100644 --- a/packages/protocol/contracts/governance/EpochRewards.sol +++ b/packages/protocol/contracts/governance/EpochRewards.sol @@ -28,14 +28,19 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry FixidityLib.Fraction overspend; } + struct RewardsMultiplierParameters { + RewardsMultiplierAdjustmentFactors adjustmentFactors; + FixidityLib.Fraction max; + } + struct TargetVotingYieldParameters { FixidityLib.Fraction target; - FixidityLib.Fraction max; FixidityLib.Fraction adjustmentFactor; + FixidityLib.Fraction max; } uint256 private startTime = 0; - RewardsMultiplierAdjustmentFactors private rewardsMultiplierAdjustmentFactors; + RewardsMultiplierParameters private rewardsMultiplierParams; TargetVotingYieldParameters private targetVotingYieldParams; FixidityLib.Fraction private targetVotingGoldFraction; uint256 public maxValidatorEpochPayment; @@ -43,7 +48,7 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry event TargetVotingGoldFractionSet(uint256 fraction); event MaxValidatorEpochPaymentSet(uint256 payment); event TargetVotingYieldParametersSet(uint256 max, uint256 adjustmentFactor); - event RewardsMultiplierAdjustmentFactorsSet(uint256 underspend, uint256 overspend); + event RewardsMultiplierParametersSet(uint256 max, uint256 underspendAdjustmentFactor, uint256 overspendAdjustmentFactor); event Debug(uint256 value, string desc); /** @@ -54,6 +59,7 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry uint256 targetVotingYieldInitial, uint256 targetVotingYieldMax, uint256 targetVotingYieldAdjustmentFactor, + uint256 rewardsMultiplierMax, uint256 rewardsMultiplierUnderspendAdjustmentFactor, uint256 rewardsMultiplierOverspendAdjustmentFactor, uint256 _targetVotingGoldFraction, @@ -65,7 +71,8 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry _transferOwnership(msg.sender); setRegistry(registryAddress); setTargetVotingYieldParameters(targetVotingYieldMax, targetVotingYieldAdjustmentFactor); - setRewardsMultiplierAdjustmentFactors( + setRewardsMultiplierParameters( + rewardsMultiplierMax, rewardsMultiplierUnderspendAdjustmentFactor, rewardsMultiplierOverspendAdjustmentFactor ); @@ -80,9 +87,9 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry return (params.target.unwrap(), params.max.unwrap(), params.adjustmentFactor.unwrap()); } - function getRewardsMultiplierAdjustmentFactors() external view returns (uint256, uint256) { - RewardsMultiplierAdjustmentFactors storage factors = rewardsMultiplierAdjustmentFactors; - return (factors.underspend.unwrap(), factors.overspend.unwrap()); + function getRewardsMultiplierParameters() external view returns (uint256, uint256, uint256) { + RewardsMultiplierParameters storage params = rewardsMultiplierParams; + return (params.max.unwrap(), params.adjustmentFactors.underspend.unwrap(), params.adjustmentFactors.overspend.unwrap()); } function setTargetVotingGoldFraction(uint256 value) public onlyOwner returns (bool) { @@ -108,16 +115,20 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry return true; } - function setRewardsMultiplierAdjustmentFactors(uint256 underspend, uint256 overspend) public onlyOwner returns (bool) { + function setRewardsMultiplierParameters(uint256 max, uint256 underspendAdjustmentFactor, uint256 overspendAdjustmentFactor) public onlyOwner returns (bool) { require( - underspend != rewardsMultiplierAdjustmentFactors.underspend.unwrap() || - overspend != rewardsMultiplierAdjustmentFactors.overspend.unwrap() + max != rewardsMultiplierParams.max.unwrap() || + underspendAdjustmentFactor != rewardsMultiplierParams.adjustmentFactors.underspend.unwrap() || + overspendAdjustmentFactor != rewardsMultiplierParams.adjustmentFactors.overspend.unwrap() ); - rewardsMultiplierAdjustmentFactors = RewardsMultiplierAdjustmentFactors( - FixidityLib.wrap(underspend), - FixidityLib.wrap(overspend) + rewardsMultiplierParams = RewardsMultiplierParameters( + RewardsMultiplierAdjustmentFactors( + FixidityLib.wrap(underspendAdjustmentFactor), + FixidityLib.wrap(overspendAdjustmentFactor) + ), + FixidityLib.wrap(max) ); - emit RewardsMultiplierAdjustmentFactorsSet(underspend, overspend); + emit RewardsMultiplierParametersSet(max, underspendAdjustmentFactor, overspendAdjustmentFactor); return true; } @@ -136,7 +147,7 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry return true; } - function _getTargetGoldTotalSupply() internal view returns (uint256) { + function getTargetGoldTotalSupply() public view returns (uint256) { uint256 timeSinceInitialization = now.sub(startTime); if (timeSinceInitialization < SECONDS_LINEAR) { // Pay out half of all block rewards linearly. @@ -150,30 +161,47 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry } function _getRewardsMultiplier(uint256 targetGoldSupplyIncrease) internal view returns (FixidityLib.Fraction memory) { - uint256 targetSupply = _getTargetGoldTotalSupply(); + uint256 targetSupply = getTargetGoldTotalSupply(); uint256 totalSupply = getGoldToken().totalSupply(); uint256 remainingSupply = GOLD_SUPPLY_CAP.sub(totalSupply.add(targetGoldSupplyIncrease)); uint256 targetRemainingSupply = GOLD_SUPPLY_CAP.sub(targetSupply); FixidityLib.Fraction memory ratio = FixidityLib.newFixed(remainingSupply).divide(FixidityLib.newFixed(targetRemainingSupply)); - /* if (ratio.gt(FixidityLib.fixed1())) { - FixidityLib.Fraction memory delta = ratio.subtract(FixidityLib.fixed1()); - return delta.multiply(rewardsMultiplierAdjustmentFactors.underspend).add(FixidityLib.fixed1()); + FixidityLib.Fraction memory delta = ratio.subtract(FixidityLib.fixed1()).multiply( + rewardsMultiplierParams.adjustmentFactors.underspend + ); + FixidityLib.Fraction memory r = FixidityLib.fixed1().add(delta); + if (r.lt(rewardsMultiplierParams.max)) { + return r; + } else { + return rewardsMultiplierParams.max; + } } else if (ratio.lt(FixidityLib.fixed1())) { - FixidityLib.Fraction memory delta = FixidityLib.fixed1().subtract(ratio); - return FixidityLib.fixed1().subtract(delta.multiply(rewardsMultiplierAdjustmentFactors.overspend)); + FixidityLib.Fraction memory delta = FixidityLib.fixed1().subtract(ratio).multiply( + rewardsMultiplierParams.adjustmentFactors.overspend + ); + if (delta.lt(FixidityLib.fixed1())) { + return FixidityLib.fixed1().subtract(delta); + } else { + return FixidityLib.wrap(0); + } } else { return FixidityLib.fixed1(); } - */ - return FixidityLib.fixed1(); } - function _getTargetEpochRewards() internal view returns (uint256) { + function getRewardsMultiplier() external view returns (uint256) { + uint256 targetEpochRewards = getTargetEpochRewards(); + uint256 targetTotalEpochPaymentsInGold = getTargetTotalEpochPaymentsInGold(); + uint256 targetGoldSupplyIncrease = targetEpochRewards.add(targetTotalEpochPaymentsInGold); + return _getRewardsMultiplier(targetGoldSupplyIncrease).unwrap(); + } + + function getTargetEpochRewards() public view returns (uint256) { return FixidityLib.newFixed(getElection().getActiveVotes()).multiply(targetVotingYieldParams.target).fromFixed(); } - function _getTargetTotalEpochPaymentsInGold() internal view returns (uint256) { + function getTargetTotalEpochPaymentsInGold() public view returns (uint256) { address stableTokenAddress = registry.getAddressForOrDie(STABLE_TOKEN_REGISTRY_ID); (uint256 numerator, uint256 denominator) = getSortedOracles().medianRate(stableTokenAddress); uint256 targetEpochPayment = numberValidatorsInCurrentSet().mul(maxValidatorEpochPayment).mul(denominator).div(numerator); @@ -213,21 +241,17 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry function updateTargetVotingYield() external { require(msg.sender == address(0)); - _updateTargetVotingYield(); + // _updateTargetVotingYield(); } function calculateTargetEpochPaymentAndRewards() external view returns (uint256, uint256) { - uint256 targetEpochRewards = _getTargetEpochRewards(); - uint256 targetTotalEpochPaymentsInGold = _getTargetTotalEpochPaymentsInGold(); + uint256 targetEpochRewards = getTargetEpochRewards(); + uint256 targetTotalEpochPaymentsInGold = getTargetTotalEpochPaymentsInGold(); uint256 targetGoldSupplyIncrease = targetEpochRewards.add(targetTotalEpochPaymentsInGold); FixidityLib.Fraction memory rewardsMultiplier = _getRewardsMultiplier(targetGoldSupplyIncrease); return ( - /* FixidityLib.newFixed(maxValidatorEpochPayment).multiply(rewardsMultiplier).fromFixed(), FixidityLib.newFixed(targetEpochRewards).multiply(rewardsMultiplier).fromFixed() - */ - maxValidatorEpochPayment, - targetEpochRewards ); } } diff --git a/packages/protocol/contracts/governance/test/EpochRewardsTest.sol b/packages/protocol/contracts/governance/test/EpochRewardsTest.sol index d2b7f82a17f..8dd822aab0a 100644 --- a/packages/protocol/contracts/governance/test/EpochRewardsTest.sol +++ b/packages/protocol/contracts/governance/test/EpochRewardsTest.sol @@ -8,18 +8,6 @@ import "../../common/FixidityLib.sol"; */ contract EpochRewardsTest is EpochRewards { - function getTargetGoldTotalSupply() external view returns (uint256) { - return _getTargetGoldTotalSupply(); - } - - function getTargetTotalEpochPaymentsInGold() external view returns (uint256) { - return _getTargetTotalEpochPaymentsInGold(); - } - - function getTargetEpochRewards() external view returns (uint256) { - return _getTargetEpochRewards(); - } - function getRewardsMultiplier(uint256 targetGoldTotalSupplyIncrease) external view returns (uint256) { return _getRewardsMultiplier(targetGoldTotalSupplyIncrease).unwrap(); } diff --git a/packages/protocol/migrations/13_epoch_rewards.ts b/packages/protocol/migrations/13_epoch_rewards.ts index 6562c70cd41..43dfa88fc49 100644 --- a/packages/protocol/migrations/13_epoch_rewards.ts +++ b/packages/protocol/migrations/13_epoch_rewards.ts @@ -10,8 +10,9 @@ const initializeArgs = async (): Promise => { toFixed(config.epochRewards.targetVotingYieldParameters.initial).toFixed(), toFixed(config.epochRewards.targetVotingYieldParameters.max).toFixed(), toFixed(config.epochRewards.targetVotingYieldParameters.adjustmentFactor).toFixed(), - toFixed(config.epochRewards.rewardsMultiplierAdjustmentFactors.underspend).toFixed(), - toFixed(config.epochRewards.rewardsMultiplierAdjustmentFactors.overspend).toFixed(), + toFixed(config.epochRewards.rewardsMultiplierParameters.max).toFixed(), + toFixed(config.epochRewards.rewardsMultiplierParameters.adjustmentFactors.underspend).toFixed(), + toFixed(config.epochRewards.rewardsMultiplierParameters.adjustmentFactors.overspend).toFixed(), toFixed(config.epochRewards.targetVotingGoldFraction).toFixed(), config.epochRewards.maxValidatorEpochPayment, ] diff --git a/packages/protocol/migrationsConfig.js b/packages/protocol/migrationsConfig.js index de3ba5e4146..fdb57a5abd2 100644 --- a/packages/protocol/migrationsConfig.js +++ b/packages/protocol/migrationsConfig.js @@ -30,9 +30,12 @@ const DefaultConfig = { max: 2 / 10, adjustmentFactor: 1 / 365, }, - rewardsMultiplierAdjustmentFactors: { - underspend: 1 / 2, - overspend: 5, + rewardsMultiplierParameters: { + max: 2, + adjustmentFactors: { + underspend: 1 / 2, + overspend: 5, + }, }, targetVotingGoldFraction: 2 / 3, maxValidatorEpochPayment: '1000000000000000000', From 568659c0eadf48e532247becad2bb965d5528ff3 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Fri, 1 Nov 2019 15:45:29 -0700 Subject: [PATCH 095/149] end-to-end tests working --- .../src/e2e-tests/governance_tests.ts | 36 ++++++++++++++----- .../contracts/governance/EpochRewards.sol | 17 ++++----- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/packages/celotool/src/e2e-tests/governance_tests.ts b/packages/celotool/src/e2e-tests/governance_tests.ts index cf5ce53a7cf..f1cc464b134 100644 --- a/packages/celotool/src/e2e-tests/governance_tests.ts +++ b/packages/celotool/src/e2e-tests/governance_tests.ts @@ -132,7 +132,12 @@ describe('governance tests', () => { if (expected.isZero()) { assert.equal(difference.toFixed(), expected.toFixed()) } else { - assert.closeTo(difference.toNumber(), expected.toNumber(), delta.toNumber()) + const isCloseTo = + difference.plus(delta).gte(expected) || difference.minus(delta).lte(expected) + assert( + isCloseTo, + `expected ${expected.toString()} to be close to ${difference.toString()} +/- ${delta.toString()}` + ) } } @@ -513,7 +518,17 @@ describe('governance tests', () => { const previousTarget = new BigNumber( (await epochRewards.methods.getTargetVotingYieldParameters().call({}, blockNumber - 1))[0] ) - assert.equal(currentTarget.minus(previousTarget).toFixed(), expected.toFixed()) + const difference = currentTarget.minus(previousTarget) + + // Assert equal to 10 decimal places due to rounding errors. + assert.equal( + fromFixed(difference) + .dp(10) + .toFixed(), + fromFixed(expected) + .dp(10) + .toFixed() + ) } const assertTargetVotingYieldUnchanged = async (blockNumber: number) => { @@ -522,16 +537,21 @@ describe('governance tests', () => { for (const blockNumber of blockNumbers) { if (isLastBlockOfEpoch(blockNumber, epoch)) { - const actualVotingPercentage = toFixed(new BigNumber(1)) - const targetVotingGoldPercentage = new BigNumber( + // We use the voting gold fraction from before the rewards are granted. + const votingGoldFraction = new BigNumber( + await epochRewards.methods.getVotingGoldFraction().call({}, blockNumber - 1) + ) + const targetVotingGoldFraction = new BigNumber( await epochRewards.methods.getTargetVotingGoldFraction().call({}, blockNumber) ) - const difference = actualVotingPercentage.minus(targetVotingGoldPercentage) - const adjustmentFactor = new BigNumber( - (await epochRewards.methods.getTargetVotingYieldParameters().call({}, blockNumber))[1] + const difference = targetVotingGoldFraction.minus(votingGoldFraction) + const adjustmentFactor = fromFixed( + new BigNumber( + (await epochRewards.methods.getTargetVotingYieldParameters().call({}, blockNumber))[2] + ) ) const delta = difference.times(adjustmentFactor) - await assertTargetVotingYieldChanged(blockNumber, fromFixed(delta)) + await assertTargetVotingYieldChanged(blockNumber, delta) } else { await assertTargetVotingYieldUnchanged(blockNumber) } diff --git a/packages/protocol/contracts/governance/EpochRewards.sol b/packages/protocol/contracts/governance/EpochRewards.sol index dc58ee8f9d0..bbbd9a4effd 100644 --- a/packages/protocol/contracts/governance/EpochRewards.sol +++ b/packages/protocol/contracts/governance/EpochRewards.sol @@ -49,7 +49,6 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry event MaxValidatorEpochPaymentSet(uint256 payment); event TargetVotingYieldParametersSet(uint256 max, uint256 adjustmentFactor); event RewardsMultiplierParametersSet(uint256 max, uint256 underspendAdjustmentFactor, uint256 overspendAdjustmentFactor); - event Debug(uint256 value, string desc); /** * @param _maxValidatorEpochPayment The duration the above gold remains locked after deregistration. @@ -208,20 +207,20 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry return targetEpochPayment; } - function _updateTargetVotingYield() internal { + function getVotingGoldFraction() public view returns (uint256) { // TODO(asa): Ignore custodial accounts. address reserveAddress = registry.getAddressForOrDie(RESERVE_REGISTRY_ID); uint256 liquidGold = getGoldToken().totalSupply().sub(reserveAddress.balance); // TODO(asa): Should this be active votes? uint256 votingGold = getElection().getTotalVotes(); - FixidityLib.Fraction memory votingGoldFraction = FixidityLib.newFixed(votingGold).divide(FixidityLib.newFixed(liquidGold)); - emit Debug(votingGoldFraction.unwrap(), "voting gold fraction"); - emit Debug(targetVotingGoldFraction.unwrap(), "target voting gold fraction"); + return FixidityLib.newFixed(votingGold).divide(FixidityLib.newFixed(liquidGold)).unwrap(); + } + + function _updateTargetVotingYield() internal { + FixidityLib.Fraction memory votingGoldFraction = FixidityLib.wrap(getVotingGoldFraction()); if (votingGoldFraction.gt(targetVotingGoldFraction)) { FixidityLib.Fraction memory votingGoldFractionDelta = votingGoldFraction.subtract(targetVotingGoldFraction); - emit Debug(votingGoldFractionDelta.unwrap(), "voting gold fraction delta"); FixidityLib.Fraction memory targetVotingYieldDelta = votingGoldFractionDelta.multiply(targetVotingYieldParams.adjustmentFactor); - emit Debug(targetVotingYieldDelta.unwrap(), "target voting yield delta"); if (targetVotingYieldDelta.gte(targetVotingYieldParams.target)) { targetVotingYieldParams.target = FixidityLib.newFixed(0); } else { @@ -229,9 +228,7 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry } } else if (votingGoldFraction.lt(targetVotingGoldFraction)) { FixidityLib.Fraction memory votingGoldFractionDelta = targetVotingGoldFraction.subtract(votingGoldFraction); - emit Debug(votingGoldFractionDelta.unwrap(), "voting gold fraction delta"); FixidityLib.Fraction memory targetVotingYieldDelta = votingGoldFractionDelta.multiply(targetVotingYieldParams.adjustmentFactor); - emit Debug(targetVotingYieldDelta.unwrap(), "target voting yield delta"); targetVotingYieldParams.target = targetVotingYieldParams.target.add(targetVotingYieldDelta); if (targetVotingYieldParams.target.gt(targetVotingYieldParams.max)) { targetVotingYieldParams.target = targetVotingYieldParams.max; @@ -241,7 +238,7 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry function updateTargetVotingYield() external { require(msg.sender == address(0)); - // _updateTargetVotingYield(); + _updateTargetVotingYield(); } function calculateTargetEpochPaymentAndRewards() external view returns (uint256, uint256) { From a871d6755f94dc1fbc17e20a6d9cd35959792454 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Fri, 1 Nov 2019 16:00:25 -0700 Subject: [PATCH 096/149] Fix merge conflicts --- .../protocol/contracts/governance/Governance.sol | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/packages/protocol/contracts/governance/Governance.sol b/packages/protocol/contracts/governance/Governance.sol index 2e484b2c965..8954f8f9007 100644 --- a/packages/protocol/contracts/governance/Governance.sol +++ b/packages/protocol/contracts/governance/Governance.sol @@ -775,19 +775,6 @@ contract Governance is return true; } - /** - * @notice Returns the participation parameters. - * @return The participation parameters. - */ - function getParticipationParameters() external view returns (uint256, uint256, uint256, uint256) { - return ( - participationParameters.baseline.unwrap(), - participationParameters.baselineFloor.unwrap(), - participationParameters.baselineUpdateFactor.unwrap(), - participationParameters.baselineQuorumFactor.unwrap() - ); - } - /** * @notice Returns whether or not a particular account is voting on proposals. * @param account The address of the account. From b4638d521f87a30c06aa97d62639dceea973e05a Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Fri, 1 Nov 2019 18:55:07 -0700 Subject: [PATCH 097/149] Fix linting --- .../contracts/governance/EpochRewards.sol | 93 ++++++++++++++----- .../contracts/governance/Validators.sol | 17 +++- .../governance/test/EpochRewardsTest.sol | 8 +- .../governance/test/ValidatorsTest.sol | 8 +- 4 files changed, 99 insertions(+), 27 deletions(-) diff --git a/packages/protocol/contracts/governance/EpochRewards.sol b/packages/protocol/contracts/governance/EpochRewards.sol index bbbd9a4effd..fe6837be6c2 100644 --- a/packages/protocol/contracts/governance/EpochRewards.sol +++ b/packages/protocol/contracts/governance/EpochRewards.sol @@ -29,7 +29,7 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry } struct RewardsMultiplierParameters { - RewardsMultiplierAdjustmentFactors adjustmentFactors; + RewardsMultiplierAdjustmentFactors adjustmentFactors; FixidityLib.Fraction max; } @@ -48,11 +48,12 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry event TargetVotingGoldFractionSet(uint256 fraction); event MaxValidatorEpochPaymentSet(uint256 payment); event TargetVotingYieldParametersSet(uint256 max, uint256 adjustmentFactor); - event RewardsMultiplierParametersSet(uint256 max, uint256 underspendAdjustmentFactor, uint256 overspendAdjustmentFactor); + event RewardsMultiplierParametersSet( + uint256 max, + uint256 underspendAdjustmentFactor, + uint256 overspendAdjustmentFactor + ); - /** - * @param _maxValidatorEpochPayment The duration the above gold remains locked after deregistration. - */ function initialize( address registryAddress, uint256 targetVotingYieldInitial, @@ -88,7 +89,11 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry function getRewardsMultiplierParameters() external view returns (uint256, uint256, uint256) { RewardsMultiplierParameters storage params = rewardsMultiplierParams; - return (params.max.unwrap(), params.adjustmentFactors.underspend.unwrap(), params.adjustmentFactors.overspend.unwrap()); + return ( + params.max.unwrap(), + params.adjustmentFactors.underspend.unwrap(), + params.adjustmentFactors.overspend.unwrap() + ); } function setTargetVotingGoldFraction(uint256 value) public onlyOwner returns (bool) { @@ -114,11 +119,19 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry return true; } - function setRewardsMultiplierParameters(uint256 max, uint256 underspendAdjustmentFactor, uint256 overspendAdjustmentFactor) public onlyOwner returns (bool) { + function setRewardsMultiplierParameters( + uint256 max, + uint256 underspendAdjustmentFactor, + uint256 overspendAdjustmentFactor + ) + public + onlyOwner + returns (bool) + { require( max != rewardsMultiplierParams.max.unwrap() || - underspendAdjustmentFactor != rewardsMultiplierParams.adjustmentFactors.underspend.unwrap() || - overspendAdjustmentFactor != rewardsMultiplierParams.adjustmentFactors.overspend.unwrap() + overspendAdjustmentFactor != rewardsMultiplierParams.adjustmentFactors.overspend.unwrap() || + underspendAdjustmentFactor != rewardsMultiplierParams.adjustmentFactors.underspend.unwrap() ); rewardsMultiplierParams = RewardsMultiplierParameters( RewardsMultiplierAdjustmentFactors( @@ -127,11 +140,22 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry ), FixidityLib.wrap(max) ); - emit RewardsMultiplierParametersSet(max, underspendAdjustmentFactor, overspendAdjustmentFactor); + emit RewardsMultiplierParametersSet( + max, + underspendAdjustmentFactor, + overspendAdjustmentFactor + ); return true; } - function setTargetVotingYieldParameters(uint256 max, uint256 adjustmentFactor) public onlyOwner returns (bool) { + function setTargetVotingYieldParameters( + uint256 max, + uint256 adjustmentFactor + ) + public + onlyOwner + returns (bool) + { require( max != targetVotingYieldParams.max.unwrap() || adjustmentFactor != targetVotingYieldParams.adjustmentFactor.unwrap() @@ -159,12 +183,20 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry } } - function _getRewardsMultiplier(uint256 targetGoldSupplyIncrease) internal view returns (FixidityLib.Fraction memory) { + function _getRewardsMultiplier( + uint256 targetGoldSupplyIncrease + ) + internal + view + returns (FixidityLib.Fraction memory) + { uint256 targetSupply = getTargetGoldTotalSupply(); uint256 totalSupply = getGoldToken().totalSupply(); uint256 remainingSupply = GOLD_SUPPLY_CAP.sub(totalSupply.add(targetGoldSupplyIncrease)); uint256 targetRemainingSupply = GOLD_SUPPLY_CAP.sub(targetSupply); - FixidityLib.Fraction memory ratio = FixidityLib.newFixed(remainingSupply).divide(FixidityLib.newFixed(targetRemainingSupply)); + FixidityLib.Fraction memory ratio = FixidityLib.newFixed(remainingSupply).divide( + FixidityLib.newFixed(targetRemainingSupply) + ); if (ratio.gt(FixidityLib.fixed1())) { FixidityLib.Fraction memory delta = ratio.subtract(FixidityLib.fixed1()).multiply( rewardsMultiplierParams.adjustmentFactors.underspend @@ -180,7 +212,7 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry rewardsMultiplierParams.adjustmentFactors.overspend ); if (delta.lt(FixidityLib.fixed1())) { - return FixidityLib.fixed1().subtract(delta); + return FixidityLib.fixed1().subtract(delta); } else { return FixidityLib.wrap(0); } @@ -197,14 +229,17 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry } function getTargetEpochRewards() public view returns (uint256) { - return FixidityLib.newFixed(getElection().getActiveVotes()).multiply(targetVotingYieldParams.target).fromFixed(); + return FixidityLib.newFixed(getElection().getActiveVotes()).multiply( + targetVotingYieldParams.target + ).fromFixed(); } function getTargetTotalEpochPaymentsInGold() public view returns (uint256) { address stableTokenAddress = registry.getAddressForOrDie(STABLE_TOKEN_REGISTRY_ID); (uint256 numerator, uint256 denominator) = getSortedOracles().medianRate(stableTokenAddress); - uint256 targetEpochPayment = numberValidatorsInCurrentSet().mul(maxValidatorEpochPayment).mul(denominator).div(numerator); - return targetEpochPayment; + return numberValidatorsInCurrentSet().mul(maxValidatorEpochPayment).mul(denominator).div( + numerator + ); } function getVotingGoldFraction() public view returns (uint256) { @@ -219,16 +254,26 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry function _updateTargetVotingYield() internal { FixidityLib.Fraction memory votingGoldFraction = FixidityLib.wrap(getVotingGoldFraction()); if (votingGoldFraction.gt(targetVotingGoldFraction)) { - FixidityLib.Fraction memory votingGoldFractionDelta = votingGoldFraction.subtract(targetVotingGoldFraction); - FixidityLib.Fraction memory targetVotingYieldDelta = votingGoldFractionDelta.multiply(targetVotingYieldParams.adjustmentFactor); + FixidityLib.Fraction memory votingGoldFractionDelta = votingGoldFraction.subtract( + targetVotingGoldFraction + ); + FixidityLib.Fraction memory targetVotingYieldDelta = votingGoldFractionDelta.multiply( + targetVotingYieldParams.adjustmentFactor + ); if (targetVotingYieldDelta.gte(targetVotingYieldParams.target)) { targetVotingYieldParams.target = FixidityLib.newFixed(0); } else { - targetVotingYieldParams.target = targetVotingYieldParams.target.subtract(targetVotingYieldDelta); + targetVotingYieldParams.target = targetVotingYieldParams.target.subtract( + targetVotingYieldDelta + ); } } else if (votingGoldFraction.lt(targetVotingGoldFraction)) { - FixidityLib.Fraction memory votingGoldFractionDelta = targetVotingGoldFraction.subtract(votingGoldFraction); - FixidityLib.Fraction memory targetVotingYieldDelta = votingGoldFractionDelta.multiply(targetVotingYieldParams.adjustmentFactor); + FixidityLib.Fraction memory votingGoldFractionDelta = targetVotingGoldFraction.subtract( + votingGoldFraction + ); + FixidityLib.Fraction memory targetVotingYieldDelta = votingGoldFractionDelta.multiply( + targetVotingYieldParams.adjustmentFactor + ); targetVotingYieldParams.target = targetVotingYieldParams.target.add(targetVotingYieldDelta); if (targetVotingYieldParams.target.gt(targetVotingYieldParams.max)) { targetVotingYieldParams.target = targetVotingYieldParams.max; @@ -245,7 +290,9 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry uint256 targetEpochRewards = getTargetEpochRewards(); uint256 targetTotalEpochPaymentsInGold = getTargetTotalEpochPaymentsInGold(); uint256 targetGoldSupplyIncrease = targetEpochRewards.add(targetTotalEpochPaymentsInGold); - FixidityLib.Fraction memory rewardsMultiplier = _getRewardsMultiplier(targetGoldSupplyIncrease); + FixidityLib.Fraction memory rewardsMultiplier = _getRewardsMultiplier( + targetGoldSupplyIncrease + ); return ( FixidityLib.newFixed(maxValidatorEpochPayment).multiply(rewardsMultiplier).fromFixed(), FixidityLib.newFixed(targetEpochRewards).multiply(rewardsMultiplier).fromFixed() diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index 0d234b5eda1..a838a80fc0d 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -386,14 +386,27 @@ contract Validators is /** * @notice Distributes epoch payments to `validator` and its group. */ - function distributeEpochPayment(address validator, uint256 maxPayment) external onlyVm() returns (uint256) { + function distributeEpochPayment( + address validator, + uint256 maxPayment + ) + external + onlyVm() + returns (uint256) + { return _distributeEpochPayment(validator, maxPayment); } /** * @notice Distributes epoch payments to `validator` and its group. */ - function _distributeEpochPayment(address validator, uint256 maxPayment) internal returns (uint256) { + function _distributeEpochPayment( + address validator, + uint256 maxPayment + ) + internal + returns (uint256) + { address account = getAccounts().validationSignerToAccount(validator); require(isValidator(account)); // The group that should be paid is the group that the validator was a member of at the diff --git a/packages/protocol/contracts/governance/test/EpochRewardsTest.sol b/packages/protocol/contracts/governance/test/EpochRewardsTest.sol index 8dd822aab0a..e0ab169ab0d 100644 --- a/packages/protocol/contracts/governance/test/EpochRewardsTest.sol +++ b/packages/protocol/contracts/governance/test/EpochRewardsTest.sol @@ -8,7 +8,13 @@ import "../../common/FixidityLib.sol"; */ contract EpochRewardsTest is EpochRewards { - function getRewardsMultiplier(uint256 targetGoldTotalSupplyIncrease) external view returns (uint256) { + function getRewardsMultiplier( + uint256 targetGoldTotalSupplyIncrease + ) + external + view + returns (uint256) + { return _getRewardsMultiplier(targetGoldTotalSupplyIncrease).unwrap(); } diff --git a/packages/protocol/contracts/governance/test/ValidatorsTest.sol b/packages/protocol/contracts/governance/test/ValidatorsTest.sol index dc017bd6092..36b08b02a9d 100644 --- a/packages/protocol/contracts/governance/test/ValidatorsTest.sol +++ b/packages/protocol/contracts/governance/test/ValidatorsTest.sol @@ -12,7 +12,13 @@ contract ValidatorsTest is Validators { return _updateValidatorScore(validator, uptime); } - function distributeEpochPayment(address validator, uint256 maxPayment) external returns (uint256) { + function distributeEpochPayment( + address validator, + uint256 maxPayment + ) + external + returns (uint256) + { return _distributeEpochPayment(validator, maxPayment); } } From 9fe10d77a5be7ba1f02f35493ab001db55f6010d Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Fri, 1 Nov 2019 19:14:15 -0700 Subject: [PATCH 098/149] Add natspecs --- .../src/e2e-tests/governance_tests.ts | 2 +- .../contracts/governance/EpochRewards.sol | 109 +++++++++++++++--- 2 files changed, 97 insertions(+), 14 deletions(-) diff --git a/packages/celotool/src/e2e-tests/governance_tests.ts b/packages/celotool/src/e2e-tests/governance_tests.ts index 7f83ee75033..ed737f0c6cb 100644 --- a/packages/celotool/src/e2e-tests/governance_tests.ts +++ b/packages/celotool/src/e2e-tests/governance_tests.ts @@ -144,7 +144,7 @@ describe('governance tests', () => { } } - describe.only('when the validator set is changing', () => { + describe('when the validator set is changing', () => { let epoch: number const blockNumbers: number[] = [] let allValidators: string[] diff --git a/packages/protocol/contracts/governance/EpochRewards.sol b/packages/protocol/contracts/governance/EpochRewards.sol index fe6837be6c2..4985d492666 100644 --- a/packages/protocol/contracts/governance/EpochRewards.sol +++ b/packages/protocol/contracts/governance/EpochRewards.sol @@ -20,8 +20,6 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry uint256 constant GOLD_SUPPLY_CAP = 1000000000000000000000000000; uint256 constant YEARS_LINEAR = 15; uint256 constant SECONDS_LINEAR = YEARS_LINEAR * 365 * 1 days; - uint256 constant FIXIDITY_E = 2718281828459045235360287; - uint256 constant FIXIDITY_LN2 = 693147180559945309417232; struct RewardsMultiplierAdjustmentFactors { FixidityLib.Fraction underspend; @@ -43,10 +41,10 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry RewardsMultiplierParameters private rewardsMultiplierParams; TargetVotingYieldParameters private targetVotingYieldParams; FixidityLib.Fraction private targetVotingGoldFraction; - uint256 public maxValidatorEpochPayment; + uint256 public targetValidatorEpochPayment; event TargetVotingGoldFractionSet(uint256 fraction); - event MaxValidatorEpochPaymentSet(uint256 payment); + event TargetValidatorEpochPaymentSet(uint256 payment); event TargetVotingYieldParametersSet(uint256 max, uint256 adjustmentFactor); event RewardsMultiplierParametersSet( uint256 max, @@ -54,6 +52,21 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry uint256 overspendAdjustmentFactor ); + /** + * @notice Initializes critical variables. + * @param registryAddress The address of the registry contract. + * @param targetVotingYieldInitial The initial relative target block reward for voters. + * @param targetVotingYieldMax The max relative target block reward for voters. + * @param targetVotingYieldAdjustmentFactor The target block reward adjustment factor for voters. + * @param rewardsMultiplierMax The max multiplier on target epoch rewards. + * @param rewardsMultiplierUnderspendAdjustmentFactor Adjusts the multiplier on target epoch + * rewards when the protocol is running behind the target gold supply. + * @param rewardsMultiplierOverspendAdjustmentFactor Adjusts the multiplier on target epoch + * rewards when the protocol is running ahead of the target gold supply. + * @param _targetVotingGoldFracion The percentage of floating gold voting to target. + * @param _targetValidatorEpochPayment The target validator epoch payment. + * @dev Should be called only once. + */ function initialize( address registryAddress, uint256 targetVotingYieldInitial, @@ -63,7 +76,7 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry uint256 rewardsMultiplierUnderspendAdjustmentFactor, uint256 rewardsMultiplierOverspendAdjustmentFactor, uint256 _targetVotingGoldFraction, - uint256 _maxValidatorEpochPayment + uint256 _targetValidatorEpochPayment ) external initializer @@ -77,16 +90,24 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry rewardsMultiplierOverspendAdjustmentFactor ); setTargetVotingGoldFraction(_targetVotingGoldFraction); - setMaxValidatorEpochPayment(_maxValidatorEpochPayment); + setTargetValidatorEpochPayment(_targetValidatorEpochPayment); targetVotingYieldParams.target = FixidityLib.wrap(targetVotingYieldInitial); startTime = now; } + /** + * @notice Returns the target voting yield parameters. + * @return The target, max, and adjustment factor for target voting yield. + */ function getTargetVotingYieldParameters() external view returns (uint256, uint256, uint256) { TargetVotingYieldParameters storage params = targetVotingYieldParams; return (params.target.unwrap(), params.max.unwrap(), params.adjustmentFactor.unwrap()); } + /** + * @notice Returns the rewards multiplier parameters. + * @return The max multiplier and under/over spend adjustment factors. + */ function getRewardsMultiplierParameters() external view returns (uint256, uint256, uint256) { RewardsMultiplierParameters storage params = rewardsMultiplierParams; return ( @@ -96,6 +117,11 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry ); } + /** + * @notice Sets the target voting gold fraction. + * @param value The percentage of floating gold voting to target. + * @return True upon success. + */ function setTargetVotingGoldFraction(uint256 value) public onlyOwner returns (bool) { require(value != targetVotingGoldFraction.unwrap() && value < FixidityLib.fixed1().unwrap()); targetVotingGoldFraction = FixidityLib.wrap(value); @@ -103,22 +129,35 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry return true; } + /** + * @notice Returns the target voting gold fraction. + * @return The percentage of floating gold voting to target. + */ function getTargetVotingGoldFraction() external view returns (uint256) { return targetVotingGoldFraction.unwrap(); } /** - * @notice Sets the max per-epoch payment in Celo Dollars for validators. + * @notice Sets the target per-epoch payment in Celo Dollars for validators. * @param value The value in Celo Dollars. * @return True upon success. */ - function setMaxValidatorEpochPayment(uint256 value) public onlyOwner returns (bool) { - require(value != maxValidatorEpochPayment); - maxValidatorEpochPayment = value; - emit MaxValidatorEpochPaymentSet(value); + function setTargetValidatorEpochPayment(uint256 value) public onlyOwner returns (bool) { + require(value != targetValidatorEpochPayment); + targetValidatorEpochPayment = value; + emit TargetValidatorEpochPaymentSet(value); return true; } + /** + * @notice Sets the rewards multiplier parameters. + * @param max The max multiplier on target epoch rewards. + * @param underspendAdjustmentFactor Adjusts the multiplier on target epoch rewards when the + * protocol is running behind the target gold supply. + * @param overspendAdjustmentFactor Adjusts the multiplier on target epoch rewards when the + * protocol is running ahead of the target gold supply. + * @return True upon success. + */ function setRewardsMultiplierParameters( uint256 max, uint256 underspendAdjustmentFactor, @@ -148,6 +187,12 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry return true; } + /** + * @notice Sets the target voting yield parameters. + * @param max The max relative target block reward for voters. + * @param adjustmentFactor The target block reward adjustment factor for voters. + * @return True upon success. + */ function setTargetVotingYieldParameters( uint256 max, uint256 adjustmentFactor @@ -170,6 +215,10 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry return true; } + /** + * @notice Returns the target gold supply according to the epoch rewards target schedule. + * @return The target gold supply according to the epoch rewards target schedule. + */ function getTargetGoldTotalSupply() public view returns (uint256) { uint256 timeSinceInitialization = now.sub(startTime); if (timeSinceInitialization < SECONDS_LINEAR) { @@ -183,6 +232,11 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry } } + /** + * @notice Returns the rewards multiplier based on the current and target gold supplies. + * @param targetGoldSupplyIncrease The target increase in current gold supply. + * @return The rewards multiplier based on the current and target gold supplies. + */ function _getRewardsMultiplier( uint256 targetGoldSupplyIncrease ) @@ -221,6 +275,10 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry } } + /** + * @notice Returns the rewards multiplier based on the current and target gold supplies. + * @return The rewards multiplier based on the current and target gold supplies. + */ function getRewardsMultiplier() external view returns (uint256) { uint256 targetEpochRewards = getTargetEpochRewards(); uint256 targetTotalEpochPaymentsInGold = getTargetTotalEpochPaymentsInGold(); @@ -228,20 +286,32 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry return _getRewardsMultiplier(targetGoldSupplyIncrease).unwrap(); } + /** + * @notice Returns the total target epoch rewards for voters. + * @return the total target epoch rewards for voters. + */ function getTargetEpochRewards() public view returns (uint256) { return FixidityLib.newFixed(getElection().getActiveVotes()).multiply( targetVotingYieldParams.target ).fromFixed(); } + /** + * @notice Returns the total target epoch payments to validators, converted to gold. + * @return The total target epoch payments to validators, converted to gold. + */ function getTargetTotalEpochPaymentsInGold() public view returns (uint256) { address stableTokenAddress = registry.getAddressForOrDie(STABLE_TOKEN_REGISTRY_ID); (uint256 numerator, uint256 denominator) = getSortedOracles().medianRate(stableTokenAddress); - return numberValidatorsInCurrentSet().mul(maxValidatorEpochPayment).mul(denominator).div( + return numberValidatorsInCurrentSet().mul(targetValidatorEpochPayment).mul(denominator).div( numerator ); } + /** + * @notice Returns the fraction of floating gold being used for voting in validator elections. + * @return The fraction of floating gold being used for voting in validator elections. + */ function getVotingGoldFraction() public view returns (uint256) { // TODO(asa): Ignore custodial accounts. address reserveAddress = registry.getAddressForOrDie(RESERVE_REGISTRY_ID); @@ -251,6 +321,10 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry return FixidityLib.newFixed(votingGold).divide(FixidityLib.newFixed(liquidGold)).unwrap(); } + /** + * @notice Updates the target voting yield based on the difference between the target and current + * voting gold fraction. + */ function _updateTargetVotingYield() internal { FixidityLib.Fraction memory votingGoldFraction = FixidityLib.wrap(getVotingGoldFraction()); if (votingGoldFraction.gt(targetVotingGoldFraction)) { @@ -281,11 +355,20 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry } } + /** + * @notice Updates the target voting yield based on the difference between the target and current + * voting gold fraction. + * @dev Only called directly by the protocol. + */ function updateTargetVotingYield() external { require(msg.sender == address(0)); _updateTargetVotingYield(); } + /** + * @notice Calculates the per validator epoch payment and the total rewards to voters. + * @return The per validator epoch payment and the total rewards to voters. + */ function calculateTargetEpochPaymentAndRewards() external view returns (uint256, uint256) { uint256 targetEpochRewards = getTargetEpochRewards(); uint256 targetTotalEpochPaymentsInGold = getTargetTotalEpochPaymentsInGold(); @@ -294,7 +377,7 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry targetGoldSupplyIncrease ); return ( - FixidityLib.newFixed(maxValidatorEpochPayment).multiply(rewardsMultiplier).fromFixed(), + FixidityLib.newFixed(targetValidatorEpochPayment).multiply(rewardsMultiplier).fromFixed(), FixidityLib.newFixed(targetEpochRewards).multiply(rewardsMultiplier).fromFixed() ); } From 990fc148a69365b2aeb366cfe1c4353de917fbe3 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Fri, 1 Nov 2019 19:33:17 -0700 Subject: [PATCH 099/149] reduce gold requirement for contractkit tests --- packages/protocol/migrationsConfig.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/protocol/migrationsConfig.js b/packages/protocol/migrationsConfig.js index ba165e4b72f..cb034a6bcfc 100644 --- a/packages/protocol/migrationsConfig.js +++ b/packages/protocol/migrationsConfig.js @@ -82,11 +82,11 @@ const DefaultConfig = { }, validators: { groupLockedGoldRequirements: { - value: '10000000000000000000000', // 10000 gold + value: '1000000000000000000', // 1 gold duration: 60 * 24 * 60 * 60, // 60 days }, validatorLockedGoldRequirements: { - value: '10000000000000000000000', // 10000 gold + value: '1000000000000000000', // 1 gold duration: 60 * 24 * 60 * 60, // 60 days }, validatorScoreParameters: { From 5d910c9d1c7deda400e5cc6df0852e77a15ea870 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Fri, 1 Nov 2019 20:24:49 -0700 Subject: [PATCH 100/149] Be sure to migrate random contract in validator order end-to-end tests --- packages/celotool/src/e2e-tests/validator_order_tests.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/celotool/src/e2e-tests/validator_order_tests.ts b/packages/celotool/src/e2e-tests/validator_order_tests.ts index 8360be0101b..2e71aafc78f 100644 --- a/packages/celotool/src/e2e-tests/validator_order_tests.ts +++ b/packages/celotool/src/e2e-tests/validator_order_tests.ts @@ -10,7 +10,7 @@ const BLOCK_COUNT = EPOCH * EPOCHS_TO_WAIT describe('governance tests', () => { const gethConfig: GethTestConfig = { - migrateTo: 13, + migrateTo: 14, instances: _.range(VALIDATORS).map((i) => ({ name: `validator${i}`, validating: true, From e4a9c0b0bffde1771401290f8c5a3f4e59cbf55a Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Mon, 4 Nov 2019 09:27:49 -0800 Subject: [PATCH 101/149] Small cleanup --- packages/protocol/contracts/governance/EpochRewards.sol | 2 +- packages/protocol/contracts/governance/Validators.sol | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/protocol/contracts/governance/EpochRewards.sol b/packages/protocol/contracts/governance/EpochRewards.sol index 4985d492666..abd0c401693 100644 --- a/packages/protocol/contracts/governance/EpochRewards.sol +++ b/packages/protocol/contracts/governance/EpochRewards.sol @@ -63,7 +63,7 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry * rewards when the protocol is running behind the target gold supply. * @param rewardsMultiplierOverspendAdjustmentFactor Adjusts the multiplier on target epoch * rewards when the protocol is running ahead of the target gold supply. - * @param _targetVotingGoldFracion The percentage of floating gold voting to target. + * @param _targetVotingGoldFraction The percentage of floating gold voting to target. * @param _targetValidatorEpochPayment The target validator epoch payment. * @dev Should be called only once. */ diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index a689c9da30f..a838a80fc0d 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -158,7 +158,6 @@ contract Validators is setValidatorLockedGoldRequirements(validatorRequirementValue, validatorRequirementDuration); setValidatorScoreParameters(validatorScoreExponent, validatorScoreAdjustmentSpeed); setMaxGroupSize(_maxGroupSize); - setValidatorEpochPayment(_validatorEpochPayment); setMembershipHistoryLength(_membershipHistoryLength); } From e51e20d22e678f403003864466da38014b56a23d Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Wed, 6 Nov 2019 19:18:42 -0800 Subject: [PATCH 102/149] Update packages/protocol/contracts/governance/EpochRewards.sol Co-Authored-By: Victor "Nate" Graf --- packages/protocol/contracts/governance/EpochRewards.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/protocol/contracts/governance/EpochRewards.sol b/packages/protocol/contracts/governance/EpochRewards.sol index abd0c401693..fea4d922375 100644 --- a/packages/protocol/contracts/governance/EpochRewards.sol +++ b/packages/protocol/contracts/governance/EpochRewards.sol @@ -16,7 +16,7 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry using FixidityLib for FixidityLib.Fraction; using SafeMath for uint256; - uint256 constant GENESIS_GOLD_SUPPLY = 600000000000000000000000000; + uint256 constant GENESIS_GOLD_SUPPLY = 600000000000000000000000000; // 600 million Gold uint256 constant GOLD_SUPPLY_CAP = 1000000000000000000000000000; uint256 constant YEARS_LINEAR = 15; uint256 constant SECONDS_LINEAR = YEARS_LINEAR * 365 * 1 days; From 8d831ec125bd3cc0780966450b6e43ced3860ca7 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Wed, 6 Nov 2019 19:18:53 -0800 Subject: [PATCH 103/149] Update packages/protocol/contracts/governance/EpochRewards.sol Co-Authored-By: Victor "Nate" Graf --- packages/protocol/contracts/governance/EpochRewards.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/protocol/contracts/governance/EpochRewards.sol b/packages/protocol/contracts/governance/EpochRewards.sol index fea4d922375..f4ea1768583 100644 --- a/packages/protocol/contracts/governance/EpochRewards.sol +++ b/packages/protocol/contracts/governance/EpochRewards.sol @@ -17,7 +17,7 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry using SafeMath for uint256; uint256 constant GENESIS_GOLD_SUPPLY = 600000000000000000000000000; // 600 million Gold - uint256 constant GOLD_SUPPLY_CAP = 1000000000000000000000000000; + uint256 constant GOLD_SUPPLY_CAP = 1000000000000000000000000000; // 1 billion Gold uint256 constant YEARS_LINEAR = 15; uint256 constant SECONDS_LINEAR = YEARS_LINEAR * 365 * 1 days; From 9ee4f02b1f4b6473557c3823911d1424ff567c18 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Wed, 6 Nov 2019 19:46:11 -0800 Subject: [PATCH 104/149] Address comments --- .../src/e2e-tests/governance_tests.ts | 38 ++++--------------- .../contracts/governance/EpochRewards.sol | 15 ++++++++ .../contracts/governance/Validators.sol | 11 +++++- 3 files changed, 33 insertions(+), 31 deletions(-) diff --git a/packages/celotool/src/e2e-tests/governance_tests.ts b/packages/celotool/src/e2e-tests/governance_tests.ts index ed737f0c6cb..33652857004 100644 --- a/packages/celotool/src/e2e-tests/governance_tests.ts +++ b/packages/celotool/src/e2e-tests/governance_tests.ts @@ -125,13 +125,11 @@ describe('governance tests', () => { return blockNumber % epochSize === 0 } - const assertDifferenceCloseTo = ( - current: BigNumber, - previous: BigNumber, + const assertAlmostEqual = ( + actual: BigNumber, expected: BigNumber, - delta: BigNumber + delta: BigNumber = new BigNumber(10).pow(12).times(5) ) => { - const difference = current.minus(previous) if (expected.isZero()) { assert.equal(difference.toFixed(), expected.toFixed()) } else { @@ -139,7 +137,7 @@ describe('governance tests', () => { difference.plus(delta).gte(expected) || difference.minus(delta).lte(expected) assert( isCloseTo, - `expected ${expected.toString()} to be close to ${difference.toString()} +/- ${delta.toString()}` + `expected ${expected.toString()} to almost equal ${difference.toString()} +/- ${delta.toString()}` ) } } @@ -322,12 +320,7 @@ describe('governance tests', () => { ) assert.isNotNaN(currentBalance) assert.isNotNaN(previousBalance) - assertDifferenceCloseTo( - currentBalance, - previousBalance, - expected, - new BigNumber(10).pow(12).times(5) - ) + assertAlmostEqual(currentBalance.minus(previousBalance), expected) } const assertBalanceUnchanged = async (validator: string, blockNumber: number) => { @@ -389,12 +382,7 @@ describe('governance tests', () => { const previousVotes = new BigNumber( await election.methods.getTotalVotesForGroup(group).call({}, blockNumber - 1) ) - assertDifferenceCloseTo( - currentVotes, - previousVotes, - expected, - new BigNumber(10).pow(12).times(5) - ) + assertAlmostEqual(currentVotes.minus(previousVotes), expected) } const assertGoldTokenTotalSupplyChanged = async ( @@ -407,12 +395,7 @@ describe('governance tests', () => { const previousSupply = new BigNumber( await goldToken.methods.totalSupply().call({}, blockNumber - 1) ) - assertDifferenceCloseTo( - currentSupply, - previousSupply, - expected, - new BigNumber(10).pow(12).times(5) - ) + assertAlmostEqual(currentSupply.minus(previousSupply), expected) } const assertBalanceChanged = async ( @@ -426,12 +409,7 @@ describe('governance tests', () => { const previousBalance = new BigNumber( await goldToken.methods.balanceOf(address).call({}, blockNumber - 1) ) - assertDifferenceCloseTo( - currentBalance, - previousBalance, - expected, - new BigNumber(10).pow(12).times(5) - ) + assertAlmostEqual(currentBalance.minus(previousBalance), expected) } const assertLockedGoldBalanceChanged = async (blockNumber: number, expected: BigNumber) => { diff --git a/packages/protocol/contracts/governance/EpochRewards.sol b/packages/protocol/contracts/governance/EpochRewards.sol index f4ea1768583..eb9d0f9505e 100644 --- a/packages/protocol/contracts/governance/EpochRewards.sol +++ b/packages/protocol/contracts/governance/EpochRewards.sol @@ -21,19 +21,33 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry uint256 constant YEARS_LINEAR = 15; uint256 constant SECONDS_LINEAR = YEARS_LINEAR * 365 * 1 days; + // This struct governs how the rewards multiplier should deviate from 1.0 based on the ratio of + // supply remaining to target supply remaining. struct RewardsMultiplierAdjustmentFactors { FixidityLib.Fraction underspend; FixidityLib.Fraction overspend; } + // This struct governs the multiplier on the target rewards to give out in a given epoch due to + // potential deviations in the actual Gold total supply from the target total supply. + // In the case where the actual exceeds the target (i.e. the protocol has "overspent" with + // respect to epoch rewards and payments) the multiplier will be less than one. + // In the case where the actual is less than the target (i.e. the protocol has "underspent" with + // respect to epoch rewards and payments) the multiplier will be greater than one. struct RewardsMultiplierParameters { RewardsMultiplierAdjustmentFactors adjustmentFactors; + // The maximum rewards multiplier. FixidityLib.Fraction max; } + // This struct governs the target yield awarded to voters in validator elections. struct TargetVotingYieldParameters { + // The target yield awarded to users voting in validator elections. FixidityLib.Fraction target; + // Governs the adjustment of the target yield based on the deviation of the percentage of + // Gold voting in validator elections from the `targetVotingGoldFraction`. FixidityLib.Fraction adjustmentFactor; + // The maximum target yield awarded to users voting in validator elections. FixidityLib.Fraction max; } @@ -228,6 +242,7 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry return targetRewards.add(GENESIS_GOLD_SUPPLY); } else { // TODO(asa): Implement block reward calculation for years 15-30. + require(false); return 0; } } diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index a838a80fc0d..61df6b13ac1 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -385,6 +385,10 @@ contract Validators is /** * @notice Distributes epoch payments to `validator` and its group. + * @param validator The validator to distribute the epoch payment to. + * @param maxPayment The maximum payment to the validator. Actual payment is based on score and + * group commission. + * @return The total payment paid to the validator and their group. */ function distributeEpochPayment( address validator, @@ -399,6 +403,10 @@ contract Validators is /** * @notice Distributes epoch payments to `validator` and its group. + * @param validator The validator to distribute the epoch payment to. + * @param maxPayment The maximum payment to the validator. Actual payment is based on score and + * group commission. + * @return The total payment paid to the validator and their group. */ function _distributeEpochPayment( address validator, @@ -423,8 +431,9 @@ contract Validators is getStableToken().mint(group, groupPayment); getStableToken().mint(account, validatorPayment); return totalPayment.fromFixed(); + } else { + return 0; } - return 0; } /** From 464ec625893ef1657b65054fa0959a2326cff1e2 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Wed, 6 Nov 2019 19:46:58 -0800 Subject: [PATCH 105/149] Address comments --- packages/contractkit/src/kit.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/contractkit/src/kit.ts b/packages/contractkit/src/kit.ts index 9fb45094880..a997ff88e06 100644 --- a/packages/contractkit/src/kit.ts +++ b/packages/contractkit/src/kit.ts @@ -89,7 +89,6 @@ export class ContractKit { this.contracts.getReserve(), this.contracts.getStableToken(), this.contracts.getValidators(), - // this.contracts.getEpochRewards(), ]) const res = await Promise.all([ contracts[0].getConfig(), @@ -102,7 +101,6 @@ export class ContractKit { contracts[7].getConfig(), contracts[8].getConfig(), contracts[9].getConfig(), - // contracts[10].getConfig(), ]) return { exchange: res[0], @@ -115,7 +113,6 @@ export class ContractKit { reserve: res[7], stableToken: res[8], validators: res[9], - // epochRewards: res[10], } } From c2e9f00f65141643b877369a0314d5ac6ec6c938 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Thu, 7 Nov 2019 21:17:56 -0800 Subject: [PATCH 106/149] WIP --- .../src/e2e-tests/governance_tests.ts | 275 +++++++++++++----- packages/celotool/src/e2e-tests/utils.ts | 2 +- packages/contractkit/src/base.ts | 2 +- .../contractkit/src/web3-contract-cache.ts | 6 +- packages/contractkit/src/wrappers/Accounts.ts | 56 ++-- .../protocol/contracts/common/Accounts.sol | 26 +- .../contracts/governance/Validators.sol | 17 +- packages/protocol/scripts/bash/ganache.sh | 2 +- packages/protocol/test/common/accounts.ts | 4 +- 9 files changed, 267 insertions(+), 123 deletions(-) diff --git a/packages/celotool/src/e2e-tests/governance_tests.ts b/packages/celotool/src/e2e-tests/governance_tests.ts index 33652857004..84d9af27937 100644 --- a/packages/celotool/src/e2e-tests/governance_tests.ts +++ b/packages/celotool/src/e2e-tests/governance_tests.ts @@ -1,5 +1,4 @@ import { ContractKit, newKitFromWeb3 } from '@celo/contractkit' -import { AccountsWrapper } from '@celo/contractkit/lib/wrappers/Accounts' import { fromFixed, toFixed } from '@celo/utils/lib/fixidity' import BigNumber from 'bignumber.js' import { assert } from 'chai' @@ -10,8 +9,10 @@ describe('governance tests', () => { const gethConfig = { migrate: true, instances: [ + // Validators 0 and 1 are swapped in and out of the group. { name: 'validator0', validating: true, syncmode: 'full', port: 30303, rpcport: 8545 }, { name: 'validator1', validating: true, syncmode: 'full', port: 30305, rpcport: 8547 }, + // Validator 2 will authorize a validating key every other epoch. { name: 'validator2', validating: true, syncmode: 'full', port: 30307, rpcport: 8549 }, { name: 'validator3', validating: true, syncmode: 'full', port: 30309, rpcport: 8551 }, { name: 'validator4', validating: true, syncmode: 'full', port: 30311, rpcport: 8553 }, @@ -27,12 +28,12 @@ describe('governance tests', () => { let goldToken: any let registry: any let validators: any - let accounts: AccountsWrapper + let accounts: any let kit: ContractKit before(async function(this: any) { this.timeout(0) - await context.hooks.before() + // await context.hooks.before() }) after(context.hooks.after) @@ -48,7 +49,7 @@ describe('governance tests', () => { registry = await kit._web3Contracts.getRegistry() election = await kit._web3Contracts.getElection() epochRewards = await kit._web3Contracts.getEpochRewards() - accounts = await kit.contracts.getAccounts() + accounts = await kit._web3Contracts.getAccounts() } const unlockAccount = async (address: string, theWeb3: any) => { @@ -72,9 +73,17 @@ describe('governance tests', () => { } } - const getValidatorGroupKeys = async () => { + const getValidationSigner = async (address: string, blockNumber?: number) => { + if (blockNumber) { + return await accounts.methods.getValidationSigner(address).call({}, blockNumber) + } else { + return await accounts.methods.getValidationSigner(address).call() + } + } + + const getValidatorGroupPrivateKey = async () => { const [groupAddress] = await validators.methods.getRegisteredValidatorGroups().call() - const name = await accounts.getName(groupAddress) + const name = await accounts.methods.getName(groupAddress).call() const encryptedKeystore64 = name.split(' ')[1] const encryptedKeystore = JSON.parse(Buffer.from(encryptedKeystore64, 'base64').toString()) // The validator group ID is the validator group keystore encrypted with validator 0's @@ -82,7 +91,7 @@ describe('governance tests', () => { // @ts-ignore const encryptionKey = `0x${gethConfig.instances[0].privateKey}` const decryptedKeystore = web3.eth.accounts.decrypt(encryptedKeystore, encryptionKey) - return [groupAddress, decryptedKeystore.privateKey] + return decryptedKeystore.privateKey } const activate = async (account: string, txOptions: any = {}) => { @@ -96,12 +105,8 @@ describe('governance tests', () => { return tx.send({ from: account, ...txOptions, gas }) } - const removeMember = async ( - groupWeb3: any, - group: string, - member: string, - txOptions: any = {} - ) => { + const removeMember = async (groupWeb3: any, member: string, txOptions: any = {}) => { + const group = (await groupWeb3.eth.getAccounts())[0] await unlockAccount(group, groupWeb3) const tx = validators.methods.removeMember(member) let gas = txOptions.gas @@ -111,7 +116,8 @@ describe('governance tests', () => { return tx.send({ from: group, ...txOptions, gas }) } - const addMember = async (groupWeb3: any, group: string, member: string, txOptions: any = {}) => { + const addMember = async (groupWeb3: any, member: string, txOptions: any = {}) => { + const group = (await groupWeb3.eth.getAccounts())[0] await unlockAccount(group, groupWeb3) const tx = validators.methods.addMember(member) let gas = txOptions.gas @@ -121,6 +127,28 @@ describe('governance tests', () => { return tx.send({ from: group, ...txOptions, gas }) } + const authorizeValidationSigner = async ( + validatorWeb3: any, + signerWeb3: any, + txOptions: any = {} + ) => { + const validator = (await validatorWeb3.eth.getAccounts())[0] + const signer = (await signerWeb3.eth.getAccounts())[0] + await unlockAccount(validator, validatorWeb3) + await unlockAccount(signer, signerWeb3) + const pop = await (await newKitFromWeb3( + signerWeb3 + ).contracts.getAccounts()).generateProofOfSigningKeyPossession(validator, signer) + const validatorKit = newKitFromWeb3(validatorWeb3) + const validatorAccounts = await validatorKit._web3Contracts.getAccounts() + const tx = validatorAccounts.methods.authorizeValidationSigner(signer, pop.v, pop.r, pop.s) + let gas = txOptions.gas + if (!gas) { + gas = await tx.estimateGas({ ...txOptions }) + } + return tx.send({ from: validator, ...txOptions, gas }) + } + const isLastBlockOfEpoch = (blockNumber: number, epochSize: number) => { return blockNumber % epochSize === 0 } @@ -131,63 +159,125 @@ describe('governance tests', () => { delta: BigNumber = new BigNumber(10).pow(12).times(5) ) => { if (expected.isZero()) { - assert.equal(difference.toFixed(), expected.toFixed()) + assert.equal(actual.toFixed(), expected.toFixed()) } else { - const isCloseTo = - difference.plus(delta).gte(expected) || difference.minus(delta).lte(expected) + const isCloseTo = actual.plus(delta).gte(expected) || actual.minus(delta).lte(expected) assert( isCloseTo, - `expected ${expected.toString()} to almost equal ${difference.toString()} +/- ${delta.toString()}` + `expected ${actual.toString()} to almost equal ${expected.toString()} +/- ${delta.toString()}` ) } } - describe('when the validator set is changing', () => { + describe.only('when the validator set is changing', () => { let epoch: number const blockNumbers: number[] = [] - let allValidators: string[] + let validatorAccounts: string[] before(async function(this: any) { this.timeout(0) // Disable test timeout await restart() - const [groupAddress, groupPrivateKey] = await getValidatorGroupKeys() - - const groupInstance = { - name: 'validatorGroup', - validating: false, - syncmode: 'full', - port: 30325, - wsport: 8567, - privateKey: groupPrivateKey.slice(2), - peers: [await getEnode(8545)], - } - await initAndStartGeth(context.hooks.gethBinaryPath, groupInstance) - allValidators = await getValidatorGroupMembers() - assert.equal(allValidators.length, 5) + const groupPrivateKey = await getValidatorGroupPrivateKey() + const additionalNodes: any[] = [ + { + name: 'validatorGroup', + validating: false, + syncmode: 'full', + port: 30313, + wsport: 8555, + privateKey: groupPrivateKey.slice(2), + peers: [await getEnode(8545)], + }, + { + name: 'validator2KeyRotation0', + validating: true, + syncmode: 'full', + lightserv: false, + port: 30315, + wsport: 8557, + privateKey: 'a42ac9c99f6ab2c96ee6cae1b40d36187f65cd878737f6623cd363fb94ba7087', + peers: [await getEnode(8545)], + }, + { + name: 'validator2KeyRotation1', + validating: true, + syncmode: 'full', + lightserv: false, + port: 30317, + wsport: 8559, + privateKey: '4519cae145fb9499358be484ca60c80d8f5b7f9c13ff82c88ec9e13283e9de1a', + peers: [await getEnode(8545)], + }, + ] + await Promise.all( + additionalNodes.map((nodeConfig) => + initAndStartGeth(context.hooks.gethBinaryPath, nodeConfig) + ) + ) + + validatorAccounts = await getValidatorGroupMembers() + assert.equal(validatorAccounts.length, 5) epoch = new BigNumber(await validators.methods.getEpochSize().call()).toNumber() assert.equal(epoch, 10) - // Give the node time to sync, and time for an epoch transition so we can activate our vote. - await sleep(20) - await activate(allValidators[0]) - const groupWeb3 = new Web3('ws://localhost:8567') + // Give the nodes time to sync, and time for an epoch transition so we can activate our vote. + let blockNumber: number + do { + blockNumber = await web3.eth.getBlockNumber() + await sleep(0.1) + } while (blockNumber % epoch != 1) + + console.log('activating') + await activate(validatorAccounts[0]) + console.log('activated') + + // Prepare for member swapping. + const groupWeb3 = new Web3('ws://localhost:8555') const groupKit = newKitFromWeb3(groupWeb3) validators = await groupKit._web3Contracts.getValidators() - const membersToSwap = [allValidators[0], allValidators[1]] - let includedMemberIndex = 1 - await removeMember(groupWeb3, groupAddress, membersToSwap[0]) + const membersToSwap = [validatorAccounts[0], validatorAccounts[1]] + console.log('removing') + await removeMember(groupWeb3, membersToSwap[1]) + + // Prepare for key rotation. + const validatorWeb3 = new Web3('http://localhost:8549') + const authorizedWeb3s = [new Web3('ws://localhost:8557'), new Web3('ws://localhost:8559')] + + let index = 0 + let errorWhileChangingValidatorSet = '' + // Can't recycle signing keys. + let doneAuthorizing = false const changeValidatorSet = async (header: any) => { - blockNumbers.push(header.number) - // At the start of epoch N, swap members so the validator set is different for epoch N + 1. - if (header.number % epoch === 1) { - const memberToRemove = membersToSwap[includedMemberIndex] - const memberToAdd = membersToSwap[(includedMemberIndex + 1) % 2] - await removeMember(groupWeb3, groupAddress, memberToRemove) - await addMember(groupWeb3, groupAddress, memberToAdd) - includedMemberIndex = (includedMemberIndex + 1) % 2 - const newMembers = await getValidatorGroupMembers() - assert.include(newMembers, memberToAdd) - assert.notInclude(newMembers, memberToRemove) + try { + blockNumbers.push(header.number) + // At the start of epoch N, perform actions so the validator set is different for epoch N + 1. + if (header.number % epoch === 1) { + // 1. Swap validator0 and validator1 so one is a member of the group and the other is not. + const memberToRemove = membersToSwap[index] + const memberToAdd = membersToSwap[(index + 1) % 2] + console.log('swapping') + await removeMember(groupWeb3, memberToRemove) + await addMember(groupWeb3, memberToAdd) + const newMembers = await getValidatorGroupMembers() + assert.include(newMembers, memberToAdd) + assert.notInclude(newMembers, memberToRemove) + // 2. Rotate keys for validator 2 by authorizing a new validating key. + if (!doneAuthorizing) { + console.log('authorizing') + await authorizeValidationSigner(validatorWeb3, authorizedWeb3s[index]) + } + doneAuthorizing = doneAuthorizing || index === 1 + const signingKeys = await Promise.all( + newMembers.map((v: string) => getValidationSigner(v)) + ) + // Confirm that authorizing signing keys worked. + // @ts-ignore Type does not include `notSameMembers` + assert.notSameMembers(newMembers, signingKeys) + index = (index + 1) % 2 + } + } catch (e) { + console.error(e) + errorWhileChangingValidatorSet = e } } @@ -198,9 +288,10 @@ describe('governance tests', () => { ;(subscription as any).unsubscribe() // Wait for the current epoch to complete. await sleep(epoch) + assert.equal(errorWhileChangingValidatorSet, '') }) - const getValidatorSetAtBlock = async (blockNumber: number) => { + const getValidatorSetSigningKeysAtBlock = async (blockNumber: number) => { const validatorSetSize = await election.methods .numberValidatorsInCurrentSet() .call({}, blockNumber) @@ -213,6 +304,15 @@ describe('governance tests', () => { return validatorSet } + const getValidatorSetAccountKeysAtBlock = async (blockNumber: number) => { + const signingKeys = await getValidatorSetSigningKeysAtBlock(blockNumber) + return await Promise.all( + signingKeys.map((address: string) => + accounts.methods.validationSignerToAccount(address).call({}, blockNumber) + ) + ) + } + const getLastEpochBlock = (blockNumber: number) => { const epochNumber = Math.floor((blockNumber - 1) / epoch) return epochNumber * epoch @@ -229,20 +329,41 @@ describe('governance tests', () => { } }) - it('should always return a validator set equal to the group members at the end of the last epoch', async () => { + it('should always return a validator set equal to the signing keys of the group members at the end of the last epoch', async () => { for (const blockNumber of blockNumbers) { const lastEpochBlock = getLastEpochBlock(blockNumber) - const groupMembership = await getValidatorGroupMembers(lastEpochBlock) - const validatorSet = await getValidatorSetAtBlock(blockNumber) - assert.sameMembers(groupMembership, validatorSet) + const memberAccounts = await getValidatorGroupMembers(lastEpochBlock) + const memberSigningKeys = await Promise.all( + memberAccounts.map((v: string) => getValidationSigner(v, lastEpochBlock)) + ) + const validatorSetSigningKeys = await getValidatorSetSigningKeysAtBlock(blockNumber) + const validatorSetAccounts = await getValidatorSetAccountKeysAtBlock(blockNumber) + assert.sameMembers(memberSigningKeys, validatorSetSigningKeys) + assert.sameMembers(memberAccounts, validatorSetAccounts) } }) - it('should only have created blocks whose miner was in the current validator set', async () => { + // This appears to be failing for real, the first time we authorize an address it doesn't + // wind up proposing a block (announce issues?). + it('should block propose in a round robin manner', async () => { + let roundRobinOrder: string[] = [] for (const blockNumber of blockNumbers) { - const validatorSet = await getValidatorSetAtBlock(blockNumber) + const lastEpochBlock = getLastEpochBlock(blockNumber) + // Fetch the round robin order if it hasn't already been set for this epoch. + if (roundRobinOrder.length == 0 || blockNumber == lastEpochBlock + 1) { + const validatorSet = await getValidatorSetSigningKeysAtBlock(blockNumber) + roundRobinOrder = await Promise.all( + validatorSet.map( + async (_, i) => (await web3.eth.getBlock(lastEpochBlock + i + 1)).miner + ) + ) + console.log(roundRobinOrder, validatorSet) + assert.sameMembers(validatorSet, roundRobinOrder) + } + const indexInEpoch = blockNumber - lastEpochBlock - 1 + const expectedProposer = roundRobinOrder[indexInEpoch % roundRobinOrder.length] const block = await web3.eth.getBlock(blockNumber) - assert.include(validatorSet.map((x) => x.toLowerCase()), block.miner.toLowerCase()) + assert.equal(block.miner.toLowerCase(), expectedProposer.toLowerCase()) } }) @@ -266,16 +387,16 @@ describe('governance tests', () => { const assertScoreChanged = async (validator: string, blockNumber: number) => { const score = new BigNumber( - (await validators.methods.getValidator(validator).call({}, blockNumber))[3] + (await validators.methods.getValidator(validator).call({}, blockNumber))[2] ) const previousScore = new BigNumber( - (await validators.methods.getValidator(validator).call({}, blockNumber - 1))[3] + (await validators.methods.getValidator(validator).call({}, blockNumber - 1))[2] ) const expectedScore = adjustmentSpeed .times(uptime) .plus(new BigNumber(1).minus(adjustmentSpeed).times(fromFixed(previousScore))) - assert.isNotNaN(score) - assert.isNotNaN(previousScore) + assert.isFalse(score.isNaN()) + assert.isFalse(previousScore.isNaN()) assert.equal(score.toFixed(), toFixed(expectedScore).toFixed()) } @@ -283,10 +404,10 @@ describe('governance tests', () => { let expectUnchangedScores: string[] let expectChangedScores: string[] if (isLastBlockOfEpoch(blockNumber, epoch)) { - expectChangedScores = await getValidatorSetAtBlock(blockNumber) - expectUnchangedScores = allValidators.filter((x) => !expectChangedScores.includes(x)) + expectChangedScores = await getValidatorSetAccountKeysAtBlock(blockNumber) + expectUnchangedScores = validatorAccounts.filter((x) => !expectChangedScores.includes(x)) } else { - expectUnchangedScores = allValidators + expectUnchangedScores = validatorAccounts expectChangedScores = [] } @@ -302,8 +423,8 @@ describe('governance tests', () => { it('should distribute epoch payments at the end of each epoch', async () => { const commission = 0.1 - const maxValidatorEpochPayment = new BigNumber( - await epochRewards.methods.maxValidatorEpochPayment().call() + const targetValidatorEpochPayment = new BigNumber( + await epochRewards.methods.targetValidatorEpochPayment().call() ) const [group] = await validators.methods.getRegisteredValidatorGroups().call() @@ -337,17 +458,21 @@ describe('governance tests', () => { const rewardsMultiplier = new BigNumber( await epochRewards.methods.getRewardsMultiplier().call({}, blockNumber - 1) ) - return maxValidatorEpochPayment.times(fromFixed(score)).times(fromFixed(rewardsMultiplier)) + return targetValidatorEpochPayment + .times(fromFixed(score)) + .times(fromFixed(rewardsMultiplier)) } for (const blockNumber of blockNumbers) { let expectUnchangedBalances: string[] let expectChangedBalances: string[] if (isLastBlockOfEpoch(blockNumber, epoch)) { - expectChangedBalances = await getValidatorSetAtBlock(blockNumber) - expectUnchangedBalances = allValidators.filter((x) => !expectChangedBalances.includes(x)) + expectChangedBalances = await getValidatorSetAccountKeysAtBlock(blockNumber) + expectUnchangedBalances = validatorAccounts.filter( + (x) => !expectChangedBalances.includes(x) + ) } else { - expectUnchangedBalances = allValidators + expectUnchangedBalances = validatorAccounts expectChangedBalances = [] } @@ -504,10 +629,10 @@ describe('governance tests', () => { // Assert equal to 10 decimal places due to rounding errors. assert.equal( fromFixed(difference) - .dp(10) + .dp(9) .toFixed(), fromFixed(expected) - .dp(10) + .dp(9) .toFixed() ) } diff --git a/packages/celotool/src/e2e-tests/utils.ts b/packages/celotool/src/e2e-tests/utils.ts index 608fa7c9139..2d240542573 100644 --- a/packages/celotool/src/e2e-tests/utils.ts +++ b/packages/celotool/src/e2e-tests/utils.ts @@ -196,7 +196,7 @@ export async function init(gethBinaryPath: string, datadir: string, genesisPath: } export async function importPrivateKey(gethBinaryPath: string, instance: GethInstanceConfig) { - const keyFile = '/tmp/key.txt' + const keyFile = `/${getDatadir(instance)}/key.txt` fs.writeFileSync(keyFile, instance.privateKey) console.info(`geth:${instance.name}: import account`) await execCmdWithExitOnFailure( diff --git a/packages/contractkit/src/base.ts b/packages/contractkit/src/base.ts index 9ebd96abc9b..e16116338a4 100644 --- a/packages/contractkit/src/base.ts +++ b/packages/contractkit/src/base.ts @@ -6,7 +6,7 @@ export enum CeloContract { BlockchainParameters = 'BlockchainParameters', Election = 'Election', EpochRewards = 'EpochRewards', - Escrow = 'Escrow', + // Escrow = 'Escrow', Exchange = 'Exchange', GasCurrencyWhitelist = 'GasCurrencyWhitelist', GasPriceMinimum = 'GasPriceMinimum', diff --git a/packages/contractkit/src/web3-contract-cache.ts b/packages/contractkit/src/web3-contract-cache.ts index 2eab4113c6e..83f8a3f3262 100644 --- a/packages/contractkit/src/web3-contract-cache.ts +++ b/packages/contractkit/src/web3-contract-cache.ts @@ -5,7 +5,7 @@ import { newAttestations } from './generated/Attestations' import { newBlockchainParameters } from './generated/BlockchainParameters' import { newElection } from './generated/Election' import { newEpochRewards } from './generated/EpochRewards' -import { newEscrow } from './generated/Escrow' +// import { newEscrow } from './generated/Escrow' import { newExchange } from './generated/Exchange' import { newGasCurrencyWhitelist } from './generated/GasCurrencyWhitelist' import { newGasPriceMinimum } from './generated/GasPriceMinimum' @@ -28,7 +28,7 @@ const ContractFactories = { [CeloContract.BlockchainParameters]: newBlockchainParameters, [CeloContract.Election]: newElection, [CeloContract.EpochRewards]: newEpochRewards, - [CeloContract.Escrow]: newEscrow, + // [CeloContract.Escrow]: newEscrow, [CeloContract.Exchange]: newExchange, [CeloContract.GasCurrencyWhitelist]: newGasCurrencyWhitelist, [CeloContract.GasPriceMinimum]: newGasPriceMinimum, @@ -73,9 +73,11 @@ export class Web3ContractCache { getEpochRewards() { return this.getContract(CeloContract.EpochRewards) } + /* getEscrow() { return this.getContract(CeloContract.Escrow) } + */ getExchange() { return this.getContract(CeloContract.Exchange) } diff --git a/packages/contractkit/src/wrappers/Accounts.ts b/packages/contractkit/src/wrappers/Accounts.ts index a13aec9b0d1..174a7fa3194 100644 --- a/packages/contractkit/src/wrappers/Accounts.ts +++ b/packages/contractkit/src/wrappers/Accounts.ts @@ -1,4 +1,5 @@ import Web3 from 'web3' +import { parseSignature } from '@celo/utils/lib/signatureUtils' import { Address } from '../base' import { Accounts } from '../generated/types/Accounts' import { @@ -14,6 +15,13 @@ enum SignerRole { Validation, Vote, } + +export interface Signature { + r: string + s: string + v: number +} + /** * Contract for handling deposits needed for voting. */ @@ -57,40 +65,52 @@ export class AccountsWrapper extends BaseWrapper { /** * Authorize an attestation signing key on behalf of this account to another address. - * @param account Address of the active account. * @param attestationSigner The address of the signing key to authorize. + * @param proofOfSigningKeyPossession The account address signed by the signer address. * @return A CeloTransactionObject */ async authorizeAttestationSigner( - account: Address, - attestationSigner: Address + attestationSigner: Address, + proofOfSigningKeyPossession: Signature ): Promise> { - return this.authorizeSigner(SignerRole.Attestation, account, attestationSigner) + return this.authorizeSigner( + SignerRole.Attestation, + attestationSigner, + proofOfSigningKeyPossession + ) } /** * Authorizes an address to sign votes on behalf of the account. - * @param account Address of the active account. * @param voteSigner The address of the vote signing key to authorize. + * @param proofOfSigningKeyPossession The account address signed by the signer address. * @return A CeloTransactionObject */ async authorizeVoteSigner( - account: Address, - voteSigner: Address + voteSigner: Address, + proofOfSigningKeyPossession: Signature ): Promise> { - return this.authorizeSigner(SignerRole.Vote, account, voteSigner) + return this.authorizeSigner(SignerRole.Vote, voteSigner, proofOfSigningKeyPossession) } /** * Authorizes an address to sign consensus messages on behalf of the account. - * @param account Address of the active account. * @param validationSigner The address of the signing key to authorize. + * @param proofOfSigningKeyPossession The account address signed by the signer address. * @return A CeloTransactionObject */ async authorizeValidationSigner( - account: Address, - validationSigner: Address + validationSigner: Address, + proofOfSigningKeyPossession: Signature ): Promise> { - return this.authorizeSigner(SignerRole.Validation, account, validationSigner) + return this.authorizeSigner( + SignerRole.Validation, + validationSigner, + proofOfSigningKeyPossession + ) + } + + async generateProofOfSigningKeyPossession(account: Address, signer: Address) { + return this.getParsedSignatureOfAddress(account, signer) } /** @@ -158,19 +178,13 @@ export class AccountsWrapper extends BaseWrapper { [SignerRole.Vote]: this.contract.methods.authorizeVoteSigner, } - private async authorizeSigner(role: SignerRole, account: Address, signer: Address) { - const sig = await this.getParsedSignatureOfAddress(account, signer) - // TODO(asa): Pass default tx "from" argument. + private async authorizeSigner(role: SignerRole, signer: Address, sig: Signature) { return toTransactionObject(this.kit, this.authorizeFns[role](signer, sig.v, sig.r, sig.s)) } private async getParsedSignatureOfAddress(address: Address, signer: string) { const hash = Web3.utils.soliditySha3({ type: 'address', value: address }) - const signature = (await this.kit.web3.eth.sign(hash, signer)).slice(2) - return { - r: `0x${signature.slice(0, 64)}`, - s: `0x${signature.slice(64, 128)}`, - v: Web3.utils.hexToNumber(signature.slice(128, 130)) + 27, - } + const signature = await this.kit.web3.eth.sign(hash, signer) + return parseSignature(hash, signature, signer) } } diff --git a/packages/protocol/contracts/common/Accounts.sol b/packages/protocol/contracts/common/Accounts.sol index 2bff9c2d99b..472e9fe5d0b 100644 --- a/packages/protocol/contracts/common/Accounts.sol +++ b/packages/protocol/contracts/common/Accounts.sol @@ -8,8 +8,9 @@ import "./interfaces/IAccounts.sol"; import "../common/Initializable.sol"; import "../common/Signatures.sol"; import "../common/UsingRegistry.sol"; +import "../common/UsingPrecompiles.sol"; -contract Accounts is IAccounts, ReentrancyGuard, Initializable, UsingRegistry { +contract Accounts is IAccounts, ReentrancyGuard, Initializable, UsingRegistry, UsingPrecompiles { using SafeMath for uint256; @@ -113,7 +114,7 @@ contract Accounts is IAccounts, ReentrancyGuard, Initializable, UsingRegistry { nonReentrant { Account storage account = accounts[msg.sender]; - authorize(voter, account.signers.voting, v, r, s); + authorize(voter, v, r, s); account.signers.voting = voter; emit VoteSignerAuthorized(msg.sender, voter); } @@ -136,7 +137,7 @@ contract Accounts is IAccounts, ReentrancyGuard, Initializable, UsingRegistry { nonReentrant { Account storage account = accounts[msg.sender]; - authorize(validator, account.signers.validating, v, r, s); + authorize(validator, v, r, s); account.signers.validating = validator; emit ValidationSignerAuthorized(msg.sender, validator); } @@ -305,7 +306,7 @@ contract Accounts is IAccounts, ReentrancyGuard, Initializable, UsingRegistry { public { Account storage account = accounts[msg.sender]; - authorize(attestor, account.signers.attesting, v, r, s); + authorize(attestor, v, r, s); account.signers.attesting = attestor; emit AttestationSignerAuthorized(msg.sender, attestor); } @@ -435,9 +436,8 @@ contract Accounts is IAccounts, ReentrancyGuard, Initializable, UsingRegistry { } /** - * @notice Authorizes voting or validating power of `msg.sender`'s account to another address. - * @param current The address to authorize. - * @param previous The previous authorized address. + * @notice Authorizes some role of of `msg.sender`'s account to another address. + * @param authorized The address to authorize. * @param v The recovery id of the incoming ECDSA signature. * @param r Output value r of the ECDSA signature. * @param s Output value s of the ECDSA signature. @@ -445,20 +445,18 @@ contract Accounts is IAccounts, ReentrancyGuard, Initializable, UsingRegistry { * @dev v, r, s constitute `current`'s signature on `msg.sender`. */ function authorize( - address current, - address previous, + address authorized, uint8 v, bytes32 r, bytes32 s ) private { - require(isAccount(msg.sender) && isNotAccount(current) && isNotAuthorized(current)); + require(isAccount(msg.sender) && isNotAccount(authorized) && isNotAuthorized(authorized)); address signer = Signatures.getSignerOfAddress(msg.sender, v, r, s); - require(signer == current); + require(signer == authorized); - authorizedBy[previous] = address(0); - authorizedBy[current] = msg.sender; + authorizedBy[authorized] = msg.sender; } -} \ No newline at end of file +} diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index 61df6b13ac1..fb88dda69ee 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -349,9 +349,13 @@ contract Validators is * @return True upon success. */ function _updateValidatorScore(address validator, uint256 uptime) internal { + uint256 epochNumber = getEpochNumber(); address account = getAccounts().validationSignerToAccount(validator); + epochNumber = getEpochNumber(); require(isValidator(account)); + epochNumber = getEpochNumber(); require(uptime <= FixidityLib.fixed1().unwrap()); + epochNumber = getEpochNumber(); uint256 numerator; uint256 denominator; @@ -695,11 +699,11 @@ contract Validators is /** * @notice Returns validator information. - * @param account The account that registered the validator. + * @param validator The account that registered the validator or its authorized signing address. * @return The unpacked validator struct. */ function getValidator( - address account + address validator ) external view @@ -709,12 +713,13 @@ contract Validators is uint256 score ) { + address account = getAccounts().validationSignerToAccount(validator); require(isValidator(account)); - Validator storage validator = validators[account]; + Validator storage _validator = validators[account]; return ( - validator.publicKeysData, - validator.affiliation, - validator.score.unwrap() + _validator.publicKeysData, + _validator.affiliation, + _validator.score.unwrap() ); } diff --git a/packages/protocol/scripts/bash/ganache.sh b/packages/protocol/scripts/bash/ganache.sh index 51e4cc71370..a0032a6f4f9 100755 --- a/packages/protocol/scripts/bash/ganache.sh +++ b/packages/protocol/scripts/bash/ganache.sh @@ -5,7 +5,7 @@ set -euo pipefail yarn run ganache-cli \ --deterministic \ - --mnemonic 'concert load couple harbor equip island argue ramp clarify fence smart topic' \ + --mnemonic 'concert load couple harbor equip island argue ramp clarify fence smart blah' \ --gasPrice 0 \ --networkId 1101 \ --gasLimit 10000000 \ diff --git a/packages/protocol/test/common/accounts.ts b/packages/protocol/test/common/accounts.ts index edc93be38c8..ee1c6d06313 100644 --- a/packages/protocol/test/common/accounts.ts +++ b/packages/protocol/test/common/accounts.ts @@ -394,8 +394,8 @@ contract('Accounts', (accounts: string[]) => { ) }) - it('should reset the previous authorization', async () => { - assert.equal(await accountsInstance.authorizedBy(authorized), NULL_ADDRESS) + it('should preserve the previous authorization', async () => { + assert.equal(await accountsInstance.authorizedBy(authorized), account) }) }) }) From 106a27f85ad210f82a86dbb6d57a0fadede35c71 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Fri, 8 Nov 2019 14:51:01 -0800 Subject: [PATCH 107/149] Tests passing other than round robin proposing --- .../src/e2e-tests/governance_tests.ts | 32 +- packages/contractkit/src/wrappers/Accounts.ts | 38 +- .../protocol/contracts/common/Accounts.sol | 379 +++++++++--------- .../contracts/common/interfaces/IAccounts.sol | 6 +- .../contracts/governance/Election.sol | 2 +- .../contracts/governance/Validators.sol | 123 ++++-- .../governance/interfaces/IElection.sol | 2 +- .../governance/test/MockElection.sol | 2 +- .../governance/test/ValidatorsTest.sol | 10 +- .../contracts/identity/Attestations.sol | 2 +- packages/protocol/test/common/accounts.ts | 14 +- packages/protocol/test/governance/election.ts | 16 +- .../protocol/test/governance/validators.ts | 26 +- .../protocol/test/identity/attestations.ts | 2 +- 14 files changed, 343 insertions(+), 311 deletions(-) diff --git a/packages/celotool/src/e2e-tests/governance_tests.ts b/packages/celotool/src/e2e-tests/governance_tests.ts index 84d9af27937..b889ba0556d 100644 --- a/packages/celotool/src/e2e-tests/governance_tests.ts +++ b/packages/celotool/src/e2e-tests/governance_tests.ts @@ -33,7 +33,7 @@ describe('governance tests', () => { before(async function(this: any) { this.timeout(0) - // await context.hooks.before() + await context.hooks.before() }) after(context.hooks.after) @@ -73,11 +73,11 @@ describe('governance tests', () => { } } - const getValidationSigner = async (address: string, blockNumber?: number) => { + const getValidatorSigner = async (address: string, blockNumber?: number) => { if (blockNumber) { - return await accounts.methods.getValidationSigner(address).call({}, blockNumber) + return await accounts.methods.getValidatorSigner(address).call({}, blockNumber) } else { - return await accounts.methods.getValidationSigner(address).call() + return await accounts.methods.getValidatorSigner(address).call() } } @@ -127,7 +127,7 @@ describe('governance tests', () => { return tx.send({ from: group, ...txOptions, gas }) } - const authorizeValidationSigner = async ( + const authorizeValidatorSigner = async ( validatorWeb3: any, signerWeb3: any, txOptions: any = {} @@ -141,7 +141,7 @@ describe('governance tests', () => { ).contracts.getAccounts()).generateProofOfSigningKeyPossession(validator, signer) const validatorKit = newKitFromWeb3(validatorWeb3) const validatorAccounts = await validatorKit._web3Contracts.getAccounts() - const tx = validatorAccounts.methods.authorizeValidationSigner(signer, pop.v, pop.r, pop.s) + const tx = validatorAccounts.methods.authorizeValidatorSigner(signer, pop.v, pop.r, pop.s) let gas = txOptions.gas if (!gas) { gas = await tx.estimateGas({ ...txOptions }) @@ -264,11 +264,11 @@ describe('governance tests', () => { // 2. Rotate keys for validator 2 by authorizing a new validating key. if (!doneAuthorizing) { console.log('authorizing') - await authorizeValidationSigner(validatorWeb3, authorizedWeb3s[index]) + await authorizeValidatorSigner(validatorWeb3, authorizedWeb3s[index]) } doneAuthorizing = doneAuthorizing || index === 1 const signingKeys = await Promise.all( - newMembers.map((v: string) => getValidationSigner(v)) + newMembers.map((v: string) => getValidatorSigner(v)) ) // Confirm that authorizing signing keys worked. // @ts-ignore Type does not include `notSameMembers` @@ -291,7 +291,7 @@ describe('governance tests', () => { assert.equal(errorWhileChangingValidatorSet, '') }) - const getValidatorSetSigningKeysAtBlock = async (blockNumber: number) => { + const getValidatorSetSignersAtBlock = async (blockNumber: number) => { const validatorSetSize = await election.methods .numberValidatorsInCurrentSet() .call({}, blockNumber) @@ -305,10 +305,10 @@ describe('governance tests', () => { } const getValidatorSetAccountKeysAtBlock = async (blockNumber: number) => { - const signingKeys = await getValidatorSetSigningKeysAtBlock(blockNumber) + const signingKeys = await getValidatorSetSignersAtBlock(blockNumber) return await Promise.all( signingKeys.map((address: string) => - accounts.methods.validationSignerToAccount(address).call({}, blockNumber) + accounts.methods.validatorSignerToAccount(address).call({}, blockNumber) ) ) } @@ -333,12 +333,12 @@ describe('governance tests', () => { for (const blockNumber of blockNumbers) { const lastEpochBlock = getLastEpochBlock(blockNumber) const memberAccounts = await getValidatorGroupMembers(lastEpochBlock) - const memberSigningKeys = await Promise.all( - memberAccounts.map((v: string) => getValidationSigner(v, lastEpochBlock)) + const memberSigners = await Promise.all( + memberAccounts.map((v: string) => getValidatorSigner(v, lastEpochBlock)) ) - const validatorSetSigningKeys = await getValidatorSetSigningKeysAtBlock(blockNumber) + const validatorSetSigners = await getValidatorSetSignersAtBlock(blockNumber) const validatorSetAccounts = await getValidatorSetAccountKeysAtBlock(blockNumber) - assert.sameMembers(memberSigningKeys, validatorSetSigningKeys) + assert.sameMembers(memberSigners, validatorSetSigners) assert.sameMembers(memberAccounts, validatorSetAccounts) } }) @@ -351,7 +351,7 @@ describe('governance tests', () => { const lastEpochBlock = getLastEpochBlock(blockNumber) // Fetch the round robin order if it hasn't already been set for this epoch. if (roundRobinOrder.length == 0 || blockNumber == lastEpochBlock + 1) { - const validatorSet = await getValidatorSetSigningKeysAtBlock(blockNumber) + const validatorSet = await getValidatorSetSignersAtBlock(blockNumber) roundRobinOrder = await Promise.all( validatorSet.map( async (_, i) => (await web3.eth.getBlock(lastEpochBlock + i + 1)).miner diff --git a/packages/contractkit/src/wrappers/Accounts.ts b/packages/contractkit/src/wrappers/Accounts.ts index 174a7fa3194..c78c846d2e9 100644 --- a/packages/contractkit/src/wrappers/Accounts.ts +++ b/packages/contractkit/src/wrappers/Accounts.ts @@ -12,7 +12,7 @@ import { enum SignerRole { Attestation, - Validation, + Validator, Vote, } @@ -48,12 +48,12 @@ export class AccountsWrapper extends BaseWrapper { this.contract.methods.getVoteSigner ) /** - * Returns the validation signere for the specified account. + * Returns the validator signere for the specified account. * @param account The address of the account. * @return The address with which the account can register a validator or group. */ - getValidationSigner: (account: string) => Promise
= proxyCall( - this.contract.methods.getValidationSigner + getValidatorSigner: (account: string) => Promise
= proxyCall( + this.contract.methods.getValidatorSigner ) /** @@ -65,48 +65,40 @@ export class AccountsWrapper extends BaseWrapper { /** * Authorize an attestation signing key on behalf of this account to another address. - * @param attestationSigner The address of the signing key to authorize. + * @param signer The address of the signing key to authorize. * @param proofOfSigningKeyPossession The account address signed by the signer address. * @return A CeloTransactionObject */ async authorizeAttestationSigner( - attestationSigner: Address, + signer: Address, proofOfSigningKeyPossession: Signature ): Promise> { - return this.authorizeSigner( - SignerRole.Attestation, - attestationSigner, - proofOfSigningKeyPossession - ) + return this.authorizeSigner(SignerRole.Attestation, signer, proofOfSigningKeyPossession) } /** * Authorizes an address to sign votes on behalf of the account. - * @param voteSigner The address of the vote signing key to authorize. + * @param signer The address of the vote signing key to authorize. * @param proofOfSigningKeyPossession The account address signed by the signer address. * @return A CeloTransactionObject */ async authorizeVoteSigner( - voteSigner: Address, + signer: Address, proofOfSigningKeyPossession: Signature ): Promise> { - return this.authorizeSigner(SignerRole.Vote, voteSigner, proofOfSigningKeyPossession) + return this.authorizeSigner(SignerRole.Vote, signer, proofOfSigningKeyPossession) } /** * Authorizes an address to sign consensus messages on behalf of the account. - * @param validationSigner The address of the signing key to authorize. + * @param signer The address of the signing key to authorize. * @param proofOfSigningKeyPossession The account address signed by the signer address. * @return A CeloTransactionObject */ - async authorizeValidationSigner( - validationSigner: Address, + async authorizeValidatorSigner( + signer: Address, proofOfSigningKeyPossession: Signature ): Promise> { - return this.authorizeSigner( - SignerRole.Validation, - validationSigner, - proofOfSigningKeyPossession - ) + return this.authorizeSigner(SignerRole.Validator, signer, proofOfSigningKeyPossession) } async generateProofOfSigningKeyPossession(account: Address, signer: Address) { @@ -174,7 +166,7 @@ export class AccountsWrapper extends BaseWrapper { private authorizeFns = { [SignerRole.Attestation]: this.contract.methods.authorizeAttestationSigner, - [SignerRole.Validation]: this.contract.methods.authorizeValidationSigner, + [SignerRole.Validator]: this.contract.methods.authorizeValidatorSigner, [SignerRole.Vote]: this.contract.methods.authorizeVoteSigner, } diff --git a/packages/protocol/contracts/common/Accounts.sol b/packages/protocol/contracts/common/Accounts.sol index 472e9fe5d0b..71e78790e31 100644 --- a/packages/protocol/contracts/common/Accounts.sol +++ b/packages/protocol/contracts/common/Accounts.sol @@ -17,17 +17,17 @@ contract Accounts is IAccounts, ReentrancyGuard, Initializable, UsingRegistry, U struct Signers { //The address that is authorized to vote in governance and validator elections on behalf of the // account. The account can vote as well, whether or not an vote signing key has been specified. - address voting; + address vote; // The address that is authorized to manage a validator or validator group and sign consensus // messages on behalf of the account. The account can manage the validator, whether or not an - // validation signing key has been specified. However if an validation signing key has been + // validator signing key has been specified. However if an validator signing key has been // specified, only that key may actually participate in consensus. - address validating; + address validator; // The address of the key with which this account wants to sign attestations on the Attestations // contract - address attesting; + address attestation; } struct Account { @@ -52,13 +52,13 @@ contract Accounts is IAccounts, ReentrancyGuard, Initializable, UsingRegistry, U } mapping(address => Account) private accounts; - // Maps voting and validating keys to the account that provided the authorization. + // Maps authorized signers to the account that provided the authorization. mapping(address => address) public authorizedBy; event AttestationSignerAuthorized(address indexed account, address signer); event VoteSignerAuthorized(address indexed account, address signer); - event ValidationSignerAuthorized(address indexed account, address signer); + event ValidatorSignerAuthorized(address indexed account, address signer); event AccountDataEncryptionKeySet(address indexed account, bytes dataEncryptionKey); event AccountNameSet(address indexed account, string name); event AccountMetadataURLSet(address indexed account, string metadataURL); @@ -86,6 +86,48 @@ contract Accounts is IAccounts, ReentrancyGuard, Initializable, UsingRegistry, U setWalletAddress(walletAddress); } + /** + * @notice Creates an account. + * @return True if account creation succeeded. + */ + function createAccount() public returns (bool) { + require(isNotAccount(msg.sender) && isNotAuthorizedSigner(msg.sender)); + Account storage account = accounts[msg.sender]; + account.exists = true; + emit AccountCreated(msg.sender); + return true; + } + + /** + * @notice Setter for the name of an account. + * @param name The name to set. + */ + function setName(string memory name) public { + require(isAccount(msg.sender)); + accounts[msg.sender].name = name; + emit AccountNameSet(msg.sender, name); + } + + /** + * @notice Setter for the wallet address for an account + * @param walletAddress The wallet address to set for the account + */ + function setWalletAddress(address walletAddress) public { + require(isAccount(msg.sender)); + accounts[msg.sender].walletAddress = walletAddress; + emit AccountWalletAddressSet(msg.sender, walletAddress); + } + + /** + * @notice Setter for the data encryption key and version. + * @param dataEncryptionKey secp256k1 public key for data encryption. Preferably compressed. + */ + function setAccountDataEncryptionKey(bytes memory dataEncryptionKey) public { + require(dataEncryptionKey.length >= 33, "data encryption key length <= 32"); + accounts[msg.sender].dataEncryptionKey = dataEncryptionKey; + emit AccountDataEncryptionKeySet(msg.sender, dataEncryptionKey); + } + /** * @notice Setter for the metadata of an account. * @param metadataURL The URL to access the metadata. @@ -98,14 +140,14 @@ contract Accounts is IAccounts, ReentrancyGuard, Initializable, UsingRegistry, U /** * @notice Authorizes an address to sign votes on behalf of the account. - * @param voter The address of the vote signing key to authorize. + * @param signer The address of the signing key to authorize. * @param v The recovery id of the incoming ECDSA signature. * @param r Output value r of the ECDSA signature. * @param s Output value s of the ECDSA signature. - * @dev v, r, s constitute `voter`'s signature on `msg.sender`. + * @dev v, r, s constitute `signer`'s signature on `msg.sender`. */ function authorizeVoteSigner( - address voter, + address signer, uint8 v, bytes32 r, bytes32 s @@ -114,21 +156,21 @@ contract Accounts is IAccounts, ReentrancyGuard, Initializable, UsingRegistry, U nonReentrant { Account storage account = accounts[msg.sender]; - authorize(voter, v, r, s); - account.signers.voting = voter; - emit VoteSignerAuthorized(msg.sender, voter); + authorize(signer, v, r, s); + account.signers.vote = signer; + emit VoteSignerAuthorized(msg.sender, signer); } /** * @notice Authorizes an address to sign consensus messages on behalf of the account. - * @param validator The address of the signing key to authorize. + * @param signer The address of the signing key to authorize. * @param v The recovery id of the incoming ECDSA signature. * @param r Output value r of the ECDSA signature. * @param s Output value s of the ECDSA signature. - * @dev v, r, s constitute `validator`'s signature on `msg.sender`. + * @dev v, r, s constitute `signer`'s signature on `msg.sender`. */ - function authorizeValidationSigner( - address validator, + function authorizeValidatorSigner( + address signer, uint8 v, bytes32 r, bytes32 s @@ -137,241 +179,149 @@ contract Accounts is IAccounts, ReentrancyGuard, Initializable, UsingRegistry, U nonReentrant { Account storage account = accounts[msg.sender]; - authorize(validator, v, r, s); - account.signers.validating = validator; - emit ValidationSignerAuthorized(msg.sender, validator); + authorize(signer, v, r, s); + account.signers.validator = signer; + emit ValidatorSignerAuthorized(msg.sender, signer); } /** - * @notice Check if an address has been authorized by an account for voting or validating. - * @param account The possibly authorized address. - * @return Returns `true` if authorized. Returns `false` otherwise. + * @notice Authorizes an address to sign attestations on behalf of the account. + * @param signer The address of the signing key to authorize. + * @param v The recovery id of the incoming ECDSA signature. + * @param r Output value r of the ECDSA signature. + * @param s Output value s of the ECDSA signature. + * @dev v, r, s constitute `signer`'s signature on `msg.sender`. */ - function isAuthorized(address account) external view returns (bool) { - return (authorizedBy[account] != address(0)); + function authorizeAttestationSigner( + address signer, + uint8 v, + bytes32 r, + bytes32 s + ) + public + { + Account storage account = accounts[msg.sender]; + authorize(signer, v, r, s); + account.signers.attestation = signer; + emit AttestationSignerAuthorized(msg.sender, signer); } /** - * @notice Returns the account associated with `accountOrAttestationSigner`. - * @param accountOrAttestationSigner The address of the account or active authorized attestation - signer. - * @dev Fails if the `accountOrAttestationSigner` is not an account or active authorized - attestation signer. + * @notice Returns the account associated with `signer`. + * @param signer The address of the account or currently authorized attestation signer. + * @dev Fails if the `signer` is not an account or currently authorized attestation signer. * @return The associated account. */ - function activeAttesttationSignerToAccount(address accountOrAttestationSigner) + function activeAttesttationSignerToAccount(address signer) external view returns (address) { - address authorizingAccount = authorizedBy[accountOrAttestationSigner]; + address authorizingAccount = authorizedBy[signer]; if (authorizingAccount != address(0)) { - require(accounts[authorizingAccount].signers.attesting == accountOrAttestationSigner); + require(accounts[authorizingAccount].signers.attestation == signer); return authorizingAccount; } else { - require(isAccount(accountOrAttestationSigner)); - return accountOrAttestationSigner; + require(isAccount(signer)); + return signer; } } /** - * @notice Returns the account associated with `accountOrVoteSigner`. - * @param accountOrVoteSigner The address of the account or active authorized vote signer. - * @dev Fails if the `accountOrVoteSigner` is not an account or active authorized vote signer. + * @notice Returns the account associated with `signer`. + * @param signer The address of an account or currently authorized validator signer. + * @dev Fails if the `signer` is not an account or active authorized validator. * @return The associated account. */ - function activeVoteSignerToAccount(address accountOrVoteSigner) - external + function activeValidatorSignerToAccount(address signer) + public view returns (address) { - address authorizingAccount = authorizedBy[accountOrVoteSigner]; + address authorizingAccount = authorizedBy[signer]; if (authorizingAccount != address(0)) { - require(accounts[authorizingAccount].signers.voting == accountOrVoteSigner); + require(accounts[authorizingAccount].signers.validator == signer); return authorizingAccount; } else { - require(isAccount(accountOrVoteSigner)); - return accountOrVoteSigner; + require(isAccount(signer)); + return signer; } } /** - * @notice Returns the account associated with `accountOrVoteSigner`. - * @param accountOrVoteSigner The address of the account or previously authorized vote signer. - * @dev Fails if the `accountOrVoteSigner` is not an account or previously authorized vote signer. + * @notice Returns the account associated with `signer`. + * @param signer The address of the account or currently authorized vote signer. + * @dev Fails if the `signer` is not an account or currently authorized vote signer. * @return The associated account. */ - function voteSignerToAccount(address accountOrVoteSigner) external view returns (address) { - address authorizingAccount = authorizedBy[accountOrVoteSigner]; + function activeVoteSignerToAccount(address signer) + external + view + returns (address) + { + address authorizingAccount = authorizedBy[signer]; if (authorizingAccount != address(0)) { + require(accounts[authorizingAccount].signers.vote == signer); return authorizingAccount; } else { - require(isAccount(accountOrVoteSigner)); - return accountOrVoteSigner; + require(isAccount(signer)); + return signer; } } /** - * @notice Getter for the name of an account. - * @param account The address of the account to get the name for. - * @return name The name of the account. - */ - function getName(address account) external view returns (string memory) { - return accounts[account].name; - } - - /** - * @notice Getter for the metadata of an account. - * @param account The address of the account to get the metadata for. - * @return metadataURL The URL to access the metadata. - */ - function getMetadataURL(address account) external view returns (string memory) { - return accounts[account].metadataURL; - } - - /** - * @notice Getter for the data encryption key and version. - * @param account The address of the account to get the key for - * @return dataEncryptionKey secp256k1 public key for data encryption. Preferably compressed. - */ - function getDataEncryptionKey(address account) external view returns (bytes memory) { - return accounts[account].dataEncryptionKey; - } - - /** - * @notice Getter for the wallet address for an account - * @param account The address of the account to get the wallet address for - * @return Wallet address - */ - function getWalletAddress(address account) external view returns (address) { - return accounts[account].walletAddress; - } - - /** - * @notice Creates an account. - * @return True if account creation succeeded. - */ - function createAccount() public returns (bool) { - require(isNotAccount(msg.sender) && isNotAuthorized(msg.sender)); - Account storage account = accounts[msg.sender]; - account.exists = true; - emit AccountCreated(msg.sender); - return true; - } - - /** - * @notice Setter for the name of an account. - * @param name The name to set. - */ - function setName(string memory name) public { - require(isAccount(msg.sender)); - accounts[msg.sender].name = name; - emit AccountNameSet(msg.sender, name); - } - - /** - * @notice Setter for the wallet address for an account - * @param walletAddress The wallet address to set for the account - */ - function setWalletAddress(address walletAddress) public { - require(isAccount(msg.sender)); - accounts[msg.sender].walletAddress = walletAddress; - emit AccountWalletAddressSet(msg.sender, walletAddress); - } - - /** - * @notice Setter for the data encryption key and version. - * @param dataEncryptionKey secp256k1 public key for data encryption. Preferably compressed. - */ - function setAccountDataEncryptionKey(bytes memory dataEncryptionKey) public { - require(dataEncryptionKey.length >= 33, "data encryption key length <= 32"); - accounts[msg.sender].dataEncryptionKey = dataEncryptionKey; - emit AccountDataEncryptionKeySet(msg.sender, dataEncryptionKey); - } - - /** - * @notice Authorizes an address to sign attestations on behalf of the account. - * @param attestor The address of the signing key to authorize. - * @param v The recovery id of the incoming ECDSA signature. - * @param r Output value r of the ECDSA signature. - * @param s Output value s of the ECDSA signature. - * @dev v, r, s constitute `attestor`'s signature on `msg.sender`. - */ - function authorizeAttestationSigner( - address attestor, - uint8 v, - bytes32 r, - bytes32 s - ) - public - { - Account storage account = accounts[msg.sender]; - authorize(attestor, v, r, s); - account.signers.attesting = attestor; - emit AttestationSignerAuthorized(msg.sender, attestor); - } - - /** - * @notice Returns the account associated with `accountOrAttestationSigner`. - * @param accountOrAttestationSigner The address of the account or previously authorized - * attestation signing key. - * @dev Fails if the `accountOrAttestationSigner` is not an account or previously authorized - * attestation signing key. + * @notice Returns the account associated with `signer`. + * @param signer The address of the account or previously authorized vote signer. + * @dev Fails if the `signer` is not an account or previously authorized vote signer. * @return The associated account. */ - function attestationSignerToAccount(address accountOrAttestationSigner) - public - view - returns (address) - { - address authorizingAccount = authorizedBy[accountOrAttestationSigner]; + function voteSignerToAccount(address signer) external view returns (address) { + address authorizingAccount = authorizedBy[signer]; if (authorizingAccount != address(0)) { return authorizingAccount; } else { - require(isAccount(accountOrAttestationSigner)); - return accountOrAttestationSigner; + require(isAccount(signer)); + return signer; } } /** - * @notice Returns the account associated with `accountOrValidationSigner`. - * @param accountOrValidationSigner The address of the account or active authorized validator. - * @dev Fails if the `accountOrValidationSigner` is not an account or active authorized validator. + * @notice Returns the account associated with `signer`. + * @param signer The address of an account or previously authorized attestation signer. + * @dev Fails if the `signer` is not an account or previously authorized attestation signer. * @return The associated account. */ - function activeValidationSignerToAccount(address accountOrValidationSigner) + function attestationSignerToAccount(address signer) public view returns (address) { - address authorizingAccount = authorizedBy[accountOrValidationSigner]; + address authorizingAccount = authorizedBy[signer]; if (authorizingAccount != address(0)) { - require(accounts[authorizingAccount].signers.validating == accountOrValidationSigner); return authorizingAccount; } else { - require(isAccount(accountOrValidationSigner)); - return accountOrValidationSigner; + require(isAccount(signer)); + return signer; } } /** - * @notice Returns the account associated with `accountOrValidationSigner`. - * @param accountOrValidationSigner The address of the account or previously authorized validator. - * @dev Fails if the `accountOrValidationSigner` is not an account or previously authorized - validator. + * @notice Returns the account associated with `signer`. + * @param signer The address of an account or previously authorized validator signer. + * @dev Fails if `signer` is not an account or previously authorized validator signer. * @return The associated account. */ - function validationSignerToAccount(address accountOrValidationSigner) + function validatorSignerToAccount(address signer) public view returns (address) { - address authorizingAccount = authorizedBy[accountOrValidationSigner]; + address authorizingAccount = authorizedBy[signer]; if (authorizingAccount != address(0)) { return authorizingAccount; } else { - require(isAccount(accountOrValidationSigner)); - return accountOrValidationSigner; + require(isAccount(signer)); + return signer; } } @@ -382,19 +332,19 @@ contract Accounts is IAccounts, ReentrancyGuard, Initializable, UsingRegistry, U */ function getVoteSigner(address account) public view returns (address) { require(isAccount(account)); - address voter = accounts[account].signers.voting; - return voter == address(0) ? account : voter; + address signer = accounts[account].signers.vote; + return signer == address(0) ? account : signer; } /** - * @notice Returns the validation signer for the specified account. + * @notice Returns the validator signer for the specified account. * @param account The address of the account. * @return The address with which the account can register a validator or group. */ - function getValidationSigner(address account) public view returns (address) { + function getValidatorSigner(address account) public view returns (address) { require(isAccount(account)); - address validator = accounts[account].signers.validating; - return validator == address(0) ? account : validator; + address signer = accounts[account].signers.validator; + return signer == address(0) ? account : signer; } /** @@ -404,8 +354,44 @@ contract Accounts is IAccounts, ReentrancyGuard, Initializable, UsingRegistry, U */ function getAttestationSigner(address account) public view returns (address) { require(isAccount(account)); - address attestor = accounts[account].signers.attesting; - return attestor == address(0) ? account : attestor; + address signer = accounts[account].signers.attestation; + return signer == address(0) ? account : signer; + } + + /** + * @notice Getter for the name of an account. + * @param account The address of the account to get the name for. + * @return name The name of the account. + */ + function getName(address account) external view returns (string memory) { + return accounts[account].name; + } + + /** + * @notice Getter for the metadata of an account. + * @param account The address of the account to get the metadata for. + * @return metadataURL The URL to access the metadata. + */ + function getMetadataURL(address account) external view returns (string memory) { + return accounts[account].metadataURL; + } + + /** + * @notice Getter for the data encryption key and version. + * @param account The address of the account to get the key for + * @return dataEncryptionKey secp256k1 public key for data encryption. Preferably compressed. + */ + function getDataEncryptionKey(address account) external view returns (bytes memory) { + return accounts[account].dataEncryptionKey; + } + + /** + * @notice Getter for the wallet address for an account + * @param account The address of the account to get the wallet address for + * @return Wallet address + */ + function getWalletAddress(address account) external view returns (address) { + return accounts[account].walletAddress; } /** @@ -427,12 +413,21 @@ contract Accounts is IAccounts, ReentrancyGuard, Initializable, UsingRegistry, U } /** - * @notice Check if an address has been authorized by an account for voting or validating. - * @param account The possibly authorized address. + * @notice Check if an address has been an authorized signer for an account. + * @param signer The possibly authorized address. + * @return Returns `true` if authorized. Returns `false` otherwise. + */ + function isAuthorizedSigner(address signer) external view returns (bool) { + return (authorizedBy[signer] != address(0)); + } + + /** + * @notice Check if an address has been an authorized signer for an account. + * @param signer The possibly authorized address. * @return Returns `false` if authorized. Returns `true` otherwise. */ - function isNotAuthorized(address account) internal view returns (bool) { - return (authorizedBy[account] == address(0)); + function isNotAuthorizedSigner(address signer) internal view returns (bool) { + return (authorizedBy[signer] == address(0)); } /** @@ -452,7 +447,7 @@ contract Accounts is IAccounts, ReentrancyGuard, Initializable, UsingRegistry, U ) private { - require(isAccount(msg.sender) && isNotAccount(authorized) && isNotAuthorized(authorized)); + require(isAccount(msg.sender) && isNotAccount(authorized) && isNotAuthorizedSigner(authorized)); address signer = Signatures.getSignerOfAddress(msg.sender, v, r, s); require(signer == authorized); diff --git a/packages/protocol/contracts/common/interfaces/IAccounts.sol b/packages/protocol/contracts/common/interfaces/IAccounts.sol index da5f9942c57..3a1310c5255 100644 --- a/packages/protocol/contracts/common/interfaces/IAccounts.sol +++ b/packages/protocol/contracts/common/interfaces/IAccounts.sol @@ -5,9 +5,9 @@ interface IAccounts { function isAccount(address) external view returns (bool); function activeVoteSignerToAccount(address) external view returns (address); function voteSignerToAccount(address) external view returns (address); - function activeValidationSignerToAccount(address) external view returns (address); - function validationSignerToAccount(address) external view returns (address); - function getValidationSigner(address) external view returns (address); + function activeValidatorSignerToAccount(address) external view returns (address); + function validatorSignerToAccount(address) external view returns (address); + function getValidatorSigner(address) external view returns (address); function activeAttesttationSignerToAccount(address) external view returns (address); function attestationSignerToAccount(address) external view returns (address); function getAttestationSigner(address) external view returns (address); diff --git a/packages/protocol/contracts/governance/Election.sol b/packages/protocol/contracts/governance/Election.sol index 16d9e6a354a..f219fc7af60 100644 --- a/packages/protocol/contracts/governance/Election.sol +++ b/packages/protocol/contracts/governance/Election.sol @@ -789,7 +789,7 @@ contract Election is * @return The list of elected validators. * @dev See https://en.wikipedia.org/wiki/D%27Hondt_method#Allocation for more information. */ - function electValidators() external view returns (address[] memory) { + function electValidatorSigners() external view returns (address[] memory) { // Groups must have at least `electabilityThreshold` proportion of the total votes to be // considered for the election. uint256 requiredVotes = electabilityThreshold.multiply( diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index fb88dda69ee..14b2fcdc868 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -288,7 +288,7 @@ contract Validators is // Use the proof of possession bytes require(checkProofOfPossession(msg.sender, publicKeysData.slice(64, 48 + 96))); - address account = getAccounts().activeValidationSignerToAccount(msg.sender); + address account = getAccounts().activeValidatorSignerToAccount(msg.sender); require(!isValidator(account) && !isValidatorGroup(account)); uint256 lockedGoldBalance = getLockedGold().getAccountTotalLockedGold(account); require(lockedGoldBalance >= validatorLockedGoldRequirements.value); @@ -333,29 +333,25 @@ contract Validators is /** * @notice Updates a validator's score based on its uptime for the epoch. - * @param validator The address of the validator. + * @param signer The validator signer of the validator account whose score needs updating. * @param uptime The Fixidity representation of the validator's uptime, between 0 and 1. * @return True upon success. */ - function updateValidatorScore(address validator, uint256 uptime) external onlyVm() { - _updateValidatorScore(validator, uptime); + function updateValidatorScoreFromSigner(address signer, uint256 uptime) external onlyVm() { + _updateValidatorScoreFromSigner(signer, uptime); } /** * @notice Updates a validator's score based on its uptime for the epoch. - * @param validator The address of the validator. + * @param signer The validator signer of the validator whose score needs updating. * @param uptime The Fixidity representation of the validator's uptime, between 0 and 1. * @dev new_score = uptime ** exponent * adjustmentSpeed + old_score * (1 - adjustmentSpeed) * @return True upon success. */ - function _updateValidatorScore(address validator, uint256 uptime) internal { - uint256 epochNumber = getEpochNumber(); - address account = getAccounts().validationSignerToAccount(validator); - epochNumber = getEpochNumber(); + function _updateValidatorScoreFromSigner(address signer, uint256 uptime) internal { + address account = getAccounts().validatorSignerToAccount(signer); require(isValidator(account)); - epochNumber = getEpochNumber(); require(uptime <= FixidityLib.fixed1().unwrap()); - epochNumber = getEpochNumber(); uint256 numerator; uint256 denominator; @@ -388,38 +384,38 @@ contract Validators is } /** - * @notice Distributes epoch payments to `validator` and its group. - * @param validator The validator to distribute the epoch payment to. + * @notice Distributes epoch payments to the account associated with `signer` and its group. + * @param signer The validator signer of the account to distribute the epoch payment to. * @param maxPayment The maximum payment to the validator. Actual payment is based on score and * group commission. * @return The total payment paid to the validator and their group. */ - function distributeEpochPayment( - address validator, + function distributeEpochPaymentsFromSigner( + address signer, uint256 maxPayment ) external onlyVm() returns (uint256) { - return _distributeEpochPayment(validator, maxPayment); + return _distributeEpochPaymentsFromSigner(signer, maxPayment); } /** - * @notice Distributes epoch payments to `validator` and its group. - * @param validator The validator to distribute the epoch payment to. + * @notice Distributes epoch payments to the account associated with `signer` and its group. + * @param signer The validator signer of the validator to distribute the epoch payment to. * @param maxPayment The maximum payment to the validator. Actual payment is based on score and * group commission. * @return The total payment paid to the validator and their group. */ - function _distributeEpochPayment( - address validator, + function _distributeEpochPaymentsFromSigner( + address signer, uint256 maxPayment ) internal returns (uint256) { - address account = getAccounts().validationSignerToAccount(validator); + address account = getAccounts().validatorSignerToAccount(signer); require(isValidator(account)); // The group that should be paid is the group that the validator was a member of at the // time it was elected. @@ -447,7 +443,7 @@ contract Validators is * @dev Fails if the account is not a validator. */ function deregisterValidator(uint256 index) external nonReentrant returns (bool) { - address account = getAccounts().activeValidationSignerToAccount(msg.sender); + address account = getAccounts().activeValidatorSignerToAccount(msg.sender); require(isValidator(account)); // Require that the validator has not been a member of a validator group for @@ -475,7 +471,7 @@ contract Validators is * @dev De-affiliates with the previously affiliated group if present. */ function affiliate(address group) external nonReentrant returns (bool) { - address account = getAccounts().activeValidationSignerToAccount(msg.sender); + address account = getAccounts().activeValidatorSignerToAccount(msg.sender); require(isValidator(account) && isValidatorGroup(group)); require(meetsAccountLockedGoldRequirements(account)); require(meetsAccountLockedGoldRequirements(group)); @@ -494,7 +490,7 @@ contract Validators is * @dev Fails if the account is not a validator with non-zero affiliation. */ function deaffiliate() external nonReentrant returns (bool) { - address account = getAccounts().activeValidationSignerToAccount(msg.sender); + address account = getAccounts().activeValidatorSignerToAccount(msg.sender); require(isValidator(account)); Validator storage validator = validators[account]; require(validator.affiliation != address(0)); @@ -518,7 +514,7 @@ contract Validators is returns (bool) { require(commission <= FixidityLib.fixed1().unwrap(), "Commission can't be greater than 100%"); - address account = getAccounts().activeValidationSignerToAccount(msg.sender); + address account = getAccounts().activeValidatorSignerToAccount(msg.sender); require(!isValidator(account) && !isValidatorGroup(account)); uint256 lockedGoldBalance = getLockedGold().getAccountTotalLockedGold(account); require(lockedGoldBalance >= groupLockedGoldRequirements.value); @@ -537,7 +533,7 @@ contract Validators is * @dev Fails if the account is not a validator group with no members. */ function deregisterValidatorGroup(uint256 index) external nonReentrant returns (bool) { - address account = getAccounts().activeValidationSignerToAccount(msg.sender); + address account = getAccounts().activeValidatorSignerToAccount(msg.sender); // Only Validator Groups that have never had members or have been empty for at least // `groupLockedGoldRequirements.duration` seconds can be deregistered. require(isValidatorGroup(account) && groups[account].members.numElements == 0); @@ -559,7 +555,7 @@ contract Validators is * @dev Fails if the group has zero members. */ function addMember(address validator) external nonReentrant returns (bool) { - address account = getAccounts().activeValidationSignerToAccount(msg.sender); + address account = getAccounts().activeValidatorSignerToAccount(msg.sender); require(groups[account].members.numElements > 0); return _addMember(account, validator, address(0), address(0)); } @@ -582,7 +578,7 @@ contract Validators is nonReentrant returns (bool) { - address account = getAccounts().activeValidationSignerToAccount(msg.sender); + address account = getAccounts().activeValidatorSignerToAccount(msg.sender); require(groups[account].members.numElements == 0); return _addMember(account, validator, lesser, greater); } @@ -630,7 +626,7 @@ contract Validators is * @dev Fails if `validator` is not a member of the account's group. */ function removeMember(address validator) external nonReentrant returns (bool) { - address account = getAccounts().activeValidationSignerToAccount(msg.sender); + address account = getAccounts().activeValidatorSignerToAccount(msg.sender); require(isValidatorGroup(account) && isValidator(validator), "is not group and validator"); return _removeMember(account, validator); } @@ -654,7 +650,7 @@ contract Validators is nonReentrant returns (bool) { - address account = getAccounts().activeValidationSignerToAccount(msg.sender); + address account = getAccounts().activeValidatorSignerToAccount(msg.sender); require(isValidatorGroup(account) && isValidator(validator)); ValidatorGroup storage group = groups[account]; require(group.members.contains(validator)); @@ -699,11 +695,11 @@ contract Validators is /** * @notice Returns validator information. - * @param validator The account that registered the validator or its authorized signing address. + * @param signer The account that registered the validator or its authorized signing address. * @return The unpacked validator struct. */ - function getValidator( - address validator + function getValidatorFromSigner( + address signer ) external view @@ -713,13 +709,32 @@ contract Validators is uint256 score ) { - address account = getAccounts().validationSignerToAccount(validator); + address account = getAccounts().validatorSignerToAccount(signer); + return getValidator(account); + } + + /** + * @notice Returns validator information. + * @param account The account that registered the validator. + * @return The unpacked validator struct. + */ + function getValidator( + address account + ) + public + view + returns ( + bytes memory publicKeysData, + address affiliation, + uint256 score + ) + { require(isValidator(account)); - Validator storage _validator = validators[account]; + Validator storage validator = validators[account]; return ( - _validator.publicKeysData, - _validator.affiliation, - _validator.score.unwrap() + validator.publicKeysData, + validator.affiliation, + validator.score.unwrap() ); } @@ -771,7 +786,7 @@ contract Validators is address[] memory topAccounts = groups[account].members.headN(n); address[] memory topValidators = new address[](n); for (uint256 i = 0; i < n; i = i.add(1)) { - topValidators[i] = getAccounts().getValidationSigner(topAccounts[i]); + topValidators[i] = getAccounts().getValidatorSigner(topAccounts[i]); } return topValidators; } @@ -827,6 +842,19 @@ contract Validators is return registeredValidators; } + /** + * @notice Returns the list of signers for the registered validator accounts. + * @return The list of signers for registered validator accounts. + */ + function getRegisteredValidatorSigners() external view returns (address[] memory) { + IAccounts accounts = getAccounts(); + address[] memory signers = new address[](registeredValidators.length); + for (uint256 i = 0; i < signers.length; i = i.add(1)) { + signers[i] = accounts.getValidatorSigner(registeredValidators[i]); + } + return signers; + } + /** * @notice Returns the list of registered validator group accounts. * @return The list of registered validator group addresses. @@ -934,6 +962,12 @@ contract Validators is return true; } + /** + * @notice Updates the size history of a validator group. + * @param group The account whose group size has changed. + * @param size The new size of the group. + * @dev Used to determine how much gold an account needs to keep locked. + */ function updateSizeHistory(address group, uint256 size) private { uint256[] storage sizeHistory = groups[group].sizeHistory; if (size == sizeHistory.length) { @@ -945,6 +979,17 @@ contract Validators is } } + /** + * @notice Returns the group that `account` was a member of at the end of the last epoch. + * @param signer The signer of the account whose group membership should be returned. + * @return The group that `account` was a member of at the end of the last epoch. + */ + function getMembershipInLastEpochFromSigner(address signer) external view returns (address) { + address account = getAccounts().validatorSignerToAccount(signer); + require(isValidator(account)); + return getMembershipInLastEpoch(account); + } + /** * @notice Returns the group that `account` was a member of at the end of the last epoch. * @param account The account whose group membership should be returned. diff --git a/packages/protocol/contracts/governance/interfaces/IElection.sol b/packages/protocol/contracts/governance/interfaces/IElection.sol index ec4f76ee62a..4abff6ed639 100644 --- a/packages/protocol/contracts/governance/interfaces/IElection.sol +++ b/packages/protocol/contracts/governance/interfaces/IElection.sol @@ -7,5 +7,5 @@ interface IElection { function getTotalVotesByAccount(address) external view returns (uint256); function markGroupIneligible(address) external; function markGroupEligible(address,address,address) external; - function electValidators() external view returns (address[] memory); + function electValidatorSigners() external view returns (address[] memory); } diff --git a/packages/protocol/contracts/governance/test/MockElection.sol b/packages/protocol/contracts/governance/test/MockElection.sol index e4065c5c2ba..65bc22dd89d 100644 --- a/packages/protocol/contracts/governance/test/MockElection.sol +++ b/packages/protocol/contracts/governance/test/MockElection.sol @@ -45,7 +45,7 @@ contract MockElection is IElection { electedValidators = _electedValidators; } - function electValidators() external view returns (address[] memory) { + function electValidatorSigners() external view returns (address[] memory) { return electedValidators; } } diff --git a/packages/protocol/contracts/governance/test/ValidatorsTest.sol b/packages/protocol/contracts/governance/test/ValidatorsTest.sol index 36b08b02a9d..2fecbf7e597 100644 --- a/packages/protocol/contracts/governance/test/ValidatorsTest.sol +++ b/packages/protocol/contracts/governance/test/ValidatorsTest.sol @@ -8,17 +8,17 @@ import "../../common/FixidityLib.sol"; */ contract ValidatorsTest is Validators { - function updateValidatorScore(address validator, uint256 uptime) external { - return _updateValidatorScore(validator, uptime); + function updateValidatorScoreFromSigner(address signer, uint256 uptime) external { + return _updateValidatorScoreFromSigner(signer, uptime); } - function distributeEpochPayment( - address validator, + function distributeEpochPaymentsFromSigner( + address signer, uint256 maxPayment ) external returns (uint256) { - return _distributeEpochPayment(validator, maxPayment); + return _distributeEpochPaymentsFromSigner(signer, maxPayment); } } diff --git a/packages/protocol/contracts/identity/Attestations.sol b/packages/protocol/contracts/identity/Attestations.sol index 2fc64e07d0d..74cb91c43a8 100644 --- a/packages/protocol/contracts/identity/Attestations.sol +++ b/packages/protocol/contracts/identity/Attestations.sol @@ -626,7 +626,7 @@ contract Attestations is while (currentIndex < unselectedRequest.attestationsRequested) { seed = keccak256(abi.encodePacked(seed)); validator = validatorAddressFromCurrentSet(uint256(seed) % numberValidators); - issuer = getAccounts().activeValidationSignerToAccount(validator); + issuer = getAccounts().activeValidatorSignerToAccount(validator); Attestation storage attestation = state.issuedAttestations[issuer]; // Attestation issuers can only be added if they haven't been already. diff --git a/packages/protocol/test/common/accounts.ts b/packages/protocol/test/common/accounts.ts index ee1c6d06313..8221f9ef67f 100644 --- a/packages/protocol/test/common/accounts.ts +++ b/packages/protocol/test/common/accounts.ts @@ -15,8 +15,8 @@ const authorizationTestDescriptions = { subject: 'voteSigner', }, validating: { - me: 'validation signing key', - subject: 'validationSigner', + me: 'validator signing key', + subject: 'validatorSigner', }, attesting: { me: 'attestation signing key', @@ -47,11 +47,11 @@ contract('Accounts', (accounts: string[]) => { getAccountFromActiveAuthorized: accountsInstance.activeVoteSignerToAccount, } authorizationTests.validating = { - fn: accountsInstance.authorizeValidationSigner, - eventName: 'ValidationSignerAuthorized', - getAuthorizedFromAccount: accountsInstance.getValidationSigner, - getAccountFromAuthorized: accountsInstance.validationSignerToAccount, - getAccountFromActiveAuthorized: accountsInstance.activeValidationSignerToAccount, + fn: accountsInstance.authorizeValidatorSigner, + eventName: 'ValidatorSignerAuthorized', + getAuthorizedFromAccount: accountsInstance.getValidatorSigner, + getAccountFromAuthorized: accountsInstance.validatorSignerToAccount, + getAccountFromActiveAuthorized: accountsInstance.activeValidatorSignerToAccount, } authorizationTests.attesting = { fn: accountsInstance.authorizeAttestationSigner, diff --git a/packages/protocol/test/governance/election.ts b/packages/protocol/test/governance/election.ts index b4469376c56..c0b98704a9b 100644 --- a/packages/protocol/test/governance/election.ts +++ b/packages/protocol/test/governance/election.ts @@ -751,7 +751,7 @@ contract('Election', (accounts: string[]) => { }) }) - describe('#electValidators', () => { + describe('#electValidatorSigners', () => { let random: MockRandomInstance let totalLockedGold: number const group1 = accounts[0] @@ -812,7 +812,7 @@ contract('Election', (accounts: string[]) => { it("should return that group's member list", async () => { await setRandomness(hash1) - assertSameAddresses(await election.electValidators(), [ + assertSameAddresses(await election.electValidatorSigners(), [ validator1, validator2, validator3, @@ -830,7 +830,7 @@ contract('Election', (accounts: string[]) => { it('should return maxElectableValidators elected validators', async () => { await setRandomness(hash1) - assertSameAddresses(await election.electValidators(), [ + assertSameAddresses(await election.electValidatorSigners(), [ validator1, validator2, validator3, @@ -850,9 +850,9 @@ contract('Election', (accounts: string[]) => { it('should return different results', async () => { await setRandomness(hash1) - const valsWithHash1 = (await election.electValidators()).map((x) => x.toLowerCase()) + const valsWithHash1 = (await election.electValidatorSigners()).map((x) => x.toLowerCase()) await setRandomness(hash2) - const valsWithHash2 = (await election.electValidators()).map((x) => x.toLowerCase()) + const valsWithHash2 = (await election.electValidatorSigners()).map((x) => x.toLowerCase()) assert.sameMembers(valsWithHash1, valsWithHash2) assert.notDeepEqual(valsWithHash1, valsWithHash2) }) @@ -872,7 +872,7 @@ contract('Election', (accounts: string[]) => { it('should elect only n members from that group', async () => { await setRandomness(hash1) - assertSameAddresses(await election.electValidators(), [ + assertSameAddresses(await election.electValidatorSigners(), [ validator7, validator1, validator2, @@ -894,7 +894,7 @@ contract('Election', (accounts: string[]) => { it('should not elect any members from that group', async () => { await setRandomness(hash1) - assertSameAddresses(await election.electValidators(), [ + assertSameAddresses(await election.electValidatorSigners(), [ validator1, validator2, validator3, @@ -913,7 +913,7 @@ contract('Election', (accounts: string[]) => { it('should revert', async () => { await setRandomness(hash1) - await assertRevert(election.electValidators()) + await assertRevert(election.electValidatorSigners()) }) }) }) diff --git a/packages/protocol/test/governance/validators.ts b/packages/protocol/test/governance/validators.ts index 98087c39cd5..2b8313a4a8c 100644 --- a/packages/protocol/test/governance/validators.ts +++ b/packages/protocol/test/governance/validators.ts @@ -1505,7 +1505,7 @@ contract('Validators', (accounts: string[]) => { }) }) - describe('#updateValidatorScore', () => { + describe('#updateValidatorScoreFromSigner', () => { const validator = accounts[0] beforeEach(async () => { await registerValidator(validator) @@ -1517,7 +1517,7 @@ contract('Validators', (accounts: string[]) => { const epochScore = uptime.pow(validatorScoreParameters.exponent) const adjustmentSpeed = fromFixed(validatorScoreParameters.adjustmentSpeed) beforeEach(async () => { - await validators.updateValidatorScore(validator, toFixed(uptime)) + await validators.updateValidatorScoreFromSigner(validator, toFixed(uptime)) }) it('should update the validator score', async () => { @@ -1528,7 +1528,7 @@ contract('Validators', (accounts: string[]) => { describe('when the validator already has a non-zero score', () => { beforeEach(async () => { - await validators.updateValidatorScore(validator, toFixed(uptime)) + await validators.updateValidatorScoreFromSigner(validator, toFixed(uptime)) }) it('should update the validator score', async () => { @@ -1546,7 +1546,7 @@ contract('Validators', (accounts: string[]) => { describe('when uptime > 1.0', () => { const uptime = 1.01 it('should revert', async () => { - await assertRevert(validators.updateValidatorScore(validator, toFixed(uptime))) + await assertRevert(validators.updateValidatorScoreFromSigner(validator, toFixed(uptime))) }) }) }) @@ -1738,7 +1738,7 @@ contract('Validators', (accounts: string[]) => { }) }) - describe('#distributeEpochPayment', () => { + describe('#distributeEpochPaymentsFromSigner', () => { const validator = accounts[0] const group = accounts[1] const maxPayment = new BigNumber(20122394876) @@ -1755,19 +1755,19 @@ contract('Validators', (accounts: string[]) => { const adjustmentSpeed = fromFixed(validatorScoreParameters.adjustmentSpeed) // @ts-ignore const expectedScore = adjustmentSpeed.times(uptime.pow(validatorScoreParameters.exponent)) - const expectedTotalPayment = expectedScore.times(validatorEpochPayment) + const expectedTotalPayment = expectedScore.times(maxPayment).dp(0, BigNumber.ROUND_FLOOR) const expectedGroupPayment = expectedTotalPayment .times(fromFixed(commission)) .dp(0, BigNumber.ROUND_FLOOR) const expectedValidatorPayment = expectedTotalPayment.minus(expectedGroupPayment) beforeEach(async () => { - await validators.updateValidatorScore(validator, toFixed(uptime)) + await validators.updateValidatorScoreFromSigner(validator, toFixed(uptime)) }) describe('when the validator and group meet the balance requirements', () => { beforeEach(async () => { - ret = await validators.distributeEpochPayment(validator, maxPayment).call() - await validators.distributeEpochPayment(validator, maxPayment) + ret = await validators.distributeEpochPaymentsFromSigner.call(validator, maxPayment) + await validators.distributeEpochPaymentsFromSigner(validator, maxPayment) }) it('should pay the validator', async () => { @@ -1789,8 +1789,8 @@ contract('Validators', (accounts: string[]) => { validator, validatorLockedGoldRequirements.value.minus(1) ) - ret = await validators.distributeEpochPayment(validator, maxPayment).call() - await validators.distributeEpochPayment(validator, maxPayment) + ret = await validators.distributeEpochPaymentsFromSigner.call(validator, maxPayment) + await validators.distributeEpochPaymentsFromSigner(validator, maxPayment) }) it('should not pay the validator', async () => { @@ -1812,8 +1812,8 @@ contract('Validators', (accounts: string[]) => { group, groupLockedGoldRequirements.value.minus(1) ) - ret = await validators.distributeEpochPayment(validator, maxPayment).call() - await validators.distributeEpochPayment(validator, maxPayment) + ret = await validators.distributeEpochPaymentsFromSigner.call(validator, maxPayment) + await validators.distributeEpochPaymentsFromSigner(validator, maxPayment) }) it('should not pay the validator', async () => { diff --git a/packages/protocol/test/identity/attestations.ts b/packages/protocol/test/identity/attestations.ts index 33b38af0d94..8bd53f0d97e 100644 --- a/packages/protocol/test/identity/attestations.ts +++ b/packages/protocol/test/identity/attestations.ts @@ -140,7 +140,7 @@ contract('Attestations', (accounts: string[]) => { await accountsInstance.createAccount({ from: account }) await unlockAndAuthorizeKey( KeyOffsets.VALIDATING_KEY_OFFSET, - accountsInstance.authorizeValidationSigner, + accountsInstance.authorizeValidatorSigner, account ) }) From 531e3196d9475b660793e956e2110c837bd8156c Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Sat, 9 Nov 2019 09:38:40 -0800 Subject: [PATCH 108/149] Address comments --- .../src/e2e-tests/governance_tests.ts | 26 ++-- packages/celotool/src/e2e-tests/utils.ts | 17 +++ .../contracts/governance/EpochRewards.sol | 129 ++++++++---------- .../contracts/governance/Validators.sol | 16 +-- .../governance/proxies/EpochRewardsProxy.sol | 4 +- .../governance/test/EpochRewardsTest.sol | 5 +- .../governance/test/ValidatorsTest.sol | 5 +- .../stability/test/MockSortedOracles.sol | 1 - packages/protocol/migrationsConfig.js | 2 +- .../protocol/test/governance/epochrewards.ts | 5 +- 10 files changed, 94 insertions(+), 116 deletions(-) diff --git a/packages/celotool/src/e2e-tests/governance_tests.ts b/packages/celotool/src/e2e-tests/governance_tests.ts index 33652857004..94abc60e9a1 100644 --- a/packages/celotool/src/e2e-tests/governance_tests.ts +++ b/packages/celotool/src/e2e-tests/governance_tests.ts @@ -4,7 +4,14 @@ import { fromFixed, toFixed } from '@celo/utils/lib/fixidity' import BigNumber from 'bignumber.js' import { assert } from 'chai' import Web3 from 'web3' -import { getContext, getEnode, importGenesis, initAndStartGeth, sleep } from './utils' +import { + assertAlmostEqual, + getContext, + getEnode, + importGenesis, + initAndStartGeth, + sleep, +} from './utils' describe('governance tests', () => { const gethConfig = { @@ -125,23 +132,6 @@ describe('governance tests', () => { return blockNumber % epochSize === 0 } - const assertAlmostEqual = ( - actual: BigNumber, - expected: BigNumber, - delta: BigNumber = new BigNumber(10).pow(12).times(5) - ) => { - if (expected.isZero()) { - assert.equal(difference.toFixed(), expected.toFixed()) - } else { - const isCloseTo = - difference.plus(delta).gte(expected) || difference.minus(delta).lte(expected) - assert( - isCloseTo, - `expected ${expected.toString()} to almost equal ${difference.toString()} +/- ${delta.toString()}` - ) - } - } - describe('when the validator set is changing', () => { let epoch: number const blockNumbers: number[] = [] diff --git a/packages/celotool/src/e2e-tests/utils.ts b/packages/celotool/src/e2e-tests/utils.ts index 608fa7c9139..bfe5e018db0 100644 --- a/packages/celotool/src/e2e-tests/utils.ts +++ b/packages/celotool/src/e2e-tests/utils.ts @@ -1,3 +1,4 @@ +import BigNumber from 'bignumber.js' import { assert } from 'chai' import { spawn, SpawnOptions } from 'child_process' import fs from 'fs' @@ -40,6 +41,22 @@ const GENESIS_PATH = `${TEST_DIR}/genesis.json` const NetworkId = 1101 const MonorepoRoot = resolvePath(joinPath(__dirname, '../..', '../..')) +export function assertAlmostEqual( + actual: BigNumber, + expected: BigNumber, + delta: BigNumber = new BigNumber(10).pow(12).times(5) +) { + if (expected.isZero()) { + assert.equal(actual.toFixed(), expected.toFixed()) + } else { + const isCloseTo = actual.plus(delta).gte(expected) || actual.minus(delta).lte(expected) + assert( + isCloseTo, + `expected ${actual.toString()} to almost equal ${expected.toString()} +/- ${delta.toString()}` + ) + } +} + export function spawnWithLog(cmd: string, args: string[], logsFilepath: string) { try { fs.unlinkSync(logsFilepath) diff --git a/packages/protocol/contracts/governance/EpochRewards.sol b/packages/protocol/contracts/governance/EpochRewards.sol index eb9d0f9505e..ca591d41399 100644 --- a/packages/protocol/contracts/governance/EpochRewards.sol +++ b/packages/protocol/contracts/governance/EpochRewards.sol @@ -12,12 +12,11 @@ import "../common/UsingPrecompiles.sol"; * @title Contract for calculating epoch rewards. */ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry { - using FixidityLib for FixidityLib.Fraction; using SafeMath for uint256; - uint256 constant GENESIS_GOLD_SUPPLY = 600000000000000000000000000; // 600 million Gold - uint256 constant GOLD_SUPPLY_CAP = 1000000000000000000000000000; // 1 billion Gold + uint256 constant GENESIS_GOLD_SUPPLY = 600000000 ether; // 600 million Gold + uint256 constant GOLD_SUPPLY_CAP = 1000000000 ether; // 1 billion Gold uint256 constant YEARS_LINEAR = 15; uint256 constant SECONDS_LINEAR = YEARS_LINEAR * 365 * 1 days; @@ -74,10 +73,10 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry * @param targetVotingYieldAdjustmentFactor The target block reward adjustment factor for voters. * @param rewardsMultiplierMax The max multiplier on target epoch rewards. * @param rewardsMultiplierUnderspendAdjustmentFactor Adjusts the multiplier on target epoch - * rewards when the protocol is running behind the target gold supply. + * rewards when the protocol is running behind the target Gold supply. * @param rewardsMultiplierOverspendAdjustmentFactor Adjusts the multiplier on target epoch - * rewards when the protocol is running ahead of the target gold supply. - * @param _targetVotingGoldFraction The percentage of floating gold voting to target. + * rewards when the protocol is running ahead of the target Gold supply. + * @param _targetVotingGoldFraction The percentage of floating Gold voting to target. * @param _targetValidatorEpochPayment The target validator epoch payment. * @dev Should be called only once. */ @@ -91,10 +90,7 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry uint256 rewardsMultiplierOverspendAdjustmentFactor, uint256 _targetVotingGoldFraction, uint256 _targetValidatorEpochPayment - ) - external - initializer - { + ) external initializer { _transferOwnership(msg.sender); setRegistry(registryAddress); setTargetVotingYieldParameters(targetVotingYieldMax, targetVotingYieldAdjustmentFactor); @@ -132,8 +128,8 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry } /** - * @notice Sets the target voting gold fraction. - * @param value The percentage of floating gold voting to target. + * @notice Sets the target voting Gold fraction. + * @param value The percentage of floating Gold voting to target. * @return True upon success. */ function setTargetVotingGoldFraction(uint256 value) public onlyOwner returns (bool) { @@ -144,8 +140,8 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry } /** - * @notice Returns the target voting gold fraction. - * @return The percentage of floating gold voting to target. + * @notice Returns the target voting Gold fraction. + * @return The percentage of floating Gold voting to target. */ function getTargetVotingGoldFraction() external view returns (uint256) { return targetVotingGoldFraction.unwrap(); @@ -167,24 +163,20 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry * @notice Sets the rewards multiplier parameters. * @param max The max multiplier on target epoch rewards. * @param underspendAdjustmentFactor Adjusts the multiplier on target epoch rewards when the - * protocol is running behind the target gold supply. + * protocol is running behind the target Gold supply. * @param overspendAdjustmentFactor Adjusts the multiplier on target epoch rewards when the - * protocol is running ahead of the target gold supply. + * protocol is running ahead of the target Gold supply. * @return True upon success. */ function setRewardsMultiplierParameters( uint256 max, uint256 underspendAdjustmentFactor, uint256 overspendAdjustmentFactor - ) - public - onlyOwner - returns (bool) - { + ) public onlyOwner returns (bool) { require( max != rewardsMultiplierParams.max.unwrap() || - overspendAdjustmentFactor != rewardsMultiplierParams.adjustmentFactors.overspend.unwrap() || - underspendAdjustmentFactor != rewardsMultiplierParams.adjustmentFactors.underspend.unwrap() + overspendAdjustmentFactor != rewardsMultiplierParams.adjustmentFactors.overspend.unwrap() || + underspendAdjustmentFactor != rewardsMultiplierParams.adjustmentFactors.underspend.unwrap() ); rewardsMultiplierParams = RewardsMultiplierParameters( RewardsMultiplierAdjustmentFactors( @@ -193,11 +185,7 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry ), FixidityLib.wrap(max) ); - emit RewardsMultiplierParametersSet( - max, - underspendAdjustmentFactor, - overspendAdjustmentFactor - ); + emit RewardsMultiplierParametersSet(max, underspendAdjustmentFactor, overspendAdjustmentFactor); return true; } @@ -207,17 +195,14 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry * @param adjustmentFactor The target block reward adjustment factor for voters. * @return True upon success. */ - function setTargetVotingYieldParameters( - uint256 max, - uint256 adjustmentFactor - ) + function setTargetVotingYieldParameters(uint256 max, uint256 adjustmentFactor) public onlyOwner returns (bool) { require( max != targetVotingYieldParams.max.unwrap() || - adjustmentFactor != targetVotingYieldParams.adjustmentFactor.unwrap() + adjustmentFactor != targetVotingYieldParams.adjustmentFactor.unwrap() ); targetVotingYieldParams.max = FixidityLib.wrap(max); targetVotingYieldParams.adjustmentFactor = FixidityLib.wrap(adjustmentFactor); @@ -230,8 +215,8 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry } /** - * @notice Returns the target gold supply according to the epoch rewards target schedule. - * @return The target gold supply according to the epoch rewards target schedule. + * @notice Returns the target Gold supply according to the epoch rewards target schedule. + * @return The target Gold supply according to the epoch rewards target schedule. */ function getTargetGoldTotalSupply() public view returns (uint256) { uint256 timeSinceInitialization = now.sub(startTime); @@ -248,13 +233,11 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry } /** - * @notice Returns the rewards multiplier based on the current and target gold supplies. - * @param targetGoldSupplyIncrease The target increase in current gold supply. - * @return The rewards multiplier based on the current and target gold supplies. + * @notice Returns the rewards multiplier based on the current and target Gold supplies. + * @param targetGoldSupplyIncrease The target increase in current Gold supply. + * @return The rewards multiplier based on the current and target Gold supplies. */ - function _getRewardsMultiplier( - uint256 targetGoldSupplyIncrease - ) + function _getRewardsMultiplier(uint256 targetGoldSupplyIncrease) internal view returns (FixidityLib.Fraction memory) @@ -263,23 +246,24 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry uint256 totalSupply = getGoldToken().totalSupply(); uint256 remainingSupply = GOLD_SUPPLY_CAP.sub(totalSupply.add(targetGoldSupplyIncrease)); uint256 targetRemainingSupply = GOLD_SUPPLY_CAP.sub(targetSupply); - FixidityLib.Fraction memory ratio = FixidityLib.newFixed(remainingSupply).divide( - FixidityLib.newFixed(targetRemainingSupply) - ); - if (ratio.gt(FixidityLib.fixed1())) { - FixidityLib.Fraction memory delta = ratio.subtract(FixidityLib.fixed1()).multiply( - rewardsMultiplierParams.adjustmentFactors.underspend - ); - FixidityLib.Fraction memory r = FixidityLib.fixed1().add(delta); - if (r.lt(rewardsMultiplierParams.max)) { - return r; + FixidityLib.Fraction memory remainingToTargetRatio = FixidityLib + .newFixed(remainingSupply) + .divide(FixidityLib.newFixed(targetRemainingSupply)); + if (remainingToTargetRatio.gt(FixidityLib.fixed1())) { + FixidityLib.Fraction memory delta = remainingToTargetRatio + .subtract(FixidityLib.fixed1()) + .multiply(rewardsMultiplierParams.adjustmentFactors.underspend); + FixidityLib.Fraction memory multiplier = FixidityLib.fixed1().add(delta); + if (multiplier.lt(rewardsMultiplierParams.max)) { + return multiplier; } else { return rewardsMultiplierParams.max; } - } else if (ratio.lt(FixidityLib.fixed1())) { - FixidityLib.Fraction memory delta = FixidityLib.fixed1().subtract(ratio).multiply( - rewardsMultiplierParams.adjustmentFactors.overspend - ); + } else if (remainingToTargetRatio.lt(FixidityLib.fixed1())) { + FixidityLib.Fraction memory delta = FixidityLib + .fixed1() + .subtract(remainingToTargetRatio) + .multiply(rewardsMultiplierParams.adjustmentFactors.overspend); if (delta.lt(FixidityLib.fixed1())) { return FixidityLib.fixed1().subtract(delta); } else { @@ -291,8 +275,8 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry } /** - * @notice Returns the rewards multiplier based on the current and target gold supplies. - * @return The rewards multiplier based on the current and target gold supplies. + * @notice Returns the rewards multiplier based on the current and target Gold supplies. + * @return The rewards multiplier based on the current and target Gold supplies. */ function getRewardsMultiplier() external view returns (uint256) { uint256 targetEpochRewards = getTargetEpochRewards(); @@ -306,26 +290,29 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry * @return the total target epoch rewards for voters. */ function getTargetEpochRewards() public view returns (uint256) { - return FixidityLib.newFixed(getElection().getActiveVotes()).multiply( - targetVotingYieldParams.target - ).fromFixed(); + return + FixidityLib + .newFixed(getElection().getActiveVotes()) + .multiply(targetVotingYieldParams.target) + .fromFixed(); } /** - * @notice Returns the total target epoch payments to validators, converted to gold. - * @return The total target epoch payments to validators, converted to gold. + * @notice Returns the total target epoch payments to validators, converted to Gold. + * @return The total target epoch payments to validators, converted to Gold. */ function getTargetTotalEpochPaymentsInGold() public view returns (uint256) { address stableTokenAddress = registry.getAddressForOrDie(STABLE_TOKEN_REGISTRY_ID); (uint256 numerator, uint256 denominator) = getSortedOracles().medianRate(stableTokenAddress); - return numberValidatorsInCurrentSet().mul(targetValidatorEpochPayment).mul(denominator).div( - numerator - ); + return + numberValidatorsInCurrentSet().mul(targetValidatorEpochPayment).mul(denominator).div( + numerator + ); } /** - * @notice Returns the fraction of floating gold being used for voting in validator elections. - * @return The fraction of floating gold being used for voting in validator elections. + * @notice Returns the fraction of floating Gold being used for voting in validator elections. + * @return The fraction of floating Gold being used for voting in validator elections. */ function getVotingGoldFraction() public view returns (uint256) { // TODO(asa): Ignore custodial accounts. @@ -338,7 +325,7 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry /** * @notice Updates the target voting yield based on the difference between the target and current - * voting gold fraction. + * voting Gold fraction. */ function _updateTargetVotingYield() internal { FixidityLib.Fraction memory votingGoldFraction = FixidityLib.wrap(getVotingGoldFraction()); @@ -372,7 +359,7 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry /** * @notice Updates the target voting yield based on the difference between the target and current - * voting gold fraction. + * voting Gold fraction. * @dev Only called directly by the protocol. */ function updateTargetVotingYield() external { @@ -388,9 +375,7 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry uint256 targetEpochRewards = getTargetEpochRewards(); uint256 targetTotalEpochPaymentsInGold = getTargetTotalEpochPaymentsInGold(); uint256 targetGoldSupplyIncrease = targetEpochRewards.add(targetTotalEpochPaymentsInGold); - FixidityLib.Fraction memory rewardsMultiplier = _getRewardsMultiplier( - targetGoldSupplyIncrease - ); + FixidityLib.Fraction memory rewardsMultiplier = _getRewardsMultiplier(targetGoldSupplyIncrease); return ( FixidityLib.newFixed(targetValidatorEpochPayment).multiply(rewardsMultiplier).fromFixed(), FixidityLib.newFixed(targetEpochRewards).multiply(rewardsMultiplier).fromFixed() diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index 40b92e8ca05..d3f80dcd1ce 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -371,10 +371,7 @@ contract Validators is * group commission. * @return The total payment paid to the validator and their group. */ - function distributeEpochPayment( - address validator, - uint256 maxPayment - ) + function distributeEpochPayment(address validator, uint256 maxPayment) external onlyVm() returns (uint256) @@ -389,10 +386,7 @@ contract Validators is * group commission. * @return The total payment paid to the validator and their group. */ - function _distributeEpochPayment( - address validator, - uint256 maxPayment - ) + function _distributeEpochPayment(address validator, uint256 maxPayment) internal returns (uint256) { @@ -404,9 +398,9 @@ contract Validators is // Both the validator and the group must maintain the minimum locked gold balance in order to // receive epoch payments. if (meetsAccountLockedGoldRequirements(account) && meetsAccountLockedGoldRequirements(group)) { - FixidityLib.Fraction memory totalPayment = FixidityLib - .newFixed(maxPayment) - .multiply(validators[account].score); + FixidityLib.Fraction memory totalPayment = FixidityLib.newFixed(maxPayment).multiply( + validators[account].score + ); uint256 groupPayment = totalPayment.multiply(groups[group].commission).fromFixed(); uint256 validatorPayment = totalPayment.fromFixed().sub(groupPayment); getStableToken().mint(group, groupPayment); diff --git a/packages/protocol/contracts/governance/proxies/EpochRewardsProxy.sol b/packages/protocol/contracts/governance/proxies/EpochRewardsProxy.sol index 205f71fdb56..2adeb479d67 100644 --- a/packages/protocol/contracts/governance/proxies/EpochRewardsProxy.sol +++ b/packages/protocol/contracts/governance/proxies/EpochRewardsProxy.sol @@ -2,7 +2,5 @@ pragma solidity ^0.5.3; import "../../common/Proxy.sol"; - /* solhint-disable no-empty-blocks */ -contract EpochRewardsProxy is Proxy { -} +contract EpochRewardsProxy is Proxy {} diff --git a/packages/protocol/contracts/governance/test/EpochRewardsTest.sol b/packages/protocol/contracts/governance/test/EpochRewardsTest.sol index e0ab169ab0d..774e89501f6 100644 --- a/packages/protocol/contracts/governance/test/EpochRewardsTest.sol +++ b/packages/protocol/contracts/governance/test/EpochRewardsTest.sol @@ -7,10 +7,7 @@ import "../../common/FixidityLib.sol"; * @title A wrapper around EpochRewards that exposes internal functions for testing. */ contract EpochRewardsTest is EpochRewards { - - function getRewardsMultiplier( - uint256 targetGoldTotalSupplyIncrease - ) + function getRewardsMultiplier(uint256 targetGoldTotalSupplyIncrease) external view returns (uint256) diff --git a/packages/protocol/contracts/governance/test/ValidatorsTest.sol b/packages/protocol/contracts/governance/test/ValidatorsTest.sol index a0e4727c9f3..a049ddc6efb 100644 --- a/packages/protocol/contracts/governance/test/ValidatorsTest.sol +++ b/packages/protocol/contracts/governance/test/ValidatorsTest.sol @@ -11,10 +11,7 @@ contract ValidatorsTest is Validators { return _updateValidatorScore(validator, uptime); } - function distributeEpochPayment( - address validator, - uint256 maxPayment - ) + function distributeEpochPayment(address validator, uint256 maxPayment) external returns (uint256) { diff --git a/packages/protocol/contracts/stability/test/MockSortedOracles.sol b/packages/protocol/contracts/stability/test/MockSortedOracles.sol index 707a3d2a58d..29a605776a9 100644 --- a/packages/protocol/contracts/stability/test/MockSortedOracles.sol +++ b/packages/protocol/contracts/stability/test/MockSortedOracles.sol @@ -4,7 +4,6 @@ pragma solidity ^0.5.3; * @title A mock SortedOracles for testing. */ contract MockSortedOracles { - uint256 public constant DENOMINATOR = 0x10000000000000000; mapping(address => uint256) public numerators; mapping(address => uint256) public medianTimestamp; diff --git a/packages/protocol/migrationsConfig.js b/packages/protocol/migrationsConfig.js index e733b5b51c1..7ff2433457d 100644 --- a/packages/protocol/migrationsConfig.js +++ b/packages/protocol/migrationsConfig.js @@ -41,7 +41,7 @@ const DefaultConfig = { }, }, targetVotingGoldFraction: 2 / 3, - maxValidatorEpochPayment: '1000000000000000000', + maxValidatorEpochPayment: '205479452054794520547', // (75,000 / 365) * 10 ^ 18 }, exchange: { spread: 5 / 1000, diff --git a/packages/protocol/test/governance/epochrewards.ts b/packages/protocol/test/governance/epochrewards.ts index 3818d86d083..6348ac07289 100644 --- a/packages/protocol/test/governance/epochrewards.ts +++ b/packages/protocol/test/governance/epochrewards.ts @@ -211,10 +211,10 @@ contract('EpochRewards', (accounts: string[]) => { }) describe('#setRewardsMultiplierAdjustmentFactors()', () => { - describe('when the factors are different', () => { + describe('when one of the factors is different', () => { const newFactors = { underspend: rewardsMultiplierAdjustments.underspend.plus(1), - overspend: rewardsMultiplierAdjustments.overspend.plus(1), + overspend: rewardsMultiplierAdjustments.overspend, } describe('when called by the owner', () => { @@ -443,6 +443,7 @@ contract('EpochRewards', (accounts: string[]) => { describe.only('#updateTargetVotingYield()', () => { const randomAddress = web3.utils.randomHex(20) + // Arbitrary numbers const totalSupply = new BigNumber(129762987346298761037469283746) const reserveBalance = new BigNumber(2397846127684712867321) const floatingSupply = totalSupply.minus(reserveBalance) From 5af2e3bf7cae6c0a1d7a6589b5278199e8ddb859 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Sat, 9 Nov 2019 10:22:40 -0800 Subject: [PATCH 109/149] Fix tests --- .../protocol/test/governance/epochrewards.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/protocol/test/governance/epochrewards.ts b/packages/protocol/test/governance/epochrewards.ts index 6348ac07289..307eb119ec0 100644 --- a/packages/protocol/test/governance/epochrewards.ts +++ b/packages/protocol/test/governance/epochrewards.ts @@ -54,9 +54,12 @@ contract('EpochRewards', (accounts: string[]) => { max: toFixed(new BigNumber(1 / 5)), adjustmentFactor: toFixed(new BigNumber(1 / 365)), } - const rewardsMultiplierAdjustments = { - underspend: toFixed(new BigNumber(1 / 2)), - overspend: toFixed(new BigNumber(5)), + const rewardsMultiplier = { + max: toFixed(new BigNumber(2)), + adjustments: { + underspend: toFixed(new BigNumber(1 / 2)), + overspend: toFixed(new BigNumber(5)), + }, } const targetVotingGoldFraction = toFixed(new BigNumber(2 / 3)) const maxValidatorEpochPayment = new BigNumber(10000000000000) @@ -74,8 +77,9 @@ contract('EpochRewards', (accounts: string[]) => { targetVotingYieldParams.initial, targetVotingYieldParams.max, targetVotingYieldParams.adjustmentFactor, - rewardsMultiplierAdjustments.underspend, - rewardsMultiplierAdjustments.overspend, + rewardsMultiplier.max, + rewardsMultiplier.adjustments.underspend, + rewardsMultiplier.adjustments.overspend, targetVotingGoldFraction, maxValidatorEpochPayment ) @@ -111,8 +115,9 @@ contract('EpochRewards', (accounts: string[]) => { targetVotingYieldParams.initial, targetVotingYieldParams.max, targetVotingYieldParams.adjustmentFactor, - rewardsMultiplierAdjustments.underspend, - rewardsMultiplierAdjustments.overspend, + rewardsMultiplier.max, + rewardsMultiplier.adjustments.underspend, + rewardsMultiplier.adjustments.overspend, targetVotingGoldFraction, maxValidatorEpochPayment ) From bec525b90866b00adbeb19523bfb2cc8ff5b61f6 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Sat, 9 Nov 2019 11:02:35 -0800 Subject: [PATCH 110/149] Fix tests --- .../protocol/test/governance/epochrewards.ts | 161 +++++++++--------- 1 file changed, 81 insertions(+), 80 deletions(-) diff --git a/packages/protocol/test/governance/epochrewards.ts b/packages/protocol/test/governance/epochrewards.ts index 307eb119ec0..9215b00c6d1 100644 --- a/packages/protocol/test/governance/epochrewards.ts +++ b/packages/protocol/test/governance/epochrewards.ts @@ -62,7 +62,10 @@ contract('EpochRewards', (accounts: string[]) => { }, } const targetVotingGoldFraction = toFixed(new BigNumber(2 / 3)) - const maxValidatorEpochPayment = new BigNumber(10000000000000) + const targetValidatorEpochPayment = new BigNumber(10000000000000) + const exchangeRate = 7 + const randomAddress = web3.utils.randomHex(20) + const sortedOraclesDenominator = new BigNumber('0x10000000000000000') beforeEach(async () => { epochRewards = await EpochRewards.new() mockElection = await MockElection.new() @@ -72,6 +75,12 @@ contract('EpochRewards', (accounts: string[]) => { await registry.setAddressFor(CeloContractName.Election, mockElection.address) await registry.setAddressFor(CeloContractName.GoldToken, mockGoldToken.address) await registry.setAddressFor(CeloContractName.SortedOracles, mockSortedOracles.address) + await registry.setAddressFor(CeloContractName.StableToken, randomAddress) + await mockSortedOracles.setMedianRate( + randomAddress, + sortedOraclesDenominator.times(exchangeRate) + ) + await epochRewards.initialize( registry.address, targetVotingYieldParams.initial, @@ -81,7 +90,7 @@ contract('EpochRewards', (accounts: string[]) => { rewardsMultiplier.adjustments.underspend, rewardsMultiplier.adjustments.overspend, targetVotingGoldFraction, - maxValidatorEpochPayment + targetValidatorEpochPayment ) }) @@ -91,8 +100,8 @@ contract('EpochRewards', (accounts: string[]) => { assert.equal(owner, accounts[0]) }) - it('should have set the max validator epoch payment', async () => { - assertEqualBN(await epochRewards.maxValidatorEpochPayment(), maxValidatorEpochPayment) + it('should have set the target validator epoch payment', async () => { + assertEqualBN(await epochRewards.targetValidatorEpochPayment(), targetValidatorEpochPayment) }) it('should have set the target voting yield parameters', async () => { @@ -102,10 +111,11 @@ contract('EpochRewards', (accounts: string[]) => { assertEqualBN(adjustmentFactor, targetVotingYieldParams.adjustmentFactor) }) - it('should have set the rewards multiplier adjustment factors', async () => { - const [underspend, overspend] = await epochRewards.getRewardsMultiplierAdjustmentFactors() - assertEqualBN(underspend, rewardsMultiplierAdjustments.underspend) - assertEqualBN(overspend, rewardsMultiplierAdjustments.overspend) + it('should have set the rewards multiplier parameters', async () => { + const [max, underspend, overspend] = await epochRewards.getRewardsMultiplierParameters() + assertEqualBN(max, rewardsMultiplier.max) + assertEqualBN(underspend, rewardsMultiplier.adjustments.underspend) + assertEqualBN(overspend, rewardsMultiplier.adjustments.overspend) }) it('should not be callable again', async () => { @@ -119,7 +129,7 @@ contract('EpochRewards', (accounts: string[]) => { rewardsMultiplier.adjustments.underspend, rewardsMultiplier.adjustments.overspend, targetVotingGoldFraction, - maxValidatorEpochPayment + targetValidatorEpochPayment ) ) }) @@ -170,26 +180,26 @@ contract('EpochRewards', (accounts: string[]) => { }) }) - describe('#setMaxValidatorEpochPayment()', () => { + describe('#setTargetValidatorEpochPayment()', () => { describe('when the payment is different', () => { - const newPayment = maxValidatorEpochPayment.plus(1) + const newPayment = targetValidatorEpochPayment.plus(1) describe('when called by the owner', () => { let resp: any beforeEach(async () => { - resp = await epochRewards.setMaxValidatorEpochPayment(newPayment) + resp = await epochRewards.setTargetValidatorEpochPayment(newPayment) }) - it('should set the max validator epoch payment', async () => { - assertEqualBN(await epochRewards.maxValidatorEpochPayment(), newPayment) + it('should set the target validator epoch payment', async () => { + assertEqualBN(await epochRewards.targetValidatorEpochPayment(), newPayment) }) - it('should emit the MaxValidatorEpochPaymentSet event', async () => { + it('should emit the TargetValidatorEpochPaymentSet event', async () => { assert.equal(resp.logs.length, 1) const log = resp.logs[0] assertContainSubset(log, { - event: 'MaxValidatorEpochPaymentSet', + event: 'TargetValidatorEpochPaymentSet', args: { payment: newPayment, }, @@ -199,7 +209,7 @@ contract('EpochRewards', (accounts: string[]) => { describe('when called by a non-owner', () => { it('should revert', async () => { await assertRevert( - epochRewards.setMaxValidatorEpochPayment(newPayment, { + epochRewards.setTargetValidatorEpochPayment(newPayment, { from: nonOwner, }) ) @@ -209,43 +219,49 @@ contract('EpochRewards', (accounts: string[]) => { describe('when the payment is the same', () => { it('should revert', async () => { - await assertRevert(epochRewards.setMaxValidatorEpochPayment(maxValidatorEpochPayment)) + await assertRevert( + epochRewards.setTargetValidatorEpochPayment(targetValidatorEpochPayment) + ) }) }) }) }) - describe('#setRewardsMultiplierAdjustmentFactors()', () => { - describe('when one of the factors is different', () => { - const newFactors = { - underspend: rewardsMultiplierAdjustments.underspend.plus(1), - overspend: rewardsMultiplierAdjustments.overspend, + describe('#setRewardsMultiplierParameters()', () => { + describe('when one of the parameters is different', () => { + const newParams = { + max: rewardsMultiplier.max, + underspend: rewardsMultiplier.adjustments.underspend.plus(1), + overspend: rewardsMultiplier.adjustments.overspend, } describe('when called by the owner', () => { let resp: any beforeEach(async () => { - resp = await epochRewards.setRewardsMultiplierAdjustmentFactors( - newFactors.underspend, - newFactors.overspend + resp = await epochRewards.setRewardsMultiplierParameters( + newParams.max, + newParams.underspend, + newParams.overspend ) }) - it('should set the new rewards multiplier adjustment factors', async () => { - const [underspend, overspend] = await epochRewards.getRewardsMultiplierAdjustmentFactors() - assertEqualBN(underspend, newFactors.underspend) - assertEqualBN(overspend, newFactors.overspend) + it('should set the new rewards multiplier adjustment params', async () => { + const [max, underspend, overspend] = await epochRewards.getRewardsMultiplierParameters() + assertEqualBN(max, newParams.max) + assertEqualBN(underspend, newParams.underspend) + assertEqualBN(overspend, newParams.overspend) }) - it('should emit the RewardsMultiplierAdjustmentFactorsSet event', async () => { + it('should emit the RewardsMultiplierParametersSet event', async () => { assert.equal(resp.logs.length, 1) const log = resp.logs[0] assertContainSubset(log, { - event: 'RewardsMultiplierAdjustmentFactorsSet', + event: 'RewardsMultiplierParametersSet', args: { - underspend: newFactors.underspend, - overspend: newFactors.overspend, + max: newParams.max, + underspendAdjustmentFactor: newParams.underspend, + overspendAdjustmentFactor: newParams.overspend, }, }) }) @@ -253,9 +269,10 @@ contract('EpochRewards', (accounts: string[]) => { describe('when called by a non-owner', () => { it('should revert', async () => { await assertRevert( - epochRewards.setRewardsMultiplierAdjustmentFactors( - newFactors.underspend, - newFactors.overspend, + epochRewards.setRewardsMultiplierParameters( + newParams.max, + newParams.underspend, + newParams.overspend, { from: nonOwner, } @@ -268,9 +285,10 @@ contract('EpochRewards', (accounts: string[]) => { describe('when the parameters are the same', () => { it('should revert', async () => { await assertRevert( - epochRewards.setRewardsMultiplierAdjustmentFactors( - rewardsMultiplierAdjustments.underspend, - rewardsMultiplierAdjustments.overspend + epochRewards.setRewardsMultiplierParameters( + rewardsMultiplier.max, + rewardsMultiplier.adjustments.underspend, + rewardsMultiplier.adjustments.overspend ) ) }) @@ -362,24 +380,12 @@ contract('EpochRewards', (accounts: string[]) => { }) }) - describe.only('#getTargetTotalEpochPaymentsInGold()', () => { + describe('#getTargetTotalEpochPaymentsInGold()', () => { describe('when a StableToken exchange rate is set', () => { - // 7 StableToken to one Celo Gold - const exchangeRate = 7 - const sortedOraclesDenominator = new BigNumber('0x10000000000000000') - const randomAddress = web3.utils.randomHex(20) // Hard coded in EpochRewardsTest.sol const numValidators = 100 - beforeEach(async () => { - await registry.setAddressFor(CeloContractName.StableToken, randomAddress) - await mockSortedOracles.setMedianRate( - randomAddress, - sortedOraclesDenominator.times(exchangeRate) - ) - }) - it('should return the number of validators times the max payment divided by the exchange rate', async () => { - const expected = maxValidatorEpochPayment + const expected = targetValidatorEpochPayment .times(numValidators) .div(exchangeRate) .integerValue(BigNumber.ROUND_FLOOR) @@ -388,13 +394,17 @@ contract('EpochRewards', (accounts: string[]) => { }) }) - describe.only('#getRewardsMultiplier()', () => { + describe('#getRewardsMultiplier()', () => { const timeDelta = YEAR.times(10) const expectedTargetTotalSupply = getExpectedTargetTotalSupply(timeDelta) const expectedTargetRemainingSupply = SUPPLY_CAP.minus(expectedTargetTotalSupply) - const targetEpochReward = new BigNumber(120397694238746) + let targetEpochReward: BigNumber beforeEach(async () => { await timeTravel(timeDelta.toNumber(), web3) + targetEpochReward = await epochRewards.getTargetEpochRewards() + targetEpochReward = targetEpochReward.plus( + await epochRewards.getTargetTotalEpochPaymentsInGold() + ) }) describe('when the target supply is equal to the actual supply after rewards', () => { @@ -403,7 +413,7 @@ contract('EpochRewards', (accounts: string[]) => { }) it('should return one', async () => { - assertEqualBN(await epochRewards.getRewardsMultiplier(targetEpochReward), toFixed(1)) + assertEqualBN(await epochRewards.getRewardsMultiplier(), toFixed(1)) }) }) @@ -417,12 +427,12 @@ contract('EpochRewards', (accounts: string[]) => { }) it('should return one plus 10% times the underspend adjustment', async () => { - const actual = fromFixed(await epochRewards.getRewardsMultiplier(targetEpochReward)) + const actual = fromFixed(await epochRewards.getRewardsMultiplier()) const expected = new BigNumber(1).plus( - fromFixed(rewardsMultiplierAdjustments.underspend).times(0.1) + fromFixed(rewardsMultiplier.adjustments.underspend).times(0.1) ) - // Assert equal to 9 decimal places due to fixidity imprecision. - assert.equal(expected.dp(9).toFixed(), actual.dp(9).toFixed()) + // Assert equal to 7 decimal places due to fixidity imprecision. + assert.equal(expected.dp(7).toFixed(), actual.dp(7).toFixed()) }) }) @@ -436,17 +446,17 @@ contract('EpochRewards', (accounts: string[]) => { }) it('should return one minus 10% times the underspend adjustment', async () => { - const actual = fromFixed(await epochRewards.getRewardsMultiplier(targetEpochReward)) + const actual = fromFixed(await epochRewards.getRewardsMultiplier()) const expected = new BigNumber(1).minus( - fromFixed(rewardsMultiplierAdjustments.overspend).times(0.1) + fromFixed(rewardsMultiplier.adjustments.overspend).times(0.1) ) - // Assert equal to 9 decimal places due to fixidity imprecision. - assert.equal(expected.dp(9).toFixed(), actual.dp(9).toFixed()) + // Assert equal to 7 decimal places due to fixidity imprecision. + assert.equal(expected.dp(7).toFixed(), actual.dp(7).toFixed()) }) }) }) - describe.only('#updateTargetVotingYield()', () => { + describe('#updateTargetVotingYield()', () => { const randomAddress = web3.utils.randomHex(20) // Arbitrary numbers const totalSupply = new BigNumber(129762987346298761037469283746) @@ -518,26 +528,17 @@ contract('EpochRewards', (accounts: string[]) => { }) }) - describe.only('#calculateTargetEpochPaymentAndRewards()', () => { + describe('#calculateTargetEpochPaymentAndRewards()', () => { describe('when there are active votes, a stable token exchange rate is set and the actual remaining supply is 10% more than the target remaining supply after rewards', () => { const activeVotes = 1000000 - const sortedOraclesDenominator = new BigNumber('0x10000000000000000') - const randomAddress = web3.utils.randomHex(20) const timeDelta = YEAR.times(10) - // 7 StableToken to one Celo Gold - const exchangeRate = 7 // Hard coded in EpochRewardsTest.sol const numValidators = 100 let expectedMultiplier: BigNumber beforeEach(async () => { await mockElection.setActiveVotes(activeVotes) - await registry.setAddressFor(CeloContractName.StableToken, randomAddress) - await mockSortedOracles.setMedianRate( - randomAddress, - sortedOraclesDenominator.times(exchangeRate) - ) await timeTravel(timeDelta.toNumber(), web3) - const expectedTargetTotalEpochPaymentsInGold = maxValidatorEpochPayment + const expectedTargetTotalEpochPaymentsInGold = targetValidatorEpochPayment .times(numValidators) .div(exchangeRate) .integerValue(BigNumber.ROUND_FLOOR) @@ -555,12 +556,12 @@ contract('EpochRewards', (accounts: string[]) => { .integerValue(BigNumber.ROUND_FLOOR) await mockGoldToken.setTotalSupply(totalSupply) expectedMultiplier = new BigNumber(1).plus( - fromFixed(rewardsMultiplierAdjustments.underspend).times(0.1) + fromFixed(rewardsMultiplier.adjustments.underspend).times(0.1) ) }) - it('should return the max validator epoch payment times the rewards multiplier', async () => { - const expected = maxValidatorEpochPayment.times(expectedMultiplier) + it('should return the target validator epoch payment times the rewards multiplier', async () => { + const expected = targetValidatorEpochPayment.times(expectedMultiplier) assertEqualBN((await epochRewards.calculateTargetEpochPaymentAndRewards())[0], expected) }) From 10f31490b5044e6c4cf7fe0c264f5b2c3a40e2fe Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Sat, 9 Nov 2019 11:24:20 -0800 Subject: [PATCH 111/149] Delte comment --- .../contracts/governance/Validators.sol | 170 +++++------------- 1 file changed, 46 insertions(+), 124 deletions(-) diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index 14b2fcdc868..778c59a6fb4 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -1,28 +1,32 @@ pragma solidity ^0.5.3; -import "openzeppelin-solidity/contracts/math/Math.sol"; -import "openzeppelin-solidity/contracts/math/SafeMath.sol"; -import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; -import "openzeppelin-solidity/contracts/utils/ReentrancyGuard.sol"; -import "solidity-bytes-utils/contracts/BytesLib.sol"; +import 'openzeppelin-solidity/contracts/math/Math.sol'; +import 'openzeppelin-solidity/contracts/math/SafeMath.sol'; +import 'openzeppelin-solidity/contracts/ownership/Ownable.sol'; +import 'openzeppelin-solidity/contracts/utils/ReentrancyGuard.sol'; +import 'solidity-bytes-utils/contracts/BytesLib.sol'; -import "./interfaces/IValidators.sol"; +import './interfaces/IValidators.sol'; -import "../identity/interfaces/IRandom.sol"; - -import "../common/Initializable.sol"; -import "../common/FixidityLib.sol"; -import "../common/linkedlists/AddressLinkedList.sol"; -import "../common/UsingRegistry.sol"; -import "../common/UsingPrecompiles.sol"; +import '../identity/interfaces/IRandom.sol'; +import '../common/Initializable.sol'; +import '../common/FixidityLib.sol'; +import '../common/linkedlists/AddressLinkedList.sol'; +import '../common/UsingRegistry.sol'; +import '../common/UsingPrecompiles.sol'; /** * @title A contract for registering and electing Validator Groups and Validators. */ contract Validators is - IValidators, Ownable, ReentrancyGuard, Initializable, UsingRegistry, UsingPrecompiles { - + IValidators, + Ownable, + ReentrancyGuard, + Initializable, + UsingRegistry, + UsingPrecompiles +{ using FixidityLib for FixidityLib.Fraction; using AddressLinkedList for LinkedList.List; using SafeMath for uint256; @@ -49,10 +53,6 @@ contract Validators is uint256 duration; } - // If we knew what time the validator was last in a group, we could enforce that to deregister a - // group, you need to have had 0 members for `duration`, and to deregister a validator, you need - // to have been out of a group for `duration`... - struct ValidatorGroup { bool exists; LinkedList.List members; @@ -148,10 +148,7 @@ contract Validators is uint256 validatorScoreAdjustmentSpeed, uint256 _membershipHistoryLength, uint256 _maxGroupSize - ) - external - initializer - { + ) external initializer { _transferOwnership(msg.sender); setRegistry(registryAddress); setGroupLockedGoldRequirements(groupRequirementValue, groupRequirementDuration); @@ -191,10 +188,7 @@ contract Validators is * @param adjustmentSpeed The speed at which the score is adjusted. * @return True upon success. */ - function setValidatorScoreParameters( - uint256 exponent, - uint256 adjustmentSpeed - ) + function setValidatorScoreParameters(uint256 exponent, uint256 adjustmentSpeed) public onlyOwner returns (bool) @@ -202,7 +196,7 @@ contract Validators is require(adjustmentSpeed <= FixidityLib.fixed1().unwrap()); require( exponent != validatorScoreParameters.exponent || - !FixidityLib.wrap(adjustmentSpeed).equals(validatorScoreParameters.adjustmentSpeed) + !FixidityLib.wrap(adjustmentSpeed).equals(validatorScoreParameters.adjustmentSpeed) ); validatorScoreParameters = ValidatorScoreParameters( exponent, @@ -226,10 +220,7 @@ contract Validators is * @param duration The time (in seconds) that these requirements persist for. * @return True upon success. */ - function setGroupLockedGoldRequirements( - uint256 value, - uint256 duration - ) + function setGroupLockedGoldRequirements(uint256 value, uint256 duration) public onlyOwner returns (bool) @@ -247,10 +238,7 @@ contract Validators is * @param duration The time (in seconds) that these requirements persist for. * @return True upon success. */ - function setValidatorLockedGoldRequirements( - uint256 value, - uint256 duration - ) + function setValidatorLockedGoldRequirements(uint256 value, uint256 duration) public onlyOwner returns (bool) @@ -274,13 +262,7 @@ contract Validators is * @dev Fails if the account is already a validator or validator group. * @dev Fails if the account does not have sufficient Locked Gold. */ - function registerValidator( - bytes calldata publicKeysData - ) - external - nonReentrant - returns (bool) - { + function registerValidator(bytes calldata publicKeysData) external nonReentrant returns (bool) { require( // secp256k1 public key + BLS public key + BLS proof of possession publicKeysData.length == (64 + 48 + 96) @@ -313,9 +295,7 @@ contract Validators is * @param account The validator whose membership history to return. * @return The group membership history of a validator. */ - function getMembershipHistory( - address account - ) + function getMembershipHistory(address account) external view returns (uint256[] memory, address[] memory, uint256) @@ -376,10 +356,7 @@ contract Validators is ); currentComponent = currentComponent.multiply(validators[account].score); validators[account].score = FixidityLib.wrap( - Math.min( - epochScore.unwrap(), - newComponent.add(currentComponent).unwrap() - ) + Math.min(epochScore.unwrap(), newComponent.add(currentComponent).unwrap()) ); } @@ -390,10 +367,7 @@ contract Validators is * group commission. * @return The total payment paid to the validator and their group. */ - function distributeEpochPaymentsFromSigner( - address signer, - uint256 maxPayment - ) + function distributeEpochPaymentsFromSigner(address signer, uint256 maxPayment) external onlyVm() returns (uint256) @@ -408,10 +382,7 @@ contract Validators is * group commission. * @return The total payment paid to the validator and their group. */ - function _distributeEpochPaymentsFromSigner( - address signer, - uint256 maxPayment - ) + function _distributeEpochPaymentsFromSigner(address signer, uint256 maxPayment) internal returns (uint256) { @@ -506,13 +477,7 @@ contract Validators is * @dev Fails if the account is already a validator or validator group. * @dev Fails if the account does not have sufficient weight. */ - function registerValidatorGroup( - uint256 commission - ) - external - nonReentrant - returns (bool) - { + function registerValidatorGroup(uint256 commission) external nonReentrant returns (bool) { require(commission <= FixidityLib.fixed1().unwrap(), "Commission can't be greater than 100%"); address account = getAccounts().activeValidatorSignerToAccount(msg.sender); require(!isValidator(account) && !isValidatorGroup(account)); @@ -569,11 +534,7 @@ contract Validators is * @dev Fails if `validator` has not set their affiliation to this account. * @dev Fails if the group has > 0 members. */ - function addFirstMember( - address validator, - address lesser, - address greater - ) + function addFirstMember(address validator, address lesser, address greater) external nonReentrant returns (bool) @@ -593,18 +554,13 @@ contract Validators is * @dev Fails if `validator` has not set their affiliation to this account. * @dev Fails if the group has > 0 members. */ - function _addMember( - address group, - address validator, - address lesser, - address greater - ) + function _addMember(address group, address validator, address lesser, address greater) private returns (bool) { require(isValidatorGroup(group) && isValidator(validator)); ValidatorGroup storage _group = groups[group]; - require(_group.members.numElements < maxGroupSize, "group would exceed maximum size"); + require(_group.members.numElements < maxGroupSize, 'group would exceed maximum size'); require(validators[validator].affiliation == group && !_group.members.contains(validator)); uint256 numMembers = _group.members.numElements.add(1); require(meetsAccountLockedGoldRequirements(group)); @@ -627,7 +583,7 @@ contract Validators is */ function removeMember(address validator) external nonReentrant returns (bool) { address account = getAccounts().activeValidatorSignerToAccount(msg.sender); - require(isValidatorGroup(account) && isValidator(validator), "is not group and validator"); + require(isValidatorGroup(account) && isValidator(validator), 'is not group and validator'); return _removeMember(account, validator); } @@ -641,11 +597,7 @@ contract Validators is * @return True upon success. * @dev Fails if `validator` is not a member of the account's validator group. */ - function reorderMember( - address validator, - address lesserMember, - address greaterMember - ) + function reorderMember(address validator, address lesserMember, address greaterMember) external nonReentrant returns (bool) @@ -698,16 +650,10 @@ contract Validators is * @param signer The account that registered the validator or its authorized signing address. * @return The unpacked validator struct. */ - function getValidatorFromSigner( - address signer - ) + function getValidatorFromSigner(address signer) external view - returns ( - bytes memory publicKeysData, - address affiliation, - uint256 score - ) + returns (bytes memory publicKeysData, address affiliation, uint256 score) { address account = getAccounts().validatorSignerToAccount(signer); return getValidator(account); @@ -718,24 +664,14 @@ contract Validators is * @param account The account that registered the validator. * @return The unpacked validator struct. */ - function getValidator( - address account - ) + function getValidator(address account) public view - returns ( - bytes memory publicKeysData, - address affiliation, - uint256 score - ) + returns (bytes memory publicKeysData, address affiliation, uint256 score) { require(isValidator(account)); Validator storage validator = validators[account]; - return ( - validator.publicKeysData, - validator.affiliation, - validator.score.unwrap() - ); + return (validator.publicKeysData, validator.affiliation, validator.score.unwrap()); } /** @@ -743,20 +679,14 @@ contract Validators is * @param account The account that registered the validator group. * @return The unpacked validator group struct. */ - function getValidatorGroup( - address account - ) + function getValidatorGroup(address account) external view returns (address[] memory, uint256, uint256[] memory) { require(isValidatorGroup(account)); ValidatorGroup storage group = groups[account]; - return ( - group.members.getKeys(), - group.commission.unwrap(), - group.sizeHistory - ); + return (group.members.getKeys(), group.commission.unwrap(), group.sizeHistory); } /** @@ -775,10 +705,7 @@ contract Validators is * @param n The number of members to return. * @return The top n group members for a particular group. */ - function getTopGroupValidators( - address account, - uint256 n - ) + function getTopGroupValidators(address account, uint256 n) external view returns (address[] memory) @@ -796,9 +723,7 @@ contract Validators is * @param accounts The addresses of the validator groups. * @return The number of members in the provided validator groups. */ - function getGroupsNumMembers( - address[] calldata accounts - ) + function getGroupsNumMembers(address[] calldata accounts) external view returns (uint256[] memory) @@ -975,7 +900,7 @@ contract Validators is } else if (size < sizeHistory.length) { sizeHistory[size] = now; } else { - require(false, "Unable to update size history"); + require(false, 'Unable to update size history'); } } @@ -1015,10 +940,7 @@ contract Validators is * @param validatorAccount The LockedGold account of the validator. * @return True upon success. */ - function _deaffiliate( - Validator storage validator, - address validatorAccount - ) + function _deaffiliate(Validator storage validator, address validatorAccount) private returns (bool) { From 767afe33075608fafce7f3b67513b018a74a948a Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Sat, 9 Nov 2019 18:44:34 -0800 Subject: [PATCH 112/149] Key rotation e2e test working --- .../src/e2e-tests/governance_tests.ts | 39 ++++--- packages/celotool/src/e2e-tests/utils.ts | 2 +- .../cli/src/commands/account/authorize.ts | 35 +++--- .../src/commands/account/proofOfPossession.ts | 27 +++++ packages/contractkit/src/wrappers/Accounts.ts | 65 +++++++---- .../protocol/contracts/common/Accounts.sol | 43 +++++++- .../contracts/governance/Validators.sol | 43 ++++++-- .../governance/interfaces/IValidators.sol | 2 + .../governance/test/MockValidators.sol | 26 ++--- packages/protocol/lib/web3-utils.ts | 25 +---- packages/protocol/migrations/10_accounts.ts | 18 +++- .../migrations/20_elect_validators.ts | 30 ++---- packages/protocol/test/common/accounts.ts | 71 +++++++++++- .../protocol/test/governance/validators.ts | 102 ++++++++++-------- packages/utils/src/address.ts | 8 +- packages/utils/src/bls.ts | 15 +++ 16 files changed, 376 insertions(+), 175 deletions(-) create mode 100644 packages/cli/src/commands/account/proofOfPossession.ts diff --git a/packages/celotool/src/e2e-tests/governance_tests.ts b/packages/celotool/src/e2e-tests/governance_tests.ts index b889ba0556d..c766797dfd3 100644 --- a/packages/celotool/src/e2e-tests/governance_tests.ts +++ b/packages/celotool/src/e2e-tests/governance_tests.ts @@ -1,4 +1,5 @@ import { ContractKit, newKitFromWeb3 } from '@celo/contractkit' +import { getPublicKeysData } from '@celo/utils/lib/bls' import { fromFixed, toFixed } from '@celo/utils/lib/fixidity' import BigNumber from 'bignumber.js' import { assert } from 'chai' @@ -130,6 +131,7 @@ describe('governance tests', () => { const authorizeValidatorSigner = async ( validatorWeb3: any, signerWeb3: any, + publicKeysData: string, txOptions: any = {} ) => { const validator = (await validatorWeb3.eth.getAccounts())[0] @@ -141,7 +143,13 @@ describe('governance tests', () => { ).contracts.getAccounts()).generateProofOfSigningKeyPossession(validator, signer) const validatorKit = newKitFromWeb3(validatorWeb3) const validatorAccounts = await validatorKit._web3Contracts.getAccounts() - const tx = validatorAccounts.methods.authorizeValidatorSigner(signer, pop.v, pop.r, pop.s) + const tx = validatorAccounts.methods.authorizeValidatorSigner( + signer, + publicKeysData, + pop.v, + pop.r, + pop.s + ) let gas = txOptions.gas if (!gas) { gas = await tx.estimateGas({ ...txOptions }) @@ -177,6 +185,10 @@ describe('governance tests', () => { this.timeout(0) // Disable test timeout await restart() const groupPrivateKey = await getValidatorGroupPrivateKey() + const rotation0PrivateKey = + '0xa42ac9c99f6ab2c96ee6cae1b40d36187f65cd878737f6623cd363fb94ba7087' + const rotation1PrivateKey = + '0x4519cae145fb9499358be484ca60c80d8f5b7f9c13ff82c88ec9e13283e9de1a' const additionalNodes: any[] = [ { name: 'validatorGroup', @@ -194,7 +206,7 @@ describe('governance tests', () => { lightserv: false, port: 30315, wsport: 8557, - privateKey: 'a42ac9c99f6ab2c96ee6cae1b40d36187f65cd878737f6623cd363fb94ba7087', + privateKey: rotation0PrivateKey.slice(2), peers: [await getEnode(8545)], }, { @@ -204,7 +216,7 @@ describe('governance tests', () => { lightserv: false, port: 30317, wsport: 8559, - privateKey: '4519cae145fb9499358be484ca60c80d8f5b7f9c13ff82c88ec9e13283e9de1a', + privateKey: rotation1PrivateKey.slice(2), peers: [await getEnode(8545)], }, ] @@ -226,21 +238,22 @@ describe('governance tests', () => { await sleep(0.1) } while (blockNumber % epoch != 1) - console.log('activating') await activate(validatorAccounts[0]) - console.log('activated') // Prepare for member swapping. const groupWeb3 = new Web3('ws://localhost:8555') const groupKit = newKitFromWeb3(groupWeb3) validators = await groupKit._web3Contracts.getValidators() const membersToSwap = [validatorAccounts[0], validatorAccounts[1]] - console.log('removing') await removeMember(groupWeb3, membersToSwap[1]) // Prepare for key rotation. const validatorWeb3 = new Web3('http://localhost:8549') const authorizedWeb3s = [new Web3('ws://localhost:8557'), new Web3('ws://localhost:8559')] + const authorizedPublicKeysData = [ + getPublicKeysData(rotation0PrivateKey), + getPublicKeysData(rotation1PrivateKey), + ] let index = 0 let errorWhileChangingValidatorSet = '' @@ -255,7 +268,6 @@ describe('governance tests', () => { // 1. Swap validator0 and validator1 so one is a member of the group and the other is not. const memberToRemove = membersToSwap[index] const memberToAdd = membersToSwap[(index + 1) % 2] - console.log('swapping') await removeMember(groupWeb3, memberToRemove) await addMember(groupWeb3, memberToAdd) const newMembers = await getValidatorGroupMembers() @@ -263,8 +275,11 @@ describe('governance tests', () => { assert.notInclude(newMembers, memberToRemove) // 2. Rotate keys for validator 2 by authorizing a new validating key. if (!doneAuthorizing) { - console.log('authorizing') - await authorizeValidatorSigner(validatorWeb3, authorizedWeb3s[index]) + await authorizeValidatorSigner( + validatorWeb3, + authorizedWeb3s[index], + authorizedPublicKeysData[index] + ) } doneAuthorizing = doneAuthorizing || index === 1 const signingKeys = await Promise.all( @@ -329,7 +344,8 @@ describe('governance tests', () => { } }) - it('should always return a validator set equal to the signing keys of the group members at the end of the last epoch', async () => { + it('should always return a validator set equal to the signing keys of the group members at the end of the last epoch', async function(this: any) { + this.timeout(0) for (const blockNumber of blockNumbers) { const lastEpochBlock = getLastEpochBlock(blockNumber) const memberAccounts = await getValidatorGroupMembers(lastEpochBlock) @@ -343,8 +359,6 @@ describe('governance tests', () => { } }) - // This appears to be failing for real, the first time we authorize an address it doesn't - // wind up proposing a block (announce issues?). it('should block propose in a round robin manner', async () => { let roundRobinOrder: string[] = [] for (const blockNumber of blockNumbers) { @@ -357,7 +371,6 @@ describe('governance tests', () => { async (_, i) => (await web3.eth.getBlock(lastEpochBlock + i + 1)).miner ) ) - console.log(roundRobinOrder, validatorSet) assert.sameMembers(validatorSet, roundRobinOrder) } const indexInEpoch = blockNumber - lastEpochBlock - 1 diff --git a/packages/celotool/src/e2e-tests/utils.ts b/packages/celotool/src/e2e-tests/utils.ts index 2d240542573..2a8164a72d4 100644 --- a/packages/celotool/src/e2e-tests/utils.ts +++ b/packages/celotool/src/e2e-tests/utils.ts @@ -265,7 +265,7 @@ export async function startGeth(gethBinaryPath: string, instance: GethInstanceCo '--networkid', NetworkId.toString(), '--verbosity', - '4', + '5', '--consoleoutput=stdout', // Send all logs to stdout '--consoleformat=term', '--nat', diff --git a/packages/cli/src/commands/account/authorize.ts b/packages/cli/src/commands/account/authorize.ts index eaf496f0ffa..c8aab2c56ab 100644 --- a/packages/cli/src/commands/account/authorize.ts +++ b/packages/cli/src/commands/account/authorize.ts @@ -3,48 +3,45 @@ import { BaseCommand } from '../../base' import { displaySendTx } from '../../utils/cli' import { Flags } from '../../utils/command' +// TODO: Support authorizing a validator signer when a validator is registered. export default class Authorize extends BaseCommand { - static description = 'Authorize an attestation, validation or vote signing key' + static description = 'Authorize an attestation, validator or vote signing key' static flags = { ...BaseCommand.flags, from: Flags.address({ required: true }), role: flags.string({ char: 'r', - options: ['vote', 'validation', 'attestation'], + options: ['vote', 'validator', 'attestation'], description: 'Role to delegate', + required: true, }), - to: Flags.address({ required: true }), + pop: flags.string({ + description: 'Proof-of-possession of the signer key', + required: true, + }), + signer: Flags.address({ required: true }), } static args = [] static examples = [ - 'authorize --from 0x5409ED021D9299bf6814279A6A1411A7e866A631 --role vote --to 0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d', + 'authorize --from 0x5409ED021D9299bf6814279A6A1411A7e866A631 --role vote --signer 0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d --pop 0xTODO', ] async run() { const res = this.parse(Authorize) - - if (!res.flags.role) { - this.error(`Specify role with --role`) - return - } - - if (!res.flags.to) { - this.error(`Specify authorized address with --to`) - return - } - this.kit.defaultAccount = res.flags.from const accounts = await this.kit.contracts.getAccounts() + const sig = accounts.parseSignatureOfAddress(res.flags.from, res.flags.signer, res.flags.pop) + let tx: any if (res.flags.role === 'vote') { - tx = await accounts.authorizeVoteSigner(res.flags.from, res.flags.to) - } else if (res.flags.role === 'validation') { - tx = await accounts.authorizeValidationSigner(res.flags.from, res.flags.to) + tx = await accounts.authorizeVoteSigner(res.flags.signer, sig) + } else if (res.flags.role === 'validator') { + tx = await accounts.authorizeValidatorSigner(res.flags.signer, sig) } else if (res.flags.role === 'attestation') { - tx = await accounts.authorizeAttestationSigner(res.flags.from, res.flags.to) + tx = await accounts.authorizeAttestationSigner(res.flags.signer, sig) } else { this.error(`Invalid role provided`) return diff --git a/packages/cli/src/commands/account/proofOfPossession.ts b/packages/cli/src/commands/account/proofOfPossession.ts new file mode 100644 index 00000000000..8fd2b7d5204 --- /dev/null +++ b/packages/cli/src/commands/account/proofOfPossession.ts @@ -0,0 +1,27 @@ +import { BaseCommand } from '../../base' +import { Args, Flags } from '../../utils/command' + +export default class ProofOfPossession extends BaseCommand { + static description = 'Generate proof-of-possession to be used to authorize a signer' + + static flags = { + ...BaseCommand.flags, + account: Flags.address({ required: true }), + } + + static args = [Args.address('signer')] + + static examples = [ + 'proof-of-possession 0x5409ed021d9299bf6814279a6a1411a7e866a631 --signer 0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d', + ] + + async run() { + const res = this.parse(ProofOfPossession) + const accounts = await this.kit.contracts.getAccounts() + const pop = await accounts.generateProofOfSigningKeyPossession( + res.flags.account, + res.args.signer + ) + console.log(pop) + } +} diff --git a/packages/contractkit/src/wrappers/Accounts.ts b/packages/contractkit/src/wrappers/Accounts.ts index c78c846d2e9..02be5501d39 100644 --- a/packages/contractkit/src/wrappers/Accounts.ts +++ b/packages/contractkit/src/wrappers/Accounts.ts @@ -10,12 +10,6 @@ import { toTransactionObject, } from '../wrappers/BaseWrapper' -enum SignerRole { - Attestation, - Validator, - Vote, -} - export interface Signature { r: string s: string @@ -73,7 +67,15 @@ export class AccountsWrapper extends BaseWrapper { signer: Address, proofOfSigningKeyPossession: Signature ): Promise> { - return this.authorizeSigner(SignerRole.Attestation, signer, proofOfSigningKeyPossession) + return toTransactionObject( + this.kit, + this.contract.methods.authorizeAttestationSigner( + signer, + proofOfSigningKeyPossession.v, + proofOfSigningKeyPossession.r, + proofOfSigningKeyPossession.s + ) + ) } /** * Authorizes an address to sign votes on behalf of the account. @@ -85,20 +87,52 @@ export class AccountsWrapper extends BaseWrapper { signer: Address, proofOfSigningKeyPossession: Signature ): Promise> { - return this.authorizeSigner(SignerRole.Vote, signer, proofOfSigningKeyPossession) + return toTransactionObject( + this.kit, + this.contract.methods.authorizeVoteSigner( + signer, + proofOfSigningKeyPossession.v, + proofOfSigningKeyPossession.r, + proofOfSigningKeyPossession.s + ) + ) } /** * Authorizes an address to sign consensus messages on behalf of the account. * @param signer The address of the signing key to authorize. * @param proofOfSigningKeyPossession The account address signed by the signer address. + * @param proofOfBlsKeyPossession Proof-of-possession generated for the corresponding BLS key. Only needed if a validator has been registered. * @return A CeloTransactionObject */ async authorizeValidatorSigner( signer: Address, - proofOfSigningKeyPossession: Signature + proofOfSigningKeyPossession: Signature, + proofOfBlsKeyPossession?: string ): Promise> { - return this.authorizeSigner(SignerRole.Validator, signer, proofOfSigningKeyPossession) + if (proofOfBlsKeyPossession) { + return toTransactionObject( + this.kit, + this.contract.methods.authorizeValidatorSigner( + signer, + proofOfSigningKeyPossession.v, + proofOfSigningKeyPossession.r, + proofOfSigningKeyPossession.s + ) + ) + } else { + // @ts-ignore Typechain doesn't handle function overloading. + return toTransactionObject( + this.kit, + this.contract.methods.authorizeValidatorSigner( + signer, + proofOfBlsKeyPossession, + proofOfSigningKeyPossession.v, + proofOfSigningKeyPossession.r, + proofOfSigningKeyPossession.s + ) + ) + } } async generateProofOfSigningKeyPossession(account: Address, signer: Address) { @@ -164,14 +198,9 @@ export class AccountsWrapper extends BaseWrapper { */ setWalletAddress = proxySend(this.kit, this.contract.methods.setWalletAddress) - private authorizeFns = { - [SignerRole.Attestation]: this.contract.methods.authorizeAttestationSigner, - [SignerRole.Validator]: this.contract.methods.authorizeValidatorSigner, - [SignerRole.Vote]: this.contract.methods.authorizeVoteSigner, - } - - private async authorizeSigner(role: SignerRole, signer: Address, sig: Signature) { - return toTransactionObject(this.kit, this.authorizeFns[role](signer, sig.v, sig.r, sig.s)) + parseSignatureOfAddress(address: Address, signer: string, signature: string) { + const hash = Web3.utils.soliditySha3({ type: 'address', value: address }) + return parseSignature(hash, signature, signer) } private async getParsedSignatureOfAddress(address: Address, signer: string) { diff --git a/packages/protocol/contracts/common/Accounts.sol b/packages/protocol/contracts/common/Accounts.sol index 71e78790e31..80ba720beef 100644 --- a/packages/protocol/contracts/common/Accounts.sol +++ b/packages/protocol/contracts/common/Accounts.sol @@ -1,7 +1,8 @@ pragma solidity ^0.5.3; -import "openzeppelin-solidity/contracts/utils/ReentrancyGuard.sol"; import "openzeppelin-solidity/contracts/math/SafeMath.sol"; +import 'openzeppelin-solidity/contracts/ownership/Ownable.sol'; +import "openzeppelin-solidity/contracts/utils/ReentrancyGuard.sol"; import "./interfaces/IAccounts.sol"; @@ -10,7 +11,7 @@ import "../common/Signatures.sol"; import "../common/UsingRegistry.sol"; import "../common/UsingPrecompiles.sol"; -contract Accounts is IAccounts, ReentrancyGuard, Initializable, UsingRegistry, UsingPrecompiles { +contract Accounts is IAccounts, Ownable, ReentrancyGuard, Initializable, UsingRegistry, UsingPrecompiles { using SafeMath for uint256; @@ -65,6 +66,11 @@ contract Accounts is IAccounts, ReentrancyGuard, Initializable, UsingRegistry, U event AccountWalletAddressSet(address indexed account, address walletAddress); event AccountCreated(address indexed account); + function initialize(address registryAddress) external initializer { + _transferOwnership(msg.sender); + setRegistry(registryAddress); + } + /** * @notice Convenience Setter for the dataEncryptionKey and wallet address for an account * @param name A string to set as the name of the account @@ -181,6 +187,39 @@ contract Accounts is IAccounts, ReentrancyGuard, Initializable, UsingRegistry, U Account storage account = accounts[msg.sender]; authorize(signer, v, r, s); account.signers.validator = signer; + // Registered validators must update their BLS public key data when updating their validator + // signer. + require(!getValidators().isValidator(msg.sender)); + emit ValidatorSignerAuthorized(msg.sender, signer); + } + /** + * @notice Authorizes an address to sign consensus messages on behalf of the account. + * @param signer The address of the signing key to authorize. + * @param publicKeysData Comprised of three tightly-packed elements: + * - publicKey - The public key that the validator is using for consensus, should match + * `signer`. 64 bytes. + * - blsPublicKey - The BLS public key that the validator is using for consensus, should pass + * proof of possession. 48 bytes. + * - blsPoP - The BLS public key proof of possession. 96 bytes. + * @param v The recovery id of the incoming ECDSA signature. + * @param r Output value r of the ECDSA signature. + * @param s Output value s of the ECDSA signature. + * @dev v, r, s constitute `signer`'s signature on `msg.sender`. + */ + function authorizeValidatorSigner( + address signer, + bytes calldata publicKeysData, + uint8 v, + bytes32 r, + bytes32 s + ) + external + nonReentrant + { + Account storage account = accounts[msg.sender]; + authorize(signer, v, r, s); + account.signers.validator = signer; + require(getValidators().updatePublicKeysData(msg.sender, signer, publicKeysData)); emit ValidatorSignerAuthorized(msg.sender, signer); } diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index 778c59a6fb4..9be8a10cc82 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -114,6 +114,7 @@ contract Validators is event ValidatorDeregistered(address indexed validator); event ValidatorAffiliated(address indexed validator, address indexed group); event ValidatorDeaffiliated(address indexed validator, address indexed group); + event ValidatorPublicKeysDataUpdated(address indexed validator, bytes publicKeysData); event ValidatorGroupRegistered(address indexed group, uint256 commission); event ValidatorGroupDeregistered(address indexed group); event ValidatorGroupMemberAdded(address indexed group, address indexed validator); @@ -263,19 +264,13 @@ contract Validators is * @dev Fails if the account does not have sufficient Locked Gold. */ function registerValidator(bytes calldata publicKeysData) external nonReentrant returns (bool) { - require( - // secp256k1 public key + BLS public key + BLS proof of possession - publicKeysData.length == (64 + 48 + 96) - ); - // Use the proof of possession bytes - require(checkProofOfPossession(msg.sender, publicKeysData.slice(64, 48 + 96))); - address account = getAccounts().activeValidatorSignerToAccount(msg.sender); require(!isValidator(account) && !isValidatorGroup(account)); uint256 lockedGoldBalance = getLockedGold().getAccountTotalLockedGold(account); require(lockedGoldBalance >= validatorLockedGoldRequirements.value); Validator storage validator = validators[account]; - validator.publicKeysData = publicKeysData; + address signer = getAccounts().getValidatorSigner(account); + _updatePublicKeysData(validator, signer, publicKeysData); registeredValidators.push(account); updateMembershipHistory(account, address(0)); emit ValidatorRegistered(account, publicKeysData); @@ -469,6 +464,38 @@ contract Validators is return true; } + function updatePublicKeysData(address account, address signer, bytes calldata publicKeysData) external onlyRegisteredContract(ACCOUNTS_REGISTRY_ID) returns (bool) { + require(isValidator(account)); + Validator storage validator = validators[account]; + _updatePublicKeysData(validator, signer, publicKeysData); + emit ValidatorPublicKeysDataUpdated(account, publicKeysData); + return true; + } + + /** + * @notice Updates a validator's public keys data. + * @param validator The validator whose public keys data should be updated. + * @param signer The address used to sign consensus message. Coupled to the BLS key. + * @param publicKeysData Comprised of three tightly-packed elements: + * - publicKey - The public key that the validator is using for consensus, should match + * `signer`. 64 bytes. + * - blsPublicKey - The BLS public key that the validator is using for consensus, should pass + * proof of possession. 48 bytes. + * - blsPoP - The BLS public key proof of possession. 96 bytes. + * @return True upon success. + */ + function _updatePublicKeysData(Validator storage validator, address signer, bytes memory publicKeysData) + private + returns (bool) + { + // secp256k1 public key + BLS public key + BLS proof of possession + require(publicKeysData.length == (64 + 48 + 96)); + // Use the proof of possession bytes + require(checkProofOfPossession(signer, publicKeysData.slice(64, 48 + 96))); + validator.publicKeysData = publicKeysData; + return true; + } + /** * @notice Registers a validator group with no member validators. * @param commission Fixidity representation of the commission this group receives on epoch diff --git a/packages/protocol/contracts/governance/interfaces/IValidators.sol b/packages/protocol/contracts/governance/interfaces/IValidators.sol index 6650c34686f..4b35c9a778f 100644 --- a/packages/protocol/contracts/governance/interfaces/IValidators.sol +++ b/packages/protocol/contracts/governance/interfaces/IValidators.sol @@ -8,4 +8,6 @@ interface IValidators { function getGroupsNumMembers(address[] calldata) external view returns (uint256[] memory); function getNumRegisteredValidators() external view returns (uint256); function getTopGroupValidators(address, uint256) external view returns (address[] memory); + function isValidator(address) external view returns (bool); + function updatePublicKeysData(address, address, bytes calldata) external returns (bool); } diff --git a/packages/protocol/contracts/governance/test/MockValidators.sol b/packages/protocol/contracts/governance/test/MockValidators.sol index 203466a6ff8..5ebd46186b3 100644 --- a/packages/protocol/contracts/governance/test/MockValidators.sol +++ b/packages/protocol/contracts/governance/test/MockValidators.sol @@ -7,13 +7,19 @@ import "../interfaces/IValidators.sol"; */ contract MockValidators is IValidators { - mapping(address => bool) private _isValidating; - mapping(address => bool) private _isVoting; + mapping(address => bool) public isValidator; mapping(address => uint256) private numGroupMembers; mapping(address => uint256) private lockedGoldRequirements; mapping(address => bool) private doesNotMeetAccountLockedGoldRequirements; mapping(address => address[]) private members; uint256 private numRegisteredValidators; + mapping(address => bytes) public publicKeysData; + + function updatePublicKeysData(address account, address, bytes calldata data) external returns (bool) { + require(isValidator[account]); + publicKeysData[account] = data; + return true; + } function setDoesNotMeetAccountLockedGoldRequirements(address account) external { doesNotMeetAccountLockedGoldRequirements[account] = true; @@ -23,24 +29,12 @@ contract MockValidators is IValidators { return !doesNotMeetAccountLockedGoldRequirements[account]; } - function isValidating(address account) external view returns (bool) { - return _isValidating[account]; - } - - function isVoting(address account) external view returns (bool) { - return _isVoting[account]; - } - function getGroupNumMembers(address group) public view returns (uint256) { return members[group].length; } - function setValidating(address account) external { - _isValidating[account] = true; - } - - function setVoting(address account) external { - _isVoting[account] = true; + function setValidator(address account) external { + isValidator[account] = true; } function setNumRegisteredValidators(uint256 value) external { diff --git a/packages/protocol/lib/web3-utils.ts b/packages/protocol/lib/web3-utils.ts index d5b96f21e8e..b7dc2624564 100644 --- a/packages/protocol/lib/web3-utils.ts +++ b/packages/protocol/lib/web3-utils.ts @@ -3,41 +3,20 @@ import { setAndInitializeImplementation } from '@celo/protocol/lib/proxy-utils' import { CeloContractName } from '@celo/protocol/lib/registry-utils' import { signTransaction } from '@celo/protocol/lib/signing-utils' +import { privateKeyToAddress } from '@celo/utils/lib/address' import { BigNumber } from 'bignumber.js' -import { ec as EC } from 'elliptic' import { EscrowInstance, GoldTokenInstance, MultiSigInstance, OwnableInstance, ProxyContract, ProxyInstance, RegistryInstance, StableTokenInstance } from 'types' import { TransactionObject } from 'web3/eth/types' import Web3 = require('web3') -const ec = new EC('secp256k1') -const cachedWeb3 = new Web3() - -export function add0x(str: string) { - return '0x' + str -} - -export function generatePublicKeyFromPrivateKey(privateKey: string) { - const ecPrivateKey = ec.keyFromPrivate(Buffer.from(privateKey, 'hex')) - const ecPublicKey: string = ecPrivateKey.getPublic('hex') - return ecPublicKey.slice(2) -} - -export function generateAccountAddressFromPrivateKey(privateKey: string) { - if (!privateKey.startsWith('0x')) { - privateKey = '0x' + privateKey - } - // @ts-ignore-next-line - return cachedWeb3.eth.accounts.privateKeyToAccount(privateKey).address -} - export async function sendTransactionWithPrivateKey( web3: Web3, tx: TransactionObject, privateKey: string, txArgs: any ) { - const address = generateAccountAddressFromPrivateKey(privateKey.slice(2)) + const address = privateKeyToAddress(privateKey) const encodedTxData = tx.encodeABI() const estimatedGas = await tx.estimateGas({ ...txArgs, diff --git a/packages/protocol/migrations/10_accounts.ts b/packages/protocol/migrations/10_accounts.ts index 4a9542cdc76..592087734c8 100644 --- a/packages/protocol/migrations/10_accounts.ts +++ b/packages/protocol/migrations/10_accounts.ts @@ -1,9 +1,21 @@ import { CeloContractName } from '@celo/protocol/lib/registry-utils' -import { deploymentForCoreContract } from '@celo/protocol/lib/web3-utils' -import { AccountsInstance } from 'types' +import { + deploymentForCoreContract, + getDeployedProxiedContract, +} from '@celo/protocol/lib/web3-utils' +import { AccountsInstance, RegistryInstance } from 'types' + +const initializeArgs = async (): Promise<[string]> => { + const registry: RegistryInstance = await getDeployedProxiedContract( + 'Registry', + artifacts + ) + return [registry.address] +} module.exports = deploymentForCoreContract( web3, artifacts, - CeloContractName.Accounts + CeloContractName.Accounts, + initializeArgs ) diff --git a/packages/protocol/migrations/20_elect_validators.ts b/packages/protocol/migrations/20_elect_validators.ts index d994167ed8a..6a1c3d1d41b 100644 --- a/packages/protocol/migrations/20_elect_validators.ts +++ b/packages/protocol/migrations/20_elect_validators.ts @@ -1,17 +1,14 @@ /* tslint:disable:no-console */ import { NULL_ADDRESS } from '@celo/protocol/lib/test-utils' import { - add0x, - generateAccountAddressFromPrivateKey, - generatePublicKeyFromPrivateKey, getDeployedProxiedContract, sendTransactionWithPrivateKey, } from '@celo/protocol/lib/web3-utils' import { config } from '@celo/protocol/migrationsConfig' -import { blsPrivateKeyToProcessedPrivateKey } from '@celo/utils/lib/bls' +import { privateKeyToAddress, privateKeyToPublicKey } from '@celo/utils/lib/address' +import { getPublicKeysData } from '@celo/utils/lib/bls' import { toFixed } from '@celo/utils/lib/fixidity' import { BigNumber } from 'bignumber.js' -import * as bls12377js from 'bls12377js' import { AccountsInstance, ElectionInstance, LockedGoldInstance, ValidatorsInstance } from 'types' const Web3 = require('web3') @@ -66,7 +63,7 @@ async function registerValidatorGroup( ) await web3.eth.sendTransaction({ - from: generateAccountAddressFromPrivateKey(privateKey.slice(0)), + from: privateKeyToAddress(privateKey), to: account.address, value: lockedGoldValue.times(1.01).toFixed(), // Add a premium to cover tx fees }) @@ -102,20 +99,7 @@ async function registerValidator( index: number, networkName: string ) { - const validatorPrivateKeyHexStripped = validatorPrivateKey.slice(2) - const address = generateAccountAddressFromPrivateKey(validatorPrivateKey) - const publicKey = generatePublicKeyFromPrivateKey(validatorPrivateKeyHexStripped) - const blsValidatorPrivateKeyBytes = blsPrivateKeyToProcessedPrivateKey( - validatorPrivateKeyHexStripped - ) - const blsPublicKey = bls12377js.BLS.privateToPublicBytes(blsValidatorPrivateKeyBytes).toString( - 'hex' - ) - const blsPoP = bls12377js.BLS.signPoP( - blsValidatorPrivateKeyBytes, - Buffer.from(address.slice(2), 'hex') - ).toString('hex') - const publicKeysData = publicKey + blsPublicKey + blsPoP + const publicKeysData = getPublicKeysData(validatorPrivateKey) console.log('locking gold for validator registration') await lockGold( @@ -133,7 +117,7 @@ async function registerValidator( }) // @ts-ignore - const registerTx = validators.contract.methods.registerValidator(add0x(publicKeysData)) + const registerTx = validators.contract.methods.registerValidator(publicKeysData) await sendTransactionWithPrivateKey(web3, registerTx, validatorPrivateKey, { to: validators.address, @@ -149,7 +133,7 @@ async function registerValidator( // @ts-ignore const registerDataEncryptionKeyTx = accounts.contract.methods.setAccountDataEncryptionKey( - add0x(publicKey) + privateKeyToPublicKey(validatorPrivateKey) ) await sendTransactionWithPrivateKey(web3, registerDataEncryptionKeyTx, validatorPrivateKey, { @@ -219,7 +203,7 @@ module.exports = async (_deployer: any, networkName: string) => { console.info(' Adding Validators to Validator Group ...') for (let i = 0; i < valKeys.length; i++) { const key = valKeys[i] - const address = generateAccountAddressFromPrivateKey(key.slice(2)) + const address = privateKeyToAddress(key) if (i === 0) { // @ts-ignore const addTx = validators.contract.methods.addFirstMember(address, NULL_ADDRESS, NULL_ADDRESS) diff --git a/packages/protocol/test/common/accounts.ts b/packages/protocol/test/common/accounts.ts index 8221f9ef67f..1b283ff63d4 100644 --- a/packages/protocol/test/common/accounts.ts +++ b/packages/protocol/test/common/accounts.ts @@ -1,13 +1,22 @@ import { upperFirst } from 'lodash' -import { AccountsInstance } from 'types' -import { getParsedSignatureOfAddress } from '../../lib/signing-utils' +import { CeloContractName } from '@celo/protocol/lib/registry-utils' +import { getParsedSignatureOfAddress } from '@celo/protocol/lib/signing-utils' import { assertLogMatches, assertLogMatches2, assertRevert, NULL_ADDRESS, -} from '../../lib/test-utils' -const Accounts: Truffle.Contract = artifacts.require('Accounts') +} from '@celo/protocol/lib/test-utils' +import { + AccountsContract, + AccountsInstance, + MockValidatorsContract, + MockValidatorsInstance, + RegistryContract, +} from 'types' +const Accounts: AccountsContract = artifacts.require('Accounts') +const Registry: RegistryContract = artifacts.require('Registry') +const MockValidators: MockValidatorsContract = artifacts.require('MockValidators') const authorizationTests: any = {} const authorizationTestDescriptions = { voting: { @@ -26,11 +35,13 @@ const authorizationTestDescriptions = { contract('Accounts', (accounts: string[]) => { let accountsInstance: AccountsInstance + let mockValidators: MockValidatorsInstance const account = accounts[0] const caller = accounts[0] const name = 'Account' const metadataURL = 'https://www.celo.org' + const publicKeysData = '0x02f2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e01611111111' const dataEncryptionKey = '0x02f2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e01611111111' const longDataEncryptionKey = '0x04f2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e01611111111' + @@ -38,6 +49,10 @@ contract('Accounts', (accounts: string[]) => { beforeEach(async () => { accountsInstance = await Accounts.new({ from: account }) + mockValidators = await MockValidators.new() + const registry = await Registry.new() + await registry.setAddressFor(CeloContractName.Validators, mockValidators.address) + await accountsInstance.initialize(registry.address) authorizationTests.voting = { fn: accountsInstance.authorizeVoteSigner, @@ -318,6 +333,54 @@ contract('Accounts', (accounts: string[]) => { }) }) + // authorizeValidatoSigner deviates slightly from the other authorize*Signer functions. + // This block tests those deviations. + describe('#authorizeValidatorSigner', async () => { + const authorized = accounts[1] + let sig + + beforeEach(async () => { + await accountsInstance.createAccount() + sig = await getParsedSignatureOfAddress(web3, account, authorized) + }) + + describe('when a validator has not been registered', () => { + it('should succeed when no public keys data is passed', async () => { + await accountsInstance.authorizeValidatorSigner(authorized, sig.v, sig.r, sig.s) + }) + + it('should revert when public keys data is passed', async () => { + await assertRevert( + accountsInstance.authorizeValidatorSigner(authorized, publicKeysData, sig.v, sig.r, sig.s) + ) + }) + }) + + describe('when a validator has been registered', () => { + beforeEach(async () => { + await mockValidators.setValidator(account) + }) + + it('should revert when no public keys data is passed', async () => { + await assertRevert( + accountsInstance.authorizeValidatorSigner(authorized, sig.v, sig.r, sig.s) + ) + }) + + it('should succeed when public keys data is passed', async () => { + await accountsInstance.authorizeValidatorSigner( + authorized, + publicKeysData, + sig.v, + sig.r, + sig.s + ) + // @ts-ignore Typechain can't handle 'bytes' type. + assert.equal(await mockValidators.publicKeysData(account), publicKeysData) + }) + }) + }) + Object.keys(authorizationTestDescriptions).forEach((key) => { describe('authorization tests:', () => { let authorizationTest: any diff --git a/packages/protocol/test/governance/validators.ts b/packages/protocol/test/governance/validators.ts index 2b8313a4a8c..4667dea9819 100644 --- a/packages/protocol/test/governance/validators.ts +++ b/packages/protocol/test/governance/validators.ts @@ -1,4 +1,5 @@ import { CeloContractName } from '@celo/protocol/lib/registry-utils' +import { getParsedSignatureOfAddress } from '@celo/protocol/lib/signing-utils' import { assertContainSubset, assertEqualBN, @@ -25,8 +26,8 @@ import { ValidatorsTestContract, ValidatorsTestInstance, } from 'types' -const Accounts: AccountsContract = artifacts.require('Accounts') +const Accounts: AccountsContract = artifacts.require('Accounts') const Validators: ValidatorsTestContract = artifacts.require('ValidatorsTest') const MockElection: MockElectionContract = artifacts.require('MockElection') const MockLockedGold: MockLockedGoldContract = artifacts.require('MockLockedGold') @@ -100,14 +101,19 @@ contract('Validators', (accounts: string[]) => { const commission = toFixed(1 / 100) beforeEach(async () => { accountsInstance = await Accounts.new() - await Promise.all(accounts.map((account) => accountsInstance.createAccount({ from: account }))) + // Do not register an account for the last address so it can be used as an authorized validator signer. + await Promise.all( + accounts.slice(0, -1).map((account) => accountsInstance.createAccount({ from: account })) + ) mockElection = await MockElection.new() mockLockedGold = await MockLockedGold.new() registry = await Registry.new() validators = await Validators.new() + await accountsInstance.initialize(registry.address) await registry.setAddressFor(CeloContractName.Accounts, accountsInstance.address) await registry.setAddressFor(CeloContractName.Election, mockElection.address) await registry.setAddressFor(CeloContractName.LockedGold, mockLockedGold.address) + await registry.setAddressFor(CeloContractName.Validators, validators.address) await validators.initialize( registry.address, groupLockedGoldRequirements.value, @@ -519,59 +525,67 @@ contract('Validators', (accounts: string[]) => { const validator = accounts[0] let resp: any describe('when the account is not a registered validator', () => { - let validatorRegistrationEpochNumber: number beforeEach(async () => { await mockLockedGold.setAccountTotalLockedGold( validator, validatorLockedGoldRequirements.value ) - resp = await validators.registerValidator( - // @ts-ignore bytes type - publicKeysData - ) - const blockNumber = (await web3.eth.getBlock('latest')).number - validatorRegistrationEpochNumber = Math.floor(blockNumber / EPOCH) }) - it('should mark the account as a validator', async () => { - assert.isTrue(await validators.isValidator(validator)) - }) + describe('when the account has authorized a validator signer', () => { + let validatorRegistrationEpochNumber: number + beforeEach(async () => { + const signer = accounts[9] + const sig = await getParsedSignatureOfAddress(web3, validator, signer) + await accountsInstance.authorizeValidatorSigner(signer, sig.v, sig.r, sig.s) + resp = await validators.registerValidator( + // @ts-ignore bytes type + publicKeysData + ) + const blockNumber = (await web3.eth.getBlock('latest')).number + validatorRegistrationEpochNumber = Math.floor(blockNumber / EPOCH) + }) - it('should add the account to the list of validators', async () => { - assert.deepEqual(await validators.getRegisteredValidators(), [validator]) - }) + it('should mark the account as a validator', async () => { + assert.isTrue(await validators.isValidator(validator)) + }) - it('should set the validator public key', async () => { - const parsedValidator = parseValidatorParams(await validators.getValidator(validator)) - assert.equal(parsedValidator.publicKeysData, publicKeysData) - }) + it('should add the account to the list of validators', async () => { + assert.deepEqual(await validators.getRegisteredValidators(), [validator]) + }) - it('should set account locked gold requirements', async () => { - const requirement = await validators.getAccountLockedGoldRequirement(validator) - assertEqualBN(requirement, validatorLockedGoldRequirements.value) - }) + it('should set the validator public key', async () => { + const parsedValidator = parseValidatorParams(await validators.getValidator(validator)) + assert.equal(parsedValidator.publicKeysData, publicKeysData) + }) - it('should set the validator membership history', async () => { - const membershipHistory = await validators.getMembershipHistory(validator) - assertEqualBNArray(membershipHistory[0], [validatorRegistrationEpochNumber]) - assert.deepEqual(membershipHistory[1], [NULL_ADDRESS]) - }) + it('should set account locked gold requirements', async () => { + const requirement = await validators.getAccountLockedGoldRequirement(validator) + assertEqualBN(requirement, validatorLockedGoldRequirements.value) + }) - it('should set the validator membership history', async () => { - const membershipHistory = await validators.getMembershipHistory(validator) - assertEqualBNArray(membershipHistory[0], [validatorRegistrationEpochNumber]) - assert.deepEqual(membershipHistory[1], [NULL_ADDRESS]) - }) + it('should set the validator membership history', async () => { + const membershipHistory = await validators.getMembershipHistory(validator) + assertEqualBNArray(membershipHistory[0], [validatorRegistrationEpochNumber]) + assert.deepEqual(membershipHistory[1], [NULL_ADDRESS]) + }) - it('should emit the ValidatorRegistered event', async () => { - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'ValidatorRegistered', - args: { - validator, - publicKeysData, - }, + it('should set the validator membership history', async () => { + const membershipHistory = await validators.getMembershipHistory(validator) + assertEqualBNArray(membershipHistory[0], [validatorRegistrationEpochNumber]) + assert.deepEqual(membershipHistory[1], [NULL_ADDRESS]) + }) + + it('should emit the ValidatorRegistered event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorRegistered', + args: { + validator, + publicKeysData, + }, + }) }) }) }) @@ -1553,7 +1567,7 @@ contract('Validators', (accounts: string[]) => { describe('#updateMembershipHistory', () => { const validator = accounts[0] - const groups = accounts.slice(1) + const groups = accounts.slice(1, -1) let validatorRegistrationEpochNumber: number beforeEach(async () => { await registerValidator(validator) @@ -1635,7 +1649,7 @@ contract('Validators', (accounts: string[]) => { describe('#getMembershipInLastEpoch', () => { const validator = accounts[0] - const groups = accounts.slice(1) + const groups = accounts.slice(1, -1) beforeEach(async () => { await registerValidator(validator) for (const group of groups) { diff --git a/packages/utils/src/address.ts b/packages/utils/src/address.ts index cf739060de3..9f7808601dc 100644 --- a/packages/utils/src/address.ts +++ b/packages/utils/src/address.ts @@ -1,4 +1,4 @@ -import { privateToAddress, toChecksumAddress } from 'ethereumjs-util' +import { privateToAddress, privateToPublic, toChecksumAddress } from 'ethereumjs-util' export type Address = string @@ -11,3 +11,9 @@ export const privateKeyToAddress = (privateKey: string) => { '0x' + privateToAddress(Buffer.from(privateKey.slice(2), 'hex')).toString('hex') ) } + +export const privateKeyToPublicKey = (privateKey: string) => { + return toChecksumAddress( + '0x' + privateToPublic(Buffer.from(privateKey.slice(2), 'hex')).toString('hex') + ) +} diff --git a/packages/utils/src/bls.ts b/packages/utils/src/bls.ts index b6e3889ee36..5dc1c78d7a0 100644 --- a/packages/utils/src/bls.ts +++ b/packages/utils/src/bls.ts @@ -2,6 +2,8 @@ const keccak256 = require('keccak256') const BigInteger = require('bigi') const reverse = require('buffer-reverse') +import * as bls12377js from 'bls12377js' +import { privateKeyToAddress, privateKeyToPublicKey } from './address' const n = BigInteger.fromHex('12ab655e9a2ca55660b44d1e5c37b00159aa76fed00000010a11800000000001', 16) @@ -35,3 +37,16 @@ export const blsPrivateKeyToProcessedPrivateKey = (privateKeyHex: string) => { throw new Error("couldn't derive BLS key from ECDSA key") } +export const getPublicKeysData = (privateKeyHex: string) => { + const address = privateKeyToAddress(privateKeyHex) + const publicKey = privateKeyToPublicKey(privateKeyHex) + const blsValidatorPrivateKeyBytes = blsPrivateKeyToProcessedPrivateKey(privateKeyHex.slice(2)) + const blsPublicKey = bls12377js.BLS.privateToPublicBytes(blsValidatorPrivateKeyBytes).toString( + 'hex' + ) + const blsPoP = bls12377js.BLS.signPoP( + blsValidatorPrivateKeyBytes, + Buffer.from(address.slice(2), 'hex') + ).toString('hex') + return publicKey + blsPublicKey + blsPoP +} From 1f0079b486ea5d8f1e3a5dda1cbcf817f32a993f Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Sat, 9 Nov 2019 18:52:04 -0800 Subject: [PATCH 113/149] remove \.only --- packages/celotool/src/e2e-tests/governance_tests.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/celotool/src/e2e-tests/governance_tests.ts b/packages/celotool/src/e2e-tests/governance_tests.ts index c766797dfd3..bc0582496ae 100644 --- a/packages/celotool/src/e2e-tests/governance_tests.ts +++ b/packages/celotool/src/e2e-tests/governance_tests.ts @@ -177,7 +177,7 @@ describe('governance tests', () => { } } - describe.only('when the validator set is changing', () => { + describe('when the validator set is changing', () => { let epoch: number const blockNumbers: number[] = [] let validatorAccounts: string[] From 47d2c654fe88bde9ed01f3d802d49f20e69fe1ad Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Sat, 9 Nov 2019 19:30:58 -0800 Subject: [PATCH 114/149] Authorize and pop cli commands seem to work --- .../src/e2e-tests/governance_tests.ts | 10 ++--- packages/celotool/src/e2e-tests/utils.ts | 2 +- .../cli/src/commands/account/authorize.ts | 4 +- .../src/commands/account/proofOfPossession.ts | 11 ++--- packages/cli/src/commands/account/register.ts | 11 +++-- .../cli/src/commands/validator/publicKey.ts | 43 ------------------- packages/cli/src/utils/checks.ts | 2 +- packages/cli/src/utils/helpers.ts | 1 + packages/contractkit/src/wrappers/Accounts.ts | 12 +++--- .../contractkit/src/wrappers/Validators.ts | 3 +- .../contracts/governance/Validators.sol | 28 ++---------- 11 files changed, 35 insertions(+), 92 deletions(-) delete mode 100644 packages/cli/src/commands/validator/publicKey.ts diff --git a/packages/celotool/src/e2e-tests/governance_tests.ts b/packages/celotool/src/e2e-tests/governance_tests.ts index bc0582496ae..37fb30d4d7c 100644 --- a/packages/celotool/src/e2e-tests/governance_tests.ts +++ b/packages/celotool/src/e2e-tests/governance_tests.ts @@ -319,7 +319,7 @@ describe('governance tests', () => { return validatorSet } - const getValidatorSetAccountKeysAtBlock = async (blockNumber: number) => { + const getValidatorSetAccountsAtBlock = async (blockNumber: number) => { const signingKeys = await getValidatorSetSignersAtBlock(blockNumber) return await Promise.all( signingKeys.map((address: string) => @@ -353,13 +353,13 @@ describe('governance tests', () => { memberAccounts.map((v: string) => getValidatorSigner(v, lastEpochBlock)) ) const validatorSetSigners = await getValidatorSetSignersAtBlock(blockNumber) - const validatorSetAccounts = await getValidatorSetAccountKeysAtBlock(blockNumber) + const validatorSetAccounts = await getValidatorSetAccountsAtBlock(blockNumber) assert.sameMembers(memberSigners, validatorSetSigners) assert.sameMembers(memberAccounts, validatorSetAccounts) } }) - it('should block propose in a round robin manner', async () => { + it('should block propose in a round robin fashion', async () => { let roundRobinOrder: string[] = [] for (const blockNumber of blockNumbers) { const lastEpochBlock = getLastEpochBlock(blockNumber) @@ -417,7 +417,7 @@ describe('governance tests', () => { let expectUnchangedScores: string[] let expectChangedScores: string[] if (isLastBlockOfEpoch(blockNumber, epoch)) { - expectChangedScores = await getValidatorSetAccountKeysAtBlock(blockNumber) + expectChangedScores = await getValidatorSetAccountsAtBlock(blockNumber) expectUnchangedScores = validatorAccounts.filter((x) => !expectChangedScores.includes(x)) } else { expectUnchangedScores = validatorAccounts @@ -480,7 +480,7 @@ describe('governance tests', () => { let expectUnchangedBalances: string[] let expectChangedBalances: string[] if (isLastBlockOfEpoch(blockNumber, epoch)) { - expectChangedBalances = await getValidatorSetAccountKeysAtBlock(blockNumber) + expectChangedBalances = await getValidatorSetAccountsAtBlock(blockNumber) expectUnchangedBalances = validatorAccounts.filter( (x) => !expectChangedBalances.includes(x) ) diff --git a/packages/celotool/src/e2e-tests/utils.ts b/packages/celotool/src/e2e-tests/utils.ts index 2a8164a72d4..2d240542573 100644 --- a/packages/celotool/src/e2e-tests/utils.ts +++ b/packages/celotool/src/e2e-tests/utils.ts @@ -265,7 +265,7 @@ export async function startGeth(gethBinaryPath: string, instance: GethInstanceCo '--networkid', NetworkId.toString(), '--verbosity', - '5', + '4', '--consoleoutput=stdout', // Send all logs to stdout '--consoleformat=term', '--nat', diff --git a/packages/cli/src/commands/account/authorize.ts b/packages/cli/src/commands/account/authorize.ts index b0b73a6be61..be995562e6e 100644 --- a/packages/cli/src/commands/account/authorize.ts +++ b/packages/cli/src/commands/account/authorize.ts @@ -6,7 +6,7 @@ import { Flags } from '../../utils/command' // TODO: Support authorizing a validator signer when a validator is registered. export default class Authorize extends BaseCommand { - static description = 'Authorize an attestation, validator or vote signing key' + static description = 'Authorize an attestation, validator, or vote signer' static flags = { ...BaseCommand.flags, @@ -27,7 +27,7 @@ export default class Authorize extends BaseCommand { static args = [] static examples = [ - 'authorize --from 0x5409ED021D9299bf6814279A6A1411A7e866A631 --role vote --signer 0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d --pop 0xTODO', + 'authorize --from 0x5409ED021D9299bf6814279A6A1411A7e866A631 --role vote --signer 0x6ecbe1db9ef729cbe972c83fb886247691fb6beb --pop 0x1b9fca4bbb5bfb1dbe69ef1cddbd9b4202dcb6b134c5170611e1e36ecfa468d7b46c85328d504934fce6c2a1571603a50ae224d2b32685e84d4d1a1eebad8452eb', ] async run() { diff --git a/packages/cli/src/commands/account/proofOfPossession.ts b/packages/cli/src/commands/account/proofOfPossession.ts index 8fd2b7d5204..d0495f048bd 100644 --- a/packages/cli/src/commands/account/proofOfPossession.ts +++ b/packages/cli/src/commands/account/proofOfPossession.ts @@ -1,18 +1,19 @@ import { BaseCommand } from '../../base' +import { printValueMap } from '../../utils/cli' import { Args, Flags } from '../../utils/command' +import { serializeSignature } from '@celo/utils/lib/signatureUtils' export default class ProofOfPossession extends BaseCommand { static description = 'Generate proof-of-possession to be used to authorize a signer' static flags = { ...BaseCommand.flags, + signer: Flags.address({ required: true }), account: Flags.address({ required: true }), } - static args = [Args.address('signer')] - static examples = [ - 'proof-of-possession 0x5409ed021d9299bf6814279a6a1411a7e866a631 --signer 0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d', + 'proof-of-possession --account 0x5409ed021d9299bf6814279a6a1411a7e866a631 --signer 0x6ecbe1db9ef729cbe972c83fb886247691fb6beb', ] async run() { @@ -20,8 +21,8 @@ export default class ProofOfPossession extends BaseCommand { const accounts = await this.kit.contracts.getAccounts() const pop = await accounts.generateProofOfSigningKeyPossession( res.flags.account, - res.args.signer + res.flags.signer ) - console.log(pop) + printValueMap({ signature: serializeSignature(pop) }) } } diff --git a/packages/cli/src/commands/account/register.ts b/packages/cli/src/commands/account/register.ts index ea121e9cdfc..9dd445c5838 100644 --- a/packages/cli/src/commands/account/register.ts +++ b/packages/cli/src/commands/account/register.ts @@ -9,13 +9,16 @@ export default class Register extends BaseCommand { static flags = { ...BaseCommand.flags, - name: flags.string({ required: true }), + name: flags.string(), from: Flags.address({ required: true }), } static args = [] - static examples = ['register'] + static examples = [ + 'register --from 0x5409ed021d9299bf6814279a6a1411a7e866a631', + 'register --from 0x5409ed021d9299bf6814279a6a1411a7e866a631 --name test-account', + ] async run() { const res = this.parse(Register) @@ -26,6 +29,8 @@ export default class Register extends BaseCommand { .isNotAccount(res.flags.from) .runChecks() await displaySendTx('register', accounts.createAccount()) - await displaySendTx('setName', accounts.setName(res.flags.name)) + if (res.flags.name) { + await displaySendTx('setName', accounts.setName(res.flags.name)) + } } } diff --git a/packages/cli/src/commands/validator/publicKey.ts b/packages/cli/src/commands/validator/publicKey.ts deleted file mode 100644 index cfe11ab125a..00000000000 --- a/packages/cli/src/commands/validator/publicKey.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { BaseCommand } from '../../base' -import { newCheckBuilder } from '../../utils/checks' -import { displaySendTx } from '../../utils/cli' -import { Flags } from '../../utils/command' -import { getPubKeyFromAddrAndWeb3 } from '../../utils/helpers' - -export default class ValidatorPublicKey extends BaseCommand { - static description = 'Manage BLS public key data for a validator' - - static flags = { - ...BaseCommand.flags, - from: Flags.address({ required: true, description: "Validator's address" }), - publicKey: Flags.publicKey({ required: true }), - } - - static examples = [ - 'publickey --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --publicKey 0xc52f3fab06e22a54915a8765c4f6826090cfac5e40282b43844bf1c0df83aaa632e55b67869758f2291d1aabe0ebecc7cbf4236aaa45e3e0cfbf997eda082ae19d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d7405011220a66a6257562d0c26dabf64485a1d96bad27bb1c0fd6080a75b0ec9f75b50298a2a8e04b02b2688c8104fca61fb00', - ] - async run() { - const res = this.parse(ValidatorPublicKey) - this.kit.defaultAccount = res.flags.from - const validators = await this.kit.contracts.getValidators() - const accounts = await this.kit.contracts.getAccounts() - - await newCheckBuilder(this, res.flags.from) - .isSignerOrAccount() - .canSignValidatorTxs() - .signerAccountIsValidator() - .runChecks() - - await displaySendTx( - 'updatePublicKeysData', - validators.updatePublicKeysData(res.flags.publicKey as any) - ) - - // register encryption key on accounts contract - // TODO: Use a different key data encryption - const pubKey = await getPubKeyFromAddrAndWeb3(res.flags.from, this.web3) - // TODO fix typing - const setKeyTx = accounts.setAccountDataEncryptionKey(pubKey as any) - await displaySendTx('Set encryption key', setKeyTx) - } -} diff --git a/packages/cli/src/utils/checks.ts b/packages/cli/src/utils/checks.ts index 0e79ab09874..d55e7898706 100644 --- a/packages/cli/src/utils/checks.ts +++ b/packages/cli/src/utils/checks.ts @@ -73,7 +73,7 @@ class CheckBuilder { 'Signer can sign Validator Txs', this.withAccounts((lg) => lg - .activeValidationSignerToAccount(this.signer!) + .activeValidatorSignerToAccount(this.signer!) .then(() => true) .catch(() => false) ) diff --git a/packages/cli/src/utils/helpers.ts b/packages/cli/src/utils/helpers.ts index d95c1354c57..f222ff3d651 100644 --- a/packages/cli/src/utils/helpers.ts +++ b/packages/cli/src/utils/helpers.ts @@ -25,6 +25,7 @@ export async function getPubKeyFromAddrAndWeb3(addr: string, web3: Web3) { } export async function nodeIsSynced(web3: Web3): Promise { + return true if (process.env.NO_SYNCCHECK) { return true } diff --git a/packages/contractkit/src/wrappers/Accounts.ts b/packages/contractkit/src/wrappers/Accounts.ts index 9562399410c..8d04970fc66 100644 --- a/packages/contractkit/src/wrappers/Accounts.ts +++ b/packages/contractkit/src/wrappers/Accounts.ts @@ -42,7 +42,7 @@ export class AccountsWrapper extends BaseWrapper { this.contract.methods.getVoteSigner ) /** - * Returns the validator signere for the specified account. + * Returns the validator signer for the specified account. * @param account The address of the account. * @return The address with which the account can register a validator or group. */ @@ -55,8 +55,8 @@ export class AccountsWrapper extends BaseWrapper { * @param signer Address that is authorized to sign the tx as validator * @return The Account address */ - activeValidationSignerToAccount: (signer: Address) => Promise
= proxyCall( - this.contract.methods.activeValidationSignerToAccount + activeValidatorSignerToAccount: (signer: Address) => Promise
= proxyCall( + this.contract.methods.activeValidatorSignerToAccount ) /** @@ -71,7 +71,9 @@ export class AccountsWrapper extends BaseWrapper { * @param address The address of the account * @return Returns `true` if account exists. Returns `false` otherwise. */ - isSigner: (address: string) => Promise = proxyCall(this.contract.methods.isAuthorized) + isSigner: (address: string) => Promise = proxyCall( + this.contract.methods.isAuthorizedSigner + ) /** * Authorize an attestation signing key on behalf of this account to another address. @@ -137,7 +139,6 @@ export class AccountsWrapper extends BaseWrapper { ) ) } else { - // @ts-ignore Typechain doesn't handle function overloading. return toTransactionObject( this.kit, this.contract.methods.authorizeValidatorSigner( @@ -145,6 +146,7 @@ export class AccountsWrapper extends BaseWrapper { proofOfBlsKeyPossession, proofOfSigningKeyPossession.v, proofOfSigningKeyPossession.r, + // @ts-ignore Typechain doesn't handle function overloading. proofOfSigningKeyPossession.s ) ) diff --git a/packages/contractkit/src/wrappers/Validators.ts b/packages/contractkit/src/wrappers/Validators.ts index 548f877d823..d74aa62c976 100644 --- a/packages/contractkit/src/wrappers/Validators.ts +++ b/packages/contractkit/src/wrappers/Validators.ts @@ -57,7 +57,6 @@ export class ValidatorsWrapper extends BaseWrapper { this.contract.methods.updateCommission(toFixed(commission).toFixed()) ) } - updatePublicKeysData = proxySend(this.kit, this.contract.methods.updatePublicKeysData) /** * Returns the Locked Gold requirements for validators. * @returns The Locked Gold requirements for validators. @@ -100,7 +99,7 @@ export class ValidatorsWrapper extends BaseWrapper { async signerToAccount(signerAddress: Address) { const accounts = await this.kit.contracts.getAccounts() - return accounts.activeValidationSignerToAccount(signerAddress) + return accounts.activeValidatorSignerToAccount(signerAddress) } /** diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index 5153d61c0ee..bab56f0d74d 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -265,22 +265,13 @@ contract Validators is * @dev Fails if the account does not have sufficient Locked Gold. */ function registerValidator(bytes calldata publicKeysData) external nonReentrant returns (bool) { -<<<<<<< HEAD address account = getAccounts().activeValidatorSignerToAccount(msg.sender); -======= - address account = getAccounts().activeValidationSignerToAccount(msg.sender); ->>>>>>> master require(!isValidator(account) && !isValidatorGroup(account)); uint256 lockedGoldBalance = getLockedGold().getAccountTotalLockedGold(account); require(lockedGoldBalance >= validatorLockedGoldRequirements.value); Validator storage validator = validators[account]; -<<<<<<< HEAD address signer = getAccounts().getValidatorSigner(account); _updatePublicKeysData(validator, signer, publicKeysData); -======= - _updatePublicKeysData(validator, publicKeysData); - validator.publicKeysData = publicKeysData; ->>>>>>> master registeredValidators.push(account); updateMembershipHistory(account, address(0)); emit ValidatorRegistered(account, publicKeysData); @@ -399,15 +390,9 @@ contract Validators is // Both the validator and the group must maintain the minimum locked gold balance in order to // receive epoch payments. if (meetsAccountLockedGoldRequirements(account) && meetsAccountLockedGoldRequirements(group)) { -<<<<<<< HEAD - FixidityLib.Fraction memory totalPayment = FixidityLib.newFixed(maxPayment).multiply( - validators[account].score - ); -======= FixidityLib.Fraction memory totalPayment = FixidityLib - .newFixed(validatorEpochPayment) + .newFixed(maxPayment) .multiply(validators[account].score); ->>>>>>> master uint256 groupPayment = totalPayment.multiply(groups[group].commission).fromFixed(); uint256 validatorPayment = totalPayment.fromFixed().sub(groupPayment); getStableToken().mint(group, groupPayment); @@ -523,7 +508,7 @@ contract Validators is * @return True upon success. */ function updatePublicKeysData(bytes calldata publicKeysData) external returns (bool) { - address account = getAccounts().activeValidationSignerToAccount(msg.sender); + address account = getAccounts().activeValidatorSignerToAccount(msg.sender); require(isValidator(account)); Validator storage validator = validators[account]; _updatePublicKeysData(validator, publicKeysData); @@ -705,7 +690,7 @@ contract Validators is * @return True upon success. */ function updateCommission(uint256 commission) external returns (bool) { - address account = getAccounts().activeValidationSignerToAccount(msg.sender); + address account = getAccounts().activeValidatorSignerToAccount(msg.sender); require(isValidatorGroup(account)); ValidatorGroup storage group = groups[account]; require(commission <= FixidityLib.fixed1().unwrap(), "Commission can't be greater than 100%"); @@ -754,7 +739,6 @@ contract Validators is * @param signer The account that registered the validator or its authorized signing address. * @return The unpacked validator struct. */ -<<<<<<< HEAD function getValidatorFromSigner(address signer) external view @@ -773,12 +757,6 @@ contract Validators is public view returns (bytes memory publicKeysData, address affiliation, uint256 score) -======= - function getValidator(address account) - external - view - returns (bytes memory publicKeysData, address affiliation, uint256 score) ->>>>>>> master { require(isValidator(account)); Validator storage validator = validators[account]; From 1e3b012ae7fe900d4252fd7145f2746561cd517c Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Sat, 9 Nov 2019 19:49:24 -0800 Subject: [PATCH 115/149] Cleanup --- ...OfPossession.ts => proof-of-possession.ts} | 0 packages/cli/src/utils/helpers.ts | 1 - .../protocol/contracts/common/Accounts.sol | 83 +++++-------------- .../contracts/governance/Validators.sol | 66 +++++++++------ .../governance/interfaces/IElection.sol | 2 +- .../governance/test/MockValidators.sol | 6 +- .../governance/test/ValidatorsTest.sol | 6 +- packages/protocol/scripts/bash/ganache.sh | 2 +- packages/protocol/test/common/accounts.ts | 3 +- 9 files changed, 71 insertions(+), 98 deletions(-) rename packages/cli/src/commands/account/{proofOfPossession.ts => proof-of-possession.ts} (100%) diff --git a/packages/cli/src/commands/account/proofOfPossession.ts b/packages/cli/src/commands/account/proof-of-possession.ts similarity index 100% rename from packages/cli/src/commands/account/proofOfPossession.ts rename to packages/cli/src/commands/account/proof-of-possession.ts diff --git a/packages/cli/src/utils/helpers.ts b/packages/cli/src/utils/helpers.ts index f222ff3d651..d95c1354c57 100644 --- a/packages/cli/src/utils/helpers.ts +++ b/packages/cli/src/utils/helpers.ts @@ -25,7 +25,6 @@ export async function getPubKeyFromAddrAndWeb3(addr: string, web3: Web3) { } export async function nodeIsSynced(web3: Web3): Promise { - return true if (process.env.NO_SYNCCHECK) { return true } diff --git a/packages/protocol/contracts/common/Accounts.sol b/packages/protocol/contracts/common/Accounts.sol index f66c7b4e981..d698789db95 100644 --- a/packages/protocol/contracts/common/Accounts.sol +++ b/packages/protocol/contracts/common/Accounts.sol @@ -1,7 +1,7 @@ pragma solidity ^0.5.3; import "openzeppelin-solidity/contracts/math/SafeMath.sol"; -import 'openzeppelin-solidity/contracts/ownership/Ownable.sol'; +import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; import "openzeppelin-solidity/contracts/utils/ReentrancyGuard.sol"; import "./interfaces/IAccounts.sol"; @@ -11,21 +11,25 @@ import "../common/Signatures.sol"; import "../common/UsingRegistry.sol"; import "../common/UsingPrecompiles.sol"; -contract Accounts is IAccounts, Ownable, ReentrancyGuard, Initializable, UsingRegistry, UsingPrecompiles { - +contract Accounts is + IAccounts, + Ownable, + ReentrancyGuard, + Initializable, + UsingRegistry, + UsingPrecompiles +{ using SafeMath for uint256; struct Signers { //The address that is authorized to vote in governance and validator elections on behalf of the // account. The account can vote as well, whether or not an vote signing key has been specified. address vote; - // The address that is authorized to manage a validator or validator group and sign consensus // messages on behalf of the account. The account can manage the validator, whether or not an // validator signing key has been specified. However if an validator signing key has been // specified, only that key may actually participate in consensus. address validator; - // The address of the key with which this account wants to sign attestations on the Attestations // contract address attestation; @@ -143,12 +147,7 @@ contract Accounts is IAccounts, Ownable, ReentrancyGuard, Initializable, UsingRe * @param s Output value s of the ECDSA signature. * @dev v, r, s constitute `signer`'s signature on `msg.sender`. */ - function authorizeVoteSigner( - address signer, - uint8 v, - bytes32 r, - bytes32 s - ) + function authorizeVoteSigner(address signer, uint8 v, bytes32 r, bytes32 s) external nonReentrant { @@ -166,12 +165,7 @@ contract Accounts is IAccounts, Ownable, ReentrancyGuard, Initializable, UsingRe * @param s Output value s of the ECDSA signature. * @dev v, r, s constitute `signer`'s signature on `msg.sender`. */ - function authorizeValidatorSigner( - address signer, - uint8 v, - bytes32 r, - bytes32 s - ) + function authorizeValidatorSigner(address signer, uint8 v, bytes32 r, bytes32 s) external nonReentrant { @@ -203,10 +197,7 @@ contract Accounts is IAccounts, Ownable, ReentrancyGuard, Initializable, UsingRe uint8 v, bytes32 r, bytes32 s - ) - external - nonReentrant - { + ) external nonReentrant { Account storage account = accounts[msg.sender]; authorize(signer, v, r, s); account.signers.validator = signer; @@ -222,14 +213,7 @@ contract Accounts is IAccounts, Ownable, ReentrancyGuard, Initializable, UsingRe * @param s Output value s of the ECDSA signature. * @dev v, r, s constitute `signer`'s signature on `msg.sender`. */ - function authorizeAttestationSigner( - address signer, - uint8 v, - bytes32 r, - bytes32 s - ) - public - { + function authorizeAttestationSigner(address signer, uint8 v, bytes32 r, bytes32 s) public { Account storage account = accounts[msg.sender]; authorize(signer, v, r, s); account.signers.attestation = signer; @@ -242,11 +226,7 @@ contract Accounts is IAccounts, Ownable, ReentrancyGuard, Initializable, UsingRe * @dev Fails if the `signer` is not an account or currently authorized attestation signer. * @return The associated account. */ - function activeAttesttationSignerToAccount(address signer) - external - view - returns (address) - { + function activeAttesttationSignerToAccount(address signer) external view returns (address) { address authorizingAccount = authorizedBy[signer]; if (authorizingAccount != address(0)) { require(accounts[authorizingAccount].signers.attestation == signer); @@ -263,11 +243,7 @@ contract Accounts is IAccounts, Ownable, ReentrancyGuard, Initializable, UsingRe * @dev Fails if the `signer` is not an account or active authorized validator. * @return The associated account. */ - function activeValidatorSignerToAccount(address signer) - public - view - returns (address) - { + function activeValidatorSignerToAccount(address signer) public view returns (address) { address authorizingAccount = authorizedBy[signer]; if (authorizingAccount != address(0)) { require(accounts[authorizingAccount].signers.validator == signer); @@ -284,11 +260,7 @@ contract Accounts is IAccounts, Ownable, ReentrancyGuard, Initializable, UsingRe * @dev Fails if the `signer` is not an account or currently authorized vote signer. * @return The associated account. */ - function activeVoteSignerToAccount(address signer) - external - view - returns (address) - { + function activeVoteSignerToAccount(address signer) external view returns (address) { address authorizingAccount = authorizedBy[signer]; if (authorizingAccount != address(0)) { require(accounts[authorizingAccount].signers.vote == signer); @@ -321,11 +293,7 @@ contract Accounts is IAccounts, Ownable, ReentrancyGuard, Initializable, UsingRe * @dev Fails if the `signer` is not an account or previously authorized attestation signer. * @return The associated account. */ - function attestationSignerToAccount(address signer) - public - view - returns (address) - { + function attestationSignerToAccount(address signer) public view returns (address) { address authorizingAccount = authorizedBy[signer]; if (authorizingAccount != address(0)) { return authorizingAccount; @@ -341,11 +309,7 @@ contract Accounts is IAccounts, Ownable, ReentrancyGuard, Initializable, UsingRe * @dev Fails if `signer` is not an account or previously authorized validator signer. * @return The associated account. */ - function validatorSignerToAccount(address signer) - public - view - returns (address) - { + function validatorSignerToAccount(address signer) public view returns (address) { address authorizingAccount = authorizedBy[signer]; if (authorizingAccount != address(0)) { return authorizingAccount; @@ -406,7 +370,7 @@ contract Accounts is IAccounts, Ownable, ReentrancyGuard, Initializable, UsingRe return accounts[account].metadataURL; } - /** + /** * @notice Getter for the data encryption key and version. * @param account The address of the account to get the key for * @return dataEncryptionKey secp256k1 public key for data encryption. Preferably compressed. @@ -469,14 +433,7 @@ contract Accounts is IAccounts, Ownable, ReentrancyGuard, Initializable, UsingRe * @dev Fails if the address is already authorized or is an account. * @dev v, r, s constitute `current`'s signature on `msg.sender`. */ - function authorize( - address authorized, - uint8 v, - bytes32 r, - bytes32 s - ) - private - { + function authorize(address authorized, uint8 v, bytes32 r, bytes32 s) private { require(isAccount(msg.sender) && isNotAccount(authorized) && isNotAuthorizedSigner(authorized)); address signer = Signatures.getSignerOfAddress(msg.sender, v, r, s); diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index bab56f0d74d..5127720367a 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -1,20 +1,20 @@ pragma solidity ^0.5.3; -import 'openzeppelin-solidity/contracts/math/Math.sol'; -import 'openzeppelin-solidity/contracts/math/SafeMath.sol'; -import 'openzeppelin-solidity/contracts/ownership/Ownable.sol'; -import 'openzeppelin-solidity/contracts/utils/ReentrancyGuard.sol'; -import 'solidity-bytes-utils/contracts/BytesLib.sol'; +import "openzeppelin-solidity/contracts/math/Math.sol"; +import "openzeppelin-solidity/contracts/math/SafeMath.sol"; +import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; +import "openzeppelin-solidity/contracts/utils/ReentrancyGuard.sol"; +import "solidity-bytes-utils/contracts/BytesLib.sol"; -import './interfaces/IValidators.sol'; +import "./interfaces/IValidators.sol"; -import '../identity/interfaces/IRandom.sol'; +import "../identity/interfaces/IRandom.sol"; -import '../common/Initializable.sol'; -import '../common/FixidityLib.sol'; -import '../common/linkedlists/AddressLinkedList.sol'; -import '../common/UsingRegistry.sol'; -import '../common/UsingPrecompiles.sol'; +import "../common/Initializable.sol"; +import "../common/FixidityLib.sol"; +import "../common/linkedlists/AddressLinkedList.sol"; +import "../common/UsingRegistry.sol"; +import "../common/UsingPrecompiles.sol"; /** * @title A contract for registering and electing Validator Groups and Validators. @@ -271,7 +271,7 @@ contract Validators is require(lockedGoldBalance >= validatorLockedGoldRequirements.value); Validator storage validator = validators[account]; address signer = getAccounts().getValidatorSigner(account); - _updatePublicKeysData(validator, signer, publicKeysData); + _updatePublicKeysData(validator, signer, publicKeysData); registeredValidators.push(account); updateMembershipHistory(account, address(0)); emit ValidatorRegistered(account, publicKeysData); @@ -390,9 +390,9 @@ contract Validators is // Both the validator and the group must maintain the minimum locked gold balance in order to // receive epoch payments. if (meetsAccountLockedGoldRequirements(account) && meetsAccountLockedGoldRequirements(group)) { - FixidityLib.Fraction memory totalPayment = FixidityLib - .newFixed(maxPayment) - .multiply(validators[account].score); + FixidityLib.Fraction memory totalPayment = FixidityLib.newFixed(maxPayment).multiply( + validators[account].score + ); uint256 groupPayment = totalPayment.multiply(groups[group].commission).fromFixed(); uint256 validatorPayment = totalPayment.fromFixed().sub(groupPayment); getStableToken().mint(group, groupPayment); @@ -465,7 +465,24 @@ contract Validators is return true; } - function updatePublicKeysData(address account, address signer, bytes calldata publicKeysData) external onlyRegisteredContract(ACCOUNTS_REGISTRY_ID) returns (bool) { + /** + * @notice Updates a validator's public keys data. + * @param validator The validator whose public keys data should be updated. + * @param signer The address used to sign consensus message. Coupled to the BLS key. + * @param publicKeysData Comprised of three tightly-packed elements: + * - publicKey - The public key that the validator is using for consensus, should match + * `signer`. 64 bytes. + * - blsPublicKey - The BLS public key that the validator is using for consensus, should pass + * proof of possession. 48 bytes. + * - blsPoP - The BLS public key proof of possession. 96 bytes. + * @dev Called by the registered `Accounts` contract when a validator signer is authorized. + * @return True upon success. + */ + function updatePublicKeysData(address account, address signer, bytes calldata publicKeysData) + external + onlyRegisteredContract(ACCOUNTS_REGISTRY_ID) + returns (bool) + { require(isValidator(account)); Validator storage validator = validators[account]; _updatePublicKeysData(validator, signer, publicKeysData); @@ -485,10 +502,11 @@ contract Validators is * - blsPoP - The BLS public key proof of possession. 96 bytes. * @return True upon success. */ - function _updatePublicKeysData(Validator storage validator, address signer, bytes memory publicKeysData) - private - returns (bool) - { + function _updatePublicKeysData( + Validator storage validator, + address signer, + bytes memory publicKeysData + ) private returns (bool) { // secp256k1 public key + BLS public key + BLS proof of possession require(publicKeysData.length == (64 + 48 + 96)); // Use the proof of possession bytes @@ -632,7 +650,7 @@ contract Validators is { require(isValidatorGroup(group) && isValidator(validator)); ValidatorGroup storage _group = groups[group]; - require(_group.members.numElements < maxGroupSize, 'group would exceed maximum size'); + require(_group.members.numElements < maxGroupSize, "group would exceed maximum size"); require(validators[validator].affiliation == group && !_group.members.contains(validator)); uint256 numMembers = _group.members.numElements.add(1); require(meetsAccountLockedGoldRequirements(group)); @@ -655,7 +673,7 @@ contract Validators is */ function removeMember(address validator) external nonReentrant returns (bool) { address account = getAccounts().activeValidatorSignerToAccount(msg.sender); - require(isValidatorGroup(account) && isValidator(validator), 'is not group and validator'); + require(isValidatorGroup(account) && isValidator(validator), "is not group and validator"); return _removeMember(account, validator); } @@ -989,7 +1007,7 @@ contract Validators is } else if (size < sizeHistory.length) { sizeHistory[size] = now; } else { - require(false, 'Unable to update size history'); + require(false, "Unable to update size history"); } } diff --git a/packages/protocol/contracts/governance/interfaces/IElection.sol b/packages/protocol/contracts/governance/interfaces/IElection.sol index fed416dae2f..ea20aa76978 100644 --- a/packages/protocol/contracts/governance/interfaces/IElection.sol +++ b/packages/protocol/contracts/governance/interfaces/IElection.sol @@ -5,6 +5,6 @@ interface IElection { function getActiveVotes() external view returns (uint256); function getTotalVotesByAccount(address) external view returns (uint256); function markGroupIneligible(address) external; - function markGroupEligible(address,address,address) external; + function markGroupEligible(address, address, address) external; function electValidatorSigners() external view returns (address[] memory); } diff --git a/packages/protocol/contracts/governance/test/MockValidators.sol b/packages/protocol/contracts/governance/test/MockValidators.sol index 9394fed173a..51b30ef4275 100644 --- a/packages/protocol/contracts/governance/test/MockValidators.sol +++ b/packages/protocol/contracts/governance/test/MockValidators.sol @@ -6,7 +6,6 @@ import "../interfaces/IValidators.sol"; * @title Holds a list of addresses of validators */ contract MockValidators is IValidators { - mapping(address => bool) public isValidator; mapping(address => uint256) private numGroupMembers; mapping(address => uint256) private lockedGoldRequirements; @@ -15,7 +14,10 @@ contract MockValidators is IValidators { uint256 private numRegisteredValidators; mapping(address => bytes) public publicKeysData; - function updatePublicKeysData(address account, address, bytes calldata data) external returns (bool) { + function updatePublicKeysData(address account, address, bytes calldata data) + external + returns (bool) + { require(isValidator[account]); publicKeysData[account] = data; return true; diff --git a/packages/protocol/contracts/governance/test/ValidatorsTest.sol b/packages/protocol/contracts/governance/test/ValidatorsTest.sol index 2fecbf7e597..86af7f26a3b 100644 --- a/packages/protocol/contracts/governance/test/ValidatorsTest.sol +++ b/packages/protocol/contracts/governance/test/ValidatorsTest.sol @@ -7,15 +7,11 @@ import "../../common/FixidityLib.sol"; * @title A wrapper around Validators that exposes onlyVm functions for testing. */ contract ValidatorsTest is Validators { - function updateValidatorScoreFromSigner(address signer, uint256 uptime) external { return _updateValidatorScoreFromSigner(signer, uptime); } - function distributeEpochPaymentsFromSigner( - address signer, - uint256 maxPayment - ) + function distributeEpochPaymentsFromSigner(address signer, uint256 maxPayment) external returns (uint256) { diff --git a/packages/protocol/scripts/bash/ganache.sh b/packages/protocol/scripts/bash/ganache.sh index a0032a6f4f9..51e4cc71370 100755 --- a/packages/protocol/scripts/bash/ganache.sh +++ b/packages/protocol/scripts/bash/ganache.sh @@ -5,7 +5,7 @@ set -euo pipefail yarn run ganache-cli \ --deterministic \ - --mnemonic 'concert load couple harbor equip island argue ramp clarify fence smart blah' \ + --mnemonic 'concert load couple harbor equip island argue ramp clarify fence smart topic' \ --gasPrice 0 \ --networkId 1101 \ --gasLimit 10000000 \ diff --git a/packages/protocol/test/common/accounts.ts b/packages/protocol/test/common/accounts.ts index 1b283ff63d4..3673ad96199 100644 --- a/packages/protocol/test/common/accounts.ts +++ b/packages/protocol/test/common/accounts.ts @@ -41,7 +41,6 @@ contract('Accounts', (accounts: string[]) => { const name = 'Account' const metadataURL = 'https://www.celo.org' - const publicKeysData = '0x02f2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e01611111111' const dataEncryptionKey = '0x02f2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e01611111111' const longDataEncryptionKey = '0x04f2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e01611111111' + @@ -337,6 +336,8 @@ contract('Accounts', (accounts: string[]) => { // This block tests those deviations. describe('#authorizeValidatorSigner', async () => { const authorized = accounts[1] + // Arbitrary hex string as MockValidators does not verify this info. + const publicKeysData = '0x02f2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e01611111111' let sig beforeEach(async () => { From e8bc7448fe39afa739a057f1365feb028223d10c Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Sat, 9 Nov 2019 20:19:22 -0800 Subject: [PATCH 116/149] Update exchange tests --- packages/protocol/test/stability/exchange.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/protocol/test/stability/exchange.ts b/packages/protocol/test/stability/exchange.ts index b9f2e81b1a9..a75de0bb0bd 100644 --- a/packages/protocol/test/stability/exchange.ts +++ b/packages/protocol/test/stability/exchange.ts @@ -60,8 +60,8 @@ contract('Exchange', (accounts: string[]) => { const initialGoldBucket = initialReserveBalance .times(fromFixed(reserveFraction)) .integerValue(BigNumber.ROUND_FLOOR) - const stableAmountForRate = new BigNumber(2) - const goldAmountForRate = new BigNumber(1) + const goldAmountForRate = new BigNumber('0x10000000000000000') + const stableAmountForRate = new BigNumber(2).times(goldAmountForRate) const initialStableBucket = initialGoldBucket.times(stableAmountForRate).div(goldAmountForRate) function getBuyTokenAmount( sellAmount: BigNumber, @@ -109,11 +109,7 @@ contract('Exchange', (accounts: string[]) => { mockSortedOracles = await MockSortedOracles.new() await registry.setAddressFor(CeloContractName.SortedOracles, mockSortedOracles.address) - await mockSortedOracles.setMedianRate( - stableToken.address, - stableAmountForRate, - goldAmountForRate - ) + await mockSortedOracles.setMedianRate(stableToken.address, stableAmountForRate) await mockSortedOracles.setMedianTimestampToNow(stableToken.address) await mockSortedOracles.setNumRates(stableToken.address, 2) @@ -330,7 +326,7 @@ contract('Exchange', (accounts: string[]) => { describe('after an oracle update', () => { beforeEach(async () => { - await mockSortedOracles.setMedianRate(stableToken.address, 4, 1) + await mockSortedOracles.setMedianRate(stableToken.address, goldAmountForRate.times(4)) }) it(`should return the same value if updateFrequency seconds haven't passed yet`, async () => { From d5f0bca56f2d0e15d7d4a63b2c0d6ec745c0d060 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Sat, 9 Nov 2019 20:22:23 -0800 Subject: [PATCH 117/149] Fix docstring --- packages/protocol/contracts/governance/Validators.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index 5127720367a..b1414c2f379 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -467,7 +467,7 @@ contract Validators is /** * @notice Updates a validator's public keys data. - * @param validator The validator whose public keys data should be updated. + * @param account The validator whose public keys data should be updated. * @param signer The address used to sign consensus message. Coupled to the BLS key. * @param publicKeysData Comprised of three tightly-packed elements: * - publicKey - The public key that the validator is using for consensus, should match From 3f7cb22fd16346741d7b354974eeef20690dcbe4 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Sat, 9 Nov 2019 20:51:59 -0800 Subject: [PATCH 118/149] Do not connect validator signers directly to any validators --- .../src/e2e-tests/governance_tests.ts | 24 +++++++++++++------ .../commands/account/proof-of-possession.ts | 2 +- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/packages/celotool/src/e2e-tests/governance_tests.ts b/packages/celotool/src/e2e-tests/governance_tests.ts index 1e10a6338f0..f9002c5bb1c 100644 --- a/packages/celotool/src/e2e-tests/governance_tests.ts +++ b/packages/celotool/src/e2e-tests/governance_tests.ts @@ -187,18 +187,28 @@ describe('governance tests', () => { syncmode: 'full', port: 30313, wsport: 8555, + rpcport: 8557, privateKey: groupPrivateKey.slice(2), peers: [await getEnode(8545)], }, + ] + await Promise.all( + additionalNodes.map( + async (nodeConfig) => await initAndStartGeth(context.hooks.gethBinaryPath, nodeConfig) + ) + ) + // Connect the validating nodes to the non-validating nodes, to test that announce messages + // are properly gossiped. + const additionalValidatingNodes = [ { name: 'validator2KeyRotation0', validating: true, syncmode: 'full', lightserv: false, port: 30315, - wsport: 8557, + wsport: 8559, privateKey: rotation0PrivateKey.slice(2), - peers: [await getEnode(8545)], + peers: [await getEnode(8557)], }, { name: 'validator2KeyRotation1', @@ -206,14 +216,14 @@ describe('governance tests', () => { syncmode: 'full', lightserv: false, port: 30317, - wsport: 8559, + wsport: 8561, privateKey: rotation1PrivateKey.slice(2), - peers: [await getEnode(8545)], + peers: [await getEnode(8557)], }, ] await Promise.all( - additionalNodes.map((nodeConfig) => - initAndStartGeth(context.hooks.gethBinaryPath, nodeConfig) + additionalValidatingNodes.map( + async (nodeConfig) => await initAndStartGeth(context.hooks.gethBinaryPath, nodeConfig) ) ) @@ -240,7 +250,7 @@ describe('governance tests', () => { // Prepare for key rotation. const validatorWeb3 = new Web3('http://localhost:8549') - const authorizedWeb3s = [new Web3('ws://localhost:8557'), new Web3('ws://localhost:8559')] + const authorizedWeb3s = [new Web3('ws://localhost:8559'), new Web3('ws://localhost:8561')] const authorizedPublicKeysData = [ getPublicKeysData(rotation0PrivateKey), getPublicKeysData(rotation1PrivateKey), diff --git a/packages/cli/src/commands/account/proof-of-possession.ts b/packages/cli/src/commands/account/proof-of-possession.ts index d0495f048bd..1c94153dc88 100644 --- a/packages/cli/src/commands/account/proof-of-possession.ts +++ b/packages/cli/src/commands/account/proof-of-possession.ts @@ -1,6 +1,6 @@ import { BaseCommand } from '../../base' import { printValueMap } from '../../utils/cli' -import { Args, Flags } from '../../utils/command' +import { Flags } from '../../utils/command' import { serializeSignature } from '@celo/utils/lib/signatureUtils' export default class ProofOfPossession extends BaseCommand { From 6364667e9f6125c58562261bf64426562b49624e Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Sat, 9 Nov 2019 20:53:04 -0800 Subject: [PATCH 119/149] Fix tests --- packages/protocol/test/governance/validators.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/protocol/test/governance/validators.ts b/packages/protocol/test/governance/validators.ts index 297806c1afd..340aacb9d41 100644 --- a/packages/protocol/test/governance/validators.ts +++ b/packages/protocol/test/governance/validators.ts @@ -1852,7 +1852,7 @@ contract('Validators', (accounts: string[]) => { const adjustmentSpeed = fromFixed(validatorScoreParameters.adjustmentSpeed) // @ts-ignore const expectedScore = adjustmentSpeed.times(uptime.pow(validatorScoreParameters.exponent)) - const expectedTotalPayment = expectedScore.times(validatorEpochPayment) + const expectedTotalPayment = expectedScore.times(maxPayment) const expectedGroupPayment = expectedTotalPayment .times(fromFixed(commission)) .dp(0, BigNumber.ROUND_FLOOR) @@ -1863,7 +1863,7 @@ contract('Validators', (accounts: string[]) => { describe('when the validator and group meet the balance requirements', () => { beforeEach(async () => { - ret = await validators.distributeEpochPayment(validator, maxPayment).call() + ret = await validators.distributeEpochPayment.call(validator, maxPayment) await validators.distributeEpochPayment(validator, maxPayment) }) @@ -1886,7 +1886,7 @@ contract('Validators', (accounts: string[]) => { validator, validatorLockedGoldRequirements.value.minus(1) ) - ret = await validators.distributeEpochPayment(validator, maxPayment).call() + ret = await validators.distributeEpochPayment.call(validator, maxPayment) await validators.distributeEpochPayment(validator, maxPayment) }) @@ -1909,7 +1909,7 @@ contract('Validators', (accounts: string[]) => { group, groupLockedGoldRequirements.value.minus(1) ) - ret = await validators.distributeEpochPayment(validator, maxPayment).call() + ret = await validators.distributeEpochPayment.call(validator, maxPayment) await validators.distributeEpochPayment(validator, maxPayment) }) From a33012b4a45a0aecf00d9d5b45596ffd9d640c77 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Sat, 9 Nov 2019 21:10:06 -0800 Subject: [PATCH 120/149] Fix authorize test --- .../src/commands/account/authorize.test.ts | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/commands/account/authorize.test.ts b/packages/cli/src/commands/account/authorize.test.ts index 1127bd6afcf..fada8a235c7 100644 --- a/packages/cli/src/commands/account/authorize.test.ts +++ b/packages/cli/src/commands/account/authorize.test.ts @@ -8,14 +8,32 @@ process.env.NO_SYNCCHECK = 'true' testWithGanache('account:authorize cmd', (web3: Web3) => { test('can authorize account', async () => { const accounts = await web3.eth.getAccounts() - await Register.run(['--from', accounts[0], '--name', 'Chapulin Colorado']) - await Authorize.run(['--from', accounts[0], '--role', 'validation', '--to', accounts[1]]) + await Register.run(['--from', accounts[0]]) + await Authorize.run([ + '--from', + accounts[0], + '--role', + 'validator', + '--signer', + accounts[1], + '--pop', + '0x1b9fca4bbb5bfb1dbe69ef1cddbd9b4202dcb6b134c5170611e1e36ecfa468d7b46c85328d504934fce6c2a1571603a50ae224d2b32685e84d4d1a1eebad8452eb', + ]) }) test('fails if from is not an account', async () => { const accounts = await web3.eth.getAccounts() await expect( - Authorize.run(['--from', accounts[0], '--role', 'validation', '--to', accounts[1]]) + Authorize.run([ + '--from', + accounts[0], + '--role', + 'validator', + '--signer', + accounts[1], + '--pop', + '0x1b9fca4bbb5bfb1dbe69ef1cddbd9b4202dcb6b134c5170611e1e36ecfa468d7b46c85328d504934fce6c2a1571603a50ae224d2b32685e84d4d1a1eebad8452eb', + ]) ).rejects.toThrow() }) }) From 90796c106a95d627fbd30bfcb076e68d1e2e0df3 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Sat, 9 Nov 2019 21:15:49 -0800 Subject: [PATCH 121/149] Fix linting --- .../src/e2e-tests/governance_tests.ts | 20 +++++++++---------- .../commands/account/proof-of-possession.ts | 2 +- packages/contractkit/src/wrappers/Accounts.ts | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/celotool/src/e2e-tests/governance_tests.ts b/packages/celotool/src/e2e-tests/governance_tests.ts index f9002c5bb1c..3b171dce3fc 100644 --- a/packages/celotool/src/e2e-tests/governance_tests.ts +++ b/packages/celotool/src/e2e-tests/governance_tests.ts @@ -81,11 +81,11 @@ describe('governance tests', () => { } } - const getValidatorSigner = async (address: string, blockNumber?: number) => { + const getValidatorSigner = (address: string, blockNumber?: number) => { if (blockNumber) { - return await accounts.methods.getValidatorSigner(address).call({}, blockNumber) + return accounts.methods.getValidatorSigner(address).call({}, blockNumber) } else { - return await accounts.methods.getValidatorSigner(address).call() + return accounts.methods.getValidatorSigner(address).call() } } @@ -193,8 +193,8 @@ describe('governance tests', () => { }, ] await Promise.all( - additionalNodes.map( - async (nodeConfig) => await initAndStartGeth(context.hooks.gethBinaryPath, nodeConfig) + additionalNodes.map((nodeConfig) => + initAndStartGeth(context.hooks.gethBinaryPath, nodeConfig) ) ) // Connect the validating nodes to the non-validating nodes, to test that announce messages @@ -222,8 +222,8 @@ describe('governance tests', () => { }, ] await Promise.all( - additionalValidatingNodes.map( - async (nodeConfig) => await initAndStartGeth(context.hooks.gethBinaryPath, nodeConfig) + additionalValidatingNodes.map((nodeConfig) => + initAndStartGeth(context.hooks.gethBinaryPath, nodeConfig) ) ) @@ -237,7 +237,7 @@ describe('governance tests', () => { do { blockNumber = await web3.eth.getBlockNumber() await sleep(0.1) - } while (blockNumber % epoch != 1) + } while (blockNumber % epoch !== 1) await activate(validatorAccounts[0]) @@ -322,7 +322,7 @@ describe('governance tests', () => { const getValidatorSetAccountsAtBlock = async (blockNumber: number) => { const signingKeys = await getValidatorSetSignersAtBlock(blockNumber) - return await Promise.all( + return Promise.all( signingKeys.map((address: string) => accounts.methods.validatorSignerToAccount(address).call({}, blockNumber) ) @@ -365,7 +365,7 @@ describe('governance tests', () => { for (const blockNumber of blockNumbers) { const lastEpochBlock = getLastEpochBlock(blockNumber) // Fetch the round robin order if it hasn't already been set for this epoch. - if (roundRobinOrder.length == 0 || blockNumber == lastEpochBlock + 1) { + if (roundRobinOrder.length === 0 || blockNumber === lastEpochBlock + 1) { const validatorSet = await getValidatorSetSignersAtBlock(blockNumber) roundRobinOrder = await Promise.all( validatorSet.map( diff --git a/packages/cli/src/commands/account/proof-of-possession.ts b/packages/cli/src/commands/account/proof-of-possession.ts index 1c94153dc88..24b0ab206ba 100644 --- a/packages/cli/src/commands/account/proof-of-possession.ts +++ b/packages/cli/src/commands/account/proof-of-possession.ts @@ -1,7 +1,7 @@ +import { serializeSignature } from '@celo/utils/lib/signatureUtils' import { BaseCommand } from '../../base' import { printValueMap } from '../../utils/cli' import { Flags } from '../../utils/command' -import { serializeSignature } from '@celo/utils/lib/signatureUtils' export default class ProofOfPossession extends BaseCommand { static description = 'Generate proof-of-possession to be used to authorize a signer' diff --git a/packages/contractkit/src/wrappers/Accounts.ts b/packages/contractkit/src/wrappers/Accounts.ts index 8d04970fc66..756bb12bb5f 100644 --- a/packages/contractkit/src/wrappers/Accounts.ts +++ b/packages/contractkit/src/wrappers/Accounts.ts @@ -1,5 +1,5 @@ -import Web3 from 'web3' import { parseSignature } from '@celo/utils/lib/signatureUtils' +import Web3 from 'web3' import { Address } from '../base' import { Accounts } from '../generated/types/Accounts' import { From d8265aa6da7c23d754237a2cf79873651f52f1ab Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Sat, 9 Nov 2019 21:35:50 -0800 Subject: [PATCH 122/149] Fix reserve tests --- packages/protocol/test/stability/reserve.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/protocol/test/stability/reserve.ts b/packages/protocol/test/stability/reserve.ts index 0f660a9d920..b73153b2731 100644 --- a/packages/protocol/test/stability/reserve.ts +++ b/packages/protocol/test/stability/reserve.ts @@ -35,7 +35,7 @@ contract('Reserve', (accounts: string[]) => { const nonOwner: string = accounts[1] const spender: string = accounts[2] const aTobinTaxStalenessThreshold: number = 600 - + const sortedOraclesDenominator = new BigNumber('0x10000000000000000') beforeEach(async () => { reserve = await Reserve.new() registry = await Registry.new() @@ -80,7 +80,7 @@ contract('Reserve', (accounts: string[]) => { describe('#addToken()', () => { beforeEach(async () => { - await mockSortedOracles.setMedianRate(anAddress, 1, 1) + await mockSortedOracles.setMedianRate(anAddress, sortedOraclesDenominator) }) it('should allow owner to add a token', async () => { @@ -124,7 +124,7 @@ contract('Reserve', (accounts: string[]) => { describe('when the token has already been added', async () => { beforeEach(async () => { - await mockSortedOracles.setMedianRate(anAddress, 1, 1) + await mockSortedOracles.setMedianRate(anAddress, sortedOraclesDenominator) await reserve.addToken(anAddress) const tokenList = await reserve.getTokens() index = -1 @@ -186,7 +186,10 @@ contract('Reserve', (accounts: string[]) => { beforeEach(async () => { mockStableToken = await MockStableToken.new() await registry.setAddressFor(CeloContractName.SortedOracles, mockSortedOracles.address) - await mockSortedOracles.setMedianRate(mockStableToken.address, 10, 1) + await mockSortedOracles.setMedianRate( + mockStableToken.address, + sortedOraclesDenominator.times(10) + ) await reserve.addToken(mockStableToken.address) const reserveGoldBalance = new BigNumber(10).pow(19) await web3.eth.sendTransaction({ @@ -230,7 +233,10 @@ contract('Reserve', (accounts: string[]) => { let anotherMockStableToken: MockStableTokenInstance beforeEach(async () => { anotherMockStableToken = await MockStableToken.new() - await mockSortedOracles.setMedianRate(anotherMockStableToken.address, 10, 1) + await mockSortedOracles.setMedianRate( + anotherMockStableToken.address, + sortedOraclesDenominator.times(10) + ) await reserve.addToken(anotherMockStableToken.address) }) From 11d120d2ac0e346922b22ba54320925f400c4ada Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Sat, 9 Nov 2019 21:40:48 -0800 Subject: [PATCH 123/149] Fix end-to-end tests --- .../celotool/src/e2e-tests/blockchain_parameters_tests.ts | 2 +- packages/celotool/src/e2e-tests/governance_tests.ts | 8 +++++--- packages/celotool/src/e2e-tests/transfer_tests.ts | 2 +- packages/celotool/src/e2e-tests/validator_order_tests.ts | 2 +- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/celotool/src/e2e-tests/blockchain_parameters_tests.ts b/packages/celotool/src/e2e-tests/blockchain_parameters_tests.ts index 03955381546..a8db6bb4a1a 100644 --- a/packages/celotool/src/e2e-tests/blockchain_parameters_tests.ts +++ b/packages/celotool/src/e2e-tests/blockchain_parameters_tests.ts @@ -13,7 +13,7 @@ describe('Blockchain parameters tests', function(this: any) { let parameters: BlockchainParametersWrapper const gethConfig: GethTestConfig = { - migrateTo: 17, + migrateTo: 18, instances: [ { name: 'validator', validating: true, syncmode: 'full', port: 30303, rpcport: 8545 }, ], diff --git a/packages/celotool/src/e2e-tests/governance_tests.ts b/packages/celotool/src/e2e-tests/governance_tests.ts index 94abc60e9a1..44d8e2141d7 100644 --- a/packages/celotool/src/e2e-tests/governance_tests.ts +++ b/packages/celotool/src/e2e-tests/governance_tests.ts @@ -292,8 +292,8 @@ describe('governance tests', () => { it('should distribute epoch payments at the end of each epoch', async () => { const commission = 0.1 - const maxValidatorEpochPayment = new BigNumber( - await epochRewards.methods.maxValidatorEpochPayment().call() + const targetValidatorEpochPayment = new BigNumber( + await epochRewards.methods.targetValidatorEpochPayment().call() ) const [group] = await validators.methods.getRegisteredValidatorGroups().call() @@ -327,7 +327,9 @@ describe('governance tests', () => { const rewardsMultiplier = new BigNumber( await epochRewards.methods.getRewardsMultiplier().call({}, blockNumber - 1) ) - return maxValidatorEpochPayment.times(fromFixed(score)).times(fromFixed(rewardsMultiplier)) + return targetValidatorEpochPayment + .times(fromFixed(score)) + .times(fromFixed(rewardsMultiplier)) } for (const blockNumber of blockNumbers) { diff --git a/packages/celotool/src/e2e-tests/transfer_tests.ts b/packages/celotool/src/e2e-tests/transfer_tests.ts index 52f3cd711aa..eab473053ae 100644 --- a/packages/celotool/src/e2e-tests/transfer_tests.ts +++ b/packages/celotool/src/e2e-tests/transfer_tests.ts @@ -180,7 +180,7 @@ describe('Transfer tests', function(this: any) { const syncModes = ['full', 'fast', 'light', 'ultralight'] const gethConfig: GethTestConfig = { - migrateTo: 17, + migrateTo: 18, instances: [ { name: 'validator', validating: true, syncmode: 'full', port: 30303, rpcport: 8545 }, ], diff --git a/packages/celotool/src/e2e-tests/validator_order_tests.ts b/packages/celotool/src/e2e-tests/validator_order_tests.ts index 2e71aafc78f..8051b43cecf 100644 --- a/packages/celotool/src/e2e-tests/validator_order_tests.ts +++ b/packages/celotool/src/e2e-tests/validator_order_tests.ts @@ -10,7 +10,7 @@ const BLOCK_COUNT = EPOCH * EPOCHS_TO_WAIT describe('governance tests', () => { const gethConfig: GethTestConfig = { - migrateTo: 14, + migrateTo: 15, instances: _.range(VALIDATORS).map((i) => ({ name: `validator${i}`, validating: true, From 117d5816bf317fd6ee4b88bea811259a36918c78 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Sat, 9 Nov 2019 21:48:57 -0800 Subject: [PATCH 124/149] Remove unsupported test --- packages/cli/src/commands/account/authorize.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/cli/src/commands/account/authorize.test.ts b/packages/cli/src/commands/account/authorize.test.ts index fada8a235c7..a9ad96415ff 100644 --- a/packages/cli/src/commands/account/authorize.test.ts +++ b/packages/cli/src/commands/account/authorize.test.ts @@ -6,6 +6,9 @@ import Register from './register' process.env.NO_SYNCCHECK = 'true' testWithGanache('account:authorize cmd', (web3: Web3) => { + // TODO(asa): Fix this test once CLI command for authorizing signer for registered validator + // is supported. + /* test('can authorize account', async () => { const accounts = await web3.eth.getAccounts() await Register.run(['--from', accounts[0]]) @@ -20,6 +23,7 @@ testWithGanache('account:authorize cmd', (web3: Web3) => { '0x1b9fca4bbb5bfb1dbe69ef1cddbd9b4202dcb6b134c5170611e1e36ecfa468d7b46c85328d504934fce6c2a1571603a50ae224d2b32685e84d4d1a1eebad8452eb', ]) }) + */ test('fails if from is not an account', async () => { const accounts = await web3.eth.getAccounts() From cf506938c5045d00ece1b8d27a1453b9f2906ac8 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Sun, 10 Nov 2019 11:01:13 -0800 Subject: [PATCH 125/149] Fix tests --- packages/protocol/test/governance/validators.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/protocol/test/governance/validators.ts b/packages/protocol/test/governance/validators.ts index 340aacb9d41..749a8581df3 100644 --- a/packages/protocol/test/governance/validators.ts +++ b/packages/protocol/test/governance/validators.ts @@ -1852,7 +1852,7 @@ contract('Validators', (accounts: string[]) => { const adjustmentSpeed = fromFixed(validatorScoreParameters.adjustmentSpeed) // @ts-ignore const expectedScore = adjustmentSpeed.times(uptime.pow(validatorScoreParameters.exponent)) - const expectedTotalPayment = expectedScore.times(maxPayment) + const expectedTotalPayment = expectedScore.times(maxPayment).dp(0, BigNumber.ROUND_FLOOR) const expectedGroupPayment = expectedTotalPayment .times(fromFixed(commission)) .dp(0, BigNumber.ROUND_FLOOR) From 4f7d450ef6ffdb83b1ad36126c80d0de3617c280 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Sun, 10 Nov 2019 12:38:30 -0800 Subject: [PATCH 126/149] Remove unused import --- packages/cli/src/commands/account/authorize.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/cli/src/commands/account/authorize.test.ts b/packages/cli/src/commands/account/authorize.test.ts index a9ad96415ff..a81169d88f5 100644 --- a/packages/cli/src/commands/account/authorize.test.ts +++ b/packages/cli/src/commands/account/authorize.test.ts @@ -1,7 +1,6 @@ import Web3 from 'web3' import { testWithGanache } from '../../test-utils/ganache-test' import Authorize from './authorize' -import Register from './register' process.env.NO_SYNCCHECK = 'true' From 6fb7a330d31b264cb61a41dc6bc9a586c388e8bd Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Mon, 11 Nov 2019 13:44:10 -0800 Subject: [PATCH 127/149] Address comments --- .../contracts/governance/EpochRewards.sol | 4 +- .../governance/test/EpochRewardsTest.sol | 6 +-- packages/protocol/lib/test-utils.ts | 15 ++++++ .../migrations/20_elect_validators.ts | 3 -- .../protocol/test/governance/epochrewards.ts | 50 +++++++++---------- 5 files changed, 43 insertions(+), 35 deletions(-) diff --git a/packages/protocol/contracts/governance/EpochRewards.sol b/packages/protocol/contracts/governance/EpochRewards.sol index ca591d41399..6642ee226d6 100644 --- a/packages/protocol/contracts/governance/EpochRewards.sol +++ b/packages/protocol/contracts/governance/EpochRewards.sol @@ -30,9 +30,9 @@ contract EpochRewards is Ownable, Initializable, UsingPrecompiles, UsingRegistry // This struct governs the multiplier on the target rewards to give out in a given epoch due to // potential deviations in the actual Gold total supply from the target total supply. // In the case where the actual exceeds the target (i.e. the protocol has "overspent" with - // respect to epoch rewards and payments) the multiplier will be less than one. + // respect to epoch rewards and payments) the rewards multiplier will be less than one. // In the case where the actual is less than the target (i.e. the protocol has "underspent" with - // respect to epoch rewards and payments) the multiplier will be greater than one. + // respect to epoch rewards and payments) the rewards multiplier will be greater than one. struct RewardsMultiplierParameters { RewardsMultiplierAdjustmentFactors adjustmentFactors; // The maximum rewards multiplier. diff --git a/packages/protocol/contracts/governance/test/EpochRewardsTest.sol b/packages/protocol/contracts/governance/test/EpochRewardsTest.sol index 774e89501f6..bd0103b0826 100644 --- a/packages/protocol/contracts/governance/test/EpochRewardsTest.sol +++ b/packages/protocol/contracts/governance/test/EpochRewardsTest.sol @@ -1,12 +1,12 @@ pragma solidity ^0.5.8; import "../EpochRewards.sol"; -import "../../common/FixidityLib.sol"; /** * @title A wrapper around EpochRewards that exposes internal functions for testing. */ contract EpochRewardsTest is EpochRewards { + uint256 public numValidatorsInCurrentSet; function getRewardsMultiplier(uint256 targetGoldTotalSupplyIncrease) external view @@ -19,7 +19,7 @@ contract EpochRewardsTest is EpochRewards { _updateTargetVotingYield(); } - function numberValidatorsInCurrentSet() public view returns (uint256) { - return 100; + function setNumberValidatorsInCurrentSet(uint256 value) external { + numValidatorsInCurrentSet = value; } } diff --git a/packages/protocol/lib/test-utils.ts b/packages/protocol/lib/test-utils.ts index 2e9ff68d5d5..812669b0f5a 100644 --- a/packages/protocol/lib/test-utils.ts +++ b/packages/protocol/lib/test-utils.ts @@ -239,6 +239,21 @@ export function assertEqualBN( ) } +export function assertEqualDpBN( + value: number | BN | BigNumber, + expected: number | BN | BigNumber, + decimals: number, + msg?: string +) { + const valueDp = new BigNumber(value.toString()).dp(decimals) + const expectedDp = new BigNumber(expected.toString()).dp(decimals) + assert( + valueDp.isEqualTo(expectedDp), + `expected ${expectedDp.toString()} and got ${valueDp.toString()}. ${msg || ''}` + ) +} + + export function assertEqualBNArray(value: number[] | BN[] | BigNumber[], expected: number[] | BN[] | BigNumber[], msg?: string) { assert.equal(value.length, expected.length, msg) value.forEach((x, i) => assertEqualBN(x, expected[i])) diff --git a/packages/protocol/migrations/20_elect_validators.ts b/packages/protocol/migrations/20_elect_validators.ts index d994167ed8a..a42077e0ba2 100644 --- a/packages/protocol/migrations/20_elect_validators.ts +++ b/packages/protocol/migrations/20_elect_validators.ts @@ -117,7 +117,6 @@ async function registerValidator( ).toString('hex') const publicKeysData = publicKey + blsPublicKey + blsPoP - console.log('locking gold for validator registration') await lockGold( accounts, lockedGold, @@ -125,7 +124,6 @@ async function registerValidator( validatorPrivateKey ) - console.log('registering validator') // @ts-ignore const setNameTx = accounts.contract.methods.setName(`CLabs Validator #${index} on ${networkName}`) await sendTransactionWithPrivateKey(web3, setNameTx, validatorPrivateKey, { @@ -139,7 +137,6 @@ async function registerValidator( to: validators.address, }) - console.log('affiliating') // @ts-ignore const affiliateTx = validators.contract.methods.affiliate(groupAddress) diff --git a/packages/protocol/test/governance/epochrewards.ts b/packages/protocol/test/governance/epochrewards.ts index 9215b00c6d1..d90b2ebe562 100644 --- a/packages/protocol/test/governance/epochrewards.ts +++ b/packages/protocol/test/governance/epochrewards.ts @@ -2,6 +2,7 @@ import { CeloContractName } from '@celo/protocol/lib/registry-utils' import { assertContainSubset, assertEqualBN, + assertEqualDpBN, assertRevert, timeTravel, } from '@celo/protocol/lib/test-utils' @@ -64,7 +65,7 @@ contract('EpochRewards', (accounts: string[]) => { const targetVotingGoldFraction = toFixed(new BigNumber(2 / 3)) const targetValidatorEpochPayment = new BigNumber(10000000000000) const exchangeRate = 7 - const randomAddress = web3.utils.randomHex(20) + const mockStableTokenAddress = web3.utils.randomHex(20) const sortedOraclesDenominator = new BigNumber('0x10000000000000000') beforeEach(async () => { epochRewards = await EpochRewards.new() @@ -75,9 +76,9 @@ contract('EpochRewards', (accounts: string[]) => { await registry.setAddressFor(CeloContractName.Election, mockElection.address) await registry.setAddressFor(CeloContractName.GoldToken, mockGoldToken.address) await registry.setAddressFor(CeloContractName.SortedOracles, mockSortedOracles.address) - await registry.setAddressFor(CeloContractName.StableToken, randomAddress) + await registry.setAddressFor(CeloContractName.StableToken, mockStableTokenAddress) await mockSortedOracles.setMedianRate( - randomAddress, + mockStableTokenAddress, sortedOraclesDenominator.times(exchangeRate) ) @@ -140,17 +141,13 @@ contract('EpochRewards', (accounts: string[]) => { const newFraction = targetVotingGoldFraction.plus(1) describe('when called by the owner', () => { - let resp: any - - beforeEach(async () => { - resp = await epochRewards.setTargetVotingGoldFraction(newFraction) - }) - it('should set the target voting gold fraction', async () => { + await epochRewards.setTargetVotingGoldFraction(newFraction) assertEqualBN(await epochRewards.getTargetVotingGoldFraction(), newFraction) }) it('should emit the TargetVotingGoldFractionSet event', async () => { + const resp = await epochRewards.setTargetVotingGoldFraction(newFraction) assert.equal(resp.logs.length, 1) const log = resp.logs[0] assertContainSubset(log, { @@ -185,17 +182,13 @@ contract('EpochRewards', (accounts: string[]) => { const newPayment = targetValidatorEpochPayment.plus(1) describe('when called by the owner', () => { - let resp: any - - beforeEach(async () => { - resp = await epochRewards.setTargetValidatorEpochPayment(newPayment) - }) - it('should set the target validator epoch payment', async () => { + await epochRewards.setTargetValidatorEpochPayment(newPayment) assertEqualBN(await epochRewards.targetValidatorEpochPayment(), newPayment) }) it('should emit the TargetValidatorEpochPaymentSet event', async () => { + const resp = await epochRewards.setTargetValidatorEpochPayment(newPayment) assert.equal(resp.logs.length, 1) const log = resp.logs[0] assertContainSubset(log, { @@ -206,21 +199,21 @@ contract('EpochRewards', (accounts: string[]) => { }) }) - describe('when called by a non-owner', () => { + describe('when the payment is the same', () => { it('should revert', async () => { await assertRevert( - epochRewards.setTargetValidatorEpochPayment(newPayment, { - from: nonOwner, - }) + epochRewards.setTargetValidatorEpochPayment(targetValidatorEpochPayment) ) }) }) }) - describe('when the payment is the same', () => { + describe('when called by a non-owner', () => { it('should revert', async () => { await assertRevert( - epochRewards.setTargetValidatorEpochPayment(targetValidatorEpochPayment) + epochRewards.setTargetValidatorEpochPayment(newPayment, { + from: nonOwner, + }) ) }) }) @@ -382,8 +375,11 @@ contract('EpochRewards', (accounts: string[]) => { describe('#getTargetTotalEpochPaymentsInGold()', () => { describe('when a StableToken exchange rate is set', () => { - // Hard coded in EpochRewardsTest.sol const numValidators = 100 + beforeEach(async () => { + await epochRewards.setNumValidatorsInCurrentSet(numValidators) + }) + it('should return the number of validators times the max payment divided by the exchange rate', async () => { const expected = targetValidatorEpochPayment .times(numValidators) @@ -432,7 +428,7 @@ contract('EpochRewards', (accounts: string[]) => { fromFixed(rewardsMultiplier.adjustments.underspend).times(0.1) ) // Assert equal to 7 decimal places due to fixidity imprecision. - assert.equal(expected.dp(7).toFixed(), actual.dp(7).toFixed()) + assertEqualDpBN(actual, expected, 7) }) }) @@ -451,25 +447,25 @@ contract('EpochRewards', (accounts: string[]) => { fromFixed(rewardsMultiplier.adjustments.overspend).times(0.1) ) // Assert equal to 7 decimal places due to fixidity imprecision. - assert.equal(expected.dp(7).toFixed(), actual.dp(7).toFixed()) + assertEqualDpBN(actual, expected, 7) }) }) }) describe('#updateTargetVotingYield()', () => { - const randomAddress = web3.utils.randomHex(20) // Arbitrary numbers const totalSupply = new BigNumber(129762987346298761037469283746) const reserveBalance = new BigNumber(2397846127684712867321) const floatingSupply = totalSupply.minus(reserveBalance) + const mockReserveAddress = web3.utils.randomHex(20) beforeEach(async () => { await mockGoldToken.setTotalSupply(totalSupply) await web3.eth.sendTransaction({ from: accounts[9], - to: randomAddress, + to: mockReserveAddress, value: reserveBalance.toString(), }) - await registry.setAddressFor(CeloContractName.Reserve, randomAddress) + await registry.setAddressFor(CeloContractName.Reserve, mockReserveAddress) }) describe('when the percentage of voting gold is equal to the target', () => { From 2e1703703b330d7337a0f2c42228849400b35b74 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Mon, 11 Nov 2019 13:46:17 -0800 Subject: [PATCH 128/149] Whoops --- .../contracts/governance/test/EpochRewardsTest.sol | 4 ++-- packages/protocol/test/governance/epochrewards.ts | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/protocol/contracts/governance/test/EpochRewardsTest.sol b/packages/protocol/contracts/governance/test/EpochRewardsTest.sol index bd0103b0826..3e7987da48d 100644 --- a/packages/protocol/contracts/governance/test/EpochRewardsTest.sol +++ b/packages/protocol/contracts/governance/test/EpochRewardsTest.sol @@ -6,7 +6,7 @@ import "../EpochRewards.sol"; * @title A wrapper around EpochRewards that exposes internal functions for testing. */ contract EpochRewardsTest is EpochRewards { - uint256 public numValidatorsInCurrentSet; + uint256 public numberValidatorsInCurrentSet; function getRewardsMultiplier(uint256 targetGoldTotalSupplyIncrease) external view @@ -20,6 +20,6 @@ contract EpochRewardsTest is EpochRewards { } function setNumberValidatorsInCurrentSet(uint256 value) external { - numValidatorsInCurrentSet = value; + numberValidatorsInCurrentSet = value; } } diff --git a/packages/protocol/test/governance/epochrewards.ts b/packages/protocol/test/governance/epochrewards.ts index d90b2ebe562..ca16cfdb9ca 100644 --- a/packages/protocol/test/governance/epochrewards.ts +++ b/packages/protocol/test/governance/epochrewards.ts @@ -375,14 +375,14 @@ contract('EpochRewards', (accounts: string[]) => { describe('#getTargetTotalEpochPaymentsInGold()', () => { describe('when a StableToken exchange rate is set', () => { - const numValidators = 100 + const numberValidators = 100 beforeEach(async () => { - await epochRewards.setNumValidatorsInCurrentSet(numValidators) + await epochRewards.setNumValidatorsInCurrentSet(numberValidators) }) it('should return the number of validators times the max payment divided by the exchange rate', async () => { const expected = targetValidatorEpochPayment - .times(numValidators) + .times(numberValidators) .div(exchangeRate) .integerValue(BigNumber.ROUND_FLOOR) assertEqualBN(await epochRewards.getTargetTotalEpochPaymentsInGold(), expected) @@ -528,14 +528,14 @@ contract('EpochRewards', (accounts: string[]) => { describe('when there are active votes, a stable token exchange rate is set and the actual remaining supply is 10% more than the target remaining supply after rewards', () => { const activeVotes = 1000000 const timeDelta = YEAR.times(10) - // Hard coded in EpochRewardsTest.sol - const numValidators = 100 + const numberValidators = 100 let expectedMultiplier: BigNumber beforeEach(async () => { + await epochRewards.setNumberValidatorsInCurrentSet(numberValidators) await mockElection.setActiveVotes(activeVotes) await timeTravel(timeDelta.toNumber(), web3) const expectedTargetTotalEpochPaymentsInGold = targetValidatorEpochPayment - .times(numValidators) + .times(numberValidators) .div(exchangeRate) .integerValue(BigNumber.ROUND_FLOOR) const expectedTargetEpochRewards = fromFixed(targetVotingYieldParams.initial).times( From 23776ee75b218829190f4f69dc89c8dffa191528 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Mon, 11 Nov 2019 13:54:04 -0800 Subject: [PATCH 129/149] Fix epoch rewards tests --- .../contracts/governance/test/EpochRewardsTest.sol | 8 ++++++-- packages/protocol/test/governance/epochrewards.ts | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/protocol/contracts/governance/test/EpochRewardsTest.sol b/packages/protocol/contracts/governance/test/EpochRewardsTest.sol index 3e7987da48d..e46e78f77be 100644 --- a/packages/protocol/contracts/governance/test/EpochRewardsTest.sol +++ b/packages/protocol/contracts/governance/test/EpochRewardsTest.sol @@ -6,7 +6,7 @@ import "../EpochRewards.sol"; * @title A wrapper around EpochRewards that exposes internal functions for testing. */ contract EpochRewardsTest is EpochRewards { - uint256 public numberValidatorsInCurrentSet; + uint256 private numValidatorsInCurrentSet; function getRewardsMultiplier(uint256 targetGoldTotalSupplyIncrease) external view @@ -19,7 +19,11 @@ contract EpochRewardsTest is EpochRewards { _updateTargetVotingYield(); } + function numberValidatorsInCurrentSet() public view returns (uint256) { + return numValidatorsInCurrentSet; + } + function setNumberValidatorsInCurrentSet(uint256 value) external { - numberValidatorsInCurrentSet = value; + numValidatorsInCurrentSet = value; } } diff --git a/packages/protocol/test/governance/epochrewards.ts b/packages/protocol/test/governance/epochrewards.ts index ca16cfdb9ca..b0605142ad1 100644 --- a/packages/protocol/test/governance/epochrewards.ts +++ b/packages/protocol/test/governance/epochrewards.ts @@ -377,7 +377,7 @@ contract('EpochRewards', (accounts: string[]) => { describe('when a StableToken exchange rate is set', () => { const numberValidators = 100 beforeEach(async () => { - await epochRewards.setNumValidatorsInCurrentSet(numberValidators) + await epochRewards.setNumberValidatorsInCurrentSet(numberValidators) }) it('should return the number of validators times the max payment divided by the exchange rate', async () => { From d80ab3261ddb90d8a3f06dc9839ef3b09eeebe14 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Mon, 11 Nov 2019 17:05:23 -0800 Subject: [PATCH 130/149] WIP --- .../src/e2e-tests/governance_tests.ts | 44 +++-- .../src/commands/account/authorize.test.ts | 5 +- .../cli/src/commands/account/authorize.ts | 1 - .../cli/src/commands/validator/register.ts | 17 +- .../src/commands/validator/update-bls-key.ts | 43 +++++ packages/cli/src/utils/command.ts | 34 +++- packages/cli/src/utils/helpers.ts | 21 --- packages/contractkit/src/wrappers/Accounts.ts | 33 +--- .../src/wrappers/Validators.test.ts | 8 +- .../contractkit/src/wrappers/Validators.ts | 43 +++-- .../protocol/contracts/common/Accounts.sol | 46 +----- .../contracts/common/UsingPrecompiles.sol | 65 +++++++- .../contracts/governance/Validators.sol | 154 ++++++++++-------- .../governance/interfaces/IValidators.sol | 2 +- .../governance/test/MockValidators.sol | 12 +- .../migrations/20_elect_validators.ts | 10 +- packages/protocol/test/common/accounts.ts | 50 ------ .../protocol/test/governance/validators.ts | 101 ++++++++---- packages/utils/src/address.ts | 30 +++- packages/utils/src/bls.ts | 25 +-- 20 files changed, 425 insertions(+), 319 deletions(-) create mode 100644 packages/cli/src/commands/validator/update-bls-key.ts diff --git a/packages/celotool/src/e2e-tests/governance_tests.ts b/packages/celotool/src/e2e-tests/governance_tests.ts index 3b171dce3fc..e9a81e2466e 100644 --- a/packages/celotool/src/e2e-tests/governance_tests.ts +++ b/packages/celotool/src/e2e-tests/governance_tests.ts @@ -1,5 +1,5 @@ import { ContractKit, newKitFromWeb3 } from '@celo/contractkit' -import { getPublicKeysData } from '@celo/utils/lib/bls' +import { getBlsPublicKey, getBlsPoP } from '@celo/utils/lib/bls' import { fromFixed, toFixed } from '@celo/utils/lib/fixidity' import BigNumber from 'bignumber.js' import { assert } from 'chai' @@ -138,7 +138,6 @@ describe('governance tests', () => { const authorizeValidatorSigner = async ( validatorWeb3: any, signerWeb3: any, - publicKeysData: string, txOptions: any = {} ) => { const validator = (await validatorWeb3.eth.getAccounts())[0] @@ -150,18 +149,33 @@ describe('governance tests', () => { ).contracts.getAccounts()).generateProofOfSigningKeyPossession(validator, signer) const validatorKit = newKitFromWeb3(validatorWeb3) const validatorAccounts = await validatorKit._web3Contracts.getAccounts() - const tx = validatorAccounts.methods.authorizeValidatorSigner( - signer, - publicKeysData, - pop.v, - pop.r, - pop.s - ) + const tx = validatorAccounts.methods.authorizeValidatorSigner(signer, pop.v, pop.r, pop.s) let gas = txOptions.gas if (!gas) { gas = await tx.estimateGas({ ...txOptions }) } - return tx.send({ from: validator, ...txOptions, gas }) + await tx.send({ from: validator, ...txOptions, gas }) + } + + const updateValidatorBlsKey = async ( + validatorWeb3: any, + signerWeb3: any, + signerPrivateKey: string, + txOptions: any = {} + ) => { + const validator = (await validatorWeb3.eth.getAccounts())[0] + const signer = (await signerWeb3.eth.getAccounts())[0] + await unlockAccount(signer, signerWeb3) + const blsPublicKey = getBlsPublicKey(signerPrivateKey) + const blsPop = getBlsPop(validator, signerPrivateKey) + const signerKit = newKitFromWeb3(signerWeb3) + const signerValidators = await validatorKit._web3Contracts.getValidators() + const tx = signerValidators.methods.updateBlsKey(blsPublicKey, blsPop) + let gas = txOptions.gas + if (!gas) { + gas = await tx.estimateGas({ ...txOptions }) + } + await tx.send({ from: signer, ...txOptions, gas }) } const isLastBlockOfEpoch = (blockNumber: number, epochSize: number) => { @@ -251,10 +265,7 @@ describe('governance tests', () => { // Prepare for key rotation. const validatorWeb3 = new Web3('http://localhost:8549') const authorizedWeb3s = [new Web3('ws://localhost:8559'), new Web3('ws://localhost:8561')] - const authorizedPublicKeysData = [ - getPublicKeysData(rotation0PrivateKey), - getPublicKeysData(rotation1PrivateKey), - ] + const authorizedPrivateKeys = [rotation0PrivateKey, rotation1PrivateKey] let index = 0 let errorWhileChangingValidatorSet = '' @@ -276,10 +287,11 @@ describe('governance tests', () => { assert.notInclude(newMembers, memberToRemove) // 2. Rotate keys for validator 2 by authorizing a new validating key. if (!doneAuthorizing) { - await authorizeValidatorSigner( + await authorizeValidatorSigner(validatorWeb3, authorizedWeb3s[index]) + await updateValidatorBlsKey( validatorWeb3, authorizedWeb3s[index], - authorizedPublicKeysData[index] + authorizedPrivateKeys[index] ) } doneAuthorizing = doneAuthorizing || index === 1 diff --git a/packages/cli/src/commands/account/authorize.test.ts b/packages/cli/src/commands/account/authorize.test.ts index a81169d88f5..fada8a235c7 100644 --- a/packages/cli/src/commands/account/authorize.test.ts +++ b/packages/cli/src/commands/account/authorize.test.ts @@ -1,13 +1,11 @@ import Web3 from 'web3' import { testWithGanache } from '../../test-utils/ganache-test' import Authorize from './authorize' +import Register from './register' process.env.NO_SYNCCHECK = 'true' testWithGanache('account:authorize cmd', (web3: Web3) => { - // TODO(asa): Fix this test once CLI command for authorizing signer for registered validator - // is supported. - /* test('can authorize account', async () => { const accounts = await web3.eth.getAccounts() await Register.run(['--from', accounts[0]]) @@ -22,7 +20,6 @@ testWithGanache('account:authorize cmd', (web3: Web3) => { '0x1b9fca4bbb5bfb1dbe69ef1cddbd9b4202dcb6b134c5170611e1e36ecfa468d7b46c85328d504934fce6c2a1571603a50ae224d2b32685e84d4d1a1eebad8452eb', ]) }) - */ test('fails if from is not an account', async () => { const accounts = await web3.eth.getAccounts() diff --git a/packages/cli/src/commands/account/authorize.ts b/packages/cli/src/commands/account/authorize.ts index be995562e6e..4878f7e4013 100644 --- a/packages/cli/src/commands/account/authorize.ts +++ b/packages/cli/src/commands/account/authorize.ts @@ -4,7 +4,6 @@ import { newCheckBuilder } from '../../utils/checks' import { displaySendTx } from '../../utils/cli' import { Flags } from '../../utils/command' -// TODO: Support authorizing a validator signer when a validator is registered. export default class Authorize extends BaseCommand { static description = 'Authorize an attestation, validator, or vote signer' diff --git a/packages/cli/src/commands/validator/register.ts b/packages/cli/src/commands/validator/register.ts index bc50dd33dd6..19d899c0495 100644 --- a/packages/cli/src/commands/validator/register.ts +++ b/packages/cli/src/commands/validator/register.ts @@ -1,8 +1,8 @@ +import { addressToPublicKey } from '@celo/lib/address' import { BaseCommand } from '../../base' import { newCheckBuilder } from '../../utils/checks' import { displaySendTx } from '../../utils/cli' import { Flags } from '../../utils/command' -import { getPubKeyFromAddrAndWeb3 } from '../../utils/helpers' export default class ValidatorRegister extends BaseCommand { static description = 'Register a new Validator' @@ -10,12 +10,15 @@ export default class ValidatorRegister extends BaseCommand { static flags = { ...BaseCommand.flags, from: Flags.address({ required: true, description: 'Address for the Validator' }), - publicKey: Flags.publicKey({ required: true }), + ecdsaKey: Flags.ecdsaPublicKey({ required: true }), + blsKey: Flags.blsPublicKey({ required: true }), + blsPop: Flags.blsProofOfPossession({ required: true }), } static examples = [ - 'register --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --publicKey 0xc52f3fab06e22a54915a8765c4f6826090cfac5e40282b43844bf1c0df83aaa632e55b67869758f2291d1aabe0ebecc7cbf4236aaa45e3e0cfbf997eda082ae19d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d7405011220a66a6257562d0c26dabf64485a1d96bad27bb1c0fd6080a75b0ec9f75b50298a2a8e04b02b2688c8104fca61fb00', + 'register --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --ecdsaKey 0xc52f3fab06e22a54915a8765c4f6826090cfac5e40282b43844bf1c0df83aaa632e55b67869758f2291d1aabe0ebecc7cbf4236aaa45e3e0cfbf997eda082ae1 --blsKey 0x9d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae300 --blsPop 0x05d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d7405011220a66a6257562d0c26dabf64485a1d96bad27bb1c0fd6080a75b0ec9f75b50298a2a8e04b02b2688c8104fca61fb00', ] + async run() { const res = this.parse(ValidatorRegister) this.kit.defaultAccount = res.flags.from @@ -31,12 +34,16 @@ export default class ValidatorRegister extends BaseCommand { await displaySendTx( 'registerValidator', - validators.registerValidator(res.flags.publicKey as any) + validators.registerValidator( + res.flags.ecdsaKey as any, + res.flags.blsKey as any, + res.flags.blsPop as any + ) ) // register encryption key on accounts contract // TODO: Use a different key data encryption - const pubKey = await getPubKeyFromAddrAndWeb3(res.flags.from, this.web3) + const pubKey = await addressToPublicKey(res.flags.from, this.web3) // TODO fix typing const setKeyTx = accounts.setAccountDataEncryptionKey(pubKey as any) await displaySendTx('Set encryption key', setKeyTx) diff --git a/packages/cli/src/commands/validator/update-bls-key.ts b/packages/cli/src/commands/validator/update-bls-key.ts new file mode 100644 index 00000000000..cfe11ab125a --- /dev/null +++ b/packages/cli/src/commands/validator/update-bls-key.ts @@ -0,0 +1,43 @@ +import { BaseCommand } from '../../base' +import { newCheckBuilder } from '../../utils/checks' +import { displaySendTx } from '../../utils/cli' +import { Flags } from '../../utils/command' +import { getPubKeyFromAddrAndWeb3 } from '../../utils/helpers' + +export default class ValidatorPublicKey extends BaseCommand { + static description = 'Manage BLS public key data for a validator' + + static flags = { + ...BaseCommand.flags, + from: Flags.address({ required: true, description: "Validator's address" }), + publicKey: Flags.publicKey({ required: true }), + } + + static examples = [ + 'publickey --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --publicKey 0xc52f3fab06e22a54915a8765c4f6826090cfac5e40282b43844bf1c0df83aaa632e55b67869758f2291d1aabe0ebecc7cbf4236aaa45e3e0cfbf997eda082ae19d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d7405011220a66a6257562d0c26dabf64485a1d96bad27bb1c0fd6080a75b0ec9f75b50298a2a8e04b02b2688c8104fca61fb00', + ] + async run() { + const res = this.parse(ValidatorPublicKey) + this.kit.defaultAccount = res.flags.from + const validators = await this.kit.contracts.getValidators() + const accounts = await this.kit.contracts.getAccounts() + + await newCheckBuilder(this, res.flags.from) + .isSignerOrAccount() + .canSignValidatorTxs() + .signerAccountIsValidator() + .runChecks() + + await displaySendTx( + 'updatePublicKeysData', + validators.updatePublicKeysData(res.flags.publicKey as any) + ) + + // register encryption key on accounts contract + // TODO: Use a different key data encryption + const pubKey = await getPubKeyFromAddrAndWeb3(res.flags.from, this.web3) + // TODO fix typing + const setKeyTx = accounts.setAccountDataEncryptionKey(pubKey as any) + await displaySendTx('Set encryption key', setKeyTx) + } +} diff --git a/packages/cli/src/utils/command.ts b/packages/cli/src/utils/command.ts index c1ad8113bfa..fc6acc0dd05 100644 --- a/packages/cli/src/utils/command.ts +++ b/packages/cli/src/utils/command.ts @@ -4,14 +4,24 @@ import { IArg, ParseFn } from '@oclif/parser/lib/args' import { pathExistsSync } from 'fs-extra' import Web3 from 'web3' -const parsePublicKey: ParseFn = (input) => { - // Check that the string starts with 0x and has byte length of ecdsa pub key (64 bytes) + bls pub key (48 bytes) + proof of pos (96 bytes) - if (Web3.utils.isHex(input) && input.length === 418 && input.startsWith('0x')) { +const parseBytes = (input, length, msg) => { + // Check that the string starts with 0x and has byte length of `length`. + if (Web3.utils.isHex(input) && input.length === length && input.startsWith('0x')) { return input } else { - throw new CLIError(`${input} is not a public key`) + throw new CLIError(msg) } } + +const parseEcdsaPublicKey: ParseFn = (input) => { + parseBytes(input, 64, `${input} is not an ECDSA public key`) +} +const parseBlsPublicKey: ParseFn = (input) => { + parseBytes(input, 48, `${input} is not a BLS public key`) +} +const parseBlsProofOfPossession: ParseFn = (input) => { + parseBytes(input, 96, `${input} is not a BLS proof-of-possession`) +} const parseAddress: ParseFn = (input) => { if (Web3.utils.isAddress(input)) { return input @@ -58,9 +68,19 @@ export const Flags = { description: 'Account Address', helpValue: '0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d', }), - publicKey: flags.build({ - parse: parsePublicKey, - description: 'Public Key', + ecdsaPublicKey: flags.build({ + parse: parseEcdsaPublicKey, + description: 'ECDSA Public Key', + helpValue: '0x', + }), + blsPublicKey: flags.build({ + parse: parseBlsPublicKey, + description: 'BLS Public Key', + helpValue: '0x', + }), + blsProofOfPossession: flags.build({ + parse: parseBlsProofOfPossession, + description: 'BLS Proof-of-Possession', helpValue: '0x', }), url: flags.build({ diff --git a/packages/cli/src/utils/helpers.ts b/packages/cli/src/utils/helpers.ts index d95c1354c57..54ed4047ecd 100644 --- a/packages/cli/src/utils/helpers.ts +++ b/packages/cli/src/utils/helpers.ts @@ -1,29 +1,8 @@ import { eqAddress } from '@celo/utils/lib/address' -import ethjsutil from 'ethereumjs-util' import Web3 from 'web3' import { Block } from 'web3/eth/types' import { failWith } from './cli' -import assert = require('assert') - -export async function getPubKeyFromAddrAndWeb3(addr: string, web3: Web3) { - const msg = new Buffer('dummy_msg_data') - const data = '0x' + msg.toString('hex') - // Note: Eth.sign typing displays incorrect parameter order - const sig = await web3.eth.sign(data, addr) - - const rawsig = ethjsutil.fromRpcSig(sig) - - const prefix = new Buffer('\x19Ethereum Signed Message:\n') - const prefixedMsg = ethjsutil.sha3(Buffer.concat([prefix, new Buffer(String(msg.length)), msg])) - const pubKey = ethjsutil.ecrecover(prefixedMsg, rawsig.v, rawsig.r, rawsig.s) - - const computedAddr = ethjsutil.pubToAddress(pubKey).toString('hex') - assert(eqAddress(computedAddr, addr), 'computed address !== addr') - - return pubKey -} - export async function nodeIsSynced(web3: Web3): Promise { if (process.env.NO_SYNCCHECK) { return true diff --git a/packages/contractkit/src/wrappers/Accounts.ts b/packages/contractkit/src/wrappers/Accounts.ts index 756bb12bb5f..967b7593161 100644 --- a/packages/contractkit/src/wrappers/Accounts.ts +++ b/packages/contractkit/src/wrappers/Accounts.ts @@ -125,32 +125,17 @@ export class AccountsWrapper extends BaseWrapper { */ async authorizeValidatorSigner( signer: Address, - proofOfSigningKeyPossession: Signature, - proofOfBlsKeyPossession?: string + proofOfSigningKeyPossession: Signature ): Promise> { - if (proofOfBlsKeyPossession) { - return toTransactionObject( - this.kit, - this.contract.methods.authorizeValidatorSigner( - signer, - proofOfSigningKeyPossession.v, - proofOfSigningKeyPossession.r, - proofOfSigningKeyPossession.s - ) - ) - } else { - return toTransactionObject( - this.kit, - this.contract.methods.authorizeValidatorSigner( - signer, - proofOfBlsKeyPossession, - proofOfSigningKeyPossession.v, - proofOfSigningKeyPossession.r, - // @ts-ignore Typechain doesn't handle function overloading. - proofOfSigningKeyPossession.s - ) + return toTransactionObject( + this.kit, + this.contract.methods.authorizeValidatorSigner( + signer, + proofOfSigningKeyPossession.v, + proofOfSigningKeyPossession.r, + proofOfSigningKeyPossession.s ) - } + ) } async generateProofOfSigningKeyPossession(account: Address, signer: Address) { diff --git a/packages/contractkit/src/wrappers/Validators.test.ts b/packages/contractkit/src/wrappers/Validators.test.ts index c8b0dfcde1c..d41ecec59bd 100644 --- a/packages/contractkit/src/wrappers/Validators.test.ts +++ b/packages/contractkit/src/wrappers/Validators.test.ts @@ -21,8 +21,6 @@ const blsPublicKey = const blsPoP = '9d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d740501' -const publicKeysData = '0x' + publicKey + blsPublicKey + blsPoP - testWithGanache('Validators Wrapper', (web3) => { const kit = newKitFromWeb3(web3) let accounts: string[] = [] @@ -57,7 +55,11 @@ testWithGanache('Validators Wrapper', (web3) => { await validators .registerValidator( // @ts-ignore - publicKeysData + publicKey, + // @ts-ignore + blsPublicKey, + // @ts-ignore + blsPoP ) .sendAndWaitForReceipt({ from: validatorAccount }) } diff --git a/packages/contractkit/src/wrappers/Validators.ts b/packages/contractkit/src/wrappers/Validators.ts index d74aa62c976..2b06cf384e6 100644 --- a/packages/contractkit/src/wrappers/Validators.ts +++ b/packages/contractkit/src/wrappers/Validators.ts @@ -18,7 +18,8 @@ import { export interface Validator { address: Address - publicKey: string + ecdsaKey: string + blsKey: string affiliation: string | null score: BigNumber } @@ -102,6 +103,20 @@ export class ValidatorsWrapper extends BaseWrapper { return accounts.activeValidatorSignerToAccount(signerAddress) } + /** + * Updates a validator's BLS key. + * @param blsKey The BLS public key that the validator is using for consensus, should pass proof + * of possession. 48 bytes. + * @param blsPop The BLS public key proof-of-possession, which consists of a signature on the + * account address. 96 bytes. + * @return True upon success. + */ + updateBlsKey: (blsKey: string, blsPop: string) => CeloTransactionObject = proxySend( + this.kit, + this.contract.methods.updateBlsKey, + tupleParser(parseBytes) + ) + /** * Returns whether a particular account has a registered validator. * @param account The account. @@ -146,9 +161,10 @@ export class ValidatorsWrapper extends BaseWrapper { const res = await this.contract.methods.getValidator(address).call() return { address, - publicKey: res[0] as any, - affiliation: res[1], - score: fromFixed(new BigNumber(res[2])), + ecdsaKey: res[0] as any, + blsKey: res[1] as any, + affiliation: res[2], + score: fromFixed(new BigNumber(res[3])), } } @@ -213,17 +229,20 @@ export class ValidatorsWrapper extends BaseWrapper { * Registers a validator unaffiliated with any validator group. * * Fails if the account is already a validator or validator group. - * Fails if the account does not have sufficient weight. * - * @param publicKeysData Comprised of three tightly-packed elements: - * - publicKey - The public key that the validator is using for consensus, should match - * msg.sender. 64 bytes. - * - blsPublicKey - The BLS public key that the validator is using for consensus, should pass - * proof of possession. 48 bytes. - * - blsPoP - The BLS public key proof of possession. 96 bytes. + * @param ecdsaKey The ECDSA public key that the validator is using for consensus, should match + * the validator signer. 64 bytes. + * @param blsKey The BLS public key that the validator is using for consensus, should pass proof + * of possession. 48 bytes. + * @param blsPop The BLS public key proof-of-possession, which consists of a signature on the + * account address. 96 bytes. */ - registerValidator: (publicKeysData: string) => CeloTransactionObject = proxySend( + registerValidator: ( + ecdsaKey: string, + blsKey: string, + blsPop: string + ) => CeloTransactionObject = proxySend( this.kit, this.contract.methods.registerValidator, tupleParser(parseBytes) diff --git a/packages/protocol/contracts/common/Accounts.sol b/packages/protocol/contracts/common/Accounts.sol index d45dd584879..1bb4b182720 100644 --- a/packages/protocol/contracts/common/Accounts.sol +++ b/packages/protocol/contracts/common/Accounts.sol @@ -9,16 +9,8 @@ import "./interfaces/IAccounts.sol"; import "../common/Initializable.sol"; import "../common/Signatures.sol"; import "../common/UsingRegistry.sol"; -import "../common/UsingPrecompiles.sol"; - -contract Accounts is - IAccounts, - Ownable, - ReentrancyGuard, - Initializable, - UsingRegistry, - UsingPrecompiles -{ + +contract Accounts is IAccounts, Ownable, ReentrancyGuard, Initializable, UsingRegistry { using SafeMath for uint256; struct Signers { @@ -172,36 +164,10 @@ contract Accounts is Account storage account = accounts[msg.sender]; authorize(signer, v, r, s); account.signers.validator = signer; - // Registered validators must update their BLS public key data when updating their validator - // signer. - require(!getValidators().isValidator(msg.sender)); - emit ValidatorSignerAuthorized(msg.sender, signer); - } - /** - * @notice Authorizes an address to sign consensus messages on behalf of the account. - * @param signer The address of the signing key to authorize. - * @param publicKeysData Comprised of three tightly-packed elements: - * - publicKey - The public key that the validator is using for consensus, should match - * `signer`. 64 bytes. - * - blsPublicKey - The BLS public key that the validator is using for consensus, should pass - * proof of possession. 48 bytes. - * - blsPoP - The BLS public key proof of possession. 96 bytes. - * @param v The recovery id of the incoming ECDSA signature. - * @param r Output value r of the ECDSA signature. - * @param s Output value s of the ECDSA signature. - * @dev v, r, s constitute `signer`'s signature on `msg.sender`. - */ - function authorizeValidatorSigner( - address signer, - bytes calldata publicKeysData, - uint8 v, - bytes32 r, - bytes32 s - ) external nonReentrant { - Account storage account = accounts[msg.sender]; - authorize(signer, v, r, s); - account.signers.validator = signer; - require(getValidators().updatePublicKeysData(msg.sender, signer, publicKeysData)); + IValidators validators = getValidators(); + if (validators.isValidator(msg.sender)) { + require(getValidators().updateEcdsaKey(msg.sender, signer, v, r, s)); + } emit ValidatorSignerAuthorized(msg.sender, signer); } diff --git a/packages/protocol/contracts/common/UsingPrecompiles.sol b/packages/protocol/contracts/common/UsingPrecompiles.sol index 4b93a055c67..a384db571ea 100644 --- a/packages/protocol/contracts/common/UsingPrecompiles.sol +++ b/packages/protocol/contracts/common/UsingPrecompiles.sol @@ -61,6 +61,62 @@ contract UsingPrecompiles { return (returnNumerator, returnDenominator); } + /** + * @notice Recover the public key of a signed message. + * @param messageHash The hash of a message. + * @param v The recovery id of the incoming ECDSA signature. + * @param r Output value r of the ECDSA signature. + * @param s Output value s of the ECDSA signature. + * @return numerator/denominator of the computed quantity (not reduced). + */ + function ecrecoverPublicKey(bytes32 messageHash, uint8 v, bytes32 r, bytes32 s) + public + returns (bytes memory) + { + /* + bytes publicKey; + // solhint-disable-next-line no-inline-assembly + assembly { + let newCallDataPosition := mload(0x40) + mstore(0x40, add(newCallDataPosition, calldatasize)) + mstore(newCallDataPosition, messageHash) + mstore(add(newCallDataPosition, 1), v) + mstore(add(newCallDataPosition, 33), r) + mstore(add(newCallDataPosition, 65), s) + let success := staticcall( + 3000, // estimated gas cost for this function + 0xfe, + newCallDataPosition, + 0x61, // input size, 3 * 32 + 1 = 97 bytes + 0, + 0 + ) + + let returnDataSize := returndatasize + let returnDataPosition := mload(0x40) + mstore(0x40, add(returnDataPosition, returnDataSize)) + returndatacopy(returnDataPosition, 0, returnDataSize) + + switch success + case 0 { + revert(returnDataPosition, returnDataSize) + } + default { + returnNumerator := mload(returnDataPosition) + returnDenominator := mload(add(returnDataPosition, 32)) + } + } + return (returnNumerator, returnDenominator); + */ + bool success; + bytes memory publicKey; + (success, publicKey) = address(0xfe).call.gas(gasleft())( + abi.encodePacked(messageHash, v, r, s) + ); + require(success); + return publicKey; + } + /** * @notice Returns the current epoch size in blocks. * @return The current epoch size in blocks. @@ -123,16 +179,19 @@ contract UsingPrecompiles { /** * @notice Checks a BLS proof of possession. - * @param proofOfPossessionBytes The public key and signature of the proof of possession. + * @param blsKey The BLS public key that the validator is using for consensus, should pass proof + * of possession. 48 bytes. + * @param blsPop The BLS public key proof-of-possession, which consists of a signature on the + * account address. 96 bytes. * @return True upon success. */ - function checkProofOfPossession(address sender, bytes memory proofOfPossessionBytes) + function checkProofOfPossession(address sender, bytes memory blsKey, bytes memory blsPop) public returns (bool) { bool success; (success, ) = PROOF_OF_POSSESSION.call.value(0).gas(gasleft())( - abi.encodePacked(sender, proofOfPossessionBytes) + abi.encodePacked(sender, blsKey, blsPop) ); return success; } diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index b1414c2f379..27291f7e7c4 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -81,8 +81,13 @@ contract Validators is uint256 lastRemovedFromGroupTimestamp; } + struct PublicKeys { + bytes ecdsa; + bytes bls; + } + struct Validator { - bytes publicKeysData; + PublicKeys keys; address affiliation; FixidityLib.Fraction score; MembershipHistory membershipHistory; @@ -110,11 +115,12 @@ contract Validators is event GroupLockedGoldRequirementsSet(uint256 value, uint256 duration); event ValidatorLockedGoldRequirementsSet(uint256 value, uint256 duration); event MembershipHistoryLengthSet(uint256 length); - event ValidatorRegistered(address indexed validator, bytes publicKeysData); + event ValidatorRegistered(address indexed validator, bytes ecdsaKey, bytes blsKey); event ValidatorDeregistered(address indexed validator); event ValidatorAffiliated(address indexed validator, address indexed group); event ValidatorDeaffiliated(address indexed validator, address indexed group); - event ValidatorPublicKeysDataUpdated(address indexed validator, bytes publicKeysData); + event ValidatorEcdsaKeyUpdated(address indexed validator, bytes ecdsaKey); + event ValidatorBlsKeyUpdated(address indexed validator, bytes blsKey); event ValidatorGroupRegistered(address indexed group, uint256 commission); event ValidatorGroupDeregistered(address indexed group); event ValidatorGroupMemberAdded(address indexed group, address indexed validator); @@ -254,27 +260,32 @@ contract Validators is /** * @notice Registers a validator unaffiliated with any validator group. - * @param publicKeysData Comprised of three tightly-packed elements: - * - publicKey - The public key that the validator is using for consensus, should match - * msg.sender. 64 bytes. - * - blsPublicKey - The BLS public key that the validator is using for consensus, should pass - * proof of possession. 48 bytes. - * - blsPoP - The BLS public key proof of possession. 96 bytes. + * @param ecdsaKey The ECDSA public key that the validator is using for consensus, should match + * the validator signer. 64 bytes. + * @param blsKey The BLS public key that the validator is using for consensus, should pass proof + * of possession. 48 bytes. + * @param blsPop The BLS public key proof-of-possession, which consists of a signature on the + * account address. 96 bytes. * @return True upon success. * @dev Fails if the account is already a validator or validator group. * @dev Fails if the account does not have sufficient Locked Gold. */ - function registerValidator(bytes calldata publicKeysData) external nonReentrant returns (bool) { + function registerValidator(bytes calldata ecdsaKey, bytes calldata blsKey, bytes calldata blsPop) + external + nonReentrant + returns (bool) + { address account = getAccounts().activeValidatorSignerToAccount(msg.sender); require(!isValidator(account) && !isValidatorGroup(account)); uint256 lockedGoldBalance = getLockedGold().getAccountTotalLockedGold(account); require(lockedGoldBalance >= validatorLockedGoldRequirements.value); Validator storage validator = validators[account]; address signer = getAccounts().getValidatorSigner(account); - _updatePublicKeysData(validator, signer, publicKeysData); + _updateEcdsaKey(validator, signer, ecdsaKey); + _updateBlsKey(validator, account, blsKey, blsPop); registeredValidators.push(account); updateMembershipHistory(account, address(0)); - emit ValidatorRegistered(account, publicKeysData); + emit ValidatorRegistered(account, ecdsaKey, blsKey); return true; } @@ -466,96 +477,90 @@ contract Validators is } /** - * @notice Updates a validator's public keys data. - * @param account The validator whose public keys data should be updated. - * @param signer The address used to sign consensus message. Coupled to the BLS key. - * @param publicKeysData Comprised of three tightly-packed elements: - * - publicKey - The public key that the validator is using for consensus, should match - * `signer`. 64 bytes. - * - blsPublicKey - The BLS public key that the validator is using for consensus, should pass - * proof of possession. 48 bytes. - * - blsPoP - The BLS public key proof of possession. 96 bytes. - * @dev Called by the registered `Accounts` contract when a validator signer is authorized. + * @notice Updates a validator's BLS key. + * @param blsKey The BLS public key that the validator is using for consensus, should pass proof + * of possession. 48 bytes. + * @param blsPop The BLS public key proof-of-possession, which consists of a signature on the + * account address. 96 bytes. * @return True upon success. */ - function updatePublicKeysData(address account, address signer, bytes calldata publicKeysData) - external - onlyRegisteredContract(ACCOUNTS_REGISTRY_ID) - returns (bool) - { + function updateBlsKey(bytes calldata blsKey, bytes calldata blsPop) external returns (bool) { + address account = getAccounts().activeValidatorSignerToAccount(msg.sender); require(isValidator(account)); Validator storage validator = validators[account]; - _updatePublicKeysData(validator, signer, publicKeysData); - emit ValidatorPublicKeysDataUpdated(account, publicKeysData); + _updateBlsKey(validator, account, blsKey, blsPop); + emit ValidatorBlsKeyUpdated(account, blsKey); return true; } /** - * @notice Updates a validator's public keys data. + * @notice Updates a validator's BLS key. * @param validator The validator whose public keys data should be updated. - * @param signer The address used to sign consensus message. Coupled to the BLS key. - * @param publicKeysData Comprised of three tightly-packed elements: - * - publicKey - The public key that the validator is using for consensus, should match - * `signer`. 64 bytes. - * - blsPublicKey - The BLS public key that the validator is using for consensus, should pass - * proof of possession. 48 bytes. - * - blsPoP - The BLS public key proof of possession. 96 bytes. + * @param account The address under which the validator is registered. + * @param blsKey The BLS public key that the validator is using for consensus, should pass proof + * of possession. 48 bytes. + * @param blsPop The BLS public key proof-of-possession, which consists of a signature on the + * account address. 96 bytes. * @return True upon success. */ - function _updatePublicKeysData( + function _updateBlsKey( Validator storage validator, - address signer, - bytes memory publicKeysData + address account, + bytes memory blsKey, + bytes memory blsPop ) private returns (bool) { - // secp256k1 public key + BLS public key + BLS proof of possession - require(publicKeysData.length == (64 + 48 + 96)); + require(blsKey.length == 48); + require(blsPop.length == 96); // Use the proof of possession bytes - require(checkProofOfPossession(signer, publicKeysData.slice(64, 48 + 96))); - validator.publicKeysData = publicKeysData; + // TODO: Should this be the `account` or `signer` key here? + require(checkProofOfPossession(account, blsKey, blsPop)); + validator.keys.bls = blsKey; return true; } /** - * @notice Updates a validator's public keys data. - * @param publicKeysData Comprised of three tightly-packed elements: - * - publicKey - The public key that the validator is using for consensus, should match - * msg.sender. 64 bytes. - * - blsPublicKey - The BLS public key that the validator is using for consensus, should pass - * proof of possession. 48 bytes. - * - blsPoP - The BLS public key proof of possession. 96 bytes. + * @notice Updates a validator's ECDSA key. + * @param account The address under which the validator is registered. + * @param v The recovery id of the incoming ECDSA signature. + * @param r Output value r of the ECDSA signature. + * @param s Output value s of the ECDSA signature. + * @dev v, r, s constitute the ECDSA key's signature on `account`. * @return True upon success. */ - function updatePublicKeysData(bytes calldata publicKeysData) external returns (bool) { - address account = getAccounts().activeValidatorSignerToAccount(msg.sender); + function updateEcdsaKey(address account, address signer, uint8 v, bytes32 r, bytes32 s) + external + onlyRegisteredContract(ACCOUNTS_REGISTRY_ID) + returns (bool) + { require(isValidator(account)); Validator storage validator = validators[account]; - _updatePublicKeysData(validator, publicKeysData); - emit ValidatorPublicKeysDataUpdated(account, publicKeysData); + bytes32 addressHash = keccak256(abi.encodePacked(account)); + bytes memory prefix = "\x19Ethereum Signed Message:\n32"; + bytes32 prefixedHash = keccak256(abi.encodePacked(prefix, addressHash)); + bytes memory ecdsaKey = ecrecoverPublicKey(prefixedHash, v, r, s); + require(_updateEcdsaKey(validator, signer, ecdsaKey)); + emit ValidatorEcdsaKeyUpdated(account, ecdsaKey); return true; } /** - * @notice Updates a validator's public keys data. + * @notice Updates a validator's ECDSA key. * @param validator The validator whose public keys data should be updated. - * @param publicKeysData Comprised of three tightly-packed elements: - * - publicKey - The public key that the validator is using for consensus, should match - * msg.sender. 64 bytes. - * - blsPublicKey - The BLS public key that the validator is using for consensus, should pass - * proof of possession. 48 bytes. - * - blsPoP - The BLS public key proof of possession. 96 bytes. + * @param signer The address with which the validator is signing consensus messages. + * @param ecdsaKey The ECDSA public key that the validator is using for consensus. Should match + * `signer`. 64 bytes. * @return True upon success. */ - function _updatePublicKeysData(Validator storage validator, bytes memory publicKeysData) + function _updateEcdsaKey(Validator storage validator, address signer, bytes memory ecdsaKey) private returns (bool) { + require(ecdsaKey.length == 64); require( - // secp256k1 public key + BLS public key + BLS proof of possession - publicKeysData.length == (64 + 48 + 96) + address(uint256(keccak256(ecdsaKey)) >> 96) == signer, + "ECDSA key does not match signer" ); - // Use the proof of possession bytes - require(checkProofOfPossession(msg.sender, publicKeysData.slice(64, 48 + 96))); - validator.publicKeysData = publicKeysData; + validator.keys.ecdsa = ecdsaKey; return true; } @@ -760,7 +765,7 @@ contract Validators is function getValidatorFromSigner(address signer) external view - returns (bytes memory publicKeysData, address affiliation, uint256 score) + returns (bytes memory ecdsaKey, bytes memory blsKey, address affiliation, uint256 score) { address account = getAccounts().validatorSignerToAccount(signer); return getValidator(account); @@ -774,11 +779,16 @@ contract Validators is function getValidator(address account) public view - returns (bytes memory publicKeysData, address affiliation, uint256 score) + returns (bytes memory ecdsaKey, bytes memory blsKey, address affiliation, uint256 score) { require(isValidator(account)); Validator storage validator = validators[account]; - return (validator.publicKeysData, validator.affiliation, validator.score.unwrap()); + return ( + validator.keys.ecdsa, + validator.keys.bls, + validator.affiliation, + validator.score.unwrap() + ); } /** @@ -910,7 +920,7 @@ contract Validators is * @return Whether a particular address is a registered validator. */ function isValidator(address account) public view returns (bool) { - return validators[account].publicKeysData.length > 0; + return validators[account].keys.bls.length > 0; } /** diff --git a/packages/protocol/contracts/governance/interfaces/IValidators.sol b/packages/protocol/contracts/governance/interfaces/IValidators.sol index 90c50258cec..273748ccc3b 100644 --- a/packages/protocol/contracts/governance/interfaces/IValidators.sol +++ b/packages/protocol/contracts/governance/interfaces/IValidators.sol @@ -7,6 +7,6 @@ interface IValidators { function getGroupsNumMembers(address[] calldata) external view returns (uint256[] memory); function getNumRegisteredValidators() external view returns (uint256); function getTopGroupValidators(address, uint256) external view returns (address[] memory); + function updateEcdsaKey(address, address, uint8, bytes32, bytes32) external returns (bool); function isValidator(address) external view returns (bool); - function updatePublicKeysData(address, address, bytes calldata) external returns (bool); } diff --git a/packages/protocol/contracts/governance/test/MockValidators.sol b/packages/protocol/contracts/governance/test/MockValidators.sol index 51b30ef4275..c2daa76eda9 100644 --- a/packages/protocol/contracts/governance/test/MockValidators.sol +++ b/packages/protocol/contracts/governance/test/MockValidators.sol @@ -6,7 +6,6 @@ import "../interfaces/IValidators.sol"; * @title Holds a list of addresses of validators */ contract MockValidators is IValidators { - mapping(address => bool) public isValidator; mapping(address => uint256) private numGroupMembers; mapping(address => uint256) private lockedGoldRequirements; mapping(address => bool) private doesNotMeetAccountLockedGoldRequirements; @@ -14,12 +13,7 @@ contract MockValidators is IValidators { uint256 private numRegisteredValidators; mapping(address => bytes) public publicKeysData; - function updatePublicKeysData(address account, address, bytes calldata data) - external - returns (bool) - { - require(isValidator[account]); - publicKeysData[account] = data; + function updateEcdsaKey(address, address, uint8, bytes32, bytes32) external returns (bool) { return true; } @@ -35,10 +29,6 @@ contract MockValidators is IValidators { return members[group].length; } - function setValidator(address account) external { - isValidator[account] = true; - } - function setNumRegisteredValidators(uint256 value) external { numRegisteredValidators = value; } diff --git a/packages/protocol/migrations/20_elect_validators.ts b/packages/protocol/migrations/20_elect_validators.ts index c735b204771..f3ab8eece13 100644 --- a/packages/protocol/migrations/20_elect_validators.ts +++ b/packages/protocol/migrations/20_elect_validators.ts @@ -6,7 +6,7 @@ import { } from '@celo/protocol/lib/web3-utils' import { config } from '@celo/protocol/migrationsConfig' import { privateKeyToAddress, privateKeyToPublicKey } from '@celo/utils/lib/address' -import { getPublicKeysData } from '@celo/utils/lib/bls' +import { getBlsPublicKey, getBlsPoP } from '@celo/utils/lib/bls' import { toFixed } from '@celo/utils/lib/fixidity' import { BigNumber } from 'bignumber.js' import { AccountsInstance, ElectionInstance, LockedGoldInstance, ValidatorsInstance } from 'types' @@ -99,8 +99,6 @@ async function registerValidator( index: number, networkName: string ) { - const publicKeysData = getPublicKeysData(validatorPrivateKey) - await lockGold( accounts, lockedGold, @@ -114,8 +112,12 @@ async function registerValidator( to: accounts.address, }) + const publicKey = privateKeyToPublicKey(validatorPrivateKey) + const blsPublicKey = getBlsPublicKey(validatorPrivateKey) + const blsPoP = getBlsPoP(validatorPrivateKey) + // @ts-ignore - const registerTx = validators.contract.methods.registerValidator(publicKeysData) + const registerTx = validators.contract.methods.registerValidator(publicKey, blsPublicKey, blsPoP) await sendTransactionWithPrivateKey(web3, registerTx, validatorPrivateKey, { to: validators.address, diff --git a/packages/protocol/test/common/accounts.ts b/packages/protocol/test/common/accounts.ts index bf7a6095031..1088953bddf 100644 --- a/packages/protocol/test/common/accounts.ts +++ b/packages/protocol/test/common/accounts.ts @@ -353,56 +353,6 @@ contract('Accounts', (accounts: string[]) => { }) }) - // authorizeValidatoSigner deviates slightly from the other authorize*Signer functions. - // This block tests those deviations. - describe('#authorizeValidatorSigner', async () => { - const authorized = accounts[1] - // Arbitrary hex string as MockValidators does not verify this info. - const publicKeysData = '0x02f2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e01611111111' - let sig - - beforeEach(async () => { - await accountsInstance.createAccount() - sig = await getParsedSignatureOfAddress(web3, account, authorized) - }) - - describe('when a validator has not been registered', () => { - it('should succeed when no public keys data is passed', async () => { - await accountsInstance.authorizeValidatorSigner(authorized, sig.v, sig.r, sig.s) - }) - - it('should revert when public keys data is passed', async () => { - await assertRevert( - accountsInstance.authorizeValidatorSigner(authorized, publicKeysData, sig.v, sig.r, sig.s) - ) - }) - }) - - describe('when a validator has been registered', () => { - beforeEach(async () => { - await mockValidators.setValidator(account) - }) - - it('should revert when no public keys data is passed', async () => { - await assertRevert( - accountsInstance.authorizeValidatorSigner(authorized, sig.v, sig.r, sig.s) - ) - }) - - it('should succeed when public keys data is passed', async () => { - await accountsInstance.authorizeValidatorSigner( - authorized, - publicKeysData, - sig.v, - sig.r, - sig.s - ) - // @ts-ignore Typechain can't handle 'bytes' type. - assert.equal(await mockValidators.publicKeysData(account), publicKeysData) - }) - }) - }) - Object.keys(authorizationTestDescriptions).forEach((key) => { describe('authorization tests:', () => { let authorizationTest: any diff --git a/packages/protocol/test/governance/validators.ts b/packages/protocol/test/governance/validators.ts index 374bab5877d..c39bd299be2 100644 --- a/packages/protocol/test/governance/validators.ts +++ b/packages/protocol/test/governance/validators.ts @@ -11,6 +11,7 @@ import { timeTravel, } from '@celo/protocol/lib/test-utils' import { fixed1, fromFixed, toFixed } from '@celo/utils/lib/fixidity' +import { addressToPublicKey } from '@celo/utils/lib/address' import BigNumber from 'bignumber.js' import { AccountsContract, @@ -40,9 +41,10 @@ Validators.numberFormat = 'BigNumber' const parseValidatorParams = (validatorParams: any) => { return { - publicKeysData: validatorParams[0], - affiliation: validatorParams[1], - score: validatorParams[2], + ecdsaKey: validatorParams[0], + blsKey: validatorParams[1], + affiliation: validatorParams[2], + score: validatorParams[3], } } @@ -91,13 +93,10 @@ contract('Validators', (accounts: string[]) => { const maxGroupSize = new BigNumber(5) // A random 64 byte hex string. - const publicKey = - 'ea0733ad275e2b9e05541341a97ee82678c58932464fad26164657a111a7e37a9fa0300266fb90e2135a1f1512350cb4e985488a88809b14e3cbe415e76e82b2' const blsPublicKey = - '4d23d8cd06f30b1fa7cf368e2f5399ab04bb6846c682f493a98a607d3dfb7e53a712bb79b475c57b0ac2785460f91301' + '0x4d23d8cd06f30b1fa7cf368e2f5399ab04bb6846c682f493a98a607d3dfb7e53a712bb79b475c57b0ac2785460f91301' const blsPoP = - '9d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d740501' - const publicKeysData = '0x' + publicKey + blsPublicKey + blsPoP + '0x9d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d740501' const commission = toFixed(1 / 100) beforeEach(async () => { accountsInstance = await Accounts.new() @@ -129,9 +128,14 @@ contract('Validators', (accounts: string[]) => { const registerValidator = async (validator: string) => { await mockLockedGold.setAccountTotalLockedGold(validator, validatorLockedGoldRequirements.value) + const publicKey = await addressToPublicKey(validator, web3) await validators.registerValidator( // @ts-ignore bytes type - publicKeysData, + publicKey, + // @ts-ignore bytes type + blsPublicKey, + // @ts-ignore bytes type + blsPoP, { from: validator } ) } @@ -521,9 +525,10 @@ contract('Validators', (accounts: string[]) => { }) }) - describe('#registerValidator', () => { + describe.only('#registerValidator', () => { const validator = accounts[0] let resp: any + let publicKey: string describe('when the account is not a registered validator', () => { beforeEach(async () => { await mockLockedGold.setAccountTotalLockedGold( @@ -538,9 +543,14 @@ contract('Validators', (accounts: string[]) => { const signer = accounts[9] const sig = await getParsedSignatureOfAddress(web3, validator, signer) await accountsInstance.authorizeValidatorSigner(signer, sig.v, sig.r, sig.s) + publicKey = await addressToPublicKey(validator, web3) resp = await validators.registerValidator( // @ts-ignore bytes type - publicKeysData + publicKey, + // @ts-ignore bytes type + blsPublicKey, + // @ts-ignore bytes type + blsPoP ) const blockNumber = (await web3.eth.getBlock('latest')).number validatorRegistrationEpochNumber = Math.floor(blockNumber / EPOCH) @@ -554,9 +564,14 @@ contract('Validators', (accounts: string[]) => { assert.deepEqual(await validators.getRegisteredValidators(), [validator]) }) - it('should set the validator public key', async () => { + it('should set the validator ecdsa public key', async () => { + const parsedValidator = parseValidatorParams(await validators.getValidator(validator)) + assert.equal(parsedValidator.ecdsaKey, publicKey) + }) + + it('should set the validator bls public key', async () => { const parsedValidator = parseValidatorParams(await validators.getValidator(validator)) - assert.equal(parsedValidator.publicKeysData, publicKeysData) + assert.equal(parsedValidator.blsKey, blsPublicKey) }) it('should set account locked gold requirements', async () => { @@ -583,7 +598,8 @@ contract('Validators', (accounts: string[]) => { event: 'ValidatorRegistered', args: { validator, - publicKeysData, + ecdsaKey: publicKey, + blsKey: blsPublicKey, }, }) }) @@ -597,14 +613,25 @@ contract('Validators', (accounts: string[]) => { validatorLockedGoldRequirements.value ) // @ts-ignore bytes type - await validators.registerValidator(publicKeysData) + await validators.registerValidator( + // @ts-ignore bytes type + publicKey, + // @ts-ignore bytes type + blsPublicKey, + // @ts-ignore bytes type + blsPoP + ) }) it('should revert', async () => { await assertRevert( validators.registerValidator( // @ts-ignore bytes type - publicKeysData + publicKey, + // @ts-ignore bytes type + blsPublicKey, + // @ts-ignore bytes type + blsPoP ) ) }) @@ -620,7 +647,11 @@ contract('Validators', (accounts: string[]) => { await assertRevert( validators.registerValidator( // @ts-ignore bytes type - publicKeysData + publicKey, + // @ts-ignore bytes type + blsPublicKey, + // @ts-ignore bytes type + blsPoP ) ) }) @@ -638,7 +669,11 @@ contract('Validators', (accounts: string[]) => { await assertRevert( validators.registerValidator( // @ts-ignore bytes type - publicKeysData + publicKey, + // @ts-ignore bytes type + blsPublicKey, + // @ts-ignore bytes type + blsPoP ) ) }) @@ -1030,53 +1065,51 @@ contract('Validators', (accounts: string[]) => { }) }) - describe('#updatePublicKeysData()', () => { - const newPublicKey = web3.utils.randomHex(64).slice(2) - const newBlsPublicKey = web3.utils.randomHex(48).slice(2) - const newBlsPoP = web3.utils.randomHex(96).slice(2) - const newPublicKeysData = '0x' + newPublicKey + newBlsPublicKey + newBlsPoP + describe('#updateBlsKey()', () => { + const newBlsPublicKey = web3.utils.randomHex(48) + const newBlsPoP = web3.utils.randomHex(96) describe('when called by a registered validator', () => { const validator = accounts[0] beforeEach(async () => { await registerValidator(validator) }) - describe('when the public keys data is the right length', () => { + describe('when the keys are the right length', () => { let resp: any beforeEach(async () => { // @ts-ignore Broken typechain typing for bytes - resp = await validators.updatePublicKeysData(newPublicKeysData) + resp = await validators.updateBlsKey(newBlsPublicKey, newBlsPoP) }) - it('should set the validator public keys data', async () => { + it('should set the validator bls public key', async () => { const parsedValidator = parseValidatorParams(await validators.getValidator(validator)) - assert.equal(parsedValidator.publicKeysData, newPublicKeysData) + assert.equal(parsedValidator.blsKey, newBlsPublicKey) }) - it('should emit the ValidatorPublicKeysDataUpdated event', async () => { + it('should emit the ValidatorBlsKeyUpdated event', async () => { assert.equal(resp.logs.length, 1) const log = resp.logs[0] assertContainSubset(log, { - event: 'ValidatorPublicKeysDataUpdated', + event: 'ValidatorBlsKeyUpdated', args: { validator, - publicKeysData: newPublicKeysData, + blsKey: newBlsPublicKey, }, }) }) }) - describe('when the public keys data is too long', () => { + describe('when the public key is not 48 bytes', () => { it('should revert', async () => { // @ts-ignore Broken typechain typing for bytes - await assertRevert(validators.updatePublicKeysData(newPublicKeysData + '00')) + await assertRevert(validators.updateBlsKey(newBlsPublicKey + '01', newBlsPoP)) }) }) - describe('when the public keys data is too short', () => { + describe('when the proof of possession is not 96 bytes', () => { it('should revert', async () => { // @ts-ignore Broken typechain typing for bytes - await assertRevert(validators.updatePublicKeysData(newPublicKeysData.slice(0, -2))) + await assertRevert(validators.updateBlsKey(newBlsPublicKey, newBlsPoP + '01')) }) }) }) diff --git a/packages/utils/src/address.ts b/packages/utils/src/address.ts index c7d4b782ecd..d1f6121a1e9 100644 --- a/packages/utils/src/address.ts +++ b/packages/utils/src/address.ts @@ -1,7 +1,35 @@ -import { privateToAddress, privateToPublic, toChecksumAddress } from 'ethereumjs-util' +import { + ecrecover, + fromRpcSig, + pubToAddress, + privateToAddress, + privateToPublic, + sha3, + toChecksumAddress, +} from 'ethereumjs-util' +import Web3 from 'web3' +import assert = require('assert') export type Address = string +export async function addressToPublicKey(address: string, web3: Web3) { + const msg = new Buffer('dummy_msg_data') + const data = '0x' + msg.toString('hex') + // Note: Eth.sign typing displays incorrect parameter order + const sig = await web3.eth.sign(data, address) + + const rawsig = fromRpcSig(sig) + + const prefix = new Buffer('\x19Ethereum Signed Message:\n') + const prefixedMsg = sha3(Buffer.concat([prefix, new Buffer(String(msg.length)), msg])) + const pubKey = ecrecover(prefixedMsg, rawsig.v, rawsig.r, rawsig.s) + + const computedAddr = pubToAddress(pubKey).toString('hex') + assert(eqAddress(computedAddr, address), 'computed address !== address') + + return '0x' + pubKey.toString('hex') +} + export function eqAddress(a: Address, b: Address) { return a.replace('0x', '').toLowerCase() === b.replace('0x', '').toLowerCase() } diff --git a/packages/utils/src/bls.ts b/packages/utils/src/bls.ts index 5dc1c78d7a0..73b2ebd8f8f 100644 --- a/packages/utils/src/bls.ts +++ b/packages/utils/src/bls.ts @@ -3,7 +3,7 @@ const keccak256 = require('keccak256') const BigInteger = require('bigi') const reverse = require('buffer-reverse') import * as bls12377js from 'bls12377js' -import { privateKeyToAddress, privateKeyToPublicKey } from './address' +import { privateKeyToAddress } from './address' const n = BigInteger.fromHex('12ab655e9a2ca55660b44d1e5c37b00159aa76fed00000010a11800000000001', 16) @@ -37,16 +37,21 @@ export const blsPrivateKeyToProcessedPrivateKey = (privateKeyHex: string) => { throw new Error("couldn't derive BLS key from ECDSA key") } -export const getPublicKeysData = (privateKeyHex: string) => { + +const getBlsPrivateKey = (privateKeyHex: string) => { + const blsPrivateKeyBytes = blsPrivateKeyToProcessedPrivateKey(privateKeyHex.slice(2)) + return blsPrivateKeyBytes +} + +export const getBlsPublicKey = (privateKeyHex: string) => { + const blsPrivateKeyBytes = getBlsPrivateKey(privateKeyHex) + return bls12377js.BLS.privateToPublicBytes(blsPrivateKeyBytes).toString('hex') +} + +export const getBlsPoP = (privateKeyHex: string) => { + const blsPrivateKeyBytes = getBlsPrivateKey(privateKeyHex) const address = privateKeyToAddress(privateKeyHex) - const publicKey = privateKeyToPublicKey(privateKeyHex) - const blsValidatorPrivateKeyBytes = blsPrivateKeyToProcessedPrivateKey(privateKeyHex.slice(2)) - const blsPublicKey = bls12377js.BLS.privateToPublicBytes(blsValidatorPrivateKeyBytes).toString( + return bls12377js.BLS.signPoP(blsPrivateKeyBytes, Buffer.from(address.slice(2), 'hex')).toString( 'hex' ) - const blsPoP = bls12377js.BLS.signPoP( - blsValidatorPrivateKeyBytes, - Buffer.from(address.slice(2), 'hex') - ).toString('hex') - return publicKey + blsPublicKey + blsPoP } From 73730fb319eabc8f9f02c2ceb33fa2c12771e54d Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Mon, 11 Nov 2019 18:11:50 -0800 Subject: [PATCH 131/149] Most unit tests passing --- .../src/e2e-tests/governance_tests.ts | 4 +- .../src/commands/validator/update-bls-key.ts | 23 +++---- packages/contractkit/src/base.ts | 2 +- .../contractkit/src/web3-contract-cache.ts | 6 +- .../contractkit/src/wrappers/Validators.ts | 4 +- .../contracts/governance/Validators.sol | 2 +- .../migrations/20_elect_validators.ts | 2 +- .../protocol/test/governance/validators.ts | 65 +++++++++++++++++-- packages/utils/src/bls.ts | 4 +- 9 files changed, 76 insertions(+), 36 deletions(-) diff --git a/packages/celotool/src/e2e-tests/governance_tests.ts b/packages/celotool/src/e2e-tests/governance_tests.ts index e9a81e2466e..004d7e69184 100644 --- a/packages/celotool/src/e2e-tests/governance_tests.ts +++ b/packages/celotool/src/e2e-tests/governance_tests.ts @@ -167,9 +167,9 @@ describe('governance tests', () => { const signer = (await signerWeb3.eth.getAccounts())[0] await unlockAccount(signer, signerWeb3) const blsPublicKey = getBlsPublicKey(signerPrivateKey) - const blsPop = getBlsPop(validator, signerPrivateKey) + const blsPop = getBlsPoP(validator, signerPrivateKey) const signerKit = newKitFromWeb3(signerWeb3) - const signerValidators = await validatorKit._web3Contracts.getValidators() + const signerValidators = await signerKit._web3Contracts.getValidators() const tx = signerValidators.methods.updateBlsKey(blsPublicKey, blsPop) let gas = txOptions.gas if (!gas) { diff --git a/packages/cli/src/commands/validator/update-bls-key.ts b/packages/cli/src/commands/validator/update-bls-key.ts index cfe11ab125a..1b2e1a0aa55 100644 --- a/packages/cli/src/commands/validator/update-bls-key.ts +++ b/packages/cli/src/commands/validator/update-bls-key.ts @@ -2,22 +2,22 @@ import { BaseCommand } from '../../base' import { newCheckBuilder } from '../../utils/checks' import { displaySendTx } from '../../utils/cli' import { Flags } from '../../utils/command' -import { getPubKeyFromAddrAndWeb3 } from '../../utils/helpers' -export default class ValidatorPublicKey extends BaseCommand { - static description = 'Manage BLS public key data for a validator' +export default class ValidatorUpdateBlsKey extends BaseCommand { + static description = 'Update BLS key for a validator' static flags = { ...BaseCommand.flags, from: Flags.address({ required: true, description: "Validator's address" }), - publicKey: Flags.publicKey({ required: true }), + blsKey: Flags.blsPublicKey({ required: true }), + blsPop: Flags.blsProofOfPossession({ required: true }), } static examples = [ - 'publickey --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --publicKey 0xc52f3fab06e22a54915a8765c4f6826090cfac5e40282b43844bf1c0df83aaa632e55b67869758f2291d1aabe0ebecc7cbf4236aaa45e3e0cfbf997eda082ae19d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d7405011220a66a6257562d0c26dabf64485a1d96bad27bb1c0fd6080a75b0ec9f75b50298a2a8e04b02b2688c8104fca61fb00', + 'update-bls-key --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --blsKey 0x9d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae300 --blsPop 0x05d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d7405011220a66a6257562d0c26dabf64485a1d96bad27bb1c0fd6080a75b0ec9f75b50298a2a8e04b02b2688c8104fca61fb00', ] async run() { - const res = this.parse(ValidatorPublicKey) + const res = this.parse(ValidatorUpdateBlsKey) this.kit.defaultAccount = res.flags.from const validators = await this.kit.contracts.getValidators() const accounts = await this.kit.contracts.getAccounts() @@ -29,15 +29,8 @@ export default class ValidatorPublicKey extends BaseCommand { .runChecks() await displaySendTx( - 'updatePublicKeysData', - validators.updatePublicKeysData(res.flags.publicKey as any) + 'updateBlsKey', + validators.updateBlsKey(res.flags.blsKey as any, res.flags.blsPop as any) ) - - // register encryption key on accounts contract - // TODO: Use a different key data encryption - const pubKey = await getPubKeyFromAddrAndWeb3(res.flags.from, this.web3) - // TODO fix typing - const setKeyTx = accounts.setAccountDataEncryptionKey(pubKey as any) - await displaySendTx('Set encryption key', setKeyTx) } } diff --git a/packages/contractkit/src/base.ts b/packages/contractkit/src/base.ts index e16116338a4..9ebd96abc9b 100644 --- a/packages/contractkit/src/base.ts +++ b/packages/contractkit/src/base.ts @@ -6,7 +6,7 @@ export enum CeloContract { BlockchainParameters = 'BlockchainParameters', Election = 'Election', EpochRewards = 'EpochRewards', - // Escrow = 'Escrow', + Escrow = 'Escrow', Exchange = 'Exchange', GasCurrencyWhitelist = 'GasCurrencyWhitelist', GasPriceMinimum = 'GasPriceMinimum', diff --git a/packages/contractkit/src/web3-contract-cache.ts b/packages/contractkit/src/web3-contract-cache.ts index 83f8a3f3262..2eab4113c6e 100644 --- a/packages/contractkit/src/web3-contract-cache.ts +++ b/packages/contractkit/src/web3-contract-cache.ts @@ -5,7 +5,7 @@ import { newAttestations } from './generated/Attestations' import { newBlockchainParameters } from './generated/BlockchainParameters' import { newElection } from './generated/Election' import { newEpochRewards } from './generated/EpochRewards' -// import { newEscrow } from './generated/Escrow' +import { newEscrow } from './generated/Escrow' import { newExchange } from './generated/Exchange' import { newGasCurrencyWhitelist } from './generated/GasCurrencyWhitelist' import { newGasPriceMinimum } from './generated/GasPriceMinimum' @@ -28,7 +28,7 @@ const ContractFactories = { [CeloContract.BlockchainParameters]: newBlockchainParameters, [CeloContract.Election]: newElection, [CeloContract.EpochRewards]: newEpochRewards, - // [CeloContract.Escrow]: newEscrow, + [CeloContract.Escrow]: newEscrow, [CeloContract.Exchange]: newExchange, [CeloContract.GasCurrencyWhitelist]: newGasCurrencyWhitelist, [CeloContract.GasPriceMinimum]: newGasPriceMinimum, @@ -73,11 +73,9 @@ export class Web3ContractCache { getEpochRewards() { return this.getContract(CeloContract.EpochRewards) } - /* getEscrow() { return this.getContract(CeloContract.Escrow) } - */ getExchange() { return this.getContract(CeloContract.Exchange) } diff --git a/packages/contractkit/src/wrappers/Validators.ts b/packages/contractkit/src/wrappers/Validators.ts index 2b06cf384e6..6b006b56729 100644 --- a/packages/contractkit/src/wrappers/Validators.ts +++ b/packages/contractkit/src/wrappers/Validators.ts @@ -114,7 +114,7 @@ export class ValidatorsWrapper extends BaseWrapper { updateBlsKey: (blsKey: string, blsPop: string) => CeloTransactionObject = proxySend( this.kit, this.contract.methods.updateBlsKey, - tupleParser(parseBytes) + tupleParser(parseBytes, parseBytes) ) /** @@ -245,7 +245,7 @@ export class ValidatorsWrapper extends BaseWrapper { ) => CeloTransactionObject = proxySend( this.kit, this.contract.methods.registerValidator, - tupleParser(parseBytes) + tupleParser(parseBytes, parseBytes, parseBytes) ) /** diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index 27291f7e7c4..c8815801802 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -557,7 +557,7 @@ contract Validators is { require(ecdsaKey.length == 64); require( - address(uint256(keccak256(ecdsaKey)) >> 96) == signer, + address(uint160(uint256(keccak256(ecdsaKey)))) == signer, "ECDSA key does not match signer" ); validator.keys.ecdsa = ecdsaKey; diff --git a/packages/protocol/migrations/20_elect_validators.ts b/packages/protocol/migrations/20_elect_validators.ts index f3ab8eece13..31e8fb4d2c6 100644 --- a/packages/protocol/migrations/20_elect_validators.ts +++ b/packages/protocol/migrations/20_elect_validators.ts @@ -114,7 +114,7 @@ async function registerValidator( const publicKey = privateKeyToPublicKey(validatorPrivateKey) const blsPublicKey = getBlsPublicKey(validatorPrivateKey) - const blsPoP = getBlsPoP(validatorPrivateKey) + const blsPoP = getBlsPoP(privateKeyToAddress(validatorPrivateKey), validatorPrivateKey) // @ts-ignore const registerTx = validators.contract.methods.registerValidator(publicKey, blsPublicKey, blsPoP) diff --git a/packages/protocol/test/governance/validators.ts b/packages/protocol/test/governance/validators.ts index c39bd299be2..355a7eef27e 100644 --- a/packages/protocol/test/governance/validators.ts +++ b/packages/protocol/test/governance/validators.ts @@ -525,10 +525,9 @@ contract('Validators', (accounts: string[]) => { }) }) - describe.only('#registerValidator', () => { + describe('#registerValidator', () => { const validator = accounts[0] let resp: any - let publicKey: string describe('when the account is not a registered validator', () => { beforeEach(async () => { await mockLockedGold.setAccountTotalLockedGold( @@ -539,11 +538,12 @@ contract('Validators', (accounts: string[]) => { describe('when the account has authorized a validator signer', () => { let validatorRegistrationEpochNumber: number + let publicKey: string beforeEach(async () => { const signer = accounts[9] const sig = await getParsedSignatureOfAddress(web3, validator, signer) await accountsInstance.authorizeValidatorSigner(signer, sig.v, sig.r, sig.s) - publicKey = await addressToPublicKey(validator, web3) + publicKey = await addressToPublicKey(signer, web3) resp = await validators.registerValidator( // @ts-ignore bytes type publicKey, @@ -607,12 +607,16 @@ contract('Validators', (accounts: string[]) => { }) describe('when the account is already a registered validator ', () => { + let publicKey: string beforeEach(async () => { await mockLockedGold.setAccountTotalLockedGold( validator, validatorLockedGoldRequirements.value ) - // @ts-ignore bytes type + }) + + it('should revert', async () => { + publicKey = await addressToPublicKey(validator, web3) await validators.registerValidator( // @ts-ignore bytes type publicKey, @@ -621,9 +625,6 @@ contract('Validators', (accounts: string[]) => { // @ts-ignore bytes type blsPoP ) - }) - - it('should revert', async () => { await assertRevert( validators.registerValidator( // @ts-ignore bytes type @@ -644,6 +645,7 @@ contract('Validators', (accounts: string[]) => { }) it('should revert', async () => { + const publicKey = await addressToPublicKey(validator, web3) await assertRevert( validators.registerValidator( // @ts-ignore bytes type @@ -666,6 +668,7 @@ contract('Validators', (accounts: string[]) => { }) it('should revert', async () => { + const publicKey = await addressToPublicKey(validator, web3) await assertRevert( validators.registerValidator( // @ts-ignore bytes type @@ -1065,6 +1068,54 @@ contract('Validators', (accounts: string[]) => { }) }) + /* + TODO(asa): Restore once ganache supports this precompile. + describe('#updateEcdsaKey()', () => { + let sig: any + let newPublicKey: string + describe('when called by a registered validator', () => { + const validator = accounts[0] + const signer = accounts[9] + beforeEach(async () => { + await registerValidator(validator) + newPublicKey = await addressToPublicKey(signer, web3) + }) + + describe('when called by the registered `Accounts` contract', () => { + beforeEach(async () => { + await registry.setAddressFor(CeloContractName.Accounts, accounts[0]) + }) + + describe('when the signature matches the `signer`', () => { + let resp: any + beforeEach(async () => { + const sig = await getParsedSignatureOfAddress(web3, validator, signer) + // @ts-ignore Broken typechain typing for bytes + resp = await validators.updateEcdsaKey(validator, signer, sig.v, sig.r, sig.s) + }) + + it('should set the validator ecdsa public key', async () => { + const parsedValidator = parseValidatorParams(await validators.getValidator(validator)) + assert.equal(parsedValidator.ecdsaKey, newPublicKey) + }) + + it('should emit the ValidatorEcdsaKeyUpdated event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorEcdsaKeyUpdated', + args: { + validator, + ecdsaKey: newPublicKey, + }, + }) + }) + }) + }) + }) + }) + */ + describe('#updateBlsKey()', () => { const newBlsPublicKey = web3.utils.randomHex(48) const newBlsPoP = web3.utils.randomHex(96) diff --git a/packages/utils/src/bls.ts b/packages/utils/src/bls.ts index 73b2ebd8f8f..e9fa245a3cb 100644 --- a/packages/utils/src/bls.ts +++ b/packages/utils/src/bls.ts @@ -3,7 +3,6 @@ const keccak256 = require('keccak256') const BigInteger = require('bigi') const reverse = require('buffer-reverse') import * as bls12377js from 'bls12377js' -import { privateKeyToAddress } from './address' const n = BigInteger.fromHex('12ab655e9a2ca55660b44d1e5c37b00159aa76fed00000010a11800000000001', 16) @@ -48,9 +47,8 @@ export const getBlsPublicKey = (privateKeyHex: string) => { return bls12377js.BLS.privateToPublicBytes(blsPrivateKeyBytes).toString('hex') } -export const getBlsPoP = (privateKeyHex: string) => { +export const getBlsPoP = (address: string, privateKeyHex: string) => { const blsPrivateKeyBytes = getBlsPrivateKey(privateKeyHex) - const address = privateKeyToAddress(privateKeyHex) return bls12377js.BLS.signPoP(blsPrivateKeyBytes, Buffer.from(address.slice(2), 'hex')).toString( 'hex' ) From 036d9343f2cc39df794c99c27c1e34dc6bd4b4f3 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Mon, 11 Nov 2019 22:53:35 -0800 Subject: [PATCH 132/149] WIP --- .../src/e2e-tests/governance_tests.ts | 21 +++++++++++++------ .../contracts/common/UsingPrecompiles.sol | 5 ++--- .../contracts/governance/Validators.sol | 11 +++++----- packages/utils/src/bls.ts | 7 ++++--- 4 files changed, 27 insertions(+), 17 deletions(-) diff --git a/packages/celotool/src/e2e-tests/governance_tests.ts b/packages/celotool/src/e2e-tests/governance_tests.ts index 004d7e69184..b2ead9a3c29 100644 --- a/packages/celotool/src/e2e-tests/governance_tests.ts +++ b/packages/celotool/src/e2e-tests/governance_tests.ts @@ -1,5 +1,6 @@ import { ContractKit, newKitFromWeb3 } from '@celo/contractkit' import { getBlsPublicKey, getBlsPoP } from '@celo/utils/lib/bls' +import { privateKeyToPublicKey } from '@celo/utils/lib/address' import { fromFixed, toFixed } from '@celo/utils/lib/fixidity' import BigNumber from 'bignumber.js' import { assert } from 'chai' @@ -13,6 +14,7 @@ import { sleep, } from './utils' +// TODO(asa): Test independent rotation of ecdsa, bls keys. describe('governance tests', () => { const gethConfig = { migrate: true, @@ -154,7 +156,7 @@ describe('governance tests', () => { if (!gas) { gas = await tx.estimateGas({ ...txOptions }) } - await tx.send({ from: validator, ...txOptions, gas }) + return tx.send({ from: validator, ...txOptions, gas }) } const updateValidatorBlsKey = async ( @@ -168,21 +170,22 @@ describe('governance tests', () => { await unlockAccount(signer, signerWeb3) const blsPublicKey = getBlsPublicKey(signerPrivateKey) const blsPop = getBlsPoP(validator, signerPrivateKey) - const signerKit = newKitFromWeb3(signerWeb3) - const signerValidators = await signerKit._web3Contracts.getValidators() - const tx = signerValidators.methods.updateBlsKey(blsPublicKey, blsPop) + // TODO(asa): Send this from the signer instead. + const validatorKit = newKitFromWeb3(validatorWeb3) + const validatorValidators = await validatorKit._web3Contracts.getValidators() + const tx = validatorValidators.methods.updateBlsKey(blsPublicKey, blsPop) let gas = txOptions.gas if (!gas) { gas = await tx.estimateGas({ ...txOptions }) } - await tx.send({ from: signer, ...txOptions, gas }) + return tx.send({ from: validator, ...txOptions, gas }) } const isLastBlockOfEpoch = (blockNumber: number, epochSize: number) => { return blockNumber % epochSize === 0 } - describe('when the validator set is changing', () => { + describe.only('when the validator set is changing', () => { let epoch: number const blockNumbers: number[] = [] let validatorAccounts: string[] @@ -266,6 +269,8 @@ describe('governance tests', () => { const validatorWeb3 = new Web3('http://localhost:8549') const authorizedWeb3s = [new Web3('ws://localhost:8559'), new Web3('ws://localhost:8561')] const authorizedPrivateKeys = [rotation0PrivateKey, rotation1PrivateKey] + console.log('pubKey0', privateKeyToPublicKey(rotation0PrivateKey)) + console.log('pubKey1', privateKeyToPublicKey(rotation1PrivateKey)) let index = 0 let errorWhileChangingValidatorSet = '' @@ -280,14 +285,18 @@ describe('governance tests', () => { // 1. Swap validator0 and validator1 so one is a member of the group and the other is not. const memberToRemove = membersToSwap[index] const memberToAdd = membersToSwap[(index + 1) % 2] + console.log('removing member') await removeMember(groupWeb3, memberToRemove) + console.log('adding member') await addMember(groupWeb3, memberToAdd) const newMembers = await getValidatorGroupMembers() assert.include(newMembers, memberToAdd) assert.notInclude(newMembers, memberToRemove) // 2. Rotate keys for validator 2 by authorizing a new validating key. if (!doneAuthorizing) { + console.log('authorizing signer') await authorizeValidatorSigner(validatorWeb3, authorizedWeb3s[index]) + console.log('updating bls key') await updateValidatorBlsKey( validatorWeb3, authorizedWeb3s[index], diff --git a/packages/protocol/contracts/common/UsingPrecompiles.sol b/packages/protocol/contracts/common/UsingPrecompiles.sol index a384db571ea..9dd38d83521 100644 --- a/packages/protocol/contracts/common/UsingPrecompiles.sol +++ b/packages/protocol/contracts/common/UsingPrecompiles.sol @@ -110,9 +110,7 @@ contract UsingPrecompiles { */ bool success; bytes memory publicKey; - (success, publicKey) = address(0xfe).call.gas(gasleft())( - abi.encodePacked(messageHash, v, r, s) - ); + (success, publicKey) = address(0xfe).call.gas(gasleft())(abi.encode(messageHash, v, r, s)); require(success); return publicKey; } @@ -179,6 +177,7 @@ contract UsingPrecompiles { /** * @notice Checks a BLS proof of possession. + * @param sender The address signed by the BLS key to generate the proof of possession. * @param blsKey The BLS public key that the validator is using for consensus, should pass proof * of possession. 48 bytes. * @param blsPop The BLS public key proof-of-possession, which consists of a signature on the diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index c8815801802..3a0d53ab8da 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -758,17 +758,18 @@ contract Validators is } /** - * @notice Returns validator information. + * @notice Returns the validator BLS key. * @param signer The account that registered the validator or its authorized signing address. - * @return The unpacked validator struct. + * @return The validator BLS key. */ - function getValidatorFromSigner(address signer) + function getValidatorBlsKeyFromSigner(address signer) external view - returns (bytes memory ecdsaKey, bytes memory blsKey, address affiliation, uint256 score) + returns (bytes memory blsKey) { address account = getAccounts().validatorSignerToAccount(signer); - return getValidator(account); + require(isValidator(account)); + return validators[account].keys.bls; } /** diff --git a/packages/utils/src/bls.ts b/packages/utils/src/bls.ts index e9fa245a3cb..80df761d426 100644 --- a/packages/utils/src/bls.ts +++ b/packages/utils/src/bls.ts @@ -44,12 +44,13 @@ const getBlsPrivateKey = (privateKeyHex: string) => { export const getBlsPublicKey = (privateKeyHex: string) => { const blsPrivateKeyBytes = getBlsPrivateKey(privateKeyHex) - return bls12377js.BLS.privateToPublicBytes(blsPrivateKeyBytes).toString('hex') + return '0x' + bls12377js.BLS.privateToPublicBytes(blsPrivateKeyBytes).toString('hex') } export const getBlsPoP = (address: string, privateKeyHex: string) => { const blsPrivateKeyBytes = getBlsPrivateKey(privateKeyHex) - return bls12377js.BLS.signPoP(blsPrivateKeyBytes, Buffer.from(address.slice(2), 'hex')).toString( - 'hex' + return ( + '0x' + + bls12377js.BLS.signPoP(blsPrivateKeyBytes, Buffer.from(address.slice(2), 'hex')).toString('hex') ) } From 41f0dcf67c21b1e22fa36aaad8aee18dce4cd4bd Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Tue, 12 Nov 2019 08:12:41 -0800 Subject: [PATCH 133/149] end-to-end governance test passing --- .../src/e2e-tests/governance_tests.ts | 26 +++++++------------ 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/packages/celotool/src/e2e-tests/governance_tests.ts b/packages/celotool/src/e2e-tests/governance_tests.ts index b2ead9a3c29..56ef7bb0d37 100644 --- a/packages/celotool/src/e2e-tests/governance_tests.ts +++ b/packages/celotool/src/e2e-tests/governance_tests.ts @@ -269,8 +269,6 @@ describe('governance tests', () => { const validatorWeb3 = new Web3('http://localhost:8549') const authorizedWeb3s = [new Web3('ws://localhost:8559'), new Web3('ws://localhost:8561')] const authorizedPrivateKeys = [rotation0PrivateKey, rotation1PrivateKey] - console.log('pubKey0', privateKeyToPublicKey(rotation0PrivateKey)) - console.log('pubKey1', privateKeyToPublicKey(rotation1PrivateKey)) let index = 0 let errorWhileChangingValidatorSet = '' @@ -285,18 +283,14 @@ describe('governance tests', () => { // 1. Swap validator0 and validator1 so one is a member of the group and the other is not. const memberToRemove = membersToSwap[index] const memberToAdd = membersToSwap[(index + 1) % 2] - console.log('removing member') await removeMember(groupWeb3, memberToRemove) - console.log('adding member') await addMember(groupWeb3, memberToAdd) const newMembers = await getValidatorGroupMembers() assert.include(newMembers, memberToAdd) assert.notInclude(newMembers, memberToRemove) // 2. Rotate keys for validator 2 by authorizing a new validating key. if (!doneAuthorizing) { - console.log('authorizing signer') await authorizeValidatorSigner(validatorWeb3, authorizedWeb3s[index]) - console.log('updating bls key') await updateValidatorBlsKey( validatorWeb3, authorizedWeb3s[index], @@ -410,22 +404,22 @@ describe('governance tests', () => { const assertScoreUnchanged = async (validator: string, blockNumber: number) => { const score = new BigNumber( - (await validators.methods.getValidator(validator).call({}, blockNumber))[3] + (await validators.methods.getValidator(validator).call({}, blockNumber)).score ) const previousScore = new BigNumber( - (await validators.methods.getValidator(validator).call({}, blockNumber - 1))[3] + (await validators.methods.getValidator(validator).call({}, blockNumber - 1)).score ) - assert.isNotNaN(score) - assert.isNotNaN(previousScore) + assert.isFalse(score.isNaN()) + assert.isFalse(previousScore.isNaN()) assert.equal(score.toFixed(), previousScore.toFixed()) } const assertScoreChanged = async (validator: string, blockNumber: number) => { const score = new BigNumber( - (await validators.methods.getValidator(validator).call({}, blockNumber))[2] + (await validators.methods.getValidator(validator).call({}, blockNumber)).score ) const previousScore = new BigNumber( - (await validators.methods.getValidator(validator).call({}, blockNumber - 1))[2] + (await validators.methods.getValidator(validator).call({}, blockNumber - 1)).score ) const expectedScore = adjustmentSpeed .times(uptime) @@ -474,8 +468,8 @@ describe('governance tests', () => { const previousBalance = new BigNumber( await stableToken.methods.balanceOf(validator).call({}, blockNumber - 1) ) - assert.isNotNaN(currentBalance) - assert.isNotNaN(previousBalance) + assert.isFalse(currentBalance.isNaN()) + assert.isFalse(previousBalance.isNaN()) assertAlmostEqual(currentBalance.minus(previousBalance), expected) } @@ -485,9 +479,9 @@ describe('governance tests', () => { const getExpectedTotalPayment = async (validator: string, blockNumber: number) => { const score = new BigNumber( - (await validators.methods.getValidator(validator).call({}, blockNumber))[2] + (await validators.methods.getValidator(validator).call({}, blockNumber)).score ) - assert.isNotNaN(score) + assert.isFalse(score.isNaN()) // We need to calculate the rewards multiplier for the previous block, before // the rewards actually are awarded. const rewardsMultiplier = new BigNumber( From c29f51346d78b5a6f0bd6fea92322c2f0be32dbe Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Tue, 12 Nov 2019 11:22:03 -0800 Subject: [PATCH 134/149] Remove ecrecoverpublickey precompile --- .circleci/config.yml | 14 ++-- .../src/e2e-tests/governance_tests.ts | 3 +- .../cli/src/commands/account/claims.test.ts | 2 +- .../cli/src/commands/validator/register.ts | 2 +- .../src/commands/validator/update-bls-key.ts | 2 - packages/cli/src/utils/command.ts | 8 +- packages/cli/src/utils/helpers.ts | 1 - .../contractkit/src/wrappers/Accounts.test.ts | 83 +++++++++++++++++++ packages/contractkit/src/wrappers/Accounts.ts | 41 +++++++-- .../src/wrappers/Validators.test.ts | 9 +- .../protocol/contracts/common/Accounts.sol | 28 ++++++- .../contracts/common/UsingPrecompiles.sol | 54 ------------ .../contracts/governance/Validators.sol | 12 +-- .../governance/interfaces/IValidators.sol | 2 +- .../governance/test/MockValidators.sol | 8 +- .../migrations/20_elect_validators.ts | 2 +- packages/utils/src/address.ts | 4 +- packages/utils/src/signatureUtils.ts | 10 +++ 18 files changed, 182 insertions(+), 103 deletions(-) create mode 100644 packages/contractkit/src/wrappers/Accounts.test.ts diff --git a/.circleci/config.yml b/.circleci/config.yml index 97a9a7acd55..8bdbc718d11 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -524,7 +524,7 @@ jobs: command: | set -e cd packages/celotool - ./ci_test_transfers.sh checkout master + ./ci_test_transfers.sh checkout asaj/key-rotation-plus-enode end-to-end-geth-blockchain-parameters-test: <<: *e2e-defaults @@ -542,7 +542,7 @@ jobs: command: | set -e cd packages/celotool - ./ci_test_blockchain_parameters.sh checkout master + ./ci_test_blockchain_parameters.sh checkout asaj/key-rotation-plus-enode end-to-end-geth-governance-test: <<: *e2e-defaults @@ -562,7 +562,7 @@ jobs: command: | set -e cd packages/celotool - ./ci_test_governance.sh checkout master + ./ci_test_governance.sh checkout asaj/key-rotation-plus-enode end-to-end-geth-sync-test: <<: *e2e-defaults @@ -581,7 +581,7 @@ jobs: command: | set -e cd packages/celotool - ./ci_test_sync.sh checkout master + ./ci_test_sync.sh checkout asaj/key-rotation-plus-enode end-to-end-geth-integration-sync-test: <<: *e2e-defaults @@ -598,7 +598,7 @@ jobs: command: | set -e cd packages/celotool - ./ci_test_sync_with_network.sh checkout master + ./ci_test_sync_with_network.sh checkout asaj/key-rotation-plus-enode end-to-end-geth-attestations-test: <<: *e2e-defaults @@ -616,7 +616,7 @@ jobs: command: | set -e cd packages/celotool - ./ci_test_attestations.sh checkout master + ./ci_test_attestations.sh checkout asaj/key-rotation-plus-enode end-to-end-geth-validator-order-test: <<: *e2e-defaults @@ -634,7 +634,7 @@ jobs: command: | set -e cd packages/celotool - ./ci_test_validator_order.sh checkout master + ./ci_test_validator_order.sh checkout asaj/key-rotation-plus-enode web: working_directory: ~/app diff --git a/packages/celotool/src/e2e-tests/governance_tests.ts b/packages/celotool/src/e2e-tests/governance_tests.ts index 56ef7bb0d37..a3c951af00f 100644 --- a/packages/celotool/src/e2e-tests/governance_tests.ts +++ b/packages/celotool/src/e2e-tests/governance_tests.ts @@ -1,6 +1,5 @@ import { ContractKit, newKitFromWeb3 } from '@celo/contractkit' import { getBlsPublicKey, getBlsPoP } from '@celo/utils/lib/bls' -import { privateKeyToPublicKey } from '@celo/utils/lib/address' import { fromFixed, toFixed } from '@celo/utils/lib/fixidity' import BigNumber from 'bignumber.js' import { assert } from 'chai' @@ -185,7 +184,7 @@ describe('governance tests', () => { return blockNumber % epochSize === 0 } - describe.only('when the validator set is changing', () => { + describe('when the validator set is changing', () => { let epoch: number const blockNumbers: number[] = [] let validatorAccounts: string[] diff --git a/packages/cli/src/commands/account/claims.test.ts b/packages/cli/src/commands/account/claims.test.ts index 3f993f5841b..4b7a62b4eb6 100644 --- a/packages/cli/src/commands/account/claims.test.ts +++ b/packages/cli/src/commands/account/claims.test.ts @@ -10,7 +10,7 @@ import CreateMetadata from './create-metadata' import RegisterMetadata from './register-metadata' process.env.NO_SYNCCHECK = 'true' -testWithGanache('account:authorize cmd', (web3: Web3) => { +testWithGanache('account metadata cmds', (web3: Web3) => { let account: string beforeEach(async () => { diff --git a/packages/cli/src/commands/validator/register.ts b/packages/cli/src/commands/validator/register.ts index 19d899c0495..7d13c090f85 100644 --- a/packages/cli/src/commands/validator/register.ts +++ b/packages/cli/src/commands/validator/register.ts @@ -1,4 +1,4 @@ -import { addressToPublicKey } from '@celo/lib/address' +import { addressToPublicKey } from '@celo/utils/lib/address' import { BaseCommand } from '../../base' import { newCheckBuilder } from '../../utils/checks' import { displaySendTx } from '../../utils/cli' diff --git a/packages/cli/src/commands/validator/update-bls-key.ts b/packages/cli/src/commands/validator/update-bls-key.ts index 1b2e1a0aa55..499c162e459 100644 --- a/packages/cli/src/commands/validator/update-bls-key.ts +++ b/packages/cli/src/commands/validator/update-bls-key.ts @@ -20,8 +20,6 @@ export default class ValidatorUpdateBlsKey extends BaseCommand { const res = this.parse(ValidatorUpdateBlsKey) this.kit.defaultAccount = res.flags.from const validators = await this.kit.contracts.getValidators() - const accounts = await this.kit.contracts.getAccounts() - await newCheckBuilder(this, res.flags.from) .isSignerOrAccount() .canSignValidatorTxs() diff --git a/packages/cli/src/utils/command.ts b/packages/cli/src/utils/command.ts index fc6acc0dd05..60e61c256ab 100644 --- a/packages/cli/src/utils/command.ts +++ b/packages/cli/src/utils/command.ts @@ -4,7 +4,7 @@ import { IArg, ParseFn } from '@oclif/parser/lib/args' import { pathExistsSync } from 'fs-extra' import Web3 from 'web3' -const parseBytes = (input, length, msg) => { +const parseBytes = (input: string, length: number, msg: string) => { // Check that the string starts with 0x and has byte length of `length`. if (Web3.utils.isHex(input) && input.length === length && input.startsWith('0x')) { return input @@ -14,13 +14,13 @@ const parseBytes = (input, length, msg) => { } const parseEcdsaPublicKey: ParseFn = (input) => { - parseBytes(input, 64, `${input} is not an ECDSA public key`) + return parseBytes(input, 64, `${input} is not an ECDSA public key`) } const parseBlsPublicKey: ParseFn = (input) => { - parseBytes(input, 48, `${input} is not a BLS public key`) + return parseBytes(input, 48, `${input} is not a BLS public key`) } const parseBlsProofOfPossession: ParseFn = (input) => { - parseBytes(input, 96, `${input} is not a BLS proof-of-possession`) + return parseBytes(input, 96, `${input} is not a BLS proof-of-possession`) } const parseAddress: ParseFn = (input) => { if (Web3.utils.isAddress(input)) { diff --git a/packages/cli/src/utils/helpers.ts b/packages/cli/src/utils/helpers.ts index 54ed4047ecd..4d8c7c0f768 100644 --- a/packages/cli/src/utils/helpers.ts +++ b/packages/cli/src/utils/helpers.ts @@ -1,4 +1,3 @@ -import { eqAddress } from '@celo/utils/lib/address' import Web3 from 'web3' import { Block } from 'web3/eth/types' import { failWith } from './cli' diff --git a/packages/contractkit/src/wrappers/Accounts.test.ts b/packages/contractkit/src/wrappers/Accounts.test.ts new file mode 100644 index 00000000000..39a5c3eb427 --- /dev/null +++ b/packages/contractkit/src/wrappers/Accounts.test.ts @@ -0,0 +1,83 @@ +import { addressToPublicKey } from '@celo/utils/lib/address' +import { parseSignature } from '@celo/utils/lib/signatureUtils' +import Web3 from 'web3' +import { newKitFromWeb3 } from '../kit' +import { testWithGanache } from '../test-utils/ganache-test' +import { AccountsWrapper } from './Accounts' +import { LockedGoldWrapper } from './LockedGold' +import { ValidatorsWrapper } from './Validators' + +/* +TEST NOTES: +- In migrations: The only account that has cUSD is accounts[0] +*/ + +const minLockedGoldValue = Web3.utils.toWei('10', 'ether') // 10 gold + +// Random hex strings +const blsPublicKey = + '0x4d23d8cd06f30b1fa7cf368e2f5399ab04bb6846c682f493a98a607d3dfb7e53a712bb79b475c57b0ac2785460f91301' +const blsPoP = + '0x9d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d740501' + +testWithGanache('Accounts Wrapper', (web3) => { + const kit = newKitFromWeb3(web3) + let accounts: string[] = [] + let accountsInstance: AccountsWrapper + let validators: ValidatorsWrapper + let lockedGold: LockedGoldWrapper + + const registerAccountWithLockedGold = async (account: string) => { + if (!(await accountsInstance.isAccount(account))) { + await accountsInstance.createAccount().sendAndWaitForReceipt({ from: account }) + } + await lockedGold.lock().sendAndWaitForReceipt({ from: account, value: minLockedGoldValue }) + } + + const getParsedSignatureOfAddress = async (address: string, signer: string) => { + const addressHash = web3.utils.soliditySha3({ type: 'address', value: address }) + console.log('account', address) + console.log('addressHash', addressHash) + const signature = await web3.eth.sign(addressHash, signer) + return parseSignature(addressHash, signature, signer) + } + + beforeAll(async () => { + accounts = await web3.eth.getAccounts() + validators = await kit.contracts.getValidators() + lockedGold = await kit.contracts.getLockedGold() + accountsInstance = await kit.contracts.getAccounts() + }) + + const setupValidator = async (validatorAccount: string) => { + const publicKey = await addressToPublicKey(validatorAccount, web3) + await registerAccountWithLockedGold(validatorAccount) + await validators + .registerValidator( + // @ts-ignore + publicKey, + // @ts-ignore + blsPublicKey, + // @ts-ignore + blsPoP + ) + .sendAndWaitForReceipt({ from: validatorAccount }) + } + + test('SBAT authorize validator key when not a validator', async () => { + const account = accounts[0] + const signer = accounts[1] + await accountsInstance.createAccount() + const sig = await getParsedSignatureOfAddress(account, signer) + await accountsInstance.authorizeValidatorSigner(signer, sig) + }) + + test('SBAT authorize validator key when a validator', async () => { + const account = accounts[0] + const signer = accounts[1] + await accountsInstance.createAccount() + await setupValidator(account) + const sig = await getParsedSignatureOfAddress(account, signer) + await accountsInstance.authorizeValidatorSigner(signer, sig) + }) +}) diff --git a/packages/contractkit/src/wrappers/Accounts.ts b/packages/contractkit/src/wrappers/Accounts.ts index 967b7593161..e0e6035d1a5 100644 --- a/packages/contractkit/src/wrappers/Accounts.ts +++ b/packages/contractkit/src/wrappers/Accounts.ts @@ -1,4 +1,8 @@ -import { parseSignature } from '@celo/utils/lib/signatureUtils' +import { + hashMessageWithPrefix, + parseSignature, + signedMessageToPublicKey, +} from '@celo/utils/lib/signatureUtils' import Web3 from 'web3' import { Address } from '../base' import { Accounts } from '../generated/types/Accounts' @@ -120,22 +124,45 @@ export class AccountsWrapper extends BaseWrapper { * Authorizes an address to sign consensus messages on behalf of the account. * @param signer The address of the signing key to authorize. * @param proofOfSigningKeyPossession The account address signed by the signer address. - * @param proofOfBlsKeyPossession Proof-of-possession generated for the corresponding BLS key. Only needed if a validator has been registered. * @return A CeloTransactionObject */ async authorizeValidatorSigner( signer: Address, proofOfSigningKeyPossession: Signature ): Promise> { - return toTransactionObject( - this.kit, - this.contract.methods.authorizeValidatorSigner( - signer, + const validators = await this.kit.contracts.getValidators() + const account = this.kit.defaultAccount || (await this.kit.web3.eth.getAccounts())[0] + if (await validators.isValidator(account)) { + const message = this.kit.web3.utils.soliditySha3({ type: 'address', value: account }) + const prefixedMsg = hashMessageWithPrefix(message) + const pubKey = signedMessageToPublicKey( + prefixedMsg, proofOfSigningKeyPossession.v, proofOfSigningKeyPossession.r, proofOfSigningKeyPossession.s ) - ) + return toTransactionObject( + this.kit, + this.contract.methods.authorizeValidatorSigner( + signer, + pubKey, + proofOfSigningKeyPossession.v, + proofOfSigningKeyPossession.r, + // @ts-ignore Typescript does not support overloading. + proofOfSigningKeyPossession.s + ) + ) + } else { + return toTransactionObject( + this.kit, + this.contract.methods.authorizeValidatorSigner( + signer, + proofOfSigningKeyPossession.v, + proofOfSigningKeyPossession.r, + proofOfSigningKeyPossession.s + ) + ) + } } async generateProofOfSigningKeyPossession(account: Address, signer: Address) { diff --git a/packages/contractkit/src/wrappers/Validators.test.ts b/packages/contractkit/src/wrappers/Validators.test.ts index d41ecec59bd..0175ea5643e 100644 --- a/packages/contractkit/src/wrappers/Validators.test.ts +++ b/packages/contractkit/src/wrappers/Validators.test.ts @@ -1,3 +1,4 @@ +import { addressToPublicKey } from '@celo/utils/lib/address' import BigNumber from 'bignumber.js' import Web3 from 'web3' import { newKitFromWeb3 } from '../kit' @@ -13,13 +14,10 @@ TEST NOTES: const minLockedGoldValue = Web3.utils.toWei('10', 'ether') // 10 gold -// A random 64 byte hex string. -const publicKey = - 'ea0733ad275e2b9e05541341a97ee82678c58932464fad26164657a111a7e37a9fa0300266fb90e2135a1f1512350cb4e985488a88809b14e3cbe415e76e82b2' const blsPublicKey = - '4d23d8cd06f30b1fa7cf368e2f5399ab04bb6846c682f493a98a607d3dfb7e53a712bb79b475c57b0ac2785460f91301' + '0x4d23d8cd06f30b1fa7cf368e2f5399ab04bb6846c682f493a98a607d3dfb7e53a712bb79b475c57b0ac2785460f91301' const blsPoP = - '9d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d740501' + '0x9d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d740501' testWithGanache('Validators Wrapper', (web3) => { const kit = newKitFromWeb3(web3) @@ -50,6 +48,7 @@ testWithGanache('Validators Wrapper', (web3) => { } const setupValidator = async (validatorAccount: string) => { + const publicKey = await addressToPublicKey(validatorAccount, web3) await registerAccountWithLockedGold(validatorAccount) // set account1 as the validator await validators diff --git a/packages/protocol/contracts/common/Accounts.sol b/packages/protocol/contracts/common/Accounts.sol index 1bb4b182720..12ab841e330 100644 --- a/packages/protocol/contracts/common/Accounts.sol +++ b/packages/protocol/contracts/common/Accounts.sol @@ -164,10 +164,30 @@ contract Accounts is IAccounts, Ownable, ReentrancyGuard, Initializable, UsingRe Account storage account = accounts[msg.sender]; authorize(signer, v, r, s); account.signers.validator = signer; - IValidators validators = getValidators(); - if (validators.isValidator(msg.sender)) { - require(getValidators().updateEcdsaKey(msg.sender, signer, v, r, s)); - } + require(!getValidators().isValidator(msg.sender)); + emit ValidatorSignerAuthorized(msg.sender, signer); + } + + /** + * @notice Authorizes an address to sign consensus messages on behalf of the account. + * @param signer The address of the signing key to authorize. + * @param ecdsaKey The ECDSA key corresponding to `signer`. + * @param v The recovery id of the incoming ECDSA signature. + * @param r Output value r of the ECDSA signature. + * @param s Output value s of the ECDSA signature. + * @dev v, r, s constitute `signer`'s signature on `msg.sender`. + */ + function authorizeValidatorSigner( + address signer, + bytes calldata ecdsaKey, + uint8 v, + bytes32 r, + bytes32 s + ) external nonReentrant { + Account storage account = accounts[msg.sender]; + authorize(signer, v, r, s); + account.signers.validator = signer; + require(getValidators().updateEcdsaKey(msg.sender, signer, ecdsaKey)); emit ValidatorSignerAuthorized(msg.sender, signer); } diff --git a/packages/protocol/contracts/common/UsingPrecompiles.sol b/packages/protocol/contracts/common/UsingPrecompiles.sol index 9dd38d83521..542846af9a6 100644 --- a/packages/protocol/contracts/common/UsingPrecompiles.sol +++ b/packages/protocol/contracts/common/UsingPrecompiles.sol @@ -61,60 +61,6 @@ contract UsingPrecompiles { return (returnNumerator, returnDenominator); } - /** - * @notice Recover the public key of a signed message. - * @param messageHash The hash of a message. - * @param v The recovery id of the incoming ECDSA signature. - * @param r Output value r of the ECDSA signature. - * @param s Output value s of the ECDSA signature. - * @return numerator/denominator of the computed quantity (not reduced). - */ - function ecrecoverPublicKey(bytes32 messageHash, uint8 v, bytes32 r, bytes32 s) - public - returns (bytes memory) - { - /* - bytes publicKey; - // solhint-disable-next-line no-inline-assembly - assembly { - let newCallDataPosition := mload(0x40) - mstore(0x40, add(newCallDataPosition, calldatasize)) - mstore(newCallDataPosition, messageHash) - mstore(add(newCallDataPosition, 1), v) - mstore(add(newCallDataPosition, 33), r) - mstore(add(newCallDataPosition, 65), s) - let success := staticcall( - 3000, // estimated gas cost for this function - 0xfe, - newCallDataPosition, - 0x61, // input size, 3 * 32 + 1 = 97 bytes - 0, - 0 - ) - - let returnDataSize := returndatasize - let returnDataPosition := mload(0x40) - mstore(0x40, add(returnDataPosition, returnDataSize)) - returndatacopy(returnDataPosition, 0, returnDataSize) - - switch success - case 0 { - revert(returnDataPosition, returnDataSize) - } - default { - returnNumerator := mload(returnDataPosition) - returnDenominator := mload(add(returnDataPosition, 32)) - } - } - return (returnNumerator, returnDenominator); - */ - bool success; - bytes memory publicKey; - (success, publicKey) = address(0xfe).call.gas(gasleft())(abi.encode(messageHash, v, r, s)); - require(success); - return publicKey; - } - /** * @notice Returns the current epoch size in blocks. * @return The current epoch size in blocks. diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index 3a0d53ab8da..66b5383475b 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -521,23 +521,17 @@ contract Validators is /** * @notice Updates a validator's ECDSA key. * @param account The address under which the validator is registered. - * @param v The recovery id of the incoming ECDSA signature. - * @param r Output value r of the ECDSA signature. - * @param s Output value s of the ECDSA signature. - * @dev v, r, s constitute the ECDSA key's signature on `account`. + * @param signer The address which the validator is using to sign consensus messages. + * @param ecdsaKey The ECDSA public key corresponding to `signer`. * @return True upon success. */ - function updateEcdsaKey(address account, address signer, uint8 v, bytes32 r, bytes32 s) + function updateEcdsaKey(address account, address signer, bytes calldata ecdsaKey) external onlyRegisteredContract(ACCOUNTS_REGISTRY_ID) returns (bool) { require(isValidator(account)); Validator storage validator = validators[account]; - bytes32 addressHash = keccak256(abi.encodePacked(account)); - bytes memory prefix = "\x19Ethereum Signed Message:\n32"; - bytes32 prefixedHash = keccak256(abi.encodePacked(prefix, addressHash)); - bytes memory ecdsaKey = ecrecoverPublicKey(prefixedHash, v, r, s); require(_updateEcdsaKey(validator, signer, ecdsaKey)); emit ValidatorEcdsaKeyUpdated(account, ecdsaKey); return true; diff --git a/packages/protocol/contracts/governance/interfaces/IValidators.sol b/packages/protocol/contracts/governance/interfaces/IValidators.sol index 273748ccc3b..6be5006e534 100644 --- a/packages/protocol/contracts/governance/interfaces/IValidators.sol +++ b/packages/protocol/contracts/governance/interfaces/IValidators.sol @@ -7,6 +7,6 @@ interface IValidators { function getGroupsNumMembers(address[] calldata) external view returns (uint256[] memory); function getNumRegisteredValidators() external view returns (uint256); function getTopGroupValidators(address, uint256) external view returns (address[] memory); - function updateEcdsaKey(address, address, uint8, bytes32, bytes32) external returns (bool); + function updateEcdsaKey(address, address, bytes calldata) external returns (bool); function isValidator(address) external view returns (bool); } diff --git a/packages/protocol/contracts/governance/test/MockValidators.sol b/packages/protocol/contracts/governance/test/MockValidators.sol index c2daa76eda9..4e7b3a75d27 100644 --- a/packages/protocol/contracts/governance/test/MockValidators.sol +++ b/packages/protocol/contracts/governance/test/MockValidators.sol @@ -6,17 +6,21 @@ import "../interfaces/IValidators.sol"; * @title Holds a list of addresses of validators */ contract MockValidators is IValidators { + mapping(address => bool) public isValidator; mapping(address => uint256) private numGroupMembers; mapping(address => uint256) private lockedGoldRequirements; mapping(address => bool) private doesNotMeetAccountLockedGoldRequirements; mapping(address => address[]) private members; uint256 private numRegisteredValidators; - mapping(address => bytes) public publicKeysData; - function updateEcdsaKey(address, address, uint8, bytes32, bytes32) external returns (bool) { + function updateEcdsaKey(address, address, bytes calldata) external returns (bool) { return true; } + function setValidator(address account) external { + isValidator[account] = true; + } + function setDoesNotMeetAccountLockedGoldRequirements(address account) external { doesNotMeetAccountLockedGoldRequirements[account] = true; } diff --git a/packages/protocol/migrations/20_elect_validators.ts b/packages/protocol/migrations/20_elect_validators.ts index 31e8fb4d2c6..24baec893e4 100644 --- a/packages/protocol/migrations/20_elect_validators.ts +++ b/packages/protocol/migrations/20_elect_validators.ts @@ -6,7 +6,7 @@ import { } from '@celo/protocol/lib/web3-utils' import { config } from '@celo/protocol/migrationsConfig' import { privateKeyToAddress, privateKeyToPublicKey } from '@celo/utils/lib/address' -import { getBlsPublicKey, getBlsPoP } from '@celo/utils/lib/bls' +import { getBlsPoP, getBlsPublicKey } from '@celo/utils/lib/bls' import { toFixed } from '@celo/utils/lib/fixidity' import { BigNumber } from 'bignumber.js' import { AccountsInstance, ElectionInstance, LockedGoldInstance, ValidatorsInstance } from 'types' diff --git a/packages/utils/src/address.ts b/packages/utils/src/address.ts index d1f6121a1e9..96c1020a62c 100644 --- a/packages/utils/src/address.ts +++ b/packages/utils/src/address.ts @@ -1,14 +1,14 @@ +import assert = require('assert') import { ecrecover, fromRpcSig, - pubToAddress, privateToAddress, privateToPublic, + pubToAddress, sha3, toChecksumAddress, } from 'ethereumjs-util' import Web3 from 'web3' -import assert = require('assert') export type Address = string diff --git a/packages/utils/src/signatureUtils.ts b/packages/utils/src/signatureUtils.ts index 177874535ed..d8aebacf066 100644 --- a/packages/utils/src/signatureUtils.ts +++ b/packages/utils/src/signatureUtils.ts @@ -45,6 +45,16 @@ export function LocalSigner(privateKey: string): Signer { } } +export function signedMessageToPublicKey(message: string, v: number, r: string, s: string) { + const pubKeyBuf = ethjsutil.ecrecover( + Buffer.from(message.slice(2), 'hex'), + v, + Buffer.from(r.slice(2), 'hex'), + Buffer.from(s.slice(2), 'hex') + ) + return '0x' + pubKeyBuf.toString('hex') +} + export function signMessage(message: string, privateKey: string, address: string) { return signMessageWithoutPrefix(hashMessageWithPrefix(message), privateKey, address) } From 8799b0a7efac9fbf47b94deab15eb5575df8d5ab Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Tue, 12 Nov 2019 11:31:33 -0800 Subject: [PATCH 135/149] Add updateEcdsaKey test --- .../contracts/governance/Validators.sol | 2 - .../protocol/test/governance/validators.ts | 39 +++++++++++++------ 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index 66b5383475b..194c3833d94 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -511,8 +511,6 @@ contract Validators is ) private returns (bool) { require(blsKey.length == 48); require(blsPop.length == 96); - // Use the proof of possession bytes - // TODO: Should this be the `account` or `signer` key here? require(checkProofOfPossession(account, blsKey, blsPop)); validator.keys.bls = blsKey; return true; diff --git a/packages/protocol/test/governance/validators.ts b/packages/protocol/test/governance/validators.ts index 355a7eef27e..867a876351c 100644 --- a/packages/protocol/test/governance/validators.ts +++ b/packages/protocol/test/governance/validators.ts @@ -1068,17 +1068,11 @@ contract('Validators', (accounts: string[]) => { }) }) - /* - TODO(asa): Restore once ganache supports this precompile. - describe('#updateEcdsaKey()', () => { - let sig: any - let newPublicKey: string + describe.only('#updateEcdsaKey()', () => { describe('when called by a registered validator', () => { const validator = accounts[0] - const signer = accounts[9] beforeEach(async () => { await registerValidator(validator) - newPublicKey = await addressToPublicKey(signer, web3) }) describe('when called by the registered `Accounts` contract', () => { @@ -1086,12 +1080,14 @@ contract('Validators', (accounts: string[]) => { await registry.setAddressFor(CeloContractName.Accounts, accounts[0]) }) - describe('when the signature matches the `signer`', () => { + describe('when the public key matches the signer', () => { let resp: any + let newPublicKey: string + const signer = accounts[9] beforeEach(async () => { - const sig = await getParsedSignatureOfAddress(web3, validator, signer) + newPublicKey = await addressToPublicKey(signer, web3) // @ts-ignore Broken typechain typing for bytes - resp = await validators.updateEcdsaKey(validator, signer, sig.v, sig.r, sig.s) + resp = await validators.updateEcdsaKey(validator, signer, newPublicKey) }) it('should set the validator ecdsa public key', async () => { @@ -1111,10 +1107,31 @@ contract('Validators', (accounts: string[]) => { }) }) }) + + describe('when the public key does not match the signer', () => { + let newPublicKey: string + const signer = accounts[9] + it('should revert', async () => { + newPublicKey = await addressToPublicKey(accounts[8], web3) + // @ts-ignore Broken typechain typing for bytes + await assertRevert(validators.updateEcdsaKey(validator, signer, newPublicKey)) + }) + }) + }) + + describe('when not called by the registered `Accounts` contract', () => { + describe('when the public key matches the signer', () => { + let newPublicKey: string + const signer = accounts[9] + it('should revert', async () => { + newPublicKey = await addressToPublicKey(signer, web3) + // @ts-ignore Broken typechain typing for bytes + await assertRevert(validators.updateEcdsaKey(validator, signer, newPublicKey)) + }) + }) }) }) }) - */ describe('#updateBlsKey()', () => { const newBlsPublicKey = web3.utils.randomHex(48) From 30390429b5aca6cf0fc415bcd437a8e8aaa375f2 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Tue, 12 Nov 2019 11:33:31 -0800 Subject: [PATCH 136/149] remove .only --- packages/protocol/test/governance/validators.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/protocol/test/governance/validators.ts b/packages/protocol/test/governance/validators.ts index 867a876351c..403cd5a82c2 100644 --- a/packages/protocol/test/governance/validators.ts +++ b/packages/protocol/test/governance/validators.ts @@ -1068,7 +1068,7 @@ contract('Validators', (accounts: string[]) => { }) }) - describe.only('#updateEcdsaKey()', () => { + describe('#updateEcdsaKey()', () => { describe('when called by a registered validator', () => { const validator = accounts[0] beforeEach(async () => { From 10171dcdc0b20222d1f2f9a99e83d30cd854d4ce Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Tue, 12 Nov 2019 11:53:14 -0800 Subject: [PATCH 137/149] Update docs --- .../src/e2e-tests/governance_tests.ts | 2 +- .../docs/command-line-interface/account.md | 41 ++++++++++--- .../docs/command-line-interface/validator.md | 57 ++++++++++--------- 3 files changed, 63 insertions(+), 37 deletions(-) diff --git a/packages/celotool/src/e2e-tests/governance_tests.ts b/packages/celotool/src/e2e-tests/governance_tests.ts index a3c951af00f..ddcc857c370 100644 --- a/packages/celotool/src/e2e-tests/governance_tests.ts +++ b/packages/celotool/src/e2e-tests/governance_tests.ts @@ -1,5 +1,5 @@ import { ContractKit, newKitFromWeb3 } from '@celo/contractkit' -import { getBlsPublicKey, getBlsPoP } from '@celo/utils/lib/bls' +import { getBlsPoP, getBlsPublicKey } from '@celo/utils/lib/bls' import { fromFixed, toFixed } from '@celo/utils/lib/fixidity' import BigNumber from 'bignumber.js' import { assert } from 'chai' diff --git a/packages/docs/command-line-interface/account.md b/packages/docs/command-line-interface/account.md index 5967feae68b..0582aa02ff8 100644 --- a/packages/docs/command-line-interface/account.md +++ b/packages/docs/command-line-interface/account.md @@ -6,20 +6,23 @@ description: Manage your account, send and receive Celo Gold and Celo Dollars ### Authorize -Authorize an attestation, validation or vote signing key +Authorize an attestation, validator, or vote signer ``` USAGE $ celocli account:authorize OPTIONS - -r, --role=vote|validation|attestation Role to delegate - --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Account Address - --to=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Account Address + -r, --role=vote|validator|attestation (required) Role to delegate + --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Account Address + --pop=pop (required) Proof-of-possession of the signer key + --signer=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Account Address EXAMPLE - authorize --from 0x5409ED021D9299bf6814279A6A1411A7e866A631 --role vote --to - 0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d + authorize --from 0x5409ED021D9299bf6814279A6A1411A7e866A631 --role vote --signer + 0x6ecbe1db9ef729cbe972c83fb886247691fb6beb --pop + 0x1b9fca4bbb5bfb1dbe69ef1cddbd9b4202dcb6b134c5170611e1e36ecfa468d7b46c85328d504934fce6c2a1571603a50ae224d2b32685e84d4d + 1a1eebad8452eb ``` _See code: [packages/cli/src/commands/account/authorize.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/account/authorize.ts)_ @@ -205,6 +208,25 @@ EXAMPLE _See code: [packages/cli/src/commands/account/new.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/account/new.ts)_ +### Proof-of-possession + +Generate proof-of-possession to be used to authorize a signer + +``` +USAGE + $ celocli account:proof-of-possession + +OPTIONS + --account=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Account Address + --signer=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Account Address + +EXAMPLE + proof-of-possession --account 0x5409ed021d9299bf6814279a6a1411a7e866a631 --signer + 0x6ecbe1db9ef729cbe972c83fb886247691fb6beb +``` + +_See code: [packages/cli/src/commands/account/proof-of-possession.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/account/proof-of-possession.ts)_ + ### Register Register an account @@ -215,10 +237,11 @@ USAGE OPTIONS --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Account Address - --name=name (required) + --name=name -EXAMPLE - register +EXAMPLES + register --from 0x5409ed021d9299bf6814279a6a1411a7e866a631 + register --from 0x5409ed021d9299bf6814279a6a1411a7e866a631 --name test-account ``` _See code: [packages/cli/src/commands/account/register.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/account/register.ts)_ diff --git a/packages/docs/command-line-interface/validator.md b/packages/docs/command-line-interface/validator.md index 308a79300f3..1eb6d7276f8 100644 --- a/packages/docs/command-line-interface/validator.md +++ b/packages/docs/command-line-interface/validator.md @@ -72,28 +72,6 @@ EXAMPLE _See code: [packages/cli/src/commands/validator/list.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validator/list.ts)_ -### PublicKey - -Manage BLS public key data for a validator - -``` -USAGE - $ celocli validator:publicKey - -OPTIONS - --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Validator's address - --publicKey=0x (required) Public Key - -EXAMPLE - publickey --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --publicKey - 0xc52f3fab06e22a54915a8765c4f6826090cfac5e40282b43844bf1c0df83aaa632e55b67869758f2291d1aabe0ebecc7cbf4236aaa45e3e0cfbf - 997eda082ae19d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d - 785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d7405011220a66a6257562d0c26dabf64485a1d - 96bad27bb1c0fd6080a75b0ec9f75b50298a2a8e04b02b2688c8104fca61fb00 -``` - -_See code: [packages/cli/src/commands/validator/publicKey.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validator/publicKey.ts)_ - ### Register Register a new Validator @@ -103,15 +81,18 @@ USAGE $ celocli validator:register OPTIONS + --blsKey=0x (required) BLS Public Key + --blsPop=0x (required) BLS Proof-of-Possession + --ecdsaKey=0x (required) ECDSA Public Key --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Address for the Validator - --publicKey=0x (required) Public Key EXAMPLE - register --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --publicKey + register --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --ecdsaKey 0xc52f3fab06e22a54915a8765c4f6826090cfac5e40282b43844bf1c0df83aaa632e55b67869758f2291d1aabe0ebecc7cbf4236aaa45e3e0cfbf - 997eda082ae19d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d - 785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d7405011220a66a6257562d0c26dabf64485a1d - 96bad27bb1c0fd6080a75b0ec9f75b50298a2a8e04b02b2688c8104fca61fb00 + 997eda082ae1 --blsKey + 0x9d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae300 --blsPop + 0x05d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d7405011220a66a6257562d0c26 + dabf64485a1d96bad27bb1c0fd6080a75b0ec9f75b50298a2a8e04b02b2688c8104fca61fb00 ``` _See code: [packages/cli/src/commands/validator/register.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validator/register.ts)_ @@ -146,3 +127,25 @@ EXAMPLE ``` _See code: [packages/cli/src/commands/validator/show.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validator/show.ts)_ + +### Update-bls-key + +Update BLS key for a validator + +``` +USAGE + $ celocli validator:update-bls-key + +OPTIONS + --blsKey=0x (required) BLS Public Key + --blsPop=0x (required) BLS Proof-of-Possession + --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Validator's address + +EXAMPLE + update-bls-key --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --blsKey + 0x9d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae300 --blsPop + 0x05d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d7405011220a66a6257562d0c26 + dabf64485a1d96bad27bb1c0fd6080a75b0ec9f75b50298a2a8e04b02b2688c8104fca61fb00 +``` + +_See code: [packages/cli/src/commands/validator/update-bls-key.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validator/update-bls-key.ts)_ From 05fb26c1934c32800559d641c24c4c14ace6a4fa Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Tue, 12 Nov 2019 12:19:34 -0800 Subject: [PATCH 138/149] Update dependencies --- packages/cli/package.json | 1 + packages/protocol/package.json | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 9b79107c054..fb0430edf33 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -38,6 +38,7 @@ "@oclif/plugin-help": "^2", "bip32": "^1.0.2", "bip39": "^2.5.0", + "bls12377js": "https://github.com/celo-org/bls12377js#cada1105f4a5e4c2ddd239c1874df3bf33144a10", "chalk": "^2.4.2", "cli-table": "^0.3.1", "cli-ux": "^5.3.1", diff --git a/packages/protocol/package.json b/packages/protocol/package.json index fb42227a875..1e3b6884ec9 100644 --- a/packages/protocol/package.json +++ b/packages/protocol/package.json @@ -43,7 +43,6 @@ "@0x/subproviders": "^5.0.0", "@celo/utils": "^0.1.0", "apollo-client": "^2.4.13", - "bls12377js": "https://github.com/celo-org/bls12377js#cada1105f4a5e4c2ddd239c1874df3bf33144a10", "chai-subset": "^1.6.0", "csv-parser": "^2.0.0", "csv-stringify": "^4.3.1", From a15ffaae6ccaabef75dcc54ebd385b17c561fb67 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Tue, 12 Nov 2019 12:23:01 -0800 Subject: [PATCH 139/149] Fix governance test --- packages/celotool/src/e2e-tests/governance_tests.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/celotool/src/e2e-tests/governance_tests.ts b/packages/celotool/src/e2e-tests/governance_tests.ts index ddcc857c370..89cc6658ff6 100644 --- a/packages/celotool/src/e2e-tests/governance_tests.ts +++ b/packages/celotool/src/e2e-tests/governance_tests.ts @@ -1,4 +1,5 @@ import { ContractKit, newKitFromWeb3 } from '@celo/contractkit' +import { addressToPublicKey } from '@celo/utils/lib/address' import { getBlsPoP, getBlsPublicKey } from '@celo/utils/lib/bls' import { fromFixed, toFixed } from '@celo/utils/lib/fixidity' import BigNumber from 'bignumber.js' @@ -150,7 +151,14 @@ describe('governance tests', () => { ).contracts.getAccounts()).generateProofOfSigningKeyPossession(validator, signer) const validatorKit = newKitFromWeb3(validatorWeb3) const validatorAccounts = await validatorKit._web3Contracts.getAccounts() - const tx = validatorAccounts.methods.authorizeValidatorSigner(signer, pop.v, pop.r, pop.s) + const publicKey = await addressToPublicKey(signer, signerWeb3) + const tx = validatorAccounts.methods.authorizeValidatorSigner( + signer, + publicKey, + pop.v, + pop.r, + pop.s + ) let gas = txOptions.gas if (!gas) { gas = await tx.estimateGas({ ...txOptions }) From 1d6ad5705d12974f2f3e28aacd56479efc8cda42 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Wed, 13 Nov 2019 20:43:37 -0800 Subject: [PATCH 140/149] Address comments --- .../src/e2e-tests/governance_tests.ts | 62 ++------ .../cli/src/commands/validator/register.ts | 4 +- ...te-bls-key.ts => update-bls-public-key.ts} | 8 +- packages/cli/src/utils/checks.ts | 2 +- packages/cli/src/utils/command.ts | 5 +- .../contractkit/src/wrappers/Accounts.test.ts | 17 +-- packages/contractkit/src/wrappers/Accounts.ts | 11 +- .../src/wrappers/Validators.test.ts | 14 +- .../contractkit/src/wrappers/Validators.ts | 27 ++-- .../protocol/contracts/common/Accounts.sol | 53 ++----- .../contracts/common/interfaces/IAccounts.sol | 6 +- .../contracts/governance/Election.sol | 8 +- .../contracts/governance/Governance.sol | 6 +- .../contracts/governance/Validators.sol | 141 ++++++++++-------- .../governance/interfaces/IValidators.sol | 2 +- .../contracts/identity/Attestations.sol | 4 +- packages/protocol/test/common/accounts.ts | 27 ++-- .../protocol/test/governance/validators.ts | 44 +++--- packages/utils/src/address.ts | 33 +--- packages/utils/src/bls.ts | 6 + packages/utils/src/signatureUtils.ts | 27 +++- 21 files changed, 211 insertions(+), 296 deletions(-) rename packages/cli/src/commands/validator/{update-bls-key.ts => update-bls-public-key.ts} (83%) diff --git a/packages/celotool/src/e2e-tests/governance_tests.ts b/packages/celotool/src/e2e-tests/governance_tests.ts index 79d899b3398..f5913d23e42 100644 --- a/packages/celotool/src/e2e-tests/governance_tests.ts +++ b/packages/celotool/src/e2e-tests/governance_tests.ts @@ -1,5 +1,4 @@ import { ContractKit, newKitFromWeb3 } from '@celo/contractkit' -import { addressToPublicKey } from '@celo/utils/lib/address' import { getBlsPoP, getBlsPublicKey } from '@celo/utils/lib/bls' import { fromFixed, toFixed } from '@celo/utils/lib/fixidity' import BigNumber from 'bignumber.js' @@ -9,6 +8,7 @@ import { assertAlmostEqual, getContext, getEnode, + GethInstanceConfig, importGenesis, initAndStartGeth, sleep, @@ -137,11 +137,7 @@ describe('governance tests', () => { return tx.send({ from: group, ...txOptions, gas }) } - const authorizeValidatorSigner = async ( - validatorWeb3: any, - signerWeb3: any, - txOptions: any = {} - ) => { + const authorizeValidatorSigner = async (validatorWeb3: any, signerWeb3: any) => { const validator = (await validatorWeb3.eth.getAccounts())[0] const signer = (await signerWeb3.eth.getAccounts())[0] await unlockAccount(validator, validatorWeb3) @@ -149,28 +145,14 @@ describe('governance tests', () => { const pop = await (await newKitFromWeb3( signerWeb3 ).contracts.getAccounts()).generateProofOfSigningKeyPossession(validator, signer) - const validatorKit = newKitFromWeb3(validatorWeb3) - const validatorAccounts = await validatorKit._web3Contracts.getAccounts() - const publicKey = await addressToPublicKey(signer, signerWeb3) - const tx = validatorAccounts.methods.authorizeValidatorSigner( - signer, - publicKey, - pop.v, - pop.r, - pop.s - ) - let gas = txOptions.gas - if (!gas) { - gas = await tx.estimateGas({ ...txOptions }) - } - return tx.send({ from: validator, ...txOptions, gas }) + const accountsWrapper = await newKitFromWeb3(validatorWeb3).contracts.getAccounts() + await accountsWrapper.authorizeValidatorSigner(signer, pop) } const updateValidatorBlsKey = async ( validatorWeb3: any, signerWeb3: any, - signerPrivateKey: string, - txOptions: any = {} + signerPrivateKey: string ) => { const validator = (await validatorWeb3.eth.getAccounts())[0] const signer = (await signerWeb3.eth.getAccounts())[0] @@ -178,14 +160,8 @@ describe('governance tests', () => { const blsPublicKey = getBlsPublicKey(signerPrivateKey) const blsPop = getBlsPoP(validator, signerPrivateKey) // TODO(asa): Send this from the signer instead. - const validatorKit = newKitFromWeb3(validatorWeb3) - const validatorValidators = await validatorKit._web3Contracts.getValidators() - const tx = validatorValidators.methods.updateBlsKey(blsPublicKey, blsPop) - let gas = txOptions.gas - if (!gas) { - gas = await tx.estimateGas({ ...txOptions }) - } - return tx.send({ from: validator, ...txOptions, gas }) + const validatorsWrapper = await newKitFromWeb3(validatorWeb3).contracts.getValidators() + await validatorsWrapper.updateBlsPublicKey(blsPublicKey, blsPop) } const isLastBlockOfEpoch = (blockNumber: number, epochSize: number) => { @@ -204,8 +180,8 @@ describe('governance tests', () => { const previousBalance = new BigNumber( await token.methods.balanceOf(address).call({}, blockNumber - 1) ) - assert.isNotNaN(currentBalance) - assert.isNotNaN(previousBalance) + assert.isFalse(currentBalance.isNaN()) + assert.isFalse(previousBalance.isNaN()) assertAlmostEqual(currentBalance.minus(previousBalance), expected) } @@ -221,7 +197,7 @@ describe('governance tests', () => { '0xa42ac9c99f6ab2c96ee6cae1b40d36187f65cd878737f6623cd363fb94ba7087' const rotation1PrivateKey = '0x4519cae145fb9499358be484ca60c80d8f5b7f9c13ff82c88ec9e13283e9de1a' - const additionalNodes: any[] = [ + const additionalNodes: GethInstanceConfig[] = [ { name: 'validatorGroup', validating: false, @@ -481,22 +457,6 @@ describe('governance tests', () => { ) const [group] = await validators.methods.getRegisteredValidatorGroups().call() - const assertBalanceChanged = async ( - validator: string, - blockNumber: number, - expected: BigNumber - ) => { - const currentBalance = new BigNumber( - await stableToken.methods.balanceOf(validator).call({}, blockNumber) - ) - const previousBalance = new BigNumber( - await stableToken.methods.balanceOf(validator).call({}, blockNumber - 1) - ) - assert.isFalse(currentBalance.isNaN()) - assert.isFalse(previousBalance.isNaN()) - assertAlmostEqual(currentBalance.minus(previousBalance), expected) - } - const assertBalanceUnchanged = async (validator: string, blockNumber: number) => { await assertBalanceChanged(validator, blockNumber, new BigNumber(0), stableToken) } @@ -673,7 +633,7 @@ describe('governance tests', () => { ) const difference = currentTarget.minus(previousTarget) - // Assert equal to 10 decimal places due to rounding errors. + // Assert equal to 9 decimal places due to rounding errors. assert.equal( fromFixed(difference) .dp(9) diff --git a/packages/cli/src/commands/validator/register.ts b/packages/cli/src/commands/validator/register.ts index 7d13c090f85..0753ae27088 100644 --- a/packages/cli/src/commands/validator/register.ts +++ b/packages/cli/src/commands/validator/register.ts @@ -1,4 +1,4 @@ -import { addressToPublicKey } from '@celo/utils/lib/address' +import { addressToPublicKey } from '@celo/utils/lib/signatureUtils' import { BaseCommand } from '../../base' import { newCheckBuilder } from '../../utils/checks' import { displaySendTx } from '../../utils/cli' @@ -43,7 +43,7 @@ export default class ValidatorRegister extends BaseCommand { // register encryption key on accounts contract // TODO: Use a different key data encryption - const pubKey = await addressToPublicKey(res.flags.from, this.web3) + const pubKey = await addressToPublicKey(res.flags.from, this.web3.eth.sign) // TODO fix typing const setKeyTx = accounts.setAccountDataEncryptionKey(pubKey as any) await displaySendTx('Set encryption key', setKeyTx) diff --git a/packages/cli/src/commands/validator/update-bls-key.ts b/packages/cli/src/commands/validator/update-bls-public-key.ts similarity index 83% rename from packages/cli/src/commands/validator/update-bls-key.ts rename to packages/cli/src/commands/validator/update-bls-public-key.ts index 499c162e459..70521743dea 100644 --- a/packages/cli/src/commands/validator/update-bls-key.ts +++ b/packages/cli/src/commands/validator/update-bls-public-key.ts @@ -3,7 +3,7 @@ import { newCheckBuilder } from '../../utils/checks' import { displaySendTx } from '../../utils/cli' import { Flags } from '../../utils/command' -export default class ValidatorUpdateBlsKey extends BaseCommand { +export default class ValidatorUpdateBlsPublicKey extends BaseCommand { static description = 'Update BLS key for a validator' static flags = { @@ -17,7 +17,7 @@ export default class ValidatorUpdateBlsKey extends BaseCommand { 'update-bls-key --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --blsKey 0x9d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae300 --blsPop 0x05d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d7405011220a66a6257562d0c26dabf64485a1d96bad27bb1c0fd6080a75b0ec9f75b50298a2a8e04b02b2688c8104fca61fb00', ] async run() { - const res = this.parse(ValidatorUpdateBlsKey) + const res = this.parse(ValidatorUpdateBlsPublicKey) this.kit.defaultAccount = res.flags.from const validators = await this.kit.contracts.getValidators() await newCheckBuilder(this, res.flags.from) @@ -27,8 +27,8 @@ export default class ValidatorUpdateBlsKey extends BaseCommand { .runChecks() await displaySendTx( - 'updateBlsKey', - validators.updateBlsKey(res.flags.blsKey as any, res.flags.blsPop as any) + 'updateBlsPublicKey', + validators.updateBlsPublicKey(res.flags.blsKey as any, res.flags.blsPop as any) ) } } diff --git a/packages/cli/src/utils/checks.ts b/packages/cli/src/utils/checks.ts index d55e7898706..d51d3b21df4 100644 --- a/packages/cli/src/utils/checks.ts +++ b/packages/cli/src/utils/checks.ts @@ -73,7 +73,7 @@ class CheckBuilder { 'Signer can sign Validator Txs', this.withAccounts((lg) => lg - .activeValidatorSignerToAccount(this.signer!) + .validatorSignerToAccount(this.signer!) .then(() => true) .catch(() => false) ) diff --git a/packages/cli/src/utils/command.ts b/packages/cli/src/utils/command.ts index 60e61c256ab..366b892667b 100644 --- a/packages/cli/src/utils/command.ts +++ b/packages/cli/src/utils/command.ts @@ -1,3 +1,4 @@ +import { BLS_POP_SIZE, BLS_PUBLIC_KEY_SIZE } from '@celo/utils/lib/bls' import { flags } from '@oclif/command' import { CLIError } from '@oclif/errors' import { IArg, ParseFn } from '@oclif/parser/lib/args' @@ -17,10 +18,10 @@ const parseEcdsaPublicKey: ParseFn = (input) => { return parseBytes(input, 64, `${input} is not an ECDSA public key`) } const parseBlsPublicKey: ParseFn = (input) => { - return parseBytes(input, 48, `${input} is not a BLS public key`) + return parseBytes(input, BLS_PUBLIC_KEY_SIZE, `${input} is not a BLS public key`) } const parseBlsProofOfPossession: ParseFn = (input) => { - return parseBytes(input, 96, `${input} is not a BLS proof-of-possession`) + return parseBytes(input, BLS_POP_SIZE, `${input} is not a BLS proof-of-possession`) } const parseAddress: ParseFn = (input) => { if (Web3.utils.isAddress(input)) { diff --git a/packages/contractkit/src/wrappers/Accounts.test.ts b/packages/contractkit/src/wrappers/Accounts.test.ts index 39a5c3eb427..95bd22d4a18 100644 --- a/packages/contractkit/src/wrappers/Accounts.test.ts +++ b/packages/contractkit/src/wrappers/Accounts.test.ts @@ -1,5 +1,4 @@ -import { addressToPublicKey } from '@celo/utils/lib/address' -import { parseSignature } from '@celo/utils/lib/signatureUtils' +import { addressToPublicKey, parseSignature } from '@celo/utils/lib/signatureUtils' import Web3 from 'web3' import { newKitFromWeb3 } from '../kit' import { testWithGanache } from '../test-utils/ganache-test' @@ -36,8 +35,6 @@ testWithGanache('Accounts Wrapper', (web3) => { const getParsedSignatureOfAddress = async (address: string, signer: string) => { const addressHash = web3.utils.soliditySha3({ type: 'address', value: address }) - console.log('account', address) - console.log('addressHash', addressHash) const signature = await web3.eth.sign(addressHash, signer) return parseSignature(addressHash, signature, signer) } @@ -50,17 +47,11 @@ testWithGanache('Accounts Wrapper', (web3) => { }) const setupValidator = async (validatorAccount: string) => { - const publicKey = await addressToPublicKey(validatorAccount, web3) + const publicKey = await addressToPublicKey(validatorAccount, web3.eth.sign) await registerAccountWithLockedGold(validatorAccount) await validators - .registerValidator( - // @ts-ignore - publicKey, - // @ts-ignore - blsPublicKey, - // @ts-ignore - blsPoP - ) + // @ts-ignore + .registerValidator(publicKey, blsPublicKey, blsPoP) .sendAndWaitForReceipt({ from: validatorAccount }) } diff --git a/packages/contractkit/src/wrappers/Accounts.ts b/packages/contractkit/src/wrappers/Accounts.ts index e0e6035d1a5..a995bc664e6 100644 --- a/packages/contractkit/src/wrappers/Accounts.ts +++ b/packages/contractkit/src/wrappers/Accounts.ts @@ -1,6 +1,7 @@ import { hashMessageWithPrefix, parseSignature, + Signature, signedMessageToPublicKey, } from '@celo/utils/lib/signatureUtils' import Web3 from 'web3' @@ -14,12 +15,6 @@ import { toTransactionObject, } from '../wrappers/BaseWrapper' -export interface Signature { - r: string - s: string - v: number -} - /** * Contract for handling deposits needed for voting. */ @@ -59,8 +54,8 @@ export class AccountsWrapper extends BaseWrapper { * @param signer Address that is authorized to sign the tx as validator * @return The Account address */ - activeValidatorSignerToAccount: (signer: Address) => Promise
= proxyCall( - this.contract.methods.activeValidatorSignerToAccount + validatorSignerToAccount: (signer: Address) => Promise
= proxyCall( + this.contract.methods.validatorSignerToAccount ) /** diff --git a/packages/contractkit/src/wrappers/Validators.test.ts b/packages/contractkit/src/wrappers/Validators.test.ts index 0175ea5643e..e90aec9cdfe 100644 --- a/packages/contractkit/src/wrappers/Validators.test.ts +++ b/packages/contractkit/src/wrappers/Validators.test.ts @@ -1,4 +1,4 @@ -import { addressToPublicKey } from '@celo/utils/lib/address' +import { addressToPublicKey } from '@celo/utils/lib/signatureUtils' import BigNumber from 'bignumber.js' import Web3 from 'web3' import { newKitFromWeb3 } from '../kit' @@ -48,18 +48,12 @@ testWithGanache('Validators Wrapper', (web3) => { } const setupValidator = async (validatorAccount: string) => { - const publicKey = await addressToPublicKey(validatorAccount, web3) + const publicKey = await addressToPublicKey(validatorAccount, web3.eth.sign) await registerAccountWithLockedGold(validatorAccount) // set account1 as the validator await validators - .registerValidator( - // @ts-ignore - publicKey, - // @ts-ignore - blsPublicKey, - // @ts-ignore - blsPoP - ) + // @ts-ignore + .registerValidator(publicKey, blsPublicKey, blsPoP) .sendAndWaitForReceipt({ from: validatorAccount }) } diff --git a/packages/contractkit/src/wrappers/Validators.ts b/packages/contractkit/src/wrappers/Validators.ts index 6b006b56729..0cd4d737365 100644 --- a/packages/contractkit/src/wrappers/Validators.ts +++ b/packages/contractkit/src/wrappers/Validators.ts @@ -18,8 +18,8 @@ import { export interface Validator { address: Address - ecdsaKey: string - blsKey: string + ecdsaPublicKey: string + blsPublicKey: string affiliation: string | null score: BigNumber } @@ -100,20 +100,23 @@ export class ValidatorsWrapper extends BaseWrapper { async signerToAccount(signerAddress: Address) { const accounts = await this.kit.contracts.getAccounts() - return accounts.activeValidatorSignerToAccount(signerAddress) + return accounts.validatorSignerToAccount(signerAddress) } /** * Updates a validator's BLS key. - * @param blsKey The BLS public key that the validator is using for consensus, should pass proof + * @param blsPublicKey The BLS public key that the validator is using for consensus, should pass proof * of possession. 48 bytes. * @param blsPop The BLS public key proof-of-possession, which consists of a signature on the * account address. 96 bytes. * @return True upon success. */ - updateBlsKey: (blsKey: string, blsPop: string) => CeloTransactionObject = proxySend( + updateBlsPublicKey: ( + blsPublicKey: string, + blsPop: string + ) => CeloTransactionObject = proxySend( this.kit, - this.contract.methods.updateBlsKey, + this.contract.methods.updateBlsPublicKey, tupleParser(parseBytes, parseBytes) ) @@ -161,8 +164,8 @@ export class ValidatorsWrapper extends BaseWrapper { const res = await this.contract.methods.getValidator(address).call() return { address, - ecdsaKey: res[0] as any, - blsKey: res[1] as any, + ecdsaPublicKey: res[0] as any, + blsPublicKey: res[1] as any, affiliation: res[2], score: fromFixed(new BigNumber(res[3])), } @@ -230,17 +233,17 @@ export class ValidatorsWrapper extends BaseWrapper { * * Fails if the account is already a validator or validator group. * - * @param ecdsaKey The ECDSA public key that the validator is using for consensus, should match + * @param ecdsaPublicKey The ECDSA public key that the validator is using for consensus, should match * the validator signer. 64 bytes. - * @param blsKey The BLS public key that the validator is using for consensus, should pass proof + * @param blsPublicKey The BLS public key that the validator is using for consensus, should pass proof * of possession. 48 bytes. * @param blsPop The BLS public key proof-of-possession, which consists of a signature on the * account address. 96 bytes. */ registerValidator: ( - ecdsaKey: string, - blsKey: string, + ecdsaPublicKey: string, + blsPublicKey: string, blsPop: string ) => CeloTransactionObject = proxySend( this.kit, diff --git a/packages/protocol/contracts/common/Accounts.sol b/packages/protocol/contracts/common/Accounts.sol index 12ab841e330..9e04877b836 100644 --- a/packages/protocol/contracts/common/Accounts.sol +++ b/packages/protocol/contracts/common/Accounts.sol @@ -171,7 +171,7 @@ contract Accounts is IAccounts, Ownable, ReentrancyGuard, Initializable, UsingRe /** * @notice Authorizes an address to sign consensus messages on behalf of the account. * @param signer The address of the signing key to authorize. - * @param ecdsaKey The ECDSA key corresponding to `signer`. + * @param ecdsaPublicKey The ECDSA public key corresponding to `signer`. * @param v The recovery id of the incoming ECDSA signature. * @param r Output value r of the ECDSA signature. * @param s Output value s of the ECDSA signature. @@ -179,7 +179,7 @@ contract Accounts is IAccounts, Ownable, ReentrancyGuard, Initializable, UsingRe */ function authorizeValidatorSigner( address signer, - bytes calldata ecdsaKey, + bytes calldata ecdsaPublicKey, uint8 v, bytes32 r, bytes32 s @@ -187,7 +187,7 @@ contract Accounts is IAccounts, Ownable, ReentrancyGuard, Initializable, UsingRe Account storage account = accounts[msg.sender]; authorize(signer, v, r, s); account.signers.validator = signer; - require(getValidators().updateEcdsaKey(msg.sender, signer, ecdsaKey)); + require(getValidators().updateEcdsaPublicKey(msg.sender, signer, ecdsaPublicKey)); emit ValidatorSignerAuthorized(msg.sender, signer); } @@ -212,7 +212,7 @@ contract Accounts is IAccounts, Ownable, ReentrancyGuard, Initializable, UsingRe * @dev Fails if the `signer` is not an account or currently authorized attestation signer. * @return The associated account. */ - function activeAttesttationSignerToAccount(address signer) external view returns (address) { + function attestationSignerToAccount(address signer) external view returns (address) { address authorizingAccount = authorizedBy[signer]; if (authorizingAccount != address(0)) { require(accounts[authorizingAccount].signers.attestation == signer); @@ -226,10 +226,10 @@ contract Accounts is IAccounts, Ownable, ReentrancyGuard, Initializable, UsingRe /** * @notice Returns the account associated with `signer`. * @param signer The address of an account or currently authorized validator signer. - * @dev Fails if the `signer` is not an account or active authorized validator. + * @dev Fails if the `signer` is not an account or currently authorized validator. * @return The associated account. */ - function activeValidatorSignerToAccount(address signer) public view returns (address) { + function validatorSignerToAccount(address signer) public view returns (address) { address authorizingAccount = authorizedBy[signer]; if (authorizingAccount != address(0)) { require(accounts[authorizingAccount].signers.validator == signer); @@ -246,26 +246,10 @@ contract Accounts is IAccounts, Ownable, ReentrancyGuard, Initializable, UsingRe * @dev Fails if the `signer` is not an account or currently authorized vote signer. * @return The associated account. */ - function activeVoteSignerToAccount(address signer) external view returns (address) { - address authorizingAccount = authorizedBy[signer]; - if (authorizingAccount != address(0)) { - require(accounts[authorizingAccount].signers.vote == signer); - return authorizingAccount; - } else { - require(isAccount(signer)); - return signer; - } - } - - /** - * @notice Returns the account associated with `signer`. - * @param signer The address of the account or previously authorized vote signer. - * @dev Fails if the `signer` is not an account or previously authorized vote signer. - * @return The associated account. - */ function voteSignerToAccount(address signer) external view returns (address) { address authorizingAccount = authorizedBy[signer]; if (authorizingAccount != address(0)) { + require(accounts[authorizingAccount].signers.vote == signer); return authorizingAccount; } else { require(isAccount(signer)); @@ -275,27 +259,11 @@ contract Accounts is IAccounts, Ownable, ReentrancyGuard, Initializable, UsingRe /** * @notice Returns the account associated with `signer`. - * @param signer The address of an account or previously authorized attestation signer. - * @dev Fails if the `signer` is not an account or previously authorized attestation signer. - * @return The associated account. - */ - function attestationSignerToAccount(address signer) public view returns (address) { - address authorizingAccount = authorizedBy[signer]; - if (authorizingAccount != address(0)) { - return authorizingAccount; - } else { - require(isAccount(signer)); - return signer; - } - } - - /** - * @notice Returns the account associated with `signer`. - * @param signer The address of an account or previously authorized validator signer. - * @dev Fails if `signer` is not an account or previously authorized validator signer. + * @param signer The address of the account or previously authorized signer. + * @dev Fails if the `signer` is not an account or previously authorized signer. * @return The associated account. */ - function validatorSignerToAccount(address signer) public view returns (address) { + function signerToAccount(address signer) external view returns (address) { address authorizingAccount = authorizedBy[signer]; if (authorizingAccount != address(0)) { return authorizingAccount; @@ -447,6 +415,7 @@ contract Accounts is IAccounts, Ownable, ReentrancyGuard, Initializable, UsingRe * @param r Output value r of the ECDSA signature. * @param s Output value s of the ECDSA signature. * @dev Fails if the address is already authorized or is an account. + * @dev Note that once an address is authorized, it may never be authorized again. * @dev v, r, s constitute `current`'s signature on `msg.sender`. */ function authorize(address authorized, uint8 v, bytes32 r, bytes32 s) private { diff --git a/packages/protocol/contracts/common/interfaces/IAccounts.sol b/packages/protocol/contracts/common/interfaces/IAccounts.sol index 24f754e17f6..0360e66394e 100644 --- a/packages/protocol/contracts/common/interfaces/IAccounts.sol +++ b/packages/protocol/contracts/common/interfaces/IAccounts.sol @@ -2,13 +2,11 @@ pragma solidity ^0.5.3; interface IAccounts { function isAccount(address) external view returns (bool); - function activeVoteSignerToAccount(address) external view returns (address); function voteSignerToAccount(address) external view returns (address); - function activeValidatorSignerToAccount(address) external view returns (address); function validatorSignerToAccount(address) external view returns (address); - function getValidatorSigner(address) external view returns (address); - function activeAttesttationSignerToAccount(address) external view returns (address); function attestationSignerToAccount(address) external view returns (address); + function signerToAccount(address) external view returns (address); + function getValidatorSigner(address) external view returns (address); function getAttestationSigner(address) external view returns (address); function setAccountDataEncryptionKey(bytes calldata) external; diff --git a/packages/protocol/contracts/governance/Election.sol b/packages/protocol/contracts/governance/Election.sol index 04e3876df4b..0143baf9839 100644 --- a/packages/protocol/contracts/governance/Election.sol +++ b/packages/protocol/contracts/governance/Election.sol @@ -209,7 +209,7 @@ contract Election is require(votes.total.eligible.contains(group)); require(0 < value); require(canReceiveVotes(group, value)); - address account = getAccounts().activeVoteSignerToAccount(msg.sender); + address account = getAccounts().voteSignerToAccount(msg.sender); // Add group to the groups voted for by the account. address[] storage groups = votes.groupsVotedFor[account]; @@ -233,7 +233,7 @@ contract Election is * @dev Pending votes cannot be activated until an election has been held. */ function activate(address group) external nonReentrant returns (bool) { - address account = getAccounts().activeVoteSignerToAccount(msg.sender); + address account = getAccounts().voteSignerToAccount(msg.sender); PendingVote storage pendingVote = votes.pending.forGroup[group].byAccount[account]; require(pendingVote.epoch < getEpochNumber()); uint256 value = pendingVote.value; @@ -263,7 +263,7 @@ contract Election is uint256 index ) external nonReentrant returns (bool) { require(group != address(0)); - address account = getAccounts().activeVoteSignerToAccount(msg.sender); + address account = getAccounts().voteSignerToAccount(msg.sender); require(0 < value && value <= getPendingVotesForGroupByAccount(group, account)); decrementPendingVotes(group, account, value); decrementTotalVotes(group, value, lesser, greater); @@ -296,7 +296,7 @@ contract Election is ) external nonReentrant returns (bool) { // TODO(asa): Dedup with revokePending. require(group != address(0)); - address account = getAccounts().activeVoteSignerToAccount(msg.sender); + address account = getAccounts().voteSignerToAccount(msg.sender); require(0 < value && value <= getActiveVotesForGroupByAccount(group, account)); decrementActiveVotes(group, account, value); decrementTotalVotes(group, value, lesser, greater); diff --git a/packages/protocol/contracts/governance/Governance.sol b/packages/protocol/contracts/governance/Governance.sol index 029aa8ee062..850a1dcb464 100644 --- a/packages/protocol/contracts/governance/Governance.sol +++ b/packages/protocol/contracts/governance/Governance.sol @@ -437,7 +437,7 @@ contract Governance is nonReentrant returns (bool) { - address account = getAccounts().activeVoteSignerToAccount(msg.sender); + address account = getAccounts().voteSignerToAccount(msg.sender); // TODO(asa): When upvoting a proposal that will get dequeued, should we let the tx succeed // and return false? dequeueProposalsIfReady(); @@ -484,7 +484,7 @@ contract Governance is */ function revokeUpvote(uint256 lesser, uint256 greater) external nonReentrant returns (bool) { dequeueProposalsIfReady(); - address account = getAccounts().activeVoteSignerToAccount(msg.sender); + address account = getAccounts().voteSignerToAccount(msg.sender); Voter storage voter = voters[account]; uint256 proposalId = voter.upvote.proposalId; Proposals.Proposal storage proposal = proposals[proposalId]; @@ -548,7 +548,7 @@ contract Governance is nonReentrant returns (bool) { - address account = getAccounts().activeVoteSignerToAccount(msg.sender); + address account = getAccounts().voteSignerToAccount(msg.sender); dequeueProposalsIfReady(); Proposals.Proposal storage proposal = proposals[proposalId]; require(isDequeuedProposal(proposal, proposalId, index)); diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index 194c3833d94..261fc914b8b 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -87,7 +87,7 @@ contract Validators is } struct Validator { - PublicKeys keys; + PublicKeys publicKeys; address affiliation; FixidityLib.Fraction score; MembershipHistory membershipHistory; @@ -115,12 +115,12 @@ contract Validators is event GroupLockedGoldRequirementsSet(uint256 value, uint256 duration); event ValidatorLockedGoldRequirementsSet(uint256 value, uint256 duration); event MembershipHistoryLengthSet(uint256 length); - event ValidatorRegistered(address indexed validator, bytes ecdsaKey, bytes blsKey); + event ValidatorRegistered(address indexed validator, bytes ecdsaPublicKey, bytes blsPublicKey); event ValidatorDeregistered(address indexed validator); event ValidatorAffiliated(address indexed validator, address indexed group); event ValidatorDeaffiliated(address indexed validator, address indexed group); - event ValidatorEcdsaKeyUpdated(address indexed validator, bytes ecdsaKey); - event ValidatorBlsKeyUpdated(address indexed validator, bytes blsKey); + event ValidatorEcdsaPublicKeyUpdated(address indexed validator, bytes ecdsaPublicKey); + event ValidatorBlsPublicKeyUpdated(address indexed validator, bytes blsPublicKey); event ValidatorGroupRegistered(address indexed group, uint256 commission); event ValidatorGroupDeregistered(address indexed group); event ValidatorGroupMemberAdded(address indexed group, address indexed validator); @@ -260,32 +260,32 @@ contract Validators is /** * @notice Registers a validator unaffiliated with any validator group. - * @param ecdsaKey The ECDSA public key that the validator is using for consensus, should match - * the validator signer. 64 bytes. - * @param blsKey The BLS public key that the validator is using for consensus, should pass proof - * of possession. 48 bytes. + * @param ecdsaPublicKey The ECDSA public key that the validator is using for consensus, should + * match the validator signer. 64 bytes. + * @param blsPublicKey The BLS public key that the validator is using for consensus, should pass + * proof of possession. 48 bytes. * @param blsPop The BLS public key proof-of-possession, which consists of a signature on the * account address. 96 bytes. * @return True upon success. * @dev Fails if the account is already a validator or validator group. * @dev Fails if the account does not have sufficient Locked Gold. */ - function registerValidator(bytes calldata ecdsaKey, bytes calldata blsKey, bytes calldata blsPop) - external - nonReentrant - returns (bool) - { - address account = getAccounts().activeValidatorSignerToAccount(msg.sender); + function registerValidator( + bytes calldata ecdsaPublicKey, + bytes calldata blsPublicKey, + bytes calldata blsPop + ) external nonReentrant returns (bool) { + address account = getAccounts().signerToAccount(msg.sender); require(!isValidator(account) && !isValidatorGroup(account)); uint256 lockedGoldBalance = getLockedGold().getAccountTotalLockedGold(account); require(lockedGoldBalance >= validatorLockedGoldRequirements.value); Validator storage validator = validators[account]; address signer = getAccounts().getValidatorSigner(account); - _updateEcdsaKey(validator, signer, ecdsaKey); - _updateBlsKey(validator, account, blsKey, blsPop); + _updateEcdsaPublicKey(validator, signer, ecdsaPublicKey); + _updateBlsPublicKey(validator, account, blsPublicKey, blsPop); registeredValidators.push(account); updateMembershipHistory(account, address(0)); - emit ValidatorRegistered(account, ecdsaKey, blsKey); + emit ValidatorRegistered(account, ecdsaPublicKey, blsPublicKey); return true; } @@ -336,7 +336,7 @@ contract Validators is * @return True upon success. */ function _updateValidatorScoreFromSigner(address signer, uint256 uptime) internal { - address account = getAccounts().validatorSignerToAccount(signer); + address account = getAccounts().signerToAccount(signer); require(isValidator(account)); require(uptime <= FixidityLib.fixed1().unwrap()); @@ -393,7 +393,7 @@ contract Validators is internal returns (uint256) { - address account = getAccounts().validatorSignerToAccount(signer); + address account = getAccounts().signerToAccount(signer); require(isValidator(account)); // The group that should be paid is the group that the validator was a member of at the // time it was elected. @@ -421,7 +421,7 @@ contract Validators is * @dev Fails if the account is not a validator. */ function deregisterValidator(uint256 index) external nonReentrant returns (bool) { - address account = getAccounts().activeValidatorSignerToAccount(msg.sender); + address account = getAccounts().signerToAccount(msg.sender); require(isValidator(account)); // Require that the validator has not been a member of a validator group for @@ -449,7 +449,7 @@ contract Validators is * @dev De-affiliates with the previously affiliated group if present. */ function affiliate(address group) external nonReentrant returns (bool) { - address account = getAccounts().activeValidatorSignerToAccount(msg.sender); + address account = getAccounts().signerToAccount(msg.sender); require(isValidator(account) && isValidatorGroup(group)); require(meetsAccountLockedGoldRequirements(account)); require(meetsAccountLockedGoldRequirements(group)); @@ -468,7 +468,7 @@ contract Validators is * @dev Fails if the account is not a validator with non-zero affiliation. */ function deaffiliate() external nonReentrant returns (bool) { - address account = getAccounts().activeValidatorSignerToAccount(msg.sender); + address account = getAccounts().signerToAccount(msg.sender); require(isValidator(account)); Validator storage validator = validators[account]; require(validator.affiliation != address(0)); @@ -478,41 +478,44 @@ contract Validators is /** * @notice Updates a validator's BLS key. - * @param blsKey The BLS public key that the validator is using for consensus, should pass proof - * of possession. 48 bytes. + * @param blsPublicKey The BLS public key that the validator is using for consensus, should pass + * proof of possession. 48 bytes. * @param blsPop The BLS public key proof-of-possession, which consists of a signature on the * account address. 96 bytes. * @return True upon success. */ - function updateBlsKey(bytes calldata blsKey, bytes calldata blsPop) external returns (bool) { - address account = getAccounts().activeValidatorSignerToAccount(msg.sender); + function updateBlsPublicKey(bytes calldata blsPublicKey, bytes calldata blsPop) + external + returns (bool) + { + address account = getAccounts().signerToAccount(msg.sender); require(isValidator(account)); Validator storage validator = validators[account]; - _updateBlsKey(validator, account, blsKey, blsPop); - emit ValidatorBlsKeyUpdated(account, blsKey); + _updateBlsPublicKey(validator, account, blsPublicKey, blsPop); + emit ValidatorBlsPublicKeyUpdated(account, blsPublicKey); return true; } /** * @notice Updates a validator's BLS key. - * @param validator The validator whose public keys data should be updated. + * @param validator The validator whose BLS public key should be updated. * @param account The address under which the validator is registered. - * @param blsKey The BLS public key that the validator is using for consensus, should pass proof - * of possession. 48 bytes. + * @param blsPublicKey The BLS public key that the validator is using for consensus, should pass + * proof of possession. 48 bytes. * @param blsPop The BLS public key proof-of-possession, which consists of a signature on the * account address. 96 bytes. * @return True upon success. */ - function _updateBlsKey( + function _updateBlsPublicKey( Validator storage validator, address account, - bytes memory blsKey, + bytes memory blsPublicKey, bytes memory blsPop ) private returns (bool) { - require(blsKey.length == 48); + require(blsPublicKey.length == 48); require(blsPop.length == 96); - require(checkProofOfPossession(account, blsKey, blsPop)); - validator.keys.bls = blsKey; + require(checkProofOfPossession(account, blsPublicKey, blsPop)); + validator.publicKeys.bls = blsPublicKey; return true; } @@ -520,39 +523,40 @@ contract Validators is * @notice Updates a validator's ECDSA key. * @param account The address under which the validator is registered. * @param signer The address which the validator is using to sign consensus messages. - * @param ecdsaKey The ECDSA public key corresponding to `signer`. + * @param ecdsaPublicKey The ECDSA public key corresponding to `signer`. * @return True upon success. */ - function updateEcdsaKey(address account, address signer, bytes calldata ecdsaKey) + function updateEcdsaPublicKey(address account, address signer, bytes calldata ecdsaPublicKey) external onlyRegisteredContract(ACCOUNTS_REGISTRY_ID) returns (bool) { require(isValidator(account)); Validator storage validator = validators[account]; - require(_updateEcdsaKey(validator, signer, ecdsaKey)); - emit ValidatorEcdsaKeyUpdated(account, ecdsaKey); + require(_updateEcdsaPublicKey(validator, signer, ecdsaPublicKey)); + emit ValidatorEcdsaPublicKeyUpdated(account, ecdsaPublicKey); return true; } /** * @notice Updates a validator's ECDSA key. - * @param validator The validator whose public keys data should be updated. + * @param validator The validator whose ECDSA public key should be updated. * @param signer The address with which the validator is signing consensus messages. - * @param ecdsaKey The ECDSA public key that the validator is using for consensus. Should match - * `signer`. 64 bytes. + * @param ecdsaPublicKey The ECDSA public key that the validator is using for consensus. Should + * match `signer`. 64 bytes. * @return True upon success. */ - function _updateEcdsaKey(Validator storage validator, address signer, bytes memory ecdsaKey) - private - returns (bool) - { - require(ecdsaKey.length == 64); + function _updateEcdsaPublicKey( + Validator storage validator, + address signer, + bytes memory ecdsaPublicKey + ) private returns (bool) { + require(ecdsaPublicKey.length == 64); require( - address(uint160(uint256(keccak256(ecdsaKey)))) == signer, + address(uint160(uint256(keccak256(ecdsaPublicKey)))) == signer, "ECDSA key does not match signer" ); - validator.keys.ecdsa = ecdsaKey; + validator.publicKeys.ecdsa = ecdsaPublicKey; return true; } @@ -566,7 +570,7 @@ contract Validators is */ function registerValidatorGroup(uint256 commission) external nonReentrant returns (bool) { require(commission <= FixidityLib.fixed1().unwrap(), "Commission can't be greater than 100%"); - address account = getAccounts().activeValidatorSignerToAccount(msg.sender); + address account = getAccounts().signerToAccount(msg.sender); require(!isValidator(account) && !isValidatorGroup(account)); uint256 lockedGoldBalance = getLockedGold().getAccountTotalLockedGold(account); require(lockedGoldBalance >= groupLockedGoldRequirements.value); @@ -585,7 +589,7 @@ contract Validators is * @dev Fails if the account is not a validator group with no members. */ function deregisterValidatorGroup(uint256 index) external nonReentrant returns (bool) { - address account = getAccounts().activeValidatorSignerToAccount(msg.sender); + address account = getAccounts().signerToAccount(msg.sender); // Only Validator Groups that have never had members or have been empty for at least // `groupLockedGoldRequirements.duration` seconds can be deregistered. require(isValidatorGroup(account) && groups[account].members.numElements == 0); @@ -607,7 +611,7 @@ contract Validators is * @dev Fails if the group has zero members. */ function addMember(address validator) external nonReentrant returns (bool) { - address account = getAccounts().activeValidatorSignerToAccount(msg.sender); + address account = getAccounts().signerToAccount(msg.sender); require(groups[account].members.numElements > 0); return _addMember(account, validator, address(0), address(0)); } @@ -626,7 +630,7 @@ contract Validators is nonReentrant returns (bool) { - address account = getAccounts().activeValidatorSignerToAccount(msg.sender); + address account = getAccounts().signerToAccount(msg.sender); require(groups[account].members.numElements == 0); return _addMember(account, validator, lesser, greater); } @@ -669,7 +673,7 @@ contract Validators is * @dev Fails if `validator` is not a member of the account's group. */ function removeMember(address validator) external nonReentrant returns (bool) { - address account = getAccounts().activeValidatorSignerToAccount(msg.sender); + address account = getAccounts().signerToAccount(msg.sender); require(isValidatorGroup(account) && isValidator(validator), "is not group and validator"); return _removeMember(account, validator); } @@ -689,7 +693,7 @@ contract Validators is nonReentrant returns (bool) { - address account = getAccounts().activeValidatorSignerToAccount(msg.sender); + address account = getAccounts().signerToAccount(msg.sender); require(isValidatorGroup(account) && isValidator(validator)); ValidatorGroup storage group = groups[account]; require(group.members.contains(validator)); @@ -705,7 +709,7 @@ contract Validators is * @return True upon success. */ function updateCommission(uint256 commission) external returns (bool) { - address account = getAccounts().activeValidatorSignerToAccount(msg.sender); + address account = getAccounts().signerToAccount(msg.sender); require(isValidatorGroup(account)); ValidatorGroup storage group = groups[account]; require(commission <= FixidityLib.fixed1().unwrap(), "Commission can't be greater than 100%"); @@ -754,14 +758,14 @@ contract Validators is * @param signer The account that registered the validator or its authorized signing address. * @return The validator BLS key. */ - function getValidatorBlsKeyFromSigner(address signer) + function getValidatorBlsPublicKeyFromSigner(address signer) external view - returns (bytes memory blsKey) + returns (bytes memory blsPublicKey) { - address account = getAccounts().validatorSignerToAccount(signer); + address account = getAccounts().signerToAccount(signer); require(isValidator(account)); - return validators[account].keys.bls; + return validators[account].publicKeys.bls; } /** @@ -772,13 +776,18 @@ contract Validators is function getValidator(address account) public view - returns (bytes memory ecdsaKey, bytes memory blsKey, address affiliation, uint256 score) + returns ( + bytes memory ecdsaPublicKey, + bytes memory blsPublicKey, + address affiliation, + uint256 score + ) { require(isValidator(account)); Validator storage validator = validators[account]; return ( - validator.keys.ecdsa, - validator.keys.bls, + validator.publicKeys.ecdsa, + validator.publicKeys.bls, validator.affiliation, validator.score.unwrap() ); @@ -913,7 +922,7 @@ contract Validators is * @return Whether a particular address is a registered validator. */ function isValidator(address account) public view returns (bool) { - return validators[account].keys.bls.length > 0; + return validators[account].publicKeys.bls.length > 0; } /** @@ -1020,7 +1029,7 @@ contract Validators is * @return The group that `account` was a member of at the end of the last epoch. */ function getMembershipInLastEpochFromSigner(address signer) external view returns (address) { - address account = getAccounts().validatorSignerToAccount(signer); + address account = getAccounts().signerToAccount(signer); require(isValidator(account)); return getMembershipInLastEpoch(account); } diff --git a/packages/protocol/contracts/governance/interfaces/IValidators.sol b/packages/protocol/contracts/governance/interfaces/IValidators.sol index 6be5006e534..fcb6e3cd1ef 100644 --- a/packages/protocol/contracts/governance/interfaces/IValidators.sol +++ b/packages/protocol/contracts/governance/interfaces/IValidators.sol @@ -7,6 +7,6 @@ interface IValidators { function getGroupsNumMembers(address[] calldata) external view returns (uint256[] memory); function getNumRegisteredValidators() external view returns (uint256); function getTopGroupValidators(address, uint256) external view returns (address[] memory); - function updateEcdsaKey(address, address, bytes calldata) external returns (bool); + function updateEcdsaPublicKey(address, address, bytes calldata) external returns (bool); function isValidator(address) external view returns (bool); } diff --git a/packages/protocol/contracts/identity/Attestations.sol b/packages/protocol/contracts/identity/Attestations.sol index de98e2a4e92..3124b517400 100644 --- a/packages/protocol/contracts/identity/Attestations.sol +++ b/packages/protocol/contracts/identity/Attestations.sol @@ -508,7 +508,7 @@ contract Attestations is ) public view returns (address) { bytes32 codehash = keccak256(abi.encodePacked(identifier, account)); address signer = Signatures.getSignerOfMessageHash(codehash, v, r, s); - address issuer = getAccounts().activeAttesttationSignerToAccount(signer); + address issuer = getAccounts().attestationSignerToAccount(signer); Attestation storage attestation = identifiers[identifier].attestations[account] .issuedAttestations[issuer]; @@ -577,7 +577,7 @@ contract Attestations is while (currentIndex < unselectedRequest.attestationsRequested) { seed = keccak256(abi.encodePacked(seed)); validator = validatorAddressFromCurrentSet(uint256(seed) % numberValidators); - issuer = getAccounts().activeValidatorSignerToAccount(validator); + issuer = getAccounts().validatorSignerToAccount(validator); Attestation storage attestation = state.issuedAttestations[issuer]; // Attestation issuers can only be added if they haven't been already. diff --git a/packages/protocol/test/common/accounts.ts b/packages/protocol/test/common/accounts.ts index 1088953bddf..461dbbecc12 100644 --- a/packages/protocol/test/common/accounts.ts +++ b/packages/protocol/test/common/accounts.ts @@ -58,22 +58,19 @@ contract('Accounts', (accounts: string[]) => { fn: accountsInstance.authorizeVoteSigner, eventName: 'VoteSignerAuthorized', getAuthorizedFromAccount: accountsInstance.getVoteSigner, - getAccountFromAuthorized: accountsInstance.voteSignerToAccount, - getAccountFromActiveAuthorized: accountsInstance.activeVoteSignerToAccount, + authorizedSignerToAccount: accountsInstance.voteSignerToAccount, } authorizationTests.validating = { fn: accountsInstance.authorizeValidatorSigner, eventName: 'ValidatorSignerAuthorized', getAuthorizedFromAccount: accountsInstance.getValidatorSigner, - getAccountFromAuthorized: accountsInstance.validatorSignerToAccount, - getAccountFromActiveAuthorized: accountsInstance.activeValidatorSignerToAccount, + authorizedSignerToAccount: accountsInstance.validatorSignerToAccount, } authorizationTests.attesting = { fn: accountsInstance.authorizeAttestationSigner, eventName: 'AttestationSignerAuthorized', getAuthorizedFromAccount: accountsInstance.getAttestationSigner, - getAccountFromAuthorized: accountsInstance.attestationSignerToAccount, - getAccountFromActiveAuthorized: accountsInstance.activeAttesttationSignerToAccount, + authorizedSignerToAccount: accountsInstance.attesttationSignerToAccount, } }) @@ -373,7 +370,7 @@ contract('Accounts', (accounts: string[]) => { await authorizationTest.fn(authorized, sig.v, sig.r, sig.s) assert.equal(await accountsInstance.authorizedBy(authorized), account) assert.equal(await authorizationTest.getAuthorizedFromAccount(account), authorized) - assert.equal(await authorizationTest.getAccountFromActiveAuthorized(authorized), account) + assert.equal(await authorizationTest.authorizedSignerToAccount(authorized), account) }) it(`should emit the right event`, async () => { @@ -423,10 +420,7 @@ contract('Accounts', (accounts: string[]) => { it(`should set the new authorized ${authorizationTestDescriptions[key].me}`, async () => { assert.equal(await accountsInstance.authorizedBy(newAuthorized), account) assert.equal(await authorizationTest.getAuthorizedFromAccount(account), newAuthorized) - assert.equal( - await authorizationTest.getAccountFromActiveAuthorized(newAuthorized), - account - ) + assert.equal(await authorizationTest.authorizedSignerToAccount(newAuthorized), account) }) it('should preserve the previous authorization', async () => { @@ -440,11 +434,11 @@ contract('Accounts', (accounts: string[]) => { authorizationTestDescriptions[key].me }`, () => { it('should return the account when passed the account', async () => { - assert.equal(await authorizationTest.getAccountFromActiveAuthorized(account), account) + assert.equal(await authorizationTest.authorizedSignerToAccount(account), account) }) it('should revert when passed an address that is not an account', async () => { - await assertRevert(authorizationTest.getAccountFromActiveAuthorized(accounts[1])) + await assertRevert(authorizationTest.authorizedSignerToAccount(accounts[1])) }) }) @@ -458,16 +452,13 @@ contract('Accounts', (accounts: string[]) => { }) it('should return the account when passed the account', async () => { - assert.equal(await authorizationTest.getAccountFromActiveAuthorized(account), account) + assert.equal(await authorizationTest.authorizedSignerToAccount(account), account) }) it(`should return the account when passed the ${ authorizationTestDescriptions[key].me }`, async () => { - assert.equal( - await authorizationTest.getAccountFromActiveAuthorized(authorized), - account - ) + assert.equal(await authorizationTest.authorizedSignerToAccount(authorized), account) }) }) }) diff --git a/packages/protocol/test/governance/validators.ts b/packages/protocol/test/governance/validators.ts index 403cd5a82c2..2029ed4374b 100644 --- a/packages/protocol/test/governance/validators.ts +++ b/packages/protocol/test/governance/validators.ts @@ -41,8 +41,8 @@ Validators.numberFormat = 'BigNumber' const parseValidatorParams = (validatorParams: any) => { return { - ecdsaKey: validatorParams[0], - blsKey: validatorParams[1], + ecdsaPublicKey: validatorParams[0], + blsPublicKey: validatorParams[1], affiliation: validatorParams[2], score: validatorParams[3], } @@ -566,12 +566,12 @@ contract('Validators', (accounts: string[]) => { it('should set the validator ecdsa public key', async () => { const parsedValidator = parseValidatorParams(await validators.getValidator(validator)) - assert.equal(parsedValidator.ecdsaKey, publicKey) + assert.equal(parsedValidator.ecdsaPublicKey, publicKey) }) it('should set the validator bls public key', async () => { const parsedValidator = parseValidatorParams(await validators.getValidator(validator)) - assert.equal(parsedValidator.blsKey, blsPublicKey) + assert.equal(parsedValidator.blsPublicKey, blsPublicKey) }) it('should set account locked gold requirements', async () => { @@ -598,8 +598,8 @@ contract('Validators', (accounts: string[]) => { event: 'ValidatorRegistered', args: { validator, - ecdsaKey: publicKey, - blsKey: blsPublicKey, + ecdsaPublicKey: publicKey, + blsPublicKey: blsPublicKey, }, }) }) @@ -1068,7 +1068,7 @@ contract('Validators', (accounts: string[]) => { }) }) - describe('#updateEcdsaKey()', () => { + describe('#updateEcdsaPublicKey()', () => { describe('when called by a registered validator', () => { const validator = accounts[0] beforeEach(async () => { @@ -1087,22 +1087,22 @@ contract('Validators', (accounts: string[]) => { beforeEach(async () => { newPublicKey = await addressToPublicKey(signer, web3) // @ts-ignore Broken typechain typing for bytes - resp = await validators.updateEcdsaKey(validator, signer, newPublicKey) + resp = await validators.updateEcdsaPublicKey(validator, signer, newPublicKey) }) it('should set the validator ecdsa public key', async () => { const parsedValidator = parseValidatorParams(await validators.getValidator(validator)) - assert.equal(parsedValidator.ecdsaKey, newPublicKey) + assert.equal(parsedValidator.ecdsaPublicKey, newPublicKey) }) - it('should emit the ValidatorEcdsaKeyUpdated event', async () => { + it('should emit the ValidatorEcdsaPublicKeyUpdated event', async () => { assert.equal(resp.logs.length, 1) const log = resp.logs[0] assertContainSubset(log, { - event: 'ValidatorEcdsaKeyUpdated', + event: 'ValidatorEcdsaPublicKeyUpdated', args: { validator, - ecdsaKey: newPublicKey, + ecdsaPublicKey: newPublicKey, }, }) }) @@ -1114,7 +1114,7 @@ contract('Validators', (accounts: string[]) => { it('should revert', async () => { newPublicKey = await addressToPublicKey(accounts[8], web3) // @ts-ignore Broken typechain typing for bytes - await assertRevert(validators.updateEcdsaKey(validator, signer, newPublicKey)) + await assertRevert(validators.updateEcdsaPublicKey(validator, signer, newPublicKey)) }) }) }) @@ -1126,14 +1126,14 @@ contract('Validators', (accounts: string[]) => { it('should revert', async () => { newPublicKey = await addressToPublicKey(signer, web3) // @ts-ignore Broken typechain typing for bytes - await assertRevert(validators.updateEcdsaKey(validator, signer, newPublicKey)) + await assertRevert(validators.updateEcdsaPublicKey(validator, signer, newPublicKey)) }) }) }) }) }) - describe('#updateBlsKey()', () => { + describe('#updateBlsPublicKey()', () => { const newBlsPublicKey = web3.utils.randomHex(48) const newBlsPoP = web3.utils.randomHex(96) describe('when called by a registered validator', () => { @@ -1146,22 +1146,22 @@ contract('Validators', (accounts: string[]) => { let resp: any beforeEach(async () => { // @ts-ignore Broken typechain typing for bytes - resp = await validators.updateBlsKey(newBlsPublicKey, newBlsPoP) + resp = await validators.updateBlsPublicKey(newBlsPublicKey, newBlsPoP) }) it('should set the validator bls public key', async () => { const parsedValidator = parseValidatorParams(await validators.getValidator(validator)) - assert.equal(parsedValidator.blsKey, newBlsPublicKey) + assert.equal(parsedValidator.blsPublicKey, newBlsPublicKey) }) - it('should emit the ValidatorBlsKeyUpdated event', async () => { + it('should emit the ValidatorBlsPublicKeyUpdated event', async () => { assert.equal(resp.logs.length, 1) const log = resp.logs[0] assertContainSubset(log, { - event: 'ValidatorBlsKeyUpdated', + event: 'ValidatorBlsPublicKeyUpdated', args: { validator, - blsKey: newBlsPublicKey, + blsPublicKey: newBlsPublicKey, }, }) }) @@ -1170,14 +1170,14 @@ contract('Validators', (accounts: string[]) => { describe('when the public key is not 48 bytes', () => { it('should revert', async () => { // @ts-ignore Broken typechain typing for bytes - await assertRevert(validators.updateBlsKey(newBlsPublicKey + '01', newBlsPoP)) + await assertRevert(validators.updateBlsPublicKey(newBlsPublicKey + '01', newBlsPoP)) }) }) describe('when the proof of possession is not 96 bytes', () => { it('should revert', async () => { // @ts-ignore Broken typechain typing for bytes - await assertRevert(validators.updateBlsKey(newBlsPublicKey, newBlsPoP + '01')) + await assertRevert(validators.updateBlsPublicKey(newBlsPublicKey, newBlsPoP + '01')) }) }) }) diff --git a/packages/utils/src/address.ts b/packages/utils/src/address.ts index 96c1020a62c..f6d123abae8 100644 --- a/packages/utils/src/address.ts +++ b/packages/utils/src/address.ts @@ -1,35 +1,7 @@ -import assert = require('assert') -import { - ecrecover, - fromRpcSig, - privateToAddress, - privateToPublic, - pubToAddress, - sha3, - toChecksumAddress, -} from 'ethereumjs-util' -import Web3 from 'web3' +import { privateToAddress, privateToPublic, toChecksumAddress } from 'ethereumjs-util' export type Address = string -export async function addressToPublicKey(address: string, web3: Web3) { - const msg = new Buffer('dummy_msg_data') - const data = '0x' + msg.toString('hex') - // Note: Eth.sign typing displays incorrect parameter order - const sig = await web3.eth.sign(data, address) - - const rawsig = fromRpcSig(sig) - - const prefix = new Buffer('\x19Ethereum Signed Message:\n') - const prefixedMsg = sha3(Buffer.concat([prefix, new Buffer(String(msg.length)), msg])) - const pubKey = ecrecover(prefixedMsg, rawsig.v, rawsig.r, rawsig.s) - - const computedAddr = pubToAddress(pubKey).toString('hex') - assert(eqAddress(computedAddr, address), 'computed address !== address') - - return '0x' + pubKey.toString('hex') -} - export function eqAddress(a: Address, b: Address) { return a.replace('0x', '').toLowerCase() === b.replace('0x', '').toLowerCase() } @@ -45,4 +17,5 @@ export const privateKeyToPublicKey = (privateKey: string) => { '0x' + privateToPublic(Buffer.from(privateKey.slice(2), 'hex')).toString('hex') ) } -export { toChecksumAddress } from 'ethereumjs-util' +export { isValidAddress } from 'ethereumjs-util' +export { isValidChecksumAddress } from 'ethereumjs-util' diff --git a/packages/utils/src/bls.ts b/packages/utils/src/bls.ts index 80df761d426..287d69f6fe1 100644 --- a/packages/utils/src/bls.ts +++ b/packages/utils/src/bls.ts @@ -3,10 +3,13 @@ const keccak256 = require('keccak256') const BigInteger = require('bigi') const reverse = require('buffer-reverse') import * as bls12377js from 'bls12377js' +import { isValidAddress } from './address' const n = BigInteger.fromHex('12ab655e9a2ca55660b44d1e5c37b00159aa76fed00000010a11800000000001', 16) const MODULUSMASK = 31 +export const BLS_PUBLIC_KEY_SIZE = 48 +export const BLS_POP_SIZE = 96 export const blsPrivateKeyToProcessedPrivateKey = (privateKeyHex: string) => { for (let i = 0; i < 256; i++) { @@ -48,6 +51,9 @@ export const getBlsPublicKey = (privateKeyHex: string) => { } export const getBlsPoP = (address: string, privateKeyHex: string) => { + if (!isValidAddress(address)) { + throw new Error('Invalid checksum address for generating BLS proof-of-possession') + } const blsPrivateKeyBytes = getBlsPrivateKey(privateKeyHex) return ( '0x' + diff --git a/packages/utils/src/signatureUtils.ts b/packages/utils/src/signatureUtils.ts index d8aebacf066..ff888994438 100644 --- a/packages/utils/src/signatureUtils.ts +++ b/packages/utils/src/signatureUtils.ts @@ -1,7 +1,9 @@ +import assert = require('assert') + const ethjsutil = require('ethereumjs-util') import * as Web3Utils from 'web3-utils' -import { privateKeyToAddress } from './address' +import { Address, privateKeyToAddress } from './address' // If messages is a hex, the length of it should be the number of bytes function messageLength(message: string) { @@ -25,6 +27,29 @@ export interface Signer { sign: (message: string) => Promise } +export async function addressToPublicKey( + signer: string, + signFn: (message: string, signer: string) => Promise +) { + const msg = new Buffer('dummy_msg_data') + const data = '0x' + msg.toString('hex') + // Note: Eth.sign typing displays incorrect parameter order + const sig = await signFn(data, signer) + + const rawsig = ethjsutil.fromRpcSig(sig) + const prefixedMsg = hashMessageWithPrefix(data) + const pubKey = ethjsutil.ecrecover(prefixedMsg, rawsig.v, rawsig.r, rawsig.s) + + const computedAddr = ethjsutil.pubToAddress(pubKey).toString('hex') + assert(eqAddress(computedAddr, signer), 'computed address !== signer') + + return '0x' + pubKey.toString('hex') +} + +export function eqAddress(a: Address, b: Address) { + return a.replace('0x', '').toLowerCase() === b.replace('0x', '').toLowerCase() +} + // Uses a native function to sign (as signFn), most commonly `web.eth.sign` export function NativeSigner( signFn: (message: string, signer: string) => Promise, From 627b9f67e3913713aa6ef335040d45853a8e2efc Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Wed, 13 Nov 2019 21:20:40 -0800 Subject: [PATCH 141/149] Make things compile --- packages/docs/command-line-interface/validator.md | 6 +++--- packages/protocol/test/common/accounts.ts | 2 +- packages/utils/src/signatureUtils.ts | 7 ++++++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/docs/command-line-interface/validator.md b/packages/docs/command-line-interface/validator.md index 1eb6d7276f8..8bfdea3e57d 100644 --- a/packages/docs/command-line-interface/validator.md +++ b/packages/docs/command-line-interface/validator.md @@ -128,13 +128,13 @@ EXAMPLE _See code: [packages/cli/src/commands/validator/show.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validator/show.ts)_ -### Update-bls-key +### Update-bls-public-key Update BLS key for a validator ``` USAGE - $ celocli validator:update-bls-key + $ celocli validator:update-bls-public-key OPTIONS --blsKey=0x (required) BLS Public Key @@ -148,4 +148,4 @@ EXAMPLE dabf64485a1d96bad27bb1c0fd6080a75b0ec9f75b50298a2a8e04b02b2688c8104fca61fb00 ``` -_See code: [packages/cli/src/commands/validator/update-bls-key.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validator/update-bls-key.ts)_ +_See code: [packages/cli/src/commands/validator/update-bls-public-key.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validator/update-bls-public-key.ts)_ diff --git a/packages/protocol/test/common/accounts.ts b/packages/protocol/test/common/accounts.ts index 461dbbecc12..86c65839056 100644 --- a/packages/protocol/test/common/accounts.ts +++ b/packages/protocol/test/common/accounts.ts @@ -70,7 +70,7 @@ contract('Accounts', (accounts: string[]) => { fn: accountsInstance.authorizeAttestationSigner, eventName: 'AttestationSignerAuthorized', getAuthorizedFromAccount: accountsInstance.getAttestationSigner, - authorizedSignerToAccount: accountsInstance.attesttationSignerToAccount, + authorizedSignerToAccount: accountsInstance.attestationSignerToAccount, } }) diff --git a/packages/utils/src/signatureUtils.ts b/packages/utils/src/signatureUtils.ts index ff888994438..4b2f2ca9755 100644 --- a/packages/utils/src/signatureUtils.ts +++ b/packages/utils/src/signatureUtils.ts @@ -38,7 +38,12 @@ export async function addressToPublicKey( const rawsig = ethjsutil.fromRpcSig(sig) const prefixedMsg = hashMessageWithPrefix(data) - const pubKey = ethjsutil.ecrecover(prefixedMsg, rawsig.v, rawsig.r, rawsig.s) + const pubKey = ethjsutil.ecrecover( + Buffer.from(prefixedMsg.slice(2), 'hex'), + rawsig.v, + rawsig.r, + rawsig.s + ) const computedAddr = ethjsutil.pubToAddress(pubKey).toString('hex') assert(eqAddress(computedAddr, signer), 'computed address !== signer') From d550f1eb8c4c07d630a8f289f89e3715b953c09f Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Wed, 13 Nov 2019 21:39:16 -0800 Subject: [PATCH 142/149] Finish removal of end-to-end-geth-integration-sync-test --- .circleci/config.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 98cb80a8e10..bf303064ab7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -743,10 +743,6 @@ workflows: requires: - lint-checks - contractkit-test - - end-to-end-geth-integration-sync-test: - requires: - - lint-checks - - contractkit-test - end-to-end-geth-attestations-test: requires: - lint-checks From 998ead718214a144b4a7374d52a8f8a1c894b3fa Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Thu, 14 Nov 2019 09:43:13 -0800 Subject: [PATCH 143/149] Fix unit tests --- .../protocol/test/governance/validators.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/protocol/test/governance/validators.ts b/packages/protocol/test/governance/validators.ts index 2029ed4374b..e0191dcb87e 100644 --- a/packages/protocol/test/governance/validators.ts +++ b/packages/protocol/test/governance/validators.ts @@ -11,7 +11,7 @@ import { timeTravel, } from '@celo/protocol/lib/test-utils' import { fixed1, fromFixed, toFixed } from '@celo/utils/lib/fixidity' -import { addressToPublicKey } from '@celo/utils/lib/address' +import { addressToPublicKey } from '@celo/utils/lib/signatureUtils' import BigNumber from 'bignumber.js' import { AccountsContract, @@ -128,7 +128,7 @@ contract('Validators', (accounts: string[]) => { const registerValidator = async (validator: string) => { await mockLockedGold.setAccountTotalLockedGold(validator, validatorLockedGoldRequirements.value) - const publicKey = await addressToPublicKey(validator, web3) + const publicKey = await addressToPublicKey(validator, web3.eth.sign) await validators.registerValidator( // @ts-ignore bytes type publicKey, @@ -543,7 +543,7 @@ contract('Validators', (accounts: string[]) => { const signer = accounts[9] const sig = await getParsedSignatureOfAddress(web3, validator, signer) await accountsInstance.authorizeValidatorSigner(signer, sig.v, sig.r, sig.s) - publicKey = await addressToPublicKey(signer, web3) + publicKey = await addressToPublicKey(signer, web3.eth.sign) resp = await validators.registerValidator( // @ts-ignore bytes type publicKey, @@ -616,7 +616,7 @@ contract('Validators', (accounts: string[]) => { }) it('should revert', async () => { - publicKey = await addressToPublicKey(validator, web3) + publicKey = await addressToPublicKey(validator, web3.eth.sign) await validators.registerValidator( // @ts-ignore bytes type publicKey, @@ -645,7 +645,7 @@ contract('Validators', (accounts: string[]) => { }) it('should revert', async () => { - const publicKey = await addressToPublicKey(validator, web3) + const publicKey = await addressToPublicKey(validator, web3.eth.sign) await assertRevert( validators.registerValidator( // @ts-ignore bytes type @@ -668,7 +668,7 @@ contract('Validators', (accounts: string[]) => { }) it('should revert', async () => { - const publicKey = await addressToPublicKey(validator, web3) + const publicKey = await addressToPublicKey(validator, web3.eth.sign) await assertRevert( validators.registerValidator( // @ts-ignore bytes type @@ -1085,7 +1085,7 @@ contract('Validators', (accounts: string[]) => { let newPublicKey: string const signer = accounts[9] beforeEach(async () => { - newPublicKey = await addressToPublicKey(signer, web3) + newPublicKey = await addressToPublicKey(signer, web3.eth.sign) // @ts-ignore Broken typechain typing for bytes resp = await validators.updateEcdsaPublicKey(validator, signer, newPublicKey) }) @@ -1112,7 +1112,7 @@ contract('Validators', (accounts: string[]) => { let newPublicKey: string const signer = accounts[9] it('should revert', async () => { - newPublicKey = await addressToPublicKey(accounts[8], web3) + newPublicKey = await addressToPublicKey(accounts[8], web3.eth.sign) // @ts-ignore Broken typechain typing for bytes await assertRevert(validators.updateEcdsaPublicKey(validator, signer, newPublicKey)) }) @@ -1124,7 +1124,7 @@ contract('Validators', (accounts: string[]) => { let newPublicKey: string const signer = accounts[9] it('should revert', async () => { - newPublicKey = await addressToPublicKey(signer, web3) + newPublicKey = await addressToPublicKey(signer, web3.eth.sign) // @ts-ignore Broken typechain typing for bytes await assertRevert(validators.updateEcdsaPublicKey(validator, signer, newPublicKey)) }) From bb16d9877edb3eaeb5ad73e9c4e70711caad8a5c Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Thu, 14 Nov 2019 14:36:53 -0800 Subject: [PATCH 144/149] Fix tests --- .../src/e2e-tests/governance_tests.ts | 23 ++++++++++++------- .../governance/test/MockValidators.sol | 2 +- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/packages/celotool/src/e2e-tests/governance_tests.ts b/packages/celotool/src/e2e-tests/governance_tests.ts index c22d58b0d04..950787d845c 100644 --- a/packages/celotool/src/e2e-tests/governance_tests.ts +++ b/packages/celotool/src/e2e-tests/governance_tests.ts @@ -1,3 +1,5 @@ +// tslint:disable-next-line: no-reference (Required to make this work w/ ts-node) +/// import { ContractKit, newKitFromWeb3 } from '@celo/contractkit' import { getBlsPoP, getBlsPublicKey } from '@celo/utils/lib/bls' import { fromFixed, toFixed } from '@celo/utils/lib/fixidity' @@ -138,15 +140,18 @@ describe('governance tests', () => { } const authorizeValidatorSigner = async (validatorWeb3: any, signerWeb3: any) => { - const validator = (await validatorWeb3.eth.getAccounts())[0] - const signer = (await signerWeb3.eth.getAccounts())[0] + const validator: string = (await validatorWeb3.eth.getAccounts())[0] + const signer: string = (await signerWeb3.eth.getAccounts())[0] await unlockAccount(validator, validatorWeb3) await unlockAccount(signer, signerWeb3) const pop = await (await newKitFromWeb3( signerWeb3 ).contracts.getAccounts()).generateProofOfSigningKeyPossession(validator, signer) const accountsWrapper = await newKitFromWeb3(validatorWeb3).contracts.getAccounts() - await accountsWrapper.authorizeValidatorSigner(signer, pop) + return await (await accountsWrapper.authorizeValidatorSigner( + signer, + pop + )).sendAndWaitForReceipt({ from: validator }) } const updateValidatorBlsKey = async ( @@ -154,14 +159,16 @@ describe('governance tests', () => { signerWeb3: any, signerPrivateKey: string ) => { - const validator = (await validatorWeb3.eth.getAccounts())[0] - const signer = (await signerWeb3.eth.getAccounts())[0] + const validator: string = (await validatorWeb3.eth.getAccounts())[0] + const signer: string = (await signerWeb3.eth.getAccounts())[0] await unlockAccount(signer, signerWeb3) const blsPublicKey = getBlsPublicKey(signerPrivateKey) const blsPop = getBlsPoP(validator, signerPrivateKey) // TODO(asa): Send this from the signer instead. const validatorsWrapper = await newKitFromWeb3(validatorWeb3).contracts.getValidators() - await validatorsWrapper.updateBlsPublicKey(blsPublicKey, blsPop) + return await validatorsWrapper + .updateBlsPublicKey(blsPublicKey, blsPop) + .sendAndWaitForReceipt({ from: validator }) } const isLastBlockOfEpoch = (blockNumber: number, epochSize: number) => { @@ -339,7 +346,7 @@ describe('governance tests', () => { const signingKeys = await getValidatorSetSignersAtBlock(blockNumber) return Promise.all( signingKeys.map((address: string) => - accounts.methods.validatorSignerToAccount(address).call({}, blockNumber) + accounts.methods.signerToAccount(address).call({}, blockNumber) ) ) } @@ -387,7 +394,7 @@ describe('governance tests', () => { async (_, i) => (await web3.eth.getBlock(lastEpochBlock + i + 1)).miner ) ) - assert.sameMembers(validatorSet, roundRobinOrder) + assert.sameMembers(roundRobinOrder, validatorSet) } const indexInEpoch = blockNumber - lastEpochBlock - 1 const expectedProposer = roundRobinOrder[indexInEpoch % roundRobinOrder.length] diff --git a/packages/protocol/contracts/governance/test/MockValidators.sol b/packages/protocol/contracts/governance/test/MockValidators.sol index 4e7b3a75d27..0a81756a8cf 100644 --- a/packages/protocol/contracts/governance/test/MockValidators.sol +++ b/packages/protocol/contracts/governance/test/MockValidators.sol @@ -13,7 +13,7 @@ contract MockValidators is IValidators { mapping(address => address[]) private members; uint256 private numRegisteredValidators; - function updateEcdsaKey(address, address, bytes calldata) external returns (bool) { + function updateEcdsaKey(address, address, bytes calldata) external pure returns (bool) { return true; } From a0d7503f58d438b7cccf77feff2c56f1e8176e87 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Thu, 14 Nov 2019 14:38:37 -0800 Subject: [PATCH 145/149] Remove unnecessary awaits --- packages/celotool/src/e2e-tests/governance_tests.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/celotool/src/e2e-tests/governance_tests.ts b/packages/celotool/src/e2e-tests/governance_tests.ts index 950787d845c..383ac62a364 100644 --- a/packages/celotool/src/e2e-tests/governance_tests.ts +++ b/packages/celotool/src/e2e-tests/governance_tests.ts @@ -148,10 +148,9 @@ describe('governance tests', () => { signerWeb3 ).contracts.getAccounts()).generateProofOfSigningKeyPossession(validator, signer) const accountsWrapper = await newKitFromWeb3(validatorWeb3).contracts.getAccounts() - return await (await accountsWrapper.authorizeValidatorSigner( - signer, - pop - )).sendAndWaitForReceipt({ from: validator }) + return (await accountsWrapper.authorizeValidatorSigner(signer, pop)).sendAndWaitForReceipt({ + from: validator, + }) } const updateValidatorBlsKey = async ( @@ -166,7 +165,7 @@ describe('governance tests', () => { const blsPop = getBlsPoP(validator, signerPrivateKey) // TODO(asa): Send this from the signer instead. const validatorsWrapper = await newKitFromWeb3(validatorWeb3).contracts.getValidators() - return await validatorsWrapper + return validatorsWrapper .updateBlsPublicKey(blsPublicKey, blsPop) .sendAndWaitForReceipt({ from: validator }) } From 2c4b3c1a5fe9d88fd4e946d01378050a02d425c7 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Thu, 14 Nov 2019 21:13:02 -0800 Subject: [PATCH 146/149] Fix build --- packages/utils/src/address.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/utils/src/address.ts b/packages/utils/src/address.ts index 3d0f7b10b5d..5f296bc9884 100644 --- a/packages/utils/src/address.ts +++ b/packages/utils/src/address.ts @@ -24,10 +24,6 @@ export const publicKeyToAddress = (publicKey: string) => { ) } -export const privateKeyToPublicKey = (privateKey: string) => { - return '0x' + privateToPublic(Buffer.from(privateKey.slice(2), 'hex')).toString('hex') -} - export { toChecksumAddress } from 'ethereumjs-util' export { isValidAddress } from 'ethereumjs-util' export { isValidChecksumAddress } from 'ethereumjs-util' From c9c01d90e6b147e568209e91e12401c40e953cde Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Fri, 15 Nov 2019 13:11:16 -0800 Subject: [PATCH 147/149] WIP --- .../governance/test/MockValidators.sol | 2 +- packages/protocol/lib/test-utils.ts | 25 ----------- .../protocol/test/governance/validators.ts | 45 +++++++++++-------- .../protocol/test/identity/attestations.ts | 10 ++--- 4 files changed, 33 insertions(+), 49 deletions(-) diff --git a/packages/protocol/contracts/governance/test/MockValidators.sol b/packages/protocol/contracts/governance/test/MockValidators.sol index 0a81756a8cf..aa2406f8e0f 100644 --- a/packages/protocol/contracts/governance/test/MockValidators.sol +++ b/packages/protocol/contracts/governance/test/MockValidators.sol @@ -13,7 +13,7 @@ contract MockValidators is IValidators { mapping(address => address[]) private members; uint256 private numRegisteredValidators; - function updateEcdsaKey(address, address, bytes calldata) external pure returns (bool) { + function updateEcdsaPublicKey(address, address, bytes calldata) external returns (bool) { return true; } diff --git a/packages/protocol/lib/test-utils.ts b/packages/protocol/lib/test-utils.ts index 812669b0f5a..0605852eaed 100644 --- a/packages/protocol/lib/test-utils.ts +++ b/packages/protocol/lib/test-utils.ts @@ -34,30 +34,6 @@ export function assertContainSubset(superset: any, subset: any) { return assert2.containSubset(superset, subset) } -export async function advanceBlockNum(numBlocks: number, web3: Web3) { - let returnValue: any - for (let i: number = 0; i < numBlocks; i++) { - returnValue = new Promise((resolve, reject) => { - web3.currentProvider.send( - { - jsonrpc: '2.0', - method: 'evm_mine', - params: [], - id: new Date().getTime(), - }, - // @ts-ignore - (err: any, result: any) => { - if (err) { - return reject(err) - } - return resolve(result) - } - ) - }) - } - return returnValue -} - export async function jsonRpc(web3: Web3, method: string, params: any[] = []): Promise { return new Promise((resolve, reject) => { web3.currentProvider.send( @@ -342,7 +318,6 @@ export const matchAny = () => { } export default { - advanceBlockNum, assertContainSubset, assertRevert, timeTravel, diff --git a/packages/protocol/test/governance/validators.ts b/packages/protocol/test/governance/validators.ts index e0191dcb87e..387b5fdbd62 100644 --- a/packages/protocol/test/governance/validators.ts +++ b/packages/protocol/test/governance/validators.ts @@ -552,7 +552,7 @@ contract('Validators', (accounts: string[]) => { // @ts-ignore bytes type blsPoP ) - const blockNumber = (await web3.eth.getBlock('latest')).number + const blockNumber = await web3.eth.getBlockNumber() validatorRegistrationEpochNumber = Math.floor(blockNumber / EPOCH) }) @@ -802,7 +802,7 @@ contract('Validators', (accounts: string[]) => { describe('when the account has a registered validator', () => { beforeEach(async () => { await registerValidator(validator) - registrationEpoch = Math.floor((await web3.eth.getBlock('latest')).number / EPOCH) + registrationEpoch = Math.floor((await web3.eth.getBlockNumber()) / EPOCH) }) describe('when affiliating with a registered validator group', () => { beforeEach(async () => { @@ -883,9 +883,9 @@ contract('Validators', (accounts: string[]) => { await validators.addFirstMember(validator, NULL_ADDRESS, NULL_ADDRESS, { from: group, }) - additionEpoch = Math.floor((await web3.eth.getBlock('latest')).number / EPOCH) + additionEpoch = Math.floor((await web3.eth.getBlockNumber()) / EPOCH) resp = await validators.affiliate(otherGroup) - affiliationEpoch = Math.floor((await web3.eth.getBlock('latest')).number / EPOCH) + affiliationEpoch = Math.floor((await web3.eth.getBlockNumber()) / EPOCH) }) it('should remove the validator from the group membership list', async () => { @@ -983,7 +983,7 @@ contract('Validators', (accounts: string[]) => { let registrationEpoch: number beforeEach(async () => { await registerValidator(validator) - registrationEpoch = Math.floor((await web3.eth.getBlock('latest')).number / EPOCH) + registrationEpoch = Math.floor((await web3.eth.getBlockNumber()) / EPOCH) await registerValidatorGroup(group) await validators.affiliate(group) }) @@ -1013,9 +1013,9 @@ contract('Validators', (accounts: string[]) => { let resp: any beforeEach(async () => { await validators.addFirstMember(validator, NULL_ADDRESS, NULL_ADDRESS, { from: group }) - additionEpoch = Math.floor((await web3.eth.getBlock('latest')).number / EPOCH) + additionEpoch = Math.floor((await web3.eth.getBlockNumber()) / EPOCH) resp = await validators.deaffiliate() - deaffiliationEpoch = Math.floor((await web3.eth.getBlock('latest')).number / EPOCH) + deaffiliationEpoch = Math.floor((await web3.eth.getBlockNumber()) / EPOCH) }) it('should remove the validator from the group membership list', async () => { @@ -1391,15 +1391,19 @@ contract('Validators', (accounts: string[]) => { await registerValidatorGroup(group) }) describe('when adding a validator affiliated with the group', () => { + let registrationEpoch: number beforeEach(async () => { await registerValidator(validator) + registrationEpoch = Math.floor((await web3.eth.getBlockNumber()) / EPOCH) await validators.affiliate(group, { from: validator }) }) describe('when the group meets the locked gold requirements', () => { describe('when the validator meets the locked gold requirements', () => { + let additionEpoch: number beforeEach(async () => { resp = await validators.addFirstMember(validator, NULL_ADDRESS, NULL_ADDRESS) + additionEpoch = Math.floor((await web3.eth.getBlockNumber()) / EPOCH) }) it('should add the member to the list of members', async () => { @@ -1421,14 +1425,14 @@ contract('Validators', (accounts: string[]) => { }) it("should update the member's membership history", async () => { - const membershipHistory = await validators.getMembershipHistory(validator) - const expectedEpoch = new BigNumber( - Math.floor((await web3.eth.getBlock('latest')).number / EPOCH) + const expectedEntries = registrationEpoch == additionEpoch ? 1 : 2 + const membershipHistory = parseMembershipHistory( + await validators.getMembershipHistory(validator) ) - assert.equal(membershipHistory[0].length, 1) - assertEqualBN(membershipHistory[0][0], expectedEpoch) - assert.equal(membershipHistory[1].length, 1) - assertSameAddress(membershipHistory[1][0], group) + assert.equal(membershipHistory.epochs.length, expectedEntries) + assertEqualBN(membershipHistory.epochs[expectedEntries - 1], additionEpoch) + assert.equal(membershipHistory.groups.length, expectedEntries) + assertSameAddress(membershipHistory.groups[expectedEntries - 1], group) }) it('should mark the group as eligible', async () => { @@ -1769,7 +1773,7 @@ contract('Validators', (accounts: string[]) => { let validatorRegistrationEpochNumber: number beforeEach(async () => { await registerValidator(validator) - const blockNumber = (await web3.eth.getBlock('latest')).number + const blockNumber = await web3.eth.getBlockNumber() validatorRegistrationEpochNumber = Math.floor(blockNumber / EPOCH) for (const group of groups) { await registerValidatorGroup(group) @@ -1783,7 +1787,7 @@ contract('Validators', (accounts: string[]) => { const expectedMembershipHistoryGroups = [NULL_ADDRESS] const expectedMembershipHistoryEpochs = [new BigNumber(validatorRegistrationEpochNumber)] for (let i = 0; i < numTests; i++) { - const blockNumber = (await web3.eth.getBlock('latest')).number + const blockNumber = await web3.eth.getBlockNumber() const epochNumber = Math.floor(blockNumber / EPOCH) const blocksUntilNextEpoch = (epochNumber + 1) * EPOCH - blockNumber await mineBlocks(blocksUntilNextEpoch, web3) @@ -1822,7 +1826,7 @@ contract('Validators', (accounts: string[]) => { const expectedMembershipHistoryGroups = [NULL_ADDRESS] const expectedMembershipHistoryEpochs = [new BigNumber(validatorRegistrationEpochNumber)] for (let i = 0; i < membershipHistoryLength.plus(1).toNumber(); i++) { - const blockNumber = (await web3.eth.getBlock('latest')).number + const blockNumber = await web3.eth.getBlockNumber() const epochNumber = Math.floor(blockNumber / EPOCH) const blocksUntilNextEpoch = (epochNumber + 1) * EPOCH - blockNumber await mineBlocks(blocksUntilNextEpoch, web3) @@ -1858,7 +1862,7 @@ contract('Validators', (accounts: string[]) => { describe('when changing groups more times than membership history length', () => { it('should always return the correct membership for the last epoch', async () => { for (let i = 0; i < membershipHistoryLength.plus(1).toNumber(); i++) { - const blockNumber = (await web3.eth.getBlock('latest')).number + const blockNumber = await web3.eth.getBlockNumber() const epochNumber = Math.floor(blockNumber / EPOCH) const blocksUntilNextEpoch = (epochNumber + 1) * EPOCH - blockNumber await mineBlocks(blocksUntilNextEpoch, web3) @@ -1959,6 +1963,10 @@ contract('Validators', (accounts: string[]) => { await registerValidatorGroupWithMembers(group, [validator]) mockStableToken = await MockStableToken.new() await registry.setAddressFor(CeloContractName.StableToken, mockStableToken.address) + // Fast-forward to the next epoch, so that the getMembershipInLastEpoch(validator) == group + const blockNumber = await web3.eth.getBlockNumber() + const epochNumber = Math.floor(blockNumber / EPOCH) + await mineBlocks((epochNumber + 1) * EPOCH - blockNumber, web3) }) describe('when the validator score is non-zero', () => { @@ -1972,6 +1980,7 @@ contract('Validators', (accounts: string[]) => { .times(fromFixed(commission)) .dp(0, BigNumber.ROUND_FLOOR) const expectedValidatorPayment = expectedTotalPayment.minus(expectedGroupPayment) + beforeEach(async () => { await validators.updateValidatorScoreFromSigner(validator, toFixed(uptime)) }) diff --git a/packages/protocol/test/identity/attestations.ts b/packages/protocol/test/identity/attestations.ts index ec8e04e26e4..bcfdd7fb4f0 100644 --- a/packages/protocol/test/identity/attestations.ts +++ b/packages/protocol/test/identity/attestations.ts @@ -2,11 +2,11 @@ import Web3 = require('web3') import { CeloContractName } from '@celo/protocol/lib/registry-utils' import { - advanceBlockNum, assertEqualBN, assertLogMatches2, assertRevert, assertSameAddress, + mineBlocks, NULL_ADDRESS, } from '@celo/protocol/lib/test-utils' import { attestToIdentifier } from '@celo/utils' @@ -348,7 +348,7 @@ contract('Attestations', (accounts: string[]) => { describe('when the original request has expired', () => { it('should allow to request more attestations', async () => { - await advanceBlockNum(attestationExpiryBlocks, web3) + await mineBlocks(attestationExpiryBlocks, web3) await attestations.request(phoneHash, 1, mockStableToken.address) }) }) @@ -480,7 +480,7 @@ contract('Attestations', (accounts: string[]) => { describe('after attestationExpiryBlocks', () => { beforeEach(async () => { await attestations.selectIssuers(phoneHash) - await advanceBlockNum(attestationExpiryBlocks, web3) + await mineBlocks(attestationExpiryBlocks, web3) }) it('should no longer list the attestations in getCompletableAttestations', async () => { @@ -551,7 +551,7 @@ contract('Attestations', (accounts: string[]) => { }) it('should set the time of the successful completion', async () => { - await advanceBlockNum(1, web3) + await mineBlocks(1, web3) await attestations.complete(phoneHash, v, r, s) const expectedBlock = await web3.eth.getBlock('latest') @@ -647,7 +647,7 @@ contract('Attestations', (accounts: string[]) => { }) it('does not let you verify beyond the window', async () => { - await advanceBlockNum(attestationExpiryBlocks, web3) + await mineBlocks(attestationExpiryBlocks, web3) await assertRevert(attestations.complete(phoneHash, v, r, s)) }) }) From 360b47348b728fc351294d3b62e664dc82458498 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Fri, 15 Nov 2019 13:25:10 -0800 Subject: [PATCH 148/149] Fix attestations test --- packages/protocol/test/identity/attestations.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/protocol/test/identity/attestations.ts b/packages/protocol/test/identity/attestations.ts index bcfdd7fb4f0..4add2b99b64 100644 --- a/packages/protocol/test/identity/attestations.ts +++ b/packages/protocol/test/identity/attestations.ts @@ -26,6 +26,7 @@ import { MockRandomInstance, MockStableTokenContract, MockStableTokenInstance, + MockValidatorsContract, RegistryContract, RegistryInstance, TestAttestationsContract, @@ -43,6 +44,7 @@ const Attestations: TestAttestationsContract = artifacts.require('TestAttestatio const MockStableToken: MockStableTokenContract = artifacts.require('MockStableToken') const MockElection: MockElectionContract = artifacts.require('MockElection') const MockLockedGold: MockLockedGoldContract = artifacts.require('MockLockedGold') +const MockValidators: MockValidatorsContract = artifacts.require('MockValidators') const Random: MockRandomContract = artifacts.require('MockRandom') const Registry: RegistryContract = artifacts.require('Registry') @@ -131,10 +133,14 @@ contract('Attestations', (accounts: string[]) => { accountsInstance = await Accounts.new() mockStableToken = await MockStableToken.new() otherMockStableToken = await MockStableToken.new() + const mockValidators = await MockValidators.new() attestations = await Attestations.new() random = await Random.new() random.addTestRandomness(0, '0x00') mockLockedGold = await MockLockedGold.new() + registry = await Registry.new() + await accountsInstance.initialize(registry.address) + await registry.setAddressFor(CeloContractName.Validators, mockValidators.address) await Promise.all( accounts.map(async (account) => { @@ -153,7 +159,6 @@ contract('Attestations', (accounts: string[]) => { privateKeyToAddress(getDerivedKey(KeyOffsets.VALIDATING_KEY_OFFSET, account)) ) ) - registry = await Registry.new() await registry.setAddressFor(CeloContractName.Accounts, accountsInstance.address) await registry.setAddressFor(CeloContractName.Random, random.address) await registry.setAddressFor(CeloContractName.Election, mockElection.address) From 4e97b48d6a16b5c2ecb644c6369c9a3441c575b7 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Fri, 15 Nov 2019 13:58:24 -0800 Subject: [PATCH 149/149] Fix lint --- packages/utils/src/io.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/utils/src/io.ts b/packages/utils/src/io.ts index eba2d3a8a55..14708ff1ee8 100644 --- a/packages/utils/src/io.ts +++ b/packages/utils/src/io.ts @@ -1,8 +1,8 @@ import { isValidPublic, toChecksumAddress } from 'ethereumjs-util' import { either } from 'fp-ts/lib/Either' import * as t from 'io-ts' -import { isE164NumberStrict } from './phoneNumbers' import { isValidAddress } from './address' +import { isE164NumberStrict } from './phoneNumbers' // from http://urlregex.com/ export const URL_REGEX = new RegExp(