Skip to content

Commit 4953cae

Browse files
authored
feat: e2e protocol deployment testing (#631)
* feat: add deployment e2e tests Signed-off-by: Tomás Migone <[email protected]>
1 parent 6e3b1bb commit 4953cae

31 files changed

+901
-141
lines changed

README.md

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,12 +83,39 @@ Testing is done with the following stack:
8383
- [Typescript](https://www.typescriptlang.org/)
8484
- [Ethers](https://docs.ethers.io/v5/)
8585

86-
To test all files, use `yarn test`. To test a single file run:
86+
## Contracts
87+
To test all the smart contracts, use `yarn test`.
88+
To test a single file run: `npx hardhat test test/<FILE_NAME>.ts`
89+
90+
## E2E Testing
91+
92+
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.
93+
94+
### Hardhat local node
95+
To run e2e tests against a hardhat local node run:
96+
97+
```bash
98+
yarn test:e2e
99+
```
100+
101+
The command will invoke several hardhat tasks, including:
102+
103+
- Start a hardhat node (localhost)
104+
- Run `migrate:accounts` hardhat task to create keys for all protocol roles (deployer, governor, arbiter, etc). This currently doesn't support multisig accounts.
105+
- Run `migrate` hardhat task to deploy the protocol
106+
- Run `migrate:ownership` hardhat task to transfer ownership of governed contracts to the governor
107+
- Run `migrate:unpause` to unpause the protocol
108+
- Run e2e tests
109+
110+
### Other networks
111+
To run tests against a live testnet or even mainnet run:
87112

88113
```bash
89-
npx hardhat test test/<FILE_NAME>.ts
114+
GRAPH_CONFIG=config/graph.<network>.yml ADDRESS_BOOK=addresses.json npx hardhat test e2e/**/*.ts --network <network>
90115
```
91116

117+
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.
118+
92119
# Interacting with the contracts
93120

94121
There are three ways to interact with the contracts through this repo:

addresses.json

Lines changed: 77 additions & 77 deletions
Large diffs are not rendered by default.

cli/config.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import fs from 'fs'
22
import YAML from 'yaml'
3+
import { Scalar, YAMLMap } from 'yaml/types'
34

45
import { AddressBook } from './address-book'
56
import { CLIEnvironment } from './env'
@@ -110,3 +111,44 @@ export function getContractConfig(
110111
proxy,
111112
}
112113
}
114+
115+
// YAML helper functions
116+
const getNode = (doc: YAML.Document.Parsed, path: string[]): YAMLMap => {
117+
try {
118+
let node: YAMLMap
119+
for (const p of path) {
120+
node = node === undefined ? doc.get(p) : node.get(p)
121+
}
122+
return node
123+
} catch (error) {
124+
throw new Error(`Could not find node: ${path}.`)
125+
}
126+
}
127+
128+
function getItem(node: YAMLMap, key: string): Scalar {
129+
if (!node.has(key)) {
130+
throw new Error(`Could not find item: ${key}.`)
131+
}
132+
return node.get(key, true) as Scalar
133+
}
134+
135+
function getItemFromPath(doc: YAML.Document.Parsed, path: string) {
136+
const splitPath = path.split('/')
137+
const itemKey = splitPath.pop()
138+
139+
const node = getNode(doc, splitPath)
140+
const item = getItem(node, itemKey)
141+
return item
142+
}
143+
144+
export const getItemValue = (doc: YAML.Document.Parsed, path: string): any => {
145+
const item = getItemFromPath(doc, path)
146+
return item.value
147+
}
148+
149+
export const updateItemValue = (doc: YAML.Document.Parsed, path: string, value: any): boolean => {
150+
const item = getItemFromPath(doc, path)
151+
const updated = item.value !== value
152+
item.value = value
153+
return updated
154+
}

cli/contracts.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ import { BancorFormula } from '../build/types/BancorFormula'
1818
import { IENS } from '../build/types/IENS'
1919
import { GraphGovernance } from '../build/types/GraphGovernance'
2020
import { AllocationExchange } from '../build/types/AllocationExchange'
21+
import { SubgraphNFT } from '../build/types/SubgraphNFT'
22+
import { GraphCurationToken } from '../build/types/GraphCurationToken'
23+
import { SubgraphNFTDescriptor } from '../build/types/SubgraphNFTDescriptor'
2124

2225
export interface NetworkContracts {
2326
EpochManager: EpochManager
@@ -34,6 +37,9 @@ export interface NetworkContracts {
3437
IENS: IENS
3538
GraphGovernance: GraphGovernance
3639
AllocationExchange: AllocationExchange
40+
SubgraphNFT: SubgraphNFT
41+
SubgraphNFTDescriptor: SubgraphNFTDescriptor
42+
GraphCurationToken: GraphCurationToken
3743
}
3844

3945
export const loadContracts = (

config/graph.goerli.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ general:
22
arbitrator: &arbitrator "0xFD01aa87BeB04D0ac764FC298aCFd05FfC5439cD" # Arbitration Council
33
governor: &governor "0xf1135bFF22512FF2A585b8d4489426CE660f204c" # Graph Council
44
authority: &authority "0x52e498aE9B8A5eE2A5Cd26805F06A9f29A7F489F" # Authority that signs payment vouchers
5+
availabilityOracle: &availabilityOracle "0x14053D40ea2E81D3AB0739728a54ab84F21200F9" # Subgraph Availability Oracle
6+
pauseGuardian: &pauseGuardian "0x6855D551CaDe60754D145fb5eDCD90912D860262" # Protocol pause guardian
7+
allocationExchangeOwner: &allocationExchangeOwner "0xf1135bFF22512FF2A585b8d4489426CE660f204c" # Allocation Exchange owner
58

69
contracts:
710
Controller:
@@ -108,7 +111,7 @@ contracts:
108111
init:
109112
graphToken: "${{GraphToken.address}}"
110113
staking: "${{Staking.address}}"
111-
governor: *governor
114+
governor: *allocationExchangeOwner
112115
authority: *authority
113116
calls:
114117
- fn: "approveAll"

config/graph.localhost.yml

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
general:
2+
arbitrator: &arbitrator "0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0" # Arbitration Council
3+
governor: &governor "0x22d491Bde2303f2f43325b2108D26f1eAbA1e32b" # Governor Council
4+
authority: &authority "0xE11BA2b4D45Eaed5996Cd0823791E0C93114882d" # Authority that signs payment vouchers
5+
availabilityOracle: &availabilityOracle "0xd03ea8624C8C5987235048901fB614fDcA89b117" # Subgraph Availability Oracle
6+
pauseGuardian: &pauseGuardian "0x95cED938F7991cd0dFcb48F0a06a40FA1aF46EBC" # Protocol pause guardian
7+
allocationExchangeOwner: &allocationExchangeOwner "0x3E5e9111Ae8eB78Fe1CC3bb8915d5D461F3Ef9A9" # Allocation Exchange owner
8+
9+
contracts:
10+
Controller:
11+
calls:
12+
- fn: "setContractProxy"
13+
id: "0xe6876326c1291dfcbbd3864a6816d698cd591defc7aa2153d7f9c4c04016c89f" # keccak256('Curation')
14+
contractAddress: "${{Curation.address}}"
15+
- fn: "setContractProxy"
16+
id: "0x39605a6c26a173774ca666c67ef70cf491880e5d3d6d0ca66ec0a31034f15ea3" # keccak256('GNS')
17+
contractAddress: "${{GNS.address}}"
18+
- fn: "setContractProxy"
19+
id: "0xf942813d07d17b56de9a9afc8de0ced6e8c053bbfdcc87b7badea4ddcf27c307" # keccak256('DisputeManager')
20+
contractAddress: "${{DisputeManager.address}}"
21+
- fn: "setContractProxy"
22+
id: "0xc713c3df6d14cdf946460395d09af88993ee2b948b1a808161494e32c5f67063" # keccak256('EpochManager')
23+
contractAddress: "${{EpochManager.address}}"
24+
- fn: "setContractProxy"
25+
id: "0x966f1e8d8d8014e05f6ec4a57138da9be1f7c5a7f802928a18072f7c53180761" # keccak256('RewardsManager')
26+
contractAddress: "${{RewardsManager.address}}"
27+
- fn: "setContractProxy"
28+
id: "0x1df41cd916959d1163dc8f0671a666ea8a3e434c13e40faef527133b5d167034" # keccak256('Staking')
29+
contractAddress: "${{Staking.address}}"
30+
- fn: "setContractProxy"
31+
id: "0x45fc200c7e4544e457d3c5709bfe0d520442c30bbcbdaede89e8d4a4bbc19247" # keccak256('GraphToken')
32+
contractAddress: "${{GraphToken.address}}"
33+
- fn: "setPauseGuardian"
34+
pauseGuardian: *pauseGuardian
35+
- fn: "transferOwnership"
36+
owner: *governor
37+
GraphProxyAdmin:
38+
calls:
39+
- fn: "transferOwnership"
40+
owner: *governor
41+
ServiceRegistry:
42+
proxy: true
43+
init:
44+
controller: "${{Controller.address}}"
45+
EpochManager:
46+
proxy: true
47+
init:
48+
controller: "${{Controller.address}}"
49+
lengthInBlocks: 554 # length in hours = lengthInBlocks*13/60/60 (~13 second blocks)
50+
GraphToken:
51+
init:
52+
initialSupply: "10000000000000000000000000000" # in wei
53+
calls:
54+
- fn: "addMinter"
55+
minter: "${{RewardsManager.address}}"
56+
- fn: "renounceMinter"
57+
- fn: "transferOwnership"
58+
owner: *governor
59+
Curation:
60+
proxy: true
61+
init:
62+
controller: "${{Controller.address}}"
63+
bondingCurve: "${{BancorFormula.address}}"
64+
curationTokenMaster: "${{GraphCurationToken.address}}"
65+
reserveRatio: 500000 # in parts per million
66+
curationTaxPercentage: 10000 # in parts per million
67+
minimumCurationDeposit: "1000000000000000000" # in wei
68+
DisputeManager:
69+
proxy: true
70+
init:
71+
controller: "${{Controller.address}}"
72+
arbitrator: *arbitrator
73+
minimumDeposit: "10000000000000000000000" # in wei
74+
fishermanRewardPercentage: 500000 # in parts per million
75+
idxSlashingPercentage: 25000 # in parts per million
76+
qrySlashingPercentage: 25000 # in parts per million
77+
GNS:
78+
proxy: true
79+
init:
80+
controller: "${{Controller.address}}"
81+
bondingCurve: "${{BancorFormula.address}}"
82+
subgraphNFT: "${{SubgraphNFT.address}}"
83+
calls:
84+
- fn: "approveAll"
85+
SubgraphNFT:
86+
init:
87+
governor: "${{Env.deployer}}"
88+
calls:
89+
- fn: "setTokenDescriptor"
90+
tokenDescriptor: "${{SubgraphNFTDescriptor.address}}"
91+
- fn: "setMinter"
92+
minter: "${{GNS.address}}"
93+
- fn: "transferOwnership"
94+
owner: *governor
95+
Staking:
96+
proxy: true
97+
init:
98+
controller: "${{Controller.address}}"
99+
minimumIndexerStake: "100000000000000000000000" # in wei
100+
thawingPeriod: 6646 # in blocks
101+
protocolPercentage: 10000 # in parts per million
102+
curationPercentage: 100000 # in parts per million
103+
channelDisputeEpochs: 2 # in epochs
104+
maxAllocationEpochs: 4 # in epochs
105+
delegationUnbondingPeriod: 12 # in epochs
106+
delegationRatio: 16 # delegated stake to indexer stake multiplier
107+
rebateAlphaNumerator: 77 # rebateAlphaNumerator / rebateAlphaDenominator
108+
rebateAlphaDenominator: 100 # rebateAlphaNumerator / rebateAlphaDenominator
109+
calls:
110+
- fn: "setDelegationTaxPercentage"
111+
delegationTaxPercentage: 5000 # parts per million
112+
- fn: "setSlasher"
113+
slasher: "${{DisputeManager.address}}"
114+
allowed: true
115+
- fn: "setAssetHolder"
116+
assetHolder: "${{AllocationExchange.address}}"
117+
allowed: true
118+
RewardsManager:
119+
proxy: true
120+
init:
121+
controller: "${{Controller.address}}"
122+
issuanceRate: "1000000012184945188" # per block increase of total supply, blocks in a year = 365*60*60*24/13
123+
calls:
124+
- fn: "setSubgraphAvailabilityOracle"
125+
subgraphAvailabilityOracle: *availabilityOracle
126+
AllocationExchange:
127+
init:
128+
graphToken: "${{GraphToken.address}}"
129+
staking: "${{Staking.address}}"
130+
governor: *allocationExchangeOwner
131+
authority: *authority
132+
calls:
133+
- fn: "approveAll"

config/graph.mainnet.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ general:
22
arbitrator: &arbitrator "0xE1FDD398329C6b74C14cf19100316f0826a492d3" # Arbitration Council
33
governor: &governor "0x48301Fe520f72994d32eAd72E2B6A8447873CF50" # Graph Council
44
authority: &authority "0x982D10c56b8BBbD6e09048F5c5f01b43C65D5aE0" # Authority that signs payment vouchers
5+
availabilityOracle: &availabilityOracle "0x5d3B6F98F1cCdF873Df0173CDE7335874a396c4d" # Subgraph Availability Oracle
6+
pauseGuardian: &pauseGuardian "0x8290362Aba20D17c51995085369E001Bad99B21c" # Protocol pause guardian
7+
allocationExchangeOwner: &allocationExchangeOwner "0x74Db79268e63302d3FC69FB5a7627F7454a41732" # Allocation Exchange owner
58

69
contracts:
710
Controller:
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { expect } from 'chai'
2+
import hre from 'hardhat'
3+
import { getItemValue } from '../../cli/config'
4+
5+
describe('AllocationExchange deployment', () => {
6+
const {
7+
graphConfig,
8+
contracts: { AllocationExchange, GraphToken, Staking },
9+
} = hre.graph()
10+
11+
it('should be owned by allocationExchangeOwner', async function () {
12+
const owner = await AllocationExchange.governor()
13+
const allocationExchangeOwner = getItemValue(graphConfig, 'general/allocationExchangeOwner')
14+
expect(owner).eq(allocationExchangeOwner)
15+
})
16+
17+
it('should accept vouchers from authority', async function () {
18+
const authorityAddress = getItemValue(graphConfig, 'general/authority')
19+
const allowed = await AllocationExchange.authority(authorityAddress)
20+
expect(allowed).eq(true)
21+
})
22+
23+
// graphToken and staking are private variables so we can't verify
24+
it.skip('graphToken should match the GraphToken deployment address')
25+
it.skip('staking should match the Staking deployment address')
26+
27+
it('should allow Staking contract to spend MAX_UINT256 tokens on AllocationExchange behalf', async function () {
28+
const allowance = await GraphToken.allowance(AllocationExchange.address, Staking.address)
29+
expect(allowance).eq(hre.ethers.constants.MaxUint256)
30+
})
31+
})

e2e/deployment/controller.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { expect } from 'chai'
2+
import hre, { ethers } from 'hardhat'
3+
import { getItemValue } from '../../cli/config'
4+
5+
describe('Controller deployment', () => {
6+
const { contracts, graphConfig } = hre.graph()
7+
const { Controller } = contracts
8+
9+
const proxyContracts = [
10+
'Curation',
11+
'GNS',
12+
'DisputeManager',
13+
'EpochManager',
14+
'RewardsManager',
15+
'Staking',
16+
'GraphToken',
17+
]
18+
19+
const proxyShouldMatchDeployed = async (contractName: string) => {
20+
const curationAddress = await Controller.getContractProxy(
21+
ethers.utils.solidityKeccak256(['string'], [contractName]),
22+
)
23+
expect(curationAddress).eq(contracts[contractName].address)
24+
}
25+
26+
it('should be owned by governor', async function () {
27+
const owner = await Controller.governor()
28+
const governorAddress = getItemValue(graphConfig, 'general/governor')
29+
expect(owner).eq(governorAddress)
30+
})
31+
32+
it('pause guardian should be able to pause protocol', async function () {
33+
const pauseGuardianAddress = getItemValue(graphConfig, 'general/pauseGuardian')
34+
const pauseGuardian = await Controller.pauseGuardian()
35+
expect(pauseGuardian).eq(pauseGuardianAddress)
36+
})
37+
38+
describe('proxy contract', async function () {
39+
for (const contract of proxyContracts) {
40+
it(`${contract} should match deployed`, async function () {
41+
await proxyShouldMatchDeployed(contract)
42+
})
43+
}
44+
})
45+
})

e2e/deployment/curation.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { expect } from 'chai'
2+
import hre from 'hardhat'
3+
import { getItemValue } from '../../cli/config'
4+
5+
describe('Curation deployment', () => {
6+
const {
7+
graphConfig,
8+
contracts: { Controller, Curation, BancorFormula, GraphCurationToken },
9+
} = hre.graph()
10+
11+
it('should be controlled by Controller', async function () {
12+
const controller = await Curation.controller()
13+
expect(controller).eq(Controller.address)
14+
})
15+
16+
it('bondingCurve should match the BancorFormula deployment address', async function () {
17+
const bondingCurve = await Curation.bondingCurve()
18+
expect(bondingCurve).eq(BancorFormula.address)
19+
})
20+
21+
it('curationTokenMaster should match the GraphCurationToken deployment address', async function () {
22+
const bondingCurve = await Curation.curationTokenMaster()
23+
expect(bondingCurve).eq(GraphCurationToken.address)
24+
})
25+
26+
it('defaultReserveRatio should match "reserveRatio" in the config file', async function () {
27+
const value = await Curation.defaultReserveRatio()
28+
const expected = getItemValue(graphConfig, 'contracts/Curation/init/reserveRatio')
29+
expect(value).eq(expected)
30+
})
31+
32+
it('curationTaxPercentage should match "curationTaxPercentage" in the config file', async function () {
33+
const value = await Curation.curationTaxPercentage()
34+
const expected = getItemValue(graphConfig, 'contracts/Curation/init/curationTaxPercentage')
35+
expect(value).eq(expected)
36+
})
37+
38+
it('minimumCurationDeposit should match "minimumCurationDeposit" in the config file', async function () {
39+
const value = await Curation.minimumCurationDeposit()
40+
const expected = getItemValue(graphConfig, 'contracts/Curation/init/minimumCurationDeposit')
41+
expect(value).eq(expected)
42+
})
43+
})

0 commit comments

Comments
 (0)