From 81b7fad60053aadbadd6041a78961e8c7937d0d1 Mon Sep 17 00:00:00 2001 From: LeoSlrRf Date: Wed, 26 Aug 2026 11:40:02 +0200 Subject: [PATCH 1/2] feat(recurring-payment): contract deployment --- .../payment/erc20-recurring-payment-proxy.ts | 11 +- .../payment/erc-20-recurring-payment.test.ts | 107 ++---------------- .../scripts-create2/constructor-args.ts | 8 +- .../ERC20RecurringPaymentProxy/index.ts | 9 +- .../smart-contracts/test/lib/artifact.test.ts | 14 ++- 5 files changed, 38 insertions(+), 111 deletions(-) diff --git a/packages/payment-processor/src/payment/erc20-recurring-payment-proxy.ts b/packages/payment-processor/src/payment/erc20-recurring-payment-proxy.ts index 9fb6a0468c..840740ae3f 100644 --- a/packages/payment-processor/src/payment/erc20-recurring-payment-proxy.ts +++ b/packages/payment-processor/src/payment/erc20-recurring-payment-proxy.ts @@ -4,6 +4,7 @@ import { erc20RecurringPaymentProxyArtifact } from '@requestnetwork/smart-contra import { ERC20__factory } from '@requestnetwork/smart-contracts/types'; import { getErc20Allowance } from './erc20'; +const RECURRING_PROXY_V1 = '0.1.0'; const RECURRING_PROXY_V2 = '0.2.0'; const EIP712_DOMAIN_NAME = 'ERC20RecurringPaymentProxy'; const EIP712_DOMAIN_VERSION = '1'; @@ -39,7 +40,7 @@ function connectRecurringPaymentProxy( * @param tokenAddress - Address of the ERC-20 token involved in the recurring payment schedule. * @param provider - A Web3 provider or signer used to perform the on-chain call. * @param network - The EVM chain name (e.g. `'mainnet'`, `'goerli'`, `'matic'`). - * @param version - Artifact version. Defaults to the artifact last version (`0.1.0`). + * @param version - Artifact version. Defaults to the artifact last version (`0.2.0`). * * @returns A Promise that resolves to the allowance **as a decimal string** (same * units as `token.decimals`). An empty allowance is returned as `"0"`. @@ -83,7 +84,7 @@ export async function getPayerRecurringPaymentAllowance({ * @param amount - The amount to approve, as a BigNumberish value * @param provider - Web3 provider or signer to interact with the blockchain * @param network - The EVM chain name where the proxy is deployed - * @param version - Artifact version. Defaults to the artifact last version (`0.1.0`). + * @param version - Artifact version. Defaults to the artifact last version (`0.2.0`). * * @returns Array of transaction objects ready to be sent to the blockchain * @@ -151,7 +152,7 @@ export function encodeRecurringPaymentTrigger({ network: CurrencyTypes.EvmChainName; provider: providers.Provider | Signer; }): string { - const proxyContract = erc20RecurringPaymentProxyArtifact.connect(network, provider); + const proxyContract = connectRecurringPaymentProxy(network, provider, RECURRING_PROXY_V1); return proxyContract.interface.encodeFunctionData('triggerRecurringPayment', [ permitTuple, @@ -197,7 +198,7 @@ export async function triggerRecurringPayment({ signer: Signer; network: CurrencyTypes.EvmChainName; }): Promise { - const proxyAddress = getRecurringPaymentProxyAddress(network); + const proxyAddress = getRecurringPaymentProxyAddress(network, RECURRING_PROXY_V1); const data = encodeRecurringPaymentTrigger({ permitTuple, @@ -479,7 +480,7 @@ async function sendToRecurringProxyV2( * Returns the deployed address of the ERC20RecurringPaymentProxy contract for a given network. * * @param network - The EVM chain name (e.g. 'mainnet', 'sepolia', 'matic') - * @param version - Artifact version. Defaults to the artifact last version (`0.1.0`). + * @param version - Artifact version. Defaults to the artifact last version (`0.2.0`). * * @returns The deployed proxy contract address for the specified network * diff --git a/packages/payment-processor/test/payment/erc-20-recurring-payment.test.ts b/packages/payment-processor/test/payment/erc-20-recurring-payment.test.ts index 7ec35502d5..446d5d66b2 100644 --- a/packages/payment-processor/test/payment/erc-20-recurring-payment.test.ts +++ b/packages/payment-processor/test/payment/erc-20-recurring-payment.test.ts @@ -42,78 +42,8 @@ const schedulePermit: PaymentTypes.SchedulePermit = { }; const paymentReference = '0x0000000000000000000000000000000000000000000000000000000000000001'; - -// Helper function to create EIP-712 signature for SchedulePermit -async function createSchedulePermitSignature( - permit: PaymentTypes.SchedulePermit, - signer: Wallet, - proxyAddress: string, -): Promise { - const domain = { - name: 'ERC20RecurringPaymentProxy', - version: '1', - chainId: await signer.getChainId(), - verifyingContract: proxyAddress, - }; - - const types = { - SchedulePermit: [ - { name: 'subscriber', type: 'address' }, - { name: 'token', type: 'address' }, - { name: 'recipient', type: 'address' }, - { name: 'feeAddress', type: 'address' }, - { name: 'amount', type: 'uint128' }, - { name: 'feeAmount', type: 'uint128' }, - { name: 'relayerFee', type: 'uint128' }, - { name: 'periodSeconds', type: 'uint32' }, - { name: 'firstPayment', type: 'uint32' }, - { name: 'totalPayments', type: 'uint8' }, - { name: 'nonce', type: 'uint256' }, - { name: 'deadline', type: 'uint256' }, - { name: 'strictOrder', type: 'bool' }, - ], - }; - - // Convert string values to numbers where needed - const message = { - subscriber: permit.subscriber, - token: permit.token, - recipient: permit.recipient, - feeAddress: permit.feeAddress, - amount: permit.amount, - feeAmount: permit.feeAmount, - relayerFee: permit.relayerFee, - periodSeconds: permit.periodSeconds, - firstPayment: permit.firstPayment, - totalPayments: permit.totalPayments, - nonce: typeof permit.nonce === 'string' ? permit.nonce : permit.nonce.toString(), - deadline: permit.deadline, - strictOrder: permit.strictOrder, - }; - - try { - return await (signer.provider as any).send('eth_signTypedData', [ - await signer.getAddress(), - { - types: { - EIP712Domain: [ - { name: 'name', type: 'string' }, - { name: 'version', type: 'string' }, - { name: 'chainId', type: 'uint256' }, - { name: 'verifyingContract', type: 'address' }, - ], - ...types, - }, - primaryType: 'SchedulePermit', - domain, - message, - }, - ]); - } catch (_) { - // Fallback to ethers helper (works in most in-process Hardhat environments) - return await (signer as any)._signTypedData(domain, types, message); - } -} +const dummyPermitSignature = '0x1234'; +const RECURRING_PROXY_V1 = '0.1.0'; describe('erc20-recurring-payment-proxy', () => { afterEach(() => { @@ -128,6 +58,7 @@ describe('erc20-recurring-payment-proxy', () => { amount, provider, network, + version: RECURRING_PROXY_V1, }); expect(transactions).toHaveLength(1); @@ -148,6 +79,7 @@ describe('erc20-recurring-payment-proxy', () => { amount: '1000000000000000000', provider, network, + version: RECURRING_PROXY_V1, }); }).toThrow('ERC20RecurringPaymentProxy not found on private'); }); @@ -155,16 +87,9 @@ describe('erc20-recurring-payment-proxy', () => { describe('encodeRecurringPaymentTrigger', () => { it('should encode trigger data correctly', async () => { - const proxyAddress = erc20RecurringPaymentProxyArtifact.getAddress(network); - const permitSignature = await createSchedulePermitSignature( - schedulePermit, - wallet, - proxyAddress!, - ); - const encodedData = encodeRecurringPaymentTrigger({ permitTuple: schedulePermit, - permitSignature, + permitSignature: dummyPermitSignature, paymentIndex: 1, paymentReference, network, @@ -212,12 +137,9 @@ describe('ERC20 Recurring Payment', () => { }; it('should encode recurring payment trigger', async () => { - const proxyAddress = erc20RecurringPaymentProxyArtifact.getAddress(network); - const permitSignature = await createSchedulePermitSignature(permit, wallet, proxyAddress!); - const encoded = encodeRecurringPaymentTrigger({ permitTuple: permit, - permitSignature, + permitSignature: dummyPermitSignature, paymentIndex: 1, paymentReference, network, @@ -248,13 +170,6 @@ describe('ERC20 Recurring Payment', () => { strictOrder: false, }; - // Create a valid signature for the permit - const permitSignature = await createSchedulePermitSignature( - validPermit, - wallet, - mockProxyAddress, - ); - // Mock the provider to simulate a successful transaction const mockProvider = { sendTransaction: jest.fn().mockResolvedValue({ @@ -276,7 +191,7 @@ describe('ERC20 Recurring Payment', () => { // Test the trigger function const result = await triggerRecurringPayment({ permitTuple: validPermit, - permitSignature, + permitSignature: dummyPermitSignature, paymentIndex: 1, paymentReference, signer: mockWallet as any, @@ -313,12 +228,6 @@ describe('ERC20 Recurring Payment', () => { strictOrder: false, }; - const permitSignature = await createSchedulePermitSignature( - validPermit, - wallet, - mockProxyAddress, - ); - // Mock provider that throws an error const mockProvider = { sendTransaction: jest.fn().mockRejectedValue(new Error('Transaction failed')), @@ -334,7 +243,7 @@ describe('ERC20 Recurring Payment', () => { await expect( triggerRecurringPayment({ permitTuple: validPermit, - permitSignature, + permitSignature: dummyPermitSignature, paymentIndex: 1, paymentReference, signer: mockWallet as any, diff --git a/packages/smart-contracts/scripts-create2/constructor-args.ts b/packages/smart-contracts/scripts-create2/constructor-args.ts index 5f02e76c6a..9f95ca9e20 100644 --- a/packages/smart-contracts/scripts-create2/constructor-args.ts +++ b/packages/smart-contracts/scripts-create2/constructor-args.ts @@ -17,6 +17,10 @@ const getRecurringPaymentExecutorWalletAddress = (contract: string): string => { return getEnvVariable('RECURRING_PAYMENT_EXECUTOR_WALLET_ADDRESS', contract); }; +const getReccuringPaymentAdminAddress = (contract: string): string => { + return getEnvVariable('RECURRING_PAYMENT_ADMIN_ADDRESS', contract); +}; + export const getConstructorArgs = ( contract: string, network?: CurrencyTypes.EvmChainName, @@ -94,7 +98,9 @@ export const getConstructorArgs = ( const erc20FeeProxy = artifacts.erc20FeeProxyArtifact; const erc20FeeProxyAddress = erc20FeeProxy.getAddress(network); - const adminSafe = getAdminWalletAddress(contract); + const adminSafe = + getReccuringPaymentAdminAddress(contract) ?? + artifacts.safeAdminArtifact.getAddress(network); const executorEOA = getRecurringPaymentExecutorWalletAddress(contract); return [adminSafe, executorEOA, erc20FeeProxyAddress]; diff --git a/packages/smart-contracts/src/lib/artifacts/ERC20RecurringPaymentProxy/index.ts b/packages/smart-contracts/src/lib/artifacts/ERC20RecurringPaymentProxy/index.ts index 70984cc93e..1921c519cc 100644 --- a/packages/smart-contracts/src/lib/artifacts/ERC20RecurringPaymentProxy/index.ts +++ b/packages/smart-contracts/src/lib/artifacts/ERC20RecurringPaymentProxy/index.ts @@ -45,8 +45,13 @@ export const erc20RecurringPaymentProxyArtifact = new ContractArtifact }, '0.2.0': { abi: ABI_0_2_0, - deployment: {}, + deployment: { + sepolia: { + address: '0xD7b1553ffE25491377a505f97f92cc44427D80A0', + creationBlockNumber: 11570049, + }, + }, }, }, - '0.1.0', + '0.2.0', ); diff --git a/packages/smart-contracts/test/lib/artifact.test.ts b/packages/smart-contracts/test/lib/artifact.test.ts index 4f2eb7c07e..86f3b6595b 100644 --- a/packages/smart-contracts/test/lib/artifact.test.ts +++ b/packages/smart-contracts/test/lib/artifact.test.ts @@ -59,17 +59,23 @@ describe('Artifact', () => { ); }); - it('keeps the deployed recurring proxy ABI as default while exposing version 0.2.0', () => { + it('uses 0.2.0 as the default recurring proxy ABI while keeping 0.1.0 available', () => { expect( erc20RecurringPaymentProxyArtifact .getContractAbi() - .some(({ name }) => name === 'triggerRecurringPayment'), + .some(({ name }) => name === 'triggerRecurringPaymentBatch'), ).toBe(true); expect( erc20RecurringPaymentProxyArtifact - .getContractAbi('0.2.0') - .some(({ name }) => name === 'triggerRecurringPaymentBatch'), + .getContractAbi('0.1.0') + .some(({ name }) => name === 'triggerRecurringPayment'), ).toBe(true); + expect( + erc20RecurringPaymentProxyArtifact.getOptionalDeploymentInformation('sepolia', '0.2.0'), + ).toEqual({ + address: '0xD7b1553ffE25491377a505f97f92cc44427D80A0', + creationBlockNumber: 11570049, + }); expect( erc20RecurringPaymentProxyArtifact.getOptionalDeploymentInformation('mainnet', '0.2.0'), ).toBeNull(); From 0f4d9ba948afb569f57591698eb45380a677a0b5 Mon Sep 17 00:00:00 2001 From: LeoSlrRf Date: Wed, 26 Aug 2026 11:55:02 +0200 Subject: [PATCH 2/2] fix(recurring-payment): fall back to 0.1.0 and artifact Safe --- .../payment/erc20-recurring-payment-proxy.ts | 36 ++++-- .../payment/erc-20-recurring-payment.test.ts | 113 +++++++++++++++++- .../scripts-create2/constructor-args.ts | 15 ++- 3 files changed, 145 insertions(+), 19 deletions(-) diff --git a/packages/payment-processor/src/payment/erc20-recurring-payment-proxy.ts b/packages/payment-processor/src/payment/erc20-recurring-payment-proxy.ts index 840740ae3f..cb18217ef8 100644 --- a/packages/payment-processor/src/payment/erc20-recurring-payment-proxy.ts +++ b/packages/payment-processor/src/payment/erc20-recurring-payment-proxy.ts @@ -22,14 +22,31 @@ function getRecurringPaymentProxyInterface(version: string): utils.Interface { return new utils.Interface(erc20RecurringPaymentProxyArtifact.getContractAbi(version)); } +function resolveRecurringPaymentProxyVersion( + network: CurrencyTypes.EvmChainName, + version?: string, +): string { + if (version) { + return version; + } + if ( + erc20RecurringPaymentProxyArtifact.getOptionalDeploymentInformation(network, RECURRING_PROXY_V2) + ) { + return RECURRING_PROXY_V2; + } + return RECURRING_PROXY_V1; +} + function connectRecurringPaymentProxy( network: CurrencyTypes.EvmChainName, provider: Signer | providers.Provider, version?: string, ) { - return version - ? erc20RecurringPaymentProxyArtifact.connect(network, provider, version) - : erc20RecurringPaymentProxyArtifact.connect(network, provider); + return erc20RecurringPaymentProxyArtifact.connect( + network, + provider, + resolveRecurringPaymentProxyVersion(network, version), + ); } /** @@ -40,7 +57,7 @@ function connectRecurringPaymentProxy( * @param tokenAddress - Address of the ERC-20 token involved in the recurring payment schedule. * @param provider - A Web3 provider or signer used to perform the on-chain call. * @param network - The EVM chain name (e.g. `'mainnet'`, `'goerli'`, `'matic'`). - * @param version - Artifact version. Defaults to the artifact last version (`0.2.0`). + * @param version - Artifact version. Defaults to `0.2.0` when deployed, otherwise `0.1.0`. * * @returns A Promise that resolves to the allowance **as a decimal string** (same * units as `token.decimals`). An empty allowance is returned as `"0"`. @@ -84,7 +101,7 @@ export async function getPayerRecurringPaymentAllowance({ * @param amount - The amount to approve, as a BigNumberish value * @param provider - Web3 provider or signer to interact with the blockchain * @param network - The EVM chain name where the proxy is deployed - * @param version - Artifact version. Defaults to the artifact last version (`0.2.0`). + * @param version - Artifact version. Defaults to `0.2.0` when deployed, otherwise `0.1.0`. * * @returns Array of transaction objects ready to be sent to the blockchain * @@ -480,7 +497,7 @@ async function sendToRecurringProxyV2( * Returns the deployed address of the ERC20RecurringPaymentProxy contract for a given network. * * @param network - The EVM chain name (e.g. 'mainnet', 'sepolia', 'matic') - * @param version - Artifact version. Defaults to the artifact last version (`0.2.0`). + * @param version - Artifact version. Defaults to `0.2.0` when deployed, otherwise `0.1.0`. * * @returns The deployed proxy contract address for the specified network * @@ -496,9 +513,10 @@ export function getRecurringPaymentProxyAddress( network: CurrencyTypes.EvmChainName, version?: string, ): string { - const address = version - ? erc20RecurringPaymentProxyArtifact.getAddress(network, version) - : erc20RecurringPaymentProxyArtifact.getAddress(network); + const address = erc20RecurringPaymentProxyArtifact.getAddress( + network, + resolveRecurringPaymentProxyVersion(network, version), + ); if (!address) { throw new Error(`ERC20RecurringPaymentProxy not found on ${network}`); diff --git a/packages/payment-processor/test/payment/erc-20-recurring-payment.test.ts b/packages/payment-processor/test/payment/erc-20-recurring-payment.test.ts index 446d5d66b2..05d783cd99 100644 --- a/packages/payment-processor/test/payment/erc-20-recurring-payment.test.ts +++ b/packages/payment-processor/test/payment/erc-20-recurring-payment.test.ts @@ -42,9 +42,80 @@ const schedulePermit: PaymentTypes.SchedulePermit = { }; const paymentReference = '0x0000000000000000000000000000000000000000000000000000000000000001'; -const dummyPermitSignature = '0x1234'; const RECURRING_PROXY_V1 = '0.1.0'; +// Helper function to create EIP-712 signature for SchedulePermit +async function createSchedulePermitSignature( + permit: PaymentTypes.SchedulePermit, + signer: Wallet, + proxyAddress: string, +): Promise { + const domain = { + name: 'ERC20RecurringPaymentProxy', + version: '1', + chainId: await signer.getChainId(), + verifyingContract: proxyAddress, + }; + + const types = { + SchedulePermit: [ + { name: 'subscriber', type: 'address' }, + { name: 'token', type: 'address' }, + { name: 'recipient', type: 'address' }, + { name: 'feeAddress', type: 'address' }, + { name: 'amount', type: 'uint128' }, + { name: 'feeAmount', type: 'uint128' }, + { name: 'relayerFee', type: 'uint128' }, + { name: 'periodSeconds', type: 'uint32' }, + { name: 'firstPayment', type: 'uint32' }, + { name: 'totalPayments', type: 'uint8' }, + { name: 'nonce', type: 'uint256' }, + { name: 'deadline', type: 'uint256' }, + { name: 'strictOrder', type: 'bool' }, + ], + }; + + // Convert string values to numbers where needed + const message = { + subscriber: permit.subscriber, + token: permit.token, + recipient: permit.recipient, + feeAddress: permit.feeAddress, + amount: permit.amount, + feeAmount: permit.feeAmount, + relayerFee: permit.relayerFee, + periodSeconds: permit.periodSeconds, + firstPayment: permit.firstPayment, + totalPayments: permit.totalPayments, + nonce: typeof permit.nonce === 'string' ? permit.nonce : permit.nonce.toString(), + deadline: permit.deadline, + strictOrder: permit.strictOrder, + }; + + try { + return await (signer.provider as any).send('eth_signTypedData', [ + await signer.getAddress(), + { + types: { + EIP712Domain: [ + { name: 'name', type: 'string' }, + { name: 'version', type: 'string' }, + { name: 'chainId', type: 'uint256' }, + { name: 'verifyingContract', type: 'address' }, + ], + ...types, + }, + primaryType: 'SchedulePermit', + domain, + message, + }, + ]); + } catch (_) { + // Fallback to ethers helper (works in most in-process Hardhat environments) + return await (signer as any)._signTypedData(domain, types, message); + } +} + describe('erc20-recurring-payment-proxy', () => { afterEach(() => { jest.restoreAllMocks(); @@ -87,9 +158,19 @@ describe('erc20-recurring-payment-proxy', () => { describe('encodeRecurringPaymentTrigger', () => { it('should encode trigger data correctly', async () => { + const proxyAddress = erc20RecurringPaymentProxyArtifact.getAddress( + network, + RECURRING_PROXY_V1, + ); + const permitSignature = await createSchedulePermitSignature( + schedulePermit, + wallet, + proxyAddress!, + ); + const encodedData = encodeRecurringPaymentTrigger({ permitTuple: schedulePermit, - permitSignature: dummyPermitSignature, + permitSignature, paymentIndex: 1, paymentReference, network, @@ -137,9 +218,12 @@ describe('ERC20 Recurring Payment', () => { }; it('should encode recurring payment trigger', async () => { + const proxyAddress = erc20RecurringPaymentProxyArtifact.getAddress(network, RECURRING_PROXY_V1); + const permitSignature = await createSchedulePermitSignature(permit, wallet, proxyAddress!); + const encoded = encodeRecurringPaymentTrigger({ permitTuple: permit, - permitSignature: dummyPermitSignature, + permitSignature, paymentIndex: 1, paymentReference, network, @@ -170,6 +254,13 @@ describe('ERC20 Recurring Payment', () => { strictOrder: false, }; + // Create a valid signature for the permit + const permitSignature = await createSchedulePermitSignature( + validPermit, + wallet, + mockProxyAddress, + ); + // Mock the provider to simulate a successful transaction const mockProvider = { sendTransaction: jest.fn().mockResolvedValue({ @@ -191,7 +282,7 @@ describe('ERC20 Recurring Payment', () => { // Test the trigger function const result = await triggerRecurringPayment({ permitTuple: validPermit, - permitSignature: dummyPermitSignature, + permitSignature, paymentIndex: 1, paymentReference, signer: mockWallet as any, @@ -228,6 +319,12 @@ describe('ERC20 Recurring Payment', () => { strictOrder: false, }; + const permitSignature = await createSchedulePermitSignature( + validPermit, + wallet, + mockProxyAddress, + ); + // Mock provider that throws an error const mockProvider = { sendTransaction: jest.fn().mockRejectedValue(new Error('Transaction failed')), @@ -243,7 +340,7 @@ describe('ERC20 Recurring Payment', () => { await expect( triggerRecurringPayment({ permitTuple: validPermit, - permitSignature: dummyPermitSignature, + permitSignature, paymentIndex: 1, paymentReference, signer: mockWallet as any, @@ -299,6 +396,12 @@ describe('erc20-recurring-payment-proxy 0.2.0', () => { 'ERC20RecurringPaymentProxy not found on private', ); }); + + it('falls back to 0.1.0 when version is omitted and 0.2.0 is not deployed', () => { + expect(getRecurringPaymentProxyAddress(network)).toBe( + erc20RecurringPaymentProxyArtifact.getAddress(network, RECURRING_PROXY_V1), + ); + }); }); describe('encodeSetRecurringAllowance', () => { diff --git a/packages/smart-contracts/scripts-create2/constructor-args.ts b/packages/smart-contracts/scripts-create2/constructor-args.ts index 9f95ca9e20..28797f8bbc 100644 --- a/packages/smart-contracts/scripts-create2/constructor-args.ts +++ b/packages/smart-contracts/scripts-create2/constructor-args.ts @@ -17,8 +17,15 @@ const getRecurringPaymentExecutorWalletAddress = (contract: string): string => { return getEnvVariable('RECURRING_PAYMENT_EXECUTOR_WALLET_ADDRESS', contract); }; -const getReccuringPaymentAdminAddress = (contract: string): string => { - return getEnvVariable('RECURRING_PAYMENT_ADMIN_ADDRESS', contract); +const getRecurringPaymentAdminSafe = ( + contract: string, + network: CurrencyTypes.EvmChainName, +): string => { + try { + return artifacts.safeAdminArtifact.getAddress(network); + } catch { + return getEnvVariable('RECURRING_PAYMENT_ADMIN_ADDRESS', contract); + } }; export const getConstructorArgs = ( @@ -98,9 +105,7 @@ export const getConstructorArgs = ( const erc20FeeProxy = artifacts.erc20FeeProxyArtifact; const erc20FeeProxyAddress = erc20FeeProxy.getAddress(network); - const adminSafe = - getReccuringPaymentAdminAddress(contract) ?? - artifacts.safeAdminArtifact.getAddress(network); + const adminSafe = getRecurringPaymentAdminSafe(contract, network); const executorEOA = getRecurringPaymentExecutorWalletAddress(contract); return [adminSafe, executorEOA, erc20FeeProxyAddress];