From 7cbe2bd34e912196cf23f4eff28c3e5f489070c1 Mon Sep 17 00:00:00 2001 From: Eela Nagaraj <7308464+eelanagaraj@users.noreply.github.com> Date: Mon, 11 Apr 2022 17:14:18 +0200 Subject: [PATCH 01/30] ASv2 Scaffolding (#9430) * Add barebones FederatedAttestations contract, proxy, and interface * Make TODOs ASv2 specific * Add FA usages to existing files * Add skeleton migration file * Add test scaffolding * Add CK wrapper and test --- .../identity/FederatedAttestations.sol | 60 +++++++++++++++++++ .../interfaces/IFederatedAttestations.sol | 5 ++ .../proxies/FederatedAttestationsProxy.sol | 6 ++ packages/protocol/lib/registry-utils.ts | 3 + packages/protocol/lib/test-utils.ts | 2 + packages/protocol/migrations/25_governance.ts | 1 + .../migrations/27_federated_attestations.ts | 15 +++++ packages/protocol/migrationsConfig.js | 2 + .../protocol/scripts/bash/backupmigrations.sh | 1 + packages/protocol/scripts/build.ts | 2 + .../test/identity/federatedattestations.ts | 43 +++++++++++++ packages/sdk/contractkit/src/base.ts | 1 + .../sdk/contractkit/src/contract-cache.ts | 11 ++++ packages/sdk/contractkit/src/kit.ts | 5 ++ packages/sdk/contractkit/src/proxy.ts | 2 + .../contractkit/src/web3-contract-cache.ts | 5 ++ .../wrappers/FederatedAttestations.test.ts | 26 ++++++++ .../src/wrappers/FederatedAttestations.ts | 8 +++ 18 files changed, 198 insertions(+) create mode 100644 packages/protocol/contracts/identity/FederatedAttestations.sol create mode 100644 packages/protocol/contracts/identity/interfaces/IFederatedAttestations.sol create mode 100644 packages/protocol/contracts/identity/proxies/FederatedAttestationsProxy.sol create mode 100644 packages/protocol/migrations/27_federated_attestations.ts create mode 100644 packages/protocol/test/identity/federatedattestations.ts create mode 100644 packages/sdk/contractkit/src/wrappers/FederatedAttestations.test.ts create mode 100644 packages/sdk/contractkit/src/wrappers/FederatedAttestations.ts diff --git a/packages/protocol/contracts/identity/FederatedAttestations.sol b/packages/protocol/contracts/identity/FederatedAttestations.sol new file mode 100644 index 00000000000..3483cf0b12f --- /dev/null +++ b/packages/protocol/contracts/identity/FederatedAttestations.sol @@ -0,0 +1,60 @@ +pragma solidity ^0.5.13; + +import "openzeppelin-solidity/contracts/math/SafeMath.sol"; +import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; +import "openzeppelin-solidity/contracts/token/ERC20/IERC20.sol"; +import "openzeppelin-solidity/contracts/utils/SafeCast.sol"; + +import "./interfaces/IFederatedAttestations.sol"; +import "../common/interfaces/IAccounts.sol"; +import "../common/interfaces/ICeloVersionedContract.sol"; + +import "../common/Initializable.sol"; +import "../common/UsingRegistry.sol"; +import "../common/Signatures.sol"; +import "../common/UsingPrecompiles.sol"; +import "../common/libraries/ReentrancyGuard.sol"; + +/** + * @title Contract mapping identifiers to accounts + */ +contract FederatedAttestations is + IFederatedAttestations, + ICeloVersionedContract, + Ownable, + Initializable, + UsingRegistry, + ReentrancyGuard, + UsingPrecompiles +{ + using SafeMath for uint256; + using SafeCast for uint256; + + // TODO ASv2 State var declarations + + // TODO ASv2 Event declarations + + /** + * @notice Sets initialized == true on implementation contracts + * @param test Set to true to skip implementation initialization + */ + constructor(bool test) public Initializable(test) {} + + /** + * @notice Used in place of the constructor to allow the contract to be upgradable via proxy. + * @param registryAddress The address of the registry core smart contract. + */ + function initialize(address registryAddress) external initializer { + _transferOwnership(msg.sender); + setRegistry(registryAddress); + // TODO ASv2 initialize any other variables here + } + + /** + * @notice Returns the storage, major, minor, and patch version of the contract. + * @return The storage, major, minor, and patch version of the contract. + */ + function getVersionNumber() external pure returns (uint256, uint256, uint256, uint256) { + return (1, 1, 0, 0); + } +} diff --git a/packages/protocol/contracts/identity/interfaces/IFederatedAttestations.sol b/packages/protocol/contracts/identity/interfaces/IFederatedAttestations.sol new file mode 100644 index 00000000000..d72986621d7 --- /dev/null +++ b/packages/protocol/contracts/identity/interfaces/IFederatedAttestations.sol @@ -0,0 +1,5 @@ +pragma solidity ^0.5.13; + +// TODO ASv2 add external, view, and only owner function sigs +// separated into these three groups for clarity +interface IFederatedAttestations {} diff --git a/packages/protocol/contracts/identity/proxies/FederatedAttestationsProxy.sol b/packages/protocol/contracts/identity/proxies/FederatedAttestationsProxy.sol new file mode 100644 index 00000000000..6edd4884f1a --- /dev/null +++ b/packages/protocol/contracts/identity/proxies/FederatedAttestationsProxy.sol @@ -0,0 +1,6 @@ +pragma solidity ^0.5.13; + +import "../../common/Proxy.sol"; + +/* solhint-disable no-empty-blocks */ +contract FederatedAttestationsProxy is Proxy {} diff --git a/packages/protocol/lib/registry-utils.ts b/packages/protocol/lib/registry-utils.ts index d6dae2db095..5d4aa040bff 100644 --- a/packages/protocol/lib/registry-utils.ts +++ b/packages/protocol/lib/registry-utils.ts @@ -20,6 +20,7 @@ export enum CeloContractName { Exchange = 'Exchange', ExchangeEUR = 'ExchangeEUR', ExchangeBRL = 'ExchangeBRL', + FederatedAttestations = 'FederatedAttestations', FeeCurrencyWhitelist = 'FeeCurrencyWhitelist', Freezer = 'Freezer', GasPriceMinimum = 'GasPriceMinimum', @@ -55,6 +56,8 @@ export const hasEntryInRegistry: string[] = [ CeloContractName.Election, CeloContractName.Escrow, CeloContractName.Exchange, + // TODO ASv2 revisit this + CeloContractName.FederatedAttestations, CeloContractName.FeeCurrencyWhitelist, CeloContractName.Freezer, CeloContractName.GasPriceMinimum, diff --git a/packages/protocol/lib/test-utils.ts b/packages/protocol/lib/test-utils.ts index be796bb82ae..9a5c58237c5 100644 --- a/packages/protocol/lib/test-utils.ts +++ b/packages/protocol/lib/test-utils.ts @@ -375,6 +375,7 @@ export const isSameAddress = (minerAddress, otherAddress) => { // TODO(amy): Pull this list from the build artifacts instead export const proxiedContracts: string[] = [ 'Attestations', + // TODO ASv2 revisit if we need to update test-utils 'Escrow', 'GoldToken', 'Registry', @@ -386,6 +387,7 @@ export const proxiedContracts: string[] = [ // TODO(asa): Pull this list from the build artifacts instead export const ownedContracts: string[] = [ 'Attestations', + // TODO ASv2 revisit if we need to update test-utils 'Escrow', 'Exchange', 'Registry', diff --git a/packages/protocol/migrations/25_governance.ts b/packages/protocol/migrations/25_governance.ts index 78a731eb053..e91ad42fc0e 100644 --- a/packages/protocol/migrations/25_governance.ts +++ b/packages/protocol/migrations/25_governance.ts @@ -83,6 +83,7 @@ module.exports = deploymentForCoreContract( 'Escrow', 'Exchange', 'ExchangeEUR', + // TODO ASv2 revisit 'FeeCurrencyWhitelist', 'Freezer', 'GasPriceMinimum', diff --git a/packages/protocol/migrations/27_federated_attestations.ts b/packages/protocol/migrations/27_federated_attestations.ts new file mode 100644 index 00000000000..ad25500c0a6 --- /dev/null +++ b/packages/protocol/migrations/27_federated_attestations.ts @@ -0,0 +1,15 @@ +import { CeloContractName } from '@celo/protocol/lib/registry-utils' +import { deploymentForCoreContract } from '@celo/protocol/lib/web3-utils' +import { config } from '@celo/protocol/migrationsConfig' +import { FederatedAttestationsInstance } from 'types' + +const initializeArgs = async (): Promise<[string]> => { + return [config.registry.predeployedProxyAddress] +} + +module.exports = deploymentForCoreContract( + web3, + artifacts, + CeloContractName.FederatedAttestations, + initializeArgs +) diff --git a/packages/protocol/migrationsConfig.js b/packages/protocol/migrationsConfig.js index 6df3ee5d3f2..799b124efd3 100644 --- a/packages/protocol/migrationsConfig.js +++ b/packages/protocol/migrationsConfig.js @@ -26,6 +26,8 @@ const DAY = 24 * HOUR const WEEK = 7 * DAY const YEAR = 365 * DAY +// TODO ASv2 + const DefaultConfig = { attestations: { attestationExpiryBlocks: HOUR / 5, // ~1 hour, diff --git a/packages/protocol/scripts/bash/backupmigrations.sh b/packages/protocol/scripts/bash/backupmigrations.sh index e7005a1f8e7..a5cfedf501e 100755 --- a/packages/protocol/scripts/bash/backupmigrations.sh +++ b/packages/protocol/scripts/bash/backupmigrations.sh @@ -45,4 +45,5 @@ else # cp migrations.bak/23_governance_approver_multisig.* migrations/ # cp migrations.bak/24_governance.* migrations/ # cp migrations.bak/25_elect_validators.* migrations/ + # cp migrations.bak/27_federated_attestations.* migrations/ fi \ No newline at end of file diff --git a/packages/protocol/scripts/build.ts b/packages/protocol/scripts/build.ts index 1f4f78240f7..0a15471c3be 100644 --- a/packages/protocol/scripts/build.ts +++ b/packages/protocol/scripts/build.ts @@ -20,6 +20,7 @@ export const ProxyContracts = [ 'ExchangeBRLProxy', 'ExchangeEURProxy', 'ExchangeProxy', + 'FederatedAttestationsProxy', 'FeeCurrencyWhitelistProxy', 'GasPriceMinimumProxy', 'GoldTokenProxy', @@ -66,6 +67,7 @@ export const CoreContracts = [ // identity 'Attestations', 'Escrow', + 'FederatedAttestations', 'Random', // stability diff --git a/packages/protocol/test/identity/federatedattestations.ts b/packages/protocol/test/identity/federatedattestations.ts new file mode 100644 index 00000000000..54070cb21e9 --- /dev/null +++ b/packages/protocol/test/identity/federatedattestations.ts @@ -0,0 +1,43 @@ +import { CeloContractName } from '@celo/protocol/lib/registry-utils' +// import { getPhoneHash } from '@celo/utils/lib/phoneNumbers' +import { + AccountsContract, + AccountsInstance, + FederatedAttestationsContract, + FederatedAttestationsInstance, + RegistryContract, + RegistryInstance, +} from 'types' + +const Accounts: AccountsContract = artifacts.require('Accounts') +const FederatedAttestations: FederatedAttestationsContract = artifacts.require( + 'FederatedAttestations' +) +const Registry: RegistryContract = artifacts.require('Registry') + +contract('Attestations', (accounts: string[]) => { + let accountsInstance: AccountsInstance + let federatedAttestations: FederatedAttestationsInstance + let registry: RegistryInstance + + const caller: string = accounts[0] + // const phoneNumber: string = '+18005551212' + // const phoneHash: string = getPhoneHash(phoneNumber) + + beforeEach('FederatedAttestations setup', async () => { + accountsInstance = await Accounts.new(true) + federatedAttestations = await FederatedAttestations.new(true) + registry = await Registry.new(true) + await accountsInstance.initialize(registry.address) + await registry.setAddressFor(CeloContractName.Accounts, accountsInstance.address) + await federatedAttestations.initialize(registry.address) + }) + + describe('#initialize()', () => { + it('TODO ASv2', async () => { + // TODO ASv2 + assert(caller) + assert(federatedAttestations) + }) + }) +}) diff --git a/packages/sdk/contractkit/src/base.ts b/packages/sdk/contractkit/src/base.ts index d4b0657bfb0..082358d2f69 100644 --- a/packages/sdk/contractkit/src/base.ts +++ b/packages/sdk/contractkit/src/base.ts @@ -11,6 +11,7 @@ export enum CeloContract { Exchange = 'Exchange', ExchangeEUR = 'ExchangeEUR', ExchangeBRL = 'ExchangeBRL', + FederatedAttestations = 'FederatedAttestations', FeeCurrencyWhitelist = 'FeeCurrencyWhitelist', Freezer = 'Freezer', GasPriceMinimum = 'GasPriceMinimum', diff --git a/packages/sdk/contractkit/src/contract-cache.ts b/packages/sdk/contractkit/src/contract-cache.ts index 088e20af3a9..2aa58931444 100644 --- a/packages/sdk/contractkit/src/contract-cache.ts +++ b/packages/sdk/contractkit/src/contract-cache.ts @@ -15,6 +15,7 @@ import { EpochRewardsWrapper } from './wrappers/EpochRewards' import { Erc20Wrapper } from './wrappers/Erc20Wrapper' import { EscrowWrapper } from './wrappers/Escrow' import { ExchangeWrapper } from './wrappers/Exchange' +import { FederatedAttestationsWrapper } from './wrappers/FederatedAttestations' import { FreezerWrapper } from './wrappers/Freezer' import { GasPriceMinimumWrapper } from './wrappers/GasPriceMinimum' import { GoldTokenWrapper } from './wrappers/GoldTokenWrapper' @@ -38,6 +39,7 @@ const WrapperFactories = { [CeloContract.Exchange]: ExchangeWrapper, [CeloContract.ExchangeEUR]: ExchangeWrapper, [CeloContract.ExchangeBRL]: ExchangeWrapper, + [CeloContract.FederatedAttestations]: FederatedAttestationsWrapper, // [CeloContract.FeeCurrencyWhitelist]: FeeCurrencyWhitelistWrapper, [CeloContract.Freezer]: FreezerWrapper, [CeloContract.GasPriceMinimum]: GasPriceMinimumWrapper, @@ -91,6 +93,7 @@ interface WrapperCacheMap { [CeloContract.Exchange]?: ExchangeWrapper [CeloContract.ExchangeEUR]?: ExchangeWrapper [CeloContract.ExchangeBRL]?: ExchangeWrapper + [CeloContract.FederatedAttestations]?: FederatedAttestationsWrapper // [CeloContract.FeeCurrencyWhitelist]?: FeeCurrencyWhitelistWrapper, [CeloContract.Freezer]?: FreezerWrapper [CeloContract.GasPriceMinimum]?: GasPriceMinimumWrapper @@ -166,6 +169,14 @@ export class WrapperCache implements ContractCacheType { return this.getContract(CeloContract.Freezer) } + getFederatedAttestations() { + return this.getContract(CeloContract.FederatedAttestations) + } + + // getFeeCurrencyWhitelist() { + // return this.getWrapper(CeloContract.FeeCurrencyWhitelist, newFeeCurrencyWhitelist) + // } + getGasPriceMinimum() { return this.getContract(CeloContract.GasPriceMinimum) } diff --git a/packages/sdk/contractkit/src/kit.ts b/packages/sdk/contractkit/src/kit.ts index 3d4e72c681e..06a20913736 100644 --- a/packages/sdk/contractkit/src/kit.ts +++ b/packages/sdk/contractkit/src/kit.ts @@ -27,6 +27,7 @@ import { BlockchainParametersConfig } from './wrappers/BlockchainParameters' import { DowntimeSlasherConfig } from './wrappers/DowntimeSlasher' import { ElectionConfig } from './wrappers/Election' import { ExchangeConfig } from './wrappers/Exchange' +import { FederatedAttestationsConfig } from './wrappers/FederatedAttestations' import { GasPriceMinimumConfig } from './wrappers/GasPriceMinimum' import { GovernanceConfig } from './wrappers/Governance' import { GrandaMentoConfig } from './wrappers/GrandaMento' @@ -72,6 +73,8 @@ export interface NetworkConfig { stableTokens: EachCeloToken election: ElectionConfig attestations: AttestationsConfig + // TODO ASv2 + federatedattestations: FederatedAttestationsConfig governance: GovernanceConfig lockedGold: LockedGoldConfig sortedOracles: SortedOraclesConfig @@ -146,6 +149,8 @@ export class ContractKit { const configContracts: ValidWrappers[] = [ CeloContract.Election, CeloContract.Attestations, + // TODO ASv2 + CeloContract.FederatedAttestations, CeloContract.Governance, CeloContract.LockedGold, CeloContract.SortedOracles, diff --git a/packages/sdk/contractkit/src/proxy.ts b/packages/sdk/contractkit/src/proxy.ts index 427bc363e1f..c9a4867c713 100644 --- a/packages/sdk/contractkit/src/proxy.ts +++ b/packages/sdk/contractkit/src/proxy.ts @@ -9,6 +9,7 @@ import { ABI as ElectionABI } from './generated/Election' import { ABI as EpochRewardsABI } from './generated/EpochRewards' import { ABI as EscrowABI } from './generated/Escrow' import { ABI as ExchangeABI } from './generated/Exchange' +import { ABI as FederatedAttestationsABI } from './generated/FederatedAttestations' import { ABI as FeeCurrencyWhitelistABI } from './generated/FeeCurrencyWhitelist' import { ABI as FreezerABI } from './generated/Freezer' import { ABI as GasPriceMinimumABI } from './generated/GasPriceMinimum' @@ -104,6 +105,7 @@ const initializeAbiMap = { ExchangeProxy: findInitializeAbi(ExchangeABI), ExchangeEURProxy: findInitializeAbi(ExchangeABI), ExchangeBRLProxy: findInitializeAbi(ExchangeABI), + FederatedAttestationsProxy: findInitializeAbi(FederatedAttestationsABI), FeeCurrencyWhitelistProxy: findInitializeAbi(FeeCurrencyWhitelistABI), FreezerProxy: findInitializeAbi(FreezerABI), GasPriceMinimumProxy: findInitializeAbi(GasPriceMinimumABI), diff --git a/packages/sdk/contractkit/src/web3-contract-cache.ts b/packages/sdk/contractkit/src/web3-contract-cache.ts index ef2b8712c6f..fc9989fe661 100644 --- a/packages/sdk/contractkit/src/web3-contract-cache.ts +++ b/packages/sdk/contractkit/src/web3-contract-cache.ts @@ -13,6 +13,7 @@ import { newEscrow } from './generated/Escrow' import { newExchange } from './generated/Exchange' import { newExchangeBrl } from './generated/ExchangeBRL' import { newExchangeEur } from './generated/ExchangeEUR' +import { newFederatedAttestations } from './generated/FederatedAttestations' import { newFeeCurrencyWhitelist } from './generated/FeeCurrencyWhitelist' import { newFreezer } from './generated/Freezer' import { newGasPriceMinimum } from './generated/GasPriceMinimum' @@ -48,6 +49,7 @@ export const ContractFactories = { [CeloContract.Exchange]: newExchange, [CeloContract.ExchangeEUR]: newExchangeEur, [CeloContract.ExchangeBRL]: newExchangeBrl, + [CeloContract.FederatedAttestations]: newFederatedAttestations, [CeloContract.FeeCurrencyWhitelist]: newFeeCurrencyWhitelist, [CeloContract.Freezer]: newFreezer, [CeloContract.GasPriceMinimum]: newGasPriceMinimum, @@ -126,6 +128,9 @@ export class Web3ContractCache { getExchange(stableToken: StableToken = StableToken.cUSD) { return this.getContract(StableToExchange[stableToken]) } + getFederatedAttestations() { + return this.getContract(CeloContract.FederatedAttestations) + } getFeeCurrencyWhitelist() { return this.getContract(CeloContract.FeeCurrencyWhitelist) } diff --git a/packages/sdk/contractkit/src/wrappers/FederatedAttestations.test.ts b/packages/sdk/contractkit/src/wrappers/FederatedAttestations.test.ts new file mode 100644 index 00000000000..de69785bb59 --- /dev/null +++ b/packages/sdk/contractkit/src/wrappers/FederatedAttestations.test.ts @@ -0,0 +1,26 @@ +import { testWithGanache } from '@celo/dev-utils/lib/ganache-test' +// import { PhoneNumberUtils } from '@celo/utils' +import { newKitFromWeb3 } from '../kit' +import { FederatedAttestationsWrapper } from './FederatedAttestations' + +testWithGanache('FederatedAttestations Wrapper', (web3) => { + // const PHONE_NUMBER = '+15555555555' + // const IDENTIFIER = PhoneNumberUtils.getPhoneHash(PHONE_NUMBER) + + const kit = newKitFromWeb3(web3) + let accounts: string[] = [] + let federatedAttestations: FederatedAttestationsWrapper + + beforeAll(async () => { + accounts = await web3.eth.getAccounts() + kit.defaultAccount = accounts[0] + }) + + describe('TODO ASv2', () => { + it('TODO ASv2', async () => { + expect(accounts) + federatedAttestations = await kit.contracts.getFederatedAttestations() + expect(federatedAttestations) + }) + }) +}) diff --git a/packages/sdk/contractkit/src/wrappers/FederatedAttestations.ts b/packages/sdk/contractkit/src/wrappers/FederatedAttestations.ts new file mode 100644 index 00000000000..dbb665ce97a --- /dev/null +++ b/packages/sdk/contractkit/src/wrappers/FederatedAttestations.ts @@ -0,0 +1,8 @@ +import { FederatedAttestations } from '../generated/FederatedAttestations' +import { BaseWrapper } from './BaseWrapper' + +export interface FederatedAttestationsConfig { + // TODO ASv2 +} + +export class FederatedAttestationsWrapper extends BaseWrapper {} From dabd410415351c52540e310436706a401b21fb29 Mon Sep 17 00:00:00 2001 From: Eela Nagaraj <7308464+eelanagaraj@users.noreply.github.com> Date: Thu, 14 Apr 2022 12:15:55 +0200 Subject: [PATCH 02/30] Fix contract migrations and failing CK test (#9455) * Fix migration order and governance ownership * Fix quicktest backup migrations * Update devchain migration in other test:reset commands * Comment out FA config to fix lint tests --- packages/cli/package.json | 2 +- ...erated_attestations.ts => 25_federated_attestations.ts} | 0 .../migrations/{25_governance.ts => 26_governance.ts} | 2 +- .../{26_elect_validators.ts => 27_elect_validators.ts} | 0 packages/protocol/scripts/bash/backupmigrations.sh | 7 ++++--- packages/sdk/contractkit/package.json | 2 +- packages/sdk/contractkit/src/kit.ts | 7 ++++--- .../sdk/contractkit/src/wrappers/FederatedAttestations.ts | 6 +++--- packages/sdk/identity/package.json | 2 +- packages/sdk/transactions-uri/package.json | 2 +- 10 files changed, 16 insertions(+), 14 deletions(-) rename packages/protocol/migrations/{27_federated_attestations.ts => 25_federated_attestations.ts} (100%) rename packages/protocol/migrations/{25_governance.ts => 26_governance.ts} (99%) rename packages/protocol/migrations/{26_elect_validators.ts => 27_elect_validators.ts} (100%) diff --git a/packages/cli/package.json b/packages/cli/package.json index 26be227461e..f3bfe057037 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -28,7 +28,7 @@ "generate:shrinkwrap": "npm install --production && npm shrinkwrap", "check:shrinkwrap": "npm install --production && npm shrinkwrap && ./scripts/check_shrinkwrap_dirty.sh", "prepack": "yarn run build && oclif-dev manifest && oclif-dev readme && yarn run check:shrinkwrap", - "test:reset": "yarn --cwd ../protocol devchain generate-tar .tmp/devchain.tar.gz --migration_override ../dev-utils/src/migration-override.json --upto 25 --release_gold_contracts scripts/truffle/releaseGoldExampleConfigs.json", + "test:reset": "yarn --cwd ../protocol devchain generate-tar .tmp/devchain.tar.gz --migration_override ../dev-utils/src/migration-override.json --upto 26 --release_gold_contracts scripts/truffle/releaseGoldExampleConfigs.json", "test:livechain": "yarn --cwd ../protocol devchain run-tar .tmp/devchain.tar.gz", "test": "TZ=UTC jest --runInBand" }, diff --git a/packages/protocol/migrations/27_federated_attestations.ts b/packages/protocol/migrations/25_federated_attestations.ts similarity index 100% rename from packages/protocol/migrations/27_federated_attestations.ts rename to packages/protocol/migrations/25_federated_attestations.ts diff --git a/packages/protocol/migrations/25_governance.ts b/packages/protocol/migrations/26_governance.ts similarity index 99% rename from packages/protocol/migrations/25_governance.ts rename to packages/protocol/migrations/26_governance.ts index e91ad42fc0e..a95777f27c2 100644 --- a/packages/protocol/migrations/25_governance.ts +++ b/packages/protocol/migrations/26_governance.ts @@ -83,7 +83,7 @@ module.exports = deploymentForCoreContract( 'Escrow', 'Exchange', 'ExchangeEUR', - // TODO ASv2 revisit + 'FederatedAttestations', 'FeeCurrencyWhitelist', 'Freezer', 'GasPriceMinimum', diff --git a/packages/protocol/migrations/26_elect_validators.ts b/packages/protocol/migrations/27_elect_validators.ts similarity index 100% rename from packages/protocol/migrations/26_elect_validators.ts rename to packages/protocol/migrations/27_elect_validators.ts diff --git a/packages/protocol/scripts/bash/backupmigrations.sh b/packages/protocol/scripts/bash/backupmigrations.sh index a5cfedf501e..6dfa89132ad 100755 --- a/packages/protocol/scripts/bash/backupmigrations.sh +++ b/packages/protocol/scripts/bash/backupmigrations.sh @@ -43,7 +43,8 @@ else # cp migrations.bak/21_double_signing_slasher.* migrations/ # cp migrations.bak/22_downtime_slasher.* migrations/ # cp migrations.bak/23_governance_approver_multisig.* migrations/ - # cp migrations.bak/24_governance.* migrations/ - # cp migrations.bak/25_elect_validators.* migrations/ - # cp migrations.bak/27_federated_attestations.* migrations/ + # cp migrations.bak/24_grandamento.* migrations/ + # cp migrations.bak/25_federated_attestations.* migrations/ + # cp migrations.bak/26_governance.* migrations/ + # cp migrations.bak/27_elect_validators.* migrations/ fi \ No newline at end of file diff --git a/packages/sdk/contractkit/package.json b/packages/sdk/contractkit/package.json index 0c6935288ab..fd6d7777522 100644 --- a/packages/sdk/contractkit/package.json +++ b/packages/sdk/contractkit/package.json @@ -22,7 +22,7 @@ "clean:all": "yarn clean && rm -rf src/generated", "prepublishOnly": "yarn build", "docs": "typedoc", - "test:reset": "yarn --cwd ../../protocol devchain generate-tar .tmp/devchain.tar.gz --migration_override ../../dev-utils/src/migration-override.json --upto 25", + "test:reset": "yarn --cwd ../../protocol devchain generate-tar .tmp/devchain.tar.gz --migration_override ../../dev-utils/src/migration-override.json --upto 26", "test:livechain": "yarn --cwd ../../protocol devchain run-tar .tmp/devchain.tar.gz", "test": "jest --runInBand", "lint": "tslint -c tslint.json --project ." diff --git a/packages/sdk/contractkit/src/kit.ts b/packages/sdk/contractkit/src/kit.ts index 06a20913736..23da74900a4 100644 --- a/packages/sdk/contractkit/src/kit.ts +++ b/packages/sdk/contractkit/src/kit.ts @@ -27,7 +27,8 @@ import { BlockchainParametersConfig } from './wrappers/BlockchainParameters' import { DowntimeSlasherConfig } from './wrappers/DowntimeSlasher' import { ElectionConfig } from './wrappers/Election' import { ExchangeConfig } from './wrappers/Exchange' -import { FederatedAttestationsConfig } from './wrappers/FederatedAttestations' +// TODO ASv2 +// import { FederatedAttestationsConfig } from './wrappers/FederatedAttestations' import { GasPriceMinimumConfig } from './wrappers/GasPriceMinimum' import { GovernanceConfig } from './wrappers/Governance' import { GrandaMentoConfig } from './wrappers/GrandaMento' @@ -74,7 +75,7 @@ export interface NetworkConfig { election: ElectionConfig attestations: AttestationsConfig // TODO ASv2 - federatedattestations: FederatedAttestationsConfig + // federatedattestations: FederatedAttestationsConfig governance: GovernanceConfig lockedGold: LockedGoldConfig sortedOracles: SortedOraclesConfig @@ -150,7 +151,7 @@ export class ContractKit { CeloContract.Election, CeloContract.Attestations, // TODO ASv2 - CeloContract.FederatedAttestations, + // CeloContract.FederatedAttestations, CeloContract.Governance, CeloContract.LockedGold, CeloContract.SortedOracles, diff --git a/packages/sdk/contractkit/src/wrappers/FederatedAttestations.ts b/packages/sdk/contractkit/src/wrappers/FederatedAttestations.ts index dbb665ce97a..1400ac09a42 100644 --- a/packages/sdk/contractkit/src/wrappers/FederatedAttestations.ts +++ b/packages/sdk/contractkit/src/wrappers/FederatedAttestations.ts @@ -1,8 +1,8 @@ import { FederatedAttestations } from '../generated/FederatedAttestations' import { BaseWrapper } from './BaseWrapper' -export interface FederatedAttestationsConfig { - // TODO ASv2 -} +// TODO ASv2 -- add params or delete config if there are none +// & delete other commented-out usages of this Config in CK +// export interface FederatedAttestationsConfig {} export class FederatedAttestationsWrapper extends BaseWrapper {} diff --git a/packages/sdk/identity/package.json b/packages/sdk/identity/package.json index 965a6b7d34c..51a6c8bec66 100644 --- a/packages/sdk/identity/package.json +++ b/packages/sdk/identity/package.json @@ -18,7 +18,7 @@ "build": "tsc -b .", "clean": "tsc -b . --clean", "docs": "typedoc", - "test:reset": "yarn --cwd ../../protocol devchain generate-tar .tmp/devchain.tar.gz --migration_override ../../dev-utils/src/migration-override.json --upto 25", + "test:reset": "yarn --cwd ../../protocol devchain generate-tar .tmp/devchain.tar.gz --migration_override ../../dev-utils/src/migration-override.json --upto 26", "test:livechain": "yarn --cwd ../../protocol devchain run-tar .tmp/devchain.tar.gz", "test": "jest --runInBand", "lint": "tslint -c tslint.json --project .", diff --git a/packages/sdk/transactions-uri/package.json b/packages/sdk/transactions-uri/package.json index 2a2874cd6d5..8a847936de7 100644 --- a/packages/sdk/transactions-uri/package.json +++ b/packages/sdk/transactions-uri/package.json @@ -17,7 +17,7 @@ "build": "tsc -b .", "clean": "tsc -b . --clean", "docs": "typedoc", - "test:reset": "yarn --cwd ../../protocol devchain generate-tar .tmp/devchain.tar.gz --migration_override ../../dev-utils/src/migration-override.json --upto 25", + "test:reset": "yarn --cwd ../../protocol devchain generate-tar .tmp/devchain.tar.gz --migration_override ../../dev-utils/src/migration-override.json --upto 26", "test:livechain": "yarn --cwd ../../protocol devchain run-tar .tmp/devchain.tar.gz", "test": "jest --runInBand", "lint": "tslint -c tslint.json --project .", From 3066f8165648f6ac3aa7c95dd0173395e4af1175 Mon Sep 17 00:00:00 2001 From: isabellewei Date: Fri, 22 Apr 2022 00:48:05 +0200 Subject: [PATCH 03/30] ASv2 Onchain Registry (#9431) * Add barebones FederatedAttestations contract, proxy, and interface * Make TODOs ASv2 specific * Add FA usages to existing files * Add skeleton migration file * Add test scaffolding * Add CK wrapper and test * storage variables, reading and writing functions * add function signatures * fix merge conflict * Fix compile errors in initial SC implementation (#9472) * Fix compilation errors * Fix delete such that it compiles * Add TODO for revokeSigner Co-authored-by: Isabelle Wei * small function name capitalization * include PR comments Co-authored-by: Eela Nagaraj <7308464+eelanagaraj@users.noreply.github.com> --- .../identity/FederatedAttestations.sol | 178 +++++++++++++++++- 1 file changed, 177 insertions(+), 1 deletion(-) diff --git a/packages/protocol/contracts/identity/FederatedAttestations.sol b/packages/protocol/contracts/identity/FederatedAttestations.sol index 3483cf0b12f..9894576e5b2 100644 --- a/packages/protocol/contracts/identity/FederatedAttestations.sol +++ b/packages/protocol/contracts/identity/FederatedAttestations.sol @@ -1,4 +1,7 @@ +// TODO figure out if we can use a new solidity version for just this one contract pragma solidity ^0.5.13; +// TODO ASv2 come back to this and possibly flatten structs as arg params +pragma experimental ABIEncoderV2; import "openzeppelin-solidity/contracts/math/SafeMath.sol"; import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; @@ -30,7 +33,17 @@ contract FederatedAttestations is using SafeMath for uint256; using SafeCast for uint256; - // TODO ASv2 State var declarations + struct IdentifierOwnershipAttestation { + address account; + uint256 issuedOn; + address signer; + } + // identifier -> issuer -> attestations + mapping(bytes32 => mapping(address => IdentifierOwnershipAttestation[])) public identifierToAddresses; + // account -> issuer -> identifiers + mapping(address => mapping(address => bytes32[])) public addressToIdentifiers; + // signer => revocation time + mapping(address => uint256) public revokedSigners; // TODO ASv2 Event declarations @@ -57,4 +70,167 @@ contract FederatedAttestations is function getVersionNumber() external pure returns (uint256, uint256, uint256, uint256) { return (1, 1, 0, 0); } + + function _isRevoked(address signer, uint256 time) internal view returns (bool) { + if (revokedSigners[signer] > 0 && revokedSigners[signer] >= time) { + return true; + } + return false; + } + + function lookupAttestations( + bytes32 identifier, + address[] memory trustedIssuers, + uint256 maxAttestations + ) public view returns (IdentifierOwnershipAttestation[] memory) { + // Cannot dynamically allocate an in-memory array + // For now require a max returned parameter to pre-allocate and then shrink + // TODO ASv2 probably need a more gas-efficient lookup for the single most-recent attestation for one trusted issuer + uint256 currIndex = 0; + IdentifierOwnershipAttestation[] memory attestations = new IdentifierOwnershipAttestation[]( + maxAttestations + ); + for (uint256 i = 0; i < trustedIssuers.length; i++) { + address trustedIssuer = trustedIssuers[i]; + for (uint256 j = 0; j < identifierToAddresses[identifier][trustedIssuer].length; j++) { + // Only create and push new attestation if we haven't hit max + if (currIndex < maxAttestations) { + IdentifierOwnershipAttestation memory attestation = identifierToAddresses[identifier][trustedIssuer][j]; + if (!_isRevoked(attestation.signer, attestation.issuedOn)) { + attestations[currIndex] = attestation; + currIndex++; + } + } else { + break; + } + } + } + if (currIndex < maxAttestations) { + IdentifierOwnershipAttestation[] memory trimmedAttestations = new IdentifierOwnershipAttestation[]( + currIndex + ); + for (uint256 i = 0; i < currIndex; i++) { + trimmedAttestations[i] = attestations[i]; + } + return trimmedAttestations; + } else { + return attestations; + } + } + + function lookupIdentifiersByAddress( + address account, + address[] memory trustedIssuers, + uint256 maxIdentifiers + ) public view returns (bytes32[] memory) { + // Same as for the other lookup, preallocate and then trim for now + uint256 currIndex = 0; + bytes32[] memory identifiers = new bytes32[](maxIdentifiers); + + for (uint256 i = 0; i < trustedIssuers.length; i++) { + address trustedIssuer = trustedIssuers[i]; + for (uint256 j = 0; j < addressToIdentifiers[account][trustedIssuer].length; j++) { + // Iterate through the list of identifiers + if (currIndex < maxIdentifiers) { + bytes32 identifier = addressToIdentifiers[account][trustedIssuer][j]; + // Check if this signer for this particular signer is revoked + for (uint256 k = 0; k < identifierToAddresses[identifier][trustedIssuer].length; k++) { + IdentifierOwnershipAttestation memory attestation = identifierToAddresses[identifier][trustedIssuer][k]; + // For now, just take the first published, unrevoked signer that matches + // TODO redo this to take into account either recency or the "correct" identifier + // based on the index + if ( + attestation.account == account && + !_isRevoked(attestation.signer, attestation.issuedOn) + ) { + identifiers[currIndex] = identifier; + currIndex++; + break; + } + } + } else { + break; + } + } + } + if (currIndex < maxIdentifiers) { + // Allocate and fill properly-sized array + bytes32[] memory trimmedIdentifiers = new bytes32[](currIndex); + for (uint256 i = 0; i < currIndex; i++) { + trimmedIdentifiers[i] = identifiers[i]; + } + return trimmedIdentifiers; + } else { + return identifiers; + } + } + + function validateAttestation( + bytes32 identifier, + address issuer, + IdentifierOwnershipAttestation memory attestation, + uint8 v, + bytes32 r, + bytes32 s + ) public view returns (address) { + // TODO check if signer is revoked and is a valid signer of the account + } + + function registerAttestation( + bytes32 identifier, + address issuer, + IdentifierOwnershipAttestation memory attestation + ) public { + // TODO call validateAttestation here + require( + msg.sender == attestation.account || msg.sender == issuer || msg.sender == attestation.signer + ); + for (uint256 i = 0; i < identifierToAddresses[identifier][issuer].length; i++) { + // This enforces only one attestation to be uploaded for a given set of (identifier, issuer, account) + // Editing/upgrading an attestation requires that it be deleted before a new one is registered + require(identifierToAddresses[identifier][issuer][i].account != attestation.account); + } + identifierToAddresses[identifier][issuer].push(attestation); + addressToIdentifiers[attestation.account][issuer].push(identifier); + } + + function deleteAttestation(bytes32 identifier, address issuer, address account) public { + // TODO ASv2 this should short-circuit, but double check (i.e. succeeds if msg.sender == account) + require( + msg.sender == account || getAccounts().attestationSignerToAccount(msg.sender) == issuer + ); + + for (uint256 i = 0; i < identifierToAddresses[identifier][issuer].length; i++) { + IdentifierOwnershipAttestation memory attestation = identifierToAddresses[identifier][issuer][i]; + if (attestation.account == account) { + // This is meant to delete the attestation in the array and then move the last element in the array to that empty spot, to avoid having empty elements in the array + // Not sure if this is needed and if the added gas costs from the complexity is worth it + identifierToAddresses[identifier][issuer][i] = identifierToAddresses[identifier][issuer][identifierToAddresses[identifier][issuer] + .length - + 1]; + identifierToAddresses[identifier][issuer].pop(); + + // TODO revisit if deletedIdentifier check is necessary - not sure if there would ever be a situation where the matching identifier is not present + bool deletedIdentifier = false; + for (uint256 j = 0; j < addressToIdentifiers[account][issuer].length; j++) { + if (addressToIdentifiers[account][issuer][j] == identifier) { + addressToIdentifiers[account][issuer][j] = addressToIdentifiers[account][issuer][addressToIdentifiers[account][issuer] + .length - + 1]; + addressToIdentifiers[account][issuer].pop(); + deletedIdentifier = true; + break; + } + } + // Hard requirement to delete from both mappings in unison + require(deletedIdentifier); + break; + } + } + } + + function revokeSigner(address signer, uint256 revokedOn) public { + // TODO ASv2 add constraints on who can revoke a signer + revokedSigners[signer] = revokedOn; + } } From 085de1e298387a24e14bc2833601363e234ef7f4 Mon Sep 17 00:00:00 2001 From: Eela Nagaraj <7308464+eelanagaraj@users.noreply.github.com> Date: Wed, 11 May 2022 19:03:42 +0200 Subject: [PATCH 04/30] Implement unit tests for FederatedAttestations (#9565) * add describe blocks for federated attestation tests * ASv2 validateAttestation functionality and unit tests (#9532) * validate attestation with eip712 * validate signature of attestation * modify revokedSigners to boolean instead of time-based * validate signer * add validateAttestation docstring * ASv2 initialize unit tests (#9544) * initialize unit tests * ASv2 registerAttestation unit tests (#9543) * registerAttestation tests * add docstring and use safemath * PR comments and additional tests * Implement FederatedAttestations lookup* unit tests (#9522) * Modify lookupAttestations to not require ABIEncoderV2 * Implement unit tests for lookupAttestations * Fix _isRevoked logic * Clean up lookupAttestations tests * Fix safemath use and docstring in lookupIdentifiersByAddress * Implement unit tests for lookupIdentifiersByAddress * Remove fixed TODO * Fix some linting errors * Nit: revocation boolean return Co-authored-by: Cody Born * Address PR comments * Update lookup tests to use register and fix out of gas * Unify variable names * Remove ABIEncoderV2 * Add TODO ASv2 to fed-attestations-utils comments to revisit * Update var names (part 2) to fix linting * Fix lint checks in FA contract Co-authored-by: Cody Born * ASv2 deleteAttestation unit tests (#9563) * deleteAttestation unit tests * rename variable * gas optimizations * Fix delete loop incrementing * Add revoke test case for registration * Add case for registration when issuer == signer of attestation Co-authored-by: Isabelle Wei Co-authored-by: Cody Born --- .../contracts/common/interfaces/IAccounts.sol | 1 + .../identity/FederatedAttestations.sol | 316 ++++-- .../protocol/lib/fed-attestations-utils.ts | 80 ++ packages/protocol/migrationsConfig.js | 1 + packages/protocol/test/common/accounts.ts | 3 +- .../test/identity/federatedattestations.ts | 974 +++++++++++++++++- 6 files changed, 1279 insertions(+), 96 deletions(-) create mode 100644 packages/protocol/lib/fed-attestations-utils.ts diff --git a/packages/protocol/contracts/common/interfaces/IAccounts.sol b/packages/protocol/contracts/common/interfaces/IAccounts.sol index b32d7ac98bb..54b4d089d66 100644 --- a/packages/protocol/contracts/common/interfaces/IAccounts.sol +++ b/packages/protocol/contracts/common/interfaces/IAccounts.sol @@ -46,4 +46,5 @@ interface IAccounts { function setPaymentDelegation(address, uint256) external; function getPaymentDelegation(address) external view returns (address, uint256); + function isSigner(address, address, bytes32) external view returns (bool); } diff --git a/packages/protocol/contracts/identity/FederatedAttestations.sol b/packages/protocol/contracts/identity/FederatedAttestations.sol index 9894576e5b2..d8e0fec4d1d 100644 --- a/packages/protocol/contracts/identity/FederatedAttestations.sol +++ b/packages/protocol/contracts/identity/FederatedAttestations.sol @@ -1,7 +1,4 @@ -// TODO figure out if we can use a new solidity version for just this one contract pragma solidity ^0.5.13; -// TODO ASv2 come back to this and possibly flatten structs as arg params -pragma experimental ABIEncoderV2; import "openzeppelin-solidity/contracts/math/SafeMath.sol"; import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; @@ -33,19 +30,39 @@ contract FederatedAttestations is using SafeMath for uint256; using SafeCast for uint256; - struct IdentifierOwnershipAttestation { + struct OwnershipAttestation { address account; uint256 issuedOn; address signer; } + + // TODO ASv2 revisit linting issues & all solhint-disable-next-line max-line-length + // identifier -> issuer -> attestations - mapping(bytes32 => mapping(address => IdentifierOwnershipAttestation[])) public identifierToAddresses; + mapping(bytes32 => mapping(address => OwnershipAttestation[])) public identifierToAddresses; // account -> issuer -> identifiers mapping(address => mapping(address => bytes32[])) public addressToIdentifiers; - // signer => revocation time - mapping(address => uint256) public revokedSigners; + // signer => isRevoked + mapping(address => bool) public revokedSigners; - // TODO ASv2 Event declarations + bytes32 public constant EIP712_VALIDATE_ATTESTATION_TYPEHASH = keccak256( + "OwnershipAttestation(bytes32 identifier,address issuer,address account,uint256 issuedOn)" + ); + bytes32 public eip712DomainSeparator; + + event EIP712DomainSeparatorSet(bytes32 eip712DomainSeparator); + event AttestationRegistered( + bytes32 indexed identifier, + address indexed issuer, + address indexed account, + uint256 issuedOn, + address signer + ); + event AttestationDeleted( + bytes32 indexed identifier, + address indexed issuer, + address indexed account + ); /** * @notice Sets initialized == true on implementation contracts @@ -60,9 +77,33 @@ contract FederatedAttestations is function initialize(address registryAddress) external initializer { _transferOwnership(msg.sender); setRegistry(registryAddress); + setEip712DomainSeparator(); // TODO ASv2 initialize any other variables here } + /** + * @notice Sets the EIP712 domain separator for the Celo FederatedAttestations abstraction. + */ + function setEip712DomainSeparator() public { + uint256 chainId; + assembly { + chainId := chainid + } + + eip712DomainSeparator = keccak256( + abi.encode( + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ), + keccak256(bytes("FederatedAttestations")), + keccak256("1.0"), + chainId, + address(this) + ) + ); + emit EIP712DomainSeparatorSet(eip712DomainSeparator); + } + /** * @notice Returns the storage, major, minor, and patch version of the contract. * @return The storage, major, minor, and patch version of the contract. @@ -71,53 +112,87 @@ contract FederatedAttestations is return (1, 1, 0, 0); } - function _isRevoked(address signer, uint256 time) internal view returns (bool) { - if (revokedSigners[signer] > 0 && revokedSigners[signer] >= time) { - return true; - } - return false; - } + /** + * @notice Returns info about attestations (with unrevoked signers) + * for an identifier produced by a list of issuers + * @param identifier Hash of the identifier + * @param trustedIssuers Array of n issuers whose attestations will be included + * @param maxAttestations Limit the number of attestations that will be returned + * @return for m found attestations, m <= maxAttestations: + * [ + * Array of m accounts, + * Array of m issuedOns, + * Array of m signers + * ]; index corresponds to the same attestation + * @dev Adds attestation info to the arrays in order of provided trustedIssuers + * @dev Expectation that only one attestation exists per (identifier, issuer, account) + */ + // TODO ASv2 consider also returning an array with counts of attestations per issuer function lookupAttestations( bytes32 identifier, address[] memory trustedIssuers, uint256 maxAttestations - ) public view returns (IdentifierOwnershipAttestation[] memory) { + ) public view returns (address[] memory, uint256[] memory, address[] memory) { // Cannot dynamically allocate an in-memory array // For now require a max returned parameter to pre-allocate and then shrink - // TODO ASv2 probably need a more gas-efficient lookup for the single most-recent attestation for one trusted issuer + // TODO ASv2 is it a risk to allocate an array of size maxAttestations? + // Same index corresponds to same attestation + address[] memory accounts = new address[](maxAttestations); + uint256[] memory issuedOns = new uint256[](maxAttestations); + address[] memory signers = new address[](maxAttestations); + uint256 currIndex = 0; - IdentifierOwnershipAttestation[] memory attestations = new IdentifierOwnershipAttestation[]( - maxAttestations - ); - for (uint256 i = 0; i < trustedIssuers.length; i++) { - address trustedIssuer = trustedIssuers[i]; - for (uint256 j = 0; j < identifierToAddresses[identifier][trustedIssuer].length; j++) { + + for (uint256 i = 0; i < trustedIssuers.length; i = i.add(1)) { + uint256 numTrustedIssuers = identifierToAddresses[identifier][trustedIssuers[i]].length; + for (uint256 j = 0; j < numTrustedIssuers; j = j.add(1)) { // Only create and push new attestation if we haven't hit max if (currIndex < maxAttestations) { - IdentifierOwnershipAttestation memory attestation = identifierToAddresses[identifier][trustedIssuer][j]; - if (!_isRevoked(attestation.signer, attestation.issuedOn)) { - attestations[currIndex] = attestation; - currIndex++; + // solhint-disable-next-line max-line-length + OwnershipAttestation memory attestation = identifierToAddresses[identifier][trustedIssuers[i]][j]; + if (!revokedSigners[attestation.signer]) { + accounts[currIndex] = attestation.account; + issuedOns[currIndex] = attestation.issuedOn; + signers[currIndex] = attestation.signer; + currIndex = currIndex.add(1); } } else { break; } } } + + // Trim returned structs if necessary if (currIndex < maxAttestations) { - IdentifierOwnershipAttestation[] memory trimmedAttestations = new IdentifierOwnershipAttestation[]( - currIndex - ); - for (uint256 i = 0; i < currIndex; i++) { - trimmedAttestations[i] = attestations[i]; + address[] memory trimmedAccounts = new address[](currIndex); + uint256[] memory trimmedIssuedOns = new uint256[](currIndex); + address[] memory trimmedSigners = new address[](currIndex); + + for (uint256 i = 0; i < currIndex; i = i.add(1)) { + trimmedAccounts[i] = accounts[i]; + trimmedIssuedOns[i] = issuedOns[i]; + trimmedSigners[i] = signers[i]; } - return trimmedAttestations; + return (trimmedAccounts, trimmedIssuedOns, trimmedSigners); } else { - return attestations; + return (accounts, issuedOns, signers); } } + /** + * @notice Returns identifiers mapped (by unrevoked signers) to a + * given account address by a list of trusted issuers + * @param account Address of the account + * @param trustedIssuers Array of n issuers whose identifier mappings will be used + * @param maxIdentifiers Limit the number of identifiers that will be returned + * @return Array (length <= maxIdentifiers) of identifiers + * @dev Adds identifier info to the arrays in order of provided trustedIssuers + * @dev Expectation that only one attestation exists per (identifier, issuer, account) + */ + + // TODO ASv2 consider also returning an array with counts of identifiers per issuer + function lookupIdentifiersByAddress( address account, address[] memory trustedIssuers, @@ -127,24 +202,22 @@ contract FederatedAttestations is uint256 currIndex = 0; bytes32[] memory identifiers = new bytes32[](maxIdentifiers); - for (uint256 i = 0; i < trustedIssuers.length; i++) { + for (uint256 i = 0; i < trustedIssuers.length; i = i.add(1)) { address trustedIssuer = trustedIssuers[i]; - for (uint256 j = 0; j < addressToIdentifiers[account][trustedIssuer].length; j++) { + uint256 numIssuersForAddress = addressToIdentifiers[account][trustedIssuer].length; + for (uint256 j = 0; j < numIssuersForAddress; j = j.add(1)) { // Iterate through the list of identifiers if (currIndex < maxIdentifiers) { bytes32 identifier = addressToIdentifiers[account][trustedIssuer][j]; - // Check if this signer for this particular signer is revoked - for (uint256 k = 0; k < identifierToAddresses[identifier][trustedIssuer].length; k++) { - IdentifierOwnershipAttestation memory attestation = identifierToAddresses[identifier][trustedIssuer][k]; - // For now, just take the first published, unrevoked signer that matches - // TODO redo this to take into account either recency or the "correct" identifier - // based on the index - if ( - attestation.account == account && - !_isRevoked(attestation.signer, attestation.issuedOn) - ) { + // Check if the mapping was produced by a revoked signer + // solhint-disable-next-line max-line-length + OwnershipAttestation[] memory attestationsForIssuer = identifierToAddresses[identifier][trustedIssuer]; + for (uint256 k = 0; k < attestationsForIssuer.length; k = k.add(1)) { + OwnershipAttestation memory attestation = attestationsForIssuer[k]; + // (identifier, account, issuer) tuples should be unique + if (attestation.account == account && !revokedSigners[attestation.signer]) { identifiers[currIndex] = identifier; - currIndex++; + currIndex = currIndex.add(1); break; } } @@ -156,7 +229,7 @@ contract FederatedAttestations is if (currIndex < maxIdentifiers) { // Allocate and fill properly-sized array bytes32[] memory trimmedIdentifiers = new bytes32[](currIndex); - for (uint256 i = 0; i < currIndex; i++) { + for (uint256 i = 0; i < currIndex; i = i.add(1)) { trimmedIdentifiers[i] = identifiers[i]; } return trimmedIdentifiers; @@ -165,72 +238,149 @@ contract FederatedAttestations is } } - function validateAttestation( + // TODO do we want to restrict permissions, or should anyone + // with a valid signature be able to register an attestation? + modifier isValidUser(address issuer, address account) { + require( + msg.sender == account || + msg.sender == issuer || + getAccounts().attestationSignerToAccount(msg.sender) == issuer, + "User does not have permission to perform this action" + ); + require(!revokedSigners[msg.sender], "User has been revoked "); + _; + } + + /** + * @notice Validates the given attestation and signature + * @param identifier Hash of the identifier to be attested + * @param issuer Address of the attestation issuer + * @param account Address of the account being mapped to the identifier + * @param issuedOn Time at which the issuer issued the attestation in Unix time + * @param signer Address of the signer of the attestation + * @param v The recovery id of the incoming ECDSA signature + * @param r Output value r of the ECDSA signature + * @param s Output value s of the ECDSA signature + * @return Whether the signature is valid + * @dev Throws if signer is revoked + * @dev Throws if signer is not an authorized AttestationSigner of the issuer + */ + function isValidAttestation( bytes32 identifier, address issuer, - IdentifierOwnershipAttestation memory attestation, + address account, + uint256 issuedOn, + address signer, uint8 v, bytes32 r, bytes32 s - ) public view returns (address) { - // TODO check if signer is revoked and is a valid signer of the account + ) public view returns (bool) { + require(!revokedSigners[signer], "Signer has been revoked"); + require( + getAccounts().attestationSignerToAccount(signer) == issuer, + "Signer has not been authorized as an AttestationSigner by the issuer" + ); + bytes32 structHash = keccak256( + abi.encode(EIP712_VALIDATE_ATTESTATION_TYPEHASH, identifier, issuer, account, issuedOn) + ); + address guessedSigner = Signatures.getSignerOfTypedDataHash( + eip712DomainSeparator, + structHash, + v, + r, + s + ); + return guessedSigner == signer; } + /** + * @notice Registers an attestation with a valid signature + * @param identifier Hash of the identifier to be attested + * @param issuer Address of the attestation issuer + * @param account Address of the account being mapped to the identifier + * @param issuedOn Time at which the issuer issued the attestation in Unix time + * @param signer Address of the signer of the attestation + * @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 Throws if sender is not the issuer, account, or an authorized AttestationSigner + * @dev Throws if an attestation with the same (identifier, issuer, account) already exists + */ function registerAttestation( bytes32 identifier, address issuer, - IdentifierOwnershipAttestation memory attestation - ) public { - // TODO call validateAttestation here + address account, + uint256 issuedOn, + address signer, + uint8 v, + bytes32 r, + bytes32 s + ) public isValidUser(issuer, account) { require( - msg.sender == attestation.account || msg.sender == issuer || msg.sender == attestation.signer + isValidAttestation(identifier, issuer, account, issuedOn, signer, v, r, s), + "Signature is invalid" ); - for (uint256 i = 0; i < identifierToAddresses[identifier][issuer].length; i++) { - // This enforces only one attestation to be uploaded for a given set of (identifier, issuer, account) + for (uint256 i = 0; i < identifierToAddresses[identifier][issuer].length; i = i.add(1)) { + // This enforces only one attestation to be uploaded + // for a given set of (identifier, issuer, account) // Editing/upgrading an attestation requires that it be deleted before a new one is registered - require(identifierToAddresses[identifier][issuer][i].account != attestation.account); + require( + identifierToAddresses[identifier][issuer][i].account != account, + "Attestation for this account already exists" + ); } + OwnershipAttestation memory attestation = OwnershipAttestation(account, issuedOn, signer); identifierToAddresses[identifier][issuer].push(attestation); - addressToIdentifiers[attestation.account][issuer].push(identifier); + addressToIdentifiers[account][issuer].push(identifier); + emit AttestationRegistered(identifier, issuer, account, issuedOn, signer); } - function deleteAttestation(bytes32 identifier, address issuer, address account) public { - // TODO ASv2 this should short-circuit, but double check (i.e. succeeds if msg.sender == account) - require( - msg.sender == account || getAccounts().attestationSignerToAccount(msg.sender) == issuer - ); - - for (uint256 i = 0; i < identifierToAddresses[identifier][issuer].length; i++) { - IdentifierOwnershipAttestation memory attestation = identifierToAddresses[identifier][issuer][i]; + /** + * @notice Deletes an attestation + * @param identifier Hash of the identifier to be deleted + * @param issuer Address of the attestation issuer + * @param account Address of the account mapped to the identifier + * @dev Throws if sender is not the issuer, account, or an authorized AttestationSigner + */ + function deleteAttestation(bytes32 identifier, address issuer, address account) + public + isValidUser(issuer, account) + { + OwnershipAttestation[] memory attestations = identifierToAddresses[identifier][issuer]; + for (uint256 i = 0; i < attestations.length; i = i.add(1)) { + OwnershipAttestation memory attestation = attestations[i]; if (attestation.account == account) { - // This is meant to delete the attestation in the array and then move the last element in the array to that empty spot, to avoid having empty elements in the array - // Not sure if this is needed and if the added gas costs from the complexity is worth it - identifierToAddresses[identifier][issuer][i] = identifierToAddresses[identifier][issuer][identifierToAddresses[identifier][issuer] - .length - - 1]; + // This is meant to delete the attestation in the array + // and then move the last element in the array to that empty spot, + // to avoid having empty elements in the array + // TODO reviewers: is there a better way of doing this? + identifierToAddresses[identifier][issuer][i] = attestations[attestations.length - 1]; identifierToAddresses[identifier][issuer].pop(); - // TODO revisit if deletedIdentifier check is necessary - not sure if there would ever be a situation where the matching identifier is not present bool deletedIdentifier = false; - for (uint256 j = 0; j < addressToIdentifiers[account][issuer].length; j++) { - if (addressToIdentifiers[account][issuer][j] == identifier) { - addressToIdentifiers[account][issuer][j] = addressToIdentifiers[account][issuer][addressToIdentifiers[account][issuer] - .length - - 1]; + + bytes32[] memory identifiers = addressToIdentifiers[account][issuer]; + for (uint256 j = 0; j < identifiers.length; j = j.add(1)) { + if (identifiers[j] == identifier) { + addressToIdentifiers[account][issuer][j] = identifiers[identifiers.length - 1]; addressToIdentifiers[account][issuer].pop(); deletedIdentifier = true; break; } } - // Hard requirement to delete from both mappings in unison - require(deletedIdentifier); + // Should never be false - both mappings should always be updated in unison + assert(deletedIdentifier); + + emit AttestationDeleted(identifier, issuer, account); break; } } } - function revokeSigner(address signer, uint256 revokedOn) public { - // TODO ASv2 add constraints on who can revoke a signer - revokedSigners[signer] = revokedOn; + function revokeSigner(address signer) public { + // TODO ASv2 add constraints on who has permissions to revoke a signer + // TODO ASv2 consider whether we want to check if the signer is an authorized signer + // or to allow any address to be revoked + revokedSigners[signer] = true; } } diff --git a/packages/protocol/lib/fed-attestations-utils.ts b/packages/protocol/lib/fed-attestations-utils.ts new file mode 100644 index 00000000000..9d494e8fff8 --- /dev/null +++ b/packages/protocol/lib/fed-attestations-utils.ts @@ -0,0 +1,80 @@ +import { ensureLeading0x } from '@celo/base' +import { Address } from '@celo/utils/lib/address' +import { structHash } from '@celo/utils/lib/sign-typed-data-utils' +import { generateTypedDataHash } from '@celo/utils/src/sign-typed-data-utils' +import { parseSignatureWithoutPrefix } from '@celo/utils/src/signatureUtils' + +export interface AttestationDetails{ + identifier: string, + issuer: string, + account: string, + issuedOn: number, +} + +const getTypedData = (chainId: number, contractAddress: Address, message?: AttestationDetails) => { + const typedData = { + types: { + EIP712Domain: [ + { name: 'name', type: 'string' }, + { name: 'version', type: 'string' }, + { name: 'chainId', type: 'uint256'}, + { name: 'verifyingContract', type: 'address'}, + ], + OwnershipAttestation: [ + { name: 'identifier', type: 'bytes32' }, + { name: 'issuer', type: 'address'}, + { name: 'account', type: 'address' }, + { name: 'issuedOn', type: 'uint256' }, + // TODO ASv2 Consider including a nonce (which could also be used as an ID) + ], + }, + primaryType: 'OwnershipAttestation', + domain: { + name: 'FederatedAttestations', + version: '1.0', + chainId, + verifyingContract: contractAddress + }, + message: message ? message : {} + } + return typedData +} + +export const getSignatureForAttestation = async ( + identifier: string, + issuer: string, + account: string, + issuedOn: number, + signer: string, + chainId: number, + contractAddress: string +) => { + const typedData = getTypedData(chainId, contractAddress, { identifier,issuer,account, issuedOn}) + + const signature = await new Promise((resolve, reject) => { + web3.currentProvider.send( + { + method: 'eth_signTypedData', + params: [signer, typedData], + }, + (error, resp) => { + if (error) { + reject(error) + } else { + resolve(resp.result) + } + } + ) + }) + + const messageHash = ensureLeading0x(generateTypedDataHash(typedData).toString('hex')) + const parsedSignature = parseSignatureWithoutPrefix(messageHash, signature, signer) + return parsedSignature +} + +export const getDomainDigest = (contractAddress: Address) => { + const typedData = getTypedData(1, contractAddress) + return ensureLeading0x( + structHash('EIP712Domain', typedData.domain, typedData.types).toString('hex') + ) +} \ No newline at end of file diff --git a/packages/protocol/migrationsConfig.js b/packages/protocol/migrationsConfig.js index 799b124efd3..43cf44fb2f4 100644 --- a/packages/protocol/migrationsConfig.js +++ b/packages/protocol/migrationsConfig.js @@ -638,6 +638,7 @@ const linkedLibraries = { 'LockedGold', 'Escrow', 'MetaTransactionWallet', + 'FederatedAttestations', ], } diff --git a/packages/protocol/test/common/accounts.ts b/packages/protocol/test/common/accounts.ts index 2ea8848a728..9c7f8577129 100644 --- a/packages/protocol/test/common/accounts.ts +++ b/packages/protocol/test/common/accounts.ts @@ -13,6 +13,7 @@ import { parseSolidityStringArray } from '@celo/utils/lib/parsing' import { authorizeSigner as buildAuthorizeSignerTypedData } from '@celo/utils/lib/typed-data-constructors' import { generateTypedDataHash } from '@celo/utils/src/sign-typed-data-utils' import { parseSignatureWithoutPrefix } from '@celo/utils/src/signatureUtils' +import BigNumber from 'bignumber.js' import { AccountsContract, AccountsInstance, @@ -22,8 +23,6 @@ import { } from 'types' import { keccak256 } from 'web3-utils' -import BigNumber from 'bignumber.js' - const Accounts: AccountsContract = artifacts.require('Accounts') const Registry: RegistryContract = artifacts.require('Registry') const MockValidators: MockValidatorsContract = artifacts.require('MockValidators') diff --git a/packages/protocol/test/identity/federatedattestations.ts b/packages/protocol/test/identity/federatedattestations.ts index 54070cb21e9..b3d7cb52501 100644 --- a/packages/protocol/test/identity/federatedattestations.ts +++ b/packages/protocol/test/identity/federatedattestations.ts @@ -1,5 +1,15 @@ +import { + getDomainDigest, + getSignatureForAttestation, +} from '@celo/protocol/lib/fed-attestations-utils' import { CeloContractName } from '@celo/protocol/lib/registry-utils' -// import { getPhoneHash } from '@celo/utils/lib/phoneNumbers' +import { + assertLogMatches2, + assertRevert, + assertRevertWithReason, +} from '@celo/protocol/lib/test-utils' +import { getPhoneHash } from '@celo/utils/lib/phoneNumbers' +import BigNumber from 'bignumber.js' import { AccountsContract, AccountsInstance, @@ -8,6 +18,7 @@ import { RegistryContract, RegistryInstance, } from 'types' +import { keccak256 } from 'web3-utils' const Accounts: AccountsContract = artifacts.require('Accounts') const FederatedAttestations: FederatedAttestationsContract = artifacts.require( @@ -15,29 +26,970 @@ const FederatedAttestations: FederatedAttestationsContract = artifacts.require( ) const Registry: RegistryContract = artifacts.require('Registry') -contract('Attestations', (accounts: string[]) => { +contract('FederatedAttestations', (accounts: string[]) => { let accountsInstance: AccountsInstance let federatedAttestations: FederatedAttestationsInstance let registry: RegistryInstance + let initialize + + const chainId = 1 + + const issuer1 = accounts[0] + const signer1 = accounts[1] + const account1 = accounts[2] + + const phoneNumber: string = '+18005551212' + const identifier1 = getPhoneHash(phoneNumber) + const identifier2 = getPhoneHash(phoneNumber, 'dummySalt') + + const nowUnixTime = Math.floor(Date.now() / 1000) + + const signerRole = keccak256('celo.org/core/attestation') + let sig - const caller: string = accounts[0] - // const phoneNumber: string = '+18005551212' - // const phoneHash: string = getPhoneHash(phoneNumber) + const signAndRegisterAttestation = async ( + identifier: string, + issuer: string, // Must be a registered account + account: string, + issuedOn: number, + signer: string + ) => { + const attestationSignature = await getSignatureForAttestation( + identifier, + issuer, + account, + issuedOn, + signer, + chainId, + federatedAttestations.address + ) + if (issuer !== signer && !(await accountsInstance.isSigner(issuer, signer, signerRole))) { + await accountsInstance.authorizeSigner(signer, signerRole, { + from: issuer, + }) + await accountsInstance.completeSignerAuthorization(issuer, signerRole, { + from: signer, + }) + } + await federatedAttestations.registerAttestation( + identifier, + issuer, + account, + issuedOn, + signer, + attestationSignature.v, + attestationSignature.r, + attestationSignature.s, + { + from: issuer, + } + ) + } beforeEach('FederatedAttestations setup', async () => { accountsInstance = await Accounts.new(true) federatedAttestations = await FederatedAttestations.new(true) registry = await Registry.new(true) - await accountsInstance.initialize(registry.address) await registry.setAddressFor(CeloContractName.Accounts, accountsInstance.address) - await federatedAttestations.initialize(registry.address) + await registry.setAddressFor( + CeloContractName.FederatedAttestations, + federatedAttestations.address + ) + initialize = await federatedAttestations.initialize(registry.address) + + await accountsInstance.createAccount({ from: issuer1 }) + sig = await getSignatureForAttestation( + identifier1, + issuer1, + account1, + nowUnixTime, + signer1, + chainId, + federatedAttestations.address + ) + }) + + describe('#EIP712_VALIDATE_ATTESTATION_TYPEHASH()', () => { + it('should have set the right typehash', async () => { + const expectedTypehash = keccak256( + 'OwnershipAttestation(bytes32 identifier,address issuer,address account,uint256 issuedOn)' + ) + assert.equal( + await federatedAttestations.EIP712_VALIDATE_ATTESTATION_TYPEHASH(), + expectedTypehash + ) + }) }) describe('#initialize()', () => { - it('TODO ASv2', async () => { - // TODO ASv2 - assert(caller) - assert(federatedAttestations) + it('should have set the owner', async () => { + const owner: string = await federatedAttestations.owner() + assert.equal(owner, issuer1) + }) + + it('should have set the registry address', async () => { + const registryAddress: string = await federatedAttestations.registry() + assert.equal(registryAddress, registry.address) + }) + + it('should have set the EIP-712 domain separator', async () => { + assert.equal( + await federatedAttestations.eip712DomainSeparator(), + getDomainDigest(federatedAttestations.address) + ) + }) + + it('should emit the EIP712DomainSeparatorSet event', () => { + assertLogMatches2(initialize.logs[2], { + event: 'EIP712DomainSeparatorSet', + args: { + eip712DomainSeparator: getDomainDigest(federatedAttestations.address), + }, + }) + }) + + it('should not be callable again', async () => { + await assertRevert(federatedAttestations.initialize(registry.address)) + }) + }) + + describe('#lookupAttestations', () => { + interface AttestationTestCase { + account: string + issuedOn: number + signer: string + } + + const checkAgainstExpectedAttestations = ( + expectedAttestations: AttestationTestCase[], + actualAddresses: string[], + actualIssuedOns: BigNumber[], + actualSigners: string[] + ) => { + assert.lengthOf(actualAddresses, expectedAttestations.length) + assert.lengthOf(actualIssuedOns, expectedAttestations.length) + assert.lengthOf(actualSigners, expectedAttestations.length) + + expectedAttestations.forEach((expectedAttestation, index) => { + assert.equal(expectedAttestation.account, actualAddresses[index]) + assert.equal(expectedAttestation.issuedOn, actualIssuedOns[index].toNumber()) + assert.equal(expectedAttestation.signer, actualSigners[index]) + }) + } + + describe('when identifier has not been registered', () => { + it('should return empty list', async () => { + const [addresses, issuedOns, signers] = await federatedAttestations.lookupAttestations( + identifier1, + [issuer1], + 1 + ) + checkAgainstExpectedAttestations([], addresses, issuedOns, signers) + }) + }) + + describe('when identifier has been registered', () => { + const account2 = accounts[3] + + const issuer2 = accounts[4] + const issuer2Signer = accounts[5] + const issuer2Signer2 = accounts[6] + const issuer3 = accounts[7] + + const issuer1Attestations: AttestationTestCase[] = [ + { + account: account1, + issuedOn: nowUnixTime, + signer: signer1, + }, + // Same issuer as [0], different account + { + account: account2, + issuedOn: nowUnixTime, + signer: signer1, + }, + ] + const issuer2Attestations: AttestationTestCase[] = [ + // Same account as issuer1Attestations[0], different issuer + { + account: account1, + issuedOn: nowUnixTime, + signer: issuer2Signer, + }, + // Different account and signer + { + account: account2, + issuedOn: nowUnixTime, + signer: issuer2Signer2, + }, + ] + + beforeEach(async () => { + // Require consistent order for test cases + await accountsInstance.createAccount({ from: issuer2 }) + for (const { issuer, attestationsPerIssuer } of [ + { issuer: issuer1, attestationsPerIssuer: issuer1Attestations }, + { issuer: issuer2, attestationsPerIssuer: issuer2Attestations }, + ]) { + for (const attestation of attestationsPerIssuer) { + await signAndRegisterAttestation( + identifier1, + issuer, + attestation.account, + attestation.issuedOn, + attestation.signer + ) + } + } + }) + + it('should return all attestations from one issuer', async () => { + const [addresses, issuedOns, signers] = await federatedAttestations.lookupAttestations( + identifier1, + [issuer1], + // Do not allow for maxAttestations to coincidentally limit incorrect output + issuer1Attestations.length + 1 + ) + checkAgainstExpectedAttestations(issuer1Attestations, addresses, issuedOns, signers) + }) + + it('should return empty list if no attestations exist for an issuer', async () => { + const [addresses, issuedOns, signers] = await federatedAttestations.lookupAttestations( + identifier1, + [issuer3], + 1 + ) + checkAgainstExpectedAttestations([], addresses, issuedOns, signers) + }) + + it('should return attestations from multiple issuers in correct order', async () => { + const expectedAttestations = issuer2Attestations.concat(issuer1Attestations) + const [addresses, issuedOns, signers] = await federatedAttestations.lookupAttestations( + identifier1, + [issuer3, issuer2, issuer1], + expectedAttestations.length + 1 + ) + checkAgainstExpectedAttestations(expectedAttestations, addresses, issuedOns, signers) + }) + + it('should return empty list if maxAttestations == 0', async () => { + const [addresses, issuedOns, signers] = await federatedAttestations.lookupAttestations( + identifier1, + [issuer1], + 0 + ) + checkAgainstExpectedAttestations([], addresses, issuedOns, signers) + }) + + it('should only return maxAttestations attestations when more are present', async () => { + const expectedAttestations = issuer1Attestations.slice(0, -1) + const [addresses, issuedOns, signers] = await federatedAttestations.lookupAttestations( + identifier1, + [issuer1], + expectedAttestations.length + ) + checkAgainstExpectedAttestations(expectedAttestations, addresses, issuedOns, signers) + }) + + it('should not return attestations from revoked signers', async () => { + const attestationToRevoke = issuer2Attestations[0] + await federatedAttestations.revokeSigner(attestationToRevoke.signer) + const expectedAttestations = issuer2Attestations.slice(1) + + const [addresses, issuedOns, signers] = await federatedAttestations.lookupAttestations( + identifier1, + [issuer2], + issuer2Attestations.length + ) + checkAgainstExpectedAttestations(expectedAttestations, addresses, issuedOns, signers) + }) + }) + }) + + describe('#lookupIdentifiersByAddress', () => { + describe('when address has not been registered', () => { + it('should return empty list', async () => { + const actualIdentifiers = await federatedAttestations.lookupIdentifiersByAddress( + account1, + [issuer1], + 1 + ) + assert.equal(actualIdentifiers.length, 0) + }) + }) + + describe('when address has been registered', () => { + interface IdentifierTestCase { + identifier: string + signer: string + } + + const checkAgainstExpectedIdCases = ( + expectedIdentifiers: IdentifierTestCase[], + actualIdentifiers: string[] + ) => { + expect(expectedIdentifiers.map((idCase) => idCase.identifier)).to.eql(actualIdentifiers) + } + + const issuer2 = accounts[3] + const issuer2Signer = accounts[4] + const issuer2Signer2 = accounts[5] + const issuer3 = accounts[6] + + const issuer1IdCases: IdentifierTestCase[] = [ + { + identifier: identifier1, + signer: signer1, + }, + { + identifier: identifier2, + signer: signer1, + }, + ] + const issuer2IdCases: IdentifierTestCase[] = [ + { + identifier: identifier1, + signer: issuer2Signer2, + }, + { + identifier: identifier2, + signer: issuer2Signer, + }, + ] + + beforeEach(async () => { + await accountsInstance.createAccount({ from: issuer2 }) + // Require consistent order for test cases + for (const { issuer, idCasesPerIssuer } of [ + { issuer: issuer1, idCasesPerIssuer: issuer1IdCases }, + { issuer: issuer2, idCasesPerIssuer: issuer2IdCases }, + ]) { + for (const idCase of idCasesPerIssuer) { + await signAndRegisterAttestation( + idCase.identifier, + issuer, + account1, + nowUnixTime, + idCase.signer + ) + } + } + }) + + it('should return all identifiers from one issuer', async () => { + const actualIdentifiers = await federatedAttestations.lookupIdentifiersByAddress( + account1, + [issuer1], + issuer1IdCases.length + 1 + ) + checkAgainstExpectedIdCases(issuer1IdCases, actualIdentifiers) + }) + + it('should return empty list if no identifiers exist for an (issuer,address)', async () => { + const actualIdentifiers = await federatedAttestations.lookupIdentifiersByAddress( + account1, + [issuer3], + 1 + ) + assert.equal(actualIdentifiers.length, 0) + }) + + it('should return identifiers from multiple issuers in correct order', async () => { + const expectedIdCases = issuer2IdCases.concat(issuer1IdCases) + const actualIdentifiers = await federatedAttestations.lookupIdentifiersByAddress( + account1, + [issuer3, issuer2, issuer1], + expectedIdCases.length + 1 + ) + checkAgainstExpectedIdCases(expectedIdCases, actualIdentifiers) + }) + + it('should return empty list if maxIdentifiers == 0', async () => { + const actualIdentifiers = await federatedAttestations.lookupIdentifiersByAddress( + account1, + [issuer1], + 0 + ) + assert.equal(actualIdentifiers.length, 0) + }) + + it('should only return maxIdentifiers identifiers when more are present', async () => { + const expectedIdCases = issuer2IdCases.concat(issuer1IdCases).slice(0, -1) + const actualIdentifiers = await federatedAttestations.lookupIdentifiersByAddress( + account1, + [issuer2, issuer1], + expectedIdCases.length + ) + checkAgainstExpectedIdCases(expectedIdCases, actualIdentifiers) + }) + + it('should not return identifiers from revoked signers', async () => { + await federatedAttestations.revokeSigner(issuer2IdCases[0].signer) + const expectedIdCases = issuer2IdCases.slice(1) + const actualIdentifiers = await federatedAttestations.lookupIdentifiersByAddress( + account1, + [issuer2], + expectedIdCases.length + 1 + ) + checkAgainstExpectedIdCases(expectedIdCases, actualIdentifiers) + }) + }) + }) + + describe('#isValidAttestation', async () => { + describe('with an authorized AttestationSigner', async () => { + beforeEach(async () => { + await accountsInstance.authorizeSigner(signer1, signerRole, { from: issuer1 }) + await accountsInstance.completeSignerAuthorization(issuer1, signerRole, { from: signer1 }) + }) + + it('should return true if a valid signature is used', async () => { + assert.isTrue( + await federatedAttestations.isValidAttestation( + identifier1, + issuer1, + account1, + nowUnixTime, + signer1, + sig.v, + sig.r, + sig.s + ) + ) + }) + + it('should return false if an invalid signature is provided', async () => { + const sig2 = await getSignatureForAttestation( + identifier1, + issuer1, + account1, + nowUnixTime, + accounts[3], + chainId, + federatedAttestations.address + ) + assert.isFalse( + await federatedAttestations.isValidAttestation( + identifier1, + issuer1, + account1, + nowUnixTime, + signer1, + sig2.v, + sig2.r, + sig2.s + ) + ) + }) + + const wrongArgs = [ + [0, 'identifier', identifier2], + [1, 'issuer', accounts[3]], + [2, 'account', accounts[3]], + [3, 'issuedOn', nowUnixTime - 1], + [4, 'signer', accounts[3]], + ] + wrongArgs.forEach(([index, arg, wrongValue]) => { + it(`should fail if the provided ${arg} is different from the attestation`, async () => { + const args = [identifier1, issuer1, account1, nowUnixTime, signer1, sig.v, sig.r, sig.s] + args[index] = wrongValue + + if (arg === 'issuer' || arg === 'signer') { + await assertRevert(federatedAttestations.isValidAttestation.apply(this, args)) + } else { + assert.isFalse(await federatedAttestations.isValidAttestation.apply(this, args)) + } + }) + }) + + it('should revert if the signer is revoked', async () => { + await federatedAttestations.revokeSigner(signer1) + await assertRevertWithReason( + federatedAttestations.isValidAttestation( + identifier1, + issuer1, + account1, + nowUnixTime, + signer1, + sig.v, + sig.r, + sig.s + ), + 'Signer has been revoked' + ) + }) + }) + + it('should revert if the signer is not authorized as an AttestationSigner by the issuer', async () => { + await assertRevert( + federatedAttestations.isValidAttestation( + identifier1, + issuer1, + account1, + nowUnixTime, + signer1, + sig.v, + sig.r, + sig.s + ) + ) + }) + + it('should revert if the signer is authorized as a different role by the issuer', async () => { + const role = keccak256('random') + await accountsInstance.authorizeSigner(signer1, role, { from: issuer1 }) + await accountsInstance.completeSignerAuthorization(issuer1, role, { from: signer1 }) + + await assertRevert( + federatedAttestations.isValidAttestation( + identifier1, + issuer1, + account1, + nowUnixTime, + signer1, + sig.v, + sig.r, + sig.s + ) + ) + }) + }) + + describe('#registerAttestation', () => { + beforeEach(async () => { + await accountsInstance.authorizeSigner(signer1, signerRole, { from: issuer1 }) + await accountsInstance.completeSignerAuthorization(issuer1, signerRole, { from: signer1 }) + }) + + it('should emit AttestationRegistered for a valid attestation', async () => { + const register = await federatedAttestations.registerAttestation( + identifier1, + issuer1, + account1, + nowUnixTime, + signer1, + sig.v, + sig.r, + sig.s + ) + assertLogMatches2(register.logs[0], { + event: 'AttestationRegistered', + args: { + identifier: identifier1, + issuer: issuer1, + account: account1, + issuedOn: nowUnixTime, + signer: signer1, + }, + }) + }) + + it('should succeed if issuer == signer', async () => { + const issuerSig = await getSignatureForAttestation( + identifier1, + issuer1, + account1, + nowUnixTime, + issuer1, + chainId, + federatedAttestations.address + ) + const register = await federatedAttestations.registerAttestation( + identifier1, + issuer1, + account1, + nowUnixTime, + issuer1, + issuerSig.v, + issuerSig.r, + issuerSig.s + ) + assertLogMatches2(register.logs[0], { + event: 'AttestationRegistered', + args: { + identifier: identifier1, + issuer: issuer1, + account: account1, + issuedOn: nowUnixTime, + signer: issuer1, + }, + }) + }) + + it('should revert if an invalid signature is provided', async () => { + const sig2 = await getSignatureForAttestation( + identifier1, + issuer1, + account1, + nowUnixTime, + accounts[3], + chainId, + federatedAttestations.address + ) + await assertRevert( + federatedAttestations.registerAttestation( + identifier1, + issuer1, + account1, + nowUnixTime, + signer1, + sig2.v, + sig2.r, + sig2.s + ) + ) + }) + + it('should revert if signer has been revoked', async () => { + await federatedAttestations.revokeSigner(signer1) + await assertRevertWithReason( + federatedAttestations.registerAttestation( + identifier1, + issuer1, + account1, + nowUnixTime, + signer1, + sig.v, + sig.r, + sig.s + ), + 'Signer has been revoked' + ) + }) + + describe('when registering a second attestation', () => { + beforeEach(async () => { + // register first attestation + await federatedAttestations.registerAttestation( + identifier1, + issuer1, + account1, + nowUnixTime, + signer1, + sig.v, + sig.r, + sig.s + ) + }) + + it('should revert if an attestation with the same (issuer, identifier, account) is uploaded again', async () => { + // Upload the same attestation signed by a different signer, authorized under the same issuer + const signer2 = accounts[4] + await accountsInstance.authorizeSigner(signer2, signerRole, { from: issuer1 }) + await accountsInstance.completeSignerAuthorization(issuer1, signerRole, { from: signer2 }) + const sig2 = await getSignatureForAttestation( + identifier1, + issuer1, + account1, + nowUnixTime + 1, + signer2, + 1, + federatedAttestations.address + ) + await assertRevert( + federatedAttestations.registerAttestation( + identifier1, + issuer1, + account1, + nowUnixTime, + signer2, + sig2.v, + sig2.r, + sig2.s + ) + ) + }) + + it('should succeed with a different identifier', async () => { + const sig2 = await getSignatureForAttestation( + identifier2, + issuer1, + account1, + nowUnixTime, + signer1, + chainId, + federatedAttestations.address + ) + const register2 = await federatedAttestations.registerAttestation( + identifier2, + issuer1, + account1, + nowUnixTime, + signer1, + sig2.v, + sig2.r, + sig2.s + ) + assertLogMatches2(register2.logs[0], { + event: 'AttestationRegistered', + args: { + identifier: identifier2, + issuer: issuer1, + account: account1, + issuedOn: nowUnixTime, + signer: signer1, + }, + }) + }) + + it('should succeed with a different issuer', async () => { + const issuer2 = accounts[4] + const signer2 = accounts[5] + await accountsInstance.createAccount({ from: issuer2 }) + await accountsInstance.authorizeSigner(signer2, signerRole, { from: issuer2 }) + await accountsInstance.completeSignerAuthorization(issuer2, signerRole, { from: signer2 }) + const sig2 = await getSignatureForAttestation( + identifier1, + issuer2, + account1, + nowUnixTime, + signer2, + chainId, + federatedAttestations.address + ) + const register2 = await federatedAttestations.registerAttestation( + identifier1, + issuer2, + account1, + nowUnixTime, + signer2, + sig2.v, + sig2.r, + sig2.s, + { from: issuer2 } + ) + assertLogMatches2(register2.logs[0], { + event: 'AttestationRegistered', + args: { + identifier: identifier1, + issuer: issuer2, + account: account1, + issuedOn: nowUnixTime, + signer: signer2, + }, + }) + }) + + it('should succeed with a different account', async () => { + const account2 = accounts[4] + const sig2 = await getSignatureForAttestation( + identifier1, + issuer1, + account2, + nowUnixTime, + signer1, + chainId, + federatedAttestations.address + ) + const register2 = await federatedAttestations.registerAttestation( + identifier1, + issuer1, + account2, + nowUnixTime, + signer1, + sig2.v, + sig2.r, + sig2.s, + { from: issuer1 } + ) + assertLogMatches2(register2.logs[0], { + event: 'AttestationRegistered', + args: { + identifier: identifier1, + issuer: issuer1, + account: account2, + issuedOn: nowUnixTime, + signer: signer1, + }, + }) + }) + }) + + it('should revert if an invalid user attempts to register the attestation', async () => { + await assertRevert( + federatedAttestations.registerAttestation( + identifier1, + issuer1, + account1, + nowUnixTime, + signer1, + sig.v, + sig.r, + sig.s, + { from: accounts[4] } + ) + ) + }) + + it('should succeed if a different AttestationSigner authorized by the same issuer registers the attestation', async () => { + const signer2 = accounts[4] + await accountsInstance.authorizeSigner(signer2, signerRole, { from: issuer1 }) + await accountsInstance.completeSignerAuthorization(issuer1, signerRole, { from: signer2 }) + const register = await federatedAttestations.registerAttestation( + identifier1, + issuer1, + account1, + nowUnixTime, + signer1, + sig.v, + sig.r, + sig.s, + { from: signer2 } + ) + assertLogMatches2(register.logs[0], { + event: 'AttestationRegistered', + args: { + identifier: identifier1, + issuer: issuer1, + account: account1, + issuedOn: nowUnixTime, + signer: signer1, + }, + }) + }) + }) + + describe('#deleteAttestation', () => { + // TODO ASv2 check that the actual entries were deleted in both mappings + // (for identifiers and attestations) + beforeEach(async () => { + await accountsInstance.authorizeSigner(signer1, signerRole, { from: issuer1 }) + await accountsInstance.completeSignerAuthorization(issuer1, signerRole, { from: signer1 }) + await federatedAttestations.registerAttestation( + identifier1, + issuer1, + account1, + nowUnixTime, + signer1, + sig.v, + sig.r, + sig.s + ) + }) + + it('should emit an AttestationDeleted event after successfully deleting', async () => { + const deleteAttestation = await federatedAttestations.deleteAttestation( + identifier1, + issuer1, + account1 + ) + assertLogMatches2(deleteAttestation.logs[0], { + event: 'AttestationDeleted', + args: { + identifier: identifier1, + issuer: issuer1, + account: account1, + }, + }) + }) + + it('should succeed when >1 attestations are registered for (identifier, issuer)', async () => { + const account2 = accounts[3] + await signAndRegisterAttestation(identifier1, issuer1, account2, nowUnixTime, signer1) + const deleteAttestation = await federatedAttestations.deleteAttestation( + identifier1, + issuer1, + account2, + { + from: account2, + } + ) + assertLogMatches2(deleteAttestation.logs[0], { + event: 'AttestationDeleted', + args: { + identifier: identifier1, + issuer: issuer1, + account: account2, + }, + }) + }) + + it('should succeed when >1 identifiers are registered for (account, issuer)', async () => { + await signAndRegisterAttestation(identifier2, issuer1, account1, nowUnixTime, signer1) + const deleteAttestation = await federatedAttestations.deleteAttestation( + identifier2, + issuer1, + account1, + { + from: account1, + } + ) + assertLogMatches2(deleteAttestation.logs[0], { + event: 'AttestationDeleted', + args: { + identifier: identifier2, + issuer: issuer1, + account: account1, + }, + }) + }) + + it('should revert if an invalid user attempts to delete the attestation', async () => { + await assertRevert( + federatedAttestations.deleteAttestation(identifier1, issuer1, account1, { + from: accounts[4], + }) + ) + }) + + it('should revert if a revoked signer attempts to delete the attestation', async () => { + await federatedAttestations.revokeSigner(signer1) + await assertRevert( + federatedAttestations.deleteAttestation(identifier1, issuer1, account1, { from: signer1 }) + ) + }) + + it('should successfully delete an attestation with a revoked signer', async () => { + await federatedAttestations.revokeSigner(signer1) + const deleteAttestation = await federatedAttestations.deleteAttestation( + identifier1, + issuer1, + account1 + ) + assertLogMatches2(deleteAttestation.logs[0], { + event: 'AttestationDeleted', + args: { + identifier: identifier1, + issuer: issuer1, + account: account1, + }, + }) + }) + + it('should fail registering same attestation but succeed after deleting it', async () => { + await assertRevert( + federatedAttestations.registerAttestation( + identifier1, + issuer1, + account1, + nowUnixTime, + signer1, + sig.v, + sig.r, + sig.s + ) + ) + await federatedAttestations.deleteAttestation(identifier1, issuer1, account1) + const register = await federatedAttestations.registerAttestation( + identifier1, + issuer1, + account1, + nowUnixTime, + signer1, + sig.v, + sig.r, + sig.s + ) + assertLogMatches2(register.logs[0], { + event: 'AttestationRegistered', + args: { + identifier: identifier1, + issuer: issuer1, + account: account1, + issuedOn: nowUnixTime, + signer: signer1, + }, + }) }) }) }) From 93255c456aef89f4cf747f752d43637d5ff6e439 Mon Sep 17 00:00:00 2001 From: Eela Nagaraj <7308464+eelanagaraj@users.noreply.github.com> Date: Thu, 12 May 2022 14:12:57 +0200 Subject: [PATCH 05/30] Fix phone-utils dependency --- dependency-graph.json | 1 + packages/protocol/package.json | 1 + packages/protocol/test/identity/federatedattestations.ts | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/dependency-graph.json b/dependency-graph.json index 48736e4658d..4891d797aec 100644 --- a/dependency-graph.json +++ b/dependency-graph.json @@ -85,6 +85,7 @@ "@celo/connect", "@celo/cryptographic-utils", "@celo/flake-tracker", + "@celo/phone-utils", "@celo/typescript", "@celo/utils" ] diff --git a/packages/protocol/package.json b/packages/protocol/package.json index ccbf9424577..c8be1fbef01 100644 --- a/packages/protocol/package.json +++ b/packages/protocol/package.json @@ -93,6 +93,7 @@ }, "devDependencies": { "@celo/flake-tracker": "0.0.1-dev", + "@celo/phone-utils": "2.0.1-dev", "@celo/typechain-target-web3-v1-celo": "0.2.0", "@celo/typescript": "0.0.1", "@types/bn.js": "^4.11.0", diff --git a/packages/protocol/test/identity/federatedattestations.ts b/packages/protocol/test/identity/federatedattestations.ts index b3d7cb52501..7201c87d5ce 100644 --- a/packages/protocol/test/identity/federatedattestations.ts +++ b/packages/protocol/test/identity/federatedattestations.ts @@ -1,3 +1,4 @@ +import getPhoneHash from '@celo/phone-utils/lib/getPhoneHash' import { getDomainDigest, getSignatureForAttestation, @@ -8,7 +9,6 @@ import { assertRevert, assertRevertWithReason, } from '@celo/protocol/lib/test-utils' -import { getPhoneHash } from '@celo/utils/lib/phoneNumbers' import BigNumber from 'bignumber.js' import { AccountsContract, From 3a4e28cbc0edb713b2c444c5bd10633b6c9e93af Mon Sep 17 00:00:00 2001 From: Eela Nagaraj <7308464+eelanagaraj@users.noreply.github.com> Date: Mon, 23 May 2022 20:45:00 +0200 Subject: [PATCH 06/30] Add and fix lookup* functionality (#9577) * Return counts per issuer for lookupAttestations * Return counts per issuer for lookupIdentifiersByAddress * Add lookupAllAttestations (+ helper getTotalNumberOfAttestations) and tests * Add getNumberOfUnrevokedAttestations and tests * Nit: fix ordering of expectations and actuals in check helper funcs * Add getNumberOfUnrevokedIdentifiers and tests * Fix var naming in getTotalNumberOfAttestations * Add getTotalNumberOfIdentifiers and tests * Add lookupAllIdentifiersByAddress (+ helper getTotalNumberOfIdentifiers) and tests * Reorganize tests for looking up attestations * Reorganize tests for looking up identifiers * Fix scope of test helper functions * Address ASv2 PR comments * Add mini-fixes post benchmarking * Make getTotalNumberOf* functions internal * Add comments to address PR review * Add includeRevoked param to lookupAllAttestations * Add includeRevoked param to lookupAllIdentifiersByAddress * Fix docstrings * Rename lookup functions and helpers * Nit use ternary assignment * Rename identifierToAddresses -> identifierToAttestations * Unnest if statements in lookup funcs to address PR comments * Fix and update TODOs to include DNM * Fix lint checks * Remove DNM as this blocks linting :-| --- .../identity/FederatedAttestations.sol | 465 +++++++++--- .../test/identity/federatedattestations.ts | 670 ++++++++++++++---- 2 files changed, 915 insertions(+), 220 deletions(-) diff --git a/packages/protocol/contracts/identity/FederatedAttestations.sol b/packages/protocol/contracts/identity/FederatedAttestations.sol index d8e0fec4d1d..c9571ddedbf 100644 --- a/packages/protocol/contracts/identity/FederatedAttestations.sol +++ b/packages/protocol/contracts/identity/FederatedAttestations.sol @@ -12,8 +12,6 @@ import "../common/interfaces/ICeloVersionedContract.sol"; import "../common/Initializable.sol"; import "../common/UsingRegistry.sol"; import "../common/Signatures.sol"; -import "../common/UsingPrecompiles.sol"; -import "../common/libraries/ReentrancyGuard.sol"; /** * @title Contract mapping identifiers to accounts @@ -23,9 +21,7 @@ contract FederatedAttestations is ICeloVersionedContract, Ownable, Initializable, - UsingRegistry, - ReentrancyGuard, - UsingPrecompiles + UsingRegistry { using SafeMath for uint256; using SafeCast for uint256; @@ -39,7 +35,7 @@ contract FederatedAttestations is // TODO ASv2 revisit linting issues & all solhint-disable-next-line max-line-length // identifier -> issuer -> attestations - mapping(bytes32 => mapping(address => OwnershipAttestation[])) public identifierToAddresses; + mapping(bytes32 => mapping(address => OwnershipAttestation[])) public identifierToAttestations; // account -> issuer -> identifiers mapping(address => mapping(address => bytes32[])) public addressToIdentifiers; // signer => isRevoked @@ -78,7 +74,6 @@ contract FederatedAttestations is _transferOwnership(msg.sender); setRegistry(registryAddress); setEip712DomainSeparator(); - // TODO ASv2 initialize any other variables here } /** @@ -113,12 +108,70 @@ contract FederatedAttestations is } /** - * @notice Returns info about attestations (with unrevoked signers) - * for an identifier produced by a list of issuers + * @notice Helper function for _lookupAttestations to calculate the + total number of attestations completed for an identifier + by each trusted issuer, from unrevoked signers only + * @param identifier Hash of the identifier + * @param trustedIssuers Array of n issuers whose attestations will be included + * @return [0] Sum total of attestations found + * [1] Array of number of attestations found per issuer + */ + function getNumUnrevokedAttestations(bytes32 identifier, address[] memory trustedIssuers) + internal + view + returns (uint256, uint256[] memory) + { + uint256 totalAttestations = 0; + uint256[] memory countsPerIssuer = new uint256[](trustedIssuers.length); + + for (uint256 i = 0; i < trustedIssuers.length; i = i.add(1)) { + // solhint-disable-next-line max-line-length + OwnershipAttestation[] storage attestationsPerIssuer = identifierToAttestations[identifier][trustedIssuers[i]]; + for (uint256 j = 0; j < attestationsPerIssuer.length; j = j.add(1)) { + if (revokedSigners[attestationsPerIssuer[j].signer]) { + continue; + } + totalAttestations = totalAttestations.add(1); + countsPerIssuer[i] = countsPerIssuer[i].add(1); + } + } + return (totalAttestations, countsPerIssuer); + } + + /** + * @notice Helper function for _lookupAttestations to calculate the + total number of attestations completed for an identifier + by each trusted issuer + * @param identifier Hash of the identifier + * @param trustedIssuers Array of n issuers whose attestations will be included + * @return [0] Sum total of attestations found + * [1] Array of number of attestations found per issuer + */ + function getNumAttestations(bytes32 identifier, address[] memory trustedIssuers) + internal + view + returns (uint256, uint256[] memory) + { + uint256 totalAttestations = 0; + uint256 numAttestationsForIssuer; + uint256[] memory countsPerIssuer = new uint256[](trustedIssuers.length); + + for (uint256 i = 0; i < trustedIssuers.length; i = i.add(1)) { + numAttestationsForIssuer = identifierToAttestations[identifier][trustedIssuers[i]].length; + totalAttestations = totalAttestations.add(numAttestationsForIssuer); + countsPerIssuer[i] = numAttestationsForIssuer; + } + return (totalAttestations, countsPerIssuer); + } + + /** + * @notice Returns info about up to `maxAttestations` attestations for + * `identifier` produced by unrevoked signers of `trustedIssuers` * @param identifier Hash of the identifier * @param trustedIssuers Array of n issuers whose attestations will be included * @param maxAttestations Limit the number of attestations that will be returned - * @return for m found attestations, m <= maxAttestations: + * @return [0] Array of number of attestations returned per issuer + * @return [1 - 3] for m (== sum([0])) found attestations, m <= maxAttestations: * [ * Array of m accounts, * Array of m issuedOns, @@ -127,115 +180,357 @@ contract FederatedAttestations is * @dev Adds attestation info to the arrays in order of provided trustedIssuers * @dev Expectation that only one attestation exists per (identifier, issuer, account) */ + // TODO reviewers: is it preferable to return an array of `trustedIssuer` indices + // (indicating issuer per attestation) instead of counts per attestation? + function lookupUnrevokedAttestations( + bytes32 identifier, + address[] calldata trustedIssuers, + uint256 maxAttestations + ) external view returns (uint256[] memory, address[] memory, uint256[] memory, address[] memory) { + // TODO reviewers: this is to get around a stack too deep error; + // are there better ways of dealing with this? + return _lookupUnrevokedAttestations(identifier, trustedIssuers, maxAttestations); + } - // TODO ASv2 consider also returning an array with counts of attestations per issuer - function lookupAttestations( + /** + * @notice Helper function for lookupUnrevokedAttestations to get around stack too deep + * @param identifier Hash of the identifier + * @param trustedIssuers Array of n issuers whose attestations will be included + * @param maxAttestations Limit the number of attestations that will be returned + * @return [0] Array of number of attestations returned per issuer + * @return [1 - 3] for m (== sum([0])) found attestations, m <= maxAttestations: + * [ + * Array of m accounts, + * Array of m issuedOns, + * Array of m signers + * ]; index corresponds to the same attestation + * @dev Adds attestation info to the arrays in order of provided trustedIssuers + * @dev Expectation that only one attestation exists per (identifier, issuer, account) + */ + function _lookupUnrevokedAttestations( bytes32 identifier, address[] memory trustedIssuers, uint256 maxAttestations - ) public view returns (address[] memory, uint256[] memory, address[] memory) { - // Cannot dynamically allocate an in-memory array - // For now require a max returned parameter to pre-allocate and then shrink - // TODO ASv2 is it a risk to allocate an array of size maxAttestations? + ) internal view returns (uint256[] memory, address[] memory, uint256[] memory, address[] memory) { + uint256[] memory countsPerIssuer = new uint256[](trustedIssuers.length); + + // Pre-computing length of unrevoked attestations requires many storage lookups. + // Allow users to call that first and pass this in as maxAttestations. // Same index corresponds to same attestation address[] memory accounts = new address[](maxAttestations); uint256[] memory issuedOns = new uint256[](maxAttestations); address[] memory signers = new address[](maxAttestations); uint256 currIndex = 0; + OwnershipAttestation[] memory attestationsPerIssuer; + + for (uint256 i = 0; i < trustedIssuers.length && currIndex < maxAttestations; i = i.add(1)) { + attestationsPerIssuer = identifierToAttestations[identifier][trustedIssuers[i]]; + for ( + uint256 j = 0; + j < attestationsPerIssuer.length && currIndex < maxAttestations; + j = j.add(1) + ) { + if (revokedSigners[attestationsPerIssuer[j].signer]) { + continue; + } + accounts[currIndex] = attestationsPerIssuer[j].account; + issuedOns[currIndex] = attestationsPerIssuer[j].issuedOn; + signers[currIndex] = attestationsPerIssuer[j].signer; + currIndex = currIndex.add(1); + countsPerIssuer[i] = countsPerIssuer[i].add(1); + } + } + + if (currIndex >= maxAttestations) { + return (countsPerIssuer, accounts, issuedOns, signers); + } + + // Trim returned structs if necessary + address[] memory trimmedAccounts = new address[](currIndex); + uint256[] memory trimmedIssuedOns = new uint256[](currIndex); + address[] memory trimmedSigners = new address[](currIndex); + + for (uint256 i = 0; i < currIndex; i = i.add(1)) { + trimmedAccounts[i] = accounts[i]; + trimmedIssuedOns[i] = issuedOns[i]; + trimmedSigners[i] = signers[i]; + } + return (countsPerIssuer, trimmedAccounts, trimmedIssuedOns, trimmedSigners); + } + + /** + * @notice Similar to lookupUnrevokedAttestations but returns all attestations + * for `identifier` produced by `trustedIssuers`, + * either including or excluding attestations from revoked signers + * @param identifier Hash of the identifier + * @param trustedIssuers Array of n issuers whose attestations will be included + * @param includeRevoked Whether to include attestations produced by revoked signers + * @return [0] Array of number of attestations returned per issuer + * @return [1 - 3] for m (== sum([0])) found attestations: + * [ + * Array of m accounts, + * Array of m issuedOns, + * Array of m signers + * ]; index corresponds to the same attestation + * @dev Adds attestation info to the arrays in order of provided trustedIssuers + * @dev Expectation that only one attestation exists per (identifier, issuer, account) + */ + function lookupAttestations( + bytes32 identifier, + address[] calldata trustedIssuers, + bool includeRevoked + ) external view returns (uint256[] memory, address[] memory, uint256[] memory, address[] memory) { + // TODO reviewers: this is to get around a stack too deep error; + // are there better ways of dealing with this? + return _lookupAttestations(identifier, trustedIssuers, includeRevoked); + } + + /** + * @notice Helper function for lookupAttestations to get around stack too deep + * @param identifier Hash of the identifier + * @param trustedIssuers Array of n issuers whose attestations will be included + * @param includeRevoked Whether to include attestations produced by revoked signers + * @return [0] Array of number of attestations returned per issuer + * @return [1 - 3] for m (== sum([0])) found attestations: + * [ + * Array of m accounts, + * Array of m issuedOns, + * Array of m signers + * ]; index corresponds to the same attestation + * @dev Adds attestation info to the arrays in order of provided trustedIssuers + * @dev Expectation that only one attestation exists per (identifier, issuer, account) + */ + function _lookupAttestations( + bytes32 identifier, + address[] memory trustedIssuers, + bool includeRevoked + ) internal view returns (uint256[] memory, address[] memory, uint256[] memory, address[] memory) { + uint256 totalAttestations; + uint256[] memory countsPerIssuer; + + (totalAttestations, countsPerIssuer) = includeRevoked + ? getNumAttestations(identifier, trustedIssuers) + : getNumUnrevokedAttestations(identifier, trustedIssuers); + + address[] memory accounts = new address[](totalAttestations); + uint256[] memory issuedOns = new uint256[](totalAttestations); + address[] memory signers = new address[](totalAttestations); + + OwnershipAttestation[] memory attestationsPerIssuer; + // Reset this and use as current index to get around stack-too-deep + // TODO reviewers: is it preferable to pack two uint256 counters into a struct + // and use one for total (above) & one for currIndex (below)? + totalAttestations = 0; for (uint256 i = 0; i < trustedIssuers.length; i = i.add(1)) { - uint256 numTrustedIssuers = identifierToAddresses[identifier][trustedIssuers[i]].length; - for (uint256 j = 0; j < numTrustedIssuers; j = j.add(1)) { - // Only create and push new attestation if we haven't hit max - if (currIndex < maxAttestations) { - // solhint-disable-next-line max-line-length - OwnershipAttestation memory attestation = identifierToAddresses[identifier][trustedIssuers[i]][j]; - if (!revokedSigners[attestation.signer]) { - accounts[currIndex] = attestation.account; - issuedOns[currIndex] = attestation.issuedOn; - signers[currIndex] = attestation.signer; - currIndex = currIndex.add(1); + attestationsPerIssuer = identifierToAttestations[identifier][trustedIssuers[i]]; + for (uint256 j = 0; j < attestationsPerIssuer.length; j = j.add(1)) { + if (!includeRevoked && revokedSigners[attestationsPerIssuer[j].signer]) { + continue; + } + accounts[totalAttestations] = attestationsPerIssuer[j].account; + issuedOns[totalAttestations] = attestationsPerIssuer[j].issuedOn; + signers[totalAttestations] = attestationsPerIssuer[j].signer; + totalAttestations = totalAttestations.add(1); + } + } + return (countsPerIssuer, accounts, issuedOns, signers); + } + + /** + * @notice Helper function for lookupIdentifiers to calculate the + total number of identifiers completed for an identifier + by each trusted issuer, from unrevoked signers only + * @param account Address of the account + * @param trustedIssuers Array of n issuers whose identifiers will be included + * @return [0] Sum total of identifiers found + * [1] Array of number of identifiers found per issuer + */ + function getNumUnrevokedIdentifiers(address account, address[] memory trustedIssuers) + internal + view + returns (uint256, uint256[] memory) + { + uint256 totalIdentifiers = 0; + uint256[] memory countsPerIssuer = new uint256[](trustedIssuers.length); + + OwnershipAttestation[] memory attestationsPerIssuer; + bytes32[] memory identifiersPerIssuer; + + for (uint256 i = 0; i < trustedIssuers.length; i = i.add(1)) { + identifiersPerIssuer = addressToIdentifiers[account][trustedIssuers[i]]; + for (uint256 j = 0; j < identifiersPerIssuer.length; j = j.add(1)) { + bytes32 identifier = identifiersPerIssuer[j]; + // Check if the mapping was produced by a revoked signer + attestationsPerIssuer = identifierToAttestations[identifier][trustedIssuers[i]]; + for (uint256 k = 0; k < attestationsPerIssuer.length; k = k.add(1)) { + OwnershipAttestation memory attestation = attestationsPerIssuer[k]; + // (identifier, account, issuer) tuples are checked for uniqueness on registration + if (!(attestation.account == account) || revokedSigners[attestation.signer]) { + continue; } - } else { + totalIdentifiers = totalIdentifiers.add(1); + countsPerIssuer[i] = countsPerIssuer[i].add(1); break; } } } + return (totalIdentifiers, countsPerIssuer); + } - // Trim returned structs if necessary - if (currIndex < maxAttestations) { - address[] memory trimmedAccounts = new address[](currIndex); - uint256[] memory trimmedIssuedOns = new uint256[](currIndex); - address[] memory trimmedSigners = new address[](currIndex); - - for (uint256 i = 0; i < currIndex; i = i.add(1)) { - trimmedAccounts[i] = accounts[i]; - trimmedIssuedOns[i] = issuedOns[i]; - trimmedSigners[i] = signers[i]; - } - return (trimmedAccounts, trimmedIssuedOns, trimmedSigners); - } else { - return (accounts, issuedOns, signers); + /** + * @notice Helper function for lookupIdentifiers to calculate the + total number of identifiers completed for an identifier + by each trusted issuer + * @param account Address of the account + * @param trustedIssuers Array of n issuers whose identifiers will be included + * @return [0] Sum total of identifiers found + * [1] Array of number of identifiers found per issuer + */ + function getNumIdentifiers(address account, address[] memory trustedIssuers) + internal + view + returns (uint256, uint256[] memory) + { + uint256 totalIdentifiers = 0; + uint256 numIdentifiersForIssuer; + uint256[] memory countsPerIssuer = new uint256[](trustedIssuers.length); + + for (uint256 i = 0; i < trustedIssuers.length; i = i.add(1)) { + numIdentifiersForIssuer = addressToIdentifiers[account][trustedIssuers[i]].length; + totalIdentifiers = totalIdentifiers.add(numIdentifiersForIssuer); + countsPerIssuer[i] = numIdentifiersForIssuer; } + return (totalIdentifiers, countsPerIssuer); } /** - * @notice Returns identifiers mapped (by unrevoked signers) to a - * given account address by a list of trusted issuers + * @notice Returns up to `maxIdentifiers` identifiers mapped to `account` + * by unrevoked signers of `trustedIssuers` * @param account Address of the account * @param trustedIssuers Array of n issuers whose identifier mappings will be used * @param maxIdentifiers Limit the number of identifiers that will be returned - * @return Array (length <= maxIdentifiers) of identifiers + * @return [0] Array of number of identifiers returned per issuer + * @return [1] Array (length == sum([0]) <= maxIdentifiers) of identifiers * @dev Adds identifier info to the arrays in order of provided trustedIssuers * @dev Expectation that only one attestation exists per (identifier, issuer, account) */ - - // TODO ASv2 consider also returning an array with counts of identifiers per issuer - - function lookupIdentifiersByAddress( + function lookupUnrevokedIdentifiers( address account, - address[] memory trustedIssuers, + address[] calldata trustedIssuers, uint256 maxIdentifiers - ) public view returns (bytes32[] memory) { + ) external view returns (uint256[] memory, bytes32[] memory) { + uint256[] memory countsPerIssuer = new uint256[](trustedIssuers.length); // Same as for the other lookup, preallocate and then trim for now uint256 currIndex = 0; bytes32[] memory identifiers = new bytes32[](maxIdentifiers); - for (uint256 i = 0; i < trustedIssuers.length; i = i.add(1)) { - address trustedIssuer = trustedIssuers[i]; - uint256 numIssuersForAddress = addressToIdentifiers[account][trustedIssuer].length; - for (uint256 j = 0; j < numIssuersForAddress; j = j.add(1)) { - // Iterate through the list of identifiers - if (currIndex < maxIdentifiers) { - bytes32 identifier = addressToIdentifiers[account][trustedIssuer][j]; - // Check if the mapping was produced by a revoked signer - // solhint-disable-next-line max-line-length - OwnershipAttestation[] memory attestationsForIssuer = identifierToAddresses[identifier][trustedIssuer]; - for (uint256 k = 0; k < attestationsForIssuer.length; k = k.add(1)) { - OwnershipAttestation memory attestation = attestationsForIssuer[k]; - // (identifier, account, issuer) tuples should be unique - if (attestation.account == account && !revokedSigners[attestation.signer]) { - identifiers[currIndex] = identifier; - currIndex = currIndex.add(1); - break; - } + OwnershipAttestation[] memory attestationsPerIssuer; + bytes32[] memory identifiersPerIssuer; + + for (uint256 i = 0; i < trustedIssuers.length && currIndex < maxIdentifiers; i = i.add(1)) { + identifiersPerIssuer = addressToIdentifiers[account][trustedIssuers[i]]; + for ( + uint256 j = 0; + j < identifiersPerIssuer.length && currIndex < maxIdentifiers; + j = j.add(1) + ) { + bytes32 identifier = identifiersPerIssuer[j]; + // Check if the mapping was produced by a revoked signer + attestationsPerIssuer = identifierToAttestations[identifier][trustedIssuers[i]]; + for (uint256 k = 0; k < attestationsPerIssuer.length; k = k.add(1)) { + // (identifier, account, issuer) tuples are checked for uniqueness on registration + if ( + !(attestationsPerIssuer[k].account == account) || + revokedSigners[attestationsPerIssuer[k].signer] + ) { + continue; } - } else { + identifiers[currIndex] = identifier; + currIndex = currIndex.add(1); + countsPerIssuer[i] = countsPerIssuer[i].add(1); break; } } } - if (currIndex < maxIdentifiers) { - // Allocate and fill properly-sized array - bytes32[] memory trimmedIdentifiers = new bytes32[](currIndex); - for (uint256 i = 0; i < currIndex; i = i.add(1)) { - trimmedIdentifiers[i] = identifiers[i]; + if (currIndex >= maxIdentifiers) { + return (countsPerIssuer, identifiers); + } + // Allocate and fill properly-sized array + bytes32[] memory trimmedIdentifiers = new bytes32[](currIndex); + for (uint256 i = 0; i < currIndex; i = i.add(1)) { + trimmedIdentifiers[i] = identifiers[i]; + } + return (countsPerIssuer, trimmedIdentifiers); + } + + /** + * @notice Similar to lookupUnrevokedIdentifiers but returns all identifiers + * mapped to an address with attestations from a list of issuers, + * either including or excluding attestations from revoked signers + * @param account Address of the account + * @param trustedIssuers Array of n issuers whose identifier mappings will be used + * @param includeRevoked Whether to include identifiers attested by revoked signers + * @return [0] Array of number of identifiers returned per issuer + * @return [1] Array (length == sum([0])) of identifiers + * @dev Adds identifier info to the arrays in order of provided trustedIssuers + * @dev Expectation that only one attestation exists per (identifier, issuer, account) + */ + function lookupIdentifiers( + address account, + address[] calldata trustedIssuers, + bool includeRevoked + ) external view returns (uint256[] memory, bytes32[] memory) { + uint256 totalIdentifiers; + uint256[] memory countsPerIssuer; + + (totalIdentifiers, countsPerIssuer) = includeRevoked + ? getNumIdentifiers(account, trustedIssuers) + : getNumUnrevokedIdentifiers(account, trustedIssuers); + + bytes32[] memory identifiers = new bytes32[](totalIdentifiers); + bytes32[] memory identifiersPerIssuer; + + uint256 currIndex = 0; + + for (uint256 i = 0; i < trustedIssuers.length; i = i.add(1)) { + identifiersPerIssuer = addressToIdentifiers[account][trustedIssuers[i]]; + for (uint256 j = 0; j < identifiersPerIssuer.length; j = j.add(1)) { + if ( + !includeRevoked && + !foundUnrevokedAttestation(account, identifiersPerIssuer[j], trustedIssuers[i]) + ) { + continue; + } + identifiers[currIndex] = identifiersPerIssuer[j]; + currIndex = currIndex.add(1); + } + } + return (countsPerIssuer, identifiers); + } + + /** + * @notice Helper function for lookupIdentifiers to search through the + * attestations from `issuer` for one with an unrevoked signer + * that maps `account` -> `identifier + * @param account Address of the account + * @param identifier Hash of the identifier + * @param issuer Issuer whose attestations to search + * @return Whether or not an unrevoked attestation is found establishing the mapping + */ + function foundUnrevokedAttestation(address account, bytes32 identifier, address issuer) + internal + view + returns (bool) + { + OwnershipAttestation[] memory attestations = identifierToAttestations[identifier][issuer]; + for (uint256 i = 0; i < attestations.length; i = i.add(1)) { + if (attestations[i].account == account && !revokedSigners[attestations[i].signer]) { + return true; } - return trimmedIdentifiers; - } else { - return identifiers; } + return false; } // TODO do we want to restrict permissions, or should anyone @@ -320,17 +615,17 @@ contract FederatedAttestations is isValidAttestation(identifier, issuer, account, issuedOn, signer, v, r, s), "Signature is invalid" ); - for (uint256 i = 0; i < identifierToAddresses[identifier][issuer].length; i = i.add(1)) { + for (uint256 i = 0; i < identifierToAttestations[identifier][issuer].length; i = i.add(1)) { // This enforces only one attestation to be uploaded // for a given set of (identifier, issuer, account) // Editing/upgrading an attestation requires that it be deleted before a new one is registered require( - identifierToAddresses[identifier][issuer][i].account != account, + identifierToAttestations[identifier][issuer][i].account != account, "Attestation for this account already exists" ); } OwnershipAttestation memory attestation = OwnershipAttestation(account, issuedOn, signer); - identifierToAddresses[identifier][issuer].push(attestation); + identifierToAttestations[identifier][issuer].push(attestation); addressToIdentifiers[account][issuer].push(identifier); emit AttestationRegistered(identifier, issuer, account, issuedOn, signer); } @@ -346,7 +641,7 @@ contract FederatedAttestations is public isValidUser(issuer, account) { - OwnershipAttestation[] memory attestations = identifierToAddresses[identifier][issuer]; + OwnershipAttestation[] memory attestations = identifierToAttestations[identifier][issuer]; for (uint256 i = 0; i < attestations.length; i = i.add(1)) { OwnershipAttestation memory attestation = attestations[i]; if (attestation.account == account) { @@ -354,8 +649,8 @@ contract FederatedAttestations is // and then move the last element in the array to that empty spot, // to avoid having empty elements in the array // TODO reviewers: is there a better way of doing this? - identifierToAddresses[identifier][issuer][i] = attestations[attestations.length - 1]; - identifierToAddresses[identifier][issuer].pop(); + identifierToAttestations[identifier][issuer][i] = attestations[attestations.length - 1]; + identifierToAttestations[identifier][issuer].pop(); bool deletedIdentifier = false; @@ -379,7 +674,7 @@ contract FederatedAttestations is function revokeSigner(address signer) public { // TODO ASv2 add constraints on who has permissions to revoke a signer - // TODO ASv2 consider whether we want to check if the signer is an authorized signer + // TODO ASv2 consider whether to check if the signer is an authorized signer // or to allow any address to be revoked revokedSigners[signer] = true; } diff --git a/packages/protocol/test/identity/federatedattestations.ts b/packages/protocol/test/identity/federatedattestations.ts index 7201c87d5ce..478bea69178 100644 --- a/packages/protocol/test/identity/federatedattestations.ts +++ b/packages/protocol/test/identity/federatedattestations.ts @@ -153,7 +153,7 @@ contract('FederatedAttestations', (accounts: string[]) => { }) }) - describe('#lookupAttestations', () => { + describe('looking up attestations', () => { interface AttestationTestCase { account: string issuedOn: number @@ -161,33 +161,61 @@ contract('FederatedAttestations', (accounts: string[]) => { } const checkAgainstExpectedAttestations = ( + expectedCountsPerIssuer: number[], expectedAttestations: AttestationTestCase[], + actualCountsPerIssuer: BigNumber[], actualAddresses: string[], actualIssuedOns: BigNumber[], actualSigners: string[] ) => { + expect(actualCountsPerIssuer.map((count) => count.toNumber())).to.eql(expectedCountsPerIssuer) + assert.lengthOf(actualAddresses, expectedAttestations.length) assert.lengthOf(actualIssuedOns, expectedAttestations.length) assert.lengthOf(actualSigners, expectedAttestations.length) expectedAttestations.forEach((expectedAttestation, index) => { - assert.equal(expectedAttestation.account, actualAddresses[index]) - assert.equal(expectedAttestation.issuedOn, actualIssuedOns[index].toNumber()) - assert.equal(expectedAttestation.signer, actualSigners[index]) + assert.equal(actualAddresses[index], expectedAttestation.account) + assert.equal(actualIssuedOns[index].toNumber(), expectedAttestation.issuedOn) + assert.equal(actualSigners[index], expectedAttestation.signer) }) } describe('when identifier has not been registered', () => { - it('should return empty list', async () => { - const [addresses, issuedOns, signers] = await federatedAttestations.lookupAttestations( - identifier1, - [issuer1], - 1 - ) - checkAgainstExpectedAttestations([], addresses, issuedOns, signers) + describe('#lookupUnrevokedAttestations', () => { + it('should return empty list', async () => { + const [ + countsPerIssuer, + addresses, + issuedOns, + signers, + ] = await federatedAttestations.lookupUnrevokedAttestations(identifier1, [issuer1], 1) + checkAgainstExpectedAttestations([0], [], countsPerIssuer, addresses, issuedOns, signers) + }) + }) + describe('#lookupAttestations', () => { + ;[true, false].forEach((includeRevoked) => { + describe(`includeRevoked = ${includeRevoked}`, () => { + it('should return empty list', async () => { + const [ + countsPerIssuer, + addresses, + issuedOns, + signers, + ] = await federatedAttestations.lookupAttestations(identifier1, [issuer1], true) + checkAgainstExpectedAttestations( + [0], + [], + countsPerIssuer, + addresses, + issuedOns, + signers + ) + }) + }) + }) }) }) - describe('when identifier has been registered', () => { const account2 = accounts[3] @@ -243,94 +271,325 @@ contract('FederatedAttestations', (accounts: string[]) => { } }) - it('should return all attestations from one issuer', async () => { - const [addresses, issuedOns, signers] = await federatedAttestations.lookupAttestations( - identifier1, - [issuer1], - // Do not allow for maxAttestations to coincidentally limit incorrect output - issuer1Attestations.length + 1 - ) - checkAgainstExpectedAttestations(issuer1Attestations, addresses, issuedOns, signers) - }) + describe('#lookupUnrevokedAttestations', () => { + it('should return empty count if no issuers specified', async () => { + const [ + countsPerIssuer, + addresses, + issuedOns, + signers, + ] = await federatedAttestations.lookupUnrevokedAttestations(identifier1, [], 1) + checkAgainstExpectedAttestations([], [], countsPerIssuer, addresses, issuedOns, signers) + }) - it('should return empty list if no attestations exist for an issuer', async () => { - const [addresses, issuedOns, signers] = await federatedAttestations.lookupAttestations( - identifier1, - [issuer3], - 1 - ) - checkAgainstExpectedAttestations([], addresses, issuedOns, signers) - }) + it('should return all attestations from one issuer', async () => { + const [ + countsPerIssuer, + addresses, + issuedOns, + signers, + ] = await federatedAttestations.lookupUnrevokedAttestations( + identifier1, + [issuer1], + // Do not allow for maxAttestations to coincidentally limit incorrect output + issuer1Attestations.length + 1 + ) + checkAgainstExpectedAttestations( + [issuer1Attestations.length], + issuer1Attestations, + countsPerIssuer, + addresses, + issuedOns, + signers + ) + }) - it('should return attestations from multiple issuers in correct order', async () => { - const expectedAttestations = issuer2Attestations.concat(issuer1Attestations) - const [addresses, issuedOns, signers] = await federatedAttestations.lookupAttestations( - identifier1, - [issuer3, issuer2, issuer1], - expectedAttestations.length + 1 - ) - checkAgainstExpectedAttestations(expectedAttestations, addresses, issuedOns, signers) - }) + it('should return empty list if no attestations exist for an issuer', async () => { + const [ + countsPerIssuer, + addresses, + issuedOns, + signers, + ] = await federatedAttestations.lookupUnrevokedAttestations(identifier1, [issuer3], 1) + checkAgainstExpectedAttestations([0], [], countsPerIssuer, addresses, issuedOns, signers) + }) - it('should return empty list if maxAttestations == 0', async () => { - const [addresses, issuedOns, signers] = await federatedAttestations.lookupAttestations( - identifier1, - [issuer1], - 0 - ) - checkAgainstExpectedAttestations([], addresses, issuedOns, signers) - }) + it('should return attestations from multiple issuers in correct order', async () => { + const expectedAttestations = issuer2Attestations.concat(issuer1Attestations) + const expectedCountsPerIssuer = [ + 0, + issuer2Attestations.length, + issuer1Attestations.length, + ] + const [ + countsPerIssuer, + addresses, + issuedOns, + signers, + ] = await federatedAttestations.lookupUnrevokedAttestations( + identifier1, + [issuer3, issuer2, issuer1], + expectedAttestations.length + 1 + ) + checkAgainstExpectedAttestations( + expectedCountsPerIssuer, + expectedAttestations, + countsPerIssuer, + addresses, + issuedOns, + signers + ) + }) - it('should only return maxAttestations attestations when more are present', async () => { - const expectedAttestations = issuer1Attestations.slice(0, -1) - const [addresses, issuedOns, signers] = await federatedAttestations.lookupAttestations( - identifier1, - [issuer1], - expectedAttestations.length - ) - checkAgainstExpectedAttestations(expectedAttestations, addresses, issuedOns, signers) - }) + it('should return empty list if maxAttestations == 0', async () => { + const [ + countsPerIssuer, + addresses, + issuedOns, + signers, + ] = await federatedAttestations.lookupUnrevokedAttestations(identifier1, [issuer1], 0) + checkAgainstExpectedAttestations([0], [], countsPerIssuer, addresses, issuedOns, signers) + }) - it('should not return attestations from revoked signers', async () => { - const attestationToRevoke = issuer2Attestations[0] - await federatedAttestations.revokeSigner(attestationToRevoke.signer) - const expectedAttestations = issuer2Attestations.slice(1) + it('should only return maxAttestations attestations when more are present', async () => { + const expectedAttestations = issuer1Attestations.slice(0, -1) + const expectedCountsPerIssuer = [expectedAttestations.length] + const [ + countsPerIssuer, + addresses, + issuedOns, + signers, + ] = await federatedAttestations.lookupUnrevokedAttestations( + identifier1, + [issuer1], + expectedAttestations.length + ) + checkAgainstExpectedAttestations( + expectedCountsPerIssuer, + expectedAttestations, + countsPerIssuer, + addresses, + issuedOns, + signers + ) + }) - const [addresses, issuedOns, signers] = await federatedAttestations.lookupAttestations( - identifier1, - [issuer2], - issuer2Attestations.length - ) - checkAgainstExpectedAttestations(expectedAttestations, addresses, issuedOns, signers) + it('should not return attestations from revoked signers', async () => { + const attestationToRevoke = issuer2Attestations[0] + await federatedAttestations.revokeSigner(attestationToRevoke.signer) + const expectedAttestations = issuer2Attestations.slice(1) + const expectedCountsPerIssuer = [expectedAttestations.length] + const [ + countsPerIssuer, + addresses, + issuedOns, + signers, + ] = await federatedAttestations.lookupUnrevokedAttestations( + identifier1, + [issuer2], + issuer2Attestations.length + ) + checkAgainstExpectedAttestations( + expectedCountsPerIssuer, + expectedAttestations, + countsPerIssuer, + addresses, + issuedOns, + signers + ) + }) + }) + + describe('#lookupAttestations', () => { + ;[true, false].forEach((includeRevoked) => { + describe(`includeRevoked = ${includeRevoked}`, () => { + it('should return empty count and list if no issuers specified', async () => { + const [ + countsPerIssuer, + addresses, + issuedOns, + signers, + ] = await federatedAttestations.lookupAttestations(identifier1, [], includeRevoked) + checkAgainstExpectedAttestations( + [], + [], + countsPerIssuer, + addresses, + issuedOns, + signers + ) + }) + + it('should return all attestations from one issuer', async () => { + const [ + countsPerIssuer, + addresses, + issuedOns, + signers, + ] = await federatedAttestations.lookupAttestations( + identifier1, + [issuer1], + includeRevoked + ) + checkAgainstExpectedAttestations( + [issuer1Attestations.length], + issuer1Attestations, + countsPerIssuer, + addresses, + issuedOns, + signers + ) + }) + + it('should return empty list if no attestations exist for an issuer', async () => { + const [ + countsPerIssuer, + addresses, + issuedOns, + signers, + ] = await federatedAttestations.lookupAttestations( + identifier1, + [issuer3], + includeRevoked + ) + checkAgainstExpectedAttestations( + [0], + [], + countsPerIssuer, + addresses, + issuedOns, + signers + ) + }) + + it('should return attestations from multiple issuers in correct order', async () => { + const expectedAttestations = issuer2Attestations.concat(issuer1Attestations) + const expectedCountsPerIssuer = [ + 0, + issuer2Attestations.length, + issuer1Attestations.length, + ] + const [ + countsPerIssuer, + addresses, + issuedOns, + signers, + ] = await federatedAttestations.lookupAttestations( + identifier1, + [issuer3, issuer2, issuer1], + includeRevoked + ) + checkAgainstExpectedAttestations( + expectedCountsPerIssuer, + expectedAttestations, + countsPerIssuer, + addresses, + issuedOns, + signers + ) + }) + + if (includeRevoked) { + it('should return attestations from revoked and unrevoked signers', async () => { + await federatedAttestations.revokeSigner(issuer1Attestations[0].signer) + const expectedAttestations = issuer1Attestations.concat(issuer2Attestations) + const expectedCountsPerIssuer = [ + issuer1Attestations.length, + issuer2Attestations.length, + ] + const [ + countsPerIssuer, + addresses, + issuedOns, + signers, + ] = await federatedAttestations.lookupAttestations( + identifier1, + [issuer1, issuer2], + includeRevoked + ) + checkAgainstExpectedAttestations( + expectedCountsPerIssuer, + expectedAttestations, + countsPerIssuer, + addresses, + issuedOns, + signers + ) + }) + } else { + it('should not return attestations from revoked signers', async () => { + const attestationToRevoke = issuer2Attestations[0] + await federatedAttestations.revokeSigner(attestationToRevoke.signer) + const expectedAttestations = issuer2Attestations.slice(1) + const expectedCountsPerIssuer = [expectedAttestations.length] + const [ + countsPerIssuer, + addresses, + issuedOns, + signers, + ] = await federatedAttestations.lookupAttestations( + identifier1, + [issuer2], + includeRevoked + ) + checkAgainstExpectedAttestations( + expectedCountsPerIssuer, + expectedAttestations, + countsPerIssuer, + addresses, + issuedOns, + signers + ) + }) + } + }) + }) }) }) }) - describe('#lookupIdentifiersByAddress', () => { + describe('looking up identifiers', () => { + interface IdentifierTestCase { + identifier: string + signer: string + } + + const checkAgainstExpectedIdCases = ( + expectedCountsPerIssuer: number[], + expectedIdentifiers: IdentifierTestCase[], + actualCountsPerIssuer: BigNumber[], + actualIdentifiers: string[] + ) => { + expect(actualCountsPerIssuer.map((count) => count.toNumber())).to.eql(expectedCountsPerIssuer) + expect(actualIdentifiers).to.eql(expectedIdentifiers.map((idCase) => idCase.identifier)) + } + describe('when address has not been registered', () => { - it('should return empty list', async () => { - const actualIdentifiers = await federatedAttestations.lookupIdentifiersByAddress( - account1, - [issuer1], - 1 - ) - assert.equal(actualIdentifiers.length, 0) + describe('#lookupUnrevokedIdentifiers', () => { + it('should return empty list', async () => { + const [ + actualCountsPerIssuer, + actualIdentifiers, + ] = await federatedAttestations.lookupUnrevokedIdentifiers(account1, [issuer1], 1) + checkAgainstExpectedIdCases([0], [], actualCountsPerIssuer, actualIdentifiers) + }) + }) + describe('#lookupIdentifiers', () => { + ;[true, false].forEach((includeRevoked) => { + describe(`includeRevoked = ${includeRevoked}`, () => { + it('should return empty list', async () => { + const [ + actualCountsPerIssuer, + actualIdentifiers, + ] = await federatedAttestations.lookupIdentifiers(account1, [issuer1], true) + checkAgainstExpectedIdCases([0], [], actualCountsPerIssuer, actualIdentifiers) + }) + }) + }) }) }) describe('when address has been registered', () => { - interface IdentifierTestCase { - identifier: string - signer: string - } - - const checkAgainstExpectedIdCases = ( - expectedIdentifiers: IdentifierTestCase[], - actualIdentifiers: string[] - ) => { - expect(expectedIdentifiers.map((idCase) => idCase.identifier)).to.eql(actualIdentifiers) - } - const issuer2 = accounts[3] const issuer2Signer = accounts[4] const issuer2Signer2 = accounts[5] @@ -376,62 +635,203 @@ contract('FederatedAttestations', (accounts: string[]) => { } }) - it('should return all identifiers from one issuer', async () => { - const actualIdentifiers = await federatedAttestations.lookupIdentifiersByAddress( - account1, - [issuer1], - issuer1IdCases.length + 1 - ) - checkAgainstExpectedIdCases(issuer1IdCases, actualIdentifiers) - }) + describe('#lookupUnrevokedIdentifiers', () => { + it('should return empty count if no issuers specified', async () => { + const [ + actualCountsPerIssuer, + actualIdentifiers, + ] = await federatedAttestations.lookupUnrevokedIdentifiers(account1, [], 1) + checkAgainstExpectedIdCases([], [], actualCountsPerIssuer, actualIdentifiers) + }) - it('should return empty list if no identifiers exist for an (issuer,address)', async () => { - const actualIdentifiers = await federatedAttestations.lookupIdentifiersByAddress( - account1, - [issuer3], - 1 - ) - assert.equal(actualIdentifiers.length, 0) - }) + it('should return all identifiers from one issuer', async () => { + const [ + actualCountsPerIssuer, + actualIdentifiers, + ] = await federatedAttestations.lookupUnrevokedIdentifiers( + account1, + [issuer1], + issuer1IdCases.length + 1 + ) + checkAgainstExpectedIdCases( + [issuer1IdCases.length], + issuer1IdCases, + actualCountsPerIssuer, + actualIdentifiers + ) + }) - it('should return identifiers from multiple issuers in correct order', async () => { - const expectedIdCases = issuer2IdCases.concat(issuer1IdCases) - const actualIdentifiers = await federatedAttestations.lookupIdentifiersByAddress( - account1, - [issuer3, issuer2, issuer1], - expectedIdCases.length + 1 - ) - checkAgainstExpectedIdCases(expectedIdCases, actualIdentifiers) - }) + it('should return empty list if no identifiers exist for an (issuer,address)', async () => { + const [ + actualCountsPerIssuer, + actualIdentifiers, + ] = await federatedAttestations.lookupUnrevokedIdentifiers(account1, [issuer3], 1) + checkAgainstExpectedIdCases([0], [], actualCountsPerIssuer, actualIdentifiers) + }) - it('should return empty list if maxIdentifiers == 0', async () => { - const actualIdentifiers = await federatedAttestations.lookupIdentifiersByAddress( - account1, - [issuer1], - 0 - ) - assert.equal(actualIdentifiers.length, 0) - }) + it('should return identifiers from multiple issuers in correct order', async () => { + const expectedIdCases = issuer2IdCases.concat(issuer1IdCases) + const expectedCountsPerIssuer = [0, issuer2IdCases.length, issuer1IdCases.length] + const [ + actualCountsPerIssuer, + actualIdentifiers, + ] = await federatedAttestations.lookupUnrevokedIdentifiers( + account1, + [issuer3, issuer2, issuer1], + expectedIdCases.length + 1 + ) + checkAgainstExpectedIdCases( + expectedCountsPerIssuer, + expectedIdCases, + actualCountsPerIssuer, + actualIdentifiers + ) + }) - it('should only return maxIdentifiers identifiers when more are present', async () => { - const expectedIdCases = issuer2IdCases.concat(issuer1IdCases).slice(0, -1) - const actualIdentifiers = await federatedAttestations.lookupIdentifiersByAddress( - account1, - [issuer2, issuer1], - expectedIdCases.length - ) - checkAgainstExpectedIdCases(expectedIdCases, actualIdentifiers) + it('should return empty list if maxIdentifiers == 0', async () => { + const [ + actualCountsPerIssuer, + actualIdentifiers, + ] = await federatedAttestations.lookupUnrevokedIdentifiers(account1, [issuer1], 0) + checkAgainstExpectedIdCases([0], [], actualCountsPerIssuer, actualIdentifiers) + }) + + it('should only return maxIdentifiers identifiers when more are present', async () => { + const expectedIdCases = issuer2IdCases.concat(issuer1IdCases).slice(0, -1) + const expectedCountsPerIssuer = [ + issuer2IdCases.length, + expectedIdCases.length - issuer2IdCases.length, + ] + const [ + actualCountsPerIssuer, + actualIdentifiers, + ] = await federatedAttestations.lookupUnrevokedIdentifiers( + account1, + [issuer2, issuer1], + expectedIdCases.length + ) + checkAgainstExpectedIdCases( + expectedCountsPerIssuer, + expectedIdCases, + actualCountsPerIssuer, + actualIdentifiers + ) + }) + + it('should not return identifiers from revoked signers', async () => { + await federatedAttestations.revokeSigner(issuer2IdCases[0].signer) + const expectedIdCases = issuer2IdCases.slice(1) + const expectedCountsPerIssuer = [expectedIdCases.length] + const [ + actualCountsPerIssuer, + actualIdentifiers, + ] = await federatedAttestations.lookupUnrevokedIdentifiers( + account1, + [issuer2], + expectedIdCases.length + 1 + ) + checkAgainstExpectedIdCases( + expectedCountsPerIssuer, + expectedIdCases, + actualCountsPerIssuer, + actualIdentifiers + ) + }) }) - it('should not return identifiers from revoked signers', async () => { - await federatedAttestations.revokeSigner(issuer2IdCases[0].signer) - const expectedIdCases = issuer2IdCases.slice(1) - const actualIdentifiers = await federatedAttestations.lookupIdentifiersByAddress( - account1, - [issuer2], - expectedIdCases.length + 1 - ) - checkAgainstExpectedIdCases(expectedIdCases, actualIdentifiers) + describe('#lookupIdentifiers', () => { + ;[true, false].forEach((includeRevoked) => { + describe(`includeRevoked = ${includeRevoked}`, () => { + it('should return empty count if no issuers specified', async () => { + const [ + actualCountsPerIssuer, + actualIdentifiers, + ] = await federatedAttestations.lookupIdentifiers(account1, [], includeRevoked) + checkAgainstExpectedIdCases([], [], actualCountsPerIssuer, actualIdentifiers) + }) + + it('should return all identifiers from one issuer', async () => { + const [ + actualCountsPerIssuer, + actualIdentifiers, + ] = await federatedAttestations.lookupIdentifiers(account1, [issuer1], includeRevoked) + checkAgainstExpectedIdCases( + [issuer1IdCases.length], + issuer1IdCases, + actualCountsPerIssuer, + actualIdentifiers + ) + }) + + it('should return empty list if no identifiers exist for an (issuer,address)', async () => { + const [ + actualCountsPerIssuer, + actualIdentifiers, + ] = await federatedAttestations.lookupIdentifiers(account1, [issuer3], includeRevoked) + checkAgainstExpectedIdCases([0], [], actualCountsPerIssuer, actualIdentifiers) + }) + + it('should return identifiers from multiple issuers in correct order', async () => { + const expectedIdCases = issuer2IdCases.concat(issuer1IdCases) + const expectedCountsPerIssuer = [0, issuer2IdCases.length, issuer1IdCases.length] + const [ + actualCountsPerIssuer, + actualIdentifiers, + ] = await federatedAttestations.lookupIdentifiers( + account1, + [issuer3, issuer2, issuer1], + includeRevoked + ) + checkAgainstExpectedIdCases( + expectedCountsPerIssuer, + expectedIdCases, + actualCountsPerIssuer, + actualIdentifiers + ) + }) + + if (includeRevoked) { + it('should return identifiers from revoked and unrevoked signers', async () => { + await federatedAttestations.revokeSigner(issuer2IdCases[0].signer) + const [ + actualCountsPerIssuer, + actualIdentifiers, + ] = await federatedAttestations.lookupIdentifiers( + account1, + [issuer2], + includeRevoked + ) + checkAgainstExpectedIdCases( + [issuer2IdCases.length], + issuer2IdCases, + actualCountsPerIssuer, + actualIdentifiers + ) + }) + } else { + it('should not return identifiers from revoked signers', async () => { + await federatedAttestations.revokeSigner(issuer2IdCases[0].signer) + const expectedIdCases = issuer2IdCases.slice(1) + const expectedCountsPerIssuer = [expectedIdCases.length] + + const [ + actualCountsPerIssuer, + actualIdentifiers, + ] = await federatedAttestations.lookupIdentifiers( + account1, + [issuer2], + includeRevoked + ) + checkAgainstExpectedIdCases( + expectedCountsPerIssuer, + expectedIdCases, + actualCountsPerIssuer, + actualIdentifiers + ) + }) + } + }) + }) }) }) }) From 1d744ec7a9372c3c036a9b16e1b43e7d87639ea8 Mon Sep 17 00:00:00 2001 From: isabellewei Date: Tue, 24 May 2022 07:18:34 +0200 Subject: [PATCH 07/30] Check variable storage states in register and delete tests (#9576) * check storage state in tests * fix lint errors * PR comments * revert when deleting nonexistant attestation * fix lint --- .../identity/FederatedAttestations.sol | 3 +- .../test/identity/federatedattestations.ts | 275 ++++++------------ 2 files changed, 90 insertions(+), 188 deletions(-) diff --git a/packages/protocol/contracts/identity/FederatedAttestations.sol b/packages/protocol/contracts/identity/FederatedAttestations.sol index c9571ddedbf..1d31c8bec68 100644 --- a/packages/protocol/contracts/identity/FederatedAttestations.sol +++ b/packages/protocol/contracts/identity/FederatedAttestations.sol @@ -667,9 +667,10 @@ contract FederatedAttestations is assert(deletedIdentifier); emit AttestationDeleted(identifier, issuer, account); - break; + return; } } + revert("Attestion to be deleted does not exist"); } function revokeSigner(address signer) public { diff --git a/packages/protocol/test/identity/federatedattestations.ts b/packages/protocol/test/identity/federatedattestations.ts index 478bea69178..cbf767e2ace 100644 --- a/packages/protocol/test/identity/federatedattestations.ts +++ b/packages/protocol/test/identity/federatedattestations.ts @@ -8,6 +8,7 @@ import { assertLogMatches2, assertRevert, assertRevertWithReason, + assertThrowsAsync, } from '@celo/protocol/lib/test-utils' import BigNumber from 'bignumber.js' import { @@ -86,6 +87,46 @@ contract('FederatedAttestations', (accounts: string[]) => { ) } + const assertAttestationInStorage = async ( + identifier: string, + issuer: string, + attestationIndex: number, + account: string, + issuedOn: number, + signer: string, + identifierIndex: number + ) => { + const attestation = await federatedAttestations.identifierToAddresses( + identifier, + issuer, + attestationIndex + ) + assert.equal(attestation['account'], account) + assert.equal(attestation['issuedOn'], issuedOn) + assert.equal(attestation['signer'], signer) + const storedIdentifier = await federatedAttestations.addressToIdentifiers( + account1, + issuer1, + identifierIndex + ) + assert.equal(identifier, storedIdentifier) + } + + const assertAttestationNotInStorage = async ( + identifier: string, + issuer: string, + account: string, + addressIndex: number, + identifierIndex: number + ) => { + await assertThrowsAsync( + federatedAttestations.identifierToAddresses(identifier, issuer, addressIndex) + ) + await assertThrowsAsync( + federatedAttestations.addressToIdentifiers(account, issuer, identifierIndex) + ) + } + beforeEach('FederatedAttestations setup', async () => { accountsInstance = await Accounts.new(true) federatedAttestations = await FederatedAttestations.new(true) @@ -985,35 +1026,8 @@ contract('FederatedAttestations', (accounts: string[]) => { }) it('should succeed if issuer == signer', async () => { - const issuerSig = await getSignatureForAttestation( - identifier1, - issuer1, - account1, - nowUnixTime, - issuer1, - chainId, - federatedAttestations.address - ) - const register = await federatedAttestations.registerAttestation( - identifier1, - issuer1, - account1, - nowUnixTime, - issuer1, - issuerSig.v, - issuerSig.r, - issuerSig.s - ) - assertLogMatches2(register.logs[0], { - event: 'AttestationRegistered', - args: { - identifier: identifier1, - issuer: issuer1, - account: account1, - issuedOn: nowUnixTime, - signer: issuer1, - }, - }) + await signAndRegisterAttestation(identifier1, issuer1, account1, nowUnixTime, issuer1) + await assertAttestationInStorage(identifier1, issuer1, 0, account1, nowUnixTime, issuer1, 0) }) it('should revert if an invalid signature is provided', async () => { @@ -1057,6 +1071,12 @@ contract('FederatedAttestations', (accounts: string[]) => { ) }) + it('should modify identifierToAddresses and addresstoIdentifiers accordingly', async () => { + await assertAttestationNotInStorage(identifier1, issuer1, account1, 0, 0) + await signAndRegisterAttestation(identifier1, issuer1, account1, nowUnixTime, signer1) + await assertAttestationInStorage(identifier1, issuer1, 0, account1, nowUnixTime, signer1, 0) + }) + describe('when registering a second attestation', () => { beforeEach(async () => { // register first attestation @@ -1072,6 +1092,15 @@ contract('FederatedAttestations', (accounts: string[]) => { ) }) + it('should modify identifierToAddresses and addresstoIdentifiers accordingly', async () => { + const account2 = accounts[3] + await assertAttestationInStorage(identifier1, issuer1, 0, account1, nowUnixTime, signer1, 0) + await assertAttestationNotInStorage(identifier1, issuer1, account2, 1, 0) + + await signAndRegisterAttestation(identifier1, issuer1, account2, nowUnixTime, signer1) + await assertAttestationInStorage(identifier1, issuer1, 1, account2, nowUnixTime, signer1, 0) + }) + it('should revert if an attestation with the same (issuer, identifier, account) is uploaded again', async () => { // Upload the same attestation signed by a different signer, authorized under the same issuer const signer2 = accounts[4] @@ -1101,107 +1130,22 @@ contract('FederatedAttestations', (accounts: string[]) => { }) it('should succeed with a different identifier', async () => { - const sig2 = await getSignatureForAttestation( - identifier2, - issuer1, - account1, - nowUnixTime, - signer1, - chainId, - federatedAttestations.address - ) - const register2 = await federatedAttestations.registerAttestation( - identifier2, - issuer1, - account1, - nowUnixTime, - signer1, - sig2.v, - sig2.r, - sig2.s - ) - assertLogMatches2(register2.logs[0], { - event: 'AttestationRegistered', - args: { - identifier: identifier2, - issuer: issuer1, - account: account1, - issuedOn: nowUnixTime, - signer: signer1, - }, - }) + await signAndRegisterAttestation(identifier2, issuer1, account1, nowUnixTime, signer1) + await assertAttestationInStorage(identifier2, issuer1, 0, account1, nowUnixTime, signer1, 1) }) it('should succeed with a different issuer', async () => { const issuer2 = accounts[4] const signer2 = accounts[5] await accountsInstance.createAccount({ from: issuer2 }) - await accountsInstance.authorizeSigner(signer2, signerRole, { from: issuer2 }) - await accountsInstance.completeSignerAuthorization(issuer2, signerRole, { from: signer2 }) - const sig2 = await getSignatureForAttestation( - identifier1, - issuer2, - account1, - nowUnixTime, - signer2, - chainId, - federatedAttestations.address - ) - const register2 = await federatedAttestations.registerAttestation( - identifier1, - issuer2, - account1, - nowUnixTime, - signer2, - sig2.v, - sig2.r, - sig2.s, - { from: issuer2 } - ) - assertLogMatches2(register2.logs[0], { - event: 'AttestationRegistered', - args: { - identifier: identifier1, - issuer: issuer2, - account: account1, - issuedOn: nowUnixTime, - signer: signer2, - }, - }) + await signAndRegisterAttestation(identifier1, issuer2, account1, nowUnixTime, signer2) + await assertAttestationInStorage(identifier1, issuer2, 0, account1, nowUnixTime, signer2, 0) }) it('should succeed with a different account', async () => { const account2 = accounts[4] - const sig2 = await getSignatureForAttestation( - identifier1, - issuer1, - account2, - nowUnixTime, - signer1, - chainId, - federatedAttestations.address - ) - const register2 = await federatedAttestations.registerAttestation( - identifier1, - issuer1, - account2, - nowUnixTime, - signer1, - sig2.v, - sig2.r, - sig2.s, - { from: issuer1 } - ) - assertLogMatches2(register2.logs[0], { - event: 'AttestationRegistered', - args: { - identifier: identifier1, - issuer: issuer1, - account: account2, - issuedOn: nowUnixTime, - signer: signer1, - }, - }) + await signAndRegisterAttestation(identifier1, issuer1, account2, nowUnixTime, signer1) + await assertAttestationInStorage(identifier1, issuer1, 1, account2, nowUnixTime, signer1, 0) }) }) @@ -1225,7 +1169,7 @@ contract('FederatedAttestations', (accounts: string[]) => { const signer2 = accounts[4] await accountsInstance.authorizeSigner(signer2, signerRole, { from: issuer1 }) await accountsInstance.completeSignerAuthorization(issuer1, signerRole, { from: signer2 }) - const register = await federatedAttestations.registerAttestation( + await federatedAttestations.registerAttestation( identifier1, issuer1, account1, @@ -1236,22 +1180,11 @@ contract('FederatedAttestations', (accounts: string[]) => { sig.s, { from: signer2 } ) - assertLogMatches2(register.logs[0], { - event: 'AttestationRegistered', - args: { - identifier: identifier1, - issuer: issuer1, - account: account1, - issuedOn: nowUnixTime, - signer: signer1, - }, - }) + await assertAttestationInStorage(identifier1, issuer1, 0, account1, nowUnixTime, signer1, 0) }) }) describe('#deleteAttestation', () => { - // TODO ASv2 check that the actual entries were deleted in both mappings - // (for identifiers and attestations) beforeEach(async () => { await accountsInstance.authorizeSigner(signer1, signerRole, { from: issuer1 }) await accountsInstance.completeSignerAuthorization(issuer1, signerRole, { from: signer1 }) @@ -1283,45 +1216,27 @@ contract('FederatedAttestations', (accounts: string[]) => { }) }) + it("should revert when deleting an attestation that doesn't exist", async () => { + await assertRevert(federatedAttestations.deleteAttestation(identifier1, issuer1, accounts[4])) + }) + it('should succeed when >1 attestations are registered for (identifier, issuer)', async () => { const account2 = accounts[3] await signAndRegisterAttestation(identifier1, issuer1, account2, nowUnixTime, signer1) - const deleteAttestation = await federatedAttestations.deleteAttestation( - identifier1, - issuer1, - account2, - { - from: account2, - } - ) - assertLogMatches2(deleteAttestation.logs[0], { - event: 'AttestationDeleted', - args: { - identifier: identifier1, - issuer: issuer1, - account: account2, - }, + await federatedAttestations.deleteAttestation(identifier1, issuer1, account2, { + from: account2, }) + await assertAttestationNotInStorage(identifier1, issuer1, account2, 1, 0) + await assertAttestationInStorage(identifier1, issuer1, 0, account1, nowUnixTime, signer1, 0) }) it('should succeed when >1 identifiers are registered for (account, issuer)', async () => { await signAndRegisterAttestation(identifier2, issuer1, account1, nowUnixTime, signer1) - const deleteAttestation = await federatedAttestations.deleteAttestation( - identifier2, - issuer1, - account1, - { - from: account1, - } - ) - assertLogMatches2(deleteAttestation.logs[0], { - event: 'AttestationDeleted', - args: { - identifier: identifier2, - issuer: issuer1, - account: account1, - }, + await federatedAttestations.deleteAttestation(identifier2, issuer1, account1, { + from: account1, }) + await assertAttestationNotInStorage(identifier2, issuer1, account1, 0, 1) + await assertAttestationInStorage(identifier1, issuer1, 0, account1, nowUnixTime, signer1, 0) }) it('should revert if an invalid user attempts to delete the attestation', async () => { @@ -1341,19 +1256,8 @@ contract('FederatedAttestations', (accounts: string[]) => { it('should successfully delete an attestation with a revoked signer', async () => { await federatedAttestations.revokeSigner(signer1) - const deleteAttestation = await federatedAttestations.deleteAttestation( - identifier1, - issuer1, - account1 - ) - assertLogMatches2(deleteAttestation.logs[0], { - event: 'AttestationDeleted', - args: { - identifier: identifier1, - issuer: issuer1, - account: account1, - }, - }) + await federatedAttestations.deleteAttestation(identifier1, issuer1, account1) + await assertAttestationNotInStorage(identifier1, issuer1, account1, 0, 1) }) it('should fail registering same attestation but succeed after deleting it', async () => { @@ -1370,7 +1274,7 @@ contract('FederatedAttestations', (accounts: string[]) => { ) ) await federatedAttestations.deleteAttestation(identifier1, issuer1, account1) - const register = await federatedAttestations.registerAttestation( + await federatedAttestations.registerAttestation( identifier1, issuer1, account1, @@ -1380,16 +1284,13 @@ contract('FederatedAttestations', (accounts: string[]) => { sig.r, sig.s ) - assertLogMatches2(register.logs[0], { - event: 'AttestationRegistered', - args: { - identifier: identifier1, - issuer: issuer1, - account: account1, - issuedOn: nowUnixTime, - signer: signer1, - }, - }) + await assertAttestationInStorage(identifier1, issuer1, 0, account1, nowUnixTime, signer1, 0) + }) + + it('should modify identifierToAddresses and addresstoIdentifiers accordingly', async () => { + await assertAttestationInStorage(identifier1, issuer1, 0, account1, nowUnixTime, signer1, 0) + await federatedAttestations.deleteAttestation(identifier1, issuer1, account1) + await assertAttestationNotInStorage(identifier1, issuer1, account1, 0, 0) }) }) }) From 8c17086187883a5b3554677688866e56d305c4b5 Mon Sep 17 00:00:00 2001 From: isabellewei Date: Wed, 8 Jun 2022 11:00:42 -0400 Subject: [PATCH 08/30] [ASv2] Individual attestation revocation (#9603) * individual revocation * add back lookup functions * PR comments * tests passing * more tests * PR comments * more PR feedback * fix tests * lint fix --- .../identity/FederatedAttestations.sol | 441 +++--------- .../protocol/lib/fed-attestations-utils.ts | 3 +- .../test/identity/federatedattestations.ts | 631 ++++-------------- .../sdk/utils/src/sign-typed-data-utils.ts | 2 + 4 files changed, 226 insertions(+), 851 deletions(-) diff --git a/packages/protocol/contracts/identity/FederatedAttestations.sol b/packages/protocol/contracts/identity/FederatedAttestations.sol index 1d31c8bec68..ed41d6d3c99 100644 --- a/packages/protocol/contracts/identity/FederatedAttestations.sol +++ b/packages/protocol/contracts/identity/FederatedAttestations.sol @@ -28,8 +28,10 @@ contract FederatedAttestations is struct OwnershipAttestation { address account; - uint256 issuedOn; address signer; + uint64 issuedOn; + uint64 publishedOn; + // using uint64 to allow for extra space to add parameters } // TODO ASv2 revisit linting issues & all solhint-disable-next-line max-line-length @@ -38,11 +40,11 @@ contract FederatedAttestations is mapping(bytes32 => mapping(address => OwnershipAttestation[])) public identifierToAttestations; // account -> issuer -> identifiers mapping(address => mapping(address => bytes32[])) public addressToIdentifiers; - // signer => isRevoked - mapping(address => bool) public revokedSigners; + // unique attestation hash -> isRevoked + mapping(bytes32 => bool) public revokedAttestations; bytes32 public constant EIP712_VALIDATE_ATTESTATION_TYPEHASH = keccak256( - "OwnershipAttestation(bytes32 identifier,address issuer,address account,uint256 issuedOn)" + "OwnershipAttestation(bytes32 identifier,address issuer,address account,uint64 issuedOn)" ); bytes32 public eip712DomainSeparator; @@ -51,13 +53,17 @@ contract FederatedAttestations is bytes32 indexed identifier, address indexed issuer, address indexed account, - uint256 issuedOn, - address signer + address signer, + uint64 issuedOn, + uint64 publishedOn ); - event AttestationDeleted( + event AttestationRevoked( bytes32 indexed identifier, address indexed issuer, - address indexed account + address indexed account, + address signer, + uint64 issuedOn, + uint64 publishedOn ); /** @@ -107,37 +113,6 @@ contract FederatedAttestations is return (1, 1, 0, 0); } - /** - * @notice Helper function for _lookupAttestations to calculate the - total number of attestations completed for an identifier - by each trusted issuer, from unrevoked signers only - * @param identifier Hash of the identifier - * @param trustedIssuers Array of n issuers whose attestations will be included - * @return [0] Sum total of attestations found - * [1] Array of number of attestations found per issuer - */ - function getNumUnrevokedAttestations(bytes32 identifier, address[] memory trustedIssuers) - internal - view - returns (uint256, uint256[] memory) - { - uint256 totalAttestations = 0; - uint256[] memory countsPerIssuer = new uint256[](trustedIssuers.length); - - for (uint256 i = 0; i < trustedIssuers.length; i = i.add(1)) { - // solhint-disable-next-line max-line-length - OwnershipAttestation[] storage attestationsPerIssuer = identifierToAttestations[identifier][trustedIssuers[i]]; - for (uint256 j = 0; j < attestationsPerIssuer.length; j = j.add(1)) { - if (revokedSigners[attestationsPerIssuer[j].signer]) { - continue; - } - totalAttestations = totalAttestations.add(1); - countsPerIssuer[i] = countsPerIssuer[i].add(1); - } - } - return (totalAttestations, countsPerIssuer); - } - /** * @notice Helper function for _lookupAttestations to calculate the total number of attestations completed for an identifier @@ -166,12 +141,11 @@ contract FederatedAttestations is /** * @notice Returns info about up to `maxAttestations` attestations for - * `identifier` produced by unrevoked signers of `trustedIssuers` + * `identifier` produced by signers of `trustedIssuers` * @param identifier Hash of the identifier * @param trustedIssuers Array of n issuers whose attestations will be included - * @param maxAttestations Limit the number of attestations that will be returned * @return [0] Array of number of attestations returned per issuer - * @return [1 - 3] for m (== sum([0])) found attestations, m <= maxAttestations: + * @return [1 - 3] for m (== sum([0])) found attestations: * [ * Array of m accounts, * Array of m issuedOns, @@ -182,115 +156,22 @@ contract FederatedAttestations is */ // TODO reviewers: is it preferable to return an array of `trustedIssuer` indices // (indicating issuer per attestation) instead of counts per attestation? - function lookupUnrevokedAttestations( - bytes32 identifier, - address[] calldata trustedIssuers, - uint256 maxAttestations - ) external view returns (uint256[] memory, address[] memory, uint256[] memory, address[] memory) { - // TODO reviewers: this is to get around a stack too deep error; - // are there better ways of dealing with this? - return _lookupUnrevokedAttestations(identifier, trustedIssuers, maxAttestations); - } - - /** - * @notice Helper function for lookupUnrevokedAttestations to get around stack too deep - * @param identifier Hash of the identifier - * @param trustedIssuers Array of n issuers whose attestations will be included - * @param maxAttestations Limit the number of attestations that will be returned - * @return [0] Array of number of attestations returned per issuer - * @return [1 - 3] for m (== sum([0])) found attestations, m <= maxAttestations: - * [ - * Array of m accounts, - * Array of m issuedOns, - * Array of m signers - * ]; index corresponds to the same attestation - * @dev Adds attestation info to the arrays in order of provided trustedIssuers - * @dev Expectation that only one attestation exists per (identifier, issuer, account) - */ - function _lookupUnrevokedAttestations( - bytes32 identifier, - address[] memory trustedIssuers, - uint256 maxAttestations - ) internal view returns (uint256[] memory, address[] memory, uint256[] memory, address[] memory) { - uint256[] memory countsPerIssuer = new uint256[](trustedIssuers.length); - - // Pre-computing length of unrevoked attestations requires many storage lookups. - // Allow users to call that first and pass this in as maxAttestations. - // Same index corresponds to same attestation - address[] memory accounts = new address[](maxAttestations); - uint256[] memory issuedOns = new uint256[](maxAttestations); - address[] memory signers = new address[](maxAttestations); - - uint256 currIndex = 0; - OwnershipAttestation[] memory attestationsPerIssuer; - - for (uint256 i = 0; i < trustedIssuers.length && currIndex < maxAttestations; i = i.add(1)) { - attestationsPerIssuer = identifierToAttestations[identifier][trustedIssuers[i]]; - for ( - uint256 j = 0; - j < attestationsPerIssuer.length && currIndex < maxAttestations; - j = j.add(1) - ) { - if (revokedSigners[attestationsPerIssuer[j].signer]) { - continue; - } - accounts[currIndex] = attestationsPerIssuer[j].account; - issuedOns[currIndex] = attestationsPerIssuer[j].issuedOn; - signers[currIndex] = attestationsPerIssuer[j].signer; - currIndex = currIndex.add(1); - countsPerIssuer[i] = countsPerIssuer[i].add(1); - } - } - - if (currIndex >= maxAttestations) { - return (countsPerIssuer, accounts, issuedOns, signers); - } - - // Trim returned structs if necessary - address[] memory trimmedAccounts = new address[](currIndex); - uint256[] memory trimmedIssuedOns = new uint256[](currIndex); - address[] memory trimmedSigners = new address[](currIndex); - - for (uint256 i = 0; i < currIndex; i = i.add(1)) { - trimmedAccounts[i] = accounts[i]; - trimmedIssuedOns[i] = issuedOns[i]; - trimmedSigners[i] = signers[i]; - } - return (countsPerIssuer, trimmedAccounts, trimmedIssuedOns, trimmedSigners); - } - - /** - * @notice Similar to lookupUnrevokedAttestations but returns all attestations - * for `identifier` produced by `trustedIssuers`, - * either including or excluding attestations from revoked signers - * @param identifier Hash of the identifier - * @param trustedIssuers Array of n issuers whose attestations will be included - * @param includeRevoked Whether to include attestations produced by revoked signers - * @return [0] Array of number of attestations returned per issuer - * @return [1 - 3] for m (== sum([0])) found attestations: - * [ - * Array of m accounts, - * Array of m issuedOns, - * Array of m signers - * ]; index corresponds to the same attestation - * @dev Adds attestation info to the arrays in order of provided trustedIssuers - * @dev Expectation that only one attestation exists per (identifier, issuer, account) - */ - function lookupAttestations( - bytes32 identifier, - address[] calldata trustedIssuers, - bool includeRevoked - ) external view returns (uint256[] memory, address[] memory, uint256[] memory, address[] memory) { + // TODO: change issuedOn type, change the order of return values to match across the file, + // add publishedOn to returned lookups + function lookupAttestations(bytes32 identifier, address[] calldata trustedIssuers) + external + view + returns (uint256[] memory, address[] memory, uint256[] memory, address[] memory) + { // TODO reviewers: this is to get around a stack too deep error; // are there better ways of dealing with this? - return _lookupAttestations(identifier, trustedIssuers, includeRevoked); + return _lookupAttestations(identifier, trustedIssuers); } /** * @notice Helper function for lookupAttestations to get around stack too deep * @param identifier Hash of the identifier * @param trustedIssuers Array of n issuers whose attestations will be included - * @param includeRevoked Whether to include attestations produced by revoked signers * @return [0] Array of number of attestations returned per issuer * @return [1 - 3] for m (== sum([0])) found attestations: * [ @@ -301,17 +182,17 @@ contract FederatedAttestations is * @dev Adds attestation info to the arrays in order of provided trustedIssuers * @dev Expectation that only one attestation exists per (identifier, issuer, account) */ - function _lookupAttestations( - bytes32 identifier, - address[] memory trustedIssuers, - bool includeRevoked - ) internal view returns (uint256[] memory, address[] memory, uint256[] memory, address[] memory) { + // TODO: change issuedOn type, change the order of return values to match across the file, + // add publishedOn to returned lookups + function _lookupAttestations(bytes32 identifier, address[] memory trustedIssuers) + internal + view + returns (uint256[] memory, address[] memory, uint256[] memory, address[] memory) + { uint256 totalAttestations; uint256[] memory countsPerIssuer; - (totalAttestations, countsPerIssuer) = includeRevoked - ? getNumAttestations(identifier, trustedIssuers) - : getNumUnrevokedAttestations(identifier, trustedIssuers); + (totalAttestations, countsPerIssuer) = getNumAttestations(identifier, trustedIssuers); address[] memory accounts = new address[](totalAttestations); uint256[] memory issuedOns = new uint256[](totalAttestations); @@ -326,9 +207,6 @@ contract FederatedAttestations is for (uint256 i = 0; i < trustedIssuers.length; i = i.add(1)) { attestationsPerIssuer = identifierToAttestations[identifier][trustedIssuers[i]]; for (uint256 j = 0; j < attestationsPerIssuer.length; j = j.add(1)) { - if (!includeRevoked && revokedSigners[attestationsPerIssuer[j].signer]) { - continue; - } accounts[totalAttestations] = attestationsPerIssuer[j].account; issuedOns[totalAttestations] = attestationsPerIssuer[j].issuedOn; signers[totalAttestations] = attestationsPerIssuer[j].signer; @@ -338,47 +216,6 @@ contract FederatedAttestations is return (countsPerIssuer, accounts, issuedOns, signers); } - /** - * @notice Helper function for lookupIdentifiers to calculate the - total number of identifiers completed for an identifier - by each trusted issuer, from unrevoked signers only - * @param account Address of the account - * @param trustedIssuers Array of n issuers whose identifiers will be included - * @return [0] Sum total of identifiers found - * [1] Array of number of identifiers found per issuer - */ - function getNumUnrevokedIdentifiers(address account, address[] memory trustedIssuers) - internal - view - returns (uint256, uint256[] memory) - { - uint256 totalIdentifiers = 0; - uint256[] memory countsPerIssuer = new uint256[](trustedIssuers.length); - - OwnershipAttestation[] memory attestationsPerIssuer; - bytes32[] memory identifiersPerIssuer; - - for (uint256 i = 0; i < trustedIssuers.length; i = i.add(1)) { - identifiersPerIssuer = addressToIdentifiers[account][trustedIssuers[i]]; - for (uint256 j = 0; j < identifiersPerIssuer.length; j = j.add(1)) { - bytes32 identifier = identifiersPerIssuer[j]; - // Check if the mapping was produced by a revoked signer - attestationsPerIssuer = identifierToAttestations[identifier][trustedIssuers[i]]; - for (uint256 k = 0; k < attestationsPerIssuer.length; k = k.add(1)) { - OwnershipAttestation memory attestation = attestationsPerIssuer[k]; - // (identifier, account, issuer) tuples are checked for uniqueness on registration - if (!(attestation.account == account) || revokedSigners[attestation.signer]) { - continue; - } - totalIdentifiers = totalIdentifiers.add(1); - countsPerIssuer[i] = countsPerIssuer[i].add(1); - break; - } - } - } - return (totalIdentifiers, countsPerIssuer); - } - /** * @notice Helper function for lookupIdentifiers to calculate the total number of identifiers completed for an identifier @@ -407,87 +244,23 @@ contract FederatedAttestations is /** * @notice Returns up to `maxIdentifiers` identifiers mapped to `account` - * by unrevoked signers of `trustedIssuers` + * by signers of `trustedIssuers` * @param account Address of the account * @param trustedIssuers Array of n issuers whose identifier mappings will be used - * @param maxIdentifiers Limit the number of identifiers that will be returned - * @return [0] Array of number of identifiers returned per issuer - * @return [1] Array (length == sum([0]) <= maxIdentifiers) of identifiers - * @dev Adds identifier info to the arrays in order of provided trustedIssuers - * @dev Expectation that only one attestation exists per (identifier, issuer, account) - */ - function lookupUnrevokedIdentifiers( - address account, - address[] calldata trustedIssuers, - uint256 maxIdentifiers - ) external view returns (uint256[] memory, bytes32[] memory) { - uint256[] memory countsPerIssuer = new uint256[](trustedIssuers.length); - // Same as for the other lookup, preallocate and then trim for now - uint256 currIndex = 0; - bytes32[] memory identifiers = new bytes32[](maxIdentifiers); - - OwnershipAttestation[] memory attestationsPerIssuer; - bytes32[] memory identifiersPerIssuer; - - for (uint256 i = 0; i < trustedIssuers.length && currIndex < maxIdentifiers; i = i.add(1)) { - identifiersPerIssuer = addressToIdentifiers[account][trustedIssuers[i]]; - for ( - uint256 j = 0; - j < identifiersPerIssuer.length && currIndex < maxIdentifiers; - j = j.add(1) - ) { - bytes32 identifier = identifiersPerIssuer[j]; - // Check if the mapping was produced by a revoked signer - attestationsPerIssuer = identifierToAttestations[identifier][trustedIssuers[i]]; - for (uint256 k = 0; k < attestationsPerIssuer.length; k = k.add(1)) { - // (identifier, account, issuer) tuples are checked for uniqueness on registration - if ( - !(attestationsPerIssuer[k].account == account) || - revokedSigners[attestationsPerIssuer[k].signer] - ) { - continue; - } - identifiers[currIndex] = identifier; - currIndex = currIndex.add(1); - countsPerIssuer[i] = countsPerIssuer[i].add(1); - break; - } - } - } - if (currIndex >= maxIdentifiers) { - return (countsPerIssuer, identifiers); - } - // Allocate and fill properly-sized array - bytes32[] memory trimmedIdentifiers = new bytes32[](currIndex); - for (uint256 i = 0; i < currIndex; i = i.add(1)) { - trimmedIdentifiers[i] = identifiers[i]; - } - return (countsPerIssuer, trimmedIdentifiers); - } - - /** - * @notice Similar to lookupUnrevokedIdentifiers but returns all identifiers - * mapped to an address with attestations from a list of issuers, - * either including or excluding attestations from revoked signers - * @param account Address of the account - * @param trustedIssuers Array of n issuers whose identifier mappings will be used - * @param includeRevoked Whether to include identifiers attested by revoked signers * @return [0] Array of number of identifiers returned per issuer * @return [1] Array (length == sum([0])) of identifiers * @dev Adds identifier info to the arrays in order of provided trustedIssuers * @dev Expectation that only one attestation exists per (identifier, issuer, account) */ - function lookupIdentifiers( - address account, - address[] calldata trustedIssuers, - bool includeRevoked - ) external view returns (uint256[] memory, bytes32[] memory) { + function lookupIdentifiers(address account, address[] calldata trustedIssuers) + external + view + returns (uint256[] memory, bytes32[] memory) + { uint256 totalIdentifiers; uint256[] memory countsPerIssuer; - (totalIdentifiers, countsPerIssuer) = includeRevoked - ? getNumIdentifiers(account, trustedIssuers) - : getNumUnrevokedIdentifiers(account, trustedIssuers); + (totalIdentifiers, countsPerIssuer) = getNumIdentifiers(account, trustedIssuers); bytes32[] memory identifiers = new bytes32[](totalIdentifiers); bytes32[] memory identifiersPerIssuer; @@ -497,12 +270,6 @@ contract FederatedAttestations is for (uint256 i = 0; i < trustedIssuers.length; i = i.add(1)) { identifiersPerIssuer = addressToIdentifiers[account][trustedIssuers[i]]; for (uint256 j = 0; j < identifiersPerIssuer.length; j = j.add(1)) { - if ( - !includeRevoked && - !foundUnrevokedAttestation(account, identifiersPerIssuer[j], trustedIssuers[i]) - ) { - continue; - } identifiers[currIndex] = identifiersPerIssuer[j]; currIndex = currIndex.add(1); } @@ -510,42 +277,6 @@ contract FederatedAttestations is return (countsPerIssuer, identifiers); } - /** - * @notice Helper function for lookupIdentifiers to search through the - * attestations from `issuer` for one with an unrevoked signer - * that maps `account` -> `identifier - * @param account Address of the account - * @param identifier Hash of the identifier - * @param issuer Issuer whose attestations to search - * @return Whether or not an unrevoked attestation is found establishing the mapping - */ - function foundUnrevokedAttestation(address account, bytes32 identifier, address issuer) - internal - view - returns (bool) - { - OwnershipAttestation[] memory attestations = identifierToAttestations[identifier][issuer]; - for (uint256 i = 0; i < attestations.length; i = i.add(1)) { - if (attestations[i].account == account && !revokedSigners[attestations[i].signer]) { - return true; - } - } - return false; - } - - // TODO do we want to restrict permissions, or should anyone - // with a valid signature be able to register an attestation? - modifier isValidUser(address issuer, address account) { - require( - msg.sender == account || - msg.sender == issuer || - getAccounts().attestationSignerToAccount(msg.sender) == issuer, - "User does not have permission to perform this action" - ); - require(!revokedSigners[msg.sender], "User has been revoked "); - _; - } - /** * @notice Validates the given attestation and signature * @param identifier Hash of the identifier to be attested @@ -556,24 +287,26 @@ contract FederatedAttestations is * @param v The recovery id of the incoming ECDSA signature * @param r Output value r of the ECDSA signature * @param s Output value s of the ECDSA signature - * @return Whether the signature is valid - * @dev Throws if signer is revoked + * @dev Throws if attestation has been revoked * @dev Throws if signer is not an authorized AttestationSigner of the issuer */ - function isValidAttestation( + function validateAttestation( bytes32 identifier, address issuer, address account, - uint256 issuedOn, address signer, + uint64 issuedOn, uint8 v, bytes32 r, bytes32 s - ) public view returns (bool) { - require(!revokedSigners[signer], "Signer has been revoked"); + ) public view { + require( + !revokedAttestations[getUniqueAttestationHash(identifier, issuer, account, signer, issuedOn)], + "Attestation has been revoked" + ); require( getAccounts().attestationSignerToAccount(signer) == issuer, - "Signer has not been authorized as an AttestationSigner by the issuer" + "Signer is not a currently authorized AttestationSigner for the issuer" ); bytes32 structHash = keccak256( abi.encode(EIP712_VALIDATE_ATTESTATION_TYPEHASH, identifier, issuer, account, issuedOn) @@ -585,7 +318,7 @@ contract FederatedAttestations is r, s ); - return guessedSigner == signer; + require(guessedSigner == signer, "Signature is invalid"); } /** @@ -598,62 +331,72 @@ contract FederatedAttestations is * @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 Throws if sender is not the issuer, account, or an authorized AttestationSigner * @dev Throws if an attestation with the same (identifier, issuer, account) already exists */ function registerAttestation( bytes32 identifier, address issuer, address account, - uint256 issuedOn, address signer, + uint64 issuedOn, uint8 v, bytes32 r, bytes32 s - ) public isValidUser(issuer, account) { - require( - isValidAttestation(identifier, issuer, account, issuedOn, signer, v, r, s), - "Signature is invalid" - ); + ) external { + // TODO allow for updating existing attestation by only updating signer/publishedOn/issuedOn + validateAttestation(identifier, issuer, account, signer, issuedOn, v, r, s); for (uint256 i = 0; i < identifierToAttestations[identifier][issuer].length; i = i.add(1)) { // This enforces only one attestation to be uploaded // for a given set of (identifier, issuer, account) - // Editing/upgrading an attestation requires that it be deleted before a new one is registered + // Editing/upgrading an attestation requires that it be revoked before a new one is registered require( identifierToAttestations[identifier][issuer][i].account != account, "Attestation for this account already exists" ); } - OwnershipAttestation memory attestation = OwnershipAttestation(account, issuedOn, signer); + uint64 publishedOn = uint64(block.timestamp); + OwnershipAttestation memory attestation = OwnershipAttestation( + account, + signer, + issuedOn, + publishedOn + ); identifierToAttestations[identifier][issuer].push(attestation); addressToIdentifiers[account][issuer].push(identifier); - emit AttestationRegistered(identifier, issuer, account, issuedOn, signer); + emit AttestationRegistered(identifier, issuer, account, signer, issuedOn, publishedOn); } /** - * @notice Deletes an attestation - * @param identifier Hash of the identifier to be deleted + * @notice Revokes an attestation + * @param identifier Hash of the identifier to be revoked * @param issuer Address of the attestation issuer * @param account Address of the account mapped to the identifier - * @dev Throws if sender is not the issuer, account, or an authorized AttestationSigner + * @dev Throws if sender is not the issuer, signer, or account */ - function deleteAttestation(bytes32 identifier, address issuer, address account) - public - isValidUser(issuer, account) - { + // TODO should we pass in the issuedOn/signer parameter? ie. only revoke if the sender knows + // the issuedOn/signer for the unique attestation + function revokeAttestation(bytes32 identifier, address issuer, address account) external { OwnershipAttestation[] memory attestations = identifierToAttestations[identifier][issuer]; for (uint256 i = 0; i < attestations.length; i = i.add(1)) { OwnershipAttestation memory attestation = attestations[i]; if (attestation.account == account) { + address signer = attestation.signer; + uint64 issuedOn = attestation.issuedOn; + uint64 publishedOn = attestation.publishedOn; + // TODO reviewers: is there a risk that compromised signers could revoke legitimate + // attestations before they have been unauthorized? + require( + account == msg.sender || getAccounts().attestationSignerToAccount(msg.sender) == issuer, + "Sender does not have permission to revoke this attestation" + ); // This is meant to delete the attestation in the array // and then move the last element in the array to that empty spot, // to avoid having empty elements in the array - // TODO reviewers: is there a better way of doing this? + // TODO benchmark gas cost saving to check if array is of length 1 identifierToAttestations[identifier][issuer][i] = attestations[attestations.length - 1]; identifierToAttestations[identifier][issuer].pop(); bool deletedIdentifier = false; - bytes32[] memory identifiers = addressToIdentifiers[account][issuer]; for (uint256 j = 0; j < identifiers.length; j = j.add(1)) { if (identifiers[j] == identifier) { @@ -666,17 +409,31 @@ contract FederatedAttestations is // Should never be false - both mappings should always be updated in unison assert(deletedIdentifier); - emit AttestationDeleted(identifier, issuer, account); + bytes32 attestationHash = getUniqueAttestationHash( + identifier, + issuer, + account, + signer, + issuedOn + ); + // Should never be able to re-revoke an attestation + assert(!revokedAttestations[attestationHash]); + revokedAttestations[attestationHash] = true; + + emit AttestationRevoked(identifier, issuer, account, signer, issuedOn, publishedOn); return; } } - revert("Attestion to be deleted does not exist"); + revert("Attestion to be revoked does not exist"); } - function revokeSigner(address signer) public { - // TODO ASv2 add constraints on who has permissions to revoke a signer - // TODO ASv2 consider whether to check if the signer is an authorized signer - // or to allow any address to be revoked - revokedSigners[signer] = true; + function getUniqueAttestationHash( + bytes32 identifier, + address issuer, + address account, + address signer, + uint64 issuedOn + ) public pure returns (bytes32) { + return keccak256(abi.encode(identifier, issuer, account, signer, issuedOn)); } } diff --git a/packages/protocol/lib/fed-attestations-utils.ts b/packages/protocol/lib/fed-attestations-utils.ts index 9d494e8fff8..135919cdff2 100644 --- a/packages/protocol/lib/fed-attestations-utils.ts +++ b/packages/protocol/lib/fed-attestations-utils.ts @@ -24,8 +24,7 @@ const getTypedData = (chainId: number, contractAddress: Address, message?: Attes { name: 'identifier', type: 'bytes32' }, { name: 'issuer', type: 'address'}, { name: 'account', type: 'address' }, - { name: 'issuedOn', type: 'uint256' }, - // TODO ASv2 Consider including a nonce (which could also be used as an ID) + { name: 'issuedOn', type: 'uint64' }, ], }, primaryType: 'OwnershipAttestation', diff --git a/packages/protocol/test/identity/federatedattestations.ts b/packages/protocol/test/identity/federatedattestations.ts index cbf767e2ace..266ceffd2c4 100644 --- a/packages/protocol/test/identity/federatedattestations.ts +++ b/packages/protocol/test/identity/federatedattestations.ts @@ -76,8 +76,8 @@ contract('FederatedAttestations', (accounts: string[]) => { identifier, issuer, account, - issuedOn, signer, + issuedOn, attestationSignature.v, attestationSignature.r, attestationSignature.s, @@ -96,7 +96,7 @@ contract('FederatedAttestations', (accounts: string[]) => { signer: string, identifierIndex: number ) => { - const attestation = await federatedAttestations.identifierToAddresses( + const attestation = await federatedAttestations.identifierToAttestations( identifier, issuer, attestationIndex @@ -120,7 +120,7 @@ contract('FederatedAttestations', (accounts: string[]) => { identifierIndex: number ) => { await assertThrowsAsync( - federatedAttestations.identifierToAddresses(identifier, issuer, addressIndex) + federatedAttestations.identifierToAttestations(identifier, issuer, addressIndex) ) await assertThrowsAsync( federatedAttestations.addressToIdentifiers(account, issuer, identifierIndex) @@ -153,7 +153,7 @@ contract('FederatedAttestations', (accounts: string[]) => { describe('#EIP712_VALIDATE_ATTESTATION_TYPEHASH()', () => { it('should have set the right typehash', async () => { const expectedTypehash = keccak256( - 'OwnershipAttestation(bytes32 identifier,address issuer,address account,uint256 issuedOn)' + 'OwnershipAttestation(bytes32 identifier,address issuer,address account,uint64 issuedOn)' ) assert.equal( await federatedAttestations.EIP712_VALIDATE_ATTESTATION_TYPEHASH(), @@ -223,39 +223,17 @@ contract('FederatedAttestations', (accounts: string[]) => { } describe('when identifier has not been registered', () => { - describe('#lookupUnrevokedAttestations', () => { + describe('#lookupAttestations', () => { it('should return empty list', async () => { const [ countsPerIssuer, addresses, issuedOns, signers, - ] = await federatedAttestations.lookupUnrevokedAttestations(identifier1, [issuer1], 1) + ] = await federatedAttestations.lookupAttestations(identifier1, [issuer1]) checkAgainstExpectedAttestations([0], [], countsPerIssuer, addresses, issuedOns, signers) }) }) - describe('#lookupAttestations', () => { - ;[true, false].forEach((includeRevoked) => { - describe(`includeRevoked = ${includeRevoked}`, () => { - it('should return empty list', async () => { - const [ - countsPerIssuer, - addresses, - issuedOns, - signers, - ] = await federatedAttestations.lookupAttestations(identifier1, [issuer1], true) - checkAgainstExpectedAttestations( - [0], - [], - countsPerIssuer, - addresses, - issuedOns, - signers - ) - }) - }) - }) - }) }) describe('when identifier has been registered', () => { const account2 = accounts[3] @@ -312,14 +290,14 @@ contract('FederatedAttestations', (accounts: string[]) => { } }) - describe('#lookupUnrevokedAttestations', () => { - it('should return empty count if no issuers specified', async () => { + describe('#lookupAttestations', () => { + it('should return empty count and list if no issuers specified', async () => { const [ countsPerIssuer, addresses, issuedOns, signers, - ] = await federatedAttestations.lookupUnrevokedAttestations(identifier1, [], 1) + ] = await federatedAttestations.lookupAttestations(identifier1, []) checkAgainstExpectedAttestations([], [], countsPerIssuer, addresses, issuedOns, signers) }) @@ -329,12 +307,7 @@ contract('FederatedAttestations', (accounts: string[]) => { addresses, issuedOns, signers, - ] = await federatedAttestations.lookupUnrevokedAttestations( - identifier1, - [issuer1], - // Do not allow for maxAttestations to coincidentally limit incorrect output - issuer1Attestations.length + 1 - ) + ] = await federatedAttestations.lookupAttestations(identifier1, [issuer1]) checkAgainstExpectedAttestations( [issuer1Attestations.length], issuer1Attestations, @@ -351,7 +324,7 @@ contract('FederatedAttestations', (accounts: string[]) => { addresses, issuedOns, signers, - ] = await federatedAttestations.lookupUnrevokedAttestations(identifier1, [issuer3], 1) + ] = await federatedAttestations.lookupAttestations(identifier1, [issuer3]) checkAgainstExpectedAttestations([0], [], countsPerIssuer, addresses, issuedOns, signers) }) @@ -367,69 +340,11 @@ contract('FederatedAttestations', (accounts: string[]) => { addresses, issuedOns, signers, - ] = await federatedAttestations.lookupUnrevokedAttestations( - identifier1, - [issuer3, issuer2, issuer1], - expectedAttestations.length + 1 - ) - checkAgainstExpectedAttestations( - expectedCountsPerIssuer, - expectedAttestations, - countsPerIssuer, - addresses, - issuedOns, - signers - ) - }) - - it('should return empty list if maxAttestations == 0', async () => { - const [ - countsPerIssuer, - addresses, - issuedOns, - signers, - ] = await federatedAttestations.lookupUnrevokedAttestations(identifier1, [issuer1], 0) - checkAgainstExpectedAttestations([0], [], countsPerIssuer, addresses, issuedOns, signers) - }) - - it('should only return maxAttestations attestations when more are present', async () => { - const expectedAttestations = issuer1Attestations.slice(0, -1) - const expectedCountsPerIssuer = [expectedAttestations.length] - const [ - countsPerIssuer, - addresses, - issuedOns, - signers, - ] = await federatedAttestations.lookupUnrevokedAttestations( - identifier1, - [issuer1], - expectedAttestations.length - ) - checkAgainstExpectedAttestations( - expectedCountsPerIssuer, - expectedAttestations, - countsPerIssuer, - addresses, - issuedOns, - signers - ) - }) - - it('should not return attestations from revoked signers', async () => { - const attestationToRevoke = issuer2Attestations[0] - await federatedAttestations.revokeSigner(attestationToRevoke.signer) - const expectedAttestations = issuer2Attestations.slice(1) - const expectedCountsPerIssuer = [expectedAttestations.length] - const [ - countsPerIssuer, - addresses, - issuedOns, - signers, - ] = await federatedAttestations.lookupUnrevokedAttestations( - identifier1, - [issuer2], - issuer2Attestations.length - ) + ] = await federatedAttestations.lookupAttestations(identifier1, [ + issuer3, + issuer2, + issuer1, + ]) checkAgainstExpectedAttestations( expectedCountsPerIssuer, expectedAttestations, @@ -440,152 +355,6 @@ contract('FederatedAttestations', (accounts: string[]) => { ) }) }) - - describe('#lookupAttestations', () => { - ;[true, false].forEach((includeRevoked) => { - describe(`includeRevoked = ${includeRevoked}`, () => { - it('should return empty count and list if no issuers specified', async () => { - const [ - countsPerIssuer, - addresses, - issuedOns, - signers, - ] = await federatedAttestations.lookupAttestations(identifier1, [], includeRevoked) - checkAgainstExpectedAttestations( - [], - [], - countsPerIssuer, - addresses, - issuedOns, - signers - ) - }) - - it('should return all attestations from one issuer', async () => { - const [ - countsPerIssuer, - addresses, - issuedOns, - signers, - ] = await federatedAttestations.lookupAttestations( - identifier1, - [issuer1], - includeRevoked - ) - checkAgainstExpectedAttestations( - [issuer1Attestations.length], - issuer1Attestations, - countsPerIssuer, - addresses, - issuedOns, - signers - ) - }) - - it('should return empty list if no attestations exist for an issuer', async () => { - const [ - countsPerIssuer, - addresses, - issuedOns, - signers, - ] = await federatedAttestations.lookupAttestations( - identifier1, - [issuer3], - includeRevoked - ) - checkAgainstExpectedAttestations( - [0], - [], - countsPerIssuer, - addresses, - issuedOns, - signers - ) - }) - - it('should return attestations from multiple issuers in correct order', async () => { - const expectedAttestations = issuer2Attestations.concat(issuer1Attestations) - const expectedCountsPerIssuer = [ - 0, - issuer2Attestations.length, - issuer1Attestations.length, - ] - const [ - countsPerIssuer, - addresses, - issuedOns, - signers, - ] = await federatedAttestations.lookupAttestations( - identifier1, - [issuer3, issuer2, issuer1], - includeRevoked - ) - checkAgainstExpectedAttestations( - expectedCountsPerIssuer, - expectedAttestations, - countsPerIssuer, - addresses, - issuedOns, - signers - ) - }) - - if (includeRevoked) { - it('should return attestations from revoked and unrevoked signers', async () => { - await federatedAttestations.revokeSigner(issuer1Attestations[0].signer) - const expectedAttestations = issuer1Attestations.concat(issuer2Attestations) - const expectedCountsPerIssuer = [ - issuer1Attestations.length, - issuer2Attestations.length, - ] - const [ - countsPerIssuer, - addresses, - issuedOns, - signers, - ] = await federatedAttestations.lookupAttestations( - identifier1, - [issuer1, issuer2], - includeRevoked - ) - checkAgainstExpectedAttestations( - expectedCountsPerIssuer, - expectedAttestations, - countsPerIssuer, - addresses, - issuedOns, - signers - ) - }) - } else { - it('should not return attestations from revoked signers', async () => { - const attestationToRevoke = issuer2Attestations[0] - await federatedAttestations.revokeSigner(attestationToRevoke.signer) - const expectedAttestations = issuer2Attestations.slice(1) - const expectedCountsPerIssuer = [expectedAttestations.length] - const [ - countsPerIssuer, - addresses, - issuedOns, - signers, - ] = await federatedAttestations.lookupAttestations( - identifier1, - [issuer2], - includeRevoked - ) - checkAgainstExpectedAttestations( - expectedCountsPerIssuer, - expectedAttestations, - countsPerIssuer, - addresses, - issuedOns, - signers - ) - }) - } - }) - }) - }) }) }) @@ -606,28 +375,15 @@ contract('FederatedAttestations', (accounts: string[]) => { } describe('when address has not been registered', () => { - describe('#lookupUnrevokedIdentifiers', () => { + describe('#lookupIdentifiers', () => { it('should return empty list', async () => { const [ actualCountsPerIssuer, actualIdentifiers, - ] = await federatedAttestations.lookupUnrevokedIdentifiers(account1, [issuer1], 1) + ] = await federatedAttestations.lookupIdentifiers(account1, [issuer1]) checkAgainstExpectedIdCases([0], [], actualCountsPerIssuer, actualIdentifiers) }) }) - describe('#lookupIdentifiers', () => { - ;[true, false].forEach((includeRevoked) => { - describe(`includeRevoked = ${includeRevoked}`, () => { - it('should return empty list', async () => { - const [ - actualCountsPerIssuer, - actualIdentifiers, - ] = await federatedAttestations.lookupIdentifiers(account1, [issuer1], true) - checkAgainstExpectedIdCases([0], [], actualCountsPerIssuer, actualIdentifiers) - }) - }) - }) - }) }) describe('when address has been registered', () => { @@ -676,12 +432,12 @@ contract('FederatedAttestations', (accounts: string[]) => { } }) - describe('#lookupUnrevokedIdentifiers', () => { + describe('#lookupIdentifiers', () => { it('should return empty count if no issuers specified', async () => { const [ actualCountsPerIssuer, actualIdentifiers, - ] = await federatedAttestations.lookupUnrevokedIdentifiers(account1, [], 1) + ] = await federatedAttestations.lookupIdentifiers(account1, []) checkAgainstExpectedIdCases([], [], actualCountsPerIssuer, actualIdentifiers) }) @@ -689,11 +445,7 @@ contract('FederatedAttestations', (accounts: string[]) => { const [ actualCountsPerIssuer, actualIdentifiers, - ] = await federatedAttestations.lookupUnrevokedIdentifiers( - account1, - [issuer1], - issuer1IdCases.length + 1 - ) + ] = await federatedAttestations.lookupIdentifiers(account1, [issuer1]) checkAgainstExpectedIdCases( [issuer1IdCases.length], issuer1IdCases, @@ -706,7 +458,7 @@ contract('FederatedAttestations', (accounts: string[]) => { const [ actualCountsPerIssuer, actualIdentifiers, - ] = await federatedAttestations.lookupUnrevokedIdentifiers(account1, [issuer3], 1) + ] = await federatedAttestations.lookupIdentifiers(account1, [issuer3]) checkAgainstExpectedIdCases([0], [], actualCountsPerIssuer, actualIdentifiers) }) @@ -716,11 +468,7 @@ contract('FederatedAttestations', (accounts: string[]) => { const [ actualCountsPerIssuer, actualIdentifiers, - ] = await federatedAttestations.lookupUnrevokedIdentifiers( - account1, - [issuer3, issuer2, issuer1], - expectedIdCases.length + 1 - ) + ] = await federatedAttestations.lookupIdentifiers(account1, [issuer3, issuer2, issuer1]) checkAgainstExpectedIdCases( expectedCountsPerIssuer, expectedIdCases, @@ -728,170 +476,25 @@ contract('FederatedAttestations', (accounts: string[]) => { actualIdentifiers ) }) - - it('should return empty list if maxIdentifiers == 0', async () => { - const [ - actualCountsPerIssuer, - actualIdentifiers, - ] = await federatedAttestations.lookupUnrevokedIdentifiers(account1, [issuer1], 0) - checkAgainstExpectedIdCases([0], [], actualCountsPerIssuer, actualIdentifiers) - }) - - it('should only return maxIdentifiers identifiers when more are present', async () => { - const expectedIdCases = issuer2IdCases.concat(issuer1IdCases).slice(0, -1) - const expectedCountsPerIssuer = [ - issuer2IdCases.length, - expectedIdCases.length - issuer2IdCases.length, - ] - const [ - actualCountsPerIssuer, - actualIdentifiers, - ] = await federatedAttestations.lookupUnrevokedIdentifiers( - account1, - [issuer2, issuer1], - expectedIdCases.length - ) - checkAgainstExpectedIdCases( - expectedCountsPerIssuer, - expectedIdCases, - actualCountsPerIssuer, - actualIdentifiers - ) - }) - - it('should not return identifiers from revoked signers', async () => { - await federatedAttestations.revokeSigner(issuer2IdCases[0].signer) - const expectedIdCases = issuer2IdCases.slice(1) - const expectedCountsPerIssuer = [expectedIdCases.length] - const [ - actualCountsPerIssuer, - actualIdentifiers, - ] = await federatedAttestations.lookupUnrevokedIdentifiers( - account1, - [issuer2], - expectedIdCases.length + 1 - ) - checkAgainstExpectedIdCases( - expectedCountsPerIssuer, - expectedIdCases, - actualCountsPerIssuer, - actualIdentifiers - ) - }) - }) - - describe('#lookupIdentifiers', () => { - ;[true, false].forEach((includeRevoked) => { - describe(`includeRevoked = ${includeRevoked}`, () => { - it('should return empty count if no issuers specified', async () => { - const [ - actualCountsPerIssuer, - actualIdentifiers, - ] = await federatedAttestations.lookupIdentifiers(account1, [], includeRevoked) - checkAgainstExpectedIdCases([], [], actualCountsPerIssuer, actualIdentifiers) - }) - - it('should return all identifiers from one issuer', async () => { - const [ - actualCountsPerIssuer, - actualIdentifiers, - ] = await federatedAttestations.lookupIdentifiers(account1, [issuer1], includeRevoked) - checkAgainstExpectedIdCases( - [issuer1IdCases.length], - issuer1IdCases, - actualCountsPerIssuer, - actualIdentifiers - ) - }) - - it('should return empty list if no identifiers exist for an (issuer,address)', async () => { - const [ - actualCountsPerIssuer, - actualIdentifiers, - ] = await federatedAttestations.lookupIdentifiers(account1, [issuer3], includeRevoked) - checkAgainstExpectedIdCases([0], [], actualCountsPerIssuer, actualIdentifiers) - }) - - it('should return identifiers from multiple issuers in correct order', async () => { - const expectedIdCases = issuer2IdCases.concat(issuer1IdCases) - const expectedCountsPerIssuer = [0, issuer2IdCases.length, issuer1IdCases.length] - const [ - actualCountsPerIssuer, - actualIdentifiers, - ] = await federatedAttestations.lookupIdentifiers( - account1, - [issuer3, issuer2, issuer1], - includeRevoked - ) - checkAgainstExpectedIdCases( - expectedCountsPerIssuer, - expectedIdCases, - actualCountsPerIssuer, - actualIdentifiers - ) - }) - - if (includeRevoked) { - it('should return identifiers from revoked and unrevoked signers', async () => { - await federatedAttestations.revokeSigner(issuer2IdCases[0].signer) - const [ - actualCountsPerIssuer, - actualIdentifiers, - ] = await federatedAttestations.lookupIdentifiers( - account1, - [issuer2], - includeRevoked - ) - checkAgainstExpectedIdCases( - [issuer2IdCases.length], - issuer2IdCases, - actualCountsPerIssuer, - actualIdentifiers - ) - }) - } else { - it('should not return identifiers from revoked signers', async () => { - await federatedAttestations.revokeSigner(issuer2IdCases[0].signer) - const expectedIdCases = issuer2IdCases.slice(1) - const expectedCountsPerIssuer = [expectedIdCases.length] - - const [ - actualCountsPerIssuer, - actualIdentifiers, - ] = await federatedAttestations.lookupIdentifiers( - account1, - [issuer2], - includeRevoked - ) - checkAgainstExpectedIdCases( - expectedCountsPerIssuer, - expectedIdCases, - actualCountsPerIssuer, - actualIdentifiers - ) - }) - } - }) - }) }) }) }) - describe('#isValidAttestation', async () => { + describe('#validateAttestation', async () => { describe('with an authorized AttestationSigner', async () => { beforeEach(async () => { await accountsInstance.authorizeSigner(signer1, signerRole, { from: issuer1 }) await accountsInstance.completeSignerAuthorization(issuer1, signerRole, { from: signer1 }) }) - it('should return true if a valid signature is used', async () => { - assert.isTrue( - await federatedAttestations.isValidAttestation( + it('should return successfully if a valid signature is used', async () => { + assert.isOk( + await federatedAttestations.validateAttestation( identifier1, issuer1, account1, - nowUnixTime, signer1, + nowUnixTime, sig.v, sig.r, sig.s @@ -909,13 +512,13 @@ contract('FederatedAttestations', (accounts: string[]) => { chainId, federatedAttestations.address ) - assert.isFalse( - await federatedAttestations.isValidAttestation( + await assertRevert( + federatedAttestations.validateAttestation( identifier1, issuer1, account1, - nowUnixTime, signer1, + nowUnixTime, sig2.v, sig2.r, sig2.s @@ -927,48 +530,43 @@ contract('FederatedAttestations', (accounts: string[]) => { [0, 'identifier', identifier2], [1, 'issuer', accounts[3]], [2, 'account', accounts[3]], - [3, 'issuedOn', nowUnixTime - 1], - [4, 'signer', accounts[3]], + [3, 'signer', accounts[3]], + [4, 'issuedOn', nowUnixTime - 1], ] wrongArgs.forEach(([index, arg, wrongValue]) => { it(`should fail if the provided ${arg} is different from the attestation`, async () => { - const args = [identifier1, issuer1, account1, nowUnixTime, signer1, sig.v, sig.r, sig.s] + const args = [identifier1, issuer1, account1, signer1, nowUnixTime, sig.v, sig.r, sig.s] args[index] = wrongValue - - if (arg === 'issuer' || arg === 'signer') { - await assertRevert(federatedAttestations.isValidAttestation.apply(this, args)) - } else { - assert.isFalse(await federatedAttestations.isValidAttestation.apply(this, args)) - } + await assertRevert(federatedAttestations.validateAttestation.apply(this, args)) }) }) - it('should revert if the signer is revoked', async () => { - await federatedAttestations.revokeSigner(signer1) - await assertRevertWithReason( - federatedAttestations.isValidAttestation( + it('should revert if the attestation is revoked', async () => { + await signAndRegisterAttestation(identifier1, issuer1, account1, nowUnixTime, signer1) + await federatedAttestations.revokeAttestation(identifier1, issuer1, account1) + await assertRevert( + federatedAttestations.validateAttestation( identifier1, issuer1, account1, - nowUnixTime, signer1, + nowUnixTime, sig.v, sig.r, sig.s - ), - 'Signer has been revoked' + ) ) }) }) it('should revert if the signer is not authorized as an AttestationSigner by the issuer', async () => { await assertRevert( - federatedAttestations.isValidAttestation( + federatedAttestations.validateAttestation( identifier1, issuer1, account1, - nowUnixTime, signer1, + nowUnixTime, sig.v, sig.r, sig.s @@ -982,12 +580,12 @@ contract('FederatedAttestations', (accounts: string[]) => { await accountsInstance.completeSignerAuthorization(issuer1, role, { from: signer1 }) await assertRevert( - federatedAttestations.isValidAttestation( + federatedAttestations.validateAttestation( identifier1, issuer1, account1, - nowUnixTime, signer1, + nowUnixTime, sig.v, sig.r, sig.s @@ -1007,22 +605,33 @@ contract('FederatedAttestations', (accounts: string[]) => { identifier1, issuer1, account1, - nowUnixTime, signer1, + nowUnixTime, sig.v, sig.r, sig.s ) + + // fetching onchain publishedOn value as expected test value to avoid testing for a specific value + // as it would be a very flaky test + const attestation = await federatedAttestations.identifierToAttestations( + identifier1, + issuer1, + 0 + ) + const publishedOn = attestation['publishedOn'] assertLogMatches2(register.logs[0], { event: 'AttestationRegistered', args: { identifier: identifier1, issuer: issuer1, account: account1, - issuedOn: nowUnixTime, signer: signer1, + issuedOn: nowUnixTime, + publishedOn, }, }) + assert.isAtLeast(publishedOn.toNumber(), nowUnixTime) }) it('should succeed if issuer == signer', async () => { @@ -1045,8 +654,8 @@ contract('FederatedAttestations', (accounts: string[]) => { identifier1, issuer1, account1, - nowUnixTime, signer1, + nowUnixTime, sig2.v, sig2.r, sig2.s @@ -1054,20 +663,21 @@ contract('FederatedAttestations', (accounts: string[]) => { ) }) - it('should revert if signer has been revoked', async () => { - await federatedAttestations.revokeSigner(signer1) + it('should revert if attestation has been revoked', async () => { + await signAndRegisterAttestation(identifier1, issuer1, account1, nowUnixTime, signer1) + await federatedAttestations.revokeAttestation(identifier1, issuer1, account1) await assertRevertWithReason( federatedAttestations.registerAttestation( identifier1, issuer1, account1, - nowUnixTime, signer1, + nowUnixTime, sig.v, sig.r, sig.s ), - 'Signer has been revoked' + 'Attestation has been revoked' ) }) @@ -1084,8 +694,8 @@ contract('FederatedAttestations', (accounts: string[]) => { identifier1, issuer1, account1, - nowUnixTime, signer1, + nowUnixTime, sig.v, sig.r, sig.s @@ -1120,8 +730,8 @@ contract('FederatedAttestations', (accounts: string[]) => { identifier1, issuer1, account1, - nowUnixTime, signer2, + nowUnixTime, sig2.v, sig2.r, sig2.s @@ -1149,14 +759,14 @@ contract('FederatedAttestations', (accounts: string[]) => { }) }) - it('should revert if an invalid user attempts to register the attestation', async () => { - await assertRevert( - federatedAttestations.registerAttestation( + it('should succeed if any user attempts to register the attestation with a valid signature', async () => { + assert.isOk( + await federatedAttestations.registerAttestation( identifier1, issuer1, account1, - nowUnixTime, signer1, + nowUnixTime, sig.v, sig.r, sig.s, @@ -1173,8 +783,8 @@ contract('FederatedAttestations', (accounts: string[]) => { identifier1, issuer1, account1, - nowUnixTime, signer1, + nowUnixTime, sig.v, sig.r, sig.s, @@ -1184,7 +794,7 @@ contract('FederatedAttestations', (accounts: string[]) => { }) }) - describe('#deleteAttestation', () => { + describe('#revokeAttestation', () => { beforeEach(async () => { await accountsInstance.authorizeSigner(signer1, signerRole, { from: issuer1 }) await accountsInstance.completeSignerAuthorization(issuer1, signerRole, { from: signer1 }) @@ -1192,38 +802,59 @@ contract('FederatedAttestations', (accounts: string[]) => { identifier1, issuer1, account1, - nowUnixTime, signer1, + nowUnixTime, sig.v, sig.r, sig.s ) }) - it('should emit an AttestationDeleted event after successfully deleting', async () => { - const deleteAttestation = await federatedAttestations.deleteAttestation( + it('should modify identifierToAddresses and addresstoIdentifiers accordingly', async () => { + await assertAttestationInStorage(identifier1, issuer1, 0, account1, nowUnixTime, signer1, 0) + await federatedAttestations.revokeAttestation(identifier1, issuer1, account1) + await assertAttestationNotInStorage(identifier1, issuer1, account1, 0, 0) + }) + + it('should emit an AttestationRevoked event after successfully revoking', async () => { + const attestation = await federatedAttestations.identifierToAttestations( + identifier1, + issuer1, + 0 + ) + + // fetching onchain publishedOn value as expected test value to avoid testing for a specific value + // as it would be a very flaky test to try and predict the value + const publishedOn = attestation['publishedOn'] + const revokeAttestation = await federatedAttestations.revokeAttestation( identifier1, issuer1, account1 ) - assertLogMatches2(deleteAttestation.logs[0], { - event: 'AttestationDeleted', + assertLogMatches2(revokeAttestation.logs[0], { + event: 'AttestationRevoked', args: { identifier: identifier1, issuer: issuer1, account: account1, + signer: signer1, + issuedOn: nowUnixTime, + publishedOn, }, }) }) - it("should revert when deleting an attestation that doesn't exist", async () => { - await assertRevert(federatedAttestations.deleteAttestation(identifier1, issuer1, accounts[4])) + it("should revert when revoking an attestation that doesn't exist", async () => { + await assertRevertWithReason( + federatedAttestations.revokeAttestation(identifier1, issuer1, accounts[4]), + 'Attestion to be revoked does not exist' + ) }) it('should succeed when >1 attestations are registered for (identifier, issuer)', async () => { const account2 = accounts[3] await signAndRegisterAttestation(identifier1, issuer1, account2, nowUnixTime, signer1) - await federatedAttestations.deleteAttestation(identifier1, issuer1, account2, { + await federatedAttestations.revokeAttestation(identifier1, issuer1, account2, { from: account2, }) await assertAttestationNotInStorage(identifier1, issuer1, account2, 1, 0) @@ -1232,65 +863,51 @@ contract('FederatedAttestations', (accounts: string[]) => { it('should succeed when >1 identifiers are registered for (account, issuer)', async () => { await signAndRegisterAttestation(identifier2, issuer1, account1, nowUnixTime, signer1) - await federatedAttestations.deleteAttestation(identifier2, issuer1, account1, { + await federatedAttestations.revokeAttestation(identifier2, issuer1, account1, { from: account1, }) await assertAttestationNotInStorage(identifier2, issuer1, account1, 0, 1) await assertAttestationInStorage(identifier1, issuer1, 0, account1, nowUnixTime, signer1, 0) }) - it('should revert if an invalid user attempts to delete the attestation', async () => { - await assertRevert( - federatedAttestations.deleteAttestation(identifier1, issuer1, account1, { - from: accounts[4], - }) - ) + const newAttestation = [ + [0, 'identifier', identifier2], + // skipping issuer as it requires a different signer as well + [2, 'account', accounts[3]], + [3, 'issuedOn', nowUnixTime + 1], + [4, 'signer', accounts[3]], + ] + newAttestation.forEach(([index, arg, newVal]) => { + it(`after revoking an attestation, should succeed in registering new attestation with different ${arg}`, async () => { + await federatedAttestations.revokeAttestation(identifier1, issuer1, account1) + const args = [identifier1, issuer1, account1, nowUnixTime, signer1] + args[index] = newVal + await signAndRegisterAttestation.apply(this, args) + }) }) - it('should revert if a revoked signer attempts to delete the attestation', async () => { - await federatedAttestations.revokeSigner(signer1) + it('should revert if an invalid user attempts to revoke the attestation', async () => { await assertRevert( - federatedAttestations.deleteAttestation(identifier1, issuer1, account1, { from: signer1 }) + federatedAttestations.revokeAttestation(identifier1, issuer1, account1, { + from: accounts[4], + }) ) }) - it('should successfully delete an attestation with a revoked signer', async () => { - await federatedAttestations.revokeSigner(signer1) - await federatedAttestations.deleteAttestation(identifier1, issuer1, account1) - await assertAttestationNotInStorage(identifier1, issuer1, account1, 0, 1) - }) - - it('should fail registering same attestation but succeed after deleting it', async () => { + it('should fail to register a revoked attestation', async () => { + await federatedAttestations.revokeAttestation(identifier1, issuer1, account1) await assertRevert( federatedAttestations.registerAttestation( identifier1, issuer1, account1, - nowUnixTime, signer1, + nowUnixTime, sig.v, sig.r, sig.s ) ) - await federatedAttestations.deleteAttestation(identifier1, issuer1, account1) - await federatedAttestations.registerAttestation( - identifier1, - issuer1, - account1, - nowUnixTime, - signer1, - sig.v, - sig.r, - sig.s - ) - await assertAttestationInStorage(identifier1, issuer1, 0, account1, nowUnixTime, signer1, 0) - }) - - it('should modify identifierToAddresses and addresstoIdentifiers accordingly', async () => { - await assertAttestationInStorage(identifier1, issuer1, 0, account1, nowUnixTime, signer1, 0) - await federatedAttestations.deleteAttestation(identifier1, issuer1, account1) - await assertAttestationNotInStorage(identifier1, issuer1, account1, 0, 0) }) }) }) diff --git a/packages/sdk/utils/src/sign-typed-data-utils.ts b/packages/sdk/utils/src/sign-typed-data-utils.ts index 7dff5bb2df2..52c155543b4 100644 --- a/packages/sdk/utils/src/sign-typed-data-utils.ts +++ b/packages/sdk/utils/src/sign-typed-data-utils.ts @@ -43,7 +43,9 @@ export const EIP712_ATOMIC_TYPES = [ 'bytes1', 'bytes32', 'uint8', + 'uint64', 'uint256', + // This list should technically include all types from uint8 to uint256, and int8 to int256 'int8', 'int256', 'bool', From ab0019bddc52b30dbac4b9607dc91bf359f02780 Mon Sep 17 00:00:00 2001 From: Dave Carroll <12916551+dave-carroll7@users.noreply.github.com> Date: Wed, 8 Jun 2022 13:00:24 -0700 Subject: [PATCH 09/30] [ASv2] direct attestations from issuers (#9627) * individual revocation * add back lookup functions * PR comments * tests passing * more tests * renamed a function and separated signature check for (issuer == msg.sender) case * allows for registering attestations directly from the issuer * refactored require statements * overloaded registerAttestation function, cleaned up comments * added test cases for (issuer==msg.sender) flow * changed registerAttestation to registerAttestationAsIssuer * changed order of arguments in contract tests to reflect actual argument order * update tests and fn params ordering * fix lint Co-authored-by: Isabelle Wei --- .../identity/FederatedAttestations.sol | 75 ++++++++++++++----- .../test/identity/federatedattestations.ts | 70 +++++++++++------ .../sdk/utils/src/sign-typed-data-utils.ts | 1 + 3 files changed, 106 insertions(+), 40 deletions(-) diff --git a/packages/protocol/contracts/identity/FederatedAttestations.sol b/packages/protocol/contracts/identity/FederatedAttestations.sol index ed41d6d3c99..ce6ca01bbc4 100644 --- a/packages/protocol/contracts/identity/FederatedAttestations.sol +++ b/packages/protocol/contracts/identity/FederatedAttestations.sol @@ -290,7 +290,7 @@ contract FederatedAttestations is * @dev Throws if attestation has been revoked * @dev Throws if signer is not an authorized AttestationSigner of the issuer */ - function validateAttestation( + function validateAttestationSig( bytes32 identifier, address issuer, address account, @@ -300,10 +300,6 @@ contract FederatedAttestations is bytes32 r, bytes32 s ) public view { - require( - !revokedAttestations[getUniqueAttestationHash(identifier, issuer, account, signer, issuedOn)], - "Attestation has been revoked" - ); require( getAccounts().attestationSignerToAccount(signer) == issuer, "Signer is not a currently authorized AttestationSigner for the issuer" @@ -322,29 +318,24 @@ contract FederatedAttestations is } /** - * @notice Registers an attestation with a valid signature + * @notice Registers an attestation * @param identifier Hash of the identifier to be attested * @param issuer Address of the attestation issuer * @param account Address of the account being mapped to the identifier * @param issuedOn Time at which the issuer issued the attestation in Unix time * @param signer Address of the signer of the attestation - * @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 Throws if an attestation with the same (identifier, issuer, account) already exists */ - function registerAttestation( + function _registerAttestation( bytes32 identifier, address issuer, address account, address signer, - uint64 issuedOn, - uint8 v, - bytes32 r, - bytes32 s - ) external { - // TODO allow for updating existing attestation by only updating signer/publishedOn/issuedOn - validateAttestation(identifier, issuer, account, signer, issuedOn, v, r, s); + uint64 issuedOn + ) private { + require( + !revokedAttestations[getUniqueAttestationHash(identifier, issuer, account, signer, issuedOn)], + "Attestation has been revoked" + ); for (uint256 i = 0; i < identifierToAttestations[identifier][issuer].length; i = i.add(1)) { // This enforces only one attestation to be uploaded // for a given set of (identifier, issuer, account) @@ -366,6 +357,54 @@ contract FederatedAttestations is emit AttestationRegistered(identifier, issuer, account, signer, issuedOn, publishedOn); } + /** + * @notice Registers an attestation directly from the issuer + * @param identifier Hash of the identifier to be attested + * @param issuer Address of the attestation issuer + * @param account Address of the account being mapped to the identifier + * @param issuedOn Time at which the issuer issued the attestation in Unix time + * @param signer Address of the signer of the attestation + * @dev Throws if an attestation with the same (identifier, issuer, account) already exists + */ + function registerAttestationAsIssuer( + bytes32 identifier, + address issuer, + address account, + address signer, + uint64 issuedOn + ) external { + // TODO allow for updating existing attestation by only updating signer and publishedOn + require(issuer == msg.sender); + _registerAttestation(identifier, issuer, account, signer, issuedOn); + } + + /** + * @notice Registers an attestation with a valid signature + * @param identifier Hash of the identifier to be attested + * @param issuer Address of the attestation issuer + * @param account Address of the account being mapped to the identifier + * @param issuedOn Time at which the issuer issued the attestation in Unix time + * @param signer Address of the signer of the attestation + * @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 Throws if an attestation with the same (identifier, issuer, account) already exists + */ + function registerAttestation( + bytes32 identifier, + address issuer, + address account, + address signer, + uint64 issuedOn, + uint8 v, + bytes32 r, + bytes32 s + ) external { + // TODO allow for updating existing attestation by only updating signer and publishedOn + validateAttestationSig(identifier, issuer, account, signer, issuedOn, v, r, s); + _registerAttestation(identifier, issuer, account, signer, issuedOn); + } + /** * @notice Revokes an attestation * @param identifier Hash of the identifier to be revoked diff --git a/packages/protocol/test/identity/federatedattestations.ts b/packages/protocol/test/identity/federatedattestations.ts index 266ceffd2c4..fec7f516114 100644 --- a/packages/protocol/test/identity/federatedattestations.ts +++ b/packages/protocol/test/identity/federatedattestations.ts @@ -489,7 +489,7 @@ contract('FederatedAttestations', (accounts: string[]) => { it('should return successfully if a valid signature is used', async () => { assert.isOk( - await federatedAttestations.validateAttestation( + await federatedAttestations.validateAttestationSig( identifier1, issuer1, account1, @@ -513,7 +513,7 @@ contract('FederatedAttestations', (accounts: string[]) => { federatedAttestations.address ) await assertRevert( - federatedAttestations.validateAttestation( + federatedAttestations.validateAttestationSig( identifier1, issuer1, account1, @@ -537,31 +537,14 @@ contract('FederatedAttestations', (accounts: string[]) => { it(`should fail if the provided ${arg} is different from the attestation`, async () => { const args = [identifier1, issuer1, account1, signer1, nowUnixTime, sig.v, sig.r, sig.s] args[index] = wrongValue - await assertRevert(federatedAttestations.validateAttestation.apply(this, args)) + await assertRevert(federatedAttestations.validateAttestationSig.apply(this, args)) }) }) - - it('should revert if the attestation is revoked', async () => { - await signAndRegisterAttestation(identifier1, issuer1, account1, nowUnixTime, signer1) - await federatedAttestations.revokeAttestation(identifier1, issuer1, account1) - await assertRevert( - federatedAttestations.validateAttestation( - identifier1, - issuer1, - account1, - signer1, - nowUnixTime, - sig.v, - sig.r, - sig.s - ) - ) - }) }) it('should revert if the signer is not authorized as an AttestationSigner by the issuer', async () => { await assertRevert( - federatedAttestations.validateAttestation( + federatedAttestations.validateAttestationSig( identifier1, issuer1, account1, @@ -580,7 +563,7 @@ contract('FederatedAttestations', (accounts: string[]) => { await accountsInstance.completeSignerAuthorization(issuer1, role, { from: signer1 }) await assertRevert( - federatedAttestations.validateAttestation( + federatedAttestations.validateAttestationSig( identifier1, issuer1, account1, @@ -634,6 +617,23 @@ contract('FederatedAttestations', (accounts: string[]) => { assert.isAtLeast(publishedOn.toNumber(), nowUnixTime) }) + it('should revert if the attestation is revoked', async () => { + await signAndRegisterAttestation(identifier1, issuer1, account1, nowUnixTime, signer1) + await federatedAttestations.revokeAttestation(identifier1, issuer1, account1) + await assertRevert( + federatedAttestations.registerAttestation( + identifier1, + issuer1, + account1, + signer1, + nowUnixTime, + sig.v, + sig.r, + sig.s + ) + ) + }) + it('should succeed if issuer == signer', async () => { await signAndRegisterAttestation(identifier1, issuer1, account1, nowUnixTime, issuer1) await assertAttestationInStorage(identifier1, issuer1, 0, account1, nowUnixTime, issuer1, 0) @@ -792,6 +792,32 @@ contract('FederatedAttestations', (accounts: string[]) => { ) await assertAttestationInStorage(identifier1, issuer1, 0, account1, nowUnixTime, signer1, 0) }) + + it('should succeed if the issuer submits the attestation directly', async () => { + assert.isOk( + await federatedAttestations.registerAttestationAsIssuer( + identifier1, + issuer1, + account1, + issuer1, + nowUnixTime, + { from: issuer1 } + ) + ) + }) + + it('should revert if a non-issuer submits an attestation with no signature', async () => { + await assertRevert( + federatedAttestations.registerAttestationAsIssuer( + identifier1, + issuer1, + account1, + signer1, + nowUnixTime, + { from: signer1 } + ) + ) + }) }) describe('#revokeAttestation', () => { diff --git a/packages/sdk/utils/src/sign-typed-data-utils.ts b/packages/sdk/utils/src/sign-typed-data-utils.ts index 52c155543b4..9e7bd209ba8 100644 --- a/packages/sdk/utils/src/sign-typed-data-utils.ts +++ b/packages/sdk/utils/src/sign-typed-data-utils.ts @@ -155,6 +155,7 @@ function findDependencies(primaryType: string, types: EIP712Types, found: string // If this is not a builtin and is not defined, we cannot correctly construct a type encoding. if (types[primaryType] === undefined) { + console.log(types) throw new Error(`Unrecognized type ${primaryType} is not included in the EIP-712 type list`) } From 16e62c5b745a65e826c95d49d3235273813f53a8 Mon Sep 17 00:00:00 2001 From: Eela Nagaraj <7308464+eelanagaraj@users.noreply.github.com> Date: Thu, 9 Jun 2022 16:57:50 +0200 Subject: [PATCH 10/30] Fix Escrow bug and refactor (part 1) existing Escrow tests (#9617) * Fix and add transfer test cases * wip: withdraw helper * wip: checkpoint 2 pre-withdraw cleanup * wip: checkpoint after most withdraw tests done * wip: slightly clean up transfer checks * wip checkpoint before extracting delete * wip finish up extracting out delete checks * wip remove doubleTransfer and add revoke messages * wip: clean up and match names between Escrow.sol and tests * Add tests for events emitted * Remove get*PaymentIds (further refactoring steps) for now * Fix identifier length bug * Nit: update method for getting aPhoneHash * Simplify require conditional expressions in transfer and withdraw * Fix build after rebasing ASv2 * Fix Withdrawal event 'to' field to reference receiver of withdrawal funds * Nit: cleanup ASv2 base branch console.log --- .../protocol/contracts/identity/Escrow.sol | 10 +- packages/protocol/test/identity/escrow.ts | 803 ++++++++++++------ .../test/identity/federatedattestations.ts | 4 +- .../sdk/utils/src/sign-typed-data-utils.ts | 1 - 4 files changed, 561 insertions(+), 257 deletions(-) diff --git a/packages/protocol/contracts/identity/Escrow.sol b/packages/protocol/contracts/identity/Escrow.sol index 285dd5ce55a..2cf61f9cdb8 100644 --- a/packages/protocol/contracts/identity/Escrow.sol +++ b/packages/protocol/contracts/identity/Escrow.sol @@ -33,6 +33,8 @@ contract Escrow is event Withdrawal( bytes32 indexed identifier, + // Note that in previous versions of Escrow.sol, `to` referenced + // the original sender of the payment address indexed to, address indexed token, uint256 value, @@ -117,7 +119,7 @@ contract Escrow is ) external nonReentrant returns (bool) { require(token != address(0) && value > 0 && expirySeconds > 0, "Invalid transfer inputs."); require( - !(identifier.length <= 0 && !(minAttestations == 0)), + !(identifier == 0 && minAttestations > 0), "Invalid privacy inputs: Can't require attestations if no identifier" ); @@ -167,7 +169,9 @@ contract Escrow is EscrowedPayment memory payment = escrowedPayments[paymentId]; require(payment.token != address(0) && payment.value > 0, "Invalid withdraw value."); - if (payment.recipientIdentifier.length > 0) { + // Due to an old bug, there may exist payments with no identifier and minAttestations > 0 + // So ensure that these fail the attestations check, as they previously would have + if (payment.minAttestations > 0) { IAttestations attestations = IAttestations(registry.getAddressFor(ATTESTATIONS_REGISTRY_ID)); (uint64 completedAttestations, ) = attestations.getAttestationStats( payment.recipientIdentifier, @@ -185,7 +189,7 @@ contract Escrow is emit Withdrawal( payment.recipientIdentifier, - payment.sender, + msg.sender, payment.token, payment.value, paymentId diff --git a/packages/protocol/test/identity/escrow.ts b/packages/protocol/test/identity/escrow.ts index 8cdbc4b3627..36c87386c62 100644 --- a/packages/protocol/test/identity/escrow.ts +++ b/packages/protocol/test/identity/escrow.ts @@ -1,6 +1,12 @@ import { NULL_ADDRESS } from '@celo/base/lib/address' +import getPhoneHash from '@celo/phone-utils/lib/getPhoneHash' import { CeloContractName } from '@celo/protocol/lib/registry-utils' -import { assertRevert, timeTravel } from '@celo/protocol/lib/test-utils' +import { + assertLogMatches2, + assertRevert, + assertRevertWithReason, + timeTravel, +} from '@celo/protocol/lib/test-utils' import { EscrowContract, EscrowInstance, @@ -13,22 +19,25 @@ import { } from 'types' import { getParsedSignatureOfAddress } from '../../lib/signing-utils' -// For reference: -// accounts[0] = owner -// accounts[1] = sender -// accounts[2] = receiver -// accounts[3] = registry -// accounts[4] = a random other account -// accounts[5] = withdrawKeyAddress (temporary wallet address, no attestations) -// accounts[6] = anotherWithdrawKeyAddress (a different temporary wallet address, requires attestations) - const Escrow: EscrowContract = artifacts.require('Escrow') const MockERC20Token: MockERC20TokenContract = artifacts.require('MockERC20Token') const Registry: RegistryContract = artifacts.require('Registry') const MockAttestations: MockAttestationsContract = artifacts.require('MockAttestations') +const NULL_BYTES32 = '0x0000000000000000000000000000000000000000000000000000000000000000' +const NULL_ESCROWED_PAYMENT: EscrowedPayment = { + recipientIdentifier: NULL_BYTES32, + sender: NULL_ADDRESS, + token: NULL_ADDRESS, + value: 0, + sentIndex: 0, + receivedIndex: 0, + timestamp: 0, + expirySeconds: 0, + minAttestations: 0, +} interface EscrowedPayment { - recipientPhoneHash: string + recipientIdentifier: string sender: string token: string value: number @@ -45,7 +54,7 @@ const getEscrowedPayment = async ( ): Promise => { const payment = await escrow.escrowedPayments(paymentID) return { - recipientPhoneHash: payment[0], + recipientIdentifier: payment[0], sender: payment[1], token: payment[2], value: payment[3].toNumber(), @@ -89,7 +98,7 @@ contract('Escrow', (accounts: string[]) => { describe('#setRegistry()', () => { const nonOwner: string = accounts[1] - const anAddress: string = accounts[3] + const anAddress: string = accounts[2] it('should allow owner to set registry', async () => { await escrow.setRegistry(anAddress) @@ -104,75 +113,195 @@ contract('Escrow', (accounts: string[]) => { describe('tests with tokens', () => { let mockERC20Token: MockERC20TokenInstance const aValue: number = 10 - const receiver: string = accounts[2] const sender: string = accounts[1] + const receiver: string = accounts[2] - // @ts-ignore - const aPhoneHash: string = web3.utils.soliditySha3({ t: 'string', v: '+18005555555' }) - const withdrawKeyAddress: string = accounts[5] - const anotherWithdrawKeyAddress: string = accounts[6] + const aPhoneHash = getPhoneHash('+18005555555') + const withdrawKeyAddress: string = accounts[3] + const anotherWithdrawKeyAddress: string = accounts[4] const oneDayInSecs: number = 86400 beforeEach(async () => { mockERC20Token = await MockERC20Token.new() - await mockERC20Token.mint(sender, aValue) }) - describe('#transfer()', () => { - // in the protocol layer, the amount of verifications is not checked. So, any account - // can get sent an escrowed payment. - it('should allow users to transfer tokens to any user', async () => { + const mintAndTransfer = async ( + escrowSender: string, + identifier: string, + value: number, + expirySeconds: number, + paymentId: string, + minAttestations: number + ) => { + await mockERC20Token.mint(escrowSender, value) + await escrow.transfer( + identifier, + mockERC20Token.address, + value, + expirySeconds, + paymentId, + minAttestations, + { + from: escrowSender, + } + ) + } + + describe('#transfer()', async () => { + const transferAndCheckState = async ( + escrowSender: string, + identifier: string, + value: number, + expirySeconds: number, + paymentId: string, + minAttestations: number, + expectedSentPaymentIds: string[], + expectedReceivedPaymentIds: string[] + ) => { + const startingEscrowContractBalance = ( + await mockERC20Token.balanceOf(escrow.address) + ).toNumber() + const startingSenderBalance = (await mockERC20Token.balanceOf(escrowSender)).toNumber() + await escrow.transfer( - aPhoneHash, + identifier, mockERC20Token.address, - aValue, - oneDayInSecs, - withdrawKeyAddress, - 0, - { - from: sender, - } + value, + expirySeconds, + paymentId, + minAttestations, + { from: escrowSender } + ) + const escrowedPayment = await getEscrowedPayment(paymentId, escrow) + assert.equal( + escrowedPayment.value, + value, + 'incorrect escrowedPayment.value in payment struct' ) - const uniquePaymentID: string = withdrawKeyAddress - - const escrowedPayment = await getEscrowedPayment(uniquePaymentID, escrow) - - const received = await escrow.receivedPaymentIds(aPhoneHash, escrowedPayment.receivedIndex) + assert.equal( + (await mockERC20Token.balanceOf(escrowSender)).toNumber(), + startingSenderBalance - value, + 'incorrect final sender balance' + ) + assert.equal( + (await mockERC20Token.balanceOf(escrow.address)).toNumber(), + startingEscrowContractBalance + value, + 'incorrect final Escrow contract balance' + ) + // Check against expected receivedPaymentIds and sentPaymentIds, + // and corresponding indices in the payment struct + const receivedPaymentIds = await escrow.getReceivedPaymentIds(identifier) + assert.deepEqual( + receivedPaymentIds, + expectedReceivedPaymentIds, + 'unexpected receivedPaymentIds' + ) assert.equal( - received, - uniquePaymentID, - "Correct Escrowed Payment ID should be stored in aPhoneHash's received payments list" + receivedPaymentIds[escrowedPayment.receivedIndex], + paymentId, + "expected paymentId not found at expected index in identifier's received payments list" ) + const sentPaymentIds = await escrow.getSentPaymentIds(escrowSender) + assert.deepEqual(sentPaymentIds, expectedSentPaymentIds, 'unexpected sentPaymentIds') + assert.equal( + sentPaymentIds[escrowedPayment.sentIndex], + paymentId, + "expected paymentId not found in escrowSender's sent payments list" + ) + } - const sent = await escrow.sentPaymentIds(sender, escrowedPayment.sentIndex) + beforeEach(async () => { + await mockERC20Token.mint(sender, aValue) + }) - assert.equal( - sent, - uniquePaymentID, - "Correct Escrowed Payment ID should be stored in sender's sent payments list" + it('should allow users to transfer tokens to any user', async () => { + await transferAndCheckState( + sender, + aPhoneHash, + aValue, + oneDayInSecs, + withdrawKeyAddress, + 0, + [withdrawKeyAddress], + [withdrawKeyAddress] ) + }) - assert.equal( - escrowedPayment.value, + it('should allow transfer when minAttestations > 0 and identifier is provided', async () => { + await transferAndCheckState( + sender, + aPhoneHash, aValue, - 'Should have correct value saved in payment struct' + oneDayInSecs, + withdrawKeyAddress, + 3, + [withdrawKeyAddress], + [withdrawKeyAddress] ) + }) - assert.equal( - (await mockERC20Token.balanceOf(receiver)).toNumber(), + it('should allow transfer when no identifier is provided', async () => { + await transferAndCheckState( + sender, + NULL_BYTES32, + aValue, + oneDayInSecs, + withdrawKeyAddress, 0, - 'Should have correct total balance for receiver' + [withdrawKeyAddress], + [withdrawKeyAddress] ) - assert.equal( - (await mockERC20Token.balanceOf(escrow.address)).toNumber(), + }) + + it('should allow transfers from same sender with different paymentIds', async () => { + await mintAndTransfer( + sender, + NULL_BYTES32, + aValue, + oneDayInSecs, + anotherWithdrawKeyAddress, + 0 + ) + await transferAndCheckState( + sender, + NULL_BYTES32, aValue, - 'Should have correct total balance for the escrow contract' + oneDayInSecs, + withdrawKeyAddress, + 0, + [anotherWithdrawKeyAddress, withdrawKeyAddress], + [anotherWithdrawKeyAddress, withdrawKeyAddress] ) }) - it('should not allow two transfers with same ID', async () => { + it('should emit the Transfer event', async () => { + const receipt = await escrow.transfer( + aPhoneHash, + mockERC20Token.address, + aValue, + oneDayInSecs, + withdrawKeyAddress, + 2, + { + from: sender, + } + ) + assertLogMatches2(receipt.logs[0], { + event: 'Transfer', + args: { + from: sender, + identifier: aPhoneHash, + token: mockERC20Token.address, + value: aValue, + paymentId: withdrawKeyAddress, + minAttestations: 2, + }, + }) + }) + + it('should not allow two transfers with same paymentId', async () => { await escrow.transfer( aPhoneHash, mockERC20Token.address, @@ -184,7 +313,7 @@ contract('Escrow', (accounts: string[]) => { from: sender, } ) - await assertRevert( + await assertRevertWithReason( escrow.transfer( aPhoneHash, mockERC20Token.address, @@ -195,7 +324,8 @@ contract('Escrow', (accounts: string[]) => { { from: sender, } - ) + ), + 'paymentId already used' ) }) @@ -232,166 +362,303 @@ contract('Escrow', (accounts: string[]) => { }) it('should not allow a transfer if identifier is empty but minAttestations is > 0', async () => { - await assertRevert( - escrow.transfer('0x0', mockERC20Token.address, aValue, 0, withdrawKeyAddress, 1, { - from: sender, - }) + await assertRevertWithReason( + escrow.transfer( + NULL_BYTES32, + mockERC20Token.address, + aValue, + oneDayInSecs, + withdrawKeyAddress, + 1, + { + from: sender, + } + ), + "Invalid privacy inputs: Can't require attestations if no identifier" ) }) }) - async function doubleTransfer() { - await mockERC20Token.mint(sender, aValue) - await escrow.transfer( - aPhoneHash, - mockERC20Token.address, - aValue, - oneDayInSecs, - withdrawKeyAddress, - 0, - { - from: sender, - } + const checkStateAfterDeletingPayment = async ( + deletedPaymentId: string, + deletedPayment: EscrowedPayment, + escrowSender: string, + identifier: string, + expectedSentPaymentIds: string[], + expectedReceivedPaymentIds: string[] + ) => { + const sentPaymentIds = await escrow.getSentPaymentIds(escrowSender) + const receivedPaymentIds = await escrow.getReceivedPaymentIds(identifier) + assert.deepEqual(sentPaymentIds, expectedSentPaymentIds, 'unexpected sentPaymentIds') + assert.deepEqual( + receivedPaymentIds, + expectedReceivedPaymentIds, + 'unexpected receivedPaymentIds' ) - await timeTravel(10, web3) - await escrow.transfer( - aPhoneHash, - mockERC20Token.address, - aValue, - oneDayInSecs, - anotherWithdrawKeyAddress, - 1, - { - from: sender, - } - ) - } - - describe('#withdraw()', () => { - let uniquePaymentIDWithdraw: string - let parsedSig1: any - let parsedSig2: any - - beforeEach(async () => { - parsedSig1 = await getParsedSignatureOfAddress(web3, accounts[2], accounts[5]) - parsedSig2 = await getParsedSignatureOfAddress(web3, accounts[2], accounts[6]) - await doubleTransfer() - uniquePaymentIDWithdraw = withdrawKeyAddress - }) - - it('should allow verified users to withdraw their escrowed tokens', async () => { - const receivedPaymentsBefore = await escrow.getReceivedPaymentIds(aPhoneHash) - const sentPaymentsBefore = await escrow.getSentPaymentIds(sender) - const paymentBefore = await getEscrowedPayment(uniquePaymentIDWithdraw, escrow) - const sendersLastEscrowedPaymentBefore = await getEscrowedPayment( - sentPaymentsBefore[sentPaymentsBefore.length - 1], + // Check that indices of last payment structs in previous lists are properly updated + if (expectedSentPaymentIds.length) { + const sendersLastPaymentAfterDelete = await getEscrowedPayment( + expectedSentPaymentIds[expectedSentPaymentIds.length - 1], escrow ) - const receiversLastEscrowedPaymentBefore = await getEscrowedPayment( - receivedPaymentsBefore[receivedPaymentsBefore.length - 1], + assert.equal( + sendersLastPaymentAfterDelete.sentIndex, + deletedPayment.sentIndex, + "sentIndex of last payment in sender's sentPaymentIds not updated properly" + ) + } + if (expectedReceivedPaymentIds.length) { + const receiversLastPaymentAfterDelete = await getEscrowedPayment( + expectedReceivedPaymentIds[expectedReceivedPaymentIds.length - 1], escrow ) + assert.equal( + receiversLastPaymentAfterDelete.receivedIndex, + deletedPayment.receivedIndex, + "receivedIndex of last payment in receiver's receivedPaymentIds not updated properly" + ) + } + const deletedEscrowedPayment = await getEscrowedPayment(deletedPaymentId, escrow) + assert.deepEqual( + deletedEscrowedPayment, + NULL_ESCROWED_PAYMENT, + 'escrowedPayment not zeroed out' + ) + } - await escrow.withdraw(uniquePaymentIDWithdraw, parsedSig1.v, parsedSig1.r, parsedSig1.s, { - from: receiver, + describe('#withdraw()', () => { + const uniquePaymentIDWithdraw = withdrawKeyAddress + + const withdrawAndCheckState = async ( + escrowSender: string, + escrowReceiver: string, + identifier: string, + value: number, + paymentId: string, + attestationsToComplete: number, + expectedSentPaymentIds: string[], + expectedReceivedPaymentIds: string[] + ) => { + const receiverBalanceBefore = (await mockERC20Token.balanceOf(escrowReceiver)).toNumber() + const escrowContractBalanceBefore = ( + await mockERC20Token.balanceOf(escrow.address) + ).toNumber() + const paymentBefore = await getEscrowedPayment(paymentId, escrow) + + // Mock completed attestations + for (let i = 0; i < attestationsToComplete; i++) { + await mockAttestations.complete(identifier, 0, NULL_BYTES32, NULL_BYTES32, { + from: escrowReceiver, + }) + } + const parsedSig = await getParsedSignatureOfAddress(web3, escrowReceiver, paymentId) + await escrow.withdraw(paymentId, parsedSig.v, parsedSig.r, parsedSig.s, { + from: escrowReceiver, }) - - const receivedPaymentsAfterWithdraw = await escrow.getReceivedPaymentIds(aPhoneHash) - const sentPaymentsAfterWithdraw = await escrow.getSentPaymentIds(sender) - const sendersLastPaymentAfterWithdraw = await getEscrowedPayment( - sentPaymentsAfterWithdraw[sentPaymentsAfterWithdraw.length - 1], - escrow - ) - const receiversLastPaymentAfterWithdraw = await getEscrowedPayment( - receivedPaymentsAfterWithdraw[receivedPaymentsAfterWithdraw.length - 1], - escrow - ) - assert.equal( - (await mockERC20Token.balanceOf(receiver)).toNumber(), - aValue, - 'Should have correct total balance for receiver' + (await mockERC20Token.balanceOf(escrowReceiver)).toNumber(), + receiverBalanceBefore + value, + 'incorrect final receiver balance' ) assert.equal( (await mockERC20Token.balanceOf(escrow.address)).toNumber(), - aValue, - 'Should have correct total balance for the escrow contract' - ) - - assert.include( - receivedPaymentsBefore, - uniquePaymentIDWithdraw, - "Should have saved this escrowed payment in receiver's receivedPaymentIds list after transfer" - ) - - assert.notInclude( - receivedPaymentsAfterWithdraw, - uniquePaymentIDWithdraw, - "Should have deleted this escrowed payment from receiver's receivedPaymentIds list after withdraw" - ) - - assert.include( - sentPaymentsBefore, - uniquePaymentIDWithdraw, - "Should have saved this escrowed payment in sender's sentPaymentIds list after transfer" - ) - - assert.notInclude( - sentPaymentsAfterWithdraw, - uniquePaymentIDWithdraw, - "Should have deleted this escrowed payment from sender's sentPaymentIds list after withdraw" - ) - - assert.notEqual( - sendersLastEscrowedPaymentBefore.sentIndex, - paymentBefore.sentIndex, - "This escrowed payments sentIndex should be different from that of sender's sentPaymentIds last payment before withdraw" - ) - - assert.notEqual( - receiversLastEscrowedPaymentBefore.receivedIndex, - paymentBefore.receivedIndex, - "This escrowed payments receivedIndex should be different from that of receiver's receivedPaymentIds last payment before withdraw" - ) + escrowContractBalanceBefore - value, + 'incorrect final Escrow contract balance' + ) + + await checkStateAfterDeletingPayment( + paymentId, + paymentBefore, + escrowSender, + identifier, + expectedSentPaymentIds, + expectedReceivedPaymentIds + ) + } + + describe('when no payment has been escrowed', () => { + it('should fail to withdraw funds', async () => { + const parsedSig = await getParsedSignatureOfAddress( + web3, + receiver, + uniquePaymentIDWithdraw + ) + await assertRevertWithReason( + escrow.withdraw(uniquePaymentIDWithdraw, parsedSig.v, parsedSig.r, parsedSig.s, { + from: receiver, + }), + 'Invalid withdraw value.' + ) + }) + }) - assert.equal( - sendersLastPaymentAfterWithdraw.sentIndex, - paymentBefore.sentIndex, - "Should have changed sentIndex for this escrowed payment from sender's sentPaymentIds list after withdraw" - ) + describe('when first payment from sender is escrowed without an identifier', () => { + beforeEach(async () => { + await mintAndTransfer( + sender, + NULL_BYTES32, + aValue, + oneDayInSecs, + uniquePaymentIDWithdraw, + 0 + ) + }) - assert.equal( - receiversLastPaymentAfterWithdraw.receivedIndex, - paymentBefore.receivedIndex, - "Should have changed receivedIndex for this escrowed payment from receiver's receivedPaymentIds list after withdraw" - ) - }) + it('should allow withdrawal with possession of PK and no attestations', async () => { + await withdrawAndCheckState( + sender, + receiver, + NULL_BYTES32, + aValue, + uniquePaymentIDWithdraw, + 0, + [], + [] + ) + }) - it('should not allow a user who does not prove ownership of the withdraw key to withdraw tokens', async () => { - // The signature is invalidated if it's sent from a different address - await assertRevert( - escrow.withdraw(uniquePaymentIDWithdraw, parsedSig1.v, parsedSig1.r, parsedSig1.s, { - from: accounts[3], + it('should emit the Withdrawal event', async () => { + const parsedSig = await getParsedSignatureOfAddress( + web3, + receiver, + uniquePaymentIDWithdraw + ) + const receipt = await escrow.withdraw( + uniquePaymentIDWithdraw, + parsedSig.v, + parsedSig.r, + parsedSig.s, + { from: receiver } + ) + assertLogMatches2(receipt.logs[0], { + event: 'Withdrawal', + args: { + identifier: NULL_BYTES32, + to: receiver, + token: mockERC20Token.address, + value: aValue, + paymentId: uniquePaymentIDWithdraw, + }, }) - ) - }) + }) - it('should not allow sender to use withdraw function even if payment has expired', async () => { - await timeTravel(oneDayInSecs, web3) - await assertRevert( - escrow.withdraw(uniquePaymentIDWithdraw, parsedSig1.v, parsedSig1.r, parsedSig1.s, { - from: sender, - }) - ) + it('should withdraw properly when second payment escrowed with empty identifier', async () => { + await mintAndTransfer( + sender, + NULL_BYTES32, + aValue, + oneDayInSecs, + anotherWithdrawKeyAddress, + 0 + ) + await withdrawAndCheckState( + sender, + receiver, + NULL_BYTES32, + aValue, + uniquePaymentIDWithdraw, + 0, + [anotherWithdrawKeyAddress], + [anotherWithdrawKeyAddress] + ) + }) + it("should withdraw properly when sender's second payment has an identifier with attestations", async () => { + await mintAndTransfer( + sender, + aPhoneHash, + aValue, + oneDayInSecs, + anotherWithdrawKeyAddress, + 3 + ) + await withdrawAndCheckState( + sender, + receiver, + NULL_BYTES32, + aValue, + uniquePaymentIDWithdraw, + 0, + [anotherWithdrawKeyAddress], + [] + ) + }) + it('should not allow withdrawing without a valid signature using the withdraw key', async () => { + // The signature is invalidated if it's sent from a different address + const parsedSig = await getParsedSignatureOfAddress( + web3, + receiver, + uniquePaymentIDWithdraw + ) + await assertRevertWithReason( + escrow.withdraw(uniquePaymentIDWithdraw, parsedSig.v, parsedSig.r, parsedSig.s, { + from: sender, + }), + 'Failed to prove ownership of the withdraw key' + ) + }) }) - it('should not allow a user to withdraw a payment if they have fewer than minAttestations', async () => { - await assertRevert( - escrow.withdraw(anotherWithdrawKeyAddress, parsedSig2.v, parsedSig2.r, parsedSig2.s, { - from: receiver, - }) - ) + describe('when first payment is escrowed by a sender for an identifier && minAttestations', () => { + const minAttestations = 3 + beforeEach(async () => { + await mintAndTransfer( + sender, + aPhoneHash, + aValue, + oneDayInSecs, + uniquePaymentIDWithdraw, + minAttestations + ) + }) + + it('should allow users to withdraw after completing attestations', async () => { + await withdrawAndCheckState( + sender, + receiver, + aPhoneHash, + aValue, + uniquePaymentIDWithdraw, + minAttestations, + [], + [] + ) + }) + it('should not allow a user to withdraw a payment if they have fewer than minAttestations', async () => { + await assertRevertWithReason( + withdrawAndCheckState( + sender, + receiver, + aPhoneHash, + aValue, + uniquePaymentIDWithdraw, + minAttestations - 1, + [], + [] + ), + 'This account does not have enough attestations to withdraw this payment.' + ) + }) + it("should withdraw properly when sender's second payment has an identifier", async () => { + await mintAndTransfer( + sender, + aPhoneHash, + aValue, + oneDayInSecs, + anotherWithdrawKeyAddress, + 0 + ) + await withdrawAndCheckState( + sender, + receiver, + aPhoneHash, + aValue, + uniquePaymentIDWithdraw, + minAttestations, + [anotherWithdrawKeyAddress], + [anotherWithdrawKeyAddress] + ) + }) }) }) @@ -399,71 +666,105 @@ contract('Escrow', (accounts: string[]) => { let uniquePaymentIDRevoke: string let parsedSig1: any - beforeEach(async () => { - await doubleTransfer() - uniquePaymentIDRevoke = withdrawKeyAddress - parsedSig1 = await getParsedSignatureOfAddress(web3, accounts[2], accounts[5]) - }) - - it('should allow sender to redeem payment after payment has expired', async () => { - await timeTravel(oneDayInSecs, web3) - const escrowedPaymentBefore = await getEscrowedPayment(uniquePaymentIDRevoke, escrow) - - await escrow.revoke(uniquePaymentIDRevoke, { from: sender }) - - const sentPaymentsAfterRevoke = await escrow.getSentPaymentIds(sender) - const receivedPaymentsAfterRevoke = await escrow.getReceivedPaymentIds(aPhoneHash) - - assert.notInclude( - sentPaymentsAfterRevoke, - uniquePaymentIDRevoke, - "Should have deleted this escrowed payment from sender's sentPaymentIds list after revoke" - ) - - assert.notInclude( - receivedPaymentsAfterRevoke, - uniquePaymentIDRevoke, - "Should have deleted this escrowed payment from receiver's receivedPaymentIds list after revoke" - ) - - const escrowedPaymentAfter = await getEscrowedPayment(uniquePaymentIDRevoke, escrow) + const identifiers = [NULL_BYTES32, aPhoneHash] + identifiers.forEach((identifier) => { + describe(`when identifier is ${ + identifier === NULL_BYTES32 ? '' : 'not' + } empty`, async () => { + beforeEach(async () => { + await mintAndTransfer(sender, identifier, aValue, oneDayInSecs, withdrawKeyAddress, 0) + await mintAndTransfer( + sender, + identifier, + aValue, + oneDayInSecs, + anotherWithdrawKeyAddress, + 0 + ) + + uniquePaymentIDRevoke = withdrawKeyAddress + parsedSig1 = await getParsedSignatureOfAddress(web3, receiver, withdrawKeyAddress) + }) - assert.notEqual( - escrowedPaymentBefore, - escrowedPaymentAfter, - 'Should have zeroed this escrowed payments values after revoke' - ) - }) + it('should allow sender to redeem payment after payment has expired', async () => { + await timeTravel(oneDayInSecs, web3) + + const senderBalanceBefore = (await mockERC20Token.balanceOf(sender)).toNumber() + const escrowContractBalanceBefore = ( + await mockERC20Token.balanceOf(escrow.address) + ).toNumber() + const paymentBefore = await getEscrowedPayment(uniquePaymentIDRevoke, escrow) + + await escrow.revoke(uniquePaymentIDRevoke, { from: sender }) + + assert.equal( + (await mockERC20Token.balanceOf(sender)).toNumber(), + senderBalanceBefore + aValue, + 'incorrect final sender balance' + ) + assert.equal( + (await mockERC20Token.balanceOf(escrow.address)).toNumber(), + escrowContractBalanceBefore - aValue, + 'incorrect final Escrow contract balance' + ) + + await checkStateAfterDeletingPayment( + uniquePaymentIDRevoke, + paymentBefore, + sender, + identifier, + [anotherWithdrawKeyAddress], + [anotherWithdrawKeyAddress] + ) + }) - it('should not allow sender to revoke payment after receiver withdraws', async () => { - await escrow.withdraw(uniquePaymentIDRevoke, parsedSig1.v, parsedSig1.r, parsedSig1.s, { - from: receiver, - }) - await assertRevert(escrow.revoke(uniquePaymentIDRevoke, { from: sender })) - }) + it('should emit the Revocation event', async () => { + await timeTravel(oneDayInSecs, web3) + const receipt = await escrow.revoke(uniquePaymentIDRevoke, { from: sender }) + assertLogMatches2(receipt.logs[0], { + event: 'Revocation', + args: { + identifier, + by: sender, + token: mockERC20Token.address, + value: aValue, + paymentId: withdrawKeyAddress, + }, + }) + }) - it('should not allow receiver to redeem payment after sender revokes it', async () => { - await timeTravel(oneDayInSecs, web3) - await escrow.revoke(uniquePaymentIDRevoke, { from: sender }) - await assertRevert( - escrow.withdraw(uniquePaymentIDRevoke, parsedSig1.v, parsedSig1.r, parsedSig1.s, { - from: receiver, + it('should not allow sender to revoke payment after receiver withdraws', async () => { + await escrow.withdraw(uniquePaymentIDRevoke, parsedSig1.v, parsedSig1.r, parsedSig1.s, { + from: receiver, + }) + await assertRevert(escrow.revoke(uniquePaymentIDRevoke, { from: sender })) }) - ) - }) - it('should not allow sender to revoke payment before payment has expired', async () => { - await assertRevert(escrow.revoke(uniquePaymentIDRevoke, { from: sender })) - }) + it('should not allow receiver to redeem payment after sender revokes it', async () => { + await timeTravel(oneDayInSecs, web3) + await escrow.revoke(uniquePaymentIDRevoke, { from: sender }) + await assertRevert( + escrow.withdraw(uniquePaymentIDRevoke, parsedSig1.v, parsedSig1.r, parsedSig1.s, { + from: receiver, + }) + ) + }) - it('should not allow receiver to use revoke function', async () => { - await timeTravel(oneDayInSecs, web3) - await assertRevert(escrow.revoke(uniquePaymentIDRevoke, { from: receiver })) - }) + it('should not allow sender to revoke payment before payment has expired', async () => { + await assertRevertWithReason( + escrow.revoke(uniquePaymentIDRevoke, { from: sender }), + 'Transaction not redeemable for sender yet.' + ) + }) - it('should not allow any account who is not the sender to use revoke function', async () => { - await timeTravel(oneDayInSecs, web3) - await assertRevert(escrow.revoke(uniquePaymentIDRevoke, { from: accounts[4] })) + it('should not allow receiver to use revoke function', async () => { + await timeTravel(oneDayInSecs, web3) + await assertRevertWithReason( + escrow.revoke(uniquePaymentIDRevoke, { from: receiver }), + 'Only sender of payment can attempt to revoke payment.' + ) + }) + }) }) }) }) diff --git a/packages/protocol/test/identity/federatedattestations.ts b/packages/protocol/test/identity/federatedattestations.ts index fec7f516114..b877b5f5a4b 100644 --- a/packages/protocol/test/identity/federatedattestations.ts +++ b/packages/protocol/test/identity/federatedattestations.ts @@ -681,7 +681,7 @@ contract('FederatedAttestations', (accounts: string[]) => { ) }) - it('should modify identifierToAddresses and addresstoIdentifiers accordingly', async () => { + it('should modify identifierToAttestations and addresstoIdentifiers accordingly', async () => { await assertAttestationNotInStorage(identifier1, issuer1, account1, 0, 0) await signAndRegisterAttestation(identifier1, issuer1, account1, nowUnixTime, signer1) await assertAttestationInStorage(identifier1, issuer1, 0, account1, nowUnixTime, signer1, 0) @@ -702,7 +702,7 @@ contract('FederatedAttestations', (accounts: string[]) => { ) }) - it('should modify identifierToAddresses and addresstoIdentifiers accordingly', async () => { + it('should modify identifierToAttestations and addresstoIdentifiers accordingly', async () => { const account2 = accounts[3] await assertAttestationInStorage(identifier1, issuer1, 0, account1, nowUnixTime, signer1, 0) await assertAttestationNotInStorage(identifier1, issuer1, account2, 1, 0) diff --git a/packages/sdk/utils/src/sign-typed-data-utils.ts b/packages/sdk/utils/src/sign-typed-data-utils.ts index 9e7bd209ba8..52c155543b4 100644 --- a/packages/sdk/utils/src/sign-typed-data-utils.ts +++ b/packages/sdk/utils/src/sign-typed-data-utils.ts @@ -155,7 +155,6 @@ function findDependencies(primaryType: string, types: EIP712Types, found: string // If this is not a builtin and is not defined, we cannot correctly construct a type encoding. if (types[primaryType] === undefined) { - console.log(types) throw new Error(`Unrecognized type ${primaryType} is not included in the EIP-712 type list`) } From e48ff7e7c59143dedaca569c1c51d4cfd5e990fa Mon Sep 17 00:00:00 2001 From: isabellewei Date: Wed, 15 Jun 2022 18:31:49 -0400 Subject: [PATCH 11/30] Modify lookup functions to include publishedOn (#9634) * modify lookup functions to include publishedOn * more tests * lint fix * PR comments * fix lint * check publishedOn in lookup tests * stricter publishedOn tests --- .../identity/FederatedAttestations.sol | 51 +-- .../test/identity/federatedattestations.ts | 327 +++++++++++------- 2 files changed, 224 insertions(+), 154 deletions(-) diff --git a/packages/protocol/contracts/identity/FederatedAttestations.sol b/packages/protocol/contracts/identity/FederatedAttestations.sol index ce6ca01bbc4..6be91e6530f 100644 --- a/packages/protocol/contracts/identity/FederatedAttestations.sol +++ b/packages/protocol/contracts/identity/FederatedAttestations.sol @@ -145,58 +145,60 @@ contract FederatedAttestations is * @param identifier Hash of the identifier * @param trustedIssuers Array of n issuers whose attestations will be included * @return [0] Array of number of attestations returned per issuer - * @return [1 - 3] for m (== sum([0])) found attestations: + * @return [1 - 4] for m (== sum([0])) found attestations: * [ * Array of m accounts, + * Array of m signers, * Array of m issuedOns, - * Array of m signers + * Array of m publishedOns * ]; index corresponds to the same attestation * @dev Adds attestation info to the arrays in order of provided trustedIssuers * @dev Expectation that only one attestation exists per (identifier, issuer, account) */ // TODO reviewers: is it preferable to return an array of `trustedIssuer` indices // (indicating issuer per attestation) instead of counts per attestation? - // TODO: change issuedOn type, change the order of return values to match across the file, - // add publishedOn to returned lookups function lookupAttestations(bytes32 identifier, address[] calldata trustedIssuers) external view - returns (uint256[] memory, address[] memory, uint256[] memory, address[] memory) + returns (uint256[] memory, address[] memory, address[] memory, uint64[] memory, uint64[] memory) { // TODO reviewers: this is to get around a stack too deep error; // are there better ways of dealing with this? - return _lookupAttestations(identifier, trustedIssuers); + uint256[] memory countsPerIssuer; + uint256 totalAttestations; + (totalAttestations, countsPerIssuer) = getNumAttestations(identifier, trustedIssuers); + // solhint-disable-next-line max-line-length + (address[] memory accounts, address[] memory signers, uint64[] memory issuedOns, uint64[] memory publishedOns) = _lookupAttestations( + identifier, + trustedIssuers, + totalAttestations + ); + return (countsPerIssuer, accounts, signers, issuedOns, publishedOns); } /** * @notice Helper function for lookupAttestations to get around stack too deep * @param identifier Hash of the identifier * @param trustedIssuers Array of n issuers whose attestations will be included - * @return [0] Array of number of attestations returned per issuer - * @return [1 - 3] for m (== sum([0])) found attestations: + * @return [0 - 3] for m (== sum([0])) found attestations: * [ * Array of m accounts, + * Array of m signers, * Array of m issuedOns, - * Array of m signers + * Array of m publishedOns * ]; index corresponds to the same attestation * @dev Adds attestation info to the arrays in order of provided trustedIssuers * @dev Expectation that only one attestation exists per (identifier, issuer, account) */ - // TODO: change issuedOn type, change the order of return values to match across the file, - // add publishedOn to returned lookups - function _lookupAttestations(bytes32 identifier, address[] memory trustedIssuers) - internal - view - returns (uint256[] memory, address[] memory, uint256[] memory, address[] memory) - { - uint256 totalAttestations; - uint256[] memory countsPerIssuer; - - (totalAttestations, countsPerIssuer) = getNumAttestations(identifier, trustedIssuers); - + function _lookupAttestations( + bytes32 identifier, + address[] memory trustedIssuers, + uint256 totalAttestations + ) internal view returns (address[] memory, address[] memory, uint64[] memory, uint64[] memory) { address[] memory accounts = new address[](totalAttestations); - uint256[] memory issuedOns = new uint256[](totalAttestations); address[] memory signers = new address[](totalAttestations); + uint64[] memory issuedOns = new uint64[](totalAttestations); + uint64[] memory publishedOns = new uint64[](totalAttestations); OwnershipAttestation[] memory attestationsPerIssuer; // Reset this and use as current index to get around stack-too-deep @@ -208,12 +210,13 @@ contract FederatedAttestations is attestationsPerIssuer = identifierToAttestations[identifier][trustedIssuers[i]]; for (uint256 j = 0; j < attestationsPerIssuer.length; j = j.add(1)) { accounts[totalAttestations] = attestationsPerIssuer[j].account; - issuedOns[totalAttestations] = attestationsPerIssuer[j].issuedOn; signers[totalAttestations] = attestationsPerIssuer[j].signer; + issuedOns[totalAttestations] = attestationsPerIssuer[j].issuedOn; + publishedOns[totalAttestations] = attestationsPerIssuer[j].publishedOn; totalAttestations = totalAttestations.add(1); } } - return (countsPerIssuer, accounts, issuedOns, signers); + return (accounts, signers, issuedOns, publishedOns); } /** diff --git a/packages/protocol/test/identity/federatedattestations.ts b/packages/protocol/test/identity/federatedattestations.ts index b877b5f5a4b..a35873c878e 100644 --- a/packages/protocol/test/identity/federatedattestations.ts +++ b/packages/protocol/test/identity/federatedattestations.ts @@ -194,45 +194,59 @@ contract('FederatedAttestations', (accounts: string[]) => { }) }) - describe('looking up attestations', () => { + describe('#lookupAttestations', () => { interface AttestationTestCase { account: string - issuedOn: number signer: string + issuedOn: number } const checkAgainstExpectedAttestations = ( expectedCountsPerIssuer: number[], expectedAttestations: AttestationTestCase[], + expectedPublishedOn: number, actualCountsPerIssuer: BigNumber[], actualAddresses: string[], + actualSigners: string[], actualIssuedOns: BigNumber[], - actualSigners: string[] + actualPublishedOns: BigNumber[] ) => { + // purposefully not checking publishedOn, as it is set onchain expect(actualCountsPerIssuer.map((count) => count.toNumber())).to.eql(expectedCountsPerIssuer) assert.lengthOf(actualAddresses, expectedAttestations.length) - assert.lengthOf(actualIssuedOns, expectedAttestations.length) assert.lengthOf(actualSigners, expectedAttestations.length) + assert.lengthOf(actualIssuedOns, expectedAttestations.length) + assert.lengthOf(actualPublishedOns, expectedAttestations.length) expectedAttestations.forEach((expectedAttestation, index) => { assert.equal(actualAddresses[index], expectedAttestation.account) - assert.equal(actualIssuedOns[index].toNumber(), expectedAttestation.issuedOn) assert.equal(actualSigners[index], expectedAttestation.signer) + assert.equal(actualIssuedOns[index].toNumber(), expectedAttestation.issuedOn) + assert.isAtLeast(actualPublishedOns[index].toNumber(), actualIssuedOns[index].toNumber()) + assert.isAtMost(actualPublishedOns[index].toNumber(), expectedPublishedOn + 10) }) } describe('when identifier has not been registered', () => { - describe('#lookupAttestations', () => { - it('should return empty list', async () => { - const [ - countsPerIssuer, - addresses, - issuedOns, - signers, - ] = await federatedAttestations.lookupAttestations(identifier1, [issuer1]) - checkAgainstExpectedAttestations([0], [], countsPerIssuer, addresses, issuedOns, signers) - }) + it('should return empty list', async () => { + const [ + countsPerIssuer, + addresses, + signers, + issuedOns, + publishedOns, + ] = await federatedAttestations.lookupAttestations(identifier1, [issuer1]) + checkAgainstExpectedAttestations( + [0], + [], + 0, + countsPerIssuer, + addresses, + signers, + issuedOns, + publishedOns + ) }) }) describe('when identifier has been registered', () => { @@ -246,31 +260,33 @@ contract('FederatedAttestations', (accounts: string[]) => { const issuer1Attestations: AttestationTestCase[] = [ { account: account1, - issuedOn: nowUnixTime, signer: signer1, + issuedOn: nowUnixTime, }, // Same issuer as [0], different account { account: account2, - issuedOn: nowUnixTime, signer: signer1, + issuedOn: nowUnixTime, }, ] const issuer2Attestations: AttestationTestCase[] = [ // Same account as issuer1Attestations[0], different issuer { account: account1, - issuedOn: nowUnixTime, signer: issuer2Signer, + issuedOn: nowUnixTime, }, // Different account and signer { account: account2, - issuedOn: nowUnixTime, signer: issuer2Signer2, + issuedOn: nowUnixTime, }, ] + let expectedPublishedOn + beforeEach(async () => { // Require consistent order for test cases await accountsInstance.createAccount({ from: issuer2 }) @@ -288,77 +304,119 @@ contract('FederatedAttestations', (accounts: string[]) => { ) } } + expectedPublishedOn = Math.floor(Date.now() / 1000) }) - describe('#lookupAttestations', () => { - it('should return empty count and list if no issuers specified', async () => { - const [ - countsPerIssuer, - addresses, - issuedOns, - signers, - ] = await federatedAttestations.lookupAttestations(identifier1, []) - checkAgainstExpectedAttestations([], [], countsPerIssuer, addresses, issuedOns, signers) - }) + it('should return empty count and list if no issuers specified', async () => { + const [ + countsPerIssuer, + addresses, + signers, + issuedOns, + publishedOns, + ] = await federatedAttestations.lookupAttestations(identifier1, []) + checkAgainstExpectedAttestations( + [], + [], + 0, + countsPerIssuer, + addresses, + signers, + issuedOns, + publishedOns + ) + }) - it('should return all attestations from one issuer', async () => { - const [ - countsPerIssuer, - addresses, - issuedOns, - signers, - ] = await federatedAttestations.lookupAttestations(identifier1, [issuer1]) - checkAgainstExpectedAttestations( - [issuer1Attestations.length], - issuer1Attestations, - countsPerIssuer, - addresses, - issuedOns, - signers - ) - }) + it('should return all attestations from one issuer', async () => { + const [ + countsPerIssuer, + addresses, + signers, + issuedOns, + publishedOns, + ] = await federatedAttestations.lookupAttestations(identifier1, [issuer1]) + checkAgainstExpectedAttestations( + [issuer1Attestations.length], + issuer1Attestations, + expectedPublishedOn, + countsPerIssuer, + addresses, + signers, + issuedOns, + publishedOns + ) + }) - it('should return empty list if no attestations exist for an issuer', async () => { - const [ - countsPerIssuer, - addresses, - issuedOns, - signers, - ] = await federatedAttestations.lookupAttestations(identifier1, [issuer3]) - checkAgainstExpectedAttestations([0], [], countsPerIssuer, addresses, issuedOns, signers) - }) + it('should return empty list if no attestations exist for an issuer', async () => { + const [ + countsPerIssuer, + addresses, + signers, + issuedOns, + publishedOns, + ] = await federatedAttestations.lookupAttestations(identifier1, [issuer3]) + checkAgainstExpectedAttestations( + [0], + [], + 0, + countsPerIssuer, + addresses, + signers, + issuedOns, + publishedOns + ) + }) - it('should return attestations from multiple issuers in correct order', async () => { - const expectedAttestations = issuer2Attestations.concat(issuer1Attestations) - const expectedCountsPerIssuer = [ - 0, - issuer2Attestations.length, - issuer1Attestations.length, - ] - const [ - countsPerIssuer, - addresses, - issuedOns, - signers, - ] = await federatedAttestations.lookupAttestations(identifier1, [ - issuer3, - issuer2, - issuer1, - ]) - checkAgainstExpectedAttestations( - expectedCountsPerIssuer, - expectedAttestations, - countsPerIssuer, - addresses, - issuedOns, - signers - ) - }) + it('should return attestations from multiple issuers in correct order', async () => { + const expectedAttestations = issuer2Attestations.concat(issuer1Attestations) + const expectedCountsPerIssuer = [0, issuer2Attestations.length, issuer1Attestations.length] + const [ + countsPerIssuer, + addresses, + signers, + issuedOns, + publishedOns, + ] = await federatedAttestations.lookupAttestations(identifier1, [issuer3, issuer2, issuer1]) + checkAgainstExpectedAttestations( + expectedCountsPerIssuer, + expectedAttestations, + expectedPublishedOn, + countsPerIssuer, + addresses, + signers, + issuedOns, + publishedOns + ) + }) + }) + describe('when identifier has been registered and then revoked', () => { + beforeEach(async () => { + await signAndRegisterAttestation(identifier1, issuer1, account1, nowUnixTime, signer1) + await federatedAttestations.revokeAttestation(identifier1, issuer1, account1) + }) + it('should return empty list', async () => { + const [ + countsPerIssuer, + addresses, + signers, + issuedOns, + publishedOns, + ] = await federatedAttestations.lookupAttestations(identifier1, [issuer1]) + checkAgainstExpectedAttestations( + [0], + [], + 0, + countsPerIssuer, + addresses, + signers, + issuedOns, + publishedOns + ) }) }) }) - describe('looking up identifiers', () => { + describe('#lookupIdentifiers', () => { interface IdentifierTestCase { identifier: string signer: string @@ -375,14 +433,12 @@ contract('FederatedAttestations', (accounts: string[]) => { } describe('when address has not been registered', () => { - describe('#lookupIdentifiers', () => { - it('should return empty list', async () => { - const [ - actualCountsPerIssuer, - actualIdentifiers, - ] = await federatedAttestations.lookupIdentifiers(account1, [issuer1]) - checkAgainstExpectedIdCases([0], [], actualCountsPerIssuer, actualIdentifiers) - }) + it('should return empty list', async () => { + const [ + actualCountsPerIssuer, + actualIdentifiers, + ] = await federatedAttestations.lookupIdentifiers(account1, [issuer1]) + checkAgainstExpectedIdCases([0], [], actualCountsPerIssuer, actualIdentifiers) }) }) @@ -432,50 +488,61 @@ contract('FederatedAttestations', (accounts: string[]) => { } }) - describe('#lookupIdentifiers', () => { - it('should return empty count if no issuers specified', async () => { - const [ - actualCountsPerIssuer, - actualIdentifiers, - ] = await federatedAttestations.lookupIdentifiers(account1, []) - checkAgainstExpectedIdCases([], [], actualCountsPerIssuer, actualIdentifiers) - }) + it('should return empty count if no issuers specified', async () => { + const [ + actualCountsPerIssuer, + actualIdentifiers, + ] = await federatedAttestations.lookupIdentifiers(account1, []) + checkAgainstExpectedIdCases([], [], actualCountsPerIssuer, actualIdentifiers) + }) - it('should return all identifiers from one issuer', async () => { - const [ - actualCountsPerIssuer, - actualIdentifiers, - ] = await federatedAttestations.lookupIdentifiers(account1, [issuer1]) - checkAgainstExpectedIdCases( - [issuer1IdCases.length], - issuer1IdCases, - actualCountsPerIssuer, - actualIdentifiers - ) - }) + it('should return all identifiers from one issuer', async () => { + const [ + actualCountsPerIssuer, + actualIdentifiers, + ] = await federatedAttestations.lookupIdentifiers(account1, [issuer1]) + checkAgainstExpectedIdCases( + [issuer1IdCases.length], + issuer1IdCases, + actualCountsPerIssuer, + actualIdentifiers + ) + }) - it('should return empty list if no identifiers exist for an (issuer,address)', async () => { - const [ - actualCountsPerIssuer, - actualIdentifiers, - ] = await federatedAttestations.lookupIdentifiers(account1, [issuer3]) - checkAgainstExpectedIdCases([0], [], actualCountsPerIssuer, actualIdentifiers) - }) + it('should return empty list if no identifiers exist for an (issuer,address)', async () => { + const [ + actualCountsPerIssuer, + actualIdentifiers, + ] = await federatedAttestations.lookupIdentifiers(account1, [issuer3]) + checkAgainstExpectedIdCases([0], [], actualCountsPerIssuer, actualIdentifiers) + }) - it('should return identifiers from multiple issuers in correct order', async () => { - const expectedIdCases = issuer2IdCases.concat(issuer1IdCases) - const expectedCountsPerIssuer = [0, issuer2IdCases.length, issuer1IdCases.length] - const [ - actualCountsPerIssuer, - actualIdentifiers, - ] = await federatedAttestations.lookupIdentifiers(account1, [issuer3, issuer2, issuer1]) - checkAgainstExpectedIdCases( - expectedCountsPerIssuer, - expectedIdCases, - actualCountsPerIssuer, - actualIdentifiers - ) - }) + it('should return identifiers from multiple issuers in correct order', async () => { + const expectedIdCases = issuer2IdCases.concat(issuer1IdCases) + const expectedCountsPerIssuer = [0, issuer2IdCases.length, issuer1IdCases.length] + const [ + actualCountsPerIssuer, + actualIdentifiers, + ] = await federatedAttestations.lookupIdentifiers(account1, [issuer3, issuer2, issuer1]) + checkAgainstExpectedIdCases( + expectedCountsPerIssuer, + expectedIdCases, + actualCountsPerIssuer, + actualIdentifiers + ) + }) + }) + describe('when identifier has been registered and then revoked', () => { + beforeEach(async () => { + await signAndRegisterAttestation(identifier1, issuer1, account1, nowUnixTime, signer1) + await federatedAttestations.revokeAttestation(identifier1, issuer1, account1) + }) + it('should return empty list', async () => { + const [ + actualCountsPerIssuer, + actualIdentifiers, + ] = await federatedAttestations.lookupIdentifiers(account1, [issuer1]) + checkAgainstExpectedIdCases([0], [], actualCountsPerIssuer, actualIdentifiers) }) }) }) From a9c006b3618a0ce268f70dd8be072c18d9506fd8 Mon Sep 17 00:00:00 2001 From: Eela Nagaraj <7308464+eelanagaraj@users.noreply.github.com> Date: Thu, 16 Jun 2022 12:47:08 +0200 Subject: [PATCH 12/30] Add batch revocation functionality (#9647) * First set of benchmarks * Revoke and batchRevoke optimizations * Add tests and make small fixes in response * Clean up TODOs and benchmarking * Fix lint * Add signer deregistration case for registration * Address PR comments * Reorder comment after merge --- .../identity/FederatedAttestations.sol | 157 +++++--- .../test/identity/federatedattestations.ts | 341 +++++++++++++++--- 2 files changed, 403 insertions(+), 95 deletions(-) diff --git a/packages/protocol/contracts/identity/FederatedAttestations.sol b/packages/protocol/contracts/identity/FederatedAttestations.sol index 6be91e6530f..a861567189f 100644 --- a/packages/protocol/contracts/identity/FederatedAttestations.sol +++ b/packages/protocol/contracts/identity/FederatedAttestations.sol @@ -366,19 +366,18 @@ contract FederatedAttestations is * @param issuer Address of the attestation issuer * @param account Address of the account being mapped to the identifier * @param issuedOn Time at which the issuer issued the attestation in Unix time - * @param signer Address of the signer of the attestation + * @dev Attestation signer in storage is set to issuer * @dev Throws if an attestation with the same (identifier, issuer, account) already exists */ function registerAttestationAsIssuer( bytes32 identifier, address issuer, address account, - address signer, uint64 issuedOn ) external { // TODO allow for updating existing attestation by only updating signer and publishedOn require(issuer == msg.sender); - _registerAttestation(identifier, issuer, account, signer, issuedOn); + _registerAttestation(identifier, issuer, account, issuer, issuedOn); } /** @@ -415,58 +414,43 @@ contract FederatedAttestations is * @param account Address of the account mapped to the identifier * @dev Throws if sender is not the issuer, signer, or account */ - // TODO should we pass in the issuedOn/signer parameter? ie. only revoke if the sender knows - // the issuedOn/signer for the unique attestation function revokeAttestation(bytes32 identifier, address issuer, address account) external { - OwnershipAttestation[] memory attestations = identifierToAttestations[identifier][issuer]; - for (uint256 i = 0; i < attestations.length; i = i.add(1)) { - OwnershipAttestation memory attestation = attestations[i]; - if (attestation.account == account) { - address signer = attestation.signer; - uint64 issuedOn = attestation.issuedOn; - uint64 publishedOn = attestation.publishedOn; - // TODO reviewers: is there a risk that compromised signers could revoke legitimate - // attestations before they have been unauthorized? - require( - account == msg.sender || getAccounts().attestationSignerToAccount(msg.sender) == issuer, - "Sender does not have permission to revoke this attestation" - ); - // This is meant to delete the attestation in the array - // and then move the last element in the array to that empty spot, - // to avoid having empty elements in the array - // TODO benchmark gas cost saving to check if array is of length 1 - identifierToAttestations[identifier][issuer][i] = attestations[attestations.length - 1]; - identifierToAttestations[identifier][issuer].pop(); - - bool deletedIdentifier = false; - bytes32[] memory identifiers = addressToIdentifiers[account][issuer]; - for (uint256 j = 0; j < identifiers.length; j = j.add(1)) { - if (identifiers[j] == identifier) { - addressToIdentifiers[account][issuer][j] = identifiers[identifiers.length - 1]; - addressToIdentifiers[account][issuer].pop(); - deletedIdentifier = true; - break; - } - } - // Should never be false - both mappings should always be updated in unison - assert(deletedIdentifier); - - bytes32 attestationHash = getUniqueAttestationHash( - identifier, - issuer, - account, - signer, - issuedOn - ); - // Should never be able to re-revoke an attestation - assert(!revokedAttestations[attestationHash]); - revokedAttestations[attestationHash] = true; - - emit AttestationRevoked(identifier, issuer, account, signer, issuedOn, publishedOn); - return; - } + require( + account == msg.sender || + // Minor gas optimization to prevent storage lookup in Accounts.sol if issuer == msg.sender + issuer == msg.sender || + getAccounts().attestationSignerToAccount(msg.sender) == issuer, + "Sender does not have permission to revoke this attestation" + ); + _revokeAttestation(identifier, issuer, account); + } + + /** + * @notice Revokes attestations [identifiers <-> accounts] from issuer + * @param issuer Address of the issuer of all attestations to be revoked + * @param identifiers Hash of the identifiers + * @param accounts Addresses of the accounts mapped to the identifiers + * at the same indices + * @dev Throws if the number of identifiers and accounts is not the same + * @dev Throws if sender is not the issuer or currently registered signer of issuer + * @dev Throws if an attestation is not found for identifiers[i] <-> accounts[i] + */ + function batchRevokeAttestations( + address issuer, + bytes32[] calldata identifiers, + address[] calldata accounts + ) external { + // TODO ASv2 Reviewers: we are planning to provide sensible limits in the SDK + // to prevent out of gas errors -- is that sufficient or should we limit here as well? + require(identifiers.length == accounts.length, "Unequal number of identifiers and accounts"); + require( + issuer == msg.sender || getAccounts().attestationSignerToAccount(msg.sender) == issuer, + "Sender does not have permission to revoke attestations from this issuer" + ); + + for (uint256 i = 0; i < identifiers.length; i = i.add(1)) { + _revokeAttestation(identifiers[i], issuer, accounts[i]); } - revert("Attestion to be revoked does not exist"); } function getUniqueAttestationHash( @@ -478,4 +462,71 @@ contract FederatedAttestations is ) public pure returns (bytes32) { return keccak256(abi.encode(identifier, issuer, account, signer, issuedOn)); } + + /** + * @notice Revokes an attestation: + * helper function for revokeAttestation and batchRevokeAttestations + * @param identifier Hash of the identifier to be revoked + * @param issuer Address of the attestation issuer + * @param account Address of the account mapped to the identifier + * @dev Reverts if attestation is not found mapping identifier <-> account + */ + + function _revokeAttestation(bytes32 identifier, address issuer, address account) private { + OwnershipAttestation[] memory attestations = identifierToAttestations[identifier][issuer]; + for (uint256 i = 0; i < attestations.length; i = i.add(1)) { + OwnershipAttestation memory attestation = attestations[i]; + if (attestation.account != account) { + continue; + } + + // This is meant to delete the attestation in the array + // and then move the last element in the array to that empty spot, + // to avoid having empty elements in the array + if (i != attestations.length - 1) { + identifierToAttestations[identifier][issuer][i] = attestations[attestations.length - 1]; + } + identifierToAttestations[identifier][issuer].pop(); + + bool deletedIdentifier = false; + bytes32[] memory identifiers = addressToIdentifiers[account][issuer]; + for (uint256 j = 0; j < identifiers.length; j = j.add(1)) { + if (identifiers[j] != identifier) { + continue; + } + if (j != identifiers.length - 1) { + addressToIdentifiers[account][issuer][j] = identifiers[identifiers.length - 1]; + } + addressToIdentifiers[account][issuer].pop(); + deletedIdentifier = true; + break; + } + // Should never be false - both mappings should always be updated in unison + assert(deletedIdentifier); + + bytes32 attestationHash = getUniqueAttestationHash( + identifier, + issuer, + account, + attestation.signer, + attestation.issuedOn + ); + // Should never be able to re-revoke an attestation + // TODO reviewers: removing this storage lookup saves about 20k gas + // for 100 batch-deleted attestations + assert(!revokedAttestations[attestationHash]); + revokedAttestations[attestationHash] = true; + + emit AttestationRevoked( + identifier, + issuer, + account, + attestation.signer, + attestation.issuedOn, + attestation.publishedOn + ); + return; + } + revert("Attestation to be revoked does not exist"); + } } diff --git a/packages/protocol/test/identity/federatedattestations.ts b/packages/protocol/test/identity/federatedattestations.ts index a35873c878e..bc59875ef61 100644 --- a/packages/protocol/test/identity/federatedattestations.ts +++ b/packages/protocol/test/identity/federatedattestations.ts @@ -5,6 +5,7 @@ import { } from '@celo/protocol/lib/fed-attestations-utils' import { CeloContractName } from '@celo/protocol/lib/registry-utils' import { + assertEqualBNArray, assertLogMatches2, assertRevert, assertRevertWithReason, @@ -19,7 +20,7 @@ import { RegistryContract, RegistryInstance, } from 'types' -import { keccak256 } from 'web3-utils' +import { encodePacked, keccak256 } from 'web3-utils' const Accounts: AccountsContract = artifacts.require('Accounts') const FederatedAttestations: FederatedAttestationsContract = artifacts.require( @@ -45,7 +46,7 @@ contract('FederatedAttestations', (accounts: string[]) => { const nowUnixTime = Math.floor(Date.now() / 1000) - const signerRole = keccak256('celo.org/core/attestation') + const signerRole = keccak256(encodePacked('celo.org/core/attestation')) let sig const signAndRegisterAttestation = async ( @@ -104,9 +105,10 @@ contract('FederatedAttestations', (accounts: string[]) => { assert.equal(attestation['account'], account) assert.equal(attestation['issuedOn'], issuedOn) assert.equal(attestation['signer'], signer) + // Intentionally not checking publishedOn since this value is set on-chain const storedIdentifier = await federatedAttestations.addressToIdentifiers( - account1, - issuer1, + account, + issuer, identifierIndex ) assert.equal(identifier, storedIdentifier) @@ -127,6 +129,38 @@ contract('FederatedAttestations', (accounts: string[]) => { ) } + interface AttestationTestCase { + account: string + issuedOn: number + signer: string + } + + const checkAgainstExpectedAttestations = ( + expectedCountsPerIssuer: number[], + expectedAttestations: AttestationTestCase[], + expectedPublishedOn: number, + actualCountsPerIssuer: BigNumber[], + actualAddresses: string[], + actualSigners: string[], + actualIssuedOns: BigNumber[], + actualPublishedOns: BigNumber[] + ) => { + assertEqualBNArray(actualCountsPerIssuer, expectedCountsPerIssuer) + assert.lengthOf(actualAddresses, expectedAttestations.length) + assert.lengthOf(actualSigners, expectedAttestations.length) + assert.lengthOf(actualIssuedOns, expectedAttestations.length) + assert.lengthOf(actualPublishedOns, expectedAttestations.length) + + expectedAttestations.forEach((expectedAttestation, index) => { + assert.equal(actualAddresses[index], expectedAttestation.account) + assert.equal(actualSigners[index], expectedAttestation.signer) + assert.equal(actualIssuedOns[index].toNumber(), expectedAttestation.issuedOn) + // Check bounds for publishedOn, not exact values, as it is set onchain + assert.isAtLeast(actualPublishedOns[index].toNumber(), actualIssuedOns[index].toNumber()) + assert.isAtMost(actualPublishedOns[index].toNumber(), expectedPublishedOn + 10) + }) + } + beforeEach('FederatedAttestations setup', async () => { accountsInstance = await Accounts.new(true) federatedAttestations = await FederatedAttestations.new(true) @@ -136,6 +170,7 @@ contract('FederatedAttestations', (accounts: string[]) => { CeloContractName.FederatedAttestations, federatedAttestations.address ) + await accountsInstance.initialize(registry.address) initialize = await federatedAttestations.initialize(registry.address) await accountsInstance.createAccount({ from: issuer1 }) @@ -195,39 +230,6 @@ contract('FederatedAttestations', (accounts: string[]) => { }) describe('#lookupAttestations', () => { - interface AttestationTestCase { - account: string - signer: string - issuedOn: number - } - - const checkAgainstExpectedAttestations = ( - expectedCountsPerIssuer: number[], - expectedAttestations: AttestationTestCase[], - expectedPublishedOn: number, - actualCountsPerIssuer: BigNumber[], - actualAddresses: string[], - actualSigners: string[], - actualIssuedOns: BigNumber[], - actualPublishedOns: BigNumber[] - ) => { - // purposefully not checking publishedOn, as it is set onchain - expect(actualCountsPerIssuer.map((count) => count.toNumber())).to.eql(expectedCountsPerIssuer) - - assert.lengthOf(actualAddresses, expectedAttestations.length) - assert.lengthOf(actualSigners, expectedAttestations.length) - assert.lengthOf(actualIssuedOns, expectedAttestations.length) - assert.lengthOf(actualPublishedOns, expectedAttestations.length) - - expectedAttestations.forEach((expectedAttestation, index) => { - assert.equal(actualAddresses[index], expectedAttestation.account) - assert.equal(actualSigners[index], expectedAttestation.signer) - assert.equal(actualIssuedOns[index].toNumber(), expectedAttestation.issuedOn) - assert.isAtLeast(actualPublishedOns[index].toNumber(), actualIssuedOns[index].toNumber()) - assert.isAtMost(actualPublishedOns[index].toNumber(), expectedPublishedOn + 10) - }) - } - describe('when identifier has not been registered', () => { it('should return empty list', async () => { const [ @@ -428,8 +430,11 @@ contract('FederatedAttestations', (accounts: string[]) => { actualCountsPerIssuer: BigNumber[], actualIdentifiers: string[] ) => { - expect(actualCountsPerIssuer.map((count) => count.toNumber())).to.eql(expectedCountsPerIssuer) - expect(actualIdentifiers).to.eql(expectedIdentifiers.map((idCase) => idCase.identifier)) + assertEqualBNArray(actualCountsPerIssuer, expectedCountsPerIssuer) + assert.deepEqual( + actualIdentifiers, + expectedIdentifiers.map((idCase) => idCase.identifier) + ) } describe('when address has not been registered', () => { @@ -730,6 +735,22 @@ contract('FederatedAttestations', (accounts: string[]) => { ) }) + it('should revert if signer has been deregistered', async () => { + await accountsInstance.removeSigner(signer1, signerRole, { from: issuer1 }) + await assertRevert( + federatedAttestations.registerAttestation( + identifier1, + issuer1, + account1, + signer1, + nowUnixTime, + sig.v, + sig.r, + sig.s + ) + ) + }) + it('should revert if attestation has been revoked', async () => { await signAndRegisterAttestation(identifier1, issuer1, account1, nowUnixTime, signer1) await federatedAttestations.revokeAttestation(identifier1, issuer1, account1) @@ -866,20 +887,32 @@ contract('FederatedAttestations', (accounts: string[]) => { identifier1, issuer1, account1, - issuer1, nowUnixTime, { from: issuer1 } ) ) }) + it('should succeed if issuer is not registered in Accounts.sol', async () => { + const issuer2 = accounts[3] + assert.isFalse(await accountsInstance.isAccount(issuer2)) + assert.isOk( + await federatedAttestations.registerAttestationAsIssuer( + identifier1, + issuer2, + account1, + nowUnixTime, + { from: issuer2 } + ) + ) + }) + it('should revert if a non-issuer submits an attestation with no signature', async () => { await assertRevert( federatedAttestations.registerAttestationAsIssuer( identifier1, issuer1, account1, - signer1, nowUnixTime, { from: signer1 } ) @@ -909,6 +942,22 @@ contract('FederatedAttestations', (accounts: string[]) => { await assertAttestationNotInStorage(identifier1, issuer1, account1, 0, 0) }) + it('should succeed when revoked by a current signer of issuer', async () => { + await federatedAttestations.revokeAttestation(identifier1, issuer1, account1, { + from: signer1, + }) + await assertAttestationNotInStorage(identifier1, issuer1, account1, 0, 0) + }) + + it('should revert when signer has been deregistered', async () => { + await accountsInstance.removeSigner(signer1, signerRole, { from: issuer1 }) + await assertRevert( + federatedAttestations.revokeAttestation(identifier1, issuer1, account1, { + from: signer1, + }) + ) + }) + it('should emit an AttestationRevoked event after successfully revoking', async () => { const attestation = await federatedAttestations.identifierToAttestations( identifier1, @@ -937,10 +986,27 @@ contract('FederatedAttestations', (accounts: string[]) => { }) }) + it('should succeed if issuer is not registered in Accounts.sol', async () => { + const issuer2 = accounts[3] + assert.isFalse(await accountsInstance.isAccount(issuer2)) + await federatedAttestations.registerAttestationAsIssuer( + identifier1, + issuer2, + account1, + nowUnixTime, + { from: issuer2 } + ) + await assertAttestationInStorage(identifier1, issuer2, 0, account1, nowUnixTime, issuer2, 0) + await federatedAttestations.revokeAttestation(identifier1, issuer2, account1, { + from: issuer2, + }) + await assertAttestationNotInStorage(identifier1, issuer2, account1, 0, 0) + }) + it("should revert when revoking an attestation that doesn't exist", async () => { await assertRevertWithReason( federatedAttestations.revokeAttestation(identifier1, issuer1, accounts[4]), - 'Attestion to be revoked does not exist' + 'Attestation to be revoked does not exist' ) }) @@ -1003,4 +1069,195 @@ contract('FederatedAttestations', (accounts: string[]) => { ) }) }) + + describe('#batchRevokeAttestations', () => { + const account2 = accounts[3] + const signer2 = accounts[4] + let publishedOn + beforeEach(async () => { + await signAndRegisterAttestation(identifier1, issuer1, account1, nowUnixTime, signer1) + await signAndRegisterAttestation(identifier1, issuer1, account2, nowUnixTime, signer2) + await signAndRegisterAttestation(identifier2, issuer1, account2, nowUnixTime, signer1) + publishedOn = (await federatedAttestations.identifierToAttestations(identifier2, issuer1, 0))[ + 'publishedOn' + ].toNumber() + }) + it('should succeed if issuer batch revokes attestations', async () => { + await federatedAttestations.batchRevokeAttestations( + issuer1, + [identifier1, identifier2], + [account1, account2] + ) + const [ + countsPerIssuer1, + addresses1, + signers1, + issuedOns1, + publishedOns1, + ] = await federatedAttestations.lookupAttestations(identifier1, [issuer1]) + checkAgainstExpectedAttestations( + [1], + [{ account: account2, issuedOn: nowUnixTime, signer: signer2 }], + publishedOn, + countsPerIssuer1, + addresses1, + signers1, + issuedOns1, + publishedOns1 + ) + const [ + countsPerIssuer2, + addresses2, + signers2, + issuedOns2, + publishedOns2, + ] = await federatedAttestations.lookupAttestations(identifier2, [issuer1]) + checkAgainstExpectedAttestations( + [0], + [], + publishedOn, + countsPerIssuer2, + addresses2, + signers2, + issuedOns2, + publishedOns2 + ) + }) + + it('should succeed regardless of order of (attestations, identifiers)', async () => { + await federatedAttestations.batchRevokeAttestations( + issuer1, + [identifier2, identifier1], + [account2, account1] + ) + const [ + countsPerIssuer1, + addresses1, + signers1, + issuedOns1, + publishedOns1, + ] = await federatedAttestations.lookupAttestations(identifier1, [issuer1]) + checkAgainstExpectedAttestations( + [1], + [{ account: account2, issuedOn: nowUnixTime, signer: signer2 }], + publishedOn, + countsPerIssuer1, + addresses1, + signers1, + issuedOns1, + publishedOns1 + ) + const [ + countsPerIssuer2, + addresses2, + signers2, + issuedOns2, + publishedOns2, + ] = await federatedAttestations.lookupAttestations(identifier2, [issuer1]) + checkAgainstExpectedAttestations( + [0], + [], + publishedOn, + countsPerIssuer2, + addresses2, + signers2, + issuedOns2, + publishedOns2 + ) + }) + + it('should succeed if currently registered signer of issuer batch revokes attestations', async () => { + await federatedAttestations.batchRevokeAttestations( + issuer1, + [identifier2, identifier1], + [account2, account1], + { from: signer1 } + ) + const [ + countsPerIssuer1, + addresses1, + signers1, + issuedOns1, + publishedOns1, + ] = await federatedAttestations.lookupAttestations(identifier1, [issuer1]) + checkAgainstExpectedAttestations( + [1], + [{ account: account2, issuedOn: nowUnixTime, signer: signer2 }], + publishedOn, + countsPerIssuer1, + addresses1, + signers1, + issuedOns1, + publishedOns1 + ) + const [ + countsPerIssuer2, + addresses2, + signers2, + issuedOns2, + publishedOns2, + ] = await federatedAttestations.lookupAttestations(identifier2, [issuer1]) + checkAgainstExpectedAttestations( + [0], + [], + publishedOn, + countsPerIssuer2, + addresses2, + signers2, + issuedOns2, + publishedOns2 + ) + }) + + it('should succeed if issuer is not registered in Accounts.sol', async () => { + const issuer2 = accounts[5] + assert.isFalse(await accountsInstance.isAccount(issuer2)) + await federatedAttestations.registerAttestationAsIssuer( + identifier1, + issuer2, + account1, + nowUnixTime, + { from: issuer2 } + ) + await assertAttestationInStorage(identifier1, issuer2, 0, account1, nowUnixTime, issuer2, 0) + await federatedAttestations.batchRevokeAttestations(issuer2, [identifier1], [account1], { + from: issuer2, + }) + await assertAttestationNotInStorage(identifier1, issuer2, account1, 0, 0) + }) + + it('should revert if deregistered signer of issuer batch revokes attestations', async () => { + await accountsInstance.removeSigner(signer1, signerRole, { from: issuer1 }) + await assertRevert( + federatedAttestations.batchRevokeAttestations( + issuer1, + [identifier2, identifier1], + [account2, account1], + { from: signer1 } + ) + ) + }) + it('should revert if identifiers.length != accounts.length', async () => { + await assertRevert( + federatedAttestations.batchRevokeAttestations( + issuer1, + [identifier2], + [account2, account1], + { from: signer1 } + ) + ) + }) + it('should revert if one of the (identifier, account) pairs is invalid', async () => { + await assertRevertWithReason( + federatedAttestations.batchRevokeAttestations( + issuer1, + // (identifier2, account1) does not exist + [identifier2, identifier2], + [account2, account1], + { from: signer1 } + ), + 'Attestation to be revoked does not exist' + ) + }) + }) }) From 3888340b30df644fdefd687a5be19589e6c93ffb Mon Sep 17 00:00:00 2001 From: Eela Nagaraj <7308464+eelanagaraj@users.noreply.github.com> Date: Thu, 16 Jun 2022 17:44:36 +0200 Subject: [PATCH 13/30] Upgrade Escrow from UsingRegistry V1 to V2 (#9631) * Upgrade Escrow from UsingRegistry V1 to V2 * Change FederatedAttestations from UsingRegistry V1 to V2 * Update backupmigrations script to include correct BRL paths * Add release8 data * Remove TODOs and commented out test * Add getFederatedAttestations to UsingRegistryV2 * Fix tests in CI * Update registry-utils to fix migration tests * Add comment explaining registry placeholder * Change publishedOn check * Fix lint * Fix setup to only assumeOwnership in CI * Rename UsingRegistryV2WithStorageSlot -> UsingRegistryV2BackwardsCompatible --- .../contracts/common/UsingRegistryV2.sol | 67 +++++-- .../UsingRegistryV2BackwardsCompatible.sol | 17 ++ .../protocol/contracts/identity/Escrow.sol | 15 +- .../identity/FederatedAttestations.sol | 8 +- packages/protocol/governanceConstitution.js | 4 +- packages/protocol/lib/registry-utils.ts | 2 - packages/protocol/migrations/18_escrow.ts | 3 +- .../migrations/25_federated_attestations.ts | 5 +- .../initializationData/release8.json | 3 + .../protocol/scripts/bash/backupmigrations.sh | 6 +- packages/protocol/test/identity/escrow.ts | 37 ++-- .../test/identity/federatedattestations.ts | 189 ++++++++++-------- 12 files changed, 205 insertions(+), 151 deletions(-) create mode 100644 packages/protocol/contracts/common/UsingRegistryV2BackwardsCompatible.sol create mode 100644 packages/protocol/releaseData/initializationData/release8.json diff --git a/packages/protocol/contracts/common/UsingRegistryV2.sol b/packages/protocol/contracts/common/UsingRegistryV2.sol index 096ec33f511..1972b54580d 100644 --- a/packages/protocol/contracts/common/UsingRegistryV2.sol +++ b/packages/protocol/contracts/common/UsingRegistryV2.sol @@ -15,6 +15,7 @@ import "../governance/interfaces/IValidators.sol"; import "../identity/interfaces/IRandom.sol"; import "../identity/interfaces/IAttestations.sol"; +import "../identity/interfaces/IFederatedAttestations.sol"; import "../stability/interfaces/IExchange.sol"; import "../stability/interfaces/IReserve.sol"; @@ -23,7 +24,7 @@ import "../stability/interfaces/IStableToken.sol"; contract UsingRegistryV2 { address constant registryAddress = 0x000000000000000000000000000000000000ce10; - IRegistry public constant registry = IRegistry(registryAddress); + IRegistry public constant registryContract = IRegistry(registryAddress); // solhint-disable state-visibility bytes32 constant ACCOUNTS_REGISTRY_ID = keccak256(abi.encodePacked("Accounts")); @@ -40,6 +41,9 @@ contract UsingRegistryV2 { bytes32 constant FEE_CURRENCY_WHITELIST_REGISTRY_ID = keccak256( abi.encodePacked("FeeCurrencyWhitelist") ); + bytes32 constant FEDERATED_ATTESTATIONS_REGISTRY_ID = keccak256( + abi.encodePacked("FederatedAttestations") + ); bytes32 constant FREEZER_REGISTRY_ID = keccak256(abi.encodePacked("Freezer")); bytes32 constant GOLD_TOKEN_REGISTRY_ID = keccak256(abi.encodePacked("GoldToken")); bytes32 constant GOVERNANCE_REGISTRY_ID = keccak256(abi.encodePacked("Governance")); @@ -57,29 +61,40 @@ contract UsingRegistryV2 { // solhint-enable state-visibility modifier onlyRegisteredContract(bytes32 identifierHash) { - require(registry.getAddressForOrDie(identifierHash) == msg.sender, "only registered contract"); + require( + registryContract.getAddressForOrDie(identifierHash) == msg.sender, + "only registered contract" + ); _; } modifier onlyRegisteredContracts(bytes32[] memory identifierHashes) { - require(registry.isOneOf(identifierHashes, msg.sender), "only registered contracts"); + require(registryContract.isOneOf(identifierHashes, msg.sender), "only registered contracts"); _; } + /** + * @notice Returns the storage, major, minor, and patch version of the contract. + * @return The storage, major, minor, and patch version of the contract. + */ + function getVersionNumber() external pure returns (uint256, uint256, uint256, uint256) { + return (1, 1, 1, 0); + } + function getAccounts() internal view returns (IAccounts) { - return IAccounts(registry.getAddressForOrDie(ACCOUNTS_REGISTRY_ID)); + return IAccounts(registryContract.getAddressForOrDie(ACCOUNTS_REGISTRY_ID)); } function getAttestations() internal view returns (IAttestations) { - return IAttestations(registry.getAddressForOrDie(ATTESTATIONS_REGISTRY_ID)); + return IAttestations(registryContract.getAddressForOrDie(ATTESTATIONS_REGISTRY_ID)); } function getElection() internal view returns (IElection) { - return IElection(registry.getAddressForOrDie(ELECTION_REGISTRY_ID)); + return IElection(registryContract.getAddressForOrDie(ELECTION_REGISTRY_ID)); } function getExchange() internal view returns (IExchange) { - return IExchange(registry.getAddressForOrDie(EXCHANGE_REGISTRY_ID)); + return IExchange(registryContract.getAddressForOrDie(EXCHANGE_REGISTRY_ID)); } function getExchangeDollar() internal view returns (IExchange) { @@ -87,47 +102,57 @@ contract UsingRegistryV2 { } function getExchangeEuro() internal view returns (IExchange) { - return IExchange(registry.getAddressForOrDie(EXCHANGE_EURO_REGISTRY_ID)); + return IExchange(registryContract.getAddressForOrDie(EXCHANGE_EURO_REGISTRY_ID)); } function getExchangeREAL() internal view returns (IExchange) { - return IExchange(registry.getAddressForOrDie(EXCHANGE_REAL_REGISTRY_ID)); + return IExchange(registryContract.getAddressForOrDie(EXCHANGE_REAL_REGISTRY_ID)); } function getFeeCurrencyWhitelistRegistry() internal view returns (IFeeCurrencyWhitelist) { - return IFeeCurrencyWhitelist(registry.getAddressForOrDie(FEE_CURRENCY_WHITELIST_REGISTRY_ID)); + return + IFeeCurrencyWhitelist( + registryContract.getAddressForOrDie(FEE_CURRENCY_WHITELIST_REGISTRY_ID) + ); + } + + function getFederatedAttestations() internal view returns (IFederatedAttestations) { + return + IFederatedAttestations( + registryContract.getAddressForOrDie(FEDERATED_ATTESTATIONS_REGISTRY_ID) + ); } function getFreezer() internal view returns (IFreezer) { - return IFreezer(registry.getAddressForOrDie(FREEZER_REGISTRY_ID)); + return IFreezer(registryContract.getAddressForOrDie(FREEZER_REGISTRY_ID)); } function getGoldToken() internal view returns (IERC20) { - return IERC20(registry.getAddressForOrDie(GOLD_TOKEN_REGISTRY_ID)); + return IERC20(registryContract.getAddressForOrDie(GOLD_TOKEN_REGISTRY_ID)); } function getGovernance() internal view returns (IGovernance) { - return IGovernance(registry.getAddressForOrDie(GOVERNANCE_REGISTRY_ID)); + return IGovernance(registryContract.getAddressForOrDie(GOVERNANCE_REGISTRY_ID)); } function getLockedGold() internal view returns (ILockedGold) { - return ILockedGold(registry.getAddressForOrDie(LOCKED_GOLD_REGISTRY_ID)); + return ILockedGold(registryContract.getAddressForOrDie(LOCKED_GOLD_REGISTRY_ID)); } function getRandom() internal view returns (IRandom) { - return IRandom(registry.getAddressForOrDie(RANDOM_REGISTRY_ID)); + return IRandom(registryContract.getAddressForOrDie(RANDOM_REGISTRY_ID)); } function getReserve() internal view returns (IReserve) { - return IReserve(registry.getAddressForOrDie(RESERVE_REGISTRY_ID)); + return IReserve(registryContract.getAddressForOrDie(RESERVE_REGISTRY_ID)); } function getSortedOracles() internal view returns (ISortedOracles) { - return ISortedOracles(registry.getAddressForOrDie(SORTED_ORACLES_REGISTRY_ID)); + return ISortedOracles(registryContract.getAddressForOrDie(SORTED_ORACLES_REGISTRY_ID)); } function getStableToken() internal view returns (IStableToken) { - return IStableToken(registry.getAddressForOrDie(STABLE_TOKEN_REGISTRY_ID)); + return IStableToken(registryContract.getAddressForOrDie(STABLE_TOKEN_REGISTRY_ID)); } function getStableDollarToken() internal view returns (IStableToken) { @@ -135,14 +160,14 @@ contract UsingRegistryV2 { } function getStableEuroToken() internal view returns (IStableToken) { - return IStableToken(registry.getAddressForOrDie(STABLE_EURO_TOKEN_REGISTRY_ID)); + return IStableToken(registryContract.getAddressForOrDie(STABLE_EURO_TOKEN_REGISTRY_ID)); } function getStableRealToken() internal view returns (IStableToken) { - return IStableToken(registry.getAddressForOrDie(STABLE_REAL_TOKEN_REGISTRY_ID)); + return IStableToken(registryContract.getAddressForOrDie(STABLE_REAL_TOKEN_REGISTRY_ID)); } function getValidators() internal view returns (IValidators) { - return IValidators(registry.getAddressForOrDie(VALIDATORS_REGISTRY_ID)); + return IValidators(registryContract.getAddressForOrDie(VALIDATORS_REGISTRY_ID)); } } diff --git a/packages/protocol/contracts/common/UsingRegistryV2BackwardsCompatible.sol b/packages/protocol/contracts/common/UsingRegistryV2BackwardsCompatible.sol new file mode 100644 index 00000000000..af9f8471b4c --- /dev/null +++ b/packages/protocol/contracts/common/UsingRegistryV2BackwardsCompatible.sol @@ -0,0 +1,17 @@ +pragma solidity ^0.5.13; + +import "./UsingRegistryV2.sol"; + +contract UsingRegistryV2BackwardsCompatible is UsingRegistryV2 { + // Placeholder for registry storage var in UsingRegistry and cannot be renamed + // without breaking release tooling. + // Use `registryContract` (in UsingRegistryV2) for the actual registry address. + IRegistry public registry; + /** + * @notice Returns the storage, major, minor, and patch version of the contract. + * @return The storage, major, minor, and patch version of the contract. + */ + function getVersionNumber() external pure returns (uint256, uint256, uint256, uint256) { + return (1, 1, 0, 0); + } +} diff --git a/packages/protocol/contracts/identity/Escrow.sol b/packages/protocol/contracts/identity/Escrow.sol index 2cf61f9cdb8..39c30c27116 100644 --- a/packages/protocol/contracts/identity/Escrow.sol +++ b/packages/protocol/contracts/identity/Escrow.sol @@ -8,7 +8,7 @@ import "./interfaces/IAttestations.sol"; import "./interfaces/IEscrow.sol"; import "../common/Initializable.sol"; import "../common/interfaces/ICeloVersionedContract.sol"; -import "../common/UsingRegistry.sol"; +import "../common/UsingRegistryV2BackwardsCompatible.sol"; import "../common/Signatures.sol"; import "../common/libraries/ReentrancyGuard.sol"; @@ -18,7 +18,8 @@ contract Escrow is ReentrancyGuard, Ownable, Initializable, - UsingRegistry + // Maintain storage alignment since Escrow was initially deployed with UsingRegistry.sol + UsingRegistryV2BackwardsCompatible { using SafeMath for uint256; @@ -76,7 +77,7 @@ contract Escrow is * @return The storage, major, minor, and patch version of the contract. */ function getVersionNumber() external pure returns (uint256, uint256, uint256, uint256) { - return (1, 1, 1, 2); + return (1, 2, 0, 0); } /** @@ -87,11 +88,9 @@ contract Escrow is /** * @notice Used in place of the constructor to allow the contract to be upgradable via proxy. - * @param registryAddress The address of the registry core smart contract. */ - function initialize(address registryAddress) external initializer { + function initialize() external initializer { _transferOwnership(msg.sender); - setRegistry(registryAddress); } /** @@ -123,7 +122,7 @@ contract Escrow is "Invalid privacy inputs: Can't require attestations if no identifier" ); - IAttestations attestations = IAttestations(registry.getAddressFor(ATTESTATIONS_REGISTRY_ID)); + IAttestations attestations = getAttestations(); require( minAttestations <= attestations.getMaxAttestations(), "minAttestations larger than limit" @@ -172,7 +171,7 @@ contract Escrow is // Due to an old bug, there may exist payments with no identifier and minAttestations > 0 // So ensure that these fail the attestations check, as they previously would have if (payment.minAttestations > 0) { - IAttestations attestations = IAttestations(registry.getAddressFor(ATTESTATIONS_REGISTRY_ID)); + IAttestations attestations = getAttestations(); (uint64 completedAttestations, ) = attestations.getAttestationStats( payment.recipientIdentifier, msg.sender diff --git a/packages/protocol/contracts/identity/FederatedAttestations.sol b/packages/protocol/contracts/identity/FederatedAttestations.sol index a861567189f..761178dea5b 100644 --- a/packages/protocol/contracts/identity/FederatedAttestations.sol +++ b/packages/protocol/contracts/identity/FederatedAttestations.sol @@ -10,7 +10,7 @@ import "../common/interfaces/IAccounts.sol"; import "../common/interfaces/ICeloVersionedContract.sol"; import "../common/Initializable.sol"; -import "../common/UsingRegistry.sol"; +import "../common/UsingRegistryV2.sol"; import "../common/Signatures.sol"; /** @@ -21,7 +21,7 @@ contract FederatedAttestations is ICeloVersionedContract, Ownable, Initializable, - UsingRegistry + UsingRegistryV2 { using SafeMath for uint256; using SafeCast for uint256; @@ -74,11 +74,9 @@ contract FederatedAttestations is /** * @notice Used in place of the constructor to allow the contract to be upgradable via proxy. - * @param registryAddress The address of the registry core smart contract. */ - function initialize(address registryAddress) external initializer { + function initialize() external initializer { _transferOwnership(msg.sender); - setRegistry(registryAddress); setEip712DomainSeparator(); } diff --git a/packages/protocol/governanceConstitution.js b/packages/protocol/governanceConstitution.js index bcc1732ba9a..61f92f4b559 100644 --- a/packages/protocol/governanceConstitution.js +++ b/packages/protocol/governanceConstitution.js @@ -42,7 +42,6 @@ const DefaultConstitution = { }, Escrow: { default: 0.6, - setRegistry: 0.9, }, Exchange: { default: 0.8, @@ -71,6 +70,9 @@ const DefaultConstitution = { setSpread: 0.8, setReserveFraction: 0.8, }, + FederatedAttestations: { + default: 0.6, + }, FeeCurrencyWhitelist: { default: 0.8, addToken: 0.8, diff --git a/packages/protocol/lib/registry-utils.ts b/packages/protocol/lib/registry-utils.ts index 5d4aa040bff..ba9063d547d 100644 --- a/packages/protocol/lib/registry-utils.ts +++ b/packages/protocol/lib/registry-utils.ts @@ -42,7 +42,6 @@ export enum CeloContractName { } export const usesRegistry = [ - CeloContractName.Escrow, CeloContractName.Reserve, CeloContractName.StableToken, ] @@ -56,7 +55,6 @@ export const hasEntryInRegistry: string[] = [ CeloContractName.Election, CeloContractName.Escrow, CeloContractName.Exchange, - // TODO ASv2 revisit this CeloContractName.FederatedAttestations, CeloContractName.FeeCurrencyWhitelist, CeloContractName.Freezer, diff --git a/packages/protocol/migrations/18_escrow.ts b/packages/protocol/migrations/18_escrow.ts index 764abce7bae..643258b7f12 100644 --- a/packages/protocol/migrations/18_escrow.ts +++ b/packages/protocol/migrations/18_escrow.ts @@ -1,11 +1,10 @@ import { CeloContractName } from '@celo/protocol/lib/registry-utils' import { deploymentForCoreContract } from '@celo/protocol/lib/web3-utils' -import { config } from '@celo/protocol/migrationsConfig' import { EscrowInstance } from 'types' module.exports = deploymentForCoreContract( web3, artifacts, CeloContractName.Escrow, - async () => [config.registry.predeployedProxyAddress] + async () => [] ) diff --git a/packages/protocol/migrations/25_federated_attestations.ts b/packages/protocol/migrations/25_federated_attestations.ts index ad25500c0a6..43bace75b06 100644 --- a/packages/protocol/migrations/25_federated_attestations.ts +++ b/packages/protocol/migrations/25_federated_attestations.ts @@ -1,10 +1,9 @@ import { CeloContractName } from '@celo/protocol/lib/registry-utils' import { deploymentForCoreContract } from '@celo/protocol/lib/web3-utils' -import { config } from '@celo/protocol/migrationsConfig' import { FederatedAttestationsInstance } from 'types' -const initializeArgs = async (): Promise<[string]> => { - return [config.registry.predeployedProxyAddress] +const initializeArgs = async () => { + return [] } module.exports = deploymentForCoreContract( diff --git a/packages/protocol/releaseData/initializationData/release8.json b/packages/protocol/releaseData/initializationData/release8.json new file mode 100644 index 00000000000..151187906e6 --- /dev/null +++ b/packages/protocol/releaseData/initializationData/release8.json @@ -0,0 +1,3 @@ +{ + "FederatedAttestations": [] +} diff --git a/packages/protocol/scripts/bash/backupmigrations.sh b/packages/protocol/scripts/bash/backupmigrations.sh index 6dfa89132ad..f4d394cb865 100755 --- a/packages/protocol/scripts/bash/backupmigrations.sh +++ b/packages/protocol/scripts/bash/backupmigrations.sh @@ -27,9 +27,11 @@ else # cp migrations.bak/07_reserve_spender_multisig.* migrations/ # cp migrations.bak/08_reserve.* migrations/ # cp migrations.bak/09_0_stabletoken_USD.* migrations/ - # cp migrations.bak/09_1_stableToken_EUR.* migrations/ + # cp migrations.bak/09_01_stableToken_EUR.* migrations/ + # cp migrations.bak/09_02_stableToken_BRL.* migrations/ # cp migrations.bak/10_0_exchange_USD.* migrations/ - # cp migrations.bak/10_1_exchange_EUR.* migrations/ + # cp migrations.bak/10_01_exchange_EUR.* migrations/ + # cp migrations.bak/10_02_exchange_BRL.* migrations/ # cp migrations.bak/11_accounts.* migrations/ # cp migrations.bak/12_lockedgold.* migrations/ # cp migrations.bak/13_validators.* migrations/ diff --git a/packages/protocol/test/identity/escrow.ts b/packages/protocol/test/identity/escrow.ts index 36c87386c62..04b3a99fdcd 100644 --- a/packages/protocol/test/identity/escrow.ts +++ b/packages/protocol/test/identity/escrow.ts @@ -5,8 +5,10 @@ import { assertLogMatches2, assertRevert, assertRevertWithReason, + assumeOwnership, timeTravel, } from '@celo/protocol/lib/test-utils' +import { getDeployedProxiedContract } from '@celo/protocol/lib/web3-utils' import { EscrowContract, EscrowInstance, @@ -14,14 +16,12 @@ import { MockAttestationsInstance, MockERC20TokenContract, MockERC20TokenInstance, - RegistryContract, RegistryInstance, } from 'types' import { getParsedSignatureOfAddress } from '../../lib/signing-utils' const Escrow: EscrowContract = artifacts.require('Escrow') const MockERC20Token: MockERC20TokenContract = artifacts.require('MockERC20Token') -const Registry: RegistryContract = artifacts.require('Registry') const MockAttestations: MockAttestationsContract = artifacts.require('MockAttestations') const NULL_BYTES32 = '0x0000000000000000000000000000000000000000000000000000000000000000' @@ -72,10 +72,18 @@ contract('Escrow', (accounts: string[]) => { const owner = accounts[0] let registry: RegistryInstance + before(async () => { + registry = await getDeployedProxiedContract('Registry', artifacts) + // Take ownership of the registry contract to point it to the mocks + if ((await registry.owner()) !== owner) { + // In CI we need to assume ownership, locally using quicktest we don't + await assumeOwnership(['Registry'], owner) + } + }) + beforeEach(async () => { - registry = await Registry.new(true) escrow = await Escrow.new(true, { from: owner }) - await escrow.initialize(registry.address) + await escrow.initialize() mockAttestations = await MockAttestations.new({ from: owner }) await registry.setAddressFor(CeloContractName.Attestations, mockAttestations.address) }) @@ -86,27 +94,8 @@ contract('Escrow', (accounts: string[]) => { assert.equal(actualOwner, owner) }) - it('should have set the registry address', async () => { - const registryAddress: string = await escrow.registry() - assert.equal(registryAddress, registry.address) - }) - it('should not be callable again', async () => { - await assertRevert(escrow.initialize(registry.address)) - }) - }) - - describe('#setRegistry()', () => { - const nonOwner: string = accounts[1] - const anAddress: string = accounts[2] - - it('should allow owner to set registry', async () => { - await escrow.setRegistry(anAddress) - assert.equal(await escrow.registry(), anAddress) - }) - - it('should not allow other users to set registry', async () => { - await assertRevert(escrow.setRegistry(anAddress, { from: nonOwner })) + await assertRevert(escrow.initialize()) }) }) diff --git a/packages/protocol/test/identity/federatedattestations.ts b/packages/protocol/test/identity/federatedattestations.ts index bc59875ef61..d6dfe871f62 100644 --- a/packages/protocol/test/identity/federatedattestations.ts +++ b/packages/protocol/test/identity/federatedattestations.ts @@ -10,14 +10,15 @@ import { assertRevert, assertRevertWithReason, assertThrowsAsync, + assumeOwnership, } from '@celo/protocol/lib/test-utils' +import { getDeployedProxiedContract } from '@celo/protocol/lib/web3-utils' import BigNumber from 'bignumber.js' import { AccountsContract, AccountsInstance, FederatedAttestationsContract, FederatedAttestationsInstance, - RegistryContract, RegistryInstance, } from 'types' import { encodePacked, keccak256 } from 'web3-utils' @@ -26,7 +27,6 @@ const Accounts: AccountsContract = artifacts.require('Accounts') const FederatedAttestations: FederatedAttestationsContract = artifacts.require( 'FederatedAttestations' ) -const Registry: RegistryContract = artifacts.require('Registry') contract('FederatedAttestations', (accounts: string[]) => { let accountsInstance: AccountsInstance @@ -36,15 +36,18 @@ contract('FederatedAttestations', (accounts: string[]) => { const chainId = 1 - const issuer1 = accounts[0] - const signer1 = accounts[1] - const account1 = accounts[2] + const owner = accounts[0] + const issuer1 = accounts[1] + const signer1 = accounts[2] + const account1 = accounts[3] const phoneNumber: string = '+18005551212' const identifier1 = getPhoneHash(phoneNumber) const identifier2 = getPhoneHash(phoneNumber, 'dummySalt') const nowUnixTime = Math.floor(Date.now() / 1000) + // Set lower bound to (now - 1 hour) in seconds + const publishedOnLowerBound = nowUnixTime - 60 * 60 const signerRole = keccak256(encodePacked('celo.org/core/attestation')) let sig @@ -138,7 +141,7 @@ contract('FederatedAttestations', (accounts: string[]) => { const checkAgainstExpectedAttestations = ( expectedCountsPerIssuer: number[], expectedAttestations: AttestationTestCase[], - expectedPublishedOn: number, + expectedPublishedOnLowerBound: number, actualCountsPerIssuer: BigNumber[], actualAddresses: string[], actualSigners: string[], @@ -155,23 +158,30 @@ contract('FederatedAttestations', (accounts: string[]) => { assert.equal(actualAddresses[index], expectedAttestation.account) assert.equal(actualSigners[index], expectedAttestation.signer) assert.equal(actualIssuedOns[index].toNumber(), expectedAttestation.issuedOn) - // Check bounds for publishedOn, not exact values, as it is set onchain - assert.isAtLeast(actualPublishedOns[index].toNumber(), actualIssuedOns[index].toNumber()) - assert.isAtMost(actualPublishedOns[index].toNumber(), expectedPublishedOn + 10) + // Check min bounds for publishedOn + assert.isAtLeast(actualPublishedOns[index].toNumber(), expectedPublishedOnLowerBound) }) } + before(async () => { + registry = await getDeployedProxiedContract('Registry', artifacts) + // Take ownership of the registry contract to point it to the mocks + if ((await registry.owner()) !== owner) { + // In CI we need to assume ownership, locally using quicktest we don't + await assumeOwnership(['Registry'], owner) + } + }) + beforeEach('FederatedAttestations setup', async () => { accountsInstance = await Accounts.new(true) federatedAttestations = await FederatedAttestations.new(true) - registry = await Registry.new(true) await registry.setAddressFor(CeloContractName.Accounts, accountsInstance.address) await registry.setAddressFor( CeloContractName.FederatedAttestations, federatedAttestations.address ) await accountsInstance.initialize(registry.address) - initialize = await federatedAttestations.initialize(registry.address) + initialize = await federatedAttestations.initialize() await accountsInstance.createAccount({ from: issuer1 }) sig = await getSignatureForAttestation( @@ -199,13 +209,8 @@ contract('FederatedAttestations', (accounts: string[]) => { describe('#initialize()', () => { it('should have set the owner', async () => { - const owner: string = await federatedAttestations.owner() - assert.equal(owner, issuer1) - }) - - it('should have set the registry address', async () => { - const registryAddress: string = await federatedAttestations.registry() - assert.equal(registryAddress, registry.address) + const actualOwner: string = await federatedAttestations.owner() + assert.equal(actualOwner, owner) }) it('should have set the EIP-712 domain separator', async () => { @@ -216,7 +221,7 @@ contract('FederatedAttestations', (accounts: string[]) => { }) it('should emit the EIP712DomainSeparatorSet event', () => { - assertLogMatches2(initialize.logs[2], { + assertLogMatches2(initialize.logs[1], { event: 'EIP712DomainSeparatorSet', args: { eip712DomainSeparator: getDomainDigest(federatedAttestations.address), @@ -225,7 +230,7 @@ contract('FederatedAttestations', (accounts: string[]) => { }) it('should not be callable again', async () => { - await assertRevert(federatedAttestations.initialize(registry.address)) + await assertRevert(federatedAttestations.initialize()) }) }) @@ -252,12 +257,12 @@ contract('FederatedAttestations', (accounts: string[]) => { }) }) describe('when identifier has been registered', () => { - const account2 = accounts[3] + const account2 = accounts[4] - const issuer2 = accounts[4] - const issuer2Signer = accounts[5] - const issuer2Signer2 = accounts[6] - const issuer3 = accounts[7] + const issuer2 = accounts[5] + const issuer2Signer = accounts[6] + const issuer2Signer2 = accounts[7] + const issuer3 = accounts[8] const issuer1Attestations: AttestationTestCase[] = [ { @@ -287,8 +292,6 @@ contract('FederatedAttestations', (accounts: string[]) => { }, ] - let expectedPublishedOn - beforeEach(async () => { // Require consistent order for test cases await accountsInstance.createAccount({ from: issuer2 }) @@ -306,7 +309,6 @@ contract('FederatedAttestations', (accounts: string[]) => { ) } } - expectedPublishedOn = Math.floor(Date.now() / 1000) }) it('should return empty count and list if no issuers specified', async () => { @@ -340,7 +342,7 @@ contract('FederatedAttestations', (accounts: string[]) => { checkAgainstExpectedAttestations( [issuer1Attestations.length], issuer1Attestations, - expectedPublishedOn, + publishedOnLowerBound, countsPerIssuer, addresses, signers, @@ -382,7 +384,7 @@ contract('FederatedAttestations', (accounts: string[]) => { checkAgainstExpectedAttestations( expectedCountsPerIssuer, expectedAttestations, - expectedPublishedOn, + publishedOnLowerBound, countsPerIssuer, addresses, signers, @@ -394,7 +396,9 @@ contract('FederatedAttestations', (accounts: string[]) => { describe('when identifier has been registered and then revoked', () => { beforeEach(async () => { await signAndRegisterAttestation(identifier1, issuer1, account1, nowUnixTime, signer1) - await federatedAttestations.revokeAttestation(identifier1, issuer1, account1) + await federatedAttestations.revokeAttestation(identifier1, issuer1, account1, { + from: issuer1, + }) }) it('should return empty list', async () => { const [ @@ -448,10 +452,10 @@ contract('FederatedAttestations', (accounts: string[]) => { }) describe('when address has been registered', () => { - const issuer2 = accounts[3] - const issuer2Signer = accounts[4] - const issuer2Signer2 = accounts[5] - const issuer3 = accounts[6] + const issuer2 = accounts[4] + const issuer2Signer = accounts[5] + const issuer2Signer2 = accounts[6] + const issuer3 = accounts[7] const issuer1IdCases: IdentifierTestCase[] = [ { @@ -540,7 +544,9 @@ contract('FederatedAttestations', (accounts: string[]) => { describe('when identifier has been registered and then revoked', () => { beforeEach(async () => { await signAndRegisterAttestation(identifier1, issuer1, account1, nowUnixTime, signer1) - await federatedAttestations.revokeAttestation(identifier1, issuer1, account1) + await federatedAttestations.revokeAttestation(identifier1, issuer1, account1, { + from: issuer1, + }) }) it('should return empty list', async () => { const [ @@ -580,7 +586,7 @@ contract('FederatedAttestations', (accounts: string[]) => { issuer1, account1, nowUnixTime, - accounts[3], + accounts[4], chainId, federatedAttestations.address ) @@ -600,9 +606,9 @@ contract('FederatedAttestations', (accounts: string[]) => { const wrongArgs = [ [0, 'identifier', identifier2], - [1, 'issuer', accounts[3]], - [2, 'account', accounts[3]], - [3, 'signer', accounts[3]], + [1, 'issuer', accounts[4]], + [2, 'account', accounts[4]], + [3, 'signer', accounts[4]], [4, 'issuedOn', nowUnixTime - 1], ] wrongArgs.forEach(([index, arg, wrongValue]) => { @@ -686,12 +692,14 @@ contract('FederatedAttestations', (accounts: string[]) => { publishedOn, }, }) - assert.isAtLeast(publishedOn.toNumber(), nowUnixTime) + assert.isAtLeast(publishedOn.toNumber(), publishedOnLowerBound) }) it('should revert if the attestation is revoked', async () => { await signAndRegisterAttestation(identifier1, issuer1, account1, nowUnixTime, signer1) - await federatedAttestations.revokeAttestation(identifier1, issuer1, account1) + await federatedAttestations.revokeAttestation(identifier1, issuer1, account1, { + from: issuer1, + }) await assertRevert( federatedAttestations.registerAttestation( identifier1, @@ -717,7 +725,7 @@ contract('FederatedAttestations', (accounts: string[]) => { issuer1, account1, nowUnixTime, - accounts[3], + accounts[4], chainId, federatedAttestations.address ) @@ -753,7 +761,9 @@ contract('FederatedAttestations', (accounts: string[]) => { it('should revert if attestation has been revoked', async () => { await signAndRegisterAttestation(identifier1, issuer1, account1, nowUnixTime, signer1) - await federatedAttestations.revokeAttestation(identifier1, issuer1, account1) + await federatedAttestations.revokeAttestation(identifier1, issuer1, account1, { + from: issuer1, + }) await assertRevertWithReason( federatedAttestations.registerAttestation( identifier1, @@ -763,7 +773,8 @@ contract('FederatedAttestations', (accounts: string[]) => { nowUnixTime, sig.v, sig.r, - sig.s + sig.s, + { from: issuer1 } ), 'Attestation has been revoked' ) @@ -786,12 +797,13 @@ contract('FederatedAttestations', (accounts: string[]) => { nowUnixTime, sig.v, sig.r, - sig.s + sig.s, + { from: issuer1 } ) }) it('should modify identifierToAttestations and addresstoIdentifiers accordingly', async () => { - const account2 = accounts[3] + const account2 = accounts[4] await assertAttestationInStorage(identifier1, issuer1, 0, account1, nowUnixTime, signer1, 0) await assertAttestationNotInStorage(identifier1, issuer1, account2, 1, 0) @@ -801,7 +813,7 @@ contract('FederatedAttestations', (accounts: string[]) => { it('should revert if an attestation with the same (issuer, identifier, account) is uploaded again', async () => { // Upload the same attestation signed by a different signer, authorized under the same issuer - const signer2 = accounts[4] + const signer2 = accounts[5] await accountsInstance.authorizeSigner(signer2, signerRole, { from: issuer1 }) await accountsInstance.completeSignerAuthorization(issuer1, signerRole, { from: signer2 }) const sig2 = await getSignatureForAttestation( @@ -822,7 +834,8 @@ contract('FederatedAttestations', (accounts: string[]) => { nowUnixTime, sig2.v, sig2.r, - sig2.s + sig2.s, + { from: issuer1 } ) ) }) @@ -833,15 +846,15 @@ contract('FederatedAttestations', (accounts: string[]) => { }) it('should succeed with a different issuer', async () => { - const issuer2 = accounts[4] - const signer2 = accounts[5] + const issuer2 = accounts[5] + const signer2 = accounts[6] await accountsInstance.createAccount({ from: issuer2 }) await signAndRegisterAttestation(identifier1, issuer2, account1, nowUnixTime, signer2) await assertAttestationInStorage(identifier1, issuer2, 0, account1, nowUnixTime, signer2, 0) }) it('should succeed with a different account', async () => { - const account2 = accounts[4] + const account2 = accounts[5] await signAndRegisterAttestation(identifier1, issuer1, account2, nowUnixTime, signer1) await assertAttestationInStorage(identifier1, issuer1, 1, account2, nowUnixTime, signer1, 0) }) @@ -858,13 +871,13 @@ contract('FederatedAttestations', (accounts: string[]) => { sig.v, sig.r, sig.s, - { from: accounts[4] } + { from: accounts[5] } ) ) }) it('should succeed if a different AttestationSigner authorized by the same issuer registers the attestation', async () => { - const signer2 = accounts[4] + const signer2 = accounts[5] await accountsInstance.authorizeSigner(signer2, signerRole, { from: issuer1 }) await accountsInstance.completeSignerAuthorization(issuer1, signerRole, { from: signer2 }) await federatedAttestations.registerAttestation( @@ -894,7 +907,7 @@ contract('FederatedAttestations', (accounts: string[]) => { }) it('should succeed if issuer is not registered in Accounts.sol', async () => { - const issuer2 = accounts[3] + const issuer2 = accounts[5] assert.isFalse(await accountsInstance.isAccount(issuer2)) assert.isOk( await federatedAttestations.registerAttestationAsIssuer( @@ -932,13 +945,16 @@ contract('FederatedAttestations', (accounts: string[]) => { nowUnixTime, sig.v, sig.r, - sig.s + sig.s, + { from: issuer1 } ) }) - it('should modify identifierToAddresses and addresstoIdentifiers accordingly', async () => { + it('should modify identifierToAttestations and addresstoIdentifiers accordingly', async () => { await assertAttestationInStorage(identifier1, issuer1, 0, account1, nowUnixTime, signer1, 0) - await federatedAttestations.revokeAttestation(identifier1, issuer1, account1) + await federatedAttestations.revokeAttestation(identifier1, issuer1, account1, { + from: issuer1, + }) await assertAttestationNotInStorage(identifier1, issuer1, account1, 0, 0) }) @@ -971,7 +987,8 @@ contract('FederatedAttestations', (accounts: string[]) => { const revokeAttestation = await federatedAttestations.revokeAttestation( identifier1, issuer1, - account1 + account1, + { from: issuer1 } ) assertLogMatches2(revokeAttestation.logs[0], { event: 'AttestationRevoked', @@ -987,7 +1004,7 @@ contract('FederatedAttestations', (accounts: string[]) => { }) it('should succeed if issuer is not registered in Accounts.sol', async () => { - const issuer2 = accounts[3] + const issuer2 = accounts[4] assert.isFalse(await accountsInstance.isAccount(issuer2)) await federatedAttestations.registerAttestationAsIssuer( identifier1, @@ -1005,13 +1022,15 @@ contract('FederatedAttestations', (accounts: string[]) => { it("should revert when revoking an attestation that doesn't exist", async () => { await assertRevertWithReason( - federatedAttestations.revokeAttestation(identifier1, issuer1, accounts[4]), + federatedAttestations.revokeAttestation(identifier1, issuer1, accounts[5], { + from: issuer1, + }), 'Attestation to be revoked does not exist' ) }) it('should succeed when >1 attestations are registered for (identifier, issuer)', async () => { - const account2 = accounts[3] + const account2 = accounts[4] await signAndRegisterAttestation(identifier1, issuer1, account2, nowUnixTime, signer1) await federatedAttestations.revokeAttestation(identifier1, issuer1, account2, { from: account2, @@ -1032,13 +1051,15 @@ contract('FederatedAttestations', (accounts: string[]) => { const newAttestation = [ [0, 'identifier', identifier2], // skipping issuer as it requires a different signer as well - [2, 'account', accounts[3]], + [2, 'account', accounts[4]], [3, 'issuedOn', nowUnixTime + 1], - [4, 'signer', accounts[3]], + [4, 'signer', accounts[4]], ] newAttestation.forEach(([index, arg, newVal]) => { it(`after revoking an attestation, should succeed in registering new attestation with different ${arg}`, async () => { - await federatedAttestations.revokeAttestation(identifier1, issuer1, account1) + await federatedAttestations.revokeAttestation(identifier1, issuer1, account1, { + from: issuer1, + }) const args = [identifier1, issuer1, account1, nowUnixTime, signer1] args[index] = newVal await signAndRegisterAttestation.apply(this, args) @@ -1048,13 +1069,15 @@ contract('FederatedAttestations', (accounts: string[]) => { it('should revert if an invalid user attempts to revoke the attestation', async () => { await assertRevert( federatedAttestations.revokeAttestation(identifier1, issuer1, account1, { - from: accounts[4], + from: accounts[5], }) ) }) it('should fail to register a revoked attestation', async () => { - await federatedAttestations.revokeAttestation(identifier1, issuer1, account1) + await federatedAttestations.revokeAttestation(identifier1, issuer1, account1, { + from: issuer1, + }) await assertRevert( federatedAttestations.registerAttestation( identifier1, @@ -1064,29 +1087,28 @@ contract('FederatedAttestations', (accounts: string[]) => { nowUnixTime, sig.v, sig.r, - sig.s + sig.s, + { from: issuer1 } ) ) }) }) describe('#batchRevokeAttestations', () => { - const account2 = accounts[3] - const signer2 = accounts[4] - let publishedOn + const account2 = accounts[4] + const signer2 = accounts[5] + beforeEach(async () => { await signAndRegisterAttestation(identifier1, issuer1, account1, nowUnixTime, signer1) await signAndRegisterAttestation(identifier1, issuer1, account2, nowUnixTime, signer2) await signAndRegisterAttestation(identifier2, issuer1, account2, nowUnixTime, signer1) - publishedOn = (await federatedAttestations.identifierToAttestations(identifier2, issuer1, 0))[ - 'publishedOn' - ].toNumber() }) it('should succeed if issuer batch revokes attestations', async () => { await federatedAttestations.batchRevokeAttestations( issuer1, [identifier1, identifier2], - [account1, account2] + [account1, account2], + { from: issuer1 } ) const [ countsPerIssuer1, @@ -1098,7 +1120,7 @@ contract('FederatedAttestations', (accounts: string[]) => { checkAgainstExpectedAttestations( [1], [{ account: account2, issuedOn: nowUnixTime, signer: signer2 }], - publishedOn, + publishedOnLowerBound, countsPerIssuer1, addresses1, signers1, @@ -1115,7 +1137,7 @@ contract('FederatedAttestations', (accounts: string[]) => { checkAgainstExpectedAttestations( [0], [], - publishedOn, + publishedOnLowerBound, countsPerIssuer2, addresses2, signers2, @@ -1128,7 +1150,8 @@ contract('FederatedAttestations', (accounts: string[]) => { await federatedAttestations.batchRevokeAttestations( issuer1, [identifier2, identifier1], - [account2, account1] + [account2, account1], + { from: issuer1 } ) const [ countsPerIssuer1, @@ -1140,7 +1163,7 @@ contract('FederatedAttestations', (accounts: string[]) => { checkAgainstExpectedAttestations( [1], [{ account: account2, issuedOn: nowUnixTime, signer: signer2 }], - publishedOn, + publishedOnLowerBound, countsPerIssuer1, addresses1, signers1, @@ -1157,7 +1180,7 @@ contract('FederatedAttestations', (accounts: string[]) => { checkAgainstExpectedAttestations( [0], [], - publishedOn, + publishedOnLowerBound, countsPerIssuer2, addresses2, signers2, @@ -1183,7 +1206,7 @@ contract('FederatedAttestations', (accounts: string[]) => { checkAgainstExpectedAttestations( [1], [{ account: account2, issuedOn: nowUnixTime, signer: signer2 }], - publishedOn, + publishedOnLowerBound, countsPerIssuer1, addresses1, signers1, @@ -1200,7 +1223,7 @@ contract('FederatedAttestations', (accounts: string[]) => { checkAgainstExpectedAttestations( [0], [], - publishedOn, + publishedOnLowerBound, countsPerIssuer2, addresses2, signers2, @@ -1210,7 +1233,7 @@ contract('FederatedAttestations', (accounts: string[]) => { }) it('should succeed if issuer is not registered in Accounts.sol', async () => { - const issuer2 = accounts[5] + const issuer2 = accounts[6] assert.isFalse(await accountsInstance.isAccount(issuer2)) await federatedAttestations.registerAttestationAsIssuer( identifier1, From 27a71a3329b70768a8c46f54201fca870972e0b1 Mon Sep 17 00:00:00 2001 From: Eela Nagaraj <7308464+eelanagaraj@users.noreply.github.com> Date: Thu, 16 Jun 2022 18:25:49 +0200 Subject: [PATCH 14/30] Escrow updates for ASv2 (#9636) * Implement transferWithTrustedIssuers * Update transfer and revoke tests * Implement withdraw and mock tests * Nits: fix linting, remove some TODOs, reformat text in tests * Add logic for setting defaultTrustedIssuers * Simplify transfer logic and docstrings and add unset event * Reorder escrow functions based on solidity style guide * Nit remove TODOs * Use FederatedAttestations instead of MockFA in escrow tests * Address PR comments * Remove redundant comments --- .../contracts/common/UsingRegistry.sol | 8 + .../protocol/contracts/identity/Escrow.sol | 349 +++++++++-- .../interfaces/IFederatedAttestations.sol | 13 +- packages/protocol/governanceConstitution.js | 2 + packages/protocol/lib/test-utils.ts | 9 +- packages/protocol/test/identity/escrow.ts | 583 ++++++++++++++++-- .../test/identity/federatedattestations.ts | 1 - 7 files changed, 864 insertions(+), 101 deletions(-) diff --git a/packages/protocol/contracts/common/UsingRegistry.sol b/packages/protocol/contracts/common/UsingRegistry.sol index c2b5109e6cf..15f00a61c82 100644 --- a/packages/protocol/contracts/common/UsingRegistry.sol +++ b/packages/protocol/contracts/common/UsingRegistry.sol @@ -15,6 +15,7 @@ import "../governance/interfaces/IValidators.sol"; import "../identity/interfaces/IRandom.sol"; import "../identity/interfaces/IAttestations.sol"; +import "../identity/interfaces/IFederatedAttestations.sol"; import "../stability/interfaces/IExchange.sol"; import "../stability/interfaces/IReserve.sol"; @@ -36,6 +37,9 @@ contract UsingRegistry is Ownable { bytes32 constant FEE_CURRENCY_WHITELIST_REGISTRY_ID = keccak256( abi.encodePacked("FeeCurrencyWhitelist") ); + bytes32 constant FEDERATED_ATTESTATIONS_REGISTRY_ID = keccak256( + abi.encodePacked("FederatedAttestations") + ); bytes32 constant FREEZER_REGISTRY_ID = keccak256(abi.encodePacked("Freezer")); bytes32 constant GOLD_TOKEN_REGISTRY_ID = keccak256(abi.encodePacked("GoldToken")); bytes32 constant GOVERNANCE_REGISTRY_ID = keccak256(abi.encodePacked("Governance")); @@ -92,6 +96,10 @@ contract UsingRegistry is Ownable { return IFeeCurrencyWhitelist(registry.getAddressForOrDie(FEE_CURRENCY_WHITELIST_REGISTRY_ID)); } + function getFederatedAttestations() internal view returns (IFederatedAttestations) { + return IFederatedAttestations(registry.getAddressForOrDie(FEDERATED_ATTESTATIONS_REGISTRY_ID)); + } + function getFreezer() internal view returns (IFreezer) { return IFreezer(registry.getAddressForOrDie(FREEZER_REGISTRY_ID)); } diff --git a/packages/protocol/contracts/identity/Escrow.sol b/packages/protocol/contracts/identity/Escrow.sol index 39c30c27116..b7e70cbc62d 100644 --- a/packages/protocol/contracts/identity/Escrow.sol +++ b/packages/protocol/contracts/identity/Escrow.sol @@ -5,6 +5,7 @@ import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; import "openzeppelin-solidity/contracts/token/ERC20/ERC20.sol"; import "./interfaces/IAttestations.sol"; +import "./interfaces/IFederatedAttestations.sol"; import "./interfaces/IEscrow.sol"; import "../common/Initializable.sol"; import "../common/interfaces/ICeloVersionedContract.sol"; @@ -23,6 +24,9 @@ contract Escrow is { using SafeMath for uint256; + event DefaultTrustedIssuerAdded(address indexed trustedIssuer); + event DefaultTrustedIssuerRemoved(address indexed trustedIssuer); + event Transfer( address indexed from, bytes32 indexed identifier, @@ -32,6 +36,9 @@ contract Escrow is uint256 minAttestations ); + event TrustedIssuersSet(address indexed paymentId, address[] trustedIssuers); + event TrustedIssuersUnset(address indexed paymentId); + event Withdrawal( bytes32 indexed identifier, // Note that in previous versions of Escrow.sol, `to` referenced @@ -72,6 +79,12 @@ contract Escrow is // Maps senders' addresses to a list of sent escrowed payment IDs. mapping(address => address[]) public sentPaymentIds; + // Maps payment ID to a list of issuers whose attestations will be accepted. + mapping(address => address[]) public trustedIssuersPerPayment; + + // Governable list of trustedIssuers to set for payments by default. + address[] public defaultTrustedIssuers; + /** * @notice Returns the storage, major, minor, and patch version of the contract. * @return The storage, major, minor, and patch version of the contract. @@ -93,6 +106,45 @@ contract Escrow is _transferOwnership(msg.sender); } + /** + * @notice Add an address to the defaultTrustedIssuers list. + * @param trustedIssuer Address of the trustedIssuer to add. + * @dev Throws if trustedIssuer is null or already in defaultTrustedIssuers. + */ + function addDefaultTrustedIssuer(address trustedIssuer) external onlyOwner { + require(address(0) != trustedIssuer, "trustedIssuer can't be null"); + // Ensure list of trusted issuers is unique + for (uint256 i = 0; i < defaultTrustedIssuers.length; i = i.add(1)) { + require( + defaultTrustedIssuers[i] != trustedIssuer, + "trustedIssuer already in defaultTrustedIssuers" + ); + } + defaultTrustedIssuers.push(trustedIssuer); + emit DefaultTrustedIssuerAdded(trustedIssuer); + } + + /** + * @notice Remove an address from the defaultTrustedIssuers list. + * @param trustedIssuer Address of the trustedIssuer to remove. + * @param index Index of trustedIssuer in defaultTrustedIssuers. + * @dev Throws if trustedIssuer is not in defaultTrustedIssuers at index. + */ + function removeDefaultTrustedIssuer(address trustedIssuer, uint256 index) external onlyOwner { + uint256 numDefaultTrustedIssuers = defaultTrustedIssuers.length; + require(index < numDefaultTrustedIssuers, "index is invalid"); + require( + defaultTrustedIssuers[index] == trustedIssuer, + "trustedIssuer does not match address found at defaultTrustedIssuers[index]" + ); + if (index != numDefaultTrustedIssuers - 1) { + // Swap last index with index-to-remove + defaultTrustedIssuers[index] = defaultTrustedIssuers[numDefaultTrustedIssuers - 1]; + } + defaultTrustedIssuers.pop(); + emit DefaultTrustedIssuerRemoved(trustedIssuer); + } + /** * @notice Transfer tokens to a specific user. Supports both identity with privacy (an empty * identifier and 0 minAttestations) and without (with identifier and minAttestations). @@ -103,9 +155,13 @@ contract Escrow is * @param paymentId The address of the temporary wallet associated with this payment. Users must * prove ownership of the corresponding private key to withdraw from escrow. * @param minAttestations The min number of attestations required to withdraw the payment. + * @return True if transfer succeeded. * @dev Throws if 'token' or 'value' is 0. + * @dev Throws if identifier is null and minAttestations > 0. + * @dev If minAttestations is 0, trustedIssuers will be set to empty list. * @dev msg.sender needs to have already approved this contract to transfer - * @dev If no identifier is given, then minAttestations must be 0. + + */ // solhint-disable-next-line no-simple-event-func-name function transfer( @@ -116,37 +172,61 @@ contract Escrow is address paymentId, uint256 minAttestations ) external nonReentrant returns (bool) { - require(token != address(0) && value > 0 && expirySeconds > 0, "Invalid transfer inputs."); - require( - !(identifier == 0 && minAttestations > 0), - "Invalid privacy inputs: Can't require attestations if no identifier" - ); - - IAttestations attestations = getAttestations(); - require( - minAttestations <= attestations.getMaxAttestations(), - "minAttestations larger than limit" - ); - - uint256 sentIndex = sentPaymentIds[msg.sender].push(paymentId).sub(1); - uint256 receivedIndex = receivedPaymentIds[identifier].push(paymentId).sub(1); - - EscrowedPayment storage newPayment = escrowedPayments[paymentId]; - require(newPayment.timestamp == 0, "paymentId already used"); - newPayment.recipientIdentifier = identifier; - newPayment.sender = msg.sender; - newPayment.token = token; - newPayment.value = value; - newPayment.sentIndex = sentIndex; - newPayment.receivedIndex = receivedIndex; - // solhint-disable-next-line not-rely-on-time - newPayment.timestamp = now; - newPayment.expirySeconds = expirySeconds; - newPayment.minAttestations = minAttestations; + address[] memory trustedIssuers; + // If minAttestations == 0, trustedIssuers should remain empty + if (minAttestations > 0) { + trustedIssuers = getDefaultTrustedIssuers(); + } + return + _transfer( + identifier, + token, + value, + expirySeconds, + paymentId, + minAttestations, + trustedIssuers + ); + } - require(ERC20(token).transferFrom(msg.sender, address(this), value), "Transfer unsuccessful."); - emit Transfer(msg.sender, identifier, token, value, paymentId, minAttestations); - return true; + /** + * @notice Transfer tokens to a specific user. Supports both identity with privacy (an empty + * identifier and 0 minAttestations) and without (with identifier + * and attestations completed by trustedIssuers). + * @param identifier The hashed identifier of a user to transfer to. + * @param token The token to be transferred. + * @param value The amount to be transferred. + * @param expirySeconds The number of seconds before the sender can revoke the payment. + * @param paymentId The address of the temporary wallet associated with this payment. Users must + * prove ownership of the corresponding private key to withdraw from escrow. + * @param minAttestations The min number of attestations required to withdraw the payment. + * @param trustedIssuers Array of issuers whose attestations in FederatedAttestations.sol + * will be accepted to prove ownership over an identifier. + * @return True if transfer succeeded. + * @dev Throws if 'token' or 'value' is 0. + * @dev Throws if identifier is null and minAttestations > 0. + * @dev Throws if minAttestations == 0 but trustedIssuers are provided. + * @dev msg.sender needs to have already approved this contract to transfer. + */ + function transferWithTrustedIssuers( + bytes32 identifier, + address token, + uint256 value, + uint256 expirySeconds, + address paymentId, + uint256 minAttestations, + address[] calldata trustedIssuers + ) external nonReentrant returns (bool) { + return + _transfer( + identifier, + token, + value, + expirySeconds, + paymentId, + minAttestations, + trustedIssuers + ); } /** @@ -155,6 +235,7 @@ contract Escrow is * @param v The recovery id of the incoming ECDSA signature. * @param r Output value r of the ECDSA signature. * @param s Output value s of the ECDSA signature. + * @return True if withdraw succeeded. * @dev Throws if 'token' or 'value' is 0. * @dev Throws if msg.sender does not prove ownership of the withdraw key. */ @@ -171,14 +252,32 @@ contract Escrow is // Due to an old bug, there may exist payments with no identifier and minAttestations > 0 // So ensure that these fail the attestations check, as they previously would have if (payment.minAttestations > 0) { - IAttestations attestations = getAttestations(); - (uint64 completedAttestations, ) = attestations.getAttestationStats( - payment.recipientIdentifier, - msg.sender - ); + bool passedCheck = false; + address[] memory trustedIssuers = trustedIssuersPerPayment[paymentId]; + address attestationsAddress = registryContract.getAddressForOrDie(ATTESTATIONS_REGISTRY_ID); + + if (trustedIssuers.length > 0) { + passedCheck = + hasCompletedV1AttestationsAsTrustedIssuer( + attestationsAddress, + payment.recipientIdentifier, + msg.sender, + payment.minAttestations, + trustedIssuers + ) || + hasCompletedV2Attestations(payment.recipientIdentifier, msg.sender, trustedIssuers); + } else { + // This is for backwards compatibility, not default/fallback behavior + passedCheck = hasCompletedV1Attestations( + attestationsAddress, + payment.recipientIdentifier, + msg.sender, + payment.minAttestations + ); + } require( - uint256(completedAttestations) >= payment.minAttestations, - "This account does not have enough attestations to withdraw this payment." + passedCheck, + "This account does not have the required attestations to withdraw this payment." ); } @@ -249,6 +348,176 @@ contract Escrow is return sentPaymentIds[sender]; } + /** + * @notice Gets array of all trusted issuers set per paymentId. + * @param paymentId The ID of the payment to be deleted. + * @return An array of addresses of trusted issuers set for an escrowed payment. + */ + function getTrustedIssuersPerPayment(address paymentId) external view returns (address[] memory) { + return trustedIssuersPerPayment[paymentId]; + } + + /** + * @notice Gets trusted issuers set as default for payments by `transfer` function. + * @return An array of addresses of trusted issuers. + */ + function getDefaultTrustedIssuers() public view returns (address[] memory) { + return defaultTrustedIssuers; + } + + /** + * @notice Checks if account has completed minAttestations for identifier in Attestations.sol. + * @param attestationsAddress The address of Attestations.sol. + * @param identifier The hash of an identifier for which to look up attestations. + * @param account The account for which to look up attestations. + * @param minAttestations The minimum number of attestations to have completed. + * @return Whether or not attestations in Attestations.sol + * exceeds minAttestations for (identifier, account). + */ + function hasCompletedV1Attestations( + address attestationsAddress, + bytes32 identifier, + address account, + uint256 minAttestations + ) internal view returns (bool) { + IAttestations attestations = IAttestations(attestationsAddress); + (uint64 completedAttestations, ) = attestations.getAttestationStats(identifier, account); + return (uint256(completedAttestations) >= minAttestations); + } + + /** + * @notice Helper function that checks if one of the trustedIssuers is the old Attestations.sol + * contract and applies the escrow V1 check against minAttestations. + * @param attestationsAddress The address of Attestations.sol. + * @param identifier The hash of an identifier for which to look up attestations. + * @param account The account for which to look up attestations. + * @param minAttestations The minimum number of attestations to have completed. + * @param trustedIssuers The trustedIssuer addresses to search through. + * @return Whether or not a trustedIssuer is Attestations.sol & attestations + * exceed minAttestations for (identifier, account). + */ + function hasCompletedV1AttestationsAsTrustedIssuer( + address attestationsAddress, + bytes32 identifier, + address account, + uint256 minAttestations, + address[] memory trustedIssuers + ) internal view returns (bool) { + for (uint256 i = 0; i < trustedIssuers.length; i = i.add(1)) { + if (trustedIssuers[i] != attestationsAddress) { + continue; + } + // This can be false; one of the several trustedIssuers listed needs to prove attestations + return hasCompletedV1Attestations(attestationsAddress, identifier, account, minAttestations); + } + return false; + } + + /** + * @notice Checks if there are attestations for account <-> identifier from + * any of trustedIssuers in FederatedAttestations.sol. + * @param identifier The hash of an identifier for which to look up attestations. + * @param account The account for which to look up attestations. + * @param trustedIssuers Issuer addresses whose attestations to trust. + * @return Whether or not attestations exist in FederatedAttestations.sol + * for (identifier, account). + */ + function hasCompletedV2Attestations( + bytes32 identifier, + address account, + address[] memory trustedIssuers + ) internal view returns (bool) { + // Check for an attestation from a trusted issuer + IFederatedAttestations federatedAttestations = getFederatedAttestations(); + (, address[] memory accounts, , , ) = federatedAttestations.lookupAttestations( + identifier, + trustedIssuers + ); + // Check if an attestation was found for recipientIdentifier -> account + for (uint256 i = 0; i < accounts.length; i = i.add(1)) { + if (accounts[i] == account) { + return true; + } + } + return false; + } + + /** + * @notice Helper function for `transferWithTrustedIssuers` and `transfer`, to + * enable backwards-compatible function signature for `transfer`, + * and since `transfer` cannot directly call `transferWithTrustedIssuers` + * due to reentrancy guard. + * @param identifier The hashed identifier of a user to transfer to. + * @param token The token to be transferred. + * @param value The amount to be transferred. + * @param expirySeconds The number of seconds before the sender can revoke the payment. + * @param paymentId The address of the temporary wallet associated with this payment. Users must + * prove ownership of the corresponding private key to withdraw from escrow. + * @param minAttestations The min number of attestations required to withdraw the payment. + * @param trustedIssuers Array of issuers whose attestations in FederatedAttestations.sol + * will be accepted to prove ownership over an identifier. + * @return True if transfer succeeded. + * @dev Throws if 'token' or 'value' is 0. + * @dev Throws if identifier is null and minAttestations > 0. + * @dev Throws if minAttestations == 0 but trustedIssuers are provided. + * @dev msg.sender needs to have already approved this contract to transfer. + */ + function _transfer( + bytes32 identifier, + address token, + uint256 value, + uint256 expirySeconds, + address paymentId, + uint256 minAttestations, + address[] memory trustedIssuers + ) private returns (bool) { + require(token != address(0) && value > 0 && expirySeconds > 0, "Invalid transfer inputs."); + require( + !(identifier == 0 && minAttestations > 0), + "Invalid privacy inputs: Can't require attestations if no identifier" + ); + // Withdraw logic with trustedIssuers in FederatedAttestations disregards + // minAttestations, so ensure that this is not set to 0 to prevent confusing behavior + // This also implies: if identifier == 0 => trustedIssuers.length == 0 + require( + !(minAttestations == 0 && trustedIssuers.length > 0), + "trustedIssuers may only be set when attestations are required" + ); + + IAttestations attestations = getAttestations(); + require( + minAttestations <= attestations.getMaxAttestations(), + "minAttestations larger than limit" + ); + + uint256 sentIndex = sentPaymentIds[msg.sender].push(paymentId).sub(1); + uint256 receivedIndex = receivedPaymentIds[identifier].push(paymentId).sub(1); + + EscrowedPayment storage newPayment = escrowedPayments[paymentId]; + require(newPayment.timestamp == 0, "paymentId already used"); + newPayment.recipientIdentifier = identifier; + newPayment.sender = msg.sender; + newPayment.token = token; + newPayment.value = value; + newPayment.sentIndex = sentIndex; + newPayment.receivedIndex = receivedIndex; + // solhint-disable-next-line not-rely-on-time + newPayment.timestamp = now; + newPayment.expirySeconds = expirySeconds; + newPayment.minAttestations = minAttestations; + + // Avoid unnecessary storage write + if (trustedIssuers.length > 0) { + trustedIssuersPerPayment[paymentId] = trustedIssuers; + } + + require(ERC20(token).transferFrom(msg.sender, address(this), value), "Transfer unsuccessful."); + emit Transfer(msg.sender, identifier, token, value, paymentId, minAttestations); + // Split into a second event for ABI backwards compatibility + emit TrustedIssuersSet(paymentId, trustedIssuers); + return true; + } + /** * @notice Deletes the payment from its receiver's and sender's lists of payments, * and zeroes out all the data in the struct. @@ -268,5 +537,9 @@ contract Escrow is sent.length = sent.length.sub(1); delete escrowedPayments[paymentId]; + delete trustedIssuersPerPayment[paymentId]; + // TODO ASv2 reviewers: adding trustedIssuers to event requires an additional + // storage lookup, but we can add this in if it's still best pratice to do so! + emit TrustedIssuersUnset(paymentId); } } diff --git a/packages/protocol/contracts/identity/interfaces/IFederatedAttestations.sol b/packages/protocol/contracts/identity/interfaces/IFederatedAttestations.sol index d72986621d7..835c1fa8f5b 100644 --- a/packages/protocol/contracts/identity/interfaces/IFederatedAttestations.sol +++ b/packages/protocol/contracts/identity/interfaces/IFederatedAttestations.sol @@ -2,4 +2,15 @@ pragma solidity ^0.5.13; // TODO ASv2 add external, view, and only owner function sigs // separated into these three groups for clarity -interface IFederatedAttestations {} +interface IFederatedAttestations { + function lookupAttestations(bytes32, address[] calldata) + external + view + returns ( + uint256[] memory, + address[] memory, + address[] memory, + uint64[] memory, + uint64[] memory + ); +} diff --git a/packages/protocol/governanceConstitution.js b/packages/protocol/governanceConstitution.js index 61f92f4b559..440afb7878e 100644 --- a/packages/protocol/governanceConstitution.js +++ b/packages/protocol/governanceConstitution.js @@ -42,6 +42,8 @@ const DefaultConstitution = { }, Escrow: { default: 0.6, + addDefaultTrustedIssuer: 0.6, + removeDefaultTrustedIssuer: 0.6, }, Exchange: { default: 0.8, diff --git a/packages/protocol/lib/test-utils.ts b/packages/protocol/lib/test-utils.ts index 9a5c58237c5..5477ed5ecae 100644 --- a/packages/protocol/lib/test-utils.ts +++ b/packages/protocol/lib/test-utils.ts @@ -294,12 +294,15 @@ export function assertLogMatches( assert.deepEqual(logArgs, Object.keys(args).sort(), `Argument names do not match for ${event}`) for (const k of logArgs) { + const errorMsg = `Event ${event}, arg: ${k} do not match` if (typeof args[k] === 'function') { - args[k](log.args[k], `Event ${event}, arg: ${k} do not match`) + args[k](log.args[k], errorMsg) } else if (isNumber(log.args[k]) || isNumber(args[k])) { - assertEqualBN(log.args[k], args[k], `Event ${event}, arg: ${k} do not match`) + assertEqualBN(log.args[k], args[k], errorMsg) + } else if (Array.isArray(log.args[k])) { + assert.deepEqual(log.args[k], args[k], errorMsg) } else { - assert.equal(log.args[k], args[k], `Event ${event}, arg: ${k} do not match`) + assert.equal(log.args[k], args[k], errorMsg) } } } diff --git a/packages/protocol/test/identity/escrow.ts b/packages/protocol/test/identity/escrow.ts index 04b3a99fdcd..1f9c4100afa 100644 --- a/packages/protocol/test/identity/escrow.ts +++ b/packages/protocol/test/identity/escrow.ts @@ -12,6 +12,8 @@ import { getDeployedProxiedContract } from '@celo/protocol/lib/web3-utils' import { EscrowContract, EscrowInstance, + FederatedAttestationsContract, + FederatedAttestationsInstance, MockAttestationsContract, MockAttestationsInstance, MockERC20TokenContract, @@ -23,6 +25,9 @@ import { getParsedSignatureOfAddress } from '../../lib/signing-utils' const Escrow: EscrowContract = artifacts.require('Escrow') const MockERC20Token: MockERC20TokenContract = artifacts.require('MockERC20Token') const MockAttestations: MockAttestationsContract = artifacts.require('MockAttestations') +const FederatedAttestations: FederatedAttestationsContract = artifacts.require( + 'FederatedAttestations' +) const NULL_BYTES32 = '0x0000000000000000000000000000000000000000000000000000000000000000' const NULL_ESCROWED_PAYMENT: EscrowedPayment = { @@ -69,12 +74,24 @@ const getEscrowedPayment = async ( contract('Escrow', (accounts: string[]) => { let escrow: EscrowInstance let mockAttestations: MockAttestationsInstance - const owner = accounts[0] + let federatedAttestations: FederatedAttestationsInstance let registry: RegistryInstance + const owner = accounts[0] + const sender: string = accounts[1] + const receiver: string = accounts[2] + const withdrawKeyAddress: string = accounts[3] + const anotherWithdrawKeyAddress: string = accounts[4] + const trustedIssuer1 = accounts[5] + const trustedIssuer2 = accounts[6] + const testTrustedIssuers = [trustedIssuer1, trustedIssuer2] + + const aValue: number = 10 + const aPhoneHash = getPhoneHash('+18005555555') + const oneDayInSecs: number = 86400 + before(async () => { registry = await getDeployedProxiedContract('Registry', artifacts) - // Take ownership of the registry contract to point it to the mocks if ((await registry.owner()) !== owner) { // In CI we need to assume ownership, locally using quicktest we don't await assumeOwnership(['Registry'], owner) @@ -85,7 +102,13 @@ contract('Escrow', (accounts: string[]) => { escrow = await Escrow.new(true, { from: owner }) await escrow.initialize() mockAttestations = await MockAttestations.new({ from: owner }) + federatedAttestations = await FederatedAttestations.new(true, { from: owner }) + await federatedAttestations.initialize() await registry.setAddressFor(CeloContractName.Attestations, mockAttestations.address) + await registry.setAddressFor( + CeloContractName.FederatedAttestations, + federatedAttestations.address + ) }) describe('#initialize()', () => { @@ -99,16 +122,100 @@ contract('Escrow', (accounts: string[]) => { }) }) + describe('#addDefaultTrustedIssuer', () => { + it('allows owner to add trustedIssuer', async () => { + assert.deepEqual(await escrow.getDefaultTrustedIssuers(), []) + await escrow.addDefaultTrustedIssuer(trustedIssuer1, { from: owner }) + assert.deepEqual(await escrow.getDefaultTrustedIssuers(), [trustedIssuer1]) + }) + + it('reverts if non-owner attempts to add trustedIssuer', async () => { + await assertRevert(escrow.addDefaultTrustedIssuer(trustedIssuer1, { from: trustedIssuer1 })) + }) + + it('should emit the DefaultTrustedIssuerAdded event', async () => { + const receipt = await escrow.addDefaultTrustedIssuer(trustedIssuer1, { from: owner }) + assertLogMatches2(receipt.logs[0], { + event: 'DefaultTrustedIssuerAdded', + args: { + trustedIssuer: trustedIssuer1, + }, + }) + }) + + it('should not allow an empty address to be set as a trustedIssuer', async () => { + await assertRevertWithReason( + escrow.addDefaultTrustedIssuer(NULL_ADDRESS, { from: owner }), + "trustedIssuer can't be null" + ) + }) + + it('should not allow a trustedIssuer to be added twice', async () => { + await escrow.addDefaultTrustedIssuer(trustedIssuer1, { from: owner }) + await assertRevertWithReason( + escrow.addDefaultTrustedIssuer(trustedIssuer1, { from: owner }), + 'trustedIssuer already in defaultTrustedIssuers' + ) + }) + + it('should allow a second trustedIssuer to be added', async () => { + await escrow.addDefaultTrustedIssuer(trustedIssuer1, { from: owner }) + await escrow.addDefaultTrustedIssuer(trustedIssuer2, { from: owner }) + assert.deepEqual(await escrow.getDefaultTrustedIssuers(), [trustedIssuer1, trustedIssuer2]) + }) + }) + + describe('#removeDefaultTrustedIssuer', () => { + beforeEach(async () => { + await escrow.addDefaultTrustedIssuer(trustedIssuer1, { from: owner }) + }) + + it('allows owner to remove trustedIssuer', async () => { + assert.deepEqual(await escrow.getDefaultTrustedIssuers(), [trustedIssuer1]) + await escrow.removeDefaultTrustedIssuer(trustedIssuer1, 0, { from: owner }) + assert.deepEqual(await escrow.getDefaultTrustedIssuers(), []) + }) + + it('reverts if non-owner attempts to remove trustedIssuer', async () => { + await assertRevert( + escrow.removeDefaultTrustedIssuer(trustedIssuer1, 0, { from: trustedIssuer1 }) + ) + }) + + it('should emit the DefaultTrustedIssuerRemoved event', async () => { + const receipt = await escrow.removeDefaultTrustedIssuer(trustedIssuer1, 0, { from: owner }) + assertLogMatches2(receipt.logs[0], { + event: 'DefaultTrustedIssuerRemoved', + args: { + trustedIssuer: trustedIssuer1, + }, + }) + }) + + it('should revert if index is invalid', async () => { + await assertRevertWithReason( + escrow.removeDefaultTrustedIssuer(trustedIssuer1, 1, { from: owner }), + 'index is invalid' + ) + }) + + it('should revert if trusted issuer does not match index', async () => { + await assertRevertWithReason( + escrow.removeDefaultTrustedIssuer(trustedIssuer2, 0, { from: owner }), + 'trustedIssuer does not match address found at defaultTrustedIssuers[index]' + ) + }) + + it('allows owner to remove trustedIssuer when two are present', async () => { + await escrow.addDefaultTrustedIssuer(trustedIssuer2, { from: owner }) + assert.deepEqual(await escrow.getDefaultTrustedIssuers(), [trustedIssuer1, trustedIssuer2]) + await escrow.removeDefaultTrustedIssuer(trustedIssuer1, 0, { from: owner }) + assert.deepEqual(await escrow.getDefaultTrustedIssuers(), [trustedIssuer2]) + }) + }) + describe('tests with tokens', () => { let mockERC20Token: MockERC20TokenInstance - const aValue: number = 10 - const sender: string = accounts[1] - const receiver: string = accounts[2] - - const aPhoneHash = getPhoneHash('+18005555555') - const withdrawKeyAddress: string = accounts[3] - const anotherWithdrawKeyAddress: string = accounts[4] - const oneDayInSecs: number = 86400 beforeEach(async () => { mockERC20Token = await MockERC20Token.new() @@ -120,23 +227,25 @@ contract('Escrow', (accounts: string[]) => { value: number, expirySeconds: number, paymentId: string, - minAttestations: number + minAttestations: number, + trustedIssuers: string[] ) => { await mockERC20Token.mint(escrowSender, value) - await escrow.transfer( + await escrow.transferWithTrustedIssuers( identifier, mockERC20Token.address, value, expirySeconds, paymentId, minAttestations, + trustedIssuers, { from: escrowSender, } ) } - describe('#transfer()', async () => { + describe('#transferWithTrustedIssuers', async () => { const transferAndCheckState = async ( escrowSender: string, identifier: string, @@ -144,6 +253,7 @@ contract('Escrow', (accounts: string[]) => { expirySeconds: number, paymentId: string, minAttestations: number, + trustedIssuers: string[], expectedSentPaymentIds: string[], expectedReceivedPaymentIds: string[] ) => { @@ -151,14 +261,14 @@ contract('Escrow', (accounts: string[]) => { await mockERC20Token.balanceOf(escrow.address) ).toNumber() const startingSenderBalance = (await mockERC20Token.balanceOf(escrowSender)).toNumber() - - await escrow.transfer( + await escrow.transferWithTrustedIssuers( identifier, mockERC20Token.address, value, expirySeconds, paymentId, minAttestations, + trustedIssuers, { from: escrowSender } ) const escrowedPayment = await getEscrowedPayment(paymentId, escrow) @@ -199,6 +309,13 @@ contract('Escrow', (accounts: string[]) => { paymentId, "expected paymentId not found in escrowSender's sent payments list" ) + + const trustedIssuersPerPayment = await escrow.getTrustedIssuersPerPayment(paymentId) + assert.deepEqual( + trustedIssuersPerPayment, + trustedIssuers, + 'unexpected trustedIssuersPerPayment' + ) } beforeEach(async () => { @@ -213,6 +330,7 @@ contract('Escrow', (accounts: string[]) => { oneDayInSecs, withdrawKeyAddress, 0, + [], [withdrawKeyAddress], [withdrawKeyAddress] ) @@ -226,6 +344,21 @@ contract('Escrow', (accounts: string[]) => { oneDayInSecs, withdrawKeyAddress, 3, + [], + [withdrawKeyAddress], + [withdrawKeyAddress] + ) + }) + + it('should allow transfer when trustedIssuers are provided', async () => { + await transferAndCheckState( + sender, + aPhoneHash, + aValue, + oneDayInSecs, + withdrawKeyAddress, + 3, + testTrustedIssuers, [withdrawKeyAddress], [withdrawKeyAddress] ) @@ -239,6 +372,7 @@ contract('Escrow', (accounts: string[]) => { oneDayInSecs, withdrawKeyAddress, 0, + [], [withdrawKeyAddress], [withdrawKeyAddress] ) @@ -251,7 +385,8 @@ contract('Escrow', (accounts: string[]) => { aValue, oneDayInSecs, anotherWithdrawKeyAddress, - 0 + 0, + [] ) await transferAndCheckState( sender, @@ -260,19 +395,21 @@ contract('Escrow', (accounts: string[]) => { oneDayInSecs, withdrawKeyAddress, 0, + [], [anotherWithdrawKeyAddress, withdrawKeyAddress], [anotherWithdrawKeyAddress, withdrawKeyAddress] ) }) it('should emit the Transfer event', async () => { - const receipt = await escrow.transfer( + const receipt = await escrow.transferWithTrustedIssuers( aPhoneHash, mockERC20Token.address, aValue, oneDayInSecs, withdrawKeyAddress, 2, + [], { from: sender, } @@ -290,26 +427,50 @@ contract('Escrow', (accounts: string[]) => { }) }) + it('should emit the TrustedIssuersSet event', async () => { + const receipt = await escrow.transferWithTrustedIssuers( + aPhoneHash, + mockERC20Token.address, + aValue, + oneDayInSecs, + withdrawKeyAddress, + 2, + testTrustedIssuers, + { + from: sender, + } + ) + assertLogMatches2(receipt.logs[1], { + event: 'TrustedIssuersSet', + args: { + paymentId: withdrawKeyAddress, + trustedIssuers: testTrustedIssuers, + }, + }) + }) + it('should not allow two transfers with same paymentId', async () => { - await escrow.transfer( + await escrow.transferWithTrustedIssuers( aPhoneHash, mockERC20Token.address, aValue, oneDayInSecs, withdrawKeyAddress, 0, + [], { from: sender, } ) await assertRevertWithReason( - escrow.transfer( + escrow.transferWithTrustedIssuers( aPhoneHash, mockERC20Token.address, aValue, oneDayInSecs, withdrawKeyAddress, 0, + [], { from: sender, } @@ -320,21 +481,31 @@ contract('Escrow', (accounts: string[]) => { it('should not allow a transfer if token is 0', async () => { await assertRevert( - escrow.transfer(aPhoneHash, NULL_ADDRESS, aValue, oneDayInSecs, withdrawKeyAddress, 0, { - from: sender, - }) + escrow.transferWithTrustedIssuers( + aPhoneHash, + NULL_ADDRESS, + aValue, + oneDayInSecs, + withdrawKeyAddress, + 0, + [], + { + from: sender, + } + ) ) }) it('should not allow a transfer if value is 0', async () => { await assertRevert( - escrow.transfer( + escrow.transferWithTrustedIssuers( aPhoneHash, mockERC20Token.address, 0, oneDayInSecs, withdrawKeyAddress, 0, + [], { from: sender, } @@ -344,21 +515,31 @@ contract('Escrow', (accounts: string[]) => { it('should not allow a transfer if expirySeconds is 0', async () => { await assertRevert( - escrow.transfer(aPhoneHash, mockERC20Token.address, aValue, 0, withdrawKeyAddress, 0, { - from: sender, - }) + escrow.transferWithTrustedIssuers( + aPhoneHash, + mockERC20Token.address, + aValue, + 0, + withdrawKeyAddress, + 0, + [], + { + from: sender, + } + ) ) }) it('should not allow a transfer if identifier is empty but minAttestations is > 0', async () => { await assertRevertWithReason( - escrow.transfer( + escrow.transferWithTrustedIssuers( NULL_BYTES32, mockERC20Token.address, aValue, oneDayInSecs, withdrawKeyAddress, 1, + [], { from: sender, } @@ -366,6 +547,109 @@ contract('Escrow', (accounts: string[]) => { "Invalid privacy inputs: Can't require attestations if no identifier" ) }) + + it('should not allow a transfer if identifier is empty but trustedIssuers are provided', async () => { + await assertRevertWithReason( + escrow.transferWithTrustedIssuers( + NULL_BYTES32, + mockERC20Token.address, + aValue, + oneDayInSecs, + withdrawKeyAddress, + 0, + testTrustedIssuers, + { + from: sender, + } + ), + 'trustedIssuers may only be set when attestations are required' + ) + }) + + it('should not allow setting trustedIssuers without minAttestations', async () => { + await assertRevertWithReason( + escrow.transferWithTrustedIssuers( + aPhoneHash, + mockERC20Token.address, + aValue, + oneDayInSecs, + withdrawKeyAddress, + 0, + testTrustedIssuers, + { + from: sender, + } + ), + 'trustedIssuers may only be set when attestations are required' + ) + }) + + describe('#transfer', () => { + // transfer and transferWithTrustedIssuers both rely on _transfer + // and transfer is a restricted version of transferWithTrustedIssuers + describe('when no defaut trustedIssuers are set', async () => { + it('should set trustedIssuersPerPaymentId to empty list', async () => { + await escrow.transfer( + aPhoneHash, + mockERC20Token.address, + aValue, + oneDayInSecs, + withdrawKeyAddress, + 2, + { + from: sender, + } + ) + const actualTrustedIssuers = await escrow.getTrustedIssuersPerPayment( + withdrawKeyAddress + ) + assert.deepEqual(actualTrustedIssuers, []) + }) + }) + + describe('when default trustedIssuers are set', async () => { + beforeEach(async () => { + await escrow.addDefaultTrustedIssuer(trustedIssuer1, { from: owner }) + await escrow.addDefaultTrustedIssuer(trustedIssuer2, { from: owner }) + }) + + it('should set trustedIssuersPerPaymentId to default when minAttestations>0', async () => { + await escrow.transfer( + aPhoneHash, + mockERC20Token.address, + aValue, + oneDayInSecs, + withdrawKeyAddress, + 2, + { + from: sender, + } + ) + const actualTrustedIssuers = await escrow.getTrustedIssuersPerPayment( + withdrawKeyAddress + ) + assert.deepEqual(actualTrustedIssuers, [trustedIssuer1, trustedIssuer2]) + }) + + it('should set trustedIssuersPerPaymentId to empty list when minAttestations==0', async () => { + await escrow.transfer( + aPhoneHash, + mockERC20Token.address, + aValue, + oneDayInSecs, + withdrawKeyAddress, + 0, + { + from: sender, + } + ) + const actualTrustedIssuers = await escrow.getTrustedIssuersPerPayment( + withdrawKeyAddress + ) + assert.deepEqual(actualTrustedIssuers, []) + }) + }) + }) }) const checkStateAfterDeletingPayment = async ( @@ -413,18 +697,32 @@ contract('Escrow', (accounts: string[]) => { NULL_ESCROWED_PAYMENT, 'escrowedPayment not zeroed out' ) + const trustedIssuersPerPayment = await escrow.getTrustedIssuersPerPayment(deletedPaymentId) + assert.deepEqual(trustedIssuersPerPayment, [], 'trustedIssuersPerPayment not zeroed out') } - describe('#withdraw()', () => { + describe('#withdraw', () => { const uniquePaymentIDWithdraw = withdrawKeyAddress + const completeAttestations = async ( + account: string, + identifier: string, + attestationsToComplete: number + ) => { + // Mock completed attestations + for (let i = 0; i < attestationsToComplete; i++) { + await mockAttestations.complete(identifier, 0, NULL_BYTES32, NULL_BYTES32, { + from: account, + }) + } + } + const withdrawAndCheckState = async ( escrowSender: string, escrowReceiver: string, identifier: string, value: number, paymentId: string, - attestationsToComplete: number, expectedSentPaymentIds: string[], expectedReceivedPaymentIds: string[] ) => { @@ -433,13 +731,6 @@ contract('Escrow', (accounts: string[]) => { await mockERC20Token.balanceOf(escrow.address) ).toNumber() const paymentBefore = await getEscrowedPayment(paymentId, escrow) - - // Mock completed attestations - for (let i = 0; i < attestationsToComplete; i++) { - await mockAttestations.complete(identifier, 0, NULL_BYTES32, NULL_BYTES32, { - from: escrowReceiver, - }) - } const parsedSig = await getParsedSignatureOfAddress(web3, escrowReceiver, paymentId) await escrow.withdraw(paymentId, parsedSig.v, parsedSig.r, parsedSig.s, { from: escrowReceiver, @@ -490,7 +781,8 @@ contract('Escrow', (accounts: string[]) => { aValue, oneDayInSecs, uniquePaymentIDWithdraw, - 0 + 0, + [] ) }) @@ -501,13 +793,12 @@ contract('Escrow', (accounts: string[]) => { NULL_BYTES32, aValue, uniquePaymentIDWithdraw, - 0, [], [] ) }) - it('should emit the Withdrawal event', async () => { + it('should emit the TrustedIssuersUnset event', async () => { const parsedSig = await getParsedSignatureOfAddress( web3, receiver, @@ -521,6 +812,27 @@ contract('Escrow', (accounts: string[]) => { { from: receiver } ) assertLogMatches2(receipt.logs[0], { + event: 'TrustedIssuersUnset', + args: { + paymentId: uniquePaymentIDWithdraw, + }, + }) + }) + + it('should emit the Withdrawal event', async () => { + const parsedSig = await getParsedSignatureOfAddress( + web3, + receiver, + uniquePaymentIDWithdraw + ) + const receipt = await escrow.withdraw( + uniquePaymentIDWithdraw, + parsedSig.v, + parsedSig.r, + parsedSig.s, + { from: receiver } + ) + assertLogMatches2(receipt.logs[1], { event: 'Withdrawal', args: { identifier: NULL_BYTES32, @@ -539,7 +851,8 @@ contract('Escrow', (accounts: string[]) => { aValue, oneDayInSecs, anotherWithdrawKeyAddress, - 0 + 0, + [] ) await withdrawAndCheckState( sender, @@ -547,7 +860,6 @@ contract('Escrow', (accounts: string[]) => { NULL_BYTES32, aValue, uniquePaymentIDWithdraw, - 0, [anotherWithdrawKeyAddress], [anotherWithdrawKeyAddress] ) @@ -559,7 +871,8 @@ contract('Escrow', (accounts: string[]) => { aValue, oneDayInSecs, anotherWithdrawKeyAddress, - 3 + 3, + [] ) await withdrawAndCheckState( sender, @@ -567,7 +880,6 @@ contract('Escrow', (accounts: string[]) => { NULL_BYTES32, aValue, uniquePaymentIDWithdraw, - 0, [anotherWithdrawKeyAddress], [] ) @@ -588,7 +900,7 @@ contract('Escrow', (accounts: string[]) => { }) }) - describe('when first payment is escrowed by a sender for an identifier && minAttestations', () => { + describe('when first payment is escrowed by a sender for identifier && minAttestations', () => { const minAttestations = 3 beforeEach(async () => { await mintAndTransfer( @@ -597,23 +909,25 @@ contract('Escrow', (accounts: string[]) => { aValue, oneDayInSecs, uniquePaymentIDWithdraw, - minAttestations + minAttestations, + [] ) }) it('should allow users to withdraw after completing attestations', async () => { + await completeAttestations(receiver, aPhoneHash, minAttestations) await withdrawAndCheckState( sender, receiver, aPhoneHash, aValue, uniquePaymentIDWithdraw, - minAttestations, [], [] ) }) it('should not allow a user to withdraw a payment if they have fewer than minAttestations', async () => { + await completeAttestations(receiver, aPhoneHash, minAttestations - 1) await assertRevertWithReason( withdrawAndCheckState( sender, @@ -621,11 +935,10 @@ contract('Escrow', (accounts: string[]) => { aPhoneHash, aValue, uniquePaymentIDWithdraw, - minAttestations - 1, [], [] ), - 'This account does not have enough attestations to withdraw this payment.' + 'This account does not have the required attestations to withdraw this payment.' ) }) it("should withdraw properly when sender's second payment has an identifier", async () => { @@ -635,44 +948,187 @@ contract('Escrow', (accounts: string[]) => { aValue, oneDayInSecs, anotherWithdrawKeyAddress, - 0 + 0, + [] ) + await completeAttestations(receiver, aPhoneHash, minAttestations) await withdrawAndCheckState( sender, receiver, aPhoneHash, aValue, uniquePaymentIDWithdraw, - minAttestations, [anotherWithdrawKeyAddress], [anotherWithdrawKeyAddress] ) }) }) + + describe('when trustedIssuers are set for payment', () => { + describe('when Attestations.sol is a trustedIssuer', () => { + const minAttestations = 3 + beforeEach(async () => { + await mintAndTransfer( + sender, + aPhoneHash, + aValue, + oneDayInSecs, + uniquePaymentIDWithdraw, + minAttestations, + [mockAttestations.address, trustedIssuer1, trustedIssuer2] + ) + }) + + it('should allow withdraw after completing attestations', async () => { + await completeAttestations(receiver, aPhoneHash, minAttestations) + await withdrawAndCheckState( + sender, + receiver, + aPhoneHash, + aValue, + uniquePaymentIDWithdraw, + [], + [] + ) + }) + describe('when { + it('should not allow withdrawal if no attestations exist in FederatedAttestations', async () => { + await completeAttestations(receiver, aPhoneHash, minAttestations - 1) + await assertRevertWithReason( + withdrawAndCheckState( + sender, + receiver, + aPhoneHash, + aValue, + uniquePaymentIDWithdraw, + [], + [] + ), + 'This account does not have the required attestations to withdraw this payment.' + ) + }) + it('should allow users to withdraw if attestation is found in FederatedAttestations', async () => { + await completeAttestations(receiver, aPhoneHash, minAttestations - 1) + await federatedAttestations.registerAttestationAsIssuer( + aPhoneHash, + trustedIssuer2, + receiver, + 0, + { from: trustedIssuer2 } + ) + await withdrawAndCheckState( + sender, + receiver, + aPhoneHash, + aValue, + uniquePaymentIDWithdraw, + [], + [] + ) + }) + }) + }) + describe('when Attestations.sol is not a trusted issuer', () => { + beforeEach(async () => { + await mintAndTransfer( + sender, + aPhoneHash, + aValue, + oneDayInSecs, + uniquePaymentIDWithdraw, + 2, + testTrustedIssuers + ) + }) + it('should allow users to withdraw if attestation is found in FederatedAttestations', async () => { + await federatedAttestations.registerAttestationAsIssuer( + aPhoneHash, + trustedIssuer2, + receiver, + 0, + { from: trustedIssuer2 } + ) + await withdrawAndCheckState( + sender, + receiver, + aPhoneHash, + aValue, + uniquePaymentIDWithdraw, + [], + [] + ) + }) + it('should not allow a user to withdraw a payment if no attestations exist for trustedIssuers', async () => { + await assertRevertWithReason( + withdrawAndCheckState( + sender, + receiver, + aPhoneHash, + aValue, + uniquePaymentIDWithdraw, + [], + [] + ), + 'This account does not have the required attestations to withdraw this payment.' + ) + }) + }) + }) }) - describe('#revoke()', () => { + describe('#revoke', () => { let uniquePaymentIDRevoke: string let parsedSig1: any - const identifiers = [NULL_BYTES32, aPhoneHash] - identifiers.forEach((identifier) => { + interface TransferParams { + identifier: string + minAttestations: number + trustedIssuers: string[] + } + + const transferParams: TransferParams[] = [ + { identifier: NULL_BYTES32, minAttestations: 0, trustedIssuers: [] }, + { identifier: aPhoneHash, minAttestations: 0, trustedIssuers: [] }, + { identifier: aPhoneHash, minAttestations: 1, trustedIssuers: testTrustedIssuers }, + ] + + transferParams.forEach(({ identifier, trustedIssuers, minAttestations }) => { describe(`when identifier is ${ - identifier === NULL_BYTES32 ? '' : 'not' - } empty`, async () => { + identifier === NULL_BYTES32 ? '' : 'not ' + }empty, trustedIssuers.length=${ + trustedIssuers.length + }, and minAttestations=${minAttestations}`, async () => { beforeEach(async () => { - await mintAndTransfer(sender, identifier, aValue, oneDayInSecs, withdrawKeyAddress, 0) + await mintAndTransfer( + sender, + identifier, + aValue, + oneDayInSecs, + withdrawKeyAddress, + minAttestations, + trustedIssuers + ) await mintAndTransfer( sender, identifier, aValue, oneDayInSecs, anotherWithdrawKeyAddress, - 0 + minAttestations, + trustedIssuers ) uniquePaymentIDRevoke = withdrawKeyAddress parsedSig1 = await getParsedSignatureOfAddress(web3, receiver, withdrawKeyAddress) + if (trustedIssuers.length) { + await federatedAttestations.registerAttestationAsIssuer( + identifier, + trustedIssuers[0], + receiver, + 0, + { from: trustedIssuers[0] } + ) + } }) it('should allow sender to redeem payment after payment has expired', async () => { @@ -707,10 +1163,21 @@ contract('Escrow', (accounts: string[]) => { ) }) - it('should emit the Revocation event', async () => { + it('should emit the TrustedIssuersUnset event', async () => { await timeTravel(oneDayInSecs, web3) const receipt = await escrow.revoke(uniquePaymentIDRevoke, { from: sender }) assertLogMatches2(receipt.logs[0], { + event: 'TrustedIssuersUnset', + args: { + paymentId: withdrawKeyAddress, + }, + }) + }) + + it('should emit the Revocation event', async () => { + await timeTravel(oneDayInSecs, web3) + const receipt = await escrow.revoke(uniquePaymentIDRevoke, { from: sender }) + assertLogMatches2(receipt.logs[1], { event: 'Revocation', args: { identifier, diff --git a/packages/protocol/test/identity/federatedattestations.ts b/packages/protocol/test/identity/federatedattestations.ts index d6dfe871f62..0840a5de3dd 100644 --- a/packages/protocol/test/identity/federatedattestations.ts +++ b/packages/protocol/test/identity/federatedattestations.ts @@ -165,7 +165,6 @@ contract('FederatedAttestations', (accounts: string[]) => { before(async () => { registry = await getDeployedProxiedContract('Registry', artifacts) - // Take ownership of the registry contract to point it to the mocks if ((await registry.owner()) !== owner) { // In CI we need to assume ownership, locally using quicktest we don't await assumeOwnership(['Registry'], owner) From a2549b3e2ba19bb4830ab338326782df1319d42c Mon Sep 17 00:00:00 2001 From: isabellewei Date: Tue, 21 Jun 2022 08:44:07 -0400 Subject: [PATCH 15/30] [ASv2] PR review updates (+ solidity style guide #9651) (#9653) * reorder functions to follow solidity style guide * update visibility * PR updates * use struct to fix stack too deep * Fix named returns * Nits: solidity style guide reorderings * Nit: consistent ordering for getVersionNumber with Escrow * Remove outdated TODO - re: removed assert Co-authored-by: Eela Nagaraj <7308464+eelanagaraj@users.noreply.github.com> --- .../protocol/contracts/identity/Escrow.sol | 12 +- .../identity/FederatedAttestations.sol | 434 ++++++++---------- .../test/identity/federatedattestations.ts | 4 +- 3 files changed, 207 insertions(+), 243 deletions(-) diff --git a/packages/protocol/contracts/identity/Escrow.sol b/packages/protocol/contracts/identity/Escrow.sol index b7e70cbc62d..a35a77f5b94 100644 --- a/packages/protocol/contracts/identity/Escrow.sol +++ b/packages/protocol/contracts/identity/Escrow.sol @@ -85,6 +85,12 @@ contract Escrow is // Governable list of trustedIssuers to set for payments by default. address[] public defaultTrustedIssuers; + /** + * @notice Sets initialized == true on implementation contracts + * @param test Set to true to skip implementation initialization + */ + constructor(bool test) public Initializable(test) {} + /** * @notice Returns the storage, major, minor, and patch version of the contract. * @return The storage, major, minor, and patch version of the contract. @@ -93,12 +99,6 @@ contract Escrow is return (1, 2, 0, 0); } - /** - * @notice Sets initialized == true on implementation contracts - * @param test Set to true to skip implementation initialization - */ - constructor(bool test) public Initializable(test) {} - /** * @notice Used in place of the constructor to allow the contract to be upgradable via proxy. */ diff --git a/packages/protocol/contracts/identity/FederatedAttestations.sol b/packages/protocol/contracts/identity/FederatedAttestations.sol index 761178dea5b..6774281773a 100644 --- a/packages/protocol/contracts/identity/FederatedAttestations.sol +++ b/packages/protocol/contracts/identity/FederatedAttestations.sol @@ -43,7 +43,7 @@ contract FederatedAttestations is // unique attestation hash -> isRevoked mapping(bytes32 => bool) public revokedAttestations; - bytes32 public constant EIP712_VALIDATE_ATTESTATION_TYPEHASH = keccak256( + bytes32 public constant EIP712_OWNERSHIP_ATTESTATION_TYPEHASH = keccak256( "OwnershipAttestation(bytes32 identifier,address issuer,address account,uint64 issuedOn)" ); bytes32 public eip712DomainSeparator; @@ -72,6 +72,14 @@ contract FederatedAttestations is */ constructor(bool test) public Initializable(test) {} + /** + * @notice Returns the storage, major, minor, and patch version of the contract. + * @return The storage, major, minor, and patch version of the contract. + */ + function getVersionNumber() external pure returns (uint256, uint256, uint256, uint256) { + return (1, 1, 0, 0); + } + /** * @notice Used in place of the constructor to allow the contract to be upgradable via proxy. */ @@ -81,75 +89,109 @@ contract FederatedAttestations is } /** - * @notice Sets the EIP712 domain separator for the Celo FederatedAttestations abstraction. + * @notice Registers an attestation directly from the issuer + * @param identifier Hash of the identifier to be attested + * @param issuer Address of the attestation issuer + * @param account Address of the account being mapped to the identifier + * @param issuedOn Time at which the issuer issued the attestation in Unix time + * @dev Attestation signer in storage is set to issuer + * @dev Throws if an attestation with the same (identifier, issuer, account) already exists */ - function setEip712DomainSeparator() public { - uint256 chainId; - assembly { - chainId := chainid - } + function registerAttestationAsIssuer( + bytes32 identifier, + address issuer, + address account, + uint64 issuedOn + ) external { + // TODO allow for updating existing attestation by only updating signer and publishedOn + require(issuer == msg.sender); + _registerAttestation(identifier, issuer, account, issuer, issuedOn); + } - eip712DomainSeparator = keccak256( - abi.encode( - keccak256( - "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" - ), - keccak256(bytes("FederatedAttestations")), - keccak256("1.0"), - chainId, - address(this) - ) - ); - emit EIP712DomainSeparatorSet(eip712DomainSeparator); + /** + * @notice Registers an attestation with a valid signature + * @param identifier Hash of the identifier to be attested + * @param issuer Address of the attestation issuer + * @param account Address of the account being mapped to the identifier + * @param issuedOn Time at which the issuer issued the attestation in Unix time + * @param signer Address of the signer of the attestation + * @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 Throws if an attestation with the same (identifier, issuer, account) already exists + */ + function registerAttestation( + bytes32 identifier, + address issuer, + address account, + address signer, + uint64 issuedOn, + uint8 v, + bytes32 r, + bytes32 s + ) external { + // TODO allow for updating existing attestation by only updating signer and publishedOn + validateAttestationSig(identifier, issuer, account, signer, issuedOn, v, r, s); + _registerAttestation(identifier, issuer, account, signer, issuedOn); } /** - * @notice Returns the storage, major, minor, and patch version of the contract. - * @return The storage, major, minor, and patch version of the contract. + * @notice Revokes an attestation + * @param identifier Hash of the identifier to be revoked + * @param issuer Address of the attestation issuer + * @param account Address of the account mapped to the identifier + * @dev Throws if sender is not the issuer, signer, or account */ - function getVersionNumber() external pure returns (uint256, uint256, uint256, uint256) { - return (1, 1, 0, 0); + function revokeAttestation(bytes32 identifier, address issuer, address account) external { + require( + account == msg.sender || + // Minor gas optimization to prevent storage lookup in Accounts.sol if issuer == msg.sender + issuer == msg.sender || + getAccounts().attestationSignerToAccount(msg.sender) == issuer, + "Sender does not have permission to revoke this attestation" + ); + _revokeAttestation(identifier, issuer, account); } /** - * @notice Helper function for _lookupAttestations to calculate the - total number of attestations completed for an identifier - by each trusted issuer - * @param identifier Hash of the identifier - * @param trustedIssuers Array of n issuers whose attestations will be included - * @return [0] Sum total of attestations found - * [1] Array of number of attestations found per issuer + * @notice Revokes attestations [identifiers <-> accounts] from issuer + * @param issuer Address of the issuer of all attestations to be revoked + * @param identifiers Hash of the identifiers + * @param accounts Addresses of the accounts mapped to the identifiers + * at the same indices + * @dev Throws if the number of identifiers and accounts is not the same + * @dev Throws if sender is not the issuer or currently registered signer of issuer + * @dev Throws if an attestation is not found for identifiers[i] <-> accounts[i] */ - function getNumAttestations(bytes32 identifier, address[] memory trustedIssuers) - internal - view - returns (uint256, uint256[] memory) - { - uint256 totalAttestations = 0; - uint256 numAttestationsForIssuer; - uint256[] memory countsPerIssuer = new uint256[](trustedIssuers.length); + function batchRevokeAttestations( + address issuer, + bytes32[] calldata identifiers, + address[] calldata accounts + ) external { + // TODO ASv2 Reviewers: we are planning to provide sensible limits in the SDK + // to prevent out of gas errors -- is that sufficient or should we limit here as well? + require(identifiers.length == accounts.length, "Unequal number of identifiers and accounts"); + require( + issuer == msg.sender || getAccounts().attestationSignerToAccount(msg.sender) == issuer, + "Sender does not have permission to revoke attestations from this issuer" + ); - for (uint256 i = 0; i < trustedIssuers.length; i = i.add(1)) { - numAttestationsForIssuer = identifierToAttestations[identifier][trustedIssuers[i]].length; - totalAttestations = totalAttestations.add(numAttestationsForIssuer); - countsPerIssuer[i] = numAttestationsForIssuer; + for (uint256 i = 0; i < identifiers.length; i = i.add(1)) { + _revokeAttestation(identifiers[i], issuer, accounts[i]); } - return (totalAttestations, countsPerIssuer); } /** - * @notice Returns info about up to `maxAttestations` attestations for - * `identifier` produced by signers of `trustedIssuers` + * @notice Returns info about attestations for `identifier` produced by + * signers of `trustedIssuers` * @param identifier Hash of the identifier * @param trustedIssuers Array of n issuers whose attestations will be included - * @return [0] Array of number of attestations returned per issuer - * @return [1 - 4] for m (== sum([0])) found attestations: - * [ - * Array of m accounts, - * Array of m signers, - * Array of m issuedOns, - * Array of m publishedOns - * ]; index corresponds to the same attestation + * @return countsPerIssuer Array of number of attestations returned per issuer + * For m (== sum([0])) found attestations: + * @return accounts Array of m accounts + * @return signers Array of m signers + * @return issuedOns Array of m issuedOns + * @return publishedOns Array of m publishedOns * @dev Adds attestation info to the arrays in order of provided trustedIssuers * @dev Expectation that only one attestation exists per (identifier, issuer, account) */ @@ -158,51 +200,24 @@ contract FederatedAttestations is function lookupAttestations(bytes32 identifier, address[] calldata trustedIssuers) external view - returns (uint256[] memory, address[] memory, address[] memory, uint64[] memory, uint64[] memory) + returns ( + uint256[] memory countsPerIssuer, + address[] memory accounts, + address[] memory signers, + uint64[] memory issuedOns, + uint64[] memory publishedOns + ) { - // TODO reviewers: this is to get around a stack too deep error; - // are there better ways of dealing with this? - uint256[] memory countsPerIssuer; uint256 totalAttestations; (totalAttestations, countsPerIssuer) = getNumAttestations(identifier, trustedIssuers); - // solhint-disable-next-line max-line-length - (address[] memory accounts, address[] memory signers, uint64[] memory issuedOns, uint64[] memory publishedOns) = _lookupAttestations( - identifier, - trustedIssuers, - totalAttestations - ); - return (countsPerIssuer, accounts, signers, issuedOns, publishedOns); - } - /** - * @notice Helper function for lookupAttestations to get around stack too deep - * @param identifier Hash of the identifier - * @param trustedIssuers Array of n issuers whose attestations will be included - * @return [0 - 3] for m (== sum([0])) found attestations: - * [ - * Array of m accounts, - * Array of m signers, - * Array of m issuedOns, - * Array of m publishedOns - * ]; index corresponds to the same attestation - * @dev Adds attestation info to the arrays in order of provided trustedIssuers - * @dev Expectation that only one attestation exists per (identifier, issuer, account) - */ - function _lookupAttestations( - bytes32 identifier, - address[] memory trustedIssuers, - uint256 totalAttestations - ) internal view returns (address[] memory, address[] memory, uint64[] memory, uint64[] memory) { - address[] memory accounts = new address[](totalAttestations); - address[] memory signers = new address[](totalAttestations); - uint64[] memory issuedOns = new uint64[](totalAttestations); - uint64[] memory publishedOns = new uint64[](totalAttestations); + accounts = new address[](totalAttestations); + signers = new address[](totalAttestations); + issuedOns = new uint64[](totalAttestations); + publishedOns = new uint64[](totalAttestations); - OwnershipAttestation[] memory attestationsPerIssuer; - // Reset this and use as current index to get around stack-too-deep - // TODO reviewers: is it preferable to pack two uint256 counters into a struct - // and use one for total (above) & one for currIndex (below)? totalAttestations = 0; + OwnershipAttestation[] memory attestationsPerIssuer; for (uint256 i = 0; i < trustedIssuers.length; i = i.add(1)) { attestationsPerIssuer = identifierToAttestations[identifier][trustedIssuers[i]]; @@ -214,56 +229,27 @@ contract FederatedAttestations is totalAttestations = totalAttestations.add(1); } } - return (accounts, signers, issuedOns, publishedOns); - } - - /** - * @notice Helper function for lookupIdentifiers to calculate the - total number of identifiers completed for an identifier - by each trusted issuer - * @param account Address of the account - * @param trustedIssuers Array of n issuers whose identifiers will be included - * @return [0] Sum total of identifiers found - * [1] Array of number of identifiers found per issuer - */ - function getNumIdentifiers(address account, address[] memory trustedIssuers) - internal - view - returns (uint256, uint256[] memory) - { - uint256 totalIdentifiers = 0; - uint256 numIdentifiersForIssuer; - uint256[] memory countsPerIssuer = new uint256[](trustedIssuers.length); - - for (uint256 i = 0; i < trustedIssuers.length; i = i.add(1)) { - numIdentifiersForIssuer = addressToIdentifiers[account][trustedIssuers[i]].length; - totalIdentifiers = totalIdentifiers.add(numIdentifiersForIssuer); - countsPerIssuer[i] = numIdentifiersForIssuer; - } - return (totalIdentifiers, countsPerIssuer); + return (countsPerIssuer, accounts, signers, issuedOns, publishedOns); } /** - * @notice Returns up to `maxIdentifiers` identifiers mapped to `account` - * by signers of `trustedIssuers` + * @notice Returns identifiers mapped to `account` by signers of `trustedIssuers` * @param account Address of the account * @param trustedIssuers Array of n issuers whose identifier mappings will be used - * @return [0] Array of number of identifiers returned per issuer - * @return [1] Array (length == sum([0])) of identifiers + * @return countsPerIssuer Array of number of identifiers returned per issuer + * @return identifiers Array (length == sum([0])) of identifiers * @dev Adds identifier info to the arrays in order of provided trustedIssuers * @dev Expectation that only one attestation exists per (identifier, issuer, account) */ function lookupIdentifiers(address account, address[] calldata trustedIssuers) external view - returns (uint256[] memory, bytes32[] memory) + returns (uint256[] memory countsPerIssuer, bytes32[] memory identifiers) { uint256 totalIdentifiers; - uint256[] memory countsPerIssuer; - (totalIdentifiers, countsPerIssuer) = getNumIdentifiers(account, trustedIssuers); - bytes32[] memory identifiers = new bytes32[](totalIdentifiers); + identifiers = new bytes32[](totalIdentifiers); bytes32[] memory identifiersPerIssuer; uint256 currIndex = 0; @@ -306,7 +292,7 @@ contract FederatedAttestations is "Signer is not a currently authorized AttestationSigner for the issuer" ); bytes32 structHash = keccak256( - abi.encode(EIP712_VALIDATE_ATTESTATION_TYPEHASH, identifier, issuer, account, issuedOn) + abi.encode(EIP712_OWNERSHIP_ATTESTATION_TYPEHASH, identifier, issuer, account, issuedOn) ); address guessedSigner = Signatures.getSignerOfTypedDataHash( eip712DomainSeparator, @@ -318,6 +304,91 @@ contract FederatedAttestations is require(guessedSigner == signer, "Signature is invalid"); } + function getUniqueAttestationHash( + bytes32 identifier, + address issuer, + address account, + address signer, + uint64 issuedOn + ) public pure returns (bytes32) { + return keccak256(abi.encode(identifier, issuer, account, signer, issuedOn)); + } + + /** + * @notice Sets the EIP712 domain separator for the Celo FederatedAttestations abstraction. + */ + function setEip712DomainSeparator() internal { + uint256 chainId; + assembly { + chainId := chainid + } + + eip712DomainSeparator = keccak256( + abi.encode( + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ), + keccak256(bytes("FederatedAttestations")), + keccak256("1.0"), + chainId, + address(this) + ) + ); + emit EIP712DomainSeparatorSet(eip712DomainSeparator); + } + + /** + * @notice Helper function for lookupAttestations to calculate the + total number of attestations completed for an identifier + by each trusted issuer + * @param identifier Hash of the identifier + * @param trustedIssuers Array of n issuers whose attestations will be included + * @return totalAttestations Sum total of attestations found + * @return countsPerIssuer Array of number of attestations found per issuer + */ + function getNumAttestations(bytes32 identifier, address[] memory trustedIssuers) + internal + view + returns (uint256 totalAttestations, uint256[] memory countsPerIssuer) + { + totalAttestations = 0; + uint256 numAttestationsForIssuer; + countsPerIssuer = new uint256[](trustedIssuers.length); + + for (uint256 i = 0; i < trustedIssuers.length; i = i.add(1)) { + numAttestationsForIssuer = identifierToAttestations[identifier][trustedIssuers[i]].length; + totalAttestations = totalAttestations.add(numAttestationsForIssuer); + countsPerIssuer[i] = numAttestationsForIssuer; + } + return (totalAttestations, countsPerIssuer); + } + + /** + * @notice Helper function for lookupIdentifiers to calculate the + total number of identifiers completed for an identifier + by each trusted issuer + * @param account Address of the account + * @param trustedIssuers Array of n issuers whose identifiers will be included + * @return totalIdentifiers Sum total of identifiers found + * @return countsPerIssuer Array of number of identifiers found per issuer + */ + function getNumIdentifiers(address account, address[] memory trustedIssuers) + internal + view + returns (uint256 totalIdentifiers, uint256[] memory countsPerIssuer) + { + totalIdentifiers = 0; + uint256 numIdentifiersForIssuer; + countsPerIssuer = new uint256[](trustedIssuers.length); + + for (uint256 i = 0; i < trustedIssuers.length; i = i.add(1)) { + numIdentifiersForIssuer = addressToIdentifiers[account][trustedIssuers[i]].length; + totalIdentifiers = totalIdentifiers.add(numIdentifiersForIssuer); + countsPerIssuer[i] = numIdentifiersForIssuer; + } + return (totalIdentifiers, countsPerIssuer); + } + /** * @notice Registers an attestation * @param identifier Hash of the identifier to be attested @@ -358,109 +429,6 @@ contract FederatedAttestations is emit AttestationRegistered(identifier, issuer, account, signer, issuedOn, publishedOn); } - /** - * @notice Registers an attestation directly from the issuer - * @param identifier Hash of the identifier to be attested - * @param issuer Address of the attestation issuer - * @param account Address of the account being mapped to the identifier - * @param issuedOn Time at which the issuer issued the attestation in Unix time - * @dev Attestation signer in storage is set to issuer - * @dev Throws if an attestation with the same (identifier, issuer, account) already exists - */ - function registerAttestationAsIssuer( - bytes32 identifier, - address issuer, - address account, - uint64 issuedOn - ) external { - // TODO allow for updating existing attestation by only updating signer and publishedOn - require(issuer == msg.sender); - _registerAttestation(identifier, issuer, account, issuer, issuedOn); - } - - /** - * @notice Registers an attestation with a valid signature - * @param identifier Hash of the identifier to be attested - * @param issuer Address of the attestation issuer - * @param account Address of the account being mapped to the identifier - * @param issuedOn Time at which the issuer issued the attestation in Unix time - * @param signer Address of the signer of the attestation - * @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 Throws if an attestation with the same (identifier, issuer, account) already exists - */ - function registerAttestation( - bytes32 identifier, - address issuer, - address account, - address signer, - uint64 issuedOn, - uint8 v, - bytes32 r, - bytes32 s - ) external { - // TODO allow for updating existing attestation by only updating signer and publishedOn - validateAttestationSig(identifier, issuer, account, signer, issuedOn, v, r, s); - _registerAttestation(identifier, issuer, account, signer, issuedOn); - } - - /** - * @notice Revokes an attestation - * @param identifier Hash of the identifier to be revoked - * @param issuer Address of the attestation issuer - * @param account Address of the account mapped to the identifier - * @dev Throws if sender is not the issuer, signer, or account - */ - function revokeAttestation(bytes32 identifier, address issuer, address account) external { - require( - account == msg.sender || - // Minor gas optimization to prevent storage lookup in Accounts.sol if issuer == msg.sender - issuer == msg.sender || - getAccounts().attestationSignerToAccount(msg.sender) == issuer, - "Sender does not have permission to revoke this attestation" - ); - _revokeAttestation(identifier, issuer, account); - } - - /** - * @notice Revokes attestations [identifiers <-> accounts] from issuer - * @param issuer Address of the issuer of all attestations to be revoked - * @param identifiers Hash of the identifiers - * @param accounts Addresses of the accounts mapped to the identifiers - * at the same indices - * @dev Throws if the number of identifiers and accounts is not the same - * @dev Throws if sender is not the issuer or currently registered signer of issuer - * @dev Throws if an attestation is not found for identifiers[i] <-> accounts[i] - */ - function batchRevokeAttestations( - address issuer, - bytes32[] calldata identifiers, - address[] calldata accounts - ) external { - // TODO ASv2 Reviewers: we are planning to provide sensible limits in the SDK - // to prevent out of gas errors -- is that sufficient or should we limit here as well? - require(identifiers.length == accounts.length, "Unequal number of identifiers and accounts"); - require( - issuer == msg.sender || getAccounts().attestationSignerToAccount(msg.sender) == issuer, - "Sender does not have permission to revoke attestations from this issuer" - ); - - for (uint256 i = 0; i < identifiers.length; i = i.add(1)) { - _revokeAttestation(identifiers[i], issuer, accounts[i]); - } - } - - function getUniqueAttestationHash( - bytes32 identifier, - address issuer, - address account, - address signer, - uint64 issuedOn - ) public pure returns (bytes32) { - return keccak256(abi.encode(identifier, issuer, account, signer, issuedOn)); - } - /** * @notice Revokes an attestation: * helper function for revokeAttestation and batchRevokeAttestations @@ -509,10 +477,6 @@ contract FederatedAttestations is attestation.signer, attestation.issuedOn ); - // Should never be able to re-revoke an attestation - // TODO reviewers: removing this storage lookup saves about 20k gas - // for 100 batch-deleted attestations - assert(!revokedAttestations[attestationHash]); revokedAttestations[attestationHash] = true; emit AttestationRevoked( diff --git a/packages/protocol/test/identity/federatedattestations.ts b/packages/protocol/test/identity/federatedattestations.ts index 0840a5de3dd..ba2ab41d97a 100644 --- a/packages/protocol/test/identity/federatedattestations.ts +++ b/packages/protocol/test/identity/federatedattestations.ts @@ -194,13 +194,13 @@ contract('FederatedAttestations', (accounts: string[]) => { ) }) - describe('#EIP712_VALIDATE_ATTESTATION_TYPEHASH()', () => { + describe('#EIP712_OWNERSHIP_ATTESTATION_TYPEHASH()', () => { it('should have set the right typehash', async () => { const expectedTypehash = keccak256( 'OwnershipAttestation(bytes32 identifier,address issuer,address account,uint64 issuedOn)' ) assert.equal( - await federatedAttestations.EIP712_VALIDATE_ATTESTATION_TYPEHASH(), + await federatedAttestations.EIP712_OWNERSHIP_ATTESTATION_TYPEHASH(), expectedTypehash ) }) From e043c47409b49d360421d7a8f6abca48a89bb872 Mon Sep 17 00:00:00 2001 From: isabellewei Date: Thu, 23 Jun 2022 05:03:02 -0400 Subject: [PATCH 16/30] [ASv2] update attestation hash (#9659) * use eip712 hash for unique attestation hash * add signer to eip712 typed data * oops * lint * concaatenate strings --- .../identity/FederatedAttestations.sol | 21 ++++++++++++++----- .../protocol/lib/fed-attestations-utils.ts | 4 +++- .../test/identity/federatedattestations.ts | 2 +- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/packages/protocol/contracts/identity/FederatedAttestations.sol b/packages/protocol/contracts/identity/FederatedAttestations.sol index 6774281773a..d4c9768da20 100644 --- a/packages/protocol/contracts/identity/FederatedAttestations.sol +++ b/packages/protocol/contracts/identity/FederatedAttestations.sol @@ -44,7 +44,10 @@ contract FederatedAttestations is mapping(bytes32 => bool) public revokedAttestations; bytes32 public constant EIP712_OWNERSHIP_ATTESTATION_TYPEHASH = keccak256( - "OwnershipAttestation(bytes32 identifier,address issuer,address account,uint64 issuedOn)" + abi.encodePacked( + "OwnershipAttestation(bytes32 identifier,address issuer,", + "address account,address signer,uint64 issuedOn)" + ) ); bytes32 public eip712DomainSeparator; @@ -291,9 +294,7 @@ contract FederatedAttestations is getAccounts().attestationSignerToAccount(signer) == issuer, "Signer is not a currently authorized AttestationSigner for the issuer" ); - bytes32 structHash = keccak256( - abi.encode(EIP712_OWNERSHIP_ATTESTATION_TYPEHASH, identifier, issuer, account, issuedOn) - ); + bytes32 structHash = getUniqueAttestationHash(identifier, issuer, account, signer, issuedOn); address guessedSigner = Signatures.getSignerOfTypedDataHash( eip712DomainSeparator, structHash, @@ -311,7 +312,17 @@ contract FederatedAttestations is address signer, uint64 issuedOn ) public pure returns (bytes32) { - return keccak256(abi.encode(identifier, issuer, account, signer, issuedOn)); + return + keccak256( + abi.encode( + EIP712_OWNERSHIP_ATTESTATION_TYPEHASH, + identifier, + issuer, + account, + signer, + issuedOn + ) + ); } /** diff --git a/packages/protocol/lib/fed-attestations-utils.ts b/packages/protocol/lib/fed-attestations-utils.ts index 135919cdff2..e23b39d3480 100644 --- a/packages/protocol/lib/fed-attestations-utils.ts +++ b/packages/protocol/lib/fed-attestations-utils.ts @@ -8,6 +8,7 @@ export interface AttestationDetails{ identifier: string, issuer: string, account: string, + signer: string, issuedOn: number, } @@ -24,6 +25,7 @@ const getTypedData = (chainId: number, contractAddress: Address, message?: Attes { name: 'identifier', type: 'bytes32' }, { name: 'issuer', type: 'address'}, { name: 'account', type: 'address' }, + { name: 'signer', type: 'address' }, { name: 'issuedOn', type: 'uint64' }, ], }, @@ -48,7 +50,7 @@ export const getSignatureForAttestation = async ( chainId: number, contractAddress: string ) => { - const typedData = getTypedData(chainId, contractAddress, { identifier,issuer,account, issuedOn}) + const typedData = getTypedData(chainId, contractAddress, { identifier,issuer,account, signer, issuedOn}) const signature = await new Promise((resolve, reject) => { web3.currentProvider.send( diff --git a/packages/protocol/test/identity/federatedattestations.ts b/packages/protocol/test/identity/federatedattestations.ts index ba2ab41d97a..09fdd5448b3 100644 --- a/packages/protocol/test/identity/federatedattestations.ts +++ b/packages/protocol/test/identity/federatedattestations.ts @@ -197,7 +197,7 @@ contract('FederatedAttestations', (accounts: string[]) => { describe('#EIP712_OWNERSHIP_ATTESTATION_TYPEHASH()', () => { it('should have set the right typehash', async () => { const expectedTypehash = keccak256( - 'OwnershipAttestation(bytes32 identifier,address issuer,address account,uint64 issuedOn)' + 'OwnershipAttestation(bytes32 identifier,address issuer,address account,address signer,uint64 issuedOn)' ) assert.equal( await federatedAttestations.EIP712_OWNERSHIP_ATTESTATION_TYPEHASH(), From cec9e5f1e3709c45bd0e590a1b721199c1ab25a8 Mon Sep 17 00:00:00 2001 From: Eela Nagaraj <7308464+eelanagaraj@users.noreply.github.com> Date: Thu, 23 Jun 2022 13:26:44 +0200 Subject: [PATCH 17/30] Add limits to attestation and identifier arrays (#9656) * Add hard constraints on identifiers and accounts arrays * Nit: fix test case description * WIP: benchmarking storage vs copying for batch revocation * wip: store ugly benchmarking code * Use 20 as limit for attestations and identifiers * Nit: rename variable --- .../identity/FederatedAttestations.sol | 47 ++++++++++------ .../test/identity/federatedattestations.ts | 54 +++++++++++++++++++ 2 files changed, 85 insertions(+), 16 deletions(-) diff --git a/packages/protocol/contracts/identity/FederatedAttestations.sol b/packages/protocol/contracts/identity/FederatedAttestations.sol index d4c9768da20..8c42411d8d6 100644 --- a/packages/protocol/contracts/identity/FederatedAttestations.sol +++ b/packages/protocol/contracts/identity/FederatedAttestations.sol @@ -43,13 +43,16 @@ contract FederatedAttestations is // unique attestation hash -> isRevoked mapping(bytes32 => bool) public revokedAttestations; + bytes32 public eip712DomainSeparator; bytes32 public constant EIP712_OWNERSHIP_ATTESTATION_TYPEHASH = keccak256( abi.encodePacked( "OwnershipAttestation(bytes32 identifier,address issuer,", "address account,address signer,uint64 issuedOn)" ) ); - bytes32 public eip712DomainSeparator; + + uint256 public constant MAX_ATTESTATIONS_PER_IDENTIFIER = 20; + uint256 public constant MAX_IDENTIFIERS_PER_ADDRESS = 20; event EIP712DomainSeparatorSet(bytes32 eip712DomainSeparator); event AttestationRegistered( @@ -419,7 +422,17 @@ contract FederatedAttestations is !revokedAttestations[getUniqueAttestationHash(identifier, issuer, account, signer, issuedOn)], "Attestation has been revoked" ); - for (uint256 i = 0; i < identifierToAttestations[identifier][issuer].length; i = i.add(1)) { + uint256 numExistingAttestations = identifierToAttestations[identifier][issuer].length; + require( + numExistingAttestations.add(1) <= MAX_ATTESTATIONS_PER_IDENTIFIER, + "Max attestations already registered for identifier" + ); + require( + addressToIdentifiers[account][issuer].length.add(1) <= MAX_IDENTIFIERS_PER_ADDRESS, + "Max identifiers already registered for account" + ); + + for (uint256 i = 0; i < numExistingAttestations; i = i.add(1)) { // This enforces only one attestation to be uploaded // for a given set of (identifier, issuer, account) // Editing/upgrading an attestation requires that it be revoked before a new one is registered @@ -448,33 +461,35 @@ contract FederatedAttestations is * @param account Address of the account mapped to the identifier * @dev Reverts if attestation is not found mapping identifier <-> account */ - function _revokeAttestation(bytes32 identifier, address issuer, address account) private { - OwnershipAttestation[] memory attestations = identifierToAttestations[identifier][issuer]; - for (uint256 i = 0; i < attestations.length; i = i.add(1)) { - OwnershipAttestation memory attestation = attestations[i]; - if (attestation.account != account) { + OwnershipAttestation[] storage attestations = identifierToAttestations[identifier][issuer]; + uint256 lenAttestations = attestations.length; + for (uint256 i = 0; i < lenAttestations; i = i.add(1)) { + if (attestations[i].account != account) { continue; } - // This is meant to delete the attestation in the array + OwnershipAttestation memory attestation = attestations[i]; + // This is meant to delete the attestations in the array // and then move the last element in the array to that empty spot, // to avoid having empty elements in the array - if (i != attestations.length - 1) { - identifierToAttestations[identifier][issuer][i] = attestations[attestations.length - 1]; + if (i != lenAttestations - 1) { + attestations[i] = attestations[lenAttestations - 1]; } - identifierToAttestations[identifier][issuer].pop(); + attestations.pop(); bool deletedIdentifier = false; - bytes32[] memory identifiers = addressToIdentifiers[account][issuer]; - for (uint256 j = 0; j < identifiers.length; j = j.add(1)) { + bytes32[] storage identifiers = addressToIdentifiers[account][issuer]; + uint256 lenIdentifiers = identifiers.length; + + for (uint256 j = 0; j < lenIdentifiers; j = j.add(1)) { if (identifiers[j] != identifier) { continue; } - if (j != identifiers.length - 1) { - addressToIdentifiers[account][issuer][j] = identifiers[identifiers.length - 1]; + if (j != lenIdentifiers - 1) { + identifiers[j] = identifiers[lenIdentifiers - 1]; } - addressToIdentifiers[account][issuer].pop(); + identifiers.pop(); deletedIdentifier = true; break; } diff --git a/packages/protocol/test/identity/federatedattestations.ts b/packages/protocol/test/identity/federatedattestations.ts index 09fdd5448b3..50d733499af 100644 --- a/packages/protocol/test/identity/federatedattestations.ts +++ b/packages/protocol/test/identity/federatedattestations.ts @@ -930,6 +930,60 @@ contract('FederatedAttestations', (accounts: string[]) => { ) ) }) + + it('should revert if MAX_ATTESTATIONS_PER_IDENTIFIER have already been registered', async () => { + for ( + let i = 0; + i < (await federatedAttestations.MAX_ATTESTATIONS_PER_IDENTIFIER()).toNumber(); + i++ + ) { + // accounts[n] is limited + const newAccount = await web3.eth.accounts.create().address + await federatedAttestations.registerAttestationAsIssuer( + identifier1, + issuer1, + newAccount, + nowUnixTime, + { from: issuer1 } + ) + } + await assertRevertWithReason( + federatedAttestations.registerAttestationAsIssuer( + identifier1, + issuer1, + account1, + nowUnixTime, + { from: issuer1 } + ), + 'Max attestations already registered for identifier' + ) + }) + it('should revert if MAX_IDENTIFIERS_PER_ADDRESS have already been registered', async () => { + for ( + let i = 0; + i < (await federatedAttestations.MAX_IDENTIFIERS_PER_ADDRESS()).toNumber(); + i++ + ) { + const newIdentifier = getPhoneHash(phoneNumber, `dummysalt-${i}`) + await federatedAttestations.registerAttestationAsIssuer( + newIdentifier, + issuer1, + account1, + nowUnixTime, + { from: issuer1 } + ) + } + await assertRevertWithReason( + federatedAttestations.registerAttestationAsIssuer( + identifier1, + issuer1, + account1, + nowUnixTime, + { from: issuer1 } + ), + 'Max identifiers already registered for account' + ) + }) }) describe('#revokeAttestation', () => { From 22aafb13200790dd51afe9ddb0de1abb0ffe2ad2 Mon Sep 17 00:00:00 2001 From: Eela Nagaraj <7308464+eelanagaraj@users.noreply.github.com> Date: Thu, 23 Jun 2022 14:00:06 +0200 Subject: [PATCH 18/30] PR fixes (comments, docstrings, remove FA from UsingRegistry) (#9660) * PR fixes: comments, docstrings, remove FA from UsingRegistry * Use defaultTrustedIssuers directly instead of getter * Remove duplicate revocation test case * Remove stale TODOs * Add comments and spacing --- .../contracts/common/UsingRegistry.sol | 8 -------- .../protocol/contracts/identity/Escrow.sol | 12 +++++------- .../identity/FederatedAttestations.sol | 10 +++++----- .../test/identity/federatedattestations.ts | 19 ------------------- 4 files changed, 10 insertions(+), 39 deletions(-) diff --git a/packages/protocol/contracts/common/UsingRegistry.sol b/packages/protocol/contracts/common/UsingRegistry.sol index 15f00a61c82..c2b5109e6cf 100644 --- a/packages/protocol/contracts/common/UsingRegistry.sol +++ b/packages/protocol/contracts/common/UsingRegistry.sol @@ -15,7 +15,6 @@ import "../governance/interfaces/IValidators.sol"; import "../identity/interfaces/IRandom.sol"; import "../identity/interfaces/IAttestations.sol"; -import "../identity/interfaces/IFederatedAttestations.sol"; import "../stability/interfaces/IExchange.sol"; import "../stability/interfaces/IReserve.sol"; @@ -37,9 +36,6 @@ contract UsingRegistry is Ownable { bytes32 constant FEE_CURRENCY_WHITELIST_REGISTRY_ID = keccak256( abi.encodePacked("FeeCurrencyWhitelist") ); - bytes32 constant FEDERATED_ATTESTATIONS_REGISTRY_ID = keccak256( - abi.encodePacked("FederatedAttestations") - ); bytes32 constant FREEZER_REGISTRY_ID = keccak256(abi.encodePacked("Freezer")); bytes32 constant GOLD_TOKEN_REGISTRY_ID = keccak256(abi.encodePacked("GoldToken")); bytes32 constant GOVERNANCE_REGISTRY_ID = keccak256(abi.encodePacked("Governance")); @@ -96,10 +92,6 @@ contract UsingRegistry is Ownable { return IFeeCurrencyWhitelist(registry.getAddressForOrDie(FEE_CURRENCY_WHITELIST_REGISTRY_ID)); } - function getFederatedAttestations() internal view returns (IFederatedAttestations) { - return IFederatedAttestations(registry.getAddressForOrDie(FEDERATED_ATTESTATIONS_REGISTRY_ID)); - } - function getFreezer() internal view returns (IFreezer) { return IFreezer(registry.getAddressForOrDie(FREEZER_REGISTRY_ID)); } diff --git a/packages/protocol/contracts/identity/Escrow.sol b/packages/protocol/contracts/identity/Escrow.sol index a35a77f5b94..e314c944021 100644 --- a/packages/protocol/contracts/identity/Escrow.sol +++ b/packages/protocol/contracts/identity/Escrow.sol @@ -148,6 +148,8 @@ contract Escrow is /** * @notice Transfer tokens to a specific user. Supports both identity with privacy (an empty * identifier and 0 minAttestations) and without (with identifier and minAttestations). + * Sets trustedIssuers to the issuers listed in `defaultTrustedIssuers`. + * (To override this and set custom trusted issuers, use `transferWithTrustedIssuers`.) * @param identifier The hashed identifier of a user to transfer to. * @param token The token to be transferred. * @param value The amount to be transferred. @@ -160,8 +162,6 @@ contract Escrow is * @dev Throws if identifier is null and minAttestations > 0. * @dev If minAttestations is 0, trustedIssuers will be set to empty list. * @dev msg.sender needs to have already approved this contract to transfer - - */ // solhint-disable-next-line no-simple-event-func-name function transfer( @@ -175,7 +175,7 @@ contract Escrow is address[] memory trustedIssuers; // If minAttestations == 0, trustedIssuers should remain empty if (minAttestations > 0) { - trustedIssuers = getDefaultTrustedIssuers(); + trustedIssuers = defaultTrustedIssuers; } return _transfer( @@ -307,7 +307,6 @@ contract Escrow is EscrowedPayment memory payment = escrowedPayments[paymentId]; require(payment.sender == msg.sender, "Only sender of payment can attempt to revoke payment."); require( - // solhint-disable-next-line not-rely-on-time now >= (payment.timestamp.add(payment.expirySeconds)), "Transaction not redeemable for sender yet." ); @@ -350,7 +349,7 @@ contract Escrow is /** * @notice Gets array of all trusted issuers set per paymentId. - * @param paymentId The ID of the payment to be deleted. + * @param paymentId The ID of the payment to get. * @return An array of addresses of trusted issuers set for an escrowed payment. */ function getTrustedIssuersPerPayment(address paymentId) external view returns (address[] memory) { @@ -501,8 +500,7 @@ contract Escrow is newPayment.value = value; newPayment.sentIndex = sentIndex; newPayment.receivedIndex = receivedIndex; - // solhint-disable-next-line not-rely-on-time - newPayment.timestamp = now; + newPayment.timestamp = block.timestamp; newPayment.expirySeconds = expirySeconds; newPayment.minAttestations = minAttestations; diff --git a/packages/protocol/contracts/identity/FederatedAttestations.sol b/packages/protocol/contracts/identity/FederatedAttestations.sol index 8c42411d8d6..ef08b975afc 100644 --- a/packages/protocol/contracts/identity/FederatedAttestations.sol +++ b/packages/protocol/contracts/identity/FederatedAttestations.sol @@ -34,12 +34,16 @@ contract FederatedAttestations is // using uint64 to allow for extra space to add parameters } - // TODO ASv2 revisit linting issues & all solhint-disable-next-line max-line-length + // Mappings from identifier <-> attestation are separated by issuer, + // *requiring* users to specify issuers when retrieving attestations. + // Maintaining bidirectional mappings (vs. in Attestations.sol) makes it possible + // to perform lookups by identifier or account without indexing event data. // identifier -> issuer -> attestations mapping(bytes32 => mapping(address => OwnershipAttestation[])) public identifierToAttestations; // account -> issuer -> identifiers mapping(address => mapping(address => bytes32[])) public addressToIdentifiers; + // unique attestation hash -> isRevoked mapping(bytes32 => bool) public revokedAttestations; @@ -174,8 +178,6 @@ contract FederatedAttestations is bytes32[] calldata identifiers, address[] calldata accounts ) external { - // TODO ASv2 Reviewers: we are planning to provide sensible limits in the SDK - // to prevent out of gas errors -- is that sufficient or should we limit here as well? require(identifiers.length == accounts.length, "Unequal number of identifiers and accounts"); require( issuer == msg.sender || getAccounts().attestationSignerToAccount(msg.sender) == issuer, @@ -201,8 +203,6 @@ contract FederatedAttestations is * @dev Adds attestation info to the arrays in order of provided trustedIssuers * @dev Expectation that only one attestation exists per (identifier, issuer, account) */ - // TODO reviewers: is it preferable to return an array of `trustedIssuer` indices - // (indicating issuer per attestation) instead of counts per attestation? function lookupAttestations(bytes32 identifier, address[] calldata trustedIssuers) external view diff --git a/packages/protocol/test/identity/federatedattestations.ts b/packages/protocol/test/identity/federatedattestations.ts index 50d733499af..b9a3b605eb2 100644 --- a/packages/protocol/test/identity/federatedattestations.ts +++ b/packages/protocol/test/identity/federatedattestations.ts @@ -694,25 +694,6 @@ contract('FederatedAttestations', (accounts: string[]) => { assert.isAtLeast(publishedOn.toNumber(), publishedOnLowerBound) }) - it('should revert if the attestation is revoked', async () => { - await signAndRegisterAttestation(identifier1, issuer1, account1, nowUnixTime, signer1) - await federatedAttestations.revokeAttestation(identifier1, issuer1, account1, { - from: issuer1, - }) - await assertRevert( - federatedAttestations.registerAttestation( - identifier1, - issuer1, - account1, - signer1, - nowUnixTime, - sig.v, - sig.r, - sig.s - ) - ) - }) - it('should succeed if issuer == signer', async () => { await signAndRegisterAttestation(identifier1, issuer1, account1, nowUnixTime, issuer1) await assertAttestationInStorage(identifier1, issuer1, 0, account1, nowUnixTime, issuer1, 0) From e3911d23ca9639a4802244b54c532845c2bce828 Mon Sep 17 00:00:00 2001 From: isabellewei Date: Thu, 23 Jun 2022 10:34:49 -0400 Subject: [PATCH 19/30] Add functions to interfaces for FederatedAttestations and Escrow (#9655) * add fns to interface for FA and Escrow * add pure function * fn param names everywhere --- .../contracts/identity/interfaces/IEscrow.sol | 19 +++++-- .../interfaces/IFederatedAttestations.sol | 50 +++++++++++++++++-- 2 files changed, 63 insertions(+), 6 deletions(-) diff --git a/packages/protocol/contracts/identity/interfaces/IEscrow.sol b/packages/protocol/contracts/identity/interfaces/IEscrow.sol index 7d94f525fae..b7590e3c392 100644 --- a/packages/protocol/contracts/identity/interfaces/IEscrow.sol +++ b/packages/protocol/contracts/identity/interfaces/IEscrow.sol @@ -9,12 +9,25 @@ interface IEscrow { address paymentId, uint256 minAttestations ) external returns (bool); - + function transferWithTrustedIssuers( + bytes32 identifier, + address token, + uint256 value, + uint256 expirySeconds, + address paymentId, + uint256 minAttestations, + address[] calldata trustedIssuers + ) external returns (bool); function withdraw(address paymentID, uint8 v, bytes32 r, bytes32 s) external returns (bool); - function revoke(address paymentID) external returns (bool); + // view functions function getReceivedPaymentIds(bytes32 identifier) external view returns (address[] memory); - function getSentPaymentIds(address sender) external view returns (address[] memory); + function getTrustedIssuersPerPayment(address paymentId) external view returns (address[] memory); + function getDefaultTrustedIssuers() external view returns (address[] memory); + + // onlyOwner functions + function addDefaultTrustedIssuer(address trustedIssuer) external; + function removeDefaultTrustedIssuer(address trustedIssuer, uint256 index) external; } diff --git a/packages/protocol/contracts/identity/interfaces/IFederatedAttestations.sol b/packages/protocol/contracts/identity/interfaces/IFederatedAttestations.sol index 835c1fa8f5b..27b0cb447ec 100644 --- a/packages/protocol/contracts/identity/interfaces/IFederatedAttestations.sol +++ b/packages/protocol/contracts/identity/interfaces/IFederatedAttestations.sol @@ -1,9 +1,31 @@ pragma solidity ^0.5.13; -// TODO ASv2 add external, view, and only owner function sigs -// separated into these three groups for clarity interface IFederatedAttestations { - function lookupAttestations(bytes32, address[] calldata) + function registerAttestationAsIssuer( + bytes32 identifier, + address issuer, + address account, + uint64 issuedOn + ) external; + function registerAttestation( + bytes32 identifier, + address issuer, + address account, + address signer, + uint64 issuedOn, + uint8 v, + bytes32 r, + bytes32 s + ) external; + function revokeAttestation(bytes32 identifier, address issuer, address account) external; + function batchRevokeAttestations( + address issuer, + bytes32[] calldata identifiers, + address[] calldata accounts + ) external; + + // view functions + function lookupAttestations(bytes32 identifier, address[] calldata trustedIssuers) external view returns ( @@ -13,4 +35,26 @@ interface IFederatedAttestations { uint64[] memory, uint64[] memory ); + function lookupIdentifiers(address account, address[] calldata trustedIssuers) + external + view + returns (uint256[] memory, bytes32[] memory); + + function validateAttestationSig( + bytes32 identifier, + address issuer, + address account, + address signer, + uint64 issuedOn, + uint8 v, + bytes32 r, + bytes32 s + ) external view; + function getUniqueAttestationHash( + bytes32 identifier, + address issuer, + address account, + address signer, + uint64 issuedOn + ) external pure returns (bytes32); } From 4ee46b8aea71774567c788ce1555211de2fe1f36 Mon Sep 17 00:00:00 2001 From: Eela Nagaraj <7308464+eelanagaraj@users.noreply.github.com> Date: Fri, 24 Jun 2022 17:21:24 +0200 Subject: [PATCH 20/30] Improve bignumber handling in escrow and federatedattestations tests (#9633) * Improve bignumber handling in escrow and federatedattestations tests * fix up merge * Add comments and fix line spacing * Remove stale TODO Co-authored-by: Isabelle Wei --- .../protocol/contracts/identity/Escrow.sol | 2 - packages/protocol/lib/test-utils.ts | 34 +++-- packages/protocol/test/identity/escrow.ts | 121 +++++++----------- .../test/identity/federatedattestations.ts | 10 +- 4 files changed, 75 insertions(+), 92 deletions(-) diff --git a/packages/protocol/contracts/identity/Escrow.sol b/packages/protocol/contracts/identity/Escrow.sol index e314c944021..4f3a76ad2a3 100644 --- a/packages/protocol/contracts/identity/Escrow.sol +++ b/packages/protocol/contracts/identity/Escrow.sol @@ -536,8 +536,6 @@ contract Escrow is delete escrowedPayments[paymentId]; delete trustedIssuersPerPayment[paymentId]; - // TODO ASv2 reviewers: adding trustedIssuers to event requires an additional - // storage lookup, but we can add this in if it's still best pratice to do so! emit TrustedIssuersUnset(paymentId); } } diff --git a/packages/protocol/lib/test-utils.ts b/packages/protocol/lib/test-utils.ts index 5477ed5ecae..1194eb77751 100644 --- a/packages/protocol/lib/test-utils.ts +++ b/packages/protocol/lib/test-utils.ts @@ -286,23 +286,31 @@ export function assertLogMatches( args: Record ) { assert.equal(log.event, event, `Log event name doesn\'t match`) + assertObjectWithBNEqual(log.args, args, (arg) => `Event ${event}, arg: ${arg} do not match`) +} - const logArgs = Object.keys(log.args) +// Compares objects' properties, using assertBNEqual to compare BN fields. +// Extracted out of previous `assertLogMatches`. +export function assertObjectWithBNEqual( + actual: object, + expected: Record, + fieldErrorMsg: (field?: string) => string, +) { + const objectFields = Object.keys(actual) .filter((k) => k !== '__length__' && isNaN(parseInt(k, 10))) .sort() - assert.deepEqual(logArgs, Object.keys(args).sort(), `Argument names do not match for ${event}`) - - for (const k of logArgs) { - const errorMsg = `Event ${event}, arg: ${k} do not match` - if (typeof args[k] === 'function') { - args[k](log.args[k], errorMsg) - } else if (isNumber(log.args[k]) || isNumber(args[k])) { - assertEqualBN(log.args[k], args[k], errorMsg) - } else if (Array.isArray(log.args[k])) { - assert.deepEqual(log.args[k], args[k], errorMsg) - } else { - assert.equal(log.args[k], args[k], errorMsg) + assert.deepEqual(objectFields, Object.keys(expected).sort(), `Argument names do not match`) + for (const k of objectFields) { + if (typeof expected[k] === 'function') { + expected[k](actual[k], fieldErrorMsg(k)) + } else if (isNumber(actual[k]) || isNumber(expected[k])) { + assertEqualBN(actual[k], expected[k], fieldErrorMsg(k)) + } else if (Array.isArray(actual[k])) { + assert.deepEqual(actual[k], expected[k], fieldErrorMsg(k)) + } + else { + assert.equal(actual[k], expected[k], fieldErrorMsg(k)) } } } diff --git a/packages/protocol/test/identity/escrow.ts b/packages/protocol/test/identity/escrow.ts index 1f9c4100afa..9807a76655e 100644 --- a/packages/protocol/test/identity/escrow.ts +++ b/packages/protocol/test/identity/escrow.ts @@ -2,13 +2,16 @@ import { NULL_ADDRESS } from '@celo/base/lib/address' import getPhoneHash from '@celo/phone-utils/lib/getPhoneHash' import { CeloContractName } from '@celo/protocol/lib/registry-utils' import { + assertEqualBN, assertLogMatches2, + assertObjectWithBNEqual, assertRevert, assertRevertWithReason, assumeOwnership, timeTravel, } from '@celo/protocol/lib/test-utils' import { getDeployedProxiedContract } from '@celo/protocol/lib/web3-utils' +import BN from 'bn.js' import { EscrowContract, EscrowInstance, @@ -34,7 +37,7 @@ const NULL_ESCROWED_PAYMENT: EscrowedPayment = { recipientIdentifier: NULL_BYTES32, sender: NULL_ADDRESS, token: NULL_ADDRESS, - value: 0, + value: new BN(0), sentIndex: 0, receivedIndex: 0, timestamp: 0, @@ -45,7 +48,7 @@ interface EscrowedPayment { recipientIdentifier: string sender: string token: string - value: number + value: BN sentIndex: number receivedIndex: number timestamp: number @@ -62,10 +65,11 @@ const getEscrowedPayment = async ( recipientIdentifier: payment[0], sender: payment[1], token: payment[2], - value: payment[3].toNumber(), + // numbers expected to be small such as indices are directly casted to numbers + value: web3.utils.toBN(payment[3]), sentIndex: payment[4].toNumber(), receivedIndex: payment[5].toNumber(), - timestamp: payment[6].toNumber(), + timestamp: web3.utils.toBN(payment[6].toNumber()), expirySeconds: payment[7].toNumber(), minAttestations: payment[8].toNumber(), } @@ -257,11 +261,14 @@ contract('Escrow', (accounts: string[]) => { expectedSentPaymentIds: string[], expectedReceivedPaymentIds: string[] ) => { - const startingEscrowContractBalance = ( + const startingEscrowContractBalance: BN = web3.utils.toBN( await mockERC20Token.balanceOf(escrow.address) - ).toNumber() - const startingSenderBalance = (await mockERC20Token.balanceOf(escrowSender)).toNumber() - await escrow.transferWithTrustedIssuers( + ) + const startingSenderBalance: BN = web3.utils.toBN( + await mockERC20Token.balanceOf(escrowSender) + ) + + await await escrow.transferWithTrustedIssuers( identifier, mockERC20Token.address, value, @@ -272,20 +279,19 @@ contract('Escrow', (accounts: string[]) => { { from: escrowSender } ) const escrowedPayment = await getEscrowedPayment(paymentId, escrow) - assert.equal( + assertEqualBN( escrowedPayment.value, value, 'incorrect escrowedPayment.value in payment struct' ) - - assert.equal( - (await mockERC20Token.balanceOf(escrowSender)).toNumber(), - startingSenderBalance - value, + assertEqualBN( + await mockERC20Token.balanceOf(escrowSender), + startingSenderBalance.subn(value), 'incorrect final sender balance' ) - assert.equal( - (await mockERC20Token.balanceOf(escrow.address)).toNumber(), - startingEscrowContractBalance + value, + assertEqualBN( + await mockERC20Token.balanceOf(escrow.address), + startingEscrowContractBalance.addn(value), 'incorrect final Escrow contract balance' ) @@ -692,10 +698,10 @@ contract('Escrow', (accounts: string[]) => { ) } const deletedEscrowedPayment = await getEscrowedPayment(deletedPaymentId, escrow) - assert.deepEqual( + assertObjectWithBNEqual( deletedEscrowedPayment, NULL_ESCROWED_PAYMENT, - 'escrowedPayment not zeroed out' + (field) => `escrowedPayment not zeroed out for field: ${field}` ) const trustedIssuersPerPayment = await escrow.getTrustedIssuersPerPayment(deletedPaymentId) assert.deepEqual(trustedIssuersPerPayment, [], 'trustedIssuersPerPayment not zeroed out') @@ -721,32 +727,31 @@ contract('Escrow', (accounts: string[]) => { escrowSender: string, escrowReceiver: string, identifier: string, - value: number, paymentId: string, expectedSentPaymentIds: string[], expectedReceivedPaymentIds: string[] ) => { - const receiverBalanceBefore = (await mockERC20Token.balanceOf(escrowReceiver)).toNumber() - const escrowContractBalanceBefore = ( + const receiverBalanceBefore: BN = web3.utils.toBN( + await mockERC20Token.balanceOf(escrowReceiver) + ) + const escrowContractBalanceBefore: BN = web3.utils.toBN( await mockERC20Token.balanceOf(escrow.address) - ).toNumber() + ) const paymentBefore = await getEscrowedPayment(paymentId, escrow) const parsedSig = await getParsedSignatureOfAddress(web3, escrowReceiver, paymentId) await escrow.withdraw(paymentId, parsedSig.v, parsedSig.r, parsedSig.s, { from: escrowReceiver, }) - assert.equal( - (await mockERC20Token.balanceOf(escrowReceiver)).toNumber(), - receiverBalanceBefore + value, + assertEqualBN( + await mockERC20Token.balanceOf(escrowReceiver), + receiverBalanceBefore.add(paymentBefore.value), 'incorrect final receiver balance' ) - - assert.equal( - (await mockERC20Token.balanceOf(escrow.address)).toNumber(), - escrowContractBalanceBefore - value, + assertEqualBN( + await mockERC20Token.balanceOf(escrow.address), + escrowContractBalanceBefore.sub(paymentBefore.value), 'incorrect final Escrow contract balance' ) - await checkStateAfterDeletingPayment( paymentId, paymentBefore, @@ -791,7 +796,6 @@ contract('Escrow', (accounts: string[]) => { sender, receiver, NULL_BYTES32, - aValue, uniquePaymentIDWithdraw, [], [] @@ -858,7 +862,6 @@ contract('Escrow', (accounts: string[]) => { sender, receiver, NULL_BYTES32, - aValue, uniquePaymentIDWithdraw, [anotherWithdrawKeyAddress], [anotherWithdrawKeyAddress] @@ -878,7 +881,6 @@ contract('Escrow', (accounts: string[]) => { sender, receiver, NULL_BYTES32, - aValue, uniquePaymentIDWithdraw, [anotherWithdrawKeyAddress], [] @@ -916,28 +918,12 @@ contract('Escrow', (accounts: string[]) => { it('should allow users to withdraw after completing attestations', async () => { await completeAttestations(receiver, aPhoneHash, minAttestations) - await withdrawAndCheckState( - sender, - receiver, - aPhoneHash, - aValue, - uniquePaymentIDWithdraw, - [], - [] - ) + await withdrawAndCheckState(sender, receiver, aPhoneHash, uniquePaymentIDWithdraw, [], []) }) it('should not allow a user to withdraw a payment if they have fewer than minAttestations', async () => { await completeAttestations(receiver, aPhoneHash, minAttestations - 1) await assertRevertWithReason( - withdrawAndCheckState( - sender, - receiver, - aPhoneHash, - aValue, - uniquePaymentIDWithdraw, - [], - [] - ), + withdrawAndCheckState(sender, receiver, aPhoneHash, uniquePaymentIDWithdraw, [], []), 'This account does not have the required attestations to withdraw this payment.' ) }) @@ -956,7 +942,6 @@ contract('Escrow', (accounts: string[]) => { sender, receiver, aPhoneHash, - aValue, uniquePaymentIDWithdraw, [anotherWithdrawKeyAddress], [anotherWithdrawKeyAddress] @@ -985,7 +970,6 @@ contract('Escrow', (accounts: string[]) => { sender, receiver, aPhoneHash, - aValue, uniquePaymentIDWithdraw, [], [] @@ -999,7 +983,6 @@ contract('Escrow', (accounts: string[]) => { sender, receiver, aPhoneHash, - aValue, uniquePaymentIDWithdraw, [], [] @@ -1020,7 +1003,6 @@ contract('Escrow', (accounts: string[]) => { sender, receiver, aPhoneHash, - aValue, uniquePaymentIDWithdraw, [], [] @@ -1052,7 +1034,6 @@ contract('Escrow', (accounts: string[]) => { sender, receiver, aPhoneHash, - aValue, uniquePaymentIDWithdraw, [], [] @@ -1060,15 +1041,7 @@ contract('Escrow', (accounts: string[]) => { }) it('should not allow a user to withdraw a payment if no attestations exist for trustedIssuers', async () => { await assertRevertWithReason( - withdrawAndCheckState( - sender, - receiver, - aPhoneHash, - aValue, - uniquePaymentIDWithdraw, - [], - [] - ), + withdrawAndCheckState(sender, receiver, aPhoneHash, uniquePaymentIDWithdraw, [], []), 'This account does not have the required attestations to withdraw this payment.' ) }) @@ -1134,22 +1107,22 @@ contract('Escrow', (accounts: string[]) => { it('should allow sender to redeem payment after payment has expired', async () => { await timeTravel(oneDayInSecs, web3) - const senderBalanceBefore = (await mockERC20Token.balanceOf(sender)).toNumber() - const escrowContractBalanceBefore = ( + const senderBalanceBefore: BN = web3.utils.BN(await mockERC20Token.balanceOf(sender)) + const escrowContractBalanceBefore: BN = web3.utils.BN( await mockERC20Token.balanceOf(escrow.address) - ).toNumber() + ) const paymentBefore = await getEscrowedPayment(uniquePaymentIDRevoke, escrow) await escrow.revoke(uniquePaymentIDRevoke, { from: sender }) - assert.equal( - (await mockERC20Token.balanceOf(sender)).toNumber(), - senderBalanceBefore + aValue, + assertEqualBN( + await mockERC20Token.balanceOf(sender), + senderBalanceBefore.addn(aValue), 'incorrect final sender balance' ) - assert.equal( - (await mockERC20Token.balanceOf(escrow.address)).toNumber(), - escrowContractBalanceBefore - aValue, + assertEqualBN( + await mockERC20Token.balanceOf(escrow.address), + escrowContractBalanceBefore.subn(aValue), 'incorrect final Escrow contract balance' ) diff --git a/packages/protocol/test/identity/federatedattestations.ts b/packages/protocol/test/identity/federatedattestations.ts index b9a3b605eb2..5f6961e92be 100644 --- a/packages/protocol/test/identity/federatedattestations.ts +++ b/packages/protocol/test/identity/federatedattestations.ts @@ -5,7 +5,9 @@ import { } from '@celo/protocol/lib/fed-attestations-utils' import { CeloContractName } from '@celo/protocol/lib/registry-utils' import { + assertEqualBN, assertEqualBNArray, + assertGteBN, assertLogMatches2, assertRevert, assertRevertWithReason, @@ -157,9 +159,9 @@ contract('FederatedAttestations', (accounts: string[]) => { expectedAttestations.forEach((expectedAttestation, index) => { assert.equal(actualAddresses[index], expectedAttestation.account) assert.equal(actualSigners[index], expectedAttestation.signer) - assert.equal(actualIssuedOns[index].toNumber(), expectedAttestation.issuedOn) + assertEqualBN(actualIssuedOns[index], expectedAttestation.issuedOn) // Check min bounds for publishedOn - assert.isAtLeast(actualPublishedOns[index].toNumber(), expectedPublishedOnLowerBound) + assertGteBN(actualPublishedOns[index], expectedPublishedOnLowerBound) }) } @@ -691,7 +693,7 @@ contract('FederatedAttestations', (accounts: string[]) => { publishedOn, }, }) - assert.isAtLeast(publishedOn.toNumber(), publishedOnLowerBound) + assertGteBN(publishedOn, publishedOnLowerBound) }) it('should succeed if issuer == signer', async () => { @@ -915,6 +917,7 @@ contract('FederatedAttestations', (accounts: string[]) => { it('should revert if MAX_ATTESTATIONS_PER_IDENTIFIER have already been registered', async () => { for ( let i = 0; + // This should not overflow and if it does, the test should fail anyways i < (await federatedAttestations.MAX_ATTESTATIONS_PER_IDENTIFIER()).toNumber(); i++ ) { @@ -942,6 +945,7 @@ contract('FederatedAttestations', (accounts: string[]) => { it('should revert if MAX_IDENTIFIERS_PER_ADDRESS have already been registered', async () => { for ( let i = 0; + // This should not overflow and if it does, the test should fail anyways i < (await federatedAttestations.MAX_IDENTIFIERS_PER_ADDRESS()).toNumber(); i++ ) { From d3322f46d877db11ee705f22e7fb103085d10301 Mon Sep 17 00:00:00 2001 From: Isabelle Wei Date: Fri, 24 Jun 2022 11:56:09 -0400 Subject: [PATCH 21/30] add comment --- packages/protocol/contracts/identity/FederatedAttestations.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/protocol/contracts/identity/FederatedAttestations.sol b/packages/protocol/contracts/identity/FederatedAttestations.sol index ef08b975afc..1a0a762e5ac 100644 --- a/packages/protocol/contracts/identity/FederatedAttestations.sol +++ b/packages/protocol/contracts/identity/FederatedAttestations.sol @@ -293,6 +293,7 @@ contract FederatedAttestations is bytes32 r, bytes32 s ) public view { + // attestationSignerToAccount instead of isSigner allows the issuer to act as its own signer require( getAccounts().attestationSignerToAccount(signer) == issuer, "Signer is not a currently authorized AttestationSigner for the issuer" From 05f37854c4427f2a0e432dab7d54fa6ae3664130 Mon Sep 17 00:00:00 2001 From: Eela Nagaraj <7308464+eelanagaraj@users.noreply.github.com> Date: Wed, 20 Jul 2022 16:34:41 +0200 Subject: [PATCH 22/30] [L04] explicitly define scope of state vars in UsingRegistryV2 --- .../contracts/common/UsingRegistryV2.sol | 56 ++++++++++--------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/packages/protocol/contracts/common/UsingRegistryV2.sol b/packages/protocol/contracts/common/UsingRegistryV2.sol index 1972b54580d..b9d9291ae21 100644 --- a/packages/protocol/contracts/common/UsingRegistryV2.sol +++ b/packages/protocol/contracts/common/UsingRegistryV2.sol @@ -23,42 +23,48 @@ import "../stability/interfaces/ISortedOracles.sol"; import "../stability/interfaces/IStableToken.sol"; contract UsingRegistryV2 { - address constant registryAddress = 0x000000000000000000000000000000000000ce10; + address internal constant registryAddress = 0x000000000000000000000000000000000000ce10; IRegistry public constant registryContract = IRegistry(registryAddress); - // solhint-disable state-visibility - bytes32 constant ACCOUNTS_REGISTRY_ID = keccak256(abi.encodePacked("Accounts")); - bytes32 constant ATTESTATIONS_REGISTRY_ID = keccak256(abi.encodePacked("Attestations")); - bytes32 constant DOWNTIME_SLASHER_REGISTRY_ID = keccak256(abi.encodePacked("DowntimeSlasher")); - bytes32 constant DOUBLE_SIGNING_SLASHER_REGISTRY_ID = keccak256( + bytes32 internal constant ACCOUNTS_REGISTRY_ID = keccak256(abi.encodePacked("Accounts")); + bytes32 internal constant ATTESTATIONS_REGISTRY_ID = keccak256(abi.encodePacked("Attestations")); + bytes32 internal constant DOWNTIME_SLASHER_REGISTRY_ID = keccak256( + abi.encodePacked("DowntimeSlasher") + ); + bytes32 internal constant DOUBLE_SIGNING_SLASHER_REGISTRY_ID = keccak256( abi.encodePacked("DoubleSigningSlasher") ); - bytes32 constant ELECTION_REGISTRY_ID = keccak256(abi.encodePacked("Election")); - bytes32 constant EXCHANGE_REGISTRY_ID = keccak256(abi.encodePacked("Exchange")); - bytes32 constant EXCHANGE_EURO_REGISTRY_ID = keccak256(abi.encodePacked("ExchangeEUR")); - bytes32 constant EXCHANGE_REAL_REGISTRY_ID = keccak256(abi.encodePacked("ExchangeBRL")); + bytes32 internal constant ELECTION_REGISTRY_ID = keccak256(abi.encodePacked("Election")); + bytes32 internal constant EXCHANGE_REGISTRY_ID = keccak256(abi.encodePacked("Exchange")); + bytes32 internal constant EXCHANGE_EURO_REGISTRY_ID = keccak256(abi.encodePacked("ExchangeEUR")); + bytes32 internal constant EXCHANGE_REAL_REGISTRY_ID = keccak256(abi.encodePacked("ExchangeBRL")); - bytes32 constant FEE_CURRENCY_WHITELIST_REGISTRY_ID = keccak256( + bytes32 internal constant FEE_CURRENCY_WHITELIST_REGISTRY_ID = keccak256( abi.encodePacked("FeeCurrencyWhitelist") ); - bytes32 constant FEDERATED_ATTESTATIONS_REGISTRY_ID = keccak256( + bytes32 internal constant FEDERATED_ATTESTATIONS_REGISTRY_ID = keccak256( abi.encodePacked("FederatedAttestations") ); - bytes32 constant FREEZER_REGISTRY_ID = keccak256(abi.encodePacked("Freezer")); - bytes32 constant GOLD_TOKEN_REGISTRY_ID = keccak256(abi.encodePacked("GoldToken")); - bytes32 constant GOVERNANCE_REGISTRY_ID = keccak256(abi.encodePacked("Governance")); - bytes32 constant GOVERNANCE_SLASHER_REGISTRY_ID = keccak256( + bytes32 internal constant FREEZER_REGISTRY_ID = keccak256(abi.encodePacked("Freezer")); + bytes32 internal constant GOLD_TOKEN_REGISTRY_ID = keccak256(abi.encodePacked("GoldToken")); + bytes32 internal constant GOVERNANCE_REGISTRY_ID = keccak256(abi.encodePacked("Governance")); + bytes32 internal constant GOVERNANCE_SLASHER_REGISTRY_ID = keccak256( abi.encodePacked("GovernanceSlasher") ); - 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")); - bytes32 constant STABLE_TOKEN_REGISTRY_ID = keccak256(abi.encodePacked("StableToken")); - bytes32 constant STABLE_EURO_TOKEN_REGISTRY_ID = keccak256(abi.encodePacked("StableTokenEUR")); - bytes32 constant STABLE_REAL_TOKEN_REGISTRY_ID = keccak256(abi.encodePacked("StableTokenBRL")); - bytes32 constant VALIDATORS_REGISTRY_ID = keccak256(abi.encodePacked("Validators")); - // solhint-enable state-visibility + bytes32 internal constant LOCKED_GOLD_REGISTRY_ID = keccak256(abi.encodePacked("LockedGold")); + bytes32 internal constant RESERVE_REGISTRY_ID = keccak256(abi.encodePacked("Reserve")); + bytes32 internal constant RANDOM_REGISTRY_ID = keccak256(abi.encodePacked("Random")); + bytes32 internal constant SORTED_ORACLES_REGISTRY_ID = keccak256( + abi.encodePacked("SortedOracles") + ); + bytes32 internal constant STABLE_TOKEN_REGISTRY_ID = keccak256(abi.encodePacked("StableToken")); + bytes32 internal constant STABLE_EURO_TOKEN_REGISTRY_ID = keccak256( + abi.encodePacked("StableTokenEUR") + ); + bytes32 internal constant STABLE_REAL_TOKEN_REGISTRY_ID = keccak256( + abi.encodePacked("StableTokenBRL") + ); + bytes32 internal constant VALIDATORS_REGISTRY_ID = keccak256(abi.encodePacked("Validators")); modifier onlyRegisteredContract(bytes32 identifierHash) { require( From 0a282f9be0b56769840fe497b96c1dcceb645915 Mon Sep 17 00:00:00 2001 From: Eela Nagaraj <7308464+eelanagaraj@users.noreply.github.com> Date: Wed, 20 Jul 2022 16:38:12 +0200 Subject: [PATCH 23/30] [M03] use SafeERC20 library in Escrow --- packages/protocol/contracts/identity/Escrow.sol | 8 +++++--- packages/protocol/test/identity/escrow.ts | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/packages/protocol/contracts/identity/Escrow.sol b/packages/protocol/contracts/identity/Escrow.sol index 4f3a76ad2a3..edd2295fc06 100644 --- a/packages/protocol/contracts/identity/Escrow.sol +++ b/packages/protocol/contracts/identity/Escrow.sol @@ -3,6 +3,7 @@ pragma solidity ^0.5.13; import "openzeppelin-solidity/contracts/math/SafeMath.sol"; import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; import "openzeppelin-solidity/contracts/token/ERC20/ERC20.sol"; +import "openzeppelin-solidity/contracts/token/ERC20/SafeERC20.sol"; import "./interfaces/IAttestations.sol"; import "./interfaces/IFederatedAttestations.sol"; @@ -23,6 +24,7 @@ contract Escrow is UsingRegistryV2BackwardsCompatible { using SafeMath for uint256; + using SafeERC20 for ERC20; event DefaultTrustedIssuerAdded(address indexed trustedIssuer); event DefaultTrustedIssuerRemoved(address indexed trustedIssuer); @@ -283,7 +285,7 @@ contract Escrow is deletePayment(paymentId); - require(ERC20(payment.token).transfer(msg.sender, payment.value), "Transfer not successful."); + ERC20(payment.token).safeTransfer(msg.sender, payment.value); emit Withdrawal( payment.recipientIdentifier, @@ -313,7 +315,7 @@ contract Escrow is deletePayment(paymentId); - require(ERC20(payment.token).transfer(msg.sender, payment.value), "Transfer not successful."); + ERC20(payment.token).safeTransfer(msg.sender, payment.value); emit Revocation( payment.recipientIdentifier, @@ -509,7 +511,7 @@ contract Escrow is trustedIssuersPerPayment[paymentId] = trustedIssuers; } - require(ERC20(token).transferFrom(msg.sender, address(this), value), "Transfer unsuccessful."); + ERC20(token).safeTransferFrom(msg.sender, address(this), value); emit Transfer(msg.sender, identifier, token, value, paymentId, minAttestations); // Split into a second event for ABI backwards compatibility emit TrustedIssuersSet(paymentId, trustedIssuers); diff --git a/packages/protocol/test/identity/escrow.ts b/packages/protocol/test/identity/escrow.ts index 9807a76655e..84139d4d411 100644 --- a/packages/protocol/test/identity/escrow.ts +++ b/packages/protocol/test/identity/escrow.ts @@ -590,6 +590,23 @@ contract('Escrow', (accounts: string[]) => { ) }) + it('should revert if transfer value exceeds balance', async () => { + await assertRevert( + escrow.transferWithTrustedIssuers( + aPhoneHash, + mockERC20Token.address, + aValue + 1, + oneDayInSecs, + withdrawKeyAddress, + 2, + [], + { + from: sender, + } + ) + ) + }) + describe('#transfer', () => { // transfer and transferWithTrustedIssuers both rely on _transfer // and transfer is a restricted version of transferWithTrustedIssuers From 15be508ef22c256becd7cfea5363ffa53026ea8a Mon Sep 17 00:00:00 2001 From: Eela Nagaraj <7308464+eelanagaraj@users.noreply.github.com> Date: Wed, 20 Jul 2022 16:42:33 +0200 Subject: [PATCH 24/30] [M02] remove unnecessary issuer parameter and TODOs --- .../identity/FederatedAttestations.sol | 17 ++-- .../interfaces/IFederatedAttestations.sol | 9 +-- packages/protocol/test/identity/escrow.ts | 30 +++---- .../test/identity/federatedattestations.ts | 80 +++++-------------- 4 files changed, 36 insertions(+), 100 deletions(-) diff --git a/packages/protocol/contracts/identity/FederatedAttestations.sol b/packages/protocol/contracts/identity/FederatedAttestations.sol index 1a0a762e5ac..12a8ffc2345 100644 --- a/packages/protocol/contracts/identity/FederatedAttestations.sol +++ b/packages/protocol/contracts/identity/FederatedAttestations.sol @@ -101,21 +101,15 @@ contract FederatedAttestations is /** * @notice Registers an attestation directly from the issuer * @param identifier Hash of the identifier to be attested - * @param issuer Address of the attestation issuer * @param account Address of the account being mapped to the identifier * @param issuedOn Time at which the issuer issued the attestation in Unix time - * @dev Attestation signer in storage is set to issuer + * @dev Attestation signer and issuer in storage is set to msg.sender * @dev Throws if an attestation with the same (identifier, issuer, account) already exists */ - function registerAttestationAsIssuer( - bytes32 identifier, - address issuer, - address account, - uint64 issuedOn - ) external { - // TODO allow for updating existing attestation by only updating signer and publishedOn - require(issuer == msg.sender); - _registerAttestation(identifier, issuer, account, issuer, issuedOn); + function registerAttestationAsIssuer(bytes32 identifier, address account, uint64 issuedOn) + external + { + _registerAttestation(identifier, msg.sender, account, msg.sender, issuedOn); } /** @@ -140,7 +134,6 @@ contract FederatedAttestations is bytes32 r, bytes32 s ) external { - // TODO allow for updating existing attestation by only updating signer and publishedOn validateAttestationSig(identifier, issuer, account, signer, issuedOn, v, r, s); _registerAttestation(identifier, issuer, account, signer, issuedOn); } diff --git a/packages/protocol/contracts/identity/interfaces/IFederatedAttestations.sol b/packages/protocol/contracts/identity/interfaces/IFederatedAttestations.sol index 27b0cb447ec..03a64597028 100644 --- a/packages/protocol/contracts/identity/interfaces/IFederatedAttestations.sol +++ b/packages/protocol/contracts/identity/interfaces/IFederatedAttestations.sol @@ -1,12 +1,8 @@ pragma solidity ^0.5.13; interface IFederatedAttestations { - function registerAttestationAsIssuer( - bytes32 identifier, - address issuer, - address account, - uint64 issuedOn - ) external; + function registerAttestationAsIssuer(bytes32 identifier, address account, uint64 issuedOn) + external; function registerAttestation( bytes32 identifier, address issuer, @@ -39,7 +35,6 @@ interface IFederatedAttestations { external view returns (uint256[] memory, bytes32[] memory); - function validateAttestationSig( bytes32 identifier, address issuer, diff --git a/packages/protocol/test/identity/escrow.ts b/packages/protocol/test/identity/escrow.ts index 84139d4d411..af94d0843ae 100644 --- a/packages/protocol/test/identity/escrow.ts +++ b/packages/protocol/test/identity/escrow.ts @@ -1009,13 +1009,9 @@ contract('Escrow', (accounts: string[]) => { }) it('should allow users to withdraw if attestation is found in FederatedAttestations', async () => { await completeAttestations(receiver, aPhoneHash, minAttestations - 1) - await federatedAttestations.registerAttestationAsIssuer( - aPhoneHash, - trustedIssuer2, - receiver, - 0, - { from: trustedIssuer2 } - ) + await federatedAttestations.registerAttestationAsIssuer(aPhoneHash, receiver, 0, { + from: trustedIssuer2, + }) await withdrawAndCheckState( sender, receiver, @@ -1040,13 +1036,9 @@ contract('Escrow', (accounts: string[]) => { ) }) it('should allow users to withdraw if attestation is found in FederatedAttestations', async () => { - await federatedAttestations.registerAttestationAsIssuer( - aPhoneHash, - trustedIssuer2, - receiver, - 0, - { from: trustedIssuer2 } - ) + await federatedAttestations.registerAttestationAsIssuer(aPhoneHash, receiver, 0, { + from: trustedIssuer2, + }) await withdrawAndCheckState( sender, receiver, @@ -1111,13 +1103,9 @@ contract('Escrow', (accounts: string[]) => { uniquePaymentIDRevoke = withdrawKeyAddress parsedSig1 = await getParsedSignatureOfAddress(web3, receiver, withdrawKeyAddress) if (trustedIssuers.length) { - await federatedAttestations.registerAttestationAsIssuer( - identifier, - trustedIssuers[0], - receiver, - 0, - { from: trustedIssuers[0] } - ) + await federatedAttestations.registerAttestationAsIssuer(identifier, receiver, 0, { + from: trustedIssuers[0], + }) } }) diff --git a/packages/protocol/test/identity/federatedattestations.ts b/packages/protocol/test/identity/federatedattestations.ts index 5f6961e92be..2daf598cfe1 100644 --- a/packages/protocol/test/identity/federatedattestations.ts +++ b/packages/protocol/test/identity/federatedattestations.ts @@ -877,41 +877,19 @@ contract('FederatedAttestations', (accounts: string[]) => { }) it('should succeed if the issuer submits the attestation directly', async () => { - assert.isOk( - await federatedAttestations.registerAttestationAsIssuer( - identifier1, - issuer1, - account1, - nowUnixTime, - { from: issuer1 } - ) - ) + await federatedAttestations.registerAttestationAsIssuer(identifier1, account1, nowUnixTime, { + from: issuer1, + }) + await assertAttestationInStorage(identifier1, issuer1, 0, account1, nowUnixTime, issuer1, 0) }) it('should succeed if issuer is not registered in Accounts.sol', async () => { const issuer2 = accounts[5] assert.isFalse(await accountsInstance.isAccount(issuer2)) - assert.isOk( - await federatedAttestations.registerAttestationAsIssuer( - identifier1, - issuer2, - account1, - nowUnixTime, - { from: issuer2 } - ) - ) - }) - - it('should revert if a non-issuer submits an attestation with no signature', async () => { - await assertRevert( - federatedAttestations.registerAttestationAsIssuer( - identifier1, - issuer1, - account1, - nowUnixTime, - { from: signer1 } - ) - ) + await federatedAttestations.registerAttestationAsIssuer(identifier1, account1, nowUnixTime, { + from: issuer2, + }) + await assertAttestationInStorage(identifier1, issuer2, 0, account1, nowUnixTime, issuer2, 0) }) it('should revert if MAX_ATTESTATIONS_PER_IDENTIFIER have already been registered', async () => { @@ -925,20 +903,15 @@ contract('FederatedAttestations', (accounts: string[]) => { const newAccount = await web3.eth.accounts.create().address await federatedAttestations.registerAttestationAsIssuer( identifier1, - issuer1, newAccount, nowUnixTime, { from: issuer1 } ) } await assertRevertWithReason( - federatedAttestations.registerAttestationAsIssuer( - identifier1, - issuer1, - account1, - nowUnixTime, - { from: issuer1 } - ), + federatedAttestations.registerAttestationAsIssuer(identifier1, account1, nowUnixTime, { + from: issuer1, + }), 'Max attestations already registered for identifier' ) }) @@ -952,20 +925,15 @@ contract('FederatedAttestations', (accounts: string[]) => { const newIdentifier = getPhoneHash(phoneNumber, `dummysalt-${i}`) await federatedAttestations.registerAttestationAsIssuer( newIdentifier, - issuer1, account1, nowUnixTime, { from: issuer1 } ) } await assertRevertWithReason( - federatedAttestations.registerAttestationAsIssuer( - identifier1, - issuer1, - account1, - nowUnixTime, - { from: issuer1 } - ), + federatedAttestations.registerAttestationAsIssuer(identifier1, account1, nowUnixTime, { + from: issuer1, + }), 'Max identifiers already registered for account' ) }) @@ -1044,13 +1012,9 @@ contract('FederatedAttestations', (accounts: string[]) => { it('should succeed if issuer is not registered in Accounts.sol', async () => { const issuer2 = accounts[4] assert.isFalse(await accountsInstance.isAccount(issuer2)) - await federatedAttestations.registerAttestationAsIssuer( - identifier1, - issuer2, - account1, - nowUnixTime, - { from: issuer2 } - ) + await federatedAttestations.registerAttestationAsIssuer(identifier1, account1, nowUnixTime, { + from: issuer2, + }) await assertAttestationInStorage(identifier1, issuer2, 0, account1, nowUnixTime, issuer2, 0) await federatedAttestations.revokeAttestation(identifier1, issuer2, account1, { from: issuer2, @@ -1273,13 +1237,9 @@ contract('FederatedAttestations', (accounts: string[]) => { it('should succeed if issuer is not registered in Accounts.sol', async () => { const issuer2 = accounts[6] assert.isFalse(await accountsInstance.isAccount(issuer2)) - await federatedAttestations.registerAttestationAsIssuer( - identifier1, - issuer2, - account1, - nowUnixTime, - { from: issuer2 } - ) + await federatedAttestations.registerAttestationAsIssuer(identifier1, account1, nowUnixTime, { + from: issuer2, + }) await assertAttestationInStorage(identifier1, issuer2, 0, account1, nowUnixTime, issuer2, 0) await federatedAttestations.batchRevokeAttestations(issuer2, [identifier1], [account1], { from: issuer2, From c1bd057484c4601436eb35c563d431f80d5f94b6 Mon Sep 17 00:00:00 2001 From: Eela Nagaraj <7308464+eelanagaraj@users.noreply.github.com> Date: Wed, 20 Jul 2022 16:44:00 +0200 Subject: [PATCH 25/30] [M01] limit trustedIssuers in Escrow payments --- .../protocol/contracts/identity/Escrow.sol | 17 +++++ .../identity/FederatedAttestations.sol | 3 + .../contracts/identity/interfaces/IEscrow.sol | 1 + packages/protocol/test/identity/escrow.ts | 73 ++++++++++++++++++- 4 files changed, 90 insertions(+), 4 deletions(-) diff --git a/packages/protocol/contracts/identity/Escrow.sol b/packages/protocol/contracts/identity/Escrow.sol index edd2295fc06..9f9491476af 100644 --- a/packages/protocol/contracts/identity/Escrow.sol +++ b/packages/protocol/contracts/identity/Escrow.sol @@ -87,6 +87,10 @@ contract Escrow is // Governable list of trustedIssuers to set for payments by default. address[] public defaultTrustedIssuers; + // Based on benchmarking of FederatedAttestations lookup gas consumption + // in the worst case (with a significant amount of buffer). + uint256 public constant MAX_TRUSTED_ISSUERS_PER_PAYMENT = 100; + /** * @notice Sets initialized == true on implementation contracts * @param test Set to true to skip implementation initialization @@ -112,9 +116,15 @@ contract Escrow is * @notice Add an address to the defaultTrustedIssuers list. * @param trustedIssuer Address of the trustedIssuer to add. * @dev Throws if trustedIssuer is null or already in defaultTrustedIssuers. + * @dev Throws if defaultTrustedIssuers is already at max allowed length. */ function addDefaultTrustedIssuer(address trustedIssuer) external onlyOwner { require(address(0) != trustedIssuer, "trustedIssuer can't be null"); + require( + defaultTrustedIssuers.length.add(1) <= MAX_TRUSTED_ISSUERS_PER_PAYMENT, + "defaultTrustedIssuers.length can't exceed allowed number of trustedIssuers" + ); + // Ensure list of trusted issuers is unique for (uint256 i = 0; i < defaultTrustedIssuers.length; i = i.add(1)) { require( @@ -485,6 +495,13 @@ contract Escrow is "trustedIssuers may only be set when attestations are required" ); + // Ensure that withdrawal will not fail due to exceeding trustedIssuer limit + // in FederatedAttestations.lookupAttestations + require( + trustedIssuers.length <= MAX_TRUSTED_ISSUERS_PER_PAYMENT, + "Too many trustedIssuers provided" + ); + IAttestations attestations = getAttestations(); require( minAttestations <= attestations.getMaxAttestations(), diff --git a/packages/protocol/contracts/identity/FederatedAttestations.sol b/packages/protocol/contracts/identity/FederatedAttestations.sol index 12a8ffc2345..2013d2562dc 100644 --- a/packages/protocol/contracts/identity/FederatedAttestations.sol +++ b/packages/protocol/contracts/identity/FederatedAttestations.sol @@ -55,6 +55,9 @@ contract FederatedAttestations is ) ); + // Changing any of these constraints will require re-benchmarking + // and checking assumptions for batch revocation. + // These can only be modified by releasing a new version of this contract. uint256 public constant MAX_ATTESTATIONS_PER_IDENTIFIER = 20; uint256 public constant MAX_IDENTIFIERS_PER_ADDRESS = 20; diff --git a/packages/protocol/contracts/identity/interfaces/IEscrow.sol b/packages/protocol/contracts/identity/interfaces/IEscrow.sol index b7590e3c392..965a31d63dc 100644 --- a/packages/protocol/contracts/identity/interfaces/IEscrow.sol +++ b/packages/protocol/contracts/identity/interfaces/IEscrow.sol @@ -26,6 +26,7 @@ interface IEscrow { function getSentPaymentIds(address sender) external view returns (address[] memory); function getTrustedIssuersPerPayment(address paymentId) external view returns (address[] memory); function getDefaultTrustedIssuers() external view returns (address[] memory); + function MAX_TRUSTED_ISSUERS_PER_PAYMENT() external view returns (uint256); // onlyOwner functions function addDefaultTrustedIssuer(address trustedIssuer) external; diff --git a/packages/protocol/test/identity/escrow.ts b/packages/protocol/test/identity/escrow.ts index af94d0843ae..06f21d8f9b2 100644 --- a/packages/protocol/test/identity/escrow.ts +++ b/packages/protocol/test/identity/escrow.ts @@ -162,10 +162,39 @@ contract('Escrow', (accounts: string[]) => { ) }) - it('should allow a second trustedIssuer to be added', async () => { - await escrow.addDefaultTrustedIssuer(trustedIssuer1, { from: owner }) - await escrow.addDefaultTrustedIssuer(trustedIssuer2, { from: owner }) - assert.deepEqual(await escrow.getDefaultTrustedIssuers(), [trustedIssuer1, trustedIssuer2]) + describe('when max trusted issuers have been added', async () => { + let expectedTrustedIssuers: string[] + + beforeEach(async () => { + const maxTrustedIssuers = (await escrow.MAX_TRUSTED_ISSUERS_PER_PAYMENT()).toNumber() + expectedTrustedIssuers = [] + for (let i = 0; i < maxTrustedIssuers; i++) { + const newIssuer = await web3.eth.accounts.create().address + await escrow.addDefaultTrustedIssuer(newIssuer, { from: owner }) + expectedTrustedIssuers.push(newIssuer) + } + }) + + it('should have set expected default trusted issuers', async () => { + assert.deepEqual(await escrow.getDefaultTrustedIssuers(), expectedTrustedIssuers) + }) + + it('should not allow more trusted issuers to be added', async () => { + await assertRevertWithReason( + escrow.addDefaultTrustedIssuer(trustedIssuer1, { from: owner }), + "defaultTrustedIssuers.length can't exceed allowed number of trustedIssuers" + ) + }) + + it('should allow removing and adding an issuer', async () => { + await escrow.removeDefaultTrustedIssuer(expectedTrustedIssuers[0], 0, { from: owner }) + await escrow.addDefaultTrustedIssuer(trustedIssuer1) + expectedTrustedIssuers.push(trustedIssuer1) + assert.deepEqual( + (await escrow.getDefaultTrustedIssuers()).sort(), + expectedTrustedIssuers.slice(1).sort() + ) + }) }) }) @@ -370,6 +399,23 @@ contract('Escrow', (accounts: string[]) => { ) }) + it('should allow transfer when max trustedIssuers are provided', async () => { + const repeatedTrustedIssuers = Array.from({ + length: (await escrow.MAX_TRUSTED_ISSUERS_PER_PAYMENT()).toNumber(), + }).fill(trustedIssuer1) + await transferAndCheckState( + sender, + aPhoneHash, + aValue, + oneDayInSecs, + withdrawKeyAddress, + 3, + repeatedTrustedIssuers, + [withdrawKeyAddress], + [withdrawKeyAddress] + ) + }) + it('should allow transfer when no identifier is provided', async () => { await transferAndCheckState( sender, @@ -485,6 +531,25 @@ contract('Escrow', (accounts: string[]) => { ) }) + it('should not allow a transfer when too many trustedIssuers are provided', async () => { + const repeatedTrustedIssuers = Array.from({ + length: (await escrow.MAX_TRUSTED_ISSUERS_PER_PAYMENT()).toNumber() + 1, + }).fill(trustedIssuer1) + await assertRevertWithReason( + escrow.transferWithTrustedIssuers( + aPhoneHash, + mockERC20Token.address, + aValue, + oneDayInSecs, + withdrawKeyAddress, + 3, + repeatedTrustedIssuers, + { from: sender } + ), + 'Too many trustedIssuers provided' + ) + }) + it('should not allow a transfer if token is 0', async () => { await assertRevert( escrow.transferWithTrustedIssuers( From 1452cccf55a506454f6fec72d268b78121c027cd Mon Sep 17 00:00:00 2001 From: Eela Nagaraj <7308464+eelanagaraj@users.noreply.github.com> Date: Tue, 30 Aug 2022 22:09:09 +0200 Subject: [PATCH 26/30] Implement OdisPayments.sol and unit tests (#9740) * Implement OdisPayments.sol and unit tests * Update registry varname because of UsingRegistryV2 changes * Add release data * Fix migration override params in package.json * Address PR comments --- packages/cli/package.json | 2 +- .../contracts/identity/OdisPayments.sol | 75 +++++++++ .../identity/interfaces/IOdisPayments.sol | 6 + .../identity/proxies/OdisPaymentsProxy.sol | 6 + packages/protocol/governanceConstitution.js | 3 + packages/protocol/lib/registry-utils.ts | 2 + .../protocol/migrations/26_odispayments.ts | 14 ++ .../{26_governance.ts => 27_governance.ts} | 1 + ...t_validators.ts => 28_elect_validators.ts} | 0 .../initializationData/release8.json | 3 +- .../protocol/scripts/bash/backupmigrations.sh | 5 +- packages/protocol/scripts/build.ts | 2 + .../protocol/test/identity/odispayments.ts | 155 ++++++++++++++++++ packages/sdk/contractkit/package.json | 2 +- packages/sdk/identity/package.json | 2 +- packages/sdk/transactions-uri/package.json | 2 +- 16 files changed, 273 insertions(+), 7 deletions(-) create mode 100644 packages/protocol/contracts/identity/OdisPayments.sol create mode 100644 packages/protocol/contracts/identity/interfaces/IOdisPayments.sol create mode 100644 packages/protocol/contracts/identity/proxies/OdisPaymentsProxy.sol create mode 100644 packages/protocol/migrations/26_odispayments.ts rename packages/protocol/migrations/{26_governance.ts => 27_governance.ts} (99%) rename packages/protocol/migrations/{27_elect_validators.ts => 28_elect_validators.ts} (100%) create mode 100644 packages/protocol/test/identity/odispayments.ts diff --git a/packages/cli/package.json b/packages/cli/package.json index f3bfe057037..7dafde73a0b 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -28,7 +28,7 @@ "generate:shrinkwrap": "npm install --production && npm shrinkwrap", "check:shrinkwrap": "npm install --production && npm shrinkwrap && ./scripts/check_shrinkwrap_dirty.sh", "prepack": "yarn run build && oclif-dev manifest && oclif-dev readme && yarn run check:shrinkwrap", - "test:reset": "yarn --cwd ../protocol devchain generate-tar .tmp/devchain.tar.gz --migration_override ../dev-utils/src/migration-override.json --upto 26 --release_gold_contracts scripts/truffle/releaseGoldExampleConfigs.json", + "test:reset": "yarn --cwd ../protocol devchain generate-tar .tmp/devchain.tar.gz --migration_override ../dev-utils/src/migration-override.json --upto 27 --release_gold_contracts scripts/truffle/releaseGoldExampleConfigs.json", "test:livechain": "yarn --cwd ../protocol devchain run-tar .tmp/devchain.tar.gz", "test": "TZ=UTC jest --runInBand" }, diff --git a/packages/protocol/contracts/identity/OdisPayments.sol b/packages/protocol/contracts/identity/OdisPayments.sol new file mode 100644 index 00000000000..33caf678322 --- /dev/null +++ b/packages/protocol/contracts/identity/OdisPayments.sol @@ -0,0 +1,75 @@ +pragma solidity ^0.5.13; + +import "openzeppelin-solidity/contracts/math/SafeMath.sol"; +import "openzeppelin-solidity/contracts/token/ERC20/IERC20.sol"; +import "openzeppelin-solidity/contracts/token/ERC20/SafeERC20.sol"; +import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; + +import "./interfaces/IOdisPayments.sol"; +import "../common/interfaces/ICeloVersionedContract.sol"; + +import "../common/Initializable.sol"; +import "../common/UsingRegistryV2.sol"; +import "../common/libraries/ReentrancyGuard.sol"; + +/** + * @title Stores balance to be used for ODIS quota calculation. + */ +contract OdisPayments is + IOdisPayments, + ICeloVersionedContract, + ReentrancyGuard, + Ownable, + Initializable, + UsingRegistryV2 +{ + using SafeMath for uint256; + using SafeERC20 for IERC20; + + event PaymentMade(address indexed account, uint256 valueInCUSD); + + // Store amount sent (all time) from account to this contract. + // Values in totalPaidCUSD should only ever be incremented, since ODIS relies + // on all-time paid balance to compute every quota. + mapping(address => uint256) public totalPaidCUSD; + + /** + * @notice Sets initialized == true on implementation contracts. + * @param test Set to true to skip implementation initialization. + */ + constructor(bool test) public Initializable(test) {} + + /** + * @notice Returns the storage, major, minor, and patch version of the contract. + * @return Storage version of the contract. + * @return Major version of the contract. + * @return Minor version of the contract. + * @return Patch version of the contract. + */ + function getVersionNumber() external pure returns (uint256, uint256, uint256, uint256) { + return (1, 1, 0, 0); + } + + /** + * @notice Used in place of the constructor to allow the contract to be upgradable via proxy. + */ + function initialize() external initializer { + _transferOwnership(msg.sender); + } + + /** + * @notice Sends cUSD to this contract to pay for ODIS quota (for queries). + * @param account The account whose balance to increment. + * @param value The amount in cUSD to pay. + * @dev Throws if cUSD transfer fails. + */ + function payInCUSD(address account, uint256 value) external nonReentrant { + IERC20(registryContract.getAddressForOrDie(STABLE_TOKEN_REGISTRY_ID)).safeTransferFrom( + msg.sender, + address(this), + value + ); + totalPaidCUSD[account] = totalPaidCUSD[account].add(value); + emit PaymentMade(account, value); + } +} diff --git a/packages/protocol/contracts/identity/interfaces/IOdisPayments.sol b/packages/protocol/contracts/identity/interfaces/IOdisPayments.sol new file mode 100644 index 00000000000..98b00d2fceb --- /dev/null +++ b/packages/protocol/contracts/identity/interfaces/IOdisPayments.sol @@ -0,0 +1,6 @@ +pragma solidity ^0.5.13; + +interface IOdisPayments { + function payInCUSD(address account, uint256 value) external; + function totalPaidCUSD(address) external view returns (uint256); +} diff --git a/packages/protocol/contracts/identity/proxies/OdisPaymentsProxy.sol b/packages/protocol/contracts/identity/proxies/OdisPaymentsProxy.sol new file mode 100644 index 00000000000..9114e1a5f93 --- /dev/null +++ b/packages/protocol/contracts/identity/proxies/OdisPaymentsProxy.sol @@ -0,0 +1,6 @@ +pragma solidity ^0.5.13; + +import "../../common/Proxy.sol"; + +/* solhint-disable no-empty-blocks */ +contract OdisPaymentsProxy is Proxy {} diff --git a/packages/protocol/governanceConstitution.js b/packages/protocol/governanceConstitution.js index 440afb7878e..4ee588f4e02 100644 --- a/packages/protocol/governanceConstitution.js +++ b/packages/protocol/governanceConstitution.js @@ -133,6 +133,9 @@ const DefaultConstitution = { addSlasher: 0.9, removeSlasher: 0.8, }, + OdisPayments: { + default: 0.6, + }, // Values for all proxied contracts. proxy: { _transferOwnership: 0.9, diff --git a/packages/protocol/lib/registry-utils.ts b/packages/protocol/lib/registry-utils.ts index ba9063d547d..1bf771971a5 100644 --- a/packages/protocol/lib/registry-utils.ts +++ b/packages/protocol/lib/registry-utils.ts @@ -30,6 +30,7 @@ export enum CeloContractName { GovernanceApproverMultiSig = 'GovernanceApproverMultiSig', GrandaMento = 'GrandaMento', LockedGold = 'LockedGold', + OdisPayments = 'OdisPayments', Random = 'Random', Reserve = 'Reserve', ReserveSpenderMultiSig = 'ReserveSpenderMultiSig', @@ -62,6 +63,7 @@ export const hasEntryInRegistry: string[] = [ CeloContractName.GoldToken, CeloContractName.GovernanceSlasher, CeloContractName.GrandaMento, + CeloContractName.OdisPayments, CeloContractName.Random, CeloContractName.Reserve, CeloContractName.SortedOracles, diff --git a/packages/protocol/migrations/26_odispayments.ts b/packages/protocol/migrations/26_odispayments.ts new file mode 100644 index 00000000000..947b95214d0 --- /dev/null +++ b/packages/protocol/migrations/26_odispayments.ts @@ -0,0 +1,14 @@ +import { CeloContractName } from '@celo/protocol/lib/registry-utils' +import { deploymentForCoreContract } from '@celo/protocol/lib/web3-utils' +import { OdisPaymentsInstance } from 'types' + +const initializeArgs = async () => { + return [] +} + +module.exports = deploymentForCoreContract( + web3, + artifacts, + CeloContractName.OdisPayments, + initializeArgs +) diff --git a/packages/protocol/migrations/26_governance.ts b/packages/protocol/migrations/27_governance.ts similarity index 99% rename from packages/protocol/migrations/26_governance.ts rename to packages/protocol/migrations/27_governance.ts index a95777f27c2..ce4101a74b9 100644 --- a/packages/protocol/migrations/26_governance.ts +++ b/packages/protocol/migrations/27_governance.ts @@ -92,6 +92,7 @@ module.exports = deploymentForCoreContract( 'GovernanceSlasher', 'GrandaMento', 'LockedGold', + 'OdisPayments', 'Random', 'Registry', 'Reserve', diff --git a/packages/protocol/migrations/27_elect_validators.ts b/packages/protocol/migrations/28_elect_validators.ts similarity index 100% rename from packages/protocol/migrations/27_elect_validators.ts rename to packages/protocol/migrations/28_elect_validators.ts diff --git a/packages/protocol/releaseData/initializationData/release8.json b/packages/protocol/releaseData/initializationData/release8.json index 151187906e6..aa3ba082dc3 100644 --- a/packages/protocol/releaseData/initializationData/release8.json +++ b/packages/protocol/releaseData/initializationData/release8.json @@ -1,3 +1,4 @@ { - "FederatedAttestations": [] + "FederatedAttestations": [], + "OdisPayments": [] } diff --git a/packages/protocol/scripts/bash/backupmigrations.sh b/packages/protocol/scripts/bash/backupmigrations.sh index f4d394cb865..1c50e08d53b 100755 --- a/packages/protocol/scripts/bash/backupmigrations.sh +++ b/packages/protocol/scripts/bash/backupmigrations.sh @@ -47,6 +47,7 @@ else # cp migrations.bak/23_governance_approver_multisig.* migrations/ # cp migrations.bak/24_grandamento.* migrations/ # cp migrations.bak/25_federated_attestations.* migrations/ - # cp migrations.bak/26_governance.* migrations/ - # cp migrations.bak/27_elect_validators.* migrations/ + # cp migrations.bak/26_odispayments.* migrations/ + # cp migrations.bak/27_governance.* migrations/ + # cp migrations.bak/28_elect_validators.* migrations/ fi \ No newline at end of file diff --git a/packages/protocol/scripts/build.ts b/packages/protocol/scripts/build.ts index 0a15471c3be..dcf4889452a 100644 --- a/packages/protocol/scripts/build.ts +++ b/packages/protocol/scripts/build.ts @@ -30,6 +30,7 @@ export const ProxyContracts = [ 'LockedGoldProxy', 'MetaTransactionWalletProxy', 'MetaTransactionWalletDeployerProxy', + 'OdisPaymentsProxy', 'RegistryProxy', 'ReserveProxy', 'ReserveSpenderMultiSigProxy', @@ -69,6 +70,7 @@ export const CoreContracts = [ 'Escrow', 'FederatedAttestations', 'Random', + 'OdisPayments', // stability 'Exchange', diff --git a/packages/protocol/test/identity/odispayments.ts b/packages/protocol/test/identity/odispayments.ts new file mode 100644 index 00000000000..578d05fb2ee --- /dev/null +++ b/packages/protocol/test/identity/odispayments.ts @@ -0,0 +1,155 @@ +import { CeloContractName } from '@celo/protocol/lib/registry-utils' +import { + assertEqualBN, + assertLogMatches2, + assertRevert, + assumeOwnership, +} from '@celo/protocol/lib/test-utils' +import { getDeployedProxiedContract } from '@celo/protocol/lib/web3-utils' +import { fixed1 } from '@celo/utils/src/fixidity' +import { + FreezerContract, + FreezerInstance, + OdisPaymentsContract, + OdisPaymentsInstance, + RegistryInstance, + StableTokenContract, + StableTokenInstance, +} from 'types' + +const Freezer: FreezerContract = artifacts.require('Freezer') +const OdisPayments: OdisPaymentsContract = artifacts.require('OdisPayments') +const StableTokenCUSD: StableTokenContract = artifacts.require('StableToken') + +const SECONDS_IN_A_DAY = 60 * 60 * 24 + +contract('OdisPayments', (accounts: string[]) => { + let freezer: FreezerInstance + let odisPayments: OdisPaymentsInstance + let registry: RegistryInstance + let stableTokenCUSD: StableTokenInstance + + const owner = accounts[0] + const sender = accounts[1] + const startingBalanceCUSD = 1000 + + before(async () => { + // Mocking Registry.sol when using UsingRegistryV2.sol + registry = await getDeployedProxiedContract('Registry', artifacts) + if ((await registry.owner()) !== owner) { + // In CI we need to assume ownership, locally using quicktest we don't + await assumeOwnership(['Registry'], owner) + } + }) + + beforeEach(async () => { + odisPayments = await OdisPayments.new(true, { from: owner }) + await odisPayments.initialize() + + stableTokenCUSD = await StableTokenCUSD.new(true, { from: owner }) + await registry.setAddressFor(CeloContractName.StableToken, stableTokenCUSD.address) + await stableTokenCUSD.initialize( + 'Celo Dollar', + 'cUSD', + 18, + registry.address, + fixed1, + SECONDS_IN_A_DAY, + // Initialize owner and sender with balances + [owner, sender], + [startingBalanceCUSD, startingBalanceCUSD], + 'Exchange' // USD + ) + + // StableToken is freezable so this is necessary for transferFrom calls + freezer = await Freezer.new(true, { from: owner }) + await freezer.initialize() + await registry.setAddressFor(CeloContractName.Freezer, freezer.address) + }) + + describe('#initialize()', () => { + it('should have set the owner', async () => { + const actualOwner: string = await odisPayments.owner() + assert.equal(actualOwner, owner) + }) + + it('should not be callable again', async () => { + await assertRevert(odisPayments.initialize()) + }) + }) + + describe('#payInCUSD', () => { + const checkStateCUSD = async ( + cusdSender: string, + odisPaymentReceiver: string, + totalValueSent: number + ) => { + assertEqualBN( + await stableTokenCUSD.balanceOf(cusdSender), + startingBalanceCUSD - totalValueSent, + 'cusdSender balance' + ) + assertEqualBN( + await stableTokenCUSD.balanceOf(odisPayments.address), + totalValueSent, + 'odisPayments.address balance' + ) + assertEqualBN( + await odisPayments.totalPaidCUSD(odisPaymentReceiver), + totalValueSent, + 'odisPaymentReceiver balance' + ) + } + + const valueApprovedForTransfer = 10 + const receiver = accounts[2] + + beforeEach(async () => { + await stableTokenCUSD.approve(odisPayments.address, valueApprovedForTransfer, { + from: sender, + }) + assertEqualBN(await stableTokenCUSD.balanceOf(sender), startingBalanceCUSD) + }) + + it('should allow sender to make a payment on their behalf', async () => { + await odisPayments.payInCUSD(sender, valueApprovedForTransfer, { from: sender }) + await checkStateCUSD(sender, sender, valueApprovedForTransfer) + }) + + it('should allow sender to make a payment for another account', async () => { + await odisPayments.payInCUSD(receiver, valueApprovedForTransfer, { from: sender }) + await checkStateCUSD(sender, receiver, valueApprovedForTransfer) + }) + + it('should allow sender to make multiple payments to the contract', async () => { + const valueForSecondTransfer = 5 + const valueForFirstTransfer = valueApprovedForTransfer - valueForSecondTransfer + + await odisPayments.payInCUSD(sender, valueForFirstTransfer, { from: sender }) + await checkStateCUSD(sender, sender, valueForFirstTransfer) + + await odisPayments.payInCUSD(sender, valueForSecondTransfer, { from: sender }) + await checkStateCUSD(sender, sender, valueApprovedForTransfer) + }) + + it('should emit the PaymentMade event', async () => { + const receipt = await odisPayments.payInCUSD(receiver, valueApprovedForTransfer, { + from: sender, + }) + assertLogMatches2(receipt.logs[0], { + event: 'PaymentMade', + args: { + account: receiver, + valueInCUSD: valueApprovedForTransfer, + }, + }) + }) + + it('should revert if transfer fails', async () => { + await assertRevert( + odisPayments.payInCUSD(sender, valueApprovedForTransfer + 1, { from: sender }) + ) + assertEqualBN(await odisPayments.totalPaidCUSD(sender), 0) + }) + }) +}) diff --git a/packages/sdk/contractkit/package.json b/packages/sdk/contractkit/package.json index fd6d7777522..c8c0bdf3e64 100644 --- a/packages/sdk/contractkit/package.json +++ b/packages/sdk/contractkit/package.json @@ -22,7 +22,7 @@ "clean:all": "yarn clean && rm -rf src/generated", "prepublishOnly": "yarn build", "docs": "typedoc", - "test:reset": "yarn --cwd ../../protocol devchain generate-tar .tmp/devchain.tar.gz --migration_override ../../dev-utils/src/migration-override.json --upto 26", + "test:reset": "yarn --cwd ../../protocol devchain generate-tar .tmp/devchain.tar.gz --migration_override ../../dev-utils/src/migration-override.json --upto 27", "test:livechain": "yarn --cwd ../../protocol devchain run-tar .tmp/devchain.tar.gz", "test": "jest --runInBand", "lint": "tslint -c tslint.json --project ." diff --git a/packages/sdk/identity/package.json b/packages/sdk/identity/package.json index 51a6c8bec66..28ed98023f7 100644 --- a/packages/sdk/identity/package.json +++ b/packages/sdk/identity/package.json @@ -18,7 +18,7 @@ "build": "tsc -b .", "clean": "tsc -b . --clean", "docs": "typedoc", - "test:reset": "yarn --cwd ../../protocol devchain generate-tar .tmp/devchain.tar.gz --migration_override ../../dev-utils/src/migration-override.json --upto 26", + "test:reset": "yarn --cwd ../../protocol devchain generate-tar .tmp/devchain.tar.gz --migration_override ../../dev-utils/src/migration-override.json --upto 27", "test:livechain": "yarn --cwd ../../protocol devchain run-tar .tmp/devchain.tar.gz", "test": "jest --runInBand", "lint": "tslint -c tslint.json --project .", diff --git a/packages/sdk/transactions-uri/package.json b/packages/sdk/transactions-uri/package.json index 8a847936de7..f737b81c32d 100644 --- a/packages/sdk/transactions-uri/package.json +++ b/packages/sdk/transactions-uri/package.json @@ -17,7 +17,7 @@ "build": "tsc -b .", "clean": "tsc -b . --clean", "docs": "typedoc", - "test:reset": "yarn --cwd ../../protocol devchain generate-tar .tmp/devchain.tar.gz --migration_override ../../dev-utils/src/migration-override.json --upto 26", + "test:reset": "yarn --cwd ../../protocol devchain generate-tar .tmp/devchain.tar.gz --migration_override ../../dev-utils/src/migration-override.json --upto 27", "test:livechain": "yarn --cwd ../../protocol devchain run-tar .tmp/devchain.tar.gz", "test": "jest --runInBand", "lint": "tslint -c tslint.json --project .", From d90181ffc83da564ad9907d96024727088d9d560 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Volpe?= Date: Fri, 16 Sep 2022 18:50:21 +0200 Subject: [PATCH 27/30] RC7 -> RC8 (#9865) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Squashed commit of the following: commit 7f44cef046b465975aec476d6630db92e4b36f9e Author: Martin Volpe Date: Fri May 13 15:10:45 2022 +0200 Fixed release versions commit 24968ec330c548be622cd3a58d5ea79c7f0c64f2 Author: Martin Volpe Date: Fri May 13 15:09:06 2022 +0200 Added newline at the end commit 1e70975a232a306caa8ccf4d629898b52dcf5a5c Author: Marcin Chrzanowski Date: Tue Feb 22 10:18:55 2022 -0500 Add version report commit 8457f408b476576f67ee3ce2a963253ce63ab4fc Author: Marcin Chrzanowski Date: Tue Feb 15 11:06:21 2022 -0500 Add empty initialization data for release 6 commit a92491598c50445001d2abb7566fce0e8b5e7fb6 Author: Martín Volpe Date: Mon May 9 22:24:24 2022 +0200 Fix Oracle check (#9527) commit 292c443f52779a345b5b26d28be650b2231d2854 Author: Nina Barbakadze Date: Wed May 4 14:36:53 2022 +0200 Consistent code across GrandaMento and Exchange in setSpread (#9459) commit 3cfceab5b49eb7cc2fe752d20bbb0c05ef13ae2a Author: Nina Barbakadze Date: Thu Apr 28 15:45:35 2022 +0200 Restored a missing test (#9460) Authored-by: Nina Barbakadze commit 0b51c6576e2d6697b7bc31dda5e969df767151d6 Author: Martin Date: Fri Mar 11 11:09:29 2022 +0100 Remove oracle check (#9367) ### Description A check in the Reserve's `addToken` function appeared to be a blocker for getting quicker, single-proposal stable token deployments. The check may have been important in the past, back when the Reserve was responsible for minting/burning stable tokens, and the `isToken` mapping set in the `addToken` function was used for permissioning these behaviors. However, looking at both the monorepo contracts and celo-blockchain, the `isToken` mapping and `_tokens` list of tokens are not used anywhere, thus the check is safe to remove. See [here](https://github.com/celo-org/celo-monorepo/issues/9195#issuecomment-1057942027) for more details. ### Tested Tests still pass. ### Related issues - Fixes #9197 commit a84a8457e106b3e6bec98aeff849ffd336ffc79b Author: Eela Nagaraj <7308464+eelanagaraj@users.noreply.github.com> Date: Thu Mar 10 16:10:37 2022 +0000 Update default config file (#9372) ### Description Tiny PR that goes with [this docs PR](https://github.com/celo-org/docs/compare/eelanagaraj/link-10dlc-guidance?expand=1) update AS docs related to 10 DLC compliance. commit 3e9f8e460c428d30287cccdef50a31ed1cbf142c Author: Gaston Ponti Date: Wed Mar 9 13:37:04 2022 -0300 TxLookupLimit default to infinite (#9380) ### Description TxLookupLimit default to infinite (as it was before) ### Backwards compatibility Yes commit 1cc59c705c687deb92390ba7712128535c806765 Author: Martin Date: Wed Mar 9 09:41:21 2022 +0100 Test LinkedList.remove (#9377) commit 0edc59d0d6d05d30ca8150112d63ac3774148c21 Author: Edwin O Date: Fri Mar 4 14:28:13 2022 +0100 Get pending withdrawal (#9369) ### Description LockedGold.sol currently provides a way to retrieve all pendingWithdrawals associated to a given account. To be able to extract a pendingWithdrawal at specific index, one would need to first do getPendingWithdrawals and then loop through the result to find the record 0(n) and has a DOS vector as found by @tkporter ### Tested - [x] unit test ### Related issues https://app.zenhub.com/workspaces/yield-61b75a715d3ecb001007ab9d/issues/celo-org/yield/16 ### Backwards compatibility _Brief explanation of why these changes are/are not backwards compatible._ This change is backward compatible because it is completely transparent and does not modify existing function signatures etc commit 84194c71b14e95e76b41541cd0fdc621e3d8e8f1 Author: Eela Nagaraj <7308464+eelanagaraj@users.noreply.github.com> Date: Wed Mar 2 16:58:22 2022 +0100 Update geth version and AS image tag (#9363) commit a4af8e4413c69d9ca684b99b5212b507991a0820 Author: Victor "Nate" Graf Date: Wed Mar 2 00:26:36 2022 -0800 Update Baklava nodes to celo-blockchain 1.5.4 (#9355) ### Description Update the version of cLabs `celo-blockchain` nodes in the Baklava testnet to 1.5.4 commit 40f67a37b986b17adc54df2219a80bcfec49d867 Author: pedro-clabs <77401336+pedro-clabs@users.noreply.github.com> Date: Tue Mar 1 17:07:29 2022 +0000 Add mainnet CELOBRL oracles. (#9105) ### Tested Deployed to mainnet. commit 22faf4e6fd74bdc8559cf86ec4ff31552aa07c5c Author: Emil Hotkowski Date: Mon Feb 28 17:35:46 2022 +0100 Attestation service pepper verification (#8692) ### Description I added new, optional verification of odis pepper. ### Tested I added tests. ### Backwards compatibility The new field is for now optional, and only when provided we do the verification. commit cf87ed1f013238c98f68a06e8620b9b4620d00bb Author: Cody Born Date: Mon Feb 28 05:08:12 2022 -0800 Fix env var name for API key (#9340) ### Description ODIS Forno API Key env var was specified incorrectly. ### Tested Tested in Alfajores. commit 81f84ebdcbf3126dd4d7385a622799d1ecb1713b Author: Martín Volpe Date: Fri Feb 25 16:13:08 2022 -0300 Update oracles full nodes to version 1.5.3 (#9339) commit 8d3f3f0a5c73359820e99a070a7b48cee247f6b1 Author: Cody Born Date: Tue Feb 22 04:14:41 2022 -0800 Use Forno API key in Signer (#9334) * Update combiner to leverage new CK client * Config var name must be all lowercase * update yarn.lock * Add Forno API key to Signer requests * Update operator docs * Update signer deployment tool * Add ODIS Signer API keys commit 19e2f1347cfe350a7392ec853275223feec2db47 Author: Alec Schaefer Date: Fri Feb 18 11:54:55 2022 -0500 ODIS monitor fix (#9303) ### Description Fixes ODIS monitor function Disables domain endpoints Adds version number check to signer (and combiner, but the combiner status endpoint has apparently never worked) Updates package version numbers for deployment ### Other changes Version changes to facilitate release of ODIS combiner, signer, and monitor ### Tested NA ### Related issues NA ### Backwards compatibility Yes ### Documentation NA commit ff9d776162a8af7072d8cc3396ac87cfdc310578 Author: Valentin Rodygin Date: Fri Feb 18 11:27:13 2022 +0100 Config params for the stats page in Blockscout (#9329) ### Description Added env variables that control the Stats page in Blockscout. ### Other changes No. ### Tested Tested on Alfajores. ### Backwards compatibility Yes. commit 13906c531054d9c9629f33ef6d78d7f93c3f3176 Author: Victor "Nate" Graf Date: Thu Feb 17 15:49:49 2022 -0800 Create @celo/encrypted-backup backup for ODIS hardened password encrypted backups (#8896) * create encrypted-backup package * basic backup and open functions working without any key hardening * basic backup and open functions working without any key hardening * use io-ts to create serialization and deserialization functions * extract the encryption and decryption logic from the main functions * add io-ts schema definitions for Domain types * add domain field to backup type and schema * mix in the domain hash to the key as a simple proxy for ODIS * add a stub to hold the place of circuit breaker functions * update the dependency graph * swap order of circuit breaker and odis stubs * [broken] move domains source from @celo/identity to @celo/phone-number-privacy * [broken] remove @celo/identity dependency from @celo/phone-number-privacy-common * finish removing @celo/identity and a dependency of @celo/phone-number-privacy-common * remove duplication and inconsistencies betweem @celo/identity and @celo/phone-number-privacy-common * fix linter errors * clean up domain state and response types * [checkpoint] partially implemented key hardening through ODIS * refactor phone-number-privacy a little * initial implementaion of odis key hardening logic * wire in odis key hardening * fix issues occuring during package initialization * tests now working against a mock implementation of ODIS * add (untested) circuit breaker client implementation * add comments and pipe in circuit breaker config values * fix issues founds in manual testing * add a mock and tests for circuit breaker client * add a NO_GANACHE env variable to disable starting ganache for testing * round out tests for the circuit breaker client * refactor the mock circuit breaker to be ready to export * use the circuit breaker for key hardening * bump package versions * add more information to error handling and debug messages * add doc strings to create and open backup functions * add wrapper function createPinEncryptedBackup with documentation * refactor mock odis * handle 429 status from ODIS and add some error condition tests to backup lib * seperate handling of fetch errors and add more error case tests * add tests for error cases in openBackup * remove DO NOT MERGE note * update dependency graph * add more information to comments * add links to the new documentation * fix build error in signer * change request type definitions and checkSequentialDelay function * fix linter errors * fix linter errors * but like really, fix linter errors * ok I was only joking before about fixing the linter errors. this time for sure. * add support for computational key hardening [lacks schema or tests] * extract odis mock to new file * add computational hardening to test config * add failure case tests for mutated backups * add computational hardening to schema * Update packages/sdk/identity/src/odis/circuit-breaker.ts Co-authored-by: Alec Schaefer * consolidate imports * address review comments * add odis verification error type * add safety gate to prevent use of OPRF function for key hardening in prod * bump dep versions in phone-number-privacy-common * remove new code for encrypted backup * Revert "remove new code for encrypted backup" This reverts commit e6b4c62781a5f5ffa409caa6f809fdc277ee7fbc. * populate index.ts file * fix import * fix import again * update dep graphy * fix lint errors * fix lint error * update dependency graph * remove outdated DO NOT MERGE * add phone-number-privacy-common to package list and sort the list * add @types/express as dev dependency * update circuit breaker keys to production values * fix typos and add DO NOT MERGE comments for changes to be made * bump development version of phone-number-privacy-common package * address most of the DO NOT MERGE comments * add createPasswordEncryptedBackup function * add safety measure to prevent accidental usage of the createBackup API with an empty hardening config * fix usages of renamed Endpoint enum * fix enum types * remove accidentally added walletconnect package * fix lint error * fix another lint error * fix dangling reference to CustomSigner * remove dangling refernce to signWithRawKey Co-authored-by: Alec Schaefer commit 0ac97fd5677876654aae9296e65f76779da72266 Author: Alec Schaefer Date: Tue Feb 8 20:58:52 2022 -0500 Bump sdk versions to 1.5.3-dev (#9300) commit a85bcfe02f6a3bc7a33947e816b05ffc37f09b53 Author: Alec Schaefer Date: Tue Feb 8 02:27:11 2022 -0500 add elliptic library to import-blacklist (#9302) ### Description Adds elliptic library to ts import blacklist for entire monorepo. This is to prevent vulnerabilities such as the one patched here https://github.com/celo-org/celo-monorepo/commit/a3ede3426f1af06f2126d79a1b4591e08d2624cb ### Other changes None ### Tested NA ### Related issues NA ### Backwards compatibility Yes ### Documentation None commit c6852a0a4a237eba7918fb687bf661ef27fe724e Author: Alec Schaefer Date: Fri Feb 4 22:26:02 2022 -0500 fix elliptic signing (#9271) ### Other changes Removes elliptic as a dependency from ODIS signer and combiner Updates get-contact-matches to rely on the common package's implementation of verifyDEKSignature [adds flake tracker to @celo/keystore and increases jest.timeout](https://github.com/celo-org/celo-monorepo/pull/9271/commits/2112edc37c0daf7171103e872cd7d39199d35d2f) ### Tested Test cases added to phone-number-privacy/common package ensuring that authenticateUser accepts both the new and the old signing schemes (backwards compatibility) Test cases added to @celo/identity ensuring that signWithRawKey signs the full message digest rather than the plain text message. ### Related issues - Fixes #9237 ### Backwards compatibility The verifyDEKSignature function used by authenticateUser() will now accept requests with both the old (incorrect) and the new (correct) authorization signatures. Once clients have upgraded to use this change in the @celo/identity package, the verifyDEKSignature function will be updated again to only accept the new authorization signatures. To monitor frequency of requests with incorrect signatures, we have added a new WarningMessage so that we can easily create a log based metric. We will not remove support for the old signing method until ODIS is no longer receiving requests that use it. ### Documentation None commit b64e4030bc08b2696ead71d41cfe6d2aa70cdf8a Author: Enrique Ruiz Date: Tue Feb 1 14:47:31 2022 +0100 Publish contracts to sourcify after migration (#9220) * Publish contracts to sourcify after migration * Add useLiteralContent flag * Addressing some comments * Fix to work with a report.json file getting network and paths as parameter * Fix comments * Fix tests and add note commit 51d6b0a5d36f57427ae2363d8ca4b421240b4e9a Author: Nina Barbakadze Date: Tue Feb 1 12:09:44 2022 +0100 Added a spread bound check in the setSpread function (#9252) ### Description _A few sentences describing the overall effects and goals of the pull request's commits. What is the current behavior, and what is the updated/expected behavior with this PR?_ I've created a bound check for the spread to make sure that its value never exceeds 1. ### Other changes _Describe any minor or "drive-by" changes here._ ### Tested _An explanation of how the changes were tested or an explanation as to why they don't need to be._ I've added a condition in the test suite which checks, that the value of spread shouldn't exceed 1. ### Related issues - Fixes #[issue number here] ### Backwards compatibility _Brief explanation of why these changes are/are not backwards compatible._ ### Documentation _The set of community facing docs that have been added/modified because of this change_ commit f46bc14ff7e580b57202ad830ec4af9bb116faa8 Author: Cody Born Date: Mon Jan 31 15:00:18 2022 -0800 Bump to v1.5.2 (#9153) * Bump to v1.5.1 * Update yarn.lock * Update yarn.lock * Improve git security * Update umpirsky references * Updating ganache-core version * Update yarn.lock * Fix mergify deprecation * Fix mergify deprecation * Add condition to default merge queue * Cast AuthSigner into explicit types * Keep inter-repo dependencies in-sync * update out of sync packages * Testing changes to tsconfig * Testing moduleResolution * Revert tsconfig changes * Remove circular dependency * Add back other build steps commit 2d2d52dd054afba23d55c5992aa572609d757c45 Author: Nicolas Brugneaux Date: Fri Jan 28 17:40:40 2022 +0100 chore: remove wallet-walletconnect (#9187) Co-authored-by: Daniel Kesler commit 0118ecff10e760be1eaeb58adbd7830224968520 Author: Bogdan Dumitru <304771+bowd@users.noreply.github.com> Date: Thu Jan 27 13:29:35 2022 +0100 CELOEUR Oracle Staging deployment (#7299) * Add .env.staging config * lint Co-authored-by: Martín Volpe Co-authored-by: Martin Volpe commit 8685981001b6643b346949ea51a25a2ea18e8546 Author: Javier Cortejoso Date: Wed Jan 19 17:47:09 2022 +0100 Update deployment for celo-blockchain 1.5.x (#9021) * Updated new geth cmd options for pprof * Added new format for geth http/rcp/ws flags * Add option to increase rpc request timeouts in geth * Updated alfajores to 1.5.0 * Updated baklava to 1.5.0 commit 29e2ca9f9d85c4d6fef623953f79669af8853ffe Author: eelanagaraj <7308464+eelanagaraj@users.noreply.github.com> Date: Mon Jan 17 14:14:59 2022 +0100 Re-bump Attestation Service to 1.6.0-dev commit 3cd772b1121997635701dc80ae14c6a2f9ad7b47 Author: eelanagaraj <7308464+eelanagaraj@users.noreply.github.com> Date: Mon Jan 17 13:15:41 2022 +0100 Fix newKit bug websocket providers in prep for AS v1.5.0 commit f1f02bf9cf71f05dc8a9bca073a560555482f45d Author: eelanagaraj <7308464+eelanagaraj@users.noreply.github.com> Date: Thu Jan 13 19:07:50 2022 +0100 Bump Attestation Service version to 1.6.0-dev commit 38c753fb172af278c38f5c3f9041eba420cd6c0f Author: eelanagaraj <7308464+eelanagaraj@users.noreply.github.com> Date: Thu Jan 13 13:52:35 2022 +0100 Release attestation service v1.5.0 commit d752156e661191e5ff1104e87d70377fd16ca48a Author: Martin Volpe Date: Fri May 13 15:10:45 2022 +0200 Fixed release versions commit 2c2769985117d2ba232c97fac1b2acaf6941b930 Author: Martin Volpe Date: Fri May 13 15:09:06 2022 +0200 Added newline at the end commit 50aa1df9d3f5e4264f374eeea9ffc6b330dea7e5 Author: Marcin Chrzanowski Date: Tue Feb 22 10:18:55 2022 -0500 Add version report commit 18213327e488b610a3e2c31717a79ea1de8d000a Author: Marcin Chrzanowski Date: Tue Feb 15 11:06:21 2022 -0500 Add empty initialization data for release 6 commit 373dd350d3f4a477c431435278c600b09483b334 Author: Yorke Rhodes Date: Tue Nov 30 13:53:06 2021 -0800 Use OZ safeTransfer in genericTransfer (#9025) * Use OZ safeTransfer in genericTransfer * Add unit test for safeTransfer revert commit 680c84b0a25187be62515584ee5769aed75197ad Author: Yorke Rhodes Date: Tue Nov 30 13:52:35 2021 -0800 Use SafeMath in getOffchainStorageRoots (#9026) commit 662eacd07f94da85d55a02a2f935361b87e83589 Author: Yorke Rhodes Date: Tue Nov 30 13:44:55 2021 -0800 Add revert message to removeStorageRoot (#9010) * Add revert message to removeStorageRoot * Update unit test commit b5d80c742b7f4ed8a3615e0127e57e96f63b8341 Author: Martín Volpe Date: Thu Apr 28 14:26:00 2022 +0200 Added version report CR7 (#9504) commit 9acf3d041d949ee3aa023ee6afca2609fcd7a22c Author: Martin Date: Fri Mar 11 10:49:50 2022 +0100 Add version report for relase 6 (#9386) * Add version report for relase 6 * Generate report against v5 commit 160eed127314f0c82ee2a63b582b5feef3b1a2da Author: Martin Date: Thu Mar 10 22:57:03 2022 +0100 Fix test (#9385) commit 7e9e31a9d7c479ad265a0788886fa055422530fc Author: Edwin O Date: Thu Mar 10 19:05:40 2022 +0100 Fix typographical errors to improve readablity. (#9250) Co-authored-by: Martín Volpe Co-authored-by: Martin commit ae09bf66190a02835ebfbb26ace8506f05eb5208 Author: Martin Date: Thu Mar 10 18:46:31 2022 +0100 Increment Validators and Accounts versions (#9256) * Update prior release tag for CI version checking * Fetch the necessary branch * Use node 12 on prior release * Increment version numbers * Bump ExchangeBRL version * Add empty initalization data commit 7849aa80ddb95ef9fc75705b55eb24e72e4169b7 Author: Martin Date: Tue Mar 8 14:43:23 2022 +0100 Disallow a 0x0 address beneficiary (#9283) * Disallow address 0x0 beneficiaries * Add function to remove payment delegation * Add documentation * Require deletion of payment delegation is done by account * Specify deletePaymentDelegation as non privileged commit 7828bd633fbb108f02578a3e372336149e5d3b96 Author: Martin Date: Tue Mar 8 12:26:45 2022 +0100 Fix indentation (#9285) commit ca4afce61a5201419b7770863b108e58efcc885e Author: Nina Barbakadze Date: Mon Mar 7 15:03:15 2022 +0100 replaced unnecessary calculations with SECONDS_IN_A_WEEK constant (#9269) Co-authored-by: Nina Barbakadze Co-authored-by: Martín Volpe commit 4625c3fb2899a5145fa02bae16c2857e70ff0660 Author: Nina Barbakadze Date: Tue Feb 22 21:21:23 2022 +0100 Resolve inadequate NatSpec (#9270) * Delete .mergify.yml * Fixed wrong merge * Rolled back wallet connect * lint * fix lint * Force CircleCI to fetch all tags * Brought report from master --- .circleci/config.yml | 6 +- packages/faucet/.gitignore | 8 - packages/faucet/tslint.json | 7 - .../combiner/package.json | 2 +- .../phone-number-privacy/monitor/package.json | 2 +- .../phone-number-privacy/signer/package.json | 2 +- .../protocol/contracts/common/Accounts.sol | 17 +- .../protocol/contracts/stability/Exchange.sol | 4 +- .../contracts/stability/ExchangeBRL.sol | 14 +- .../contracts/stability/GrandaMento.sol | 7 +- .../protocol/contracts/stability/Reserve.sol | 10 +- .../contracts/stability/StableTokenBRL.sol | 14 +- .../versionReports/release7-report.json | 235 ++++++++++++++++++ .../bash/generate-old-devchain-and-build.sh | 2 +- .../protocol/specs/accountsPrivileged.spec | 1 + packages/protocol/test/common/accounts.ts | 41 +++ .../test/governance/validators/validators.ts | 2 +- .../test/governance/voting/lockedgold.ts | 10 +- .../protocol/test/stability/grandamento.ts | 13 +- packages/protocol/test/stability/reserve.ts | 24 +- packages/sdk/CHANGELOG.md | 42 ---- packages/sdk/encrypted-backup/package.json | 2 +- 22 files changed, 370 insertions(+), 95 deletions(-) delete mode 100644 packages/faucet/.gitignore delete mode 100644 packages/faucet/tslint.json create mode 100644 packages/protocol/releaseData/versionReports/release7-report.json diff --git a/.circleci/config.yml b/.circleci/config.yml index 956d3bfc63d..360c85e0cd8 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -34,7 +34,7 @@ defaults: &defaults contract-defaults: &contract-defaults <<: *defaults environment: - RELEASE_TAG: core-contracts.v6 + RELEASE_TAG: core-contracts.v7 e2e-defaults: &e2e-defaults <<: *defaults @@ -151,6 +151,7 @@ jobs: # Verify that following commands work, they are later called in the incremental testing script # There output does not matter here, the fact that they finish successfully does. git rev-parse --abbrev-ref HEAD + git fetch --all --tags - attach_workspace: at: ~/app @@ -809,6 +810,9 @@ jobs: name: Check if the test should run command: | ./scripts/ci_check_if_test_should_run_v2.sh @celo/protocol + # Disabling certora until the license is figured out + circleci step halt + exit 0 - run: name: Certora dependencies command: | diff --git a/packages/faucet/.gitignore b/packages/faucet/.gitignore deleted file mode 100644 index f13cb0d9e11..00000000000 --- a/packages/faucet/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -dist/ - -# Firebase cache -.firebase/ - -serviceAccountKey.json -firebase-debug.log -database-rules.json \ No newline at end of file diff --git a/packages/faucet/tslint.json b/packages/faucet/tslint.json deleted file mode 100644 index dfc1be41d87..00000000000 --- a/packages/faucet/tslint.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": ["@celo/typescript/tslint.json"], - "rules": { - "no-global-arrow-functions": false, - "no-console": false - } -} diff --git a/packages/phone-number-privacy/combiner/package.json b/packages/phone-number-privacy/combiner/package.json index 244117a3dcb..29ce6baaf40 100644 --- a/packages/phone-number-privacy/combiner/package.json +++ b/packages/phone-number-privacy/combiner/package.json @@ -55,4 +55,4 @@ "engines": { "node": "12" } -} \ No newline at end of file +} diff --git a/packages/phone-number-privacy/monitor/package.json b/packages/phone-number-privacy/monitor/package.json index ad5283fbd7a..a8eba9b34ea 100644 --- a/packages/phone-number-privacy/monitor/package.json +++ b/packages/phone-number-privacy/monitor/package.json @@ -37,4 +37,4 @@ "engines": { "node": "12" } -} \ No newline at end of file +} diff --git a/packages/phone-number-privacy/signer/package.json b/packages/phone-number-privacy/signer/package.json index c654aa3508e..bf7641a3bc6 100644 --- a/packages/phone-number-privacy/signer/package.json +++ b/packages/phone-number-privacy/signer/package.json @@ -64,4 +64,4 @@ "engines": { "node": ">=10" } -} \ No newline at end of file +} diff --git a/packages/protocol/contracts/common/Accounts.sol b/packages/protocol/contracts/common/Accounts.sol index bcf10119330..b24cc6c8b2a 100644 --- a/packages/protocol/contracts/common/Accounts.sol +++ b/packages/protocol/contracts/common/Accounts.sol @@ -318,22 +318,35 @@ contract Accounts is /** * @notice Sets validator payment delegation settings. - * @param beneficiary The address that should receive a portion of vaidator + * @param beneficiary The address that should receive a portion of validator * payments. * @param fraction The fraction of the validator's payment that should be - * diverted to `beneficiary` every epoch, given as FixidyLib value. Must not + * diverted to `beneficiary` every epoch, given as FixidityLib value. Must not * be greater than 1. + * @dev Use `deletePaymentDelegation` to unset the payment delegation. */ function setPaymentDelegation(address beneficiary, uint256 fraction) public { require(isAccount(msg.sender), "Not an account"); + require(beneficiary != address(0), "Beneficiary cannot be address 0x0"); FixidityLib.Fraction memory f = FixidityLib.wrap(fraction); require(f.lte(FixidityLib.fixed1()), "Fraction must not be greater than 1"); paymentDelegations[msg.sender] = PaymentDelegation(beneficiary, f); emit PaymentDelegationSet(beneficiary, fraction); } + /** + * @notice Removes a validator's payment delegation by setting benficiary and + * fraction to 0. + */ + function deletePaymentDelegation() public { + require(isAccount(msg.sender), "Not an account"); + paymentDelegations[msg.sender] = PaymentDelegation(address(0x0), FixidityLib.wrap(0)); + emit PaymentDelegationSet(address(0x0), 0); + } + /** * @notice Gets validator payment delegation settings. + * @param account Account of the validator. * @return Beneficiary address and fraction of payment delegated. */ function getPaymentDelegation(address account) external view returns (address, uint256) { diff --git a/packages/protocol/contracts/stability/Exchange.sol b/packages/protocol/contracts/stability/Exchange.sol index 9e1268c3e75..55183c52aae 100644 --- a/packages/protocol/contracts/stability/Exchange.sol +++ b/packages/protocol/contracts/stability/Exchange.sol @@ -78,6 +78,7 @@ contract Exchange is /** * @notice Used in place of the constructor to allow the contract to be upgradable via proxy. * @param registryAddress The address of the registry core smart contract. + * @param stableTokenIdentifier String identifier of stabletoken in registry * @param _spread Spread charged on exchanges * @param _reserveFraction Fraction to commit to the gold bucket * @param _updateFrequency The time period that needs to elapse between bucket @@ -85,7 +86,6 @@ contract Exchange is * @param _minimumReports The minimum number of fresh reports that need to be * present in the oracle to update buckets * commit to the gold bucket - * @param stableTokenIdentifier String identifier of stabletoken in registry */ function initialize( address registryAddress, @@ -298,7 +298,7 @@ contract Exchange is spread = FixidityLib.wrap(newSpread); require( FixidityLib.lte(spread, FixidityLib.fixed1()), - "the value of spread must be less than or equal to 1" + "Spread must be less than or equal to 1" ); emit SpreadSet(newSpread); } diff --git a/packages/protocol/contracts/stability/ExchangeBRL.sol b/packages/protocol/contracts/stability/ExchangeBRL.sol index d8dd6a8c3be..24d5c1cf800 100644 --- a/packages/protocol/contracts/stability/ExchangeBRL.sol +++ b/packages/protocol/contracts/stability/ExchangeBRL.sol @@ -4,16 +4,16 @@ import "./Exchange.sol"; contract ExchangeBRL is Exchange { /** - * @notice Sets initialized == true on implementation contracts - * @param test Set to true to skip implementation initialization - */ + * @notice Sets initialized == true on implementation contracts + * @param test Set to true to skip implementation initialization + */ constructor(bool test) public Exchange(test) {} /** - * @notice Returns the storage, major, minor, and patch version of the contract. -* @dev This function is overloaded to maintain a distinct version from Exchange.sol. -* @return The storage, major, minor, and patch version of the contract. -*/ + * @notice Returns the storage, major, minor, and patch version of the contract. + * @dev This function is overloaded to maintain a distinct version from Exchange.sol. + * @return The storage, major, minor, and patch version of the contract. + */ function getVersionNumber() external pure returns (uint256, uint256, uint256, uint256) { return (1, 2, 0, 0); } diff --git a/packages/protocol/contracts/stability/GrandaMento.sol b/packages/protocol/contracts/stability/GrandaMento.sol index 631d37be971..6c29c529773 100644 --- a/packages/protocol/contracts/stability/GrandaMento.sol +++ b/packages/protocol/contracts/stability/GrandaMento.sol @@ -167,7 +167,7 @@ contract GrandaMento is * @return The storage, major, minor, and patch version of the contract. */ function getVersionNumber() external pure returns (uint256, uint256, uint256, uint256) { - return (1, 1, 0, 0); + return (1, 1, 0, 1); } /** @@ -582,8 +582,11 @@ contract GrandaMento is * @param newSpread The new value for the spread to be wrapped. Must be <= fixed 1. */ function setSpread(uint256 newSpread) public onlyOwner { - require(newSpread <= FixidityLib.fixed1().unwrap(), "Spread must be smaller than 1"); spread = FixidityLib.wrap(newSpread); + require( + FixidityLib.lte(spread, FixidityLib.fixed1()), + "Spread must be less than or equal to 1" + ); emit SpreadSet(newSpread); } diff --git a/packages/protocol/contracts/stability/Reserve.sol b/packages/protocol/contracts/stability/Reserve.sol index c31e91ac924..61e378555af 100644 --- a/packages/protocol/contracts/stability/Reserve.sol +++ b/packages/protocol/contracts/stability/Reserve.sol @@ -538,9 +538,13 @@ contract Reserve is uint256 stableAmount; uint256 goldAmount; (stableAmount, goldAmount) = sortedOracles.medianRate(_tokens[i]); - uint256 stableTokenSupply = IERC20(_tokens[i]).totalSupply(); - uint256 aStableTokenValueInGold = stableTokenSupply.mul(goldAmount).div(stableAmount); - stableTokensValueInGold = stableTokensValueInGold.add(aStableTokenValueInGold); + + if (goldAmount != 0) { + // tokens with no oracle reports don't count towards collateralization ratio + uint256 stableTokenSupply = IERC20(_tokens[i]).totalSupply(); + uint256 aStableTokenValueInGold = stableTokenSupply.mul(goldAmount).div(stableAmount); + stableTokensValueInGold = stableTokensValueInGold.add(aStableTokenValueInGold); + } } return FixidityLib diff --git a/packages/protocol/contracts/stability/StableTokenBRL.sol b/packages/protocol/contracts/stability/StableTokenBRL.sol index c3eb7d4e7a4..c2c9392a055 100644 --- a/packages/protocol/contracts/stability/StableTokenBRL.sol +++ b/packages/protocol/contracts/stability/StableTokenBRL.sol @@ -4,16 +4,16 @@ import "./StableToken.sol"; contract StableTokenBRL is StableToken { /** - * @notice Sets initialized == true on implementation contracts. - * @param test Set to true to skip implementation initialization. - */ + * @notice Sets initialized == true on implementation contracts. + * @param test Set to true to skip implementation initialization. + */ constructor(bool test) public StableToken(test) {} /** - * @notice Returns the storage, major, minor, and patch version of the contract. - * @dev This function is overloaded to maintain a distinct version from StableToken.sol. - * @return The storage, major, minor, and patch version of the contract. - */ + * @notice Returns the storage, major, minor, and patch version of the contract. + * @dev This function is overloaded to maintain a distinct version from StableToken.sol. + * @return The storage, major, minor, and patch version of the contract. + */ function getVersionNumber() external pure returns (uint256, uint256, uint256, uint256) { return (1, 1, 0, 0); } diff --git a/packages/protocol/releaseData/versionReports/release7-report.json b/packages/protocol/releaseData/versionReports/release7-report.json new file mode 100644 index 00000000000..1d961b99f6d --- /dev/null +++ b/packages/protocol/releaseData/versionReports/release7-report.json @@ -0,0 +1,235 @@ +{ + "oldArtifactsFolder": "/home/circleci/app/packages/protocol/build/core-contracts.v6/contracts", + "newArtifactsFolder": "/home/circleci/app/packages/protocol/build/core-contracts.v7/contracts", + "exclude": "/.*Test|Mock.*|I[A-Z].*|.*Proxy|MultiSig.*|ReleaseGold|SlasherUtil|UsingPrecompiles/", + "report": { + "contracts": { + "Exchange": { + "changes": { + "storage": [], + "major": [ + { + "contract": "Exchange", + "signature": "initialize(address,address,uint256,uint256,uint256,uint256)", + "type": "MethodRemoved" + } + ], + "minor": [ + { + "contract": "Exchange", + "signature": "initialize(address,string,uint256,uint256,uint256,uint256)", + "type": "MethodAdded" + }, + { + "contract": "Exchange", + "signature": "activateStable()", + "type": "MethodAdded" + } + ], + "patch": [ + { + "contract": "Exchange", + "type": "DeployedBytecode" + } + ] + }, + "versionDelta": { + "storage": "=", + "major": "+1", + "minor": "0", + "patch": "0" + } + }, + "ExchangeBRL": { + "changes": { + "storage": [], + "major": [ + { + "contract": "ExchangeBRL", + "signature": "initialize(address,address,uint256,uint256,uint256,uint256)", + "type": "MethodRemoved" + } + ], + "minor": [ + { + "contract": "ExchangeBRL", + "signature": "initialize(address,string,uint256,uint256,uint256,uint256)", + "type": "MethodAdded" + }, + { + "contract": "ExchangeBRL", + "signature": "activateStable()", + "type": "MethodAdded" + } + ], + "patch": [ + { + "contract": "ExchangeBRL", + "type": "DeployedBytecode" + } + ] + }, + "versionDelta": { + "storage": "=", + "major": "+1", + "minor": "0", + "patch": "0" + } + }, + "ExchangeEUR": { + "changes": { + "storage": [], + "major": [ + { + "contract": "ExchangeEUR", + "signature": "initialize(address,address,uint256,uint256,uint256,uint256)", + "type": "MethodRemoved" + } + ], + "minor": [ + { + "contract": "ExchangeEUR", + "signature": "initialize(address,string,uint256,uint256,uint256,uint256)", + "type": "MethodAdded" + }, + { + "contract": "ExchangeEUR", + "signature": "activateStable()", + "type": "MethodAdded" + } + ], + "patch": [ + { + "contract": "ExchangeEUR", + "type": "DeployedBytecode" + } + ] + }, + "versionDelta": { + "storage": "=", + "major": "+1", + "minor": "0", + "patch": "0" + } + }, + "Accounts": { + "changes": { + "storage": [], + "major": [], + "minor": [ + { + "contract": "Accounts", + "signature": "setPaymentDelegation(address,uint256)", + "type": "MethodAdded" + }, + { + "contract": "Accounts", + "signature": "getPaymentDelegation(address)", + "type": "MethodAdded" + }, + { + "contract": "Accounts", + "signature": "deletePaymentDelegation()", + "type": "MethodAdded" + } + ], + "patch": [ + { + "contract": "Accounts", + "type": "DeployedBytecode" + } + ] + }, + "versionDelta": { + "storage": "=", + "major": "=", + "minor": "+1", + "patch": "0" + } + }, + "LockedGold": { + "changes": { + "storage": [], + "major": [], + "minor": [ + { + "contract": "LockedGold", + "signature": "getPendingWithdrawal(address,uint256)", + "type": "MethodAdded" + } + ], + "patch": [ + { + "contract": "LockedGold", + "type": "DeployedBytecode" + } + ] + }, + "versionDelta": { + "storage": "=", + "major": "=", + "minor": "+1", + "patch": "0" + } + }, + "GrandaMento": { + "changes": { + "storage": [], + "major": [], + "minor": [], + "patch": [ + { + "contract": "GrandaMento", + "type": "DeployedBytecode" + } + ] + }, + "versionDelta": { + "storage": "=", + "major": "=", + "minor": "=", + "patch": "+1" + } + }, + "Reserve": { + "changes": { + "storage": [], + "major": [], + "minor": [], + "patch": [ + { + "contract": "Reserve", + "type": "DeployedBytecode" + } + ] + }, + "versionDelta": { + "storage": "=", + "major": "=", + "minor": "=", + "patch": "+1" + } + }, + "Validators": { + "changes": { + "storage": [], + "major": [], + "minor": [], + "patch": [ + { + "contract": "Validators", + "type": "DeployedBytecode" + } + ] + }, + "versionDelta": { + "storage": "=", + "major": "=", + "minor": "=", + "patch": "+1" + } + } + }, + "libraries": {} + } +} diff --git a/packages/protocol/scripts/bash/generate-old-devchain-and-build.sh b/packages/protocol/scripts/bash/generate-old-devchain-and-build.sh index c44f02349ee..23e6c4ddad8 100755 --- a/packages/protocol/scripts/bash/generate-old-devchain-and-build.sh +++ b/packages/protocol/scripts/bash/generate-old-devchain-and-build.sh @@ -30,7 +30,7 @@ done [ -z "$BUILD_DIR" ] && BUILD_DIR=$(echo build/$(echo $BRANCH | sed -e 's/\//_/g')); echo "- Checkout source code at $BRANCH" -git fetch origin +'refs/tags/core-contracts.*:refs/tags/core-contracts.*' 2>>$LOG_FILE >> $LOG_FILE +git fetch origin +"$BRANCH" 2>>$LOG_FILE >> $LOG_FILE git checkout $BRANCH 2>>$LOG_FILE >> $LOG_FILE echo "- Build contract artifacts" diff --git a/packages/protocol/specs/accountsPrivileged.spec b/packages/protocol/specs/accountsPrivileged.spec index 7331007ccb5..19e293a6edf 100644 --- a/packages/protocol/specs/accountsPrivileged.spec +++ b/packages/protocol/specs/accountsPrivileged.spec @@ -25,6 +25,7 @@ definition knownAsNonPrivileged(method f) returns bool = false || f.selector == removeSigner(address,bytes32).selector || f.selector == setEip712DomainSeparator().selector || f.selector == setPaymentDelegation(address,uint256).selector + || f.selector == deletePaymentDelegation().selector ; rule privilegedOperation(method f, address privileged) diff --git a/packages/protocol/test/common/accounts.ts b/packages/protocol/test/common/accounts.ts index 9c7f8577129..7257b2ffbef 100644 --- a/packages/protocol/test/common/accounts.ts +++ b/packages/protocol/test/common/accounts.ts @@ -486,6 +486,13 @@ contract('Accounts', (accounts: string[]) => { ) }) + it('should not allow a beneficiary with address 0x0', async () => { + await assertRevertWithReason( + accountsInstance.setPaymentDelegation(NULL_ADDRESS, fraction), + 'Beneficiary cannot be address 0x0' + ) + }) + it('emits a PaymentDelegationSet event', async () => { const resp = await accountsInstance.setPaymentDelegation(beneficiary, fraction) assertLogMatches2(resp.logs[0], { @@ -496,6 +503,40 @@ contract('Accounts', (accounts: string[]) => { }) }) + describe('#deletePaymentDelegation', () => { + const beneficiary = accounts[1] + const fraction = toFixed(0.2) + + beforeEach(async () => { + await accountsInstance.createAccount() + await accountsInstance.setPaymentDelegation(beneficiary, fraction) + }) + + it('should not be callable by a non-account', async () => { + await assertRevertWithReason( + accountsInstance.setPaymentDelegation(beneficiary, fraction, { from: accounts[2] }), + 'Not an account' + ) + }) + + it('should set the address and beneficiary to 0', async () => { + await accountsInstance.deletePaymentDelegation() + const [realBeneficiary, realFraction] = await accountsInstance.getPaymentDelegation.call( + accounts[0] + ) + assert.equal(realBeneficiary, NULL_ADDRESS) + assertEqualBN(realFraction, new BigNumber(0)) + }) + + it('emits a PaymentDelegationSet event', async () => { + const resp = await accountsInstance.deletePaymentDelegation() + assertLogMatches2(resp.logs[0], { + event: 'PaymentDelegationSet', + args: { beneficiary: NULL_ADDRESS, fraction: new BigNumber(0) }, + }) + }) + }) + describe('#setName', () => { describe('when the account has not been created', () => { it('should revert', async () => { diff --git a/packages/protocol/test/governance/validators/validators.ts b/packages/protocol/test/governance/validators/validators.ts index cacefe887f4..f3154fa494a 100644 --- a/packages/protocol/test/governance/validators/validators.ts +++ b/packages/protocol/test/governance/validators/validators.ts @@ -2274,7 +2274,7 @@ contract('Validators', (accounts: string[]) => { expectedDelegatedPayment = new BigNumber(0) expectedValidatorPayment = expectedTotalPayment.minus(expectedGroupPayment) - await accountsInstance.setPaymentDelegation(NULL_ADDRESS, toFixed(0)) + await accountsInstance.deletePaymentDelegation() ret = await validators.distributeEpochPaymentsFromSigner.call(validator, maxPayment) await validators.distributeEpochPaymentsFromSigner(validator, maxPayment) diff --git a/packages/protocol/test/governance/voting/lockedgold.ts b/packages/protocol/test/governance/voting/lockedgold.ts index fa0431bf171..21265141d41 100644 --- a/packages/protocol/test/governance/voting/lockedgold.ts +++ b/packages/protocol/test/governance/voting/lockedgold.ts @@ -202,13 +202,21 @@ contract('LockedGold', (accounts: string[]) => { ) }) - it('should add a pending withdrawal', async () => { + it('should add a pending withdrawal #getPendingWithdrawal()', async () => { const [val, timestamp] = await lockedGold.getPendingWithdrawal(account, 0) assertEqualBN(val, value) assertEqualBN(timestamp, availabilityTime) await assertRevert(lockedGold.getPendingWithdrawal(account, 1)) }) + it('should add a pending withdrawal #getPendingWithdrawals()', 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) }) diff --git a/packages/protocol/test/stability/grandamento.ts b/packages/protocol/test/stability/grandamento.ts index 4e537f502b2..c87a97e7a72 100644 --- a/packages/protocol/test/stability/grandamento.ts +++ b/packages/protocol/test/stability/grandamento.ts @@ -3,6 +3,7 @@ import { assertEqualBN, assertEqualBNArray, assertLogMatches2, + assertRevert, assertRevertWithReason, timeTravel, } from '@celo/protocol/lib/test-utils' @@ -1085,10 +1086,10 @@ contract('GrandaMento', (accounts: string[]) => { }) }) - it('reverts when the spread is greater than 1', async () => { - await assertRevertWithReason( - grandaMento.setSpread(toFixed(1.0001)), - 'Spread must be smaller than 1' + it('reverts when the spread is more than 1', async () => { + await assertRevert( + grandaMento.setSpread(toFixed(1001 / 1000)), + 'Spread must be less than or equal to 1' ) }) @@ -1167,7 +1168,7 @@ contract('GrandaMento', (accounts: string[]) => { }) describe('#setVetoPeriodSeconds', () => { - const newVetoPeriodSeconds = 60 * 60 * 24 * 7 // 7 days + const newVetoPeriodSeconds = SECONDS_IN_A_WEEK it('sets the spread', async () => { await grandaMento.setVetoPeriodSeconds(newVetoPeriodSeconds) assertEqualBN(await grandaMento.vetoPeriodSeconds(), newVetoPeriodSeconds) @@ -1184,7 +1185,7 @@ contract('GrandaMento', (accounts: string[]) => { }) it('reverts when the veto period is greater than 4 weeks', async () => { - const fourWeeks = 60 * 60 * 24 * 7 * 4 + const fourWeeks = SECONDS_IN_A_WEEK * 4 await assertRevertWithReason( grandaMento.setVetoPeriodSeconds(fourWeeks + 1), 'Veto period cannot exceed 4 weeks' diff --git a/packages/protocol/test/stability/reserve.ts b/packages/protocol/test/stability/reserve.ts index 64afbb1b223..d64c6f38586 100644 --- a/packages/protocol/test/stability/reserve.ts +++ b/packages/protocol/test/stability/reserve.ts @@ -8,7 +8,6 @@ import { } from '@celo/protocol/lib/test-utils' import { fromFixed, toFixed } from '@celo/utils/lib/fixidity' import BigNumber from 'bignumber.js' -import BN = require('bn.js') import { MockSortedOraclesInstance, MockStableTokenInstance, @@ -16,6 +15,9 @@ import { ReserveInstance, } from 'types' +// tslint:disable-next-line: ordered-imports +import BN = require('bn.js') + const Registry: Truffle.Contract = artifacts.require('Registry') const Reserve: Truffle.Contract = artifacts.require('Reserve') const MockStableToken: Truffle.Contract = artifacts.require( @@ -804,6 +806,26 @@ contract('Reserve', (accounts: string[]) => { value: reserveGoldBalance, }) }) + + describe('works with stabletoken with no report', async () => { + let mockStableToken2: MockStableTokenInstance + beforeEach(async () => { + // Add another stable token + mockStableToken2 = await MockStableToken.new() + await reserve.addToken(mockStableToken2.address) + }) + + it('should return the correct ratio', async () => { + const stableTokenSupply = new BigNumber(10).pow(21) + await mockStableToken.setTotalSupply(stableTokenSupply) + const ratio = new BigNumber(await reserve.getReserveRatio()) + assert( + fromFixed(ratio).isEqualTo(reserveGoldBalance.div(stableTokenSupply.div(exchangeRate))), + 'reserve ratio should be correct' + ) + }) + }) + it('should return the correct ratio', async () => { const stableTokenSupply = new BigNumber(10).pow(21) await mockStableToken.setTotalSupply(stableTokenSupply) diff --git a/packages/sdk/CHANGELOG.md b/packages/sdk/CHANGELOG.md index b5012229faf..99a6fb86f64 100644 --- a/packages/sdk/CHANGELOG.md +++ b/packages/sdk/CHANGELOG.md @@ -11,48 +11,6 @@ Other Changes - one-line summary - [#](link PR) - migrated @celo/wallet-walletconnect from celo/celo-monorepo to celo/use-contractkit - -## Development (not published yet) - -### **[2.0.0]** - -See [Upgrade Guide](https://docs.celo.org/developer-guide/contractkit/migrating-to-contractkit-v2) - -Breaking Changes - -for detail info see [#9127](https://github.com/celo-org/celo-monorepo/pull/9127) - -- @celo/contractkit - - AccountsWrapper.authorizeValidatorSigner now requires a ValidatorWrapper as third param - - kit is now longer required when constructing WrapperCache, AddressRegistry, Contract Wrappers - - proxySend now takes a Connection rather than a Kit - -- @celo/utils - - - Removes phone and country related functions from utils. Now in [phone-utils](https://github.com/celo-org/celo-monorepo/pull/8987) - - - comment encryption, bls and mneumonic functions moved to @celo/cryptographic-utils - - -Features - -- All Packages Now marked as Side Effect: false - -- Bundle Sized Reduced 45% - -- Adds the @celo/encrypted-backup package - [#8896] [#9348] [#9413] [#9434] - -[#8896]: https://github.com/celo-org/celo-monorepo/pull/8896 -[#9348]: https://github.com/celo-org/celo-monorepo/pull/9348 -[#9413]: https://github.com/celo-org/celo-monorepo/pull/9413 -[#9434]: https://github.com/celo-org/celo-monorepo/pull/9434 - -Bug Fixes / Package Updates - -- Upgrades bls12377, bip32, - - - ## Published ### **[1.5.0]** diff --git a/packages/sdk/encrypted-backup/package.json b/packages/sdk/encrypted-backup/package.json index df9526a1223..ecf855aa4b2 100644 --- a/packages/sdk/encrypted-backup/package.json +++ b/packages/sdk/encrypted-backup/package.json @@ -43,4 +43,4 @@ "engines": { "node": ">=8.13.0" } -} \ No newline at end of file +} From 4bf959b1fde04aa953ffdb54d17c9b915e08bada Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Volpe?= Date: Tue, 20 Sep 2022 11:29:12 +0200 Subject: [PATCH 28/30] Removed versions for UsingRegistry and patch tooling (#9866) --- .../protocol/contracts/common/UsingRegistryV2.sol | 8 -------- .../common/UsingRegistryV2BackwardsCompatible.sol | 7 ------- packages/protocol/scripts/bash/check-versions.sh | 4 +--- .../protocol/scripts/bash/contract-exclusion-regex.sh | 11 +++++++++++ packages/protocol/scripts/bash/release-on-devchain.sh | 3 ++- 5 files changed, 14 insertions(+), 19 deletions(-) create mode 100644 packages/protocol/scripts/bash/contract-exclusion-regex.sh diff --git a/packages/protocol/contracts/common/UsingRegistryV2.sol b/packages/protocol/contracts/common/UsingRegistryV2.sol index b9d9291ae21..6986ad5eed6 100644 --- a/packages/protocol/contracts/common/UsingRegistryV2.sol +++ b/packages/protocol/contracts/common/UsingRegistryV2.sol @@ -79,14 +79,6 @@ contract UsingRegistryV2 { _; } - /** - * @notice Returns the storage, major, minor, and patch version of the contract. - * @return The storage, major, minor, and patch version of the contract. - */ - function getVersionNumber() external pure returns (uint256, uint256, uint256, uint256) { - return (1, 1, 1, 0); - } - function getAccounts() internal view returns (IAccounts) { return IAccounts(registryContract.getAddressForOrDie(ACCOUNTS_REGISTRY_ID)); } diff --git a/packages/protocol/contracts/common/UsingRegistryV2BackwardsCompatible.sol b/packages/protocol/contracts/common/UsingRegistryV2BackwardsCompatible.sol index af9f8471b4c..2ad711e43c4 100644 --- a/packages/protocol/contracts/common/UsingRegistryV2BackwardsCompatible.sol +++ b/packages/protocol/contracts/common/UsingRegistryV2BackwardsCompatible.sol @@ -7,11 +7,4 @@ contract UsingRegistryV2BackwardsCompatible is UsingRegistryV2 { // without breaking release tooling. // Use `registryContract` (in UsingRegistryV2) for the actual registry address. IRegistry public registry; - /** - * @notice Returns the storage, major, minor, and patch version of the contract. - * @return The storage, major, minor, and patch version of the contract. - */ - function getVersionNumber() external pure returns (uint256, uint256, uint256, uint256) { - return (1, 1, 0, 0); - } } diff --git a/packages/protocol/scripts/bash/check-versions.sh b/packages/protocol/scripts/bash/check-versions.sh index 5f25c72df0e..cb3d22e41ce 100755 --- a/packages/protocol/scripts/bash/check-versions.sh +++ b/packages/protocol/scripts/bash/check-versions.sh @@ -28,9 +28,7 @@ done [ -z "$OLD_BRANCH" ] && echo "Need to set the old branch via the -a flag" && exit 1; [ -z "$NEW_BRANCH" ] && echo "Need to set the new branch via the -b flag" && exit 1; -# Exclude test contracts, mock contracts, contract interfaces, Proxy contracts, inlined libraries, -# MultiSig contracts, and the ReleaseGold contract. -CONTRACT_EXCLUSION_REGEX=".*Test|Mock.*|I[A-Z].*|.*Proxy|MultiSig.*|ReleaseGold|SlasherUtil|UsingPrecompiles" +source scripts/bash/contract-exclusion-regex.sh REPORT_FLAG="" if [ ! -z "$REPORT" ]; then diff --git a/packages/protocol/scripts/bash/contract-exclusion-regex.sh b/packages/protocol/scripts/bash/contract-exclusion-regex.sh new file mode 100644 index 00000000000..5992e43878c --- /dev/null +++ b/packages/protocol/scripts/bash/contract-exclusion-regex.sh @@ -0,0 +1,11 @@ +# Exclude test contracts, mock contracts, contract interfaces, Proxy contracts, inlined libraries, +# MultiSig contracts, and the ReleaseGold contract. +CONTRACT_EXCLUSION_REGEX=".*Test|Mock.*|I[A-Z].*|.*Proxy|MultiSig.*|ReleaseGold|SlasherUtil|UsingPrecompiles" + +# Before CR7, UsingRegistry and UsingRegistryV2 had been deployed, they need to keep getting deployed to keep the release reports without changes. +VERSION_NUMBER=$(echo "$OLD_BRANCH" | tr -dc '0-9') + +if [ $VERSION_NUMBER -gt 6 ] + then + CONTRACT_EXCLUSION_REGEX="$CONTRACT_EXCLUSION_REGEX|^UsingRegistry" +fi diff --git a/packages/protocol/scripts/bash/release-on-devchain.sh b/packages/protocol/scripts/bash/release-on-devchain.sh index 0c0f7fb44b5..4af9b12b278 100755 --- a/packages/protocol/scripts/bash/release-on-devchain.sh +++ b/packages/protocol/scripts/bash/release-on-devchain.sh @@ -47,7 +47,8 @@ yarn run truffle exec ./scripts/truffle/verify-bytecode.js --network development echo "- Check versions of current branch" # From check-versions.sh -CONTRACT_EXCLUSION_REGEX=".*Test|Mock.*|I[A-Z].*|.*Proxy|MultiSig.*|ReleaseGold|SlasherUtil|UsingPrecompiles" +OLD_BRANCH=$BUILD_DIR +source scripts/bash/contract-exclusion-regex.sh yarn ts-node scripts/check-backward.ts sem_check --old_contracts $BUILD_DIR/contracts --new_contracts build/contracts --exclude $CONTRACT_EXCLUSION_REGEX --output_file report.json # From make-release.sh From 0b604abc41dcfc87af546475167e01a44337101c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Volpe?= Date: Wed, 21 Sep 2022 14:17:34 +0200 Subject: [PATCH 29/30] Fixed "createFn is not a function" (#9880) --- packages/sdk/contractkit/src/base.ts | 1 + packages/sdk/contractkit/src/proxy.ts | 2 ++ packages/sdk/contractkit/src/web3-contract-cache.ts | 2 ++ 3 files changed, 5 insertions(+) diff --git a/packages/sdk/contractkit/src/base.ts b/packages/sdk/contractkit/src/base.ts index 082358d2f69..99f8ae52200 100644 --- a/packages/sdk/contractkit/src/base.ts +++ b/packages/sdk/contractkit/src/base.ts @@ -22,6 +22,7 @@ export enum CeloContract { MetaTransactionWallet = 'MetaTransactionWallet', MetaTransactionWalletDeployer = 'MetaTransactionWalletDeployer', MultiSig = 'MultiSig', + OdisPayments = 'OdisPayments', Random = 'Random', Registry = 'Registry', Reserve = 'Reserve', diff --git a/packages/sdk/contractkit/src/proxy.ts b/packages/sdk/contractkit/src/proxy.ts index c9a4867c713..425e9ea0d05 100644 --- a/packages/sdk/contractkit/src/proxy.ts +++ b/packages/sdk/contractkit/src/proxy.ts @@ -20,6 +20,7 @@ import { ABI as LockedGoldABI } from './generated/LockedGold' import { ABI as MetaTransactionWalletABI } from './generated/MetaTransactionWallet' import { ABI as MetaTransactionWalletDeployerABI } from './generated/MetaTransactionWalletDeployer' import { ABI as MultiSigABI } from './generated/MultiSig' +import { ABI as OdisPaymentsABI } from './generated/OdisPayments' import { ABI as ProxyABI } from './generated/Proxy' import { ABI as RandomABI } from './generated/Random' import { ABI as RegistryABI } from './generated/Registry' @@ -116,6 +117,7 @@ const initializeAbiMap = { MetaTransactionWalletProxy: findInitializeAbi(MetaTransactionWalletABI), MetaTransactionWalletDeployerProxy: findInitializeAbi(MetaTransactionWalletDeployerABI), MultiSigProxy: findInitializeAbi(MultiSigABI), + OdisPaymentsProxy: findInitializeAbi(OdisPaymentsABI), ProxyProxy: findInitializeAbi(ProxyABI), RandomProxy: findInitializeAbi(RandomABI), RegistryProxy: findInitializeAbi(RegistryABI), diff --git a/packages/sdk/contractkit/src/web3-contract-cache.ts b/packages/sdk/contractkit/src/web3-contract-cache.ts index fc9989fe661..0c08cc47ebe 100644 --- a/packages/sdk/contractkit/src/web3-contract-cache.ts +++ b/packages/sdk/contractkit/src/web3-contract-cache.ts @@ -25,6 +25,7 @@ import { newLockedGold } from './generated/LockedGold' import { newMetaTransactionWallet } from './generated/MetaTransactionWallet' import { newMetaTransactionWalletDeployer } from './generated/MetaTransactionWalletDeployer' import { newMultiSig } from './generated/MultiSig' +import { newOdisPayments } from './generated/OdisPayments' import { newProxy } from './generated/Proxy' import { newRandom } from './generated/Random' import { newRegistry } from './generated/Registry' @@ -60,6 +61,7 @@ export const ContractFactories = { [CeloContract.MetaTransactionWallet]: newMetaTransactionWallet, [CeloContract.MetaTransactionWalletDeployer]: newMetaTransactionWalletDeployer, [CeloContract.MultiSig]: newMultiSig, + [CeloContract.OdisPayments]: newOdisPayments, [CeloContract.Random]: newRandom, [CeloContract.Registry]: newRegistry, [CeloContract.Reserve]: newReserve, From a4a8700dcad2df9577281418f9c23c59eb2b7193 Mon Sep 17 00:00:00 2001 From: Martin Volpe Date: Mon, 26 Sep 2022 15:34:26 +0200 Subject: [PATCH 30/30] Leftover changes for CR8 merge (#9881) * Made certora disabling more explicit * Added report for CR7 and CI fixing * Lint * Removed leftover comments --- .circleci/config.yml | 1 + .../migrations/27_elect_validators.ts | 403 --------- packages/protocol/migrationsConfig.js | 2 - packages/protocol/package.json | 2 +- .../versionReports/release8-report.json | 114 +++ .../bash/generate-old-devchain-and-build.sh | 4 + .../scripts/bash/release-on-devchain.sh | 2 + packages/protocol/test/stability/reserve.ts | 839 ------------------ .../sdk/contractkit/src/contract-cache.ts | 4 - 9 files changed, 122 insertions(+), 1249 deletions(-) delete mode 100644 packages/protocol/migrations/27_elect_validators.ts create mode 100644 packages/protocol/releaseData/versionReports/release8-report.json delete mode 100644 packages/protocol/test/stability/reserve.ts diff --git a/.circleci/config.yml b/.circleci/config.yml index bcade150dea..ed6a66c2afe 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -811,6 +811,7 @@ jobs: command: | ./scripts/ci_check_if_test_should_run_v2.sh @celo/protocol # Disabling certora until the license is figured out + echo "Certora tests are disabled" circleci step halt exit 0 - run: diff --git a/packages/protocol/migrations/27_elect_validators.ts b/packages/protocol/migrations/27_elect_validators.ts deleted file mode 100644 index 3317182219f..00000000000 --- a/packages/protocol/migrations/27_elect_validators.ts +++ /dev/null @@ -1,403 +0,0 @@ -/* tslint:disable:no-console */ -import { NULL_ADDRESS } from '@celo/base/lib/address' -import { CeloTxObject } from '@celo/connect' -import { getBlsPoP, getBlsPublicKey } from '@celo/cryptographic-utils/lib/bls' -import { - getDeployedProxiedContract, - sendTransactionWithPrivateKey, -} from '@celo/protocol/lib/web3-utils' -import { config } from '@celo/protocol/migrationsConfig' -import { privateKeyToAddress, privateKeyToPublicKey } from '@celo/utils/lib/address' -import { toFixed } from '@celo/utils/lib/fixidity' -import { signMessage } from '@celo/utils/lib/signatureUtils' -import { BigNumber } from 'bignumber.js' -import { AccountsInstance, ElectionInstance, LockedGoldInstance, ValidatorsInstance } from 'types' -import Web3 from 'web3' - -const truffle = require('@celo/protocol/truffle-config.js') -const bip39 = require('bip39') -const hdkey = require('ethereumjs-wallet/hdkey') - -function ganachePrivateKey(num) { - const seed = bip39.mnemonicToSeedSync(truffle.networks.development.mnemonic) - const hdk = hdkey.fromMasterSeed(seed) - const addrNode = hdk.derivePath("m/44'/60'/0'/0/" + num) // m/44'/60'/0'/0/0 is derivation path for the first account. m/44'/60'/0'/0/1 is the derivation path for the second account and so on - return addrNode.getWallet().getPrivateKey().toString('hex') -} - -function serializeKeystore(keystore: any) { - return Buffer.from(JSON.stringify(keystore)).toString('base64') -} - -let isGanache = false - -// Will include Ganache private keys for accounts 7-9, used for group keys -let extraKeys = [] - -async function sendTransaction( - web3: Web3, - tx: CeloTxObject | null, - privateKey: string, - txArgs: any -) { - if (isGanache) { - const from = privateKeyToAddress(privateKey) - if (tx == null) { - await web3.eth.sendTransaction({ ...txArgs, from }) - } else { - await tx.send({ ...txArgs, from, gasLimit: '10000000' }) - } - } else { - await sendTransactionWithPrivateKey(web3, tx, privateKey, txArgs) - } -} - -async function lockGold( - accounts: AccountsInstance, - lockedGold: LockedGoldInstance, - value: BigNumber, - privateKey: string -) { - // @ts-ignore - const createAccountTx = accounts.contract.methods.createAccount() - await sendTransaction(web3, createAccountTx, privateKey, { - to: accounts.address, - }) - - // @ts-ignore - const lockTx = lockedGold.contract.methods.lock() - - await sendTransaction(web3, lockTx, privateKey, { - to: lockedGold.address, - value: value.toString(10), - }) -} - -function createAccountOrUseFromGanache() { - if (isGanache) { - const privateKey = extraKeys.pop() - return { address: privateKeyToAddress(privateKey), privateKey } - } else { - return web3.eth.accounts.create() - } -} - -async function registerValidatorGroup( - name: string, - accounts: AccountsInstance, - lockedGold: LockedGoldInstance, - validators: ValidatorsInstance, - privateKey: string, - lockedGoldValue: BigNumber -) { - // Validators can't also be validator groups, so we create a new account to register the - // validator group with, and set the name of the group account to the private key of this account - // encrypted with the private key of the first validator, so that the group private key - // can be recovered. - const account = createAccountOrUseFromGanache() - - // We do not use web3 provided by Truffle since the eth.accounts.encrypt behaves differently - // in the version we use elsewhere. - const encryptionWeb3 = new Web3('http://localhost:8545') - const encryptedPrivateKey = encryptionWeb3.eth.accounts.encrypt(account.privateKey, privateKey) - const encodedKey = serializeKeystore(encryptedPrivateKey) - - // Add a premium to cover tx fees - const v = lockedGoldValue.times(1.01).integerValue() - - console.info(` - send funds ${v} to group address ${account.address}`) - await sendTransaction(web3, null, privateKey, { - to: account.address, - value: v, - }) - - console.info(` - lock gold`) - await lockGold(accounts, lockedGold, lockedGoldValue, account.privateKey) - - console.info(` - setName`) - // @ts-ignore - const setNameTx = accounts.contract.methods.setName(`${name} ${encodedKey}`) - await sendTransaction(web3, setNameTx, account.privateKey, { - to: accounts.address, - }) - - console.info(` - registerValidatorGroup`) - // @ts-ignore - const tx = validators.contract.methods.registerValidatorGroup( - toFixed(config.validators.commission).toString() - ) - - await sendTransaction(web3, tx, account.privateKey, { - to: validators.address, - }) - - return account -} - -async function registerValidator( - accounts: AccountsInstance, - lockedGold: LockedGoldInstance, - validators: ValidatorsInstance, - validatorPrivateKey: string, - attestationKey: string, - groupAddress: string, - index: number, - networkName: string -) { - const valName = `CLabs Validator #${index} on ${networkName}` - - console.info(` - lockGold ${valName}`) - await lockGold( - accounts, - lockedGold, - config.validators.validatorLockedGoldRequirements.value, - validatorPrivateKey - ) - - console.info(` - setName ${valName}`) - - // @ts-ignore - const setNameTx = accounts.contract.methods.setName(valName) - await sendTransaction(web3, setNameTx, validatorPrivateKey, { - to: accounts.address, - }) - - console.info(` - registerValidator ${valName}`) - const publicKey = privateKeyToPublicKey(validatorPrivateKey) - const blsPublicKey = getBlsPublicKey(validatorPrivateKey) - const blsPoP = getBlsPoP(privateKeyToAddress(validatorPrivateKey), validatorPrivateKey) - - // @ts-ignore - const registerTx = validators.contract.methods.registerValidator(publicKey, blsPublicKey, blsPoP) - - await sendTransaction(web3, registerTx, validatorPrivateKey, { - to: validators.address, - }) - - console.info(` - affiliate ${valName}`) - - // @ts-ignore - const affiliateTx = validators.contract.methods.affiliate(groupAddress) - - await sendTransaction(web3, affiliateTx, validatorPrivateKey, { - to: validators.address, - }) - - console.info(` - setAccountDataEncryptionKey ${valName}`) - - // @ts-ignore - const registerDataEncryptionKeyTx = accounts.contract.methods.setAccountDataEncryptionKey( - privateKeyToPublicKey(validatorPrivateKey) - ) - - await sendTransaction(web3, registerDataEncryptionKeyTx, validatorPrivateKey, { - to: accounts.address, - }) - - if (!isGanache) { - // Authorize the attestation signer - const attestationKeyAddress = privateKeyToAddress(attestationKey) - console.info(` - authorizeAttestationSigner ${valName}->${attestationKeyAddress}`) - const message = web3.utils.soliditySha3({ - type: 'address', - value: privateKeyToAddress(validatorPrivateKey), - }) - const signature = signMessage(message, attestationKey, attestationKeyAddress) - - // @ts-ignore - const registerAttestationKeyTx = accounts.contract.methods.authorizeAttestationSigner( - attestationKeyAddress, - signature.v, - signature.r, - signature.s - ) - - await sendTransaction(web3, registerAttestationKeyTx, validatorPrivateKey, { - to: accounts.address, - }) - } - - console.info(` - done ${valName}`) - return -} - -module.exports = async (_deployer: any, networkName: string) => { - const accounts: AccountsInstance = await getDeployedProxiedContract( - 'Accounts', - artifacts - ) - - const validators: ValidatorsInstance = await getDeployedProxiedContract( - 'Validators', - artifacts - ) - - const lockedGold: LockedGoldInstance = await getDeployedProxiedContract( - 'LockedGold', - artifacts - ) - - const election: ElectionInstance = await getDeployedProxiedContract( - 'Election', - artifacts - ) - - if (networkName === 'development') { - isGanache = true - const addr0 = privateKeyToAddress('0x' + ganachePrivateKey(0)) - for (let i = 10; i < 36; i++) { - const key = '0x' + ganachePrivateKey(i) - const addr = privateKeyToAddress(key) - // @ts-ignore - await web3.eth.personal.importRawKey(key, 'passphrase') - await web3.eth.personal.unlockAccount(addr, 'passphrase', 1000000) - await web3.eth.sendTransaction({ from: addr0, to: addr, value: new BigNumber(11000e18) }) - } - config.validators.validatorKeys = [...Array(30)].map((_, i) => ganachePrivateKey(i)) - extraKeys = [...Array(6)].map((_, i) => ganachePrivateKey(i + 30)) - config.validators.attestationKeys = config.validators.validatorKeys - } - - const valKeys: string[] = config.validators.validatorKeys - const attestationKeys: string[] = config.validators.attestationKeys - - if (valKeys.length === 0) { - console.info(' No validators to register') - return - } - - if (config.validators.votesRatioOfLastVsFirstGroup < 1) { - throw new Error(`votesRatioOfLastVsFirstGroup needs to be >= 1`) - } - - // Assumptions about where funds are located: - // * Validator 0 holds funds for all groups' stakes - // * Validator 1-n holds funds needed for their own stake - const validator0Key = valKeys[0] - - if (valKeys.length < parseInt(config.election.minElectableValidators, 10)) { - console.info( - ` Warning: Have ${valKeys.length} Validator keys but require a minimum of ${config.election.minElectableValidators} Validators in order for a new validator set to be elected.` - ) - } - - // Split the validator keys into groups that will fit within the max group size. - const valKeyGroups: string[][] = [] - const maxGroupSize: number = Number(config.validators.maxGroupSize) - for (let i = 0; i < valKeys.length; i += maxGroupSize) { - valKeyGroups.push(valKeys.slice(i, Math.min(i + maxGroupSize, valKeys.length))) - } - - // Calculate per validator locked gold for first group... - const lockedGoldPerValAtFirstGroup = new BigNumber( - config.validators.groupLockedGoldRequirements.value - ) - // ...and the delta for each subsequent group - const lockedGoldPerValEachGroup = new BigNumber( - config.validators.votesRatioOfLastVsFirstGroup - 1 - ) - .times(lockedGoldPerValAtFirstGroup) - .div(Math.max(valKeyGroups.length - 1, 1)) - .integerValue() - - const groups = valKeyGroups.map((keys, i) => { - const lockedGoldAmount = lockedGoldPerValAtFirstGroup - .plus(lockedGoldPerValEachGroup.times(i)) - .times(keys.length) - return { - valKeys: keys, - name: valKeyGroups.length - ? config.validators.groupName + `(${i + 1})` - : config.validators.groupName, - lockedGold: lockedGoldAmount, - voteAmount: - i === 0 || i === valKeyGroups.length - 1 - ? lockedGoldAmount - : new BigNumber(config.validators.groupLockedGoldRequirements.value), - account: null, - } - }) - - for (const [idx, group] of groups.entries()) { - console.info( - ` Registering validator group: ${group.name} with: ${group.lockedGold} CG locked...` - ) - group.account = await registerValidatorGroup( - group.name, - accounts, - lockedGold, - validators, - validator0Key, - group.lockedGold - ) - - console.info(` * Registering ${group.valKeys.length} validators ...`) - await Promise.all( - group.valKeys.map((key, i) => { - const index = idx * config.validators.maxGroupSize + i - return registerValidator( - accounts, - lockedGold, - validators, - key, - attestationKeys[index], - group.account.address, - index, - networkName - ) - }) - ) - - console.info(` * Adding Validators to ${group.name} ...`) - for (const [i, key] of group.valKeys.entries()) { - const address = privateKeyToAddress(key) - console.info(` - Adding ${address} ...`) - if (i === 0) { - const groupsWithVotes = groups.slice(0, idx) - groupsWithVotes.sort((a, b) => a.voteAmount.comparedTo(b.voteAmount)) - - // @ts-ignore - const addTx = validators.contract.methods.addFirstMember( - address, - NULL_ADDRESS, - groupsWithVotes.length ? groupsWithVotes[0].account.address : NULL_ADDRESS - ) - await sendTransaction(web3, addTx, group.account.privateKey, { - to: validators.address, - }) - } else { - // @ts-ignore - const addTx = validators.contract.methods.addMember(address) - await sendTransaction(web3, addTx, group.account.privateKey, { - to: validators.address, - }) - } - } - - // Determine the lesser and greater group addresses after voting. - const sortedGroups = groups.slice(0, idx + 1) - sortedGroups.sort((a, b) => a.voteAmount.comparedTo(b.voteAmount)) - const groupSortedIndex = sortedGroups.indexOf(group) - const lesser = - groupSortedIndex > 0 ? sortedGroups[groupSortedIndex - 1].account.address : NULL_ADDRESS - const greater = - groupSortedIndex < idx ? sortedGroups[groupSortedIndex + 1].account.address : NULL_ADDRESS - - // Note: Only the groups vote for themselves here. The validators do not vote. - console.info(' * Group voting for itself ...') - - // Make first and last group high votes so we can maintain presence. - const voteAmount = '0x' + group.voteAmount.toString(16) - - // @ts-ignore - const voteTx = election.contract.methods.vote( - group.account.address, - voteAmount, - lesser, - greater - ) - await sendTransaction(web3, voteTx, group.account.privateKey, { - to: election.address, - }) - } -} diff --git a/packages/protocol/migrationsConfig.js b/packages/protocol/migrationsConfig.js index f9bb96e3f8f..99537c1d90e 100644 --- a/packages/protocol/migrationsConfig.js +++ b/packages/protocol/migrationsConfig.js @@ -26,8 +26,6 @@ const DAY = 24 * HOUR const WEEK = 7 * DAY const YEAR = 365 * DAY -// TODO ASv2 - const DefaultConfig = { attestations: { attestationExpiryBlocks: HOUR / 5, // ~1 hour, diff --git a/packages/protocol/package.json b/packages/protocol/package.json index d200f0465b6..5b9d464403b 100644 --- a/packages/protocol/package.json +++ b/packages/protocol/package.json @@ -93,7 +93,7 @@ }, "devDependencies": { "@celo/flake-tracker": "0.0.1-dev", - "@celo/phone-utils": "2.0.1-dev", + "@celo/phone-utils": "2.1.1-dev", "@celo/typechain-target-web3-v1-celo": "0.2.0", "@celo/typescript": "0.0.1", "@types/bn.js": "^4.11.0", diff --git a/packages/protocol/releaseData/versionReports/release8-report.json b/packages/protocol/releaseData/versionReports/release8-report.json new file mode 100644 index 00000000000..68321a45b25 --- /dev/null +++ b/packages/protocol/releaseData/versionReports/release8-report.json @@ -0,0 +1,114 @@ +{ + "oldArtifactsFolder": "/home/circleci/app/packages/protocol/build/core-contracts.v7/contracts", + "newArtifactsFolder": "/home/circleci/app/packages/protocol/build/core-contracts.v8/contracts", + "exclude": "/.*Test|Mock.*|I[A-Z].*|.*Proxy|MultiSig.*|ReleaseGold|SlasherUtil|UsingPrecompiles|^UsingRegistry/", + "report": { + "contracts": { + "Escrow": { + "changes": { + "storage": [], + "major": [ + { + "contract": "Escrow", + "signature": "setRegistry(address)", + "type": "MethodRemoved" + }, + { + "contract": "Escrow", + "signature": "initialize(address)", + "type": "MethodRemoved" + } + ], + "minor": [ + { + "contract": "Escrow", + "signature": "transferWithTrustedIssuers(bytes32,address,uint256,uint256,address,uint256,address[])", + "type": "MethodAdded" + }, + { + "contract": "Escrow", + "signature": "getTrustedIssuersPerPayment(address)", + "type": "MethodAdded" + }, + { + "contract": "Escrow", + "signature": "getDefaultTrustedIssuers()", + "type": "MethodAdded" + }, + { + "contract": "Escrow", + "signature": "MAX_TRUSTED_ISSUERS_PER_PAYMENT()", + "type": "MethodAdded" + }, + { + "contract": "Escrow", + "signature": "addDefaultTrustedIssuer(address)", + "type": "MethodAdded" + }, + { + "contract": "Escrow", + "signature": "removeDefaultTrustedIssuer(address,uint256)", + "type": "MethodAdded" + }, + { + "contract": "Escrow", + "signature": "initialize()", + "type": "MethodAdded" + } + ], + "patch": [ + { + "contract": "Escrow", + "type": "DeployedBytecode" + } + ] + }, + "versionDelta": { + "storage": "=", + "major": "+1", + "minor": "0", + "patch": "0" + } + }, + "FederatedAttestations": { + "changes": { + "storage": [], + "major": [ + { + "contract": "FederatedAttestations", + "type": "NewContract" + } + ], + "minor": [], + "patch": [] + }, + "versionDelta": { + "storage": "=", + "major": "+1", + "minor": "0", + "patch": "0" + } + }, + "OdisPayments": { + "changes": { + "storage": [], + "major": [ + { + "contract": "OdisPayments", + "type": "NewContract" + } + ], + "minor": [], + "patch": [] + }, + "versionDelta": { + "storage": "=", + "major": "+1", + "minor": "0", + "patch": "0" + } + } + }, + "libraries": {} + } +} diff --git a/packages/protocol/scripts/bash/generate-old-devchain-and-build.sh b/packages/protocol/scripts/bash/generate-old-devchain-and-build.sh index 23e6c4ddad8..aa51f626af5 100755 --- a/packages/protocol/scripts/bash/generate-old-devchain-and-build.sh +++ b/packages/protocol/scripts/bash/generate-old-devchain-and-build.sh @@ -35,6 +35,10 @@ git checkout $BRANCH 2>>$LOG_FILE >> $LOG_FILE echo "- Build contract artifacts" rm -rf build/contracts +rm -rf ../sdk/cryptographic-utils/lib +cd ../sdk/cryptographic-utils +yarn build +cd ../../protocol yarn install >> $LOG_FILE yarn build >> $LOG_FILE diff --git a/packages/protocol/scripts/bash/release-on-devchain.sh b/packages/protocol/scripts/bash/release-on-devchain.sh index 02e6c13a07d..bdbd7508215 100755 --- a/packages/protocol/scripts/bash/release-on-devchain.sh +++ b/packages/protocol/scripts/bash/release-on-devchain.sh @@ -53,6 +53,8 @@ git checkout $BRANCH -- migrationsConfig.js OLD_BRANCH=$BUILD_DIR source scripts/bash/contract-exclusion-regex.sh +yarn ts-node scripts/check-backward.ts sem_check --old_contracts $BUILD_DIR/contracts --new_contracts build/contracts --exclude $CONTRACT_EXCLUSION_REGEX --output_file report.json + git checkout - -- migrationsConfig.js diff --git a/packages/protocol/test/stability/reserve.ts b/packages/protocol/test/stability/reserve.ts deleted file mode 100644 index d64c6f38586..00000000000 --- a/packages/protocol/test/stability/reserve.ts +++ /dev/null @@ -1,839 +0,0 @@ -import { CeloContractName } from '@celo/protocol/lib/registry-utils' -import { - assertEqualBN, - assertLogMatches2, - assertRevert, - assertSameAddress, - timeTravel, -} from '@celo/protocol/lib/test-utils' -import { fromFixed, toFixed } from '@celo/utils/lib/fixidity' -import BigNumber from 'bignumber.js' -import { - MockSortedOraclesInstance, - MockStableTokenInstance, - RegistryInstance, - ReserveInstance, -} from 'types' - -// tslint:disable-next-line: ordered-imports -import BN = require('bn.js') - -const Registry: Truffle.Contract = artifacts.require('Registry') -const Reserve: Truffle.Contract = artifacts.require('Reserve') -const MockStableToken: Truffle.Contract = artifacts.require( - 'MockStableToken' -) -const MockSortedOracles: Truffle.Contract = artifacts.require( - 'MockSortedOracles' -) - -// @ts-ignore -Reserve.numberFormat = 'BigNumber' - -contract('Reserve', (accounts: string[]) => { - let reserve: ReserveInstance - let registry: RegistryInstance - let mockSortedOracles: MockSortedOraclesInstance - const anAddress: string = '0x00000000000000000000000000000000deadbeef' - const nonOwner: string = accounts[1] - const spender: string = accounts[2] - const exchangeAddress: string = accounts[3] - const exchangeSpenderAddress: string = accounts[4] - const aTobinTaxStalenessThreshold: number = 600 - const aTobinTax = toFixed(0.005) - const aTobinTaxReserveRatio = toFixed(2) - const aDailySpendingRatio: string = '1000000000000000000000000' - const sortedOraclesDenominator = new BigNumber('1000000000000000000000000') - const initialAssetAllocationSymbols = [web3.utils.padRight(web3.utils.utf8ToHex('cGLD'), 64)] - const initialAssetAllocationWeights = [toFixed(1)] - beforeEach(async () => { - reserve = await Reserve.new(true) - registry = await Registry.new(true) - mockSortedOracles = await MockSortedOracles.new() - await registry.setAddressFor(CeloContractName.SortedOracles, mockSortedOracles.address) - await registry.setAddressFor(CeloContractName.Exchange, exchangeAddress) - - await reserve.initialize( - registry.address, - aTobinTaxStalenessThreshold, - aDailySpendingRatio, - 0, - 0, - initialAssetAllocationSymbols, - initialAssetAllocationWeights, - aTobinTax, - aTobinTaxReserveRatio - ) - }) - - describe('#initialize()', () => { - it('should have set the owner', async () => { - const owner: string = await reserve.owner() - assert.equal(owner, accounts[0]) - }) - - it('should have set the registry address', async () => { - const registryAddress: string = await reserve.registry() - assertSameAddress(registryAddress, registry.address) - }) - - it('should have set the tobin tax staleness threshold', async () => { - const tobinTaxStalenessThreshold = await reserve.tobinTaxStalenessThreshold() - assertEqualBN(tobinTaxStalenessThreshold, aTobinTaxStalenessThreshold) - }) - - it('should not be callable again', async () => { - await assertRevert( - reserve.initialize( - registry.address, - aTobinTaxStalenessThreshold, - aDailySpendingRatio, - 0, - 0, - initialAssetAllocationSymbols, - initialAssetAllocationWeights, - aTobinTax, - aTobinTaxReserveRatio - ) - ) - }) - }) - - describe('#setTobinTax()', async () => { - const value = 123 - it('should allow owner to set the tax', async () => { - await reserve.setTobinTax(value) - assert.equal(value, (await reserve.tobinTax()).toNumber()) - }) - it('should emit corresponding event', async () => { - const response = await reserve.setTobinTax(value) - const events = response.logs - assert.equal(events.length, 1) - assert.equal(events[0].event, 'TobinTaxSet') - assert.equal(events[0].args.value.toNumber(), value) - }) - it('should not allow other users to set the tobin tax', async () => { - await assertRevert(reserve.setTobinTax(value, { from: nonOwner })) - }) - it('should not be allowed to set it larger than 100%', async () => { - await assertRevert(reserve.setTobinTax(toFixed(1).plus(1))) - }) - }) - - describe('#setTobinTaxReserveRatio()', async () => { - const value = 123 - it('should allow owner to set the reserve ratio', async () => { - await reserve.setTobinTaxReserveRatio(value) - assert.equal(value, (await reserve.tobinTaxReserveRatio()).toNumber()) - }) - it('should emit corresponding event', async () => { - const response = await reserve.setTobinTaxReserveRatio(value) - const events = response.logs - assert.equal(events.length, 1) - assert.equal(events[0].event, 'TobinTaxReserveRatioSet') - assert.equal(events[0].args.value.toNumber(), value) - }) - it('should not allow other users to set the ratio', async () => { - await assertRevert(reserve.setTobinTaxReserveRatio(value, { from: nonOwner })) - }) - }) - - describe('#setDailySpendingRatio()', async () => { - it('should allow owner to set the ratio', async () => { - await reserve.setDailySpendingRatio(123) - assert.equal(123, (await reserve.getDailySpendingRatio()).toNumber()) - }) - it('should emit corresponding event', async () => { - const response = await reserve.setDailySpendingRatio(123) - const events = response.logs - assert.equal(events.length, 1) - assert.equal(events[0].event, 'DailySpendingRatioSet') - assert.equal(events[0].args.ratio.toNumber(), 123) - }) - it('should not allow other users to set the ratio', async () => { - await assertRevert(reserve.setDailySpendingRatio(123, { from: nonOwner })) - }) - it('should not be allowed to set it larger than 100%', async () => { - await assertRevert(reserve.setDailySpendingRatio(toFixed(1.3))) - }) - }) - - describe('#setRegistry()', () => { - it('should allow owner to set registry', async () => { - await reserve.setRegistry(anAddress) - assertSameAddress(await reserve.registry(), anAddress) - }) - - it('should not allow other users to set registry', async () => { - await assertRevert(reserve.setRegistry(anAddress, { from: nonOwner })) - }) - }) - - describe('#addToken()', () => { - beforeEach(async () => { - await mockSortedOracles.setMedianRate(anAddress, sortedOraclesDenominator) - }) - - it('should allow owner to add a token', async () => { - await reserve.addToken(anAddress) - assert.isTrue(await reserve.isToken(anAddress)) - }) - - it('should not allow other users to add a token', async () => { - await assertRevert(reserve.addToken(anAddress, { from: nonOwner })) - }) - - it('should emit a TokenAdded event', async () => { - const response = await reserve.addToken(anAddress) - const events = response.logs - assert.equal(events.length, 1) - assert.equal(events[0].event, 'TokenAdded') - assert.equal(events[0].args.token.toLowerCase(), anAddress.toLowerCase()) - }) - - describe('when the token has already been added', () => { - beforeEach(async () => { - await reserve.addToken(anAddress) - }) - - it('should not allow owner to add an existing token', async () => { - await assertRevert(reserve.addToken(anAddress)) - }) - }) - }) - - describe('#removeToken()', () => { - let index: number = 0 - - it('should not allow owner to remove an unadded token', async () => { - await assertRevert(reserve.removeToken(anAddress, index)) - }) - - describe('when the token has already been added', () => { - beforeEach(async () => { - await mockSortedOracles.setMedianRate(anAddress, sortedOraclesDenominator) - await reserve.addToken(anAddress) - const tokenList = await reserve.getTokens() - index = -1 - for (let i = 0; i < tokenList.length; i++) { - if (tokenList[i].toLowerCase() === anAddress.toLowerCase()) { - index = i - } - } - }) - - it('should allow owner to remove a token', async () => { - await reserve.removeToken(anAddress, index) - assert.isFalse(await reserve.isToken(anAddress)) - }) - - it('should not allow other users to remove a token', async () => { - await assertRevert(reserve.removeToken(anAddress, index, { from: nonOwner })) - }) - - it('should emit a TokenRemoved event', async () => { - const response = await reserve.removeToken(anAddress, index) - const events = response.logs - assert.equal(events.length, 1) - assert.equal(events[0].event, 'TokenRemoved') - assertSameAddress(events[0].args.token, anAddress) - assert.equal(events[0].args.index, index) - }) - }) - }) - - describe('#transferGold()', () => { - const aValue = 10000 - let otherReserveAddress: string = '' - beforeEach(async () => { - otherReserveAddress = web3.utils.randomHex(20) - await web3.eth.sendTransaction({ to: reserve.address, value: aValue, from: accounts[0] }) - await reserve.addSpender(spender) - await reserve.addOtherReserveAddress(otherReserveAddress) - }) - - it('should allow a spender to call transferGold', async () => { - await reserve.transferGold(otherReserveAddress, aValue, { from: spender }) - }) - - it('should not allow a spender to transfer more than daily ratio', async () => { - await reserve.setDailySpendingRatio(toFixed(0.2)) - await assertRevert(reserve.transferGold(otherReserveAddress, aValue / 2, { from: spender })) - }) - - it('daily spending accumulates', async () => { - await reserve.setDailySpendingRatio(toFixed(0.15)) - await reserve.transferGold(otherReserveAddress, aValue * 0.1, { from: spender }) - await assertRevert(reserve.transferGold(otherReserveAddress, aValue * 0.1, { from: spender })) - }) - - it('daily spending limit should be reset after 24 hours', async () => { - await reserve.setDailySpendingRatio(toFixed(0.15)) - await reserve.transferGold(otherReserveAddress, aValue * 0.1, { from: spender }) - await timeTravel(3600 * 24, web3) - await reserve.transferGold(otherReserveAddress, aValue * 0.1, { from: spender }) - }) - - it('should not allow a removed spender to call transferGold', async () => { - await reserve.removeSpender(spender) - await assertRevert(reserve.transferGold(otherReserveAddress, aValue, { from: spender })) - }) - - it('should not allow other addresses to call transferGold', async () => { - await assertRevert(reserve.transferGold(otherReserveAddress, aValue, { from: nonOwner })) - }) - - it('can only transfer gold to other reverse addresses', async () => { - await assertRevert(reserve.transferGold(nonOwner, aValue, { from: spender })) - }) - }) - - describe('#addExchangeSpender(exchangeAddress)', () => { - it('only allows owner', async () => { - await assertRevert(reserve.addExchangeSpender(exchangeSpenderAddress, { from: nonOwner })) - }) - - it('should emit addExchangeSpender event on add', async () => { - const resp = await reserve.addExchangeSpender(exchangeSpenderAddress) - const log = resp.logs[0] - assert.equal(resp.logs.length, 1) - assertLogMatches2(log, { - event: 'ExchangeSpenderAdded', - args: { - exchangeSpender: exchangeSpenderAddress, - }, - }) - }) - - it('does not allow an empty address', async () => { - await assertRevert(reserve.addExchangeSpender('0x0000000000000000000000000000000000000000')) - }) - - it('has the right list of exchange spenders after addition', async () => { - const spendersBeforeAdditions = await reserve.getExchangeSpenders() - assert.deepEqual(spendersBeforeAdditions, []) - await reserve.addExchangeSpender(exchangeAddress) - await reserve.addExchangeSpender(accounts[1]) - const spenders = await reserve.getExchangeSpenders() - assert.deepEqual(spenders, [exchangeAddress, accounts[1]]) - }) - }) - - describe('#removeExchangeSpender(exchangeAddress)', () => { - beforeEach(async () => { - await reserve.addExchangeSpender(exchangeSpenderAddress) - }) - - it('only allows owner', async () => { - await assertRevert( - reserve.removeExchangeSpender(exchangeSpenderAddress, 0, { from: nonOwner }) - ) - }) - - it('should emit removeExchangeSpender event on remove', async () => { - const resp = await reserve.removeExchangeSpender(exchangeSpenderAddress, 0) - const log = resp.logs[0] - assert.equal(resp.logs.length, 1) - assertLogMatches2(log, { - event: 'ExchangeSpenderRemoved', - args: { - exchangeSpender: exchangeSpenderAddress, - }, - }) - }) - - it('has the right list of exchange after removing one', async () => { - await reserve.removeExchangeSpender(exchangeSpenderAddress, 0) - const spenders = await reserve.getExchangeSpenders() - assert.deepEqual(spenders, []) - }) - - it("can't be removed twice", async () => { - await reserve.removeExchangeSpender(exchangeSpenderAddress, 0) - await assertRevert(reserve.removeExchangeSpender(exchangeSpenderAddress, 0)) - }) - - it("can't delete an index out of range", async () => { - await assertRevert(reserve.removeExchangeSpender(exchangeSpenderAddress, 1)) - }) - - it('removes from a big array', async () => { - await reserve.addExchangeSpender(accounts[1]) - await reserve.removeExchangeSpender(exchangeSpenderAddress, 0) - const spenders = await reserve.getExchangeSpenders() - assert.deepEqual(spenders, [accounts[1]]) - }) - - it("doesn't remove an address with the wrong index", async () => { - await reserve.addExchangeSpender(accounts[1]) - await assertRevert(reserve.removeExchangeSpender(exchangeSpenderAddress, 1)) - }) - }) - - describe('#addSpender(spender)', () => { - it('emits on add', async () => { - const addSpenderTx = await reserve.addSpender(spender) - - const addSpenderTxLogs = addSpenderTx.logs.filter((x) => x.event === 'SpenderAdded') - assert(addSpenderTxLogs.length === 1, 'Did not receive event') - }) - - it('only allows owner', async () => { - await assertRevert(reserve.addSpender(nonOwner, { from: nonOwner })) - }) - - it('does not allow an empty address', async () => { - await assertRevert(reserve.addSpender('0x0000000000000000000000000000000000000000')) - }) - }) - - describe('#removeSpender(spender)', () => { - it('emits on remove', async () => { - const addSpenderTx = await reserve.removeSpender(spender) - - const addExchangeSpenderTxLogs = addSpenderTx.logs.filter((x) => x.event === 'SpenderRemoved') - assert(addExchangeSpenderTxLogs.length === 1, 'Did not receive event') - }) - - it('only allows owner', async () => { - await assertRevert(reserve.removeSpender(nonOwner, { from: nonOwner })) - }) - }) - - describe('#transferExchangeGold()', () => { - const aValue = 10000 - let otherReserveAddress: string = '' - beforeEach(async () => { - otherReserveAddress = web3.utils.randomHex(20) - await web3.eth.sendTransaction({ to: reserve.address, value: aValue, from: accounts[0] }) - await reserve.addSpender(spender) - await reserve.addOtherReserveAddress(otherReserveAddress) - }) - - it('should allow an exchange to call transferExchangeGold', async () => { - await reserve.transferExchangeGold(nonOwner, aValue, { from: exchangeAddress }) - }) - - it('should not allow spenders to call transferExchangeGold', async () => { - await assertRevert(reserve.transferExchangeGold(nonOwner, aValue, { from: spender })) - }) - - it('should not allow other addresses to call transferExchangeGold', async () => { - await assertRevert(reserve.transferExchangeGold(nonOwner, aValue, { from: nonOwner })) - }) - - it('should not allow removed exchange spender addresses to call transferExchangeGold', async () => { - await reserve.addExchangeSpender(exchangeSpenderAddress) - await reserve.removeExchangeSpender(exchangeSpenderAddress, 0) - await assertRevert( - reserve.transferExchangeGold(nonOwner, aValue, { from: exchangeSpenderAddress }) - ) - }) - - it('should not allow freezing more gold than is available', async () => { - await assertRevert(reserve.setFrozenGold(aValue + 1, 1)) - }) - - it('should not allow a spender to transfer more gold than is unfrozen', async () => { - await reserve.setFrozenGold(1, 1) - await assertRevert(reserve.transferGold(otherReserveAddress, aValue, { from: spender })) - }) - - for (let i = 0; i < 3; i++) { - it('unfrozen gold should increase every 24 hours', async () => { - const expectedFrozenGold = 3 - i - await reserve.setFrozenGold(3, 3) - await timeTravel(3600 * 24 * i, web3) - await assertRevert( - reserve.transferGold(otherReserveAddress, aValue - expectedFrozenGold + 1, { - from: spender, - }) - ) - await reserve.transferGold(otherReserveAddress, aValue - expectedFrozenGold, { - from: spender, - }) - }) - } - }) - - describe('#getOrComputeTobinTax()', () => { - const newAssetAllocationSymbols = [ - web3.utils.padRight(web3.utils.utf8ToHex('cGLD'), 64), - web3.utils.padRight(web3.utils.utf8ToHex('empty'), 64), - ] - const newAssetAllocationWeights = [ - new BigNumber(10).pow(24).dividedBy(new BigNumber(2)).integerValue(), - new BigNumber(10).pow(24).dividedBy(new BigNumber(2)).integerValue(), - ] - let mockStableToken: MockStableTokenInstance - - const expectedNoTobinTax: [BN, BN] = [new BN(0), new BN(10).pow(new BN(24))] - const expectedTobinTax: [BN, BN] = [ - new BN(5).mul(new BN(10).pow(new BN(21))), - new BN(10).pow(new BN(24)), - ] - - beforeEach(async () => { - mockStableToken = await MockStableToken.new() - await registry.setAddressFor(CeloContractName.SortedOracles, mockSortedOracles.address) - await mockSortedOracles.setMedianRate( - mockStableToken.address, - sortedOraclesDenominator.times(10) - ) - await reserve.setAssetAllocations(newAssetAllocationSymbols, newAssetAllocationWeights) - await reserve.addToken(mockStableToken.address) - const reserveGoldBalance = new BigNumber(10).pow(19) - await web3.eth.sendTransaction({ - from: accounts[0], - to: reserve.address, - value: reserveGoldBalance, - }) - }) - - async function getOrComputeTobinTax(): Promise<[BN, BN]> { - // @ts-ignore TODO(mcortesi): bad typings - const tobinTax = await reserve.getOrComputeTobinTax.call() - const actual = Object.keys(tobinTax).map((key) => web3.utils.toBN(tobinTax[key])) - return actual as [BN, BN] - } - - describe('when there is one stable token', () => { - it('should set tobin tax to 0% when reserve gold balance > gold value of floating stable tokens', async () => { - const stableTokenSupply = new BN(10).pow(new BN(19)).toString() - await mockStableToken.setTotalSupply(stableTokenSupply) - const actual = await getOrComputeTobinTax() - actual.forEach((v, i) => assert(expectedNoTobinTax[i].eq(v))) - }) - - it('should set tobin tax to 0% when reserve gold balance = gold value of floating stable tokens', async () => { - const stableTokenSupply = new BN(10).pow(new BN(20)).toString() - await mockStableToken.setTotalSupply(stableTokenSupply) - const actual = await getOrComputeTobinTax() - actual.forEach((v, i) => assert(expectedNoTobinTax[i].eq(v))) - }) - - it('should set tobin tax to 0.5% when reserve gold balance < gold value of floating stable tokens', async () => { - const stableTokenSupply = new BN(10).pow(new BN(21)).toString() - await mockStableToken.setTotalSupply(stableTokenSupply) - const actual = await getOrComputeTobinTax() - actual.forEach((v, i) => assert(expectedTobinTax[i].eq(v))) - }) - }) - - describe('when there are two stable tokens', () => { - let anotherMockStableToken: MockStableTokenInstance - beforeEach(async () => { - anotherMockStableToken = await MockStableToken.new() - await mockSortedOracles.setMedianRate( - anotherMockStableToken.address, - sortedOraclesDenominator.times(10) - ) - await reserve.addToken(anotherMockStableToken.address) - }) - - it('should set tobin tax to 0% when reserve gold balance > gold value of floating stable tokens', async () => { - const stableTokenSupply = new BN(10).pow(new BN(19)).toString() - await mockStableToken.setTotalSupply(stableTokenSupply) - await anotherMockStableToken.setTotalSupply(stableTokenSupply) - const actual = await getOrComputeTobinTax() - actual.forEach((v, i) => assert(expectedNoTobinTax[i].eq(v))) - }) - - it('should set tobin tax to 0.5% when reserve gold balance < gold value of floating stable tokens', async () => { - const stableTokenSupply = new BN(10).pow(new BN(21)).toString() - await mockStableToken.setTotalSupply(stableTokenSupply) - await anotherMockStableToken.setTotalSupply(stableTokenSupply) - const actual = await getOrComputeTobinTax() - actual.forEach((v, i) => assert(expectedTobinTax[i].eq(v))) - }) - - it('should set tobin tax to 0.5% when reserve gold balance < gold value of floating stable tokens', async () => { - const stableTokenSupply = new BN(10).pow(new BN(20)).mul(new BN(6)).toString() - await mockStableToken.setTotalSupply(stableTokenSupply) - await anotherMockStableToken.setTotalSupply(stableTokenSupply) - const actual = await getOrComputeTobinTax() - actual.forEach((v, i) => assert(expectedTobinTax[i].eq(v))) - }) - }) - - describe('when getOrComputeTobinTax() is called twice', () => { - const tobinTaxStalenessThreshold = 3600 - let tobinTax1 - beforeEach(async () => { - await reserve.setTobinTaxStalenessThreshold(tobinTaxStalenessThreshold) - const stableTokenSupply1 = new BN(10).pow(new BN(21)).toString() - await mockStableToken.setTotalSupply(stableTokenSupply1) - await reserve.getOrComputeTobinTax() - tobinTax1 = await getOrComputeTobinTax() - }) - - it('should get cached tobin tax value when called within staleness threshold', async () => { - const stableTokenSupply2 = new BN(10).pow(new BN(19)).toString() - await mockStableToken.setTotalSupply(stableTokenSupply2) - await reserve.getOrComputeTobinTax() - const tobinTax2 = await getOrComputeTobinTax() - assert.deepEqual(tobinTax2, tobinTax1) - }) - - it('should get updated tobin tax value when called after staleness threshold is passed', async () => { - await timeTravel(tobinTaxStalenessThreshold + 1, web3) - const stableTokenSupply2 = new BN(10).pow(new BN(19)).toString() - await mockStableToken.setTotalSupply(stableTokenSupply2) - await reserve.getOrComputeTobinTax() - const tobinTax2 = await getOrComputeTobinTax() - assert.notDeepEqual(tobinTax2, tobinTax1) - }) - }) - }) - - describe('#setTobinTaxStalenessThreshold', () => { - const newTobinTaxStalenessThreshold = 1 - it('should allow owner to set tobin tax staleness threshold', async () => { - await reserve.setTobinTaxStalenessThreshold(newTobinTaxStalenessThreshold) - assert.equal( - (await reserve.tobinTaxStalenessThreshold()).toNumber(), - newTobinTaxStalenessThreshold - ) - }) - - it('should not allow other users to set tobin tax staleness threshold', async () => { - await assertRevert( - reserve.setTobinTaxStalenessThreshold(newTobinTaxStalenessThreshold, { from: nonOwner }) - ) - }) - - it('should emit a TobinTaxStalenessThresholdSet event', async () => { - const response = await reserve.setTobinTaxStalenessThreshold(newTobinTaxStalenessThreshold) - const events = response.logs - assert.equal(events.length, 1) - assert.equal(events[0].event, 'TobinTaxStalenessThresholdSet') - assert.equal(events[0].args.value, newTobinTaxStalenessThreshold) - }) - }) - - describe('#addOtherReserveAddress()', () => { - it('should allow owner to add another reserve address', async () => { - await reserve.addOtherReserveAddress(anAddress) - assert.isTrue(await reserve.isOtherReserveAddress(anAddress)) - }) - - it('should not allow other users to add another reserve address', async () => { - await assertRevert(reserve.addOtherReserveAddress(anAddress, { from: nonOwner })) - }) - - it('should emit a OtherReserveAddressAdded event', async () => { - const response = await reserve.addOtherReserveAddress(anAddress) - const events = response.logs - assert.equal(events.length, 1) - assert.equal(events[0].event, 'OtherReserveAddressAdded') - assert.equal(events[0].args.otherReserveAddress.toLowerCase(), anAddress.toLowerCase()) - }) - - describe('when another reserve address has already been added', async () => { - beforeEach(async () => { - await reserve.addOtherReserveAddress(anAddress) - }) - - it('should not allow owner to add an existing reserve address', async () => { - await assertRevert(reserve.addOtherReserveAddress(anAddress)) - }) - }) - - it('should include the other reserve addresses in the reserve balance', async () => { - await reserve.addOtherReserveAddress(anAddress) - const otherReserveAddresses = await reserve.getOtherReserveAddresses() - assert.equal(otherReserveAddresses.length, 1) - assert.equal(otherReserveAddresses[0].toLowerCase(), anAddress.toLowerCase()) - - const reserveGoldBalance = new BigNumber(10).pow(18).times(6) - const otherReserveGoldBalance = new BigNumber(10).pow(18).times(4) - await web3.eth.sendTransaction({ - from: accounts[0], - to: reserve.address, - value: reserveGoldBalance, - }) - await web3.eth.sendTransaction({ - from: accounts[0], - to: anAddress, - value: otherReserveGoldBalance, - }) - - const reserveBalance = await reserve.getReserveGoldBalance() - assertEqualBN(reserveBalance, reserveGoldBalance.plus(otherReserveGoldBalance)) - }) - }) - - describe('#removeOtherReserveAddress()', () => { - const index: number = 0 - - it('should not allow owner to remove an unadded reserve address', async () => { - await assertRevert(reserve.removeOtherReserveAddress(anAddress, index)) - }) - - describe('when another reserve address has already been added', async () => { - beforeEach(async () => { - await reserve.addOtherReserveAddress(anAddress) - const otherReserveAddresses = await reserve.getOtherReserveAddresses() - assert.equal(otherReserveAddresses.length, 1) - assert.equal(otherReserveAddresses[0].toLowerCase(), anAddress.toLowerCase()) - }) - - it('should allow owner to remove another reserve address', async () => { - await reserve.removeOtherReserveAddress(anAddress, index) - assert.isFalse(await reserve.isOtherReserveAddress(anAddress)) - const otherReserveAddresses = await reserve.getOtherReserveAddresses() - assert.equal(otherReserveAddresses.length, 0) - }) - - it('should not allow other users to remove another reserve address', async () => { - await assertRevert(reserve.removeOtherReserveAddress(anAddress, index, { from: nonOwner })) - }) - - it('should emit a OtherReserveAddressRemoved event', async () => { - const response = await reserve.removeOtherReserveAddress(anAddress, index) - const events = response.logs - assert.equal(events.length, 1) - assert.equal(events[0].event, 'OtherReserveAddressRemoved') - assertSameAddress(events[0].args.otherReserveAddress, anAddress) - assert.equal(events[0].args.index, index) - }) - }) - }) - - describe('#setAssetAllocations', () => { - const newAssetAllocationSymbols = [ - web3.utils.padRight(web3.utils.utf8ToHex('cGLD'), 64), - web3.utils.padRight(web3.utils.utf8ToHex('BTC'), 64), - web3.utils.padRight(web3.utils.utf8ToHex('ETH'), 64), - ] - const newAssetAllocationWeights = [ - new BigNumber(10).pow(24).dividedBy(new BigNumber(3)).integerValue().plus(new BigNumber(1)), - new BigNumber(10).pow(24).dividedBy(new BigNumber(3)).integerValue(), - new BigNumber(10).pow(24).dividedBy(new BigNumber(3)).integerValue(), - ] - - it('should allow owner to set asset allocations', async () => { - await reserve.setAssetAllocations(newAssetAllocationSymbols, newAssetAllocationWeights) - const assetAllocationSymbols = await reserve.getAssetAllocationSymbols() - const assetAllocationWeights = await reserve.getAssetAllocationWeights() - assert.equal(assetAllocationSymbols.length, newAssetAllocationSymbols.length) - assert.equal(assetAllocationWeights.length, newAssetAllocationWeights.length) - assert.equal(web3.utils.hexToUtf8(assetAllocationSymbols[0]), 'cGLD') - assert.equal(web3.utils.hexToUtf8(assetAllocationSymbols[1]), 'BTC') - assert.equal(web3.utils.hexToUtf8(assetAllocationSymbols[2]), 'ETH') - assert.equal(newAssetAllocationWeights[0].isEqualTo(assetAllocationWeights[0]), true) - assert.equal(newAssetAllocationWeights[1].isEqualTo(assetAllocationWeights[1]), true) - assert.equal(newAssetAllocationWeights[2].isEqualTo(assetAllocationWeights[2]), true) - }) - - it('should not allow other users to set asset allocations', async () => { - await assertRevert( - reserve.setAssetAllocations(newAssetAllocationSymbols, newAssetAllocationWeights, { - from: nonOwner, - }) - ) - }) - - it('should emit a AssetAllocationSet event', async () => { - const response = await reserve.setAssetAllocations( - newAssetAllocationSymbols, - newAssetAllocationWeights - ) - const events = response.logs - assert.equal(events.length, 1) - assert.equal(events[0].event, 'AssetAllocationSet') - assert.equal(events[0].args.symbols.length, 3) - assert.equal(events[0].args.weights.length, 3) - assert.equal(web3.utils.hexToUtf8(events[0].args.symbols[0]), 'cGLD') - assert.equal(web3.utils.hexToUtf8(events[0].args.symbols[1]), 'BTC') - assert.equal(web3.utils.hexToUtf8(events[0].args.symbols[2]), 'ETH') - assert.equal(newAssetAllocationWeights[0].isEqualTo(events[0].args.weights[0]), true) - assert.equal(newAssetAllocationWeights[1].isEqualTo(events[0].args.weights[1]), true) - assert.equal(newAssetAllocationWeights[2].isEqualTo(events[0].args.weights[2]), true) - }) - - it("should fail if the asset allocation doesn't sum to one", async () => { - const badAssetAllocationWeights = newAssetAllocationWeights - badAssetAllocationWeights[0] = badAssetAllocationWeights[0].minus(1) - await assertRevert( - reserve.setAssetAllocations(newAssetAllocationSymbols, badAssetAllocationWeights) - ) - const assetAllocationWeights = await reserve.getAssetAllocationWeights() - assert.equal(assetAllocationWeights.length, initialAssetAllocationWeights.length) - }) - - it('should fail if the asset allocation includes multiple weights for one symbol', async () => { - const badAssetAllocationSymbols = newAssetAllocationSymbols - badAssetAllocationSymbols[1] = web3.utils.padRight(web3.utils.utf8ToHex('cGLD'), 64) - await assertRevert( - reserve.setAssetAllocations(badAssetAllocationSymbols, newAssetAllocationWeights) - ) - const assetAllocationWeights = await reserve.getAssetAllocationWeights() - assert.equal(assetAllocationWeights.length, initialAssetAllocationWeights.length) - }) - - it("should fail if the asset allocation doesn't include cGLD", async () => { - const badAssetAllocationSymbols = newAssetAllocationSymbols - badAssetAllocationSymbols[0] = web3.utils.padRight(web3.utils.utf8ToHex('empty'), 64) - await assertRevert( - reserve.setAssetAllocations(badAssetAllocationSymbols, newAssetAllocationWeights) - ) - const assetAllocationWeights = await reserve.getAssetAllocationWeights() - assert.equal(assetAllocationWeights.length, initialAssetAllocationWeights.length) - }) - }) - - describe('#getReserveRatio', () => { - let mockStableToken: MockStableTokenInstance - let reserveGoldBalance: BigNumber - const exchangeRate = 10 - const newAssetAllocationSymbols = [web3.utils.padRight(web3.utils.utf8ToHex('cGLD'), 64)] - const newAssetAllocationWeights = [new BigNumber(10).pow(24)] - - beforeEach(async () => { - mockStableToken = await MockStableToken.new() - await registry.setAddressFor(CeloContractName.SortedOracles, mockSortedOracles.address) - await mockSortedOracles.setMedianRate( - mockStableToken.address, - sortedOraclesDenominator.times(exchangeRate) - ) - await reserve.addToken(mockStableToken.address) - reserveGoldBalance = new BigNumber(10).pow(19) - await reserve.setAssetAllocations(newAssetAllocationSymbols, newAssetAllocationWeights) - await web3.eth.sendTransaction({ - from: accounts[0], - to: reserve.address, - value: reserveGoldBalance, - }) - }) - - describe('works with stabletoken with no report', async () => { - let mockStableToken2: MockStableTokenInstance - beforeEach(async () => { - // Add another stable token - mockStableToken2 = await MockStableToken.new() - await reserve.addToken(mockStableToken2.address) - }) - - it('should return the correct ratio', async () => { - const stableTokenSupply = new BigNumber(10).pow(21) - await mockStableToken.setTotalSupply(stableTokenSupply) - const ratio = new BigNumber(await reserve.getReserveRatio()) - assert( - fromFixed(ratio).isEqualTo(reserveGoldBalance.div(stableTokenSupply.div(exchangeRate))), - 'reserve ratio should be correct' - ) - }) - }) - - it('should return the correct ratio', async () => { - const stableTokenSupply = new BigNumber(10).pow(21) - await mockStableToken.setTotalSupply(stableTokenSupply) - const ratio = new BigNumber(await reserve.getReserveRatio()) - assert( - fromFixed(ratio).isEqualTo(reserveGoldBalance.div(stableTokenSupply.div(exchangeRate))), - 'reserve ratio should be correct' - ) - }) - }) -}) diff --git a/packages/sdk/contractkit/src/contract-cache.ts b/packages/sdk/contractkit/src/contract-cache.ts index 7c81cbcc049..9d6a6fc59b6 100644 --- a/packages/sdk/contractkit/src/contract-cache.ts +++ b/packages/sdk/contractkit/src/contract-cache.ts @@ -176,10 +176,6 @@ export class WrapperCache implements ContractCacheType { return this.getContract(CeloContract.FederatedAttestations) } - // getFeeCurrencyWhitelist() { - // return this.getWrapper(CeloContract.FeeCurrencyWhitelist, newFeeCurrencyWhitelist) - // } - getGasPriceMinimum() { return this.getContract(CeloContract.GasPriceMinimum) }