diff --git a/.env.example b/.env.example index 031a45459..5e072b164 100644 --- a/.env.example +++ b/.env.example @@ -2,5 +2,5 @@ MNEMONIC= ETHERSCAN_API_KEY= INFURA_KEY= ADDRESS_BOOK="addresses.json" -GRAPH_CONFIG=""graph.config.yml"" +GRAPH_CONFIG=""config/graph.mainnet.yml"" PROVIDER_URL="http://localhost:8545" \ No newline at end of file diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 5b6dc9ed3..f7c11977c 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -49,12 +49,12 @@ To deploy using your own wallet add the HD Wallet Config to the `hardhat.config. ### Configuration -A configuration file called `graph.config.yml` contains the parameters needed to deploy the contracts. Please edit these params as you see fit. +A configuration file called `graph..yml` located in the `config` folder contains the parameters needed to deploy the contracts. Please edit these params as you see fit. You can use a different set of configuration options by specifying the file location in the command line: ``` -yarn deploy -- --graph-config another-graph.config.yml +yarn deploy -- --graph-config another-graph.mainnet.yml ``` Rules: @@ -67,7 +67,7 @@ Rules: Example: -[https://github.com/graphprotocol/contracts/blob/master/graph.config.yml](https://github.com/graphprotocol/contracts/blob/master/graph.config.yml) +[https://github.com/graphprotocol/contracts/blob/master/config/graph.mainnet.yml](https://github.com/graphprotocol/contracts/blob/master/config/graph.mainnet.yml) ### Address book diff --git a/cli/README.md b/cli/README.md index d191ca802..fbd31b21a 100644 --- a/cli/README.md +++ b/cli/README.md @@ -11,7 +11,7 @@ MNEMONIC= ETHERSCAN_API_KEY= INFURA_KEY= ADDRESS_BOOK="addresses.json" -GRAPH_CONFIG=""graph.config.yml"" +GRAPH_CONFIG=""config/graph.mainnet.yml"" PROVIDER_URL="http://localhost:8545" ``` diff --git a/cli/config.ts b/cli/config.ts index bcb05bf40..970246fa8 100644 --- a/cli/config.ts +++ b/cli/config.ts @@ -42,9 +42,13 @@ function parseAddressBookRef(addressBook: AddressBook, value: string, cli: CLIEn } // eslint-disable-next-line @typescript-eslint/no-explicit-any -export function readConfig(path: string): any { +export function readConfig(path: string, retainMetadata = false): any { const file = fs.readFileSync(path, 'utf8') - return YAML.parse(file) + return retainMetadata ? YAML.parseDocument(file) : YAML.parse(file) +} + +export function writeConfig(path: string, data: string): void { + fs.writeFileSync(path, data) } export function loadCallParams( diff --git a/cli/contracts.ts b/cli/contracts.ts index 187ae8e97..fbe79062f 100644 --- a/cli/contracts.ts +++ b/cli/contracts.ts @@ -18,6 +18,7 @@ import { BancorFormula } from '../build/types/BancorFormula' import { IENS } from '../build/types/IENS' import { IEthereumDIDRegistry } from '../build/types/IEthereumDIDRegistry' import { GraphGovernance } from '../build/types/GraphGovernance' +import { AllocationExchange } from '../build/types/AllocationExchange' export interface NetworkContracts { EpochManager: EpochManager @@ -34,6 +35,7 @@ export interface NetworkContracts { IENS: IENS IEthereumDIDRegistry: IEthereumDIDRegistry GraphGovernance: GraphGovernance + AllocationExchange: AllocationExchange } export const loadContracts = ( diff --git a/cli/defaults.ts b/cli/defaults.ts index 3fde113c8..c1be71eb2 100644 --- a/cli/defaults.ts +++ b/cli/defaults.ts @@ -5,7 +5,7 @@ export const local = { mnemonic: 'myth like bonus scare over problem client lizard pioneer submit female collect', providerUrl: 'http://localhost:8545', addressBookPath: './addresses.json', - graphConfigPath: './graph.config.yml', + graphConfigPath: './config/graph.mainnet.yml', accountNumber: '0', } diff --git a/graph.config.yml b/config/graph.mainnet.yml similarity index 70% rename from graph.config.yml rename to config/graph.mainnet.yml index e3d6937ba..72cadcc67 100644 --- a/graph.config.yml +++ b/config/graph.mainnet.yml @@ -1,7 +1,7 @@ general: arbitrator: &arbitrator "0xE1FDD398329C6b74C14cf19100316f0826a492d3" # Arbitration Council governor: &governor "0x48301Fe520f72994d32eAd72E2B6A8447873CF50" # Graph Council - authority: &authority "0x79fd74da4c906509862c8fe93e87a9602e370bc4" # Authority that signs payment vouchers + authority: &authority "0x982D10c56b8BBbD6e09048F5c5f01b43C65D5aE0" # Authority that signs payment vouchers contracts: Controller: @@ -35,10 +35,10 @@ contracts: proxy: true init: controller: "${{Controller.address}}" - lengthInBlocks: 1108 # 4 hours (in 13 second blocks) + lengthInBlocks: 6646 # length in hours = lengthInBlocks*13/60/60 (~13 second blocks) GraphToken: init: - initialSupply: "10000000000000000000000000000" # 10,000,000,000 GRT + initialSupply: "10000000000000000000000000000" # in wei calls: - fn: "addMinter" minter: "${{RewardsManager.address}}" @@ -48,18 +48,18 @@ contracts: controller: "${{Controller.address}}" bondingCurve: "${{BancorFormula.address}}" curationTokenMaster: "${{GraphCurationToken.address}}" - reserveRatio: 500000 # 50% (parts per million) - curationTaxPercentage: 10000 # 1% (parts per million) - minimumCurationDeposit: "1000000000000000000" # 1 GRT + reserveRatio: 500000 # in parts per million + curationTaxPercentage: 10000 # in parts per million + minimumCurationDeposit: "1000000000000000000" # in wei DisputeManager: proxy: true init: controller: "${{Controller.address}}" arbitrator: *arbitrator - minimumDeposit: "10000000000000000000000" # 10,000 GRT (in wei) - fishermanRewardPercentage: 500000 # 50% (parts per million) - idxSlashingPercentage: 25000 # 2.5% (parts per million) - qrySlashingPercentage: 5000 # 0.5% (parts per million) + minimumDeposit: "10000000000000000000000" # in wei + fishermanRewardPercentage: 500000 # in parts per million + idxSlashingPercentage: 25000 # in parts per million + qrySlashingPercentage: 25000 # in parts per million GNS: proxy: true init: @@ -80,19 +80,19 @@ contracts: proxy: true init: controller: "${{Controller.address}}" - minimumIndexerStake: "100000000000000000000000" # 100,000 GRT (in wei) - thawingPeriod: 6646 # 10 days (in blocks) - protocolPercentage: 10000 # 1% (parts per million) - curationPercentage: 100000 # 10% (parts per million) - channelDisputeEpochs: 2 # (in epochs) - maxAllocationEpochs: 6 # Based on epoch length this is 28 days (in epochs) - delegationUnbondingPeriod: 6 # Based on epoch length this is 28 days (in epochs) - delegationRatio: 16 # 16x (delegated stake to indexer stake multiplier) + minimumIndexerStake: "100000000000000000000000" # in wei + thawingPeriod: 186092 # in blocks + protocolPercentage: 10000 # in parts per million + curationPercentage: 100000 # in parts per million + channelDisputeEpochs: 7 # in epochs + maxAllocationEpochs: 28 # in epochs + delegationUnbondingPeriod: 28 # in epochs + delegationRatio: 16 # delegated stake to indexer stake multiplier rebateAlphaNumerator: 77 # rebateAlphaNumerator / rebateAlphaDenominator rebateAlphaDenominator: 100 # rebateAlphaNumerator / rebateAlphaDenominator calls: - fn: "setDelegationTaxPercentage" - delegationTaxPercentage: 5000 # 0.5% (parts per million) + delegationTaxPercentage: 5000 # parts per million - fn: "setSlasher" slasher: "${{DisputeManager.address}}" allowed: true @@ -103,7 +103,7 @@ contracts: proxy: true init: controller: "${{Controller.address}}" - issuanceRate: "1000000012184945188" # 3% annual rate (per block increase of total supply, blocks in a year = 365*60*60*24/13) + issuanceRate: "1000000012184945188" # per block increase of total supply, blocks in a year = 365*60*60*24/13 AllocationExchange: init: graphToken: "${{GraphToken.address}}" diff --git a/config/graph.rinkeby.yml b/config/graph.rinkeby.yml new file mode 100644 index 000000000..bd8ab6bf2 --- /dev/null +++ b/config/graph.rinkeby.yml @@ -0,0 +1,114 @@ +general: + arbitrator: &arbitrator "0x87D11BD744b882b7bc5A6b5450cbA8C35D90eb10" # Arbitration Council + governor: &governor "0x1679A1D1caf1252BA43Fb8Fc17ebF914a0C725AE" # Graph Council + authority: &authority "0xe1EC4339019eC9628438F8755f847e3023e4ff9c" # Authority that signs payment vouchers + +contracts: + Controller: + calls: + - fn: "setContractProxy" + id: "0xe6876326c1291dfcbbd3864a6816d698cd591defc7aa2153d7f9c4c04016c89f" # keccak256('Curation') + contractAddress: "${{Curation.address}}" + - fn: "setContractProxy" + id: "0x39605a6c26a173774ca666c67ef70cf491880e5d3d6d0ca66ec0a31034f15ea3" # keccak256('GNS') + contractAddress: "${{GNS.address}}" + - fn: "setContractProxy" + id: "0xf942813d07d17b56de9a9afc8de0ced6e8c053bbfdcc87b7badea4ddcf27c307" # keccak256('DisputeManager') + contractAddress: "${{DisputeManager.address}}" + - fn: "setContractProxy" + id: "0xc713c3df6d14cdf946460395d09af88993ee2b948b1a808161494e32c5f67063" # keccak256('EpochManager') + contractAddress: "${{EpochManager.address}}" + - fn: "setContractProxy" + id: "0x966f1e8d8d8014e05f6ec4a57138da9be1f7c5a7f802928a18072f7c53180761" # keccak256('RewardsManager') + contractAddress: "${{RewardsManager.address}}" + - fn: "setContractProxy" + id: "0x1df41cd916959d1163dc8f0671a666ea8a3e434c13e40faef527133b5d167034" # keccak256('Staking') + contractAddress: "${{Staking.address}}" + - fn: "setContractProxy" + id: "0x45fc200c7e4544e457d3c5709bfe0d520442c30bbcbdaede89e8d4a4bbc19247" # keccak256('GraphToken') + contractAddress: "${{GraphToken.address}}" + ServiceRegistry: + proxy: true + init: + controller: "${{Controller.address}}" + EpochManager: + proxy: true + init: + controller: "${{Controller.address}}" + lengthInBlocks: 277 # length in hours = lengthInBlocks*13/60/60 (~13 second blocks) + GraphToken: + init: + initialSupply: "10000000000000000000000000000" # in wei + calls: + - fn: "addMinter" + minter: "${{RewardsManager.address}}" + Curation: + proxy: true + init: + controller: "${{Controller.address}}" + bondingCurve: "${{BancorFormula.address}}" + curationTokenMaster: "${{GraphCurationToken.address}}" + reserveRatio: 500000 # in parts per million + curationTaxPercentage: 10000 # in parts per million + minimumCurationDeposit: "1000000000000000000" # in wei + DisputeManager: + proxy: true + init: + controller: "${{Controller.address}}" + arbitrator: *arbitrator + minimumDeposit: "10000000000000000000000" # in wei + fishermanRewardPercentage: 500000 # in parts per million + idxSlashingPercentage: 20000 # in parts per million + qrySlashingPercentage: 5000 # in parts per million + GNS: + proxy: true + init: + controller: "${{Controller.address}}" + bondingCurve: "${{BancorFormula.address}}" + subgraphNFT: "${{SubgraphNFT.address}}" + calls: + - fn: "approveAll" + SubgraphNFT: + init: + governor: "${{Env.deployer}}" + calls: + - fn: "setTokenDescriptor" + tokenDescriptor: "${{SubgraphNFTDescriptor.address}}" + - fn: "setMinter" + minter: "${{GNS.address}}" + Staking: + proxy: true + init: + controller: "${{Controller.address}}" + minimumIndexerStake: "100000000000000000000000" # in wei + thawingPeriod: 6646 # in blocks + protocolPercentage: 10000 # in parts per million + curationPercentage: 100000 # in parts per million + channelDisputeEpochs: 2 # in epochs + maxAllocationEpochs: 2 # in epochs + delegationUnbondingPeriod: 6 # in epochs + delegationRatio: 16 # delegated stake to indexer stake multiplier + rebateAlphaNumerator: 77 # rebateAlphaNumerator / rebateAlphaDenominator + rebateAlphaDenominator: 100 # rebateAlphaNumerator / rebateAlphaDenominator + calls: + - fn: "setDelegationTaxPercentage" + delegationTaxPercentage: 5000 # parts per million + - fn: "setSlasher" + slasher: "${{DisputeManager.address}}" + allowed: true + - fn: "setAssetHolder" + assetHolder: "${{AllocationExchange.address}}" + allowed: true + RewardsManager: + proxy: true + init: + controller: "${{Controller.address}}" + issuanceRate: "1000000012184945188" # per block increase of total supply, blocks in a year = 365*60*60*24/13 + AllocationExchange: + init: + graphToken: "${{GraphToken.address}}" + staking: "${{Staking.address}}" + governor: *governor + authority: *authority + calls: + - fn: "approveAll" diff --git a/package.json b/package.json index 8cab599c9..5d9d07bfe 100644 --- a/package.json +++ b/package.json @@ -81,9 +81,9 @@ "compile": "hardhat compile", "deploy": "scripts/predeploy && hardhat migrate", "deploy-ganache": "yarn deploy -- --force", - "deploy-ganache-manual": "yarn deploy -- --network ganache --force", - "deploy-hardhat": "yarn deploy -- --network hardhat --force", - "deploy-rinkeby": "yarn deploy -- --force --network rinkeby", + "deploy-ganache-manual": "yarn deploy -- --force --network ganache", + "deploy-hardhat": "yarn deploy -- --force --network hardhat", + "deploy-rinkeby": "yarn deploy -- --force --network rinkeby --graph-config config/graph.rinkeby.yml", "predeploy": "scripts/predeploy", "test": "scripts/test", "test:gas": "RUN_EVM=true REPORT_GAS=true scripts/test", diff --git a/tasks/deployment/config.ts b/tasks/deployment/config.ts new file mode 100644 index 000000000..b7aa0b823 --- /dev/null +++ b/tasks/deployment/config.ts @@ -0,0 +1,209 @@ +import { task } from 'hardhat/config' +import '@nomiclabs/hardhat-ethers' +import { cliOpts } from '../../cli/defaults' +import { readConfig, writeConfig } from '../../cli/config' +import YAML from 'yaml' + +import { Scalar, YAMLMap } from 'yaml/types' +import { HardhatRuntimeEnvironment } from 'hardhat/types' +import fs from 'fs' +import inquirer from 'inquirer' + +interface Contract { + name: string + initParams: ContractInitParam[] +} + +interface GeneralParam { + contract: string // contract where the param is defined + name: string // name of the parameter +} + +interface ContractInitParam { + name: string // as declared in config.yml + type: 'number' | 'BigNumber' // as returned by the contract + getter?: string // name of function to get the value from the contract. Defaults to 'name' + format?: 'number' // some parameters are stored in different formats than what the contract reports. +} + +const epochManager: Contract = { + name: 'EpochManager', + initParams: [ + { name: 'lengthInBlocks', type: 'BigNumber', getter: 'epochLength', format: 'number' }, + ], +} + +const curation: Contract = { + name: 'Curation', + initParams: [ + { name: 'reserveRatio', type: 'number', getter: 'defaultReserveRatio' }, + { name: 'curationTaxPercentage', type: 'number' }, + { name: 'minimumCurationDeposit', type: 'BigNumber' }, + ], +} + +const disputeManager: Contract = { + name: 'DisputeManager', + initParams: [ + { name: 'minimumDeposit', type: 'BigNumber' }, + { name: 'fishermanRewardPercentage', type: 'number' }, + { name: 'idxSlashingPercentage', type: 'number' }, + { name: 'qrySlashingPercentage', type: 'number' }, + ], +} + +const staking: Contract = { + name: 'Staking', + initParams: [ + { name: 'minimumIndexerStake', type: 'BigNumber' }, + { name: 'thawingPeriod', type: 'number' }, + { name: 'protocolPercentage', type: 'number' }, + { name: 'curationPercentage', type: 'number' }, + { name: 'channelDisputeEpochs', type: 'number' }, + { name: 'maxAllocationEpochs', type: 'number' }, + { name: 'delegationUnbondingPeriod', type: 'number' }, + { name: 'delegationRatio', type: 'number' }, + { name: 'rebateAlphaNumerator', type: 'number', getter: 'alphaNumerator' }, + { name: 'rebateAlphaDenominator', type: 'number', getter: 'alphaDenominator' }, + ], +} + +const rewardsManager: Contract = { + name: 'RewardsManager', + initParams: [{ name: 'issuanceRate', type: 'BigNumber' }], +} + +const contractList: Contract[] = [epochManager, curation, disputeManager, staking, rewardsManager] + +const generalParams: GeneralParam[] = [ + { + contract: 'DisputeManager', + name: 'arbitrator', + }, + { + contract: 'Controller', + name: 'governor', + }, + { + contract: 'AllocationExchange', + name: 'authority', + }, +] + +task('update-config', 'Update graph config parameters with onchain data') + .addParam('graphConfig', cliOpts.graphConfig.description, cliOpts.graphConfig.default) + .addFlag('dryRun', "Only print the changes, don't write them to the config file") + .setAction(async (taskArgs, hre) => { + const networkName = hre.network.name + const configFile = taskArgs.graphConfig + const dryRun = taskArgs.dryRun + + if (!fs.existsSync(configFile)) { + throw new Error(`Could not find config file: ${configFile}`) + } + + console.log('## Update graph config ##') + console.log(`Network: ${networkName}`) + console.log(`Config file: ${configFile}\n`) + + // Prompt to avoid accidentally overwriting the config file with data from another network + if (!configFile.includes(networkName)) { + const res = await inquirer.prompt({ + name: 'confirm', + type: 'confirm', + default: false, + message: `Config file ${configFile} doesn't match 'graph..yml'. Are you sure you want to continue?`, + }) + if (!res.confirm) { + return + } + } + + const graphConfig = readConfig(configFile, true) + + // general parameters + console.log(`> General`) + for (const param of generalParams) { + await updateGeneralParams(hre, param, graphConfig) + } + + // contracts parameters + for (const contract of contractList) { + console.log(`> ${contract.name}`) + await updateContractParams(hre, contract, graphConfig) + } + + if (dryRun) { + console.log('\n Dry run enabled, printing changes to console (no files updated)\n') + console.log(graphConfig.toString()) + } else { + writeConfig(configFile, graphConfig.toString()) + } + }) + +const updateGeneralParams = async ( + hre: HardhatRuntimeEnvironment, + param: GeneralParam, + config: YAML.Document.Parsed, +) => { + const value = await hre.contracts[param.contract][param.name]() + const updated = updateItem(config, `general/${param.name}`, value) + if (updated) { + console.log(`\t- Updated ${param.name} to ${value}`) + } +} + +const updateContractParams = async ( + hre: HardhatRuntimeEnvironment, + contract: Contract, + config: YAML.Document.Parsed, +) => { + for (const param of contract.initParams) { + let value = await hre.contracts[contract.name][param.getter ?? param.name]() + if (param.type === 'BigNumber') { + if (param.format === 'number') { + value = value.toNumber() + } else { + value = value.toString() + } + } + + const updated = updateItem(config, `contracts/${contract.name}/init/${param.name}`, value) + if (updated) { + console.log(`\t- Updated ${param.name} to ${value}`) + } + } +} + +// YAML helper functions +const getNode = (doc: YAML.Document.Parsed, path: string[]): YAMLMap => { + try { + let node: YAMLMap + for (const p of path) { + node = node === undefined ? doc.get(p) : node.get(p) + } + return node + } catch (error) { + throw new Error(`Could not find node: ${path}.`) + } +} + +const getItem = (node: YAMLMap, key: string): Scalar => { + if (!node.has(key)) { + throw new Error(`Could not find item: ${key}.`) + } + return node.get(key, true) as Scalar +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const updateItem = (doc: YAML.Document.Parsed, path: string, value: any): boolean => { + const splitPath = path.split('/') + const itemKey = splitPath.pop() + + const node = getNode(doc, splitPath) + const item = getItem(node, itemKey) + + const updated = item.value !== value + item.value = value + return updated +}