From 8c8f6aab8579a8083a7d7cf4131cabf1511a7139 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Migone?= Date: Thu, 22 Sep 2022 17:16:59 -0400 Subject: [PATCH] Sync: l2-testnet > pcv/l2-bridge (#704) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Migone Co-authored-by: Pablo Carranza Vélez Co-authored-by: Ariel Barmat --- TESTING.md | 24 ++- cli/commands/bridge/to-l1.ts | 25 ++- cli/commands/bridge/to-l2.ts | 152 +++++++++++------- cli/commands/migrate.ts | 14 +- cli/commands/protocol/configure-bridge.ts | 2 +- cli/contracts.ts | 2 +- cli/cross-chain.ts | 63 ++++++++ cli/env.ts | 2 +- cli/network.ts | 26 +++ cli/utils.ts | 30 ---- config/graph.arbitrum-goerli.yml | 140 ++++++++++++++++ ...nkeby.yml => graph.arbitrum-localhost.yml} | 54 +++++-- config/graph.arbitrum-one.yml | 59 ++++--- config/graph.goerli.yml | 11 ++ config/graph.localhost.yml | 11 ++ e2e/deployment/config/controller.test.ts | 36 ++++- e2e/deployment/config/graphToken.test.ts | 2 +- e2e/deployment/config/l1/bridgeEscrow.test.ts | 17 ++ .../config/l1/l1GraphTokenGateway.test.ts | 83 ++++++++++ .../config/l1/rewardsManager.test.ts | 17 ++ e2e/deployment/config/l2/l2GraphToken.test.ts | 39 +++++ .../config/l2/l2GraphTokenGateway.test.ts | 61 +++++++ .../config/l2/rewardsManager.test.ts | 17 ++ e2e/deployment/config/protocol.test.ts | 11 -- e2e/deployment/config/rewardsManager.test.ts | 5 - e2e/deployment/init/graphToken.test.ts | 16 -- e2e/deployment/init/l1/graphToken.test.ts | 19 +++ e2e/deployment/init/l2/graphToken.test.ts | 17 ++ e2e/scenarios/lib/accounts.ts | 16 +- e2e/scenarios/lib/staking.ts | 4 +- gre/config.ts | 28 +++- gre/helpers/network.ts | 24 +++ gre/test/config.test.ts | 38 ++++- .../hardhat.config.ts | 59 +++++++ gre/test/helpers.ts | 1 + hardhat.config.ts | 10 ++ package.json | 3 +- scripts/e2e | 62 ++++--- tasks/deployment/accounts.ts | 67 ++++++++ tasks/deployment/ownership.ts | 13 +- tasks/deployment/sync.ts | 71 ++++++++ tasks/deployment/unpause.ts | 18 ++- yarn.lock | 8 +- 43 files changed, 1146 insertions(+), 231 deletions(-) create mode 100644 cli/cross-chain.ts delete mode 100644 cli/utils.ts create mode 100644 config/graph.arbitrum-goerli.yml rename config/{graph.rinkeby.yml => graph.arbitrum-localhost.yml} (66%) create mode 100644 e2e/deployment/config/l1/bridgeEscrow.test.ts create mode 100644 e2e/deployment/config/l1/l1GraphTokenGateway.test.ts create mode 100644 e2e/deployment/config/l1/rewardsManager.test.ts create mode 100644 e2e/deployment/config/l2/l2GraphToken.test.ts create mode 100644 e2e/deployment/config/l2/l2GraphTokenGateway.test.ts create mode 100644 e2e/deployment/config/l2/rewardsManager.test.ts delete mode 100644 e2e/deployment/config/protocol.test.ts delete mode 100644 e2e/deployment/init/graphToken.test.ts create mode 100644 e2e/deployment/init/l1/graphToken.test.ts create mode 100644 e2e/deployment/init/l2/graphToken.test.ts create mode 100644 gre/test/fixture-projects/graph-config-desambiguate/hardhat.config.ts create mode 100644 tasks/deployment/sync.ts diff --git a/TESTING.md b/TESTING.md index e9ac1b0e9..88b72a31c 100644 --- a/TESTING.md +++ b/TESTING.md @@ -82,4 +82,26 @@ Scenarios are defined by an optional script and a test file: - They run before the test file. - Test file - Should be named e2e/scenarios/{scenario-name}.test.ts. - - Standard chai/mocha/hardhat/ethers test file. \ No newline at end of file + - Standard chai/mocha/hardhat/ethers test file. + +## Setting up Arbitrum's testnodes + +Arbitrum provides a quick way of setting up L1 and L2 testnodes for local development and testing. The following steps will guide you through the process of setting them up. Note that a local installation of Docker and Docker Compose is required. + +```bash +git clone https://github.com/offchainlabs/nitro +cd nitro +git submodule update --init --recursive + +# Apply any changes you might want, see below for more info, and then start the testnodes +./test-node.bash --init +``` + +**Useful information** +- L1 RPC: [http://localhost:8545](http://localhost:8545/) +- L2 RPC: [http://localhost:8547](http://localhost:8547/) +- Blockscout explorer (L2 only): [http://localhost:4000/](http://localhost:4000/) +- Prefunded genesis key (L1 and L2): `e887f7d17d07cc7b8004053fb8826f6657084e88904bb61590e498ca04704cf2` + +**Enable automine on L1** +In `docker-compose.yml` file edit the `geth` service command by removing the `--dev.period 1` flag. \ No newline at end of file diff --git a/cli/commands/bridge/to-l1.ts b/cli/commands/bridge/to-l1.ts index f3673a53a..42f319eb4 100644 --- a/cli/commands/bridge/to-l1.ts +++ b/cli/commands/bridge/to-l1.ts @@ -2,18 +2,14 @@ import { loadEnv, CLIArgs, CLIEnvironment } from '../../env' import { logger } from '../../logging' import { getAddressBook } from '../../address-book' import { getProvider, sendTransaction, toGRT } from '../../network' -import { chainIdIsL2 } from '../../utils' +import { chainIdIsL2 } from '../../cross-chain' import { loadAddressBookContract } from '../../contracts' -import { - L2TransactionReceipt, - getL2Network, - L2ToL1MessageStatus, - L2ToL1MessageWriter, -} from '@arbitrum/sdk' +import { L2TransactionReceipt, L2ToL1MessageStatus, L2ToL1MessageWriter } from '@arbitrum/sdk' import { L2GraphTokenGateway } from '../../../build/types/L2GraphTokenGateway' import { BigNumber } from 'ethers' import { JsonRpcProvider } from '@ethersproject/providers' import { providers } from 'ethers' +import { L2GraphToken } from '../../../build/types/L2GraphToken' const FOURTEEN_DAYS_IN_SECONDS = 24 * 3600 * 14 @@ -82,12 +78,17 @@ export const startSendToL1 = async (cli: CLIEnvironment, cliArgs: CLIArgs): Prom const l2AddressBook = getAddressBook(cliArgs.addressBook, l2ChainId.toString()) const gateway = loadAddressBookContract('L2GraphTokenGateway', l2AddressBook, l2Wallet) - const l2GRT = loadAddressBookContract('L2GraphToken', l2AddressBook, l2Wallet) + const l2GRT = loadAddressBookContract('L2GraphToken', l2AddressBook, l2Wallet) as L2GraphToken const l1Gateway = cli.contracts['L1GraphTokenGateway'] logger.info(`Will send ${cliArgs.amount} GRT to ${recipient}`) logger.info(`Using L2 gateway ${gateway.address} and L1 gateway ${l1Gateway.address}`) + const senderBalance = await l2GRT.balanceOf(cli.wallet.address) + if (senderBalance.lt(amount)) { + throw new Error('Sender balance is insufficient for the transfer') + } + const params = [l1GRTAddress, recipient, amount, '0x'] logger.info('Approving token transfer') await sendTransaction(l2Wallet, l2GRT, 'approve', [gateway.address, amount]) @@ -99,9 +100,7 @@ export const startSendToL1 = async (cli: CLIEnvironment, cliArgs: CLIArgs): Prom params, ) const l2Receipt = new L2TransactionReceipt(receipt) - const l2ToL1Message = ( - await l2Receipt.getL2ToL1Messages(cli.wallet, await getL2Network(l2Provider)) - )[0] + const l2ToL1Message = (await l2Receipt.getL2ToL1Messages(cli.wallet))[0] logger.info(`The transaction generated an L2 to L1 message in outbox with eth block number:`) logger.info(l2ToL1Message.event.ethBlockNum.toString()) @@ -157,9 +156,7 @@ export const finishSendToL1 = async ( const l2Receipt = new L2TransactionReceipt(receipt) logger.info(`Getting L2 to L1 message...`) - const l2ToL1Message = ( - await l2Receipt.getL2ToL1Messages(cli.wallet, await getL2Network(l2Provider)) - )[0] + const l2ToL1Message = (await l2Receipt.getL2ToL1Messages(cli.wallet))[0] if (wait) { const retryDelayMs = cliArgs.retryDelaySeconds ? cliArgs.retryDelaySeconds * 1000 : 60000 diff --git a/cli/commands/bridge/to-l2.ts b/cli/commands/bridge/to-l2.ts index f176bc482..307886e30 100644 --- a/cli/commands/bridge/to-l2.ts +++ b/cli/commands/bridge/to-l2.ts @@ -1,15 +1,11 @@ +import { Argv } from 'yargs' +import { utils } from 'ethers' +import { L1TransactionReceipt, L1ToL2MessageStatus, L1ToL2MessageWriter } from '@arbitrum/sdk' + import { loadEnv, CLIArgs, CLIEnvironment } from '../../env' import { logger } from '../../logging' -import { getProvider, sendTransaction, toGRT } from '../../network' -import { utils } from 'ethers' -import { parseEther } from '@ethersproject/units' -import { - L1TransactionReceipt, - L1ToL2MessageStatus, - L1ToL2MessageWriter, - L1ToL2MessageGasEstimator, -} from '@arbitrum/sdk' -import { chainIdIsL2 } from '../../utils' +import { getProvider, sendTransaction, toGRT, ensureAllowance, toBN } from '../../network' +import { chainIdIsL2, estimateRetryableTxGas } from '../../cross-chain' const logAutoRedeemReason = (autoRedeemRec) => { if (autoRedeemRec == null) { @@ -32,7 +28,12 @@ const checkAndRedeemMessage = async (l1ToL2Message: L1ToL2MessageWriter) => { logAutoRedeemReason(autoRedeemRec) logger.info('Attempting to redeem...') await l1ToL2Message.redeem() - l2TxHash = (await l1ToL2Message.getSuccessfulRedeem()).transactionHash + const redeemAttempt = await l1ToL2Message.getSuccessfulRedeem() + if (redeemAttempt.status == L1ToL2MessageStatus.REDEEMED) { + l2TxHash = redeemAttempt.l2TxReceipt ? redeemAttempt.l2TxReceipt.transactionHash : 'null' + } else { + throw new Error(`Unexpected L1ToL2MessageStatus after redeem attempt: ${res.status}`) + } } else if (res.status != L1ToL2MessageStatus.REDEEMED) { throw new Error(`Unexpected L1ToL2MessageStatus ${res.status}`) } @@ -41,75 +42,78 @@ const checkAndRedeemMessage = async (l1ToL2Message: L1ToL2MessageWriter) => { export const sendToL2 = async (cli: CLIEnvironment, cliArgs: CLIArgs): Promise => { logger.info(`>>> Sending tokens to L2 <<<\n`) + + // parse provider + const l1Provider = cli.wallet.provider const l2Provider = getProvider(cliArgs.l2ProviderUrl) + const l1ChainId = cli.chainId const l2ChainId = (await l2Provider.getNetwork()).chainId - - if (chainIdIsL2(cli.chainId) || !chainIdIsL2(l2ChainId)) { + if (chainIdIsL2(l1ChainId) || !chainIdIsL2(l2ChainId)) { throw new Error( 'Please use an L1 provider in --provider-url, and an L2 provider in --l2-provider-url', ) } - const gateway = cli.contracts['L1GraphTokenGateway'] - const l1GRT = cli.contracts['GraphToken'] - const l1GRTAddress = l1GRT.address + + // parse params + const { L1GraphTokenGateway: l1Gateway, GraphToken: l1GRT } = cli.contracts const amount = toGRT(cliArgs.amount) - const recipient = cliArgs.recipient ? cliArgs.recipient : cli.wallet.address - const l2Dest = await gateway.l2Counterpart() + const recipient = cliArgs.recipient ?? cli.wallet.address + const l1GatewayAddress = l1Gateway.address + const l2GatewayAddress = await l1Gateway.l2Counterpart() + const calldata = cliArgs.calldata ?? '0x' + // transport tokens logger.info(`Will send ${cliArgs.amount} GRT to ${recipient}`) - logger.info(`Using L1 gateway ${gateway.address} and L2 gateway ${l2Dest}`) + logger.info(`Using L1 gateway ${l1GatewayAddress} and L2 gateway ${l2GatewayAddress}`) + await ensureAllowance(cli.wallet, l1GatewayAddress, l1GRT, amount) + + // estimate L2 ticket // See https://github.com/OffchainLabs/arbitrum/blob/master/packages/arb-ts/src/lib/bridge.ts - const depositCalldata = await gateway.getOutboundCalldata( - l1GRTAddress, + const depositCalldata = await l1Gateway.getOutboundCalldata( + l1GRT.address, cli.wallet.address, recipient, amount, - '0x', + calldata, ) - - // Comment from Offchain Labs' implementation: - // we add a 0.05 ether "deposit" buffer to pay for execution in the gas estimation - logger.info('Estimating retryable ticket gas:') - const baseFee = (await cli.wallet.provider.getBlock('latest')).baseFeePerGas - const gasEstimator = new L1ToL2MessageGasEstimator(l2Provider) - const gasParams = await gasEstimator.estimateMessage( - gateway.address, - l2Dest, + const { maxGas, gasPriceBid, maxSubmissionCost } = await estimateRetryableTxGas( + l1Provider, + l2Provider, + l1GatewayAddress, + l2GatewayAddress, depositCalldata, - parseEther('0'), - baseFee, - gateway.address, - gateway.address, + { + maxGas: cliArgs.maxGas, + gasPriceBid: cliArgs.gasPriceBid, + maxSubmissionCost: cliArgs.maxSubmissionCost, + }, ) - const maxGas = gasParams.maxGasBid - const gasPriceBid = gasParams.maxGasPriceBid - const maxSubmissionPrice = gasParams.maxSubmissionPriceBid + const ethValue = maxSubmissionCost.add(gasPriceBid.mul(maxGas)) logger.info( - `Using max gas: ${maxGas}, gas price bid: ${gasPriceBid}, max submission price: ${maxSubmissionPrice}`, + `Using maxGas:${maxGas}, gasPriceBid:${gasPriceBid}, maxSubmissionCost:${maxSubmissionCost} = tx value: ${ethValue}`, ) - const ethValue = maxSubmissionPrice.add(gasPriceBid.mul(maxGas)) - logger.info(`tx value: ${ethValue}`) - const data = utils.defaultAbiCoder.encode(['uint256', 'bytes'], [maxSubmissionPrice, '0x']) - - const params = [l1GRTAddress, recipient, amount, maxGas, gasPriceBid, data] - logger.info('Approving token transfer') - await sendTransaction(cli.wallet, l1GRT, 'approve', [gateway.address, amount]) + // build transaction logger.info('Sending outbound transfer transaction') - const receipt = await sendTransaction(cli.wallet, gateway, 'outboundTransfer', params, { + const txData = utils.defaultAbiCoder.encode(['uint256', 'bytes'], [maxSubmissionCost, calldata]) + const txParams = [l1GRT.address, recipient, amount, maxGas, gasPriceBid, txData] + const txReceipt = await sendTransaction(cli.wallet, l1Gateway, 'outboundTransfer', txParams, { value: ethValue, }) - const l1Receipt = new L1TransactionReceipt(receipt) - const l1ToL2Message = await l1Receipt.getL1ToL2Message(cli.wallet.connect(l2Provider)) - - logger.info('Waiting for message to propagate to L2...') - try { - await checkAndRedeemMessage(l1ToL2Message) - } catch (e) { - logger.error('Auto redeem failed') - logger.error(e) - logger.error('You can re-attempt using redeem-send-to-l2 with the following txHash:') - logger.error(receipt.transactionHash) + // get l2 ticket status + if (txReceipt.status == 1) { + logger.info('Waiting for message to propagate to L2...') + const l1Receipt = new L1TransactionReceipt(txReceipt) + const l1ToL2Messages = await l1Receipt.getL1ToL2Messages(cli.wallet.connect(l2Provider)) + const l1ToL2Message = l1ToL2Messages[0] + try { + await checkAndRedeemMessage(l1ToL2Message) + } catch (e) { + logger.error('Auto redeem failed') + logger.error(e) + logger.error('You can re-attempt using redeem-send-to-l2 with the following txHash:') + logger.error(txReceipt.transactionHash) + } } } @@ -135,8 +139,38 @@ export const redeemSendToL2 = async (cli: CLIEnvironment, cliArgs: CLIArgs): Pro } export const sendToL2Command = { - command: 'send-to-l2 [recipient]', + command: 'send-to-l2 [recipient] [calldata]', describe: 'Perform an L1-to-L2 Graph Token transaction', + builder: (yargs: Argv): Argv => { + return yargs + .option('max-gas', { + description: 'Max gas for the L2 redemption attempt', + requiresArg: true, + type: 'string', + }) + .option('gas-price-bid', { + description: 'Gas price for the L2 redemption attempt', + requiresArg: true, + type: 'string', + }) + .option('max-submission-cost', { + description: 'Max submission cost for the retryable ticket', + requiresArg: true, + type: 'string', + }) + .positional('amount', { description: 'Amount to send (will be converted to wei)' }) + .positional('recipient', { + description: 'Receiving address in L2. Same to L1 address if empty', + }) + .positional('calldata', { + description: 'Calldata to pass to the recipient. Must be whitelisted in the bridge', + }) + .coerce({ + maxGas: toBN, + gasPriceBid: toBN, + maxSubmissionCost: toBN, + }) + }, handler: async (argv: CLIArgs): Promise => { return sendToL2(await loadEnv(argv), argv) }, diff --git a/cli/commands/migrate.ts b/cli/commands/migrate.ts index 374f28b16..335b6fa32 100644 --- a/cli/commands/migrate.ts +++ b/cli/commands/migrate.ts @@ -11,8 +11,8 @@ import { sendTransaction, } from '../network' import { loadEnv, CLIArgs, CLIEnvironment } from '../env' +import { chainIdIsL2 } from '../cross-chain' import { confirm } from '../helpers' -import { chainIdIsL2 } from '../utils' const { EtherSymbol } = constants const { formatEther } = utils @@ -73,8 +73,8 @@ export const migrate = async ( if (!sure) return if (chainId == 1337) { - await (cli.wallet.provider as providers.JsonRpcProvider).send('evm_setAutomine', [true]) allContracts = ['EthereumDIDRegistry', ...allContracts] + await setAutoMine(cli.wallet.provider as providers.JsonRpcProvider, true) } else if (chainIdIsL2(chainId)) { allContracts = l2Contracts } @@ -169,7 +169,15 @@ export const migrate = async ( logger.info(`Sent ${nTx} transaction${nTx === 1 ? '' : 's'} & spent ${EtherSymbol} ${spent}`) if (chainId == 1337) { - await (cli.wallet.provider as providers.JsonRpcProvider).send('evm_setAutomine', [autoMine]) + await setAutoMine(cli.wallet.provider as providers.JsonRpcProvider, autoMine) + } +} + +const setAutoMine = async (provider: providers.JsonRpcProvider, automine: boolean) => { + try { + await provider.send('evm_setAutomine', [automine]) + } catch (error) { + logger.warn('The method evm_setAutomine does not exist/is not available!') } } diff --git a/cli/commands/protocol/configure-bridge.ts b/cli/commands/protocol/configure-bridge.ts index d96d462a5..057ebc653 100644 --- a/cli/commands/protocol/configure-bridge.ts +++ b/cli/commands/protocol/configure-bridge.ts @@ -2,7 +2,7 @@ import { loadEnv, CLIArgs, CLIEnvironment } from '../../env' import { logger } from '../../logging' import { getAddressBook } from '../../address-book' import { sendTransaction } from '../../network' -import { chainIdIsL2, l1ToL2ChainIdMap, l2ToL1ChainIdMap } from '../../utils' +import { chainIdIsL2, l1ToL2ChainIdMap, l2ToL1ChainIdMap } from '../../cross-chain' export const configureL1Bridge = async (cli: CLIEnvironment, cliArgs: CLIArgs): Promise => { logger.info(`>>> Setting L1 Bridge Configuration <<<\n`) diff --git a/cli/contracts.ts b/cli/contracts.ts index 9af5c126d..b01727c20 100644 --- a/cli/contracts.ts +++ b/cli/contracts.ts @@ -1,6 +1,7 @@ import { BaseContract, providers, Signer } from 'ethers' import { AddressBook } from './address-book' +import { chainIdIsL2 } from './cross-chain' import { logger } from './logging' import { getContractAt } from './network' @@ -25,7 +26,6 @@ import { L1GraphTokenGateway } from '../build/types/L1GraphTokenGateway' import { L2GraphToken } from '../build/types/L2GraphToken' import { L2GraphTokenGateway } from '../build/types/L2GraphTokenGateway' import { BridgeEscrow } from '../build/types/BridgeEscrow' -import { chainIdIsL2 } from './utils' export interface NetworkContracts { EpochManager: EpochManager diff --git a/cli/cross-chain.ts b/cli/cross-chain.ts new file mode 100644 index 000000000..ea3b081ac --- /dev/null +++ b/cli/cross-chain.ts @@ -0,0 +1,63 @@ +import { L1ToL2MessageGasEstimator } from '@arbitrum/sdk' +import { BigNumber, providers } from 'ethers' +import { parseEther } from 'ethers/lib/utils' + +import { logger } from './logging' + +export const l1ToL2ChainIdMap = { + '1': '42161', + '4': '421611', + '5': '421613', + '1337': '412346', +} + +export const l2ChainIds = Object.values(l1ToL2ChainIdMap).map(Number) +export const l2ToL1ChainIdMap = Object.fromEntries( + Object.entries(l1ToL2ChainIdMap).map(([k, v]) => [v, k]), +) + +export const chainIdIsL2 = (chainId: number | string): boolean => { + return l2ChainIds.includes(Number(chainId)) +} + +interface L2GasParams { + maxGas: BigNumber + gasPriceBid: BigNumber + maxSubmissionCost: BigNumber +} + +export const estimateRetryableTxGas = async ( + l1Provider: providers.Provider, + l2Provider: providers.Provider, + gatewayAddress: string, + l2Dest: string, + depositCalldata: string, + opts: L2GasParams, +): Promise => { + const autoEstimate = opts && (!opts.maxGas || !opts.gasPriceBid || !opts.maxSubmissionCost) + if (!autoEstimate) { + return opts + } + + // Comment from Offchain Labs' implementation: + // we add a 0.05 ether "deposit" buffer to pay for execution in the gas estimation + logger.info('Estimating retryable ticket gas:') + const baseFee = (await l1Provider.getBlock('latest')).baseFeePerGas + const gasEstimator = new L1ToL2MessageGasEstimator(l2Provider) + const gasParams = await gasEstimator.estimateAll( + gatewayAddress, + l2Dest, + depositCalldata, + parseEther('0'), + baseFee as BigNumber, + gatewayAddress, + gatewayAddress, + l1Provider, + ) + // override fixed values + return { + maxGas: opts.maxGas ?? gasParams.gasLimit, + gasPriceBid: opts.gasPriceBid ?? gasParams.maxFeePerGas, + maxSubmissionCost: opts.maxSubmissionCost ?? gasParams.maxSubmissionFee, + } +} diff --git a/cli/env.ts b/cli/env.ts index ab89c9d85..d607e4be5 100644 --- a/cli/env.ts +++ b/cli/env.ts @@ -4,7 +4,7 @@ import { Argv } from 'yargs' import { logger } from './logging' import { getAddressBook, AddressBook } from './address-book' import { defaultOverrides } from './defaults' -import { getProvider } from './utils' +import { getProvider } from './network' import { loadContracts, NetworkContracts } from './contracts' const { formatEther } = utils diff --git a/cli/network.ts b/cli/network.ts index 3dc2e69c1..6667a960d 100644 --- a/cli/network.ts +++ b/cli/network.ts @@ -10,16 +10,19 @@ import { Overrides, BigNumber, PayableOverrides, + Wallet, } from 'ethers' import { logger } from './logging' import { AddressBook } from './address-book' import { loadArtifact } from './artifacts' import { defaultOverrides } from './defaults' +import { GraphToken } from '../build/types/GraphToken' const { keccak256, randomBytes, parseUnits, hexlify } = utils export const randomHexBytes = (n = 32): string => hexlify(randomBytes(n)) +export const toBN = (value: string | number | BigNumber): BigNumber => BigNumber.from(value) export const toGRT = (value: string | number): BigNumber => { return parseUnits(typeof value === 'number' ? value.toString() : value, '18') } @@ -368,3 +371,26 @@ export const linkLibraries = ( } return bytecode } + +export const ensureAllowance = async ( + sender: Wallet, + spenderAddress: string, + token: GraphToken, + amount: BigNumber, +) => { + // check balance + const senderBalance = await token.balanceOf(sender.address) + if (senderBalance.lt(amount)) { + throw new Error('Sender balance is insufficient for the transfer') + } + + // check allowance + const allowance = await token.allowance(sender.address, spenderAddress) + if (allowance.gte(amount)) { + return + } + + // approve + logger.info('Approving token transfer') + return sendTransaction(sender, token, 'approve', [spenderAddress, amount]) +} diff --git a/cli/utils.ts b/cli/utils.ts deleted file mode 100644 index 846c5ce9d..000000000 --- a/cli/utils.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { addCustomNetwork } from '@arbitrum/sdk' -import { Contract, Wallet, providers } from 'ethers' - -import { loadArtifact } from './artifacts' - -export const l1ToL2ChainIdMap = { - '1': '42161', - '4': '421611', - '5': '421613', -} - -export const l2ChainIds = Object.values(l1ToL2ChainIdMap).map(Number) -export const l2ToL1ChainIdMap = Object.fromEntries( - Object.entries(l1ToL2ChainIdMap).map(([k, v]) => [v, k]), -) - -export const contractAt = ( - contractName: string, - contractAddress: string, - wallet: Wallet, -): Contract => { - return new Contract(contractAddress, loadArtifact(contractName).abi, wallet.provider) -} - -export const getProvider = (providerUrl: string, network?: number): providers.JsonRpcProvider => - new providers.JsonRpcProvider(providerUrl, network) - -export const chainIdIsL2 = (chainId: number | string): boolean => { - return l2ChainIds.includes(Number(chainId)) -} diff --git a/config/graph.arbitrum-goerli.yml b/config/graph.arbitrum-goerli.yml new file mode 100644 index 000000000..73021f042 --- /dev/null +++ b/config/graph.arbitrum-goerli.yml @@ -0,0 +1,140 @@ +general: + arbitrator: &arbitrator "0x113DC95e796836b8F0Fa71eE7fB42f221740c3B0" # Arbitration Council (TODO: update) + governor: &governor "0x3e43EF77fAAd296F65eF172E8eF06F8231c9DeAd" # Graph Council (TODO: update) + authority: &authority "0x79fd74da4c906509862c8fe93e87a9602e370bc4" # Authority that signs payment vouchers (TODO: update) + availabilityOracle: &availabilityOracle "0x5d3B6F98F1cCdF873Df0173CDE7335874a396c4d" # Subgraph Availability Oracle (TODO: update) + pauseGuardian: &pauseGuardian "0x8290362Aba20D17c51995085369E001Bad99B21c" # Protocol pause guardian (TODO: update) + allocationExchangeOwner: &allocationExchangeOwner "0x74Db79268e63302d3FC69FB5a7627F7454a41732" # Allocation Exchange owner (TODO: update) + +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: "${{L2GraphToken.address}}" + - fn: "setContractProxy" + id: "0xd362cac9cb75c10d67bcc0b7eeb0b1ef48bb5420b556c092d4fd7f758816fcf0" # keccak256('GraphTokenGateway') + contractAddress: "${{L2GraphTokenGateway.address}}" + - fn: "setPauseGuardian" + pauseGuardian: *pauseGuardian + - fn: "transferOwnership" + owner: *governor + GraphProxyAdmin: + calls: + - fn: "transferOwnership" + owner: *governor + ServiceRegistry: + proxy: true + init: + controller: "${{Controller.address}}" + EpochManager: + proxy: true + init: + controller: "${{Controller.address}}" + lengthInBlocks: 554 # length in hours = lengthInBlocks*13/60/60 (~13 second blocks) + L2GraphToken: + proxy: true + init: + governor: "${{Env.deployer}}" + calls: + - fn: "addMinter" + minter: "${{RewardsManager.address}}" + - fn: "renounceMinter" + - fn: "transferOwnership" + owner: *governor + 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: 25000 # in parts per million + qrySlashingPercentage: 25000 # 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}}" + - fn: "transferOwnership" + owner: *governor + 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: 4 # in epochs + delegationUnbondingPeriod: 12 # 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}}" + calls: + - fn: "setSubgraphAvailabilityOracle" + subgraphAvailabilityOracle: *availabilityOracle + AllocationExchange: + init: + graphToken: "${{L2GraphToken.address}}" + staking: "${{Staking.address}}" + governor: *allocationExchangeOwner + authority: *authority + calls: + - fn: "approveAll" + L2GraphTokenGateway: + proxy: true + init: + controller: "${{Controller.address}}" diff --git a/config/graph.rinkeby.yml b/config/graph.arbitrum-localhost.yml similarity index 66% rename from config/graph.rinkeby.yml rename to config/graph.arbitrum-localhost.yml index bd8ab6bf2..2e185a14a 100644 --- a/config/graph.rinkeby.yml +++ b/config/graph.arbitrum-localhost.yml @@ -1,7 +1,10 @@ general: - arbitrator: &arbitrator "0x87D11BD744b882b7bc5A6b5450cbA8C35D90eb10" # Arbitration Council - governor: &governor "0x1679A1D1caf1252BA43Fb8Fc17ebF914a0C725AE" # Graph Council - authority: &authority "0xe1EC4339019eC9628438F8755f847e3023e4ff9c" # Authority that signs payment vouchers + arbitrator: &arbitrator "0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0" # Arbitration Council + governor: &governor "0x22d491Bde2303f2f43325b2108D26f1eAbA1e32b" # Graph Council + authority: &authority "0xE11BA2b4D45Eaed5996Cd0823791E0C93114882d" # Authority that signs payment vouchers + availabilityOracle: &availabilityOracle "0xd03ea8624C8C5987235048901fB614fDcA89b117" # Subgraph Availability Oracle + pauseGuardian: &pauseGuardian "0x95cED938F7991cd0dFcb48F0a06a40FA1aF46EBC" # Protocol pause guardian + allocationExchangeOwner: &allocationExchangeOwner "0x3E5e9111Ae8eB78Fe1CC3bb8915d5D461F3Ef9A9" # Allocation Exchange owner contracts: Controller: @@ -26,7 +29,18 @@ contracts: contractAddress: "${{Staking.address}}" - fn: "setContractProxy" id: "0x45fc200c7e4544e457d3c5709bfe0d520442c30bbcbdaede89e8d4a4bbc19247" # keccak256('GraphToken') - contractAddress: "${{GraphToken.address}}" + contractAddress: "${{L2GraphToken.address}}" + - fn: "setContractProxy" + id: "0xd362cac9cb75c10d67bcc0b7eeb0b1ef48bb5420b556c092d4fd7f758816fcf0" # keccak256('GraphTokenGateway') + contractAddress: "${{L2GraphTokenGateway.address}}" + - fn: "setPauseGuardian" + pauseGuardian: *pauseGuardian + - fn: "transferOwnership" + owner: *governor + GraphProxyAdmin: + calls: + - fn: "transferOwnership" + owner: *governor ServiceRegistry: proxy: true init: @@ -35,13 +49,17 @@ contracts: proxy: true init: controller: "${{Controller.address}}" - lengthInBlocks: 277 # length in hours = lengthInBlocks*13/60/60 (~13 second blocks) - GraphToken: + lengthInBlocks: 554 # length in hours = lengthInBlocks*13/60/60 (~13 second blocks) + L2GraphToken: + proxy: true init: - initialSupply: "10000000000000000000000000000" # in wei + governor: "${{Env.deployer}}" calls: - fn: "addMinter" minter: "${{RewardsManager.address}}" + - fn: "renounceMinter" + - fn: "transferOwnership" + owner: *governor Curation: proxy: true init: @@ -58,8 +76,8 @@ contracts: arbitrator: *arbitrator minimumDeposit: "10000000000000000000000" # in wei fishermanRewardPercentage: 500000 # in parts per million - idxSlashingPercentage: 20000 # in parts per million - qrySlashingPercentage: 5000 # in parts per million + idxSlashingPercentage: 25000 # in parts per million + qrySlashingPercentage: 25000 # in parts per million GNS: proxy: true init: @@ -76,6 +94,8 @@ contracts: tokenDescriptor: "${{SubgraphNFTDescriptor.address}}" - fn: "setMinter" minter: "${{GNS.address}}" + - fn: "transferOwnership" + owner: *governor Staking: proxy: true init: @@ -85,8 +105,8 @@ contracts: protocolPercentage: 10000 # in parts per million curationPercentage: 100000 # in parts per million channelDisputeEpochs: 2 # in epochs - maxAllocationEpochs: 2 # in epochs - delegationUnbondingPeriod: 6 # in epochs + maxAllocationEpochs: 4 # in epochs + delegationUnbondingPeriod: 12 # in epochs delegationRatio: 16 # delegated stake to indexer stake multiplier rebateAlphaNumerator: 77 # rebateAlphaNumerator / rebateAlphaDenominator rebateAlphaDenominator: 100 # rebateAlphaNumerator / rebateAlphaDenominator @@ -103,12 +123,18 @@ contracts: proxy: true 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}}" + graphToken: "${{L2GraphToken.address}}" staking: "${{Staking.address}}" - governor: *governor + governor: *allocationExchangeOwner authority: *authority calls: - fn: "approveAll" + L2GraphTokenGateway: + proxy: true + init: + controller: "${{Controller.address}}" diff --git a/config/graph.arbitrum-one.yml b/config/graph.arbitrum-one.yml index 53117372b..8fb876d8f 100644 --- a/config/graph.arbitrum-one.yml +++ b/config/graph.arbitrum-one.yml @@ -2,6 +2,9 @@ general: arbitrator: &arbitrator "0x113DC95e796836b8F0Fa71eE7fB42f221740c3B0" # Arbitration Council governor: &governor "0x3e43EF77fAAd296F65eF172E8eF06F8231c9DeAd" # Graph Council authority: &authority "0x79fd74da4c906509862c8fe93e87a9602e370bc4" # Authority that signs payment vouchers + availabilityOracle: &availabilityOracle "0x5d3B6F98F1cCdF873Df0173CDE7335874a396c4d" # Subgraph Availability Oracle (TODO: update) + pauseGuardian: &pauseGuardian "0x8290362Aba20D17c51995085369E001Bad99B21c" # Protocol pause guardian (TODO: update) + allocationExchangeOwner: &allocationExchangeOwner "0x74Db79268e63302d3FC69FB5a7627F7454a41732" # Allocation Exchange owner (TODO: update) contracts: Controller: @@ -30,6 +33,14 @@ contracts: - fn: "setContractProxy" id: "0xd362cac9cb75c10d67bcc0b7eeb0b1ef48bb5420b556c092d4fd7f758816fcf0" # keccak256('GraphTokenGateway') contractAddress: "${{L2GraphTokenGateway.address}}" + - fn: "setPauseGuardian" + pauseGuardian: *pauseGuardian + - fn: "transferOwnership" + owner: *governor + GraphProxyAdmin: + calls: + - fn: "transferOwnership" + owner: *governor ServiceRegistry: proxy: true init: @@ -38,32 +49,35 @@ 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) L2GraphToken: proxy: true init: - owner: *governor + governor: "${{Env.deployer}}" calls: - fn: "addMinter" minter: "${{RewardsManager.address}}" + - fn: "renounceMinter" + - fn: "transferOwnership" + owner: *governor Curation: proxy: true init: 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,23 +94,25 @@ contracts: tokenDescriptor: "${{SubgraphNFTDescriptor.address}}" - fn: "setMinter" minter: "${{GNS.address}}" + - fn: "transferOwnership" + owner: *governor Staking: 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 @@ -107,11 +123,14 @@ contracts: proxy: true init: controller: "${{Controller.address}}" + calls: + - fn: "setSubgraphAvailabilityOracle" + subgraphAvailabilityOracle: *availabilityOracle AllocationExchange: init: - graphToken: "${{GraphToken.address}}" + graphToken: "${{L2GraphToken.address}}" staking: "${{Staking.address}}" - governor: *governor + governor: *allocationExchangeOwner authority: *authority calls: - fn: "approveAll" diff --git a/config/graph.goerli.yml b/config/graph.goerli.yml index b79890149..d24e3c23d 100644 --- a/config/graph.goerli.yml +++ b/config/graph.goerli.yml @@ -30,6 +30,9 @@ contracts: - fn: "setContractProxy" id: "0x45fc200c7e4544e457d3c5709bfe0d520442c30bbcbdaede89e8d4a4bbc19247" # keccak256('GraphToken') contractAddress: "${{GraphToken.address}}" + - fn: "setContractProxy" + id: "0xd362cac9cb75c10d67bcc0b7eeb0b1ef48bb5420b556c092d4fd7f758816fcf0" # keccak256('GraphTokenGateway') + contractAddress: "${{L1GraphTokenGateway.address}}" - fn: "setPauseGuardian" pauseGuardian: *pauseGuardian - fn: "transferOwnership" @@ -132,3 +135,11 @@ contracts: authority: *authority calls: - fn: "approveAll" + L1GraphTokenGateway: + proxy: true + init: + controller: "${{Controller.address}}" + BridgeEscrow: + proxy: true + init: + controller: "${{Controller.address}}" \ No newline at end of file diff --git a/config/graph.localhost.yml b/config/graph.localhost.yml index bb039f405..d6268c269 100644 --- a/config/graph.localhost.yml +++ b/config/graph.localhost.yml @@ -30,6 +30,9 @@ contracts: - fn: "setContractProxy" id: "0x45fc200c7e4544e457d3c5709bfe0d520442c30bbcbdaede89e8d4a4bbc19247" # keccak256('GraphToken') contractAddress: "${{GraphToken.address}}" + - fn: "setContractProxy" + id: "0xd362cac9cb75c10d67bcc0b7eeb0b1ef48bb5420b556c092d4fd7f758816fcf0" # keccak256('GraphTokenGateway') + contractAddress: "${{L1GraphTokenGateway.address}}" - fn: "setPauseGuardian" pauseGuardian: *pauseGuardian - fn: "transferOwnership" @@ -132,3 +135,11 @@ contracts: authority: *authority calls: - fn: "approveAll" + L1GraphTokenGateway: + proxy: true + init: + controller: "${{Controller.address}}" + BridgeEscrow: + proxy: true + init: + controller: "${{Controller.address}}" diff --git a/e2e/deployment/config/controller.test.ts b/e2e/deployment/config/controller.test.ts index 9bde5d85e..8fbdf4834 100644 --- a/e2e/deployment/config/controller.test.ts +++ b/e2e/deployment/config/controller.test.ts @@ -1,12 +1,13 @@ import { expect } from 'chai' import hre, { ethers } from 'hardhat' import { NamedAccounts } from '../../../gre/type-extensions' +import GraphChain from '../../../gre/helpers/network' describe('Controller configuration', () => { - const { contracts, getNamedAccounts } = hre.graph() - const { Controller } = contracts + const graph = hre.graph() + const { Controller } = graph.contracts - const proxyContracts = [ + const l1ProxyContracts = [ 'Curation', 'GNS', 'DisputeManager', @@ -14,21 +15,41 @@ describe('Controller configuration', () => { 'RewardsManager', 'Staking', 'GraphToken', + 'L1GraphTokenGateway', + ] + + const l2ProxyContracts = [ + 'Curation', + 'GNS', + 'DisputeManager', + 'EpochManager', + 'RewardsManager', + 'Staking', + 'L2GraphToken', + 'L2GraphTokenGateway', ] let namedAccounts: NamedAccounts before(async () => { - namedAccounts = await getNamedAccounts() + namedAccounts = await graph.getNamedAccounts() }) const proxyShouldMatchDeployed = async (contractName: string) => { - const curationAddress = await Controller.getContractProxy( - ethers.utils.solidityKeccak256(['string'], [contractName]), + // remove L1/L2 prefix, contracts are not registered as L1/L2 on controller + const name = contractName.replace(/(^L1|L2)/gi, '') + + const address = await Controller.getContractProxy( + ethers.utils.solidityKeccak256(['string'], [name]), ) - expect(curationAddress).eq(contracts[contractName].address) + expect(address).eq(graph.contracts[contractName].address) } + it('protocol should be unpaused', async function () { + const paused = await Controller.paused() + expect(paused).eq(false) + }) + it('should be owned by governor', async function () { const owner = await Controller.governor() expect(owner).eq(namedAccounts.governor.address) @@ -40,6 +61,7 @@ describe('Controller configuration', () => { }) describe('proxy contract', async function () { + const proxyContracts = GraphChain.isL1(graph.chainId) ? l1ProxyContracts : l2ProxyContracts for (const contract of proxyContracts) { it(`${contract} should match deployed`, async function () { await proxyShouldMatchDeployed(contract) diff --git a/e2e/deployment/config/graphToken.test.ts b/e2e/deployment/config/graphToken.test.ts index cc5e60618..8b84208f3 100644 --- a/e2e/deployment/config/graphToken.test.ts +++ b/e2e/deployment/config/graphToken.test.ts @@ -23,7 +23,7 @@ describe('GraphToken configuration', () => { 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) + expect(deployerIsMinter).eq(false) }) it('RewardsManager should be minter', async function () { diff --git a/e2e/deployment/config/l1/bridgeEscrow.test.ts b/e2e/deployment/config/l1/bridgeEscrow.test.ts new file mode 100644 index 000000000..2304a2063 --- /dev/null +++ b/e2e/deployment/config/l1/bridgeEscrow.test.ts @@ -0,0 +1,17 @@ +import { expect } from 'chai' +import hre from 'hardhat' +import GraphChain from '../../../../gre/helpers/network' + +describe('[L1] BridgeEscrow configuration', function () { + const graph = hre.graph() + const { Controller, BridgeEscrow } = graph.contracts + + before(async function () { + if (GraphChain.isL2(graph.chainId)) this.skip() + }) + + it('should be controlled by Controller', async function () { + const controller = await BridgeEscrow.controller() + expect(controller).eq(Controller.address) + }) +}) diff --git a/e2e/deployment/config/l1/l1GraphTokenGateway.test.ts b/e2e/deployment/config/l1/l1GraphTokenGateway.test.ts new file mode 100644 index 000000000..d2424d0ce --- /dev/null +++ b/e2e/deployment/config/l1/l1GraphTokenGateway.test.ts @@ -0,0 +1,83 @@ +import { expect } from 'chai' +import hre from 'hardhat' +import GraphChain from '../../../../gre/helpers/network' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' + +describe('[L1] L1GraphTokenGateway configuration', function () { + const graph = hre.graph() + const { Controller, L1GraphTokenGateway } = graph.contracts + + let unauthorized: SignerWithAddress + before(async function () { + if (GraphChain.isL2(graph.chainId)) this.skip() + unauthorized = (await graph.getTestAccounts())[0] + }) + + it('bridge should be unpaused', async function () { + const paused = await L1GraphTokenGateway.paused() + expect(paused).eq(false) + }) + + it('should be controlled by Controller', async function () { + const controller = await L1GraphTokenGateway.controller() + expect(controller).eq(Controller.address) + }) + + describe('calls with unauthorized user', () => { + it('initialize should revert', async function () { + const tx = L1GraphTokenGateway.connect(unauthorized).initialize(unauthorized.address) + await expect(tx).revertedWith('Caller must be the implementation') + }) + + it('setArbitrumAddresses should revert', async function () { + const tx = L1GraphTokenGateway.connect(unauthorized).setArbitrumAddresses( + unauthorized.address, + unauthorized.address, + ) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + + it('setL2TokenAddress should revert', async function () { + const tx = L1GraphTokenGateway.connect(unauthorized).setL2TokenAddress(unauthorized.address) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + + it('setL2CounterpartAddress should revert', async function () { + const tx = L1GraphTokenGateway.connect(unauthorized).setL2CounterpartAddress( + unauthorized.address, + ) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + + it('setEscrowAddress should revert', async function () { + const tx = L1GraphTokenGateway.connect(unauthorized).setEscrowAddress(unauthorized.address) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + + it('addToCallhookWhitelist should revert', async function () { + const tx = L1GraphTokenGateway.connect(unauthorized).addToCallhookWhitelist( + unauthorized.address, + ) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + + it('removeFromCallhookWhitelist should revert', async function () { + const tx = L1GraphTokenGateway.connect(unauthorized).removeFromCallhookWhitelist( + unauthorized.address, + ) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + + it('finalizeInboundTransfer should revert', async function () { + const tx = L1GraphTokenGateway.connect(unauthorized).finalizeInboundTransfer( + unauthorized.address, + unauthorized.address, + unauthorized.address, + '100', + '0x00', + ) + + await expect(tx).revertedWith('INBOX_NOT_SET') + }) + }) +}) diff --git a/e2e/deployment/config/l1/rewardsManager.test.ts b/e2e/deployment/config/l1/rewardsManager.test.ts new file mode 100644 index 000000000..4cc990161 --- /dev/null +++ b/e2e/deployment/config/l1/rewardsManager.test.ts @@ -0,0 +1,17 @@ +import { expect } from 'chai' +import hre from 'hardhat' +import GraphChain from '../../../../gre/helpers/network' + +describe('[L1] RewardsManager configuration', () => { + const graph = hre.graph() + const { RewardsManager } = graph.contracts + + before(async function () { + if (GraphChain.isL2(graph.chainId)) this.skip() + }) + + it('issuanceRate should match "issuanceRate" in the config file', async function () { + const value = await RewardsManager.issuanceRate() + expect(value).eq('1000000012184945188') // hardcoded as it's set with a function call rather than init parameter + }) +}) diff --git a/e2e/deployment/config/l2/l2GraphToken.test.ts b/e2e/deployment/config/l2/l2GraphToken.test.ts new file mode 100644 index 000000000..0d9f4b025 --- /dev/null +++ b/e2e/deployment/config/l2/l2GraphToken.test.ts @@ -0,0 +1,39 @@ +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { expect } from 'chai' +import hre from 'hardhat' +import GraphChain from '../../../../gre/helpers/network' + +describe('[L2] L2GraphToken', () => { + const graph = hre.graph() + const { L2GraphToken } = graph.contracts + + let unauthorized: SignerWithAddress + + before(async function () { + if (GraphChain.isL1(graph.chainId)) this.skip() + unauthorized = (await graph.getTestAccounts())[0] + }) + + describe('calls with unauthorized user', () => { + it('mint should revert', async function () { + const tx = L2GraphToken.connect(unauthorized).mint( + unauthorized.address, + '1000000000000000000000', + ) + await expect(tx).revertedWith('Only minter can call') + }) + + it('bridgeMint should revert', async function () { + const tx = L2GraphToken.connect(unauthorized).bridgeMint( + unauthorized.address, + '1000000000000000000000', + ) + await expect(tx).revertedWith('NOT_GATEWAY') + }) + + it('setGateway should revert', async function () { + const tx = L2GraphToken.connect(unauthorized).setGateway(unauthorized.address) + await expect(tx).revertedWith('Only Governor can call') + }) + }) +}) diff --git a/e2e/deployment/config/l2/l2GraphTokenGateway.test.ts b/e2e/deployment/config/l2/l2GraphTokenGateway.test.ts new file mode 100644 index 000000000..04732498b --- /dev/null +++ b/e2e/deployment/config/l2/l2GraphTokenGateway.test.ts @@ -0,0 +1,61 @@ +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { expect } from 'chai' +import hre from 'hardhat' +import GraphChain from '../../../../gre/helpers/network' + +describe('[L2] L2GraphTokenGateway configuration', function () { + const graph = hre.graph() + const { Controller, L2GraphTokenGateway } = graph.contracts + + let unauthorized: SignerWithAddress + before(async function () { + if (GraphChain.isL1(graph.chainId)) this.skip() + unauthorized = (await graph.getTestAccounts())[0] + }) + + it('bridge should be unpaused', async function () { + const paused = await L2GraphTokenGateway.paused() + expect(paused).eq(false) + }) + + it('should be controlled by Controller', async function () { + const controller = await L2GraphTokenGateway.controller() + expect(controller).eq(Controller.address) + }) + + describe('calls with unauthorized user', () => { + it('initialize should revert', async function () { + const tx = L2GraphTokenGateway.connect(unauthorized).initialize(unauthorized.address) + await expect(tx).revertedWith('Caller must be the implementation') + }) + + it('setL2Router should revert', async function () { + const tx = L2GraphTokenGateway.connect(unauthorized).setL2Router(unauthorized.address) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + + it('setL1TokenAddress should revert', async function () { + const tx = L2GraphTokenGateway.connect(unauthorized).setL1TokenAddress(unauthorized.address) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + + it('setL1CounterpartAddress should revert', async function () { + const tx = L2GraphTokenGateway.connect(unauthorized).setL1CounterpartAddress( + unauthorized.address, + ) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + + it('finalizeInboundTransfer should revert', async function () { + const tx = L2GraphTokenGateway.connect(unauthorized).finalizeInboundTransfer( + unauthorized.address, + unauthorized.address, + unauthorized.address, + '1000000000000', + '0x00', + ) + + await expect(tx).revertedWith('ONLY_COUNTERPART_GATEWAY') + }) + }) +}) diff --git a/e2e/deployment/config/l2/rewardsManager.test.ts b/e2e/deployment/config/l2/rewardsManager.test.ts new file mode 100644 index 000000000..29ad83c5f --- /dev/null +++ b/e2e/deployment/config/l2/rewardsManager.test.ts @@ -0,0 +1,17 @@ +import { expect } from 'chai' +import hre from 'hardhat' +import GraphChain from '../../../../gre/helpers/network' + +describe('[L2] RewardsManager configuration', () => { + const graph = hre.graph() + const { RewardsManager } = graph.contracts + + before(async function () { + if (GraphChain.isL1(graph.chainId)) this.skip() + }) + + it('issuanceRate should be zero', async function () { + const value = await RewardsManager.issuanceRate() + expect(value).eq('0') + }) +}) diff --git a/e2e/deployment/config/protocol.test.ts b/e2e/deployment/config/protocol.test.ts deleted file mode 100644 index 4e12f5088..000000000 --- a/e2e/deployment/config/protocol.test.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { expect } from 'chai' -import hre from 'hardhat' - -describe('Protocol configuration', () => { - const { contracts } = hre.graph() - - it('should be unpaused', async function () { - const paused = await contracts.Controller.paused() - expect(paused).eq(false) - }) -}) diff --git a/e2e/deployment/config/rewardsManager.test.ts b/e2e/deployment/config/rewardsManager.test.ts index 475abd9f4..ddbcbb835 100644 --- a/e2e/deployment/config/rewardsManager.test.ts +++ b/e2e/deployment/config/rewardsManager.test.ts @@ -19,11 +19,6 @@ describe('RewardsManager configuration', () => { expect(controller).eq(Controller.address) }) - it('issuanceRate should match "issuanceRate" in the config file', async function () { - const value = await RewardsManager.issuanceRate() - expect(value).eq('1000000012184945188') // hardcoded as it's set with a function call rather than init parameter - }) - it('should allow subgraph availability oracle to deny rewards', async function () { const availabilityOracle = await RewardsManager.subgraphAvailabilityOracle() expect(availabilityOracle).eq(namedAccounts.availabilityOracle.address) diff --git a/e2e/deployment/init/graphToken.test.ts b/e2e/deployment/init/graphToken.test.ts deleted file mode 100644 index 8c2230adb..000000000 --- a/e2e/deployment/init/graphToken.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -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/deployment/init/l1/graphToken.test.ts b/e2e/deployment/init/l1/graphToken.test.ts new file mode 100644 index 000000000..aa0cc04e3 --- /dev/null +++ b/e2e/deployment/init/l1/graphToken.test.ts @@ -0,0 +1,19 @@ +import { expect } from 'chai' +import hre from 'hardhat' +import { getItemValue } from '../../../../cli/config' +import GraphChain from '../../../../gre/helpers/network' + +describe('[L1] GraphToken initialization', () => { + const graph = hre.graph() + const { GraphToken } = graph.contracts + + before(async function () { + if (GraphChain.isL2(graph.chainId)) this.skip() + }) + + it('total supply should match "initialSupply" on the config file', async function () { + const value = await GraphToken.totalSupply() + const expected = getItemValue(graph.graphConfig, 'contracts/GraphToken/init/initialSupply') + expect(value).eq(expected) + }) +}) diff --git a/e2e/deployment/init/l2/graphToken.test.ts b/e2e/deployment/init/l2/graphToken.test.ts new file mode 100644 index 000000000..90b232531 --- /dev/null +++ b/e2e/deployment/init/l2/graphToken.test.ts @@ -0,0 +1,17 @@ +import { expect } from 'chai' +import hre from 'hardhat' +import GraphChain from '../../../../gre/helpers/network' + +describe('[L2] GraphToken initialization', () => { + const graph = hre.graph() + const { GraphToken } = graph.contracts + + before(async function () { + if (GraphChain.isL1(graph.chainId)) this.skip() + }) + + it('total supply should be zero', async function () { + const value = await GraphToken.totalSupply() + expect(value).eq(0) + }) +}) diff --git a/e2e/scenarios/lib/accounts.ts b/e2e/scenarios/lib/accounts.ts index 6796a7aea..45c1a0e5b 100644 --- a/e2e/scenarios/lib/accounts.ts +++ b/e2e/scenarios/lib/accounts.ts @@ -12,7 +12,9 @@ const checkBalance = async ( const balance = await getBalanceFn(address) if (balance.lt(amount)) { throw new Error( - `Sender does not have enough funds to distribute! Required ${amount} - Balance ${balance}`, + `Sender does not have enough funds to distribute! Required ${amount} - Balance ${ethers.utils.formatEther( + balance, + )}`, ) } } @@ -20,6 +22,7 @@ const checkBalance = async ( const ensureBalance = async ( beneficiary: string, amount: BigNumberish, + symbol: string, getBalanceFn: (address: string) => Promise, transferFn: ( address: string, @@ -30,7 +33,7 @@ const ensureBalance = async ( const balanceDif = BigNumber.from(amount).sub(balance) if (balanceDif.gt(0)) { - console.log(`Funding ${beneficiary} with ${balanceDif}...`) + console.log(`Funding ${beneficiary} with ${ethers.utils.formatEther(balanceDif)} ${symbol}...`) const tx = await transferFn(beneficiary, balanceDif) await tx.wait() } @@ -48,6 +51,7 @@ export const ensureETHBalance = async ( await ensureBalance( beneficiaries[index], amounts[index], + 'ETH', ethers.provider.getBalance, (address: string, amount: BigNumber) => { return sender.sendTransaction({ to: address, value: amount }) @@ -66,9 +70,12 @@ export const ensureGRTAllowance = async ( const allowTokens = BigNumber.from(amount).sub(allowance) if (allowTokens.gt(0)) { console.log( - `\nApproving ${spender} to spend ${allowTokens} tokens on ${owner.address} behalf...`, + `\nApproving ${spender} to spend ${ethers.utils.formatEther(allowTokens)} GRT on ${ + owner.address + } behalf...`, ) - await grt.connect(owner).approve(spender, amount) + const tx = await grt.connect(owner).approve(spender, amount) + await tx.wait() } } @@ -112,6 +119,7 @@ export const fundAccountsGRT = async ( await ensureBalance( beneficiaries[index], amounts[index], + 'GRT', grt.balanceOf, grt.connect(sender).transfer, ) diff --git a/e2e/scenarios/lib/staking.ts b/e2e/scenarios/lib/staking.ts index 234d6b1ce..d221afccd 100644 --- a/e2e/scenarios/lib/staking.ts +++ b/e2e/scenarios/lib/staking.ts @@ -11,9 +11,11 @@ export const stake = async ( ): Promise => { // Approve await ensureGRTAllowance(indexer, contracts.Staking.address, amount, contracts.GraphToken) + const allowance = await contracts.GraphToken.allowance(indexer.address, contracts.Staking.address) + console.log(`Allowance: ${ethers.utils.formatEther(allowance)}`) // Stake - console.log(`\nStaking ${amount} tokens...`) + console.log(`\nStaking ${ethers.utils.formatEther(amount)} tokens...`) await sendTransaction(indexer, contracts.Staking, 'stake', [amount]) } diff --git a/gre/config.ts b/gre/config.ts index 14e4008ac..9899f6d13 100644 --- a/gre/config.ts +++ b/gre/config.ts @@ -7,7 +7,7 @@ import { HttpNetworkConfig } from 'hardhat/types/config' import { GraphRuntimeEnvironmentOptions } from './type-extensions' import { GREPluginError } from './helpers/error' -import GraphNetwork from './helpers/network' +import GraphNetwork, { counterpartName } from './helpers/network' import { createProvider } from 'hardhat/internal/core/providers/construction' import { EthersProviderWrapper } from '@nomiclabs/hardhat-ethers/internal/ethers-provider-wrapper' @@ -217,7 +217,7 @@ function getNetworkConfig( chainId: number, mainNetworkName: string, ): (NetworkConfig & { name: string }) | undefined { - let candidateNetworks = Object.keys(networks) + const candidateNetworks = Object.keys(networks) .map((n) => ({ ...networks[n], name: n })) .filter((n) => n.chainId === chainId) @@ -226,14 +226,26 @@ function getNetworkConfig( `Found multiple networks with chainId ${chainId}, trying to use main network name to desambiguate`, ) - candidateNetworks = candidateNetworks.filter((n) => n.name === mainNetworkName) + const filteredByMainNetworkName = candidateNetworks.filter((n) => n.name === mainNetworkName) - if (candidateNetworks.length === 1) { - return candidateNetworks[0] + if (filteredByMainNetworkName.length === 1) { + logDebug(`Found network with chainId ${chainId} and name ${mainNetworkName}`) + return filteredByMainNetworkName[0] } else { - throw new GREPluginError( - `Found multiple networks with chainID ${chainId}. This is not supported!`, + logWarn(`Could not desambiguate with main network name, trying secondary network name`) + const secondaryNetworkName = counterpartName(mainNetworkName) + const filteredBySecondaryNetworkName = candidateNetworks.filter( + (n) => n.name === secondaryNetworkName, ) + + if (filteredBySecondaryNetworkName.length === 1) { + logDebug(`Found network with chainId ${chainId} and name ${mainNetworkName}`) + return filteredBySecondaryNetworkName[0] + } else { + throw new GREPluginError( + `Could not desambiguate network with chainID ${chainId}. Use case not supported!`, + ) + } } } else if (candidateNetworks.length === 1) { return candidateNetworks[0] @@ -242,7 +254,7 @@ function getNetworkConfig( } } -function getNetworkName( +export function getNetworkName( networks: NetworksConfig, chainId: number, mainNetworkName: string, diff --git a/gre/helpers/network.ts b/gre/helpers/network.ts index 5fc2829ff..4530904b6 100644 --- a/gre/helpers/network.ts +++ b/gre/helpers/network.ts @@ -16,15 +16,32 @@ const chainMap = new MapWithGetKey([ [1337, 412346], // Localhost - Arbitrum Localhost ]) +// Hardhat network names as per our convention +const nameMap = new MapWithGetKey([ + ['mainnet', 'arbitrum-one'], // Ethereum Mainnet - Arbitrum One + ['rinkeby', 'arbitrum-rinkeby'], // Ethereum Rinkeby - Arbitrum Rinkeby + ['goerli', 'arbitrum-goerli'], // Ethereum Goerli - Arbitrum Goerli + ['localnitrol1', 'localnitrol2'], // Arbitrum testnode L1 - Arbitrum testnode L2 +]) + export const l1Chains = Array.from(chainMap.keys()) export const l2Chains = Array.from(chainMap.values()) export const chains = [...l1Chains, ...l2Chains] +export const l1ChainNames = Array.from(nameMap.keys()) +export const l2ChainNames = Array.from(nameMap.values()) +export const chainNames = [...l1ChainNames, ...l2ChainNames] + export const isL1 = (chainId: number): boolean => l1Chains.includes(chainId) export const isL2 = (chainId: number): boolean => l2Chains.includes(chainId) export const isSupported = (chainId: number | undefined): boolean => chainId !== undefined && chains.includes(chainId) +export const isL1Name = (name: string): boolean => l1ChainNames.includes(name) +export const isL2Name = (name: string): boolean => l2ChainNames.includes(name) +export const isSupportedName = (name: string | undefined): boolean => + name !== undefined && chainNames.includes(name) + export const l1ToL2 = (chainId: number): number | undefined => chainMap.get(chainId) export const l2ToL1 = (chainId: number): number | undefined => chainMap.getKey(chainId) export const counterpart = (chainId: number): number | undefined => { @@ -32,6 +49,13 @@ export const counterpart = (chainId: number): number | undefined => { return isL1(chainId) ? l1ToL2(chainId) : l2ToL1(chainId) } +export const l1ToL2Name = (name: string): string | undefined => nameMap.get(name) +export const l2ToL1Name = (name: string): string | undefined => nameMap.getKey(name) +export const counterpartName = (name: string): string | undefined => { + if (!isSupportedName(name)) return + return isL1Name(name) ? l1ToL2Name(name) : l2ToL1Name(name) +} + export default { l1Chains, l2Chains, diff --git a/gre/test/config.test.ts b/gre/test/config.test.ts index d206220e6..de6287e2e 100644 --- a/gre/test/config.test.ts +++ b/gre/test/config.test.ts @@ -1,7 +1,13 @@ import { expect } from 'chai' import { useEnvironment } from './helpers' -import { getAddressBookPath, getChains, getGraphConfigPaths, getProviders } from '../config' +import { + getAddressBookPath, + getChains, + getGraphConfigPaths, + getNetworkName, + getProviders, +} from '../config' import path from 'path' describe('GRE init functions', function () { @@ -104,6 +110,36 @@ describe('GRE init functions', function () { }) }) + describe('getProviders with graph-config-desambiguate project', function () { + useEnvironment('graph-config-desambiguate', 'localnitrol1') + + it('should use main network name to desambiguate if multiple chains are defined with same chainId', async function () { + const { l1Provider, l2Provider } = getProviders(this.hre, 1337, 412346, true) + expect(l1Provider).to.be.an('object') + expect(l2Provider).to.be.an('object') + + const l1NetworkName = getNetworkName(this.hre.config.networks, 1337, 'localnitrol1') + const l2NetworkName = getNetworkName(this.hre.config.networks, 412346, 'localnitrol1') + expect(l1NetworkName).to.equal('localnitrol1') + expect(l2NetworkName).to.equal('localnitrol2') + }) + }) + + describe('getProviders with graph-config-desambiguate project', function () { + useEnvironment('graph-config-desambiguate', 'localnitrol2') + + it('should use secondary network name to desambiguate if multiple chains are defined with same chainId', async function () { + const { l1Provider, l2Provider } = getProviders(this.hre, 1337, 412346, true) + expect(l1Provider).to.be.an('object') + expect(l2Provider).to.be.an('object') + + const l1NetworkName = getNetworkName(this.hre.config.networks, 1337, 'localnitrol2') + const l2NetworkName = getNetworkName(this.hre.config.networks, 412346, 'localnitrol2') + expect(l1NetworkName).to.equal('localnitrol1') + expect(l2NetworkName).to.equal('localnitrol2') + }) + }) + describe('getGraphConfigPaths with graph-config-full project', function () { useEnvironment('graph-config-full') diff --git a/gre/test/fixture-projects/graph-config-desambiguate/hardhat.config.ts b/gre/test/fixture-projects/graph-config-desambiguate/hardhat.config.ts new file mode 100644 index 000000000..485ed8665 --- /dev/null +++ b/gre/test/fixture-projects/graph-config-desambiguate/hardhat.config.ts @@ -0,0 +1,59 @@ +import '../../../gre' + +module.exports = { + paths: { + graph: '../../files', + }, + solidity: '0.8.9', + defaultNetwork: 'hardhat', + networks: { + hardhat: { + chainId: 1337, + }, + localhost: { + chainId: 1337, + url: `http://localhost:8545`, + }, + localnitrol1: { + chainId: 1337, + url: `http://localhost:8545`, + }, + localnitrol2: { + chainId: 412346, + url: `http://localhost:8547`, + }, + mainnet: { + chainId: 1, + graphConfig: 'config/graph.mainnet.yml', + url: `https://mainnet.infura.io/v3/123456`, + }, + 'arbitrum-one': { + chainId: 42161, + url: 'https://arb1.arbitrum.io/rpc', + graphConfig: 'config/graph.arbitrum-goerli.yml', + }, + goerli: { + chainId: 5, + url: `https://goerli.infura.io/v3/123456`, + graphConfig: 'config/graph.goerli.yml', + }, + 'arbitrum-goerli': { + chainId: 421613, + url: 'https://goerli-rollup.arbitrum.io/rpc', + graphConfig: 'config/graph.arbitrum-goerli.yml', + }, + rinkeby: { + chainId: 4, + url: `https://goerli.infura.io/v3/123456`, + }, + 'arbitrum-rinkeby': { + chainId: 421611, + url: `https://goerli.infura.io/v3/123456`, + }, + }, + graph: { + addressBook: 'addresses-hre.json', + l1GraphConfig: 'config/graph.hre.yml', + l2GraphConfig: 'config/graph.arbitrum-hre.yml', + }, +} diff --git a/gre/test/helpers.ts b/gre/test/helpers.ts index f0875a002..81c97cd9b 100644 --- a/gre/test/helpers.ts +++ b/gre/test/helpers.ts @@ -20,5 +20,6 @@ export function useEnvironment(fixtureProjectName: string, network?: string): vo afterEach('Resetting hardhat', function () { resetHardhatContext() + delete process.env.HARDHAT_NETWORK }) } diff --git a/hardhat.config.ts b/hardhat.config.ts index 3502efd6c..aa89f8715 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -152,6 +152,16 @@ const config: HardhatUserConfig = { accounts: process.env.FORK === 'true' ? getAccountsKeys() : { mnemonic: DEFAULT_TEST_MNEMONIC }, }, + localnitrol1: { + chainId: 1337, + url: 'http://localhost:8545', + accounts: { mnemonic: DEFAULT_TEST_MNEMONIC }, + }, + localnitrol2: { + chainId: 412346, + url: 'http://localhost:8547', + accounts: { mnemonic: DEFAULT_TEST_MNEMONIC }, + }, }, graph: { addressBook: process.env.ADDRESS_BOOK ?? 'addresses.json', diff --git a/package.json b/package.json index e1d368ffa..728c61e11 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "ethers": "^5.6.0" }, "devDependencies": { - "@arbitrum/sdk": "^3.0.0-beta.5", + "@arbitrum/sdk": "^3.0.0-beta.6", "@commitlint/cli": "^13.2.1", "@commitlint/config-conventional": "^13.2.0", "@defi-wonderland/smock": "^2.0.7", @@ -89,6 +89,7 @@ "deploy-localhost": "yarn deploy --force --network localhost --graph-config config/graph.localhost.yml", "deploy-rinkeby": "yarn deploy --force --network rinkeby --graph-config config/graph.rinkeby.yml", "deploy-goerli": "yarn deploy --force --network goerli --graph-config config/graph.goerli.yml", + "deploy-arbitrum-goerli": "yarn deploy --force --network arbitrum-goerli --graph-config config/graph.arbitrum-goerli.yml", "predeploy": "scripts/predeploy", "test": "scripts/test", "test:e2e": "scripts/e2e", diff --git a/scripts/e2e b/scripts/e2e index 049579d9c..3690cd6ac 100755 --- a/scripts/e2e +++ b/scripts/e2e @@ -4,44 +4,70 @@ set -eo pipefail source $(pwd)/scripts/evm # Allow overriding config -GRAPH_CONFIG=${1:-"config/graph.localhost.yml"} -ADDRESS_BOOK=${2:-"addresses.json"} +GRAPH_CONFIG=${GRAPH_CONFIG:-"config/graph.localhost.yml"} +ADDRESS_BOOK=${ADDRESS_BOOK:-"addresses.json"} +NETWORK=${NETWORK:-"localhost"} + +echo "Running e2e tests" +echo "- Using config: $GRAPH_CONFIG" +echo "- Using address book: $ADDRESS_BOOK" +echo "- Using network: $NETWORK" ### Setup # Compile contracts yarn build # Start evm -evm_kill -evm_start -sleep 5 +if [[ "$NETWORK" == "localhost" ]]; then + evm_kill + evm_start + sleep 5 +fi + +# Create address book if needed +if [[ ! -f "$ADDRESS_BOOK" ]]; then + echo '{}' > "$ADDRESS_BOOK" +fi # Pre-deploy actions -npx hardhat migrate:accounts --network localhost --graph-config "$GRAPH_CONFIG" +npx hardhat migrate:accounts --network "$NETWORK" --graph-config "$GRAPH_CONFIG" +if [[ "$NETWORK" == *"localnitro"* ]]; then + npx hardhat migrate:accounts:nitro --network "$NETWORK" --graph-config "$GRAPH_CONFIG" +fi # Deploy protocol npx hardhat migrate \ - --network localhost \ + --network "$NETWORK" \ --skip-confirmation \ --auto-mine \ --graph-config "$GRAPH_CONFIG" \ --address-book "$ADDRESS_BOOK" # Post deploy actions -npx hardhat migrate:ownership --network localhost --graph-config "$GRAPH_CONFIG" --address-book "$ADDRESS_BOOK" -npx hardhat migrate:unpause --network localhost --graph-config "$GRAPH_CONFIG" --address-book "$ADDRESS_BOOK" +npx hardhat migrate:ownership --network "$NETWORK" --graph-config "$GRAPH_CONFIG" --address-book "$ADDRESS_BOOK" +npx hardhat migrate:unpause --network "$NETWORK" --graph-config "$GRAPH_CONFIG" --address-book "$ADDRESS_BOOK" ### Test -# Run tests using the localhost evm instance and the localhost graph config -npx hardhat e2e --network localhost --graph-config "$GRAPH_CONFIG" --address-book "$ADDRESS_BOOK" -npx hardhat e2e:scenario create-subgraphs --network localhost --graph-config "$GRAPH_CONFIG" --address-book "$ADDRESS_BOOK" -npx hardhat e2e:scenario open-allocations --network localhost --graph-config "$GRAPH_CONFIG" --address-book "$ADDRESS_BOOK" -npx hardhat e2e:scenario close-allocations --network localhost --graph-config "$GRAPH_CONFIG" --address-book "$ADDRESS_BOOK" +# Run tests +npx hardhat e2e --network "$NETWORK" --graph-config "$GRAPH_CONFIG" --address-book "$ADDRESS_BOOK" + +# Skip GRT scenarios in L2 as we don't have bridged GRT yet +if [[ "$NETWORK" != "localnitrol2" ]]; then + npx hardhat e2e:scenario create-subgraphs --network "$NETWORK" --graph-config "$GRAPH_CONFIG" --address-book "$ADDRESS_BOOK" + npx hardhat e2e:scenario open-allocations --network "$NETWORK" --graph-config "$GRAPH_CONFIG" --address-book "$ADDRESS_BOOK" +fi + +# skip close-allocations for arbitrum testnodes as we can't advance epoch +if [[ "$NETWORK" != *"localnitro"* ]]; then + npx hardhat e2e:scenario close-allocations --network "$NETWORK" --graph-config "$GRAPH_CONFIG" --address-book "$ADDRESS_BOOK" +fi ### Cleanup # Exit error mode so the evm instance always gets killed -set +e -result=0 +if [[ "$NETWORK" == "localhost" ]]; then + set +e + result=0 -evm_kill -exit $result + evm_kill + exit $result +fi diff --git a/tasks/deployment/accounts.ts b/tasks/deployment/accounts.ts index e5be935ea..c09ff2a2f 100644 --- a/tasks/deployment/accounts.ts +++ b/tasks/deployment/accounts.ts @@ -2,6 +2,7 @@ import { task } from 'hardhat/config' import { cliOpts } from '../../cli/defaults' import { updateItemValue, writeConfig } from '../../cli/config' +import { BigNumber, ContractTransaction } from 'ethers' task('migrate:accounts', 'Creates protocol accounts and saves them in graph config') .addOptionalParam('graphConfig', cliOpts.graphConfig.description) @@ -38,3 +39,69 @@ task('migrate:accounts', 'Creates protocol accounts and saves them in graph conf writeConfig(taskArgs.graphConfig, graphConfig.toString()) }) + +task('migrate:accounts:nitro', 'Funds protocol accounts on Arbitrum Nitro testnodes') + .addOptionalParam('graphConfig', cliOpts.graphConfig.description) + .addOptionalParam('privateKey', 'The private key for Arbitrum testnode genesis account') + .addOptionalParam('amount', 'The amount to fund each account with') + .setAction(async (taskArgs, hre) => { + // Arbitrum Nitro testnodes have a pre-funded genesis account whose private key is hardcoded here: + // - L1 > https://github.com/OffchainLabs/nitro/blob/01c558c06ad9cbaa083bebe3e51960e195c3fd6b/test-node.bash#L136 + // - L2 > https://github.com/OffchainLabs/nitro/blob/01c558c06ad9cbaa083bebe3e51960e195c3fd6b/testnode-scripts/config.ts#L22 + const genesisAccountPrivateKey = + taskArgs.privateKey ?? 'e887f7d17d07cc7b8004053fb8826f6657084e88904bb61590e498ca04704cf2' + const genesisAccount = new hre.ethers.Wallet(genesisAccountPrivateKey) + + // Get protocol accounts + const { getDeployer, getNamedAccounts, getTestAccounts, provider } = hre.graph(taskArgs) + const deployer = await getDeployer() + const testAccounts = await getTestAccounts() + const namedAccounts = await getNamedAccounts() + const accounts = [ + deployer, + ...testAccounts, + ...Object.keys(namedAccounts).map((k) => namedAccounts[k]), + ] + + // Amount to fund + // - If amount is specified, use that + // - Otherwise, use 95% of genesis account balance with a maximum of 100 Eth + let amount: BigNumber + const maxAmount = hre.ethers.utils.parseEther('100') + const genesisAccountBalance = await provider.getBalance(genesisAccount.address) + + if (taskArgs.amount) { + amount = hre.ethers.BigNumber.from(taskArgs.amount) + } else { + const splitGenesisBalance = genesisAccountBalance.mul(95).div(100).div(accounts.length) + if (splitGenesisBalance.gt(maxAmount)) { + amount = maxAmount + } else { + amount = splitGenesisBalance + } + } + + // Check genesis account balance + const requiredFunds = amount.mul(accounts.length) + if (genesisAccountBalance.lt(requiredFunds)) { + throw new Error('Insufficient funds in genesis account') + } + + // Fund accounts + console.log('> Funding protocol addresses') + console.log(`Genesis account: ${genesisAccount.address}`) + console.log(`Total accounts: ${accounts.length}`) + console.log(`Amount per account: ${hre.ethers.utils.formatEther(amount)}`) + console.log(`Required funds: ${hre.ethers.utils.formatEther(requiredFunds)}`) + + const txs: ContractTransaction[] = [] + for (const account of accounts) { + const tx = await genesisAccount.connect(provider).sendTransaction({ + value: amount, + to: account.address, + }) + txs.push(tx) + } + await Promise.all(txs.map((tx) => tx.wait())) + console.log('Done!') + }) diff --git a/tasks/deployment/ownership.ts b/tasks/deployment/ownership.ts index 68f5561b0..86591c974 100644 --- a/tasks/deployment/ownership.ts +++ b/tasks/deployment/ownership.ts @@ -6,17 +6,18 @@ task('migrate:ownership', 'Accepts ownership of protocol contracts on behalf of .addOptionalParam('addressBook', cliOpts.addressBook.description) .addOptionalParam('graphConfig', cliOpts.graphConfig.description) .setAction(async (taskArgs, hre) => { - const { contracts, getNamedAccounts } = hre.graph(taskArgs) - const { governor } = await getNamedAccounts() + const graph = hre.graph(taskArgs) + const { GraphToken, Controller, GraphProxyAdmin, SubgraphNFT } = graph.contracts + const { governor } = await graph.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 GraphToken.connect(governor).acceptOwnership()) + txs.push(await Controller.connect(governor).acceptOwnership()) + txs.push(await GraphProxyAdmin.connect(governor).acceptOwnership()) + txs.push(await SubgraphNFT.connect(governor).acceptOwnership()) await Promise.all(txs.map((tx) => tx.wait())) console.log('Done!') diff --git a/tasks/deployment/sync.ts b/tasks/deployment/sync.ts new file mode 100644 index 000000000..429f5aba6 --- /dev/null +++ b/tasks/deployment/sync.ts @@ -0,0 +1,71 @@ +import { ContractTransaction } from 'ethers' +import { task } from 'hardhat/config' +import { cliOpts } from '../../cli/defaults' +import { chainIdIsL2 } from '../../cli/cross-chain' + +task('migrate:sync', 'Sync controller contracts') + .addParam('addressBook', cliOpts.addressBook.description, cliOpts.addressBook.default) + .addParam('graphConfig', cliOpts.graphConfig.description, cliOpts.graphConfig.default) + .setAction(async (taskArgs, hre) => { + const { contracts, getDeployer } = hre.graph({ + addressBook: taskArgs.addressBook, + graphConfig: taskArgs.graphConfig, + }) + const deployer = await getDeployer() + + const chainId = hre.network.config.chainId?.toString() ?? '1337' + const isL2 = chainIdIsL2(chainId) + + // Sync contracts + console.log( + `Syncing cache for contract addresses on chainId ${chainId} (${isL2 ? 'L2' : 'L1'})`, + ) + const txs: ContractTransaction[] = [] + console.log('> Syncing cache on Curation') + txs.push(await contracts['Curation'].connect(deployer).syncAllContracts()) + console.log('> Syncing cache on GNS') + txs.push(await contracts['GNS'].connect(deployer).syncAllContracts()) + console.log('> Syncing cache on ServiceRegistry') + txs.push(await contracts['ServiceRegistry'].connect(deployer).syncAllContracts()) + console.log('> Syncing cache on DisputeManager') + txs.push(await contracts['DisputeManager'].connect(deployer).syncAllContracts()) + console.log('> Syncing cache on RewardsManager') + txs.push(await contracts['RewardsManager'].connect(deployer).syncAllContracts()) + console.log('> Syncing cache on Staking') + txs.push(await contracts['Staking'].connect(deployer).syncAllContracts()) + if (isL2) { + console.log('> Syncing cache on L2GraphTokenGateway') + txs.push(await contracts['L2GraphTokenGateway'].connect(deployer).syncAllContracts()) + if (contracts['L2Reservoir']) { + console.log('> Syncing cache on L2Reservoir') + txs.push(await contracts['L2Reservoir'].connect(deployer).syncAllContracts()) + } + } else { + // L1 chains might not have these contracts deployed yet... + if (contracts['L1GraphTokenGateway']) { + console.log('> Syncing cache on L1GraphTokenGateway') + txs.push(await contracts['L1GraphTokenGateway'].connect(deployer).syncAllContracts()) + } else { + console.log('Skipping L1GraphTokenGateway as it does not seem to be deployed yet') + } + if (contracts['BridgeEscrow']) { + console.log('> Syncing cache on BridgeEscrow') + txs.push(await contracts['BridgeEscrow'].connect(deployer).syncAllContracts()) + } else { + console.log('Skipping BridgeEscrow as it does not seem to be deployed yet') + } + if (contracts['L1Reservoir']) { + console.log('> Syncing cache on L1Reservoir') + txs.push(await contracts['L1Reservoir'].connect(deployer).syncAllContracts()) + } else { + console.log('Skipping L1Reservoir as it does not seem to be deployed yet') + } + } + await Promise.all( + txs.map((tx) => { + console.log(tx.hash) + return tx.wait() + }), + ) + console.log('Done!') + }) diff --git a/tasks/deployment/unpause.ts b/tasks/deployment/unpause.ts index 2065c5573..fc10c766f 100644 --- a/tasks/deployment/unpause.ts +++ b/tasks/deployment/unpause.ts @@ -1,15 +1,25 @@ import { task } from 'hardhat/config' import { cliOpts } from '../../cli/defaults' +import GraphChain from '../../gre/helpers/network' -task('migrate:unpause', 'Unpause protocol') +task('migrate:unpause', 'Unpause protocol and bridge') .addOptionalParam('addressBook', cliOpts.addressBook.description) .addOptionalParam('graphConfig', cliOpts.graphConfig.description) .setAction(async (taskArgs, hre) => { - const { contracts, getNamedAccounts } = hre.graph(taskArgs) - const { governor } = await getNamedAccounts() + const graph = hre.graph(taskArgs) + const { governor } = await graph.getNamedAccounts() + const { Controller, L1GraphTokenGateway, L2GraphTokenGateway } = graph.contracts console.log('> Unpausing protocol') - const tx = await contracts.Controller.connect(governor).setPaused(false) + const tx = await Controller.connect(governor).setPaused(false) await tx.wait() + + console.log('> Unpausing bridge') + const GraphTokenGateway = GraphChain.isL2(graph.chainId) + ? L2GraphTokenGateway + : L1GraphTokenGateway + const tx2 = await GraphTokenGateway.connect(governor).setPaused(false) + await tx2.wait() + console.log('Done!') }) diff --git a/yarn.lock b/yarn.lock index b174870d3..49f5fb1b7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@arbitrum/sdk@^3.0.0-beta.5": - version "3.0.0-beta.5" - resolved "https://registry.yarnpkg.com/@arbitrum/sdk/-/sdk-3.0.0-beta.5.tgz#ef1c81de58db9e76defb4a1971274316a375133b" - integrity sha512-qeNdK7es4uKRFciz4zznPEnGRZaAHkrwNqUN1F4U6d4i8olhK0KMdSodx2ZjajBvVVwOo5kFsw5ocAaTvkf28g== +"@arbitrum/sdk@^3.0.0-beta.6": + version "3.0.0-beta.6" + resolved "https://registry.yarnpkg.com/@arbitrum/sdk/-/sdk-3.0.0-beta.6.tgz#a36c3e39a7358396b5533f3288125107da6ae59e" + integrity sha512-kPCfgj72MeyVcIXQKoztLO29UTcpSbXFzc/S0oDgVNNcHcXp1hWUJqqkVRg0O43P2yKjZRT/I94K0Nj2nZNiiQ== dependencies: "@ethersproject/address" "^5.0.8" "@ethersproject/bignumber" "^5.1.1"