From 1b3c2d9ee08c91cabc518ddfab91145b6eb0e3be Mon Sep 17 00:00:00 2001 From: Anna K Date: Thu, 10 Oct 2019 13:54:54 -0700 Subject: [PATCH 01/37] Add ability to generate addresses directly from env file in celotool (#1294) --- .../src/cmds/generate/address-from-env.ts | 46 +++++++++++++++++++ packages/celotool/src/lib/generate_utils.ts | 6 +++ 2 files changed, 52 insertions(+) create mode 100644 packages/celotool/src/cmds/generate/address-from-env.ts diff --git a/packages/celotool/src/cmds/generate/address-from-env.ts b/packages/celotool/src/cmds/generate/address-from-env.ts new file mode 100644 index 00000000000..3c0337fae19 --- /dev/null +++ b/packages/celotool/src/cmds/generate/address-from-env.ts @@ -0,0 +1,46 @@ +/* tslint:disable no-console */ +import { addCeloEnvMiddleware, CeloEnvArgv } from 'src/lib/env-utils' +import { + coerceMnemonicAccountType, + getAddressFromEnv, + MNEMONIC_ACCOUNT_TYPE_CHOICES, +} from 'src/lib/generate_utils' +import * as yargs from 'yargs' + +export const command = 'address-from-env' + +export const describe = + 'command for fetching addresses (validator, load_testing, tx_node, bootnode and faucet) as specified by the current environment' + +interface AccountAddressArgv { + index: number + accountType: string +} + +export const builder = (argv: yargs.Argv) => { + return addCeloEnvMiddleware( + argv + .option('index', { + alias: 'i', + type: 'number', + description: 'account index', + demand: 'Please specifiy account index', + }) + .option('accountType', { + alias: 'a', + type: 'string', + choices: MNEMONIC_ACCOUNT_TYPE_CHOICES, + description: 'account type', + demand: 'Please specifiy account type', + required: true, + }) + ) +} + +export const handler = async (argv: CeloEnvArgv & AccountAddressArgv) => { + const validatorAddress = getAddressFromEnv( + coerceMnemonicAccountType(argv.accountType), + argv.index + ) + console.info(validatorAddress) +} diff --git a/packages/celotool/src/lib/generate_utils.ts b/packages/celotool/src/lib/generate_utils.ts index f7ddc588ee4..fceaab5170b 100644 --- a/packages/celotool/src/lib/generate_utils.ts +++ b/packages/celotool/src/lib/generate_utils.ts @@ -103,6 +103,12 @@ export const getValidators = (mnemonic: string, n: number) => { }) } +export const getAddressFromEnv = (accountType: AccountType, n: number) => { + const mnemonic = fetchEnv(envVar.MNEMONIC) + const privateKey = generatePrivateKey(mnemonic, accountType, n) + return privateKeyToAddress(privateKey) +} + export const generateGenesisFromEnv = (enablePetersburg: boolean = true) => { const mnemonic = fetchEnv(envVar.MNEMONIC) const validatorEnv = fetchEnv(envVar.VALIDATORS) From 0daabd8fc6f0016dc81439d4fcc51b8c6ece7063 Mon Sep 17 00:00:00 2001 From: Trevor Porter Date: Thu, 10 Oct 2019 14:49:02 -0700 Subject: [PATCH 02/37] Set IN_MEMORY_DISCOVERY_TABLE=true for integration (#1296) --- .env.integration | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env.integration b/.env.integration index 5baf89429ca..866f1309219 100644 --- a/.env.integration +++ b/.env.integration @@ -53,7 +53,7 @@ TX_NODES=2 STATIC_IPS_FOR_GETH_NODES=false # Whether tx_nodes/validators stateful set should use ssd persistent disks GETH_NODES_SSD_DISKS=true -IN_MEMORY_DISCOVERY_TABLE=false +IN_MEMORY_DISCOVERY_TABLE=true # Testnet vars GETH_NODES_BACKUP_CRONJOB_ENABLED=true From 3e4b1866c0917b417a2696ea8a95b9f88db4ce3d Mon Sep 17 00:00:00 2001 From: Jean Regisser Date: Fri, 11 Oct 2019 10:45:56 +0200 Subject: [PATCH 03/37] [Wallet] Fix ListView deprecation warning (#1293) --- packages/mobile/package.json | 3 +-- packages/mobile/src/index.d.ts | 1 - .../__snapshots__/JoinCelo.test.tsx.snap | 6 ++++-- .../verify/__snapshots__/Verify.test.tsx.snap | 3 ++- .../components/PhoneNumberInput.test.tsx | 2 +- .../components/PhoneNumberInput.tsx | 7 ++++++- packages/react-components/index.d.ts | 1 - packages/react-components/package.json | 3 ++- packages/verifier/package.json | 2 -- .../PhoneNumberInput.test.tsx.snap | 3 ++- yarn.lock | 18 ++++++++---------- 11 files changed, 26 insertions(+), 23 deletions(-) diff --git a/packages/mobile/package.json b/packages/mobile/package.json index 7bc8622cf43..4ffbc087179 100644 --- a/packages/mobile/package.json +++ b/packages/mobile/package.json @@ -47,6 +47,7 @@ }, "dependencies": { "@celo/client": "640a41f", + "@celo/react-components": "1.0.0", "@celo/react-native-sms-retriever": "git+https://github.com/celo-org/react-native-sms-retriever#d3a2fdb", "@celo/utils": "^0.1.1", "@celo/walletkit": "^0.0.14", @@ -80,7 +81,6 @@ "react-i18next": "^8.3.8", "react-native": "0.59.10", "react-native-android-open-settings": "^1.2.0", - "react-native-autocomplete-input": "^3.6.0", "react-native-bip39": "git://github.com/celo-org/react-native-bip39#1488fa1", "react-native-camera": "2.9.0", "react-native-clock-sync": "^1.0.0", @@ -137,7 +137,6 @@ "devDependencies": { "@babel/core": "^7.4.3", "@babel/runtime": "^7.4.3", - "@celo/react-components": "1.0.0", "@celo/typescript": "0.0.1", "@types/enzyme": "3.1.10", "@types/enzyme-adapter-react-16": "1.0.4", diff --git a/packages/mobile/src/index.d.ts b/packages/mobile/src/index.d.ts index bfc7f5a544c..9b76c70cf92 100644 --- a/packages/mobile/src/index.d.ts +++ b/packages/mobile/src/index.d.ts @@ -22,7 +22,6 @@ declare module 'react-native-flag-secure-android' declare module 'svgs' declare module 'react-navigation-tabs' declare module 'react-native-languages' -declare module 'react-native-autocomplete-input' declare module 'react-native-swiper' declare module 'react-native-version-check' declare module 'react-native-randombytes' diff --git a/packages/mobile/src/invite/__snapshots__/JoinCelo.test.tsx.snap b/packages/mobile/src/invite/__snapshots__/JoinCelo.test.tsx.snap index 08d0b3e8cf7..3710d7a47e7 100644 --- a/packages/mobile/src/invite/__snapshots__/JoinCelo.test.tsx.snap +++ b/packages/mobile/src/invite/__snapshots__/JoinCelo.test.tsx.snap @@ -224,6 +224,7 @@ exports[`JoinCeloScreen renders correctly 1`] = ` autoCorrect={false} data={Array []} defaultValue="Canadá" + flatListProps={Object {}} inputContainerStyle={ Array [ Object { @@ -240,6 +241,7 @@ exports[`JoinCeloScreen renders correctly 1`] = ` }, ] } + keyExtractor={[Function]} keyboardShouldPersistTaps="always" listContainerStyle={ Object { @@ -279,7 +281,6 @@ exports[`JoinCeloScreen renders correctly 1`] = ` renderItem={[Function]} renderSeparator={null} renderTextInput={[Function]} - rowHasChanged={[Function]} style={ Array [ Object { @@ -749,6 +750,7 @@ exports[`JoinCeloScreen renders with an error 1`] = ` autoCorrect={false} data={Array []} defaultValue="Canadá" + flatListProps={Object {}} inputContainerStyle={ Array [ Object { @@ -765,6 +767,7 @@ exports[`JoinCeloScreen renders with an error 1`] = ` }, ] } + keyExtractor={[Function]} keyboardShouldPersistTaps="always" listContainerStyle={ Object { @@ -804,7 +807,6 @@ exports[`JoinCeloScreen renders with an error 1`] = ` renderItem={[Function]} renderSeparator={null} renderTextInput={[Function]} - rowHasChanged={[Function]} style={ Array [ Object { diff --git a/packages/mobile/src/verify/__snapshots__/Verify.test.tsx.snap b/packages/mobile/src/verify/__snapshots__/Verify.test.tsx.snap index b694249367f..9023c581f9b 100644 --- a/packages/mobile/src/verify/__snapshots__/Verify.test.tsx.snap +++ b/packages/mobile/src/verify/__snapshots__/Verify.test.tsx.snap @@ -2487,6 +2487,7 @@ exports[`renders the Input step correctly 1`] = ` autoCorrect={false} data={Array []} defaultValue="" + flatListProps={Object {}} inputContainerStyle={ Array [ Object { @@ -2503,6 +2504,7 @@ exports[`renders the Input step correctly 1`] = ` }, ] } + keyExtractor={[Function]} keyboardShouldPersistTaps="always" listContainerStyle={ Object { @@ -2542,7 +2544,6 @@ exports[`renders the Input step correctly 1`] = ` renderItem={[Function]} renderSeparator={null} renderTextInput={[Function]} - rowHasChanged={[Function]} style={ Array [ Object { diff --git a/packages/react-components/components/PhoneNumberInput.test.tsx b/packages/react-components/components/PhoneNumberInput.test.tsx index 86c5b6a3727..ee98fc9c73a 100644 --- a/packages/react-components/components/PhoneNumberInput.test.tsx +++ b/packages/react-components/components/PhoneNumberInput.test.tsx @@ -49,7 +49,7 @@ describe('PhoneNumberInput', () => { const instance = numberInput.instance() // @ts-ignore - const renderedItem = shallow(instance.renderItem('GB')) + const renderedItem = shallow(instance.renderItem({ item: 'GB' })) expect( renderedItem .find(Text) diff --git a/packages/react-components/components/PhoneNumberInput.tsx b/packages/react-components/components/PhoneNumberInput.tsx index 417904e3d1a..49e75a3532c 100644 --- a/packages/react-components/components/PhoneNumberInput.tsx +++ b/packages/react-components/components/PhoneNumberInput.tsx @@ -124,7 +124,11 @@ export default class PhoneNumberInput extends React.Component { } } - renderItem = (countryCode: string) => { + keyExtractor = (item: string, index: number) => { + return item + } + + renderItem = ({ item: countryCode }: { item: string }) => { const { displayName, emoji, countryCallingCodes } = this.state.countries.getCountryByCode( countryCode ) @@ -176,6 +180,7 @@ export default class PhoneNumberInput extends React.Component { inputContainerStyle={[style.borderedBox, style.inputBox, style.inputCountry]} listStyle={[style.borderedBox, style.listAutocomplete]} data={filteredCountries} + keyExtractor={this.keyExtractor} defaultValue={countryQuery} onChangeText={this.onChangeCountryQuery} onEndEditing={this.props.onEndEditingCountryCode} diff --git a/packages/react-components/index.d.ts b/packages/react-components/index.d.ts index 05978b5fbf0..56021016b13 100644 --- a/packages/react-components/index.d.ts +++ b/packages/react-components/index.d.ts @@ -1,4 +1,3 @@ -declare module 'react-native-autocomplete-input' declare module 'svgs' declare module 'react-native-platform-touchable' declare module 'web3-utils' diff --git a/packages/react-components/package.json b/packages/react-components/package.json index 408f8e65e47..e6d12cabe29 100644 --- a/packages/react-components/package.json +++ b/packages/react-components/package.json @@ -17,7 +17,7 @@ "@celo/utils": "^0.1.0", "hoist-non-react-statics": "^3.3.0", "lodash": "^4.17.14", - "react-native-autocomplete-input": "^3.6.0", + "react-native-autocomplete-input": "^4.1.0", "react-native-contacts": "git://github.com/celo-org/react-native-contacts#4989b0b", "react-native-platform-touchable": "^1.1.1", "svgs": "^4.1.0" @@ -28,6 +28,7 @@ "@types/hoist-non-react-statics": "^3.3.1", "@types/react": "^16.8.19", "@types/react-native": "^0.57.47", + "@types/react-native-autocomplete-input": "^4.0.1", "enzyme": "^3.9.0", "enzyme-adapter-react-16": "^1.11.2", "react": "16.8.3", diff --git a/packages/verifier/package.json b/packages/verifier/package.json index 5119768b01f..dbc4636eddb 100644 --- a/packages/verifier/package.json +++ b/packages/verifier/package.json @@ -40,7 +40,6 @@ "react-apollo": "^2.4.1", "react-i18next": "^8.3.8", "react-native": "^0.59.5", - "react-native-autocomplete-input": "^3.6.0", "react-native-config": "https://github.com/luggit/react-native-config#89a602b", "react-native-device-info": "^2.1.0", "react-native-firebase": "5.5.4", @@ -65,7 +64,6 @@ "@types/graphql": "^14.0.7", "@types/react": "^16.8.19", "@types/react-native": "^0.57.47", - "@types/react-native-autocomplete-input": "^3.5.1", "@types/react-test-renderer": "^16.0.3", "@types/web3": "^1.0.18", "babel-core": "7.0.0-bridge.0", diff --git a/packages/verifier/src/components/__snapshots__/PhoneNumberInput.test.tsx.snap b/packages/verifier/src/components/__snapshots__/PhoneNumberInput.test.tsx.snap index 69aabe316fa..7286762fea3 100644 --- a/packages/verifier/src/components/__snapshots__/PhoneNumberInput.test.tsx.snap +++ b/packages/verifier/src/components/__snapshots__/PhoneNumberInput.test.tsx.snap @@ -220,6 +220,7 @@ exports[`PhoneNumberInput when no defaultCountry renders AutoComplete 1`] = ` autoCorrect={false} data={Array []} defaultValue="" + flatListProps={Object {}} inputContainerStyle={ Array [ Object { @@ -236,6 +237,7 @@ exports[`PhoneNumberInput when no defaultCountry renders AutoComplete 1`] = ` }, ] } + keyExtractor={[Function]} keyboardShouldPersistTaps="always" listContainerStyle={ Object { @@ -278,7 +280,6 @@ exports[`PhoneNumberInput when no defaultCountry renders AutoComplete 1`] = ` renderItem={[Function]} renderSeparator={null} renderTextInput={[Function]} - rowHasChanged={[Function]} style={ Array [ Object { diff --git a/yarn.lock b/yarn.lock index b4660afa1a6..5db3afaaf4c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5197,10 +5197,10 @@ "@types/react" "*" "@types/webpack" "*" -"@types/react-native-autocomplete-input@^3.5.1": - version "3.5.1" - resolved "https://registry.yarnpkg.com/@types/react-native-autocomplete-input/-/react-native-autocomplete-input-3.5.1.tgz#38286633407c6627ca29d3faa1a3233395c1b69e" - integrity sha512-asqm2c96AEUNJHMVnZ4bTYK+nQbsTntpOdLotXVGWaz6yTI5hPG2S6Hl7hBgc4RpM8RALtzbxSEkvFGwsl23Ig== +"@types/react-native-autocomplete-input@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/react-native-autocomplete-input/-/react-native-autocomplete-input-4.0.1.tgz#6a1556ab6658b5b52fe197da2b964f60363d2e99" + integrity sha512-um2iICwQezPIT9oTZYrK44tjuHcqDZGrY2dKuLEEYPmN+o8v/xoWV6XJTiJbAxXbIYjNO7zkrpfDmaDno09KVw== dependencies: "@types/react" "*" "@types/react-native" "*" @@ -26096,12 +26096,10 @@ react-native-animatable@^1.2.4: dependencies: prop-types "^15.5.10" -react-native-autocomplete-input@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/react-native-autocomplete-input/-/react-native-autocomplete-input-3.6.0.tgz#24d29d8c3e156a86771c1db0d58f2fa5611eee06" - integrity sha512-dHDpbazoECwA+pUc3S3qV6igf5+LLjJ/xox66cm0Wrf2nrNteH2wW38hfbi5zSNmtLkIePNTWQ6J49onZwzOiA== - dependencies: - prop-types "^15.5.10" +react-native-autocomplete-input@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/react-native-autocomplete-input/-/react-native-autocomplete-input-4.1.0.tgz#979ece28d891b245ecb967b6d31f1f924445b8ab" + integrity sha512-Yn4GulZ9F6tde74UUGZHdVFeYWVuL7+EbUZy6kt+QHrzMc5B4OuRop1FT4RyWLpvbySW/vvqYgj9LAmlzkuEqA== "react-native-bip39@git://github.com/celo-org/react-native-bip39#1488fa1": version "2.2.0" From 2cb725c36b69e7ae608875610af080f4f3fa79bd Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Fri, 11 Oct 2019 18:30:30 +0200 Subject: [PATCH 04/37] [Wallet] Add script to build sdk for env before running yarn dev (#1312) --- packages/mobile/package.json | 1 + packages/mobile/scripts/pre-dev.sh | 12 ++++++++++++ 2 files changed, 13 insertions(+) create mode 100755 packages/mobile/scripts/pre-dev.sh diff --git a/packages/mobile/package.json b/packages/mobile/package.json index 4ffbc087179..484436de104 100644 --- a/packages/mobile/package.json +++ b/packages/mobile/package.json @@ -15,6 +15,7 @@ "build:ts": "tsc --noEmit", "build:metro": "echo 'NOT WORKING RIGHT NOW'", "build:gen-graphql-types": "gql-gen --schema http://localhost:8080/graphql --template graphql-codegen-typescript-template --out ./typings/ 'src/**/*.tsx'", + "predev": "./scripts/pre-dev.sh", "dev": "react-native run-android --appIdSuffix \"debug\"", "dev:show-menu": "adb devices | grep '\t' | awk '{print $1}' | sed 's/\\s//g' | xargs -I {} adb -s {} shell input keyevent 82", "dev:clear-data": "adb shell pm clear org.celo.mobile.debug", diff --git a/packages/mobile/scripts/pre-dev.sh b/packages/mobile/scripts/pre-dev.sh new file mode 100755 index 00000000000..13cdea50861 --- /dev/null +++ b/packages/mobile/scripts/pre-dev.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ==================================== +# Tasks to run before running yarn dev +# ==================================== + +# Detect network from .env and build the sdk for it +export $(grep -v '^#' .env | xargs) +echo "Building sdk for testnet $DEFAULT_TESTNET" +yarn build:sdk $DEFAULT_TESTNET +echo "Done building sdk" \ No newline at end of file From 659a11dee2dce96a8d01e9e7070f2a1088c730d5 Mon Sep 17 00:00:00 2001 From: "Victor \"Nate\" Graf" Date: Fri, 11 Oct 2019 12:13:01 -0700 Subject: [PATCH 05/37] Add step to install typescript and other minor edits (#1256) * Add step to install typescript and other minor edits * Fix typos --- SETUP.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/SETUP.md b/SETUP.md index b26e638de2b..1c4c93a6bdd 100644 --- a/SETUP.md +++ b/SETUP.md @@ -55,11 +55,14 @@ Install `nvm` (allows you to manage multiple versions of Node), Node 8 and `yarn ```bash brew install nvm + # follow the instructions from the command above to edit your .bash_profile # then restart the terminal +nvm install 8 nvm install 10 nvm alias default 10 brew install yarn +npm install -g typescript ``` #### Java @@ -112,7 +115,7 @@ sdkmanager 'platforms;android-28' We use Yarn to build all of the [celo-monorepo] repo. -Install `nvm` (allows you to manage multiple versions of Node), Node 8 and `yarn`: +Install `nvm` (allows you to manage multiple versions of Node), Node 8, Node 10 and `yarn`: ```bash # Installing Node @@ -125,12 +128,16 @@ source ~/.bashrc # Setting up the right version of Nvm nvm install 8 -nvm alias default 8 +nvm install 10 +nvm alias default 10 # Installing Yarn - https://yarnpkg.com/en/docs/install#debian-stable curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list sudo apt-get update && sudo apt-get install yarn + +# Install typescript +npm install -g typescript ``` #### Installing OpenJDK 8 @@ -211,9 +218,9 @@ Run the emulator with: emulator -avd Nexus_5X_API_28 ``` -#### Optional: Genymotion +#### Optional: Install Genymotion Emulator Manager -Optionally, as alternative to other emulators you can install Genymotion, a nice emulator manager: +Optionally, as alternative to other emulators you can install Genymotion ##### MacOS From 50cd8cf02cf032cc66851bde196a3de503f96946 Mon Sep 17 00:00:00 2001 From: Nam Chu Hoai Date: Fri, 11 Oct 2019 17:45:01 -0700 Subject: [PATCH 06/37] Allow validators to specify an attestationKey with which they sign attestations (#1313) --- .../contracts/identity/Attestations.sol | 126 +++++++++++++++- .../identity/interfaces/IAttestations.sol | 4 + packages/protocol/lib/signing-utils.ts | 10 ++ packages/protocol/migrationsConfig.js | 2 +- .../test/governance/bondeddeposits.ts | 32 ++--- .../protocol/test/identity/attestations.ts | 135 +++++++++++++++++- packages/utils/src/address.ts | 8 ++ packages/utils/src/attestations.ts | 8 +- packages/utils/src/signatureUtils.test.ts | 14 ++ 9 files changed, 304 insertions(+), 35 deletions(-) create mode 100644 packages/utils/src/signatureUtils.test.ts diff --git a/packages/protocol/contracts/identity/Attestations.sol b/packages/protocol/contracts/identity/Attestations.sol index dc5b2b439d1..c240c3da9ce 100644 --- a/packages/protocol/contracts/identity/Attestations.sol +++ b/packages/protocol/contracts/identity/Attestations.sol @@ -10,13 +10,22 @@ import "../common/interfaces/IERC20Token.sol"; import "../governance/interfaces/IValidators.sol"; import "../common/Initializable.sol"; +import "../governance/UsingLockedGold.sol"; import "../common/UsingRegistry.sol"; +import "../common/Signatures.sol"; /** * @title Contract mapping identifiers to accounts */ -contract Attestations is IAttestations, Ownable, Initializable, UsingRegistry, ReentrancyGuard { +contract Attestations is + IAttestations, + Ownable, + Initializable, + UsingRegistry, + ReentrancyGuard, + UsingLockedGold +{ using SafeMath for uint256; @@ -51,6 +60,11 @@ contract Attestations is IAttestations, Ownable, Initializable, UsingRegistry, R uint256 value ); + event AttestorAuthorized( + address indexed account, + address attestor + ); + event AccountDataEncryptionKeySet( address indexed account, bytes dataEncryptionKey @@ -90,6 +104,9 @@ contract Attestations is IAttestations, Ownable, Initializable, UsingRegistry, R // The token with which attestation request fees are paid address attestationRequestFeeToken; + // The address of the key with which this account wants to sign attestations + address authorizedAttestor; + // The ECDSA public key used to encrypt and decrypt data for this account bytes dataEncryptionKey; @@ -116,6 +133,9 @@ contract Attestations is IAttestations, Ownable, Initializable, UsingRegistry, R mapping(bytes32 => IdentifierState) identifiers; mapping(address => Account) accounts; + // Maps attestation keys to the account that provided the authorization. + mapping(address => address) authorizedBy; + // Address of the RequestAttestation precompiled contract. // solhint-disable-next-line state-visibility address constant REQUEST_ATTESTATION = address(0xff); @@ -526,6 +546,100 @@ contract Attestations is IAttestations, Ownable, Initializable, UsingRegistry, R return accounts[account].dataEncryptionKey; } + /** + * @notice Authorizes attestation 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 `current`'s signature on `msg.sender`. + */ + function authorize( + address current, + address previous, + uint8 v, + bytes32 r, + bytes32 s + ) + private + { + require(isNotAuthorized(current)); + + address signer = Signatures.getSignerOfAddress(msg.sender, v, r, s); + require(signer == current); + + authorizedBy[previous] = address(0); + authorizedBy[current] = msg.sender; + } + + /** + * @notice Check if an address has been authorized by an account for attestation. + * @param account The possibly authorized address. + * @return Returns `true` if authorized. Returns `false` otherwise. + */ + function isAuthorized(address account) external view returns (bool) { + return (authorizedBy[account] != address(0)); + } + + /** + * @notice Check if an address has been authorized by an account for attestation. + * @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)); + } + + /** + * @notice Authorizes an address to attest on behalf + * @param attestor The address of the attestor to set for the account + * @param v The recovery id of the incoming ECDSA signature. + * @param r Output value r of the ECDSA signature. + * @param s Output value s of the ECDSA signature. + * @dev v, r, s constitute `attestor`'s signature on `msg.sender`. + */ + function authorizeAttestor( + address attestor, + uint8 v, + bytes32 r, + bytes32 s + ) + public + { + Account storage account = accounts[msg.sender]; + authorize(attestor, account.authorizedAttestor, v, r, s); + account.authorizedAttestor = attestor; + emit AttestorAuthorized(msg.sender, attestor); + } + + /** + * @notice Returns the attestor for the specified account. + * @param account The address of the account. + * @return The address with which the account can attest. + */ + function getAttestorFromAccount(address account) public view returns (address) { + address attestor = accounts[account].authorizedAttestor; + return attestor == address(0) ? account : attestor; + } + + /** + * @notice Returns the account associated with `accountOrAttestor`. + * @param accountOrAttestor The address of the account or authorized attestor. + * @dev Fails if the `accountOrAttestor` is not an account or authorized attestor. + * @return The associated account. + */ + function getAccountFromAttestor(address accountOrAttestor) public view returns (address) { + address authorizingAccount = authorizedBy[accountOrAttestor]; + if (authorizingAccount != address(0)) { + require(accounts[authorizingAccount].authorizedAttestor == accountOrAttestor); + return authorizingAccount; + } else { + return accountOrAttestor; + } + } + /** * @notice Setter for the wallet address for an account * @param walletAddress The wallet address to set for the account @@ -566,7 +680,8 @@ contract Attestations is IAttestations, Ownable, Initializable, UsingRegistry, R returns (address) { bytes32 codehash = keccak256(abi.encodePacked(identifier, account)); - address issuer = ecrecover(codehash, v, r, s); + address signer = ecrecover(codehash, v, r, s); + address issuer = getAccountFromAttestor(signer); Attestation storage attestation = identifiers[identifier].attestations[account].issuedAttestations[issuer]; @@ -648,13 +763,14 @@ contract Attestations is IAttestations, Ownable, Initializable, UsingRegistry, R uint256 currentIndex = 0; address validator; + address issuer; while (currentIndex < n) { seed = keccak256(abi.encodePacked(seed)); validator = validators[uint256(seed) % validators.length]; - + issuer = getAccountFromValidator(validator); Attestation storage attestations = - state.issuedAttestations[validator]; + state.issuedAttestations[issuer]; // Attestation issuers can only be added if they haven't already if (attestations.status != AttestationStatus.None) { @@ -665,7 +781,7 @@ contract Attestations is IAttestations, Ownable, Initializable, UsingRegistry, R attestations.status = AttestationStatus.Incomplete; // solhint-disable-next-line not-rely-on-time attestations.time = uint128(now); - state.issuers.push(validator); + state.issuers.push(issuer); } } diff --git a/packages/protocol/contracts/identity/interfaces/IAttestations.sol b/packages/protocol/contracts/identity/interfaces/IAttestations.sol index bba667d3d8d..c0d104e57c0 100644 --- a/packages/protocol/contracts/identity/interfaces/IAttestations.sol +++ b/packages/protocol/contracts/identity/interfaces/IAttestations.sol @@ -15,10 +15,14 @@ interface IAttestations { function setAttestationExpirySeconds(uint256) external; function setAccountDataEncryptionKey(bytes calldata) external; + function authorizeAttestor(address, uint8, bytes32, bytes32) external; function setMetadataURL(string calldata) external; function setWalletAddress(address) external; function setAccount(bytes calldata, address) external; + function getAttestorFromAccount(address) external view returns (address); + function getAccountFromAttestor(address) external view returns (address); + function getDataEncryptionKey(address) external view returns (bytes memory); function getWalletAddress(address) external view returns (address); function getMetadataURL(address) external view returns (string memory); diff --git a/packages/protocol/lib/signing-utils.ts b/packages/protocol/lib/signing-utils.ts index 44f390fb495..d9a63162b76 100644 --- a/packages/protocol/lib/signing-utils.ts +++ b/packages/protocol/lib/signing-utils.ts @@ -26,6 +26,16 @@ function makeEven(hex: string) { return hex } +export const getParsedSignatureOfAddress = async (web3: Web3, address: string, signer: string) => { + const addressHash = web3.utils.soliditySha3({ type: 'address', value: address }) + const signature = (await web3.eth.sign(addressHash, 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, + } +} + export async function signTransaction(web3: Web3, txn: any, privateKey: string) { let result: any diff --git a/packages/protocol/migrationsConfig.js b/packages/protocol/migrationsConfig.js index 2e696aa4eec..0d418ddc240 100644 --- a/packages/protocol/migrationsConfig.js +++ b/packages/protocol/migrationsConfig.js @@ -105,7 +105,7 @@ const linkedLibraries = { AddressSortedLinkedList: ['Validators'], IntegerSortedLinkedList: ['Governance', 'IntegerSortedLinkedListTest'], AddressSortedLinkedListWithMedian: ['SortedOracles', 'AddressSortedLinkedListWithMedianTest'], - Signatures: ['LockedGold', 'Escrow'], + Signatures: ['Attestations', 'LockedGold', 'Escrow'], } const argv = minimist(process.argv.slice(2), { diff --git a/packages/protocol/test/governance/bondeddeposits.ts b/packages/protocol/test/governance/bondeddeposits.ts index f11036f4bde..f2fc5d2bb20 100644 --- a/packages/protocol/test/governance/bondeddeposits.ts +++ b/packages/protocol/test/governance/bondeddeposits.ts @@ -1,4 +1,5 @@ import { CeloContractName } from '@celo/protocol/lib/registry-utils' +import { getParsedSignatureOfAddress } from '@celo/protocol/lib/signing-utils' import { assertEqualBN, assertLogMatches, @@ -45,17 +46,6 @@ contract('LockedGold', (accounts: string[]) => { 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, @@ -94,7 +84,7 @@ contract('LockedGold', (accounts: string[]) => { const delegate = accounts[1] beforeEach(async () => { - const sig = await getParsedSignatureOfAddress(account, delegate) + const sig = await getParsedSignatureOfAddress(web3, account, delegate) await lockedGold.delegateRole(roles.voting, delegate, sig.v, sig.r, sig.s) }) @@ -168,7 +158,7 @@ contract('LockedGold', (accounts: string[]) => { let sig beforeEach(async () => { - sig = await getParsedSignatureOfAddress(account, delegate) + sig = await getParsedSignatureOfAddress(web3, account, delegate) }) forEachRole((role) => { @@ -198,7 +188,7 @@ contract('LockedGold', (accounts: string[]) => { it('should revert if the address is already being delegated to', async () => { const otherAccount = accounts[2] - const otherSig = await getParsedSignatureOfAddress(otherAccount, delegate) + 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, @@ -208,7 +198,7 @@ contract('LockedGold', (accounts: string[]) => { it('should revert if the signature is incorrect', async () => { const nonDelegate = accounts[3] - const incorrectSig = await getParsedSignatureOfAddress(account, nonDelegate) + const incorrectSig = await getParsedSignatureOfAddress(web3, account, nonDelegate) await assertRevert( lockedGold.delegateRole(role, delegate, incorrectSig.v, incorrectSig.r, incorrectSig.s) ) @@ -219,7 +209,7 @@ contract('LockedGold', (accounts: string[]) => { let newSig beforeEach(async () => { await lockedGold.delegateRole(role, delegate, sig.v, sig.r, sig.s) - newSig = await getParsedSignatureOfAddress(account, newDelegate) + newSig = await getParsedSignatureOfAddress(web3, account, newDelegate) }) it('should set the new delegate', async () => { @@ -666,7 +656,7 @@ contract('LockedGold', (accounts: string[]) => { 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) + const sig = await getParsedSignatureOfAddress(web3, account, delegate) await lockedGold.delegateRole(role, delegate, sig.v, sig.r, sig.s) await assertRevert(lockedGold.getAccountFromDelegateAndRole(delegate, diffRole)) }) @@ -676,7 +666,7 @@ contract('LockedGold', (accounts: string[]) => { const delegate = accounts[1] beforeEach(async () => { - const sig = await getParsedSignatureOfAddress(account, delegate) + const sig = await getParsedSignatureOfAddress(web3, account, delegate) await lockedGold.delegateRole(role, delegate, sig.v, sig.r, sig.s) }) @@ -691,7 +681,7 @@ contract('LockedGold', (accounts: string[]) => { 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) + const sig = await getParsedSignatureOfAddress(web3, account, delegate) await lockedGold.delegateRole(role, delegate, sig.v, sig.r, sig.s) await assertRevert(lockedGold.getAccountFromDelegateAndRole(delegate, diffRole)) }) @@ -711,7 +701,7 @@ contract('LockedGold', (accounts: string[]) => { const delegate = accounts[1] beforeEach(async () => { - const sig = await getParsedSignatureOfAddress(account, delegate) + const sig = await getParsedSignatureOfAddress(web3, account, delegate) await lockedGold.delegateRole(role, delegate, sig.v, sig.r, sig.s) }) @@ -754,7 +744,7 @@ contract('LockedGold', (accounts: string[]) => { const delegate = accounts[1] beforeEach(async () => { - const sig = await getParsedSignatureOfAddress(account, delegate) + const sig = await getParsedSignatureOfAddress(web3, account, delegate) await lockedGold.delegateRole(roles.voting, delegate, sig.v, sig.r, sig.s) }) diff --git a/packages/protocol/test/identity/attestations.ts b/packages/protocol/test/identity/attestations.ts index c20a6d91141..d3d582d053e 100644 --- a/packages/protocol/test/identity/attestations.ts +++ b/packages/protocol/test/identity/attestations.ts @@ -8,12 +8,15 @@ import { timeTravel, } from '@celo/protocol/lib/test-utils' import { attestToIdentifier } from '@celo/utils' +import { privateKeyToAddress } from '@celo/utils/lib/address' import { getPhoneHash } from '@celo/utils/lib/phoneNumbers' import BigNumber from 'bignumber.js' import { uniq } from 'lodash' import { AttestationsContract, AttestationsInstance, + MockLockedGoldContract, + MockLockedGoldInstance, MockStableTokenContract, MockStableTokenInstance, MockValidatorsContract, @@ -23,10 +26,12 @@ import { RegistryContract, RegistryInstance, } from 'types' +import { getParsedSignatureOfAddress } from '../../lib/signing-utils' const Attestations: AttestationsContract = artifacts.require('Attestations') const MockStableToken: MockStableTokenContract = artifacts.require('MockStableToken') const MockValidators: MockValidatorsContract = artifacts.require('MockValidators') +const MockLockedGold: MockLockedGoldContract = artifacts.require('MockLockedGold') const Random: RandomContract = artifacts.require('Random') const Registry: RegistryContract = artifacts.require('Registry') @@ -41,6 +46,7 @@ contract('Attestations', (accounts: string[]) => { let otherMockStableToken: MockStableTokenInstance let random: RandomInstance let mockValidators: MockValidatorsInstance + let mockLockedGold: MockLockedGoldInstance let registry: RegistryInstance const provider = new Web3.providers.HttpProvider('http://localhost:8545') const metadataURL = 'https://www.celo.org' @@ -89,17 +95,62 @@ contract('Attestations', (accounts: string[]) => { return accounts[nonIssuerIndex] } + const getValidatingKeyAddress = (address: string) => { + const pKey = accountPrivateKeys[accounts.indexOf(address)] + const aKey = Buffer.from(pKey.slice(2), 'hex') + aKey.write((aKey[0] + 2).toString(16)) + return privateKeyToAddress('0x' + aKey.toString('hex')) + } + + const getAttestationKey = (address: string) => { + const pKey = accountPrivateKeys[accounts.indexOf(address)] + const aKey = Buffer.from(pKey.slice(2), 'hex') + aKey.write((aKey[0] + 1).toString(16)) + return '0x' + aKey.toString('hex') + } + + const unlockAttestationKey = async (address: string) => { + const attestationKey = getAttestationKey(address) + const authorizedAttestor = privateKeyToAddress(attestationKey) + // @ts-ignore + await web3.eth.personal.importRawKey(attestationKey, 'passphrase') + await web3.eth.personal.unlockAccount(authorizedAttestor, 'passphrase', 1000000) + return authorizedAttestor + } + + const authorizeAttestor = async (address: string) => { + const attestationKey = getAttestationKey(address) + const authorizedAttestor = privateKeyToAddress(attestationKey) + const attestationSig = await getParsedSignatureOfAddress(web3, address, authorizedAttestor) + + return attestations.authorizeAttestor( + authorizedAttestor, + attestationSig.v, + attestationSig.r, + attestationSig.s, + { from: address } + ) + } + beforeEach(async () => { mockStableToken = await MockStableToken.new() 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))) + await Promise.all( + accounts.map((account) => mockValidators.addValidator(getValidatingKeyAddress(account))) + ) + mockLockedGold = await MockLockedGold.new() + await Promise.all( + accounts.map((account) => + mockLockedGold.delegateValidating(account, getValidatingKeyAddress(account)) + ) + ) registry = await Registry.new() await registry.setAddressFor(CeloContractName.Random, random.address) await registry.setAddressFor(CeloContractName.Validators, mockValidators.address) - + await registry.setAddressFor(CeloContractName.LockedGold, mockLockedGold.address) await attestations.initialize( registry.address, attestationExpirySeconds, @@ -188,6 +239,56 @@ contract('Attestations', (accounts: string[]) => { }) }) + describe('#authorizeAttestor', async () => { + let authorizedAttestor + beforeEach(async () => { + authorizedAttestor = await unlockAttestationKey(caller) + }) + + it('should authorizes an attestor with the right signature', async () => { + await authorizeAttestor(caller) + const result = await attestations.getAttestorFromAccount(caller) + assert.equal(result, authorizedAttestor) + }) + + it('should just return the account address if attestor was authorized', async () => { + const result = await attestations.getAccountFromAttestor(caller) + assert.equal(result, caller) + }) + + it('should retrieve the account address if attestor has been authorized', async () => { + await authorizeAttestor(caller) + const result = await attestations.getAccountFromAttestor(authorizedAttestor) + assert.equal(result, caller) + }) + + it('should emit the AttestorAuthorized event', async () => { + const response = await authorizeAttestor(caller) + assert.lengthOf(response.logs, 1) + const event = response.logs[0] + assertLogMatches2(event, { + event: 'AttestorAuthorized', + args: { account: caller, attestor: authorizedAttestor }, + }) + }) + + it('should revert with the incorrect signature', async () => { + const attestationSig = await getParsedSignatureOfAddress( + web3, + accounts[1], + authorizedAttestor + ) + await assertRevert( + attestations.authorizeAttestor( + authorizedAttestor, + attestationSig.v, + attestationSig.r, + attestationSig.s + ) + ) + }) + }) + describe('#setWalletAddress', async () => { it('should set the walletAddress', async () => { await attestations.setWalletAddress(caller) @@ -427,6 +528,7 @@ contract('Attestations', (accounts: string[]) => { let issuer: string let v: number let r: string, s: string + beforeEach(async () => { await attestations.request(phoneHash, attestationsRequested, mockStableToken.address) issuer = (await attestations.getAttestationIssuers(phoneHash, caller))[0] @@ -498,6 +600,35 @@ contract('Attestations', (accounts: string[]) => { }) }) + describe('when an attestor has been authorized', () => { + beforeEach(async () => { + const attestationKey = getAttestationKey(issuer) + await unlockAttestationKey(issuer) + await authorizeAttestor(issuer) + ;({ v, r, s } = attestToIdentifier(phoneNumber, caller, attestationKey)) + }) + + it('should correctly complete the attestation', async () => { + let attestedAccounts = await attestations.lookupAccountsForIdentifier(phoneHash) + assert.isEmpty(attestedAccounts) + + await attestations.complete(phoneHash, v, r, s) + attestedAccounts = await attestations.lookupAccountsForIdentifier(phoneHash) + assert.lengthOf(attestedAccounts, 1) + assert.equal(attestedAccounts[0], caller) + }) + + it('should mark the attestation by the issuer as complete', async () => { + await attestations.complete(phoneHash, v, r, s) + const [status, _blockNumber] = await attestations.getAttestationState( + phoneHash, + caller, + issuer + ) + assert.equal(status.toNumber(), 2) + }) + }) + it('should revert when an invalid attestation code is provided', async () => { ;[v, r, s] = await getVerificationCodeSignature(accounts[1], issuer) await assertRevert(attestations.complete(phoneHash, v, r, s)) diff --git a/packages/utils/src/address.ts b/packages/utils/src/address.ts index 1dd8e4f219f..cf739060de3 100644 --- a/packages/utils/src/address.ts +++ b/packages/utils/src/address.ts @@ -1,5 +1,13 @@ +import { privateToAddress, toChecksumAddress } from 'ethereumjs-util' + export type Address = string export function eqAddress(a: Address, b: Address) { return a.replace('0x', '').toLowerCase() === b.replace('0x', '').toLowerCase() } + +export const privateKeyToAddress = (privateKey: string) => { + return toChecksumAddress( + '0x' + privateToAddress(Buffer.from(privateKey.slice(2), 'hex')).toString('hex') + ) +} diff --git a/packages/utils/src/attestations.ts b/packages/utils/src/attestations.ts index 097ddcdd84a..87eed41a84d 100644 --- a/packages/utils/src/attestations.ts +++ b/packages/utils/src/attestations.ts @@ -1,11 +1,7 @@ -import { privateToAddress } from 'ethereumjs-util' import * as Web3Utils from 'web3-utils' +import { privateKeyToAddress } from './address' import { Signature, SignatureUtils } from './signatureUtils' -const privateKeyToAddress = (privateKey: string) => { - return '0x' + privateToAddress(Buffer.from(privateKey.slice(2), 'hex')).toString('hex') -} - enum IdentifierType { PHONE_NUMBER, } @@ -19,7 +15,7 @@ function hashIdentifier(identifier: string, type: IdentifierType) { } } -function attestationMessageToSign(identifier: string, account: string) { +export function attestationMessageToSign(identifier: string, account: string) { const messageHash: string = Web3Utils.soliditySha3( { type: 'bytes32', value: hashIdentifier(identifier, IdentifierType.PHONE_NUMBER) }, { type: 'address', value: account } diff --git a/packages/utils/src/signatureUtils.test.ts b/packages/utils/src/signatureUtils.test.ts new file mode 100644 index 00000000000..c85ac04dfb7 --- /dev/null +++ b/packages/utils/src/signatureUtils.test.ts @@ -0,0 +1,14 @@ +import * as Web3Utils from 'web3-utils' +import { privateKeyToAddress } from './address' +import { parseSignature, serializeSignature, signMessage } from './signatureUtils' + +describe('signatures', () => { + it('should sign appropriately', () => { + const pKey = '0x62633f7c9583780a7d3904a2f55d792707c345f21de1bacb2d389934d82796b2' + const address = privateKeyToAddress(pKey) + const message = Web3Utils.soliditySha3({ type: 'string', value: 'identifier' }) + const signature = signMessage(message, pKey, address) + const serializedSig = serializeSignature(signature) + parseSignature(message, '0x' + serializedSig, address) + }) +}) From 1a23e261b1aa70b9ebd9ceab4e505cf120649f28 Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Mon, 14 Oct 2019 10:54:23 +0200 Subject: [PATCH 07/37] Add GRADLE_OPTS note to SETUP.md (#1311) --- SETUP.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/SETUP.md b/SETUP.md index 1c4c93a6bdd..d4b9d9ca26f 100644 --- a/SETUP.md +++ b/SETUP.md @@ -101,6 +101,8 @@ Execute the following (and make sure the lines are in your `~/.bash_profile`): ```bash export ANDROID_HOME=/usr/local/share/android-sdk export ANDROID_NDK=/usr/local/share/android-ndk +# Optional to speedup java builds +export GRADLE_OPTS='-Dorg.gradle.daemon=true -Dorg.gradle.parallel=true -Dorg.gradle.jvmargs="-Xmx4096m -XX:+HeapDumpOnOutOfMemoryError"' ``` Then install the Android 28 platform: From 5fb6a6ac1cc145153949fc68df8beb452db4a6ca Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Mon, 14 Oct 2019 14:49:11 +0200 Subject: [PATCH 08/37] [Wallet] Add check for $ENVFILE to pre-dev script (#1324) --- packages/mobile/scripts/pre-dev.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/mobile/scripts/pre-dev.sh b/packages/mobile/scripts/pre-dev.sh index 13cdea50861..8dabe085faa 100755 --- a/packages/mobile/scripts/pre-dev.sh +++ b/packages/mobile/scripts/pre-dev.sh @@ -6,7 +6,8 @@ set -euo pipefail # ==================================== # Detect network from .env and build the sdk for it -export $(grep -v '^#' .env | xargs) +ENV_FILENAME="${ENVFILE:-.env}" +export $(grep -v '^#' $ENV_FILENAME | xargs) echo "Building sdk for testnet $DEFAULT_TESTNET" yarn build:sdk $DEFAULT_TESTNET echo "Done building sdk" \ No newline at end of file From e8f6f1d366d975209bc0f21cb91ae2ef2119f810 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Mon, 14 Oct 2019 09:54:35 -0700 Subject: [PATCH 09/37] Fix broken Discord link (#1317) --- packages/docs/developer-resources/introduction.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/docs/developer-resources/introduction.md b/packages/docs/developer-resources/introduction.md index 1546c85540d..236d19e9ec1 100644 --- a/packages/docs/developer-resources/introduction.md +++ b/packages/docs/developer-resources/introduction.md @@ -6,7 +6,7 @@ Welcome to the Celo SDK Docs Homepage! Here, you can find resources for DAppKit - **[DAppKit](dappkit/README.md)** is a lightweight set of functions that allow mobile DApps to work with the Celo Wallet to sign transactions and access the user's account. This allows for a better user experience: DApps can focus on a great native experience without having to worry about key management. It also provides a simpler development experience, as no state or connection management is necessary. -If you'd like to see the Celo SDK expanded to support your favorite technology or application, please join the conversation on the [Celo Forum](https://forum.celo.org/c/applications-and-ecosystem-development) or [Discord](https://discordapp.com/channels/600834479145353243/600839784700837926) and tell us what you'd like to build- we highly encourage contributions! +If you'd like to see the Celo SDK expanded to support your favorite technology or application, please join the conversation on the [Celo Forum](https://forum.celo.org/c/applications-and-ecosystem-development) or [Discord](https://discordapp.com/channels/600834479145353243/600834479145353245) and tell us what you'd like to build- we highly encourage contributions! {% hint style="info" %} Please note, financial products built on the Celo protocol may be regulated in their respective jurisdictions, so it is important that developers are responsible and attentive to their end consumers and the regulations that may apply. This commitment is also part of the [Celo Code of Conduct](https://celo.org/code-of-conduct). From 82a9d1c4745a6b74e05cb8bc3c865a181ad4123e Mon Sep 17 00:00:00 2001 From: Trevor Porter Date: Mon, 14 Oct 2019 13:15:37 -0700 Subject: [PATCH 10/37] Integration deploy, don't overwrite genesis block if upgrading testnet (#1315) --- .env.integration | 12 ++++++------ packages/celotool/src/cmds/deploy/upgrade/testnet.ts | 2 +- packages/mobile/package.json | 2 +- yarn.lock | 8 ++++---- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.env.integration b/.env.integration index 866f1309219..12da522873d 100644 --- a/.env.integration +++ b/.env.integration @@ -14,27 +14,27 @@ BLOCKSCOUT_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/blockscout" BLOCKSCOUT_WEB_DOCKER_IMAGE_TAG="web-f6c3e0888d1d0ef72dc8bf870808702b7fd13730" BLOCKSCOUT_INDEXER_DOCKER_IMAGE_TAG="indexer-f6c3e0888d1d0ef72dc8bf870808702b7fd13730" BLOCKSCOUT_WEB_REPLICAS=3 -BLOCKSCOUT_DB_SUFFIX="24" +BLOCKSCOUT_DB_SUFFIX="25" BLOCKSCOUT_SUBNETWORK_NAME="Integration" GETH_NODE_DOCKER_IMAGE_REPOSITORY="us.gcr.io/celo-testnet/geth" # When upgrading change this to latest commit hash from the master of the geth repo # `geth $ git show | head -n 1` -GETH_NODE_DOCKER_IMAGE_TAG="640a41fd970e0edbf5f30a90d53660165f5e98bd" +GETH_NODE_DOCKER_IMAGE_TAG="c1ae452c707f8bee91a9a0bf49193e78e9c8512e" GETH_BOOTNODE_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/geth-all" # When upgrading change this to latest commit hash from the master of the geth repo # `geth $ git show | head -n 1` -GETH_BOOTNODE_DOCKER_IMAGE_TAG="640a41fd970e0edbf5f30a90d53660165f5e98bd" +GETH_BOOTNODE_DOCKER_IMAGE_TAG="c1ae452c707f8bee91a9a0bf49193e78e9c8512e" CELOTOOL_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/celo-monorepo" -CELOTOOL_DOCKER_IMAGE_TAG="celotool-dfdc3e8b26e98aa294b27e2b5621c184488a10db" +CELOTOOL_DOCKER_IMAGE_TAG="celotool-2cb725c36b69e7ae608875610af080f4f3fa79bd" TRANSACTION_METRICS_EXPORTER_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/celo-monorepo" -TRANSACTION_METRICS_EXPORTER_DOCKER_IMAGE_TAG="transaction-metrics-exporter-d3d165a7db548d175cd703c86c20c1657c04368d" +TRANSACTION_METRICS_EXPORTER_DOCKER_IMAGE_TAG="transaction-metrics-exporter-2cb725c36b69e7ae608875610af080f4f3fa79bd" ATTESTATION_SERVICE_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/celo-monorepo" -ATTESTATION_SERVICE_DOCKER_IMAGE_TAG="attestation-service-5035b241cbcfbd4f261e3d77e1fca8f6dc8edc32" +ATTESTATION_SERVICE_DOCKER_IMAGE_TAG="attestation-service-2cb725c36b69e7ae608875610af080f4f3fa79bd" GETH_EXPORTER_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet-production/geth-exporter" GETH_EXPORTER_DOCKER_IMAGE_TAG="ed7d21bd50592709173368cd697ef73c1774a261" diff --git a/packages/celotool/src/cmds/deploy/upgrade/testnet.ts b/packages/celotool/src/cmds/deploy/upgrade/testnet.ts index 006694c4cab..7e37df950bf 100644 --- a/packages/celotool/src/cmds/deploy/upgrade/testnet.ts +++ b/packages/celotool/src/cmds/deploy/upgrade/testnet.ts @@ -30,10 +30,10 @@ export const handler = async (argv: TestnetArgv) => { if (argv.reset) { await resetAndUpgradeHelmChart(argv.celoEnv) + await uploadGenesisBlockToGoogleStorage(argv.celoEnv) } else { await upgradeHelmChart(argv.celoEnv) } - await uploadGenesisBlockToGoogleStorage(argv.celoEnv) await uploadStaticNodesToGoogleStorage(argv.celoEnv) await uploadEnvFileToGoogleStorage(argv.celoEnv) } diff --git a/packages/mobile/package.json b/packages/mobile/package.json index 484436de104..5e624f82630 100644 --- a/packages/mobile/package.json +++ b/packages/mobile/package.json @@ -47,7 +47,7 @@ ] }, "dependencies": { - "@celo/client": "640a41f", + "@celo/client": "c1ae452", "@celo/react-components": "1.0.0", "@celo/react-native-sms-retriever": "git+https://github.com/celo-org/react-native-sms-retriever#d3a2fdb", "@celo/utils": "^0.1.1", diff --git a/yarn.lock b/yarn.lock index 5db3afaaf4c..d35f38c8378 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2243,10 +2243,10 @@ lodash "^4.17.13" to-fast-properties "^2.0.0" -"@celo/client@640a41f": - version "0.0.142" - resolved "https://registry.yarnpkg.com/@celo/client/-/client-0.0.142.tgz#ea15cf543f512c6d7ad1bc6611d3836c5bf06f40" - integrity sha512-F5CEEsKNegFJzb9btk9OpUWTSL9gDNAqoi4fEZ+fEeYJR6763PPW8CHub+KrwslALcCeuSQiRSFzvtuCq85SgA== +"@celo/client@c1ae452": + version "0.0.155" + resolved "https://registry.yarnpkg.com/@celo/client/-/client-0.0.155.tgz#71f658ba39f66d28dedc2c424b983306dd335d7e" + integrity sha512-vSKYdJy649CHVBcpzHxeI2MNH9wZA4AsJJdRyDW9etzkeIBbHm1eL1pkUrxgeY/96nWb20YhjC+NL8cmujiqQQ== "@celo/contractkit@0.1.1": version "0.1.1" From e81fcbefe2d022c6064090807ab37e4f00baab68 Mon Sep 17 00:00:00 2001 From: Aitor Date: Tue, 15 Oct 2019 11:10:45 +0200 Subject: [PATCH 11/37] Feature/909 proxy delegatecall (#1289) --- .../protocol/contracts/common/MultiSig.sol | 5 ++++ packages/protocol/contracts/common/Proxy.sol | 16 ++++++++++++- .../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, 76 insertions(+), 1 deletion(-) create 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 c979e61e7e2..ee6944ffd1a 100644 --- a/packages/protocol/contracts/common/MultiSig.sol +++ b/packages/protocol/contracts/common/MultiSig.sol @@ -2,6 +2,7 @@ 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 @@ -261,6 +262,10 @@ 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 f5027a22f6e..c32d19944e3 100644 --- a/packages/protocol/contracts/common/Proxy.sol +++ b/packages/protocol/contracts/common/Proxy.sol @@ -1,6 +1,7 @@ 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. @@ -32,8 +33,19 @@ contract Proxy { function () external payable { bytes32 implementationPosition = IMPLEMENTATION_POSITION; + address implementationAddress; + + assembly { + implementationAddress := sload(implementationPosition) + } + + // Avoid checking if address is a contract or executing delegated call when + // implementation address is 0x0 + if (implementationAddress == address(0)) return; + + require(AddressesHelper.isContract(implementationAddress), "Invalid contract address"); + assembly { - let implementationAddress := sload(implementationPosition) let newCallDataPosition := mload(0x40) mstore(0x40, add(newCallDataPosition, calldatasize)) @@ -114,6 +126,8 @@ 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 new file mode 100644 index 00000000000..ee26b42d8a4 --- /dev/null +++ b/packages/protocol/contracts/common/libraries/AddressesHelper.sol @@ -0,0 +1,21 @@ +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 35602df088f..71e1a44e738 100644 --- a/packages/protocol/contracts/governance/Proposals.sol +++ b/packages/protocol/contracts/governance/Proposals.sol @@ -4,6 +4,7 @@ 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. @@ -324,6 +325,10 @@ 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 a70c3e724d2..9a2a810fba5 100644 --- a/packages/protocol/test/common/proxy.ts +++ b/packages/protocol/test/common/proxy.ts @@ -116,6 +116,13 @@ 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 38f5f02d48e..a72f1132eed 100644 --- a/packages/protocol/test/governance/governance.ts +++ b/packages/protocol/test/governance/governance.ts @@ -1625,6 +1625,29 @@ 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 35b9cd229e0b3761c7aa2c4f53cb75706fbb0a7f Mon Sep 17 00:00:00 2001 From: Mariano Cortesi Date: Tue, 15 Oct 2019 08:45:14 -0300 Subject: [PATCH 12/37] [cli][ck] Add Support for Validators.reorderMember() (#1232) Also: - Transform CeloTransactionObject into a Class - Add default tx params to CeloTransactionObject - tsconfig project reference cli -> ck --- .../celotool/src/e2e-tests/transfer_tests.ts | 3 + packages/celotool/tsconfig.json | 4 +- .../cli/src/commands/validatorgroup/member.ts | 29 +++- packages/cli/src/utils/cli.ts | 6 +- packages/cli/tsconfig.json | 4 +- .../contractkit/src/wrappers/Attestations.ts | 8 +- .../contractkit/src/wrappers/BaseWrapper.ts | 41 +++--- .../contractkit/src/wrappers/LockedGold.ts | 19 ++- .../src/wrappers/Validators.test.ts | 139 ++++++++++++++++++ .../contractkit/src/wrappers/Validators.ts | 81 +++++++++- .../command-line-interface/validatorgroup.md | 2 + 11 files changed, 297 insertions(+), 39 deletions(-) create mode 100644 packages/contractkit/src/wrappers/Validators.test.ts diff --git a/packages/celotool/src/e2e-tests/transfer_tests.ts b/packages/celotool/src/e2e-tests/transfer_tests.ts index 37e3eabe1dc..3d22ddef327 100644 --- a/packages/celotool/src/e2e-tests/transfer_tests.ts +++ b/packages/celotool/src/e2e-tests/transfer_tests.ts @@ -1,3 +1,6 @@ +// tslint:disable-next-line: no-reference (Required to make this work w/ ts-node) +/// + import { CeloContract, CeloToken, ContractKit, newKit, newKitFromWeb3 } from '@celo/contractkit' import { TransactionResult } from '@celo/contractkit/lib/utils/tx-result' import { toFixed } from '@celo/utils/lib/fixidity' diff --git a/packages/celotool/tsconfig.json b/packages/celotool/tsconfig.json index 83264d28d8a..f1e65a88714 100644 --- a/packages/celotool/tsconfig.json +++ b/packages/celotool/tsconfig.json @@ -13,7 +13,7 @@ "@google-cloud/monitoring": ["types/monitoring"] } }, - "include": ["src/"], + "include": ["src", "../contractkit/types"], "exclude": ["node_modules/"], - "references": [{ "path": "../utils" }] + "references": [{ "path": "../utils" }, { "path": "../contractkit" }] } diff --git a/packages/cli/src/commands/validatorgroup/member.ts b/packages/cli/src/commands/validatorgroup/member.ts index 6798baeaf76..be60216c69a 100644 --- a/packages/cli/src/commands/validatorgroup/member.ts +++ b/packages/cli/src/commands/validatorgroup/member.ts @@ -11,13 +11,17 @@ export default class ValidatorGroupRegister extends BaseCommand { ...BaseCommand.flags, from: Flags.address({ required: true, description: "ValidatorGroup's address" }), accept: flags.boolean({ - exclusive: ['remove'], + exclusive: ['remove', 'reorder'], description: 'Accept a validator whose affiliation is already set to the group', }), remove: flags.boolean({ - exclusive: ['accept'], + exclusive: ['accept', 'reorder'], description: 'Remove a validator from the members list', }), + reorder: flags.integer({ + exclusive: ['accept', 'remove'], + description: 'Reorder a validator within the members list', + }), } static args: IArg[] = [Args.address('validatorAddress', { description: "Validator's address" })] @@ -25,13 +29,14 @@ export default class ValidatorGroupRegister extends BaseCommand { static examples = [ 'member --accept 0x97f7333c51897469e8d98e7af8653aab468050a3 ', 'member --remove 0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95', + 'member --reorder 3 0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95', ] async run() { const res = this.parse(ValidatorGroupRegister) - if (!(res.flags.accept || res.flags.remove)) { - this.error(`Specify action: --accept or --remove`) + if (!(res.flags.accept || res.flags.remove || res.flags.reorder)) { + this.error(`Specify action: --accept, --remove or --reorder`) return } @@ -40,8 +45,20 @@ export default class ValidatorGroupRegister extends BaseCommand { if (res.flags.accept) { await displaySendTx('addMember', validators.addMember((res.args as any).validatorAddress)) - } else { - await displaySendTx('addMember', validators.removeMember((res.args as any).validatorAddress)) + } else if (res.flags.remove) { + await displaySendTx( + 'removeMember', + validators.removeMember((res.args as any).validatorAddress) + ) + } else if (res.flags.reorder != null) { + await displaySendTx( + 'reorderMember', + await validators.reorderMember( + res.flags.from, + (res.args as any).validatorAddress, + res.flags.reorder + ) + ) } } } diff --git a/packages/cli/src/utils/cli.ts b/packages/cli/src/utils/cli.ts index 4102c94ef7d..f97160513e6 100644 --- a/packages/cli/src/utils/cli.ts +++ b/packages/cli/src/utils/cli.ts @@ -5,7 +5,11 @@ import Table from 'cli-table' import { cli } from 'cli-ux' import { Tx } from 'web3/eth/types' -export async function displaySendTx(name: string, txObj: CeloTransactionObject, tx?: Tx) { +export async function displaySendTx( + name: string, + txObj: CeloTransactionObject, + tx?: Omit +) { cli.action.start(`Sending Transaction: ${name}`) const txResult = await txObj.send(tx) diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index 6b49fb45662..cfe287bd5a9 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -6,6 +6,6 @@ "esModuleInterop": true, "target": "es6" }, - "include": ["src"], - "references": [{ "path": "../utils" }] + "include": ["src", "../contractkit/types"], + "references": [{ "path": "../utils" }, { "path": "../contractkit" }] } diff --git a/packages/contractkit/src/wrappers/Attestations.ts b/packages/contractkit/src/wrappers/Attestations.ts index 03fa7040ce9..270b53bf5ce 100644 --- a/packages/contractkit/src/wrappers/Attestations.ts +++ b/packages/contractkit/src/wrappers/Attestations.ts @@ -10,8 +10,8 @@ import { proxySend, toBigNumber, toNumber, + toTransactionObject, tupleParser, - wrapSend, } from './BaseWrapper' const parseSignature = SignatureUtils.parseSignature @@ -206,7 +206,7 @@ export class AttestationsWrapper extends BaseWrapper { const phoneHash = PhoneNumberUtils.getPhoneHash(phoneNumber) const expectedSourceMessage = attestationMessageToSign(phoneHash, account) const { r, s, v } = parseSignature(expectedSourceMessage, code, issuer.toLowerCase()) - return wrapSend(this.kit, this.contract.methods.complete(phoneHash, v, r, s)) + return toTransactionObject(this.kit, this.contract.methods.complete(phoneHash, v, r, s)) } /** @@ -305,7 +305,7 @@ export class AttestationsWrapper extends BaseWrapper { async request(phoneNumber: string, attestationsRequested: number, token: CeloToken) { const phoneHash = PhoneNumberUtils.getPhoneHash(phoneNumber) const tokenAddress = await this.kit.registry.addressFor(token) - return wrapSend( + return toTransactionObject( this.kit, this.contract.methods.request(phoneHash, attestationsRequested, tokenAddress) ) @@ -332,7 +332,7 @@ export class AttestationsWrapper extends BaseWrapper { Buffer.from(phoneNumber, 'utf8') ).toString('hex') - return wrapSend( + return toTransactionObject( this.kit, this.contract.methods.reveal( PhoneNumberUtils.getPhoneHash(phoneNumber), diff --git a/packages/contractkit/src/wrappers/BaseWrapper.ts b/packages/contractkit/src/wrappers/BaseWrapper.ts index 8da2ff80fb9..5b1f624ac78 100644 --- a/packages/contractkit/src/wrappers/BaseWrapper.ts +++ b/packages/contractkit/src/wrappers/BaseWrapper.ts @@ -22,15 +22,6 @@ export abstract class BaseWrapper { } } -export interface CeloTransactionObject { - /** web3 native TransactionObject. Normally not used */ - txo: TransactionObject - /** send the transaction to the chain */ - send(params?: Omit): Promise - /** send the transaction and waits for the receipt */ - sendAndWaitForReceipt(params?: Omit): Promise -} - /** Parse string -> BigNumber */ export function toBigNumber(input: string) { return new BigNumber(input) @@ -205,18 +196,34 @@ export function proxySend wrapSend(kit, methodFn(...preParse(...args))) + return (...args: InputArgs) => toTransactionObject(kit, methodFn(...preParse(...args))) } else { const methodFn = sendArgs[0] - return (...args: InputArgs) => wrapSend(kit, methodFn(...args)) + return (...args: InputArgs) => toTransactionObject(kit, methodFn(...args)) } } -export function wrapSend(kit: ContractKit, txo: TransactionObject): CeloTransactionObject { - return { - send: (params?: Omit) => kit.sendTransactionObject(txo, params), - txo, - sendAndWaitForReceipt: (params?: Omit) => - kit.sendTransactionObject(txo, params).then((result) => result.waitReceipt()), +export function toTransactionObject( + kit: ContractKit, + txo: TransactionObject, + defaultParams?: Omit +): CeloTransactionObject { + return new CeloTransactionObject(kit, txo, defaultParams) +} + +export class CeloTransactionObject { + constructor( + private kit: ContractKit, + readonly txo: TransactionObject, + readonly defaultParams?: Omit + ) {} + + /** send the transaction to the chain */ + send = (params?: Omit): Promise => { + return this.kit.sendTransactionObject(this.txo, { ...this.defaultParams, ...params }) } + + /** send the transaction and waits for the receipt */ + sendAndWaitForReceipt = (params?: Omit): Promise => + this.send(params).then((result) => result.waitReceipt()) } diff --git a/packages/contractkit/src/wrappers/LockedGold.ts b/packages/contractkit/src/wrappers/LockedGold.ts index 87ee8c46df4..c87519a965c 100644 --- a/packages/contractkit/src/wrappers/LockedGold.ts +++ b/packages/contractkit/src/wrappers/LockedGold.ts @@ -10,7 +10,7 @@ import { proxyCall, proxySend, toBigNumber, - wrapSend, + toTransactionObject, } from '../wrappers/BaseWrapper' export interface VotingDetails { @@ -117,6 +117,21 @@ export class LockedGoldWrapper extends BaseWrapper { */ isVoting = proxyCall(this.contract.methods.isVoting) + /** + * Check if an account already exists. + * @param account The address of the account + * @return Returns `true` if account exists. Returns `false` otherwise. + * In particular it will return `false` if a delegate with given address exists. + */ + isAccount = proxyCall(this.contract.methods.isAccount) + + /** + * Check if a delegate already exists. + * @param account The address of the delegate + * @return Returns `true` if delegate exists. Returns `false` otherwise. + */ + isDelegate = proxyCall(this.contract.methods.isDelegate) + /** * Query maximum notice period. * @returns Current maximum notice period. @@ -226,7 +241,7 @@ export class LockedGoldWrapper extends BaseWrapper { role: Roles ): Promise> { const sig = await this.getParsedSignatureOfAddress(account, delegate) - return wrapSend( + return toTransactionObject( this.kit, this.contract.methods.delegateRole(role, delegate, sig.v, sig.r, sig.s) ) diff --git a/packages/contractkit/src/wrappers/Validators.test.ts b/packages/contractkit/src/wrappers/Validators.test.ts new file mode 100644 index 00000000000..3d5c831e6e3 --- /dev/null +++ b/packages/contractkit/src/wrappers/Validators.test.ts @@ -0,0 +1,139 @@ +import Web3 from 'web3' +import { newKitFromWeb3 } from '../kit' +import { testWithGanache } from '../test-utils/ganache-test' +import { LockedGoldWrapper } from './LockedGold' +import { ValidatorsWrapper } from './Validators' + +/* +TEST NOTES: +- In migrations: The only account that has cUSD is accounts[0] +*/ + +const minLockedGoldValue = Web3.utils.toWei('100', 'ether') // 1 gold +const minLockedGoldNoticePeriod = 120 * 24 * 60 * 60 // 120 days + +// A random 64 byte hex string. +const publicKey = + 'ea0733ad275e2b9e05541341a97ee82678c58932464fad26164657a111a7e37a9fa0300266fb90e2135a1f1512350cb4e985488a88809b14e3cbe415e76e82b2' +const blsPublicKey = + '4d23d8cd06f30b1fa7cf368e2f5399ab04bb6846c682f493a98a607d3dfb7e53a712bb79b475c57b0ac2785460f91301' +const blsPoP = + '9d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d740501' + +const publicKeysData = '0x' + publicKey + blsPublicKey + blsPoP + +testWithGanache('Validators Wrapper', (web3) => { + const kit = newKitFromWeb3(web3) + let accounts: string[] = [] + let validators: ValidatorsWrapper + 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 }) + } + + beforeAll(async () => { + accounts = await web3.eth.getAccounts() + validators = await kit.contracts.getValidators() + lockedGold = await kit.contracts.getLockedGold() + }) + + const setupGroup = async (groupAccount: string) => { + await registerAccountWithCommitment(groupAccount) + await validators + .registerValidatorGroup('thegroup', 'The Group', 'thegroup.com', [minLockedGoldNoticePeriod]) + .sendAndWaitForReceipt({ from: groupAccount }) + } + + const setupValidator = async (validatorAccount: string) => { + await registerAccountWithCommitment(validatorAccount) + // set account1 as the validator + await validators + .registerValidator( + 'goodoldvalidator', + 'Good old validator', + 'goodold.com', + // @ts-ignore + publicKeysData, + [minLockedGoldNoticePeriod] + ) + .sendAndWaitForReceipt({ from: validatorAccount }) + } + + test('SBAT registerValidatorGroup', async () => { + const groupAccount = accounts[0] + await setupGroup(groupAccount) + await expect(validators.isValidatorGroup(groupAccount)).resolves.toBe(true) + }) + + test('SBAT registerValidator', async () => { + const validatorAccount = accounts[1] + await setupValidator(validatorAccount) + await expect(validators.isValidator(validatorAccount)).resolves.toBe(true) + }) + + test('SBAT addMember', async () => { + const groupAccount = accounts[0] + const validatorAccount = accounts[1] + await setupGroup(groupAccount) + await setupValidator(validatorAccount) + await validators.affiliate(groupAccount).sendAndWaitForReceipt({ from: validatorAccount }) + await validators.addMember(validatorAccount).sendAndWaitForReceipt({ from: groupAccount }) + + const members = await validators.getValidatorGroup(groupAccount).then((group) => group.members) + expect(members).toContain(validatorAccount) + }) + + describe('SBAT reorderMember', () => { + let groupAccount: string, validator1: string, validator2: string + + beforeEach(async () => { + groupAccount = accounts[0] + await setupGroup(groupAccount) + + validator1 = accounts[1] + validator2 = accounts[2] + + for (const validator of [validator1, validator2]) { + await setupValidator(validator) + await validators.affiliate(groupAccount).sendAndWaitForReceipt({ from: validator }) + await validators.addMember(validator).sendAndWaitForReceipt({ from: groupAccount }) + } + + const members = await validators + .getValidatorGroup(groupAccount) + .then((group) => group.members) + expect(members).toEqual([validator1, validator2]) + }) + + test('move last to first', async () => { + await validators + .reorderMember(groupAccount, validator2, 0) + .then((x) => x.sendAndWaitForReceipt()) + + const membersAfter = await validators + .getValidatorGroup(groupAccount) + .then((group) => group.members) + expect(membersAfter).toEqual([validator2, validator1]) + }) + + test('move first to last', async () => { + await validators + .reorderMember(groupAccount, validator1, 1) + .then((x) => x.sendAndWaitForReceipt()) + + const membersAfter = await validators + .getValidatorGroup(groupAccount) + .then((group) => group.members) + expect(membersAfter).toEqual([validator2, validator1]) + }) + }) +}) diff --git a/packages/contractkit/src/wrappers/Validators.ts b/packages/contractkit/src/wrappers/Validators.ts index fe9ae06f560..2ebe2859f61 100644 --- a/packages/contractkit/src/wrappers/Validators.ts +++ b/packages/contractkit/src/wrappers/Validators.ts @@ -10,7 +10,7 @@ import { proxySend, toBigNumber, toNumber, - wrapSend, + toTransactionObject, } from './BaseWrapper' export interface Validator { @@ -53,8 +53,6 @@ export interface ValidatorConfig { 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) registerValidatorGroup = proxySend(this.kit, this.contract.methods.registerValidatorGroup) /** @@ -151,6 +149,76 @@ 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. + * @return Whether a particular address is a registered validator. + */ + isValidator = proxyCall(this.contract.methods.isValidator) + + /** + * Returns whether a particular account has a registered validator group. + * @param account The account. + * @return Whether a particular address is a registered validator group. + */ + 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) + + async reorderMember(groupAddr: Address, validator: Address, newIndex: number) { + const group = await this.getValidatorGroup(groupAddr) + + if (newIndex < 0 || newIndex >= group.members.length) { + throw new Error(`Invalid index ${newIndex}; max index is ${group.members.length - 1}`) + } + + const currentIdx = group.members.indexOf(validator) + if (currentIdx < 0) { + throw new Error(`ValidatorGroup ${groupAddr} does not inclue ${validator}`) + } else if (currentIdx === newIndex) { + throw new Error(`Validator is already in position ${newIndex}`) + } + + // remove the element + group.members.splice(currentIdx, 1) + // add it on new position + group.members.splice(newIndex, 0, validator) + + const nextMember = + newIndex === group.members.length - 1 ? NULL_ADDRESS : group.members[newIndex + 1] + const prevMember = newIndex === 0 ? NULL_ADDRESS : group.members[newIndex - 1] + + return toTransactionObject( + this.kit, + this.contract.methods.reorderMember(validator, nextMember, prevMember), + { from: groupAddr } + ) + } + async getRegisteredValidatorGroups(): Promise { const vgAddresses = await this.contract.methods.getRegisteredValidatorGroups().call() return Promise.all(vgAddresses.map((addr) => this.getValidatorGroup(addr))) @@ -191,7 +259,7 @@ export class ValidatorsWrapper extends BaseWrapper { votingDetails.weight.negated() ) - return wrapSend(this.kit, this.contract.methods.revokeVote(lesser, greater)) + return toTransactionObject(this.kit, this.contract.methods.revokeVote(lesser, greater)) } async vote(validatorGroup: Address): Promise> { @@ -207,7 +275,10 @@ export class ValidatorsWrapper extends BaseWrapper { votingDetails.weight ) - return wrapSend(this.kit, this.contract.methods.vote(validatorGroup, lesser, greater)) + return toTransactionObject( + this.kit, + this.contract.methods.vote(validatorGroup, lesser, greater) + ) } private async findLesserAndGreaterAfterVote( diff --git a/packages/docs/command-line-interface/validatorgroup.md b/packages/docs/command-line-interface/validatorgroup.md index f8e3eda5b7b..95186d3bfca 100644 --- a/packages/docs/command-line-interface/validatorgroup.md +++ b/packages/docs/command-line-interface/validatorgroup.md @@ -33,10 +33,12 @@ OPTIONS --accept Accept a validator whose affiliation is already set to the group --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) ValidatorGroup's address --remove Remove a validator from the members list + --reorder=reorder Reorder a validator within the members list EXAMPLES member --accept 0x97f7333c51897469e8d98e7af8653aab468050a3 member --remove 0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95 + member --reorder 3 0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95 ``` _See code: [packages/cli/src/commands/validatorgroup/member.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validatorgroup/member.ts)_ From a58d510e83f71829a759476e55252ff71d4c9acf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Volpe?= Date: Tue, 15 Oct 2019 16:20:47 +0200 Subject: [PATCH 13/37] Speed up CircleCI tests (#1333) --- .circleci/config.yml | 14 ++++++++++---- packages/mobile/scripts/start_emulator.sh | 11 ++++++++++- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 05a2884d27c..6b4acc76594 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -117,7 +117,7 @@ jobs: paths: - . - mobile_e2e_test: + end-to-end-mobile-test: <<: *android_config resource_class: large steps: @@ -141,6 +141,7 @@ jobs: echo 'export ANDROID_SDK_ROOT="/usr/local/share/android-sdk"' >> $BASH_ENV echo 'export QEMU_AUDIO_DRV=none' >> $BASH_ENV export PATH=$PATH:/usr/local/share/android-sdk/platform-tools/ + export GRADLE_OPTS='-Dorg.gradle.daemon=true -Dorg.gradle.parallel=true -Dorg.gradle.jvmargs="-Xmx4096m -XX:+HeapDumpOnOutOfMemoryError"' - run: name: Install Android sdk command: | @@ -207,7 +208,6 @@ jobs: command: HOMEBREW_NO_AUTO_UPDATE=1 brew install pidcat watchman - restore_cache: key: yarn-v3-{{ arch }}-{{ .Branch }}-{{ checksum "packages/mobile/android/build.gradle" }}-{{ checksum "packages/mobile/android/settings.gradle" }}-{{ checksum "packages/mobile/android/app/build.gradle" }}-{{ checksum "packages/mobile/.env.test" }} - - run: cd ~/src/packages/mobile && yarn test:build-e2e # tests seem to fail if the app is built at the same time the emulator runs - run: name: Start emulator command: cd ~/src/packages/mobile && bash ./scripts/start_emulator.sh @@ -218,7 +218,9 @@ jobs: background: true - run: cp node_modules/.bin/jest packages/mobile/node_modules/.bin/ # for some reason jest is just not there # TODO - run: lock device - - run: tree packages/mobile/android/app/build/outputs/apk/ + - run: + name: Build end-to-end test + command: cd ~/src/packages/mobile && yarn test:build-e2e - run: name: Sleep until Device connects command: cd ~/src/packages/mobile && bash ./scripts/wait_for_emulator_to_connect.sh @@ -267,6 +269,10 @@ jobs: - attach_workspace: at: ~/app + - run: + name: Set GRADLE_OPTS to speed up gradle + command: | + export GRADLE_OPTS='-Dorg.gradle.daemon=true -Dorg.gradle.parallel=true -Dorg.gradle.jvmargs="-Xmx4096m -XX:+HeapDumpOnOutOfMemoryError"' - run: name: Build Android app (debug version) command: | @@ -741,7 +747,7 @@ workflows: celo-monorepo-build: jobs: - install_dependencies - - mobile_e2e_test: + - end-to-end-mobile-test: requires: - install_dependencies - mobile-test-build-app diff --git a/packages/mobile/scripts/start_emulator.sh b/packages/mobile/scripts/start_emulator.sh index f7e9e590181..f7d391e350e 100755 --- a/packages/mobile/scripts/start_emulator.sh +++ b/packages/mobile/scripts/start_emulator.sh @@ -1,3 +1,12 @@ #!/usr/bin/env bash -$ANDROID_SDK_ROOT/emulator/emulator -avd `$ANDROID_SDK_ROOT/emulator/emulator -list-avds | grep 'x86' | head -n 1` -no-boot-anim -no-window \ No newline at end of file +ENABLE_EMULATOR_WINDOW="${ENABLE_EMULATOR_WINDOW:-false}" + +PARAMS="" + +if ! $ENABLE_EMULATOR_WINDOW ; then + PARAMS="${PARAMS} -no-window" + echo "Not showing emulator windown due ENABLE_EMULATOR_WINDOW env variable" +fi + +$ANDROID_SDK_ROOT/emulator/emulator -avd `$ANDROID_SDK_ROOT/emulator/emulator -list-avds | grep 'x86' | head -n 1` -no-boot-anim $PARAMS \ No newline at end of file From 833c2d3d2341185dc141aec901c9b2828e776de6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Volpe?= Date: Tue, 15 Oct 2019 17:48:05 +0200 Subject: [PATCH 14/37] [wallet] If an action is in the blacklist, now only logs the name (but not the payload) (#1338) --- packages/mobile/src/redux/sagas.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/mobile/src/redux/sagas.ts b/packages/mobile/src/redux/sagas.ts index 454e2228d94..798967ac62f 100644 --- a/packages/mobile/src/redux/sagas.ts +++ b/packages/mobile/src/redux/sagas.ts @@ -43,6 +43,9 @@ function* loggerSaga() { yield takeEvery('*', (action: AnyAction) => { if (action && action.type && loggerBlacklist.includes(action.type)) { + // Log only action type, but not the payload as it can have + // sensitive information. + Logger.debug('redux/saga@logger', `${action.type} (payload not logged)`) return } try { From 2d23e79d33d037adc37cfbcb25ffc36a09fcafc0 Mon Sep 17 00:00:00 2001 From: Jean Regisser Date: Tue, 15 Oct 2019 23:13:28 +0200 Subject: [PATCH 15/37] [Wallet] Use latest geth with ipc support on iOS (#1329) * Use latest geth with ipc support on iOS * Fix lint and TypeScript build --- packages/mobile/ios/Podfile | 6 +----- packages/mobile/ios/Podfile.lock | 14 +++++++++---- .../mobile/ios/celo.xcodeproj/project.pbxproj | 4 ++++ packages/mobile/package.json | 6 +++--- packages/mobile/src/geth/geth.ts | 12 +++++++++++ packages/mobile/src/web3/contracts.ts | 20 ++----------------- yarn.lock | 16 +++++++-------- 7 files changed, 40 insertions(+), 38 deletions(-) diff --git a/packages/mobile/ios/Podfile b/packages/mobile/ios/Podfile index 061ec1d5113..896d6a1d5a8 100644 --- a/packages/mobile/ios/Podfile +++ b/packages/mobile/ios/Podfile @@ -1,11 +1,6 @@ # File contents of "ios/Podfile" platform :ios, '9.0' -pre_install do |installer| - # workaround for CocoaPods/CocoaPods#3289 - Pod::Installer::Xcode::TargetValidator.send(:define_method, :verify_no_static_framework_transitive_dependencies) {} -end - target 'celo' do use_frameworks! @@ -54,6 +49,7 @@ target 'celo' do pod 'react-native-splash-screen', :path => '../../../node_modules/react-native-splash-screen' pod 'react-native-version-check', :path => '../../../node_modules/react-native-version-check' pod 'RNRandomBytes', :path => '../../../node_modules/react-native-secure-randombytes' + pod 'react-native-tcp', :path => '../../../node_modules/react-native-tcp' pod 'react-native-udp', :path => '../../../node_modules/react-native-udp' pod 'react-native-netinfo', :path => '../../../node_modules/@react-native-community/netinfo' pod 'RNShare', :path => '../../../node_modules/react-native-share' diff --git a/packages/mobile/ios/Podfile.lock b/packages/mobile/ios/Podfile.lock index 6adcbc5696c..a5429a024de 100644 --- a/packages/mobile/ios/Podfile.lock +++ b/packages/mobile/ios/Podfile.lock @@ -1,7 +1,7 @@ PODS: - Analytics (3.7.0) - boost-for-react-native (1.63.0) - - CeloBlockchain (0.0.1) + - CeloBlockchain (0.0.156) - Crashlytics (3.13.4): - Fabric (~> 1.10.2) - DoubleConversion (1.1.6) @@ -139,6 +139,8 @@ PODS: - React - react-native-splash-screen (3.1.1): - React + - react-native-tcp (3.3.0): + - React - react-native-udp (2.6.1): - React - react-native-version-check (3.0.2): @@ -266,6 +268,7 @@ DEPENDENCIES: - "react-native-netinfo (from `../../../node_modules/@react-native-community/netinfo`)" - react-native-safe-area-context (from `../../../node_modules/react-native-safe-area-context`) - react-native-splash-screen (from `../../../node_modules/react-native-splash-screen`) + - react-native-tcp (from `../../../node_modules/react-native-tcp`) - react-native-udp (from `../../../node_modules/react-native-udp`) - react-native-version-check (from `../../../node_modules/react-native-version-check`) - React/Core (from `../node_modules/react-native`) @@ -351,6 +354,8 @@ EXTERNAL SOURCES: :path: "../../../node_modules/react-native-safe-area-context" react-native-splash-screen: :path: "../../../node_modules/react-native-splash-screen" + react-native-tcp: + :path: "../../../node_modules/react-native-tcp" react-native-udp: :path: "../../../node_modules/react-native-udp" react-native-version-check: @@ -387,7 +392,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: Analytics: 77fd5fb102a4a5eedafa2c2b0245ceb7b7c15e45 boost-for-react-native: 39c7adb57c4e60d6c5479dd8623128eb5b3f0f2c - CeloBlockchain: 4779210b5840c586b131de638694044ecb362bda + CeloBlockchain: f258122010fb610057979254d4a3f88eb7fb0d21 Crashlytics: 2dfd686bcb918dc10ee0e76f7f853fe42c7bd552 DoubleConversion: bb338842f62ab1d708ceb63ec3d999f0f3d98ecd Fabric: 706c8b8098fff96c33c0db69cbf81f9c551d0d74 @@ -413,12 +418,13 @@ SPEC CHECKSUMS: react-native-camera: 571e4cfe70e7021e9077c1699a53e659db971f96 react-native-config: 8f6b2b9e017f6a5e92a97353c768e19e67294bb1 react-native-contacts: 348958bb7da4399040b1eb45bfb7f6af08e61412 - react-native-geth: 8ec5ada8b9c69eea6a515fdfe784878a625c9b6e + react-native-geth: b643560e11512a3c0b1d78df929cb9f2409cf4a2 react-native-keep-awake: abcf6d09d0cc5fe45df4a56a9382e25d10cff8b6 react-native-mail: 021d8ee60e374609f5689ef354dc8e36839a9ba6 react-native-netinfo: 1ea4efa22c02519ac8043ac3f000062a4e320795 react-native-safe-area-context: e380a6f783ccaec848e2f3cc8eb205a62362950d react-native-splash-screen: 353334c5ae82d8c74501ea7cbb916cb7cb20c8bf + react-native-tcp: e1a8c3ac010774cd71811989805ff3eaebb62f17 react-native-udp: 54a1aa9bf5c0824f930b1ba6dbfb3fd3e733bba9 react-native-version-check: 901616b6d258b385628120441bd0b285b24c7be1 RNAnalytics: 05243c1fa17186f07be3eae30514b43aeb2e0578 @@ -438,6 +444,6 @@ SPEC CHECKSUMS: SentryReactNative: 07237139c00366ea2e75ae3e5c566e7a71c27a90 yoga: 684513b14b03201579ba3cee20218c9d1298b0cc -PODFILE CHECKSUM: 74a2f21d73b361a2ed26a15896af8f4d70b123e0 +PODFILE CHECKSUM: 02a0ed956675dc8aa46eab297cf94e8823e80147 COCOAPODS: 1.7.5 diff --git a/packages/mobile/ios/celo.xcodeproj/project.pbxproj b/packages/mobile/ios/celo.xcodeproj/project.pbxproj index 090693a354a..14294ad422d 100644 --- a/packages/mobile/ios/celo.xcodeproj/project.pbxproj +++ b/packages/mobile/ios/celo.xcodeproj/project.pbxproj @@ -448,6 +448,7 @@ inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-celo/Pods-celo-frameworks.sh", "${BUILT_PRODUCTS_DIR}/Analytics/Analytics.framework", + "${BUILT_PRODUCTS_DIR}/CeloBlockchain/CeloBlockchain.framework", "${BUILT_PRODUCTS_DIR}/DoubleConversion/DoubleConversion.framework", "${BUILT_PRODUCTS_DIR}/Folly/folly.framework", "${BUILT_PRODUCTS_DIR}/GTMSessionFetcher/GTMSessionFetcher.framework", @@ -477,6 +478,7 @@ "${BUILT_PRODUCTS_DIR}/react-native-netinfo/react_native_netinfo.framework", "${BUILT_PRODUCTS_DIR}/react-native-safe-area-context/react_native_safe_area_context.framework", "${BUILT_PRODUCTS_DIR}/react-native-splash-screen/react_native_splash_screen.framework", + "${BUILT_PRODUCTS_DIR}/react-native-tcp/react_native_tcp.framework", "${BUILT_PRODUCTS_DIR}/react-native-udp/react_native_udp.framework", "${BUILT_PRODUCTS_DIR}/react-native-version-check/react_native_version_check.framework", "${BUILT_PRODUCTS_DIR}/yoga/yoga.framework", @@ -484,6 +486,7 @@ name = "[CP] Embed Pods Frameworks"; outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Analytics.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CeloBlockchain.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DoubleConversion.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/folly.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GTMSessionFetcher.framework", @@ -513,6 +516,7 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/react_native_netinfo.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/react_native_safe_area_context.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/react_native_splash_screen.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/react_native_tcp.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/react_native_udp.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/react_native_version_check.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/yoga.framework", diff --git a/packages/mobile/package.json b/packages/mobile/package.json index 5e624f82630..63129e05ba3 100644 --- a/packages/mobile/package.json +++ b/packages/mobile/package.json @@ -47,7 +47,7 @@ ] }, "dependencies": { - "@celo/client": "c1ae452", + "@celo/client": "691d471", "@celo/react-components": "1.0.0", "@celo/react-native-sms-retriever": "git+https://github.com/celo-org/react-native-sms-retriever#d3a2fdb", "@celo/utils": "^0.1.1", @@ -93,7 +93,7 @@ "react-native-flag-secure-android": "git://github.com/kristiansorens/react-native-flag-secure-android#e234251", "react-native-fs": "^2.12.1", "react-native-gesture-handler": "^1.1.0", - "react-native-geth": "git+https://github.com/celo-org/react-native-geth#4d9ee15", + "react-native-geth": "https://github.com/celo-org/react-native-geth#8ba5091", "react-native-install-referrer": "git://github.com/celo-org/react-native-install-referrer#343bf3d", "react-native-keep-awake": "^3.0.1", "react-native-keyboard-aware-scroll-view": "^0.6.0", @@ -117,7 +117,7 @@ "react-native-splash-screen": "^3.1.1", "react-native-svg": "^9.8.4", "react-native-swiper": "^1.5.13", - "react-native-tcp": "git://github.com/cmcewen/react-native-tcp#08f03c2", + "react-native-tcp": "https://github.com/cmcewen/react-native-tcp#408b674", "react-native-udp": "^2.6.1", "react-native-version-check": "^3.0.2", "react-native-webview": "^5.12.1", diff --git a/packages/mobile/src/geth/geth.ts b/packages/mobile/src/geth/geth.ts index 1987f214bf7..9bff3460b71 100644 --- a/packages/mobile/src/geth/geth.ts +++ b/packages/mobile/src/geth/geth.ts @@ -54,6 +54,17 @@ const INSTANCE_FOLDER = Platform.select({ default: 'GethMobile', }) +// Use relative path on iOS to workaround the 104 chars path limit for unix domain socket. +// On iOS the default path would be something like +// `/var/mobile/Containers/Data/Application/2E684E03-9EFA-492A-B19A-4759DD32BE67/Documents/.alfajores/geth.ipc` +// which is too long. +// So on iOS, `react-native-geth` changes the current directory to `${DocumentDirectoryPath}/.${DEFAULT_TESTNET}` +// for the relative path workaround to work. +export const IPC_PATH = + Platform.OS === 'ios' + ? './geth.ipc' + : `${RNFS.DocumentDirectoryPath}/.${DEFAULT_TESTNET}/geth.ipc` + function getNodeInstancePath(nodeDir: string) { return `${RNFS.DocumentDirectoryPath}/${nodeDir}/${INSTANCE_FOLDER}` } @@ -76,6 +87,7 @@ async function createNewGeth(): Promise { genesis, syncMode, useLightweightKDF: true, + ipcPath: IPC_PATH, } // Setup Logging diff --git a/packages/mobile/src/web3/contracts.ts b/packages/mobile/src/web3/contracts.ts index 8f3583ba34f..e74bc16463c 100644 --- a/packages/mobile/src/web3/contracts.ts +++ b/packages/mobile/src/web3/contracts.ts @@ -1,9 +1,9 @@ import { addLocalAccount as web3utilsAddLocalAccount } from '@celo/walletkit' import { Platform } from 'react-native' -import { DocumentDirectoryPath } from 'react-native-fs' import * as net from 'react-native-tcp' import { DEFAULT_INFURA_URL, DEFAULT_TESTNET } from 'src/config' import { GethSyncMode } from 'src/geth/consts' +import { IPC_PATH } from 'src/geth/geth' import networkConfig, { Testnets } from 'src/geth/networkConfig' import Logger from 'src/utils/Logger' import Web3 from 'web3' @@ -21,10 +21,7 @@ export function isZeroSyncMode(): boolean { function getIpcProvider(testnet: Testnets) { Logger.debug(tag, 'creating IPCProvider...') - const ipcProvider = new Web3.providers.IpcProvider( - `${DocumentDirectoryPath}/.${testnet}/geth.ipc`, - net - ) + const ipcProvider = new Web3.providers.IpcProvider(IPC_PATH, net) Logger.debug(tag, 'created IPCProvider') // More details on the IPC objects can be seen via this @@ -58,16 +55,6 @@ function getIpcProvider(testnet: Testnets) { return ipcProvider } -// Use Http provider on iOS until we add support for local socket on iOS in react-native-tcp -function getWeb3HttpProviderForIos(): Provider { - Logger.debug(tag, 'creating HttpProvider for iOS...') - - const httpProvider = new Web3.providers.HttpProvider('http://localhost:8545') - Logger.debug(tag, 'created HttpProvider for iOS') - - return httpProvider -} - function getWebSocketProvider(url: string): Provider { Logger.debug(tag, 'creating HttpProvider...') const provider = new Web3.providers.HttpProvider(url) @@ -89,9 +76,6 @@ function getWeb3(): Web3 { const url = DEFAULT_INFURA_URL Logger.debug('contracts@getWeb3', `Connecting to url ${url}`) return new Web3(getWebSocketProvider(url)) - } else if (Platform.OS === 'ios') { - // iOS + local geth - return new Web3(getWeb3HttpProviderForIos()) } else { return new Web3(getIpcProvider(DEFAULT_TESTNET)) } diff --git a/yarn.lock b/yarn.lock index d35f38c8378..33ed2df19c1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2243,10 +2243,10 @@ lodash "^4.17.13" to-fast-properties "^2.0.0" -"@celo/client@c1ae452": - version "0.0.155" - resolved "https://registry.yarnpkg.com/@celo/client/-/client-0.0.155.tgz#71f658ba39f66d28dedc2c424b983306dd335d7e" - integrity sha512-vSKYdJy649CHVBcpzHxeI2MNH9wZA4AsJJdRyDW9etzkeIBbHm1eL1pkUrxgeY/96nWb20YhjC+NL8cmujiqQQ== +"@celo/client@691d471": + version "0.0.156" + resolved "https://registry.yarnpkg.com/@celo/client/-/client-0.0.156.tgz#00c738c2988086aca84f585deae018ad95d743e8" + integrity sha512-ql8tsyZrMIuuYHbSjNt6IUT/hvEC51MCePcPAt6OLrmlKqBI8xOGAlinuFxo8LuPNHnqBgYc/FoX2Jww3HbCPQ== "@celo/contractkit@0.1.1": version "0.1.1" @@ -26201,9 +26201,9 @@ react-native-gesture-handler@^1.1.0: invariant "^2.2.2" prop-types "^15.5.10" -"react-native-geth@git+https://github.com/celo-org/react-native-geth#4d9ee15": +"react-native-geth@https://github.com/celo-org/react-native-geth#8ba5091": version "0.1.0-development" - resolved "git+https://github.com/celo-org/react-native-geth#4d9ee15f212a32a1f865a6fd121117130ea71de9" + resolved "https://github.com/celo-org/react-native-geth#8ba5091d232f58913026b2df1f2b834e2e0108d0" "react-native-install-referrer@git://github.com/celo-org/react-native-install-referrer#343bf3d": version "2.0.0" @@ -26399,9 +26399,9 @@ react-native-tab-view@^1.2.0, react-native-tab-view@^1.3.4: dependencies: prop-types "^15.6.1" -"react-native-tcp@git://github.com/cmcewen/react-native-tcp#08f03c2": +"react-native-tcp@https://github.com/cmcewen/react-native-tcp#408b674": version "3.3.0" - resolved "git://github.com/cmcewen/react-native-tcp#08f03c2113570baf9beef85abef83748f005199e" + resolved "https://github.com/cmcewen/react-native-tcp#408b674f20df4bfc1e6e80e54e7663c91a0f7b67" dependencies: base64-js "0.0.8" buffer "^5.0.0" From 772bf91cb49994833d149740916c60b07ad85255 Mon Sep 17 00:00:00 2001 From: Anna K Date: Tue, 15 Oct 2019 15:23:41 -0700 Subject: [PATCH 16/37] [Mobile] Show Incorrect PIN error in exchange (#1345) --- packages/mobile/src/exchange/actions.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/mobile/src/exchange/actions.ts b/packages/mobile/src/exchange/actions.ts index 8990daadeed..ae1201ce8d7 100644 --- a/packages/mobile/src/exchange/actions.ts +++ b/packages/mobile/src/exchange/actions.ts @@ -249,10 +249,15 @@ export function* exchangeGoldAndStableTokens(action: ExchangeTokensAction) { yield call(sendAndMonitorTransaction, txId, tx, account) } catch (error) { Logger.error(TAG, 'Error doing exchange', error) - yield put(showError(ErrorMessages.EXCHANGE_FAILED)) if (txId) { yield put(removeStandbyTransaction(txId)) } + + if (error.message === ErrorMessages.INCORRECT_PIN) { + yield put(showError(ErrorMessages.INCORRECT_PIN)) + } else { + yield put(showError(ErrorMessages.EXCHANGE_FAILED)) + } } } From c251b7552a390e63e1633b879f710c2e6d9c87fd Mon Sep 17 00:00:00 2001 From: Snyk bot Date: Wed, 16 Oct 2019 00:51:26 +0200 Subject: [PATCH 17/37] [Snyk] Fix for 1 vulnerabilities (#1262) --- packages/faucet/package.json | 2 +- yarn.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/faucet/package.json b/packages/faucet/package.json index 342545d1826..50a73d19c43 100644 --- a/packages/faucet/package.json +++ b/packages/faucet/package.json @@ -21,7 +21,7 @@ "debug": "^4.1.1", "eth-lib": "^0.2.8", "firebase": "^6.2.2", - "firebase-admin": "^7.0.0", + "firebase-admin": "^8.3.0", "firebase-functions": "^3.2.0", "rlp": "^2.2.3", "twilio": "^3.23.2", diff --git a/yarn.lock b/yarn.lock index 33ed2df19c1..bfdc6be3d83 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14660,7 +14660,7 @@ firebase-admin@^7.0.0: "@google-cloud/firestore" "^1.0.1" "@google-cloud/storage" "^2.3.0" -firebase-admin@^8.6.0: +firebase-admin@^8.3.0, firebase-admin@^8.6.0: version "8.6.0" resolved "https://registry.yarnpkg.com/firebase-admin/-/firebase-admin-8.6.0.tgz#ed81d6b1a0b2b1f4033c47e33595e85634d0a68b" integrity sha512-+JqOinU5bYUkg434LqEBXrHMrIBhL/+HwWEgbZpS1sBKHQRJK7LlcBrayqxvQKwJzgh5xs/JTInTmkozXk7h1w== From 5adc63421161910908ed64e9078b154da1509de6 Mon Sep 17 00:00:00 2001 From: Jean Regisser Date: Wed, 16 Oct 2019 12:23:29 +0200 Subject: [PATCH 18/37] [Wallet] Enable firebase on iOS (#1344) --- packages/mobile/.gitignore | 1 + .../mobile/ios/GoogleService-Info.plist.enc | Bin 0 -> 1273 bytes .../mobile/ios/celo.xcodeproj/project.pbxproj | 4 ++++ packages/mobile/ios/celo/AppDelegate.m | 5 ++--- patches/react-native-firebase+5.5.4.patch | 16 ++++++++++++++++ scripts/key_placer.sh | 1 + 6 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 packages/mobile/ios/GoogleService-Info.plist.enc create mode 100644 patches/react-native-firebase+5.5.4.patch diff --git a/packages/mobile/.gitignore b/packages/mobile/.gitignore index e90d10842e4..78474411445 100644 --- a/packages/mobile/.gitignore +++ b/packages/mobile/.gitignore @@ -83,5 +83,6 @@ android/app/src/debug/google-services.json android/app/src/pilot/google-services.json secrets.json android/sentry.properties +ios/**/GoogleService-Info*.plist ios/sentry.properties ios/Pods diff --git a/packages/mobile/ios/GoogleService-Info.plist.enc b/packages/mobile/ios/GoogleService-Info.plist.enc new file mode 100644 index 0000000000000000000000000000000000000000..3745b796f0e813bbda61ff6c7de66ab91d0750d7 GIT binary patch literal 1273 zcmVV*~>j10>~hW~zo z7_ki`s=K!&kakR0t}&x!tOJATcf?MWD>rd~UwBj!z~{v2*N%wLf!wu-89orx!n~ z(i1V9FCXCcuMsK(!~B^56YFIm9f*qPj%#{n#XU*Kwu2{qE4L~G*|U92zMip1$(17S67!4;IqfaMHWP2D5!@dI)MXS( z0VhT1EBu9V$QKe_D7o9&O-fVs!G|ARn|J4Ln^5Spfy@SXpXuKQfenhPH z_!f%4^liA(0htJ#RFdvGl=wLO=Bv33H7Tc)>`OOr{m)#nW;a%qF!cMIG5V;%k|qd+ zso_})lh8zTf7@2OtTfO0v=^^fe9M?O*y0I8(Jyj#dkWk)8+X=LbA51g@tTsW^Yw>yBd9Y8*E3f_YR5*>qgT9&b49Fa}+eWLP84KFSzgB zTUZbOoSS*CoZ|k?AX%C1rlrg*;wZc*O7B)GCw?9t=<8&K?fld%3E?6!65f%y-{c@p z*RR2zWz~1ZGi;iLA_H%=QMG|XmW&W4kBH^Rd&0BttsXgQ!Ld*SM~#M{-(@qr?k*yd zJ9Z?x+1Ic*eS~AkDGhVF<;>5aU{2tzRwzB?%Ie|!OSeCe;*A#kfKWcYS=Q%Tb%Lil zXNxmy!_cNJ*rar>rf#1Yn-iL%#clZb!hzC+E@3HndzLjIjz z3{1g+{Q1N`bI}oUziGxz5au;0+r$Wi5_>^DY1^NV;gJaL`F>6I66y?wB9=-%&`|p?U7%YvVn(zuaAKa8LYE)|o4kQMNwlv|ZwDNq&*4Qs}zQp~| z5?@*W#-vs`r8^Ub3DgSsAh|%ao6C))+Tu_CsM_ z*Zr&IdWN!{+f7DGb-3@fu$tEBq7H%T4n8V;5b%?NoN!X2t0XMA@=|T|3X@e)1C^P^ jI}vM|T!1By_s33zY@{4no(D4oK;|h<8@eMh2y2tav;%vH literal 0 HcmV?d00001 diff --git a/packages/mobile/ios/celo.xcodeproj/project.pbxproj b/packages/mobile/ios/celo.xcodeproj/project.pbxproj index 14294ad422d..4078f41ebcf 100644 --- a/packages/mobile/ios/celo.xcodeproj/project.pbxproj +++ b/packages/mobile/ios/celo.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ 0581BC2D3AEB42D086D403E3 /* Hind-Medium.ttf in Resources */ = {isa = PBXBuildFile; fileRef = AAE92820119F4CC5B23C6636 /* Hind-Medium.ttf */; }; 0F1E1EB52346439C00274556 /* rings@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 0F1E1EB32346439C00274556 /* rings@2x.png */; }; 0F1E1EB62346439C00274556 /* rings@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 0F1E1EB42346439C00274556 /* rings@3x.png */; }; + 0FF4C5D62355F18C009E07DD /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 0FF4C5D52355F18B009E07DD /* GoogleService-Info.plist */; }; 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.m */; }; 13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB11A68108700A75B9A /* LaunchScreen.xib */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; @@ -53,6 +54,7 @@ 0F1E1EB32346439C00274556 /* rings@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "rings@2x.png"; sourceTree = ""; }; 0F1E1EB42346439C00274556 /* rings@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "rings@3x.png"; sourceTree = ""; }; 0FE3DE8E2347740700EA87A0 /* celo.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = celo.entitlements; path = celo/celo.entitlements; sourceTree = ""; }; + 0FF4C5D52355F18B009E07DD /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; 114969FB1F3AA04FFDC6831F /* Pods-celoTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-celoTests.debug.xcconfig"; path = "Target Support Files/Pods-celoTests/Pods-celoTests.debug.xcconfig"; sourceTree = ""; }; 13B07F961A680F5B00A75B9A /* celo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = celo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = celo/AppDelegate.h; sourceTree = ""; }; @@ -142,6 +144,7 @@ 13B07FAE1A68108700A75B9A /* celo */ = { isa = PBXGroup; children = ( + 0FF4C5D52355F18B009E07DD /* GoogleService-Info.plist */, 0FE3DE8E2347740700EA87A0 /* celo.entitlements */, 008F07F21AC5B25A0029DE68 /* main.jsbundle */, 13B07FAF1A68108700A75B9A /* AppDelegate.h */, @@ -367,6 +370,7 @@ 0F1E1EB62346439C00274556 /* rings@3x.png in Resources */, 0F1E1EB52346439C00274556 /* rings@2x.png in Resources */, 0581BC2D3AEB42D086D403E3 /* Hind-Medium.ttf in Resources */, + 0FF4C5D62355F18C009E07DD /* GoogleService-Info.plist in Resources */, 2CE54C7D16D44C5380C53609 /* Hind-Regular.ttf in Resources */, 5575263ED1EA4F568D328DE9 /* Hind-SemiBold.ttf in Resources */, ); diff --git a/packages/mobile/ios/celo/AppDelegate.m b/packages/mobile/ios/celo/AppDelegate.m index 58a80e5d650..46aac86390a 100644 --- a/packages/mobile/ios/celo/AppDelegate.m +++ b/packages/mobile/ios/celo/AppDelegate.m @@ -19,14 +19,13 @@ #import "RNSentry.h" // This is used for versions of react < 0.40 #endif -#import +@import Firebase; @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - // TODO: add GoogleService-Info.plist and enable this - // [FIRApp configure]; + [FIRApp configure]; RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions]; RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge moduleName:@"celo" diff --git a/patches/react-native-firebase+5.5.4.patch b/patches/react-native-firebase+5.5.4.patch new file mode 100644 index 00000000000..b36ea9b8621 --- /dev/null +++ b/patches/react-native-firebase+5.5.4.patch @@ -0,0 +1,16 @@ +patch-package +--- a/node_modules/react-native-firebase/ios/RNFirebase/database/RNFirebaseDatabase.m ++++ b/node_modules/react-native-firebase/ios/RNFirebase/database/RNFirebaseDatabase.m +@@ -96,11 +96,11 @@ RCT_EXPORT_METHOD(transactionStart:(NSString *)appDisplayName + path:(NSString *)path + transactionId:(nonnull NSNumber *)transactionId + applyLocally:(BOOL)applyLocally) { ++ FIRDatabaseReference *ref = [self getReferenceForAppPath:appDisplayName dbURL:dbURL path:path]; + dispatch_async(_transactionQueue, ^{ + NSMutableDictionary *transactionState = [NSMutableDictionary new]; + dispatch_semaphore_t sema = dispatch_semaphore_create(0); + transactionState[@"semaphore"] = sema; +- FIRDatabaseReference *ref = [self getReferenceForAppPath:appDisplayName dbURL:dbURL path:path]; + + [ref runTransactionBlock:^FIRTransactionResult *_Nonnull (FIRMutableData *_Nonnull currentData) { + dispatch_barrier_async(_transactionQueue, ^{ diff --git a/scripts/key_placer.sh b/scripts/key_placer.sh index ee289957d59..a5372f196ca 100755 --- a/scripts/key_placer.sh +++ b/scripts/key_placer.sh @@ -16,6 +16,7 @@ files=( "packages/mobile/android/app/src/debug/google-services.json" "packages/mobile/android/app/src/pilot/google-services.json" "packages/mobile/android/sentry.properties" + "packages/mobile/ios/GoogleService-Info.plist" "packages/mobile/ios/sentry.properties" "packages/verifier/android/app/google-services.json" "packages/verifier/android/app/src/staging/google-services.json" From aeddeefbfb230db51d2ef76d50c5f882644a1cd3 Mon Sep 17 00:00:00 2001 From: Nam Chu Hoai Date: Wed, 16 Oct 2019 03:52:38 -0700 Subject: [PATCH 19/37] Fix Metadata registration during contract deploy (#1346) --- packages/celotool/src/cmds/deploy/initial/contracts.ts | 3 +-- packages/cli/src/commands/identity/create-metadata.ts | 2 +- packages/contractkit/src/identity/metadata.ts | 6 ++++-- packages/utils/src/async.ts | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/celotool/src/cmds/deploy/initial/contracts.ts b/packages/celotool/src/cmds/deploy/initial/contracts.ts index 59b1d41f77c..161ae6604d1 100644 --- a/packages/celotool/src/cmds/deploy/initial/contracts.ts +++ b/packages/celotool/src/cmds/deploy/initial/contracts.ts @@ -73,10 +73,9 @@ async function makeMetadata(testnet: string, address: string, index: number) { const fileName = `validator-${testnet}-${address}-metadata.json` const filePath = `/tmp/${fileName}` - const metadata = new IdentityMetadataWrapper(IdentityMetadataWrapper.emptyData) + const metadata = IdentityMetadataWrapper.fromEmpty() metadata.addClaim(nameClaim) metadata.addClaim(attestationServiceClaim) - writeFileSync(filePath, metadata.toString()) await uploadFileToGoogleStorage( diff --git a/packages/cli/src/commands/identity/create-metadata.ts b/packages/cli/src/commands/identity/create-metadata.ts index 0e3543e248d..fad6f339d14 100644 --- a/packages/cli/src/commands/identity/create-metadata.ts +++ b/packages/cli/src/commands/identity/create-metadata.ts @@ -19,7 +19,7 @@ export default class CreateMetadata extends BaseCommand { async run() { const { args } = this.parse(CreateMetadata) - const metadata = new IdentityMetadataWrapper(IdentityMetadataWrapper.emptyData) + const metadata = new IdentityMetadataWrapper(IdentityMetadataWrapper.fromEmpty()) writeFileSync(args.file, metadata.toString()) } } diff --git a/packages/contractkit/src/identity/metadata.ts b/packages/contractkit/src/identity/metadata.ts index f7a6fcd4534..c59ab44645a 100644 --- a/packages/contractkit/src/identity/metadata.ts +++ b/packages/contractkit/src/identity/metadata.ts @@ -62,8 +62,10 @@ const isOfType = (type: K) => ( export class IdentityMetadataWrapper { data: IdentityMetadata - static emptyData: IdentityMetadata = { - claims: [], + static fromEmpty() { + return new IdentityMetadataWrapper({ + claims: [], + }) } static async fetchFromURL(url: string) { diff --git a/packages/utils/src/async.ts b/packages/utils/src/async.ts index f4a696d5ece..7b26b1f8ae7 100644 --- a/packages/utils/src/async.ts +++ b/packages/utils/src/async.ts @@ -71,7 +71,7 @@ export async function concurrentMap( const remaining = xs.length - i const sliceSize = Math.min(remaining, concurrency) const slice = xs.slice(i, i + sliceSize) - res = res.concat(await Promise.all(slice.map(mapFn))) + res = res.concat(await Promise.all(slice.map((elem, index) => mapFn(elem, i + index)))) } return res } From 8689634a1d10d74ba6d4f3b36b2484db60a95bdb Mon Sep 17 00:00:00 2001 From: Ashish Bhatia Date: Wed, 16 Oct 2019 10:27:14 -0700 Subject: [PATCH 20/37] [wallet]Add documentation for ZeroSync mode (#1361) --- packages/mobile/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/mobile/README.md b/packages/mobile/README.md index c0660c6e384..c5ba066fdd1 100644 --- a/packages/mobile/README.md +++ b/packages/mobile/README.md @@ -176,6 +176,12 @@ Where `YOUR_BUILD_VARIANT` can be any of the app's build variants, such as debug We're using [GraphQL Code Generator][graphql code generator] to properly type GraphQL queries. If you make a change to a query, run `yarn build:gen-graphql-types` to update the typings in the `typings` directory. +## Running Wallet app in ZeroSync mode + +By default, the mobile wallet app runs geth in ultralight sync mode where all the epoch headers are fetched. The default sync mode is defined in [packages/mobile/.env](https://github.com/celo-org/celo-monorepo/blob/master/packages/mobile/.env#L4) file. + +To run wallet in zero sync mode, where it would connect to the remote nodes and sign transactions in web3, change the default sync mode in the aforementioned file to -1. The mode has only been tested on Android and is hard-coded to be [crash](https://github.com/celo-org/celo-monorepo/blob/aeddeefbfb230db51d2ef76d50c5f882644a1cd3/packages/mobile/src/web3/contracts.ts#L73) on iOS. + ## Troubleshooting ### `Activity class {org.celo.mobile.staging/org.celo.mobile.MainActivity} does not exist.` From c3252245f346fdf2e679578fba79d25b02b6206f Mon Sep 17 00:00:00 2001 From: Audrey Penven Date: Wed, 16 Oct 2019 20:12:14 +0200 Subject: [PATCH 21/37] Allow most recently reporting oracle to report again (#1288) --- .../contracts/stability/SortedOracles.sol | 33 ++++--- .../protocol/test/stability/sortedoracles.ts | 97 ++++++++++++++++++- 2 files changed, 115 insertions(+), 15 deletions(-) diff --git a/packages/protocol/contracts/stability/SortedOracles.sol b/packages/protocol/contracts/stability/SortedOracles.sol index d62ece7160d..adc6c729eee 100644 --- a/packages/protocol/contracts/stability/SortedOracles.sol +++ b/packages/protocol/contracts/stability/SortedOracles.sol @@ -156,23 +156,28 @@ contract SortedOracles is ISortedOracles, Ownable, Initializable { uint256 value = numerator.mul(DENOMINATOR).div(denominator); if (rates[token].contains(msg.sender)) { rates[token].update(msg.sender, value, lesserKey, greaterKey); - timestamps[token].update( - msg.sender, - // solhint-disable-next-line not-rely-on-time - now, - timestamps[token].getHead(), - address(0) - ); + + // Rather than update the timestamp, we remove it and re-add it at the + // head of the list later. The reason for this is that we need to handle + // a few different cases: + // 1. This oracle is the only one to report so far. lesserKey = address(0) + // 2. Other oracles have reported since this one's last report. lesserKey = getHead() + // 3. Other oracles have reported, but the most recent is this one. + // lesserKey = key immediately after getHead() + // + // However, if we just remove this timestamp, timestamps[token].getHead() + // does the right thing in all cases. + timestamps[token].remove(msg.sender); } else { rates[token].insert(msg.sender, value, lesserKey, greaterKey); - timestamps[token].insert( - msg.sender, - // solhint-disable-next-line not-rely-on-time - now, - timestamps[token].getHead(), - address(0) - ); } + timestamps[token].insert( + msg.sender, + // solhint-disable-next-line not-rely-on-time + now, + timestamps[token].getHead(), + address(0) + ); emit OracleReported(token, msg.sender, now, value, DENOMINATOR); uint256 newMedian = rates[token].getMedianValue(); if (newMedian != originalMedian) { diff --git a/packages/protocol/test/stability/sortedoracles.ts b/packages/protocol/test/stability/sortedoracles.ts index 2b2230956fa..bac7cce8526 100644 --- a/packages/protocol/test/stability/sortedoracles.ts +++ b/packages/protocol/test/stability/sortedoracles.ts @@ -253,10 +253,18 @@ contract('SortedOracles', (accounts: string[]) => { }) describe('#report', () => { + function expectedNumeratorFromGiven( + givenNumerator: number | BigNumber, + givenDenominator: number | BigNumber + ): BigNumber { + return expectedDenominator.times(givenNumerator).div(givenDenominator) + } + const numerator = 10 const denominator = 1 const expectedDenominator = new BigNumber(2).pow(64) - const expectedNumerator = expectedDenominator.times(numerator).div(denominator) + const expectedNumerator = expectedNumeratorFromGiven(numerator, denominator) + beforeEach(async () => { await sortedOracles.addOracle(aToken, anOracle) }) @@ -330,5 +338,92 @@ contract('SortedOracles', (accounts: string[]) => { sortedOracles.report(aToken, numerator, denominator, NULL_ADDRESS, NULL_ADDRESS) ) }) + + describe('when there exists exactly one other report, made by this oracle', () => { + const newNumerator = 12 + const newExpectedNumerator = expectedNumeratorFromGiven(newNumerator, denominator) + + beforeEach(async () => { + await sortedOracles.report(aToken, numerator, denominator, NULL_ADDRESS, NULL_ADDRESS, { + from: anOracle, + }) + }) + it('should reset the median rate', async () => { + const [initialNumerator, initialDenominator] = await sortedOracles.medianRate(aToken) + assertEqualBN(initialNumerator, expectedNumerator) + assertEqualBN(initialDenominator, expectedDenominator) + + await sortedOracles.report(aToken, newNumerator, denominator, NULL_ADDRESS, NULL_ADDRESS, { + from: anOracle, + }) + + const [actualNumerator, actualDenominator] = await sortedOracles.medianRate(aToken) + assertEqualBN(actualNumerator, newExpectedNumerator) + assertEqualBN(actualDenominator, expectedDenominator) + }) + it('should not change the number of total reports', async () => { + const initialNumReports = await sortedOracles.numRates(aToken) + await sortedOracles.report(aToken, newNumerator, denominator, NULL_ADDRESS, NULL_ADDRESS, { + from: anOracle, + }) + + assertEqualBN(initialNumReports, await sortedOracles.numRates(aToken)) + }) + }) + + describe('when there are multiple reports, the most recent one done by this oracle', () => { + const anotherOracle = accounts[6] + const anOracleNumerator1 = 2 + const anOracleNumerator2 = 3 + const anotherOracleNumerator = 1 + + const anOracleExpectedNumerator1 = expectedNumeratorFromGiven(anOracleNumerator1, denominator) + const anOracleExpectedNumerator2 = expectedNumeratorFromGiven(anOracleNumerator2, denominator) + + const anotherOracleExpectedNumerator = expectedNumeratorFromGiven( + anotherOracleNumerator, + denominator + ) + + beforeEach(async () => { + sortedOracles.addOracle(aToken, anotherOracle) + await sortedOracles.report(aToken, anotherOracleNumerator, 1, NULL_ADDRESS, NULL_ADDRESS, { + from: anotherOracle, + }) + await timeTravel(5, web3) + await sortedOracles.report(aToken, anOracleNumerator1, 1, anotherOracle, NULL_ADDRESS, { + from: anOracle, + }) + await timeTravel(5, web3) + + // confirm the setup worked + const initialRates = await sortedOracles.getRates(aToken) + assertEqualBN(initialRates['1'][0], anOracleExpectedNumerator1) + assertEqualBN(initialRates['1'][1], anotherOracleExpectedNumerator) + }) + + it('updates the list of rates correctly', async () => { + await sortedOracles.report(aToken, anOracleNumerator2, 1, anotherOracle, NULL_ADDRESS, { + from: anOracle, + }) + const resultRates = await sortedOracles.getRates(aToken) + assertEqualBN(resultRates['1'][0], anOracleExpectedNumerator2) + assertEqualBN(resultRates['1'][1], anotherOracleExpectedNumerator) + }) + + it('updates the latest timestamp', async () => { + const initialTimestamps = await sortedOracles.getTimestamps(aToken) + await sortedOracles.report(aToken, anOracleNumerator2, 1, anotherOracle, NULL_ADDRESS, { + from: anOracle, + }) + const resultTimestamps = await sortedOracles.getTimestamps(aToken) + + // the second timestamp, belonging to anotherOracle should be unchanged + assertEqualBN(initialTimestamps['1']['1'], resultTimestamps['1']['1']) + + // the most recent timestamp, belonging to anOracle in both cases, should change + assert.isTrue(resultTimestamps['1']['0'].gt(initialTimestamps['1']['0'])) + }) + }) }) }) From 936d1e5f3bf3ff4584bbd28e5966a8f401e32b6b Mon Sep 17 00:00:00 2001 From: Anna K Date: Wed, 16 Oct 2019 11:57:29 -0700 Subject: [PATCH 22/37] [Celotool] Update blockchain-api deploy script to automatically update faucet address (#1347) --- .../src/cmds/deploy/initial/blockchain-api.ts | 10 ++++++++++ .../src/cmds/deploy/upgrade/blockchain-api.ts | 13 +++---------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/packages/celotool/src/cmds/deploy/initial/blockchain-api.ts b/packages/celotool/src/cmds/deploy/initial/blockchain-api.ts index e94e3db1313..85e79488e1d 100644 --- a/packages/celotool/src/cmds/deploy/initial/blockchain-api.ts +++ b/packages/celotool/src/cmds/deploy/initial/blockchain-api.ts @@ -1,7 +1,9 @@ import { switchToClusterFromEnv } from 'src/lib/cluster' import { envVar, fetchEnv } from 'src/lib/env-utils' +import { AccountType, getAddressFromEnv } from 'src/lib/generate_utils' import { execCmd } from 'src/lib/utils' import { UpgradeArgv } from '../../deploy/upgrade' + export const command = 'blockchain-api' export const describe = 'command for upgrading blockchain-api' @@ -12,6 +14,14 @@ type BlockchainApiArgv = UpgradeArgv export const handler = async (argv: BlockchainApiArgv) => { await switchToClusterFromEnv() const testnetProjectName = fetchEnv(envVar.TESTNET_PROJECT_NAME) + const newFaucetAddress = getAddressFromEnv(AccountType.VALIDATOR, 0) // We use the 0th validator as the faucet + console.info(`updating blockchain-api yaml file for env ${argv.celoEnv}`) + await execCmd( + `sed -i.bak 's/FAUCET_ADDRESS: .*$/FAUCET_ADDRESS: \"${newFaucetAddress}\"/g' ../blockchain-api/app.${ + argv.celoEnv + }.yaml` + ) + await execCmd(`rm ../blockchain-api/app.${argv.celoEnv}.yaml.bak`) // Removing temporary bak file console.info(`deploying blockchain-api for env ${argv.config} to ${testnetProjectName}`) await execCmd( `yarn --cwd ../blockchain-api run deploy -p ${testnetProjectName} -n ${argv.celoEnv}` diff --git a/packages/celotool/src/cmds/deploy/upgrade/blockchain-api.ts b/packages/celotool/src/cmds/deploy/upgrade/blockchain-api.ts index e94e3db1313..adc2f9b9d04 100644 --- a/packages/celotool/src/cmds/deploy/upgrade/blockchain-api.ts +++ b/packages/celotool/src/cmds/deploy/upgrade/blockchain-api.ts @@ -1,7 +1,6 @@ -import { switchToClusterFromEnv } from 'src/lib/cluster' -import { envVar, fetchEnv } from 'src/lib/env-utils' -import { execCmd } from 'src/lib/utils' +import { handler as deployInitialBlockchainApiHandler } from '../../deploy/initial/blockchain-api' import { UpgradeArgv } from '../../deploy/upgrade' + export const command = 'blockchain-api' export const describe = 'command for upgrading blockchain-api' @@ -10,11 +9,5 @@ export const describe = 'command for upgrading blockchain-api' type BlockchainApiArgv = UpgradeArgv export const handler = async (argv: BlockchainApiArgv) => { - await switchToClusterFromEnv() - const testnetProjectName = fetchEnv(envVar.TESTNET_PROJECT_NAME) - console.info(`deploying blockchain-api for env ${argv.config} to ${testnetProjectName}`) - await execCmd( - `yarn --cwd ../blockchain-api run deploy -p ${testnetProjectName} -n ${argv.celoEnv}` - ) - console.info(`blockchain-api deploy complete`) + await deployInitialBlockchainApiHandler(argv) } From 17ff68313a3cec5297d1d501ff9cd6e8a2cacfac Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Wed, 16 Oct 2019 13:23:35 -0700 Subject: [PATCH 23/37] Implement proof-of-stake changes (#1177) --- .circleci/config.yml | 8 +- .../src/e2e-tests/governance_tests.ts | 159 +-- packages/celotool/src/e2e-tests/utils.ts | 4 +- packages/cli/package.json | 9 +- .../cli/src/commands/account/isvalidator.ts | 6 +- .../commands/{ => election}/validatorset.ts | 6 +- packages/cli/src/commands/election/vote.ts | 31 + .../cli/src/commands/lockedgold/authorize.ts | 52 + .../cli/src/commands/lockedgold/delegate.ts | 49 - packages/cli/src/commands/lockedgold/list.ts | 54 - packages/cli/src/commands/lockedgold/lock.ts | 40 + .../cli/src/commands/lockedgold/lockup.ts | 51 - .../cli/src/commands/lockedgold/notify.ts | 30 - .../cli/src/commands/lockedgold/rewards.ts | 51 - packages/cli/src/commands/lockedgold/show.ts | 47 +- .../cli/src/commands/lockedgold/unlock.ts | 26 + .../cli/src/commands/lockedgold/withdraw.ts | 38 +- packages/cli/src/commands/validator/list.ts | 1 - .../cli/src/commands/validator/register.ts | 17 +- .../cli/src/commands/validatorgroup/list.ts | 4 +- .../cli/src/commands/validatorgroup/member.ts | 11 +- .../src/commands/validatorgroup/register.ts | 26 +- .../cli/src/commands/validatorgroup/vote.ts | 56 - packages/cli/src/utils/key_generator.test.ts | 2 +- packages/cli/src/utils/lockedgold.ts | 17 +- 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 +- packages/contractkit/src/wrappers/Election.ts | 218 +++ .../contractkit/src/wrappers/LockedGold.ts | 319 ++--- .../src/wrappers/Validators.test.ts | 29 +- .../contractkit/src/wrappers/Validators.ts | 241 +--- .../docs/command-line-interface/election.md | 39 + .../docs/command-line-interface/lockedgold.md | 114 +- .../docs/command-line-interface/validator.md | 11 +- .../command-line-interface/validatorgroup.md | 35 +- .../contracts/common/UsingRegistry.sol | 34 +- .../common/linkedlists/AddressLinkedList.sol | 1 + .../linkedlists/AddressSortedLinkedList.sol | 29 + .../common/linkedlists/LinkedList.sol | 1 + .../common/linkedlists/SortedLinkedList.sol | 17 +- .../contracts/governance/Election.sol | 868 ++++++++++++ .../contracts/governance/Governance.sol | 95 +- .../contracts/governance/LockedGold.sol | 877 ++++-------- .../contracts/governance/Proposals.sol | 18 +- .../contracts/governance/UsingLockedGold.sol | 99 -- .../contracts/governance/Validators.sol | 715 ++++------ .../governance/interfaces/IElection.sol | 5 +- .../governance/interfaces/IGovernance.sol | 3 +- .../governance/interfaces/ILockedGold.sol | 30 +- .../governance/interfaces/IValidators.sol | 8 +- .../governance/proxies/ElectionProxy.sol | 8 + .../governance/test/MockElection.sol | 32 + .../governance/test/MockLockedGold.sol | 123 +- .../governance/test/MockValidators.sol | 53 +- .../contracts/identity/Attestations.sol | 26 +- .../contracts/identity/test/MockRandom.sol | 24 +- .../protocol/contracts/stability/Exchange.sol | 11 +- .../protocol/contracts/stability/Reserve.sol | 4 +- 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 | 21 + .../migrations/{12_random.ts => 13_random.ts} | 0 ...{13_attestations.ts => 14_attestations.ts} | 0 .../migrations/{14_escrow.ts => 15_escrow.ts} | 0 packages/protocol/migrations/16_governance.ts | 1 + .../migrations/17_elect_validators.ts | 57 +- packages/protocol/migrationsConfig.js | 26 +- packages/protocol/scripts/build.ts | 13 +- .../addresssortedlinkedlistwithmedian.ts | 12 +- packages/protocol/test/common/integration.ts | 11 +- .../test/governance/bondeddeposits.ts | 1088 --------------- packages/protocol/test/governance/election.ts | 853 ++++++++++++ .../protocol/test/governance/governance.ts | 145 +- .../protocol/test/governance/lockedgold.ts | 486 +++++++ .../protocol/test/governance/validators.ts | 1187 +++++------------ .../protocol/test/identity/attestations.ts | 20 +- 80 files changed, 4307 insertions(+), 4562 deletions(-) rename packages/cli/src/commands/{ => election}/validatorset.ts (63%) 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/delegate.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/lockup.ts delete mode 100644 packages/cli/src/commands/lockedgold/notify.ts delete mode 100644 packages/cli/src/commands/lockedgold/rewards.ts create mode 100644 packages/cli/src/commands/lockedgold/unlock.ts delete mode 100644 packages/cli/src/commands/validatorgroup/vote.ts create mode 100644 packages/contractkit/src/wrappers/Election.ts create mode 100644 packages/docs/command-line-interface/election.md create mode 100644 packages/protocol/contracts/governance/Election.sol delete mode 100644 packages/protocol/contracts/governance/UsingLockedGold.sol create mode 100644 packages/protocol/contracts/governance/proxies/ElectionProxy.sol create mode 100644 packages/protocol/contracts/governance/test/MockElection.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%) 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/.circleci/config.yml b/.circleci/config.yml index 6b4acc76594..066d92bd043 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -243,7 +243,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 @@ -602,7 +602,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 @@ -642,7 +642,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 @@ -675,7 +675,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 diff --git a/packages/celotool/src/e2e-tests/governance_tests.ts b/packages/celotool/src/e2e-tests/governance_tests.ts index cccd2048795..a4a97f87cb0 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[]', @@ -234,7 +196,7 @@ describe('governance tests', () => { const context: any = getContext(gethConfig) let web3: any - let lockedGold: any + let election: any let validators: any let goldToken: any @@ -248,9 +210,9 @@ describe('governance tests', () => { 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')) + election = new web3.eth.Contract(electionAbi, await getContractAddress('ElectionProxy')) } const unlockAccount = async (address: string, theWeb3: any) => { @@ -258,27 +220,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 @@ -312,44 +264,14 @@ 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')) }) 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) }) @@ -371,6 +293,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') @@ -383,34 +306,33 @@ 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() - 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) }) @@ -418,7 +340,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}`, }) ) @@ -459,13 +381,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) }) @@ -473,7 +395,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}`, }) ) @@ -540,7 +462,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++) { @@ -554,35 +475,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[] = [] @@ -627,7 +519,6 @@ describe('governance tests', () => { b.plus(total) ) assert.isAtLeast(expectedGoldTotalSupply.toNumber(), goldGenesisSupply.toNumber()) - // assert.equal(goldTotalSupply.toString(), expectedGoldTotalSupply.toString()) }) }) diff --git a/packages/celotool/src/e2e-tests/utils.ts b/packages/celotool/src/e2e-tests/utils.ts index 71a563d783b..aae32ca88ac 100644 --- a/packages/celotool/src/e2e-tests/utils.ts +++ b/packages/celotool/src/e2e-tests/utils.ts @@ -321,9 +321,11 @@ export async function startGeth(gethBinaryPath: string, instance: GethInstanceCo export async function migrateContracts(validatorPrivateKeys: string[], to: number = 1000) { const migrationOverrides = { validators: { - minElectableValidators: '1', validatorKeys: validatorPrivateKeys.map(ensure0x), }, + election: { + minElectableValidators: '1', + }, } const args = [ '--cwd', diff --git a/packages/cli/package.json b/packages/cli/package.json index 21b253a6243..ac18f9c6249 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -80,20 +80,23 @@ "config": { "description": "Configure CLI options which persist across commands" }, + "election": { + "description": "View and manage validator elections" + }, "exchange": { "description": "Commands for interacting with the Exchange" }, "lockedgold": { - "description": "Manage Locked Gold to participate in governance and earn rewards" + "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" } }, "bin": "celocli", diff --git a/packages/cli/src/commands/account/isvalidator.ts b/packages/cli/src/commands/account/isvalidator.ts index 9a6738df870..316514dfa8e 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.getElection() + 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/validatorset.ts b/packages/cli/src/commands/election/validatorset.ts similarity index 63% rename from packages/cli/src/commands/validatorset.ts rename to packages/cli/src/commands/election/validatorset.ts index 37ee81e5d76..74f56f97874 100644 --- a/packages/cli/src/commands/validatorset.ts +++ b/packages/cli/src/commands/election/validatorset.ts @@ -1,4 +1,4 @@ -import { BaseCommand } from '../base' +import { BaseCommand } from '../../base' export default class ValidatorSet extends BaseCommand { static description = 'Outputs the current validator set' @@ -10,8 +10,8 @@ export default class ValidatorSet extends BaseCommand { static examples = ['validatorset'] async run() { - const validators = await this.kit.contracts.getValidators() - const validatorSet = await validators.getValidatorSetAddresses() + 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..cf957d74755 --- /dev/null +++ b/packages/cli/src/commands/election/vote.ts @@ -0,0 +1,31 @@ +import { flags } from '@oclif/command' +import BigNumber from 'bignumber.js' +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..aae72011553 --- /dev/null +++ b/packages/cli/src/commands/lockedgold/authorize.ts @@ -0,0 +1,52 @@ +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) + } else { + this.error(`Invalid role provided`) + return + } + await displaySendTx('authorizeTx', tx) + } +} 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/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..d9f59f2bcaf --- /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 }), + value: flags.string({ ...LockedGoldArgs.valueArg, required: true }), + } + + static args = [] + + static examples = [ + 'lock --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --value 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 value = new BigNumber(res.flags.value) + + if (!value.gt(new BigNumber(0))) { + failWith(`Provided value must be greater than zero => [${value.toString()}]`) + } + + const tx = lockedGold.lock() + await displaySendTx('lock', tx, { value: value.toString() }) + } +} 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/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 2848cdee657..9c9c90c089c 100644 --- a/packages/cli/src/commands/lockedgold/show.ts +++ b/packages/cli/src/commands/lockedgold/show.ts @@ -1,60 +1,23 @@ -import { flags } from '@oclif/command' -import BigNumber from 'bignumber.js' -import chalk from 'chalk' -import { cli } from 'cli-ux' import { BaseCommand } from '../../base' +import { printValueMapRecursive } 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 { args } = this.parse(Show) 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 - } - - cli.action.stop() - - cli.log(chalk.bold.yellow('Gold Locked \t') + value.toString()) - cli.log(chalk.bold.red('Account Weight Contributed \t') + contributingWeight.toString()) + printValueMapRecursive(await lockedGold.getAccountSummary(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..1b04e9980c1 --- /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 }), + value: flags.string({ ...LockedGoldArgs.valueArg, required: true }), + } + + static args = [] + + 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.value)) + } +} diff --git a/packages/cli/src/commands/lockedgold/withdraw.ts b/packages/cli/src/commands/lockedgold/withdraw.ts index e34fa24b954..06383dea399 100644 --- a/packages/cli/src/commands/lockedgold/withdraw.ts +++ b/packages/cli/src/commands/lockedgold/withdraw.ts @@ -1,25 +1,49 @@ 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 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 --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() - await displaySendTx('withdrawCommitment', lockedgold.withdrawCommitment(args.availabilityTime)) + const currentTime = Math.round(new Date().getTime() / 1000) + + while (true) { + let madeWithdrawal = false + const pendingWithdrawals = await lockedgold.getPendingWithdrawals(flags.from) + 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` + ) + await displaySendTx('withdraw', lockedgold.withdraw(i)) + madeWithdrawal = true + break + } + } + if (!madeWithdrawal) { + break + } + } + 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) + .toString()} seconds.` + ) + } } } 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..7743ab63f9a 100644 --- a/packages/cli/src/commands/validatorgroup/list.ts +++ b/packages/cli/src/commands/validatorgroup/list.ts @@ -16,15 +16,13 @@ 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() }, + 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 be60216c69a..86f8ab0df03 100644 --- a/packages/cli/src/commands/validatorgroup/member.ts +++ b/packages/cli/src/commands/validatorgroup/member.ts @@ -4,8 +4,8 @@ import { BaseCommand } from '../../base' import { displaySendTx } from '../../utils/cli' import { Args, Flags } from '../../utils/command' -export default class ValidatorGroupRegister extends BaseCommand { - static description = 'Manage members of a Validator Group' +export default class ValidatorGroupMembers extends BaseCommand { + static description = 'Add or remove members from a Validator Group' static flags = { ...BaseCommand.flags, @@ -33,7 +33,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 || res.flags.reorder)) { this.error(`Specify action: --accept, --remove or --reorder`) @@ -42,9 +42,14 @@ 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)).isEqualTo(1)) { + const tx = await election.markGroupEligible(res.flags.from) + await displaySendTx('markGroupEligible', tx) + } } else if (res.flags.remove) { await displaySendTx( 'removeMember', diff --git a/packages/cli/src/commands/validatorgroup/register.ts b/packages/cli/src/commands/validatorgroup/register.ts index ca513c3ca78..365bc9cf7f6 100644 --- a/packages/cli/src/commands/validatorgroup/register.ts +++ b/packages/cli/src/commands/validatorgroup/register.ts @@ -1,4 +1,5 @@ import { flags } from '@oclif/command' +import BigNumber from 'bignumber.js' import { BaseCommand } from '../../base' import { displaySendTx } from '../../utils/cli' import { Flags } from '../../utils/command' @@ -9,34 +10,25 @@ 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) this.kit.defaultAccount = res.flags.from const validators = await this.kit.contracts.getValidators() - - await displaySendTx( - 'registerValidatorGroup', - validators.registerValidatorGroup( - res.flags.id, - res.flags.name, - res.flags.url, - res.flags.noticePeriod - ) + const tx = await validators.registerValidatorGroup( + res.flags.name, + res.flags.url, + new BigNumber(res.flags.commission) ) + await displaySendTx('registerValidatorGroup', tx) } } 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/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/cli/src/utils/lockedgold.ts b/packages/cli/src/utils/lockedgold.ts index c715315334e..9e9aee7645e 100644 --- a/packages/cli/src/utils/lockedgold.ts +++ b/packages/cli/src/utils/lockedgold.ts @@ -1,15 +1,10 @@ export const LockedGoldArgs = { - noticePeriodArg: { - name: 'noticePeriod', - description: - 'duration (seconds) from notice to withdrawable; doubles as ID of a Locked Gold commitment; ', + pendingWithdrawalIndexArg: { + name: 'pendingWithdrawalIndex', + description: 'index of pending withdrawal whose unlocking period has passed', }, - availabilityTimeArg: { - name: 'availabilityTime', - description: 'unix timestamp at which withdrawable; doubles as ID of a notified commitment', - }, - 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/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 18f163bd4f0..c93a5cf7713 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, @@ -64,8 +67,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) @@ -85,6 +88,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 1c8b6bdecf4..7ca7df24dff 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' /** * Creates a new web3 instance diff --git a/packages/contractkit/src/kit.ts b/packages/contractkit/src/kit.ts index ffdc769da74..a997ff88e06 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' const debug = debugFactory('kit:kit') @@ -36,6 +37,7 @@ export function newKitFromWeb3(web3: Web3) { } export interface NetworkConfig { + election: ElectionConfig exchange: ExchangeConfig attestations: AttestationsConfig governance: GovernanceConfig @@ -44,7 +46,7 @@ export interface NetworkConfig { gasPriceMinimum: GasPriceMinimumConfig reserve: ReserveConfig stableToken: StableTokenConfig - validators: ValidatorConfig + validators: ValidatorsConfig } export interface KitOptions { @@ -78,6 +80,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(), @@ -89,25 +92,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 d6475497aa2..35d705170eb 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, @@ -57,6 +59,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/Election.ts b/packages/contractkit/src/wrappers/Election.ts new file mode 100644 index 00000000000..fbf35b6c87d --- /dev/null +++ b/packages/contractkit/src/wrappers/Election.ts @@ -0,0 +1,218 @@ +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, + identity, + proxyCall, + proxySend, + toBigNumber, + toNumber, + toTransactionObject, + tupleParser, +} from './BaseWrapper' + +export interface Validator { + address: Address + name: string + url: string + publicKey: string + affiliation: Address | null +} + +export interface ValidatorGroup { + address: Address + name: string + url: string + members: Address[] +} + +export interface ValidatorGroupVote { + address: Address + votes: BigNumber + eligible: boolean +} + +export interface ElectableValidators { + min: BigNumber + max: BigNumber +} + +export interface ElectionConfig { + electableValidators: ElectableValidators + 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 and maximum number of validators that can be elected. + * @returns The minimum and maximum number of validators that can be elected. + */ + 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. + */ + electabilityThreshold = proxyCall( + this.contract.methods.getElectabilityThreshold, + undefined, + toBigNumber + ) + validatorAddressFromCurrentSet: (index: number) => Promise
= proxyCall( + this.contract.methods.validatorAddressFromCurrentSet, + tupleParser(identity) + ) + + numberValidatorsInCurrentSet = proxyCall( + this.contract.methods.numberValidatorsInCurrentSet, + undefined, + 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 + ) + + /** + * Returns current configuration parameters. + */ + async getConfig(): Promise { + const res = await Promise.all([ + this.electableValidators(), + this.electabilityThreshold(), + this.contract.methods.maxNumGroupsVotedFor().call(), + ]) + return { + electableValidators: res[0], + electabilityThreshold: res[1], + maxNumGroupsVotedFor: toBigNumber(res[2]), + } + } + + /** + * Returns the addresses in the current validator set. + */ + 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) + } + + /** + * 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( + (g) => g.address + ) + const validatorGroupVotes = await Promise.all( + validatorGroupAddresses.map((g) => this.contract.methods.getTotalVotesForGroup(g).call()) + ) + const validatorGroupEligible = await Promise.all( + validatorGroupAddresses.map((g) => this.contract.methods.getGroupEligibility(g).call()) + ) + return validatorGroupAddresses.map((a, i) => ({ + address: a, + votes: toBigNumber(validatorGroupVotes[i]), + eligible: validatorGroupEligible[i], + })) + } + + /** + * 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 kit.defaultAccount`) + } + + const value = toBigNumber( + await this.contract.methods.getTotalVotesForGroup(validatorGroup).call() + ) + const { lesser, greater } = await this.findLesserAndGreaterAfterVote(validatorGroup, value) + + return toTransactionObject(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 kit.defaultAccount`) + } + + const { lesser, greater } = await this.findLesserAndGreaterAfterVote(validatorGroup, value) + + return toTransactionObject( + this.kit, + this.contract.methods.vote(validatorGroup, value.toString(), lesser, greater) + ) + } + + async findLesserAndGreaterAfterVote( + votedGroup: Address, + voteWeight: BigNumber + ): Promise<{ lesser: Address; greater: Address }> { + const currentVotes = await this.getEligibleValidatorGroupsVotes() + + const selectedGroup = currentVotes.find((votes) => eqAddress(votes.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((votes) => eqAddress(votes.address, votedGroup)) + + return { + lesser: newIdx === 0 ? NULL_ADDRESS : currentVotes[newIdx - 1].address, + greater: newIdx === currentVotes.length - 1 ? NULL_ADDRESS : currentVotes[newIdx + 1].address, + } + } +} diff --git a/packages/contractkit/src/wrappers/LockedGold.ts b/packages/contractkit/src/wrappers/LockedGold.ts index c87519a965c..bc3833f090c 100644 --- a/packages/contractkit/src/wrappers/LockedGold.ts +++ b/packages/contractkit/src/wrappers/LockedGold.ts @@ -1,16 +1,19 @@ +import { eqAddress } from '@celo/utils/lib/address' 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 { BaseWrapper, CeloTransactionObject, + NumberLike, + parseNumber, proxyCall, proxySend, toBigNumber, toTransactionObject, + tupleParser, } from '../wrappers/BaseWrapper' export interface VotingDetails { @@ -20,28 +23,25 @@ export interface VotingDetails { weight: BigNumber } -interface Commitment { - time: BigNumber - value: BigNumber -} - -export interface Commitments { - locked: Commitment[] - notified: Commitment[] - total: { - gold: BigNumber - weight: BigNumber +interface AccountSummary { + lockedGold: { + total: BigNumber + nonvoting: BigNumber + } + authorizations: { + voter: null | string + validator: null | string } + pendingWithdrawals: PendingWithdrawal[] } -export enum Roles { - Validating = '0', - Voting = '1', - Rewards = '2', +interface PendingWithdrawal { + time: BigNumber + value: BigNumber } export interface LockedGoldConfig { - maxNoticePeriod: BigNumber + unlockingPeriod: BigNumber } /** @@ -49,239 +49,155 @@ export interface LockedGoldConfig { */ export class LockedGoldWrapper extends BaseWrapper { /** - * 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 CeloTransactionObject + * Unlocks gold that becomes withdrawable after the unlocking period. + * @param value The amount of gold to unlock. */ - notifyCommitment: ( - value: string | number, - noticePeriod: string | number - ) => CeloTransactionObject = proxySend(this.kit, this.contract.methods.notifyCommitment) - + unlock: (value: NumberLike) => CeloTransactionObject = proxySend( + this.kit, + this.contract.methods.unlock, + tupleParser(parseNumber) + ) /** * Creates an account. - * @return CeloTransactionObject */ - createAccount: () => CeloTransactionObject = proxySend( + 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.createAccount + this.contract.methods.withdraw ) - /** - * Withdraws a notified commitment after the duration of the notice period. - * @param availabilityTime The availability time of the notified commitment. - * @return CeloTransactionObject + * @notice Locks gold to be used for voting. */ - withdrawCommitment: ( - availabilityTime: string | number - ) => CeloTransactionObject = proxySend(this.kit, this.contract.methods.withdrawCommitment) - + lock = proxySend(this.kit, this.contract.methods.lock) /** - * Redeems rewards accrued since the last redemption for the specified account. - * @return CeloTransactionObject + * Relocks gold that has been unlocked but not withdrawn. + * @param index The index of the pending withdrawal to relock. */ - redeemRewards: () => CeloTransactionObject = proxySend( + relock: (index: number) => CeloTransactionObject = proxySend( this.kit, - this.contract.methods.redeemRewards + this.contract.methods.relock ) /** - * Adds a Locked Gold commitment to `msg.sender`'s account. - * @param noticePeriod The notice period for the commitment. - * @return CeloTransactionObject + * Returns the total amount of locked gold for an account. + * @param account The account. + * @return The total amount of locked gold for an account. */ - newCommitment: (noticePeriod: string | number) => CeloTransactionObject = proxySend( - this.kit, - this.contract.methods.newCommitment + getAccountTotalLockedGold = proxyCall( + this.contract.methods.getAccountTotalLockedGold, + undefined, + toBigNumber ) - /** - * 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 CeloTransactionObject + * 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. */ - extendCommitment: ( - value: string | number, - availabilityTime: string | number - ) => CeloTransactionObject = proxySend(this.kit, this.contract.methods.extendCommitment) - + getAccountNonvotingLockedGold = proxyCall( + this.contract.methods.getAccountNonvotingLockedGold, + undefined, + toBigNumber + ) /** - * Returns whether or not a specified account is voting. + * Returns the voter for the specified account. * @param account The address of the account. - * @return Whether or not the account is voting. + * @return The address with which the account can vote. */ - isVoting = proxyCall(this.contract.methods.isVoting) - + 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 + ) /** * Check if an account already exists. * @param account The address of the account * @return Returns `true` if account exists. Returns `false` otherwise. - * In particular it will return `false` if a delegate with given address exists. - */ - isAccount = proxyCall(this.contract.methods.isAccount) - - /** - * Check if a delegate already exists. - * @param account The address of the delegate - * @return Returns `true` if delegate exists. Returns `false` otherwise. - */ - isDelegate = proxyCall(this.contract.methods.isDelegate) - - /** - * Query maximum notice period. - * @returns Current maximum notice period. - */ - maxNoticePeriod = proxyCall(this.contract.methods.maxNoticePeriod, undefined, toBigNumber) - - /** - * Returns the weight of a specified account. - * @param _account The address of the account. - * @return The weight of the specified account. */ - 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 - ) - + isAccount: (account: string) => Promise = proxyCall(this.contract.methods.isAccount) /** * Returns current configuration parameters. */ - async getConfig(): Promise { return { - maxNoticePeriod: await this.maxNoticePeriod(), + unlockingPeriod: toBigNumber(await this.contract.methods.unlockingPeriod().call()), } } - /** - * Get voting details for an address - * @param accountOrVoterAddress Accout or Voter address - */ - async getVotingDetails(accountOrVoterAddress: Address): Promise { - const accountAddress = await this.contract.methods - .getAccountFromDelegateAndRole(accountOrVoterAddress, Roles.Voting) - .call() - + async getAccountSummary(account: string): Promise { + 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 { - accountAddress, - voterAddress: accountOrVoterAddress, - weight: await this.getAccountWeight(accountAddress), + lockedGold: { + total, + nonvoting, + }, + authorizations: { + voter: eqAddress(voter, account) ? null : voter, + validator: eqAddress(validator, account) ? null : validator, + }, + pendingWithdrawals, } } - async getLockedCommitmentValue(account: Address, noticePeriod: string): Promise { - const commitment = await this.contract.methods.getLockedCommitment(account, noticePeriod).call() - return this.getValueFromCommitment(commitment) - } - - async getLockedCommitments(account: Address): Promise { - return this.zipAccountTimesAndValuesToCommitments( - account, - this.contract.methods.getNoticePeriods, - this.getLockedCommitmentValue.bind(this) - ) - } - - async getNotifiedCommitmentValue(account: Address, availTime: string): Promise { - const commitment = await this.contract.methods.getNotifiedCommitment(account, availTime).call() - return this.getValueFromCommitment(commitment) - } - - async getNotifiedCommitments(account: Address): Promise { - return this.zipAccountTimesAndValuesToCommitments( - account, - this.contract.methods.getAvailabilityTimes, - this.getNotifiedCommitmentValue.bind(this) - ) - } - /** - * Get commitments for an Account - * @param account Account address + * Authorize voting on behalf of this account to another address. + * @param account Address of the active account. + * @param voter Address to be used for voting. + * @return A CeloTransactionObject */ - async getCommitments(account: Address): 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) + async authorizeVoter(account: Address, voter: Address): Promise> { + const sig = await this.getParsedSignatureOfAddress(account, voter) + // TODO(asa): Pass default tx "from" argument. + return toTransactionObject( + this.kit, + this.contract.methods.authorizeVoter(voter, sig.v, sig.r, sig.s) ) - const gold = notified.reduce((acc, commitment) => acc.plus(commitment.value), totalLocked) - - return { - locked, - notified, - total: { weight, gold }, - } } /** - * Delegate a Role 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 role one of Roles Enum ("Validating", "Voting", "Rewards") + * @param voter Address to be used for validating. * @return A CeloTransactionObject */ - async delegateRoleTx( + async authorizeValidator( account: Address, - delegate: Address, - role: Roles + validator: Address ): Promise> { - const sig = await this.getParsedSignatureOfAddress(account, delegate) + const sig = await this.getParsedSignatureOfAddress(account, validator) return toTransactionObject( this.kit, - this.contract.methods.delegateRole(role, delegate, sig.v, sig.r, sig.s) + this.contract.methods.authorizeValidator(validator, sig.v, sig.r, sig.s) ) } /** - * Delegate a Rewards to another account. - * @param account Address of the active account. - * @param delegate Address of the delegate - * @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 + * 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 delegateValidating( - account: Address, - delegate: Address - ): Promise> { - return this.delegateRoleTx(account, delegate, Roles.Validating) - } - - 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( + (time, value) => + // tslint:disable-next-line: no-object-literal-type-assertion + ({ time: toBigNumber(time), value: toBigNumber(value) } as PendingWithdrawal), + withdrawals[1], + withdrawals[0] + ) } private async getParsedSignatureOfAddress(address: Address, signer: string) { @@ -293,19 +209,4 @@ export class LockedGoldWrapper extends BaseWrapper { v: Web3.utils.hexToNumber(signature.slice(128, 130)) + 27, } } - - private async zipAccountTimesAndValuesToCommitments( - account: Address, - 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.test.ts b/packages/contractkit/src/wrappers/Validators.test.ts index 3d5c831e6e3..1d54ed80bf8 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('10', 'ether') // 10 gold // A random 64 byte hex string. const publicKey = @@ -28,16 +28,11 @@ testWithGanache('Validators Wrapper', (web3) => { let validators: ValidatorsWrapper let lockedGold: LockedGoldWrapper - const registerAccountWithCommitment = async (account: string) => { - // console.log('isAccount', ) - // console.log('isDelegate', await lockedGold.isDelegate(account)) - + const registerAccountWithLockedGold = async (account: string) => { 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 () => { @@ -47,23 +42,23 @@ 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 registerAccountWithLockedGold(groupAccount) + await (await validators.registerValidatorGroup( + 'The Group', + 'thegroup.com', + new BigNumber(0.1) + )).sendAndWaitForReceipt({ from: groupAccount }) } const setupValidator = async (validatorAccount: string) => { - await registerAccountWithCommitment(validatorAccount) + await registerAccountWithLockedGold(validatorAccount) // 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 2ebe2859f61..754bedcadc2 100644 --- a/packages/contractkit/src/wrappers/Validators.ts +++ b/packages/contractkit/src/wrappers/Validators.ts @@ -1,5 +1,4 @@ -import { eqAddress } from '@celo/utils/lib/address' -import { zip } from '@celo/utils/lib/collections' +import { fromFixed, toFixed } from '@celo/utils/lib/fixidity' import BigNumber from 'bignumber.js' import { Address, NULL_ADDRESS } from '../base' import { Validators } from '../generated/types/Validators' @@ -9,13 +8,11 @@ import { proxyCall, proxySend, toBigNumber, - toNumber, toTransactionObject, } from './BaseWrapper' export interface Validator { address: Address - id: string name: string url: string publicKey: string @@ -24,27 +21,26 @@ export interface Validator { export interface ValidatorGroup { address: Address - id: string name: string url: string members: Address[] + commission: BigNumber } -export interface ValidatorGroupVote { - address: Address - votes: BigNumber +export interface BalanceRequirements { + 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 { + balanceRequirements: BalanceRequirements + deregistrationLockups: DeregistrationLockups + maxGroupSize: BigNumber } /** @@ -54,68 +50,53 @@ export class ValidatorsWrapper extends BaseWrapper { affiliate = proxySend(this.kit, this.contract.methods.affiliate) deaffiliate = proxySend(this.kit, this.contract.methods.deaffiliate) 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. - */ - maxElectableValidators = proxyCall( - this.contract.methods.maxElectableValidators, - undefined, - toBigNumber - ) + async registerValidatorGroup( + name: string, + url: string, + commission: BigNumber + ): Promise> { + return toTransactionObject( + this.kit, + this.contract.methods.registerValidatorGroup(name, url, toFixed(commission).toFixed()) + ) + } /** - * Returns the current election threshold. - * @returns Election threshold. + * Returns the current registration requirements. + * @returns Group and validator registration requirements. */ - 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 getBalanceRequirements(): Promise { + const res = await this.contract.methods.getBalanceRequirements().call() + return { + group: toBigNumber(res[0]), + validator: toBigNumber(res[1]), + } + } /** - * Returns the current registrations requirements. - * @returns Minimum deposit and notice period. + * Returns the lockup periods after deregistering groups and validators. + * @return The lockup periods after deregistering groups and validators. */ - 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.getBalanceRequirements(), + this.getDeregistrationLockups(), + this.contract.methods.maxGroupSize().call(), ]) return { - minElectableValidators: res[0], - maxElectableValidators: res[1], - electionThreshold: res[2], - registrationRequirement: res[3], + balanceRequirements: res[0], + deregistrationLockups: res[1], + maxGroupSize: toBigNumber(res[2]), } } @@ -125,44 +106,23 @@ 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) - } + getGroupNumMembers: (group: Address) => Promise = proxyCall( + this.contract.methods.getGroupNumMembers, + undefined, + toBigNumber + ) 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], } } - /** - * 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. @@ -177,15 +137,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) @@ -226,88 +177,12 @@ 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 toTransactionObject(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 toTransactionObject( - 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, + address, + name: res[0], + url: res[1], + members: res[2], + commission: fromFixed(new BigNumber(res[3])), } } } 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)_ 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 95186d3bfca..291c735fc92 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 @@ -52,18 +52,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)_ @@ -84,25 +79,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)_ diff --git a/packages/protocol/contracts/common/UsingRegistry.sol b/packages/protocol/contracts/common/UsingRegistry.sol index 378e43ea5ca..a0dbf5ce45b 100644 --- a/packages/protocol/contracts/common/UsingRegistry.sol +++ b/packages/protocol/contracts/common/UsingRegistry.sol @@ -2,8 +2,15 @@ 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"; +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. @@ -15,12 +22,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 +37,10 @@ contract UsingRegistry is Ownable { IRegistry public registry; + modifier onlyRegisteredContract(bytes32 identifierHash) { + 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. @@ -37,4 +49,24 @@ contract UsingRegistry is Ownable { registry = IRegistry(registryAddress); emit RegistrySet(registryAddress); } + + function getElection() internal view returns (IElection) { + 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)); + } + + 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/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/AddressSortedLinkedList.sol b/packages/protocol/contracts/common/linkedlists/AddressSortedLinkedList.sol index 3949b873124..61ea7d0fdef 100644 --- a/packages/protocol/contracts/common/linkedlists/AddressSortedLinkedList.sol +++ b/packages/protocol/contracts/common/linkedlists/AddressSortedLinkedList.sol @@ -105,4 +105,33 @@ 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; + } + + /** + * @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/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 80a057324dc..9489f00802c 100644 --- a/packages/protocol/contracts/common/linkedlists/SortedLinkedList.sol +++ b/packages/protocol/contracts/common/linkedlists/SortedLinkedList.sol @@ -144,6 +144,17 @@ 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. + * @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); + } + + // 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. @@ -173,9 +184,13 @@ 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 ( + greaterKey != bytes32(0) && isValueBetween(list, value, list.list.elements[greaterKey].previousKey, greaterKey) ) { return (list.list.elements[greaterKey].previousKey, greaterKey); diff --git a/packages/protocol/contracts/governance/Election.sol b/packages/protocol/contracts/governance/Election.sol new file mode 100644 index 00000000000..807d1a74f69 --- /dev/null +++ b/packages/protocol/contracts/governance/Election.sol @@ -0,0 +1,868 @@ +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 "./interfaces/IElection.sol"; +import "../common/Initializable.sol"; +import "../common/FixidityLib.sol"; +import "../common/linkedlists/AddressSortedLinkedList.sol"; +import "../common/UsingRegistry.sol"; + + +contract Election is IElection, Ownable, ReentrancyGuard, Initializable, UsingRegistry { + + using AddressSortedLinkedList for SortedLinkedList.List; + using FixidityLib for FixidityLib.Fraction; + using SafeMath for uint256; + + struct PendingVote { + // 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 => PendingVote) 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 { + // 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 { + // The total number of active votes cast across all groups. + uint256 total; + mapping(address => GroupActiveVotes) forGroup; + } + + struct TotalVotes { + // 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; + } + + struct Votes { + PendingVotes pending; + ActiveVotes active; + TotalVotes total; + // Maps an account to the list of groups it's voting for. + mapping(address => address[]) groupsVotedFor; + } + + struct ElectableValidators { + uint256 min; + uint256 max; + } + + Votes private votes; + // 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 ElectableValidatorsSet( + uint256 min, + uint256 max + ); + + event MaxNumGroupsVotedForSet( + uint256 maxNumGroupsVotedFor + ); + + event ElectabilityThresholdSet( + uint256 electabilityThreshold + ); + + event ValidatorGroupMarkedEligible( + address group + ); + + event ValidatorGroupMarkedIneligible( + address group + ); + + event ValidatorGroupVoteCast( + address indexed account, + address indexed group, + uint256 value + ); + + event ValidatorGroupVoteActivated( + address indexed account, + address indexed group, + uint256 value + ); + + event ValidatorGroupVoteRevoked( + address indexed account, + address indexed group, + uint256 value + ); + + /** + * @notice Initializes critical variables. + * @param registryAddress The address of the registry contract. + * @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. + * @dev Should be called only once. + */ + function initialize( + address registryAddress, + uint256 minElectableValidators, + uint256 maxElectableValidators, + uint256 _maxNumGroupsVotedFor, + uint256 _electabilityThreshold + ) + external + initializer + { + _transferOwnership(msg.sender); + setRegistry(registryAddress); + _setElectableValidators(minElectableValidators, maxElectableValidators); + _setMaxNumGroupsVotedFor(_maxNumGroupsVotedFor); + _setElectabilityThreshold(_electabilityThreshold); + } + + /** + * @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) external onlyOwner returns (bool) { + 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); + } + + /** + * @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. + * @return True upon success. + */ + function setMaxNumGroupsVotedFor( + uint256 _maxNumGroupsVotedFor + ) + external + 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); + return true; + } + + /** + * @notice Sets the electability threshold. + * @param threshold Electability threshold as unwrapped Fraction. + * @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()), + "Electability threshold must be lower than 100%" + ); + emit ElectabilityThresholdSet(threshold); + return true; + } + + /** + * @notice Gets the election threshold. + * @return Threshold value as unwrapped fraction. + */ + function getElectabilityThreshold() external view returns (uint256) { + return electabilityThreshold.unwrap(); + } + + /** + * @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(votes.total.eligible.contains(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); + getLockedGold().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. + * @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.forGroup[group].byAccount[account].value; + require(value > 0); + decrementPendingVotes(group, account, value); + incrementActiveVotes(group, account, value); + emit ValidatorGroupVoteActivated(account, group, value); + } + + /** + * @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 = getLockedGold().getAccountFromVoter(msg.sender); + require(0 < value && value <= getPendingVotesForGroupByAccount(group, account)); + decrementPendingVotes(group, account, value); + decrementTotalVotes(group, value, lesser, greater); + getLockedGold().incrementNonvotingAccountBalance(account, value); + if (getTotalVotesForGroupByAccount(group, account) == 0) { + deleteElement(votes.groupsVotedFor[account], group, index); + } + emit ValidatorGroupVoteRevoked(account, group, value); + 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) + { + // TODO(asa): Dedup with revokePending. + require(group != address(0)); + address account = getLockedGold().getAccountFromVoter(msg.sender); + require(0 < value && value <= getActiveVotesForGroupByAccount(group, account)); + decrementActiveVotes(group, account, value); + decrementTotalVotes(group, value, lesser, greater); + getLockedGold().incrementNonvotingAccountBalance(account, value); + if (getTotalVotesForGroupByAccount(group, account) == 0) { + deleteElement(votes.groupsVotedFor[account], group, index); + } + emit ValidatorGroupVoteRevoked(account, group, value); + 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]; + for (uint256 i = 0; i < groups.length; i = i.add(1)) { + total = total.add(getTotalVotesForGroupByAccount(groups[i], account)); + } + return total; + } + + /** + * @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 getPendingVotesForGroupByAccount( + address group, + address account + ) + public + view + returns (uint256) + { + return votes.pending.forGroup[group].byAccount[account].value; + } + + /** + * @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 getActiveVotesForGroupByAccount( + address group, + address account + ) + public + view + returns (uint256) + { + GroupActiveVotes storage groupActiveVotes = votes.active.forGroup[group]; + uint256 numerator = groupActiveVotes.unitsByAccount[account].mul(groupActiveVotes.total); + if (numerator == 0) { + return 0; + } + uint256 denominator = groupActiveVotes.totalUnits; + return numerator.div(denominator); + } + + /** + * @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 getTotalVotesForGroupByAccount( + address group, + address account + ) + public + view + returns (uint256) + { + uint256 pending = getPendingVotesForGroupByAccount(group, account); + uint256 active = getActiveVotesForGroupByAccount(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 getTotalVotesForGroup(address group) public view returns (uint256) { + 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); + } + + /** + * @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 + { + uint256 newVoteTotal = votes.total.eligible.getValue(group).add(value); + votes.total.eligible.update(group, newVoteTotal, lesser, greater); + } + + /** + * @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.total.eligible.contains(group)) { + uint256 newVoteTotal = votes.total.eligible.getValue(group).sub(value); + votes.total.eligible.update(group, newVoteTotal, lesser, greater); + } + } + + /** + * @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); + } + + /** + * @notice 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. + */ + function markGroupEligible( + 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 = getTotalVotesForGroup(group); + votes.total.eligible.insert(group, value, lesser, greater); + emit ValidatorGroupMarkedEligible(group); + return true; + } + + /** + * @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.total = pending.total.add(value); + + GroupPendingVotes storage groupPending = pending.forGroup[group]; + groupPending.total = groupPending.total.add(value); + + PendingVote storage pendingVote = groupPending.byAccount[account]; + pendingVote.value = pendingVote.value.add(value); + pendingVote.blockNumber = block.number; + } + + /** + * @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; + pending.total = pending.total.sub(value); + + GroupPendingVotes storage groupPending = pending.forGroup[group]; + groupPending.total = groupPending.total.sub(value); + + PendingVote storage pendingVote = groupPending.byAccount[account]; + pendingVote.value = pendingVote.value.sub(value); + if (pendingVote.value == 0) { + pendingVote.blockNumber = 0; + } + } + + /** + * @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 { + ActiveVotes storage active = votes.active; + 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); + } + + /** + * @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 { + ActiveVotes storage active = votes.active; + 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 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) { + if (votes.active.forGroup[group].total == 0) { + return value; + } else { + return value.mul(votes.active.forGroup[group].totalUnits).div( + votes.active.forGroup[group].total + ); + } + } + + /** + * @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]; + } + + /** + * @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 { + // 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.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) + * @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); + 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) + * @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( + getLockedGold().getTotalLockedGold() + ); + uint256 denominator = Math.min( + electableValidators.max, + 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() public view returns (uint256) { + return votes.active.total.add(votes.pending.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(); + } + + /** + * @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 getTotalVotesForEligibleValidatorGroups() + external + view + returns (address[] memory, uint256[] memory) + { + 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. + * @return The list of elected validators. + * @dev See https://en.wikipedia.org/wiki/D%27Hondt_method#Allocation for more information. + */ + function electValidators() external view returns (address[] memory) { + // Only members of these validator groups are eligible for election. + uint256 maxNumElectionGroups = Math.min( + electableValidators.max, + votes.total.eligible.list.numElements + ); + // TODO(asa): Filter by > requiredVotes + 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; + // Assign a number of seats to each validator group. + while (totalNumMembersElected < electableValidators.max) { + uint256 groupIndex = 0; + bool memberElected = false; + (groupIndex, memberElected) = dHondt(electionGroups, numMembers, numMembersElected); + + if (memberElected) { + numMembersElected[groupIndex] = numMembersElected[groupIndex].add(1); + totalNumMembersElected = totalNumMembersElected.add(1); + } else { + break; + } + } + require(totalNumMembersElected >= electableValidators.min); + // 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)) { + // We use the validating delegate if one is set. + address[] memory electedGroupValidators = getValidators().getTopValidatorsFromGroup( + electionGroups[i], + numMembersElected[i] + ); + for (uint256 j = 0; j < electedGroupValidators.length; j = j.add(1)) { + electedValidators[totalNumMembersElected] = electedGroupValidators[j]; + totalNumMembersElected = totalNumMembersElected.add(1); + } + } + // Shuffle the validator set using validator-supplied entropy + return shuffleArray(electedValidators); + } + + /** + * @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 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 + ) + private + view + returns (uint256, bool) + { + 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.total.eligible.getValue(group) + ).divide( + FixidityLib.newFixed(numMembersElected[i].add(1)) + ); + if (n.gt(maxN)) { + maxN = n; + groupIndex = i; + memberElected = true; + } + } + } + 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)) { + 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/Governance.sol b/packages/protocol/contracts/governance/Governance.sol index f8cf8a605f6..c32e9f10991 100644 --- a/packages/protocol/contracts/governance/Governance.sol +++ b/packages/protocol/contracts/governance/Governance.sol @@ -7,18 +7,18 @@ import "openzeppelin-solidity/contracts/math/SafeMath.sol"; import "./interfaces/IGovernance.sol"; import "./Proposals.sol"; -import "./UsingLockedGold.sol"; +import "../common/ExtractFunctionSignature.sol"; import "../common/Initializable.sol"; import "../common/FixidityLib.sol"; import "../common/FractionUtil.sol"; import "../common/linkedlists/IntegerSortedLinkedList.sol"; -import "../common/ExtractFunctionSignature.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 Proposals for Proposals.Proposal; using FixidityLib for FixidityLib.Fraction; using FractionUtil for FractionUtil.Fraction; @@ -45,14 +45,20 @@ contract Governance is IGovernance, Ownable, Initializable, UsingLockedGold, Ree Yes } + struct UpvoteRecord { + uint256 proposalId; + uint256 weight; + } + struct VoteRecord { Proposals.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; @@ -89,7 +95,7 @@ contract Governance is IGovernance, Ownable, Initializable, UsingLockedGold, Ree mapping(address => uint256) public refundedDeposits; mapping(address => ContractConstitution) private constitution; mapping(uint256 => Proposals.Proposal) private proposals; - mapping(address => Voter) public voters; + mapping(address => Voter) private voters; SortedLinkedList.List private queue; uint256[] public dequeued; uint256[] public emptyIndices; @@ -479,8 +485,7 @@ contract Governance is IGovernance, Ownable, Initializable, UsingLockedGold, Ree nonReentrant returns (bool) { - address account = getAccountFromVoter(msg.sender); - require(!isVotingFrozen(account)); + 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(); @@ -492,16 +497,26 @@ contract Governance is IGovernance, Ownable, Initializable, UsingLockedGold, Ree 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 = getAccountWeight(account); + 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.upvotedProposal == 0 || !queue.contains(voter.upvotedProposal)) && - weight > 0 + voter.upvote.proposalId == 0 || !queue.contains(voter.upvote.proposalId), + "cannot upvote more than one queued proposal" ); - 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; } @@ -524,9 +539,9 @@ 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.upvotedProposal; + uint256 proposalId = voter.upvote.proposalId; Proposals.Proposal storage proposal = proposals[proposalId]; require(proposal.exists()); // If acting on an expired proposal, expire the proposal. @@ -537,13 +552,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), lesser, greater); - emit ProposalUpvoteRevoked(proposalId, account, weight); + queue.update( + proposalId, + queue.getValue(proposalId).sub(voter.upvote.weight), + lesser, + greater + ); + emit ProposalUpvoteRevoked(proposalId, account, voter.upvote.weight); } } - voter.upvotedProposal = 0; + voter.upvote = UpvoteRecord(0, 0); return true; } @@ -567,7 +585,7 @@ contract Governance is IGovernance, Ownable, Initializable, UsingLockedGold, Ree require(msg.sender == approver && !proposal.isApproved() && stage == Proposals.Stage.Approval); proposal.approved = true; // Ensures networkWeight is set by the end of the Referendum stage, even if 0 votes are cast. - proposal.networkWeight = getTotalWeight(); + proposal.networkWeight = getLockedGold().getTotalLockedGold(); emit ProposalApproved(proposalId); return true; } @@ -589,8 +607,7 @@ contract Governance is IGovernance, Ownable, Initializable, UsingLockedGold, Ree nonReentrant returns (bool) { - address account = getAccountFromVoter(msg.sender); - require(!isVotingFrozen(account)); + address account = getLockedGold().getAccountFromVoter(msg.sender); dequeueProposalsIfReady(); Proposals.Proposal storage proposal = proposals[proposalId]; require(isDequeuedProposal(proposal, proposalId, index)); @@ -600,7 +617,7 @@ contract Governance is IGovernance, Ownable, Initializable, UsingLockedGold, Ree return false; } Voter storage voter = voters[account]; - uint256 weight = getAccountWeight(account); + uint256 weight = getLockedGold().getAccountTotalLockedGold(account); require( proposal.isApproved() && stage == Proposals.Stage.Referendum && @@ -609,13 +626,13 @@ contract Governance is IGovernance, Ownable, Initializable, UsingLockedGold, Ree ); VoteRecord storage voteRecord = voter.referendumVotes[index]; proposal.updateVote( + voteRecord.weight, weight, (voteRecord.proposalId == proposalId) ? voteRecord.value : Proposals.VoteValue.None, value ); - proposal.networkWeight = getTotalWeight(); - voteRecord.proposalId = proposalId; - voteRecord.value = value; + proposal.networkWeight = getLockedGold().getTotalLockedGold(); + voter.referendumVotes[index] = VoteRecord(value, proposalId, weight); if (proposal.timestamp > voter.mostRecentReferendumProposal) { voter.mostRecentReferendumProposal = proposalId; } @@ -803,12 +820,13 @@ contract Governance is IGovernance, Ownable, Initializable, UsingLockedGold, Ree } /** - * @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); } /** @@ -820,21 +838,6 @@ contract Governance is IGovernance, Ownable, Initializable, UsingLockedGold, Ree 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); - Proposals.Proposal storage proposal = proposals[voter.mostRecentReferendumProposal]; - bool isVotingReferendum = ( - proposal.getDequeuedStage(stageDurations) == Proposals.Stage.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 ade20cf7e36..23cf607f67b 100644 --- a/packages/protocol/contracts/governance/LockedGold.sol +++ b/packages/protocol/contracts/governance/LockedGold.sol @@ -5,222 +5,92 @@ 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"; +import "../common/UsingRegistry.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; + 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; } - 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 PendingWithdrawal { + // The value of the pending withdrawal. + uint256 value; + // The timestamp at which the pending withdrawal becomes available. + uint256 timestamp; + } + + // NOTE: This contract does not store an account's locked gold that is being used in electing + // validators. + struct Balances { + // 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; } 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; - } - - // 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 - ); - - event CommitmentExtended( - address indexed account, - uint256 value, - uint256 noticePeriod, - uint256 availabilityTime - ); - - event Withdrawal( - address indexed account, - uint256 value - ); - - event NoticePeriodIncreased( - address indexed account, - uint256 value, - uint256 noticePeriod, - uint256 increase - ); + // 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; + } - function initialize(address registryAddress, uint256 _maxNoticePeriod) external initializer { + mapping(address => Account) private accounts; + // Maps voting and validating keys to the account that provided the authorization. + mapping(address => address) public authorizedBy; + 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); + event GoldUnlocked(address indexed account, uint256 value, uint256 available); + event GoldWithdrawn(address indexed account, uint256 value); + + function initialize(address registryAddress, uint256 _unlockingPeriod) 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 { - require(blockReward > 0, "placeholder to suppress warning"); - 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); + unlockingPeriod = _unlockingPeriod; } /** * @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. + * @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. - * @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`. + * @dev v, r, s constitute `voter`'s signature on `msg.sender`. */ - function delegateRole( - DelegateRole role, - address delegate, + function authorizeVoter( + address voter, uint8 v, bytes32 r, bytes32 s @@ -228,554 +98,347 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr external nonReentrant { - // TODO: split and add error messages for better dev feedback - 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. + * @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. + * @dev v, r, s constitute `validator`'s signature on `msg.sender`. */ - 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 Sets the duration in seconds users must wait before withdrawing gold after unlocking. + * @param value The unlocking period in seconds. */ - 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); - - // 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); - - emit CommitmentNotified(msg.sender, value, noticePeriod, availabilityTime); - return account.weight; + function setUnlockingPeriod(uint256 value) external onlyOwner { + require(value != unlockingPeriod); + unlockingPeriod = value; + emit UnlockingPeriodSet(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. + * @notice Locks gold to be used for voting. */ - 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); - 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; + function lock() external payable nonReentrant { + require(isAccount(msg.sender), "not account"); + require(msg.value > 0, "no value"); + _incrementNonvotingAccountBalance(msg.sender, msg.value); + emit GoldLocked(msg.sender, msg.value); } /** - * @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. + * @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 withdrawCommitment( - uint256 availabilityTime + function incrementNonvotingAccountBalance( + address account, + uint256 value ) external - nonReentrant - returns (uint256) + onlyRegisteredContract(ELECTION_REGISTRY_ID) { - require(isAccount(msg.sender) && !isVoting(msg.sender)); - // _redeemRewards(msg.sender); - // solhint-disable-next-line not-rely-on-time - require(now >= availabilityTime); - _redeemRewards(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); - - IERC20Token goldToken = IERC20Token(registry.getAddressFor(GOLD_TOKEN_REGISTRY_ID)); - require(goldToken.transfer(msg.sender, value)); - emit Withdrawal(msg.sender, value); - return account.weight; + _incrementNonvotingAccountBalance(account, 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. + * @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 increaseNoticePeriod( - uint256 value, - uint256 noticePeriod, - uint256 increase + function decrementNonvotingAccountBalance( + address account, + uint256 value ) external - nonReentrant - returns (uint256) + onlyRegisteredContract(ELECTION_REGISTRY_ID) { - 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; + _decrementNonvotingAccountBalance(account, value); } /** - * @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 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 isVotingFrozen(address account) external view returns (bool) { - return accounts[account].votingFrozen; + function _incrementNonvotingAccountBalance(address account, uint256 value) private { + accounts[account].balances.nonvoting = accounts[account].balances.nonvoting.add(value); + totalNonvoting = totalNonvoting.add(value); } /** - * @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. + * @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 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); + function _decrementNonvotingAccountBalance(address account, uint256 value) private { + accounts[account].balances.nonvoting = accounts[account].balances.nonvoting.sub(value); + totalNonvoting = totalNonvoting.sub(value); } /** - * @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`. + * @notice Unlocks gold that becomes withdrawable after the unlocking period. + * @param value The amount of gold to unlock. */ - function getNoticePeriods(address _account) external view returns (uint256[] memory) { - Account storage account = accounts[_account]; - return account.commitments.noticePeriods; + function unlock(uint256 value) external nonReentrant { + require(isAccount(msg.sender)); + Account storage account = accounts[msg.sender]; + uint256 balanceRequirement = getValidators().getAccountBalanceRequirement(msg.sender); + 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)); + emit GoldUnlocked(msg.sender, value, available); } + // TODO(asa): Allow partial relock /** - * @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`. + * @notice Relocks gold that has been unlocked but not withdrawn. + * @param index The index of the pending withdrawal to relock. */ - function getAvailabilityTimes(address _account) external view returns (uint256[] memory) { - Account storage account = accounts[_account]; - return account.commitments.availabilityTimes; + function relock(uint256 index) external nonReentrant { + require(isAccount(msg.sender)); + Account storage account = accounts[msg.sender]; + 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); } /** - * @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. + * @notice Withdraws gold that has been unlocked after the unlocking period has passed. + * @param index The index of the pending withdrawal to withdraw. */ - 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); + function withdraw(uint256 index) external nonReentrant { + require(isAccount(msg.sender)); + Account storage account = accounts[msg.sender]; + require(index < account.balances.pendingWithdrawals.length); + PendingWithdrawal storage pendingWithdrawal = account.balances.pendingWithdrawals[index]; + require(now >= pendingWithdrawal.timestamp); + uint256 value = pendingWithdrawal.value; + deletePendingWithdrawal(account.balances.pendingWithdrawals, index); + require(getGoldToken().transfer(msg.sender, value)); + emit GoldWithdrawn(msg.sender, value); } + // TODO(asa): Dedup /** - * @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. + * @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 getNotifiedCommitment( - address _account, - uint256 availabilityTime - ) - external - view - returns (uint256, uint256) - { - Account storage account = accounts[_account]; - Commitment storage notified = account.commitments.notified[availabilityTime]; - return (notified.value, notified.index); + function getAccountFromVoter(address accountOrVoter) external view returns (address) { + address authorizingAccount = authorizedBy[accountOrVoter]; + if (authorizingAccount != address(0)) { + require(accounts[authorizingAccount].authorizations.voting == accountOrVoter); + return authorizingAccount; + } else { + require(isAccount(accountOrVoter)); + return accountOrVoter; + } } /** - * @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. - * @return The associated account. + * @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 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; - } else { - return accountOrDelegate; - } + function getTotalLockedGold() external view returns (uint256) { + return totalNonvoting.add(getElection().getTotalVotes()); } /** - * @notice Returns the weight of a specified account. - * @param _account The address of the account. - * @return The weight of the specified account. + * @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 getAccountWeight(address _account) external view returns (uint256) { - Account storage account = accounts[_account]; - return account.weight; + function getNonvotingLockedGold() external view returns (uint256) { + return totalNonvoting; } /** - * @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. + * @notice Returns the total amount of locked gold for an account. + * @param account The account. + * @return The total amount of locked gold for an account. */ - 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(getElection().getTotalVotesByAccount(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. + * @notice 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. */ - 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); + function getAccountNonvotingLockedGold(address account) external view returns (uint256) { + return accounts[account].balances.nonvoting; } /** - * @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 `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 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 = authorizedBy[accountOrValidator]; + if (authorizingAccount != address(0)) { + require(accounts[authorizingAccount].authorizations.validating == accountOrValidator); + return authorizingAccount; } else { - return delegate; + require(isAccount(accountOrValidator)); + return accountOrValidator; } } - // 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.voting; + 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. + * @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 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); - } + function getValidatorFromAccount(address account) public view returns (address) { + require(isAccount(account)); + address validator = accounts[account].authorizations.validating; + return validator == address(0) ? account : validator; } /** - * @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 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 updateNotifiedDeposit( - Account storage account, - uint256 value, - uint256 availabilityTime + function getPendingWithdrawals( + address account ) - private + external + view + returns (uint256[] memory, uint256[] memory) { - 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); + 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 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 `current`'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 { - 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; + authorizedBy[previous] = address(0); + authorizedBy[current] = msg.sender; } /** * @notice Check if an account already exists. * @param account The address of the account * @return Returns `true` if account exists. Returns `false` otherwise. - * In particular it will return `false` if a delegate with given address exists. */ function isAccount(address account) public view returns (bool) { return (accounts[account].exists); } /** - * @notice Check if a delegate already exists. - * @param account The address of the delegate - * @return Returns `true` if delegate exists. Returns `false` otherwise. + * @notice Check if an account already exists. + * @param account The address of the account + * @return Returns `false` if account exists. Returns `true` otherwise. */ - function isDelegate(address account) external view returns (bool) { - return (delegations[account] != address(0)); - } - - function isNotAccount(address account) internal view returns (bool) { 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)); + /** + * @notice Check if an address has been authorized by an account for voting or validating. + * @param account The possibly authorized address. + * @return Returns `true` if authorized. Returns `false` otherwise. + */ + function isAuthorized(address account) external view returns (bool) { + return (authorizedBy[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)); + /** + * @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)); } - // 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`. + * @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 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/Proposals.sol b/packages/protocol/contracts/governance/Proposals.sol index 71e1a44e738..c7ec5fcce6b 100644 --- a/packages/protocol/contracts/governance/Proposals.sol +++ b/packages/protocol/contracts/governance/Proposals.sol @@ -101,13 +101,15 @@ library Proposals { /** * @notice Adds or changes a vote on a proposal. * @param proposal The proposal struct. - * @param weight The weight of the vote. + * @param previousWeight The previous weight of the vote. + * @param currentWeight The current weight of the vote. * @param previousVote The vote to be removed, or None for a new vote. * @param currentVote The vote to be set. */ function updateVote( Proposal storage proposal, - uint256 weight, + uint256 previousWeight, + uint256 currentWeight, VoteValue previousVote, VoteValue currentVote ) @@ -115,20 +117,20 @@ library Proposals { { // Subtract previous vote. if (previousVote == VoteValue.Abstain) { - proposal.votes.abstain = proposal.votes.abstain.sub(weight); + proposal.votes.abstain = proposal.votes.abstain.sub(previousWeight); } else if (previousVote == VoteValue.Yes) { - proposal.votes.yes = proposal.votes.yes.sub(weight); + proposal.votes.yes = proposal.votes.yes.sub(previousWeight); } else if (previousVote == VoteValue.No) { - proposal.votes.no = proposal.votes.no.sub(weight); + proposal.votes.no = proposal.votes.no.sub(previousWeight); } // Add new vote. if (currentVote == VoteValue.Abstain) { - proposal.votes.abstain = proposal.votes.abstain.add(weight); + proposal.votes.abstain = proposal.votes.abstain.add(currentWeight); } else if (currentVote == VoteValue.Yes) { - proposal.votes.yes = proposal.votes.yes.add(weight); + proposal.votes.yes = proposal.votes.yes.add(currentWeight); } else if (currentVote == VoteValue.No) { - proposal.votes.no = proposal.votes.no.add(weight); + proposal.votes.no = proposal.votes.no.add(currentWeight); } } diff --git a/packages/protocol/contracts/governance/UsingLockedGold.sol b/packages/protocol/contracts/governance/UsingLockedGold.sol deleted file mode 100644 index 462895f8f79..00000000000 --- a/packages/protocol/contracts/governance/UsingLockedGold.sol +++ /dev/null @@ -1,99 +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 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) internal view returns (bool) { - return getLockedGold().isVotingFrozen(account); - } - - /** - * @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. - * @return The associated account. - */ - function getAccountFromVoter(address accountOrDelegate) internal view returns (address) { - return getLockedGold().getAccountFromDelegateAndRole( - accountOrDelegate, - ILockedGold.DelegateRole.Voting - ); - } - - /** - * @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().getDelegateFromAccountAndRole( - account, - ILockedGold.DelegateRole.Validating - ); - } - - /** - * @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 - ); - } - - /** - * @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); - } - - /** - * @notice Returns the total weight. - * @return Total account weight. - */ - function getTotalWeight() internal view returns (uint256) { - return getLockedGold().totalWeight(); - } - - /** - * @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 getLockedGold() private 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 695aaa61ea8..d13e03a892a 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -5,7 +5,6 @@ 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 "../identity/interfaces/IRandom.sol"; @@ -13,87 +12,89 @@ import "../identity/interfaces/IRandom.sol"; import "../common/Initializable.sol"; import "../common/FixidityLib.sol"; import "../common/linkedlists/AddressLinkedList.sol"; -import "../common/linkedlists/AddressSortedLinkedList.sol"; +import "../common/UsingRegistry.sol"; /** * @title A contract for registering and electing Validator Groups and Validators. */ -contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, UsingLockedGold { +contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, UsingRegistry { 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); + 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; + } - // TODO(asa): These strings should be modifiable struct ValidatorGroup { - string identifier; string name; string url; + // TODO(asa): Add a function that allows groups to update their commission. + 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; + mapping(address => DeregistrationTimestamps) private deregistrationTimestamps; 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; - FixidityLib.Fraction electionThreshold; - uint256 totalVotes; // keeps track of total weight of accounts that have active votes - - address constant PROOF_OF_POSSESSION = address(0xff - 4); - + BalanceRequirements public balanceRequirements; + DeregistrationLockups public deregistrationLockups; uint256 public maxGroupSize; - event MinElectableValidatorsSet( - uint256 minElectableValidators - ); - - event ElectionThresholdSet( - uint256 electionThreshold - ); - - event MaxElectableValidatorsSet( - uint256 maxElectableValidators + event MaxGroupSizeSet( + uint256 size ); - event MaxGroupSizeSet( - uint256 maxGroupSize + event BalanceRequirementsSet( + uint256 group, + uint256 validator ); - event RegistrationRequirementSet( - uint256 value, - uint256 noticePeriod + event DeregistrationLockupsSet( + uint256 group, + uint256 validator ); event ValidatorRegistered( address indexed validator, - string identifier, string name, string url, bytes publicKeysData @@ -115,7 +116,6 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi event ValidatorGroupRegistered( address indexed group, - string identifier, string name, string url ); @@ -139,179 +139,98 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi address indexed validator ); - event ValidatorGroupEmptied( - 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 threshold The minimum ratio of votes a group needs before its members can be elected. + * @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 _maxGroupSize The maximum group size. * @dev Should be called only once. */ function initialize( address registryAddress, - uint256 _minElectableValidators, - uint256 _maxElectableValidators, - uint256 requirementValue, - uint256 requirementNoticePeriod, - uint256 _maxGroupSize, - uint256 threshold + uint256 groupRequirement, + uint256 validatorRequirement, + uint256 groupLockup, + uint256 validatorLockup, + uint256 _maxGroupSize ) external initializer { - require(_minElectableValidators > 0 && _maxElectableValidators >= _minElectableValidators); _transferOwnership(msg.sender); setRegistry(registryAddress); - setElectionThreshold(threshold); - minElectableValidators = _minElectableValidators; - maxElectableValidators = _maxElectableValidators; - registrationRequirement.value = requirementValue; - registrationRequirement.noticePeriod = requirementNoticePeriod; + setBalanceRequirements(groupRequirement, validatorRequirement); + setDeregistrationLockups(groupLockup, validatorLockup); setMaxGroupSize(_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); + function setMaxGroupSize(uint256 size) public onlyOwner returns (bool) { + require(0 < size && size != maxGroupSize); + maxGroupSize = size; + emit MaxGroupSizeSet(size); 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. + * @notice Returns the maximum number of members a group can add. + * @return The maximum number of members a group can add. */ - function setMaxElectableValidators( - uint256 _maxElectableValidators - ) - external - onlyOwner - returns (bool) - { - require( - _maxElectableValidators != maxElectableValidators && - _maxElectableValidators >= minElectableValidators - ); - maxElectableValidators = _maxElectableValidators; - emit MaxElectableValidatorsSet(_maxElectableValidators); - return true; + function getMaxGroupSize() external view returns (uint256) { + return maxGroupSize; } /** - * @notice Changes the maximum group size. - * @param _maxGroupSize The maximum number of validators for each group. + * @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. */ - function setMaxGroupSize( - uint256 _maxGroupSize + function setBalanceRequirements( + uint256 group, + uint256 validator ) public onlyOwner returns (bool) { - require(_maxGroupSize > 0); - maxGroupSize = _maxGroupSize; - emit MaxGroupSizeSet(_maxGroupSize); + require(group != balanceRequirements.group || validator != balanceRequirements.validator); + balanceRequirements = BalanceRequirements(group, validator); + emit BalanceRequirementsSet(group, validator); 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 duration for which gold remains locked after deregistration. + * @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 registrations. */ - function setRegistrationRequirement( - uint256 value, - uint256 noticePeriod + function setDeregistrationLockups( + uint256 group, + uint256 validator ) - external - onlyOwner - returns (bool) - { - require( - value != registrationRequirement.value || - noticePeriod != registrationRequirement.noticePeriod - ); - registrationRequirement.value = value; - registrationRequirement.noticePeriod = noticePeriod; - emit RegistrationRequirementSet(value, noticePeriod); - return true; - } - - /** - * @notice Sets the election threshold. - * @param threshold Election threshold as unwrapped Fraction. - * @return True upon success. - */ - function setElectionThreshold(uint256 threshold) public onlyOwner returns (bool) { - electionThreshold = FixidityLib.wrap(threshold); - require( - electionThreshold.lt(FixidityLib.fixed1()), - "Election threshold must be lower than 100%" - ); - emit ElectionThresholdSet(threshold); + require(group != deregistrationLockups.group || validator != deregistrationLockups.validator); + deregistrationLockups = DeregistrationLockups(group, validator); + emit DeregistrationLockupsSet(group, validator); return true; } - /** - * @notice Gets the election threshold. - * @return Threshold value as unwrapped fraction. - */ - function getElectionThreshold() external view returns (uint256) { - return electionThreshold.unwrap(); - } - - /** * @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 noticePeriods The notice periods of the Locked Gold commitments that - * cumulatively meet 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. @@ -323,18 +242,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[] calldata noticePeriods + bytes calldata publicKeysData ) 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 @@ -343,14 +259,13 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi // Use the proof of possession bytes require(checkProofOfPossession(publicKeysData.slice(64, 48 + 96))); - address account = getAccountFromValidator(msg.sender); + address account = getLockedGold().getAccountFromValidator(msg.sender); require(!isValidator(account) && !isValidatorGroup(account)); - require(meetsRegistrationRequirements(account, noticePeriods)); + require(meetsValidatorBalanceRequirements(account)); - Validator memory validator = Validator(identifier, name, url, publicKeysData, address(0)); - validators[account] = validator; + validators[account] = Validator(name, url, publicKeysData, address(0)); _validators.push(account); - emit ValidatorRegistered(account, identifier, name, url, publicKeysData); + emit ValidatorRegistered(account, name, url, publicKeysData); return true; } @@ -365,6 +280,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 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 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. @@ -372,7 +305,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 = getLockedGold().getAccountFromValidator(msg.sender); require(isValidator(account)); Validator storage validator = validators[account]; if (validator.affiliation != address(0)) { @@ -380,6 +313,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi } delete validators[account]; deleteElement(_validators, account, index); + deregistrationTimestamps[account].validator = now; emit ValidatorDeregistered(account); return true; } @@ -391,7 +325,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 = getLockedGold().getAccountFromValidator(msg.sender); require(isValidator(account) && isValidatorGroup(group)); Validator storage validator = validators[account]; if (validator.affiliation != address(0)) { @@ -408,7 +342,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 = getLockedGold().getAccountFromValidator(msg.sender); require(isValidator(account)); Validator storage validator = validators[account]; require(validator.affiliation != address(0)); @@ -418,35 +352,36 @@ 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 noticePeriods The notice periods of the Locked Gold commitments that - * cumulatively meet the requirements for validator registration. + * @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. */ function registerValidatorGroup( - string calldata identifier, string calldata name, string calldata url, - uint256[] calldata noticePeriods + uint256 commission ) external nonReentrant returns (bool) { - require(bytes(identifier).length > 0 && bytes(name).length > 0 && bytes(url).length > 0); - address account = getAccountFromValidator(msg.sender); + 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); require(!isValidator(account) && !isValidatorGroup(account)); - require(meetsRegistrationRequirements(account, noticePeriods)); + require(meetsValidatorGroupBalanceRequirements(account)); + ValidatorGroup storage group = groups[account]; - group.identifier = identifier; group.name = name; group.url = url; + group.commission = FixidityLib.wrap(commission); _groups.push(account); - emit ValidatorGroupRegistered(account, identifier, name, url); + emit ValidatorGroupRegistered(account, name, url); return true; } @@ -457,11 +392,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 = getLockedGold().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); + deregistrationTimestamps[account].group = now; emit ValidatorGroupDeregistered(account); return true; } @@ -473,13 +409,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 = getAccountFromValidator(msg.sender); + address account = getLockedGold().getAccountFromValidator(msg.sender); require(isValidatorGroup(account) && isValidator(validator)); - ValidatorGroup storage group = groups[account]; - require(validators[validator].affiliation == account && !group.members.contains(validator)); - require(group.members.numElements < maxGroupSize, "Maximum group size exceeded"); - group.members.push(validator); - emit ValidatorGroupMemberAdded(account, validator); + 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, "group would exceed maximum size"); + require(validators[validator].affiliation == group && !_group.members.contains(validator)); + _group.members.push(validator); + emit ValidatorGroupMemberAdded(group, validator); return true; } @@ -490,8 +436,8 @@ 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); - require(isValidatorGroup(account) && isValidator(validator)); + address account = getLockedGold().getAccountFromValidator(msg.sender); + require(isValidatorGroup(account) && isValidator(validator), "is not group and validator"); return _removeMember(account, validator); } @@ -514,7 +460,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi nonReentrant returns (bool) { - address account = getAccountFromValidator(msg.sender); + address account = getLockedGold().getAccountFromValidator(msg.sender); require(isValidatorGroup(account) && isValidator(validator)); ValidatorGroup storage group = groups[account]; require(group.members.contains(validator)); @@ -524,134 +470,34 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi } /** - * @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. + * @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 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); - totalVotes = totalVotes.add(weight); - if (votes.contains(group)) { - votes.update( - group, - votes.getValue(group).add(uint256(weight)), - lesser, - greater - ); - } else { - votes.insert( - group, - weight, - lesser, - greater - ); + 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; } - voters[account] = group; - emit ValidatorGroupVoteCast(account, group, weight); - return true; + if ( + isValidatorGroup(account) || + (timestamps.group > 0 && now < timestamps.group.add(deregistrationLockups.group)) + ) { + return balanceRequirements.group; + } + return 0; } /** - * @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. + * @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 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); - totalVotes = totalVotes.sub(weight); - // 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; + function getDeregistrationTimestamps(address account) external view returns (uint256, uint256) { + return (deregistrationTimestamps[account].group, deregistrationTimestamps[account].validator); } /** @@ -665,7 +511,6 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi external view returns ( - string memory identifier, string memory name, string memory url, bytes memory publicKeysData, @@ -675,7 +520,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, @@ -693,142 +537,103 @@ 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, uint256) { require(isValidatorGroup(account)); ValidatorGroup storage group = groups[account]; - return (group.identifier, group.name, group.url, group.members.getKeys()); + return (group.name, group.url, group.members.getKeys(), group.commission.unwrap()); } /** - * @notice Returns electable validator group addresses and their vote totals. - * @return Electable validator group addresses and their vote totals. + * @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 getValidatorGroupVotes() external view returns (address[] memory, uint256[] memory) { - return votes.getElements(); + function getGroupNumMembers(address account) public view returns (uint256) { + require(isValidatorGroup(account)); + return groups[account].members.numElements; } /** - * @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. + * @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 getVotesReceived(address group) external view returns (uint256) { - return votes.getValue(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)) { + topValidators[i] = getLockedGold().getValidatorFromAccount(topAccounts[i]); + } + return topValidators; } /** - * @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 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 getRegistrationRequirement() external view returns (uint256, uint256) { - return (registrationRequirement.value, registrationRequirement.noticePeriod); + 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]); + } + return numMembers; } /** - * @notice Returns the list of registered validator accounts. - * @return The list of registered validator accounts. + * @notice Returns the number of registered validators. + * @return The number of registered validators. */ - function getRegisteredValidators() external view returns (address[] memory) { - return _validators; + function getNumRegisteredValidators() external view returns (uint256) { + return _validators.length; } /** - * @notice Returns the list of registered validator group accounts. - * @return The list of registered validator group addresses. + * @notice Returns the Locked Gold requirements to register a validator or group. + * @return The locked gold requirements to register a validator or group. */ - function getRegisteredValidatorGroups() external view returns (address[] memory) { - return _groups; + function getBalanceRequirements() external view returns (uint256, uint256) { + return (balanceRequirements.group, balanceRequirements.validator); } /** - * @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. + * @notice Returns the lockup periods after deregistering groups and validators. + * @return The lockup periods after deregistering groups and validators. */ - function isValidating(address account) external view returns (bool) { - return isValidator(account) || isValidatorGroup(account); + function getDeregistrationLockups() external view returns (uint256, uint256) { + return (deregistrationLockups.group, deregistrationLockups.validator); } /** - * @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. + * @notice Returns the list of registered validator accounts. + * @return The list of registered validator accounts. */ - function isVoting(address account) external view returns (bool) { - return (voters[account] != address(0)); + function getRegisteredValidators() external view returns (address[] memory) { + return _validators; } /** - * @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. + * @notice Returns the list of registered validator group accounts. + * @return The list of registered validator group addresses. */ - /* 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; - } - require(numElectionGroups > 0, "No votes have been cast"); - 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; - uint256 numVotes = votes.getValue(electionGroups[i]); - FixidityLib.Fraction memory percentVotes = FixidityLib.newFixedFraction( - numVotes, - totalVotes - ); - if (percentVotes.lt(electionThreshold)) break; - (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); - } - } - // Shuffle the validator set using validator-supplied entropy - IRandom random = IRandom(registry.getAddressForOrDie(RANDOM_REGISTRY_ID)); - bytes32 r = random.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; + function getRegisteredValidatorGroups() external view returns (address[] memory) { + return _groups; } - /* solhint-enable code-complexity */ /** * @notice Returns whether a particular account has a registered validator group. @@ -836,7 +641,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi * @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; } /** @@ -845,34 +650,7 @@ 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; - } - - /** - * @notice 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. - */ - function meetsRegistrationRequirements( - address account, - uint256[] memory noticePeriods - ) - public - view - returns (bool) - { - uint256 lockedValueSum = 0; - for (uint256 i = 0; i < noticePeriods.length; i = i.add(1)) { - if (noticePeriods[i] >= registrationRequirement.noticePeriod) { - lockedValueSum = lockedValueSum.add(getLockedCommitmentValue(account, noticePeriods[i])); - if (lockedValueSum >= registrationRequirement.value) { - return true; - } - } - } - return false; + return bytes(validators[account].name).length > 0; } /** @@ -885,7 +663,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; } @@ -905,10 +683,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)) { - votes.remove(group); - } - emit ValidatorGroupEmptied(group); + getElection().markGroupIneligible(group); } return true; } @@ -935,34 +710,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); - } } diff --git a/packages/protocol/contracts/governance/interfaces/IElection.sol b/packages/protocol/contracts/governance/interfaces/IElection.sol index dbb105d708b..24226b3f7a9 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 getTotalVotesByAccount(address) 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 8b669933899..2db70134e22 100644 --- a/packages/protocol/contracts/governance/interfaces/IGovernance.sol +++ b/packages/protocol/contracts/governance/interfaces/IGovernance.sol @@ -49,9 +49,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 7a007feb0cc..d9a25b9ab64 100644 --- a/packages/protocol/contracts/governance/interfaces/ILockedGold.sol +++ b/packages/protocol/contracts/governance/interfaces/ILockedGold.sol @@ -2,27 +2,11 @@ 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 totalWeight() external view returns (uint256); + 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); } diff --git a/packages/protocol/contracts/governance/interfaces/IValidators.sol b/packages/protocol/contracts/governance/interfaces/IValidators.sol index adde44d0178..fa931072877 100644 --- a/packages/protocol/contracts/governance/interfaces/IValidators.sol +++ b/packages/protocol/contracts/governance/interfaces/IValidators.sol @@ -2,7 +2,9 @@ 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 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); + function getTopValidatorsFromGroup(address, uint256) 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 new file mode 100644 index 00000000000..212133020b1 --- /dev/null +++ b/packages/protocol/contracts/governance/test/MockElection.sol @@ -0,0 +1,32 @@ +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; + address[] public electedValidators; + + function markGroupIneligible(address account) external { + isIneligible[account] = true; + } + + function getTotalVotes() external view returns (uint256) { + return 0; + } + + function getTotalVotesByAccount(address) external view returns (uint256) { + return 0; + } + + function setElectedValidators(address[] calldata _electedValidators) external { + electedValidators = _electedValidators; + } + + function electValidators() external view returns (address[] memory) { + return electedValidators; + } +} diff --git a/packages/protocol/contracts/governance/test/MockLockedGold.sol b/packages/protocol/contracts/governance/test/MockLockedGold.sol index 8409bceefd4..e57dd8cb34d 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,107 +9,62 @@ 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; - uint256 public totalWeight; - - 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; + using SafeMath for uint256; + + struct Authorizations { + address validator; + address voter; } - function setTotalWeight(uint256 weight) external { - totalWeight = weight; + mapping(address => uint256) public accountTotalLockedGold; + mapping(address => uint256) public nonvotingAccountBalance; + mapping(address => address) public authorizedValidators; + mapping(address => address) public authorizedBy; + uint256 private totalLockedGold; + + function authorizeValidator(address account, address validator) external { + authorizedValidators[account] = validator; + authorizedBy[validator] = account; } - function setLockedCommitment(address account, uint256 noticePeriod, uint256 value) external { - locked[account][noticePeriod] = value; + function getAccountFromValidator(address accountOrValidator) external view returns (address) { + if (authorizedBy[accountOrValidator] == address(0)) { + return accountOrValidator; + } else { + return authorizedBy[accountOrValidator]; + } } - function setVotingFrozen(address account) external { - frozen[account] = true; + function getAccountFromVoter(address accountOrVoter) external view returns (address) { + return accountOrVoter; } - function delegateVoting(address account, address delegate) external { - delegations[delegate] = account; - voters[account] = delegate; + function getValidatorFromAccount(address account) external view returns (address) { + address authorizedValidator = authorizedValidators[account]; + return authorizedValidator == address(0) ? account : authorizedValidator; } - function delegateValidating(address account, address delegate) external { - delegations[delegate] = account; - validators[account] = delegate; + function incrementNonvotingAccountBalance(address account, uint256 value) external { + nonvotingAccountBalance[account] = nonvotingAccountBalance[account].add(value); } - function getAccountWeight(address account) external view returns (uint256) { - return weights[account]; + function decrementNonvotingAccountBalance(address account, uint256 value) external { + nonvotingAccountBalance[account] = nonvotingAccountBalance[account].sub(value); } - function getAccountFromDelegateAndRole(address delegate, DelegateRole) - external view returns (address) - { - address a = delegations[delegate]; - if (a != address(0)) { - return a; - } else { - return delegate; - } + function setAccountTotalLockedGold(address account, uint256 value) external { + accountTotalLockedGold[account] = value; } - 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 getAccountTotalLockedGold(address account) external view returns (uint256) { + return accountTotalLockedGold[account]; } - function getLockedCommitment( - address account, - uint256 noticePeriod - ) - external - view - returns (uint256, uint256) - { - // Always return 0 for the index. - return (locked[account][noticePeriod], 0); + function setTotalLockedGold(uint256 value) external { + totalLockedGold = value; + } + function getTotalLockedGold() external view returns (uint256) { + return totalLockedGold; } } diff --git a/packages/protocol/contracts/governance/test/MockValidators.sol b/packages/protocol/contracts/governance/test/MockValidators.sol index 4ed212ae6f1..2f642113fe8 100644 --- a/packages/protocol/contracts/governance/test/MockValidators.sol +++ b/packages/protocol/contracts/governance/test/MockValidators.sol @@ -9,7 +9,10 @@ contract MockValidators is IValidators { mapping(address => bool) private _isValidating; mapping(address => bool) private _isVoting; - address[] private validators; + mapping(address => uint256) private numGroupMembers; + mapping(address => uint256) private balanceRequirements; + mapping(address => address[]) private members; + uint256 private numRegisteredValidators; function isValidating(address account) external view returns (bool) { return _isValidating[account]; @@ -19,8 +22,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 +34,47 @@ 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 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 + ) + 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/Attestations.sol b/packages/protocol/contracts/identity/Attestations.sol index c240c3da9ce..789aac588b7 100644 --- a/packages/protocol/contracts/identity/Attestations.sol +++ b/packages/protocol/contracts/identity/Attestations.sol @@ -10,7 +10,6 @@ import "../common/interfaces/IERC20Token.sol"; import "../governance/interfaces/IValidators.sol"; import "../common/Initializable.sol"; -import "../governance/UsingLockedGold.sol"; import "../common/UsingRegistry.sol"; import "../common/Signatures.sol"; @@ -18,15 +17,7 @@ import "../common/Signatures.sol"; /** * @title Contract mapping identifiers to accounts */ -contract Attestations is - IAttestations, - Ownable, - Initializable, - UsingRegistry, - ReentrancyGuard, - UsingLockedGold -{ - +contract Attestations is IAttestations, Ownable, Initializable, UsingRegistry, ReentrancyGuard { using SafeMath for uint256; using SafeMath for uint128; @@ -706,17 +697,6 @@ contract Attestations is 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 @@ -759,7 +739,7 @@ contract Attestations is 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; @@ -768,7 +748,7 @@ contract Attestations is while (currentIndex < n) { seed = keccak256(abi.encodePacked(seed)); validator = validators[uint256(seed) % validators.length]; - issuer = getAccountFromValidator(validator); + issuer = getLockedGold().getAccountFromValidator(validator); Attestation storage attestations = state.issuedAttestations[issuer]; diff --git a/packages/protocol/contracts/identity/test/MockRandom.sol b/packages/protocol/contracts/identity/test/MockRandom.sol index feb688dec73..6953fdb56bf 100644 --- a/packages/protocol/contracts/identity/test/MockRandom.sol +++ b/packages/protocol/contracts/identity/test/MockRandom.sol @@ -1,23 +1,13 @@ -pragma solidity ^0.5.8; +pragma solidity ^0.5.3; -import "../interfaces/IRandom.sol"; +import "openzeppelin-solidity/contracts/math/SafeMath.sol"; -/** - * @title Returns a fixed value to test 'random' things - */ -contract MockRandom is IRandom { - bytes32 public _r; +contract MockRandom { - function revealAndCommit( - bytes32 randomness, - bytes32 newCommitment, - address proposer - ) external { - _r = randomness; - } - - function random() external view returns (bytes32) { - return _r; + bytes32 public random; + + function setRandom(bytes32 value) external { + random = value; } } diff --git a/packages/protocol/contracts/stability/Exchange.sol b/packages/protocol/contracts/stability/Exchange.sol index 12ce4d5020d..5e0e457422e 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,9 @@ 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 +389,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 315030f4af5..707e0dfbb02 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"; /** @@ -144,8 +143,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/lib/registry-utils.ts b/packages/protocol/lib/registry-utils.ts index 5a3a41cba48..82d3f155d0c 100644 --- a/packages/protocol/lib/registry-utils.ts +++ b/packages/protocol/lib/registry-utils.ts @@ -1,6 +1,7 @@ export enum CeloContractName { Attestations = 'Attestations', BlockchainParameters = 'BlockchainParameters', + Election = 'Election', Escrow = 'Escrow', Exchange = 'Exchange', GasCurrencyWhitelist = 'GasCurrencyWhitelist', @@ -26,6 +27,7 @@ export const usesRegistry = [ export const hasEntryInRegistry: string[] = [ CeloContractName.Attestations, CeloContractName.BlockchainParameters, + 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 868941128f2..4a76fac9fcd 100644 --- a/packages/protocol/migrations/11_validators.ts +++ b/packages/protocol/migrations/11_validators.ts @@ -6,12 +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, - config.validators.electionThreshold, ] } diff --git a/packages/protocol/migrations/12_election.ts b/packages/protocol/migrations/12_election.ts new file mode 100644 index 00000000000..84c89838fa4 --- /dev/null +++ b/packages/protocol/migrations/12_election.ts @@ -0,0 +1,21 @@ +import { CeloContractName } from '@celo/protocol/lib/registry-utils' +import { deploymentForCoreContract } from '@celo/protocol/lib/web3-utils' +import { config } from '@celo/protocol/migrationsConfig' +import { ElectionInstance } from 'types' + +const initializeArgs = async (): Promise => { + return [ + config.registry.predeployedProxyAddress, + config.election.minElectableValidators, + config.election.maxElectableValidators, + config.election.maxVotesPerAccount, + config.election.electabilityThreshold, + ] +} + +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/16_governance.ts b/packages/protocol/migrations/16_governance.ts index 450d40a7438..914a49d16db 100644 --- a/packages/protocol/migrations/16_governance.ts +++ b/packages/protocol/migrations/16_governance.ts @@ -55,6 +55,7 @@ module.exports = deploymentForCoreContract( const proxyAndImplementationOwnedByGovernance = [ 'Attestations', 'BlockchainParameters', + 'Election', 'Escrow', 'Exchange', 'GasCurrencyWhitelist', diff --git a/packages/protocol/migrations/17_elect_validators.ts b/packages/protocol/migrations/17_elect_validators.ts index 46da8d13d2e..4d39ba9fb19 100644 --- a/packages/protocol/migrations/17_elect_validators.ts +++ b/packages/protocol/migrations/17_elect_validators.ts @@ -9,9 +9,10 @@ import { } from '@celo/protocol/lib/web3-utils' import { config } from '@celo/protocol/migrationsConfig' 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 { LockedGoldInstance, ValidatorsInstance } from 'types' +import { ElectionInstance, LockedGoldInstance, ValidatorsInstance } from 'types' const Web3 = require('web3') @@ -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, { @@ -27,13 +28,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, }) } @@ -57,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, { @@ -95,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, { @@ -131,6 +131,11 @@ module.exports = async (_deployer: any) => { artifacts ) + const election: ElectionInstance = await getDeployedProxiedContract( + 'Election', + artifacts + ) + const valKeys: string[] = config.validators.validatorKeys if (valKeys.length === 0) { @@ -165,14 +170,20 @@ module.exports = async (_deployer: any) => { }) } + 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 - await lockedGold.newCommitment(0, { - // @ts-ignore - value: new BigNumber(valKeys.length) - .times(minLockedGoldVotePerValidator) - .times(config.validators.minLockedGoldValue), - }) - await validators.vote(account.address, NULL_ADDRESS, NULL_ADDRESS) + const value = new BigNumber(valKeys.length) + .times(minLockedGoldVotePerValidator) + .times(web3.utils.toWei('1')) + // @ts-ignore + await lockedGold.lock({ value }) + await election.vote(account.address, value, NULL_ADDRESS, NULL_ADDRESS) } diff --git a/packages/protocol/migrationsConfig.js b/packages/protocol/migrationsConfig.js index 0d418ddc240..d8499adc68c 100644 --- a/packages/protocol/migrationsConfig.js +++ b/packages/protocol/migrationsConfig.js @@ -12,11 +12,17 @@ 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: '22', + maxElectableValidators: '100', + maxVotesPerAccount: 3, + electabilityThreshold: '0', // no threshold + }, exchange: { spread: 5 / 1000, reserveFraction: 1, @@ -61,17 +67,21 @@ const DefaultConfig = { initialAccounts: [], }, validators: { - minElectableValidators: '10', - maxElectableValidators: '100', - minLockedGoldValue: '1000000000000000000', // 1 gold - minLockedGoldNoticePeriod: 60 * 24 * 60 * 60, // 60 days - electionThreshold: '0', // no threshold + 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: '70', validatorKeys: [], // We register a single validator group during the migration. groupName: 'C-Labs', - groupUrl: 'https://www.celo.org', + groupUrl: 'celo.org', + commission: 0.1, }, blockchainParameters: { minimumClientVersion: { @@ -102,7 +112,7 @@ const linkedLibraries = { ], SortedLinkedListWithMedian: ['AddressSortedLinkedListWithMedian'], AddressLinkedList: ['Validators'], - AddressSortedLinkedList: ['Validators'], + AddressSortedLinkedList: ['Election'], IntegerSortedLinkedList: ['Governance', 'IntegerSortedLinkedListTest'], AddressSortedLinkedListWithMedian: ['SortedOracles', 'AddressSortedLinkedListWithMedianTest'], Signatures: ['Attestations', 'LockedGold', 'Escrow'], 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', diff --git a/packages/protocol/test/common/addresssortedlinkedlistwithmedian.ts b/packages/protocol/test/common/addresssortedlinkedlistwithmedian.ts index d6b962cd5c7..64f62b32326 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().isLessThan(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/common/integration.ts b/packages/protocol/test/common/integration.ts index 65c3a12688a..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 weight: BigNumber + const value = new BigNumber('1000000000000000000') before(async () => { lockedGold = await getDeployedProxiedContract('LockedGold', artifacts) @@ -39,11 +39,8 @@ 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') // @ts-ignore - await lockedGold.newCommitment(noticePeriod, { value }) - weight = await lockedGold.getAccountWeight(accounts[0]) + await lockedGold.lock({ value }) proposalTransactions = [ { value: 0, @@ -94,7 +91,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) }) }) @@ -117,7 +114,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/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/election.ts b/packages/protocol/test/governance/election.ts new file mode 100644 index 00000000000..9763611f5c2 --- /dev/null +++ b/packages/protocol/test/governance/election.ts @@ -0,0 +1,853 @@ +import { CeloContractName } from '@celo/protocol/lib/registry-utils' +import { + assertContainSubset, + assertEqualBN, + assertRevert, + NULL_ADDRESS, +} from '@celo/protocol/lib/test-utils' +import { toFixed } from '@celo/utils/lib/fixidity' +import BigNumber from 'bignumber.js' +import { + MockLockedGoldContract, + MockLockedGoldInstance, + MockValidatorsContract, + MockValidatorsInstance, + MockRandomContract, + MockRandomInstance, + RegistryContract, + RegistryInstance, + ElectionContract, + ElectionInstance, +} from 'types' + +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') + +// @ts-ignore +// TODO(mcortesi): Use BN +Election.numberFormat = 'BigNumber' + +contract('Election', (accounts: string[]) => { + let election: ElectionInstance + let registry: RegistryInstance + let mockLockedGold: MockLockedGoldInstance + let mockValidators: MockValidatorsInstance + + const nonOwner = accounts[1] + const electableValidators = { + min: new BigNumber(4), + max: new BigNumber(6), + } + const maxNumGroupsVotedFor = new BigNumber(3) + const electabilityThreshold = new BigNumber(0) + + beforeEach(async () => { + election = await Election.new() + mockLockedGold = await MockLockedGold.new() + mockValidators = await MockValidators.new() + registry = await Registry.new() + await registry.setAddressFor(CeloContractName.LockedGold, mockLockedGold.address) + await registry.setAddressFor(CeloContractName.Validators, mockValidators.address) + await election.initialize( + registry.address, + electableValidators.min, + electableValidators.max, + maxNumGroupsVotedFor, + electabilityThreshold + ) + }) + + describe('#initialize()', () => { + it('should have set the owner', async () => { + const owner: string = await election.owner() + assert.equal(owner, accounts[0]) + }) + + 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 () => { + const actualMaxNumGroupsVotedFor = await election.maxNumGroupsVotedFor() + 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, + electableValidators.min, + electableValidators.max, + maxNumGroupsVotedFor, + electabilityThreshold + ) + ) + }) + }) + + describe('#setElectabilityThreshold', () => { + it('should set the electability threshold', async () => { + const threshold = toFixed(1 / 10) + await election.setElectabilityThreshold(threshold) + const result = await election.getElectabilityThreshold() + assertEqualBN(result, threshold) + }) + + it('should revert when the threshold is larger than 100%', async () => { + const threshold = toFixed(new BigNumber('2')) + await assertRevert(election.setElectabilityThreshold(threshold)) + }) + }) + + describe('#setElectableValidators', () => { + const newElectableValidators = { + min: electableValidators.min.plus(1), + max: electableValidators.max.plus(1), + } + + it('should set the minimum electable valdiators', async () => { + 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 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: 'ElectableValidatorsSet', + args: { + min: newElectableValidators.min, + max: newElectableValidators.max, + }, + }) + }) + + it('should revert when the minElectableValidators is zero', async () => { + await assertRevert(election.setElectableValidators(0, newElectableValidators.max)) + }) + + it('should revert when the min is greater than max', async () => { + await assertRevert( + election.setElectableValidators( + newElectableValidators.max.plus(1), + newElectableValidators.max + ) + ) + }) + + 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.setElectableValidators(newElectableValidators.min, newElectableValidators.max, { + from: nonOwner, + }) + ) + }) + }) + + describe('#setMaxNumGroupsVotedFor', () => { + const newMaxNumGroupsVotedFor = maxNumGroupsVotedFor.plus(1) + it('should set the max electable validators', async () => { + await election.setMaxNumGroupsVotedFor(newMaxNumGroupsVotedFor) + assertEqualBN(await election.maxNumGroupsVotedFor(), newMaxNumGroupsVotedFor) + }) + + 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: 'MaxNumGroupsVotedForSet', + args: { + maxNumGroupsVotedFor: new BigNumber(newMaxNumGroupsVotedFor), + }, + }) + }) + + 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.setMaxNumGroupsVotedFor(newMaxNumGroupsVotedFor, { from: nonOwner }) + ) + }) + }) + + describe('#markGroupEligible', () => { + const group = accounts[1] + describe('when the group has members', () => { + beforeEach(async () => { + await mockValidators.setMembers(group, [accounts[9]]) + }) + + describe('when the group has no votes', () => { + let resp: any + beforeEach(async () => { + resp = await election.markGroupEligible(NULL_ADDRESS, NULL_ADDRESS, { from: group }) + }) + + it('should add the group to the list of eligible groups', async () => { + assert.deepEqual(await election.getEligibleValidatorGroups(), [group]) + }) + + it('should emit the ValidatorGroupMarkedEligible event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorGroupMarkedEligible', + args: { + group, + }, + }) + }) + + describe('when the group has already been marked eligible', () => { + it('should revert', async () => { + await assertRevert( + election.markGroupEligible(NULL_ADDRESS, NULL_ADDRESS, { from: group }) + ) + }) + }) + }) + }) + + describe('when the group has no members', () => { + it('should revert', async () => { + await assertRevert(election.markGroupEligible(NULL_ADDRESS, NULL_ADDRESS, { from: group })) + }) + }) + }) + + describe('#markGroupIneligible', () => { + const group = accounts[1] + describe('when the group is eligible', () => { + beforeEach(async () => { + await mockValidators.setMembers(group, [accounts[9]]) + await election.markGroupEligible(NULL_ADDRESS, NULL_ADDRESS, { from: group }) + }) + + describe('when called by the registered Validators contract', () => { + let resp: any + beforeEach(async () => { + await registry.setAddressFor(CeloContractName.Validators, accounts[0]) + resp = await election.markGroupIneligible(group) + }) + + it('should remove the group from the list of eligible groups', async () => { + assert.deepEqual(await election.getEligibleValidatorGroups(), []) + }) + + it('should emit the ValidatorGroupMarkedIneligible event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorGroupMarkedIneligible', + args: { + group, + }, + }) + }) + }) + + describe('when not called by the registered Validators contract', () => { + it('should revert', async () => { + await assertRevert(election.markGroupIneligible(group)) + }) + }) + }) + + 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', async () => { + await assertRevert(election.markGroupIneligible(group)) + }) + }) + }) + }) + + describe('#vote', () => { + const voter = accounts[0] + const group = accounts[1] + 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 }) + }) + + describe('when the group can receive votes', () => { + beforeEach(async () => { + await mockLockedGold.setTotalLockedGold(value) + await mockValidators.setNumRegisteredValidators(1) + }) + + 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) + }) + + it('should add the group to the list of groups the account has voted for', async () => { + assert.deepEqual(await election.getGroupsVotedForByAccount(voter), [group]) + }) + + it("should increment the account's pending votes for the group", async () => { + assertEqualBN(await election.getPendingVotesForGroupByAccount(group, voter), value) + }) + + it("should increment the account's total votes for the group", async () => { + assertEqualBN(await election.getTotalVotesForGroupByAccount(group, voter), value) + }) + + it("should increment the account's total votes", async () => { + assertEqualBN(await election.getTotalVotesByAccount(voter), value) + }) + + it('should increment the total votes for the group', async () => { + assertEqualBN(await election.getTotalVotesForGroup(group), value) + }) + + it('should increment the total votes', async () => { + assertEqualBN(await election.getTotalVotes(), value) + }) + + it("should decrement the account's nonvoting locked gold balance", async () => { + assertEqualBN(await mockLockedGold.nonvotingAccountBalance(voter), 0) + }) + + 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), + }, + }) + }) + }) + + describe('when the voter does not have sufficient non-voting balance', () => { + beforeEach(async () => { + await mockLockedGold.incrementNonvotingAccountBalance(voter, value.minus(1)) + }) + + it('should revert', async () => { + await assertRevert(election.vote(group, value, NULL_ADDRESS, NULL_ADDRESS)) + }) + }) + }) + + 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 < maxNumGroupsVotedFor.toNumber(); i++) { + newGroup = accounts[i + 2] + await mockValidators.setMembers(newGroup, [accounts[9]]) + 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.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)) + }) + }) + }) + + describe('when the group is not eligible', () => { + it('should revert', async () => { + await assertRevert(election.vote(group, value, NULL_ADDRESS, NULL_ADDRESS)) + }) + }) + }) + + describe('#activate', () => { + const voter = accounts[0] + 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 mockLockedGold.setTotalLockedGold(value) + await mockValidators.setNumRegisteredValidators(1) + await mockLockedGold.incrementNonvotingAccountBalance(voter, value) + }) + + 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.getPendingVotesForGroupByAccount(group, voter), 0) + }) + + it("should increment the account's active votes for the group", async () => { + assertEqualBN(await election.getActiveVotesForGroupByAccount(group, voter), value) + }) + + it("should not modify the account's total votes for the group", async () => { + assertEqualBN(await election.getTotalVotesForGroupByAccount(group, voter), value) + }) + + it("should not modify the account's total votes", async () => { + assertEqualBN(await election.getTotalVotesByAccount(voter), value) + }) + + it('should not modify the total votes for the group', async () => { + assertEqualBN(await election.getTotalVotesForGroup(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 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 not modify the first account's active votes for the group", async () => { + assertEqualBN(await election.getActiveVotesForGroupByAccount(group, voter), value) + }) + + it("should not modify the first account's total votes for the group", async () => { + assertEqualBN(await election.getTotalVotesForGroupByAccount(group, voter), value) + }) + + it("should not modify the first account's total votes", async () => { + assertEqualBN(await election.getTotalVotesByAccount(voter), value) + }) + + it("should decrement the second account's pending votes for the group", async () => { + assertEqualBN(await election.getPendingVotesForGroupByAccount(group, voter2), 0) + }) + + it("should increment the second account's active votes for the group", async () => { + assertEqualBN(await election.getActiveVotesForGroupByAccount(group, voter2), value2) + }) + + it("should not modify the second account's total votes for the group", async () => { + assertEqualBN(await election.getTotalVotesForGroupByAccount(group, voter2), value2) + }) + + it("should not modify the second account's total votes", async () => { + assertEqualBN(await election.getTotalVotesByAccount(voter2), value2) + }) + + it('should not modify the total votes for the group', async () => { + assertEqualBN(await election.getTotalVotesForGroup(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', () => { + it('should revert', async () => { + await assertRevert(election.activate(group)) + }) + }) + }) + }) + + describe('#revokePending', () => { + const voter = accounts[0] + 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(NULL_ADDRESS, NULL_ADDRESS, { from: group }) + 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 decrement the account's pending votes for the group", async () => { + assertEqualBN(await election.getPendingVotesForGroupByAccount(group, voter), remaining) + }) + + it("should decrement the account's total votes for the group", async () => { + assertEqualBN(await election.getTotalVotesForGroupByAccount(group, voter), remaining) + }) + + it("should decrement the account's total votes", async () => { + assertEqualBN(await election.getTotalVotesByAccount(voter), remaining) + }) + + it('should decrement the total votes for the group', async () => { + assertEqualBN(await election.getTotalVotesForGroup(group), remaining) + }) + + it('should decrement the total votes', async () => { + assertEqualBN(await election.getTotalVotes(), remaining) + }) + + it("should increment the account's nonvoting locked gold balance", async () => { + assertEqualBN(await mockLockedGold.nonvotingAccountBalance(voter), revokedValue) + }) + + 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 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) + }) + + it('should remove the group to the list of groups the account has voted for', async () => { + assert.deepEqual(await election.getGroupsVotedForByAccount(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) + ) + }) + }) + }) + + 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('#revokeActive', () => { + const voter = accounts[0] + const group = accounts[1] + 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 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 decrement the account's active votes for the group", async () => { + assertEqualBN(await election.getActiveVotesForGroupByAccount(group, voter), remaining) + }) + + it("should decrement the account's total votes for the group", async () => { + assertEqualBN(await election.getTotalVotesForGroupByAccount(group, voter), remaining) + }) + + it("should decrement the account's total votes", async () => { + assertEqualBN(await election.getTotalVotesByAccount(voter), remaining) + }) + + it('should decrement the total votes for the group', async () => { + assertEqualBN(await election.getTotalVotesForGroup(group), remaining) + }) + + it('should decrement the total votes', async () => { + assertEqualBN(await election.getTotalVotes(), remaining) + }) + + it("should increment the account's nonvoting locked gold balance", async () => { + assertEqualBN(await mockLockedGold.nonvotingAccountBalance(voter), revokedValue) + }) + + 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 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.getGroupsVotedForByAccount(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) + ) + }) + }) + }) + + 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('#electValidators', () => { + let random: MockRandomInstance + let totalLockedGold: number + 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 } + 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 mockValidators.setMembers(group1, [validator1, validator2, validator3, validator4]) + 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 }) + + for (const voter of [voter1, voter2, voter3]) { + await mockLockedGold.incrementNonvotingAccountBalance(voter.address, voter.weight) + } + 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 election.vote(group1, voter1.weight, group2, NULL_ADDRESS, { from: voter1.address }) + }) + + it("should return that group's member list", async () => { + assertSameAddresses(await election.electValidators(), [ + validator1, + validator2, + validator3, + validator4, + ]) + }) + }) + + 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 }) + await election.vote(group3, voter3.weight, NULL_ADDRESS, group2, { from: voter3.address }) + }) + + it('should return maxElectableValidators elected validators', async () => { + assertSameAddresses(await election.electValidators(), [ + validator1, + validator2, + validator3, + validator5, + validator6, + validator7, + ]) + }) + }) + + describe('when different random values are provided', () => { + 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 }) + await election.vote(group3, voter3.weight, NULL_ADDRESS, group2, { from: voter3.address }) + }) + + it('should return different results', async () => { + 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) + }) + }) + + describe('when a group receives enough votes for > n seats but only has n members', () => { + beforeEach(async () => { + // 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 election.electValidators(), [ + validator7, + validator1, + validator2, + validator3, + validator5, + validator6, + ]) + }) + }) + + describe('when there are not enough electable validators', () => { + beforeEach(async () => { + 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(election.electValidators()) + }) + }) + }) +}) diff --git a/packages/protocol/test/governance/governance.ts b/packages/protocol/test/governance/governance.ts index a72f1132eed..d4e11d534f7 100644 --- a/packages/protocol/test/governance/governance.ts +++ b/packages/protocol/test/governance/governance.ts @@ -109,8 +109,8 @@ contract('Governance', (accounts: string[]) => { baselineQuorumFactor ) await registry.setAddressFor(CeloContractName.LockedGold, mockLockedGold.address) - await mockLockedGold.setWeight(account, weight) - await mockLockedGold.setTotalWeight(weight) + await mockLockedGold.setAccountTotalLockedGold(account, weight) + await mockLockedGold.setTotalLockedGold(weight) transactionSuccess1 = { value: 0, destination: testTransactions.address, @@ -920,7 +920,7 @@ contract('Governance', (accounts: string[]) => { describe('#upvote()', () => { const proposalId = new BigNumber(1) beforeEach(async () => { - await mockLockedGold.setWeight(account, weight) + await mockLockedGold.setAccountTotalLockedGold(account, weight) await governance.propose( [transactionSuccess1.value], [transactionSuccess1.destination], @@ -938,7 +938,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 () => { @@ -960,16 +962,6 @@ 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 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)) }) @@ -1009,8 +1001,7 @@ contract('Governance', (accounts: string[]) => { { value: minDeposit } ) const otherAccount = accounts[1] - await mockLockedGold.setWeight(otherAccount, weight) - await mockLockedGold.setTotalWeight(weight * 2) + await mockLockedGold.setAccountTotalLockedGold(otherAccount, weight) await governance.upvote(otherProposalId, proposalId, 0, { from: otherAccount }) await timeTravel(queueExpiry, web3) }) @@ -1069,12 +1060,73 @@ 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()', () => { const proposalId = new BigNumber(1) beforeEach(async () => { - await mockLockedGold.setWeight(account, weight) + await mockLockedGold.setAccountTotalLockedGold(account, weight) await governance.propose( [transactionSuccess1.value], [transactionSuccess1.destination], @@ -1098,12 +1150,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 () => { @@ -1125,11 +1174,6 @@ contract('Governance', (accounts: string[]) => { await assertRevert(governance.revokeUpvote(0, 0)) }) - it('should revert when the account weight is 0', async () => { - await mockLockedGold.setWeight(account, 0) - await assertRevert(governance.revokeUpvote(0, 0)) - }) - describe('when the upvoted proposal has expired', () => { beforeEach(async () => { await timeTravel(queueExpiry, web3) @@ -1145,7 +1189,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 () => { @@ -1175,7 +1221,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) }) }) }) @@ -1359,7 +1407,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 () => { @@ -1403,13 +1451,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)) }) @@ -1552,7 +1595,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) }) @@ -1616,7 +1659,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) }) @@ -1639,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) }) @@ -1665,7 +1708,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) }) @@ -1732,7 +1775,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) }) @@ -1756,7 +1799,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) }) @@ -1781,7 +1824,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) @@ -1829,6 +1872,7 @@ contract('Governance', (accounts: string[]) => { }) }) + /* describe('#isVoting()', () => { describe('when the account has never acted on a proposal', () => { it('should return false', async () => { @@ -1839,7 +1883,7 @@ contract('Governance', (accounts: string[]) => { describe('when the account has upvoted a proposal', () => { const proposalId = 1 beforeEach(async () => { - await mockLockedGold.setWeight(account, weight) + await mockLockedGold.setAccountTotalLockedGold(account, weight) await governance.propose( [transactionSuccess1.value], [transactionSuccess1.destination], @@ -1892,7 +1936,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) }) @@ -1911,6 +1955,7 @@ contract('Governance', (accounts: string[]) => { }) }) }) + */ describe('#isProposalPassing()', () => { const proposalId = 1 @@ -1931,8 +1976,8 @@ contract('Governance', (accounts: string[]) => { describe('when the adjusted support is greater than threshold', () => { beforeEach(async () => { - await mockLockedGold.setWeight(account, (weight * 51) / 100) - await mockLockedGold.setWeight(otherAccount, (weight * 49) / 100) + await mockLockedGold.setAccountTotalLockedGold(account, (weight * 51) / 100) + await mockLockedGold.setAccountTotalLockedGold(otherAccount, (weight * 49) / 100) await governance.vote(proposalId, index, VoteValue.Yes) await governance.vote(proposalId, index, VoteValue.No, { from: otherAccount }) }) @@ -1945,8 +1990,8 @@ contract('Governance', (accounts: string[]) => { describe('when the adjusted support is less than or equal to threshold', () => { beforeEach(async () => { - await mockLockedGold.setWeight(account, (weight * 50) / 100) - await mockLockedGold.setWeight(otherAccount, (weight * 50) / 100) + await mockLockedGold.setAccountTotalLockedGold(account, (weight * 50) / 100) + await mockLockedGold.setAccountTotalLockedGold(otherAccount, (weight * 50) / 100) await governance.vote(proposalId, index, VoteValue.Yes) await governance.vote(proposalId, index, VoteValue.No, { from: otherAccount }) }) diff --git a/packages/protocol/test/governance/lockedgold.ts b/packages/protocol/test/governance/lockedgold.ts new file mode 100644 index 00000000000..c16d476da5c --- /dev/null +++ b/packages/protocol/test/governance/lockedgold.ts @@ -0,0 +1,486 @@ +import { CeloContractName } from '@celo/protocol/lib/registry-utils' +import { getParsedSignatureOfAddress } from '@celo/protocol/lib/signing-utils' +import { + assertEqualBN, + assertLogMatches, + assertLogMatches2, + assertRevert, + NULL_ADDRESS, + timeTravel, +} from '@celo/protocol/lib/test-utils' +import BigNumber from 'bignumber.js' +import { + LockedGoldContract, + LockedGoldInstance, + MockElectionContract, + MockElectionInstance, + MockGoldTokenContract, + MockGoldTokenInstance, + MockValidatorsContract, + MockValidatorsInstance, + RegistryContract, + RegistryInstance, +} from 'types' + +const LockedGold: LockedGoldContract = artifacts.require('LockedGold') +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 +LockedGold.numberFormat = 'BigNumber' + +const HOUR = 60 * 60 +const DAY = 24 * HOUR +let authorizationTests = { voter: {}, validator: {} } + +contract('LockedGold', (accounts: string[]) => { + let account = accounts[0] + const nonOwner = accounts[1] + const unlockingPeriod = 3 * DAY + let lockedGold: LockedGoldInstance + let mockElection: MockElectionInstance + let mockValidators: MockValidatorsInstance + let registry: RegistryInstance + + const capitalize = (s: string) => { + return s.charAt(0).toUpperCase() + s.slice(1) + } + + beforeEach(async () => { + const mockGoldToken: MockGoldTokenInstance = await MockGoldToken.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() + + authorizationTests.voter = { + fn: lockedGold.authorizeVoter, + getAuthorizedFromAccount: lockedGold.getVoterFromAccount, + getAccountFromAuthorized: lockedGold.getAccountFromVoter, + } + authorizationTests.validator = { + fn: lockedGold.authorizeValidator, + getAuthorizedFromAccount: lockedGold.getValidatorFromAccount, + getAccountFromAuthorized: lockedGold.getAccountFromValidator, + } + }) + + 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 = await lockedGold.unlockingPeriod() + assertEqualBN(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 })) + }) + }) + + 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 + beforeEach(async () => { + authorizationTest = authorizationTests[key] + }) + + describe(`#authorize${capitalize(key)}()`, () => { + const authorized = accounts[1] + let sig + + beforeEach(async () => { + sig = await getParsedSignatureOfAddress(web3, account, authorized) + }) + + 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) + }) + + 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 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 revert if the ${key} is already authorized`, async () => { + const otherAccount = accounts[2] + const otherSig = await getParsedSignatureOfAddress(web3, 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)) + }) + + it('should revert if the signature is incorrect', async () => { + const nonVoter = accounts[3] + const incorrectSig = await getParsedSignatureOfAddress(web3, account, nonVoter) + await assertRevert( + authorizationTest.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 authorizationTest.fn(authorized, sig.v, sig.r, sig.s) + newSig = await getParsedSignatureOfAddress(web3, 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(`#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 revert when passed an address that is not an account', async () => { + await assertRevert(authorizationTest.getAccountFromAuthorized(accounts[1])) + }) + }) + + describe(`when the account has authorized a ${key}`, () => { + const authorized = accounts[1] + beforeEach(async () => { + const sig = await getParsedSignatureOfAddress(web3, account, authorized) + await authorizationTest.fn(authorized, sig.v, sig.r, sig.s) + }) + + 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 ${key}`, async () => { + assert.equal(await authorizationTest.getAccountFromAuthorized(authorized), account) + }) + }) + }) + + 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) + }) + + it('should revert when not passed an account', async () => { + await assertRevert(authorizationTest.getAuthorizedFromAccount(accounts[1]), account) + }) + }) + + describe(`when the account has authorized a ${key}`, () => { + const authorized = accounts[1] + + beforeEach(async () => { + const sig = await getParsedSignatureOfAddress(web3, 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 + + it("should increase the account's nonvoting locked gold balance", async () => { + // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails + await lockedGold.lock({ 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 }) + 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 }) + 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 }) + assertEqualBN(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 () => { + // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails + 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', () => { + 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 + ) + }) + + 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 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 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, + }) + }) + }) + + describe('when there are balance requirements', () => { + const balanceRequirement = 10 + beforeEach(async () => { + // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails + await lockedGold.lock({ value }) + await mockValidators.setAccountBalanceRequirement(account, balanceRequirement) + }) + + 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 unlocking would yield a locked gold balance equal to the required value', () => { + it('should succeed', async () => { + await lockedGold.unlock(value - balanceRequirement) + }) + }) + }) + }) + + describe('#relock()', () => { + const value = 1000 + const index = 0 + let resp: any + describe('when a pending withdrawal exists', () => { + beforeEach(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 () => { + assertEqualBN(await lockedGold.getAccountNonvotingLockedGold(account), value) + }) + + it("should increase the account's total locked gold balance", async () => { + assertEqualBN(await lockedGold.getAccountTotalLockedGold(account), value) + }) + + it('should increase the nonvoting locked gold balance', async () => { + assertEqualBN(await lockedGold.getNonvotingLockedGold(), value) + }) + + it('should increase the total locked gold balance', async () => { + assertEqualBN(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 [values, timestamps] = await lockedGold.getPendingWithdrawals(account) + assert.equal(values.length, 0) + assert.equal(timestamps.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 resp: any + describe('when a pending withdrawal exists', () => { + beforeEach(async () => { + // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails + await lockedGold.lock({ value }) + resp = await lockedGold.unlock(value) + }) + + describe('when it is after the availablity time', () => { + beforeEach(async () => { + await timeTravel(unlockingPeriod, web3) + resp = await lockedGold.withdraw(index) + }) + + it('should remove the pending withdrawal', async () => { + const [values, timestamps] = await lockedGold.getPendingWithdrawals(account) + assert.equal(values.length, 0) + assert.equal(timestamps.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 7a637b5c30b..e11ce9ee871 100644 --- a/packages/protocol/test/governance/validators.ts +++ b/packages/protocol/test/governance/validators.ts @@ -9,8 +9,8 @@ import BigNumber from 'bignumber.js' import { MockLockedGoldContract, MockLockedGoldInstance, - MockRandomContract, - MockRandomInstance, + MockElectionContract, + MockElectionInstance, RegistryContract, RegistryInstance, ValidatorsContract, @@ -20,8 +20,8 @@ import { toFixed } from '@celo/utils/lib/fixidity' const Validators: ValidatorsContract = artifacts.require('Validators') const MockLockedGold: MockLockedGoldContract = artifacts.require('MockLockedGold') +const MockElection: MockElectionContract = artifacts.require('MockElection') const Registry: RegistryContract = artifacts.require('Registry') -const Random: MockRandomContract = artifacts.require('MockRandom') // @ts-ignore // TODO(mcortesi): Use BN @@ -29,29 +29,29 @@ Validators.numberFormat = 'BigNumber' const parseValidatorParams = (validatorParams: any) => { return { - identifier: validatorParams[0], - 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 { - identifier: groupParams[0], - 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 + contract('Validators', (accounts: string[]) => { let validators: ValidatorsInstance let registry: RegistryInstance let mockLockedGold: MockLockedGoldInstance - let random: MockRandomInstance - + let mockElection: MockElectionInstance // A random 64 byte hex string. const publicKey = 'ea0733ad275e2b9e05541341a97ee82678c58932464fad26164657a111a7e37a9fa0300266fb90e2135a1f1512350cb4e985488a88809b14e3cbe415e76e82b2' @@ -63,62 +63,46 @@ 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 electionThreshold = new BigNumber(0) - const maxGroupSize = 10 - const identifier = 'test-identifier' + const balanceRequirements = { 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() - random = await Random.new() + mockElection = await MockElection.new() registry = await Registry.new() await registry.setAddressFor(CeloContractName.LockedGold, mockLockedGold.address) - await registry.setAddressFor(CeloContractName.Random, random.address) + await registry.setAddressFor(CeloContractName.Election, mockElection.address) await validators.initialize( registry.address, - minElectableValidators, - maxElectableValidators, - registrationRequirement.value, - registrationRequirement.noticePeriod, - maxGroupSize, - electionThreshold + balanceRequirements.group, + balanceRequirements.validator, + deregistrationLockups.group, + deregistrationLockups.validator, + maxGroupSize ) }) const registerValidator = async (validator: string) => { - await mockLockedGold.setLockedCommitment( - validator, - registrationRequirement.noticePeriod, - registrationRequirement.value - ) + await mockLockedGold.setAccountTotalLockedGold(validator, balanceRequirements.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, balanceRequirements.group) + await validators.registerValidatorGroup(name, url, commission, { from: group }) } const registerValidatorGroupWithMembers = async (group: string, members: string[]) => { @@ -136,398 +120,288 @@ 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 the balance requirements', async () => { + const [group, validator] = await validators.getBalanceRequirements() + assertEqualBN(group, balanceRequirements.group) + assertEqualBN(validator, balanceRequirements.validator) }) - it('should have set maxElectableValidators', async () => { - const actualMaxElectableValidators = await validators.maxElectableValidators() - assertEqualBN(actualMaxElectableValidators, maxElectableValidators) + 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 registration requirements', async () => { - const [value, noticePeriod] = await validators.getRegistrationRequirement() - assertEqualBN(value, registrationRequirement.value) - assertEqualBN(noticePeriod, registrationRequirement.noticePeriod) + 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( registry.address, - minElectableValidators, - maxElectableValidators, - registrationRequirement.value, - registrationRequirement.noticePeriod, - maxGroupSize, - electionThreshold + balanceRequirements.group, + balanceRequirements.validator, + deregistrationLockups.group, + deregistrationLockups.validator, + maxGroupSize ) ) }) }) - describe('#setElectionThreshold', () => { - it('should set the election threshold', async () => { - const threshold = toFixed(1 / 10) - await validators.setElectionThreshold(threshold) - const result = await validators.getElectionThreshold() - assertEqualBN(result, threshold) - }) - - it('should revert when the threshold is larger than 100%', async () => { - const threshold = toFixed(new BigNumber('2')) - await assertRevert(validators.setElectionThreshold(threshold)) - }) - }) + describe('#setBalanceRequirements()', () => { + describe('when the requirements are different', () => { + const newRequirements = { + group: balanceRequirements.group.plus(1), + validator: balanceRequirements.validator.plus(1), + } - describe('#setMinElectableValidators', () => { - const newMinElectableValidators = minElectableValidators.plus(1) - it('should set the minimum elected validators', async () => { - await validators.setMinElectableValidators(newMinElectableValidators) - assertEqualBN(await validators.minElectableValidators(), newMinElectableValidators) - }) + describe('when called by the owner', () => { + let resp: any - 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), - }, - }) - }) + beforeEach(async () => { + resp = await validators.setBalanceRequirements( + 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.getBalanceRequirements() + 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 BalanceRequirementsSet event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'BalanceRequirementsSet', + 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 called by a non-owner', () => { + it('should revert', async () => { + await assertRevert( + validators.setBalanceRequirements(newRequirements.group, newRequirements.validator, { + from: nonOwner, + }) + ) + }) + }) + }) - it('should revert when called by anyone other than the owner', async () => { - await assertRevert( - validators.setMinElectableValidators(newMinElectableValidators, { from: nonOwner }) - ) + describe('when the requirements are the same', () => { + it('should revert', async () => { + await assertRevert( + validators.setBalanceRequirements( + balanceRequirements.group, + balanceRequirements.validator + ) + ) + }) + }) }) }) - describe('#setMaxElectableValidators', () => { - const newMaxElectableValidators = maxElectableValidators.plus(1) - it('should set the maximum elected validators', async () => { - await validators.setMaxElectableValidators(newMaxElectableValidators) - assertEqualBN(await validators.maxElectableValidators(), newMaxElectableValidators) - }) + describe('#setDeregistrationLockups()', () => { + describe('when the requirements are different', () => { + const newLockups = { + group: deregistrationLockups.group.plus(1), + validator: deregistrationLockups.validator.plus(1), + } - 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), - }, - }) - }) + describe('when called by the owner', () => { + let resp: any - 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)) - }) + beforeEach(async () => { + resp = await validators.setDeregistrationLockups(newLockups.group, newLockups.validator) + }) - it('should revert when called by anyone other than the owner', async () => { - await assertRevert( - validators.setMaxElectableValidators(newMaxElectableValidators, { from: nonOwner }) - ) - }) - }) + it('should set the group and validator requirements', async () => { + const [group, validator] = await validators.getDeregistrationLockups() + assertEqualBN(group, newLockups.group) + assertEqualBN(validator, newLockups.validator) + }) - describe('#setMaxGroupSize', () => { - const newMaxGroupSize = 11 - it('should set the maximum group size', async () => { - await validators.setMaxGroupSize(newMaxGroupSize) - assertEqualBN(await validators.maxGroupSize(), newMaxGroupSize) - }) + 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 emit the MaxElectableValidatorsSet event', async () => { - const resp = await validators.setMaxGroupSize(newMaxGroupSize) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'MaxGroupSizeSet', - args: { - maxGroupSize: new BigNumber(newMaxGroupSize), - }, + describe('when called by a non-owner', () => { + it('should revert', async () => { + await assertRevert( + validators.setDeregistrationLockups(newLockups.group, newLockups.validator, { + from: nonOwner, + }) + ) + }) + }) }) - }) - it('should revert when called by anyone other than the owner', async () => { - await assertRevert(validators.setMaxGroupSize(newMaxGroupSize, { from: nonOwner })) + describe('when the requirements are the same', () => { + it('should revert', async () => { + await assertRevert( + validators.setDeregistrationLockups( + deregistrationLockups.group, + deregistrationLockups.validator + ) + ) + }) + }) }) }) - 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 + 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) - }) + beforeEach(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: { + size: 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 - ) - }) - - 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, + let resp: any + describe('when the account is not a registered validator', () => { + beforeEach(async () => { + await mockLockedGold.setAccountTotalLockedGold(validator, balanceRequirements.validator) + resp = await validators.registerValidator( name, url, - publicKeysData, - }, + // @ts-ignore bytes type + publicKeysData + ) }) - }) - describe('when multiple commitment notice periods are provided', () => { - it('should accept a sufficient combination of commitments as stake', async () => { - // create registrationRequirement.value different locked commitments each - // with value 1 and unique noticePeriods greater than registrationRequirement.noticePeriod - const commitmentCount = registrationRequirement.value - const noticePeriods = [] - for (let i = 1; i <= commitmentCount.toNumber(); i++) { - const noticePeriod = registrationRequirement.noticePeriod.plus(i) - noticePeriods.push(noticePeriod) - await mockLockedGold.setLockedCommitment(validator, noticePeriod, 1) - } + it('should mark the account as a validator', async () => { + assert.isTrue(await validators.isValidator(validator)) + }) - await validators.registerValidator( - identifier, - name, - url, - // @ts-ignore bytes type - publicKeysData, - noticePeriods - ) + it('should add the account to the list of validators', async () => { assert.deepEqual(await validators.getRegisteredValidators(), [validator]) }) - it('should revert when the combined commitment value is insufficient with all valid notice periods', async () => { - // create registrationRequirement.value - 1 different locked commitments each - // with value 1 and valid noticePeriods - const commitmentCount = registrationRequirement.value.minus(1) - const noticePeriods = [] - for (let i = 1; i <= commitmentCount.toNumber(); i++) { - const noticePeriod = registrationRequirement.noticePeriod.plus(i) - noticePeriods.push(noticePeriod) - await mockLockedGold.setLockedCommitment(validator, noticePeriod, 1) - } - - await assertRevert( - validators.registerValidator( - identifier, - name, - url, - // @ts-ignore bytes type - publicKeysData, - noticePeriods - ) - ) + 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 revert when the combined commitment value of valid notice periods is insufficient', async () => { - // create registrationRequirement.value different locked commitments each - // with value 1, but with one noticePeriod that is less than - // registrationRequirement.noticePeriod - const commitmentCount = registrationRequirement.value.minus(1) - const invalidNoticePeriod = registrationRequirement.noticePeriod.minus(1) - const noticePeriods = [invalidNoticePeriod] - await mockLockedGold.setLockedCommitment(validator, invalidNoticePeriod, 1) - for (let i = 1; i < commitmentCount.toNumber(); i++) { - const noticePeriod = registrationRequirement.noticePeriod.plus(i) - noticePeriods.push(noticePeriod) - await mockLockedGold.setLockedCommitment(validator, noticePeriod, 1) - } + it('should set account balance requirements', async () => { + const requirement = await validators.getAccountBalanceRequirement(validator) + assertEqualBN(requirement, balanceRequirements.validator) + }) - await assertRevert( - validators.registerValidator( - identifier, + 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, - // @ts-ignore bytes type publicKeysData, - noticePeriods - ) - ) + }, + }) }) }) describe('when the account is already a registered validator', () => { beforeEach(async () => { + await mockLockedGold.setAccountTotalLockedGold(validator, balanceRequirements.validator) 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] - ) + publicKeysData ) + assert.deepEqual(await validators.getRegisteredValidators(), [validator]) }) }) - describe('when the account is already a registered validator group', () => { + describe('when the account is already a registered validator', () => { beforeEach(async () => { - await validators.registerValidatorGroup(identifier, name, url, [ - registrationRequirement.noticePeriod, - ]) + await mockLockedGold.setAccountTotalLockedGold(validator, balanceRequirements.group) + 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 ) ) }) }) - describe('when the account does not meet the registration requirements', () => { + describe('when the account does not meet the balance requirements', () => { beforeEach(async () => { - await mockLockedGold.setLockedCommitment( + await mockLockedGold.setAccountTotalLockedGold( validator, - registrationRequirement.noticePeriod, - registrationRequirement.value.minus(1) + balanceRequirements.validator.minus(1) ) }) it('should revert', async () => { await assertRevert( validators.registerValidator( - identifier, name, url, // @ts-ignore bytes type - publicKeysData, - [registrationRequirement.noticePeriod] + publicKeysData ) ) }) @@ -537,35 +411,51 @@ 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 a registered validator', () => { + beforeEach(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 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 is affiliated with a validator group', () => { const group = accounts[1] beforeEach(async () => { + await registerValidator(validator) await registerValidatorGroup(group) await validators.affiliate(group) }) @@ -596,7 +486,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', @@ -608,31 +498,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)) }) }) }) @@ -726,7 +594,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', @@ -738,31 +606,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)) }) }) }) @@ -818,7 +664,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', @@ -830,31 +676,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)) }) }) }) @@ -871,52 +695,43 @@ 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', () => { + beforeEach(async () => { + await mockLockedGold.setAccountTotalLockedGold(group, balanceRequirements.group) + resp = await validators.registerValidatorGroup(name, url, commission) + }) - 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 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] + assertContainSubset(log, { + event: 'ValidatorGroupRegistered', + args: { + group, + name, + url, + }, + }) }) }) @@ -926,45 +741,28 @@ contract('Validators', (accounts: string[]) => { }) it('should revert', async () => { - await assertRevert( - validators.registerValidatorGroup(identifier, name, url, [ - registrationRequirement.noticePeriod, - ]) - ) + await assertRevert(validators.registerValidatorGroup(name, url, balanceRequirements.group)) }) }) describe('when the account is already a registered validator group', () => { beforeEach(async () => { - await validators.registerValidatorGroup(identifier, name, url, [ - registrationRequirement.noticePeriod, - ]) + await mockLockedGold.setAccountTotalLockedGold(group, balanceRequirements.group) + 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', () => { + describe('when the account does not meet the balance requirements', () => { beforeEach(async () => { - await mockLockedGold.setLockedCommitment( - group, - registrationRequirement.noticePeriod, - registrationRequirement.value.minus(1) - ) + await mockLockedGold.setAccountTotalLockedGold(group, balanceRequirements.group.minus(1)) }) it('should revert', async () => { - await assertRevert( - validators.registerValidatorGroup(identifier, name, url, [ - registrationRequirement.noticePeriod, - ]) - ) + await assertRevert(validators.registerValidatorGroup(name, url, commission)) }) }) }) @@ -972,22 +770,35 @@ contract('Validators', (accounts: string[]) => { describe('#deregisterValidatorGroup', () => { 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 () => { - 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 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 [groupTimestamp, validatorTimestamp] = await validators.getDeregistrationTimestamps( + group + ) + assertEqualBN(groupTimestamp, latestTimestamp) + assertEqualBN(validatorTimestamp, 0) + }) + 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, { @@ -1009,6 +820,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) @@ -1023,20 +835,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, { @@ -1058,7 +870,6 @@ contract('Validators', (accounts: string[]) => { it('should revert when trying to add too many members to group', async () => { await validators.setMaxGroupSize(1) - await validators.addMember(validator) await registerValidator(accounts[2]) await validators.affiliate(group, { from: accounts[2] }) await assertRevert(validators.addMember(accounts[2])) @@ -1075,10 +886,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)) }) @@ -1100,7 +907,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', @@ -1112,31 +919,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)) }) }) @@ -1206,322 +991,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] - - 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()) - }) - }) - - describe('when election threshold is set to 20%', () => { - beforeEach(async () => { - const threshold = toFixed(1 / 5) - await validators.setElectionThreshold(threshold) - 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 elected validators from two largest parties', async () => { - assertSameAddresses(await validators.getValidators(), [ - validator1, - validator2, - validator3, - validator4, - validator5, - validator6, - ]) - }) - }) - }) }) diff --git a/packages/protocol/test/identity/attestations.ts b/packages/protocol/test/identity/attestations.ts index d3d582d053e..3990d633b1c 100644 --- a/packages/protocol/test/identity/attestations.ts +++ b/packages/protocol/test/identity/attestations.ts @@ -19,8 +19,8 @@ import { MockLockedGoldInstance, MockStableTokenContract, MockStableTokenInstance, - MockValidatorsContract, - MockValidatorsInstance, + MockElectionContract, + MockElectionInstance, RandomContract, RandomInstance, RegistryContract, @@ -30,7 +30,7 @@ import { getParsedSignatureOfAddress } from '../../lib/signing-utils' 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 MockLockedGold: MockLockedGoldContract = artifacts.require('MockLockedGold') const Random: RandomContract = artifacts.require('Random') const Registry: RegistryContract = artifacts.require('Registry') @@ -45,7 +45,7 @@ contract('Attestations', (accounts: string[]) => { let mockStableToken: MockStableTokenInstance let otherMockStableToken: MockStableTokenInstance let random: RandomInstance - let mockValidators: MockValidatorsInstance + let mockElection: MockElectionInstance let mockLockedGold: MockLockedGoldInstance let registry: RegistryInstance const provider = new Web3.providers.HttpProvider('http://localhost:8545') @@ -137,19 +137,19 @@ 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(getValidatingKeyAddress(account))) - ) mockLockedGold = await MockLockedGold.new() await Promise.all( accounts.map((account) => - mockLockedGold.delegateValidating(account, getValidatingKeyAddress(account)) + mockLockedGold.authorizeValidator(account, getValidatingKeyAddress(account)) ) ) + mockElection = await MockElection.new() + await mockElection.setElectedValidators( + accounts.map((account) => getValidatingKeyAddress(account)) + ) 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 registry.setAddressFor(CeloContractName.LockedGold, mockLockedGold.address) await attestations.initialize( registry.address, From 5797b0f08c82940756553bda40168a8424551587 Mon Sep 17 00:00:00 2001 From: Trevor Porter Date: Wed, 16 Oct 2019 16:23:09 -0700 Subject: [PATCH 24/37] Reconfigure terraform local configuration during init to allow multiple envs (#773) From 14c65c181f418ad3335701a0268e5f37c88cb3e7 Mon Sep 17 00:00:00 2001 From: Trevor Porter Date: Wed, 16 Oct 2019 16:43:21 -0700 Subject: [PATCH 25/37] Alfajores changes & comment on unlocking accounts (#1297) --- dockerfiles/cli/Dockerfile | 6 +++--- packages/cli/package.json | 10 +++++----- packages/cli/start_geth.sh | 4 ++-- packages/docs/getting-started/running-a-full-node.md | 2 +- packages/docs/getting-started/running-a-validator.md | 10 +++++----- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/dockerfiles/cli/Dockerfile b/dockerfiles/cli/Dockerfile index 2bfc8f36fc3..cc65e38f3a3 100644 --- a/dockerfiles/cli/Dockerfile +++ b/dockerfiles/cli/Dockerfile @@ -49,7 +49,7 @@ RUN npm install @celo/celocli FROM node:10-alpine as final_image ARG network_name="alfajores" -ARG network_id="44782" +ARG network_id="44784" # Without musl-dev, geth will fail with a confusing "No such file or directory" error. # bash is required for start_geth.sh @@ -62,8 +62,8 @@ COPY packages/cli/start_geth.sh /celo/start_geth.sh COPY --from=geth /usr/local/bin/geth /usr/local/bin/geth COPY --from=geth /celo/genesis.json /celo COPY --from=geth /celo/static-nodes.json /celo -COPY --from=node /celo-monorepo/node_modules /celo-monorepo/node_modules +COPY --from=node /celo-monorepo/node_modules /celo-monorepo/node_modules RUN chmod ugo+x /celo/start_geth.sh && ln -s /celo-monorepo/node_modules/.bin/celocli /usr/local/bin/celocli EXPOSE 8545 8546 30303 30303/udp -ENTRYPOINT ["/celo/start_geth.sh", "/usr/local/bin/geth", "alfajores", "full", "44782", "/root/.celo", "/celo/genesis.json", "/celo/static-nodes.json"] +ENTRYPOINT ["/celo/start_geth.sh", "/usr/local/bin/geth", "alfajores", "full", "44784", "/root/.celo", "/celo/genesis.json", "/celo/static-nodes.json"] diff --git a/packages/cli/package.json b/packages/cli/package.json index ac18f9c6249..11a52986e43 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "name": "@celo/celocli", "description": "CLI Tool for transacting with the Celo protocol", - "version": "0.0.18", + "version": "0.0.27", "author": "Celo", "license": "Apache-2.0", "repository": "celo-org/celo-monorepo", @@ -29,12 +29,11 @@ "test": "TZ=UTC jest" }, "dependencies": { + "@celo/contractkit": "^0.1.6", "@celo/utils": "^0.1.0", - "@celo/contractkit": "^0.1.1", "@oclif/command": "^1", "@oclif/config": "^1", "@oclif/plugin-help": "^2", - "ethereumjs-util": "^5.2.0", "bip32": "^1.0.2", "bip39": "^2.5.0", "chalk": "^2.4.2", @@ -42,6 +41,7 @@ "cli-ux": "^5.3.1", "debug": "^4.1.1", "elliptic": "^6.4.1", + "ethereumjs-util": "^5.2.0", "events": "^3.0.0", "firebase": "^6.2.4", "fs-extra": "^8.1.0", @@ -54,11 +54,11 @@ "@celo/dev-cli": "^2.0.3", "@types/bip32": "^1.0.1", "@types/bip39": "^2.4.2", - "@types/elliptic": "^6.4.9", - "@types/mocha": "^5.2.7", "@types/cli-table": "^0.3.0", "@types/debug": "^4.1.4", + "@types/elliptic": "^6.4.9", "@types/fs-extra": "^8.0.0", + "@types/mocha": "^5.2.7", "@types/node": "^10", "@types/web3": "^1.0.18", "globby": "^8", diff --git a/packages/cli/start_geth.sh b/packages/cli/start_geth.sh index 3dc99208d4a..3290494b0e5 100644 --- a/packages/cli/start_geth.sh +++ b/packages/cli/start_geth.sh @@ -11,8 +11,8 @@ GETH_BINARY=${1:-"/usr/local/bin/geth"} NETWORK_NAME=${2:-"alfajores"} # Default to testing the ultralight sync mode SYNCMODE=${3:-"ultralight"} -# Default to 44782 -NETWORK_ID=${4:-"44782"} +# Default to 44784 +NETWORK_ID=${4:-"44784"} DATA_DIR=${5:-"/tmp/tmp1"} GENESIS_FILE_PATH=${6:-"/celo/genesis.json"} STATIC_NODES_FILE_PATH=${7:-"/celo/static-nodes.json"} diff --git a/packages/docs/getting-started/running-a-full-node.md b/packages/docs/getting-started/running-a-full-node.md index 436f3a1e550..7024020256d 100644 --- a/packages/docs/getting-started/running-a-full-node.md +++ b/packages/docs/getting-started/running-a-full-node.md @@ -68,7 +68,7 @@ In order to allow the node to sync with the network, give it the address of exis This command specifies the settings needed to run the node, and gets it started. -`` $ docker run -p 127.0.0.1:8545:8545 -p 127.0.0.1:8546:8546 -p 30303:30303 -p 30303:30303/udp -v `pwd`:/root/.celo us.gcr.io/celo-testnet/celo-node:alfajores --verbosity 3 --networkid 44782 --syncmode full --rpc --rpcaddr 0.0.0.0 --rpcapi eth,net,web3,debug,admin,personal --lightserv 90 --lightpeers 1000 --maxpeers 1100 --etherbase $CELO_ACCOUNT_ADDRESS `` +`` $ docker run -p 127.0.0.1:8545:8545 -p 127.0.0.1:8546:8546 -p 30303:30303 -p 30303:30303/udp -v `pwd`:/root/.celo us.gcr.io/celo-testnet/celo-node:alfajores --verbosity 3 --networkid 44784 --syncmode full --rpc --rpcaddr 0.0.0.0 --rpcapi eth,net,web3,debug,admin,personal --lightserv 90 --lightpeers 1000 --maxpeers 1100 --etherbase $CELO_ACCOUNT_ADDRESS `` You'll start seeing some output. There may be some errors or warnings that are ignorable. After a few minutes, you should see lines that look like this. This means your node has synced with the network and is receiving blocks. diff --git a/packages/docs/getting-started/running-a-validator.md b/packages/docs/getting-started/running-a-validator.md index 69d1e7ca197..207442ff4f8 100644 --- a/packages/docs/getting-started/running-a-validator.md +++ b/packages/docs/getting-started/running-a-validator.md @@ -95,7 +95,7 @@ In order to allow the node to sync with the network, give it the address of exis Start up the node: -`` $ docker run -p 127.0.0.1:8545:8545 -p 127.0.0.1:8546:8546 -p 30303:30303 -p 30303:30303/udp -v `pwd`:/root/.celo us.gcr.io/celo-testnet/celo-node:alfajores --verbosity 3 --networkid 44782 --syncmode full --rpc --rpcaddr 0.0.0.0 --rpcapi eth,net,web3,debug,admin,personal --maxpeers 1100 --mine --miner.verificationpool=https://us-central1-celo-testnet-production.cloudfunctions.net/handleVerificationRequestalfajores/v0.1/sms/ --etherbase $CELO_VALIDATOR_ADDRESS `` +`` $ docker run -p 127.0.0.1:8545:8545 -p 127.0.0.1:8546:8546 -p 30303:30303 -p 30303:30303/udp -v `pwd`:/root/.celo us.gcr.io/celo-testnet/celo-node:alfajores --verbosity 3 --networkid 44784 --syncmode full --rpc --rpcaddr 0.0.0.0 --rpcapi eth,net,web3,debug,admin,personal --maxpeers 1100 --mine --miner.verificationpool=https://us-central1-celo-testnet-production.cloudfunctions.net/handleVerificationRequestalfajores/v0.1/sms/ --etherbase $CELO_VALIDATOR_ADDRESS `` {% hint style="danger" %} **Security**: The command line above includes the parameter `--rpcaddr 0.0.0.0` which makes the Celo Blockchain software listen for incoming RPC requests on all network adaptors. Exercise extreme caution in doing this when running outside Docker, as it means that any unlocked accounts and their funds may be accessed from other machines on the Internet. In the context of running a Docker container on your local machine, this together with the `docker -p` flags allows you to make RPC calls from outside the container, i.e from your local host, but not from outside your machine. Read more about [Docker Networking](https://docs.docker.com/network/network-tutorial-standalone/#use-user-defined-bridge-networks) here. @@ -103,13 +103,13 @@ Start up the node: The `mine` flag does not mean the node starts mining blocks, but rather starts trying to participate in the BFT consensus protocol. It cannot do this until it gets elected -- so next we need to stand for election. -The `networkid` parameter value of `44782` indicates we are connecting the Alfajores Testnet. +The `networkid` parameter value of `44784` indicates we are connecting the Alfajores Testnet. ## Obtain and lock up some Celo Gold for staking Visit the [Alfajores Faucet](https://celo.org/build/faucet) to send **both** of your accounts some funds. -In a new tab, unlock your accounts so that you can send transactions: +In a new tab, unlock your accounts so that you can send transactions. This only unlocks the accounts for the lifetime of the validator that's running, so be sure to unlock `$CELO_VALIDATOR_ADDRESS` again if your node gets restarted: ``` $ celocli account:unlock --account $CELO_VALIDATOR_GROUP_ADDRESS --password @@ -126,8 +126,8 @@ $ celocli lockedgold:register --from $CELO_VALIDATOR_ADDRESS Make a locked Gold commitment for both accounts in order to secure the right to register a validator and validator group. The current requirement is 1 Celo Gold with a notice period of 60 days. If you choose to stake more gold, or a longer notice period, be sure to use those values below: ``` -$ celocli lockedgold:deposit --from $CELO_VALIDATOR_GROUP_ADDRESS --goldAmount 1000000000000000000 --noticePeriod 5184000 -$ celocli lockedgold:deposit --from $CELO_VALIDATOR_ADDRESS --goldAmount 1000000000000000000 --noticePeriod 5184000 +$ celocli lockedgold:lockup --from $CELO_VALIDATOR_GROUP_ADDRESS --goldAmount 1000000000000000000 --noticePeriod 5184000 +$ celocli lockedgold:lockup --from $CELO_VALIDATOR_ADDRESS --goldAmount 1000000000000000000 --noticePeriod 5184000 ``` ## Run for election From df639427636d5fbb6ef123856c06d7617de0a5ef Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Wed, 16 Oct 2019 17:10:07 -0700 Subject: [PATCH 26/37] Point end-to-end tests back to master (#1372) --- .circleci/config.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 066d92bd043..ff893fbe1d3 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -602,7 +602,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 master end-to-end-geth-sync-test: <<: *defaults @@ -642,7 +642,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 master end-to-end-geth-integration-sync-test: <<: *defaults @@ -675,7 +675,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 master web: working_directory: ~/app From ea8510ea2813a8b51d1e3b1d45e55c0b105feebe Mon Sep 17 00:00:00 2001 From: Ashish Bhatia Date: Wed, 16 Oct 2019 17:28:45 -0700 Subject: [PATCH 27/37] [wallet]Add documentation for jndcrash (#1364) --- packages/mobile/README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/mobile/README.md b/packages/mobile/README.md index c5ba066fdd1..58438f4d732 100644 --- a/packages/mobile/README.md +++ b/packages/mobile/README.md @@ -181,6 +181,27 @@ GraphQL queries. If you make a change to a query, run `yarn build:gen-graphql-ty By default, the mobile wallet app runs geth in ultralight sync mode where all the epoch headers are fetched. The default sync mode is defined in [packages/mobile/.env](https://github.com/celo-org/celo-monorepo/blob/master/packages/mobile/.env#L4) file. To run wallet in zero sync mode, where it would connect to the remote nodes and sign transactions in web3, change the default sync mode in the aforementioned file to -1. The mode has only been tested on Android and is hard-coded to be [crash](https://github.com/celo-org/celo-monorepo/blob/aeddeefbfb230db51d2ef76d50c5f882644a1cd3/packages/mobile/src/web3/contracts.ts#L73) on iOS. +======= +## How we handle Geth crashes in wallet app on Android + +Our Celo app has three types of codes. + +1. Javascript code - generated from Typescript, this runs in Javascript interpreter. +2. Java bytecode - This runs on Dalvik/Art Virtual Machine. +3. Native code - Geth code is written in Golang which compiles to native code - this runs directly on the +CPU, no virtual machines involved. + +One should note that, on iOS, there is no byte code and therefore, there are only two layers, one is the Javascript code, and the other is the Native code. Till now, we have been blind towards native crashes except Google Playstore logs. + +Sentry, the crash logging mechanism we use, can catch both Javascript Errors as well as unhandled Java exceptions. It, however, does not catch Native crashes. There are quite a few tools to catch native crashes like [Bugsnag](https://www.bugsnag.com) and [Crashlytics](https://firebase.google.com/products/crashlytics). They would have worked for us under normal circumstances. However, the Geth code produced by the Gomobile library and Go compiler logs a major chunk of information about the crash at Error level and not at the Fatal level. We hypothesize that this leads to incomplete stack traces showing up in Google Play store health checks. This issue is [publicly known](https://github.com/golang/go/issues/25035) but has not been fixed. + +We cannot use libraries like [Bugsnag](https://www.bugsnag.com) since they do not allow us to extract logcat logs immediately after the crash. Therefore, We use [jndcrash](https://github.com/ivanarh/jndcrash), which uses [ndcrash](https://github.com/ivanarh/ndcrash) and enable us to log the logcat logs immediately after a native crash. We capture the results into a file and on next restart Sentry reads it. We need to do this two-step setup because once a native crash happens, running code to upload the data would be fragile. An error in sentry looks like [this](https://sentry.io/organizations/celo/issues/918120991/events/48285729031/) + +Relevant code references: + +1. [NDKCrashService](https://github.com/celo-org/celo-monorepo/blob/master/packages/mobile/android/app/src/main/java/org/celo/mobile/NdkCrashService.java) +2. [Initialization](https://github.com/celo-org/celo-monorepo/blob/8689634a1d10d74ba6d4f3b36b2484db60a95bdb/packages/mobile/android/app/src/main/java/org/celo/mobile/MainApplication.java#L156) of the NDKCrashService +3. [Sentry code](https://github.com/celo-org/celo-monorepo/blob/799d74675dc09327543c210e88cbf5cc796721a0/packages/mobile/src/sentry/Sentry.ts#L53) to read NDK crash logs on restart ## Troubleshooting From a2192b053418a5d6de175dd2eea55d374752485f Mon Sep 17 00:00:00 2001 From: Ashish Bhatia Date: Wed, 16 Oct 2019 17:53:09 -0700 Subject: [PATCH 28/37] [wallet]Add more documentation on ZeroSync mode (#1367) --- packages/mobile/README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/mobile/README.md b/packages/mobile/README.md index 58438f4d732..01eaff317a4 100644 --- a/packages/mobile/README.md +++ b/packages/mobile/README.md @@ -203,6 +203,15 @@ Relevant code references: 2. [Initialization](https://github.com/celo-org/celo-monorepo/blob/8689634a1d10d74ba6d4f3b36b2484db60a95bdb/packages/mobile/android/app/src/main/java/org/celo/mobile/MainApplication.java#L156) of the NDKCrashService 3. [Sentry code](https://github.com/celo-org/celo-monorepo/blob/799d74675dc09327543c210e88cbf5cc796721a0/packages/mobile/src/sentry/Sentry.ts#L53) to read NDK crash logs on restart +There are two major differencs in ZeroSync mode: + +1. Geth won't run at all. The web3 would instead connect to -infura.celo-testnet.org using an https provider, for example, [https://integration-infura.celo-testnet.org](https://integration-infura.celo-testnet.org). +2. Changes to [sendTransactionAsyncWithWeb3Signing](https://github.com/celo-org/celo-monorepo/blob/8689634a1d10d74ba6d4f3b36b2484db60a95bdb/packages/walletkit/src/contract-utils.ts#L362) in walletkit to poll after sending a transaction for the transaction to succeed. This is needed because http provider, unliked web sockets and IPC provider, does not support subscriptions. + +#### Why http(s) provider? + +Websockets (`ws`) would have been a better choicee but we cannot use unencrypted `ws` provider since it would be bad to send plain-text data from a privacy perspective. Geth does not support `wss` by [default](https://github.com/ethereum/go-ethereum/issues/16423). And Kubernetes does not support it either. This forced us to use https provider. + ## Troubleshooting ### `Activity class {org.celo.mobile.staging/org.celo.mobile.MainActivity} does not exist.` From f9a72a6db001b3d990f6e1f8b724111b0bba5dba Mon Sep 17 00:00:00 2001 From: Mariano Cortesi Date: Thu, 17 Oct 2019 13:51:30 -0300 Subject: [PATCH 29/37] Document npm inter-repo dependencies instructions (#1370) --- README-dev.md | 21 ++++++++++++ package.json | 3 +- scripts/check-packages.js | 69 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 scripts/check-packages.js diff --git a/README-dev.md b/README-dev.md index e854714392c..e8587875919 100644 --- a/README-dev.md +++ b/README-dev.md @@ -1,5 +1,26 @@ # README GUIDE FOR CELO DEVELOPERS +## Monorepo inter-package dependencies + +Many packages depend on other packages within the monorepo. When this happens, follow these rules: + + 1. All packages must use **master version** of sibling packages. + 2. Exception to (1) are packages that represent a GAE/firebase app which must use the last published version. + 3. To differentiate published vs unpublished version. Master version (in package.json) must end with suffix `-dev` and should not be published. + 4. If a developer want to publish a version; then after publishing it needs to set master version to next `-dev` version and change all package.json that require on it. + +To check which pakages need amending, you can run (in the root pkg): + + yarn check:packages + +A practical example: + + * In any given moment, `contractkit/package.json#version` field **must** of the form `x.y.z-dev` + * If current version of contractkit is: `0.1.6-dev` and we want to publish a new version, we should: + * publish version `0.1.6` + * change `package.json#version` to `0.1.7-dev` + * change in other packages within monorepo that were using `0.1.6-dev` to `0.1.7-dev` + ## How to publish a new npm package First checkout the alfajores branch. diff --git a/package.json b/package.json index c56245f2164..eec07c2a6cf 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,8 @@ "postinstall": "yarn run lerna run postinstall && patch-package && yarn keys:decrypt", "preinstall": "bash scripts/create_key_templates.sh", "keys:decrypt": "bash scripts/key_placer.sh decrypt", - "keys:encrypt": "bash scripts/key_placer.sh encrypt" + "keys:encrypt": "bash scripts/key_placer.sh encrypt", + "check:packages": "node ./scripts/check-packages.js" }, "husky": { "hooks": { diff --git a/scripts/check-packages.js b/scripts/check-packages.js new file mode 100644 index 00000000000..15447b31c31 --- /dev/null +++ b/scripts/check-packages.js @@ -0,0 +1,69 @@ +const fs = require('fs') +const path = require('path') +const chalk = require('chalk') + +const FirebaseOrGAEProjects = ['faucet', 'web', 'verification-pool-api', 'notification-service'] + +// Set CWD to monorepo root +process.cwd(path.join(__dirname, '..')) + +const pkgJsonPath = (name) => path.join('./packages', name, 'package.json') +const readJSON = (path) => JSON.parse(fs.readFileSync(path)) +const readPkgJson = (name) => readJSON(pkgJsonPath(name)) + +const extractVersionNumber = (semverString) => { + if (semverString[0] == '~' || semverString[0] == '^') { + return semverString.slice(1) + } else { + return semverString + } +} + +const npmPackages = fs + .readdirSync('./packages') + .filter((name) => fs.existsSync(pkgJsonPath(name))) + .filter((name) => !FirebaseOrGAEProjects.includes(name)) +const pkgJsons = npmPackages.map((name) => readPkgJson(name)) +const versionMap = new Map(pkgJsons.map((p) => [p.name, p.version])) + +function getErrors(pkgJson) { + const interDependencies = [] + .concat( + Object.keys(pkgJson.dependencies || {}) + .filter((name) => versionMap.has(name)) + .map((name) => [name, pkgJson.dependencies[name]]) + ) + .concat( + Object.keys(pkgJson.devDependencies || {}) + .filter((name) => versionMap.has(name)) + .map((name) => [name, pkgJson.devDependencies[name]]) + ) + + const errors = [] + for ([name, versionString] of interDependencies) { + if (extractVersionNumber(versionString) != versionMap.get(name)) { + errors.push({ + from: pkgJson.name, + to: name, + expected: versionMap.get(name), + got: versionString, + }) + } + } + return errors +} + +const errors = [].concat(...pkgJsons.map(getErrors).filter((err) => err.length > 0)) + +console.log( + errors + .map( + (err) => + chalk`{red ${err.from}} => {red ${err.to}}. expected: {blue ${err.expected}} got: {green ${ + err.got + }}` + ) + .join('\n') +) + +process.exit(errors.length > 0 ? 1 : 0) From 72ffef86cd4a42a47ddc20360d5eb5f4ba3e8c81 Mon Sep 17 00:00:00 2001 From: Anna K Date: Thu, 17 Oct 2019 12:51:15 -0700 Subject: [PATCH 30/37] [Wallet] Add Celo Lite toggle (UI only, zeroSync on/off in other PR) (#1369) --- .../mobile/locales/en-US/accountScreen10.json | 4 + .../locales/es-419/accountScreen10.json | 4 + packages/mobile/src/account/Account.tsx | 7 ++ .../mobile/src/account/Analytics.test.tsx | 17 ++++ packages/mobile/src/account/Analytics.tsx | 18 ---- packages/mobile/src/account/CeloLite.test.tsx | 17 ++++ packages/mobile/src/account/CeloLite.tsx | 65 +++++++++++++ packages/mobile/src/account/EditProfile.tsx | 7 -- packages/mobile/src/account/Education.tsx | 14 --- packages/mobile/src/account/Invite.tsx | 9 -- packages/mobile/src/account/Licenses.test.tsx | 17 ++++ packages/mobile/src/account/Profile.tsx | 23 ----- .../__snapshots__/Analytics.test.tsx.snap | 93 +++++++++++++++++++ .../__snapshots__/CeloLite.test.tsx.snap | 93 +++++++++++++++++++ .../__snapshots__/Licenses.test.tsx.snap | 50 ++++++++++ packages/mobile/src/navigator/Navigator.tsx | 2 + packages/mobile/src/navigator/Screens.tsx | 1 + packages/mobile/src/redux/sagas.ts | 2 + packages/mobile/src/web3/actions.ts | 14 +++ packages/mobile/src/web3/reducer.ts | 9 ++ packages/mobile/src/web3/saga.ts | 17 +++- packages/mobile/src/web3/selectors.ts | 1 + packages/mobile/test/schemas.ts | 1 + 23 files changed, 413 insertions(+), 72 deletions(-) create mode 100644 packages/mobile/src/account/Analytics.test.tsx create mode 100644 packages/mobile/src/account/CeloLite.test.tsx create mode 100644 packages/mobile/src/account/CeloLite.tsx create mode 100644 packages/mobile/src/account/Licenses.test.tsx create mode 100644 packages/mobile/src/account/__snapshots__/Analytics.test.tsx.snap create mode 100644 packages/mobile/src/account/__snapshots__/CeloLite.test.tsx.snap create mode 100644 packages/mobile/src/account/__snapshots__/Licenses.test.tsx.snap diff --git a/packages/mobile/locales/en-US/accountScreen10.json b/packages/mobile/locales/en-US/accountScreen10.json index 925756cb466..6faaba98784 100644 --- a/packages/mobile/locales/en-US/accountScreen10.json +++ b/packages/mobile/locales/en-US/accountScreen10.json @@ -10,6 +10,10 @@ "shareAnalytics": "Share Analytics", "shareAnalytics_detail": "We collect anonymized data about how you use Celo to help improve the application for everyone.", + "celoLite": "Celo Lite", + "enableCeloLite": "Enable Celo Lite", + "celoLiteDetail": + "Celo Lite mode allows you to communicate with the Celo Network through a trusted node. You can always change this mode in app settings.", "testFaqHere": "<0>Test FAQ is <1>here", "termsOfServiceHere": "<0>Terms of service are <1>here", "editProfile": "Edit Profile", diff --git a/packages/mobile/locales/es-419/accountScreen10.json b/packages/mobile/locales/es-419/accountScreen10.json index 6bc3d025916..acd51bba54f 100755 --- a/packages/mobile/locales/es-419/accountScreen10.json +++ b/packages/mobile/locales/es-419/accountScreen10.json @@ -10,6 +10,10 @@ "shareAnalytics": "Compartir estadisticas de uso", "shareAnalytics_detail": "Recopilamos datos anónimos sobre cómo utiliza Celo para ayudar a mejorar la aplicación para todos.", + "celoLite": "Celo Lite", + "enableCeloLite": "Habilitar Celo Lite", + "celoLiteDetail": + "El modo Celo Lite te permite comunicarte con la Red Celo a través de un nodo confiable. Puedes cambiar este modo en la configuración de la aplicación.", "testFaqHere": "<1>Aquí<0> están las preguntas frecuentes de la prueba. ", "termsOfServiceHere": "<1>Aquí<0> están las Condiciones de servicio.", "editProfile": "Editar perfil", diff --git a/packages/mobile/src/account/Account.tsx b/packages/mobile/src/account/Account.tsx index bd017c8beea..6865c11aff9 100644 --- a/packages/mobile/src/account/Account.tsx +++ b/packages/mobile/src/account/Account.tsx @@ -109,6 +109,10 @@ export class Account extends React.Component { navigate(Screens.Analytics, { nextScreen: Screens.Account }) } + goToCeloLite() { + navigate(Screens.CeloLite, { nextScreen: Screens.Account }) + } + goToFAQ() { navigateToURI(FAQ_LINK) } @@ -229,6 +233,9 @@ export class Account extends React.Component { )} + {/* // TODO(anna) Disabled until switch geth on/off is implemented + + */} { + it('renders correctly', () => { + const tree = renderer.create( + + + + ) + expect(tree).toMatchSnapshot() + }) +}) diff --git a/packages/mobile/src/account/Analytics.tsx b/packages/mobile/src/account/Analytics.tsx index 6eb0fc2ad04..33cfabcfa39 100644 --- a/packages/mobile/src/account/Analytics.tsx +++ b/packages/mobile/src/account/Analytics.tsx @@ -1,7 +1,6 @@ import SettingsSwitchItem from '@celo/react-components/components/SettingsSwitchItem' import colors from '@celo/react-components/styles/colors' import fontStyles from '@celo/react-components/styles/fonts' -import variables from '@celo/react-components/styles/variables' import * as React from 'react' import { WithNamespaces, withNamespaces } from 'react-i18next' import { ScrollView, StyleSheet, Text } from 'react-native' @@ -50,27 +49,10 @@ export class Analytics extends React.Component { } const style = StyleSheet.create({ - accountHeader: { - paddingTop: 20, - }, - input: { - borderWidth: 1, - borderRadius: 3, - borderColor: '#EEEEEE', - padding: 5, - height: 54, - margin: 20, - width: variables.width - 40, - fontSize: 16, - }, scrollView: { flex: 1, backgroundColor: colors.background, }, - container: { - flex: 1, - paddingLeft: 20, - }, }) export default connect( diff --git a/packages/mobile/src/account/CeloLite.test.tsx b/packages/mobile/src/account/CeloLite.test.tsx new file mode 100644 index 00000000000..84804279238 --- /dev/null +++ b/packages/mobile/src/account/CeloLite.test.tsx @@ -0,0 +1,17 @@ +import * as React from 'react' +import 'react-native' +import { Provider } from 'react-redux' +import * as renderer from 'react-test-renderer' +import CeloLite from 'src/account/CeloLite' +import { createMockStore } from 'test/utils' + +describe('CeloLite', () => { + it('renders correctly', () => { + const tree = renderer.create( + + + + ) + expect(tree).toMatchSnapshot() + }) +}) diff --git a/packages/mobile/src/account/CeloLite.tsx b/packages/mobile/src/account/CeloLite.tsx new file mode 100644 index 00000000000..2711ae78abb --- /dev/null +++ b/packages/mobile/src/account/CeloLite.tsx @@ -0,0 +1,65 @@ +import SettingsSwitchItem from '@celo/react-components/components/SettingsSwitchItem' +import colors from '@celo/react-components/styles/colors' +import fontStyles from '@celo/react-components/styles/fonts' +import * as React from 'react' +import { WithNamespaces, withNamespaces } from 'react-i18next' +import { ScrollView, StyleSheet, Text } from 'react-native' +import { connect } from 'react-redux' +import i18n, { Namespaces } from 'src/i18n' +import { headerWithCancelButton } from 'src/navigator/Headers' +import { RootState } from 'src/redux/reducers' +import { setZeroSyncMode } from 'src/web3/actions' + +interface StateProps { + zeroSyncEnabled: boolean +} + +interface DispatchProps { + setZeroSyncMode: typeof setZeroSyncMode +} + +type Props = StateProps & DispatchProps & WithNamespaces + +const mapDispatchToProps = { + setZeroSyncMode, +} + +const mapStateToProps = (state: RootState): StateProps => { + return { + zeroSyncEnabled: state.web3.zeroSyncMode, + } +} + +export class CeloLite extends React.Component { + static navigationOptions = () => ({ + ...headerWithCancelButton, + headerTitle: i18n.t('accountScreen10:celoLite'), + }) + + render() { + const { zeroSyncEnabled, t } = this.props + return ( + + + {t('enableCeloLite')} + + + ) + } +} + +const style = StyleSheet.create({ + scrollView: { + flex: 1, + backgroundColor: colors.background, + }, +}) + +export default connect( + mapStateToProps, + mapDispatchToProps +)(withNamespaces(Namespaces.accountScreen10)(CeloLite)) diff --git a/packages/mobile/src/account/EditProfile.tsx b/packages/mobile/src/account/EditProfile.tsx index 0b0fe4c3cb9..76b394338c3 100644 --- a/packages/mobile/src/account/EditProfile.tsx +++ b/packages/mobile/src/account/EditProfile.tsx @@ -73,9 +73,6 @@ export class EditProfile extends React.Component { } const style = StyleSheet.create({ - accountHeader: { - paddingTop: 20, - }, input: { borderWidth: 1, borderRadius: 3, @@ -90,10 +87,6 @@ const style = StyleSheet.create({ flex: 1, backgroundColor: colors.background, }, - container: { - flex: 1, - paddingLeft: 20, - }, }) export default connect( diff --git a/packages/mobile/src/account/Education.tsx b/packages/mobile/src/account/Education.tsx index 66a86b0385f..471d673e468 100644 --- a/packages/mobile/src/account/Education.tsx +++ b/packages/mobile/src/account/Education.tsx @@ -189,20 +189,6 @@ const style = StyleSheet.create({ flex: 1, paddingHorizontal: 20, }, - footerLink: { - color: colors.celoGreen, - textAlign: 'center', - marginTop: 10, - marginBottom: 25, - }, - circleContainer: { - flex: 0, - width: PROGRESS_CIRCLE_PASSIVE_SIZE, - height: PROGRESS_CIRCLE_PASSIVE_SIZE, - alignItems: 'center', - justifyContent: 'center', - margin: 5, - }, circle: { flex: 0, backgroundColor: colors.inactive, diff --git a/packages/mobile/src/account/Invite.tsx b/packages/mobile/src/account/Invite.tsx index 57739ab2989..0b8dd33ebd0 100644 --- a/packages/mobile/src/account/Invite.tsx +++ b/packages/mobile/src/account/Invite.tsx @@ -135,15 +135,6 @@ const style = StyleSheet.create({ flex: 1, backgroundColor: colors.background, }, - inviteHeadline: { - fontSize: 24, - lineHeight: 39, - color: colors.dark, - }, - label: { - alignSelf: 'center', - textAlign: 'center', - }, }) export default componentWithAnalytics( diff --git a/packages/mobile/src/account/Licenses.test.tsx b/packages/mobile/src/account/Licenses.test.tsx new file mode 100644 index 00000000000..fc603cf69e7 --- /dev/null +++ b/packages/mobile/src/account/Licenses.test.tsx @@ -0,0 +1,17 @@ +import * as React from 'react' +import 'react-native' +import { Provider } from 'react-redux' +import * as renderer from 'react-test-renderer' +import Licenses from 'src/account/Licenses' +import { createMockStore } from 'test/utils' + +describe('Licenses', () => { + it('renders correctly', () => { + const tree = renderer.create( + + + + ) + expect(tree).toMatchSnapshot() + }) +}) diff --git a/packages/mobile/src/account/Profile.tsx b/packages/mobile/src/account/Profile.tsx index fe0e4936533..ee712ead8b5 100644 --- a/packages/mobile/src/account/Profile.tsx +++ b/packages/mobile/src/account/Profile.tsx @@ -65,9 +65,6 @@ export class Profile extends React.Component { } const style = StyleSheet.create({ - accountHeader: { - paddingTop: 20, - }, accountProfile: { paddingLeft: 10, paddingTop: 30, @@ -76,26 +73,6 @@ const style = StyleSheet.create({ flexDirection: 'column', alignItems: 'center', }, - accountFooter: { - flex: 1, - flexDirection: 'row', - justifyContent: 'center', - alignItems: 'center', - height: 50, - margin: 10, - }, - accountFooterText: { - paddingBottom: 10, - }, - editProfileButton: { - height: 28, - width: 110, - }, - image: { - height: 55, - width: 55, - borderRadius: 50, - }, underlinedBox: { borderTopWidth: 1, borderColor: '#EEEEEE', diff --git a/packages/mobile/src/account/__snapshots__/Analytics.test.tsx.snap b/packages/mobile/src/account/__snapshots__/Analytics.test.tsx.snap new file mode 100644 index 00000000000..74b75f43f3e --- /dev/null +++ b/packages/mobile/src/account/__snapshots__/Analytics.test.tsx.snap @@ -0,0 +1,93 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Analytics renders correctly 1`] = ` + + + + + + + shareAnalytics + + + + + + + + + shareAnalytics_detail + + + + + +`; diff --git a/packages/mobile/src/account/__snapshots__/CeloLite.test.tsx.snap b/packages/mobile/src/account/__snapshots__/CeloLite.test.tsx.snap new file mode 100644 index 00000000000..0a3080730d9 --- /dev/null +++ b/packages/mobile/src/account/__snapshots__/CeloLite.test.tsx.snap @@ -0,0 +1,93 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CeloLite renders correctly 1`] = ` + + + + + + + enableCeloLite + + + + + + + + + celoLiteDetail + + + + + +`; diff --git a/packages/mobile/src/account/__snapshots__/Licenses.test.tsx.snap b/packages/mobile/src/account/__snapshots__/Licenses.test.tsx.snap new file mode 100644 index 00000000000..3c6640ba55c --- /dev/null +++ b/packages/mobile/src/account/__snapshots__/Licenses.test.tsx.snap @@ -0,0 +1,50 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Licenses renders correctly 1`] = ` + + + +`; diff --git a/packages/mobile/src/navigator/Navigator.tsx b/packages/mobile/src/navigator/Navigator.tsx index df1f10a5988..ed22c7231ff 100644 --- a/packages/mobile/src/navigator/Navigator.tsx +++ b/packages/mobile/src/navigator/Navigator.tsx @@ -2,6 +2,7 @@ import { Platform } from 'react-native' import { createStackNavigator, createSwitchNavigator, StackNavigatorConfig } from 'react-navigation' import Account from 'src/account/Account' import Analytics from 'src/account/Analytics' +import CeloLite from 'src/account/CeloLite' import DollarEducation from 'src/account/DollarEducation' import EditProfile from 'src/account/EditProfile' import GoldEducation from 'src/account/GoldEducation' @@ -152,6 +153,7 @@ const AppStack = createStackNavigator( [Stacks.RequestStack]: { screen: RequestStack }, [Screens.Language]: { screen: Language }, [Screens.Analytics]: { screen: Analytics }, + [Screens.CeloLite]: { screen: CeloLite }, [Screens.SetClock]: { screen: SetClock }, [Screens.EditProfile]: { screen: EditProfile }, [Screens.Profile]: { screen: Profile }, diff --git a/packages/mobile/src/navigator/Screens.tsx b/packages/mobile/src/navigator/Screens.tsx index 7c2fc7347d1..4f19dbeb6b8 100644 --- a/packages/mobile/src/navigator/Screens.tsx +++ b/packages/mobile/src/navigator/Screens.tsx @@ -11,6 +11,7 @@ export enum Screens { Account = 'Account', Analytics = 'Analytics', Backup = 'Backup', + CeloLite = 'CeloLite', DappKitAccountAuth = 'DappKitAccountAuth', DappKitSignTxScreen = 'DappKitSignTxScreen', DappKitTxDataScreen = 'DappKitTxDataScreen', diff --git a/packages/mobile/src/redux/sagas.ts b/packages/mobile/src/redux/sagas.ts index 798967ac62f..fe3250573ac 100644 --- a/packages/mobile/src/redux/sagas.ts +++ b/packages/mobile/src/redux/sagas.ts @@ -19,6 +19,7 @@ import { networkInfoSaga } from 'src/networkInfo/saga' import { sendSaga } from 'src/send/saga' import { stableTokenSaga } from 'src/stableToken/saga' import Logger from 'src/utils/Logger' +import { web3Saga } from 'src/web3/saga' const loggerBlacklist = [ 'persist/REHYDRATE', @@ -75,4 +76,5 @@ export function* rootSaga() { yield spawn(dappKitSaga) yield spawn(feesSaga) yield spawn(localCurrencySaga) + yield spawn(web3Saga) } diff --git a/packages/mobile/src/web3/actions.ts b/packages/mobile/src/web3/actions.ts index fad10e98b10..3726b9a3088 100644 --- a/packages/mobile/src/web3/actions.ts +++ b/packages/mobile/src/web3/actions.ts @@ -10,6 +10,7 @@ export enum Actions { SET_COMMENT_KEY = 'WEB3/SET_COMMENT_KEY', SET_PROGRESS = 'WEB3/SET_PROGRESS', SET_IS_READY = 'WEB3/SET_IS_READY', + SET_IS_ZERO_SYNC = 'WEB3/SET_IS_ZERO_SYNC', SET_BLOCK_NUMBER = 'WEB3/SET_BLOCK_NUMBER', REQUEST_SYNC_PROGRESS = 'WEB3/REQUEST_SYNC_PROGRESS', UPDATE_WEB3_SYNC_PROGRESS = 'WEB3/UPDATE_WEB3_SYNC_PROGRESS', @@ -20,6 +21,11 @@ export interface SetAccountAction { address: string } +export interface SetIsZeroSyncAction { + type: Actions.SET_IS_ZERO_SYNC + zeroSyncMode: boolean +} + export interface SetCommentKeyAction { type: Actions.SET_COMMENT_KEY commentKey: string @@ -41,6 +47,7 @@ export interface UpdateWeb3SyncProgressAction { export type ActionTypes = | SetAccountAction + | SetIsZeroSyncAction | SetCommentKeyAction | SetLatestBlockNumberAction | UpdateWeb3SyncProgressAction @@ -53,6 +60,13 @@ export const setAccount = (address: string): SetAccountAction => { } } +export const setZeroSyncMode = (zeroSyncMode: boolean): SetIsZeroSyncAction => { + return { + type: Actions.SET_IS_ZERO_SYNC, + zeroSyncMode, + } +} + export const setPrivateCommentKey = (commentKey: string): SetCommentKeyAction => { return { type: Actions.SET_COMMENT_KEY, diff --git a/packages/mobile/src/web3/reducer.ts b/packages/mobile/src/web3/reducer.ts index 7c10b037aba..68184ca2667 100644 --- a/packages/mobile/src/web3/reducer.ts +++ b/packages/mobile/src/web3/reducer.ts @@ -1,3 +1,5 @@ +import { GethSyncMode } from 'src/geth/consts' +import networkConfig from 'src/geth/networkConfig' import { getRehydratePayload, REHYDRATE, RehydrateAction } from 'src/redux/persist-helper' import { Actions, ActionTypes } from 'src/web3/actions' @@ -10,6 +12,7 @@ export interface State { latestBlockNumber: number account: string | null commentKey: string | null + zeroSyncMode: boolean } const initialState: State = { @@ -21,6 +24,7 @@ const initialState: State = { latestBlockNumber: 0, account: null, commentKey: null, + zeroSyncMode: networkConfig.syncMode === GethSyncMode.ZeroSync, } export const reducer = ( @@ -46,6 +50,11 @@ export const reducer = ( ...state, account: action.address, } + case Actions.SET_IS_ZERO_SYNC: + return { + ...state, + zeroSyncMode: action.zeroSyncMode, + } case Actions.SET_COMMENT_KEY: return { ...state, diff --git a/packages/mobile/src/web3/saga.ts b/packages/mobile/src/web3/saga.ts index cde696e55de..fc5550dbef4 100644 --- a/packages/mobile/src/web3/saga.ts +++ b/packages/mobile/src/web3/saga.ts @@ -4,7 +4,7 @@ import * as Crypto from 'crypto' import { generateMnemonic, mnemonicToSeedHex } from 'react-native-bip39' import * as RNFS from 'react-native-fs' import { REHYDRATE } from 'redux-persist/es/constants' -import { call, delay, put, race, select, take } from 'redux-saga/effects' +import { call, delay, put, race, select, spawn, take, takeLatest } from 'redux-saga/effects' import { setAccountCreationTime } from 'src/account/actions' import { getPincode } from 'src/account/saga' import CeloAnalytics from 'src/analytics/CeloAnalytics' @@ -22,6 +22,7 @@ import { Actions, getLatestBlock, setAccount, + SetIsZeroSyncAction, setLatestBlockNumber, setPrivateCommentKey, updateWeb3SyncProgress, @@ -344,3 +345,17 @@ export function* getConnectedUnlockedAccount() { throw new Error(ErrorMessages.INCORRECT_PIN) } } + +export function* switchZeroSyncMode(action: SetIsZeroSyncAction) { + Logger.info(TAG + '@switchZeroSyncMode', `Zero sync mode will change to: ${action.zeroSyncMode}`) + // TODO(anna) implement switching geth on/off, changing web3 provider + return true +} + +export function* watchZeroSyncMode() { + yield takeLatest(Actions.SET_IS_ZERO_SYNC, switchZeroSyncMode) +} + +export function* web3Saga() { + yield spawn(watchZeroSyncMode) +} diff --git a/packages/mobile/src/web3/selectors.ts b/packages/mobile/src/web3/selectors.ts index bb30016381d..90495276a22 100644 --- a/packages/mobile/src/web3/selectors.ts +++ b/packages/mobile/src/web3/selectors.ts @@ -1,5 +1,6 @@ import { RootState } from 'src/redux/reducers' export const currentAccountSelector = (state: RootState) => state.web3.account +export const zeroSyncSelector = (state: RootState) => state.web3.zeroSyncMode export const privateCommentKeySelector = (state: RootState) => state.web3.commentKey diff --git a/packages/mobile/test/schemas.ts b/packages/mobile/test/schemas.ts index f4f259f0fa7..1ced74f71bc 100644 --- a/packages/mobile/test/schemas.ts +++ b/packages/mobile/test/schemas.ts @@ -56,6 +56,7 @@ export const vNeg1Schema = { account: '0x0000000000000000000000000000000000007E57', commentKey: '0x0000000000000000000000000000000000008F68', gasPriceLastUpdated: 0, + zeroSyncMode: false, }, identity: { attestationCodes: [], From 43c9ebf475578829c92f157b81c65764ef71054c Mon Sep 17 00:00:00 2001 From: Jean Regisser Date: Fri, 18 Oct 2019 01:33:01 +0200 Subject: [PATCH 31/37] [Wallet] Enable push notifications on iOS (#1389) --- .../mobile/ios/celo.xcodeproj/project.pbxproj | 3 +++ packages/mobile/ios/celo/AppDelegate.m | 16 ++++++++++++++++ packages/mobile/ios/celo/Info.plist | 4 ++++ 3 files changed, 23 insertions(+) diff --git a/packages/mobile/ios/celo.xcodeproj/project.pbxproj b/packages/mobile/ios/celo.xcodeproj/project.pbxproj index 4078f41ebcf..abe887e2507 100644 --- a/packages/mobile/ios/celo.xcodeproj/project.pbxproj +++ b/packages/mobile/ios/celo.xcodeproj/project.pbxproj @@ -313,6 +313,9 @@ DevelopmentTeam = HDPUB8C3KG; LastSwiftMigration = 1020; SystemCapabilities = { + com.apple.BackgroundModes = { + enabled = 1; + }; com.apple.Push = { enabled = 1; }; diff --git a/packages/mobile/ios/celo/AppDelegate.m b/packages/mobile/ios/celo/AppDelegate.m index 46aac86390a..d1c06486b66 100644 --- a/packages/mobile/ios/celo/AppDelegate.m +++ b/packages/mobile/ios/celo/AppDelegate.m @@ -20,12 +20,15 @@ #endif @import Firebase; +#import "RNFirebaseNotifications.h" +#import "RNFirebaseMessaging.h" @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [FIRApp configure]; + [RNFirebaseNotifications configure]; RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions]; RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge moduleName:@"celo" @@ -52,4 +55,17 @@ - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge #endif } +- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification { + [[RNFirebaseNotifications instance] didReceiveLocalNotification:notification]; +} + +- (void)application:(UIApplication *)application didReceiveRemoteNotification:(nonnull NSDictionary *)userInfo +fetchCompletionHandler:(nonnull void (^)(UIBackgroundFetchResult))completionHandler{ + [[RNFirebaseNotifications instance] didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler]; +} + +- (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings { + [[RNFirebaseMessaging instance] didRegisterUserNotificationSettings:notificationSettings]; +} + @end diff --git a/packages/mobile/ios/celo/Info.plist b/packages/mobile/ios/celo/Info.plist index 1aca0e103f2..b44a9fa7dca 100644 --- a/packages/mobile/ios/celo/Info.plist +++ b/packages/mobile/ios/celo/Info.plist @@ -51,6 +51,10 @@ Hind-Regular.ttf Hind-SemiBold.ttf + UIBackgroundModes + + remote-notification + UILaunchStoryboardName LaunchScreen UIRequiredDeviceCapabilities From 5d7db195a621a5c44ca20240df04e5f19fccbb15 Mon Sep 17 00:00:00 2001 From: Jean Regisser Date: Fri, 18 Oct 2019 15:48:45 +0200 Subject: [PATCH 32/37] [Wallet] New camera permission flow (#1398) --- packages/mobile/locales/en-US/sendFlow7.json | 7 +- packages/mobile/locales/es-419/sendFlow7.json | 8 +- .../src/qrcode/NotAuthorizedView.test.tsx | 11 ++ .../mobile/src/qrcode/NotAuthorizedView.tsx | 53 ++++++++ packages/mobile/src/qrcode/QRScanner.tsx | 113 +++++++--------- .../NotAuthorizedView.test.tsx.snap | 124 ++++++++++++++++++ .../mobile/src/utils/permissions.android.ts | 8 -- packages/mobile/src/utils/permissions.ios.ts | 10 -- 8 files changed, 247 insertions(+), 87 deletions(-) create mode 100644 packages/mobile/src/qrcode/NotAuthorizedView.test.tsx create mode 100644 packages/mobile/src/qrcode/NotAuthorizedView.tsx create mode 100644 packages/mobile/src/qrcode/__snapshots__/NotAuthorizedView.test.tsx.snap diff --git a/packages/mobile/locales/en-US/sendFlow7.json b/packages/mobile/locales/en-US/sendFlow7.json index f28d4b66aea..3ed8c4b8b80 100644 --- a/packages/mobile/locales/en-US/sendFlow7.json +++ b/packages/mobile/locales/en-US/sendFlow7.json @@ -55,8 +55,11 @@ "scanCode": "Scan Code", "writeStorageNeededForQrDownload": "Storage write permission is needed for downloading the QR code", - "ScanCodeByPlacingItInTheBox": "Scan code by placing it in the box", - "needCameraPermissionToScan": "App needs camera permission to scan QR codes", + "cameraScanInfo": "Scan code by placing it in the box", + "cameraNotAuthorizedTitle": "Enable Camera", + "cameraNotAuthorizedDescription": + "Please enable the Camera in your phone’s Settings. You’ll need it to scan QR codes.", + "cameraSettings": "Settings", "showYourQRCode": "Show your QR code", "toSentOrRequestPayment": "to send or request payment", "requestSent": "Request Sent", diff --git a/packages/mobile/locales/es-419/sendFlow7.json b/packages/mobile/locales/es-419/sendFlow7.json index d804e2d7573..3981f806ff9 100755 --- a/packages/mobile/locales/es-419/sendFlow7.json +++ b/packages/mobile/locales/es-419/sendFlow7.json @@ -55,9 +55,11 @@ "scanCode": "Escanear código", "writeStorageNeededForQrDownload": "Se necesita permiso de escritura de almacenamiento para descargar el código QR", - "ScanCodeByPlacingItInTheBox": "Escanee el código colocándolo en la caja", - "needCameraPermissionToScan": - "La aplicación necesita permiso de la cámara para escanear códigos QR", + "cameraScanInfo": "Escanee el código colocándolo en la caja", + "cameraNotAuthorizedTitle": "Habilitar Cámara", + "cameraNotAuthorizedDescription": + "Habilite la cámara en la configuración de su teléfono. Lo necesitará para escanear códigos QR.", + "cameraSettings": "Configuraciones", "showYourQRCode": "Muestra tu código QR", "toSentOrRequestPayment": "enviar o solicitar pago", "requestSent": "Solicitud Enviada", diff --git a/packages/mobile/src/qrcode/NotAuthorizedView.test.tsx b/packages/mobile/src/qrcode/NotAuthorizedView.test.tsx new file mode 100644 index 00000000000..aa2dee1a75e --- /dev/null +++ b/packages/mobile/src/qrcode/NotAuthorizedView.test.tsx @@ -0,0 +1,11 @@ +import * as React from 'react' +import { render } from 'react-native-testing-library' +import NotAuthorizedView from 'src/qrcode/NotAuthorizedView' + +describe('NotAuthorizedView', () => { + it('renders correctly', () => { + const { toJSON } = render() + + expect(toJSON()).toMatchSnapshot() + }) +}) diff --git a/packages/mobile/src/qrcode/NotAuthorizedView.tsx b/packages/mobile/src/qrcode/NotAuthorizedView.tsx new file mode 100644 index 00000000000..0d1fdae5113 --- /dev/null +++ b/packages/mobile/src/qrcode/NotAuthorizedView.tsx @@ -0,0 +1,53 @@ +import Button, { BtnTypes } from '@celo/react-components/components/Button' +import fontStyles from '@celo/react-components/styles/fonts' +import React, { useCallback } from 'react' +import { withNamespaces, WithNamespaces } from 'react-i18next' +import { Platform, StyleSheet, Text, View } from 'react-native' +import * as AndroidOpenSettings from 'react-native-android-open-settings' +import { Namespaces } from 'src/i18n' +import { navigateToURI } from 'src/utils/linking' + +type Props = WithNamespaces + +function NotAuthorizedView({ t }: Props) { + const onPressSettings = useCallback(() => { + if (Platform.OS === 'ios') { + navigateToURI('app-settings:') + } else if (Platform.OS === 'android') { + AndroidOpenSettings.appDetailsSettings() + } + }, []) + + return ( + + {t('cameraNotAuthorizedTitle')} + {t('cameraNotAuthorizedDescription')} + + + ) } } @@ -116,6 +109,9 @@ const styles = StyleSheet.create({ container: { flex: 1, }, + innerContainer: { + flex: 1, + }, preview: { flex: 1, justifyContent: 'flex-end', @@ -125,51 +121,43 @@ const styles = StyleSheet.create({ height: 200, width: 200, borderRadius: 4, - zIndex: 99, }, view: { flex: 1, - position: 'absolute', - top: 0, - left: 0, - right: 0, - bottom: 0, alignItems: 'center', justifyContent: 'center', }, - viewFillVertical: { + fillVertical: { backgroundColor: 'rgba(46, 51, 56, 0.3)', width: variables.width, flex: 1, }, - viewFillHorizontal: { + fillHorizontal: { backgroundColor: 'rgba(46, 51, 56, 0.3)', flex: 1, }, - viewCameraRow: { + cameraRow: { display: 'flex', flexDirection: 'row', }, - viewCameraContainer: { + cameraContainer: { height: 200, }, - viewInfoBox: { + infoBox: { paddingVertical: 9, paddingHorizontal: 5, backgroundColor: colors.dark, opacity: 1, marginTop: 15, + borderRadius: 3, + }, + infoText: { + ...fontStyles.bodySmall, + lineHeight: undefined, color: colors.white, - zIndex: 99, }, footerContainer: { - height: 50, - width: variables.width, - backgroundColor: 'white', - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - textAlign: 'center', + backgroundColor: colors.background, }, footerIcon: { borderWidth: 1, @@ -177,9 +165,6 @@ const styles = StyleSheet.create({ borderColor: colors.celoGreen, padding: 4, }, - footerText: { - color: colors.celoGreen, - }, }) export default componentWithAnalytics( diff --git a/packages/mobile/src/qrcode/__snapshots__/NotAuthorizedView.test.tsx.snap b/packages/mobile/src/qrcode/__snapshots__/NotAuthorizedView.test.tsx.snap new file mode 100644 index 00000000000..e5339bf5932 --- /dev/null +++ b/packages/mobile/src/qrcode/__snapshots__/NotAuthorizedView.test.tsx.snap @@ -0,0 +1,124 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`NotAuthorizedView renders correctly 1`] = ` + + + cameraNotAuthorizedTitle + + + cameraNotAuthorizedDescription + + + + + + cameraSettings + + + + + +`; diff --git a/packages/mobile/src/utils/permissions.android.ts b/packages/mobile/src/utils/permissions.android.ts index c7dce05db5d..1f343546880 100644 --- a/packages/mobile/src/utils/permissions.android.ts +++ b/packages/mobile/src/utils/permissions.android.ts @@ -21,18 +21,10 @@ export async function requestContactsPermission() { ) } -export async function requestCameraPermission() { - return requestPermission(PermissionsAndroid.PERMISSIONS.CAMERA) -} - export async function checkContactsPermission() { return PermissionsAndroid.check(PermissionsAndroid.PERMISSIONS.READ_CONTACTS) } -export async function checkCameraPermission() { - return PermissionsAndroid.check(PermissionsAndroid.PERMISSIONS.CAMERA) -} - async function requestPermission(permission: Permission, title?: string, message?: string) { try { const granted = await PermissionsAndroid.request( diff --git a/packages/mobile/src/utils/permissions.ios.ts b/packages/mobile/src/utils/permissions.ios.ts index 4ee9cb4562d..fbf540ef8ba 100644 --- a/packages/mobile/src/utils/permissions.ios.ts +++ b/packages/mobile/src/utils/permissions.ios.ts @@ -17,11 +17,6 @@ export async function requestContactsPermission(): Promise { }) } -export async function requestCameraPermission() { - throw new Error('Unimplemented method') - return false -} - export async function checkContactsPermission(): Promise { return new Promise((resolve, reject) => { Contacts.checkPermission((err, permission) => { @@ -33,8 +28,3 @@ export async function checkContactsPermission(): Promise { }) }) } - -export async function checkCameraPermission() { - throw new Error('Unimplemented method') - return false -} From 3805194cebb8098c39e1ee2fdd47d00986a29077 Mon Sep 17 00:00:00 2001 From: Aaron DeRuvo Date: Mon, 21 Oct 2019 12:54:02 -0700 Subject: [PATCH 33/37] Update Footer (#1331) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add link to “terms” page on the footer * Add Discord to universal footer * Lint fix --- packages/web/src/shared/Button.3.tsx | 1 + packages/web/src/shared/Footer.3.tsx | 19 ++++++++++++++++++- packages/web/src/shared/menu-items.ts | 4 ++++ packages/web/static/locales/en/common.json | 3 ++- 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/packages/web/src/shared/Button.3.tsx b/packages/web/src/shared/Button.3.tsx index 0cf933e5881..b2867ba2125 100644 --- a/packages/web/src/shared/Button.3.tsx +++ b/packages/web/src/shared/Button.3.tsx @@ -45,6 +45,7 @@ type PrimaryProps = { type InlineProps = { kind: BTN.INLINE + style?: TextStyle } & AllButtonProps type NavProps = { diff --git a/packages/web/src/shared/Footer.3.tsx b/packages/web/src/shared/Footer.3.tsx index d476b00ac3c..a7e5edd7580 100644 --- a/packages/web/src/shared/Footer.3.tsx +++ b/packages/web/src/shared/Footer.3.tsx @@ -1,7 +1,8 @@ import Link from 'next/link' import * as React from 'react' import { StyleSheet, Text, View } from 'react-native' -import { I18nProps, NameSpaces, withNamespaces } from 'src/i18n' +import { I18nProps, NameSpaces, Trans, withNamespaces } from 'src/i18n' +import Discord from 'src/icons/Discord' import Discourse from 'src/icons/Discourse' import MediumLogo from 'src/icons/MediumLogo' import Octocat from 'src/icons/Octocat' @@ -70,6 +71,11 @@ const Social = React.memo(function _Social() { + + + + + ) @@ -132,6 +138,13 @@ const Details = React.memo(function _Details({ t }: { t: I18nProps['t'] }) { {t('disclaimer')} + + + + Terms of Service + + + {t('copyRight')} @@ -139,6 +152,10 @@ const Details = React.memo(function _Details({ t }: { t: I18nProps['t'] }) { ) }) +function LinkButon({ children }) { + return