diff --git a/README.md b/README.md index 64110ce9a..18c5f7be0 100644 --- a/README.md +++ b/README.md @@ -90,34 +90,58 @@ To test a single file run: `npx hardhat test test/.ts` ## E2E Testing -End to end tests are also available and can be run against a local network or a live network. These can be useful to validate a protocol deployment is configured and working as expected. +End to end tests are also available and can be run against a local network or a live network. These can be useful to validate a protocol deployment is configured and working as expected. + +There are several types of e2e tests which can be run separately: +- **deployment/config** + - Test the configuration of deployed contracts (parameters that don't change over time). + - Can be run against any network at any time and the tests should pass. + - Only read only interactions with the blockchain. + - Example: a test validating the curation default reserve ratio matches the value in the graph config file. +- **deployment/init** + - Test the initialization of deployed contracts (parameters that change with protocol usage). + - Can be run against a "fresh" protocol deployment. Running these tests against a protocol with pre-existing state will probably fail. + - Only read only interactions with the blockchain. + - Example: a test validating that the GRT total supply equals 10B, this is only true on a freshly deployed protocol until the first allocation is closed and protocol issuance kicks in. +- **scenarios** + - Test the execution of common protocol actions. + - Can be run against any network at any time and the tests should pass. + - Read and write interactions with the blockchain. _Requires an account with sufficient balance!_ + - Example: a test validating that a user can add signal to a subgraph. ### Hardhat local node -To run e2e tests against a hardhat local node run: +To run all e2e tests against a hardhat local node run: ```bash yarn test:e2e ``` -The command will invoke several hardhat tasks, including: +The command will perform the following actions: - Start a hardhat node (localhost) - Run `migrate:accounts` hardhat task to create keys for all protocol roles (deployer, governor, arbiter, etc). This currently doesn't support multisig accounts. - Run `migrate` hardhat task to deploy the protocol - Run `migrate:ownership` hardhat task to transfer ownership of governed contracts to the governor - Run `migrate:unpause` to unpause the protocol -- Run e2e tests +- Run `e2e` hardhat task to run all e2e tests ### Other networks To run tests against a live testnet or even mainnet run: ```bash -GRAPH_CONFIG=config/graph..yml ADDRESS_BOOK=addresses.json npx hardhat test e2e/**/*.ts --network +# All e2e tests +npx hardhat e2e --network --graph-config config/graph..yml + +# Only deployment config tests +npx hardhat e2e:config --network --graph-config config/graph..yml + +# Only deployment init tests +npx hardhat e2e:init --network --graph-config config/graph..yml ``` -This command will only run the tests so you need to be sure the protocol is already deployed and the graph config file and address book files are up to date. +Note that this command will only run the tests so you need to be sure the protocol is already deployed and the graph config file and address book files are up to date. # Interacting with the contracts diff --git a/config/graph.goerli.yml b/config/graph.goerli.yml index 1d8261b67..fe9189bf0 100644 --- a/config/graph.goerli.yml +++ b/config/graph.goerli.yml @@ -30,6 +30,14 @@ contracts: - fn: "setContractProxy" id: "0x45fc200c7e4544e457d3c5709bfe0d520442c30bbcbdaede89e8d4a4bbc19247" # keccak256('GraphToken') contractAddress: "${{GraphToken.address}}" + - fn: "setPauseGuardian" + pauseGuardian: *pauseGuardian + - fn: "transferOwnership" + owner: *governor + GraphProxyAdmin: + calls: + - fn: "transferOwnership" + owner: *governor ServiceRegistry: proxy: true init: @@ -45,6 +53,9 @@ contracts: calls: - fn: "addMinter" minter: "${{RewardsManager.address}}" + - fn: "renounceMinter" + - fn: "transferOwnership" + owner: *governor Curation: proxy: true init: @@ -79,6 +90,8 @@ contracts: tokenDescriptor: "${{SubgraphNFTDescriptor.address}}" - fn: "setMinter" minter: "${{GNS.address}}" + - fn: "transferOwnership" + owner: *governor Staking: proxy: true init: @@ -107,6 +120,9 @@ contracts: init: controller: "${{Controller.address}}" issuanceRate: "1000000012184945188" # per block increase of total supply, blocks in a year = 365*60*60*24/13 + calls: + - fn: "setSubgraphAvailabilityOracle" + subgraphAvailabilityOracle: *availabilityOracle AllocationExchange: init: graphToken: "${{GraphToken.address}}" diff --git a/e2e/deployment/allocationExchange.test.ts b/e2e/deployment/allocationExchange.test.ts deleted file mode 100644 index d242e96cd..000000000 --- a/e2e/deployment/allocationExchange.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { expect } from 'chai' -import hre from 'hardhat' -import { getItemValue } from '../../cli/config' - -describe('AllocationExchange deployment', () => { - const { - graphConfig, - contracts: { AllocationExchange, GraphToken, Staking }, - } = hre.graph() - - it('should be owned by allocationExchangeOwner', async function () { - const owner = await AllocationExchange.governor() - const allocationExchangeOwner = getItemValue(graphConfig, 'general/allocationExchangeOwner') - expect(owner).eq(allocationExchangeOwner) - }) - - it('should accept vouchers from authority', async function () { - const authorityAddress = getItemValue(graphConfig, 'general/authority') - const allowed = await AllocationExchange.authority(authorityAddress) - expect(allowed).eq(true) - }) - - // graphToken and staking are private variables so we can't verify - it.skip('graphToken should match the GraphToken deployment address') - it.skip('staking should match the Staking deployment address') - - it('should allow Staking contract to spend MAX_UINT256 tokens on AllocationExchange behalf', async function () { - const allowance = await GraphToken.allowance(AllocationExchange.address, Staking.address) - expect(allowance).eq(hre.ethers.constants.MaxUint256) - }) -}) diff --git a/e2e/deployment/config/allocationExchange.test.ts b/e2e/deployment/config/allocationExchange.test.ts new file mode 100644 index 000000000..ee023ce58 --- /dev/null +++ b/e2e/deployment/config/allocationExchange.test.ts @@ -0,0 +1,30 @@ +import { expect } from 'chai' +import hre from 'hardhat' +import { NamedAccounts } from '../../../tasks/type-extensions' + +describe('AllocationExchange configuration', () => { + const { + contracts: { AllocationExchange }, + getNamedAccounts, + } = hre.graph() + + let namedAccounts: NamedAccounts + + before(async () => { + namedAccounts = await getNamedAccounts() + }) + + it('should be owned by allocationExchangeOwner', async function () { + const owner = await AllocationExchange.governor() + expect(owner).eq(namedAccounts.allocationExchangeOwner.address) + }) + + it('should accept vouchers from authority', async function () { + const allowed = await AllocationExchange.authority(namedAccounts.authority.address) + expect(allowed).eq(true) + }) + + // graphToken and staking are private variables so we can't verify + it.skip('graphToken should match the GraphToken deployment address') + it.skip('staking should match the Staking deployment address') +}) diff --git a/e2e/deployment/controller.test.ts b/e2e/deployment/config/controller.test.ts similarity index 72% rename from e2e/deployment/controller.test.ts rename to e2e/deployment/config/controller.test.ts index 269521e55..00caf2130 100644 --- a/e2e/deployment/controller.test.ts +++ b/e2e/deployment/config/controller.test.ts @@ -1,9 +1,9 @@ import { expect } from 'chai' import hre, { ethers } from 'hardhat' -import { getItemValue } from '../../cli/config' +import { NamedAccounts } from '../../../tasks/type-extensions' -describe('Controller deployment', () => { - const { contracts, graphConfig } = hre.graph() +describe('Controller configuration', () => { + const { contracts, getNamedAccounts } = hre.graph() const { Controller } = contracts const proxyContracts = [ @@ -16,6 +16,12 @@ describe('Controller deployment', () => { 'GraphToken', ] + let namedAccounts: NamedAccounts + + before(async () => { + namedAccounts = await getNamedAccounts() + }) + const proxyShouldMatchDeployed = async (contractName: string) => { const curationAddress = await Controller.getContractProxy( ethers.utils.solidityKeccak256(['string'], [contractName]), @@ -25,14 +31,12 @@ describe('Controller deployment', () => { it('should be owned by governor', async function () { const owner = await Controller.governor() - const governorAddress = getItemValue(graphConfig, 'general/governor') - expect(owner).eq(governorAddress) + expect(owner).eq(namedAccounts.governor.address) }) it('pause guardian should be able to pause protocol', async function () { - const pauseGuardianAddress = getItemValue(graphConfig, 'general/pauseGuardian') const pauseGuardian = await Controller.pauseGuardian() - expect(pauseGuardian).eq(pauseGuardianAddress) + expect(pauseGuardian).eq(namedAccounts.pauseGuardian.address) }) describe('proxy contract', async function () { diff --git a/e2e/deployment/curation.test.ts b/e2e/deployment/config/curation.test.ts similarity index 94% rename from e2e/deployment/curation.test.ts rename to e2e/deployment/config/curation.test.ts index 64d1b9d8a..6d6a4d1ea 100644 --- a/e2e/deployment/curation.test.ts +++ b/e2e/deployment/config/curation.test.ts @@ -1,8 +1,8 @@ import { expect } from 'chai' import hre from 'hardhat' -import { getItemValue } from '../../cli/config' +import { getItemValue } from '../../../cli/config' -describe('Curation deployment', () => { +describe('Curation configuration', () => { const { graphConfig, contracts: { Controller, Curation, BancorFormula, GraphCurationToken }, diff --git a/e2e/deployment/disputeManager.test.ts b/e2e/deployment/config/disputeManager.test.ts similarity index 94% rename from e2e/deployment/disputeManager.test.ts rename to e2e/deployment/config/disputeManager.test.ts index 247710901..00ceaf176 100644 --- a/e2e/deployment/disputeManager.test.ts +++ b/e2e/deployment/config/disputeManager.test.ts @@ -1,8 +1,8 @@ import { expect } from 'chai' import hre from 'hardhat' -import { getItemValue } from '../../cli/config' +import { getItemValue } from '../../../cli/config' -describe('DisputeManager deployment', () => { +describe('DisputeManager configuration', () => { const { graphConfig, contracts: { Controller, DisputeManager }, diff --git a/e2e/deployment/epochManager.test.ts b/e2e/deployment/config/epochManager.test.ts similarity index 85% rename from e2e/deployment/epochManager.test.ts rename to e2e/deployment/config/epochManager.test.ts index ad09a7857..3f80321ec 100644 --- a/e2e/deployment/epochManager.test.ts +++ b/e2e/deployment/config/epochManager.test.ts @@ -1,8 +1,8 @@ import { expect } from 'chai' import hre from 'hardhat' -import { getItemValue } from '../../cli/config' +import { getItemValue } from '../../../cli/config' -describe('EpochManager deployment', () => { +describe('EpochManager configuration', () => { const { graphConfig, contracts: { EpochManager, Controller }, diff --git a/e2e/deployment/gns.test.ts b/e2e/deployment/config/gns.test.ts similarity index 63% rename from e2e/deployment/gns.test.ts rename to e2e/deployment/config/gns.test.ts index 6c0b17d54..4337dffd0 100644 --- a/e2e/deployment/gns.test.ts +++ b/e2e/deployment/config/gns.test.ts @@ -1,9 +1,9 @@ import { expect } from 'chai' import hre from 'hardhat' -describe('GNS deployment', () => { +describe('GNS configuration', () => { const { - contracts: { Controller, GNS, BancorFormula, SubgraphNFT, GraphToken, Curation }, + contracts: { Controller, GNS, BancorFormula, SubgraphNFT }, } = hre.graph() it('should be controlled by Controller', async function () { @@ -20,9 +20,4 @@ describe('GNS deployment', () => { const subgraphNFT = await GNS.subgraphNFT() expect(subgraphNFT).eq(SubgraphNFT.address) }) - - it('should allow Curation contract to spend MAX_UINT256 tokens on GNS behalf', async function () { - const allowance = await GraphToken.allowance(GNS.address, Curation.address) - expect(allowance).eq(hre.ethers.constants.MaxUint256) - }) }) diff --git a/e2e/deployment/config/graphProxyAdmin.test.ts b/e2e/deployment/config/graphProxyAdmin.test.ts new file mode 100644 index 000000000..0d43984de --- /dev/null +++ b/e2e/deployment/config/graphProxyAdmin.test.ts @@ -0,0 +1,21 @@ +import { expect } from 'chai' +import hre from 'hardhat' +import { NamedAccounts } from '../../../tasks/type-extensions' + +describe('GraphProxyAdmin configuration', () => { + const { + contracts: { GraphProxyAdmin }, + getNamedAccounts, + } = hre.graph() + + let namedAccounts: NamedAccounts + + before(async () => { + namedAccounts = await getNamedAccounts() + }) + + it('should be owned by governor', async function () { + const owner = await GraphProxyAdmin.governor() + expect(owner).eq(namedAccounts.governor.address) + }) +}) diff --git a/e2e/deployment/config/graphToken.test.ts b/e2e/deployment/config/graphToken.test.ts new file mode 100644 index 000000000..b0e9a53c8 --- /dev/null +++ b/e2e/deployment/config/graphToken.test.ts @@ -0,0 +1,33 @@ +import { expect } from 'chai' +import hre from 'hardhat' +import { NamedAccounts } from '../../../tasks/type-extensions' + +describe('GraphToken configuration', () => { + const { + getNamedAccounts, + contracts: { GraphToken, RewardsManager }, + getDeployer, + } = hre.graph() + + let namedAccounts: NamedAccounts + + before(async () => { + namedAccounts = await getNamedAccounts() + }) + + it('should be owned by governor', async function () { + const owner = await GraphToken.governor() + expect(owner).eq(namedAccounts.governor.address) + }) + + it('deployer should not be minter', async function () { + const deployer = await getDeployer() + const deployerIsMinter = await GraphToken.isMinter(deployer.address) + hre.network.config.chainId === 1337 ? this.skip() : expect(deployerIsMinter).eq(false) + }) + + it('RewardsManager should be minter', async function () { + const deployerIsMinter = await GraphToken.isMinter(RewardsManager.address) + expect(deployerIsMinter).eq(true) + }) +}) diff --git a/e2e/deployment/protocol.ts b/e2e/deployment/config/protocol.test.ts similarity index 84% rename from e2e/deployment/protocol.ts rename to e2e/deployment/config/protocol.test.ts index 6391df8d3..4e12f5088 100644 --- a/e2e/deployment/protocol.ts +++ b/e2e/deployment/config/protocol.test.ts @@ -1,7 +1,7 @@ import { expect } from 'chai' import hre from 'hardhat' -describe('Protocol', () => { +describe('Protocol configuration', () => { const { contracts } = hre.graph() it('should be unpaused', async function () { diff --git a/e2e/deployment/rewardsManager.test.ts b/e2e/deployment/config/rewardsManager.test.ts similarity index 67% rename from e2e/deployment/rewardsManager.test.ts rename to e2e/deployment/config/rewardsManager.test.ts index bec6bee05..42a8192de 100644 --- a/e2e/deployment/rewardsManager.test.ts +++ b/e2e/deployment/config/rewardsManager.test.ts @@ -1,13 +1,21 @@ import { expect } from 'chai' import hre from 'hardhat' -import { getItemValue } from '../../cli/config' +import { getItemValue } from '../../../cli/config' +import { NamedAccounts } from '../../../tasks/type-extensions' -describe('RewardsManager deployment', () => { +describe('RewardsManager configuration', () => { const { graphConfig, + getNamedAccounts, contracts: { RewardsManager, Controller }, } = hre.graph() + let namedAccounts: NamedAccounts + + before(async () => { + namedAccounts = await getNamedAccounts() + }) + it('should be controlled by Controller', async function () { const controller = await RewardsManager.controller() expect(controller).eq(Controller.address) @@ -20,8 +28,7 @@ describe('RewardsManager deployment', () => { }) it('should allow subgraph availability oracle to deny rewards', async function () { - const availabilityOracleAddress = getItemValue(graphConfig, 'general/availabilityOracle') const availabilityOracle = await RewardsManager.subgraphAvailabilityOracle() - expect(availabilityOracle).eq(availabilityOracleAddress) + expect(availabilityOracle).eq(namedAccounts.availabilityOracle.address) }) }) diff --git a/e2e/deployment/serviceRegistry.test..ts b/e2e/deployment/config/serviceRegistry.test..ts similarity index 86% rename from e2e/deployment/serviceRegistry.test..ts rename to e2e/deployment/config/serviceRegistry.test..ts index c747b2188..c7e953ce9 100644 --- a/e2e/deployment/serviceRegistry.test..ts +++ b/e2e/deployment/config/serviceRegistry.test..ts @@ -1,7 +1,7 @@ import { expect } from 'chai' import hre from 'hardhat' -describe('ServiceRegistry deployment', () => { +describe('ServiceRegistry configuration', () => { const { contracts: { ServiceRegistry, Controller }, } = hre.graph() diff --git a/e2e/deployment/staking.test.ts b/e2e/deployment/config/staking.test.ts similarity index 97% rename from e2e/deployment/staking.test.ts rename to e2e/deployment/config/staking.test.ts index d6ad9f6fb..e2b1fe5e9 100644 --- a/e2e/deployment/staking.test.ts +++ b/e2e/deployment/config/staking.test.ts @@ -1,8 +1,8 @@ import { expect } from 'chai' import hre from 'hardhat' -import { getItemValue } from '../../cli/config' +import { getItemValue } from '../../../cli/config' -describe('Staking deployment', () => { +describe('Staking configuration', () => { const { graphConfig, contracts: { Staking, Controller, DisputeManager, AllocationExchange }, diff --git a/e2e/deployment/subgraphNFT.test.ts b/e2e/deployment/config/subgraphNFT.test.ts similarity index 68% rename from e2e/deployment/subgraphNFT.test.ts rename to e2e/deployment/config/subgraphNFT.test.ts index d43cd15db..2220a1e1b 100644 --- a/e2e/deployment/subgraphNFT.test.ts +++ b/e2e/deployment/config/subgraphNFT.test.ts @@ -1,17 +1,22 @@ import { expect } from 'chai' import hre from 'hardhat' -import { getItemValue } from '../../cli/config' +import { NamedAccounts } from '../../../tasks/type-extensions' -describe('SubgraphNFT deployment', () => { +describe('SubgraphNFT configuration', () => { const { - graphConfig, + getNamedAccounts, contracts: { SubgraphNFT, GNS, SubgraphNFTDescriptor }, } = hre.graph() + let namedAccounts: NamedAccounts + + before(async () => { + namedAccounts = await getNamedAccounts() + }) + it('should be owned by governor', async function () { const owner = await SubgraphNFT.governor() - const governorAddress = getItemValue(graphConfig, 'general/governor') - expect(owner).eq(governorAddress) + expect(owner).eq(namedAccounts.governor.address) }) it('should allow GNS to mint NFTs', async function () { diff --git a/e2e/deployment/graphProxyAdmin.test.ts b/e2e/deployment/graphProxyAdmin.test.ts deleted file mode 100644 index 08869a1f8..000000000 --- a/e2e/deployment/graphProxyAdmin.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { expect } from 'chai' -import hre from 'hardhat' -import { getItemValue } from '../../cli/config' - -describe('GraphProxyAdmin deployment', () => { - const { - contracts: { GraphProxyAdmin }, - graphConfig, - } = hre.graph() - - it('should be owned by governor', async function () { - const owner = await GraphProxyAdmin.governor() - const governorAddress = getItemValue(graphConfig, 'general/governor') - expect(owner).eq(governorAddress) - }) -}) diff --git a/e2e/deployment/graphToken.test.ts b/e2e/deployment/graphToken.test.ts deleted file mode 100644 index df089f3dc..000000000 --- a/e2e/deployment/graphToken.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' -import { expect } from 'chai' -import hre from 'hardhat' -import { getItemValue } from '../../cli/config' - -describe('GraphToken deployment', () => { - const { - graphConfig, - contracts: { GraphToken, RewardsManager }, - } = hre.graph() - - let deployer: SignerWithAddress - - before(async () => { - ;[deployer] = await hre.ethers.getSigners() - }) - - it('should be owned by governor', async function () { - const owner = await GraphToken.governor() - const governorAddress = getItemValue(graphConfig, 'general/governor') - expect(owner).eq(governorAddress) - }) - - it('deployer should not be minter', async function () { - const deployerIsMinter = await GraphToken.isMinter(deployer.address) - expect(deployerIsMinter).eq(false) - }) - - it('RewardsManager should be minter', async function () { - const deployerIsMinter = await GraphToken.isMinter(RewardsManager.address) - expect(deployerIsMinter).eq(true) - }) - - it('total supply should match "initialSupply" on the config file', async function () { - const value = await GraphToken.totalSupply() - const expected = getItemValue(graphConfig, 'contracts/GraphToken/init/initialSupply') - if (hre.network.config.chainId === 1337) { - expect(value).eq(expected) - } else { - expect(value).gte(expected) - } - }) -}) diff --git a/e2e/deployment/init/allocationExchange.test.ts b/e2e/deployment/init/allocationExchange.test.ts new file mode 100644 index 000000000..5a759ae56 --- /dev/null +++ b/e2e/deployment/init/allocationExchange.test.ts @@ -0,0 +1,13 @@ +import { expect } from 'chai' +import hre from 'hardhat' + +describe('AllocationExchange initialization', () => { + const { + contracts: { AllocationExchange, GraphToken, Staking }, + } = hre.graph() + + it('should allow Staking contract to spend MAX_UINT256 tokens on AllocationExchange behalf', async function () { + const allowance = await GraphToken.allowance(AllocationExchange.address, Staking.address) + expect(allowance).eq(hre.ethers.constants.MaxUint256) + }) +}) diff --git a/e2e/deployment/init/gns.test.ts b/e2e/deployment/init/gns.test.ts new file mode 100644 index 000000000..ccf5451d9 --- /dev/null +++ b/e2e/deployment/init/gns.test.ts @@ -0,0 +1,13 @@ +import { expect } from 'chai' +import hre from 'hardhat' + +describe('GNS initialization', () => { + const { + contracts: { GNS, GraphToken, Curation }, + } = hre.graph() + + it('should allow Curation contract to spend MAX_UINT256 tokens on GNS behalf', async function () { + const allowance = await GraphToken.allowance(GNS.address, Curation.address) + expect(allowance).eq(hre.ethers.constants.MaxUint256) + }) +}) diff --git a/e2e/deployment/init/graphToken.test.ts b/e2e/deployment/init/graphToken.test.ts new file mode 100644 index 000000000..8c2230adb --- /dev/null +++ b/e2e/deployment/init/graphToken.test.ts @@ -0,0 +1,16 @@ +import { expect } from 'chai' +import hre from 'hardhat' +import { getItemValue } from '../../../cli/config' + +describe('GraphToken initialization', () => { + const { + graphConfig, + contracts: { GraphToken }, + } = hre.graph() + + it('total supply should match "initialSupply" on the config file', async function () { + const value = await GraphToken.totalSupply() + const expected = getItemValue(graphConfig, 'contracts/GraphToken/init/initialSupply') + hre.network.config.chainId === 1337 ? expect(value).eq(expected) : expect(value).gte(expected) + }) +}) diff --git a/e2e/scenarios/lib/staking.ts b/e2e/scenarios/lib/staking.ts new file mode 100644 index 000000000..6fab0e5cb --- /dev/null +++ b/e2e/scenarios/lib/staking.ts @@ -0,0 +1,13 @@ +import hre from 'hardhat' +import { Staking } from '../../../build/types/Staking' + +export const stake = async (amountWei: string): Promise => { + const graph = hre.graph() + + // Approve + const stakeAmountWei = hre.ethers.utils.parseEther(amountWei).toString() + await graph.contracts.GraphToken.approve(graph.contracts.Staking.address, stakeAmountWei) + + // Stake + await graph.contracts.Staking.stake(stakeAmountWei) +} diff --git a/hardhat.config.ts b/hardhat.config.ts index 5ce3b085d..b2acd6fce 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -28,7 +28,7 @@ const SKIP_LOAD = process.env.SKIP_LOAD === 'true' function loadTasks() { require('./tasks/gre.ts') - ;['contracts', 'misc', 'deployment', 'actions', 'verify'].forEach((folder) => { + ;['contracts', 'misc', 'deployment', 'actions', 'verify', 'e2e'].forEach((folder) => { const tasksPath = path.join(__dirname, 'tasks', folder) fs.readdirSync(tasksPath) .filter((pth) => pth.includes('.ts')) diff --git a/package.json b/package.json index 914d4173b..aa8055506 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "@typechain/hardhat": "^2.0.0", "@types/bs58": "^4.0.1", "@types/dotenv": "^8.2.0", + "@types/glob": "^7.2.0", "@types/inquirer": "^7.3.1", "@types/minimist": "^1.2.1", "@types/mocha": "^8.2.2", @@ -49,6 +50,7 @@ "eslint-plugin-prettier": "^3.4.0", "ethereum-waffle": "^3.3.0", "form-data": "^4.0.0", + "glob": "^8.0.3", "graphql-tag": "^2.12.4", "hardhat": "^2.9.5", "hardhat-abi-exporter": "^2.2.0", diff --git a/scripts/e2e b/scripts/e2e index 7b90d77dd..adf3c2166 100755 --- a/scripts/e2e +++ b/scripts/e2e @@ -19,12 +19,12 @@ npx hardhat migrate:accounts --network localhost --graph-config config/graph.loc npx hardhat migrate --network localhost --skip-confirmation --graph-config config/graph.localhost.yml # Post deploy actions -npx hardhat migrate:ownership --network localhost -npx hardhat migrate:unpause --network localhost +npx hardhat migrate:ownership --network localhost --graph-config config/graph.localhost.yml +npx hardhat migrate:unpause --network localhost --graph-config config/graph.localhost.yml ### Test # Run tests using the localhost evm instance and the localhost graph config -GRAPH_CONFIG='config/graph.localhost.yml' npx hardhat test e2e/**/*.ts --network localhost +npx hardhat e2e --network localhost --graph-config config/graph.localhost.yml ### Cleanup diff --git a/tasks/deployment/accounts.ts b/tasks/deployment/accounts.ts index 6562104de..da30366b5 100644 --- a/tasks/deployment/accounts.ts +++ b/tasks/deployment/accounts.ts @@ -10,19 +10,22 @@ task('migrate:accounts', '[localhost] Creates protocol accounts and saves them i throw new Error('This task can only be run on localhost network') } - const { graphConfig } = hre.graph({ graphConfig: taskArgs.graphConfig }) + const { graphConfig, getNamedAccounts, getDeployer } = hre.graph({ + graphConfig: taskArgs.graphConfig, + }) console.log('> Generating addresses') - const [ - deployer, + const deployer = await getDeployer() + const { arbitrator, governor, authority, availabilityOracle, pauseGuardian, allocationExchangeOwner, - ] = await hre.ethers.getSigners() + } = await getNamedAccounts() + console.log(await getNamedAccounts()) console.log(`- Deployer: ${deployer.address}`) console.log(`- Arbitrator: ${arbitrator.address}`) diff --git a/tasks/deployment/ownership.ts b/tasks/deployment/ownership.ts index 473ae8cc7..c5a872806 100644 --- a/tasks/deployment/ownership.ts +++ b/tasks/deployment/ownership.ts @@ -7,22 +7,26 @@ task( '[localhost] Accepts ownership of protocol contracts on behalf of governor', ) .addParam('addressBook', cliOpts.addressBook.description, cliOpts.addressBook.default) + .addParam('graphConfig', cliOpts.graphConfig.description, cliOpts.graphConfig.default) .setAction(async (taskArgs, hre) => { if (hre.network.name !== 'localhost') { throw new Error('This task can only be run on localhost network') } - const { contracts } = hre.graph({ addressBook: taskArgs.addressBook }) - const [, , governor] = await hre.ethers.getSigners() + const { contracts, getNamedAccounts } = hre.graph({ + addressBook: taskArgs.addressBook, + graphConfig: taskArgs.graphConfig, + }) + const { governor } = await getNamedAccounts() console.log('> Accepting ownership of contracts') console.log(`- Governor: ${governor.address}`) const txs: ContractTransaction[] = [] - txs.push(await contracts.GraphToken.connect(governor).acceptOwnership()) - txs.push(await contracts.Controller.connect(governor).acceptOwnership()) - txs.push(await contracts.GraphProxyAdmin.connect(governor).acceptOwnership()) - txs.push(await contracts.SubgraphNFT.connect(governor).acceptOwnership()) + txs.push(await contracts.GraphToken.connect(governor.signer).acceptOwnership()) + txs.push(await contracts.Controller.connect(governor.signer).acceptOwnership()) + txs.push(await contracts.GraphProxyAdmin.connect(governor.signer).acceptOwnership()) + txs.push(await contracts.SubgraphNFT.connect(governor.signer).acceptOwnership()) await Promise.all(txs.map((tx) => tx.wait())) console.log('Done!') diff --git a/tasks/deployment/unpause.ts b/tasks/deployment/unpause.ts index 6a9fceecd..fa5767de5 100644 --- a/tasks/deployment/unpause.ts +++ b/tasks/deployment/unpause.ts @@ -3,12 +3,16 @@ import { cliOpts } from '../../cli/defaults' task('migrate:unpause', 'Unpause protocol') .addParam('addressBook', cliOpts.addressBook.description, cliOpts.addressBook.default) + .addParam('graphConfig', cliOpts.graphConfig.description, cliOpts.graphConfig.default) .setAction(async (taskArgs, hre) => { - const { contracts } = hre.graph({ addressBook: taskArgs.addressBook }) - const [, , governor] = await hre.ethers.getSigners() + const { contracts, getNamedAccounts } = hre.graph({ + addressBook: taskArgs.addressBook, + graphConfig: taskArgs.graphConfig, + }) + const { governor } = await getNamedAccounts() console.log('> Unpausing protocol') - const tx = await contracts.Controller.connect(governor).setPaused(false) + const tx = await contracts.Controller.connect(governor.signer).setPaused(false) await tx.wait() console.log('Done!') }) diff --git a/tasks/e2e/e2e.ts b/tasks/e2e/e2e.ts new file mode 100644 index 000000000..117f5ea50 --- /dev/null +++ b/tasks/e2e/e2e.ts @@ -0,0 +1,48 @@ +import { task } from 'hardhat/config' +import { HardhatRuntimeEnvironment, TaskArguments } from 'hardhat/types' +import { TASK_TEST } from 'hardhat/builtin-tasks/task-names' +import glob from 'glob' +import { cliOpts } from '../../cli/defaults' + +const CONFIG_TESTS = 'e2e/deployment/config/*.test.ts' +const INIT_TESTS = 'e2e/deployment/init/*.test.ts' + +// Built-in test task doesn't support our arguments +// we can pass them to GRE via env vars +const setGraphEnvVars = async (args: TaskArguments) => { + process.env.GRAPH_CONFIG = args.graphConfig + process.env.ADDRESS_BOOK = args.addressBook +} + +task('e2e', 'Run all e2e tests') + .addParam('graphConfig', cliOpts.graphConfig.description, cliOpts.graphConfig.default) + .addParam('addressBook', cliOpts.addressBook.description, cliOpts.addressBook.default) + .setAction(async (args, hre: HardhatRuntimeEnvironment) => { + const files = [...new glob.GlobSync(CONFIG_TESTS).found, ...new glob.GlobSync(INIT_TESTS).found] + setGraphEnvVars(args) + await hre.run(TASK_TEST, { + testFiles: files, + }) + }) + +task('e2e:config', 'Run deployment configuration e2e tests') + .addParam('graphConfig', cliOpts.graphConfig.description, cliOpts.graphConfig.default) + .addParam('addressBook', cliOpts.addressBook.description, cliOpts.addressBook.default) + .setAction(async (args, hre: HardhatRuntimeEnvironment) => { + const files = new glob.GlobSync(CONFIG_TESTS).found + setGraphEnvVars(args) + await hre.run(TASK_TEST, { + testFiles: files, + }) + }) + +task('e2e:init', 'Run deployment initialization e2e tests') + .addParam('graphConfig', cliOpts.graphConfig.description, cliOpts.graphConfig.default) + .addParam('addressBook', cliOpts.addressBook.description, cliOpts.addressBook.default) + .setAction(async (args, hre: HardhatRuntimeEnvironment) => { + const files = new glob.GlobSync(INIT_TESTS).found + setGraphEnvVars(args) + await hre.run(TASK_TEST, { + testFiles: files, + }) + }) diff --git a/tasks/gre.ts b/tasks/gre.ts index baced85d9..f2a062747 100644 --- a/tasks/gre.ts +++ b/tasks/gre.ts @@ -1,12 +1,13 @@ import { HardhatRuntimeEnvironment } from 'hardhat/types' import { extendEnvironment } from 'hardhat/config' -import { lazyObject } from 'hardhat/plugins' +import { lazyFunction, lazyObject } from 'hardhat/plugins' import { getAddressBook } from '../cli/address-book' import { loadContracts } from '../cli/contracts' -import { readConfig } from '../cli/config' -import { GREOptions } from './type-extensions' +import { getItemValue, readConfig } from '../cli/config' +import { Account, GREOptions, NamedAccounts } from './type-extensions' import fs from 'fs' +import { Signer, VoidSigner } from 'ethers' // Graph Runtime Environment (GRE) extensions for the HRE extendEnvironment((hre: HardhatRuntimeEnvironment) => { @@ -23,12 +24,57 @@ extendEnvironment((hre: HardhatRuntimeEnvironment) => { throw new Error(`Graph config not found: ${graphConfigPath}`) } + const namedAccountList = [ + 'arbitrator', + 'governor', + 'authority', + 'availabilityOracle', + 'pauseGuardian', + 'allocationExchangeOwner', + ] + + const getTestAccounts = async (): Promise => { + const accounts = [] + const signers: Signer[] = await hre.ethers.getSigners() + + // Skip deployer and named accounts + for (let i = namedAccountList.length + 1; i < signers.length; i++) { + accounts.push({ signer: signers[i], address: await signers[i].getAddress() }) + } + return accounts + } + + // Returns void signers. Upgrades to signer on loca networks. + const getNamedAccounts = async (): Promise => { + const namedAccounts = namedAccountList.reduce((acc, name) => { + const address = getItemValue(readConfig(graphConfigPath, true), `general/${name}`) + + if (chainId === '1337') { + const signer = hre.ethers.provider.getSigner(address) + acc[name] = { signer, address: address } + } else { + const signer = new VoidSigner(address) + acc[name] = { signer, address: signer.address } + } + + return acc + }, {} as NamedAccounts) + + return namedAccounts + } + return { addressBook: lazyObject(() => getAddressBook(addressBookPath, chainId)), graphConfig: lazyObject(() => readConfig(graphConfigPath, true)), contracts: lazyObject(() => loadContracts(getAddressBook(addressBookPath, chainId), hre.ethers.provider), ), + getNamedAccounts: lazyFunction(() => getNamedAccounts), + getTestAccounts: lazyFunction(() => getTestAccounts), + getDeployer: lazyFunction(() => async () => { + const signer = hre.ethers.provider.getSigner(0) + return { signer, address: await signer.getAddress() } + }), } } }) diff --git a/tasks/type-extensions.d.ts b/tasks/type-extensions.d.ts index d1d4a848b..400be724f 100644 --- a/tasks/type-extensions.d.ts +++ b/tasks/type-extensions.d.ts @@ -1,17 +1,35 @@ +import { Signer } from 'ethers' import { AddressBook } from '../cli/address-book' import { NetworkContracts } from '../cli/contracts' -interface GREOptions { +export interface GREOptions { addressBook?: string graphConfig?: string } +export interface Account { + readonly signer: Signer + readonly address: string +} + +export interface NamedAccounts { + arbitrator: Account + governor: Account + authority: Account + availabilityOracle: Account + pauseGuardian: Account + allocationExchangeOwner: Account +} + declare module 'hardhat/types/runtime' { export interface HardhatRuntimeEnvironment { graph: (opts?: GREOptions) => { contracts: NetworkContracts graphConfig: any addressBook: AddressBook + getNamedAccounts: () => Promise + getTestAccounts: () => Promise + getDeployer: () => Promise } } } diff --git a/yarn.lock b/yarn.lock index 798643bbe..6201843bb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1510,7 +1510,7 @@ dependencies: "@types/node" "*" -"@types/glob@^7.1.1": +"@types/glob@^7.1.1", "@types/glob@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb" integrity sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA== @@ -6123,6 +6123,17 @@ glob@^7.0.0, glob@^7.1.2, glob@^7.1.3, glob@^7.1.6, glob@~7.2.0: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^8.0.3: + version "8.0.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-8.0.3.tgz#415c6eb2deed9e502c68fa44a272e6da6eeca42e" + integrity sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^5.0.1" + once "^1.3.0" + global-dirs@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-0.1.1.tgz#b319c0dd4607f353f3be9cca4c72fc148c49f445" @@ -8421,7 +8432,7 @@ minimalistic-crypto-utils@^1.0.1: resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== -minimatch@*: +minimatch@*, minimatch@^5.0.1: version "5.1.0" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.0.tgz#1717b464f4971b144f6aabe8f2d0b8e4511e09c7" integrity sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==