From 6e858298bacfba43b57632a8fdfdab9fe6f039f6 Mon Sep 17 00:00:00 2001 From: Derek Croote Date: Sun, 12 Mar 2023 13:17:18 -0700 Subject: [PATCH 01/13] Implement RequesterAuthorizerWithErc721 authorizers --- .changeset/tiny-pumpkins-cross.md | 7 + .../config/config.example.json | 4 +- .../test/fixtures/config.aws.valid.json | 4 +- .../config.example.json | 4 +- .../create-config.ts | 2 + .../config.example.json | 4 +- .../create-config.ts | 2 + .../config.example.json | 4 +- .../coingecko-http-gateways/create-config.ts | 2 + .../config.example.json | 4 +- .../create-config.ts | 2 + .../config.example.json | 4 +- .../coingecko-pre-processing/create-config.ts | 2 + .../coingecko-template/config.example.json | 4 +- .../coingecko-template/create-config.ts | 2 + .../coingecko/config.example.json | 4 +- .../integrations/coingecko/create-config.ts | 2 + .../failing-example/config.example.json | 4 +- .../failing-example/create-config.ts | 2 + .../config.example.json | 4 +- .../relay-security-schemes/create-config.ts | 2 + .../weather-multi-value/config.example.json | 4 +- .../weather-multi-value/create-config.ts | 2 + .../airnode-node/config/config.example.json | 4 +- .../coordinator/calls/chain-limits.test.ts | 2 + .../authorization-fetching.test.ts | 91 ++- .../authorization/authorization-fetching.ts | 143 ++++- .../evm/handlers/initialize-provider.test.ts | 35 +- .../src/evm/handlers/initialize-provider.ts | 81 ++- .../src/providers/actions.test.ts | 13 +- .../airnode-node/src/providers/state.test.ts | 13 +- .../test/fixtures/config/config.ts | 2 + .../test/fixtures/operation/deploy-config.ts | 2 + .../test/fixtures/provider-states/evm.ts | 2 + packages/airnode-node/test/setup/e2e/utils.ts | 2 + .../src/config/evm-dev-config.json | 4 +- packages/airnode-operation/src/types.ts | 24 +- .../dev/RequesterAuthorizerWithErc721.sol | 545 ++++++++++++++++++ .../AccessControlRegistryAdminned.sol | 56 ++ .../AccessControlRegistryUser.sol | 18 + .../access-control-registry/RoleDeriver.sol | 49 ++ .../interfaces/IAccessControlRegistry.sol | 32 + .../IAccessControlRegistryAdminned.sol | 8 + .../interfaces/IAccessControlRegistryUser.sol | 6 + .../IRequesterAuthorizerWithErc721.sol | 187 ++++++ packages/airnode-protocol/hardhat.config.js | 25 +- packages/airnode-protocol/src/index.ts | 3 + .../src/config/config.test.ts | 25 +- .../airnode-validator/src/config/config.ts | 37 +- .../test/fixtures/config.valid.json | 19 + .../fixtures/interpolated-config.valid.json | 19 + .../fixtures/invalid-secret-name/config.json | 4 +- 52 files changed, 1453 insertions(+), 73 deletions(-) create mode 100644 .changeset/tiny-pumpkins-cross.md create mode 100644 packages/airnode-protocol/contracts/dev/RequesterAuthorizerWithErc721.sol create mode 100644 packages/airnode-protocol/contracts/dev/access-control-registry/AccessControlRegistryAdminned.sol create mode 100644 packages/airnode-protocol/contracts/dev/access-control-registry/AccessControlRegistryUser.sol create mode 100644 packages/airnode-protocol/contracts/dev/access-control-registry/RoleDeriver.sol create mode 100644 packages/airnode-protocol/contracts/dev/access-control-registry/interfaces/IAccessControlRegistry.sol create mode 100644 packages/airnode-protocol/contracts/dev/access-control-registry/interfaces/IAccessControlRegistryAdminned.sol create mode 100644 packages/airnode-protocol/contracts/dev/access-control-registry/interfaces/IAccessControlRegistryUser.sol create mode 100644 packages/airnode-protocol/contracts/dev/interfaces/IRequesterAuthorizerWithErc721.sol diff --git a/.changeset/tiny-pumpkins-cross.md b/.changeset/tiny-pumpkins-cross.md new file mode 100644 index 0000000000..5f408ce14f --- /dev/null +++ b/.changeset/tiny-pumpkins-cross.md @@ -0,0 +1,7 @@ +--- +'@api3/airnode-node': minor +'@api3/airnode-protocol': minor +'@api3/airnode-validator': minor +--- + +Implement RequesterAuthorizerWithErc721 authorizers diff --git a/packages/airnode-deployer/config/config.example.json b/packages/airnode-deployer/config/config.example.json index dbd4ddde0a..72856a46ae 100644 --- a/packages/airnode-deployer/config/config.example.json +++ b/packages/airnode-deployer/config/config.example.json @@ -4,7 +4,9 @@ "maxConcurrency": 100, "authorizers": { "requesterEndpointAuthorizers": [], - "crossChainRequesterAuthorizers": [] + "crossChainRequesterAuthorizers": [], + "requesterAuthorizersWithErc721": [], + "crossChainRequesterAuthorizersWithErc721": [] }, "authorizations": { "requesterEndpointAuthorizations": {} diff --git a/packages/airnode-deployer/test/fixtures/config.aws.valid.json b/packages/airnode-deployer/test/fixtures/config.aws.valid.json index 00c4a08581..a0c6f2418d 100644 --- a/packages/airnode-deployer/test/fixtures/config.aws.valid.json +++ b/packages/airnode-deployer/test/fixtures/config.aws.valid.json @@ -4,7 +4,9 @@ "maxConcurrency": 100, "authorizers": { "requesterEndpointAuthorizers": [], - "crossChainRequesterAuthorizers": [] + "crossChainRequesterAuthorizers": [], + "requesterAuthorizersWithErc721": [], + "crossChainRequesterAuthorizersWithErc721": [] }, "authorizations": { "requesterEndpointAuthorizations": {} diff --git a/packages/airnode-examples/integrations/authenticated-coinmarketcap/config.example.json b/packages/airnode-examples/integrations/authenticated-coinmarketcap/config.example.json index 5e34188818..09f9b9871c 100644 --- a/packages/airnode-examples/integrations/authenticated-coinmarketcap/config.example.json +++ b/packages/airnode-examples/integrations/authenticated-coinmarketcap/config.example.json @@ -4,7 +4,9 @@ "maxConcurrency": 100, "authorizers": { "requesterEndpointAuthorizers": [], - "crossChainRequesterAuthorizers": [] + "crossChainRequesterAuthorizers": [], + "requesterAuthorizersWithErc721": [], + "crossChainRequesterAuthorizersWithErc721": [] }, "authorizations": { "requesterEndpointAuthorizations": {} diff --git a/packages/airnode-examples/integrations/authenticated-coinmarketcap/create-config.ts b/packages/airnode-examples/integrations/authenticated-coinmarketcap/create-config.ts index b5f0e65fbc..c5d6451467 100644 --- a/packages/airnode-examples/integrations/authenticated-coinmarketcap/create-config.ts +++ b/packages/airnode-examples/integrations/authenticated-coinmarketcap/create-config.ts @@ -14,6 +14,8 @@ const createConfig = async (generateExampleFile: boolean): Promise => ({ authorizers: { requesterEndpointAuthorizers: [], crossChainRequesterAuthorizers: [], + requesterAuthorizersWithErc721: [], + crossChainRequesterAuthorizersWithErc721: [], }, authorizations: { requesterEndpointAuthorizations: {}, diff --git a/packages/airnode-examples/integrations/coingecko-cross-chain-authorizer/config.example.json b/packages/airnode-examples/integrations/coingecko-cross-chain-authorizer/config.example.json index 8847b74a45..0db3dc9f19 100644 --- a/packages/airnode-examples/integrations/coingecko-cross-chain-authorizer/config.example.json +++ b/packages/airnode-examples/integrations/coingecko-cross-chain-authorizer/config.example.json @@ -16,7 +16,9 @@ "url": "${CROSS_CHAIN_PROVIDER_URL}" } } - ] + ], + "requesterAuthorizersWithErc721": [], + "crossChainRequesterAuthorizersWithErc721": [] }, "authorizations": { "requesterEndpointAuthorizations": {} diff --git a/packages/airnode-examples/integrations/coingecko-cross-chain-authorizer/create-config.ts b/packages/airnode-examples/integrations/coingecko-cross-chain-authorizer/create-config.ts index 6472cf7ccd..c2395903a5 100644 --- a/packages/airnode-examples/integrations/coingecko-cross-chain-authorizer/create-config.ts +++ b/packages/airnode-examples/integrations/coingecko-cross-chain-authorizer/create-config.ts @@ -27,6 +27,8 @@ const createConfig = async (generateExampleFile: boolean): Promise => ({ }, }, ], + requesterAuthorizersWithErc721: [], + crossChainRequesterAuthorizersWithErc721: [], }, authorizations: { requesterEndpointAuthorizations: {}, diff --git a/packages/airnode-examples/integrations/coingecko-http-gateways/config.example.json b/packages/airnode-examples/integrations/coingecko-http-gateways/config.example.json index 24afe1cba7..3c2cdfd87d 100644 --- a/packages/airnode-examples/integrations/coingecko-http-gateways/config.example.json +++ b/packages/airnode-examples/integrations/coingecko-http-gateways/config.example.json @@ -4,7 +4,9 @@ "maxConcurrency": 100, "authorizers": { "requesterEndpointAuthorizers": [], - "crossChainRequesterAuthorizers": [] + "crossChainRequesterAuthorizers": [], + "requesterAuthorizersWithErc721": [], + "crossChainRequesterAuthorizersWithErc721": [] }, "authorizations": { "requesterEndpointAuthorizations": {} diff --git a/packages/airnode-examples/integrations/coingecko-http-gateways/create-config.ts b/packages/airnode-examples/integrations/coingecko-http-gateways/create-config.ts index fcc5313da6..da9032b18e 100644 --- a/packages/airnode-examples/integrations/coingecko-http-gateways/create-config.ts +++ b/packages/airnode-examples/integrations/coingecko-http-gateways/create-config.ts @@ -14,6 +14,8 @@ const createConfig = async (generateExampleFile: boolean): Promise => ({ authorizers: { requesterEndpointAuthorizers: [], crossChainRequesterAuthorizers: [], + requesterAuthorizersWithErc721: [], + crossChainRequesterAuthorizersWithErc721: [], }, authorizations: { requesterEndpointAuthorizations: {}, diff --git a/packages/airnode-examples/integrations/coingecko-post-processing/config.example.json b/packages/airnode-examples/integrations/coingecko-post-processing/config.example.json index b422cf3f0d..9b8c0f2a67 100644 --- a/packages/airnode-examples/integrations/coingecko-post-processing/config.example.json +++ b/packages/airnode-examples/integrations/coingecko-post-processing/config.example.json @@ -4,7 +4,9 @@ "maxConcurrency": 100, "authorizers": { "requesterEndpointAuthorizers": [], - "crossChainRequesterAuthorizers": [] + "crossChainRequesterAuthorizers": [], + "requesterAuthorizersWithErc721": [], + "crossChainRequesterAuthorizersWithErc721": [] }, "authorizations": { "requesterEndpointAuthorizations": {} diff --git a/packages/airnode-examples/integrations/coingecko-post-processing/create-config.ts b/packages/airnode-examples/integrations/coingecko-post-processing/create-config.ts index fade47d266..f6ffb444db 100644 --- a/packages/airnode-examples/integrations/coingecko-post-processing/create-config.ts +++ b/packages/airnode-examples/integrations/coingecko-post-processing/create-config.ts @@ -14,6 +14,8 @@ const createConfig = async (generateExampleFile: boolean): Promise => ({ authorizers: { requesterEndpointAuthorizers: [], crossChainRequesterAuthorizers: [], + requesterAuthorizersWithErc721: [], + crossChainRequesterAuthorizersWithErc721: [], }, authorizations: { requesterEndpointAuthorizations: {}, diff --git a/packages/airnode-examples/integrations/coingecko-pre-processing/config.example.json b/packages/airnode-examples/integrations/coingecko-pre-processing/config.example.json index 5f6d5a0bc8..d700c6a450 100644 --- a/packages/airnode-examples/integrations/coingecko-pre-processing/config.example.json +++ b/packages/airnode-examples/integrations/coingecko-pre-processing/config.example.json @@ -4,7 +4,9 @@ "maxConcurrency": 100, "authorizers": { "requesterEndpointAuthorizers": [], - "crossChainRequesterAuthorizers": [] + "crossChainRequesterAuthorizers": [], + "requesterAuthorizersWithErc721": [], + "crossChainRequesterAuthorizersWithErc721": [] }, "authorizations": { "requesterEndpointAuthorizations": {} diff --git a/packages/airnode-examples/integrations/coingecko-pre-processing/create-config.ts b/packages/airnode-examples/integrations/coingecko-pre-processing/create-config.ts index dcd27525a6..eeb42a8ac6 100644 --- a/packages/airnode-examples/integrations/coingecko-pre-processing/create-config.ts +++ b/packages/airnode-examples/integrations/coingecko-pre-processing/create-config.ts @@ -14,6 +14,8 @@ const createConfig = async (generateExampleFile: boolean): Promise => ({ authorizers: { requesterEndpointAuthorizers: [], crossChainRequesterAuthorizers: [], + requesterAuthorizersWithErc721: [], + crossChainRequesterAuthorizersWithErc721: [], }, authorizations: { requesterEndpointAuthorizations: {}, diff --git a/packages/airnode-examples/integrations/coingecko-template/config.example.json b/packages/airnode-examples/integrations/coingecko-template/config.example.json index 745d12b89c..16b404e95f 100644 --- a/packages/airnode-examples/integrations/coingecko-template/config.example.json +++ b/packages/airnode-examples/integrations/coingecko-template/config.example.json @@ -4,7 +4,9 @@ "maxConcurrency": 100, "authorizers": { "requesterEndpointAuthorizers": [], - "crossChainRequesterAuthorizers": [] + "crossChainRequesterAuthorizers": [], + "requesterAuthorizersWithErc721": [], + "crossChainRequesterAuthorizersWithErc721": [] }, "authorizations": { "requesterEndpointAuthorizations": {} diff --git a/packages/airnode-examples/integrations/coingecko-template/create-config.ts b/packages/airnode-examples/integrations/coingecko-template/create-config.ts index 6eb9a4ec94..90da078396 100644 --- a/packages/airnode-examples/integrations/coingecko-template/create-config.ts +++ b/packages/airnode-examples/integrations/coingecko-template/create-config.ts @@ -15,6 +15,8 @@ const createConfig = async (generateExampleFile: boolean): Promise => ({ authorizers: { requesterEndpointAuthorizers: [], crossChainRequesterAuthorizers: [], + requesterAuthorizersWithErc721: [], + crossChainRequesterAuthorizersWithErc721: [], }, authorizations: { requesterEndpointAuthorizations: {}, diff --git a/packages/airnode-examples/integrations/coingecko/config.example.json b/packages/airnode-examples/integrations/coingecko/config.example.json index c6faff8d15..cc89f4c6fb 100644 --- a/packages/airnode-examples/integrations/coingecko/config.example.json +++ b/packages/airnode-examples/integrations/coingecko/config.example.json @@ -4,7 +4,9 @@ "maxConcurrency": 100, "authorizers": { "requesterEndpointAuthorizers": [], - "crossChainRequesterAuthorizers": [] + "crossChainRequesterAuthorizers": [], + "requesterAuthorizersWithErc721": [], + "crossChainRequesterAuthorizersWithErc721": [] }, "authorizations": { "requesterEndpointAuthorizations": {} diff --git a/packages/airnode-examples/integrations/coingecko/create-config.ts b/packages/airnode-examples/integrations/coingecko/create-config.ts index 51d80ee6d8..9319665c7e 100644 --- a/packages/airnode-examples/integrations/coingecko/create-config.ts +++ b/packages/airnode-examples/integrations/coingecko/create-config.ts @@ -14,6 +14,8 @@ const createConfig = async (generateExampleFile: boolean): Promise => ({ authorizers: { requesterEndpointAuthorizers: [], crossChainRequesterAuthorizers: [], + requesterAuthorizersWithErc721: [], + crossChainRequesterAuthorizersWithErc721: [], }, authorizations: { requesterEndpointAuthorizations: {}, diff --git a/packages/airnode-examples/integrations/failing-example/config.example.json b/packages/airnode-examples/integrations/failing-example/config.example.json index add44b1fe8..439cf431da 100644 --- a/packages/airnode-examples/integrations/failing-example/config.example.json +++ b/packages/airnode-examples/integrations/failing-example/config.example.json @@ -4,7 +4,9 @@ "maxConcurrency": 100, "authorizers": { "requesterEndpointAuthorizers": [], - "crossChainRequesterAuthorizers": [] + "crossChainRequesterAuthorizers": [], + "requesterAuthorizersWithErc721": [], + "crossChainRequesterAuthorizersWithErc721": [] }, "authorizations": { "requesterEndpointAuthorizations": {} diff --git a/packages/airnode-examples/integrations/failing-example/create-config.ts b/packages/airnode-examples/integrations/failing-example/create-config.ts index 457c8cafc3..d3690fccc6 100644 --- a/packages/airnode-examples/integrations/failing-example/create-config.ts +++ b/packages/airnode-examples/integrations/failing-example/create-config.ts @@ -14,6 +14,8 @@ const createConfig = async (generateExampleFile: boolean): Promise => ({ authorizers: { requesterEndpointAuthorizers: [], crossChainRequesterAuthorizers: [], + requesterAuthorizersWithErc721: [], + crossChainRequesterAuthorizersWithErc721: [], }, authorizations: { requesterEndpointAuthorizations: {}, diff --git a/packages/airnode-examples/integrations/relay-security-schemes/config.example.json b/packages/airnode-examples/integrations/relay-security-schemes/config.example.json index aa966a2324..430b520830 100644 --- a/packages/airnode-examples/integrations/relay-security-schemes/config.example.json +++ b/packages/airnode-examples/integrations/relay-security-schemes/config.example.json @@ -4,7 +4,9 @@ "maxConcurrency": 100, "authorizers": { "requesterEndpointAuthorizers": [], - "crossChainRequesterAuthorizers": [] + "crossChainRequesterAuthorizers": [], + "requesterAuthorizersWithErc721": [], + "crossChainRequesterAuthorizersWithErc721": [] }, "authorizations": { "requesterEndpointAuthorizations": {} diff --git a/packages/airnode-examples/integrations/relay-security-schemes/create-config.ts b/packages/airnode-examples/integrations/relay-security-schemes/create-config.ts index 40b3a2527e..ea59d10eeb 100644 --- a/packages/airnode-examples/integrations/relay-security-schemes/create-config.ts +++ b/packages/airnode-examples/integrations/relay-security-schemes/create-config.ts @@ -14,6 +14,8 @@ const createConfig = async (generateExampleFile: boolean): Promise => ({ authorizers: { requesterEndpointAuthorizers: [], crossChainRequesterAuthorizers: [], + requesterAuthorizersWithErc721: [], + crossChainRequesterAuthorizersWithErc721: [], }, authorizations: { requesterEndpointAuthorizations: {}, diff --git a/packages/airnode-examples/integrations/weather-multi-value/config.example.json b/packages/airnode-examples/integrations/weather-multi-value/config.example.json index b5092cc14f..25e2a9163d 100644 --- a/packages/airnode-examples/integrations/weather-multi-value/config.example.json +++ b/packages/airnode-examples/integrations/weather-multi-value/config.example.json @@ -4,7 +4,9 @@ "maxConcurrency": 100, "authorizers": { "requesterEndpointAuthorizers": [], - "crossChainRequesterAuthorizers": [] + "crossChainRequesterAuthorizers": [], + "requesterAuthorizersWithErc721": [], + "crossChainRequesterAuthorizersWithErc721": [] }, "authorizations": { "requesterEndpointAuthorizations": {} diff --git a/packages/airnode-examples/integrations/weather-multi-value/create-config.ts b/packages/airnode-examples/integrations/weather-multi-value/create-config.ts index 3151367184..3708dc544f 100644 --- a/packages/airnode-examples/integrations/weather-multi-value/create-config.ts +++ b/packages/airnode-examples/integrations/weather-multi-value/create-config.ts @@ -14,6 +14,8 @@ const createConfig = async (generateExampleFile: boolean): Promise => ({ authorizers: { requesterEndpointAuthorizers: [], crossChainRequesterAuthorizers: [], + requesterAuthorizersWithErc721: [], + crossChainRequesterAuthorizersWithErc721: [], }, authorizations: { requesterEndpointAuthorizations: {}, diff --git a/packages/airnode-node/config/config.example.json b/packages/airnode-node/config/config.example.json index e0f37594d1..1fd69a25ad 100644 --- a/packages/airnode-node/config/config.example.json +++ b/packages/airnode-node/config/config.example.json @@ -4,7 +4,9 @@ "maxConcurrency": 100, "authorizers": { "requesterEndpointAuthorizers": [], - "crossChainRequesterAuthorizers": [] + "crossChainRequesterAuthorizers": [], + "requesterAuthorizersWithErc721": [], + "crossChainRequesterAuthorizersWithErc721": [] }, "authorizations": { "requesterEndpointAuthorizations": {} diff --git a/packages/airnode-node/src/coordinator/calls/chain-limits.test.ts b/packages/airnode-node/src/coordinator/calls/chain-limits.test.ts index 90e2895583..29b6ef7087 100644 --- a/packages/airnode-node/src/coordinator/calls/chain-limits.test.ts +++ b/packages/airnode-node/src/coordinator/calls/chain-limits.test.ts @@ -10,6 +10,8 @@ const createChainConfig = (overrides: Partial): ChainConfig => { authorizers: { requesterEndpointAuthorizers: [], crossChainRequesterAuthorizers: [], + requesterAuthorizersWithErc721: [], + crossChainRequesterAuthorizersWithErc721: [], }, authorizations: { requesterEndpointAuthorizations: {}, diff --git a/packages/airnode-node/src/evm/authorization/authorization-fetching.test.ts b/packages/airnode-node/src/evm/authorization/authorization-fetching.test.ts index ae3f2868ad..b08dad3e89 100644 --- a/packages/airnode-node/src/evm/authorization/authorization-fetching.test.ts +++ b/packages/airnode-node/src/evm/authorization/authorization-fetching.test.ts @@ -9,16 +9,18 @@ mockEthers({ }); import { ethers } from 'ethers'; +import { RequesterAuthorizerWithErc721Factory } from '@api3/airnode-protocol'; import * as authorization from './authorization-fetching'; import * as fixtures from '../../../test/fixtures'; import { AirnodeRrpV0 } from '../contracts'; import { ApiCall, Request } from '../../../src/types'; describe('fetch (authorizations)', () => { - let mutableFetchOptions: authorization.FetchOptions; + let mutableFetchOptions: authorization.AirnodeRrpFetchOptions; beforeEach(() => { mutableFetchOptions = { + type: 'airnodeRrp', requesterEndpointAuthorizers: [ '0x711c93B32c0D28a5d18feD87434cce11C3e5699B', '0x9E0e23766b0ed0C492804872c5164E9187fB56f5', @@ -135,8 +137,17 @@ describe('fetch (authorizations)', () => { const apiCall = fixtures.requests.buildApiCall(); const [logs, res] = await authorization.fetch([apiCall], mutableFetchOptions); expect(logs).toEqual([ - { level: 'ERROR', message: 'Failed to fetch group authorization details', error: new Error('Server says no') }, - { level: 'INFO', message: `Fetched authorization status for Request:${apiCall.id}` }, + { + level: 'WARN', + message: + 'Failed to fetch requesterEndpointAuthorizers authorization using checkAuthorizationStatuses.' + + 'Falling back to fetching authorizations individually.', + error: new Error('Server says no'), + }, + { + level: 'INFO', + message: `Fetched requesterEndpointAuthorizers authorization using checkAuthorizationStatus for Request:${apiCall.id}`, + }, ]); expect(res).toEqual({ [apiCall.id]: true }); }); @@ -151,8 +162,17 @@ describe('fetch (authorizations)', () => { const apiCall = fixtures.requests.buildApiCall(); const [logs, res] = await authorization.fetch([apiCall], mutableFetchOptions); expect(logs).toEqual([ - { level: 'ERROR', message: 'Failed to fetch group authorization details', error: new Error('Server says no') }, - { level: 'INFO', message: `Fetched authorization status for Request:${apiCall.id}` }, + { + level: 'WARN', + message: + 'Failed to fetch requesterEndpointAuthorizers authorization using checkAuthorizationStatuses.' + + 'Falling back to fetching authorizations individually.', + error: new Error('Server says no'), + }, + { + level: 'INFO', + message: `Fetched requesterEndpointAuthorizers authorization using checkAuthorizationStatus for Request:${apiCall.id}`, + }, ]); expect(res).toEqual({ [apiCall.id]: false }); }); @@ -167,10 +187,16 @@ describe('fetch (authorizations)', () => { const apiCall = fixtures.requests.buildApiCall(); const [logs, res] = await authorization.fetch([apiCall], mutableFetchOptions); expect(logs).toEqual([ - { level: 'ERROR', message: 'Failed to fetch group authorization details', error: new Error('Server says no') }, + { + level: 'WARN', + message: + 'Failed to fetch requesterEndpointAuthorizers authorization using checkAuthorizationStatuses.' + + 'Falling back to fetching authorizations individually.', + error: new Error('Server says no'), + }, { level: 'ERROR', - message: `Failed to fetch authorization details for Request:${apiCall.id}`, + message: `Failed to fetch requesterEndpointAuthorizers authorization using checkAuthorizationStatus for Request:${apiCall.id}`, error: new Error('Server still says no'), }, ]); @@ -333,7 +359,12 @@ describe('fetchAuthorizationStatus', () => { airnodeAddress, apiCall ); - expect(logs).toEqual([{ level: 'INFO', message: `Fetched authorization status for Request:${apiCall.id}` }]); + expect(logs).toEqual([ + { + level: 'INFO', + message: `Fetched requesterEndpointAuthorizers authorization using checkAuthorizationStatus for Request:${apiCall.id}`, + }, + ]); expect(res).toEqual(true); }); @@ -347,7 +378,12 @@ describe('fetchAuthorizationStatus', () => { airnodeAddress, apiCall ); - expect(logs).toEqual([{ level: 'INFO', message: `Fetched authorization status for Request:${apiCall.id}` }]); + expect(logs).toEqual([ + { + level: 'INFO', + message: `Fetched requesterEndpointAuthorizers authorization using checkAuthorizationStatus for Request:${apiCall.id}`, + }, + ]); expect(res).toEqual(false); }); @@ -364,10 +400,45 @@ describe('fetchAuthorizationStatus', () => { expect(logs).toEqual([ { level: 'ERROR', - message: `Failed to fetch authorization details for Request:${apiCall.id}`, + message: `Failed to fetch requesterEndpointAuthorizers authorization using checkAuthorizationStatus for Request:${apiCall.id}`, error: new Error('Server still says no'), }, ]); expect(res).toEqual(null); }); }); + +describe('decodeMulticall', () => { + it('decodes the results of a multicall', () => { + const requesterAuthorizerWithErc721 = RequesterAuthorizerWithErc721Factory.connect( + '0x0', + new ethers.providers.JsonRpcProvider() + ); + const data = [ + ethers.utils.defaultAbiCoder.encode(['bool'], [true]), + ethers.utils.defaultAbiCoder.encode(['bool'], [false]), + ]; + expect(authorization.decodeMulticall(requesterAuthorizerWithErc721, data)).toEqual([true, false]); + }); +}); + +describe('applyErc721Authorizations', () => { + it('returns authorization statuses in the same order that they were requested', () => { + const apiCalls: Request[] = [ + fixtures.requests.buildApiCall({ id: '0x1' }), + fixtures.requests.buildApiCall({ id: '0x2' }), + fixtures.requests.buildApiCall({ id: '0x3' }), + fixtures.requests.buildApiCall({ id: '0x4' }), + ]; + const erc721s = ['0x1', '0x2']; + const results = [true, true, true, false, false, true, false, false]; + // The requester is authorized if authorized by any Erc721 + const expected = { + '0x1': true, + '0x2': true, + '0x3': true, + '0x4': false, + }; + expect(authorization.applyErc721Authorizations(apiCalls, erc721s, results)).toEqual(expected); + }); +}); diff --git a/packages/airnode-node/src/evm/authorization/authorization-fetching.ts b/packages/airnode-node/src/evm/authorization/authorization-fetching.ts index bbbd0826c9..e3f6f16bf1 100644 --- a/packages/airnode-node/src/evm/authorization/authorization-fetching.ts +++ b/packages/airnode-node/src/evm/authorization/authorization-fetching.ts @@ -5,19 +5,31 @@ import isEmpty from 'lodash/isEmpty'; import isNil from 'lodash/isNil'; import { logger } from '@api3/airnode-utilities'; import { go } from '@api3/promise-utils'; +import { RequesterAuthorizerWithErc721, RequesterAuthorizerWithErc721Factory } from '@api3/airnode-protocol'; import { ApiCall, AuthorizationByRequestId, Request, LogsData } from '../../types'; import { CONVENIENCE_BATCH_SIZE, BLOCKCHAIN_CALL_ATTEMPT_TIMEOUT } from '../../constants'; import { AirnodeRrpV0, AirnodeRrpV0Factory } from '../contracts'; -import { RequesterEndpointAuthorizers, ChainAuthorizations } from '../../config'; +import { Erc721s, ChainAuthorizations, RequesterEndpointAuthorizers } from '../../config'; export interface FetchOptions { - readonly requesterEndpointAuthorizers: RequesterEndpointAuthorizers; - readonly authorizations: ChainAuthorizations; readonly airnodeAddress: string; - readonly airnodeRrpAddress: string; + readonly authorizations: ChainAuthorizations; readonly provider: ethers.providers.JsonRpcProvider; } +export interface AirnodeRrpFetchOptions extends FetchOptions { + readonly type: 'airnodeRrp'; + readonly airnodeRrpAddress: string; + readonly requesterEndpointAuthorizers: RequesterEndpointAuthorizers; +} + +export interface Erc721FetchOptions extends FetchOptions { + readonly type: 'erc721'; + readonly chainId: string; + readonly erc721s: Erc721s; + readonly RequesterAuthorizerWithErc721Address: string; +} + export async function fetchAuthorizationStatus( airnodeRrp: AirnodeRrpV0, requesterEndpointAuthorizers: RequesterEndpointAuthorizers, @@ -39,16 +51,22 @@ export async function fetchAuthorizationStatus( if (!goAuthorized.success) { const log = logger.pend( 'ERROR', - `Failed to fetch authorization details for Request:${apiCall.id}`, + `Failed to fetch requesterEndpointAuthorizers authorization using checkAuthorizationStatus for Request:${apiCall.id}`, goAuthorized.error ); return [[log], null]; } if (isNil(goAuthorized.data)) { - const log = logger.pend('ERROR', `Failed to fetch authorization details for Request:${apiCall.id}`); + const log = logger.pend( + 'ERROR', + `Failed to fetch requesterEndpointAuthorizers authorization using checkAuthorizationStatus for Request:${apiCall.id}` + ); return [[log], null]; } - const successLog = logger.pend('INFO', `Fetched authorization status for Request:${apiCall.id}`); + const successLog = logger.pend( + 'INFO', + `Fetched requesterEndpointAuthorizers authorization using checkAuthorizationStatus for Request:${apiCall.id}` + ); return [[successLog], goAuthorized.data]; } @@ -77,7 +95,12 @@ async function fetchAuthorizationStatuses( const goData = await go(contractCall, { retries: 1, attemptTimeoutMs: BLOCKCHAIN_CALL_ATTEMPT_TIMEOUT }); if (!goData.success) { - const groupLog = logger.pend('ERROR', 'Failed to fetch group authorization details', goData.error); + const groupLog = logger.pend( + 'WARN', + 'Failed to fetch requesterEndpointAuthorizers authorization using checkAuthorizationStatuses.' + + 'Falling back to fetching authorizations individually.', + goData.error + ); // If the authorization batch cannot be fetched, fallback to fetching authorizations individually const promises: Promise>[] = apiCalls.map( @@ -111,6 +134,63 @@ async function fetchAuthorizationStatuses( return [[], authorizationsById]; } +export function decodeMulticall( + requesterAuthorizerWithErc721: RequesterAuthorizerWithErc721, + data: string[] +): boolean[] { + return data.map((d) => requesterAuthorizerWithErc721.interface.decodeFunctionResult('isAuthorized', d)[0]); +} + +/** + * Returns authorization statuses by id in their requested order from the decoded multicall boolean array + */ +export function applyErc721Authorizations( + apiCalls: Request[], + erc721s: Erc721s, + authorizations: boolean[] +): AuthorizationByRequestId { + return apiCalls.reduce((acc, apiCall, index) => { + // Erc721s as an array requires slicing the authorizations array to get each api call's authorizations + const resultIndex = index * erc721s.length; + // The requester is authorized if authorized by any Erc721 + const authorized = authorizations.slice(resultIndex, resultIndex + erc721s.length).some((r) => r); + return { ...acc, [apiCall.id]: authorized }; + }, {}); +} + +async function fetchErc721AuthorizationStatuses( + requesterAuthorizerWithErc721: RequesterAuthorizerWithErc721, + airnodeAddress: string, + erc721s: Erc721s, + chainId: string, + apiCalls: Request[] +): Promise> { + // Batch isAuthorized calls using multicall. + const calldata = apiCalls.flatMap((apiCall) => { + return erc721s.map((erc721) => { + return requesterAuthorizerWithErc721.interface.encodeFunctionData('isAuthorized', [ + airnodeAddress, + chainId, + apiCall.requesterAddress, + erc721, + ]); + }); + }); + const contractCall = () => requesterAuthorizerWithErc721.callStatic.multicall(calldata); + const goData = await go(contractCall, { retries: 1, attemptTimeoutMs: BLOCKCHAIN_CALL_ATTEMPT_TIMEOUT }); + + if (!goData.success) { + const groupLog = logger.pend('ERROR', 'Failed to fetch Erc721 batch authorizations', goData.error); + + return [[groupLog], null]; + } + + const decodedMulticall = decodeMulticall(requesterAuthorizerWithErc721, goData.data); + const authorizationsById = applyErc721Authorizations(apiCalls, erc721s, decodedMulticall); + + return [[], authorizationsById]; +} + export const checkConfigAuthorizations = (apiCalls: Request[], fetchOptions: FetchOptions) => { return apiCalls.reduce((acc: AuthorizationByRequestId, apiCall) => { // Check if an authorization is found in config for the apiCall endpointId @@ -129,15 +209,17 @@ export const checkConfigAuthorizations = (apiCalls: Request[], fetchOpt export async function fetch( apiCalls: Request[], - fetchOptions: FetchOptions + fetchOptions: AirnodeRrpFetchOptions | Erc721FetchOptions ): Promise> { // If there are no pending API calls then there is no need to make an ETH call if (isEmpty(apiCalls)) { return [[], {}]; } - // If there are no authorizer contracts then endpoint is public - if (isEmpty(fetchOptions.requesterEndpointAuthorizers)) { + // If there are no authorizer or ERC721 contracts then endpoint is public + const contracts = + fetchOptions.type === 'airnodeRrp' ? fetchOptions.requesterEndpointAuthorizers : fetchOptions.erc721s; + if (isEmpty(contracts)) { const authorizationByRequestIds = apiCalls.map((pendingApiCall) => ({ [pendingApiCall.id]: true, })); @@ -158,17 +240,34 @@ export async function fetch( const groupedPairs = chunk(apiCallsToFetchAuthorizationStatus, CONVENIENCE_BATCH_SIZE); // Create an instance of the contract that we can re-use - const airnodeRrp = AirnodeRrpV0Factory.connect(fetchOptions.airnodeRrpAddress, fetchOptions.provider); - - // Fetch all authorization statuses in parallel - const promises = groupedPairs.map((pairs) => - fetchAuthorizationStatuses( - airnodeRrp, - fetchOptions.requesterEndpointAuthorizers, - fetchOptions.airnodeAddress, - pairs - ) - ); + let promises: Promise>[]; + switch (fetchOptions.type) { + case 'airnodeRrp': + // Fetch all authorization statuses in parallel + promises = groupedPairs.map((pairs) => + fetchAuthorizationStatuses( + AirnodeRrpV0Factory.connect(fetchOptions.airnodeRrpAddress, fetchOptions.provider), + fetchOptions.requesterEndpointAuthorizers, + fetchOptions.airnodeAddress, + pairs + ) + ); + break; + case 'erc721': + promises = groupedPairs.map((pairs) => + fetchErc721AuthorizationStatuses( + RequesterAuthorizerWithErc721Factory.connect( + fetchOptions.RequesterAuthorizerWithErc721Address, + fetchOptions.provider + ), + fetchOptions.airnodeAddress, + fetchOptions.erc721s, + fetchOptions.chainId, + pairs + ) + ); + break; + } const responses = await Promise.all(promises); const responseLogs = flatMap(responses, (r) => r[0]); diff --git a/packages/airnode-node/src/evm/handlers/initialize-provider.test.ts b/packages/airnode-node/src/evm/handlers/initialize-provider.test.ts index d92b2a4625..6a0283eb7b 100644 --- a/packages/airnode-node/src/evm/handlers/initialize-provider.test.ts +++ b/packages/airnode-node/src/evm/handlers/initialize-provider.test.ts @@ -241,6 +241,9 @@ describe('initializeProvider', () => { '0x4': false, '0x5': false, '0x6': true, + '0x7': false, + '0x8': false, + '0x9': false, }; const crossChainAuthorizations: AuthorizationByRequestId = { '0x1': true, @@ -248,8 +251,35 @@ describe('initializeProvider', () => { '0x3': false, '0x4': false, }; + const erc721authorizations: AuthorizationByRequestId = { + '0x1': false, + '0x2': false, + '0x3': false, + '0x4': false, + '0x5': false, + '0x6': false, + '0x7': true, + '0x8': false, + '0x9': false, + }; + const erc721CrossChainAuthorizations: AuthorizationByRequestId = { + '0x1': false, + '0x2': false, + '0x3': false, + '0x4': false, + '0x5': false, + '0x6': false, + '0x7': false, + '0x8': true, + '0x9': false, + }; - const merged = mergeAuthorizationsByRequestId([authorizations, crossChainAuthorizations]); + const merged = mergeAuthorizationsByRequestId([ + authorizations, + crossChainAuthorizations, + erc721authorizations, + erc721CrossChainAuthorizations, + ]); expect(merged).toEqual({ '0x1': true, '0x2': true, @@ -257,6 +287,9 @@ describe('initializeProvider', () => { '0x4': false, '0x5': false, '0x6': true, + '0x7': true, + '0x8': true, + '0x9': false, } as AuthorizationByRequestId); }); }); diff --git a/packages/airnode-node/src/evm/handlers/initialize-provider.ts b/packages/airnode-node/src/evm/handlers/initialize-provider.ts index 2872679931..bd3ce936e0 100644 --- a/packages/airnode-node/src/evm/handlers/initialize-provider.ts +++ b/packages/airnode-node/src/evm/handlers/initialize-provider.ts @@ -10,12 +10,13 @@ import * as templates from '../templates'; import * as transactionCounts from '../transaction-counts'; import * as verification from '../verification'; import { buildEVMProvider } from '../evm-provider'; -import { AuthorizationByRequestId, EVMProviderState, ProviderState } from '../../types'; +import { AuthorizationByRequestId, EVMProviderState, LogsData, ProviderState } from '../../types'; type ParallelPromise = Promise<{ readonly id: string; readonly data: any; readonly logs: PendingLog[] }>; async function fetchSameChainAuthorizations(currentState: ProviderState) { - const fetchOptions: authorizations.FetchOptions = { + const fetchOptions: authorizations.AirnodeRrpFetchOptions = { + type: 'airnodeRrp', requesterEndpointAuthorizers: currentState.settings.authorizers.requesterEndpointAuthorizers, authorizations: currentState.settings.authorizations, airnodeAddress: currentState.settings.airnodeAddress, @@ -26,14 +27,16 @@ async function fetchSameChainAuthorizations(currentState: ProviderState) { - const promises = currentState.settings.authorizers.crossChainRequesterAuthorizers.map(async (authorizer) => { - const fetchOptions: authorizations.FetchOptions = { - requesterEndpointAuthorizers: authorizer.requesterEndpointAuthorizers, - authorizations: currentState.settings.authorizations, +async function fetchSameChainErc721Authorizations(currentState: ProviderState) { + const promises = currentState.settings.authorizers.requesterAuthorizersWithErc721.map(async (authorizer) => { + const fetchOptions: authorizations.Erc721FetchOptions = { + type: 'erc721', airnodeAddress: currentState.settings.airnodeAddress, - airnodeRrpAddress: authorizer.contracts.AirnodeRrp, - provider: buildEVMProvider(authorizer.chainProvider.url, authorizer.chainId), + authorizations: currentState.settings.authorizations, + chainId: currentState.settings.chainId, + erc721s: authorizer.erc721s, + provider: currentState.provider, + RequesterAuthorizerWithErc721Address: authorizer.RequesterAuthorizerWithErc721, }; const result = await authorizations.fetch(currentState.requests.apiCalls, fetchOptions); return result; @@ -43,7 +46,51 @@ async function fetchCrossChainAuthorizations(currentState: ProviderState r[0]); const authorizationStatuses = responses.map((r) => r[1]); - return { id: 'crossChainAuthorizations', data: authorizationStatuses, logs }; + return { id: 'erc721Authorizations', data: authorizationStatuses, logs }; +} + +async function fetchCrossChainAuthorizations( + currentState: ProviderState, + id: 'crossChainAuthorizations' | 'erc721CrossChainAuthorizations' +) { + let promises: Promise>[]; + switch (id) { + case 'crossChainAuthorizations': + promises = currentState.settings.authorizers.crossChainRequesterAuthorizers.map(async (authorizer) => { + const fetchOptions: authorizations.AirnodeRrpFetchOptions = { + type: 'airnodeRrp', + requesterEndpointAuthorizers: authorizer.requesterEndpointAuthorizers, + authorizations: currentState.settings.authorizations, + airnodeAddress: currentState.settings.airnodeAddress, + airnodeRrpAddress: authorizer.contracts.AirnodeRrp, + provider: buildEVMProvider(authorizer.chainProvider.url, authorizer.chainId), + }; + const result = await authorizations.fetch(currentState.requests.apiCalls, fetchOptions); + return result; + }); + break; + case 'erc721CrossChainAuthorizations': + promises = currentState.settings.authorizers.crossChainRequesterAuthorizersWithErc721.map(async (authorizer) => { + const fetchOptions: authorizations.Erc721FetchOptions = { + type: 'erc721', + airnodeAddress: currentState.settings.airnodeAddress, + authorizations: currentState.settings.authorizations, + chainId: authorizer.chainId, + erc721s: authorizer.erc721s, + provider: buildEVMProvider(authorizer.chainProvider.url, authorizer.chainId), + RequesterAuthorizerWithErc721Address: authorizer.contracts.RequesterAuthorizerWithErc721, + }; + const result = await authorizations.fetch(currentState.requests.apiCalls, fetchOptions); + return result; + }); + break; + } + + const responses = await Promise.all(promises); + const logs = flatMap(responses, (r) => r[0]); + const authorizationStatuses = responses.map((r) => r[1]); + + return { id: id, data: authorizationStatuses, logs }; } async function fetchTransactionCounts(currentState: ProviderState) { @@ -144,8 +191,10 @@ export async function initializeProvider( // NOTE: None of these promises can fail otherwise Promise.all will reject const authAndTxCountPromises: readonly ParallelPromise[] = [ fetchSameChainAuthorizations(state5), + fetchSameChainErc721Authorizations(state5), fetchTransactionCounts(state5), - fetchCrossChainAuthorizations(state5), + fetchCrossChainAuthorizations(state5, 'crossChainAuthorizations'), + fetchCrossChainAuthorizations(state5, 'erc721CrossChainAuthorizations'), ]; const authAndTxResults = await Promise.all(authAndTxCountPromises); @@ -159,14 +208,24 @@ export async function initializeProvider( const crossAuthRes = authAndTxResults.find((r) => r.id === 'crossChainAuthorizations')!; logger.logPending(crossAuthRes.logs); + const erc721AuthRes = authAndTxResults.find((r) => r.id === 'erc721Authorizations')!; + logger.logPending(erc721AuthRes.logs); + + const erc721CrossAuthRes = authAndTxResults.find((r) => r.id === 'erc721CrossChainAuthorizations')!; + logger.logPending(erc721CrossAuthRes.logs); + const transactionCountsBySponsorAddress = txCountRes.data!; // Merge authorization statuses const authorizationsByRequestId: AuthorizationByRequestId = authRes.data!; const crossAuthorizationsByRequestId: AuthorizationByRequestId[] = crossAuthRes.data!; + const erc721AuthorizationsByRequestId: AuthorizationByRequestId[] = erc721AuthRes.data!; + const erc721crossAuthorizationsByRequestId: AuthorizationByRequestId[] = erc721CrossAuthRes.data!; const mergedAuthorizationsByRequestId = mergeAuthorizationsByRequestId([ authorizationsByRequestId, ...crossAuthorizationsByRequestId, + ...erc721AuthorizationsByRequestId, + ...erc721crossAuthorizationsByRequestId, ]); const state6 = state.update(state5, { transactionCountsBySponsorAddress }); diff --git a/packages/airnode-node/src/providers/actions.test.ts b/packages/airnode-node/src/providers/actions.test.ts index 8a108a085f..cf6f6a8118 100644 --- a/packages/airnode-node/src/providers/actions.test.ts +++ b/packages/airnode-node/src/providers/actions.test.ts @@ -43,6 +43,8 @@ const chains: ChainConfig[] = [ authorizers: { requesterEndpointAuthorizers: [ethers.constants.AddressZero], crossChainRequesterAuthorizers: [], + requesterAuthorizersWithErc721: [], + crossChainRequesterAuthorizersWithErc721: [], }, authorizations: { requesterEndpointAuthorizations: {}, @@ -72,7 +74,12 @@ const chains: ChainConfig[] = [ }, }, { - authorizers: { requesterEndpointAuthorizers: [ethers.constants.AddressZero], crossChainRequesterAuthorizers: [] }, + authorizers: { + requesterEndpointAuthorizers: [ethers.constants.AddressZero], + crossChainRequesterAuthorizers: [], + requesterAuthorizersWithErc721: [], + crossChainRequesterAuthorizersWithErc721: [], + }, authorizations: { requesterEndpointAuthorizations: {}, }, @@ -131,6 +138,8 @@ describe('initialize', () => { authorizers: { requesterEndpointAuthorizers: [ethers.constants.AddressZero], crossChainRequesterAuthorizers: [], + requesterAuthorizersWithErc721: [], + crossChainRequesterAuthorizersWithErc721: [], }, authorizations: { requesterEndpointAuthorizations: {}, @@ -186,6 +195,8 @@ describe('initialize', () => { authorizers: { requesterEndpointAuthorizers: [ethers.constants.AddressZero], crossChainRequesterAuthorizers: [], + requesterAuthorizersWithErc721: [], + crossChainRequesterAuthorizersWithErc721: [], }, authorizations: { requesterEndpointAuthorizations: {}, diff --git a/packages/airnode-node/src/providers/state.test.ts b/packages/airnode-node/src/providers/state.test.ts index ad4db6ef75..09176ed2a1 100644 --- a/packages/airnode-node/src/providers/state.test.ts +++ b/packages/airnode-node/src/providers/state.test.ts @@ -22,6 +22,8 @@ describe('create', () => { authorizers: { requesterEndpointAuthorizers: [ethers.constants.AddressZero], crossChainRequesterAuthorizers: [], + requesterAuthorizersWithErc721: [], + crossChainRequesterAuthorizersWithErc721: [], }, authorizations: { requesterEndpointAuthorizations: {}, @@ -61,6 +63,8 @@ describe('create', () => { authorizers: { requesterEndpointAuthorizers: [ethers.constants.AddressZero], crossChainRequesterAuthorizers: [], + requesterAuthorizersWithErc721: [], + crossChainRequesterAuthorizersWithErc721: [], }, authorizations: { requesterEndpointAuthorizations: {}, @@ -116,7 +120,12 @@ describe('create', () => { const airnodeAddress = '0xA30CA71Ba54E83127214D3271aEA8F5D6bD4Dace'; const chainConfig: ChainConfig = { maxConcurrency: 100, - authorizers: { requesterEndpointAuthorizers: [ethers.constants.AddressZero], crossChainRequesterAuthorizers: [] }, + authorizers: { + requesterEndpointAuthorizers: [ethers.constants.AddressZero], + crossChainRequesterAuthorizers: [], + requesterAuthorizersWithErc721: [], + crossChainRequesterAuthorizersWithErc721: [], + }, authorizations: { requesterEndpointAuthorizations: {}, }, @@ -157,6 +166,8 @@ describe('create', () => { authorizers: { requesterEndpointAuthorizers: [ethers.constants.AddressZero], crossChainRequesterAuthorizers: [], + requesterAuthorizersWithErc721: [], + crossChainRequesterAuthorizersWithErc721: [], }, authorizations: { requesterEndpointAuthorizations: {}, diff --git a/packages/airnode-node/test/fixtures/config/config.ts b/packages/airnode-node/test/fixtures/config/config.ts index 3efdc66976..741198f741 100644 --- a/packages/airnode-node/test/fixtures/config/config.ts +++ b/packages/airnode-node/test/fixtures/config/config.ts @@ -39,6 +39,8 @@ export function buildConfig(overrides?: Partial): Config { authorizers: { requesterEndpointAuthorizers: [], crossChainRequesterAuthorizers: [], + requesterAuthorizersWithErc721: [], + crossChainRequesterAuthorizersWithErc721: [], }, authorizations: { requesterEndpointAuthorizations: {}, diff --git a/packages/airnode-node/test/fixtures/operation/deploy-config.ts b/packages/airnode-node/test/fixtures/operation/deploy-config.ts index 0cac664e8e..3c2160767e 100644 --- a/packages/airnode-node/test/fixtures/operation/deploy-config.ts +++ b/packages/airnode-node/test/fixtures/operation/deploy-config.ts @@ -9,6 +9,8 @@ export function buildDeployConfig(mnemonic: string, config?: Partial): C authorizers: { requesterEndpointAuthorizers: [], crossChainRequesterAuthorizers: [], + requesterAuthorizersWithErc721: [], + crossChainRequesterAuthorizersWithErc721: [], }, authorizations: { requesterEndpointAuthorizations: {}, diff --git a/packages/airnode-node/test/fixtures/provider-states/evm.ts b/packages/airnode-node/test/fixtures/provider-states/evm.ts index a112f9276f..926e35d8f9 100644 --- a/packages/airnode-node/test/fixtures/provider-states/evm.ts +++ b/packages/airnode-node/test/fixtures/provider-states/evm.ts @@ -16,6 +16,8 @@ export function buildEVMProviderState( authorizers: { requesterEndpointAuthorizers: [], crossChainRequesterAuthorizers: [], + requesterAuthorizersWithErc721: [], + crossChainRequesterAuthorizersWithErc721: [], }, authorizations: { requesterEndpointAuthorizations: {}, diff --git a/packages/airnode-node/test/setup/e2e/utils.ts b/packages/airnode-node/test/setup/e2e/utils.ts index 18c67725fa..5487a7c484 100644 --- a/packages/airnode-node/test/setup/e2e/utils.ts +++ b/packages/airnode-node/test/setup/e2e/utils.ts @@ -18,6 +18,8 @@ export function buildChainConfig(contracts: Contracts): ChainConfig { authorizers: { requesterEndpointAuthorizers: [], crossChainRequesterAuthorizers: [], + requesterAuthorizersWithErc721: [], + crossChainRequesterAuthorizersWithErc721: [], }, authorizations: { requesterEndpointAuthorizations: {}, diff --git a/packages/airnode-operation/src/config/evm-dev-config.json b/packages/airnode-operation/src/config/evm-dev-config.json index 65906af883..4ee6207cb9 100644 --- a/packages/airnode-operation/src/config/evm-dev-config.json +++ b/packages/airnode-operation/src/config/evm-dev-config.json @@ -5,7 +5,9 @@ "mnemonic": "achieve climb couple wait accident symbol spy blouse reduce foil echo label", "authorizers": { "requesterEndpointAuthorizers": [], - "crossChainRequesterAuthorizers": [] + "crossChainRequesterAuthorizers": [], + "requesterAuthorizersWithErc721": [], + "crossChainRequesterAuthorizersWithErc721": [] }, "authorizations": { "requesterEndpointAuthorizations": {} diff --git a/packages/airnode-operation/src/types.ts b/packages/airnode-operation/src/types.ts index b0f5a3a269..dea4505235 100644 --- a/packages/airnode-operation/src/types.ts +++ b/packages/airnode-operation/src/types.ts @@ -122,8 +122,30 @@ export interface CrossChainConfig { }; } +export interface Erc721CrossChainConfig { + readonly erc721s: string[]; + readonly chainType: string; + readonly chainId: string; + readonly contracts: { + readonly RequesterAuthorizerWithErc721: string; + }; + readonly chainProvider: { + url: string; + }; +} + +export interface Erc721Config { + readonly erc721s: string[]; + readonly RequesterAuthorizerWithErc721: string; +} + export interface ConfigAirnode { - readonly authorizers: { requesterEndpointAuthorizers: string[]; crossChainRequesterAuthorizers: CrossChainConfig[] }; + readonly authorizers: { + requesterEndpointAuthorizers: string[]; + crossChainRequesterAuthorizers: CrossChainConfig[]; + requesterAuthorizersWithErc721: Erc721Config[]; + crossChainRequesterAuthorizersWithErc721: Erc721CrossChainConfig[]; + }; readonly authorizations: { requesterEndpointAuthorizations: { [endpointId: string]: string[] }; }; diff --git a/packages/airnode-protocol/contracts/dev/RequesterAuthorizerWithErc721.sol b/packages/airnode-protocol/contracts/dev/RequesterAuthorizerWithErc721.sol new file mode 100644 index 0000000000..83ddcbe7d3 --- /dev/null +++ b/packages/airnode-protocol/contracts/dev/RequesterAuthorizerWithErc721.sol @@ -0,0 +1,545 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import "@openzeppelin/contracts/metatx/ERC2771Context.sol"; +import "./access-control-registry/AccessControlRegistryAdminned.sol"; +import "./interfaces/IRequesterAuthorizerWithErc721.sol"; +import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; + +/// @title Authorizer contract that users can deposit the ERC721 tokens +/// recognized by the Airnode to receive authorization for the requester contract +/// on the chain +/// @notice For an Airnode to recognize an ERC721 token, it needs to be +/// configured to do so at deploy-time. It can be expected for Airnodes to only +/// recognize the respective NFT keys that their operators have issued, but +/// this is not necessarily true. +/// For an Airnode to serve requesters on a chain, it needs to be configured to +/// do so at deploy-time. +/// Airnodes are allowed to block specific requesters. It can be expected for +/// Airnodes to only do this when the requester is breaking T&C. The tokens +/// that have been deposited to authorize requesters that have been blocked can +/// be revoked, which transfers them to the Airnode account. This can be seen +/// as a staking–slashing mechanism. +contract RequesterAuthorizerWithErc721 is + ERC2771Context, + AccessControlRegistryAdminned, + IRequesterAuthorizerWithErc721 +{ + struct TokenDeposits { + uint256 count; + mapping(address => Deposit) depositorToDeposit; + } + + struct Deposit { + uint256 tokenId; + uint32 withdrawalLeadTime; + uint32 earliestWithdrawalTime; + } + + /// @notice Withdrawal lead time setter role description + string + public constant + override WITHDRAWAL_LEAD_TIME_SETTER_ROLE_DESCRIPTION = + "Withdrawal lead time setter"; + + /// @notice Requester blocker role description + string public constant override REQUESTER_BLOCKER_ROLE_DESCRIPTION = + "Requester blocker"; + + bytes32 private constant WITHDRAWAL_LEAD_TIME_SETTER_ROLE_DESCRIPTION_HASH = + keccak256( + abi.encodePacked(WITHDRAWAL_LEAD_TIME_SETTER_ROLE_DESCRIPTION) + ); + + bytes32 private constant REQUESTER_BLOCKER_ROLE_DESCRIPTION_HASH = + keccak256(abi.encodePacked(REQUESTER_BLOCKER_ROLE_DESCRIPTION)); + + /// @notice Deposits of the token with the address made for the Airnode to + /// authorize the requester address on the chain + mapping(address => mapping(uint256 => mapping(address => mapping(address => TokenDeposits)))) + public + override airnodeToChainIdToRequesterToTokenAddressToTokenDeposits; + + /// @notice Withdrawal lead time of the Airnode. This creates the window of + /// opportunity during which a requester can be blocked for breaking T&C + /// and the respective tokens can be revoked. + /// The withdrawal lead time at deposit-time will apply to a specific + /// deposit. + mapping(address => uint32) public override airnodeToWithdrawalLeadTime; + + /// @notice If the Airnode has blocked the requester on the chain. Tokens + /// deposited to authorize a blocked requester are revocable. + mapping(address => mapping(uint256 => mapping(address => bool))) + public + override airnodeToChainIdToRequesterToBlockStatus; + + /// @param _accessControlRegistry AccessControlRegistry contract address + /// @param _adminRoleDescription Admin role description + constructor( + address _accessControlRegistry, + string memory _adminRoleDescription + ) + ERC2771Context(_accessControlRegistry) + AccessControlRegistryAdminned( + _accessControlRegistry, + _adminRoleDescription + ) + {} + + /// @notice Called by the Airnode or its withdrawal lead time setters to + /// set withdrawal lead time + /// @param airnode Airnode address + /// @param withdrawalLeadTime Withdrawal lead time + function setWithdrawalLeadTime( + address airnode, + uint32 withdrawalLeadTime + ) external override { + require( + airnode == _msgSender() || + IAccessControlRegistry(accessControlRegistry).hasRole( + deriveWithdrawalLeadTimeSetterRole(airnode), + _msgSender() + ), + "Sender cannot set lead time" + ); + require(withdrawalLeadTime <= 30 days, "Lead time too long"); + airnodeToWithdrawalLeadTime[airnode] = withdrawalLeadTime; + emit SetWithdrawalLeadTime(airnode, withdrawalLeadTime, _msgSender()); + } + + /// @notice Called by the Airnode or its requester blockers to set + /// requester block statuses + /// @param airnode Airnode address + /// @param chainId Chain ID + /// @param requester Requester address + /// @param status Block status + function setRequesterBlockStatus( + address airnode, + uint256 chainId, + address requester, + bool status + ) external override { + require( + airnode == _msgSender() || + IAccessControlRegistry(accessControlRegistry).hasRole( + deriveRequesterBlockerRole(airnode), + _msgSender() + ), + "Sender cannot block requester" + ); + require(chainId != 0, "Chain ID zero"); + require(requester != address(0), "Requester address zero"); + airnodeToChainIdToRequesterToBlockStatus[airnode][chainId][ + requester + ] = status; + emit SetRequesterBlockStatus( + airnode, + requester, + chainId, + status, + _msgSender() + ); + } + + /// @notice Called by the ERC721 contract upon `safeTransferFrom()` to this + /// contract to deposit a token to authorize the requester + /// @dev The first argument is the operator, which we do not need + /// @param _from Account from which the token is transferred + /// @param _tokenId Token ID + /// @param _data Airnode address, chain ID and requester address in + /// ABI-encoded form + /// @return `onERC721Received()` function selector + function onERC721Received( + address, + address _from, + uint256 _tokenId, + bytes calldata _data + ) external override returns (bytes4) { + require(_data.length == 96, "Unexpected data length"); + (address airnode, uint256 chainId, address requester) = abi.decode( + _data, + (address, uint256, address) + ); + require(airnode != address(0), "Airnode address zero"); + require(chainId != 0, "Chain ID zero"); + require(requester != address(0), "Requester address zero"); + require( + !airnodeToChainIdToRequesterToBlockStatus[airnode][chainId][ + requester + ], + "Requester blocked" + ); + TokenDeposits + storage tokenDeposits = airnodeToChainIdToRequesterToTokenAddressToTokenDeposits[ + airnode + ][chainId][requester][_msgSender()]; + uint256 tokenDepositCount; + unchecked { + tokenDepositCount = ++tokenDeposits.count; + } + require( + tokenDeposits.depositorToDeposit[_from].earliestWithdrawalTime == 0, + "Token already deposited" + ); + tokenDeposits.depositorToDeposit[_from] = Deposit({ + tokenId: _tokenId, + withdrawalLeadTime: airnodeToWithdrawalLeadTime[airnode], + earliestWithdrawalTime: type(uint32).max + }); + emit DepositedToken( + airnode, + requester, + _from, + chainId, + _msgSender(), + _tokenId, + tokenDepositCount + ); + return this.onERC721Received.selector; + } + + /// @notice Called by a token depositor to update the requester for which + /// they have deposited the token for + /// @dev This is especially useful for not having to wait when the Airnode + /// has set a non-zero withdrawal lead time + /// @param airnode Airnode address + /// @param chainIdPrevious Previous chain ID + /// @param requesterPrevious Previous requester address + /// @param chainIdNext Next chain ID + /// @param requesterNext Next requester address + /// @param token Token address + function updateDepositRequester( + address airnode, + uint256 chainIdPrevious, + address requesterPrevious, + uint256 chainIdNext, + address requesterNext, + address token + ) external override { + require( + !airnodeToChainIdToRequesterToBlockStatus[airnode][chainIdPrevious][ + requesterPrevious + ], + "Previous requester blocked" + ); + require( + !airnodeToChainIdToRequesterToBlockStatus[airnode][chainIdNext][ + requesterNext + ], + "Next requester blocked" + ); + TokenDeposits + storage requesterPreviousTokenDeposits = airnodeToChainIdToRequesterToTokenAddressToTokenDeposits[ + airnode + ][chainIdPrevious][requesterPrevious][token]; + Deposit + storage requesterPreviousDeposit = requesterPreviousTokenDeposits + .depositorToDeposit[_msgSender()]; + require( + requesterPreviousDeposit.earliestWithdrawalTime != 0, + "Token not deposited" + ); + require( + requesterPreviousDeposit.earliestWithdrawalTime == type(uint32).max, + "Withdrawal initiated" + ); + TokenDeposits + storage requesterNextTokenDeposits = airnodeToChainIdToRequesterToTokenAddressToTokenDeposits[ + airnode + ][chainIdNext][requesterNext][token]; + require( + requesterNextTokenDeposits + .depositorToDeposit[_msgSender()] + .earliestWithdrawalTime == 0, + "Token already deposited" + ); + uint256 requesterNextTokenDepositCount = ++requesterNextTokenDeposits + .count; + requesterNextTokenDeposits.count = requesterNextTokenDepositCount; + uint256 requesterPreviousTokenDepositCount = --requesterPreviousTokenDeposits + .count; + requesterPreviousTokenDeposits + .count = requesterPreviousTokenDepositCount; + uint256 tokenId = requesterPreviousDeposit.tokenId; + requesterNextTokenDeposits.depositorToDeposit[_msgSender()] = Deposit({ + tokenId: tokenId, + withdrawalLeadTime: requesterPreviousDeposit.withdrawalLeadTime, + earliestWithdrawalTime: 0 + }); + requesterPreviousTokenDeposits.depositorToDeposit[ + _msgSender() + ] = Deposit({ + tokenId: 0, + withdrawalLeadTime: 0, + earliestWithdrawalTime: 0 + }); + emit UpdatedDepositRequesterTo( + airnode, + requesterNext, + _msgSender(), + chainIdNext, + token, + tokenId, + requesterNextTokenDepositCount + ); + emit UpdatedDepositRequesterFrom( + airnode, + requesterPrevious, + _msgSender(), + chainIdPrevious, + token, + tokenId, + requesterPreviousTokenDepositCount + ); + } + + /// @notice Called by a token depositor to initiate withdrawal + /// @dev The depositor is allowed to initiate a withdrawal even if the + /// respective requester is blocked. However, the withdrawal will not be + /// executable as long as the requester is blocked. + /// Token withdrawals can be initiated even if withdrawal lead time is + /// zero. + /// @param airnode Airnode address + /// @param chainId Chain ID + /// @param requester Requester address + /// @param token Token address + /// @return earliestWithdrawalTime Earliest withdrawal time + function initiateTokenWithdrawal( + address airnode, + uint256 chainId, + address requester, + address token + ) external override returns (uint32 earliestWithdrawalTime) { + TokenDeposits + storage tokenDeposits = airnodeToChainIdToRequesterToTokenAddressToTokenDeposits[ + airnode + ][chainId][requester][token]; + Deposit storage deposit = tokenDeposits.depositorToDeposit[ + _msgSender() + ]; + require(deposit.earliestWithdrawalTime != 0, "Token not deposited"); + require( + deposit.earliestWithdrawalTime == type(uint32).max, + "Withdrawal already initiated" + ); + uint256 tokenDepositCount; + unchecked { + tokenDepositCount = --tokenDeposits.count; + } + earliestWithdrawalTime = uint32( + block.timestamp + deposit.withdrawalLeadTime + ); + deposit.earliestWithdrawalTime = earliestWithdrawalTime; + emit InitiatedTokenWithdrawal( + airnode, + requester, + _msgSender(), + chainId, + token, + deposit.tokenId, + earliestWithdrawalTime, + tokenDepositCount + ); + } + + /// @notice Called by a token depositor to withdraw + /// @param airnode Airnode address + /// @param chainId Chain ID + /// @param requester Requester address + /// @param token Token address + function withdrawToken( + address airnode, + uint256 chainId, + address requester, + address token + ) external override { + require( + !airnodeToChainIdToRequesterToBlockStatus[airnode][chainId][ + requester + ], + "Requester blocked" + ); + TokenDeposits + storage tokenDeposits = airnodeToChainIdToRequesterToTokenAddressToTokenDeposits[ + airnode + ][chainId][requester][token]; + Deposit storage deposit = tokenDeposits.depositorToDeposit[ + _msgSender() + ]; + require(deposit.earliestWithdrawalTime != 0, "Token not deposited"); + uint256 tokenDepositCount; + if (deposit.earliestWithdrawalTime == type(uint32).max) { + require( + deposit.withdrawalLeadTime == 0, + "Withdrawal not initiated" + ); + unchecked { + tokenDepositCount = --tokenDeposits.count; + } + } else { + require( + block.timestamp >= deposit.earliestWithdrawalTime, + "Cannot withdraw yet" + ); + unchecked { + tokenDepositCount = tokenDeposits.count; + } + } + uint256 tokenId = deposit.tokenId; + tokenDeposits.depositorToDeposit[_msgSender()] = Deposit({ + tokenId: 0, + withdrawalLeadTime: 0, + earliestWithdrawalTime: 0 + }); + emit WithdrewToken( + airnode, + requester, + _msgSender(), + chainId, + token, + tokenId, + tokenDepositCount + ); + IERC721(token).safeTransferFrom(address(this), _msgSender(), tokenId); + } + + /// @notice Called to revoke the token deposited to authorize a requester + /// that is blocked now + /// @param airnode Airnode address + /// @param chainId Chain ID + /// @param requester Requester address + /// @param token Token address + /// @param depositor Depositor address + function revokeToken( + address airnode, + uint256 chainId, + address requester, + address token, + address depositor + ) external override { + require( + airnodeToChainIdToRequesterToBlockStatus[airnode][chainId][ + requester + ], + "Airnode did not block requester" + ); + TokenDeposits + storage tokenDeposits = airnodeToChainIdToRequesterToTokenAddressToTokenDeposits[ + airnode + ][chainId][requester][token]; + Deposit storage deposit = tokenDeposits.depositorToDeposit[depositor]; + require(deposit.earliestWithdrawalTime != 0, "Token not deposited"); + uint256 tokenDepositCount; + if (deposit.earliestWithdrawalTime == type(uint32).max) { + unchecked { + tokenDepositCount = --tokenDeposits.count; + } + } else { + unchecked { + tokenDepositCount = tokenDeposits.count; + } + } + uint256 tokenId = deposit.tokenId; + tokenDeposits.depositorToDeposit[depositor] = Deposit({ + tokenId: 0, + withdrawalLeadTime: 0, + earliestWithdrawalTime: 0 + }); + emit RevokedToken( + airnode, + requester, + depositor, + chainId, + token, + tokenId, + tokenDepositCount + ); + IERC721(token).safeTransferFrom(address(this), airnode, tokenId); + } + + /// @notice Returns the deposit of the token with the address made by the + /// depositor for the Airnode to authorize the requester address on the + /// chain + /// @param airnode Airnode address + /// @param chainId Chain ID + /// @param requester Requester address + /// @param token Token address + /// @param depositor Depositor address + /// @return tokenId Token ID + /// @return withdrawalLeadTime Withdrawal lead time captured at + /// deposit-time + /// @return earliestWithdrawalTime Earliest withdrawal time + function airnodeToChainIdToRequesterToTokenToDepositorToDeposit( + address airnode, + uint256 chainId, + address requester, + address token, + address depositor + ) + external + view + override + returns ( + uint256 tokenId, + uint32 withdrawalLeadTime, + uint32 earliestWithdrawalTime + ) + { + Deposit + storage deposit = airnodeToChainIdToRequesterToTokenAddressToTokenDeposits[ + airnode + ][chainId][requester][token].depositorToDeposit[depositor]; + (tokenId, withdrawalLeadTime, earliestWithdrawalTime) = ( + deposit.tokenId, + deposit.withdrawalLeadTime, + deposit.earliestWithdrawalTime + ); + } + + /// @notice Returns if the requester on the chain is authorized for the + /// Airnode due to a token with the address being deposited + /// @param airnode Airnode address + /// @param chainId Chain ID + /// @param requester Requester address + /// @param token Token address + /// @return Authorization status + function isAuthorized( + address airnode, + uint256 chainId, + address requester, + address token + ) external view override returns (bool) { + return + !airnodeToChainIdToRequesterToBlockStatus[airnode][chainId][ + requester + ] && + airnodeToChainIdToRequesterToTokenAddressToTokenDeposits[airnode][ + chainId + ][requester][token].count > + 0; + } + + /// @notice Derives the withdrawal lead time setter role for the Airnode + /// @param airnode Airnode address + /// @return withdrawalLeadTimeSetterRole Withdrawal lead time setter role + function deriveWithdrawalLeadTimeSetterRole( + address airnode + ) public view override returns (bytes32 withdrawalLeadTimeSetterRole) { + withdrawalLeadTimeSetterRole = _deriveRole( + _deriveAdminRole(airnode), + WITHDRAWAL_LEAD_TIME_SETTER_ROLE_DESCRIPTION_HASH + ); + } + + /// @notice Derives the requester blocker role for the Airnode + /// @param airnode Airnode address + /// @return requesterBlockerRole Requester blocker role + function deriveRequesterBlockerRole( + address airnode + ) public view override returns (bytes32 requesterBlockerRole) { + requesterBlockerRole = _deriveRole( + _deriveAdminRole(airnode), + REQUESTER_BLOCKER_ROLE_DESCRIPTION_HASH + ); + } +} diff --git a/packages/airnode-protocol/contracts/dev/access-control-registry/AccessControlRegistryAdminned.sol b/packages/airnode-protocol/contracts/dev/access-control-registry/AccessControlRegistryAdminned.sol new file mode 100644 index 0000000000..aa519cfcd7 --- /dev/null +++ b/packages/airnode-protocol/contracts/dev/access-control-registry/AccessControlRegistryAdminned.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/utils/Multicall.sol"; +import "./RoleDeriver.sol"; +import "./AccessControlRegistryUser.sol"; +import "./interfaces/IAccessControlRegistryAdminned.sol"; + +/// @title Contract to be inherited by contracts whose adminship functionality +/// will be implemented using AccessControlRegistry +contract AccessControlRegistryAdminned is + Multicall, + RoleDeriver, + AccessControlRegistryUser, + IAccessControlRegistryAdminned +{ + /// @notice Admin role description + string public override adminRoleDescription; + + bytes32 internal immutable adminRoleDescriptionHash; + + /// @dev Contracts deployed with the same admin role descriptions will have + /// the same roles, meaning that granting an account a role will authorize + /// it in multiple contracts. Unless you want your deployed contract to + /// share the role configuration of another contract, use a unique admin + /// role description. + /// @param _accessControlRegistry AccessControlRegistry contract address + /// @param _adminRoleDescription Admin role description + constructor( + address _accessControlRegistry, + string memory _adminRoleDescription + ) AccessControlRegistryUser(_accessControlRegistry) { + require( + bytes(_adminRoleDescription).length > 0, + "Admin role description empty" + ); + adminRoleDescription = _adminRoleDescription; + adminRoleDescriptionHash = keccak256( + abi.encodePacked(_adminRoleDescription) + ); + } + + /// @notice Derives the admin role for the specific manager address + /// @param manager Manager address + /// @return adminRole Admin role + function _deriveAdminRole(address manager) + internal + view + returns (bytes32 adminRole) + { + adminRole = _deriveRole( + _deriveRootRole(manager), + adminRoleDescriptionHash + ); + } +} diff --git a/packages/airnode-protocol/contracts/dev/access-control-registry/AccessControlRegistryUser.sol b/packages/airnode-protocol/contracts/dev/access-control-registry/AccessControlRegistryUser.sol new file mode 100644 index 0000000000..c9ff2254ef --- /dev/null +++ b/packages/airnode-protocol/contracts/dev/access-control-registry/AccessControlRegistryUser.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "./interfaces/IAccessControlRegistry.sol"; +import "./interfaces/IAccessControlRegistryUser.sol"; + +/// @title Contract to be inherited by contracts that will interact with +/// AccessControlRegistry +contract AccessControlRegistryUser is IAccessControlRegistryUser { + /// @notice AccessControlRegistry contract address + address public immutable override accessControlRegistry; + + /// @param _accessControlRegistry AccessControlRegistry contract address + constructor(address _accessControlRegistry) { + require(_accessControlRegistry != address(0), "ACR address zero"); + accessControlRegistry = _accessControlRegistry; + } +} diff --git a/packages/airnode-protocol/contracts/dev/access-control-registry/RoleDeriver.sol b/packages/airnode-protocol/contracts/dev/access-control-registry/RoleDeriver.sol new file mode 100644 index 0000000000..58964fe7d7 --- /dev/null +++ b/packages/airnode-protocol/contracts/dev/access-control-registry/RoleDeriver.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/// @title Contract to be inherited by contracts that will derive +/// AccessControlRegistry roles +/// @notice If a contract interfaces with AccessControlRegistry and needs to +/// derive roles, it should inherit this contract instead of re-implementing +/// the logic +contract RoleDeriver { + /// @notice Derives the root role of the manager + /// @param manager Manager address + /// @return rootRole Root role + function _deriveRootRole(address manager) + internal + pure + returns (bytes32 rootRole) + { + rootRole = keccak256(abi.encodePacked(manager)); + } + + /// @notice Derives the role using its admin role and description + /// @dev This implies that roles adminned by the same role cannot have the + /// same description + /// @param adminRole Admin role + /// @param description Human-readable description of the role + /// @return role Role + function _deriveRole(bytes32 adminRole, string memory description) + internal + pure + returns (bytes32 role) + { + role = _deriveRole(adminRole, keccak256(abi.encodePacked(description))); + } + + /// @notice Derives the role using its admin role and description hash + /// @dev This implies that roles adminned by the same role cannot have the + /// same description + /// @param adminRole Admin role + /// @param descriptionHash Hash of the human-readable description of the + /// role + /// @return role Role + function _deriveRole(bytes32 adminRole, bytes32 descriptionHash) + internal + pure + returns (bytes32 role) + { + role = keccak256(abi.encodePacked(adminRole, descriptionHash)); + } +} diff --git a/packages/airnode-protocol/contracts/dev/access-control-registry/interfaces/IAccessControlRegistry.sol b/packages/airnode-protocol/contracts/dev/access-control-registry/interfaces/IAccessControlRegistry.sol new file mode 100644 index 0000000000..3b999f91f6 --- /dev/null +++ b/packages/airnode-protocol/contracts/dev/access-control-registry/interfaces/IAccessControlRegistry.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/access/IAccessControl.sol"; + +interface IAccessControlRegistry is IAccessControl { + event InitializedManager(bytes32 indexed rootRole, address indexed manager); + + event InitializedRole( + bytes32 indexed role, + bytes32 indexed adminRole, + string description, + address sender + ); + + function initializeManager(address manager) external; + + function initializeRoleAndGrantToSender( + bytes32 adminRole, + string calldata description + ) external returns (bytes32 role); + + function deriveRootRole(address manager) + external + pure + returns (bytes32 rootRole); + + function deriveRole(bytes32 adminRole, string calldata description) + external + pure + returns (bytes32 role); +} diff --git a/packages/airnode-protocol/contracts/dev/access-control-registry/interfaces/IAccessControlRegistryAdminned.sol b/packages/airnode-protocol/contracts/dev/access-control-registry/interfaces/IAccessControlRegistryAdminned.sol new file mode 100644 index 0000000000..2afc3a541b --- /dev/null +++ b/packages/airnode-protocol/contracts/dev/access-control-registry/interfaces/IAccessControlRegistryAdminned.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "./IAccessControlRegistryUser.sol"; + +interface IAccessControlRegistryAdminned is IAccessControlRegistryUser { + function adminRoleDescription() external view returns (string memory); +} diff --git a/packages/airnode-protocol/contracts/dev/access-control-registry/interfaces/IAccessControlRegistryUser.sol b/packages/airnode-protocol/contracts/dev/access-control-registry/interfaces/IAccessControlRegistryUser.sol new file mode 100644 index 0000000000..a194e1ac6f --- /dev/null +++ b/packages/airnode-protocol/contracts/dev/access-control-registry/interfaces/IAccessControlRegistryUser.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IAccessControlRegistryUser { + function accessControlRegistry() external view returns (address); +} diff --git a/packages/airnode-protocol/contracts/dev/interfaces/IRequesterAuthorizerWithErc721.sol b/packages/airnode-protocol/contracts/dev/interfaces/IRequesterAuthorizerWithErc721.sol new file mode 100644 index 0000000000..053e16e024 --- /dev/null +++ b/packages/airnode-protocol/contracts/dev/interfaces/IRequesterAuthorizerWithErc721.sol @@ -0,0 +1,187 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; +import "../access-control-registry/interfaces/IAccessControlRegistryAdminned.sol"; + +interface IRequesterAuthorizerWithErc721 is + IERC721Receiver, + IAccessControlRegistryAdminned +{ + event SetWithdrawalLeadTime( + address indexed airnode, + uint32 withdrawalLeadTime, + address sender + ); + + event SetRequesterBlockStatus( + address indexed airnode, + address indexed requester, + uint256 chainId, + bool status, + address sender + ); + + event DepositedToken( + address indexed airnode, + address indexed requester, + address indexed depositor, + uint256 chainId, + address token, + uint256 tokenId, + uint256 tokenDepositCount + ); + + event UpdatedDepositRequesterFrom( + address indexed airnode, + address indexed requester, + address indexed depositor, + uint256 chainId, + address token, + uint256 tokenId, + uint256 tokenDepositCount + ); + + event UpdatedDepositRequesterTo( + address indexed airnode, + address indexed requester, + address indexed depositor, + uint256 chainId, + address token, + uint256 tokenId, + uint256 tokenDepositCount + ); + + event InitiatedTokenWithdrawal( + address indexed airnode, + address indexed requester, + address indexed depositor, + uint256 chainId, + address token, + uint256 tokenId, + uint32 earliestWithdrawalTime, + uint256 tokenDepositCount + ); + + event WithdrewToken( + address indexed airnode, + address indexed requester, + address indexed depositor, + uint256 chainId, + address token, + uint256 tokenId, + uint256 tokenDepositCount + ); + + event RevokedToken( + address indexed airnode, + address indexed requester, + address indexed depositor, + uint256 chainId, + address token, + uint256 tokenId, + uint256 tokenDepositCount + ); + + function setWithdrawalLeadTime( + address airnode, + uint32 withdrawalLeadTime + ) external; + + function setRequesterBlockStatus( + address airnode, + uint256 chainId, + address requester, + bool status + ) external; + + function updateDepositRequester( + address airnode, + uint256 chainIdPrevious, + address requesterPrevious, + uint256 chainIdNext, + address requesterNext, + address token + ) external; + + function initiateTokenWithdrawal( + address airnode, + uint256 chainId, + address requester, + address token + ) external returns (uint32 earliestWithdrawalTime); + + function withdrawToken( + address airnode, + uint256 chainId, + address requester, + address token + ) external; + + function revokeToken( + address airnode, + uint256 chainId, + address requester, + address token, + address depositor + ) external; + + function airnodeToChainIdToRequesterToTokenToDepositorToDeposit( + address airnode, + uint256 chainId, + address requester, + address token, + address depositor + ) + external + view + returns ( + uint256 tokenId, + uint32 withdrawalLeadTime, + uint32 earliestWithdrawalTime + ); + + function isAuthorized( + address airnode, + uint256 chainId, + address requester, + address token + ) external view returns (bool); + + function deriveWithdrawalLeadTimeSetterRole( + address airnode + ) external view returns (bytes32 withdrawalLeadTimeSetterRole); + + function deriveRequesterBlockerRole( + address airnode + ) external view returns (bytes32 requesterBlockerRole); + + // solhint-disable-next-line func-name-mixedcase + function WITHDRAWAL_LEAD_TIME_SETTER_ROLE_DESCRIPTION() + external + view + returns (string memory); + + // solhint-disable-next-line func-name-mixedcase + function REQUESTER_BLOCKER_ROLE_DESCRIPTION() + external + view + returns (string memory); + + function airnodeToChainIdToRequesterToTokenAddressToTokenDeposits( + address airnode, + uint256 chainId, + address requester, + address token + ) external view returns (uint256 tokenDepositCount); + + function airnodeToWithdrawalLeadTime( + address airnode + ) external view returns (uint32 withdrawalLeadTime); + + function airnodeToChainIdToRequesterToBlockStatus( + address airnode, + uint256 chainId, + address requester + ) external view returns (bool isBlocked); +} diff --git a/packages/airnode-protocol/hardhat.config.js b/packages/airnode-protocol/hardhat.config.js index 1f22a88209..3cc7cc85c6 100644 --- a/packages/airnode-protocol/hardhat.config.js +++ b/packages/airnode-protocol/hardhat.config.js @@ -22,11 +22,26 @@ module.exports = { tests: process.env.EXTENDED_TEST ? './extended-test' : './test', }, solidity: { - version: '0.8.9', - settings: { - optimizer: { - enabled: true, - runs: 1000, + compilers: [ + { + version: '0.8.9', + settings: { + optimizer: { + enabled: true, + runs: 1000, + }, + }, + }, + ], + overrides: { + 'contracts/dev/RequesterAuthorizerWithErc721.sol': { + version: '0.8.17', + settings: { + optimizer: { + enabled: true, + runs: 200, + }, + }, }, }, }, diff --git a/packages/airnode-protocol/src/index.ts b/packages/airnode-protocol/src/index.ts index 904d444549..b9a4d05cea 100644 --- a/packages/airnode-protocol/src/index.ts +++ b/packages/airnode-protocol/src/index.ts @@ -17,6 +17,7 @@ import { AccessControlRegistry__factory as AccessControlRegistryFactory, RequesterAuthorizerWithAirnode__factory as RequesterAuthorizerWithAirnodeFactory, RrpBeaconServerV0__factory as RrpBeaconServerV0Factory, + RequesterAuthorizerWithErc721__factory as RequesterAuthorizerWithErc721Factory, } from './contracts'; import references from '../deployments/references.json'; @@ -49,6 +50,7 @@ export { AirnodeRrpV0Factory, AccessControlRegistryFactory, RrpBeaconServerV0Factory, + RequesterAuthorizerWithErc721Factory, mocks, authorizers, networks, @@ -60,6 +62,7 @@ export type { MockRrpRequesterV0, AccessControlRegistry, RequesterAuthorizerWithAirnode, + RequesterAuthorizerWithErc721, RrpBeaconServerV0, } from './contracts'; diff --git a/packages/airnode-validator/src/config/config.test.ts b/packages/airnode-validator/src/config/config.test.ts index a1c5b41073..38cd8016f3 100644 --- a/packages/airnode-validator/src/config/config.test.ts +++ b/packages/airnode-validator/src/config/config.test.ts @@ -623,7 +623,7 @@ describe('chainConfigSchema', () => { }); describe('authorizers', () => { - it('allows cross-chain and same-chain authorizers', () => { + it('allows simultaneous authorizers', () => { const config = JSON.parse( readFileSync(join(__dirname, '../../test/fixtures/interpolated-config.valid.json')).toString() ); @@ -644,12 +644,31 @@ describe('authorizers', () => { }, }, ], + requesterAuthorizersWithErc721: [ + { + erc721s: ['0x00bDB2315678afecb367f032d93F642f64180a00'], + RequesterAuthorizerWithErc721: '0x999DB2315678afecb367f032d93F642f64180aa9', + }, + ], + crossChainRequesterAuthorizersWithErc721: [ + { + erc721s: ['0x4abDB2315678afecb367f032d93F642f64180aa7'], + chainType: 'evm', + chainId: '1', + chainProvider: { + url: 'http://some.random.url', + }, + contracts: { + RequesterAuthorizerWithErc721: '0x3FbDB2315678afecb367f032d93F642f64180aa6', + }, + }, + ], }, }; expect(() => chainConfigSchema.parse(validAuthorizersChainConfig)).not.toThrow(); }); - it('allows cross-chain and same-chain authorizers to be empty', () => { + it('allows authorizers to be empty', () => { const config = JSON.parse( readFileSync(join(__dirname, '../../test/fixtures/interpolated-config.valid.json')).toString() ); @@ -658,6 +677,8 @@ describe('authorizers', () => { authorizers: { requesterEndpointAuthorizers: [], crossChainRequesterAuthorizers: [], + requesterAuthorizersWithErc721: [], + crossChainRequesterAuthorizersWithErc721: [], }, }; expect(() => chainConfigSchema.parse(validAuthorizersChainConfig)).not.toThrow(); diff --git a/packages/airnode-validator/src/config/config.ts b/packages/airnode-validator/src/config/config.ts index 5926a2f9d2..2ef0b1e769 100644 --- a/packages/airnode-validator/src/config/config.ts +++ b/packages/airnode-validator/src/config/config.ts @@ -54,7 +54,7 @@ export const logFormatSchema = z.union([z.literal('json'), z.literal('plain')]); export const chainTypeSchema = z.literal('evm'); -export const chainContractsSchema = z +export const airnodeRrpContractSchema = z .object({ AirnodeRrp: evmAddressSchema, }) @@ -153,23 +153,51 @@ export const chainOptionsSchema = z }) .strict(); +// Authorizations export const chainAuthorizationsSchema = z.object({ requesterEndpointAuthorizations: z.record(endpointIdSchema, z.array(evmAddressSchema)), }); +// Authorizers: requesterEndpointAuthorizers export const requesterEndpointAuthorizersSchema = z.array(evmAddressSchema); export const crossChainRequesterAuthorizerSchema = z.object({ - requesterEndpointAuthorizers: requesterEndpointAuthorizersSchema, + requesterEndpointAuthorizers: requesterEndpointAuthorizersSchema.nonempty(), + chainType: chainTypeSchema, + chainId: z.string(), + contracts: airnodeRrpContractSchema, + chainProvider: providerSchema, +}); + +// Authorizers: requesterAuthorizersWithErc721 +export const erc721sSchema = z.array(evmAddressSchema); + +export const requesterAuthorizerWithErc721Schema = z.object({ + erc721s: erc721sSchema.nonempty(), + RequesterAuthorizerWithErc721: evmAddressSchema, +}); + +export const requesterAuthorizersWithErc721Schema = z.array(requesterAuthorizerWithErc721Schema); + +export const requesterAuthorizerWithErc721ContractSchema = z + .object({ + RequesterAuthorizerWithErc721: evmAddressSchema, + }) + .strict(); + +export const crossChainRequesterAuthorizersWithErc721Schema = z.object({ + erc721s: erc721sSchema.nonempty(), chainType: chainTypeSchema, chainId: z.string(), - contracts: chainContractsSchema, + contracts: requesterAuthorizerWithErc721ContractSchema, chainProvider: providerSchema, }); export const chainAuthorizersSchema = z.object({ requesterEndpointAuthorizers: requesterEndpointAuthorizersSchema, crossChainRequesterAuthorizers: z.array(crossChainRequesterAuthorizerSchema), + requesterAuthorizersWithErc721: requesterAuthorizersWithErc721Schema, + crossChainRequesterAuthorizersWithErc721: z.array(crossChainRequesterAuthorizersWithErc721Schema), }); export const maxConcurrencySchema = z.number().int().positive(); @@ -192,7 +220,7 @@ export const chainConfigSchema = z authorizers: chainAuthorizersSchema, authorizations: chainAuthorizationsSchema, blockHistoryLimit: z.number().int().optional(), // Defaults to BLOCK_COUNT_HISTORY_LIMIT defined in airnode-node - contracts: chainContractsSchema, + contracts: airnodeRrpContractSchema, id: z.string(), // Defaults to BLOCK_MIN_CONFIRMATIONS defined in airnode-node but may be overridden // by a requester if the _minConfirmations reserved parameter is configured @@ -470,6 +498,7 @@ export type Gateway = SchemaType; export type ChainAuthorizers = SchemaType; export type CrossChainAuthorizer = SchemaType; export type RequesterEndpointAuthorizers = SchemaType; +export type Erc721s = SchemaType; export type ChainAuthorizations = SchemaType; export type ChainOptions = SchemaType; export type ChainType = SchemaType; diff --git a/packages/airnode-validator/test/fixtures/config.valid.json b/packages/airnode-validator/test/fixtures/config.valid.json index d854315529..561553aede 100644 --- a/packages/airnode-validator/test/fixtures/config.valid.json +++ b/packages/airnode-validator/test/fixtures/config.valid.json @@ -16,6 +16,25 @@ "url": "http://127.0.0.2" } } + ], + "requesterAuthorizersWithErc721": [ + { + "erc721s": ["0x00bDB2315678afecb367f032d93F642f64180a00"], + "RequesterAuthorizerWithErc721": "0x999DB2315678afecb367f032d93F642f64180aa9" + } + ], + "crossChainRequesterAuthorizersWithErc721": [ + { + "erc721s": ["0x3FbDB2315678afecb367f032d93F642f64180aa6"], + "chainType": "evm", + "chainId": "4", + "contracts": { + "RequesterAuthorizerWithErc721": "0x6bbbb2315678afecb367f032d93F642f64180aa4" + }, + "chainProvider": { + "url": "http://127.0.0.2" + } + } ] }, "authorizations": { diff --git a/packages/airnode-validator/test/fixtures/interpolated-config.valid.json b/packages/airnode-validator/test/fixtures/interpolated-config.valid.json index 38ebb25cc6..a962a1576d 100644 --- a/packages/airnode-validator/test/fixtures/interpolated-config.valid.json +++ b/packages/airnode-validator/test/fixtures/interpolated-config.valid.json @@ -16,6 +16,25 @@ "url": "http://127.0.0.2" } } + ], + "requesterAuthorizersWithErc721": [ + { + "erc721s": ["0x00bDB2315678afecb367f032d93F642f64180a00"], + "RequesterAuthorizerWithErc721": "0x999DB2315678afecb367f032d93F642f64180aa9" + } + ], + "crossChainRequesterAuthorizersWithErc721": [ + { + "erc721s": ["0x3FbDB2315678afecb367f032d93F642f64180aa6"], + "chainType": "evm", + "chainId": "4", + "contracts": { + "RequesterAuthorizerWithErc721": "0x6bbbb2315678afecb367f032d93F642f64180aa4" + }, + "chainProvider": { + "url": "http://127.0.0.2" + } + } ] }, "authorizations": { diff --git a/packages/airnode-validator/test/fixtures/invalid-secret-name/config.json b/packages/airnode-validator/test/fixtures/invalid-secret-name/config.json index 219eee7d09..915c99e660 100644 --- a/packages/airnode-validator/test/fixtures/invalid-secret-name/config.json +++ b/packages/airnode-validator/test/fixtures/invalid-secret-name/config.json @@ -4,7 +4,9 @@ "maxConcurrency": 100, "authorizers": { "requesterEndpointAuthorizers": [], - "crossChainRequesterAuthorizers": [] + "crossChainRequesterAuthorizers": [], + "requesterAuthorizersWithErc721": [], + "crossChainRequesterAuthorizersWithErc721": [] }, "authorizations": { "requesterEndpointAuthorizations": {} From 54bffbca9fddc0e6b06bb1406d188bebf1ec5987 Mon Sep 17 00:00:00 2001 From: Derek Croote Date: Sun, 12 Mar 2023 22:21:47 -0700 Subject: [PATCH 02/13] WIP requesterAuthorizerWithErc721 e2e test See TODOs --- .../test/e2e/erc721-authorizers.feature.ts | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 packages/airnode-node/test/e2e/erc721-authorizers.feature.ts diff --git a/packages/airnode-node/test/e2e/erc721-authorizers.feature.ts b/packages/airnode-node/test/e2e/erc721-authorizers.feature.ts new file mode 100644 index 0000000000..6c960172e7 --- /dev/null +++ b/packages/airnode-node/test/e2e/erc721-authorizers.feature.ts @@ -0,0 +1,48 @@ +import { RequesterAuthorizerWithErc721Factory, AccessControlRegistryFactory } from '@api3/airnode-protocol'; +import * as local from '../../src/workers/local-handlers'; +import { operation } from '../fixtures'; +import { fetchAllLogs, deployAirnodeAndMakeRequests, increaseTestTimeout } from '../setup/e2e'; + +increaseTestTimeout(); + +it('deploys a requesterAuthorizerWithErc721 contract and authorizes requests', async () => { + const { deployment, provider } = await deployAirnodeAndMakeRequests(__filename, [operation.buildFullRequest()]); + const deployer = provider.getSigner(); + + const AccessControlRegistry = new AccessControlRegistryFactory(deployer); + const accessControlRegistry = await AccessControlRegistry.deploy(); + await accessControlRegistry.deployed(); + + const RequesterAuthorizersWithErc721 = new RequesterAuthorizerWithErc721Factory(deployer); + const requesterAuthorizersWithErc721 = await RequesterAuthorizersWithErc721.deploy( + accessControlRegistry.address, + 'RequesterAuthorizerWithErc721 admin' + ); + await requesterAuthorizersWithErc721.deployed(); + + // TODO - create ERC721, transfer token to requesterAuthorizersWithErc721 + + const config = local.loadConfig(); + + // Configure authorizers so that only the requesterAuthorizersWithErc721 can authorize + // An empty requesterEndpointAuthorizers array means that no authorizers are required hence the 0x0... + config.chains[0].authorizers.requesterEndpointAuthorizers = ['0x0000000000000000000000000000000000000000']; + config.chains[0].authorizers.crossChainRequesterAuthorizers = []; + config.chains[0].authorizers.crossChainRequesterAuthorizersWithErc721 = []; + config.chains[0].authorizers.requesterAuthorizersWithErc721 = [ + { + erc721s: ['TODOaddressOfERC721above'], + RequesterAuthorizerWithErc721: requesterAuthorizersWithErc721.address, + }, + ]; + + jest.spyOn(local, 'loadConfig').mockReturnValueOnce(config); + const preInvokeExpectedLogs = ['SetSponsorshipStatus', 'SetSponsorshipStatus', 'CreatedTemplate', 'MadeFullRequest']; + + await local.startCoordinator(); + + // FulfilledRequest is absent if the request was not authorized + const postInvokeExpectedLogs = [...preInvokeExpectedLogs, 'FulfilledRequest']; + const postInvokeLogs = await fetchAllLogs(provider, deployment.contracts.AirnodeRrp); + expect(postInvokeLogs.map(({ name }) => name)).toEqual(postInvokeExpectedLogs); +}); From 294f2fcab1fd9ad527dd01e0cae42c61bff2fe01 Mon Sep 17 00:00:00 2001 From: Emanuel Tesar Date: Mon, 13 Mar 2023 19:58:35 +0100 Subject: [PATCH 03/13] Fix implementing e2e test --- .prettierignore | 2 +- .swp | Bin 0 -> 12288 bytes .../test/e2e/erc721-authorizers.feature.ts | 39 +++++++----------- .../airnode-node/test/setup/e2e/deployment.ts | 30 +++++++------- .../src/evm/deploy/deployment.ts | 20 ++++++++- .../airnode-operation/src/evm/deploy/state.ts | 10 +++++ .../src/scripts/evm-dev-deploy.ts | 31 +++++++------- packages/airnode-operation/src/types.ts | 4 ++ .../contracts/authorizers/mock/MockErc721.sol | 12 ++++++ .../MockRequesterAuthorizerWithErc721.sol | 16 +++++++ packages/airnode-protocol/hardhat.config.js | 27 ++++++++++++ packages/airnode-protocol/package.json | 1 + packages/airnode-protocol/src/index.ts | 11 +++++ yarn.lock | 30 ++++++++++++++ 14 files changed, 177 insertions(+), 56 deletions(-) create mode 100644 .swp create mode 100644 packages/airnode-protocol/contracts/authorizers/mock/MockErc721.sol create mode 100644 packages/airnode-protocol/contracts/authorizers/mock/MockRequesterAuthorizerWithErc721.sol diff --git a/.prettierignore b/.prettierignore index 15de7f59b4..ea9692a1fe 100644 --- a/.prettierignore +++ b/.prettierignore @@ -38,4 +38,4 @@ config.json # !!! Temporary until the proper formatting is sorted out !!! # https://github.com/api3dao/airnode/issues/1660 -packages/airnode-protocol/contracts \ No newline at end of file +packages/airnode-protocol/contracts diff --git a/.swp b/.swp new file mode 100644 index 0000000000000000000000000000000000000000..3e0b14d7a439d6c9c762b877b472cc548e00051e GIT binary patch literal 12288 zcmeI2Uysv95Wr3QKwBXdwO?Qh4^FDYPQvNXVNsDHkScLJ@TWYW6WX)K&MMBXySwgj zjZXa({XYE`eeWmf*j~rxFx(-eBBb#{tnAM0%>3q$6~)K1Uw=7d2f@t4>w5LkTz~)N zd9dNKgxrGRx?LZ-KNXN^ky}JRcIJH6lO+J{f_ppWj-{>sG&c;|KQr zch5hWCuEoi5CI}U1c(3;AOb{y2oQn)pMWi{cz>eVFP!Nw-TlhByZcHvB0vO)01+Sp zM1Tko0U|&IhyW2F0z}|5B)}8T`}eBnee)GMkN^L-zyC+ydfq?R-1=u27r9IXhyW2F z0z`laT!w(QOvW0xfoEXsGqC|x+R-@LS_V44y_hePM1wKxE=!=u)#(PL4VoznTiaT>X$0<)k>b+ua_`4GB_?po;)n`+}Rq9 zkD@L;cw`)7&QQT>g)*5iR-e8B{DrWqCUcA@8zFt(vB33W%gx*9bnf*wbhSj+fOfX*x=$Y@x?6^3QY_vlyJgZs|k!M(TrbhQB* z)vzUg7g(l22KK4QB zonzM*eeuDoxpRnqi-du;Ay?S54Xw-%htx(!=ISH0`^T0X}7_V@P0oiN0hR^@RJt4&yuNVsZYgu)nnDu(m@{aF=vP-xgY7dj4%n%9n#u~vY_ afub~Eu$4};a { - const { deployment, provider } = await deployAirnodeAndMakeRequests(__filename, [operation.buildFullRequest()]); - const deployer = provider.getSigner(); - - const AccessControlRegistry = new AccessControlRegistryFactory(deployer); - const accessControlRegistry = await AccessControlRegistry.deploy(); - await accessControlRegistry.deployed(); - - const RequesterAuthorizersWithErc721 = new RequesterAuthorizerWithErc721Factory(deployer); - const requesterAuthorizersWithErc721 = await RequesterAuthorizersWithErc721.deploy( - accessControlRegistry.address, - 'RequesterAuthorizerWithErc721 admin' - ); - await requesterAuthorizersWithErc721.deployed(); - - // TODO - create ERC721, transfer token to requesterAuthorizersWithErc721 - - const config = local.loadConfig(); + const requests = [operation.buildFullRequest()]; + const { provider, deployment } = await deployAirnodeAndMakeRequests(__filename, requests); // Configure authorizers so that only the requesterAuthorizersWithErc721 can authorize // An empty requesterEndpointAuthorizers array means that no authorizers are required hence the 0x0... - config.chains[0].authorizers.requesterEndpointAuthorizers = ['0x0000000000000000000000000000000000000000']; + const erc721Address = deployment.erc721s.MockErc721Factory; + const requesterAuthorizersWithErc721Address = deployment.authorizers.MockRequesterAuthorizerWithErc721Factory; + const config = local.loadConfig(); + config.chains[0].authorizers.requesterEndpointAuthorizers = []; config.chains[0].authorizers.crossChainRequesterAuthorizers = []; config.chains[0].authorizers.crossChainRequesterAuthorizersWithErc721 = []; config.chains[0].authorizers.requesterAuthorizersWithErc721 = [ { - erc721s: ['TODOaddressOfERC721above'], - RequesterAuthorizerWithErc721: requesterAuthorizersWithErc721.address, + erc721s: [erc721Address], + RequesterAuthorizerWithErc721: requesterAuthorizersWithErc721Address, }, ]; - jest.spyOn(local, 'loadConfig').mockReturnValueOnce(config); + const preInvokeExpectedLogs = ['SetSponsorshipStatus', 'SetSponsorshipStatus', 'CreatedTemplate', 'MadeFullRequest']; + const preInvokelogNames = await fetchAllLogNames(provider, deployment.contracts.AirnodeRrp); + expect(preInvokelogNames).toEqual(preInvokeExpectedLogs); await local.startCoordinator(); // FulfilledRequest is absent if the request was not authorized const postInvokeExpectedLogs = [...preInvokeExpectedLogs, 'FulfilledRequest']; - const postInvokeLogs = await fetchAllLogs(provider, deployment.contracts.AirnodeRrp); - expect(postInvokeLogs.map(({ name }) => name)).toEqual(postInvokeExpectedLogs); + const postInvokeLogs = await fetchAllLogNames(provider, deployment.contracts.AirnodeRrp); + expect(postInvokeLogs).toEqual(postInvokeExpectedLogs); }); diff --git a/packages/airnode-node/test/setup/e2e/deployment.ts b/packages/airnode-node/test/setup/e2e/deployment.ts index e9f0d118b3..b0b409be24 100644 --- a/packages/airnode-node/test/setup/e2e/deployment.ts +++ b/packages/airnode-node/test/setup/e2e/deployment.ts @@ -1,31 +1,33 @@ import * as operation from '@api3/airnode-operation'; +// TODO: Why we have a similar function in evm-dev-deploy.ts? export async function deployAirnodeRrp(config: operation.Config): Promise { - const state1 = operation.buildDeployState(config); + let state = operation.buildDeployState(config); // Deploy contracts - const state2 = await operation.deployAirnodeRrp(state1); - const state3 = await operation.deployRequesters(state2); - const state4 = await operation.deployAccessControlRegistry(state3); - const state5 = await operation.deployAuthorizers(state4); + state = await operation.deployAirnodeRrp(state); + state = await operation.deployRequesters(state); + state = await operation.deployAccessControlRegistry(state); + state = await operation.deployAuthorizers(state); + state = await operation.deployErc721s(state); // Assign wallets - const state6 = await operation.assignAirnodeAccounts(state5); - const state7 = await operation.assignRequesterAccounts(state6); - const state8 = await operation.assignSponsorWallets(state7); + state = await operation.assignAirnodeAccounts(state); + state = await operation.assignRequesterAccounts(state); + state = await operation.assignSponsorWallets(state); // Fund wallets - const state9 = await operation.fundAirnodeAccounts(state8); - const state10 = await operation.fundSponsorAccounts(state9); - const state11 = await operation.fundSponsorWallets(state10); + state = await operation.fundAirnodeAccounts(state); + state = await operation.fundSponsorAccounts(state); + state = await operation.fundSponsorWallets(state); // Sponsor requester contracts - const state12 = await operation.sponsorRequesters(state11); + state = await operation.sponsorRequesters(state); // Create templates - const state13 = await operation.createTemplates(state12); + state = await operation.createTemplates(state); - const deployment = operation.buildSaveableDeployment(state13); + const deployment = operation.buildSaveableDeployment(state); return deployment; } diff --git a/packages/airnode-operation/src/evm/deploy/deployment.ts b/packages/airnode-operation/src/evm/deploy/deployment.ts index 8a9a1ba1df..a95a4ef890 100644 --- a/packages/airnode-operation/src/evm/deploy/deployment.ts +++ b/packages/airnode-operation/src/evm/deploy/deployment.ts @@ -1,4 +1,10 @@ -import { AccessControlRegistryFactory, AirnodeRrpV0Factory, authorizers, mocks } from '@api3/airnode-protocol'; +import { + AccessControlRegistryFactory, + AirnodeRrpV0Factory, + authorizers, + mocks, + erc721Mocks, +} from '@api3/airnode-protocol'; import { ethers } from 'ethers'; import { DeployState as State } from '../../types'; @@ -11,6 +17,7 @@ export async function deployAirnodeRrp(state: State): Promise { export async function deployRequesters(state: State): Promise { const requestersByName: { [name: string]: ethers.Contract } = {}; + // TODO: This uses generic mocks exported from Airnode protocol and assumes all of them are requesters for (const [mockName, MockArtifact] of Object.entries(mocks)) { const MockRequester = new MockArtifact(state.deployer); const mockRequester = await MockRequester.deploy(state.contracts.AirnodeRrp!.address); @@ -40,3 +47,14 @@ export async function deployAuthorizers(state: State): Promise { } return { ...state, authorizersByName }; } + +export async function deployErc721s(state: State): Promise { + const erc721sByName: { [name: string]: ethers.Contract } = {}; + for (const [mockName, MockArtifact] of Object.entries(erc721Mocks)) { + const MockErc721 = new MockArtifact(state.deployer); + const mockErc721 = await MockErc721.deploy(); + await mockErc721.deployed(); + erc721sByName[mockName] = mockErc721 as unknown as ethers.Contract; + } + return { ...state, erc721sByName }; +} diff --git a/packages/airnode-operation/src/evm/deploy/state.ts b/packages/airnode-operation/src/evm/deploy/state.ts index c255d7d01f..7ec6cfc3bd 100644 --- a/packages/airnode-operation/src/evm/deploy/state.ts +++ b/packages/airnode-operation/src/evm/deploy/state.ts @@ -25,6 +25,8 @@ export function buildDeployState(config: Config): State { provider, sponsorsById: {}, templatesByName: {}, + erc721sByName: {}, + authorizers: {}, }; } @@ -69,6 +71,12 @@ export function buildSaveableDeployment(state: State): Deployment { return { ...acc, [name]: requester.address }; }, {}); + const erc721Names = Object.keys(state.erc721sByName); + const erc721s: { [name: string]: string } = erc721Names.reduce((acc: any, name: string) => { + const erc721 = state.erc721sByName[name]; + return { ...acc, [name]: erc721.address }; + }, {}); + const sponsors: DeployedSponsor[] = state.config.sponsors.reduce((acc: any, configRequester: ConfigSponsor) => { const sponsor = state.sponsorsById[configRequester.id]; const data = { @@ -90,5 +98,7 @@ export function buildSaveableDeployment(state: State): Deployment { contracts, requesters, sponsors, + erc721s, + authorizers: state.authorizersByName, }; } diff --git a/packages/airnode-operation/src/scripts/evm-dev-deploy.ts b/packages/airnode-operation/src/scripts/evm-dev-deploy.ts index c7f1a0a3cb..fb82e4db2c 100644 --- a/packages/airnode-operation/src/scripts/evm-dev-deploy.ts +++ b/packages/airnode-operation/src/scripts/evm-dev-deploy.ts @@ -6,37 +6,38 @@ async function run() { logger.log('--> Loading configuration...'); const config = io.loadConfig(); - const state1 = deploy.buildDeployState(config); + let state = deploy.buildDeployState(config); logger.log('--> Deploying contracts...'); - const state2 = await deploy.deployAirnodeRrp(state1); - const state3 = await deploy.deployRequesters(state2); - const state4 = await deploy.deployAccessControlRegistry(state3); - const state5 = await deploy.deployAuthorizers(state4); + state = await deploy.deployAirnodeRrp(state); + state = await deploy.deployRequesters(state); + state = await deploy.deployAccessControlRegistry(state); + state = await deploy.deployAuthorizers(state); + state = await deploy.deployErc721s(state); logger.log('--> Assigning wallets...'); - const state6 = await deploy.assignAirnodeAccounts(state5); - const state7 = await deploy.assignRequesterAccounts(state6); - const state8 = await deploy.assignSponsorWallets(state7); + state = await deploy.assignAirnodeAccounts(state); + state = await deploy.assignRequesterAccounts(state); + state = await deploy.assignSponsorWallets(state); logger.log('--> Funding wallets...'); - const state9 = await deploy.fundAirnodeAccounts(state8); - const state10 = await deploy.fundSponsorAccounts(state9); - const state11 = await deploy.fundSponsorWallets(state10); + state = await deploy.fundAirnodeAccounts(state); + state = await deploy.fundSponsorAccounts(state); + state = await deploy.fundSponsorWallets(state); logger.log('--> Sponsoring requester contracts...'); - const state12 = await deploy.sponsorRequesters(state11); + state = await deploy.sponsorRequesters(state); logger.log('--> Creating templates...'); - const state13 = await deploy.createTemplates(state12); + state = await deploy.createTemplates(state); logger.log('--> Deployment successful!'); logger.log('--> Saving deployment...'); - io.saveDeployment(state13); + io.saveDeployment(state); logger.log('--> Deployment saved!'); - return state13; + return state; } run(); diff --git a/packages/airnode-operation/src/types.ts b/packages/airnode-operation/src/types.ts index dea4505235..59da589a11 100644 --- a/packages/airnode-operation/src/types.ts +++ b/packages/airnode-operation/src/types.ts @@ -9,6 +9,8 @@ export interface DeployState { readonly airnodesByName: { readonly [name: string]: Airnode }; readonly authorizersByName: { readonly [name: string]: string }; readonly requestersByName: { readonly [name: string]: ethers.Contract }; + readonly erc721sByName: { readonly [name: string]: ethers.Contract }; + readonly authorizers: { readonly [name: string]: ethers.Contract }; readonly config: Config; readonly contracts: { readonly AirnodeRrp?: AirnodeRrpV0; @@ -91,6 +93,8 @@ export interface Deployment { readonly AirnodeRrp: string; }; readonly sponsors: DeployedSponsor[]; + readonly erc721s: { readonly [name: string]: string }; + readonly authorizers: { readonly [name: string]: string }; } // =========================================== diff --git a/packages/airnode-protocol/contracts/authorizers/mock/MockErc721.sol b/packages/airnode-protocol/contracts/authorizers/mock/MockErc721.sol new file mode 100644 index 0000000000..5dd06a8ba4 --- /dev/null +++ b/packages/airnode-protocol/contracts/authorizers/mock/MockErc721.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; + +contract MockErc721 is ERC721 { + constructor() ERC721("Token", "TKN") { + for (uint256 tokenId = 0; tokenId < 10; tokenId++) { + _mint(msg.sender, tokenId); + } + } +} diff --git a/packages/airnode-protocol/contracts/authorizers/mock/MockRequesterAuthorizerWithErc721.sol b/packages/airnode-protocol/contracts/authorizers/mock/MockRequesterAuthorizerWithErc721.sol new file mode 100644 index 0000000000..8a81234aa1 --- /dev/null +++ b/packages/airnode-protocol/contracts/authorizers/mock/MockRequesterAuthorizerWithErc721.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import "@api3dao/nft-authorizer/contracts/RequesterAuthorizerWithErc721.sol"; + +contract MockRequesterAuthorizerWithErc721 is RequesterAuthorizerWithErc721 { + constructor( + address _accessControlRegistry, + string memory _adminRoleDescription + ) + RequesterAuthorizerWithErc721( + _accessControlRegistry, + _adminRoleDescription + ) + {} +} diff --git a/packages/airnode-protocol/hardhat.config.js b/packages/airnode-protocol/hardhat.config.js index 3cc7cc85c6..41227da19d 100644 --- a/packages/airnode-protocol/hardhat.config.js +++ b/packages/airnode-protocol/hardhat.config.js @@ -43,6 +43,33 @@ module.exports = { }, }, }, + 'contracts/authorizers/mock/MockRequesterAuthorizerWithErc721.sol': { + version: '0.8.17', + settings: { + optimizer: { + enabled: true, + runs: 200, + }, + }, + }, + 'contracts/authorizers/mock/MockErc721.sol': { + version: '0.8.17', + settings: { + optimizer: { + enabled: true, + runs: 200, + }, + }, + }, + '@api3dao/nft-authorizer/contracts/RequesterAuthorizerWithErc721.sol': { + version: '0.8.17', + settings: { + optimizer: { + enabled: true, + runs: 200, + }, + }, + }, }, }, }; diff --git a/packages/airnode-protocol/package.json b/packages/airnode-protocol/package.json index 31d4b47939..8e61057ad4 100644 --- a/packages/airnode-protocol/package.json +++ b/packages/airnode-protocol/package.json @@ -46,6 +46,7 @@ }, "dependencies": { "@api3/airnode-utilities": "^0.10.0", + "@api3dao/nft-authorizer": "api3dao/nft-authorizer#f4bba58837f6d9d3b28254f3c2764cbe6e3bb88f", "@openzeppelin/contracts": "4.4.2", "ethers": "^5.7.2" } diff --git a/packages/airnode-protocol/src/index.ts b/packages/airnode-protocol/src/index.ts index b9a4d05cea..58e0bc2970 100644 --- a/packages/airnode-protocol/src/index.ts +++ b/packages/airnode-protocol/src/index.ts @@ -18,6 +18,8 @@ import { RequesterAuthorizerWithAirnode__factory as RequesterAuthorizerWithAirnodeFactory, RrpBeaconServerV0__factory as RrpBeaconServerV0Factory, RequesterAuthorizerWithErc721__factory as RequesterAuthorizerWithErc721Factory, + MockRequesterAuthorizerWithErc721__factory as MockRequesterAuthorizerWithErc721Factory, + MockErc721__factory as MockErc721Factory, } from './contracts'; import references from '../deployments/references.json'; @@ -36,11 +38,19 @@ const PROTOCOL_IDS = { AIRKEEPER: '12345', }; +// TODO: This and the mock exports below should be defined in airnode-operations instead and not exported from here +const erc721Mocks = { + MockErc721Factory, +}; + const mocks = { MockRrpRequesterFactory, }; +// TODO: This is also used by airnode-admin, but it uses the RequesterAuthorizerWithAirnodeFactory and we might flatten +// the exports to simplify things (it shouldn't mix real and mock contracts though). const authorizers = { RequesterAuthorizerWithAirnodeFactory, + MockRequesterAuthorizerWithErc721Factory, }; export { @@ -55,6 +65,7 @@ export { authorizers, networks, PROTOCOL_IDS, + erc721Mocks, }; export type { diff --git a/yarn.lock b/yarn.lock index 6d56655810..445640b02a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10,6 +10,13 @@ "@jridgewell/gen-mapping" "^0.1.0" "@jridgewell/trace-mapping" "^0.3.9" +"@api3/airnode-protocol-v1@1.0.0-alpha.0": + version "1.0.0-alpha.0" + resolved "https://registry.npmjs.org/@api3/airnode-protocol-v1/-/airnode-protocol-v1-1.0.0-alpha.0.tgz#aede09391cecc592effe9ca9ba6e88e76698f0d1" + integrity sha512-tTfMEwxgzf+mH7CcdYIDlpJzl+yApzI5W1Qe7NE2gqMJqGTkSpQUAtcW01uNFwR5UihkAsPI3fCAJWm4Vu7fpw== + dependencies: + "@openzeppelin/contracts" "4.8.0" + "@api3/ois@2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@api3/ois/-/ois-2.0.0.tgz#c700d2dfb568ac131e7c8f0581db7a212d35dcb1" @@ -23,6 +30,14 @@ resolved "https://registry.yarnpkg.com/@api3/promise-utils/-/promise-utils-0.3.0.tgz#e7ebf92bfd8c1d39983321fc5445070c51fce176" integrity sha512-fH3CzEcsCQjoX6BZ5M+3yRIXZ2zz4/nFdzKUB4wvn3KjvvzvroHFZrzhbKa4mB9E4AS0xnou1AXhlrnN5Fcy+A== +"@api3dao/nft-authorizer@api3dao/nft-authorizer#f4bba58837f6d9d3b28254f3c2764cbe6e3bb88f": + version "0.1.0" + resolved "https://codeload.github.com/api3dao/nft-authorizer/tar.gz/f4bba58837f6d9d3b28254f3c2764cbe6e3bb88f" + dependencies: + "@api3/airnode-protocol-v1" "1.0.0-alpha.0" + "@openzeppelin/contracts" "4.8.1" + "@openzeppelin/contracts-upgradeable" "4.8.1" + "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a" @@ -2087,11 +2102,26 @@ "@opencensus/core" "^0.0.8" uuid "^3.2.1" +"@openzeppelin/contracts-upgradeable@4.8.1": + version "4.8.1" + resolved "https://registry.npmjs.org/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-4.8.1.tgz#363f7dd08f25f8f77e16d374350c3d6b43340a7a" + integrity sha512-1wTv+20lNiC0R07jyIAbHU7TNHKRwGiTGRfiNnA8jOWjKT98g5OgLpYWOi40Vgpk8SPLA9EvfJAbAeIyVn+7Bw== + "@openzeppelin/contracts@4.4.2": version "4.4.2" resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.4.2.tgz#4e889c9c66e736f7de189a53f8ba5b8d789425c2" integrity sha512-NyJV7sJgoGYqbtNUWgzzOGW4T6rR19FmX1IJgXGdapGPWsuMelGJn9h03nos0iqfforCbCB0iYIR0MtIuIFLLw== +"@openzeppelin/contracts@4.8.0": + version "4.8.0" + resolved "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.8.0.tgz#6854c37df205dd2c056bdfa1b853f5d732109109" + integrity sha512-AGuwhRRL+NaKx73WKRNzeCxOCOCxpaqF+kp8TJ89QzAipSwZy/NoflkWaL9bywXFRhIzXt8j38sfF7KBKCPWLw== + +"@openzeppelin/contracts@4.8.1": + version "4.8.1" + resolved "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.8.1.tgz#709cfc4bbb3ca9f4460d60101f15dac6b7a2d5e4" + integrity sha512-xQ6eUZl+RDyb/FiZe1h+U7qr/f4p/SrTSQcTPH2bjur3C5DbuW/zFgCU/b1P/xcIaEqJep+9ju4xDRi3rmChdQ== + "@parcel/watcher@2.0.4": version "2.0.4" resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.0.4.tgz#f300fef4cc38008ff4b8c29d92588eced3ce014b" From 7529b7098edece6b97500cb9552d1edb32205fca Mon Sep 17 00:00:00 2001 From: Derek Croote Date: Tue, 14 Mar 2023 00:44:28 -0700 Subject: [PATCH 04/13] Replace @api3dao/nft-authorizer import with local contract reference --- .../MockRequesterAuthorizerWithErc721.sol | 2 +- packages/airnode-protocol/hardhat.config.js | 9 ------ packages/airnode-protocol/package.json | 1 - yarn.lock | 30 ------------------- 4 files changed, 1 insertion(+), 41 deletions(-) diff --git a/packages/airnode-protocol/contracts/authorizers/mock/MockRequesterAuthorizerWithErc721.sol b/packages/airnode-protocol/contracts/authorizers/mock/MockRequesterAuthorizerWithErc721.sol index 8a81234aa1..72de10f8db 100644 --- a/packages/airnode-protocol/contracts/authorizers/mock/MockRequesterAuthorizerWithErc721.sol +++ b/packages/airnode-protocol/contracts/authorizers/mock/MockRequesterAuthorizerWithErc721.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.17; -import "@api3dao/nft-authorizer/contracts/RequesterAuthorizerWithErc721.sol"; +import "../../dev/RequesterAuthorizerWithErc721.sol"; contract MockRequesterAuthorizerWithErc721 is RequesterAuthorizerWithErc721 { constructor( diff --git a/packages/airnode-protocol/hardhat.config.js b/packages/airnode-protocol/hardhat.config.js index 41227da19d..8254e9186d 100644 --- a/packages/airnode-protocol/hardhat.config.js +++ b/packages/airnode-protocol/hardhat.config.js @@ -61,15 +61,6 @@ module.exports = { }, }, }, - '@api3dao/nft-authorizer/contracts/RequesterAuthorizerWithErc721.sol': { - version: '0.8.17', - settings: { - optimizer: { - enabled: true, - runs: 200, - }, - }, - }, }, }, }; diff --git a/packages/airnode-protocol/package.json b/packages/airnode-protocol/package.json index 8e61057ad4..31d4b47939 100644 --- a/packages/airnode-protocol/package.json +++ b/packages/airnode-protocol/package.json @@ -46,7 +46,6 @@ }, "dependencies": { "@api3/airnode-utilities": "^0.10.0", - "@api3dao/nft-authorizer": "api3dao/nft-authorizer#f4bba58837f6d9d3b28254f3c2764cbe6e3bb88f", "@openzeppelin/contracts": "4.4.2", "ethers": "^5.7.2" } diff --git a/yarn.lock b/yarn.lock index 445640b02a..6d56655810 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10,13 +10,6 @@ "@jridgewell/gen-mapping" "^0.1.0" "@jridgewell/trace-mapping" "^0.3.9" -"@api3/airnode-protocol-v1@1.0.0-alpha.0": - version "1.0.0-alpha.0" - resolved "https://registry.npmjs.org/@api3/airnode-protocol-v1/-/airnode-protocol-v1-1.0.0-alpha.0.tgz#aede09391cecc592effe9ca9ba6e88e76698f0d1" - integrity sha512-tTfMEwxgzf+mH7CcdYIDlpJzl+yApzI5W1Qe7NE2gqMJqGTkSpQUAtcW01uNFwR5UihkAsPI3fCAJWm4Vu7fpw== - dependencies: - "@openzeppelin/contracts" "4.8.0" - "@api3/ois@2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@api3/ois/-/ois-2.0.0.tgz#c700d2dfb568ac131e7c8f0581db7a212d35dcb1" @@ -30,14 +23,6 @@ resolved "https://registry.yarnpkg.com/@api3/promise-utils/-/promise-utils-0.3.0.tgz#e7ebf92bfd8c1d39983321fc5445070c51fce176" integrity sha512-fH3CzEcsCQjoX6BZ5M+3yRIXZ2zz4/nFdzKUB4wvn3KjvvzvroHFZrzhbKa4mB9E4AS0xnou1AXhlrnN5Fcy+A== -"@api3dao/nft-authorizer@api3dao/nft-authorizer#f4bba58837f6d9d3b28254f3c2764cbe6e3bb88f": - version "0.1.0" - resolved "https://codeload.github.com/api3dao/nft-authorizer/tar.gz/f4bba58837f6d9d3b28254f3c2764cbe6e3bb88f" - dependencies: - "@api3/airnode-protocol-v1" "1.0.0-alpha.0" - "@openzeppelin/contracts" "4.8.1" - "@openzeppelin/contracts-upgradeable" "4.8.1" - "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a" @@ -2102,26 +2087,11 @@ "@opencensus/core" "^0.0.8" uuid "^3.2.1" -"@openzeppelin/contracts-upgradeable@4.8.1": - version "4.8.1" - resolved "https://registry.npmjs.org/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-4.8.1.tgz#363f7dd08f25f8f77e16d374350c3d6b43340a7a" - integrity sha512-1wTv+20lNiC0R07jyIAbHU7TNHKRwGiTGRfiNnA8jOWjKT98g5OgLpYWOi40Vgpk8SPLA9EvfJAbAeIyVn+7Bw== - "@openzeppelin/contracts@4.4.2": version "4.4.2" resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.4.2.tgz#4e889c9c66e736f7de189a53f8ba5b8d789425c2" integrity sha512-NyJV7sJgoGYqbtNUWgzzOGW4T6rR19FmX1IJgXGdapGPWsuMelGJn9h03nos0iqfforCbCB0iYIR0MtIuIFLLw== -"@openzeppelin/contracts@4.8.0": - version "4.8.0" - resolved "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.8.0.tgz#6854c37df205dd2c056bdfa1b853f5d732109109" - integrity sha512-AGuwhRRL+NaKx73WKRNzeCxOCOCxpaqF+kp8TJ89QzAipSwZy/NoflkWaL9bywXFRhIzXt8j38sfF7KBKCPWLw== - -"@openzeppelin/contracts@4.8.1": - version "4.8.1" - resolved "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.8.1.tgz#709cfc4bbb3ca9f4460d60101f15dac6b7a2d5e4" - integrity sha512-xQ6eUZl+RDyb/FiZe1h+U7qr/f4p/SrTSQcTPH2bjur3C5DbuW/zFgCU/b1P/xcIaEqJep+9ju4xDRi3rmChdQ== - "@parcel/watcher@2.0.4": version "2.0.4" resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.0.4.tgz#f300fef4cc38008ff4b8c29d92588eced3ce014b" From beb4484d849e6296e170e5b5f190b20014323935 Mon Sep 17 00:00:00 2001 From: Derek Croote Date: Tue, 14 Mar 2023 00:49:40 -0700 Subject: [PATCH 05/13] Fix authorizers empty arrays behavior Previously requesterEndpointAuthorizers being empty would result in all requests being authorized, independent of whether the other authorizer arrays were nonempty --- .../authorization-application.test.ts | 7 +- .../authorization-application.ts | 6 +- .../authorization-fetching.test.ts | 20 ---- .../authorization/authorization-fetching.ts | 10 -- .../evm/handlers/initialize-provider.test.ts | 3 +- .../src/evm/handlers/initialize-provider.ts | 93 +++++++++++-------- .../test/e2e/erc721-authorizers.feature.ts | 6 +- 7 files changed, 73 insertions(+), 72 deletions(-) diff --git a/packages/airnode-node/src/evm/authorization/authorization-application.test.ts b/packages/airnode-node/src/evm/authorization/authorization-application.test.ts index 498c6029e1..5e337610a6 100644 --- a/packages/airnode-node/src/evm/authorization/authorization-application.test.ts +++ b/packages/airnode-node/src/evm/authorization/authorization-application.test.ts @@ -33,7 +33,12 @@ describe('mergeAuthorizations', () => { const apiCall = fixtures.requests.buildApiCall({ id: '0xapiCallId' }); const authorizationByRequestId = { '0xapiCallId': true }; const [logs, res] = authorization.mergeAuthorizations([apiCall], authorizationByRequestId); - expect(logs).toEqual([]); + expect(logs).toEqual([ + { + level: 'DEBUG', + message: 'Requester:requesterAddress is authorized to access Endpoint ID:endpointId for Request ID:0xapiCallId', + }, + ]); expect(res).toEqual([apiCall]); }); diff --git a/packages/airnode-node/src/evm/authorization/authorization-application.ts b/packages/airnode-node/src/evm/authorization/authorization-application.ts index 55f8341f2a..639f671d59 100644 --- a/packages/airnode-node/src/evm/authorization/authorization-application.ts +++ b/packages/airnode-node/src/evm/authorization/authorization-application.ts @@ -30,7 +30,11 @@ function applyAuthorization( } if (authorized) { - return { ...acc, requests: [...acc.requests, apiCall] }; + const log = logger.pend( + 'DEBUG', + `Requester:${apiCall.requesterAddress} is authorized to access Endpoint ID:${apiCall.endpointId} for Request ID:${apiCall.id}` + ); + return { ...acc, logs: [...acc.logs, log], requests: [...acc.requests, apiCall] }; } // If the request is unauthorized, update drop the request diff --git a/packages/airnode-node/src/evm/authorization/authorization-fetching.test.ts b/packages/airnode-node/src/evm/authorization/authorization-fetching.test.ts index b08dad3e89..5a7591cd14 100644 --- a/packages/airnode-node/src/evm/authorization/authorization-fetching.test.ts +++ b/packages/airnode-node/src/evm/authorization/authorization-fetching.test.ts @@ -41,26 +41,6 @@ describe('fetch (authorizations)', () => { expect(res).toEqual({}); }); - it('returns true for all pending requests if authorizers arrays are empty', async () => { - const apiCalls = Array.from(Array(19).keys()).map((n) => { - return fixtures.requests.buildApiCall({ - id: `${n}`, - endpointId: `endpointId-${n}`, - requesterAddress: `requesterAddress-${n}`, - sponsorAddress: 'sponsorAddress', - }); - }); - const [logs, res] = await authorization.fetch(apiCalls, { - ...mutableFetchOptions, - requesterEndpointAuthorizers: [], - }); - - expect(logs).toEqual([]); - expect(Object.keys(res).length).toEqual(19); - expect(res['0']).toEqual(true); - expect(res['18']).toEqual(true); - }); - it('calls the contract with groups of 10', async () => { checkAuthorizationStatusesMock.mockResolvedValueOnce(Array(10).fill(true)); checkAuthorizationStatusesMock.mockResolvedValueOnce(Array(9).fill(true)); diff --git a/packages/airnode-node/src/evm/authorization/authorization-fetching.ts b/packages/airnode-node/src/evm/authorization/authorization-fetching.ts index e3f6f16bf1..dea03a9be5 100644 --- a/packages/airnode-node/src/evm/authorization/authorization-fetching.ts +++ b/packages/airnode-node/src/evm/authorization/authorization-fetching.ts @@ -216,16 +216,6 @@ export async function fetch( return [[], {}]; } - // If there are no authorizer or ERC721 contracts then endpoint is public - const contracts = - fetchOptions.type === 'airnodeRrp' ? fetchOptions.requesterEndpointAuthorizers : fetchOptions.erc721s; - if (isEmpty(contracts)) { - const authorizationByRequestIds = apiCalls.map((pendingApiCall) => ({ - [pendingApiCall.id]: true, - })); - return [[], Object.assign({}, ...authorizationByRequestIds) as AuthorizationByRequestId]; - } - // Skip fetching authorization statuses if found in config for a specific authorization type // and requester address const configAuthorizationsByRequestId = checkConfigAuthorizations(apiCalls, fetchOptions); diff --git a/packages/airnode-node/src/evm/handlers/initialize-provider.test.ts b/packages/airnode-node/src/evm/handlers/initialize-provider.test.ts index 6a0283eb7b..c27f3fb4a9 100644 --- a/packages/airnode-node/src/evm/handlers/initialize-provider.test.ts +++ b/packages/airnode-node/src/evm/handlers/initialize-provider.test.ts @@ -42,7 +42,8 @@ describe('initializeProvider', () => { const state = fixtures.buildEVMProviderState(); const res = await initializeProvider(state); - expect(fetchAuthorizationsSpy).toHaveBeenCalledTimes(1); + // Empty authorizer arrays short-circuits authorization fetching + expect(fetchAuthorizationsSpy).toHaveBeenCalledTimes(0); expect(res?.requests.apiCalls).toEqual([ { airnodeAddress: '0xA30CA71Ba54E83127214D3271aEA8F5D6bD4Dace', diff --git a/packages/airnode-node/src/evm/handlers/initialize-provider.ts b/packages/airnode-node/src/evm/handlers/initialize-provider.ts index bd3ce936e0..09d89dd936 100644 --- a/packages/airnode-node/src/evm/handlers/initialize-provider.ts +++ b/packages/airnode-node/src/evm/handlers/initialize-provider.ts @@ -1,4 +1,5 @@ import flatMap from 'lodash/flatMap'; +import isEmpty from 'lodash/isEmpty'; import mergeWith from 'lodash/mergeWith'; import { logger, PendingLog } from '@api3/airnode-utilities'; import { go } from '@api3/promise-utils'; @@ -15,16 +16,20 @@ import { AuthorizationByRequestId, EVMProviderState, LogsData, ProviderState } f type ParallelPromise = Promise<{ readonly id: string; readonly data: any; readonly logs: PendingLog[] }>; async function fetchSameChainAuthorizations(currentState: ProviderState) { - const fetchOptions: authorizations.AirnodeRrpFetchOptions = { - type: 'airnodeRrp', - requesterEndpointAuthorizers: currentState.settings.authorizers.requesterEndpointAuthorizers, - authorizations: currentState.settings.authorizations, - airnodeAddress: currentState.settings.airnodeAddress, - airnodeRrpAddress: currentState.contracts.AirnodeRrp, - provider: currentState.provider, - }; - const [logs, res] = await authorizations.fetch(currentState.requests.apiCalls, fetchOptions); - return { id: 'authorizations', data: res, logs }; + if (isEmpty(currentState.settings.authorizers.requesterEndpointAuthorizers)) { + return { id: 'authorizations', data: {}, logs: [] }; + } else { + const fetchOptions: authorizations.AirnodeRrpFetchOptions = { + type: 'airnodeRrp', + requesterEndpointAuthorizers: currentState.settings.authorizers.requesterEndpointAuthorizers, + authorizations: currentState.settings.authorizations, + airnodeAddress: currentState.settings.airnodeAddress, + airnodeRrpAddress: currentState.contracts.AirnodeRrp, + provider: currentState.provider, + }; + const [logs, res] = await authorizations.fetch(currentState.requests.apiCalls, fetchOptions); + return { id: 'authorizations', data: res, logs }; + } } async function fetchSameChainErc721Authorizations(currentState: ProviderState) { @@ -189,44 +194,58 @@ export async function initializeProvider( // STEP 6: Fetch authorizations and transaction counts // ================================================================= // NOTE: None of these promises can fail otherwise Promise.all will reject - const authAndTxCountPromises: readonly ParallelPromise[] = [ - fetchSameChainAuthorizations(state5), - fetchSameChainErc721Authorizations(state5), - fetchTransactionCounts(state5), - fetchCrossChainAuthorizations(state5, 'crossChainAuthorizations'), - fetchCrossChainAuthorizations(state5, 'erc721CrossChainAuthorizations'), - ]; + + // If all authorizers arrays are empty then all requests are authorized + const allAuthorizersEmpty = Object.values(state5.settings.authorizers).every((arr) => isEmpty(arr)); + + const authAndTxCountPromises: readonly ParallelPromise[] = allAuthorizersEmpty + ? [fetchTransactionCounts(state5)] + : [ + fetchTransactionCounts(state5), + fetchSameChainAuthorizations(state5), + fetchSameChainErc721Authorizations(state5), + fetchCrossChainAuthorizations(state5, 'crossChainAuthorizations'), + fetchCrossChainAuthorizations(state5, 'erc721CrossChainAuthorizations'), + ]; const authAndTxResults = await Promise.all(authAndTxCountPromises); // These promises can resolve in any order, so we need to find each one by it's key const txCountRes = authAndTxResults.find((r) => r.id === 'transaction-counts')!; logger.logPending(txCountRes.logs); + const transactionCountsBySponsorAddress = txCountRes.data!; - const authRes = authAndTxResults.find((r) => r.id === 'authorizations')!; - logger.logPending(authRes.logs); + let mergedAuthorizationsByRequestId: AuthorizationByRequestId; + if (allAuthorizersEmpty) { + const authorizationByRequestIds = state5.requests.apiCalls.map((pendingApiCall) => ({ + [pendingApiCall.id]: true, + })); + mergedAuthorizationsByRequestId = Object.assign({}, ...authorizationByRequestIds); + } else { + const authRes = authAndTxResults.find((r) => r.id === 'authorizations')!; + logger.logPending(authRes.logs); - const crossAuthRes = authAndTxResults.find((r) => r.id === 'crossChainAuthorizations')!; - logger.logPending(crossAuthRes.logs); + const crossAuthRes = authAndTxResults.find((r) => r.id === 'crossChainAuthorizations')!; + logger.logPending(crossAuthRes.logs); - const erc721AuthRes = authAndTxResults.find((r) => r.id === 'erc721Authorizations')!; - logger.logPending(erc721AuthRes.logs); + const erc721AuthRes = authAndTxResults.find((r) => r.id === 'erc721Authorizations')!; + logger.logPending(erc721AuthRes.logs); - const erc721CrossAuthRes = authAndTxResults.find((r) => r.id === 'erc721CrossChainAuthorizations')!; - logger.logPending(erc721CrossAuthRes.logs); + const erc721CrossAuthRes = authAndTxResults.find((r) => r.id === 'erc721CrossChainAuthorizations')!; + logger.logPending(erc721CrossAuthRes.logs); - const transactionCountsBySponsorAddress = txCountRes.data!; + // Merge authorization statuses + const authorizationsByRequestId: AuthorizationByRequestId = authRes.data!; + const crossAuthorizationsByRequestId: AuthorizationByRequestId[] = crossAuthRes.data!; + const erc721AuthorizationsByRequestId: AuthorizationByRequestId[] = erc721AuthRes.data!; + const erc721crossAuthorizationsByRequestId: AuthorizationByRequestId[] = erc721CrossAuthRes.data!; + mergedAuthorizationsByRequestId = mergeAuthorizationsByRequestId([ + authorizationsByRequestId, + ...crossAuthorizationsByRequestId, + ...erc721AuthorizationsByRequestId, + ...erc721crossAuthorizationsByRequestId, + ]); + } - // Merge authorization statuses - const authorizationsByRequestId: AuthorizationByRequestId = authRes.data!; - const crossAuthorizationsByRequestId: AuthorizationByRequestId[] = crossAuthRes.data!; - const erc721AuthorizationsByRequestId: AuthorizationByRequestId[] = erc721AuthRes.data!; - const erc721crossAuthorizationsByRequestId: AuthorizationByRequestId[] = erc721CrossAuthRes.data!; - const mergedAuthorizationsByRequestId = mergeAuthorizationsByRequestId([ - authorizationsByRequestId, - ...crossAuthorizationsByRequestId, - ...erc721AuthorizationsByRequestId, - ...erc721crossAuthorizationsByRequestId, - ]); const state6 = state.update(state5, { transactionCountsBySponsorAddress }); // ================================================================= diff --git a/packages/airnode-node/test/e2e/erc721-authorizers.feature.ts b/packages/airnode-node/test/e2e/erc721-authorizers.feature.ts index a1ca2f7bdb..84c9eef5c4 100644 --- a/packages/airnode-node/test/e2e/erc721-authorizers.feature.ts +++ b/packages/airnode-node/test/e2e/erc721-authorizers.feature.ts @@ -8,14 +8,16 @@ it('deploys a requesterAuthorizerWithErc721 contract and authorizes requests', a const requests = [operation.buildFullRequest()]; const { provider, deployment } = await deployAirnodeAndMakeRequests(__filename, requests); - // Configure authorizers so that only the requesterAuthorizersWithErc721 can authorize - // An empty requesterEndpointAuthorizers array means that no authorizers are required hence the 0x0... const erc721Address = deployment.erc721s.MockErc721Factory; const requesterAuthorizersWithErc721Address = deployment.authorizers.MockRequesterAuthorizerWithErc721Factory; + + // TODO - deposit NFT in order to authorize the request - see isAuthorized of RequesterAuthorizerWithErc721.sol + const config = local.loadConfig(); config.chains[0].authorizers.requesterEndpointAuthorizers = []; config.chains[0].authorizers.crossChainRequesterAuthorizers = []; config.chains[0].authorizers.crossChainRequesterAuthorizersWithErc721 = []; + // Since requesterAuthorizersWithErc721 is not empty, only it can authorize config.chains[0].authorizers.requesterAuthorizersWithErc721 = [ { erc721s: [erc721Address], From 7e1de2646925835b011103eac96ed5fc85996982 Mon Sep 17 00:00:00 2001 From: Emanuel Tesar Date: Tue, 14 Mar 2023 14:07:09 +0100 Subject: [PATCH 06/13] Fix e2e test using erc721 authorizer Co-authored-by: Derek Croote --- .../authorization/authorization-fetching.ts | 1 - .../test/e2e/erc721-authorizers.feature.ts | 19 ++++++++++++++++++- .../airnode-node/test/setup/e2e/testing.ts | 2 +- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/packages/airnode-node/src/evm/authorization/authorization-fetching.ts b/packages/airnode-node/src/evm/authorization/authorization-fetching.ts index dea03a9be5..d86d1118a5 100644 --- a/packages/airnode-node/src/evm/authorization/authorization-fetching.ts +++ b/packages/airnode-node/src/evm/authorization/authorization-fetching.ts @@ -184,7 +184,6 @@ async function fetchErc721AuthorizationStatuses( return [[groupLog], null]; } - const decodedMulticall = decodeMulticall(requesterAuthorizerWithErc721, goData.data); const authorizationsById = applyErc721Authorizations(apiCalls, erc721s, decodedMulticall); diff --git a/packages/airnode-node/test/e2e/erc721-authorizers.feature.ts b/packages/airnode-node/test/e2e/erc721-authorizers.feature.ts index 84c9eef5c4..13cccc5dfc 100644 --- a/packages/airnode-node/test/e2e/erc721-authorizers.feature.ts +++ b/packages/airnode-node/test/e2e/erc721-authorizers.feature.ts @@ -1,3 +1,5 @@ +import { erc721Mocks } from '@api3/airnode-protocol'; +import { ethers } from 'ethers'; import * as local from '../../src/workers/local-handlers'; import { operation } from '../fixtures'; import { increaseTestTimeout, deployAirnodeAndMakeRequests, fetchAllLogNames } from '../setup/e2e'; @@ -6,7 +8,22 @@ increaseTestTimeout(); it('deploys a requesterAuthorizerWithErc721 contract and authorizes requests', async () => { const requests = [operation.buildFullRequest()]; - const { provider, deployment } = await deployAirnodeAndMakeRequests(__filename, requests); + const { provider, deployment, deployerIndex, mnemonic } = await deployAirnodeAndMakeRequests(__filename, requests); + + // Send the NFT to the requester + const deployer = provider.getSigner(deployerIndex); + const onERC721ReceivedArguments = ethers.utils.defaultAbiCoder.encode( + ['address', 'uint256', 'address'], + [ethers.Wallet.fromMnemonic(mnemonic).address, 31337, deployment.requesters.MockRrpRequesterFactory] + ); + await erc721Mocks.MockErc721Factory.connect(deployment.erc721s.MockErc721Factory, deployer)[ + 'safeTransferFrom(address,address,uint256,bytes)' + ]( + await deployer.getAddress(), + deployment.authorizers.MockRequesterAuthorizerWithErc721Factory, + 0, + onERC721ReceivedArguments + ); const erc721Address = deployment.erc721s.MockErc721Factory; const requesterAuthorizersWithErc721Address = deployment.authorizers.MockRequesterAuthorizerWithErc721Factory; diff --git a/packages/airnode-node/test/setup/e2e/testing.ts b/packages/airnode-node/test/setup/e2e/testing.ts index bc8aa99b1a..5beefb6e1c 100644 --- a/packages/airnode-node/test/setup/e2e/testing.ts +++ b/packages/airnode-node/test/setup/e2e/testing.ts @@ -35,5 +35,5 @@ export const deployAirnodeAndMakeRequests = async (filename: string, requests?: mockReadFileSync('config.json', JSON.stringify(config)); jest.spyOn(validator, 'unsafeParseConfigWithSecrets').mockReturnValue(config); - return { deployment, provider: buildProvider(), config, mnemonic }; + return { deployment, provider: buildProvider(), config, mnemonic, deployerIndex }; }; From 876f051b67bff5e12367e3a5060d9e1f327abb36 Mon Sep 17 00:00:00 2001 From: Derek Croote Date: Tue, 14 Mar 2023 18:03:57 -0700 Subject: [PATCH 07/13] Remove TODO --- packages/airnode-node/test/e2e/erc721-authorizers.feature.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/airnode-node/test/e2e/erc721-authorizers.feature.ts b/packages/airnode-node/test/e2e/erc721-authorizers.feature.ts index 13cccc5dfc..1df664406f 100644 --- a/packages/airnode-node/test/e2e/erc721-authorizers.feature.ts +++ b/packages/airnode-node/test/e2e/erc721-authorizers.feature.ts @@ -28,8 +28,6 @@ it('deploys a requesterAuthorizerWithErc721 contract and authorizes requests', a const erc721Address = deployment.erc721s.MockErc721Factory; const requesterAuthorizersWithErc721Address = deployment.authorizers.MockRequesterAuthorizerWithErc721Factory; - // TODO - deposit NFT in order to authorize the request - see isAuthorized of RequesterAuthorizerWithErc721.sol - const config = local.loadConfig(); config.chains[0].authorizers.requesterEndpointAuthorizers = []; config.chains[0].authorizers.crossChainRequesterAuthorizers = []; From 8d0bac99132b08fa4fd09c8e3fff32d43950e2fb Mon Sep 17 00:00:00 2001 From: Derek Croote Date: Tue, 14 Mar 2023 21:50:20 -0700 Subject: [PATCH 08/13] Minor cleanup --- .../airnode-node/src/evm/handlers/initialize-provider.ts | 8 ++++---- .../airnode-node/test/e2e/erc721-authorizers.feature.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/airnode-node/src/evm/handlers/initialize-provider.ts b/packages/airnode-node/src/evm/handlers/initialize-provider.ts index 09d89dd936..caadfa23bf 100644 --- a/packages/airnode-node/src/evm/handlers/initialize-provider.ts +++ b/packages/airnode-node/src/evm/handlers/initialize-provider.ts @@ -207,6 +207,7 @@ export async function initializeProvider( fetchCrossChainAuthorizations(state5, 'crossChainAuthorizations'), fetchCrossChainAuthorizations(state5, 'erc721CrossChainAuthorizations'), ]; + const authAndTxResults = await Promise.all(authAndTxCountPromises); // These promises can resolve in any order, so we need to find each one by it's key @@ -216,10 +217,9 @@ export async function initializeProvider( let mergedAuthorizationsByRequestId: AuthorizationByRequestId; if (allAuthorizersEmpty) { - const authorizationByRequestIds = state5.requests.apiCalls.map((pendingApiCall) => ({ - [pendingApiCall.id]: true, - })); - mergedAuthorizationsByRequestId = Object.assign({}, ...authorizationByRequestIds); + mergedAuthorizationsByRequestId = Object.fromEntries( + state5.requests.apiCalls.map((pendingApiCall) => [pendingApiCall.id, true]) + ); } else { const authRes = authAndTxResults.find((r) => r.id === 'authorizations')!; logger.logPending(authRes.logs); diff --git a/packages/airnode-node/test/e2e/erc721-authorizers.feature.ts b/packages/airnode-node/test/e2e/erc721-authorizers.feature.ts index 1df664406f..b7e682b685 100644 --- a/packages/airnode-node/test/e2e/erc721-authorizers.feature.ts +++ b/packages/airnode-node/test/e2e/erc721-authorizers.feature.ts @@ -47,7 +47,7 @@ it('deploys a requesterAuthorizerWithErc721 contract and authorizes requests', a await local.startCoordinator(); - // FulfilledRequest is absent if the request was not authorized + // FulfilledRequest being present indicates success const postInvokeExpectedLogs = [...preInvokeExpectedLogs, 'FulfilledRequest']; const postInvokeLogs = await fetchAllLogNames(provider, deployment.contracts.AirnodeRrp); expect(postInvokeLogs).toEqual(postInvokeExpectedLogs); From f8e7de7dec42d92066bb2dadb88793584d981f4a Mon Sep 17 00:00:00 2001 From: Derek Croote Date: Wed, 15 Mar 2023 21:49:49 -0700 Subject: [PATCH 09/13] Remove confusing comments --- packages/airnode-validator/src/config/config.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/airnode-validator/src/config/config.ts b/packages/airnode-validator/src/config/config.ts index 2ef0b1e769..f7eec806e9 100644 --- a/packages/airnode-validator/src/config/config.ts +++ b/packages/airnode-validator/src/config/config.ts @@ -153,12 +153,10 @@ export const chainOptionsSchema = z }) .strict(); -// Authorizations export const chainAuthorizationsSchema = z.object({ requesterEndpointAuthorizations: z.record(endpointIdSchema, z.array(evmAddressSchema)), }); -// Authorizers: requesterEndpointAuthorizers export const requesterEndpointAuthorizersSchema = z.array(evmAddressSchema); export const crossChainRequesterAuthorizerSchema = z.object({ @@ -169,7 +167,6 @@ export const crossChainRequesterAuthorizerSchema = z.object({ chainProvider: providerSchema, }); -// Authorizers: requesterAuthorizersWithErc721 export const erc721sSchema = z.array(evmAddressSchema); export const requesterAuthorizerWithErc721Schema = z.object({ From b7f0b30127bfdf322f7c32d3f73f62ce94795768 Mon Sep 17 00:00:00 2001 From: Derek Croote Date: Wed, 15 Mar 2023 21:50:26 -0700 Subject: [PATCH 10/13] Remove unnecessary else --- .../src/evm/handlers/initialize-provider.ts | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/airnode-node/src/evm/handlers/initialize-provider.ts b/packages/airnode-node/src/evm/handlers/initialize-provider.ts index caadfa23bf..fd83e39ee5 100644 --- a/packages/airnode-node/src/evm/handlers/initialize-provider.ts +++ b/packages/airnode-node/src/evm/handlers/initialize-provider.ts @@ -18,18 +18,17 @@ type ParallelPromise = Promise<{ readonly id: string; readonly data: any; readon async function fetchSameChainAuthorizations(currentState: ProviderState) { if (isEmpty(currentState.settings.authorizers.requesterEndpointAuthorizers)) { return { id: 'authorizations', data: {}, logs: [] }; - } else { - const fetchOptions: authorizations.AirnodeRrpFetchOptions = { - type: 'airnodeRrp', - requesterEndpointAuthorizers: currentState.settings.authorizers.requesterEndpointAuthorizers, - authorizations: currentState.settings.authorizations, - airnodeAddress: currentState.settings.airnodeAddress, - airnodeRrpAddress: currentState.contracts.AirnodeRrp, - provider: currentState.provider, - }; - const [logs, res] = await authorizations.fetch(currentState.requests.apiCalls, fetchOptions); - return { id: 'authorizations', data: res, logs }; } + const fetchOptions: authorizations.AirnodeRrpFetchOptions = { + type: 'airnodeRrp', + requesterEndpointAuthorizers: currentState.settings.authorizers.requesterEndpointAuthorizers, + authorizations: currentState.settings.authorizations, + airnodeAddress: currentState.settings.airnodeAddress, + airnodeRrpAddress: currentState.contracts.AirnodeRrp, + provider: currentState.provider, + }; + const [logs, res] = await authorizations.fetch(currentState.requests.apiCalls, fetchOptions); + return { id: 'authorizations', data: res, logs }; } async function fetchSameChainErc721Authorizations(currentState: ProviderState) { From 491251ac28e57096a24095f33bef069e731227db Mon Sep 17 00:00:00 2001 From: Derek Croote Date: Wed, 15 Mar 2023 22:17:46 -0700 Subject: [PATCH 11/13] Add INFO log for all authorizer arrays being empty --- packages/airnode-node/src/evm/handlers/initialize-provider.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/airnode-node/src/evm/handlers/initialize-provider.ts b/packages/airnode-node/src/evm/handlers/initialize-provider.ts index fd83e39ee5..593afca5d9 100644 --- a/packages/airnode-node/src/evm/handlers/initialize-provider.ts +++ b/packages/airnode-node/src/evm/handlers/initialize-provider.ts @@ -216,6 +216,7 @@ export async function initializeProvider( let mergedAuthorizationsByRequestId: AuthorizationByRequestId; if (allAuthorizersEmpty) { + logger.info('Authorizing all requests because all authorizer arrays are empty'); mergedAuthorizationsByRequestId = Object.fromEntries( state5.requests.apiCalls.map((pendingApiCall) => [pendingApiCall.id, true]) ); From a5ba56346f0f7482f873a5f037d95df3eab29af6 Mon Sep 17 00:00:00 2001 From: Derek Croote Date: Wed, 15 Mar 2023 23:03:53 -0700 Subject: [PATCH 12/13] Replace MockRequesterAuthorizerWithErc721 with actual deployed contract --- .../test/e2e/erc721-authorizers.feature.ts | 11 +++-------- .../airnode-node/test/setup/e2e/deployment.ts | 7 ++++--- .../src/evm/deploy/deployment.ts | 11 +++++++++++ .../airnode-operation/src/evm/deploy/state.ts | 1 + packages/airnode-operation/src/types.ts | 4 +++- .../mock/MockRequesterAuthorizerWithErc721.sol | 16 ---------------- packages/airnode-protocol/hardhat.config.js | 9 --------- packages/airnode-protocol/src/index.ts | 5 +---- 8 files changed, 23 insertions(+), 41 deletions(-) delete mode 100644 packages/airnode-protocol/contracts/authorizers/mock/MockRequesterAuthorizerWithErc721.sol diff --git a/packages/airnode-node/test/e2e/erc721-authorizers.feature.ts b/packages/airnode-node/test/e2e/erc721-authorizers.feature.ts index b7e682b685..73f9ebfc15 100644 --- a/packages/airnode-node/test/e2e/erc721-authorizers.feature.ts +++ b/packages/airnode-node/test/e2e/erc721-authorizers.feature.ts @@ -9,6 +9,7 @@ increaseTestTimeout(); it('deploys a requesterAuthorizerWithErc721 contract and authorizes requests', async () => { const requests = [operation.buildFullRequest()]; const { provider, deployment, deployerIndex, mnemonic } = await deployAirnodeAndMakeRequests(__filename, requests); + const requesterAuthorizerWithErc721Address = deployment.contracts.RequesterAuthorizerWithErc721; // Send the NFT to the requester const deployer = provider.getSigner(deployerIndex); @@ -18,15 +19,9 @@ it('deploys a requesterAuthorizerWithErc721 contract and authorizes requests', a ); await erc721Mocks.MockErc721Factory.connect(deployment.erc721s.MockErc721Factory, deployer)[ 'safeTransferFrom(address,address,uint256,bytes)' - ]( - await deployer.getAddress(), - deployment.authorizers.MockRequesterAuthorizerWithErc721Factory, - 0, - onERC721ReceivedArguments - ); + ](await deployer.getAddress(), requesterAuthorizerWithErc721Address, 0, onERC721ReceivedArguments); const erc721Address = deployment.erc721s.MockErc721Factory; - const requesterAuthorizersWithErc721Address = deployment.authorizers.MockRequesterAuthorizerWithErc721Factory; const config = local.loadConfig(); config.chains[0].authorizers.requesterEndpointAuthorizers = []; @@ -36,7 +31,7 @@ it('deploys a requesterAuthorizerWithErc721 contract and authorizes requests', a config.chains[0].authorizers.requesterAuthorizersWithErc721 = [ { erc721s: [erc721Address], - RequesterAuthorizerWithErc721: requesterAuthorizersWithErc721Address, + RequesterAuthorizerWithErc721: requesterAuthorizerWithErc721Address, }, ]; jest.spyOn(local, 'loadConfig').mockReturnValueOnce(config); diff --git a/packages/airnode-node/test/setup/e2e/deployment.ts b/packages/airnode-node/test/setup/e2e/deployment.ts index b0b409be24..dc118f8404 100644 --- a/packages/airnode-node/test/setup/e2e/deployment.ts +++ b/packages/airnode-node/test/setup/e2e/deployment.ts @@ -10,11 +10,12 @@ export async function deployAirnodeRrp(config: operation.Config): Promise { } return { ...state, erc721sByName }; } + +export async function deployRequesterAuthorizerWithErc721(state: State): Promise { + const RequesterAuthorizerWithErc721 = new RequesterAuthorizerWithErc721Factory(state.deployer); + const requesterAuthorizerWithErc721 = await RequesterAuthorizerWithErc721.deploy( + state.contracts.AccessControlRegistry!.address, + 'RequesterAuthorizerWithErc721 admin' + ); + await requesterAuthorizerWithErc721.deployed(); + return { ...state, contracts: { ...state.contracts, RequesterAuthorizerWithErc721: requesterAuthorizerWithErc721 } }; +} diff --git a/packages/airnode-operation/src/evm/deploy/state.ts b/packages/airnode-operation/src/evm/deploy/state.ts index 7ec6cfc3bd..fc48f9bf4d 100644 --- a/packages/airnode-operation/src/evm/deploy/state.ts +++ b/packages/airnode-operation/src/evm/deploy/state.ts @@ -63,6 +63,7 @@ function buildSaveableAirnode(state: State, airnodeName: string): DeployedAirnod export function buildSaveableDeployment(state: State): Deployment { const contracts = { AirnodeRrp: state.contracts.AirnodeRrp!.address, + RequesterAuthorizerWithErc721: state.contracts.RequesterAuthorizerWithErc721!.address, }; const requesterNames = Object.keys(state.requestersByName); diff --git a/packages/airnode-operation/src/types.ts b/packages/airnode-operation/src/types.ts index 59da589a11..95955c8f5f 100644 --- a/packages/airnode-operation/src/types.ts +++ b/packages/airnode-operation/src/types.ts @@ -1,6 +1,6 @@ import { ethers } from 'ethers'; import { InputParameter } from '@api3/airnode-abi'; -import { AirnodeRrpV0, AccessControlRegistry } from '@api3/airnode-protocol'; +import { AirnodeRrpV0, AccessControlRegistry, RequesterAuthorizerWithErc721 } from '@api3/airnode-protocol'; // =========================================== // General @@ -15,6 +15,7 @@ export interface DeployState { readonly contracts: { readonly AirnodeRrp?: AirnodeRrpV0; readonly AccessControlRegistry?: AccessControlRegistry; + readonly RequesterAuthorizerWithErc721?: RequesterAuthorizerWithErc721; }; readonly deployer: ethers.providers.JsonRpcSigner; readonly provider: ethers.providers.JsonRpcProvider; @@ -91,6 +92,7 @@ export interface Deployment { readonly requesters: { readonly [name: string]: string }; readonly contracts: { readonly AirnodeRrp: string; + readonly RequesterAuthorizerWithErc721: string; }; readonly sponsors: DeployedSponsor[]; readonly erc721s: { readonly [name: string]: string }; diff --git a/packages/airnode-protocol/contracts/authorizers/mock/MockRequesterAuthorizerWithErc721.sol b/packages/airnode-protocol/contracts/authorizers/mock/MockRequesterAuthorizerWithErc721.sol deleted file mode 100644 index 72de10f8db..0000000000 --- a/packages/airnode-protocol/contracts/authorizers/mock/MockRequesterAuthorizerWithErc721.sol +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.17; - -import "../../dev/RequesterAuthorizerWithErc721.sol"; - -contract MockRequesterAuthorizerWithErc721 is RequesterAuthorizerWithErc721 { - constructor( - address _accessControlRegistry, - string memory _adminRoleDescription - ) - RequesterAuthorizerWithErc721( - _accessControlRegistry, - _adminRoleDescription - ) - {} -} diff --git a/packages/airnode-protocol/hardhat.config.js b/packages/airnode-protocol/hardhat.config.js index 8254e9186d..6629eaba2d 100644 --- a/packages/airnode-protocol/hardhat.config.js +++ b/packages/airnode-protocol/hardhat.config.js @@ -43,15 +43,6 @@ module.exports = { }, }, }, - 'contracts/authorizers/mock/MockRequesterAuthorizerWithErc721.sol': { - version: '0.8.17', - settings: { - optimizer: { - enabled: true, - runs: 200, - }, - }, - }, 'contracts/authorizers/mock/MockErc721.sol': { version: '0.8.17', settings: { diff --git a/packages/airnode-protocol/src/index.ts b/packages/airnode-protocol/src/index.ts index 58e0bc2970..2f7b317061 100644 --- a/packages/airnode-protocol/src/index.ts +++ b/packages/airnode-protocol/src/index.ts @@ -18,7 +18,6 @@ import { RequesterAuthorizerWithAirnode__factory as RequesterAuthorizerWithAirnodeFactory, RrpBeaconServerV0__factory as RrpBeaconServerV0Factory, RequesterAuthorizerWithErc721__factory as RequesterAuthorizerWithErc721Factory, - MockRequesterAuthorizerWithErc721__factory as MockRequesterAuthorizerWithErc721Factory, MockErc721__factory as MockErc721Factory, } from './contracts'; import references from '../deployments/references.json'; @@ -46,11 +45,9 @@ const erc721Mocks = { const mocks = { MockRrpRequesterFactory, }; -// TODO: This is also used by airnode-admin, but it uses the RequesterAuthorizerWithAirnodeFactory and we might flatten -// the exports to simplify things (it shouldn't mix real and mock contracts though). + const authorizers = { RequesterAuthorizerWithAirnodeFactory, - MockRequesterAuthorizerWithErc721Factory, }; export { From 8720d2daaf5839427daa5dab9ee567207c6e5ea3 Mon Sep 17 00:00:00 2001 From: Derek Croote Date: Thu, 16 Mar 2023 21:46:20 -0700 Subject: [PATCH 13/13] Minor cleanup --- .../src/evm/authorization/authorization-fetching.test.ts | 6 +++--- .../src/evm/authorization/authorization-fetching.ts | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/airnode-node/src/evm/authorization/authorization-fetching.test.ts b/packages/airnode-node/src/evm/authorization/authorization-fetching.test.ts index 5a7591cd14..fe2944d2d0 100644 --- a/packages/airnode-node/src/evm/authorization/authorization-fetching.test.ts +++ b/packages/airnode-node/src/evm/authorization/authorization-fetching.test.ts @@ -120,7 +120,7 @@ describe('fetch (authorizations)', () => { { level: 'WARN', message: - 'Failed to fetch requesterEndpointAuthorizers authorization using checkAuthorizationStatuses.' + + 'Failed to fetch requesterEndpointAuthorizers authorization using checkAuthorizationStatuses. ' + 'Falling back to fetching authorizations individually.', error: new Error('Server says no'), }, @@ -145,7 +145,7 @@ describe('fetch (authorizations)', () => { { level: 'WARN', message: - 'Failed to fetch requesterEndpointAuthorizers authorization using checkAuthorizationStatuses.' + + 'Failed to fetch requesterEndpointAuthorizers authorization using checkAuthorizationStatuses. ' + 'Falling back to fetching authorizations individually.', error: new Error('Server says no'), }, @@ -170,7 +170,7 @@ describe('fetch (authorizations)', () => { { level: 'WARN', message: - 'Failed to fetch requesterEndpointAuthorizers authorization using checkAuthorizationStatuses.' + + 'Failed to fetch requesterEndpointAuthorizers authorization using checkAuthorizationStatuses. ' + 'Falling back to fetching authorizations individually.', error: new Error('Server says no'), }, diff --git a/packages/airnode-node/src/evm/authorization/authorization-fetching.ts b/packages/airnode-node/src/evm/authorization/authorization-fetching.ts index d86d1118a5..9609697f2e 100644 --- a/packages/airnode-node/src/evm/authorization/authorization-fetching.ts +++ b/packages/airnode-node/src/evm/authorization/authorization-fetching.ts @@ -97,7 +97,7 @@ async function fetchAuthorizationStatuses( if (!goData.success) { const groupLog = logger.pend( 'WARN', - 'Failed to fetch requesterEndpointAuthorizers authorization using checkAuthorizationStatuses.' + + 'Failed to fetch requesterEndpointAuthorizers authorization using checkAuthorizationStatuses. ' + 'Falling back to fetching authorizations individually.', goData.error ); @@ -228,7 +228,6 @@ export async function fetch( // Request groups of 10 at a time const groupedPairs = chunk(apiCallsToFetchAuthorizationStatus, CONVENIENCE_BATCH_SIZE); - // Create an instance of the contract that we can re-use let promises: Promise>[]; switch (fetchOptions.type) { case 'airnodeRrp':