From 02f39db4f6b3abef4fd095bd39b43751c3d27cf0 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Wed, 18 Sep 2019 17:44:17 -0700 Subject: [PATCH 01/92] 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 02/92] 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 03/92] 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 04/92] 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 05/92] 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 06/92] 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 07/92] 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 08/92] 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 09/92] 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 10/92] 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 11/92] 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 12/92] 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 13/92] 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 14/92] 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 15/92] 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 16/92] 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 17/92] 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 18/92] 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 19/92] 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 20/92] 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 21/92] 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 22/92] 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 23/92] 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 24/92] 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 25/92] 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 26/92] 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 27/92] 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 28/92] 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 29/92] 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 30/92] 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 31/92] 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 32/92] 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 33/92] 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 34/92] 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 35/92] 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 36/92] 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 37/92] 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 38/92] 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 39/92] 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 40/92] 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 41/92] 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 42/92] 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 43/92] 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 44/92] 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 45/92] 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 46/92] 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 47/92] 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 48/92] 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 49/92] 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 50/92] 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 51/92] 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 52/92] 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 53/92] 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 54/92] 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 55/92] 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 56/92] 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 57/92] 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 58/92] 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 59/92] 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 60/92] 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 61/92] 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 62/92] 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 63/92] 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 64/92] 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 65/92] 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 66/92] 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 67/92] 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 68/92] 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 69/92] 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 70/92] 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 71/92] 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 72/92] 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 73/92] 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 74/92] 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 75/92] 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 76/92] 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 77/92] 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 78/92] 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 79/92] 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 80/92] 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 81/92] 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 82/92] 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 83/92] 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 44d6f51a2abe02bc21590ce65fddd3944cfe3821 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Thu, 24 Oct 2019 11:07:25 -0700 Subject: [PATCH 84/92] 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 85/92] 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 59dedeab9833705e7428306a957629d09ef744e2 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Fri, 25 Oct 2019 11:24:52 -0700 Subject: [PATCH 86/92] 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 9281b4c7429ef9d2d73615af4a2f5e7d77357aa4 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Thu, 31 Oct 2019 14:11:05 -0700 Subject: [PATCH 87/92] 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 88/92] 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 2bc75cc35d6d913921a9fe758c19f64527e9953c Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Fri, 1 Nov 2019 13:14:37 -0700 Subject: [PATCH 89/92] 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 a871d6755f94dc1fbc17e20a6d9cd35959792454 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Fri, 1 Nov 2019 16:00:25 -0700 Subject: [PATCH 90/92] 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 990fc148a69365b2aeb366cfe1c4353de917fbe3 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Fri, 1 Nov 2019 19:33:17 -0700 Subject: [PATCH 91/92] 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 92/92] 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,