From db853e94c9b48416b0a9229214378ec0b1ec9d36 Mon Sep 17 00:00:00 2001 From: Antonio Morrone Date: Mon, 27 Feb 2023 10:00:16 +0100 Subject: [PATCH] feat: add scripts to manage collector tokens (#125) * feat: add hardhat scripts to add/remove/get tokens from the collector * doc: update readme to include task description * feat: change the removeToken script not to require the token index --- Readme.md | 49 ++++++++++++ hardhat.config.ts | 48 +++++++++++- tasks/collector/addToken.ts | 22 ++++++ tasks/{ => collector}/deployCollector.ts | 2 +- tasks/collector/getTokens.ts | 22 ++++++ tasks/collector/removeToken.ts | 26 +++++++ test/tasks/collector/addToken.test.ts | 54 +++++++++++++ .../{ => collector}/deployCollector.test.ts | 2 +- test/tasks/collector/getTokens.test.ts | 63 +++++++++++++++ test/tasks/collector/removeToken.test.ts | 78 +++++++++++++++++++ 10 files changed, 362 insertions(+), 4 deletions(-) create mode 100644 tasks/collector/addToken.ts rename tasks/{ => collector}/deployCollector.ts (98%) create mode 100644 tasks/collector/getTokens.ts create mode 100644 tasks/collector/removeToken.ts create mode 100644 test/tasks/collector/addToken.test.ts rename test/tasks/{ => collector}/deployCollector.test.ts (99%) create mode 100644 test/tasks/collector/getTokens.test.ts create mode 100644 test/tasks/collector/removeToken.test.ts diff --git a/Readme.md b/Readme.md index 8615ac74..2dad51c4 100644 --- a/Readme.md +++ b/Readme.md @@ -93,6 +93,55 @@ Example: npx hardhat collector:change-partners --collector-address "<0x9b91c655AaE10E6cd0a941Aa90A6e7aa97FB02F4" --partner-config "partner-shares.json" --gas-limit "200000" --network regtest ``` +#### Add collector token + +Pre-requirements: +- the collector we want to change must be deployed. See the [related section](#collector-deployment) to deploy a collector. + +Usage: + +```bash +npx hardhat collector:addToken --collector-address "" --token-address "" --network regtest +``` + +Example: +```bash +npx hardhat collector:addToken --collector-address "0xeFb80DB9E2d943A492Bd988f4c619495cA815643" --token-address "0xfD1dda8C3BC734Bc1C8e71F69F25BFBEe9cE9535" --network regtest +``` + +#### Get collector tokens + +Pre-requirements: +- the collector we want to change must be deployed. See the [related section](#collector-deployment) to deploy a collector. + +Usage: + +```bash +npx hardhat collector:getTokens --collector-address "" --network regtest +``` + +Example: +```bash +npx hardhat collector:getTokens --collector-address "0xeFb80DB9E2d943A492Bd988f4c619495cA815643" --network regtest +``` + +#### Remove collector token + +Pre-requirements: +- the collector we want to change must be deployed. See the [related section](#collector-deployment) to deploy a collector; +- the token that we want to remove should have no balance for the collector we're modifying; + +Usage: + +```bash +npx hardhat collector:removeToken --collector-address "" --token-address "" --network regtest +``` + +Example: +```bash +npx hardhat collector:removeToken --collector-address "0xeFb80DB9E2d943A492Bd988f4c619495cA815643" --token-address "0x39B12C05E8503356E3a7DF0B7B33efA4c054C409" --network regtest +``` + ### Addresses Each time the smart contracts are deployed, the `contract-addresses.json` file is updated. This file contains all contracts addresses for the network they were selected to be deployed on. diff --git a/hardhat.config.ts b/hardhat.config.ts index 65588781..639fa6f8 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -13,10 +13,22 @@ import { ChangePartnerSharesArg, } from './tasks/changePartnerShares'; import { deploy } from './tasks/deploy'; -import { deployCollector, DeployCollectorArg } from './tasks/deployCollector'; +import { + deployCollector, + DeployCollectorArg, +} from './tasks/collector/deployCollector'; import { getAllowedTokens } from './tasks/getAllowedTokens'; import { removeTokens } from './tasks/removeTokens'; import { withdraw, WithdrawSharesArg } from './tasks/withdraw'; +import { + addTokenToCollector, + ManageCollectorTokenArgs, +} from './tasks/collector/addToken'; +import { + getCollectorTokens, + GetCollectorTokensArgs, +} from './tasks/collector/getTokens'; +import { removeTokenFromCollector } from './tasks/collector/removeToken'; dotenv.config(); const DEFAULT_MNEMONIC = @@ -148,6 +160,38 @@ task('collector:withdraw', 'Withdraws funds from a collector contract') await withdraw(taskArgs, hre); }); +task( + 'collector:addToken', + 'Allow the collector to receive payments in additional tokens' +) + .addParam('collectorAddress', 'address of the collector contract to modify') + .addParam( + 'tokenAddress', + 'address of the token we want to allow in the collector' + ) + .setAction(async (taskArgs: ManageCollectorTokenArgs, hre) => { + await addTokenToCollector(taskArgs, hre); + }); + +task('collector:getTokens', 'Retrieve tokens managed by the collector') + .addParam('collectorAddress', 'address of the collector contract to modify') + .setAction(async (taskArgs: GetCollectorTokensArgs, hre) => { + await getCollectorTokens(taskArgs, hre); + }); + +task( + 'collector:removeToken', + 'Remove a token from the ones that the collector can accept to receive payments' +) + .addParam('collectorAddress', 'address of the collector contract to modify') + .addParam( + 'tokenAddress', + 'address of the token we want to remove in the collector' + ) + .setAction(async (taskArgs: ManageCollectorTokenArgs, hre) => { + await removeTokenFromCollector(taskArgs, hre); + }); + task('remove-tokens', 'Removes a list of tokens') .addPositionalParam('tokenlist', 'list of tokens') .setAction(async (taskArgs: { tokenlist: string }, hre) => { @@ -155,7 +199,7 @@ task('remove-tokens', 'Removes a list of tokens') }); task('collector:change-partners', 'Change collector partners') - .addParam('collectorAddress', 'address of the collector we want to modify') + .addParam('collectorAddress', 'address of the collector contract to modify') .addParam( 'partnerConfig', 'path of the file that includes the partner shares configuration' diff --git a/tasks/collector/addToken.ts b/tasks/collector/addToken.ts new file mode 100644 index 00000000..b2b253b6 --- /dev/null +++ b/tasks/collector/addToken.ts @@ -0,0 +1,22 @@ +import { HardhatRuntimeEnvironment } from 'hardhat/types'; + +export type ManageCollectorTokenArgs = { + collectorAddress: string; + tokenAddress: string; +}; + +export const addTokenToCollector = async ( + { collectorAddress, tokenAddress }: ManageCollectorTokenArgs, + { ethers }: HardhatRuntimeEnvironment +) => { + const collector = await ethers.getContractAt('Collector', collectorAddress); + + try { + await collector.addToken(tokenAddress); + } catch (error) { + console.error( + `Error adding token with address ${tokenAddress} to allowed tokens on Collector ${collectorAddress}` + ); + throw error; + } +}; diff --git a/tasks/deployCollector.ts b/tasks/collector/deployCollector.ts similarity index 98% rename from tasks/deployCollector.ts rename to tasks/collector/deployCollector.ts index bceed853..7b983ee2 100644 --- a/tasks/deployCollector.ts +++ b/tasks/collector/deployCollector.ts @@ -1,7 +1,7 @@ import * as fs from 'fs'; import { HardhatRuntimeEnvironment } from 'hardhat/types'; -import { Collector } from '../typechain-types'; +import { Collector } from '../../typechain-types'; export const DEFAULT_CONFIG_FILE_NAME = 'deploy-collector.input.json'; export const DEFAULT_OUTPUT_FILE_NAME = 'revenue-sharing-addresses.json'; diff --git a/tasks/collector/getTokens.ts b/tasks/collector/getTokens.ts new file mode 100644 index 00000000..0477176f --- /dev/null +++ b/tasks/collector/getTokens.ts @@ -0,0 +1,22 @@ +import { HardhatRuntimeEnvironment } from 'hardhat/types'; +import { ManageCollectorTokenArgs } from './addToken'; + +export type GetCollectorTokensArgs = Omit< + ManageCollectorTokenArgs, + 'tokenAddress' +>; + +export const getCollectorTokens = async ( + { collectorAddress }: GetCollectorTokensArgs, + { ethers }: HardhatRuntimeEnvironment +) => { + const collector = await ethers.getContractAt('Collector', collectorAddress); + + try { + const tokens = await collector.getTokens(); + console.log('Allowed Tokens:', tokens); + } catch (error) { + console.error(`Error retrieving tokens from Collector ${collectorAddress}`); + throw error; + } +}; diff --git a/tasks/collector/removeToken.ts b/tasks/collector/removeToken.ts new file mode 100644 index 00000000..e062f99f --- /dev/null +++ b/tasks/collector/removeToken.ts @@ -0,0 +1,26 @@ +import { HardhatRuntimeEnvironment } from 'hardhat/types'; +import { ManageCollectorTokenArgs } from './addToken'; + +export const removeTokenFromCollector = async ( + { collectorAddress, tokenAddress }: ManageCollectorTokenArgs, + { ethers }: HardhatRuntimeEnvironment +) => { + const collector = await ethers.getContractAt('Collector', collectorAddress); + + try { + const tokens = await collector.getTokens(); + const tokenIndex = tokens.findIndex((token) => token === tokenAddress); + if (tokenIndex < 0) { + throw new Error( + `Token with address ${tokenAddress} not found. Please verify the tokens managed by the Collector ${collectorAddress}` + ); + } + console.log(`Token found with index ${tokenIndex}`); + await collector.removeToken(tokenAddress, tokenIndex); + } catch (error) { + console.error( + `Error removing token with address ${tokenAddress} from Collector ${collectorAddress}` + ); + throw error; + } +}; diff --git a/test/tasks/collector/addToken.test.ts b/test/tasks/collector/addToken.test.ts new file mode 100644 index 00000000..b03defba --- /dev/null +++ b/test/tasks/collector/addToken.test.ts @@ -0,0 +1,54 @@ +import { expect, use } from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import * as hre from 'hardhat'; +import { ethers } from 'hardhat'; +import sinon from 'sinon'; +import { + ManageCollectorTokenArgs, + addTokenToCollector, +} from '../../../tasks/collector/addToken'; +import { Collector } from '../../../typechain-types'; + +use(chaiAsPromised); + +describe('Script to add tokens to collector', function () { + describe('addToken', function () { + const taskArgs: ManageCollectorTokenArgs = { + collectorAddress: '0x06c85B7EA1AA2d030E1a747B3d8d15D5845fd714', + tokenAddress: '0x145845fd06c85B7EA1AA2d030E1a747B3d8d15D7', + }; + + afterEach(function () { + sinon.restore(); + }); + + it('should add a token when no tokens are managed', async function () { + const addToken = sinon.spy(); + const fakeCollector = { + addToken, + } as unknown as Collector; + sinon.stub(ethers, 'getContractAt').resolves(fakeCollector); + await expect( + addTokenToCollector(taskArgs, hre), + 'addTokenToCollector rejected' + ).not.to.be.rejected; + expect(addToken.called, 'Collector.addToken was not called').to.be.true; + }); + + it('should fail if the token is already managed', async function () { + const expectedError = new Error('Token already managed'); + const addToken = sinon.spy(() => { + throw expectedError; + }); + const fakeCollector = { + addToken, + } as unknown as Collector; + sinon.stub(ethers, 'getContractAt').resolves(fakeCollector); + await expect( + addTokenToCollector(taskArgs, hre), + 'addTokenToCollector did not reject' + ).to.be.rejectedWith(expectedError); + expect(addToken.called, 'Collector.addToken was not called').to.be.true; + }); + }); +}); diff --git a/test/tasks/deployCollector.test.ts b/test/tasks/collector/deployCollector.test.ts similarity index 99% rename from test/tasks/deployCollector.test.ts rename to test/tasks/collector/deployCollector.test.ts index 2c4d31c9..7bbd4df5 100644 --- a/test/tasks/deployCollector.test.ts +++ b/test/tasks/collector/deployCollector.test.ts @@ -14,7 +14,7 @@ import { deployCollector, DeployCollectorArg, OutputConfig, -} from '../../tasks/deployCollector'; +} from '../../../tasks/collector/deployCollector'; use(smock.matchers); use(chaiAsPromised); diff --git a/test/tasks/collector/getTokens.test.ts b/test/tasks/collector/getTokens.test.ts new file mode 100644 index 00000000..cc994902 --- /dev/null +++ b/test/tasks/collector/getTokens.test.ts @@ -0,0 +1,63 @@ +import { expect, use } from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import * as hre from 'hardhat'; +import { ethers } from 'hardhat'; +import sinon, { SinonSpy } from 'sinon'; +import { + getCollectorTokens, + GetCollectorTokensArgs, +} from '../../../tasks/collector/getTokens'; +import { Collector } from '../../../typechain-types'; + +use(chaiAsPromised); + +describe('Script to retrieve the collector tokens', function () { + describe('getTokens', function () { + const taskArgs: GetCollectorTokensArgs = { + collectorAddress: '0x06c85B7EA1AA2d030E1a747B3d8d15D5845fd714', + }; + const tokens = ['0x123abc', '0xabc123']; + let consoleLogSpy: SinonSpy; + + beforeEach(function () { + consoleLogSpy = sinon.spy(console, 'log'); + }); + + afterEach(function () { + sinon.restore(); + }); + + it('should get the tokens managed by the collector', async function () { + const getTokens = sinon.stub().returns(Promise.resolve(tokens)); + const fakeCollector = { + getTokens, + } as unknown as Collector; + sinon.stub(ethers, 'getContractAt').resolves(fakeCollector); + await expect( + getCollectorTokens(taskArgs, hre), + 'getCollectorTokens rejected' + ).not.to.be.rejected; + expect(getTokens.called, 'Collector.getTokens was not called').to.be.true; + expect( + consoleLogSpy.calledWithExactly('Allowed Tokens:', tokens), + 'Console.log was not called with the expected arguments' + ).to.be.true; + }); + + it('should fail if the getTokens task raises an error', async function () { + const expectedError = new Error('Token already managed'); + const getTokens = sinon.spy(() => { + throw expectedError; + }); + const stubContract = { + getTokens, + } as unknown as Collector; + sinon.stub(ethers, 'getContractAt').resolves(stubContract); + await expect( + getCollectorTokens(taskArgs, hre), + 'getCollectorTokens did not reject' + ).to.be.rejectedWith(expectedError); + expect(consoleLogSpy.called, 'Console.log was called').to.be.false; + }); + }); +}); diff --git a/test/tasks/collector/removeToken.test.ts b/test/tasks/collector/removeToken.test.ts new file mode 100644 index 00000000..b8475ed4 --- /dev/null +++ b/test/tasks/collector/removeToken.test.ts @@ -0,0 +1,78 @@ +import { expect, use } from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import * as hre from 'hardhat'; +import { ethers } from 'hardhat'; +import sinon from 'sinon'; +import { ManageCollectorTokenArgs } from 'tasks/collector/addToken'; +import { removeTokenFromCollector } from '../../../tasks/collector/removeToken'; +import { Collector } from '../../../typechain-types'; + +use(chaiAsPromised); + +describe('Script to remove tokens from collector', function () { + describe('removeToken', function () { + const taskArgs: ManageCollectorTokenArgs = { + collectorAddress: '0x06c85B7EA1AA2d030E1a747B3d8d15D5845fd714', + tokenAddress: '0x145845fd06c85B7EA1AA2d030E1a747B3d8d15D7', + }; + + afterEach(function () { + sinon.restore(); + }); + + it('should remove the token if it is among the ones that are managed', async function () { + const removeToken = sinon.spy(); + const expectedTokenIndex = 0; + const getTokens = sinon.mock().returns([taskArgs.tokenAddress]); + const fakeCollector = { + removeToken, + getTokens, + } as unknown as Collector; + sinon.stub(ethers, 'getContractAt').resolves(fakeCollector); + await expect( + removeTokenFromCollector(taskArgs, hre), + 'removeTokenFromCollector rejected' + ).not.to.be.rejected; + expect( + removeToken.calledWithExactly( + taskArgs.tokenAddress, + expectedTokenIndex + ), + 'collector.removeToken was not called' + ).to.be.true; + }); + + it('should fail if the token cannot be found', async function () { + const fakeCollector = { + removeToken: sinon.spy(), + getTokens: sinon.mock().returns(['0x123456', '0xabc123']), + } as unknown as Collector; + + sinon.stub(ethers, 'getContractAt').resolves(fakeCollector); + await expect( + removeTokenFromCollector(taskArgs, hre), + 'removeTokenFromCollector did not reject' + ).to.be.rejectedWith( + `Token with address ${taskArgs.tokenAddress} not found` + ); + }); + + it('should fail if the token removal throws an error', async function () { + const expectedError = new Error('Token not managed'); + const removeToken = sinon.spy(() => { + throw expectedError; + }); + const fakeCollector = { + removeToken, + } as unknown as Collector; + fakeCollector.getTokens = sinon.mock().returns([taskArgs.tokenAddress]); + sinon.stub(ethers, 'getContractAt').resolves(fakeCollector); + await expect( + removeTokenFromCollector(taskArgs, hre), + 'removeTokenFromCollector did not reject' + ).to.be.rejectedWith(expectedError); + expect(removeToken.called, 'collector.removeToken was not called').to.be + .true; + }); + }); +});