diff --git a/.solcover.js b/.solcover.js index 747f078e5..b10738c1f 100644 --- a/.solcover.js +++ b/.solcover.js @@ -1,4 +1,4 @@ -const skipFiles = ['bancor', 'ens', 'erc1056'] +const skipFiles = ['bancor', 'ens', 'erc1056', 'arbitrum', 'tests/arbitrum'] module.exports = { providerOptions: { diff --git a/arbitrum-addresses.json b/arbitrum-addresses.json new file mode 100644 index 000000000..4249270ad --- /dev/null +++ b/arbitrum-addresses.json @@ -0,0 +1,42 @@ +{ + "source": "https://developer.offchainlabs.com/docs/useful_addresses", + "1": { + "L1GatewayRouter": { + "address": "0x72Ce9c846789fdB6fC1f34aC4AD25Dd9ef7031ef" + }, + "IInbox": { + "address": "0x4Dbd4fc535Ac27206064B68FfCf827b0A60BAB3f" + } + }, + "4": { + "L1GatewayRouter": { + "address": "0x70C143928eCfFaf9F5b406f7f4fC28Dc43d68380" + }, + "IInbox": { + "address": "0x578BAde599406A8fE3d24Fd7f7211c0911F5B29e" + } + }, + "5": { + "L1GatewayRouter": { + "address": "0x4c7708168395aEa569453Fc36862D2ffcDaC588c" + }, + "IInbox": { + "address": "0x6BEbC4925716945D46F0Ec336D5C2564F419682C" + } + }, + "42161": { + "L2GatewayRouter": { + "address": "0x5288c571Fd7aD117beA99bF60FE0846C4E84F933" + } + }, + "421611": { + "L2GatewayRouter": { + "address": "0x9413AD42910c1eA60c737dB5f58d1C504498a3cD" + } + }, + "421613": { + "L2GatewayRouter": { + "address": "0xE5B9d8d42d656d1DcB8065A6c012FE3780246041" + } + } +} diff --git a/cli/address-book.ts b/cli/address-book.ts index 39818c82a..5c938fe2a 100644 --- a/cli/address-book.ts +++ b/cli/address-book.ts @@ -28,7 +28,7 @@ export interface AddressBook { } export const getAddressBook = (path: string, chainId: string): AddressBook => { - if (!path) throw new Error(`A path the the address book file is required.`) + if (!path) throw new Error(`A path to the address book file is required.`) if (!chainId) throw new Error(`A chainId is required.`) const addressBook = JSON.parse(fs.readFileSync(path, 'utf8') || '{}') as AddressBookJson diff --git a/cli/cli.ts b/cli/cli.ts index a9e754536..107d4f6d9 100755 --- a/cli/cli.ts +++ b/cli/cli.ts @@ -8,6 +8,7 @@ import { proxyCommand } from './commands/proxy' import { protocolCommand } from './commands/protocol' import { contractsCommand } from './commands/contracts' import { airdropCommand } from './commands/airdrop' +import { bridgeCommand } from './commands/bridge' import { cliOpts } from './defaults' @@ -27,6 +28,7 @@ yargs .option('m', cliOpts.mnemonic) .option('p', cliOpts.providerUrl) .option('n', cliOpts.accountNumber) + .option('r', cliOpts.arbitrumAddressBook) .option('s', cliOpts.skipConfirmation) .command(deployCommand) .command(migrateCommand) @@ -34,5 +36,6 @@ yargs .command(protocolCommand) .command(contractsCommand) .command(airdropCommand) + .command(bridgeCommand) .demandCommand(1, 'Choose a command from the above list') .help().argv diff --git a/cli/commands/bridge/index.ts b/cli/commands/bridge/index.ts new file mode 100644 index 000000000..fcdf1bcc1 --- /dev/null +++ b/cli/commands/bridge/index.ts @@ -0,0 +1,22 @@ +import yargs, { Argv } from 'yargs' + +import { redeemSendToL2Command, sendToL2Command } from './to-l2' +import { startSendToL1Command, finishSendToL1Command, waitFinishSendToL1Command } from './to-l1' +import { cliOpts } from '../../defaults' + +export const bridgeCommand = { + command: 'bridge', + describe: 'Graph token bridge actions.', + builder: (yargs: Argv): yargs.Argv => { + return yargs + .option('-l', cliOpts.l2ProviderUrl) + .command(sendToL2Command) + .command(redeemSendToL2Command) + .command(startSendToL1Command) + .command(finishSendToL1Command) + .command(waitFinishSendToL1Command) + }, + handler: (): void => { + yargs.showHelp() + }, +} diff --git a/cli/commands/bridge/to-l1.ts b/cli/commands/bridge/to-l1.ts new file mode 100644 index 000000000..f3673a53a --- /dev/null +++ b/cli/commands/bridge/to-l1.ts @@ -0,0 +1,214 @@ +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 { loadAddressBookContract } from '../../contracts' +import { + L2TransactionReceipt, + getL2Network, + L2ToL1MessageStatus, + L2ToL1MessageWriter, +} from '@arbitrum/sdk' +import { L2GraphTokenGateway } from '../../../build/types/L2GraphTokenGateway' +import { BigNumber } from 'ethers' +import { JsonRpcProvider } from '@ethersproject/providers' +import { providers } from 'ethers' + +const FOURTEEN_DAYS_IN_SECONDS = 24 * 3600 * 14 + +const BLOCK_SEARCH_THRESHOLD = 6 * 3600 +const searchForArbBlockByTimestamp = async ( + l2Provider: JsonRpcProvider, + timestamp: number, +): Promise => { + let step = 131072 + let block = await l2Provider.getBlock('latest') + while (block.timestamp > timestamp) { + while (block.number - step < 0) { + step = Math.round(step / 2) + } + block = await l2Provider.getBlock(block.number - step) + } + while (step > 1 && Math.abs(block.timestamp - timestamp) > BLOCK_SEARCH_THRESHOLD) { + step = Math.round(step / 2) + if (block.timestamp - timestamp > 0) { + block = await l2Provider.getBlock(block.number - step) + } else { + block = await l2Provider.getBlock(block.number + step) + } + } + return block.number +} + +const wait = (ms: number): Promise => { + return new Promise((res) => setTimeout(res, ms)) +} + +const waitUntilOutboxEntryCreatedWithCb = async ( + msg: L2ToL1MessageWriter, + provider: providers.Provider, + retryDelay: number, + callback: () => void, +) => { + let done = false + while (!done) { + const status = await msg.status(provider) + if (status == L2ToL1MessageStatus.CONFIRMED || status == L2ToL1MessageStatus.EXECUTED) { + done = true + } else { + callback() + await wait(retryDelay) + } + } +} + +export const startSendToL1 = async (cli: CLIEnvironment, cliArgs: CLIArgs): Promise => { + logger.info(`>>> Sending tokens to L1 <<<\n`) + const l2Provider = getProvider(cliArgs.l2ProviderUrl) + const l2ChainId = (await l2Provider.getNetwork()).chainId + + if (chainIdIsL2(cli.chainId) || !chainIdIsL2(l2ChainId)) { + throw new Error( + 'Please use an L1 provider in --provider-url, and an L2 provider in --l2-provider-url', + ) + } + + const l1GRT = cli.contracts['GraphToken'] + const l1GRTAddress = l1GRT.address + const amount = toGRT(cliArgs.amount) + const recipient = cliArgs.recipient ? cliArgs.recipient : cli.wallet.address + const l2Wallet = cli.wallet.connect(l2Provider) + const l2AddressBook = getAddressBook(cliArgs.addressBook, l2ChainId.toString()) + + const gateway = loadAddressBookContract('L2GraphTokenGateway', l2AddressBook, l2Wallet) + const l2GRT = loadAddressBookContract('L2GraphToken', l2AddressBook, l2Wallet) + + 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 params = [l1GRTAddress, recipient, amount, '0x'] + logger.info('Approving token transfer') + await sendTransaction(l2Wallet, l2GRT, 'approve', [gateway.address, amount]) + logger.info('Sending outbound transfer transaction') + const receipt = await sendTransaction( + l2Wallet, + gateway, + 'outboundTransfer(address,address,uint256,bytes)', + params, + ) + const l2Receipt = new L2TransactionReceipt(receipt) + const l2ToL1Message = ( + await l2Receipt.getL2ToL1Messages(cli.wallet, await getL2Network(l2Provider)) + )[0] + + logger.info(`The transaction generated an L2 to L1 message in outbox with eth block number:`) + logger.info(l2ToL1Message.event.ethBlockNum.toString()) + logger.info( + `After the dispute period is finalized (in ~1 week), you can finalize this by calling`, + ) + logger.info(`finish-send-to-l1 with the following txhash:`) + logger.info(l2Receipt.transactionHash) +} + +export const finishSendToL1 = async ( + cli: CLIEnvironment, + cliArgs: CLIArgs, + wait: boolean, +): Promise => { + logger.info(`>>> Finishing transaction sending tokens to L1 <<<\n`) + const l2Provider = getProvider(cliArgs.l2ProviderUrl) + const l2ChainId = (await l2Provider.getNetwork()).chainId + + if (chainIdIsL2(cli.chainId) || !chainIdIsL2(l2ChainId)) { + throw new Error( + 'Please use an L1 provider in --provider-url, and an L2 provider in --l2-provider-url', + ) + } + + const l2AddressBook = getAddressBook(cliArgs.addressBook, l2ChainId.toString()) + + const gateway = loadAddressBookContract( + 'L2GraphTokenGateway', + l2AddressBook, + l2Provider, + ) as L2GraphTokenGateway + let txHash: string + if (cliArgs.txHash) { + txHash = cliArgs.txHash + } else { + logger.info( + `Looking for withdrawals initiated by ${cli.wallet.address} in roughly the last 14 days`, + ) + const fromBlock = await searchForArbBlockByTimestamp( + l2Provider, + Math.round(Date.now() / 1000) - FOURTEEN_DAYS_IN_SECONDS, + ) + const filt = gateway.filters.WithdrawalInitiated(null, cli.wallet.address) + const allEvents = await gateway.queryFilter(filt, BigNumber.from(fromBlock).toHexString()) + if (allEvents.length == 0) { + throw new Error('No withdrawals found') + } + txHash = allEvents[allEvents.length - 1].transactionHash + } + logger.info(`Getting receipt from transaction ${txHash}`) + const receipt = await l2Provider.getTransactionReceipt(txHash) + + const l2Receipt = new L2TransactionReceipt(receipt) + logger.info(`Getting L2 to L1 message...`) + const l2ToL1Message = ( + await l2Receipt.getL2ToL1Messages(cli.wallet, await getL2Network(l2Provider)) + )[0] + + if (wait) { + const retryDelayMs = cliArgs.retryDelaySeconds ? cliArgs.retryDelaySeconds * 1000 : 60000 + logger.info('Waiting for outbox entry to be created, this can take a full week...') + await waitUntilOutboxEntryCreatedWithCb(l2ToL1Message, l2Provider, retryDelayMs, () => { + logger.info('Still waiting...') + }) + } else { + const status = await l2ToL1Message.status(l2Provider) + if (status == L2ToL1MessageStatus.EXECUTED) { + throw new Error('Message already executed!') + } else if (status != L2ToL1MessageStatus.CONFIRMED) { + throw new Error( + `Transaction is not confirmed, status is ${status} when it should be ${L2ToL1MessageStatus.CONFIRMED}. Has the dispute period passed?`, + ) + } + } + + logger.info('Executing outbox transaction') + const tx = await l2ToL1Message.execute(l2Provider) + const outboxExecuteReceipt = await tx.wait() + logger.info('Transaction succeeded! tx hash:') + logger.info(outboxExecuteReceipt.transactionHash) +} + +export const startSendToL1Command = { + command: 'start-send-to-l1 [recipient]', + describe: 'Start an L2-to-L1 Graph Token transaction', + handler: async (argv: CLIArgs): Promise => { + return startSendToL1(await loadEnv(argv), argv) + }, +} + +export const finishSendToL1Command = { + command: 'finish-send-to-l1 [txHash]', + describe: + 'Finish an L2-to-L1 Graph Token transaction. L2 dispute period must have completed. ' + + 'If txHash is not specified, the last withdrawal from the main account in the past 14 days will be redeemed.', + handler: async (argv: CLIArgs): Promise => { + return finishSendToL1(await loadEnv(argv), argv, false) + }, +} + +export const waitFinishSendToL1Command = { + command: 'wait-finish-send-to-l1 [txHash] [retryDelaySeconds]', + describe: + "Wait for an L2-to-L1 Graph Token transaction's dispute period to complete (which takes about a week), and then finalize it. " + + 'If txHash is not specified, the last withdrawal from the main account in the past 14 days will be redeemed.', + handler: async (argv: CLIArgs): Promise => { + return finishSendToL1(await loadEnv(argv), argv, true) + }, +} diff --git a/cli/commands/bridge/to-l2.ts b/cli/commands/bridge/to-l2.ts new file mode 100644 index 000000000..f176bc482 --- /dev/null +++ b/cli/commands/bridge/to-l2.ts @@ -0,0 +1,151 @@ +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' + +const logAutoRedeemReason = (autoRedeemRec) => { + if (autoRedeemRec == null) { + logger.info(`Auto redeem was not attempted.`) + return + } + logger.info(`Auto redeem reverted.`) +} + +const checkAndRedeemMessage = async (l1ToL2Message: L1ToL2MessageWriter) => { + logger.info(`Waiting for status of ${l1ToL2Message.retryableCreationId}`) + const res = await l1ToL2Message.waitForStatus() + logger.info('Getting auto redeem attempt') + const autoRedeemRec = await l1ToL2Message.getAutoRedeemAttempt() + const l2TxReceipt = res.status === L1ToL2MessageStatus.REDEEMED ? res.l2TxReceipt : autoRedeemRec + let l2TxHash = l2TxReceipt ? l2TxReceipt.transactionHash : 'null' + if (res.status === L1ToL2MessageStatus.FUNDS_DEPOSITED_ON_L2) { + /** Message wasn't auto-redeemed! */ + logger.warn('Funds were deposited on L2 but the retryable ticket was not redeemed') + logAutoRedeemReason(autoRedeemRec) + logger.info('Attempting to redeem...') + await l1ToL2Message.redeem() + l2TxHash = (await l1ToL2Message.getSuccessfulRedeem()).transactionHash + } else if (res.status != L1ToL2MessageStatus.REDEEMED) { + throw new Error(`Unexpected L1ToL2MessageStatus ${res.status}`) + } + logger.info(`Transfer successful: ${l2TxHash}`) +} + +export const sendToL2 = async (cli: CLIEnvironment, cliArgs: CLIArgs): Promise => { + logger.info(`>>> Sending tokens to L2 <<<\n`) + const l2Provider = getProvider(cliArgs.l2ProviderUrl) + const l2ChainId = (await l2Provider.getNetwork()).chainId + + if (chainIdIsL2(cli.chainId) || !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 + const amount = toGRT(cliArgs.amount) + const recipient = cliArgs.recipient ? cliArgs.recipient : cli.wallet.address + const l2Dest = await gateway.l2Counterpart() + + logger.info(`Will send ${cliArgs.amount} GRT to ${recipient}`) + logger.info(`Using L1 gateway ${gateway.address} and L2 gateway ${l2Dest}`) + // See https://github.com/OffchainLabs/arbitrum/blob/master/packages/arb-ts/src/lib/bridge.ts + const depositCalldata = await gateway.getOutboundCalldata( + l1GRTAddress, + cli.wallet.address, + recipient, + amount, + '0x', + ) + + // 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, + depositCalldata, + parseEther('0'), + baseFee, + gateway.address, + gateway.address, + ) + const maxGas = gasParams.maxGasBid + const gasPriceBid = gasParams.maxGasPriceBid + const maxSubmissionPrice = gasParams.maxSubmissionPriceBid + logger.info( + `Using max gas: ${maxGas}, gas price bid: ${gasPriceBid}, max submission price: ${maxSubmissionPrice}`, + ) + + 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]) + logger.info('Sending outbound transfer transaction') + const receipt = await sendTransaction(cli.wallet, gateway, 'outboundTransfer', params, { + 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) + } +} + +export const redeemSendToL2 = async (cli: CLIEnvironment, cliArgs: CLIArgs): Promise => { + logger.info(`>>> Redeeming pending tokens on L2 <<<\n`) + const l2Provider = getProvider(cliArgs.l2ProviderUrl) + const l2ChainId = (await l2Provider.getNetwork()).chainId + + if (chainIdIsL2(cli.chainId) || !chainIdIsL2(l2ChainId)) { + throw new Error( + 'Please use an L1 provider in --provider-url, and an L2 provider in --l2-provider-url', + ) + } + const l1Provider = cli.wallet.provider + + const receipt = await l1Provider.getTransactionReceipt(cliArgs.txHash) + const l1Receipt = new L1TransactionReceipt(receipt) + const l1ToL2Messages = await l1Receipt.getL1ToL2Messages(cli.wallet.connect(l2Provider)) + const l1ToL2Message = l1ToL2Messages[0] + + logger.info('Checking message status in L2...') + await checkAndRedeemMessage(l1ToL2Message) +} + +export const sendToL2Command = { + command: 'send-to-l2 [recipient]', + describe: 'Perform an L1-to-L2 Graph Token transaction', + handler: async (argv: CLIArgs): Promise => { + return sendToL2(await loadEnv(argv), argv) + }, +} + +export const redeemSendToL2Command = { + command: 'redeem-send-to-l2 ', + describe: 'Finish an L1-to-L2 Graph Token transaction if it failed to auto-redeem', + handler: async (argv: CLIArgs): Promise => { + return redeemSendToL2(await loadEnv(argv), argv) + }, +} diff --git a/cli/commands/migrate.ts b/cli/commands/migrate.ts index 1f33b0f89..fb817cd67 100644 --- a/cli/commands/migrate.ts +++ b/cli/commands/migrate.ts @@ -11,13 +11,14 @@ import { sendTransaction, } from '../network' import { loadEnv, CLIArgs, CLIEnvironment } from '../env' +import { chainIdIsL2 } from '../utils' import { confirm } from '../helpers' const { EtherSymbol } = constants const { formatEther } = utils // Contracts are deployed in the order defined in this list -const allContracts = [ +let allContracts = [ 'GraphProxyAdmin', 'BancorFormula', 'Controller', @@ -33,6 +34,29 @@ const allContracts = [ 'RewardsManager', 'DisputeManager', 'AllocationExchange', + 'L1GraphTokenGateway', + 'BridgeEscrow', + 'L1Reservoir', +] + +const l2Contracts = [ + 'GraphProxyAdmin', + 'BancorFormula', + 'Controller', + 'EpochManager', + 'L2GraphToken', + 'GraphCurationToken', + 'ServiceRegistry', + 'Curation', + 'SubgraphNFTDescriptor', + 'SubgraphNFT', + 'GNS', + 'Staking', + 'RewardsManager', + 'DisputeManager', + 'AllocationExchange', + 'L2GraphTokenGateway', + 'L2Reservoir', ] export const migrate = async (cli: CLIEnvironment, cliArgs: CLIArgs): Promise => { @@ -47,7 +71,10 @@ export const migrate = async (cli: CLIEnvironment, cliArgs: CLIArgs): Promise>> Migrating contracts <<<\n`) diff --git a/cli/commands/protocol/configure-bridge.ts b/cli/commands/protocol/configure-bridge.ts new file mode 100644 index 000000000..d96d462a5 --- /dev/null +++ b/cli/commands/protocol/configure-bridge.ts @@ -0,0 +1,89 @@ +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' + +export const configureL1Bridge = async (cli: CLIEnvironment, cliArgs: CLIArgs): Promise => { + logger.info(`>>> Setting L1 Bridge Configuration <<<\n`) + + if (chainIdIsL2(cli.chainId)) { + throw new Error('Cannot set L1 configuration on an L2 network!') + } + const l2ChainId = cliArgs.l2ChainId ? cliArgs.l2ChainId : l1ToL2ChainIdMap[cli.chainId] + logger.info('Connecting with the contracts on L2 chainId ' + l2ChainId) + const l2AddressBook = getAddressBook(cliArgs.addressBook, l2ChainId) + const arbAddressBook = getAddressBook(cliArgs.arbAddressBook, cli.chainId.toString()) + + const gateway = cli.contracts['L1GraphTokenGateway'] + + const l2GRT = l2AddressBook.getEntry('L2GraphToken') + logger.info('L2 GRT address: ' + l2GRT.address) + await sendTransaction(cli.wallet, gateway, 'setL2TokenAddress', [l2GRT.address]) + + const l2Counterpart = l2AddressBook.getEntry('L2GraphTokenGateway') + logger.info('L2 Gateway address: ' + l2Counterpart.address) + await sendTransaction(cli.wallet, gateway, 'setL2CounterpartAddress', [l2Counterpart.address]) + + const bridgeEscrow = cli.contracts.BridgeEscrow + logger.info('Escrow address: ' + bridgeEscrow.address) + await sendTransaction(cli.wallet, gateway, 'setEscrowAddress', [bridgeEscrow.address]) + await sendTransaction(cli.wallet, bridgeEscrow, 'approveAll', [gateway.address]) + + const l1Inbox = arbAddressBook.getEntry('IInbox') + const l1Router = arbAddressBook.getEntry('L1GatewayRouter') + logger.info( + 'L1 Inbox address: ' + l1Inbox.address + ' and L1 Router address: ' + l1Router.address, + ) + await sendTransaction(cli.wallet, gateway, 'setArbitrumAddresses', [ + l1Inbox.address, + l1Router.address, + ]) +} + +export const configureL2Bridge = async (cli: CLIEnvironment, cliArgs: CLIArgs): Promise => { + logger.info(`>>> Setting L2 Bridge Configuration <<<\n`) + + if (!chainIdIsL2(cli.chainId)) { + throw new Error('Cannot set L2 configuration on an L1 network!') + } + const l1ChainId = cliArgs.l1ChainId ? cliArgs.l1ChainId : l2ToL1ChainIdMap[cli.chainId] + logger.info('Connecting with the contracts on L1 chainId ' + l1ChainId) + const l1AddressBook = getAddressBook(cliArgs.addressBook, l1ChainId) + const arbAddressBook = getAddressBook(cliArgs.arbAddressBook, cli.chainId.toString()) + + const gateway = cli.contracts['L2GraphTokenGateway'] + const token = cli.contracts['L2GraphToken'] + + const l1GRT = l1AddressBook.getEntry('GraphToken') + logger.info('L1 GRT address: ' + l1GRT.address) + await sendTransaction(cli.wallet, gateway, 'setL1TokenAddress', [l1GRT.address]) + await sendTransaction(cli.wallet, token, 'setL1Address', [l1GRT.address]) + + const l1Counterpart = l1AddressBook.getEntry('L1GraphTokenGateway') + logger.info('L1 Gateway address: ' + l1Counterpart.address) + await sendTransaction(cli.wallet, gateway, 'setL1CounterpartAddress', [l1Counterpart.address]) + + const l2Router = arbAddressBook.getEntry('L2GatewayRouter') + logger.info('L2 Router address: ' + l2Router.address) + await sendTransaction(cli.wallet, gateway, 'setL2Router', [l2Router.address]) + + logger.info('L2 Gateway address: ' + gateway.address) + await sendTransaction(cli.wallet, token, 'setGateway', [gateway.address]) +} + +export const configureL1BridgeCommand = { + command: 'configure-l1-bridge [l2ChainId]', + describe: 'Configure L1/L2 bridge parameters (L1 side) using the address book', + handler: async (argv: CLIArgs): Promise => { + return configureL1Bridge(await loadEnv(argv), argv) + }, +} + +export const configureL2BridgeCommand = { + command: 'configure-l2-bridge [l1ChainId]', + describe: 'Configure L1/L2 bridge parameters (L2 side) using the address book', + handler: async (argv: CLIArgs): Promise => { + return configureL2Bridge(await loadEnv(argv), argv) + }, +} diff --git a/cli/commands/protocol/get.ts b/cli/commands/protocol/get.ts index 27932d209..439b539b4 100644 --- a/cli/commands/protocol/get.ts +++ b/cli/commands/protocol/get.ts @@ -51,6 +51,18 @@ export const gettersList = { 'controller-get-paused': { contract: 'Controller', name: 'paused' }, 'controller-get-partial-paused': { contract: 'Controller', name: 'partialPaused' }, 'controller-get-pause-guardian': { contract: 'Controller', name: 'pauseGuardian' }, + 'l1-gateway-l2-grt': { contract: 'L1GraphTokenGateway', name: 'l2GRT' }, + 'l1-gateway-inbox': { contract: 'L1GraphTokenGateway', name: 'inbox' }, + 'l1-gateway-escrow': { contract: 'L1GraphTokenGateway', name: 'escrow' }, + 'l1-gateway-l1-router': { contract: 'L1GraphTokenGateway', name: 'l1Router' }, + 'l1-gateway-l2-counterpart': { contract: 'L1GraphTokenGateway', name: 'l2Counterpart' }, + 'l1-gateway-paused': { contract: 'L1GraphTokenGateway', name: 'paused' }, + 'l2-gateway-l1-grt': { contract: 'L2GraphTokenGateway', name: 'l1GRT' }, + 'l2-gateway-l2-router': { contract: 'L2GraphTokenGateway', name: 'l2Router' }, + 'l2-gateway-l1-counterpart': { contract: 'L2GraphTokenGateway', name: 'l1Counterpart' }, + 'l2-gateway-paused': { contract: 'L2GraphTokenGateway', name: 'paused' }, + 'l2-token-gateway': { contract: 'L2GraphToken', name: 'gateway' }, + 'l2-token-l1-address': { contract: 'L2GraphToken', name: 'l1Address' }, } const buildHelp = () => { diff --git a/cli/commands/protocol/index.ts b/cli/commands/protocol/index.ts index 69ca4e4c2..acab2be90 100644 --- a/cli/commands/protocol/index.ts +++ b/cli/commands/protocol/index.ts @@ -3,6 +3,7 @@ import yargs, { Argv } from 'yargs' import { listCommand } from './list' import { getCommand } from './get' import { setCommand } from './set' +import { configureL1BridgeCommand, configureL2BridgeCommand } from './configure-bridge' export interface ProtocolFunction { contract: string @@ -17,7 +18,12 @@ export const protocolCommand = { command: 'protocol', describe: 'Graph protocol configuration', builder: (yargs: Argv): yargs.Argv => { - return yargs.command(getCommand).command(setCommand).command(listCommand) + return yargs + .command(getCommand) + .command(setCommand) + .command(listCommand) + .command(configureL1BridgeCommand) + .command(configureL2BridgeCommand) }, handler: (): void => { yargs.showHelp() diff --git a/cli/commands/protocol/list.ts b/cli/commands/protocol/list.ts index 228151e04..c82ca1d81 100644 --- a/cli/commands/protocol/list.ts +++ b/cli/commands/protocol/list.ts @@ -15,6 +15,9 @@ const contractNames = [ 'DisputeManager', 'RewardsManager', 'GNS', + 'L1GraphTokenGateway', + 'L2GraphToken', + 'L2GraphTokenGateway', ] export const listProtocolParams = async (cli: CLIEnvironment): Promise => { @@ -26,13 +29,15 @@ export const listProtocolParams = async (cli: CLIEnvironment): Promise => colWidths: [30, 50], }) + if (!(contractName in cli.contracts)) { + continue + } const contract = cli.contracts[contractName] table.push(['* address', contract.address]) const req = [] for (const fn of Object.values(gettersList)) { if (fn.contract != contractName) continue - const contract = cli.contracts[fn.contract] if (contract.interface.getFunction(fn.name).inputs.length == 0) { const contractFn: ContractFunction = contract.functions[fn.name] @@ -56,7 +61,7 @@ export const listProtocolParams = async (cli: CLIEnvironment): Promise => const controller = cli.contracts['Controller'] for (const contractName of contractNames) { - if (contractName === 'Controller') continue + if (contractName === 'Controller' || !(contractName in cli.contracts)) continue const contract = cli.contracts[contractName] const contractFn = contract.functions['controller'] diff --git a/cli/commands/protocol/set.ts b/cli/commands/protocol/set.ts index 4fc05f877..966d07897 100644 --- a/cli/commands/protocol/set.ts +++ b/cli/commands/protocol/set.ts @@ -59,6 +59,31 @@ export const settersList = { 'controller-set-paused': { contract: 'Controller', name: 'setPaused' }, 'controller-set-partial-paused': { contract: 'Controller', name: 'setPartialPaused' }, 'controller-set-pause-guardian': { contract: 'Controller', name: 'setPauseGuardian' }, + 'l1-gateway-set-l2-grt': { contract: 'L1GraphTokenGateway', name: 'setL2TokenAddress' }, + 'l1-gateway-set-arbitrum-addresses': { + contract: 'L1GraphTokenGateway', + name: 'setArbitrumAddresses', + }, + 'l1-gateway-set-l2-counterpart': { + contract: 'L1GraphTokenGateway', + name: 'setL2CounterpartAddress', + }, + 'l1-gateway-set-escrow-address': { + contract: 'L1GraphTokenGateway', + name: 'setEscrowAddress', + }, + 'l1-gateway-set-paused': { contract: 'L1GraphTokenGateway', name: 'setPaused' }, + 'bridge-escrow-approve-all': { contract: 'BridgeEscrow', name: 'approveAll' }, + 'bridge-escrow-revoke-all': { contract: 'BridgeEscrow', name: 'revokeAll' }, + 'l2-gateway-set-l1-grt': { contract: 'L2GraphTokenGateway', name: 'setL1TokenAddress' }, + 'l2-gateway-set-l2-router': { contract: 'L2GraphTokenGateway', name: 'setL2Router' }, + 'l2-gateway-set-l1-counterpart': { + contract: 'L2GraphTokenGateway', + name: 'setL1CounterpartAddress', + }, + 'l2-gateway-set-paused': { contract: 'L2GraphTokenGateway', name: 'setPaused' }, + 'l2-token-set-gateway': { contract: 'L2GraphToken', name: 'setGateway' }, + 'l2-token-set-l1-address': { contract: 'L2GraphToken', name: 'setL1Address' }, } const buildHelp = () => { diff --git a/cli/contracts.ts b/cli/contracts.ts index 152d5a261..ff6fb8983 100644 --- a/cli/contracts.ts +++ b/cli/contracts.ts @@ -1,4 +1,4 @@ -import { providers, Signer } from 'ethers' +import { BaseContract, providers, Signer } from 'ethers' import { AddressBook } from './address-book' import { logger } from './logging' @@ -18,6 +18,11 @@ import { BancorFormula } from '../build/types/BancorFormula' import { IENS } from '../build/types/IENS' import { GraphGovernance } from '../build/types/GraphGovernance' import { AllocationExchange } from '../build/types/AllocationExchange' +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' import { SubgraphNFT } from '../build/types/SubgraphNFT' import { GraphCurationToken } from '../build/types/GraphCurationToken' import { SubgraphNFTDescriptor } from '../build/types/SubgraphNFTDescriptor' @@ -37,23 +42,40 @@ export interface NetworkContracts { IENS: IENS GraphGovernance: GraphGovernance AllocationExchange: AllocationExchange + L1GraphTokenGateway: L1GraphTokenGateway + BridgeEscrow: BridgeEscrow + L2GraphToken: L2GraphToken + L2GraphTokenGateway: L2GraphTokenGateway SubgraphNFT: SubgraphNFT SubgraphNFTDescriptor: SubgraphNFTDescriptor GraphCurationToken: GraphCurationToken } +export const loadAddressBookContract = ( + contractName: string, + addressBook: AddressBook, + signerOrProvider?: Signer | providers.Provider, +): BaseContract => { + const contractEntry = addressBook.getEntry(contractName) + let contract = getContractAt(contractName, contractEntry.address) + if (signerOrProvider) { + contract = contract.connect(signerOrProvider) + } + return contract +} + export const loadContracts = ( addressBook: AddressBook, + chainId: number | string, signerOrProvider?: Signer | providers.Provider, ): NetworkContracts => { const contracts = {} for (const contractName of addressBook.listEntries()) { - const contractEntry = addressBook.getEntry(contractName) try { - const contract = getContractAt(contractName, contractEntry.address) - contracts[contractName] = contract - if (signerOrProvider) { - contracts[contractName] = contracts[contractName].connect(signerOrProvider) + contracts[contractName] = loadAddressBookContract(contractName, addressBook, signerOrProvider) + // On L2 networks, we alias L2GraphToken as GraphToken + if (signerOrProvider && chainIdIsL2(chainId) && contractName == 'L2GraphToken') { + contracts['GraphToken'] = contracts[contractName] } } catch (err) { logger.warn(`Could not load contract ${contractName} - ${err.message}`) diff --git a/cli/defaults.ts b/cli/defaults.ts index 5c0a1dd6a..4ef211b0f 100644 --- a/cli/defaults.ts +++ b/cli/defaults.ts @@ -7,6 +7,8 @@ export const local = { addressBookPath: './addresses.json', graphConfigPath: './config/graph.mainnet.yml', accountNumber: '0', + arbitrumAddressBookPath: './arbitrum-addresses.json', + arbProviderUrl: 'https://rinkeby.arbitrum.io/rpc', } export const defaultOverrides: Overrides = { @@ -56,6 +58,20 @@ export const cliOpts = { type: 'boolean', default: false, }, + arbitrumAddressBook: { + alias: 'arb-address-book', + description: 'The path to the address book file for Arbitrum deployments', + type: 'string', + group: 'Config', + default: local.arbitrumAddressBookPath, + }, + l2ProviderUrl: { + alias: 'l2-provider-url', + description: 'The URL of an Arbitrum provider (only for bridge commands)', + type: 'string', + group: 'Arbitrum', + default: local.arbProviderUrl, + }, skipConfirmation: { alias: 'skip-confirmation', description: 'Skip confirmation prompt on write actions', diff --git a/cli/env.ts b/cli/env.ts index d889fcdc6..ab89c9d85 100644 --- a/cli/env.ts +++ b/cli/env.ts @@ -43,7 +43,7 @@ export const loadEnv = async (argv: CLIArgs, wallet?: Wallet): Promise, - overrides?: Overrides, + overrides?: PayableOverrides, ): Promise => { // Setup overrides if (overrides) { diff --git a/cli/utils.ts b/cli/utils.ts index a11e40f4e..846c5ce9d 100644 --- a/cli/utils.ts +++ b/cli/utils.ts @@ -1,7 +1,19 @@ +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, @@ -12,3 +24,7 @@ export const contractAt = ( 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-one.yml b/config/graph.arbitrum-one.yml new file mode 100644 index 000000000..9bcbeeed1 --- /dev/null +++ b/config/graph.arbitrum-one.yml @@ -0,0 +1,127 @@ +general: + arbitrator: &arbitrator "0x113DC95e796836b8F0Fa71eE7fB42f221740c3B0" # Arbitration Council + governor: &governor "0x3e43EF77fAAd296F65eF172E8eF06F8231c9DeAd" # Graph Council + authority: &authority "0x79fd74da4c906509862c8fe93e87a9602e370bc4" # 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: "${{L2GraphToken.address}}" + - fn: "setContractProxy" + id: "0xd362cac9cb75c10d67bcc0b7eeb0b1ef48bb5420b556c092d4fd7f758816fcf0" # keccak256('GraphTokenGateway') + contractAddress: "${{L2GraphTokenGateway.address}}" + - fn: "setContractProxy" + id: "0x96ba401694892957e25e29c7a1e4171ae9945b5ee36339de79b199a530436e9e" # keccak256('Reservoir') + contractAddress: "${{L2Reservoir.address}}" + ServiceRegistry: + proxy: true + init: + controller: "${{Controller.address}}" + EpochManager: + proxy: true + init: + controller: "${{Controller.address}}" + lengthInBlocks: 1108 # 4 hours (in 13 second blocks) + L2GraphToken: + proxy: true + init: + 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 + 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) + 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" # 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) + rebateAlphaNumerator: 77 # rebateAlphaNumerator / rebateAlphaDenominator + rebateAlphaDenominator: 100 # rebateAlphaNumerator / rebateAlphaDenominator + calls: + - fn: "setDelegationTaxPercentage" + delegationTaxPercentage: 5000 # 0.5% (parts per million) + - fn: "setSlasher" + slasher: "${{DisputeManager.address}}" + allowed: true + - fn: "setAssetHolder" + assetHolder: "${{AllocationExchange.address}}" + allowed: true + RewardsManager: + proxy: true + init: + controller: "${{Controller.address}}" + AllocationExchange: + init: + graphToken: "${{GraphToken.address}}" + staking: "${{Staking.address}}" + governor: *governor + authority: *authority + calls: + - fn: "approveAll" + L2GraphTokenGateway: + proxy: true + init: + controller: "${{Controller.address}}" + L2Reservoir: + proxy: true + init: + controller: "${{Controller.address}}" + calls: + - fn: "approveRewardsManager" diff --git a/config/graph.mainnet.yml b/config/graph.mainnet.yml index d82791711..395c01447 100644 --- a/config/graph.mainnet.yml +++ b/config/graph.mainnet.yml @@ -30,6 +30,12 @@ contracts: - fn: "setContractProxy" id: "0x45fc200c7e4544e457d3c5709bfe0d520442c30bbcbdaede89e8d4a4bbc19247" # keccak256('GraphToken') contractAddress: "${{GraphToken.address}}" + - fn: "setContractProxy" + id: "0xd362cac9cb75c10d67bcc0b7eeb0b1ef48bb5420b556c092d4fd7f758816fcf0" # keccak256('GraphTokenGateway') + contractAddress: "${{L1GraphTokenGateway.address}}" + - fn: "setContractProxy" + id: "0x96ba401694892957e25e29c7a1e4171ae9945b5ee36339de79b199a530436e9e" # keccak256('Reservoir') + contractAddress: "${{L1Reservoir.address}}" ServiceRegistry: proxy: true init: @@ -44,7 +50,7 @@ contracts: initialSupply: "10000000000000000000000000000" # in wei calls: - fn: "addMinter" - minter: "${{RewardsManager.address}}" + minter: "${{L1Reservoir.address}}" Curation: proxy: true init: @@ -106,9 +112,6 @@ contracts: proxy: true init: controller: "${{Controller.address}}" - calls: - - fn: "setIssuanceRate" - _issuanceRate: "1000000012184945188" # per block increase of total supply, blocks in a year = 365*60*60*24/13 AllocationExchange: init: graphToken: "${{GraphToken.address}}" @@ -117,3 +120,19 @@ contracts: authority: *authority calls: - fn: "approveAll" + L1GraphTokenGateway: + proxy: true + init: + controller: "${{Controller.address}}" + BridgeEscrow: + proxy: true + init: + controller: "${{Controller.address}}" + L1Reservoir: + proxy: true + init: + controller: "${{Controller.address}}" + dripInterval: 50400 + calls: + - fn: "approveRewardsManager" + - fn: "initialSnapshot" diff --git a/contracts/arbitrum/AddressAliasHelper.sol b/contracts/arbitrum/AddressAliasHelper.sol new file mode 100644 index 000000000..740b70361 --- /dev/null +++ b/contracts/arbitrum/AddressAliasHelper.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright 2019-2021, Offchain Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Originally copied from: + * https://github.com/OffchainLabs/arbitrum/tree/84e64dee6ee82adbf8ec34fd4b86c207a61d9007/packages/arb-bridge-eth + * + * MODIFIED from Offchain Labs' implementation: + * - Changed solidity version to 0.7.6 (pablo@edgeandnode.com) + * + */ + +pragma solidity ^0.7.6; + +library AddressAliasHelper { + uint160 constant offset = uint160(0x1111000000000000000000000000000000001111); + + /// @notice Utility function that converts the address in the L1 that submitted a tx to + /// the inbox to the msg.sender viewed in the L2 + /// @param l1Address the address in the L1 that triggered the tx to L2 + /// @return l2Address L2 address as viewed in msg.sender + function applyL1ToL2Alias(address l1Address) internal pure returns (address l2Address) { + l2Address = address(uint160(l1Address) + offset); + } + + /// @notice Utility function that converts the msg.sender viewed in the L2 to the + /// address in the L1 that submitted a tx to the inbox + /// @param l2Address L2 address as viewed in msg.sender + /// @return l1Address the address in the L1 that triggered the tx to L2 + function undoL1ToL2Alias(address l2Address) internal pure returns (address l1Address) { + l1Address = address(uint160(l2Address) - offset); + } +} diff --git a/contracts/arbitrum/IArbToken.sol b/contracts/arbitrum/IArbToken.sol new file mode 100644 index 000000000..d7d5a2d8c --- /dev/null +++ b/contracts/arbitrum/IArbToken.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright 2020, Offchain Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Originally copied from: + * https://github.com/OffchainLabs/arbitrum/tree/e3a6307ad8a2dc2cad35728a2a9908cfd8dd8ef9/packages/arb-bridge-peripherals + * + * MODIFIED from Offchain Labs' implementation: + * - Changed solidity version to 0.7.6 (pablo@edgeandnode.com) + * + */ + +/** + * @title Minimum expected interface for L2 token that interacts with the L2 token bridge (this is the interface necessary + * for a custom token that interacts with the bridge, see TestArbCustomToken.sol for an example implementation). + */ +pragma solidity ^0.7.6; + +interface IArbToken { + /** + * @notice should increase token supply by amount, and should (probably) only be callable by the L1 bridge. + */ + function bridgeMint(address account, uint256 amount) external; + + /** + * @notice should decrease token supply by amount, and should (probably) only be callable by the L1 bridge. + */ + function bridgeBurn(address account, uint256 amount) external; + + /** + * @return address of layer 1 token + */ + function l1Address() external view returns (address); +} diff --git a/contracts/arbitrum/IBridge.sol b/contracts/arbitrum/IBridge.sol new file mode 100644 index 000000000..ff78253fc --- /dev/null +++ b/contracts/arbitrum/IBridge.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright 2021, Offchain Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Originally copied from: + * https://github.com/OffchainLabs/arbitrum/tree/e3a6307ad8a2dc2cad35728a2a9908cfd8dd8ef9/packages/arb-bridge-eth + * + * MODIFIED from Offchain Labs' implementation: + * - Changed solidity version to 0.7.6 (pablo@edgeandnode.com) + * + */ + +pragma solidity ^0.7.6; + +interface IBridge { + event MessageDelivered( + uint256 indexed messageIndex, + bytes32 indexed beforeInboxAcc, + address inbox, + uint8 kind, + address sender, + bytes32 messageDataHash + ); + + event BridgeCallTriggered( + address indexed outbox, + address indexed destAddr, + uint256 amount, + bytes data + ); + + event InboxToggle(address indexed inbox, bool enabled); + + event OutboxToggle(address indexed outbox, bool enabled); + + function deliverMessageToInbox( + uint8 kind, + address sender, + bytes32 messageDataHash + ) external payable returns (uint256); + + function executeCall( + address destAddr, + uint256 amount, + bytes calldata data + ) external returns (bool success, bytes memory returnData); + + // These are only callable by the admin + function setInbox(address inbox, bool enabled) external; + + function setOutbox(address inbox, bool enabled) external; + + // View functions + + function activeOutbox() external view returns (address); + + function allowedInboxes(address inbox) external view returns (bool); + + function allowedOutboxes(address outbox) external view returns (bool); + + function inboxAccs(uint256 index) external view returns (bytes32); + + function messageCount() external view returns (uint256); +} diff --git a/contracts/arbitrum/IInbox.sol b/contracts/arbitrum/IInbox.sol new file mode 100644 index 000000000..a9315bbf8 --- /dev/null +++ b/contracts/arbitrum/IInbox.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright 2021, Offchain Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Originally copied from: + * https://github.com/OffchainLabs/arbitrum/tree/e3a6307ad8a2dc2cad35728a2a9908cfd8dd8ef9/packages/arb-bridge-eth + * + * MODIFIED from Offchain Labs' implementation: + * - Changed solidity version to 0.7.6 (pablo@edgeandnode.com) + * + */ + +pragma solidity ^0.7.6; + +import "./IBridge.sol"; +import "./IMessageProvider.sol"; + +interface IInbox is IMessageProvider { + function sendL2Message(bytes calldata messageData) external returns (uint256); + + function sendUnsignedTransaction( + uint256 maxGas, + uint256 gasPriceBid, + uint256 nonce, + address destAddr, + uint256 amount, + bytes calldata data + ) external returns (uint256); + + function sendContractTransaction( + uint256 maxGas, + uint256 gasPriceBid, + address destAddr, + uint256 amount, + bytes calldata data + ) external returns (uint256); + + function sendL1FundedUnsignedTransaction( + uint256 maxGas, + uint256 gasPriceBid, + uint256 nonce, + address destAddr, + bytes calldata data + ) external payable returns (uint256); + + function sendL1FundedContractTransaction( + uint256 maxGas, + uint256 gasPriceBid, + address destAddr, + bytes calldata data + ) external payable returns (uint256); + + function createRetryableTicket( + address destAddr, + uint256 arbTxCallValue, + uint256 maxSubmissionCost, + address submissionRefundAddress, + address valueRefundAddress, + uint256 maxGas, + uint256 gasPriceBid, + bytes calldata data + ) external payable returns (uint256); + + function depositEth(uint256 maxSubmissionCost) external payable returns (uint256); + + function bridge() external view returns (IBridge); + + function pauseCreateRetryables() external; + + function unpauseCreateRetryables() external; + + function startRewriteAddress() external; + + function stopRewriteAddress() external; +} diff --git a/contracts/arbitrum/IMessageProvider.sol b/contracts/arbitrum/IMessageProvider.sol new file mode 100644 index 000000000..8fbfdb171 --- /dev/null +++ b/contracts/arbitrum/IMessageProvider.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright 2021, Offchain Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Originally copied from: + * https://github.com/OffchainLabs/arbitrum/tree/e3a6307ad8a2dc2cad35728a2a9908cfd8dd8ef9/packages/arb-bridge-eth + * + * MODIFIED from Offchain Labs' implementation: + * - Changed solidity version to 0.7.6 (pablo@edgeandnode.com) + * + */ + +pragma solidity ^0.7.6; + +interface IMessageProvider { + event InboxMessageDelivered(uint256 indexed messageNum, bytes data); + + event InboxMessageDeliveredFromOrigin(uint256 indexed messageNum); +} diff --git a/contracts/arbitrum/IOutbox.sol b/contracts/arbitrum/IOutbox.sol new file mode 100644 index 000000000..687c86abf --- /dev/null +++ b/contracts/arbitrum/IOutbox.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright 2021, Offchain Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Originally copied from: + * https://github.com/OffchainLabs/arbitrum/tree/e3a6307ad8a2dc2cad35728a2a9908cfd8dd8ef9/packages/arb-bridge-eth + * + * MODIFIED from Offchain Labs' implementation: + * - Changed solidity version to 0.7.6 (pablo@edgeandnode.com) + * + */ + +pragma solidity ^0.7.6; + +interface IOutbox { + event OutboxEntryCreated( + uint256 indexed batchNum, + uint256 outboxEntryIndex, + bytes32 outputRoot, + uint256 numInBatch + ); + event OutBoxTransactionExecuted( + address indexed destAddr, + address indexed l2Sender, + uint256 indexed outboxEntryIndex, + uint256 transactionIndex + ); + + function l2ToL1Sender() external view returns (address); + + function l2ToL1Block() external view returns (uint256); + + function l2ToL1EthBlock() external view returns (uint256); + + function l2ToL1Timestamp() external view returns (uint256); + + function l2ToL1BatchNum() external view returns (uint256); + + function l2ToL1OutputId() external view returns (bytes32); + + function processOutgoingMessages(bytes calldata sendsData, uint256[] calldata sendLengths) + external; + + function outboxEntryExists(uint256 batchNum) external view returns (bool); +} diff --git a/contracts/arbitrum/ITokenGateway.sol b/contracts/arbitrum/ITokenGateway.sol new file mode 100644 index 000000000..977fe07f2 --- /dev/null +++ b/contracts/arbitrum/ITokenGateway.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright 2020, Offchain Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Originally copied from: + * https://github.com/OffchainLabs/arbitrum/tree/e3a6307ad8a2dc2cad35728a2a9908cfd8dd8ef9/packages/arb-bridge-peripherals + * + * MODIFIED from Offchain Labs' implementation: + * - Changed solidity version to 0.7.6 (pablo@edgeandnode.com) + * + */ + +pragma solidity ^0.7.6; + +interface ITokenGateway { + /// @notice event deprecated in favor of DepositInitiated and WithdrawalInitiated + // event OutboundTransferInitiated( + // address token, + // address indexed _from, + // address indexed _to, + // uint256 indexed _transferId, + // uint256 _amount, + // bytes _data + // ); + + /// @notice event deprecated in favor of DepositFinalized and WithdrawalFinalized + // event InboundTransferFinalized( + // address token, + // address indexed _from, + // address indexed _to, + // uint256 indexed _transferId, + // uint256 _amount, + // bytes _data + // ); + + function outboundTransfer( + address _token, + address _to, + uint256 _amount, + uint256 _maxGas, + uint256 _gasPriceBid, + bytes calldata _data + ) external payable returns (bytes memory); + + function finalizeInboundTransfer( + address _token, + address _from, + address _to, + uint256 _amount, + bytes calldata _data + ) external payable; + + /** + * @notice Calculate the address used when bridging an ERC20 token + * @dev the L1 and L2 address oracles may not always be in sync. + * For example, a custom token may have been registered but not deployed or the contract self destructed. + * @param l1ERC20 address of L1 token + * @return L2 address of a bridged ERC20 token + */ + function calculateL2TokenAddress(address l1ERC20) external view returns (address); +} diff --git a/contracts/arbitrum/L1ArbitrumMessenger.sol b/contracts/arbitrum/L1ArbitrumMessenger.sol new file mode 100644 index 000000000..b893fa262 --- /dev/null +++ b/contracts/arbitrum/L1ArbitrumMessenger.sol @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright 2020, Offchain Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Originally copied from: + * https://github.com/OffchainLabs/arbitrum/tree/e3a6307ad8a2dc2cad35728a2a9908cfd8dd8ef9/packages/arb-bridge-peripherals + * + * MODIFIED from Offchain Labs' implementation: + * - Changed solidity version to 0.7.6 (pablo@edgeandnode.com) + * + */ + +pragma solidity ^0.7.6; + +import "./IInbox.sol"; +import "./IOutbox.sol"; + +/// @notice L1 utility contract to assist with L1 <=> L2 interactions +/// @dev this is an abstract contract instead of library so the functions can be easily overriden when testing +abstract contract L1ArbitrumMessenger { + event TxToL2(address indexed _from, address indexed _to, uint256 indexed _seqNum, bytes _data); + + struct L2GasParams { + uint256 _maxSubmissionCost; + uint256 _maxGas; + uint256 _gasPriceBid; + } + + function sendTxToL2( + address _inbox, + address _to, + address _user, + uint256 _l1CallValue, + uint256 _l2CallValue, + L2GasParams memory _l2GasParams, + bytes memory _data + ) internal virtual returns (uint256) { + // alternative function entry point when struggling with the stack size + return + sendTxToL2( + _inbox, + _to, + _user, + _l1CallValue, + _l2CallValue, + _l2GasParams._maxSubmissionCost, + _l2GasParams._maxGas, + _l2GasParams._gasPriceBid, + _data + ); + } + + function sendTxToL2( + address _inbox, + address _to, + address _user, + uint256 _l1CallValue, + uint256 _l2CallValue, + uint256 _maxSubmissionCost, + uint256 _maxGas, + uint256 _gasPriceBid, + bytes memory _data + ) internal virtual returns (uint256) { + uint256 seqNum = IInbox(_inbox).createRetryableTicket{ value: _l1CallValue }( + _to, + _l2CallValue, + _maxSubmissionCost, + _user, + _user, + _maxGas, + _gasPriceBid, + _data + ); + emit TxToL2(_user, _to, seqNum, _data); + return seqNum; + } + + function getBridge(address _inbox) internal view virtual returns (IBridge) { + return IInbox(_inbox).bridge(); + } + + /// @dev the l2ToL1Sender behaves as the tx.origin, the msg.sender should be validated to protect against reentrancies + function getL2ToL1Sender(address _inbox) internal view virtual returns (address) { + IOutbox outbox = IOutbox(getBridge(_inbox).activeOutbox()); + address l2ToL1Sender = outbox.l2ToL1Sender(); + + require(l2ToL1Sender != address(0), "NO_SENDER"); + return l2ToL1Sender; + } +} diff --git a/contracts/arbitrum/L2ArbitrumMessenger.sol b/contracts/arbitrum/L2ArbitrumMessenger.sol new file mode 100644 index 000000000..e03985bef --- /dev/null +++ b/contracts/arbitrum/L2ArbitrumMessenger.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright 2020, Offchain Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Originally copied from: + * https://github.com/OffchainLabs/arbitrum/tree/e3a6307ad8a2dc2cad35728a2a9908cfd8dd8ef9/packages/arb-bridge-peripherals + * + * MODIFIED from Offchain Labs' implementation: + * - Changed solidity version to 0.7.6 (pablo@edgeandnode.com) + * + */ + +pragma solidity ^0.7.6; + +import "arbos-precompiles/arbos/builtin/ArbSys.sol"; + +/// @notice L2 utility contract to assist with L1 <=> L2 interactions +/// @dev this is an abstract contract instead of library so the functions can be easily overriden when testing +abstract contract L2ArbitrumMessenger { + address internal constant ARB_SYS_ADDRESS = address(100); + + event TxToL1(address indexed _from, address indexed _to, uint256 indexed _id, bytes _data); + + function sendTxToL1( + uint256 _l1CallValue, + address _from, + address _to, + bytes memory _data + ) internal virtual returns (uint256) { + uint256 _id = ArbSys(ARB_SYS_ADDRESS).sendTxToL1{ value: _l1CallValue }(_to, _data); + emit TxToL1(_from, _to, _id, _data); + return _id; + } +} diff --git a/contracts/arbitrum/README.md b/contracts/arbitrum/README.md new file mode 100644 index 000000000..abc87553e --- /dev/null +++ b/contracts/arbitrum/README.md @@ -0,0 +1,5 @@ +# Arbitrum contracts + +These contracts have been copied from the [Arbitrum repo](https://github.com/OffchainLabs/arbitrum). + +They are also available as part of the npm packages [arb-bridge-eth](https://www.npmjs.com/package/arb-bridge-eth) and [arb-bridge-peripherals](https://www.npmjs.com/package/arb-bridge-peripherals). The reason for copying them rather than installing those packages is the contracts only support Solidity `^0.6.11`, so we had to change the version to `^0.7.6` for it to be compatible with our other contracts. diff --git a/contracts/gateway/BridgeEscrow.sol b/contracts/gateway/BridgeEscrow.sol new file mode 100644 index 000000000..605f13a50 --- /dev/null +++ b/contracts/gateway/BridgeEscrow.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; + +import "../upgrades/GraphUpgradeable.sol"; +import "../governance/Managed.sol"; +import "../token/IGraphToken.sol"; + +/** + * @title Bridge Escrow + * @dev This contracts acts as a gateway for an L2 bridge (or several). It simply holds GRT and has + * a set of spenders that can transfer the tokens; the L1 side of each L2 bridge has to be + * approved as a spender. + */ +contract BridgeEscrow is GraphUpgradeable, Managed { + /** + * @dev Initialize this contract. + * @param _controller Address of the Controller that manages this contract + */ + function initialize(address _controller) external onlyImpl { + Managed._initialize(_controller); + } + + /** + * @dev Approve a spender (i.e. a bridge that manages the GRT funds held by the escrow) + * @param _spender Address of the spender that will be approved + */ + function approveAll(address _spender) external onlyGovernor { + graphToken().approve(_spender, type(uint256).max); + } + + /** + * @dev Revoke a spender (i.e. a bridge that will no longer manage the GRT funds held by the escrow) + * @param _spender Address of the spender that will be revoked + */ + function revokeAll(address _spender) external onlyGovernor { + IGraphToken grt = graphToken(); + grt.decreaseAllowance(_spender, grt.allowance(address(this), _spender)); + } +} diff --git a/contracts/gateway/GraphTokenGateway.sol b/contracts/gateway/GraphTokenGateway.sol new file mode 100644 index 000000000..00e8441f5 --- /dev/null +++ b/contracts/gateway/GraphTokenGateway.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; + +import "../upgrades/GraphUpgradeable.sol"; +import "../arbitrum/ITokenGateway.sol"; +import "../governance/Pausable.sol"; +import "../governance/Managed.sol"; + +/** + * @title L1/L2 Graph Token Gateway + * @dev This includes everything that's shared between the L1 and L2 sides of the bridge. + */ +abstract contract GraphTokenGateway is GraphUpgradeable, Pausable, Managed, ITokenGateway { + /** + * @dev Check if the caller is the Controller's governor or this contract's pause guardian. + */ + modifier onlyGovernorOrGuardian() { + require( + msg.sender == controller.getGovernor() || msg.sender == pauseGuardian, + "Only Governor or Guardian can call" + ); + _; + } + + /** + * @notice Change the Pause Guardian for this contract + * @param _newPauseGuardian The address of the new Pause Guardian + */ + function setPauseGuardian(address _newPauseGuardian) external onlyGovernor { + require(_newPauseGuardian != address(0), "PauseGuardian must be set"); + _setPauseGuardian(_newPauseGuardian); + } + + /** + * @dev Override the default pausing from Managed to allow pausing this + * particular contract instead of pausing from the Controller. + */ + function _notPaused() internal view override { + require(!_paused, "Paused (contract)"); + } + + /** + * @notice Change the paused state of the contract + * @param _newPaused New value for the pause state (true means the transfers will be paused) + */ + function setPaused(bool _newPaused) external onlyGovernorOrGuardian { + _setPaused(_newPaused); + } + + /** + * @notice Getter to access paused state of this contract + */ + function paused() external view returns (bool) { + return _paused; + } +} diff --git a/contracts/gateway/L1GraphTokenGateway.sol b/contracts/gateway/L1GraphTokenGateway.sol new file mode 100644 index 000000000..19fd73be5 --- /dev/null +++ b/contracts/gateway/L1GraphTokenGateway.sol @@ -0,0 +1,357 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; +pragma abicoder v2; + +import "@openzeppelin/contracts/utils/Address.sol"; +import "@openzeppelin/contracts/math/SafeMath.sol"; + +import "../arbitrum/L1ArbitrumMessenger.sol"; +import "./GraphTokenGateway.sol"; + +/** + * @title L1 Graph Token Gateway Contract + * @dev Provides the L1 side of the Ethereum-Arbitrum GRT bridge. Sends GRT to the L2 chain + * by escrowing them and sending a message to the L2 gateway, and receives tokens from L2 by + * releasing them from escrow. + * Based on Offchain Labs' reference implementation and Livepeer's arbitrum-lpt-bridge + * (See: https://github.com/OffchainLabs/arbitrum/tree/master/packages/arb-bridge-peripherals/contracts/tokenbridge + * and https://github.com/livepeer/arbitrum-lpt-bridge) + */ +contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger { + using SafeMath for uint256; + + // Address of the Graph Token contract on L2 + address public l2GRT; + // Address of the Arbitrum Inbox + address public inbox; + // Address of the Arbitrum Gateway Router on L1 + address public l1Router; + // Address of the L2GraphTokenGateway on L2 that is the counterpart of this gateway + address public l2Counterpart; + // Address of the BridgeEscrow contract that holds the GRT in the bridge + address public escrow; + // Addresses for which this mapping is true are allowed to send callhooks in outbound transfers + mapping(address => bool) public callhookWhitelist; + + // Emitted when an outbound transfer is initiated, i.e. tokens are deposited from L1 to L2 + event DepositInitiated( + address l1Token, + address indexed from, + address indexed to, + uint256 indexed sequenceNumber, + uint256 amount + ); + + // Emitted when an incoming transfer is finalized, i.e tokens are withdrawn from L2 to L1 + event WithdrawalFinalized( + address l1Token, + address indexed from, + address indexed to, + uint256 indexed exitNum, + uint256 amount + ); + + // Emitted when the Arbitrum Inbox and Gateway Router addresses have been updated + event ArbitrumAddressesSet(address inbox, address l1Router); + // Emitted when the L2 GRT address has been updated + event L2TokenAddressSet(address l2GRT); + // Emitted when the counterpart L2GraphTokenGateway address has been updated + event L2CounterpartAddressSet(address l2Counterpart); + // Emitted when the escrow address has been updated + event EscrowAddressSet(address escrow); + // Emitted when an address is added to the callhook whitelist + event AddedToCallhookWhitelist(address newWhitelisted); + // Emitted when an address is removed from the callhook whitelist + event RemovedFromCallhookWhitelist(address notWhitelisted); + + /** + * @dev Allows a function to be called only by the gateway's L2 counterpart. + * The message will actually come from the Arbitrum Bridge, but the Outbox + * can tell us who the sender from L2 is. + */ + modifier onlyL2Counterpart() { + // a message coming from the counterpart gateway was executed by the bridge + IBridge bridge = IInbox(inbox).bridge(); + require(msg.sender == address(bridge), "NOT_FROM_BRIDGE"); + + // and the outbox reports that the L2 address of the sender is the counterpart gateway + address l2ToL1Sender = IOutbox(bridge.activeOutbox()).l2ToL1Sender(); + require(l2ToL1Sender == l2Counterpart, "ONLY_COUNTERPART_GATEWAY"); + _; + } + + /** + * @dev Initialize this contract. + * The contract will be paused. + * Note some parameters have to be set separately as they are generally + * not expected to be available at initialization time: + * - inbox and l1Router using setArbitrumAddresses + * - l2GRT using setL2TokenAddress + * - l2Counterpart using setL2CounterpartAddress + * - escrow using setEscrowAddress + * - whitelisted callhook callers using addToCallhookWhitelist + * - pauseGuardian using setPauseGuardian + * @param _controller Address of the Controller that manages this contract + */ + function initialize(address _controller) external onlyImpl { + Managed._initialize(_controller); + _paused = true; + } + + /** + * @dev Sets the addresses for L1 contracts provided by Arbitrum + * @param _inbox Address of the Inbox that is part of the Arbitrum Bridge + * @param _l1Router Address of the Gateway Router + */ + function setArbitrumAddresses(address _inbox, address _l1Router) external onlyGovernor { + require(_inbox != address(0), "INVALID_INBOX"); + require(_l1Router != address(0), "INVALID_L1_ROUTER"); + inbox = _inbox; + l1Router = _l1Router; + emit ArbitrumAddressesSet(_inbox, _l1Router); + } + + /** + * @dev Sets the address of the L2 Graph Token + * @param _l2GRT Address of the GRT contract on L2 + */ + function setL2TokenAddress(address _l2GRT) external onlyGovernor { + require(_l2GRT != address(0), "INVALID_L2_GRT"); + l2GRT = _l2GRT; + emit L2TokenAddressSet(_l2GRT); + } + + /** + * @dev Sets the address of the counterpart gateway on L2 + * @param _l2Counterpart Address of the corresponding L2GraphTokenGateway on Arbitrum + */ + function setL2CounterpartAddress(address _l2Counterpart) external onlyGovernor { + require(_l2Counterpart != address(0), "INVALID_L2_COUNTERPART"); + l2Counterpart = _l2Counterpart; + emit L2CounterpartAddressSet(_l2Counterpart); + } + + /** + * @dev Sets the address of the escrow contract on L1 + * @param _escrow Address of the BridgeEscrow + */ + function setEscrowAddress(address _escrow) external onlyGovernor { + require(_escrow != address(0) && Address.isContract(_escrow), "INVALID_ESCROW"); + escrow = _escrow; + emit EscrowAddressSet(_escrow); + } + + /** + * @dev Adds an address to the callhook whitelist. + * This address will be allowed to include callhooks when transferring tokens. + * @param _newWhitelisted Address to add to the whitelist + */ + function addToCallhookWhitelist(address _newWhitelisted) external onlyGovernor { + require(_newWhitelisted != address(0), "INVALID_ADDRESS"); + require(!callhookWhitelist[_newWhitelisted], "ALREADY_WHITELISTED"); + callhookWhitelist[_newWhitelisted] = true; + emit AddedToCallhookWhitelist(_newWhitelisted); + } + + /** + * @dev Removes an address from the callhook whitelist. + * This address will no longer be allowed to include callhooks when transferring tokens. + * @param _notWhitelisted Address to remove from the whitelist + */ + function removeFromCallhookWhitelist(address _notWhitelisted) external onlyGovernor { + require(_notWhitelisted != address(0), "INVALID_ADDRESS"); + require(callhookWhitelist[_notWhitelisted], "NOT_WHITELISTED"); + callhookWhitelist[_notWhitelisted] = false; + emit RemovedFromCallhookWhitelist(_notWhitelisted); + } + + /** + * @notice Creates and sends a retryable ticket to transfer GRT to L2 using the Arbitrum Inbox. + * The tokens are escrowed by the gateway until they are withdrawn back to L1. + * The ticket must be redeemed on L2 to receive tokens at the specified address. + * Note that the caller must previously allow the gateway to spend the specified amount of GRT. + * @dev maxGas and gasPriceBid must be set using Arbitrum's NodeInterface.estimateRetryableTicket method. + * Also note that whitelisted senders (some protocol contracts) can include additional calldata + * for a callhook to be executed on the L2 side when the tokens are received. In this case, the L2 transaction + * can revert if the callhook reverts, potentially locking the tokens on the bridge if the callhook + * never succeeds. This requires extra care when adding contracts to the whitelist, but is necessary to ensure that + * the tickets can be retried in the case of a temporary failure, and to ensure the atomicity of callhooks + * with token transfers. + * @param _l1Token L1 Address of the GRT contract (needed for compatibility with Arbitrum Gateway Router) + * @param _to Recipient address on L2 + * @param _amount Amount of tokens to tranfer + * @param _maxGas Gas limit for L2 execution of the ticket + * @param _gasPriceBid Price per gas on L2 + * @param _data Encoded maxSubmissionCost and sender address along with additional calldata + * @return Sequence number of the retryable ticket created by Inbox + */ + function outboundTransfer( + address _l1Token, + address _to, + uint256 _amount, + uint256 _maxGas, + uint256 _gasPriceBid, + bytes calldata _data + ) external payable override notPaused returns (bytes memory) { + IGraphToken token = graphToken(); + require(_l1Token == address(token), "TOKEN_NOT_GRT"); + require(_amount > 0, "INVALID_ZERO_AMOUNT"); + require(_to != address(0), "INVALID_DESTINATION"); + + // nested scopes to avoid stack too deep errors + address from; + uint256 seqNum; + { + uint256 maxSubmissionCost; + bytes memory outboundCalldata; + { + bytes memory extraData; + (from, maxSubmissionCost, extraData) = parseOutboundData(_data); + require( + extraData.length == 0 || callhookWhitelist[msg.sender] == true, + "CALL_HOOK_DATA_NOT_ALLOWED" + ); + require(maxSubmissionCost > 0, "NO_SUBMISSION_COST"); + + { + // makes sure only sufficient ETH is supplied as required for successful redemption on L2 + // if a user does not desire immediate redemption they should provide + // a msg.value of AT LEAST maxSubmissionCost + uint256 expectedEth = maxSubmissionCost.add(_maxGas.mul(_gasPriceBid)); + require(msg.value >= expectedEth, "WRONG_ETH_VALUE"); + } + outboundCalldata = getOutboundCalldata(_l1Token, from, _to, _amount, extraData); + } + { + L2GasParams memory gasParams = L2GasParams( + maxSubmissionCost, + _maxGas, + _gasPriceBid + ); + // transfer tokens to escrow + token.transferFrom(from, escrow, _amount); + seqNum = sendTxToL2( + inbox, + l2Counterpart, + from, + msg.value, + 0, + gasParams, + outboundCalldata + ); + } + } + emit DepositInitiated(_l1Token, from, _to, seqNum, _amount); + + return abi.encode(seqNum); + } + + /** + * @notice Receives withdrawn tokens from L2 + * The equivalent tokens are released from escrow and sent to the destination. + * @dev can only accept transactions coming from the L2 GRT Gateway. + * The last parameter is unused but kept for compatibility with Arbitrum gateways, + * and the encoded exitNum is assumed to be 0. + * @param _l1Token L1 Address of the GRT contract (needed for compatibility with Arbitrum Gateway Router) + * @param _from Address of the sender + * @param _to Recepient address on L1 + * @param _amount Amount of tokens transferred + */ + function finalizeInboundTransfer( + address _l1Token, + address _from, + address _to, + uint256 _amount, + bytes calldata // _data, contains exitNum, unused by this contract + ) external payable override notPaused onlyL2Counterpart { + IGraphToken token = graphToken(); + require(_l1Token == address(token), "TOKEN_NOT_GRT"); + + uint256 escrowBalance = token.balanceOf(escrow); + // If the bridge doesn't have enough tokens, something's very wrong! + require(_amount <= escrowBalance, "BRIDGE_OUT_OF_FUNDS"); + token.transferFrom(escrow, _to, _amount); + + emit WithdrawalFinalized(_l1Token, _from, _to, 0, _amount); + } + + /** + * @notice Decodes calldata required for migration of tokens + * @dev Data must include maxSubmissionCost, extraData can be left empty. When the router + * sends an outbound message, data also contains the from address. + * @param _data encoded callhook data + * @return Sender of the tx + * @return Base ether value required to keep retryable ticket alive + * @return Additional data sent to L2 + */ + function parseOutboundData(bytes memory _data) + private + view + returns ( + address, + uint256, + bytes memory + ) + { + address from; + uint256 maxSubmissionCost; + bytes memory extraData; + if (msg.sender == l1Router) { + // Data encoded by the Gateway Router includes the sender address + (from, extraData) = abi.decode(_data, (address, bytes)); + } else { + from = msg.sender; + extraData = _data; + } + // User-encoded data contains the max retryable ticket submission cost + // and additional L2 calldata + (maxSubmissionCost, extraData) = abi.decode(extraData, (uint256, bytes)); + return (from, maxSubmissionCost, extraData); + } + + /** + * @notice Creates calldata required to create a retryable ticket + * @dev encodes the target function with its params which + * will be called on L2 when the retryable ticket is redeemed + * @param _l1Token Address of the Graph token contract on L1 + * @param _from Address on L1 from which we're transferring tokens + * @param _to Address on L2 to which we're transferring tokens + * @param _amount Amount of GRT to transfer + * @param _data Additional call data for the L2 transaction, which must be empty unless the caller is whitelisted + * @return Encoded calldata (including function selector) for the L2 transaction + */ + function getOutboundCalldata( + address _l1Token, + address _from, + address _to, + uint256 _amount, + bytes memory _data + ) public pure returns (bytes memory) { + bytes memory emptyBytes; + + return + abi.encodeWithSelector( + ITokenGateway.finalizeInboundTransfer.selector, + _l1Token, + _from, + _to, + _amount, + abi.encode(emptyBytes, _data) + ); + } + + /** + * @notice Calculate the L2 address of a bridged token + * @dev In our case, this would only work for GRT. + * @param _l1ERC20 address of L1 GRT contract + * @return L2 address of the bridged GRT token + */ + function calculateL2TokenAddress(address _l1ERC20) external view override returns (address) { + IGraphToken token = graphToken(); + if (_l1ERC20 != address(token)) { + return address(0); + } + return l2GRT; + } +} diff --git a/contracts/governance/Managed.sol b/contracts/governance/Managed.sol index 716403111..20a9191e5 100644 --- a/contracts/governance/Managed.sol +++ b/contracts/governance/Managed.sol @@ -9,6 +9,8 @@ import "../epochs/IEpochManager.sol"; import "../rewards/IRewardsManager.sol"; import "../staking/IStaking.sol"; import "../token/IGraphToken.sol"; +import "../arbitrum/ITokenGateway.sol"; +import "../reservoir/IReservoir.sol"; /** * @title Graph Managed contract @@ -44,7 +46,7 @@ contract Managed { require(!controller.partialPaused(), "Partial-paused"); } - function _notPaused() internal view { + function _notPaused() internal view virtual { require(!controller.paused(), "Paused"); } @@ -145,6 +147,22 @@ contract Managed { return IGraphToken(_resolveContract(keccak256("GraphToken"))); } + /** + * @dev Return GraphTokenGateway (L1 or L2) interface. + * @return Graph token gateway contract registered with Controller + */ + function graphTokenGateway() internal view returns (ITokenGateway) { + return ITokenGateway(_resolveContract(keccak256("GraphTokenGateway"))); + } + + /** + * @dev Return Reservoir (L1 or L2) interface. + * @return Reservoir contract registered with Controller + */ + function reservoir() internal view returns (IReservoir) { + return IReservoir(_resolveContract(keccak256("Reservoir"))); + } + /** * @dev Resolve a contract address from the cache or the Controller if not found. * @return Address of the contract @@ -182,5 +200,7 @@ contract Managed { _syncContract("RewardsManager"); _syncContract("Staking"); _syncContract("GraphToken"); + _syncContract("GraphTokenGateway"); + _syncContract("Reservoir"); } } diff --git a/contracts/l2/gateway/L2GraphTokenGateway.sol b/contracts/l2/gateway/L2GraphTokenGateway.sol new file mode 100644 index 000000000..593e0e228 --- /dev/null +++ b/contracts/l2/gateway/L2GraphTokenGateway.sol @@ -0,0 +1,302 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; +pragma abicoder v2; + +import "@openzeppelin/contracts/math/SafeMath.sol"; + +import "../../arbitrum/L2ArbitrumMessenger.sol"; +import "../../arbitrum/AddressAliasHelper.sol"; +import "../../gateway/GraphTokenGateway.sol"; +import "../token/L2GraphToken.sol"; + +/** + * @title L2 Graph Token Gateway Contract + * @dev Provides the L2 side of the Ethereum-Arbitrum GRT bridge. Receives GRT from the L1 chain + * and mints them on the L2 side. Sends GRT back to L1 by burning them on the L2 side. + * Based on Offchain Labs' reference implementation and Livepeer's arbitrum-lpt-bridge + * (See: https://github.com/OffchainLabs/arbitrum/tree/master/packages/arb-bridge-peripherals/contracts/tokenbridge + * and https://github.com/livepeer/arbitrum-lpt-bridge) + */ +contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger { + using SafeMath for uint256; + + // Address of the Graph Token contract on L1 + address public l1GRT; + // Address of the L1GraphTokenGateway that is the counterpart of this gateway on L1 + address public l1Counterpart; + // Address of the Arbitrum Gateway Router on L2 + address public l2Router; + + // Calldata included in an outbound transfer, stored as a structure for convenience and stack depth + struct OutboundCalldata { + address from; + bytes extraData; + } + + // Emitted when an incoming transfer is finalized, i.e. tokens were deposited from L1 to L2 + event DepositFinalized( + address indexed l1Token, + address indexed from, + address indexed to, + uint256 amount + ); + + // Emitted when an outbound transfer is initiated, i.e. tokens are being withdrawn from L2 back to L1 + event WithdrawalInitiated( + address l1Token, + address indexed from, + address indexed to, + uint256 indexed l2ToL1Id, + uint256 exitNum, + uint256 amount + ); + + // Emitted when the Arbitrum Gateway Router address on L2 has been updated + event L2RouterSet(address l2Router); + // Emitted when the L1 Graph Token address has been updated + event L1TokenAddressSet(address l1GRT); + // Emitted when the address of the counterpart gateway on L1 has been updated + event L1CounterpartAddressSet(address l1Counterpart); + + /** + * @dev Checks that the sender is the L2 alias of the counterpart + * gateway on L1. + */ + modifier onlyL1Counterpart() { + require( + msg.sender == AddressAliasHelper.applyL1ToL2Alias(l1Counterpart), + "ONLY_COUNTERPART_GATEWAY" + ); + _; + } + + /** + * @dev Initialize this contract. + * The contract will be paused. + * Note some parameters have to be set separately as they are generally + * not expected to be available at initialization time: + * - l2Router using setL2Router + * - l1GRT using setL1TokenAddress + * - l1Counterpart using setL1CounterpartAddress + * - pauseGuardian using setPauseGuardian + * @param _controller Address of the Controller that manages this contract + */ + function initialize(address _controller) external onlyImpl { + Managed._initialize(_controller); + _paused = true; + } + + /** + * @dev Sets the address of the Arbitrum Gateway Router on L2 + * @param _l2Router Address of the L2 Router (provided by Arbitrum) + */ + function setL2Router(address _l2Router) external onlyGovernor { + require(_l2Router != address(0), "INVALID_L2_ROUTER"); + l2Router = _l2Router; + emit L2RouterSet(_l2Router); + } + + /** + * @dev Sets the address of the Graph Token on L1 + * @param _l1GRT L1 address of the Graph Token contract + */ + function setL1TokenAddress(address _l1GRT) external onlyGovernor { + require(_l1GRT != address(0), "INVALID_L1_GRT"); + l1GRT = _l1GRT; + emit L1TokenAddressSet(_l1GRT); + } + + /** + * @dev Sets the address of the counterpart gateway on L1 + * @param _l1Counterpart Address of the L1GraphTokenGateway on L1 + */ + function setL1CounterpartAddress(address _l1Counterpart) external onlyGovernor { + require(_l1Counterpart != address(0), "INVALID_L1_COUNTERPART"); + l1Counterpart = _l1Counterpart; + emit L1CounterpartAddressSet(_l1Counterpart); + } + + /** + * @notice Burns L2 tokens and initiates a transfer to L1. + * The tokens will be available on L1 only after the wait period (7 days) is over, + * and will require an Outbox.executeTransaction to finalize. + * Note that the caller must previously allow the gateway to spend the specified amount of GRT. + * @dev no additional callhook data is allowed. The two unused params are needed + * for compatibility with Arbitrum's gateway router. + * The function is payable for ITokenGateway compatibility, but msg.value must be zero. + * @param _l1Token L1 Address of GRT (needed for compatibility with Arbitrum Gateway Router) + * @param _to Recipient address on L1 + * @param _amount Amount of tokens to burn + * @param _data Contains sender and additional data (always empty) to send to L1 + * @return ID of the withdraw transaction + */ + function outboundTransfer( + address _l1Token, + address _to, + uint256 _amount, + uint256, // unused on L2 + uint256, // unused on L2 + bytes calldata _data + ) public payable override notPaused returns (bytes memory) { + require(_l1Token == l1GRT, "TOKEN_NOT_GRT"); + require(_amount > 0, "INVALID_ZERO_AMOUNT"); + require(msg.value == 0, "INVALID_NONZERO_VALUE"); + require(_to != address(0), "INVALID_DESTINATION"); + + OutboundCalldata memory outboundCalldata; + + (outboundCalldata.from, outboundCalldata.extraData) = parseOutboundData(_data); + require(outboundCalldata.extraData.length == 0, "CALL_HOOK_DATA_NOT_ALLOWED"); + + // from needs to approve this contract to burn the amount first + L2GraphToken(calculateL2TokenAddress(l1GRT)).bridgeBurn(outboundCalldata.from, _amount); + + uint256 id = sendTxToL1( + 0, + outboundCalldata.from, + l1Counterpart, + getOutboundCalldata( + _l1Token, + outboundCalldata.from, + _to, + _amount, + outboundCalldata.extraData + ) + ); + + // we don't need to track exitNums (b/c we have no fast exits) so we always use 0 + emit WithdrawalInitiated(_l1Token, outboundCalldata.from, _to, id, 0, _amount); + + return abi.encode(id); + } + + /** + * @notice Burns L2 tokens and initiates a transfer to L1. + * The tokens will be received on L1 only after the wait period (7 days) is over, + * and will require an Outbox.executeTransaction to finalize. + * @dev no additional callhook data is allowed + * @param _l1Token L1 Address of GRT (needed for compatibility with Arbitrum Gateway Router) + * @param _to Recipient address on L1 + * @param _amount Amount of tokens to burn + * @param _data Contains sender and additional data to send to L1 + * @return ID of the withdraw tx + */ + function outboundTransfer( + address _l1Token, + address _to, + uint256 _amount, + bytes calldata _data + ) external returns (bytes memory) { + return outboundTransfer(_l1Token, _to, _amount, 0, 0, _data); + } + + /** + * @notice Calculate the L2 address of a bridged token + * @dev In our case, this would only work for GRT. + * @param l1ERC20 address of L1 GRT contract + * @return L2 address of the bridged GRT token + */ + function calculateL2TokenAddress(address l1ERC20) public view override returns (address) { + if (l1ERC20 != l1GRT) { + return address(0); + } + return address(graphToken()); + } + + /** + * @notice Receives token amount from L1 and mints the equivalent tokens to the receiving address + * @dev Only accepts transactions from the L1 GRT Gateway. + * The function is payable for ITokenGateway compatibility, but msg.value must be zero. + * Note that whitelisted senders (some protocol contracts) can include additional calldata + * for a callhook to be executed on the L2 side when the tokens are received. In this case, the L2 transaction + * can revert if the callhook reverts, potentially locking the tokens on the bridge if the callhook + * never succeeds. This requires extra care when adding contracts to the whitelist, but is necessary to ensure that + * the tickets can be retried in the case of a temporary failure, and to ensure the atomicity of callhooks + * with token transfers. + * @param _l1Token L1 Address of GRT + * @param _from Address of the sender on L1 + * @param _to Recipient address on L2 + * @param _amount Amount of tokens transferred + * @param _data Extra callhook data, only used when the sender is whitelisted + */ + function finalizeInboundTransfer( + address _l1Token, + address _from, + address _to, + uint256 _amount, + bytes calldata _data + ) external payable override notPaused onlyL1Counterpart { + require(_l1Token == l1GRT, "TOKEN_NOT_GRT"); + require(msg.value == 0, "INVALID_NONZERO_VALUE"); + + L2GraphToken(calculateL2TokenAddress(l1GRT)).bridgeMint(_to, _amount); + + if (_data.length > 0) { + bytes memory callhookData; + { + bytes memory gatewayData; + (gatewayData, callhookData) = abi.decode(_data, (bytes, bytes)); + } + bool success; + // solhint-disable-next-line avoid-low-level-calls + (success, ) = _to.call(callhookData); + // Callhooks shouldn't revert, but if they do: + // we revert, so that the retryable ticket can be re-attempted + // later. + if (!success) { + revert("CALLHOOK_FAILED"); + } + } + + emit DepositFinalized(_l1Token, _from, _to, _amount); + } + + /** + * @notice Creates calldata required to send tx to L1 + * @dev encodes the target function with its params which + * will be called on L1 when the message is received on L1 + * @param _token Address of the token on L1 + * @param _from Address of the token sender on L2 + * @param _to Address to which we're sending tokens on L1 + * @param _amount Amount of GRT to transfer + * @param _data Additional calldata for the transaction + * @return Calldata for a transaction sent to L1 + */ + function getOutboundCalldata( + address _token, + address _from, + address _to, + uint256 _amount, + bytes memory _data + ) public pure returns (bytes memory) { + return + abi.encodeWithSelector( + ITokenGateway.finalizeInboundTransfer.selector, + _token, + _from, + _to, + _amount, + abi.encode(0, _data) // we don't need to track exitNums (b/c we have no fast exits) so we always use 0 + ); + } + + /** + * @notice Decodes calldata required for migration of tokens + * @dev extraData can be left empty + * @param _data Encoded callhook data + * @return Sender of the tx + * @return Any other data sent to L1 + */ + function parseOutboundData(bytes memory _data) private view returns (address, bytes memory) { + address from; + bytes memory extraData; + if (msg.sender == l2Router) { + (from, extraData) = abi.decode(_data, (address, bytes)); + } else { + from = msg.sender; + extraData = _data; + } + return (from, extraData); + } +} diff --git a/contracts/l2/reservoir/IL2Reservoir.sol b/contracts/l2/reservoir/IL2Reservoir.sol new file mode 100644 index 000000000..8144efae2 --- /dev/null +++ b/contracts/l2/reservoir/IL2Reservoir.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; + +import { IReservoir } from "../../reservoir/IReservoir.sol"; + +/** + * @title Interface for the L2 Rewards Reservoir + * @dev This exposes a specific function for the L2Reservoir that is called + * as a callhook from L1 to L2, so that state can be updated when dripped rewards + * are bridged between layers. + */ +interface IL2Reservoir is IReservoir { + /** + * @dev Receive dripped tokens from L1. + * This function can only be called by the gateway, as it is + * meant to be a callhook when receiving tokens from L1. It + * updates the issuanceBase and issuanceRate, + * and snapshots the accumulated rewards. If issuanceRate changes, + * it also triggers a snapshot of rewards per signal on the RewardsManager. + * Note that the transaction might revert if it's received out-of-order, + * because it checks an incrementing nonce. If that is the case, the retryable ticket can be redeemed + * again once the ticket for previous drip has been redeemed. + * A keeper reward will be sent to the keeper that dripped on L1, and part of it + * to whoever redeemed the current retryable ticket (as reported by ArbRetryableTx.getCurrentRedeemer) if + * the ticket is not auto-redeemed. + * @param _issuanceBase Base value for token issuance (approximation for token supply times L2 rewards fraction) + * @param _issuanceRate Rewards issuance rate, using fixed point at 1e18, and including a +1 + * @param _nonce Incrementing nonce to ensure messages are received in order + * @param _keeperReward Keeper reward to distribute between keeper that called drip and keeper that redeemed the retryable tx + * @param _l1Keeper Address of the keeper that called drip in L1 + */ + function receiveDrip( + uint256 _issuanceBase, + uint256 _issuanceRate, + uint256 _nonce, + uint256 _keeperReward, + address _l1Keeper + ) external; +} diff --git a/contracts/l2/reservoir/L2Reservoir.sol b/contracts/l2/reservoir/L2Reservoir.sol new file mode 100644 index 000000000..e5cce6fee --- /dev/null +++ b/contracts/l2/reservoir/L2Reservoir.sol @@ -0,0 +1,200 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; +pragma abicoder v2; + +import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; +import { ArbRetryableTx } from "arbos-precompiles/arbos/builtin/ArbRetryableTx.sol"; + +import { Managed } from "../../governance/Managed.sol"; +import { IGraphToken } from "../../token/IGraphToken.sol"; +import { AddressAliasHelper } from "../../arbitrum/AddressAliasHelper.sol"; +import { IReservoir } from "../../reservoir/IReservoir.sol"; +import { Reservoir } from "../../reservoir/Reservoir.sol"; +import { IL2Reservoir } from "./IL2Reservoir.sol"; +import { L2ReservoirV2Storage } from "./L2ReservoirStorage.sol"; + +/** + * @dev ArbRetryableTx with additional interface to query the current redeemer. + * This is being added by the Arbitrum team but hasn't made it into the arbos-precompiles + * package yet. + */ +interface IArbTxWithRedeemer is ArbRetryableTx { + /** + * @notice Gets the redeemer of the current retryable redeem attempt. + * Returns the zero address if the current transaction is not a retryable redeem attempt. + * If this is an auto-redeem, returns the fee refund address of the retryable. + */ + function getCurrentRedeemer() external view returns (address); +} + +/** + * @title L2 Rewards Reservoir + * @dev This contract acts as a reservoir/vault for the rewards to be distributed on Layer 2. + * It receives tokens for rewards from L1, and provides functions to compute accumulated and new + * total rewards at a particular block number. + */ +contract L2Reservoir is L2ReservoirV2Storage, Reservoir, IL2Reservoir { + using SafeMath for uint256; + + // Address for the ArbRetryableTx interface provided by Arbitrum + address public constant ARB_TX_ADDRESS = 0x000000000000000000000000000000000000006E; + + // Emitted when a rewards drip is received from L1 + event DripReceived(uint256 issuanceBase); + // Emitted when the next drip nonce is manually updated by governance + event NextDripNonceUpdated(uint256 nonce); + // Emitted when the L1Reservoir's address is updated + event L1ReservoirAddressUpdated(address l1ReservoirAddress); + // Emitted when the L2 keeper reward fraction is updated + event L2KeeperRewardFractionUpdated(uint256 l2KeeperRewardFraction); + + /** + * @dev Checks that the sender is the L2GraphTokenGateway as configured on the Controller. + */ + modifier onlyL2Gateway() { + require(msg.sender == address(graphTokenGateway()), "ONLY_GATEWAY"); + _; + } + + /** + * @dev Initialize this contract. + * The contract will be paused. Note that issuance parameters + * are not set here because they are set from L1 through the drip function. + * The RewardsManager's address might also not be available in the controller at initialization + * time, so approveRewardsManager() must be called separately. + * The l1ReservoirAddress must also be set separately through setL1ReservoirAddress + * for the same reason. + * In the same vein, the l2KeeperRewardFraction is assumed to be zero at initialization, + * so it must be set through setL2KeeperRewardFraction. + * @param _controller Address of the Controller that manages this contract + */ + function initialize(address _controller) external onlyImpl { + Managed._initialize(_controller); + } + + /** + * @dev Update the next drip nonce + * To be used only as a backup option if the two layers get out of sync. + * @param _nonce Expected value for the nonce of the next drip message + */ + function setNextDripNonce(uint256 _nonce) external onlyGovernor { + nextDripNonce = _nonce; + emit NextDripNonceUpdated(_nonce); + } + + /** + * @dev Sets the L1 Reservoir address + * This is the address on L1 that will appear as redeemer when a ticket + * was auto-redeemed. + * @param _l1ReservoirAddress New address for the L1Reservoir on L1 + */ + function setL1ReservoirAddress(address _l1ReservoirAddress) external onlyGovernor { + require(_l1ReservoirAddress != address(0), "INVALID_L1_RESERVOIR"); + l1ReservoirAddress = _l1ReservoirAddress; + emit L1ReservoirAddressUpdated(_l1ReservoirAddress); + } + + /** + * @dev Sets the L2 keeper reward fraction + * This is the fraction of the keeper reward that will be sent to the redeemer on L2 + * if the retryable ticket is not auto-redeemed + * @param _l2KeeperRewardFraction New value for the fraction, with fixed point at 1e18 + */ + function setL2KeeperRewardFraction(uint256 _l2KeeperRewardFraction) external onlyGovernor { + require(_l2KeeperRewardFraction <= FIXED_POINT_SCALING_FACTOR, "INVALID_VALUE"); + l2KeeperRewardFraction = _l2KeeperRewardFraction; + emit L2KeeperRewardFractionUpdated(_l2KeeperRewardFraction); + } + + /** + * @dev Get new total rewards accumulated since the last drip. + * This is deltaR = p * r ^ (blocknum - t0) - p, where: + * - p is the issuance base at t0 (normalized by the L2 rewards fraction) + * - t0 is the last drip block, i.e. lastRewardsUpdateBlock + * - r is the issuanceRate + * @param _blocknum Block number at which to calculate rewards + * @return New total rewards on L2 since the last drip + */ + function getNewRewards(uint256 _blocknum) + public + view + override(Reservoir, IReservoir) + returns (uint256) + { + uint256 t0 = lastRewardsUpdateBlock; + if (issuanceRate <= MIN_ISSUANCE_RATE || _blocknum == t0) { + return 0; + } + return + issuanceBase + .mul(_pow(issuanceRate, _blocknum.sub(t0), FIXED_POINT_SCALING_FACTOR)) + .div(FIXED_POINT_SCALING_FACTOR) + .sub(issuanceBase); + } + + /** + * @dev Receive dripped tokens from L1. + * This function can only be called by the gateway, as it is + * meant to be a callhook when receiving tokens from L1. It + * updates the issuanceBase and issuanceRate, + * and snapshots the accumulated rewards. If issuanceRate changes, + * it also triggers a snapshot of rewards per signal on the RewardsManager. + * Note that the transaction might revert if it's received out-of-order, + * because it checks an incrementing nonce. If that is the case, the retryable ticket can be redeemed + * again once the ticket for previous drip has been redeemed. + * A keeper reward will be sent to the keeper that dripped on L1, and part of it + * to whoever redeemed the current retryable ticket (as reported by ArbRetryableTx.getCurrentRedeemer) if + * the ticket is not auto-redeemed. + * @param _issuanceBase Base value for token issuance (approximation for token supply times L2 rewards fraction) + * @param _issuanceRate Rewards issuance rate, using fixed point at 1e18, and including a +1 + * @param _nonce Incrementing nonce to ensure messages are received in order + * @param _keeperReward Keeper reward to distribute between keeper that called drip and keeper that redeemed the retryable tx + * @param _l1Keeper Address of the keeper that called drip in L1 + */ + function receiveDrip( + uint256 _issuanceBase, + uint256 _issuanceRate, + uint256 _nonce, + uint256 _keeperReward, + address _l1Keeper + ) external override onlyL2Gateway { + require(_nonce == nextDripNonce, "INVALID_NONCE"); + nextDripNonce = nextDripNonce.add(1); + if (_issuanceRate != issuanceRate) { + rewardsManager().updateAccRewardsPerSignal(); + snapshotAccumulatedRewards(); + issuanceRate = _issuanceRate; + emit IssuanceRateUpdated(_issuanceRate); + } else { + snapshotAccumulatedRewards(); + } + issuanceBase = _issuanceBase; + IGraphToken grt = graphToken(); + + // Part of the reward always goes to whoever redeemed the ticket in L2, + // unless this was an autoredeem, in which case the "redeemer" is the sender, i.e. L1Reservoir + address redeemer = IArbTxWithRedeemer(ARB_TX_ADDRESS).getCurrentRedeemer(); + if (redeemer != AddressAliasHelper.applyL1ToL2Alias(l1ReservoirAddress)) { + uint256 _l2KeeperReward = _keeperReward.mul(l2KeeperRewardFraction).div( + FIXED_POINT_SCALING_FACTOR + ); + grt.transfer(redeemer, _l2KeeperReward); + grt.transfer(_l1Keeper, _keeperReward.sub(_l2KeeperReward)); + } else { + // In an auto-redeem, we just send all the rewards to the L1 keeper: + grt.transfer(_l1Keeper, _keeperReward); + } + + emit DripReceived(issuanceBase); + } + + /** + * @dev Snapshot accumulated rewards on this layer + * We compute accumulatedLayerRewards and mark this block as the lastRewardsUpdateBlock. + */ + function snapshotAccumulatedRewards() internal { + accumulatedLayerRewards = getAccumulatedRewards(block.number); + lastRewardsUpdateBlock = block.number; + } +} diff --git a/contracts/l2/reservoir/L2ReservoirStorage.sol b/contracts/l2/reservoir/L2ReservoirStorage.sol new file mode 100644 index 000000000..4d469f889 --- /dev/null +++ b/contracts/l2/reservoir/L2ReservoirStorage.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; + +/** + * @dev Storage variables for the L2Reservoir, version 1 + */ +contract L2ReservoirV1Storage { + // Expected nonce value for the next drip hook + uint256 public nextDripNonce; +} + +/** + * @dev Storage variables for the L2Reservoir, version 2 + * This version adds some variables needed when introducing the keeper reward. + */ +contract L2ReservoirV2Storage is L2ReservoirV1Storage { + // Fraction of the keeper reward to send to the retryable tx redeemer in L2 (fixed point 1e18) + uint256 public l2KeeperRewardFraction; + // Address of the L1Reservoir on L1, used to check if a ticket was auto-redeemed + address public l1ReservoirAddress; +} diff --git a/contracts/l2/token/GraphTokenUpgradeable.sol b/contracts/l2/token/GraphTokenUpgradeable.sol new file mode 100644 index 000000000..0df9b8e06 --- /dev/null +++ b/contracts/l2/token/GraphTokenUpgradeable.sol @@ -0,0 +1,204 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; + +import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20BurnableUpgradeable.sol"; +import "@openzeppelin/contracts/cryptography/ECDSA.sol"; +import "@openzeppelin/contracts/math/SafeMath.sol"; + +import "../../upgrades/GraphUpgradeable.sol"; +import "../../token/GraphToken.sol"; +import "../../governance/Governed.sol"; + +/** + * @title GraphTokenUpgradeable contract + * @dev This is the implementation of the ERC20 Graph Token. + * The implementation exposes a permit() function to allow for a spender to send a signed message + * and approve funds to a spender following EIP2612 to make integration with other contracts easier. + * + * The token is initially owned by the deployer address that can mint tokens to create the initial + * distribution. For convenience, an initial supply can be passed in the constructor that will be + * assigned to the deployer. + * + * The governor can add contracts allowed to mint indexing rewards. + * + * Note this is an exact copy of the original GraphToken contract, but using + * initializer functions and upgradeable OpenZeppelin contracts instead of + * the original's constructor + non-upgradeable approach. + */ +contract GraphTokenUpgradeable is GraphUpgradeable, Governed, ERC20BurnableUpgradeable { + using SafeMath for uint256; + + // -- EIP712 -- + // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md#definition-of-domainseparator + + bytes32 private constant DOMAIN_TYPE_HASH = + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)" + ); + bytes32 private constant DOMAIN_NAME_HASH = keccak256("Graph Token"); + bytes32 private constant DOMAIN_VERSION_HASH = keccak256("0"); + bytes32 private constant DOMAIN_SALT = + 0xe33842a7acd1d5a1d28f25a931703e5605152dc48d64dc4716efdae1f5659591; // Randomly generated salt + bytes32 private constant PERMIT_TYPEHASH = + keccak256( + "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" + ); + + // -- State -- + + // solhint-disable-next-line var-name-mixedcase + bytes32 private DOMAIN_SEPARATOR; + mapping(address => bool) private _minters; + mapping(address => uint256) public nonces; + + // -- Events -- + + event MinterAdded(address indexed account); + event MinterRemoved(address indexed account); + + modifier onlyMinter() { + require(isMinter(msg.sender), "Only minter can call"); + _; + } + + /** + * @dev Approve token allowance by validating a message signed by the holder. + * @param _owner Address of the token holder + * @param _spender Address of the approved spender + * @param _value Amount of tokens to approve the spender + * @param _deadline Expiration time of the signed permit (if zero, the permit will never expire, so use with caution) + * @param _v Signature recovery id + * @param _r Signature r value + * @param _s Signature s value + */ + function permit( + address _owner, + address _spender, + uint256 _value, + uint256 _deadline, + uint8 _v, + bytes32 _r, + bytes32 _s + ) external { + bytes32 digest = keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR, + keccak256( + abi.encode(PERMIT_TYPEHASH, _owner, _spender, _value, nonces[_owner], _deadline) + ) + ) + ); + + address recoveredAddress = ECDSA.recover(digest, _v, _r, _s); + require(_owner == recoveredAddress, "GRT: invalid permit"); + require(_deadline == 0 || block.timestamp <= _deadline, "GRT: expired permit"); + + nonces[_owner] = nonces[_owner].add(1); + _approve(_owner, _spender, _value); + } + + /** + * @dev Add a new minter. + * @param _account Address of the minter + */ + function addMinter(address _account) external onlyGovernor { + require(_account != address(0), "INVALID_MINTER"); + _addMinter(_account); + } + + /** + * @dev Remove a minter. + * @param _account Address of the minter + */ + function removeMinter(address _account) external onlyGovernor { + require(_minters[_account], "NOT_A_MINTER"); + _removeMinter(_account); + } + + /** + * @dev Renounce to be a minter. + */ + function renounceMinter() external { + require(_minters[msg.sender], "NOT_A_MINTER"); + _removeMinter(msg.sender); + } + + /** + * @dev Mint new tokens. + * @param _to Address to send the newly minted tokens + * @param _amount Amount of tokens to mint + */ + function mint(address _to, uint256 _amount) external onlyMinter { + _mint(_to, _amount); + } + + /** + * @dev Return if the `_account` is a minter or not. + * @param _account Address to check + * @return True if the `_account` is minter + */ + function isMinter(address _account) public view returns (bool) { + return _minters[_account]; + } + + /** + * @dev Graph Token Contract initializer. + * @param _owner Owner of this contract, who will hold the initial supply and will be a minter + * @param _initialSupply Initial supply of GRT + */ + function _initialize(address _owner, uint256 _initialSupply) internal { + __ERC20_init("Graph Token", "GRT"); + Governed._initialize(_owner); + + // The Governor has the initial supply of tokens + _mint(_owner, _initialSupply); + + // The Governor is the default minter + _addMinter(_owner); + + // EIP-712 domain separator + DOMAIN_SEPARATOR = keccak256( + abi.encode( + DOMAIN_TYPE_HASH, + DOMAIN_NAME_HASH, + DOMAIN_VERSION_HASH, + _getChainID(), + address(this), + DOMAIN_SALT + ) + ); + } + + /** + * @dev Add a new minter. + * @param _account Address of the minter + */ + function _addMinter(address _account) private { + _minters[_account] = true; + emit MinterAdded(_account); + } + + /** + * @dev Remove a minter. + * @param _account Address of the minter + */ + function _removeMinter(address _account) private { + _minters[_account] = false; + emit MinterRemoved(_account); + } + + /** + * @dev Get the running network chain ID. + * @return The chain ID + */ + function _getChainID() private pure returns (uint256) { + uint256 id; + // solhint-disable-next-line no-inline-assembly + assembly { + id := chainid() + } + return id; + } +} diff --git a/contracts/l2/token/L2GraphToken.sol b/contracts/l2/token/L2GraphToken.sol new file mode 100644 index 000000000..ec6ca4eb8 --- /dev/null +++ b/contracts/l2/token/L2GraphToken.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; + +import "@openzeppelin/contracts/math/SafeMath.sol"; + +import "./GraphTokenUpgradeable.sol"; +import "../../arbitrum/IArbToken.sol"; + +/** + * @title L2 Graph Token Contract + * @dev Provides the L2 version of the GRT token, meant to be minted/burned + * through the L2GraphTokenGateway. + */ +contract L2GraphToken is GraphTokenUpgradeable, IArbToken { + using SafeMath for uint256; + + // Address of the gateway (on L2) that is allowed to mint tokens + address public gateway; + // Address of the corresponding Graph Token contract on L1 + address public override l1Address; + + // Emitted when the bridge / gateway has minted new tokens, i.e. tokens were transferred to L2 + event BridgeMinted(address indexed account, uint256 amount); + // Emitted when the bridge / gateway has burned tokens, i.e. tokens were transferred back to L1 + event BridgeBurned(address indexed account, uint256 amount); + // Emitted when the address of the gateway has been updated + event GatewaySet(address gateway); + // Emitted when the address of the Graph Token contract on L1 has been updated + event L1AddressSet(address l1Address); + + /** + * @dev Checks that the sender is the L2 gateway from the L1/L2 token bridge + */ + modifier onlyGateway() { + require(msg.sender == gateway, "NOT_GATEWAY"); + _; + } + + /** + * @dev L2 Graph Token Contract initializer. + * Note some parameters have to be set separately as they are generally + * not expected to be available at initialization time: + * - gateway using setGateway + * - l1Address using setL1Address + * @param _owner Governance address that owns this contract + */ + function initialize(address _owner) external onlyImpl { + require(_owner != address(0), "Owner must be set"); + // Initial supply hard coded to 0 as tokens are only supposed + // to be minted through the bridge. + GraphTokenUpgradeable._initialize(_owner, 0); + } + + /** + * @dev Sets the address of the L2 gateway allowed to mint tokens + * @param _gw Address for the L2GraphTokenGateway that will be allowed to mint tokens + */ + function setGateway(address _gw) external onlyGovernor { + require(_gw != address(0), "INVALID_GATEWAY"); + gateway = _gw; + emit GatewaySet(gateway); + } + + /** + * @dev Sets the address of the counterpart token on L1 + * @param _addr Address for the GraphToken contract on L1 + */ + function setL1Address(address _addr) external onlyGovernor { + require(_addr != address(0), "INVALID_L1_ADDRESS"); + l1Address = _addr; + emit L1AddressSet(_addr); + } + + /** + * @dev Increases token supply, only callable by the L1/L2 bridge (when tokens are transferred to L2) + * @param _account Address to credit with the new tokens + * @param _amount Number of tokens to mint + */ + function bridgeMint(address _account, uint256 _amount) external override onlyGateway { + _mint(_account, _amount); + emit BridgeMinted(_account, _amount); + } + + /** + * @dev Decreases token supply, only callable by the L1/L2 bridge (when tokens are transferred to L1). + * @param _account Address from which to extract the tokens + * @param _amount Number of tokens to burn + */ + function bridgeBurn(address _account, uint256 _amount) external override onlyGateway { + burnFrom(_account, _amount); + emit BridgeBurned(_account, _amount); + } +} diff --git a/contracts/reservoir/IReservoir.sol b/contracts/reservoir/IReservoir.sol new file mode 100644 index 000000000..a101952e0 --- /dev/null +++ b/contracts/reservoir/IReservoir.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; + +/** + * @title Interface for the Rewards Reservoir + * @dev This is the shared interface between L1 and L2, for the contracts + * that hold rewards on each layer and provide functions to compute + * accumulated and new total rewards. + */ +interface IReservoir { + // Emitted when the issuance rate is updated + event IssuanceRateUpdated(uint256 newValue); + + /** + * @dev Approve the RewardsManager to manage the reservoir's token funds + */ + function approveRewardsManager() external; + + /** + * @dev Get accumulated total rewards on this layer at a particular block + * @param _blocknum Block number at which to calculate rewards + * @return Accumulated total rewards on this layer + */ + function getAccumulatedRewards(uint256 _blocknum) external view returns (uint256); + + /** + * @dev Get new total rewards on this layer at a particular block, since the last drip event + * @param _blocknum Block number at which to calculate rewards + * @return New total rewards on this layer since the last drip + */ + function getNewRewards(uint256 _blocknum) external view returns (uint256); +} diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol new file mode 100644 index 000000000..1fc1c30ce --- /dev/null +++ b/contracts/reservoir/L1Reservoir.sol @@ -0,0 +1,530 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; +pragma abicoder v2; + +import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; + +import { ITokenGateway } from "../arbitrum/ITokenGateway.sol"; + +import { Managed } from "../governance/Managed.sol"; +import { IGraphToken } from "../token/IGraphToken.sol"; +import { IStaking } from "../staking/IStaking.sol"; +import { IL2Reservoir } from "../l2/reservoir/IL2Reservoir.sol"; +import { Reservoir } from "./Reservoir.sol"; +import { L1ReservoirV2Storage } from "./L1ReservoirStorage.sol"; + +/** + * @title L1 Rewards Reservoir + * @dev This contract acts as a reservoir/vault for the rewards to be distributed on Layer 1. + * It provides a function to periodically drip rewards, and functions to compute accumulated and new + * total rewards at a particular block number. + */ +contract L1Reservoir is L1ReservoirV2Storage, Reservoir { + using SafeMath for uint256; + + // Emitted when the initial supply snapshot is taken after contract deployment + event InitialSnapshotTaken( + uint256 blockNumber, + uint256 issuanceBase, + uint256 mintedPendingRewards + ); + // Emitted when an issuance rate update is staged, to be applied on the next drip + event IssuanceRateStaged(uint256 newValue); + // Emitted when an L2 rewards fraction update is staged, to be applied on the next drip + event L2RewardsFractionStaged(uint256 newValue); + // Emitted when the L2 rewards fraction is updated (during a drip) + event L2RewardsFractionUpdated(uint256 newValue); + // Emitted when the drip interval is updated + event DripIntervalUpdated(uint256 newValue); + // Emitted when new rewards are dripped and potentially sent to L2 + event RewardsDripped(uint256 totalMinted, uint256 sentToL2, uint256 nextDeadline); + // Emitted when the address for the L2Reservoir is updated + event L2ReservoirAddressUpdated(address l2ReservoirAddress); + // Emitted when drip reward per block is updated + event DripRewardPerBlockUpdated(uint256 dripRewardPerBlock); + // Emitted when minDripInterval is updated + event MinDripIntervalUpdated(uint256 minDripInterval); + // Emitted when a new allowedDripper is added + event AllowedDripperAdded(address indexed dripper); + // Emitted when an allowedDripper is revoked + event AllowedDripperRevoked(address indexed dripper); + + /** + * @dev Checks that the sender is an indexer with stake on the Staking contract, + * or that the sender is an address whitelisted by governance to call. + */ + modifier onlyIndexerOrAllowedDripper() { + require(allowedDrippers[msg.sender] || _isIndexer(msg.sender), "UNAUTHORIZED"); + _; + } + + /** + * @dev Checks that the sender is an operator for the specified indexer + * (also checks that the specified indexer is, indeed, an indexer). + * @param _indexer Indexer for which the sender must be an operator + */ + modifier onlyIndexerOperator(address _indexer) { + require(_isIndexer(_indexer), "UNAUTHORIZED_INVALID_INDEXER"); + require(staking().isOperator(msg.sender, _indexer), "UNAUTHORIZED_INVALID_OPERATOR"); + _; + } + + /** + * @dev Initialize this contract. + * The contract will be paused. + * Note that the contract is designed to not accrue rewards until the first call + * to the drip function, that also requires the initial supply snapshot to be taken + * using initialSnapshot. For this reason, issuanceRate and l2RewardsFraction + * are not initialized here and instead need a call to setIssuanceRate and setL2RewardsFraction. + * The same applies to minDripInterval (set through setMinDripInterval) and dripRewardPerBlock + * (set through setDripRewardPerBlock). + * On the other hand, the l2ReservoirAddress is not expected to be known at initialization + * time and must therefore be set using setL2ReservoirAddress. + * The RewardsManager's address might also not be available in the controller at initialization + * time, so approveRewardsManager() must be called separately as well. + * @param _controller Address of the Controller that manages this contract + * @param _dripInterval Drip interval, i.e. time period for which rewards are minted each time we drip + */ + function initialize(address _controller, uint256 _dripInterval) external onlyImpl { + Managed._initialize(_controller); + _setDripInterval(_dripInterval); + } + + /** + * @dev Sets the drip interval. + * This is the time in the future (in blocks) for which drip() will mint rewards. + * Keep in mind that changing this value will require manually re-adjusting + * the reservoir's token balance, because the first call to drip might produce + * more or less tokens than needed. + * @param _dripInterval The new interval in blocks for which drip() will mint rewards + */ + function setDripInterval(uint256 _dripInterval) external onlyGovernor { + _setDripInterval(_dripInterval); + } + + /** + * @dev Sets the issuance rate. + * The issuance rate is defined as a relative increase of the total supply per block, plus 1. + * This means that it needs to be greater than 1.0, any number under 1.0 is not + * allowed and an issuance rate of 1.0 means no issuance. + * To accommodate a high precision the issuance rate is expressed in wei, i.e. fixed point at 1e18. + * Note: It is strongly recommended that the governor triggers a drip immediately after calling this, + * including excess gas to guarantee that the L2 retryable ticket succeeds immediately, to ensure + * good synchronization between L1 and L2. + * @param _issuanceRate Issuance rate expressed in wei / fixed point at 1e18 + */ + function setIssuanceRate(uint256 _issuanceRate) external onlyGovernor { + require(_issuanceRate >= MIN_ISSUANCE_RATE, "Issuance rate under minimum allowed"); + nextIssuanceRate = _issuanceRate; + emit IssuanceRateStaged(_issuanceRate); + } + + /** + * @dev Sets the L2 rewards fraction. + * This is the portion of the indexer rewards that are sent to L2. + * The value is in fixed point at 1e18 and must be less than or equal to 1. + * Note: It is strongly recommended that the governor triggers a drip immediately after calling this, + * including excess gas to guarantee that the L2 retryable ticket succeeds immediately, to ensure + * good synchronization between L1 and L2. + * @param _l2RewardsFraction Fraction of rewards to send to L2, in wei / fixed point at 1e18 + */ + function setL2RewardsFraction(uint256 _l2RewardsFraction) external onlyGovernor { + require( + _l2RewardsFraction <= FIXED_POINT_SCALING_FACTOR, + "L2 Rewards fraction must be <= 1" + ); + nextL2RewardsFraction = _l2RewardsFraction; + emit L2RewardsFractionStaged(_l2RewardsFraction); + } + + /** + * @dev Sets the drip reward per block + * This is the reward in GRT provided to the keeper that calls drip() + * @param _dripRewardPerBlock GRT accrued for each block after the threshold + */ + function setDripRewardPerBlock(uint256 _dripRewardPerBlock) external onlyGovernor { + dripRewardPerBlock = _dripRewardPerBlock; + emit DripRewardPerBlockUpdated(_dripRewardPerBlock); + } + + /** + * @dev Sets the minimum drip interval + * This is the minimum number of blocks between two successful drips + * @param _minDripInterval Minimum number of blocks since last drip for drip to be allowed + */ + function setMinDripInterval(uint256 _minDripInterval) external onlyGovernor { + require(_minDripInterval < dripInterval, "MUST_BE_LT_DRIP_INTERVAL"); + minDripInterval = _minDripInterval; + emit MinDripIntervalUpdated(_minDripInterval); + } + + /** + * @dev Sets the L2 Reservoir address + * This is the address on L2 to which we send tokens for rewards. + * @param _l2ReservoirAddress New address for the L2Reservoir on L2 + */ + function setL2ReservoirAddress(address _l2ReservoirAddress) external onlyGovernor { + require(_l2ReservoirAddress != address(0), "INVALID_L2_RESERVOIR"); + l2ReservoirAddress = _l2ReservoirAddress; + emit L2ReservoirAddressUpdated(_l2ReservoirAddress); + } + + /** + * @dev Grants an address permission to call drip() + * @param _dripper Address that will be an allowed dripper + */ + function grantDripPermission(address _dripper) external onlyGovernor { + require(_dripper != address(0), "INVALID_ADDRESS"); + require(!allowedDrippers[_dripper], "ALREADY_A_DRIPPER"); + allowedDrippers[_dripper] = true; + emit AllowedDripperAdded(_dripper); + } + + /** + * @dev Revokes an address' permission to call drip() + * @param _dripper Address that will not be an allowed dripper anymore + */ + function revokeDripPermission(address _dripper) external onlyGovernor { + require(_dripper != address(0), "INVALID_ADDRESS"); + require(allowedDrippers[_dripper], "NOT_A_DRIPPER"); + allowedDrippers[_dripper] = false; + emit AllowedDripperRevoked(_dripper); + } + + /** + * @dev Computes the initial snapshot for token supply and mints any pending rewards + * This will initialize the issuanceBase to the current GRT supply, after which + * we will keep an internal accounting only using newly minted rewards. This function + * will also mint any pending rewards to cover up to the current block for open allocations, + * to be computed off-chain. Can only be called once as it checks that the issuanceBase is zero. + * @param _pendingRewards Pending rewards up to the current block for open allocations, to be minted by this function + */ + function initialSnapshot(uint256 _pendingRewards) external onlyGovernor { + require(issuanceBase == 0, "Cannot call this function more than once"); + lastRewardsUpdateBlock = block.number; + IGraphToken grt = graphToken(); + grt.mint(address(this), _pendingRewards); + issuanceBase = grt.totalSupply(); + emit InitialSnapshotTaken(block.number, issuanceBase, _pendingRewards); + } + + /** + * @dev Drip indexer rewards for layers 1 and 2 + * This function will mint enough tokens to cover all indexer rewards for the next + * dripInterval number of blocks. If the l2RewardsFraction is > 0, it will also send + * tokens and a callhook to the L2Reservoir, through the GRT Arbitrum bridge. + * Any staged changes to issuanceRate or l2RewardsFraction will be applied when this function + * is called. If issuanceRate changes, it also triggers a snapshot of rewards per signal on the RewardsManager. + * The call value must be greater than or equal to l2MaxSubmissionCost + (l2MaxGas * l2GasPriceBid), and must + * only be nonzero if l2RewardsFraction is nonzero. + * Calling this function can revert if the issuance rate has recently been reduced, and the existing + * tokens are sufficient to cover the full pending period. In this case, it's necessary to wait + * until the drip amount becomes positive before calling the function again. It can also revert + * if the l2RewardsFraction has been updated and the amount already sent to L2 is more than what we + * should send now. + * Note that the transaction on the L2 side might revert if it's received out-of-order by the L2Reservoir, + * because it checks an incrementing nonce. If that is the case, the retryable ticket can be redeemed + * again once the ticket for previous drip has been redeemed. + * This function with an additional parameter is only provided so that indexer operators can call it, + * specifying the indexer for which they are an operator. + * @param _l2MaxGas Max gas for the L2 retryable ticket, only needed if l2RewardsFraction is > 0 + * @param _l2GasPriceBid Gas price for the L2 retryable ticket, only needed if l2RewardsFraction is > 0 + * @param _l2MaxSubmissionCost Max submission price for the L2 retryable ticket, only needed if l2RewardsFraction is > 0 + * @param _keeperRewardBeneficiary Address to which to credit keeper reward (will be redeemed in L2 if l2RewardsFraction is nonzero) + * @param _indexer Indexer for whom the sender must be an authorized Operator + */ + function drip( + uint256 _l2MaxGas, + uint256 _l2GasPriceBid, + uint256 _l2MaxSubmissionCost, + address _keeperRewardBeneficiary, + address _indexer + ) external payable notPaused onlyIndexerOperator(_indexer) { + _drip(_l2MaxGas, _l2GasPriceBid, _l2MaxSubmissionCost, _keeperRewardBeneficiary); + } + + /** + * @dev Drip indexer rewards for layers 1 and 2 + * This function will mint enough tokens to cover all indexer rewards for the next + * dripInterval number of blocks. If the l2RewardsFraction is > 0, it will also send + * tokens and a callhook to the L2Reservoir, through the GRT Arbitrum bridge. + * Any staged changes to issuanceRate or l2RewardsFraction will be applied when this function + * is called. If issuanceRate changes, it also triggers a snapshot of rewards per signal on the RewardsManager. + * The call value must be greater than or equal to l2MaxSubmissionCost + (l2MaxGas * l2GasPriceBid), and must + * only be nonzero if l2RewardsFraction is nonzero. + * Calling this function can revert if the issuance rate has recently been reduced, and the existing + * tokens are sufficient to cover the full pending period. In this case, it's necessary to wait + * until the drip amount becomes positive before calling the function again. It can also revert + * if the l2RewardsFraction has been updated and the amount already sent to L2 is more than what we + * should send now. + * Note that the transaction on the L2 side might revert if it's received out-of-order by the L2Reservoir, + * because it checks an incrementing nonce. If that is the case, the retryable ticket can be redeemed + * again once the ticket for previous drip has been redeemed. + * @param _l2MaxGas Max gas for the L2 retryable ticket, only needed if l2RewardsFraction is > 0 + * @param _l2GasPriceBid Gas price for the L2 retryable ticket, only needed if l2RewardsFraction is > 0 + * @param _l2MaxSubmissionCost Max submission price for the L2 retryable ticket, only needed if l2RewardsFraction is > 0 + * @param _keeperRewardBeneficiary Address to which to credit keeper reward (will be redeemed in L2 if l2RewardsFraction is nonzero) + */ + function drip( + uint256 _l2MaxGas, + uint256 _l2GasPriceBid, + uint256 _l2MaxSubmissionCost, + address _keeperRewardBeneficiary + ) external payable notPaused onlyIndexerOrAllowedDripper { + _drip(_l2MaxGas, _l2GasPriceBid, _l2MaxSubmissionCost, _keeperRewardBeneficiary); + } + + /** + * @dev Drip indexer rewards for layers 1 and 2, private implementation. + * This function will mint enough tokens to cover all indexer rewards for the next + * dripInterval number of blocks. If the l2RewardsFraction is > 0, it will also send + * tokens and a callhook to the L2Reservoir, through the GRT Arbitrum bridge. + * Any staged changes to issuanceRate or l2RewardsFraction will be applied when this function + * is called. If issuanceRate changes, it also triggers a snapshot of rewards per signal on the RewardsManager. + * The call value must be greater than or equal to l2MaxSubmissionCost + (l2MaxGas * l2GasPriceBid), and must + * only be nonzero if l2RewardsFraction is nonzero. + * Calling this function can revert if the issuance rate has recently been reduced, and the existing + * tokens are sufficient to cover the full pending period. In this case, it's necessary to wait + * until the drip amount becomes positive before calling the function again. It can also revert + * if the l2RewardsFraction has been updated and the amount already sent to L2 is more than what we + * should send now. + * Note that the transaction on the L2 side might revert if it's received out-of-order by the L2Reservoir, + * because it checks an incrementing nonce. If that is the case, the retryable ticket can be redeemed + * again once the ticket for previous drip has been redeemed. + * @param _l2MaxGas Max gas for the L2 retryable ticket, only needed if l2RewardsFraction is > 0 + * @param _l2GasPriceBid Gas price for the L2 retryable ticket, only needed if l2RewardsFraction is > 0 + * @param _l2MaxSubmissionCost Max submission price for the L2 retryable ticket, only needed if l2RewardsFraction is > 0 + * @param _keeperRewardBeneficiary Address to which to credit keeper reward (will be redeemed in L2 if l2RewardsFraction is nonzero) + */ + function _drip( + uint256 _l2MaxGas, + uint256 _l2GasPriceBid, + uint256 _l2MaxSubmissionCost, + address _keeperRewardBeneficiary + ) private { + require( + block.number > lastRewardsUpdateBlock.add(minDripInterval), + "WAIT_FOR_MIN_INTERVAL" + ); + // Note we only validate that the beneficiary is nonzero, as the caller might + // want to send the reward to an address that is different to the indexer/dripper's address. + require(_keeperRewardBeneficiary != address(0), "INVALID_BENEFICIARY"); + + uint256 mintedRewardsTotal = getNewGlobalRewards(rewardsMintedUntilBlock); + uint256 mintedRewardsActual = getNewGlobalRewards(block.number); + // eps = (signed int) mintedRewardsTotal - mintedRewardsActual + + uint256 keeperReward = dripRewardPerBlock.mul(block.number.sub(lastRewardsUpdateBlock)); + if (nextIssuanceRate != issuanceRate) { + rewardsManager().updateAccRewardsPerSignal(); + snapshotAccumulatedRewards(mintedRewardsActual); // This updates lastRewardsUpdateBlock + issuanceRate = nextIssuanceRate; + emit IssuanceRateUpdated(issuanceRate); + } else { + snapshotAccumulatedRewards(mintedRewardsActual); + } + + rewardsMintedUntilBlock = block.number.add(dripInterval); + // n = deltaR(t1, t0) + uint256 newRewardsToDistribute = getNewGlobalRewards(rewardsMintedUntilBlock); + // N = n - eps + uint256 rewardsTokensToMint; + { + uint256 newRewardsPlusMintedActual = newRewardsToDistribute.add(mintedRewardsActual); + require( + newRewardsPlusMintedActual > mintedRewardsTotal, + "Would mint negative or zero tokens, wait before calling again" + ); + rewardsTokensToMint = newRewardsPlusMintedActual.sub(mintedRewardsTotal); + } + + IGraphToken grt = graphToken(); + grt.mint(address(this), rewardsTokensToMint.add(keeperReward)); + + uint256 tokensToSendToL2 = 0; + if (l2RewardsFraction != nextL2RewardsFraction) { + tokensToSendToL2 = nextL2RewardsFraction.mul(newRewardsToDistribute).div( + FIXED_POINT_SCALING_FACTOR + ); + if (mintedRewardsTotal > mintedRewardsActual) { + // eps > 0, i.e. t < t1_old + // Note this can fail if the old l2RewardsFraction is larger + // than the new, in which case we just have to wait until enough time has passed + // so that eps is small enough. This also applies to the case where the new + // l2RewardsFraction is zero, since we still need to send one last message + // with the new values to the L2Reservoir. + uint256 l2OffsetAmount = l2RewardsFraction + .mul(mintedRewardsTotal.sub(mintedRewardsActual)) + .div(FIXED_POINT_SCALING_FACTOR); + require( + tokensToSendToL2 > l2OffsetAmount, + "Negative amount would be sent to L2, wait before calling again" + ); + tokensToSendToL2 = tokensToSendToL2.add(keeperReward).sub(l2OffsetAmount); + } else { + tokensToSendToL2 = tokensToSendToL2.add(keeperReward).add( + l2RewardsFraction.mul(mintedRewardsActual.sub(mintedRewardsTotal)).div( + FIXED_POINT_SCALING_FACTOR + ) + ); + } + l2RewardsFraction = nextL2RewardsFraction; + emit L2RewardsFractionUpdated(l2RewardsFraction); + _sendNewTokensAndStateToL2( + tokensToSendToL2, + _l2MaxGas, + _l2GasPriceBid, + _l2MaxSubmissionCost, + keeperReward, + _keeperRewardBeneficiary + ); + } else if (l2RewardsFraction > 0) { + tokensToSendToL2 = rewardsTokensToMint + .mul(l2RewardsFraction) + .div(FIXED_POINT_SCALING_FACTOR) + .add(keeperReward); + _sendNewTokensAndStateToL2( + tokensToSendToL2, + _l2MaxGas, + _l2GasPriceBid, + _l2MaxSubmissionCost, + keeperReward, + _keeperRewardBeneficiary + ); + } else { + // Avoid locking funds in this contract if we don't need to + // send a message to L2. + require(msg.value == 0, "No eth value needed"); + // If we don't send rewards to L2, pay the keeper reward in L1 + grt.transfer(_keeperRewardBeneficiary, keeperReward); + } + emit RewardsDripped( + rewardsTokensToMint.add(keeperReward), + tokensToSendToL2, + rewardsMintedUntilBlock + ); + } + + /** + * @dev Get new total rewards on both layers at a particular block, since the last drip event + * This is deltaR = p * r ^ (blocknum - t0) - p, where: + * - p is the total token supply snapshot at t0 + * - t0 is the last drip block, i.e. lastRewardsUpdateBlock + * - r is the issuanceRate + * @param _blocknum Block number at which to calculate rewards + * @return New total rewards on both layers since the last drip + */ + function getNewGlobalRewards(uint256 _blocknum) public view returns (uint256) { + uint256 t0 = lastRewardsUpdateBlock; + if (issuanceRate <= MIN_ISSUANCE_RATE || _blocknum == t0) { + return 0; + } + return + issuanceBase + .mul(_pow(issuanceRate, _blocknum.sub(t0), FIXED_POINT_SCALING_FACTOR)) + .div(FIXED_POINT_SCALING_FACTOR) + .sub(issuanceBase); + } + + /** + * @dev Get new total rewards on this layer at a particular block, since the last drip event + * This is deltaR_L1 = (1-lambda) * deltaR, where: + * - deltaR is the new global rewards on both layers (see getNewGlobalRewards) + * - lambda is the fraction of rewards sent to L2, i.e. l2RewardsFraction + * @param _blocknum Block number at which to calculate rewards + * @return New total rewards on Layer 1 since the last drip + */ + function getNewRewards(uint256 _blocknum) public view override returns (uint256) { + return + getNewGlobalRewards(_blocknum) + .mul(FIXED_POINT_SCALING_FACTOR.sub(l2RewardsFraction)) + .div(FIXED_POINT_SCALING_FACTOR); + } + + /** + * @dev Sets the drip interval (internal). + * This is the time in the future (in blocks) for which drip() will mint rewards. + * Keep in mind that changing this value will require manually re-adjusting + * the reservoir's token balance, because the first call to drip might produce + * more or less tokens than needed. + * @param _dripInterval The new interval in blocks for which drip() will mint rewards + */ + function _setDripInterval(uint256 _dripInterval) internal { + require(_dripInterval > 0, "Drip interval must be > 0"); + dripInterval = _dripInterval; + emit DripIntervalUpdated(_dripInterval); + } + + /** + * @dev Snapshot accumulated rewards on this layer + * We compute accumulatedLayerRewards and mark this block as the lastRewardsUpdateBlock. + * We also update the issuanceBase by adding the new total rewards on both layers. + * @param _globalDelta New global rewards (i.e. rewards on L1 and L2) since the last update block + */ + function snapshotAccumulatedRewards(uint256 _globalDelta) internal { + issuanceBase = issuanceBase.add(_globalDelta); + // Reimplementation of getAccumulatedRewards but reusing the _globalDelta calculated above, + // to save gas + accumulatedLayerRewards = accumulatedLayerRewards.add( + _globalDelta.mul(FIXED_POINT_SCALING_FACTOR.sub(l2RewardsFraction)).div( + FIXED_POINT_SCALING_FACTOR + ) + ); + lastRewardsUpdateBlock = block.number; + } + + /** + * @dev Send new tokens and a message with state to L2 + * This function will use the L1GraphTokenGateway to send tokens + * to L2, and will also encode a callhook to update state on the L2Reservoir. + * @param _nTokens Number of tokens to send to L2 + * @param _maxGas Max gas for the L2 retryable ticket execution + * @param _gasPriceBid Gas price for the L2 retryable ticket execution + * @param _maxSubmissionCost Max submission price for the L2 retryable ticket + * @param _keeperReward Tokens to assign as keeper reward for calling drip + * @param _keeper Address of the keeper that will be rewarded + */ + function _sendNewTokensAndStateToL2( + uint256 _nTokens, + uint256 _maxGas, + uint256 _gasPriceBid, + uint256 _maxSubmissionCost, + uint256 _keeperReward, + address _keeper + ) internal { + uint256 l2IssuanceBase = l2RewardsFraction.mul(issuanceBase).div( + FIXED_POINT_SCALING_FACTOR + ); + bytes memory extraData = abi.encodeWithSelector( + IL2Reservoir.receiveDrip.selector, + l2IssuanceBase, + issuanceRate, + nextDripNonce, + _keeperReward, + _keeper + ); + nextDripNonce = nextDripNonce.add(1); + bytes memory data = abi.encode(_maxSubmissionCost, extraData); + IGraphToken grt = graphToken(); + ITokenGateway gateway = graphTokenGateway(); + grt.approve(address(gateway), _nTokens); + gateway.outboundTransfer{ value: msg.value }( + address(grt), + l2ReservoirAddress, + _nTokens, + _maxGas, + _gasPriceBid, + data + ); + } + + /** + * @dev Checks if an address is an indexer with stake in the Staking contract + * @param _indexer Address that will be checked + */ + function _isIndexer(address _indexer) internal view returns (bool) { + IStaking staking = staking(); + return staking.hasStake(_indexer); + } +} diff --git a/contracts/reservoir/L1ReservoirStorage.sol b/contracts/reservoir/L1ReservoirStorage.sol new file mode 100644 index 000000000..9f60249bd --- /dev/null +++ b/contracts/reservoir/L1ReservoirStorage.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; + +/** + * @dev Storage variables for the L1Reservoir, version 1 + */ +contract L1ReservoirV1Storage { + // Fraction of total rewards to be sent by L2, expressed in fixed point at 1e18 + uint256 public l2RewardsFraction; + // New fraction of total rewards to be sent by L2, to be applied on the next drip + uint256 public nextL2RewardsFraction; + // Address for the L2Reservoir to which we send rewards + address public l2ReservoirAddress; + // Block until the minted supplies should last before another drip is needed + uint256 public rewardsMintedUntilBlock; + // New issuance rate to be applied on the next drip + uint256 public nextIssuanceRate; + // Interval for rewards drip, in blocks + uint256 public dripInterval; + // Auto-incrementing nonce that will be used when sending rewards to L2, to ensure ordering + uint256 public nextDripNonce; +} + +/** + * @dev Storage variables for the L1Reservoir, version 2 + * This version adds some variables that are needed when introducing keeper rewards. + */ +contract L1ReservoirV2Storage is L1ReservoirV1Storage { + // Minimum number of blocks since last drip for a new drip to be allowed + uint256 public minDripInterval; + // Drip reward in GRT for each block since lastRewardsUpdateBlock + dripRewardThreshold + uint256 public dripRewardPerBlock; + // True for addresses that are allowed to call drip() + mapping(address => bool) public allowedDrippers; +} diff --git a/contracts/reservoir/Reservoir.sol b/contracts/reservoir/Reservoir.sol new file mode 100644 index 000000000..fff7f108a --- /dev/null +++ b/contracts/reservoir/Reservoir.sol @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; +pragma abicoder v2; + +import "@openzeppelin/contracts/math/SafeMath.sol"; + +import "../upgrades/GraphUpgradeable.sol"; + +import "./ReservoirStorage.sol"; +import "./IReservoir.sol"; + +/** + * @title Rewards Reservoir base contract + * @dev This contract acts as a reservoir/vault for the rewards to be distributed on Layer 1 or Layer 2. + * It provides functions to compute accumulated and new total rewards at a particular block number. + * This base contract provides functionality that is common to L1 and L2, to be extended on each layer. + */ +abstract contract Reservoir is GraphUpgradeable, ReservoirV1Storage, IReservoir { + using SafeMath for uint256; + + // Scaling factor for all fixed point arithmetics + uint256 internal constant FIXED_POINT_SCALING_FACTOR = 1e18; + // Minimum issuance rate (expressed in fixed point at 1e18) + uint256 internal constant MIN_ISSUANCE_RATE = 1e18; + + /** + * @dev Approve the RewardsManager to manage the reservoir's token funds + */ + function approveRewardsManager() external override onlyGovernor { + graphToken().approve(address(rewardsManager()), type(uint256).max); + } + + /** + * @dev Get accumulated total rewards on this layer at a particular block + * @param _blocknum Block number at which to calculate rewards + * @return Accumulated total rewards on this layer + */ + function getAccumulatedRewards(uint256 _blocknum) public view override returns (uint256) { + // R(t) = R(t0) + (DeltaR(t, t0)) + return accumulatedLayerRewards.add(getNewRewards(_blocknum)); + } + + /** + * @dev Get new total rewards on this layer at a particular block, since the last drip event. + * Must be implemented by the reservoir on each layer. + * @param _blocknum Block number at which to calculate rewards + * @return New total rewards on this layer since the last drip + */ + function getNewRewards(uint256 _blocknum) public view virtual override returns (uint256); + + /** + * @dev Raises _x to the power of _n with scaling factor of _base. + * Based on: https://github.com/makerdao/dss/blob/master/src/pot.sol#L81 + * @param _x Base of the exponentiation + * @param _n Exponent + * @param _base Scaling factor + * @return Exponential of _n with base _x + */ + function _pow( + uint256 _x, + uint256 _n, + uint256 _base + ) internal pure returns (uint256) { + uint256 z; + // solhint-disable-next-line no-inline-assembly + assembly { + switch _x + case 0 { + switch _n + case 0 { + z := _base + } + default { + z := 0 + } + } + default { + switch mod(_n, 2) + case 0 { + z := _base + } + default { + z := _x + } + let half := div(_base, 2) // for rounding. + for { + _n := div(_n, 2) + } _n { + _n := div(_n, 2) + } { + let xx := mul(_x, _x) + if iszero(eq(div(xx, _x), _x)) { + revert(0, 0) + } + let xxRound := add(xx, half) + if lt(xxRound, xx) { + revert(0, 0) + } + _x := div(xxRound, _base) + if mod(_n, 2) { + let zx := mul(z, _x) + if and(iszero(iszero(_x)), iszero(eq(div(zx, _x), z))) { + revert(0, 0) + } + let zxRound := add(zx, half) + if lt(zxRound, zx) { + revert(0, 0) + } + z := div(zxRound, _base) + } + } + } + } + return z; + } +} diff --git a/contracts/reservoir/ReservoirStorage.sol b/contracts/reservoir/ReservoirStorage.sol new file mode 100644 index 000000000..b964d87b0 --- /dev/null +++ b/contracts/reservoir/ReservoirStorage.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; + +import "../governance/Managed.sol"; + +/** + * @dev Base storage variables for the Reservoir on both layers, version 1 + */ +contract ReservoirV1Storage is Managed { + // Relative increase of the total supply per block, plus 1, expressed in fixed point at 1e18. + uint256 public issuanceRate; + // Accumulated total rewards on the corresponding layer (L1 or L2) + uint256 public accumulatedLayerRewards; + // Last block at which rewards when updated, i.e. block at which the last drip happened or was received + uint256 public lastRewardsUpdateBlock; + // Base value for token issuance, set initially to GRT supply and afterwards using accumulated rewards to update + uint256 public issuanceBase; +} diff --git a/contracts/rewards/IRewardsManager.sol b/contracts/rewards/IRewardsManager.sol index dc17c8ba8..f92ca422a 100644 --- a/contracts/rewards/IRewardsManager.sol +++ b/contracts/rewards/IRewardsManager.sol @@ -15,8 +15,6 @@ interface IRewardsManager { // -- Config -- - function setIssuanceRate(uint256 _issuanceRate) external; - function setMinimumSubgraphSignal(uint256 _minimumSubgraphSignal) external; // -- Denylist -- @@ -54,6 +52,8 @@ interface IRewardsManager { function takeRewards(address _allocationID) external returns (uint256); + function takeAndBurnRewards(address _allocationID) external; + // -- Hooks -- function onSubgraphSignalUpdate(bytes32 _subgraphDeploymentID) external returns (uint256); diff --git a/contracts/rewards/RewardsManager.sol b/contracts/rewards/RewardsManager.sol index 6d8f1be74..c9dcf103d 100644 --- a/contracts/rewards/RewardsManager.sol +++ b/contracts/rewards/RewardsManager.sol @@ -10,6 +10,8 @@ import "../upgrades/GraphUpgradeable.sol"; import "./RewardsManagerStorage.sol"; import "./IRewardsManager.sol"; +import "../reservoir/IReservoir.sol"; + /** * @title Rewards Manager Contract * @dev Tracks how inflationary GRT rewards should be handed out. Relies on the Curation contract @@ -27,10 +29,10 @@ import "./IRewardsManager.sol"; * These functions may overestimate the actual rewards due to changes in the total supply * until the actual takeRewards function is called. */ -contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsManager { +contract RewardsManager is RewardsManagerV4Storage, GraphUpgradeable, IRewardsManager { using SafeMath for uint256; - uint256 private constant TOKEN_DECIMALS = 1e18; + uint256 private constant FIXED_POINT_SCALING_FACTOR = 1e18; uint256 private constant MIN_ISSUANCE_RATE = 1e18; // -- Events -- @@ -46,9 +48,24 @@ contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsMa ); /** - * @dev Emitted when rewards are denied to an indexer. + * @dev Emitted when rewards are denied to an indexer (and therefore burned). */ - event RewardsDenied(address indexed indexer, address indexed allocationID, uint256 epoch); + event RewardsDenied( + address indexed indexer, + address indexed allocationID, + uint256 epoch, + uint256 amount + ); + + /** + * @dev Emitted when rewards for an indexer are burned . + */ + event RewardsBurned( + address indexed indexer, + address indexed allocationID, + uint256 epoch, + uint256 amount + ); /** * @dev Emitted when a subgraph is denied for claiming rewards. @@ -74,32 +91,6 @@ contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsMa // -- Config -- - /** - * @dev Sets the issuance rate. - * The issuance rate is defined as a percentage increase of the total supply per block. - * This means that it needs to be greater than 1.0, any number under 1.0 is not - * allowed and an issuance rate of 1.0 means no issuance. - * To accommodate a high precision the issuance rate is expressed in wei. - * @param _issuanceRate Issuance rate expressed in wei - */ - function setIssuanceRate(uint256 _issuanceRate) external override onlyGovernor { - _setIssuanceRate(_issuanceRate); - } - - /** - * @dev Sets the issuance rate. - * @param _issuanceRate Issuance rate - */ - function _setIssuanceRate(uint256 _issuanceRate) private { - require(_issuanceRate >= MIN_ISSUANCE_RATE, "Issuance rate under minimum allowed"); - - // Called since `issuance rate` will change - updateAccRewardsPerSignal(); - - issuanceRate = _issuanceRate; - emit ParameterUpdated("issuanceRate"); - } - /** * @dev Sets the subgraph oracle allowed to denegate distribution of rewards to subgraphs. * @param _subgraphAvailabilityOracle Address of the subgraph availability oracle @@ -187,32 +178,13 @@ contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsMa /** * @dev Gets the issuance of rewards per signal since last updated. * - * Compound interest formula: `a = p(1 + r/n)^nt` - * The formula is simplified with `n = 1` as we apply the interest once every time step. - * The `r` is passed with +1 included. So for 10% instead of 0.1 it is 1.1 - * The simplified formula is `a = p * r^t` - * - * Notation: - * t: time steps are in blocks since last updated - * p: total supply of GRT tokens - * a: inflated amount of total supply for the period `t` when interest `r` is applied - * x: newly accrued rewards token for the period `t` + * The compound interest formula is applied in the Reservoir contract. + * This function will compare accumulated rewards at the current block + * with the value that was cached at accRewardsPerSignalLastBlockUpdated. * * @return newly accrued rewards per signal since last update */ function getNewRewardsPerSignal() public view override returns (uint256) { - // Calculate time steps - uint256 t = block.number.sub(accRewardsPerSignalLastBlockUpdated); - // Optimization to skip calculations if zero time steps elapsed - if (t == 0) { - return 0; - } - - // Zero issuance under a rate of 1.0 - if (issuanceRate <= MIN_ISSUANCE_RATE) { - return 0; - } - // Zero issuance if no signalled tokens IGraphToken graphToken = graphToken(); uint256 signalledTokens = graphToken.balanceOf(address(curation())); @@ -220,16 +192,14 @@ contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsMa return 0; } - uint256 r = issuanceRate; - uint256 p = tokenSupplySnapshot; - uint256 a = p.mul(_pow(r, t, TOKEN_DECIMALS)).div(TOKEN_DECIMALS); - - // New issuance of tokens during time steps - uint256 x = a.sub(p); + uint256 accRewardsNow = reservoir().getAccumulatedRewards(block.number); // Get the new issuance per signalled token // We multiply the decimals to keep the precision as fixed-point number - return x.mul(TOKEN_DECIMALS).div(signalledTokens); + return + (accRewardsNow.sub(accRewardsOnLastSignalUpdate)).mul(FIXED_POINT_SCALING_FACTOR).div( + signalledTokens + ); } /** @@ -261,7 +231,7 @@ contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsMa ? getAccRewardsPerSignal() .sub(subgraph.accRewardsPerSignalSnapshot) .mul(subgraphSignalledTokens) - .div(TOKEN_DECIMALS) + .div(FIXED_POINT_SCALING_FACTOR) : 0; return subgraph.accRewardsForSubgraph.add(newRewards); } @@ -292,9 +262,9 @@ contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsMa return (0, accRewardsForSubgraph); } - uint256 newRewardsPerAllocatedToken = newRewardsForSubgraph.mul(TOKEN_DECIMALS).div( - subgraphAllocatedTokens - ); + uint256 newRewardsPerAllocatedToken = newRewardsForSubgraph + .mul(FIXED_POINT_SCALING_FACTOR) + .div(subgraphAllocatedTokens); return ( subgraph.accRewardsPerAllocatedToken.add(newRewardsPerAllocatedToken), accRewardsForSubgraph @@ -304,7 +274,8 @@ contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsMa // -- Updates -- /** - * @dev Updates the accumulated rewards per signal and save checkpoint block number. + * @dev Updates the accumulated rewards per signal and saves the checkpoint block number. + * Also snapshots total accumulated rewards (`accRewardsOnLastSignalUpdate`). * Must be called before `issuanceRate` or `total signalled GRT` changes * Called from the Curation contract on mint() and burn() * @return Accumulated rewards per signal @@ -312,7 +283,7 @@ contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsMa function updateAccRewardsPerSignal() public override returns (uint256) { accRewardsPerSignal = getAccRewardsPerSignal(); accRewardsPerSignalLastBlockUpdated = block.number; - tokenSupplySnapshot = graphToken().totalSupply(); + accRewardsOnLastSignalUpdate = reservoir().getAccumulatedRewards(block.number); return accRewardsPerSignal; } @@ -393,13 +364,13 @@ contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsMa uint256 _endAccRewardsPerAllocatedToken ) private pure returns (uint256) { uint256 newAccrued = _endAccRewardsPerAllocatedToken.sub(_startAccRewardsPerAllocatedToken); - return newAccrued.mul(_tokens).div(TOKEN_DECIMALS); + return newAccrued.mul(_tokens).div(FIXED_POINT_SCALING_FACTOR); } /** * @dev Pull rewards from the contract for a particular allocation. * This function can only be called by the Staking contract. - * This function will mint the necessary tokens to reward based on the inflation calculation. + * This function will transfer the necessary tokens to reward based on the inflation calculation. * @param _allocationID Allocation * @return Assigned rewards amount */ @@ -413,90 +384,55 @@ contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsMa alloc.subgraphDeploymentID ); - // Do not do rewards on denied subgraph deployments ID - if (isDenied(alloc.subgraphDeploymentID)) { - emit RewardsDenied(alloc.indexer, _allocationID, alloc.closedAtEpoch); - return 0; - } - // Calculate rewards accrued by this allocation uint256 rewards = _calcRewards( alloc.tokens, alloc.accRewardsPerAllocatedToken, accRewardsPerAllocatedToken ); - if (rewards > 0) { - // Mint directly to staking contract for the reward amount + if (!isDenied(alloc.subgraphDeploymentID)) { + // Transfer to staking contract for the reward amount // The staking contract will do bookkeeping of the reward and // assign in proportion to each stakeholder incentive - graphToken().mint(address(staking), rewards); + if (rewards > 0) { + graphToken().transferFrom(address(reservoir()), address(staking), rewards); + } + emit RewardsAssigned(alloc.indexer, _allocationID, alloc.closedAtEpoch, rewards); + return rewards; + } else { + if (rewards > 0) { + graphToken().burnFrom(address(reservoir()), rewards); + } + emit RewardsDenied(alloc.indexer, _allocationID, alloc.closedAtEpoch, rewards); + return 0; } - - emit RewardsAssigned(alloc.indexer, _allocationID, alloc.closedAtEpoch, rewards); - - return rewards; } /** - * @dev Raises x to the power of n with scaling factor of base. - * Based on: https://github.com/makerdao/dss/blob/master/src/pot.sol#L81 - * @param x Base of the exponentiation - * @param n Exponent - * @param base Scaling factor - * @return z Exponential of n with base x + * @dev Burn rewards for a particular allocation. + * This function can only be called by the Staking contract. + * This function will burn the necessary tokens to reward based on the inflation calculation. + * @param _allocationID Allocation */ - function _pow( - uint256 x, - uint256 n, - uint256 base - ) private pure returns (uint256 z) { - assembly { - switch x - case 0 { - switch n - case 0 { - z := base - } - default { - z := 0 - } - } - default { - switch mod(n, 2) - case 0 { - z := base - } - default { - z := x - } - let half := div(base, 2) // for rounding. - for { - n := div(n, 2) - } n { - n := div(n, 2) - } { - let xx := mul(x, x) - if iszero(eq(div(xx, x), x)) { - revert(0, 0) - } - let xxRound := add(xx, half) - if lt(xxRound, xx) { - revert(0, 0) - } - x := div(xxRound, base) - if mod(n, 2) { - let zx := mul(z, x) - if and(iszero(iszero(x)), iszero(eq(div(zx, x), z))) { - revert(0, 0) - } - let zxRound := add(zx, half) - if lt(zxRound, zx) { - revert(0, 0) - } - z := div(zxRound, base) - } - } - } + function takeAndBurnRewards(address _allocationID) external override { + // Only Staking contract is authorized as caller + IStaking staking = staking(); + require(msg.sender == address(staking), "Caller must be the staking contract"); + + IStaking.Allocation memory alloc = staking.getAllocation(_allocationID); + uint256 accRewardsPerAllocatedToken = onSubgraphAllocationUpdate( + alloc.subgraphDeploymentID + ); + + // Calculate rewards accrued by this allocation + uint256 rewards = _calcRewards( + alloc.tokens, + alloc.accRewardsPerAllocatedToken, + accRewardsPerAllocatedToken + ); + if (rewards > 0) { + graphToken().burnFrom(address(reservoir()), rewards); + emit RewardsBurned(alloc.indexer, _allocationID, alloc.closedAtEpoch, rewards); } } } diff --git a/contracts/rewards/RewardsManagerStorage.sol b/contracts/rewards/RewardsManagerStorage.sol index 7626992da..d8a6284e5 100644 --- a/contracts/rewards/RewardsManagerStorage.sol +++ b/contracts/rewards/RewardsManagerStorage.sol @@ -8,7 +8,7 @@ import "../governance/Managed.sol"; contract RewardsManagerV1Storage is Managed { // -- State -- - uint256 public issuanceRate; + uint256 public issuanceRateDeprecated; uint256 public accRewardsPerSignal; uint256 public accRewardsPerSignalLastBlockUpdated; @@ -29,5 +29,10 @@ contract RewardsManagerV2Storage is RewardsManagerV1Storage { contract RewardsManagerV3Storage is RewardsManagerV2Storage { // Snapshot of the total supply of GRT when accRewardsPerSignal was last updated - uint256 public tokenSupplySnapshot; + uint256 public tokenSupplySnapshotDeprecated; +} + +contract RewardsManagerV4Storage is RewardsManagerV3Storage { + // Accumulated rewards at accRewardsPerSignalLastBlockUpdated + uint256 public accRewardsOnLastSignalUpdate; } diff --git a/contracts/staking/Staking.sol b/contracts/staking/Staking.sol index 2bcc8d74d..37c972a53 100644 --- a/contracts/staking/Staking.sol +++ b/contracts/staking/Staking.sol @@ -1218,11 +1218,12 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking, Multicall { // Process non-zero-allocation rewards tracking if (alloc.tokens > 0) { - // Distribute rewards if proof of indexing was presented by the indexer or operator + // Distribute rewards if proof of indexing was presented by the indexer or operator, + // otherwise the rewards will be burned from the reservoir. if (isIndexer && _poi != 0) { _distributeRewards(_allocationID, alloc.indexer); } else { - _updateRewards(alloc.subgraphDeploymentID); + _takeAndBurnRewards(_allocationID); } // Free allocated tokens from use @@ -1584,9 +1585,6 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking, Multicall { */ function _updateRewards(bytes32 _subgraphDeploymentID) private returns (uint256) { IRewardsManager rewardsManager = rewardsManager(); - if (address(rewardsManager) == address(0)) { - return 0; - } return rewardsManager.onSubgraphAllocationUpdate(_subgraphDeploymentID); } @@ -1596,12 +1594,9 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking, Multicall { */ function _distributeRewards(address _allocationID, address _indexer) private { IRewardsManager rewardsManager = rewardsManager(); - if (address(rewardsManager) == address(0)) { - return; - } // Automatically triggers update of rewards snapshot as allocation will change - // after this call. Take rewards mint tokens for the Staking contract to distribute + // after this call. Take rewards transfers tokens for the Staking contract to distribute // between indexer and delegators uint256 totalRewards = rewardsManager.takeRewards(_allocationID); if (totalRewards == 0) { @@ -1621,6 +1616,18 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking, Multicall { ); } + /** + * @dev Burn rewards for the closed allocation and update the allocation state. + * @param _allocationID Allocation + */ + function _takeAndBurnRewards(address _allocationID) private { + IRewardsManager rewardsManager = rewardsManager(); + + // Automatically triggers update of rewards snapshot as allocation will change + // after this call. + rewardsManager.takeAndBurnRewards(_allocationID); + } + /** * @dev Send rewards to the appropiate destination. * @param _graphToken Graph token diff --git a/contracts/tests/ReservoirMock.sol b/contracts/tests/ReservoirMock.sol new file mode 100644 index 000000000..2ce40b3f2 --- /dev/null +++ b/contracts/tests/ReservoirMock.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; +pragma abicoder v2; + +import "../reservoir/Reservoir.sol"; + +// Mock contract used for testing rewards +contract ReservoirMock is Reservoir { + function getNewRewards(uint256) public view override returns (uint256 r) {} + + /** + * @dev Raises x to the power of n with scaling factor of base. + * Based on: https://github.com/makerdao/dss/blob/master/src/pot.sol#L81 + * @param x Base of the exponentiation + * @param n Exponent + * @param base Scaling factor + * @return z Exponential of n with base x + */ + function pow( + uint256 x, + uint256 n, + uint256 base + ) public pure returns (uint256 z) { + z = _pow(x, n, base); + } +} diff --git a/contracts/tests/RewardsManagerMock.sol b/contracts/tests/RewardsManagerMock.sol deleted file mode 100644 index cbd57b2d3..000000000 --- a/contracts/tests/RewardsManagerMock.sol +++ /dev/null @@ -1,68 +0,0 @@ -pragma solidity ^0.7.6; -pragma abicoder v2; - -// Mock contract used for testing rewards -contract RewardsManagerMock { - /** - * @dev Raises x to the power of n with scaling factor of base. - * Based on: https://github.com/makerdao/dss/blob/master/src/pot.sol#L81 - * @param x Base of the exponentiation - * @param n Exponent - * @param base Scaling factor - * @return z Exponential of n with base x - */ - function pow( - uint256 x, - uint256 n, - uint256 base - ) public pure returns (uint256 z) { - assembly { - switch x - case 0 { - switch n - case 0 { - z := base - } - default { - z := 0 - } - } - default { - switch mod(n, 2) - case 0 { - z := base - } - default { - z := x - } - let half := div(base, 2) // for rounding. - for { - n := div(n, 2) - } n { - n := div(n, 2) - } { - let xx := mul(x, x) - if iszero(eq(div(xx, x), x)) { - revert(0, 0) - } - let xxRound := add(xx, half) - if lt(xxRound, xx) { - revert(0, 0) - } - x := div(xxRound, base) - if mod(n, 2) { - let zx := mul(z, x) - if and(iszero(iszero(x)), iszero(eq(div(zx, x), z))) { - revert(0, 0) - } - let zxRound := add(zx, half) - if lt(zxRound, zx) { - revert(0, 0) - } - z := div(zxRound, base) - } - } - } - } - } -} diff --git a/contracts/tests/arbitrum/BridgeMock.sol b/contracts/tests/arbitrum/BridgeMock.sol new file mode 100644 index 000000000..4f2848288 --- /dev/null +++ b/contracts/tests/arbitrum/BridgeMock.sol @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; + +import "../../arbitrum/IBridge.sol"; + +/** + * @title Arbitrum Bridge mock contract + * @dev This contract implements Arbitrum's IBridge interface for testing purposes + */ +contract BridgeMock is IBridge { + // Address of the (mock) Arbitrum Inbox + address public inbox; + // Address of the (mock) Arbitrum Outbox + address public outbox; + // Index of the next message on the inbox messages array + uint256 public messageIndex; + // Inbox messages array + bytes32[] public override inboxAccs; + + /** + * @dev Deliver a message to the inbox. The encoded message will be + * added to the inbox array, and messageIndex will be incremented. + * @param _kind Type of the message + * @param _sender Address that is sending the message + * @param _messageDataHash keccak256 hash of the message data + * @return The next index for the inbox array + */ + function deliverMessageToInbox( + uint8 _kind, + address _sender, + bytes32 _messageDataHash + ) external payable override returns (uint256) { + messageIndex = messageIndex + 1; + inboxAccs.push(keccak256(abi.encodePacked(inbox, _kind, _sender, _messageDataHash))); + emit MessageDelivered( + messageIndex, + inboxAccs[messageIndex - 1], + msg.sender, + _kind, + _sender, + _messageDataHash + ); + return messageIndex; + } + + /** + * @dev Executes an L1 function call incoing from L2. This can only be called + * by the Outbox. + * @param _destAddr Contract to call + * @param _amount ETH value to send + * @param _data Calldata for the function call + * @return True if the call was successful, false otherwise + * @return Return data from the call + */ + function executeCall( + address _destAddr, + uint256 _amount, + bytes calldata _data + ) external override returns (bool, bytes memory) { + require(outbox == msg.sender, "NOT_FROM_OUTBOX"); + bool success; + bytes memory returnData; + + // solhint-disable-next-line avoid-low-level-calls + (success, returnData) = _destAddr.call{ value: _amount }(_data); + emit BridgeCallTriggered(msg.sender, _destAddr, _amount, _data); + return (success, returnData); + } + + /** + * @dev Set the address of the inbox. Anyone can call this, because it's a mock. + * @param _inbox Address of the inbox + * @param _enabled Enable the inbox (ignored) + */ + function setInbox(address _inbox, bool _enabled) external override { + inbox = _inbox; + emit InboxToggle(inbox, _enabled); + } + + /** + * @dev Set the address of the outbox. Anyone can call this, because it's a mock. + * @param _outbox Address of the outbox + * @param _enabled Enable the outbox (ignored) + */ + function setOutbox(address _outbox, bool _enabled) external override { + outbox = _outbox; + emit OutboxToggle(outbox, _enabled); + } + + // View functions + + /** + * @dev Getter for the active outbox (in this case there's only one) + */ + function activeOutbox() external view override returns (address) { + return outbox; + } + + /** + * @dev Getter for whether an address is an allowed inbox (in this case there's only one) + * @param _inbox Address to check + * @return True if the address is the allowed inbox, false otherwise + */ + function allowedInboxes(address _inbox) external view override returns (bool) { + return _inbox == inbox; + } + + /** + * @dev Getter for whether an address is an allowed outbox (in this case there's only one) + * @param _outbox Address to check + * @return True if the address is the allowed outbox, false otherwise + */ + function allowedOutboxes(address _outbox) external view override returns (bool) { + return _outbox == outbox; + } + + /** + * @dev Getter for the count of messages in the inboxAccs + * @return Number of messages in inboxAccs + */ + function messageCount() external view override returns (uint256) { + return inboxAccs.length; + } +} diff --git a/contracts/tests/arbitrum/InboxMock.sol b/contracts/tests/arbitrum/InboxMock.sol new file mode 100644 index 000000000..b600ec3ac --- /dev/null +++ b/contracts/tests/arbitrum/InboxMock.sol @@ -0,0 +1,198 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; + +import "../../arbitrum/IInbox.sol"; +import "../../arbitrum/AddressAliasHelper.sol"; + +/** + * @title Arbitrum Inbox mock contract + * @dev This contract implements (a subset of) Arbitrum's IInbox interface for testing purposes + */ +contract InboxMock is IInbox { + // Type indicator for a standard L2 message + uint8 internal constant L2_MSG = 3; + // Type indicator for a retryable ticket message + // solhint-disable-next-line const-name-snakecase + uint8 internal constant L1MessageType_submitRetryableTx = 9; + // Address of the Bridge (mock) contract + IBridge public override bridge; + + /** + * @dev Send a message to L2 (by delivering it to the Bridge) + * @param _messageData Encoded data to send in the message + * @return message number returned by the inbox + */ + function sendL2Message(bytes calldata _messageData) external override returns (uint256) { + uint256 msgNum = deliverToBridge(L2_MSG, msg.sender, keccak256(_messageData)); + emit InboxMessageDelivered(msgNum, _messageData); + return msgNum; + } + + /** + * @dev Set the address of the (mock) bridge + * @param _bridge Address of the bridge + */ + function setBridge(address _bridge) external { + bridge = IBridge(_bridge); + } + + /** + * @dev Unimplemented in this mock + */ + function sendUnsignedTransaction( + uint256, + uint256, + uint256, + address, + uint256, + bytes calldata + ) external pure override returns (uint256) { + revert("Unimplemented"); + } + + /** + * @dev Unimplemented in this mock + */ + function sendContractTransaction( + uint256, + uint256, + address, + uint256, + bytes calldata + ) external pure override returns (uint256) { + revert("Unimplemented"); + } + + /** + * @dev Unimplemented in this mock + */ + function sendL1FundedUnsignedTransaction( + uint256, + uint256, + uint256, + address, + bytes calldata + ) external payable override returns (uint256) { + revert("Unimplemented"); + } + + /** + * @dev Unimplemented in this mock + */ + function sendL1FundedContractTransaction( + uint256, + uint256, + address, + bytes calldata + ) external payable override returns (uint256) { + revert("Unimplemented"); + } + + /** + * @dev Creates a retryable ticket for an L2 transaction + * @param _destAddr Address of the contract to call in L2 + * @param _arbTxCallValue Callvalue to use in the L2 transaction + * @param _maxSubmissionCost Max cost of submitting the ticket, in Wei + * @param _submissionRefundAddress L2 address to refund for any remaining value from the submission cost + * @param _valueRefundAddress L2 address to refund if the ticket times out or gets cancelled + * @param _maxGas Max gas for the L2 transcation + * @param _gasPriceBid Gas price bid on L2 + * @param _data Encoded calldata for the L2 transaction (including function selector) + * @return message number returned by the bridge + */ + function createRetryableTicket( + address _destAddr, + uint256 _arbTxCallValue, + uint256 _maxSubmissionCost, + address _submissionRefundAddress, + address _valueRefundAddress, + uint256 _maxGas, + uint256 _gasPriceBid, + bytes calldata _data + ) external payable override returns (uint256) { + _submissionRefundAddress = AddressAliasHelper.applyL1ToL2Alias(_submissionRefundAddress); + _valueRefundAddress = AddressAliasHelper.applyL1ToL2Alias(_valueRefundAddress); + return + _deliverMessage( + L1MessageType_submitRetryableTx, + msg.sender, + abi.encodePacked( + uint256(uint160(bytes20(_destAddr))), + _arbTxCallValue, + msg.value, + _maxSubmissionCost, + uint256(uint160(bytes20(_submissionRefundAddress))), + uint256(uint160(bytes20(_valueRefundAddress))), + _maxGas, + _gasPriceBid, + _data.length, + _data + ) + ); + } + + function depositEth(uint256) external payable override returns (uint256) { + revert("Unimplemented"); + } + + /** + * @dev Unimplemented in this mock + */ + function pauseCreateRetryables() external pure override { + revert("Unimplemented"); + } + + /** + * @dev Unimplemented in this mock + */ + function unpauseCreateRetryables() external pure override { + revert("Unimplemented"); + } + + /** + * @dev Unimplemented in this mock + */ + function startRewriteAddress() external pure override { + revert("Unimplemented"); + } + + /** + * @dev Unimplemented in this mock + */ + function stopRewriteAddress() external pure override { + revert("Unimplemented"); + } + + /** + * @dev Deliver a message to the bridge + * @param _kind Type of the message + * @param _sender Address that is sending the message + * @param _messageData Encoded message data + * @return Message number returned by the bridge + */ + function _deliverMessage( + uint8 _kind, + address _sender, + bytes memory _messageData + ) internal returns (uint256) { + uint256 msgNum = deliverToBridge(_kind, _sender, keccak256(_messageData)); + emit InboxMessageDelivered(msgNum, _messageData); + return msgNum; + } + + /** + * @dev Deliver a message to the bridge + * @param _kind Type of the message + * @param _sender Address that is sending the message + * @param _messageDataHash keccak256 hash of the encoded message data + * @return Message number returned by the bridge + */ + function deliverToBridge( + uint8 _kind, + address _sender, + bytes32 _messageDataHash + ) internal returns (uint256) { + return bridge.deliverMessageToInbox{ value: msg.value }(_kind, _sender, _messageDataHash); + } +} diff --git a/contracts/tests/arbitrum/OutboxMock.sol b/contracts/tests/arbitrum/OutboxMock.sol new file mode 100644 index 000000000..a529a975a --- /dev/null +++ b/contracts/tests/arbitrum/OutboxMock.sol @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; + +import "../../arbitrum/IOutbox.sol"; +import "../../arbitrum/IBridge.sol"; + +/** + * @title Arbitrum Outbox mock contract + * @dev This contract implements (a subset of) Arbitrum's IOutbox interface for testing purposes + */ +contract OutboxMock is IOutbox { + // Context of an L2-to-L1 function call + struct L2ToL1Context { + uint128 l2Block; + uint128 l1Block; + uint128 timestamp; + uint128 batchNum; + bytes32 outputId; + address sender; + } + // Context of the current L2-to-L1 function call (set and cleared in each transaction) + L2ToL1Context internal context; + + // Address of the (mock) Arbitrum Bridge + IBridge public bridge; + + /** + * @dev Set the address of the (mock) bridge + * @param _bridge Address of the bridge + */ + function setBridge(address _bridge) external { + bridge = IBridge(_bridge); + } + + /** + * @dev Getter for the L2 sender of the current incoming message + */ + function l2ToL1Sender() external view override returns (address) { + return context.sender; + } + + /** + * @dev Getter for the L2 block of the current incoming message + */ + function l2ToL1Block() external view override returns (uint256) { + return context.l2Block; + } + + /** + * @dev Getter for the L1 block of the current incoming message + */ + function l2ToL1EthBlock() external view override returns (uint256) { + return context.l1Block; + } + + /** + * @dev Getter for the L1 timestamp of the current incoming message + */ + function l2ToL1Timestamp() external view override returns (uint256) { + return context.timestamp; + } + + /** + * @dev Getter for the L2 batch number of the current incoming message + */ + function l2ToL1BatchNum() external view override returns (uint256) { + return context.batchNum; + } + + /** + * @dev Getter for the output ID of the current incoming message + */ + function l2ToL1OutputId() external view override returns (bytes32) { + return context.outputId; + } + + /** + * @dev Unimplemented in this mock + */ + function processOutgoingMessages(bytes calldata, uint256[] calldata) external pure override { + revert("Unimplemented"); + } + + /** + * @dev Check whether an outbox entry for a message exists. + * This mock returns always true. + */ + function outboxEntryExists(uint256) external pure override returns (bool) { + return true; + } + + /** + * @notice (Mock) Executes a messages in an Outbox entry. + * @dev This mocks what has to be called when finalizing an L2 to L1 transfer. + * In our mock scenario, we don't validate and execute unconditionally. + * @param _batchNum Index of OutboxEntry in outboxEntries array + * @param _l2Sender sender of original message (i.e., caller of ArbSys.sendTxToL1) + * @param _destAddr destination address for L1 contract call + * @param _l2Block l2 block number at which sendTxToL1 call was made + * @param _l1Block l1 block number at which sendTxToL1 call was made + * @param _l2Timestamp l2 Timestamp at which sendTxToL1 call was made + * @param _amount value in L1 message in wei + * @param _calldataForL1 abi-encoded L1 message data + */ + function executeTransaction( + uint256 _batchNum, + bytes32[] calldata, // proof + uint256, // index + address _l2Sender, + address _destAddr, + uint256 _l2Block, + uint256 _l1Block, + uint256 _l2Timestamp, + uint256 _amount, + bytes calldata _calldataForL1 + ) external virtual { + bytes32 outputId; + + context = L2ToL1Context({ + sender: _l2Sender, + l2Block: uint128(_l2Block), + l1Block: uint128(_l1Block), + timestamp: uint128(_l2Timestamp), + batchNum: uint128(_batchNum), + outputId: outputId + }); + + // set and reset vars around execution so they remain valid during call + executeBridgeCall(_destAddr, _amount, _calldataForL1); + } + + /** + * @dev Execute an L2-to-L1 function call by calling the bridge + * @param _destAddr Address of the contract to call + * @param _amount Callvalue for the function call + * @param _data Calldata for the function call + */ + function executeBridgeCall( + address _destAddr, + uint256 _amount, + bytes memory _data + ) internal { + (bool success, bytes memory returndata) = bridge.executeCall(_destAddr, _amount, _data); + if (!success) { + if (returndata.length > 0) { + // solhint-disable-next-line no-inline-assembly + assembly { + let returndata_size := mload(returndata) + revert(add(32, returndata), returndata_size) + } + } else { + revert("BRIDGE_CALL_FAILED"); + } + } + } +} diff --git a/contracts/token/IGraphToken.sol b/contracts/token/IGraphToken.sol index 2b7dbaa20..8255e18d5 100644 --- a/contracts/token/IGraphToken.sol +++ b/contracts/token/IGraphToken.sol @@ -9,6 +9,8 @@ interface IGraphToken is IERC20 { function burn(uint256 amount) external; + function burnFrom(address _from, uint256 amount) external; + function mint(address _to, uint256 _amount) external; // -- Mint Admin -- @@ -32,4 +34,10 @@ interface IGraphToken is IERC20 { bytes32 _r, bytes32 _s ) external; + + // -- Allowance -- + + function increaseAllowance(address spender, uint256 addedValue) external returns (bool); + + function decreaseAllowance(address spender, uint256 subtractedValue) external returns (bool); } diff --git a/hardhat.config.ts b/hardhat.config.ts index 5ce3b085d..89cd6ec63 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -60,6 +60,13 @@ const networkConfigs: NetworkConfig[] = [ { network: 'rinkeby', chainId: 4 }, { network: 'goerli', chainId: 5 }, { network: 'kovan', chainId: 42 }, + { network: 'arbitrum-rinkeby', chainId: 421611, url: 'https://rinkeby.arbitrum.io/rpc' }, + { network: 'arbitrum-one', chainId: 42161, url: 'https://arb1.arbitrum.io/rpc' }, + { + network: 'arbitrum-goerli', + chainId: 421613, + url: 'https://goerli-rollup.arbitrum.io/rpc', + }, ] function getAccountsKeys() { diff --git a/package.json b/package.json index 2d2b73af5..4655d1886 100644 --- a/package.json +++ b/package.json @@ -12,12 +12,14 @@ "addresses.json" ], "dependencies": { - "ethers": "^5.4.4" + "ethers": "^5.6.0" }, "devDependencies": { + "@arbitrum/sdk": "^3.0.0-beta.5", "@commitlint/cli": "^13.2.1", "@commitlint/config-conventional": "^13.2.0", - "@ethersproject/experimental": "^5.4.0", + "@defi-wonderland/smock": "^2.0.7", + "@ethersproject/experimental": "^5.6.0", "@graphprotocol/common-ts": "^1.6.0", "@nomiclabs/hardhat-ethers": "^2.0.2", "@nomiclabs/hardhat-etherscan": "^2.1.1", @@ -39,6 +41,7 @@ "@typescript-eslint/eslint-plugin": "^4.0.0", "@typescript-eslint/parser": "^4.0.0", "@urql/core": "^2.1.3", + "arbos-precompiles": "^1.0.2", "bignumber.js": "^9.0.0", "chai": "^4.3.4", "cli-table": "^0.3.6", diff --git a/tasks/gre.ts b/tasks/gre.ts index baced85d9..d8a13a13e 100644 --- a/tasks/gre.ts +++ b/tasks/gre.ts @@ -27,7 +27,7 @@ extendEnvironment((hre: HardhatRuntimeEnvironment) => { addressBook: lazyObject(() => getAddressBook(addressBookPath, chainId)), graphConfig: lazyObject(() => readConfig(graphConfigPath, true)), contracts: lazyObject(() => - loadContracts(getAddressBook(addressBookPath, chainId), hre.ethers.provider), + loadContracts(getAddressBook(addressBookPath, chainId), chainId, hre.ethers.provider), ), } } diff --git a/test/epochs.test.ts b/test/epochs.test.ts index 2ff87084a..1e807c053 100644 --- a/test/epochs.test.ts +++ b/test/epochs.test.ts @@ -12,6 +12,7 @@ import { getAccounts, toBN, Account, + initNetwork, } from './lib/testHelpers' describe('EpochManager', () => { @@ -23,6 +24,7 @@ describe('EpochManager', () => { const epochLength: BigNumber = toBN('3') before(async function () { + await initNetwork() ;[me, governor] = await getAccounts() }) diff --git a/test/gateway/bridgeEscrow.test.ts b/test/gateway/bridgeEscrow.test.ts new file mode 100644 index 000000000..aea195159 --- /dev/null +++ b/test/gateway/bridgeEscrow.test.ts @@ -0,0 +1,77 @@ +import { expect } from 'chai' +import { BigNumber } from 'ethers' + +import { GraphToken } from '../../build/types/GraphToken' +import { BridgeEscrow } from '../../build/types/BridgeEscrow' + +import { NetworkFixture } from '../lib/fixtures' + +import { getAccounts, toGRT, Account } from '../lib/testHelpers' + +describe('BridgeEscrow', () => { + let governor: Account + let tokenReceiver: Account + let spender: Account + + let fixture: NetworkFixture + + let grt: GraphToken + let bridgeEscrow: BridgeEscrow + + const nTokens = toGRT('1000') + + before(async function () { + ;[governor, tokenReceiver, spender] = await getAccounts() + + fixture = new NetworkFixture() + ;({ grt, bridgeEscrow } = await fixture.load(governor.signer)) + + // Give some funds to the Escrow + await grt.connect(governor.signer).mint(bridgeEscrow.address, nTokens) + }) + + beforeEach(async function () { + await fixture.setUp() + }) + + afterEach(async function () { + await fixture.tearDown() + }) + + describe('approveAll', function () { + it('cannot be called by someone other than the governor', async function () { + const tx = bridgeEscrow.connect(tokenReceiver.signer).approveAll(spender.address) + expect(tx).to.be.revertedWith('Caller must be Controller governor') + }) + it('allows a spender to transfer GRT held by the contract', async function () { + expect(await grt.allowance(bridgeEscrow.address, spender.address)).eq(0) + const tx = grt + .connect(spender.signer) + .transferFrom(bridgeEscrow.address, tokenReceiver.address, nTokens) + expect(tx).to.be.revertedWith('ERC20: transfer amount exceeds allowance') + await bridgeEscrow.connect(governor.signer).approveAll(spender.address) + expect( + await grt + .connect(spender.signer) + .transferFrom(bridgeEscrow.address, tokenReceiver.address, nTokens), + ).to.emit(grt, 'Transfer') + expect(await grt.balanceOf(tokenReceiver.address)).to.eq(nTokens) + }) + }) + + describe('revokeAll', function () { + it('cannot be called by someone other than the governor', async function () { + const tx = bridgeEscrow.connect(tokenReceiver.signer).revokeAll(spender.address) + expect(tx).to.be.revertedWith('Caller must be Controller governor') + }) + it("revokes a spender's permission to transfer GRT held by the contract", async function () { + await bridgeEscrow.connect(governor.signer).approveAll(spender.address) + await bridgeEscrow.connect(governor.signer).revokeAll(spender.address) + // We shouldn't be able to transfer _anything_ + const tx = grt + .connect(spender.signer) + .transferFrom(bridgeEscrow.address, tokenReceiver.address, BigNumber.from('1')) + expect(tx).to.be.revertedWith('ERC20: transfer amount exceeds allowance') + }) + }) +}) diff --git a/test/gateway/l1GraphTokenGateway.test.ts b/test/gateway/l1GraphTokenGateway.test.ts new file mode 100644 index 000000000..f14204816 --- /dev/null +++ b/test/gateway/l1GraphTokenGateway.test.ts @@ -0,0 +1,640 @@ +import { expect } from 'chai' +import { constants, Signer, utils } from 'ethers' + +import { GraphToken } from '../../build/types/GraphToken' +import { BridgeMock } from '../../build/types/BridgeMock' +import { InboxMock } from '../../build/types/InboxMock' +import { OutboxMock } from '../../build/types/OutboxMock' +import { L1GraphTokenGateway } from '../../build/types/L1GraphTokenGateway' + +import { NetworkFixture, ArbitrumL1Mocks, L1FixtureContracts } from '../lib/fixtures' + +import { + getAccounts, + latestBlock, + toBN, + toGRT, + Account, + applyL1ToL2Alias, +} from '../lib/testHelpers' +import { BridgeEscrow } from '../../build/types/BridgeEscrow' + +const { AddressZero } = constants + +describe('L1GraphTokenGateway', () => { + let governor: Account + let tokenSender: Account + let l2Receiver: Account + let mockRouter: Account + let mockL2GRT: Account + let mockL2Gateway: Account + let pauseGuardian: Account + let mockL2Reservoir: Account + let fixture: NetworkFixture + + let grt: GraphToken + let l1GraphTokenGateway: L1GraphTokenGateway + let bridgeEscrow: BridgeEscrow + let bridgeMock: BridgeMock + let inboxMock: InboxMock + let outboxMock: OutboxMock + + let arbitrumMocks: ArbitrumL1Mocks + let fixtureContracts: L1FixtureContracts + + const senderTokens = toGRT('1000') + const maxGas = toBN('1000000') + const maxSubmissionCost = toBN('7') + const gasPriceBid = toBN('2') + const defaultEthValue = maxSubmissionCost.add(maxGas.mul(gasPriceBid)) + const emptyCallHookData = '0x' + const defaultData = utils.defaultAbiCoder.encode( + ['uint256', 'bytes'], + [maxSubmissionCost, emptyCallHookData], + ) + const notEmptyCallHookData = '0x12' + const defaultDataWithNotEmptyCallHookData = utils.defaultAbiCoder.encode( + ['uint256', 'bytes'], + [maxSubmissionCost, notEmptyCallHookData], + ) + + before(async function () { + ;[ + governor, + tokenSender, + l2Receiver, + mockRouter, + mockL2GRT, + mockL2Gateway, + pauseGuardian, + mockL2Reservoir, + ] = await getAccounts() + + fixture = new NetworkFixture() + fixtureContracts = await fixture.load(governor.signer) + ;({ grt, l1GraphTokenGateway, bridgeEscrow } = fixtureContracts) + + // Give some funds to the token sender + await grt.connect(governor.signer).mint(tokenSender.address, senderTokens) + // Deploy contracts that mock Arbitrum's bridge contracts + arbitrumMocks = await fixture.loadArbitrumL1Mocks(governor.signer) + ;({ bridgeMock, inboxMock, outboxMock } = arbitrumMocks) + }) + + beforeEach(async function () { + await fixture.setUp() + }) + + afterEach(async function () { + await fixture.tearDown() + }) + + context('> immediately after deploy', function () { + describe('calculateL2TokenAddress', function () { + it('should return address zero as it was not set', async function () { + expect(await l1GraphTokenGateway.calculateL2TokenAddress(grt.address)).eq(AddressZero) + }) + }) + + describe('outboundTransfer', function () { + it('reverts because it is paused', async function () { + const tx = l1GraphTokenGateway + .connect(tokenSender.signer) + .outboundTransfer( + grt.address, + l2Receiver.address, + toGRT('10'), + maxGas, + gasPriceBid, + defaultData, + { + value: defaultEthValue, + }, + ) + await expect(tx).revertedWith('Paused (contract)') + }) + }) + + describe('finalizeInboundTransfer', function () { + it('revert because it is paused', async function () { + const tx = l1GraphTokenGateway + .connect(tokenSender.signer) + .finalizeInboundTransfer( + grt.address, + l2Receiver.address, + tokenSender.address, + toGRT('10'), + defaultData, + ) + await expect(tx).revertedWith('Paused (contract)') + }) + }) + + describe('setArbitrumAddresses', function () { + it('is not callable by addreses that are not the governor', async function () { + const tx = l1GraphTokenGateway + .connect(tokenSender.signer) + .setArbitrumAddresses(inboxMock.address, mockRouter.address) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + it('sets inbox and router address', async function () { + const tx = l1GraphTokenGateway + .connect(governor.signer) + .setArbitrumAddresses(inboxMock.address, mockRouter.address) + await expect(tx) + .emit(l1GraphTokenGateway, 'ArbitrumAddressesSet') + .withArgs(inboxMock.address, mockRouter.address) + expect(await l1GraphTokenGateway.l1Router()).eq(mockRouter.address) + expect(await l1GraphTokenGateway.inbox()).eq(inboxMock.address) + }) + }) + + describe('setL2TokenAddress', function () { + it('is not callable by addreses that are not the governor', async function () { + const tx = l1GraphTokenGateway + .connect(tokenSender.signer) + .setL2TokenAddress(mockL2GRT.address) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + it('sets l2GRT', async function () { + const tx = l1GraphTokenGateway.connect(governor.signer).setL2TokenAddress(mockL2GRT.address) + await expect(tx).emit(l1GraphTokenGateway, 'L2TokenAddressSet').withArgs(mockL2GRT.address) + expect(await l1GraphTokenGateway.l2GRT()).eq(mockL2GRT.address) + }) + }) + + describe('setL2CounterpartAddress', function () { + it('is not callable by addreses that are not the governor', async function () { + const tx = l1GraphTokenGateway + .connect(tokenSender.signer) + .setL2CounterpartAddress(mockL2Gateway.address) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + it('sets L2Counterpart', async function () { + const tx = l1GraphTokenGateway + .connect(governor.signer) + .setL2CounterpartAddress(mockL2Gateway.address) + await expect(tx) + .emit(l1GraphTokenGateway, 'L2CounterpartAddressSet') + .withArgs(mockL2Gateway.address) + expect(await l1GraphTokenGateway.l2Counterpart()).eq(mockL2Gateway.address) + }) + }) + describe('setEscrowAddress', function () { + it('is not callable by addreses that are not the governor', async function () { + const tx = l1GraphTokenGateway + .connect(tokenSender.signer) + .setEscrowAddress(bridgeEscrow.address) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + it('sets escrow', async function () { + const tx = l1GraphTokenGateway + .connect(governor.signer) + .setEscrowAddress(bridgeEscrow.address) + await expect(tx) + .emit(l1GraphTokenGateway, 'EscrowAddressSet') + .withArgs(bridgeEscrow.address) + expect(await l1GraphTokenGateway.escrow()).eq(bridgeEscrow.address) + }) + }) + describe('addToCallhookWhitelist', function () { + it('is not callable by addreses that are not the governor', async function () { + const tx = l1GraphTokenGateway + .connect(tokenSender.signer) + .addToCallhookWhitelist(tokenSender.address) + await expect(tx).revertedWith('Caller must be Controller governor') + expect(await l1GraphTokenGateway.callhookWhitelist(tokenSender.address)).eq(false) + }) + it('adds an address to the callhook whitelist', async function () { + const tx = l1GraphTokenGateway + .connect(governor.signer) + .addToCallhookWhitelist(tokenSender.address) + await expect(tx) + .emit(l1GraphTokenGateway, 'AddedToCallhookWhitelist') + .withArgs(tokenSender.address) + expect(await l1GraphTokenGateway.callhookWhitelist(tokenSender.address)).eq(true) + }) + }) + describe('removeFromCallhookWhitelist', function () { + it('is not callable by addreses that are not the governor', async function () { + await l1GraphTokenGateway + .connect(governor.signer) + .addToCallhookWhitelist(tokenSender.address) + const tx = l1GraphTokenGateway + .connect(tokenSender.signer) + .removeFromCallhookWhitelist(tokenSender.address) + await expect(tx).revertedWith('Caller must be Controller governor') + expect(await l1GraphTokenGateway.callhookWhitelist(tokenSender.address)).eq(true) + }) + it('removes an address from the callhook whitelist', async function () { + await l1GraphTokenGateway + .connect(governor.signer) + .addToCallhookWhitelist(tokenSender.address) + const tx = l1GraphTokenGateway + .connect(governor.signer) + .removeFromCallhookWhitelist(tokenSender.address) + await expect(tx) + .emit(l1GraphTokenGateway, 'RemovedFromCallhookWhitelist') + .withArgs(tokenSender.address) + expect(await l1GraphTokenGateway.callhookWhitelist(tokenSender.address)).eq(false) + }) + }) + describe('Pausable behavior', () => { + it('cannot be paused or unpaused by someone other than governor or pauseGuardian', async () => { + let tx = l1GraphTokenGateway.connect(tokenSender.signer).setPaused(false) + await expect(tx).revertedWith('Only Governor or Guardian can call') + tx = l1GraphTokenGateway.connect(tokenSender.signer).setPaused(true) + await expect(tx).revertedWith('Only Governor or Guardian can call') + }) + it('can be paused and unpaused by the governor', async function () { + let tx = l1GraphTokenGateway.connect(governor.signer).setPaused(false) + await expect(tx).emit(l1GraphTokenGateway, 'PauseChanged').withArgs(false) + await expect(await l1GraphTokenGateway.paused()).eq(false) + tx = l1GraphTokenGateway.connect(governor.signer).setPaused(true) + await expect(tx).emit(l1GraphTokenGateway, 'PauseChanged').withArgs(true) + await expect(await l1GraphTokenGateway.paused()).eq(true) + }) + describe('setPauseGuardian', function () { + it('cannot be called by someone other than governor', async function () { + const tx = l1GraphTokenGateway + .connect(tokenSender.signer) + .setPauseGuardian(pauseGuardian.address) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + it('sets a new pause guardian', async function () { + const tx = l1GraphTokenGateway + .connect(governor.signer) + .setPauseGuardian(pauseGuardian.address) + await expect(tx) + .emit(l1GraphTokenGateway, 'NewPauseGuardian') + .withArgs(AddressZero, pauseGuardian.address) + }) + it('allows a pause guardian to pause and unpause', async function () { + await l1GraphTokenGateway.connect(governor.signer).setPauseGuardian(pauseGuardian.address) + let tx = l1GraphTokenGateway.connect(pauseGuardian.signer).setPaused(false) + await expect(tx).emit(l1GraphTokenGateway, 'PauseChanged').withArgs(false) + await expect(await l1GraphTokenGateway.paused()).eq(false) + tx = l1GraphTokenGateway.connect(pauseGuardian.signer).setPaused(true) + await expect(tx).emit(l1GraphTokenGateway, 'PauseChanged').withArgs(true) + await expect(await l1GraphTokenGateway.paused()).eq(true) + }) + }) + }) + }) + + context('> after configuring and unpausing', function () { + const createMsgData = function (callHookData: string) { + const selector = l1GraphTokenGateway.interface.getSighash('finalizeInboundTransfer') + const params = utils.defaultAbiCoder.encode( + ['address', 'address', 'address', 'uint256', 'bytes'], + [ + grt.address, + tokenSender.address, + l2Receiver.address, + toGRT('10'), + utils.defaultAbiCoder.encode(['bytes', 'bytes'], [emptyCallHookData, callHookData]), + ], + ) + const outboundData = utils.hexlify(utils.concat([selector, params])) + + const msgData = utils.solidityPack( + [ + 'uint256', + 'uint256', + 'uint256', + 'uint256', + 'uint256', + 'uint256', + 'uint256', + 'uint256', + 'uint256', + 'bytes', + ], + [ + toBN(mockL2Gateway.address), + toBN('0'), + defaultEthValue, + maxSubmissionCost, + applyL1ToL2Alias(tokenSender.address), + applyL1ToL2Alias(tokenSender.address), + maxGas, + gasPriceBid, + utils.hexDataLength(outboundData), + outboundData, + ], + ) + return msgData + } + const createInboxAccsEntry = function (msgDataHash: string) { + // The real bridge would emit the InboxAccs entry that came before this one, but our mock + // emits this, making it easier for us to validate here that all the parameters we sent are correct + const expectedInboxAccsEntry = utils.keccak256( + utils.solidityPack( + ['address', 'uint8', 'address', 'bytes32'], + [inboxMock.address, 9, l1GraphTokenGateway.address, msgDataHash], + ), + ) + return expectedInboxAccsEntry + } + const testValidOutboundTransfer = async function ( + signer: Signer, + data: string, + callHookData: string, + ) { + const tx = l1GraphTokenGateway + .connect(signer) + .outboundTransfer(grt.address, l2Receiver.address, toGRT('10'), maxGas, gasPriceBid, data, { + value: defaultEthValue, + }) + // Our bridge mock returns an incrementing seqNum starting at 1 + const expectedSeqNum = 1 + await expect(tx) + .emit(l1GraphTokenGateway, 'DepositInitiated') + .withArgs(grt.address, tokenSender.address, l2Receiver.address, expectedSeqNum, toGRT('10')) + + const msgData = createMsgData(callHookData) + const msgDataHash = utils.keccak256(msgData) + const expectedInboxAccsEntry = createInboxAccsEntry(msgDataHash) + + await expect(tx).emit(inboxMock, 'InboxMessageDelivered').withArgs(1, msgData) + await expect(tx) + .emit(bridgeMock, 'MessageDelivered') + .withArgs( + expectedSeqNum, + expectedInboxAccsEntry, + inboxMock.address, + 9, + l1GraphTokenGateway.address, + msgDataHash, + ) + const escrowBalance = await grt.balanceOf(bridgeEscrow.address) + const senderBalance = await grt.balanceOf(tokenSender.address) + await expect(escrowBalance).eq(toGRT('10')) + await expect(senderBalance).eq(toGRT('990')) + } + before(async function () { + await fixture.configureL1Bridge( + governor.signer, + arbitrumMocks, + fixtureContracts, + mockRouter.address, + mockL2GRT.address, + mockL2Gateway.address, + mockL2Reservoir.address, + ) + }) + + describe('calculateL2TokenAddress', function () { + it('returns the L2 token address', async function () { + expect(await l1GraphTokenGateway.calculateL2TokenAddress(grt.address)).eq(mockL2GRT.address) + }) + it('returns the zero address if the input is any other address', async function () { + expect(await l1GraphTokenGateway.calculateL2TokenAddress(tokenSender.address)).eq( + AddressZero, + ) + }) + }) + + describe('outboundTransfer', function () { + it('reverts when called with the wrong token address', async function () { + const tx = l1GraphTokenGateway + .connect(tokenSender.signer) + .outboundTransfer( + tokenSender.address, + l2Receiver.address, + toGRT('10'), + maxGas, + gasPriceBid, + defaultData, + { + value: defaultEthValue, + }, + ) + await expect(tx).revertedWith('TOKEN_NOT_GRT') + }) + it('puts tokens in escrow and creates a retryable ticket', async function () { + await grt.connect(tokenSender.signer).approve(l1GraphTokenGateway.address, toGRT('10')) + await testValidOutboundTransfer(tokenSender.signer, defaultData, emptyCallHookData) + }) + it('decodes the sender address from messages sent by the router', async function () { + await grt.connect(tokenSender.signer).approve(l1GraphTokenGateway.address, toGRT('10')) + const routerEncodedData = utils.defaultAbiCoder.encode( + ['address', 'bytes'], + [tokenSender.address, defaultData], + ) + await testValidOutboundTransfer(mockRouter.signer, routerEncodedData, emptyCallHookData) + }) + it('reverts when called with the wrong value', async function () { + await grt.connect(tokenSender.signer).approve(l1GraphTokenGateway.address, toGRT('10')) + const tx = l1GraphTokenGateway + .connect(tokenSender.signer) + .outboundTransfer( + grt.address, + l2Receiver.address, + toGRT('10'), + maxGas, + gasPriceBid, + defaultData, + { + value: defaultEthValue.sub(1), + }, + ) + await expect(tx).revertedWith('WRONG_ETH_VALUE') + }) + it('reverts when called with nonempty calldata, if the sender is not whitelisted', async function () { + await grt.connect(tokenSender.signer).approve(l1GraphTokenGateway.address, toGRT('10')) + const tx = l1GraphTokenGateway + .connect(tokenSender.signer) + .outboundTransfer( + grt.address, + l2Receiver.address, + toGRT('10'), + maxGas, + gasPriceBid, + defaultDataWithNotEmptyCallHookData, + { + value: defaultEthValue, + }, + ) + await expect(tx).revertedWith('CALL_HOOK_DATA_NOT_ALLOWED') + }) + it('allows sending nonempty calldata, if the sender is whitelisted', async function () { + await l1GraphTokenGateway + .connect(governor.signer) + .addToCallhookWhitelist(tokenSender.address) + await grt.connect(tokenSender.signer).approve(l1GraphTokenGateway.address, toGRT('10')) + await testValidOutboundTransfer( + tokenSender.signer, + defaultDataWithNotEmptyCallHookData, + notEmptyCallHookData, + ) + }) + it('reverts when the sender does not have enough GRT', async function () { + await grt.connect(tokenSender.signer).approve(l1GraphTokenGateway.address, toGRT('1001')) + const tx = l1GraphTokenGateway + .connect(tokenSender.signer) + .outboundTransfer( + grt.address, + l2Receiver.address, + toGRT('1001'), + maxGas, + gasPriceBid, + defaultData, + { + value: defaultEthValue, + }, + ) + await expect(tx).revertedWith('ERC20: transfer amount exceeds balance') + }) + }) + + describe('finalizeInboundTransfer', function () { + it('reverts when called by an account that is not the bridge', async function () { + const tx = l1GraphTokenGateway + .connect(tokenSender.signer) + .finalizeInboundTransfer( + grt.address, + l2Receiver.address, + tokenSender.address, + toGRT('10'), + defaultData, + ) + await expect(tx).revertedWith('NOT_FROM_BRIDGE') + }) + it('reverts when called by the bridge, but the tx was not started by the L2 gateway', async function () { + const encodedCalldata = l1GraphTokenGateway.interface.encodeFunctionData( + 'finalizeInboundTransfer', + [ + grt.address, + l2Receiver.address, + tokenSender.address, + toGRT('10'), + utils.defaultAbiCoder.encode(['uint256', 'bytes'], [0, []]), + ], + ) + // The real outbox would require a proof, which would + // validate that the tx was initiated by the L2 gateway but our mock + // just executes unconditionally + const tx = outboxMock.connect(tokenSender.signer).executeTransaction( + toBN('0'), + [], + toBN('0'), + l2Receiver.address, // Note this is not mockL2Gateway + l1GraphTokenGateway.address, + toBN('1337'), + await latestBlock(), + toBN('133701337'), + toBN('0'), + encodedCalldata, + ) + await expect(tx).revertedWith('ONLY_COUNTERPART_GATEWAY') + }) + it('reverts if the gateway does not have tokens', async function () { + // This scenario should never really happen, but we still + // test that the gateway reverts in this case + const encodedCalldata = l1GraphTokenGateway.interface.encodeFunctionData( + 'finalizeInboundTransfer', + [ + grt.address, + l2Receiver.address, + tokenSender.address, + toGRT('10'), + utils.defaultAbiCoder.encode(['uint256', 'bytes'], [0, []]), + ], + ) + // The real outbox would require a proof, which would + // validate that the tx was initiated by the L2 gateway but our mock + // just executes unconditionally + const tx = outboxMock + .connect(tokenSender.signer) + .executeTransaction( + toBN('0'), + [], + toBN('0'), + mockL2Gateway.address, + l1GraphTokenGateway.address, + toBN('1337'), + await latestBlock(), + toBN('133701337'), + toBN('0'), + encodedCalldata, + ) + await expect(tx).revertedWith('BRIDGE_OUT_OF_FUNDS') + }) + it('reverts if the gateway is revoked from escrow', async function () { + await grt.connect(tokenSender.signer).approve(l1GraphTokenGateway.address, toGRT('10')) + await testValidOutboundTransfer(tokenSender.signer, defaultData, emptyCallHookData) + // At this point, the gateway holds 10 GRT in escrow + // But we revoke the gateway's permission to move the funds: + await bridgeEscrow.connect(governor.signer).revokeAll(l1GraphTokenGateway.address) + const encodedCalldata = l1GraphTokenGateway.interface.encodeFunctionData( + 'finalizeInboundTransfer', + [ + grt.address, + l2Receiver.address, + tokenSender.address, + toGRT('8'), + utils.defaultAbiCoder.encode(['uint256', 'bytes'], [0, []]), + ], + ) + // The real outbox would require a proof, which would + // validate that the tx was initiated by the L2 gateway but our mock + // just executes unconditionally + const tx = outboxMock + .connect(tokenSender.signer) + .executeTransaction( + toBN('0'), + [], + toBN('0'), + mockL2Gateway.address, + l1GraphTokenGateway.address, + toBN('1337'), + await latestBlock(), + toBN('133701337'), + toBN('0'), + encodedCalldata, + ) + await expect(tx).revertedWith('ERC20: transfer amount exceeds allowance') + }) + it('sends tokens out of escrow', async function () { + await grt.connect(tokenSender.signer).approve(l1GraphTokenGateway.address, toGRT('10')) + await testValidOutboundTransfer(tokenSender.signer, defaultData, emptyCallHookData) + // At this point, the gateway holds 10 GRT in escrow + const encodedCalldata = l1GraphTokenGateway.interface.encodeFunctionData( + 'finalizeInboundTransfer', + [ + grt.address, + l2Receiver.address, + tokenSender.address, + toGRT('8'), + utils.defaultAbiCoder.encode(['uint256', 'bytes'], [0, []]), + ], + ) + // The real outbox would require a proof, which would + // validate that the tx was initiated by the L2 gateway but our mock + // just executes unconditionally + const tx = outboxMock + .connect(tokenSender.signer) + .executeTransaction( + toBN('0'), + [], + toBN('0'), + mockL2Gateway.address, + l1GraphTokenGateway.address, + toBN('1337'), + await latestBlock(), + toBN('133701337'), + toBN('0'), + encodedCalldata, + ) + await expect(tx) + .emit(l1GraphTokenGateway, 'WithdrawalFinalized') + .withArgs(grt.address, l2Receiver.address, tokenSender.address, toBN('0'), toGRT('8')) + const escrowBalance = await grt.balanceOf(bridgeEscrow.address) + const senderBalance = await grt.balanceOf(tokenSender.address) + await expect(escrowBalance).eq(toGRT('2')) + await expect(senderBalance).eq(toGRT('998')) + }) + }) + }) +}) diff --git a/test/graphToken.test.ts b/test/graphToken.test.ts index 5f53e6866..b42274ac4 100644 --- a/test/graphToken.test.ts +++ b/test/graphToken.test.ts @@ -1,275 +1,5 @@ -import { expect } from 'chai' -import { constants, utils, BytesLike, BigNumber, Signature } from 'ethers' -import { eip712 } from '@graphprotocol/common-ts/dist/attestations' - -import { GraphToken } from '../build/types/GraphToken' - -import * as deployment from './lib/deployment' -import { getAccounts, getChainID, toBN, toGRT, Account } from './lib/testHelpers' - -const { AddressZero, MaxUint256 } = constants -const { keccak256, SigningKey } = utils - -const PERMIT_TYPE_HASH = eip712.typeHash( - 'Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)', -) -const SALT = '0x51f3d585afe6dfeb2af01bba0889a36c1db03beec88c6a4d0c53817069026afa' - -interface Permit { - owner: string - spender: string - value: BigNumber - nonce: BigNumber - deadline: BigNumber -} - -function hashEncodePermit(permit: Permit) { - return eip712.hashStruct( - PERMIT_TYPE_HASH, - ['address', 'address', 'uint256', 'uint256', 'uint256'], - [permit.owner, permit.spender, permit.value, permit.nonce, permit.deadline], - ) -} - -function signPermit( - signer: BytesLike, - chainId: number, - contractAddress: string, - permit: Permit, -): Signature { - const domainSeparator = eip712.domainSeparator({ - name: 'Graph Token', - version: '0', - chainId, - verifyingContract: contractAddress, - salt: SALT, - }) - const hashEncodedPermit = hashEncodePermit(permit) - const message = eip712.encode(domainSeparator, hashEncodedPermit) - const messageHash = keccak256(message) - const signingKey = new SigningKey(signer) - return signingKey.signDigest(messageHash) -} +import { grtTests } from './lib/graphTokenTests' describe('GraphToken', () => { - let me: Account - let other: Account - let governor: Account - - const mePrivateKey = '0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d' - const otherPrivateKey = '0x6cbed15c793ce57650b9877cf6fa156fbef513c4e6134f022a85b1ffdd59b2a1' - - let grt: GraphToken - - async function permitMaxOK(): Promise { - return permitOK(MaxUint256) - } - - async function permitOK(value: BigNumber): Promise { - const nonce = await grt.nonces(me.address) - return { - owner: me.address, - spender: other.address, - value: value, - nonce: nonce, - deadline: toBN('0'), - } - } - - async function permitExpired(): Promise { - const permit = await permitMaxOK() - permit.deadline = toBN('1') - return permit - } - - async function permitDeny(): Promise { - const permit = await permitMaxOK() - permit.value = toBN('0') - return permit - } - - async function createPermitTransaction(permit: Permit, signer: string) { - const chainID = await getChainID() - const signature: Signature = signPermit(signer, chainID, grt.address, permit) - return grt.permit( - permit.owner, - permit.spender, - permit.value, - permit.deadline, - signature.v, - signature.r, - signature.s, - ) - } - - before(async function () { - ;[me, other, governor] = await getAccounts() - }) - - beforeEach(async function () { - // Deploy graph token - grt = await deployment.deployGRT(governor.signer) - - // Mint some tokens - const tokens = toGRT('10000') - await grt.connect(governor.signer).mint(me.address, tokens) - }) - - describe('permit', function () { - it('should permit max token allowance', async function () { - // Allow to transfer tokens - const tokensToApprove = toGRT('1000') - const permit = await permitOK(tokensToApprove) - const tx = createPermitTransaction(permit, mePrivateKey) - await expect(tx).emit(grt, 'Approval').withArgs(permit.owner, permit.spender, tokensToApprove) - - // Allowance updated - const allowance = await grt.allowance(me.address, other.address) - expect(allowance).eq(tokensToApprove) - - // Transfer tokens should work - const tokens = toGRT('100') - await grt.connect(other.signer).transferFrom(me.address, other.address, tokens) - }) - - it('should permit max token allowance', async function () { - // Allow to transfer tokens - const permit = await permitMaxOK() - const tx = createPermitTransaction(permit, mePrivateKey) - await expect(tx).emit(grt, 'Approval').withArgs(permit.owner, permit.spender, MaxUint256) - - // Allowance updated - const allowance = await grt.allowance(me.address, other.address) - expect(allowance).eq(MaxUint256) - - // Transfer tokens should work - const tokens = toGRT('100') - await grt.connect(other.signer).transferFrom(me.address, other.address, tokens) - }) - - it('reject to transfer more tokens than approved by permit', async function () { - // Allow to transfer tokens - const tokensToApprove = toGRT('1000') - const permit = await permitOK(tokensToApprove) - await createPermitTransaction(permit, mePrivateKey) - - // Should not transfer more than approved - const tooManyTokens = toGRT('1001') - const tx = grt.connect(other.signer).transferFrom(me.address, other.address, tooManyTokens) - await expect(tx).revertedWith('ERC20: transfer amount exceeds allowance') - - // Should transfer up to the approved amount - await grt.connect(other.signer).transferFrom(me.address, other.address, tokensToApprove) - }) - - it('reject use two permits with same nonce', async function () { - // Allow to transfer tokens - const permit = await permitMaxOK() - await createPermitTransaction(permit, mePrivateKey) - - // Try to re-use the permit - const tx = createPermitTransaction(permit, mePrivateKey) - await expect(tx).revertedWith('GRT: invalid permit') - }) - - it('reject use expired permit', async function () { - const permit = await permitExpired() - const tx = createPermitTransaction(permit, mePrivateKey) - await expect(tx).revertedWith('GRT: expired permit') - }) - - it('reject permit if holder address does not match', async function () { - const permit = await permitMaxOK() - const tx = createPermitTransaction(permit, otherPrivateKey) - await expect(tx).revertedWith('GRT: invalid permit') - }) - - it('should deny transfer from if permit was denied', async function () { - // Allow to transfer tokens - const permit1 = await permitMaxOK() - await createPermitTransaction(permit1, mePrivateKey) - - // Deny transfer tokens - const permit2 = await permitDeny() - await createPermitTransaction(permit2, mePrivateKey) - - // Allowance updated - const allowance = await grt.allowance(me.address, other.address) - expect(allowance).eq(toBN('0')) - - // Try to transfer without permit should fail - const tokens = toGRT('100') - const tx = grt.connect(other.signer).transferFrom(me.address, other.address, tokens) - await expect(tx).revertedWith('ERC20: transfer amount exceeds allowance') - }) - }) - - describe('mint', function () { - describe('addMinter', function () { - it('reject add a new minter if not allowed', async function () { - const tx = grt.connect(me.signer).addMinter(me.address) - await expect(tx).revertedWith('Only Governor can call') - }) - - it('should add a new minter', async function () { - expect(await grt.isMinter(me.address)).eq(false) - const tx = grt.connect(governor.signer).addMinter(me.address) - await expect(tx).emit(grt, 'MinterAdded').withArgs(me.address) - expect(await grt.isMinter(me.address)).eq(true) - }) - }) - - describe('mint', async function () { - it('reject mint if not minter', async function () { - const tx = grt.connect(me.signer).mint(me.address, toGRT('100')) - await expect(tx).revertedWith('Only minter can call') - }) - }) - - context('> when is minter', function () { - beforeEach(async function () { - await grt.connect(governor.signer).addMinter(me.address) - expect(await grt.isMinter(me.address)).eq(true) - }) - - describe('mint', async function () { - it('should mint', async function () { - const beforeTokens = await grt.balanceOf(me.address) - - const tokensToMint = toGRT('100') - const tx = grt.connect(me.signer).mint(me.address, tokensToMint) - await expect(tx).emit(grt, 'Transfer').withArgs(AddressZero, me.address, tokensToMint) - - const afterTokens = await grt.balanceOf(me.address) - expect(afterTokens).eq(beforeTokens.add(tokensToMint)) - }) - - it('should mint if governor', async function () { - const tokensToMint = toGRT('100') - await grt.connect(governor.signer).mint(me.address, tokensToMint) - }) - }) - - describe('removeMinter', function () { - it('reject remove a minter if not allowed', async function () { - const tx = grt.connect(me.signer).removeMinter(me.address) - await expect(tx).revertedWith('Only Governor can call') - }) - - it('should remove a minter', async function () { - const tx = grt.connect(governor.signer).removeMinter(me.address) - await expect(tx).emit(grt, 'MinterRemoved').withArgs(me.address) - expect(await grt.isMinter(me.address)).eq(false) - }) - }) - - describe('renounceMinter', function () { - it('should renounce to be a minter', async function () { - const tx = grt.connect(me.signer).renounceMinter() - await expect(tx).emit(grt, 'MinterRemoved').withArgs(me.address) - expect(await grt.isMinter(me.address)).eq(false) - }) - }) - }) - }) + grtTests.bind(this)(false) }) diff --git a/test/l2/l2GraphToken.test.ts b/test/l2/l2GraphToken.test.ts new file mode 100644 index 000000000..56d8e5ed6 --- /dev/null +++ b/test/l2/l2GraphToken.test.ts @@ -0,0 +1,107 @@ +import { expect } from 'chai' + +import { getAccounts, toGRT, Account, initNetwork } from '../lib/testHelpers' + +import { L2GraphToken } from '../../build/types/L2GraphToken' + +import { grtTests } from '../lib/graphTokenTests' +import { NetworkFixture } from '../lib/fixtures' + +describe('L2GraphToken', () => { + describe('Base GRT behavior', () => { + grtTests.bind(this)(true) + }) + describe('Extended L2 behavior', () => { + let mockL2Gateway: Account + let mockL1GRT: Account + let governor: Account + let user: Account + + let fixture: NetworkFixture + let grt: L2GraphToken + + before(async function () { + await initNetwork() + ;[mockL1GRT, mockL2Gateway, governor, user] = await getAccounts() + fixture = new NetworkFixture() + ;({ grt } = await fixture.loadL2(governor.signer)) + }) + + beforeEach(async function () { + await fixture.setUp() + }) + + afterEach(async function () { + await fixture.tearDown() + }) + + describe('setGateway', async function () { + it('cannot be called by someone who is not the governor', async function () { + const tx = grt.connect(mockL2Gateway.signer).setGateway(mockL2Gateway.address) + await expect(tx).revertedWith('Only Governor can call') + }) + it('sets the L2 Gateway address when called by the governor', async function () { + const tx = grt.connect(governor.signer).setGateway(mockL2Gateway.address) + await expect(tx).emit(grt, 'GatewaySet').withArgs(mockL2Gateway.address) + await expect(await grt.gateway()).eq(mockL2Gateway.address) + }) + }) + describe('setL1Address', async function () { + it('cannot be called by someone who is not the governor', async function () { + const tx = grt.connect(mockL2Gateway.signer).setL1Address(mockL1GRT.address) + await expect(tx).revertedWith('Only Governor can call') + }) + it('sets the L1 GRT address when called by the governor', async function () { + const tx = grt.connect(governor.signer).setL1Address(mockL1GRT.address) + await expect(tx).emit(grt, 'L1AddressSet').withArgs(mockL1GRT.address) + await expect(await grt.l1Address()).eq(mockL1GRT.address) + }) + }) + describe('bridge minting and burning', async function () { + beforeEach(async function () { + // Configure the l1Address and gateway + await grt.connect(governor.signer).setL1Address(mockL1GRT.address) + await grt.connect(governor.signer).setGateway(mockL2Gateway.address) + }) + describe('bridgeMint', async function () { + it('cannot be called by someone who is not the gateway', async function () { + const tx = grt.connect(governor.signer).bridgeMint(user.address, toGRT('100')) + await expect(tx).revertedWith('NOT_GATEWAY') + }) + it('mints GRT into a destination account', async function () { + const tx = grt.connect(mockL2Gateway.signer).bridgeMint(user.address, toGRT('100')) + await expect(tx).emit(grt, 'BridgeMinted').withArgs(user.address, toGRT('100')) + await expect(await grt.balanceOf(user.address)).eq(toGRT('100')) + }) + }) + describe('bridgeBurn', async function () { + it('cannot be called by someone who is not the gateway', async function () { + const tx = grt.connect(governor.signer).bridgeBurn(user.address, toGRT('100')) + await expect(tx).revertedWith('NOT_GATEWAY') + }) + it('requires approval for burning', async function () { + await grt.connect(mockL2Gateway.signer).bridgeMint(user.address, toGRT('100')) + const tx = grt.connect(mockL2Gateway.signer).bridgeBurn(user.address, toGRT('20')) + await expect(tx).revertedWith('ERC20: burn amount exceeds allowance') + }) + it('fails if the user does not have enough funds', async function () { + await grt.connect(mockL2Gateway.signer).bridgeMint(user.address, toGRT('10')) + await grt.connect(user.signer).approve(mockL2Gateway.address, toGRT('20')) + const tx = grt.connect(mockL2Gateway.signer).bridgeBurn(user.address, toGRT('20')) + await expect(tx).revertedWith('ERC20: burn amount exceeds balance') + }) + it('burns GRT from an account when approved', async function () { + await grt.connect(mockL2Gateway.signer).bridgeMint(user.address, toGRT('100')) + await grt.connect(user.signer).approve(mockL2Gateway.address, toGRT('20')) + const tx = grt.connect(mockL2Gateway.signer).bridgeBurn(user.address, toGRT('20')) + await expect(tx).emit(grt, 'BridgeBurned').withArgs(user.address, toGRT('20')) + await expect(await grt.balanceOf(user.address)).eq(toGRT('80')) + }) + }) + it('does not allow the bridge to mint as a regular minter', async function () { + const tx = grt.connect(mockL2Gateway.signer).mint(user.address, toGRT('100')) + await expect(tx).revertedWith('Only minter can call') + }) + }) + }) +}) diff --git a/test/l2/l2GraphTokenGateway.test.ts b/test/l2/l2GraphTokenGateway.test.ts new file mode 100644 index 000000000..d95c92871 --- /dev/null +++ b/test/l2/l2GraphTokenGateway.test.ts @@ -0,0 +1,412 @@ +import { expect, use } from 'chai' +import { constants, ContractTransaction, Signer, utils } from 'ethers' + +import { L2GraphToken } from '../../build/types/L2GraphToken' +import { L2GraphTokenGateway } from '../../build/types/L2GraphTokenGateway' + +import { L2FixtureContracts, NetworkFixture } from '../lib/fixtures' + +import { FakeContract, smock } from '@defi-wonderland/smock' + +import path from 'path' +import { Artifacts } from 'hardhat/internal/artifacts' +const ARTIFACTS_PATH = path.resolve('build/contracts') +const artifacts = new Artifacts(ARTIFACTS_PATH) +const reservoirMockAbi = artifacts.readArtifactSync('ReservoirMock').abi + +use(smock.matchers) + +import { getAccounts, toGRT, Account, toBN, getL2SignerFromL1 } from '../lib/testHelpers' +import { Interface } from 'ethers/lib/utils' + +const { AddressZero } = constants + +describe('L2GraphTokenGateway', () => { + let me: Account + let governor: Account + let tokenSender: Account + let l1Receiver: Account + let l2Receiver: Account + let mockRouter: Account + let mockL1GRT: Account + let mockL1Gateway: Account + let pauseGuardian: Account + let mockL1Reservoir: Account + let fixture: NetworkFixture + let arbSysMock: FakeContract + + let fixtureContracts: L2FixtureContracts + let grt: L2GraphToken + let l2GraphTokenGateway: L2GraphTokenGateway + + const senderTokens = toGRT('1000') + const defaultData = '0x' + const mockIface = new Interface(reservoirMockAbi) + const notEmptyCallHookData = mockIface.encodeFunctionData('pow', [toBN(1), toBN(2), toBN(3)]) + const defaultDataWithNotEmptyCallHookData = utils.defaultAbiCoder.encode( + ['bytes', 'bytes'], + ['0x', notEmptyCallHookData], + ) + + before(async function () { + ;[ + me, + governor, + tokenSender, + l1Receiver, + mockRouter, + mockL1GRT, + mockL1Gateway, + l2Receiver, + pauseGuardian, + mockL1Reservoir, + ] = await getAccounts() + + fixture = new NetworkFixture() + fixtureContracts = await fixture.loadL2(governor.signer) + ;({ grt, l2GraphTokenGateway } = fixtureContracts) + + // Give some funds to the token sender + await grt.connect(governor.signer).mint(tokenSender.address, senderTokens) + }) + + beforeEach(async function () { + await fixture.setUp() + // Thanks to Livepeer: https://github.com/livepeer/arbitrum-lpt-bridge/blob/main/test/unit/L2/l2LPTGateway.test.ts#L86 + arbSysMock = await smock.fake('ArbSys', { + address: '0x0000000000000000000000000000000000000064', + }) + arbSysMock.sendTxToL1.returns(1) + }) + + afterEach(async function () { + await fixture.tearDown() + }) + + context('> immediately after deploy', function () { + describe('calculateL2TokenAddress', function () { + it('should return the zero address', async function () { + expect(await l2GraphTokenGateway.calculateL2TokenAddress(grt.address)).eq(AddressZero) + }) + }) + + describe('outboundTransfer', function () { + it('reverts because it is paused', async function () { + const tx = l2GraphTokenGateway + .connect(tokenSender.signer) + ['outboundTransfer(address,address,uint256,bytes)']( + grt.address, + l1Receiver.address, + toGRT('10'), + defaultData, + ) + await expect(tx).revertedWith('Paused (contract)') + }) + }) + + describe('finalizeInboundTransfer', function () { + it('revert because it is paused', async function () { + const tx = l2GraphTokenGateway + .connect(tokenSender.signer) + .finalizeInboundTransfer( + grt.address, + tokenSender.address, + l1Receiver.address, + toGRT('10'), + defaultData, + ) + await expect(tx).revertedWith('Paused (contract)') + }) + }) + + describe('setL2Router', function () { + it('is not callable by addreses that are not the governor', async function () { + const tx = l2GraphTokenGateway.connect(tokenSender.signer).setL2Router(mockRouter.address) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + it('sets router address', async function () { + const tx = l2GraphTokenGateway.connect(governor.signer).setL2Router(mockRouter.address) + await expect(tx).emit(l2GraphTokenGateway, 'L2RouterSet').withArgs(mockRouter.address) + expect(await l2GraphTokenGateway.l2Router()).eq(mockRouter.address) + }) + }) + + describe('setL1TokenAddress', function () { + it('is not callable by addreses that are not the governor', async function () { + const tx = l2GraphTokenGateway + .connect(tokenSender.signer) + .setL1TokenAddress(mockL1GRT.address) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + it('sets l2GRT', async function () { + const tx = l2GraphTokenGateway.connect(governor.signer).setL1TokenAddress(mockL1GRT.address) + await expect(tx).emit(l2GraphTokenGateway, 'L1TokenAddressSet').withArgs(mockL1GRT.address) + expect(await l2GraphTokenGateway.l1GRT()).eq(mockL1GRT.address) + }) + }) + + describe('setL1CounterpartAddress', function () { + it('is not callable by addreses that are not the governor', async function () { + const tx = l2GraphTokenGateway + .connect(tokenSender.signer) + .setL1CounterpartAddress(mockL1Gateway.address) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + it('sets L1Counterpart', async function () { + const tx = l2GraphTokenGateway + .connect(governor.signer) + .setL1CounterpartAddress(mockL1Gateway.address) + await expect(tx) + .emit(l2GraphTokenGateway, 'L1CounterpartAddressSet') + .withArgs(mockL1Gateway.address) + expect(await l2GraphTokenGateway.l1Counterpart()).eq(mockL1Gateway.address) + }) + }) + describe('Pausable behavior', () => { + it('cannot be paused or unpaused by someone other than governor or pauseGuardian', async () => { + let tx = l2GraphTokenGateway.connect(tokenSender.signer).setPaused(false) + await expect(tx).revertedWith('Only Governor or Guardian can call') + tx = l2GraphTokenGateway.connect(tokenSender.signer).setPaused(true) + await expect(tx).revertedWith('Only Governor or Guardian can call') + }) + it('can be paused and unpaused by the governor', async function () { + let tx = l2GraphTokenGateway.connect(governor.signer).setPaused(false) + await expect(tx).emit(l2GraphTokenGateway, 'PauseChanged').withArgs(false) + await expect(await l2GraphTokenGateway.paused()).eq(false) + tx = l2GraphTokenGateway.connect(governor.signer).setPaused(true) + await expect(tx).emit(l2GraphTokenGateway, 'PauseChanged').withArgs(true) + await expect(await l2GraphTokenGateway.paused()).eq(true) + }) + describe('setPauseGuardian', function () { + it('cannot be called by someone other than governor', async function () { + const tx = l2GraphTokenGateway + .connect(tokenSender.signer) + .setPauseGuardian(pauseGuardian.address) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + it('sets a new pause guardian', async function () { + const tx = l2GraphTokenGateway + .connect(governor.signer) + .setPauseGuardian(pauseGuardian.address) + await expect(tx) + .emit(l2GraphTokenGateway, 'NewPauseGuardian') + .withArgs(AddressZero, pauseGuardian.address) + }) + it('allows a pause guardian to pause and unpause', async function () { + await l2GraphTokenGateway.connect(governor.signer).setPauseGuardian(pauseGuardian.address) + let tx = l2GraphTokenGateway.connect(pauseGuardian.signer).setPaused(false) + await expect(tx).emit(l2GraphTokenGateway, 'PauseChanged').withArgs(false) + await expect(await l2GraphTokenGateway.paused()).eq(false) + tx = l2GraphTokenGateway.connect(pauseGuardian.signer).setPaused(true) + await expect(tx).emit(l2GraphTokenGateway, 'PauseChanged').withArgs(true) + await expect(await l2GraphTokenGateway.paused()).eq(true) + }) + }) + }) + }) + + context('> after configuring and unpausing', function () { + const testValidOutboundTransfer = async function (signer: Signer, data: string) { + const tx = l2GraphTokenGateway + .connect(signer) + ['outboundTransfer(address,address,uint256,bytes)']( + mockL1GRT.address, + l1Receiver.address, + toGRT('10'), + data, + ) + const expectedId = 1 + await expect(tx) + .emit(l2GraphTokenGateway, 'WithdrawalInitiated') + .withArgs( + mockL1GRT.address, + tokenSender.address, + l1Receiver.address, + expectedId, + 0, + toGRT('10'), + ) + + // Should use the L1 Gateway's interface, but both come from ITokenGateway + const calldata = l2GraphTokenGateway.interface.encodeFunctionData('finalizeInboundTransfer', [ + mockL1GRT.address, + tokenSender.address, + l1Receiver.address, + toGRT('10'), + utils.defaultAbiCoder.encode(['uint256', 'bytes'], [0, []]), + ]) + await expect(tx) + .emit(l2GraphTokenGateway, 'TxToL1') + .withArgs(tokenSender.address, mockL1Gateway.address, 1, calldata) + + // For some reason the call count doesn't work properly, + // and each function call is counted 12 times. + // Possibly related to https://github.com/defi-wonderland/smock/issues/85 ? + //expect(arbSysMock.sendTxToL1).to.have.been.calledOnce + expect(arbSysMock.sendTxToL1).to.have.been.calledWith(mockL1Gateway.address, calldata) + const senderBalance = await grt.balanceOf(tokenSender.address) + await expect(senderBalance).eq(toGRT('990')) + } + before(async function () { + await fixture.configureL2Bridge( + governor.signer, + fixtureContracts, + mockRouter.address, + mockL1GRT.address, + mockL1Gateway.address, + mockL1Reservoir.address, + ) + }) + + describe('calculateL2TokenAddress', function () { + it('returns the L2 token address', async function () { + expect(await l2GraphTokenGateway.calculateL2TokenAddress(mockL1GRT.address)).eq(grt.address) + }) + it('returns the zero address if the input is any other address', async function () { + expect(await l2GraphTokenGateway.calculateL2TokenAddress(tokenSender.address)).eq( + AddressZero, + ) + }) + }) + + describe('outboundTransfer', function () { + it('reverts when called with the wrong token address', async function () { + const tx = l2GraphTokenGateway + .connect(tokenSender.signer) + ['outboundTransfer(address,address,uint256,bytes)']( + tokenSender.address, + l1Receiver.address, + toGRT('10'), + defaultData, + ) + await expect(tx).revertedWith('TOKEN_NOT_GRT') + }) + it('burns tokens and triggers an L1 call', async function () { + await grt.connect(tokenSender.signer).approve(l2GraphTokenGateway.address, toGRT('10')) + await testValidOutboundTransfer(tokenSender.signer, defaultData) + }) + it('decodes the sender address from messages sent by the router', async function () { + await grt.connect(tokenSender.signer).approve(l2GraphTokenGateway.address, toGRT('10')) + const routerEncodedData = utils.defaultAbiCoder.encode( + ['address', 'bytes'], + [tokenSender.address, defaultData], + ) + await testValidOutboundTransfer(mockRouter.signer, routerEncodedData) + }) + it('reverts when called with nonempty calldata', async function () { + await grt.connect(tokenSender.signer).approve(l2GraphTokenGateway.address, toGRT('10')) + const tx = l2GraphTokenGateway + .connect(tokenSender.signer) + ['outboundTransfer(address,address,uint256,bytes)']( + mockL1GRT.address, + l1Receiver.address, + toGRT('10'), + defaultDataWithNotEmptyCallHookData, + ) + await expect(tx).revertedWith('CALL_HOOK_DATA_NOT_ALLOWED') + }) + it('reverts when the sender does not have enough GRT', async function () { + await grt.connect(tokenSender.signer).approve(l2GraphTokenGateway.address, toGRT('1001')) + const tx = l2GraphTokenGateway + .connect(tokenSender.signer) + ['outboundTransfer(address,address,uint256,bytes)']( + mockL1GRT.address, + l1Receiver.address, + toGRT('1001'), + defaultData, + ) + await expect(tx).revertedWith('ERC20: burn amount exceeds balance') + }) + }) + + describe('finalizeInboundTransfer', function () { + const testValidFinalizeTransfer = async function ( + data: string, + ): Promise { + const mockL1GatewayL2Alias = await getL2SignerFromL1(mockL1Gateway.address) + await me.signer.sendTransaction({ + to: await mockL1GatewayL2Alias.getAddress(), + value: utils.parseUnits('1', 'ether'), + }) + const tx = l2GraphTokenGateway + .connect(mockL1GatewayL2Alias) + .finalizeInboundTransfer( + mockL1GRT.address, + tokenSender.address, + l2Receiver.address, + toGRT('10'), + data, + ) + await expect(tx) + .emit(l2GraphTokenGateway, 'DepositFinalized') + .withArgs(mockL1GRT.address, tokenSender.address, l2Receiver.address, toGRT('10')) + + await expect(tx).emit(grt, 'BridgeMinted').withArgs(l2Receiver.address, toGRT('10')) + + // Unchanged + const senderBalance = await grt.balanceOf(tokenSender.address) + await expect(senderBalance).eq(toGRT('1000')) + // 10 newly minted GRT + const receiverBalance = await grt.balanceOf(l2Receiver.address) + await expect(receiverBalance).eq(toGRT('10')) + return tx + } + it('reverts when called by an account that is not the gateway', async function () { + const tx = l2GraphTokenGateway + .connect(tokenSender.signer) + .finalizeInboundTransfer( + mockL1GRT.address, + tokenSender.address, + l2Receiver.address, + toGRT('10'), + defaultData, + ) + await expect(tx).revertedWith('ONLY_COUNTERPART_GATEWAY') + }) + it('reverts when called by an account that is the gateway but without the L2 alias', async function () { + const tx = l2GraphTokenGateway + .connect(mockL1Gateway.signer) + .finalizeInboundTransfer( + mockL1GRT.address, + tokenSender.address, + l2Receiver.address, + toGRT('10'), + defaultData, + ) + await expect(tx).revertedWith('ONLY_COUNTERPART_GATEWAY') + }) + it('mints and sends tokens when called by the aliased gateway', async function () { + await testValidFinalizeTransfer(defaultData) + }) + it('calls a callhook if the sender is whitelisted', async function () { + const reservoirMock = await smock.fake('ReservoirMock', { + address: l2Receiver.address, + }) + reservoirMock.pow.returns(1) + await testValidFinalizeTransfer(defaultDataWithNotEmptyCallHookData) + expect(reservoirMock.pow).to.have.been.calledWith(toBN(1), toBN(2), toBN(3)) + }) + it('reverts if a callhook reverts', async function () { + const reservoirMock = await smock.fake('ReservoirMock', { + address: l2Receiver.address, + }) + reservoirMock.pow.reverts() + const mockL1GatewayL2Alias = await getL2SignerFromL1(mockL1Gateway.address) + await me.signer.sendTransaction({ + to: await mockL1GatewayL2Alias.getAddress(), + value: utils.parseUnits('1', 'ether'), + }) + const tx = l2GraphTokenGateway + .connect(mockL1GatewayL2Alias) + .finalizeInboundTransfer( + mockL1GRT.address, + tokenSender.address, + l2Receiver.address, + toGRT('10'), + defaultDataWithNotEmptyCallHookData, + ) + await expect(tx).revertedWith('CALLHOOK_FAILED') + }) + }) + }) +}) diff --git a/test/l2/l2Reservoir.test.ts b/test/l2/l2Reservoir.test.ts new file mode 100644 index 000000000..26a9ccc75 --- /dev/null +++ b/test/l2/l2Reservoir.test.ts @@ -0,0 +1,479 @@ +import { expect } from 'chai' +import { BigNumber, constants, ContractTransaction, utils } from 'ethers' + +import { L2FixtureContracts, NetworkFixture } from '../lib/fixtures' + +import { BigNumber as BN } from 'bignumber.js' +import { FakeContract, smock } from '@defi-wonderland/smock' + +import { + advanceBlocks, + getAccounts, + latestBlock, + toBN, + toGRT, + formatGRT, + Account, + RewardsTracker, + getL2SignerFromL1, + applyL1ToL2Alias, +} from '../lib/testHelpers' +import { L2Reservoir } from '../../build/types/L2Reservoir' + +import { L2GraphTokenGateway } from '../../build/types/L2GraphTokenGateway' +import { L2GraphToken } from '../../build/types/L2GraphToken' + +const toRound = (n: BigNumber) => formatGRT(n).split('.')[0] + +const dripAmount = toBN('5851557519569225000000000') +const dripNormalizedSupply = toGRT('10004000000') +const dripIssuanceRate = toBN('1000000023206889619') + +describe('L2Reservoir', () => { + let governor: Account + let testAccount1: Account + let testAccount2: Account + let mockRouter: Account + let mockL1GRT: Account + let mockL1Gateway: Account + let mockL1Reservoir: Account + let fixture: NetworkFixture + let arbTxMock: FakeContract + + let grt: L2GraphToken + let l2Reservoir: L2Reservoir + let l2GraphTokenGateway: L2GraphTokenGateway + + let fixtureContracts: L2FixtureContracts + + let normalizedSupply: BigNumber + let dripBlock: BigNumber + + const ISSUANCE_RATE_PERIODS = toBN(4) // blocks required to issue 0.05% rewards + const ISSUANCE_RATE_PER_BLOCK = toBN('1000122722344290393') // % increase every block + + // Test accumulated rewards after nBlocksToAdvance, + // asking for the value at blockToQuery + const shouldGetNewRewards = async ( + initialSupply: BigNumber, + nBlocksToAdvance: BigNumber = ISSUANCE_RATE_PERIODS, + blockToQuery?: BigNumber, + expectedValue?: BigNumber, + round = true, + ) => { + // -- t0 -- + const tracker = await RewardsTracker.create(initialSupply, ISSUANCE_RATE_PER_BLOCK) + const startAccrued = await l2Reservoir.getAccumulatedRewards(await latestBlock()) + // Jump + await advanceBlocks(nBlocksToAdvance) + + // -- t1 -- + + // Contract calculation + if (!blockToQuery) { + blockToQuery = await latestBlock() + } + const contractAccrued = await l2Reservoir.getAccumulatedRewards(blockToQuery) + // Local calculation + if (expectedValue == null) { + expectedValue = await tracker.newRewards(blockToQuery) + } + + // Check + if (round) { + expect(toRound(contractAccrued.sub(startAccrued))).eq(toRound(expectedValue)) + } else { + expect(contractAccrued.sub(startAccrued)).eq(expectedValue) + } + + return expectedValue + } + + const gatewayFinalizeTransfer = async (callhookData: string): Promise => { + const mockL1GatewayL2Alias = await getL2SignerFromL1(mockL1Gateway.address) + await testAccount1.signer.sendTransaction({ + to: await mockL1GatewayL2Alias.getAddress(), + value: utils.parseUnits('1', 'ether'), + }) + const data = utils.defaultAbiCoder.encode(['bytes', 'bytes'], ['0x', callhookData]) + const tx = l2GraphTokenGateway + .connect(mockL1GatewayL2Alias) + .finalizeInboundTransfer( + mockL1GRT.address, + mockL1Reservoir.address, + l2Reservoir.address, + dripAmount, + data, + ) + return tx + } + + const validGatewayFinalizeTransfer = async ( + callhookData: string, + keeperReward = toGRT('0'), + ): Promise => { + const tx = await gatewayFinalizeTransfer(callhookData) + await expect(tx) + .emit(l2GraphTokenGateway, 'DepositFinalized') + .withArgs(mockL1GRT.address, mockL1Reservoir.address, l2Reservoir.address, dripAmount) + + await expect(tx).emit(grt, 'BridgeMinted').withArgs(l2Reservoir.address, dripAmount) + + // newly minted GRT + const receiverBalance = await grt.balanceOf(l2Reservoir.address) + await expect(receiverBalance).eq(dripAmount.sub(keeperReward)) + return tx + } + + before(async function () { + ;[governor, testAccount1, mockRouter, mockL1GRT, mockL1Gateway, mockL1Reservoir, testAccount2] = + await getAccounts() + + fixture = new NetworkFixture() + fixtureContracts = await fixture.loadL2(governor.signer) + ;({ grt, l2Reservoir, l2GraphTokenGateway } = fixtureContracts) + await fixture.configureL2Bridge( + governor.signer, + fixtureContracts, + mockRouter.address, + mockL1GRT.address, + mockL1Gateway.address, + mockL1Reservoir.address, + ) + + arbTxMock = await smock.fake('IArbTxWithRedeemer', { + address: '0x000000000000000000000000000000000000006E', + }) + arbTxMock.getCurrentRedeemer.returns(applyL1ToL2Alias(mockL1Reservoir.address)) + }) + + beforeEach(async function () { + await fixture.setUp() + }) + + afterEach(async function () { + await fixture.tearDown() + }) + + describe('setNextDripNonce', async function () { + it('rejects unauthorized calls', async function () { + const tx = l2Reservoir.connect(testAccount1.signer).setNextDripNonce(toBN('10')) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + it('sets the next expected drip nonce', async function () { + const tx = l2Reservoir.connect(governor.signer).setNextDripNonce(toBN('10')) + await expect(tx).emit(l2Reservoir, 'NextDripNonceUpdated').withArgs(toBN('10')) + await expect(await l2Reservoir.nextDripNonce()).to.eq(toBN('10')) + }) + }) + + describe('setL1ReservoirAddress', async function () { + it('rejects unauthorized calls', async function () { + const tx = l2Reservoir + .connect(testAccount1.signer) + .setL1ReservoirAddress(testAccount1.address) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + it('rejects setting a zero address', async function () { + const tx = l2Reservoir.connect(governor.signer).setL1ReservoirAddress(constants.AddressZero) + await expect(tx).revertedWith('INVALID_L1_RESERVOIR') + }) + it('sets the L1Reservoir address', async function () { + const tx = l2Reservoir.connect(governor.signer).setL1ReservoirAddress(testAccount1.address) + await expect(tx).emit(l2Reservoir, 'L1ReservoirAddressUpdated').withArgs(testAccount1.address) + await expect(await l2Reservoir.l1ReservoirAddress()).to.eq(testAccount1.address) + }) + }) + + describe('setL2KeeperRewardFraction', async function () { + it('rejects unauthorized calls', async function () { + const tx = l2Reservoir.connect(testAccount1.signer).setL2KeeperRewardFraction(toBN(1)) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + it('rejects invalid values (> 1)', async function () { + const tx = l2Reservoir.connect(governor.signer).setL2KeeperRewardFraction(toGRT('1.000001')) + await expect(tx).revertedWith('INVALID_VALUE') + }) + it('sets the L1Reservoir address', async function () { + const tx = l2Reservoir.connect(governor.signer).setL2KeeperRewardFraction(toGRT('0.999')) + await expect(tx).emit(l2Reservoir, 'L2KeeperRewardFractionUpdated').withArgs(toGRT('0.999')) + await expect(await l2Reservoir.l2KeeperRewardFraction()).to.eq(toGRT('0.999')) + }) + }) + + describe('receiveDrip', async function () { + beforeEach(async function () { + await l2Reservoir.connect(governor.signer).setL2KeeperRewardFraction(toGRT('0.2')) + await l2Reservoir.connect(governor.signer).setL1ReservoirAddress(mockL1Reservoir.address) + }) + it('rejects the call when not called by the gateway', async function () { + const tx = l2Reservoir + .connect(governor.signer) + .receiveDrip( + dripNormalizedSupply, + dripIssuanceRate, + toBN('0'), + toBN('0'), + testAccount1.address, + ) + await expect(tx).revertedWith('ONLY_GATEWAY') + }) + it('rejects the call when received out of order', async function () { + normalizedSupply = dripNormalizedSupply + let receiveDripTx = await l2Reservoir.populateTransaction.receiveDrip( + dripNormalizedSupply, + dripIssuanceRate, + toBN('0'), + toBN('0'), + testAccount1.address, + ) + const tx = await validGatewayFinalizeTransfer(receiveDripTx.data) + dripBlock = await latestBlock() + await expect(await l2Reservoir.issuanceBase()).to.eq(dripNormalizedSupply) + await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate) + await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply) + + // Incorrect nonce + receiveDripTx = await l2Reservoir.populateTransaction.receiveDrip( + dripNormalizedSupply.add(1), + dripIssuanceRate.add(1), + toBN('2'), + toBN('0'), + testAccount1.address, + ) + const tx2 = gatewayFinalizeTransfer(receiveDripTx.data) + dripBlock = await latestBlock() + await expect(tx2).revertedWith('CALLHOOK_FAILED') // Gateway overrides revert message + }) + it('updates the normalized supply cache', async function () { + normalizedSupply = dripNormalizedSupply + const receiveDripTx = await l2Reservoir.populateTransaction.receiveDrip( + dripNormalizedSupply, + dripIssuanceRate, + toBN('0'), + toBN('0'), + testAccount1.address, + ) + const tx = await validGatewayFinalizeTransfer(receiveDripTx.data) + dripBlock = await latestBlock() + await expect(await l2Reservoir.issuanceBase()).to.eq(dripNormalizedSupply) + await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate) + await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply) + }) + it('delivers the keeper reward to the beneficiary address', async function () { + normalizedSupply = dripNormalizedSupply + const reward = toBN('15') + const receiveDripTx = await l2Reservoir.populateTransaction.receiveDrip( + dripNormalizedSupply, + dripIssuanceRate, + toBN('0'), + reward, + testAccount1.address, + ) + const tx = await validGatewayFinalizeTransfer(receiveDripTx.data, reward) + dripBlock = await latestBlock() + await expect(await l2Reservoir.issuanceBase()).to.eq(dripNormalizedSupply) + await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate) + await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply) + await expect(tx) + .emit(grt, 'Transfer') + .withArgs(l2Reservoir.address, testAccount1.address, reward) + await expect(await grt.balanceOf(testAccount1.address)).to.eq(reward) + }) + it('delivers part of the keeper reward to the L2 redeemer', async function () { + arbTxMock.getCurrentRedeemer.returns(testAccount2.address) + await l2Reservoir.connect(governor.signer).setL2KeeperRewardFraction(toGRT('0.25')) + normalizedSupply = dripNormalizedSupply + const reward = toGRT('16') + const receiveDripTx = await l2Reservoir.populateTransaction.receiveDrip( + dripNormalizedSupply, + dripIssuanceRate, + toBN('0'), + reward, + testAccount1.address, + ) + const tx = await validGatewayFinalizeTransfer(receiveDripTx.data, reward) + dripBlock = await latestBlock() + await expect(await l2Reservoir.issuanceBase()).to.eq(dripNormalizedSupply) + await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate) + await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply) + await expect(tx) + .emit(grt, 'Transfer') + .withArgs(l2Reservoir.address, testAccount1.address, toGRT('12')) + await expect(tx) + .emit(grt, 'Transfer') + .withArgs(l2Reservoir.address, testAccount2.address, toGRT('4')) + await expect(await grt.balanceOf(testAccount1.address)).to.eq(toGRT('12')) + await expect(await grt.balanceOf(testAccount2.address)).to.eq(toGRT('4')) + }) + it('updates the normalized supply cache and issuance rate', async function () { + normalizedSupply = dripNormalizedSupply + let receiveDripTx = await l2Reservoir.populateTransaction.receiveDrip( + dripNormalizedSupply, + dripIssuanceRate, + toBN('0'), + toBN('0'), + testAccount1.address, + ) + let tx = await validGatewayFinalizeTransfer(receiveDripTx.data) + dripBlock = await latestBlock() + await expect(await l2Reservoir.issuanceBase()).to.eq(dripNormalizedSupply) + await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate) + await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply) + + receiveDripTx = await l2Reservoir.populateTransaction.receiveDrip( + dripNormalizedSupply.add(1), + dripIssuanceRate.add(1), + toBN('1'), + toBN('0'), + testAccount1.address, + ) + tx = await gatewayFinalizeTransfer(receiveDripTx.data) + dripBlock = await latestBlock() + await expect(await l2Reservoir.issuanceBase()).to.eq(dripNormalizedSupply.add(1)) + await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate.add(1)) + await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply.add(1)) + await expect(await grt.balanceOf(l2Reservoir.address)).to.eq(dripAmount.mul(2)) + }) + it('accepts subsequent calls without changing issuance rate', async function () { + normalizedSupply = dripNormalizedSupply + let receiveDripTx = await l2Reservoir.populateTransaction.receiveDrip( + dripNormalizedSupply, + dripIssuanceRate, + toBN('0'), + toBN('0'), + testAccount1.address, + ) + let tx = await validGatewayFinalizeTransfer(receiveDripTx.data) + dripBlock = await latestBlock() + await expect(await l2Reservoir.issuanceBase()).to.eq(dripNormalizedSupply) + await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate) + await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply) + + receiveDripTx = await l2Reservoir.populateTransaction.receiveDrip( + dripNormalizedSupply.add(1), + dripIssuanceRate, + toBN('1'), + toBN('0'), + testAccount1.address, + ) + tx = await gatewayFinalizeTransfer(receiveDripTx.data) + dripBlock = await latestBlock() + await expect(await l2Reservoir.issuanceBase()).to.eq(dripNormalizedSupply.add(1)) + await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate) + await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply.add(1)) + await expect(await grt.balanceOf(l2Reservoir.address)).to.eq(dripAmount.mul(2)) + }) + it('accepts a different nonce set through setNextDripNonce', async function () { + normalizedSupply = dripNormalizedSupply + let receiveDripTx = await l2Reservoir.populateTransaction.receiveDrip( + dripNormalizedSupply, + dripIssuanceRate, + toBN('0'), + toBN('0'), + testAccount1.address, + ) + let tx = await validGatewayFinalizeTransfer(receiveDripTx.data) + dripBlock = await latestBlock() + await expect(await l2Reservoir.issuanceBase()).to.eq(dripNormalizedSupply) + await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate) + await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply) + + await l2Reservoir.connect(governor.signer).setNextDripNonce(toBN('2')) + receiveDripTx = await l2Reservoir.populateTransaction.receiveDrip( + dripNormalizedSupply.add(1), + dripIssuanceRate, + toBN('2'), + toBN('0'), + testAccount1.address, + ) + tx = await gatewayFinalizeTransfer(receiveDripTx.data) + dripBlock = await latestBlock() + await expect(await l2Reservoir.issuanceBase()).to.eq(dripNormalizedSupply.add(1)) + await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate) + await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply.add(1)) + await expect(await grt.balanceOf(l2Reservoir.address)).to.eq(dripAmount.mul(2)) + }) + }) + + context('calculating rewards', async function () { + beforeEach(async function () { + // 5% minute rate (4 blocks) + normalizedSupply = dripNormalizedSupply + const receiveDripTx = await l2Reservoir.populateTransaction.receiveDrip( + dripNormalizedSupply, + ISSUANCE_RATE_PER_BLOCK, + toBN('0'), + toBN('0'), + testAccount1.address, + ) + await validGatewayFinalizeTransfer(receiveDripTx.data) + dripBlock = await latestBlock() + }) + + describe('getAccumulatedRewards', function () { + it('returns rewards accrued after some blocks', async function () { + await shouldGetNewRewards(normalizedSupply) + }) + it('returns zero if evaluated at the block where reservoir had the first drip', async function () { + await shouldGetNewRewards( + normalizedSupply, + ISSUANCE_RATE_PERIODS, + dripBlock, + toBN(0), + false, + ) + }) + it('returns the supply times issuance rate one block after the first drip', async function () { + const expectedVal = normalizedSupply + .mul(ISSUANCE_RATE_PER_BLOCK.sub(toGRT(1))) + .div(toGRT(1)) + await shouldGetNewRewards( + normalizedSupply, + ISSUANCE_RATE_PERIODS, + dripBlock.add(1), + expectedVal, + false, + ) + }) + it('returns the rewards for a block some time in the future', async function () { + await shouldGetNewRewards(normalizedSupply, toBN(1), dripBlock.add(10000)) + }) + }) + describe('getNewRewards', function () { + const computeDelta = function (t1: BigNumber, t0: BigNumber, lambda = toBN(0)): BigNumber { + const deltaT = new BN(t1.toString()).minus(new BN(t0.toString())) + const rate = new BN(ISSUANCE_RATE_PER_BLOCK.toString()).div(1e18) + const supply = new BN(normalizedSupply.toString()) + return toBN(supply.times(rate.pow(deltaT)).minus(supply).precision(18).toString(10)) + .mul(toGRT('1').sub(lambda)) + .div(toGRT('1')) + } + it('computes the rewards delta between the last drip block and the current block', async function () { + const t0 = dripBlock + const t1 = t0.add(200) + const expectedVal = computeDelta(t1, t0) + expect(toRound(await l2Reservoir.getNewRewards(t1))).to.eq(toRound(expectedVal)) + }) + it('returns zero rewards if the time delta is zero', async function () { + const t0 = dripBlock + const expectedVal = toBN('0') + expect(await l2Reservoir.getNewRewards(t0)).to.eq(expectedVal) + }) + it('computes the rewards delta between a past drip block and a future block', async function () { + await advanceBlocks(20) + const t0 = dripBlock + const t1 = t0.add(100) + const expectedVal = computeDelta(t1, t0) + expect(toRound(await l2Reservoir.getNewRewards(t1))).to.eq(toRound(expectedVal)) + }) + it('computes the rewards delta between a past drip block and the current block', async function () { + await advanceBlocks(20) + const t0 = dripBlock + const t1 = await latestBlock() + const expectedVal = computeDelta(t1, t0) + expect(toRound(await l2Reservoir.getNewRewards(t1))).to.eq(toRound(expectedVal)) + }) + }) + }) +}) diff --git a/test/lib/deployment.ts b/test/lib/deployment.ts index 68f0eb18a..4bdcbe629 100644 --- a/test/lib/deployment.ts +++ b/test/lib/deployment.ts @@ -18,6 +18,12 @@ import { Staking } from '../../build/types/Staking' import { RewardsManager } from '../../build/types/RewardsManager' import { GraphGovernance } from '../../build/types/GraphGovernance' import { SubgraphNFT } from '../../build/types/SubgraphNFT' +import { L1GraphTokenGateway } from '../../build/types/L1GraphTokenGateway' +import { L2GraphTokenGateway } from '../../build/types/L2GraphTokenGateway' +import { L2GraphToken } from '../../build/types/L2GraphToken' +import { BridgeEscrow } from '../../build/types/BridgeEscrow' +import { L1Reservoir } from '../../build/types/L1Reservoir' +import { L2Reservoir } from '../../build/types/L2Reservoir' // Disable logging for tests logger.pause() @@ -53,6 +59,7 @@ export const defaults = { }, rewards: { issuanceRate: toGRT('1.000000023206889619'), // 5% annual rate + dripInterval: toBN('50400'), // 1 week in blocks (post-Merge) }, } @@ -243,3 +250,80 @@ export async function deployGraphGovernance( deployer, ) as unknown as GraphGovernance } + +export async function deployL1GraphTokenGateway( + deployer: Signer, + controller: string, + proxyAdmin: GraphProxyAdmin, +): Promise { + return network.deployContractWithProxy( + proxyAdmin, + 'L1GraphTokenGateway', + [controller], + deployer, + ) as unknown as L1GraphTokenGateway +} + +export async function deployBridgeEscrow( + deployer: Signer, + controller: string, + proxyAdmin: GraphProxyAdmin, +): Promise { + return network.deployContractWithProxy( + proxyAdmin, + 'BridgeEscrow', + [controller], + deployer, + ) as unknown as BridgeEscrow +} + +export async function deployL2GraphTokenGateway( + deployer: Signer, + controller: string, + proxyAdmin: GraphProxyAdmin, +): Promise { + return network.deployContractWithProxy( + proxyAdmin, + 'L2GraphTokenGateway', + [controller], + deployer, + ) as unknown as L2GraphTokenGateway +} + +export async function deployL2GRT( + deployer: Signer, + proxyAdmin: GraphProxyAdmin, +): Promise { + return network.deployContractWithProxy( + proxyAdmin, + 'L2GraphToken', + [await deployer.getAddress()], + deployer, + ) as unknown as L2GraphToken +} + +export async function deployL1Reservoir( + deployer: Signer, + controller: string, + proxyAdmin: GraphProxyAdmin, +): Promise { + return network.deployContractWithProxy( + proxyAdmin, + 'L1Reservoir', + [controller, defaults.rewards.dripInterval], + deployer, + ) as unknown as L1Reservoir +} + +export async function deployL2Reservoir( + deployer: Signer, + controller: string, + proxyAdmin: GraphProxyAdmin, +): Promise { + return network.deployContractWithProxy( + proxyAdmin, + 'L2Reservoir', + [controller], + deployer, + ) as unknown as L2Reservoir +} diff --git a/test/lib/fixtures.ts b/test/lib/fixtures.ts index 86541cf6d..5a4f1e53a 100644 --- a/test/lib/fixtures.ts +++ b/test/lib/fixtures.ts @@ -2,7 +2,64 @@ import { utils, Wallet, Signer } from 'ethers' import * as deployment from './deployment' -import { evmSnapshot, evmRevert, provider } from './testHelpers' +import { evmSnapshot, evmRevert, initNetwork, toBN } from './testHelpers' +import { BridgeMock } from '../../build/types/BridgeMock' +import { InboxMock } from '../../build/types/InboxMock' +import { OutboxMock } from '../../build/types/OutboxMock' +import { deployContract } from './deployment' +import { Controller } from '../../build/types/Controller' +import { DisputeManager } from '../../build/types/DisputeManager' +import { EpochManager } from '../../build/types/EpochManager' +import { GraphToken } from '../../build/types/GraphToken' +import { Curation } from '../../build/types/Curation' +import { GNS } from '../../build/types/GNS' +import { Staking } from '../../build/types/Staking' +import { RewardsManager } from '../../build/types/RewardsManager' +import { ServiceRegistry } from '../../build/types/ServiceRegistry' +import { GraphProxyAdmin } from '../../build/types/GraphProxyAdmin' +import { L1GraphTokenGateway } from '../../build/types/L1GraphTokenGateway' +import { BridgeEscrow } from '../../build/types/BridgeEscrow' +import { L1Reservoir } from '../../build/types/L1Reservoir' +import { L2GraphTokenGateway } from '../../build/types/L2GraphTokenGateway' +import { L2GraphToken } from '../../build/types/L2GraphToken' +import { L2Reservoir } from '../../build/types/L2Reservoir' + +export interface L1FixtureContracts { + controller: Controller + disputeManager: DisputeManager + epochManager: EpochManager + grt: GraphToken + curation: Curation + gns: GNS + staking: Staking + rewardsManager: RewardsManager + serviceRegistry: ServiceRegistry + proxyAdmin: GraphProxyAdmin + l1GraphTokenGateway: L1GraphTokenGateway + bridgeEscrow: BridgeEscrow + l1Reservoir: L1Reservoir +} + +export interface L2FixtureContracts { + controller: Controller + disputeManager: DisputeManager + epochManager: EpochManager + grt: L2GraphToken + curation: Curation + gns: GNS + staking: Staking + rewardsManager: RewardsManager + serviceRegistry: ServiceRegistry + proxyAdmin: GraphProxyAdmin + l2GraphTokenGateway: L2GraphTokenGateway + l2Reservoir: L2Reservoir +} + +export interface ArbitrumL1Mocks { + bridgeMock: BridgeMock + inboxMock: InboxMock + outboxMock: OutboxMock +} export class NetworkFixture { lastSnapshotId: number @@ -11,16 +68,13 @@ export class NetworkFixture { this.lastSnapshotId = 0 } - async load( + async _loadLayer( deployer: Signer, slasher: Signer = Wallet.createRandom() as Signer, arbitrator: Signer = Wallet.createRandom() as Signer, - ): Promise { - // Enable automining with each transaction, and disable - // the mining interval. Individual tests may modify this - // behavior as needed. - provider().send('evm_setIntervalMining', [0]) - provider().send('evm_setAutomine', [true]) + isL2: boolean, + ): Promise { + await initNetwork() // Roles const arbitratorAddress = await arbitrator.getAddress() @@ -34,7 +88,13 @@ export class NetworkFixture { controller.address, proxyAdmin, ) - const grt = await deployment.deployGRT(deployer) + let grt: GraphToken | L2GraphToken + if (isL2) { + grt = await deployment.deployL2GRT(deployer, proxyAdmin) + } else { + grt = await deployment.deployGRT(deployer) + } + const curation = await deployment.deployCuration(deployer, controller.address, proxyAdmin) const gns = await deployment.deployGNS(deployer, controller.address, proxyAdmin) const staking = await deployment.deployStaking(deployer, controller.address, proxyAdmin) @@ -55,6 +115,28 @@ export class NetworkFixture { proxyAdmin, ) + let l1GraphTokenGateway: L1GraphTokenGateway + let l2GraphTokenGateway: L2GraphTokenGateway + let bridgeEscrow: BridgeEscrow + let l1Reservoir: L1Reservoir + let l2Reservoir: L2Reservoir + if (isL2) { + l2GraphTokenGateway = await deployment.deployL2GraphTokenGateway( + deployer, + controller.address, + proxyAdmin, + ) + l2Reservoir = await deployment.deployL2Reservoir(deployer, controller.address, proxyAdmin) + } else { + l1GraphTokenGateway = await deployment.deployL1GraphTokenGateway( + deployer, + controller.address, + proxyAdmin, + ) + bridgeEscrow = await deployment.deployBridgeEscrow(deployer, controller.address, proxyAdmin) + l1Reservoir = await deployment.deployL1Reservoir(deployer, controller.address, proxyAdmin) + } + // Setup controller await controller.setContractProxy(utils.id('EpochManager'), epochManager.address) await controller.setContractProxy(utils.id('GraphToken'), grt.address) @@ -63,6 +145,13 @@ export class NetworkFixture { await controller.setContractProxy(utils.id('DisputeManager'), staking.address) await controller.setContractProxy(utils.id('RewardsManager'), rewardsManager.address) await controller.setContractProxy(utils.id('ServiceRegistry'), serviceRegistry.address) + if (isL2) { + await controller.setContractProxy(utils.id('GraphTokenGateway'), l2GraphTokenGateway.address) + await controller.setContractProxy(utils.id('Reservoir'), l2Reservoir.address) + } else { + await controller.setContractProxy(utils.id('GraphTokenGateway'), l1GraphTokenGateway.address) + await controller.setContractProxy(utils.id('Reservoir'), l1Reservoir.address) + } // Setup contracts await curation.connect(deployer).syncAllContracts() @@ -71,32 +160,160 @@ export class NetworkFixture { await disputeManager.connect(deployer).syncAllContracts() await rewardsManager.connect(deployer).syncAllContracts() await staking.connect(deployer).syncAllContracts() + if (isL2) { + await l2GraphTokenGateway.connect(deployer).syncAllContracts() + await l2Reservoir.connect(deployer).syncAllContracts() + } else { + await l1GraphTokenGateway.connect(deployer).syncAllContracts() + await bridgeEscrow.connect(deployer).syncAllContracts() + await l1Reservoir.connect(deployer).syncAllContracts() + } await staking.connect(deployer).setSlasher(slasherAddress, true) - await grt.connect(deployer).addMinter(rewardsManager.address) await gns.connect(deployer).approveAll() - await rewardsManager.connect(deployer).setIssuanceRate(deployment.defaults.rewards.issuanceRate) + if (isL2) { + await grt.connect(deployer).addMinter(l2GraphTokenGateway.address) + await l2Reservoir.connect(deployer).approveRewardsManager() + } else { + await grt.connect(deployer).addMinter(l1Reservoir.address) + await l1Reservoir.connect(deployer).setIssuanceRate(deployment.defaults.rewards.issuanceRate) + await l1Reservoir.connect(deployer).approveRewardsManager() + } // Unpause the protocol await controller.connect(deployer).setPaused(false) + if (isL2) { + return { + controller, + disputeManager, + epochManager, + grt: grt as L2GraphToken, + curation, + gns, + staking, + rewardsManager, + serviceRegistry, + proxyAdmin, + l2GraphTokenGateway, + l2Reservoir, + } as L2FixtureContracts + } else { + return { + controller, + disputeManager, + epochManager, + grt: grt as GraphToken, + curation, + gns, + staking, + rewardsManager, + serviceRegistry, + proxyAdmin, + l1GraphTokenGateway, + bridgeEscrow, + l1Reservoir, + } as L1FixtureContracts + } + } + + async load( + deployer: Signer, + slasher: Signer = Wallet.createRandom() as Signer, + arbitrator: Signer = Wallet.createRandom() as Signer, + ): Promise { + return this._loadLayer(deployer, slasher, arbitrator, false) as unknown as L1FixtureContracts + } + + async loadL2( + deployer: Signer, + slasher: Signer = Wallet.createRandom() as Signer, + arbitrator: Signer = Wallet.createRandom() as Signer, + ): Promise { + return this._loadLayer(deployer, slasher, arbitrator, true) as unknown as L2FixtureContracts + } + + async loadArbitrumL1Mocks(deployer: Signer): Promise { + const bridgeMock = (await deployContract('BridgeMock', deployer)) as unknown as BridgeMock + const inboxMock = (await deployContract('InboxMock', deployer)) as unknown as InboxMock + const outboxMock = (await deployContract('OutboxMock', deployer)) as unknown as OutboxMock return { - controller, - disputeManager, - epochManager, - grt, - curation, - gns, - staking, - rewardsManager, - serviceRegistry, - proxyAdmin, + bridgeMock, + inboxMock, + outboxMock, } } + async configureL1Bridge( + deployer: Signer, + arbitrumMocks: ArbitrumL1Mocks, + l1FixtureContracts: L1FixtureContracts, + mockRouterAddress: string, + mockL2GRTAddress: string, + mockL2GatewayAddress: string, + mockL2ReservoirAddress: string, + ): Promise { + // First configure the Arbitrum bridge mocks + await arbitrumMocks.bridgeMock.connect(deployer).setInbox(arbitrumMocks.inboxMock.address, true) + await arbitrumMocks.bridgeMock + .connect(deployer) + .setOutbox(arbitrumMocks.outboxMock.address, true) + await arbitrumMocks.inboxMock.connect(deployer).setBridge(arbitrumMocks.bridgeMock.address) + await arbitrumMocks.outboxMock.connect(deployer).setBridge(arbitrumMocks.bridgeMock.address) + + // Configure the gateway + await l1FixtureContracts.l1GraphTokenGateway + .connect(deployer) + .setArbitrumAddresses(arbitrumMocks.inboxMock.address, mockRouterAddress) + await l1FixtureContracts.l1GraphTokenGateway + .connect(deployer) + .setL2TokenAddress(mockL2GRTAddress) + await l1FixtureContracts.l1GraphTokenGateway + .connect(deployer) + .setL2CounterpartAddress(mockL2GatewayAddress) + await l1FixtureContracts.l1GraphTokenGateway + .connect(deployer) + .setEscrowAddress(l1FixtureContracts.bridgeEscrow.address) + await l1FixtureContracts.l1GraphTokenGateway + .connect(deployer) + .addToCallhookWhitelist(l1FixtureContracts.l1Reservoir.address) + await l1FixtureContracts.bridgeEscrow + .connect(deployer) + .approveAll(l1FixtureContracts.l1GraphTokenGateway.address) + await l1FixtureContracts.l1Reservoir + .connect(deployer) + .setL2ReservoirAddress(mockL2ReservoirAddress) + await l1FixtureContracts.l1GraphTokenGateway.connect(deployer).setPaused(false) + } + + async configureL2Bridge( + deployer: Signer, + l2FixtureContracts: L2FixtureContracts, + mockRouterAddress: string, + mockL1GRTAddress: string, + mockL1GatewayAddress: string, + mockL1ReservoirAddress: string, + ): Promise { + // Configure the L2 GRT + // Configure the gateway + await l2FixtureContracts.grt + .connect(deployer) + .setGateway(l2FixtureContracts.l2GraphTokenGateway.address) + await l2FixtureContracts.grt.connect(deployer).setL1Address(mockL1GRTAddress) + // Configure the gateway + await l2FixtureContracts.l2GraphTokenGateway.connect(deployer).setL2Router(mockRouterAddress) + await l2FixtureContracts.l2GraphTokenGateway + .connect(deployer) + .setL1TokenAddress(mockL1GRTAddress) + await l2FixtureContracts.l2GraphTokenGateway + .connect(deployer) + .setL1CounterpartAddress(mockL1GatewayAddress) + await l2FixtureContracts.l2GraphTokenGateway.connect(deployer).setPaused(false) + } + async setUp(): Promise { this.lastSnapshotId = await evmSnapshot() - provider().send('evm_setAutomine', [true]) + await initNetwork() } async tearDown(): Promise { diff --git a/test/lib/graphTokenTests.ts b/test/lib/graphTokenTests.ts new file mode 100644 index 000000000..b0a1b7c7f --- /dev/null +++ b/test/lib/graphTokenTests.ts @@ -0,0 +1,287 @@ +import { expect } from 'chai' +import { constants, utils, BytesLike, BigNumber, Signature } from 'ethers' +import { eip712 } from '@graphprotocol/common-ts/dist/attestations' + +import * as deployment from './deployment' +import { getAccounts, getChainID, toBN, toGRT, Account, initNetwork } from './testHelpers' + +import { L2GraphToken } from '../../build/types/L2GraphToken' +import { GraphToken } from '../../build/types/GraphToken' + +const { AddressZero, MaxUint256 } = constants +const { keccak256, SigningKey } = utils + +const PERMIT_TYPE_HASH = eip712.typeHash( + 'Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)', +) +const L1SALT = '0x51f3d585afe6dfeb2af01bba0889a36c1db03beec88c6a4d0c53817069026afa' +const L2SALT = '0xe33842a7acd1d5a1d28f25a931703e5605152dc48d64dc4716efdae1f5659591' + +interface Permit { + owner: string + spender: string + value: BigNumber + nonce: BigNumber + deadline: BigNumber +} + +function hashEncodePermit(permit: Permit) { + return eip712.hashStruct( + PERMIT_TYPE_HASH, + ['address', 'address', 'uint256', 'uint256', 'uint256'], + [permit.owner, permit.spender, permit.value, permit.nonce, permit.deadline], + ) +} + +function signPermit( + signer: BytesLike, + chainId: number, + contractAddress: string, + permit: Permit, + salt: string, +): Signature { + const domainSeparator = eip712.domainSeparator({ + name: 'Graph Token', + version: '0', + chainId, + verifyingContract: contractAddress, + salt: salt, + }) + const hashEncodedPermit = hashEncodePermit(permit) + const message = eip712.encode(domainSeparator, hashEncodedPermit) + const messageHash = keccak256(message) + const signingKey = new SigningKey(signer) + return signingKey.signDigest(messageHash) +} + +export function grtTests(isL2: boolean): void { + let me: Account + let other: Account + let governor: Account + let salt: string + + const mePrivateKey = '0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d' + const otherPrivateKey = '0x6cbed15c793ce57650b9877cf6fa156fbef513c4e6134f022a85b1ffdd59b2a1' + + let grt: GraphToken | L2GraphToken + + async function permitMaxOK(): Promise { + return permitOK(MaxUint256) + } + + async function permitOK(value: BigNumber): Promise { + const nonce = await grt.nonces(me.address) + return { + owner: me.address, + spender: other.address, + value: value, + nonce: nonce, + deadline: toBN('0'), + } + } + + async function permitExpired(): Promise { + const permit = await permitMaxOK() + permit.deadline = toBN('1') + return permit + } + + async function permitDeny(): Promise { + const permit = await permitMaxOK() + permit.value = toBN('0') + return permit + } + + async function createPermitTransaction(permit: Permit, signer: string, salt: string) { + const chainID = await getChainID() + const signature: Signature = signPermit(signer, chainID, grt.address, permit, salt) + return grt.permit( + permit.owner, + permit.spender, + permit.value, + permit.deadline, + signature.v, + signature.r, + signature.s, + ) + } + + before(async function () { + await initNetwork() + ;[me, other, governor] = await getAccounts() + }) + + beforeEach(async function () { + // Deploy graph token + if (isL2) { + const proxyAdmin = await deployment.deployProxyAdmin(governor.signer) + grt = await deployment.deployL2GRT(governor.signer, proxyAdmin) + salt = L2SALT + } else { + grt = await deployment.deployGRT(governor.signer) + salt = L1SALT + } + + // Mint some tokens + const tokens = toGRT('10000') + await grt.connect(governor.signer).mint(me.address, tokens) + }) + + describe('permit', function () { + it('should permit max token allowance', async function () { + // Allow to transfer tokens + const tokensToApprove = toGRT('1000') + const permit = await permitOK(tokensToApprove) + const tx = createPermitTransaction(permit, mePrivateKey, salt) + await expect(tx).emit(grt, 'Approval').withArgs(permit.owner, permit.spender, tokensToApprove) + + // Allowance updated + const allowance = await grt.allowance(me.address, other.address) + expect(allowance).eq(tokensToApprove) + + // Transfer tokens should work + const tokens = toGRT('100') + await grt.connect(other.signer).transferFrom(me.address, other.address, tokens) + }) + + it('should permit max token allowance', async function () { + // Allow to transfer tokens + const permit = await permitMaxOK() + const tx = createPermitTransaction(permit, mePrivateKey, salt) + await expect(tx).emit(grt, 'Approval').withArgs(permit.owner, permit.spender, MaxUint256) + + // Allowance updated + const allowance = await grt.allowance(me.address, other.address) + expect(allowance).eq(MaxUint256) + + // Transfer tokens should work + const tokens = toGRT('100') + await grt.connect(other.signer).transferFrom(me.address, other.address, tokens) + }) + + it('reject to transfer more tokens than approved by permit', async function () { + // Allow to transfer tokens + const tokensToApprove = toGRT('1000') + const permit = await permitOK(tokensToApprove) + await createPermitTransaction(permit, mePrivateKey, salt) + + // Should not transfer more than approved + const tooManyTokens = toGRT('1001') + const tx = grt.connect(other.signer).transferFrom(me.address, other.address, tooManyTokens) + await expect(tx).revertedWith('ERC20: transfer amount exceeds allowance') + + // Should transfer up to the approved amount + await grt.connect(other.signer).transferFrom(me.address, other.address, tokensToApprove) + }) + + it('reject use two permits with same nonce', async function () { + // Allow to transfer tokens + const permit = await permitMaxOK() + await createPermitTransaction(permit, mePrivateKey, salt) + + // Try to re-use the permit + const tx = createPermitTransaction(permit, mePrivateKey, salt) + await expect(tx).revertedWith('GRT: invalid permit') + }) + + it('reject use expired permit', async function () { + const permit = await permitExpired() + const tx = createPermitTransaction(permit, mePrivateKey, salt) + await expect(tx).revertedWith('GRT: expired permit') + }) + + it('reject permit if holder address does not match', async function () { + const permit = await permitMaxOK() + const tx = createPermitTransaction(permit, otherPrivateKey, salt) + await expect(tx).revertedWith('GRT: invalid permit') + }) + + it('should deny transfer from if permit was denied', async function () { + // Allow to transfer tokens + const permit1 = await permitMaxOK() + await createPermitTransaction(permit1, mePrivateKey, salt) + + // Deny transfer tokens + const permit2 = await permitDeny() + await createPermitTransaction(permit2, mePrivateKey, salt) + + // Allowance updated + const allowance = await grt.allowance(me.address, other.address) + expect(allowance).eq(toBN('0')) + + // Try to transfer without permit should fail + const tokens = toGRT('100') + const tx = grt.connect(other.signer).transferFrom(me.address, other.address, tokens) + await expect(tx).revertedWith('ERC20: transfer amount exceeds allowance') + }) + }) + + describe('mint', function () { + describe('addMinter', function () { + it('reject add a new minter if not allowed', async function () { + const tx = grt.connect(me.signer).addMinter(me.address) + await expect(tx).revertedWith('Only Governor can call') + }) + + it('should add a new minter', async function () { + expect(await grt.isMinter(me.address)).eq(false) + const tx = grt.connect(governor.signer).addMinter(me.address) + await expect(tx).emit(grt, 'MinterAdded').withArgs(me.address) + expect(await grt.isMinter(me.address)).eq(true) + }) + }) + + describe('mint', async function () { + it('reject mint if not minter', async function () { + const tx = grt.connect(me.signer).mint(me.address, toGRT('100')) + await expect(tx).revertedWith('Only minter can call') + }) + }) + + context('> when is minter', function () { + beforeEach(async function () { + await grt.connect(governor.signer).addMinter(me.address) + expect(await grt.isMinter(me.address)).eq(true) + }) + + describe('mint', async function () { + it('should mint', async function () { + const beforeTokens = await grt.balanceOf(me.address) + + const tokensToMint = toGRT('100') + const tx = grt.connect(me.signer).mint(me.address, tokensToMint) + await expect(tx).emit(grt, 'Transfer').withArgs(AddressZero, me.address, tokensToMint) + + const afterTokens = await grt.balanceOf(me.address) + expect(afterTokens).eq(beforeTokens.add(tokensToMint)) + }) + + it('should mint if governor', async function () { + const tokensToMint = toGRT('100') + await grt.connect(governor.signer).mint(me.address, tokensToMint) + }) + }) + + describe('removeMinter', function () { + it('reject remove a minter if not allowed', async function () { + const tx = grt.connect(me.signer).removeMinter(me.address) + await expect(tx).revertedWith('Only Governor can call') + }) + + it('should remove a minter', async function () { + const tx = grt.connect(governor.signer).removeMinter(me.address) + await expect(tx).emit(grt, 'MinterRemoved').withArgs(me.address) + expect(await grt.isMinter(me.address)).eq(false) + }) + }) + + describe('renounceMinter', function () { + it('should renounce to be a minter', async function () { + const tx = grt.connect(me.signer).renounceMinter() + await expect(tx).emit(grt, 'MinterRemoved').withArgs(me.address) + expect(await grt.isMinter(me.address)).eq(false) + }) + }) + }) + }) +} diff --git a/test/lib/testHelpers.ts b/test/lib/testHelpers.ts index ee7c87322..998d0d335 100644 --- a/test/lib/testHelpers.ts +++ b/test/lib/testHelpers.ts @@ -2,7 +2,8 @@ import hre from 'hardhat' import '@nomiclabs/hardhat-ethers' import '@nomiclabs/hardhat-waffle' import { providers, utils, BigNumber, Signer, Wallet } from 'ethers' -import { formatUnits, getAddress } from 'ethers/lib/utils' +import { formatUnits, getAddress, hexValue } from 'ethers/lib/utils' +import { BigNumber as BN } from 'bignumber.js' import { EpochManager } from '../../build/types/EpochManager' @@ -25,6 +26,14 @@ export interface Account { export const provider = (): providers.JsonRpcProvider => hre.waffle.provider +// Enable automining with each transaction, and disable +// the mining interval. Individual tests may modify this +// behavior as needed. +export async function initNetwork(): Promise { + await provider().send('evm_setIntervalMining', [0]) + await provider().send('evm_setAutomine', [true]) +} + export const getAccounts = async (): Promise => { const accounts = [] const signers: Signer[] = await hre.ethers.getSigners() @@ -57,24 +66,17 @@ export const advanceBlockTo = async (blockNumber: string | number | BigNumber): ? toBN(blockNumber) : blockNumber const currentBlock = await latestBlock() - const start = Date.now() - let notified - if (target.lt(currentBlock)) + if (target.lt(currentBlock)) { throw Error(`Target block #(${target}) is lower than current block #(${currentBlock})`) - while ((await latestBlock()).lt(target)) { - if (!notified && Date.now() - start >= 5000) { - notified = true - console.log(`advanceBlockTo: Advancing too ` + 'many blocks is causing this test to be slow.') - } - await advanceBlock() + } else if (target.eq(currentBlock)) { + return + } else { + await advanceBlocks(target.sub(currentBlock)) } } export const advanceBlocks = async (blocks: string | number | BigNumber): Promise => { - const steps = typeof blocks === 'number' || typeof blocks === 'string' ? toBN(blocks) : blocks - const currentBlock = await latestBlock() - const toBlock = currentBlock.add(steps) - return advanceBlockTo(toBlock) + await provider().send('hardhat_mine', [hexValue(BigNumber.from(blocks))]) } export const advanceToNextEpoch = async (epochManager: EpochManager): Promise => { @@ -119,3 +121,116 @@ export const deriveChannelKey = (): ChannelKey => { }, } } + +// Adapted from: +// https://github.com/livepeer/arbitrum-lpt-bridge/blob/e1a81edda3594e434dbcaa4f1ebc95b7e67ecf2a/utils/arbitrum/messaging.ts#L118 +export const applyL1ToL2Alias = (l1Address: string): string => { + const offset = toBN('0x1111000000000000000000000000000000001111') + const l1AddressAsNumber = toBN(l1Address) + const l2AddressAsNumber = l1AddressAsNumber.add(offset) + + const mask = toBN(2).pow(160) + return l2AddressAsNumber.mod(mask).toHexString() +} + +// Core formula that gets accumulated rewards for a period of time +const getRewards = (p: BN, r: BN, t: BN): string => { + BN.config({ POW_PRECISION: 100 }) + return p.times(r.pow(t)).minus(p).precision(18).toString(10) +} + +// Tracks the accumulated rewards as supply changes across snapshots +// both at a global level (like the Reservoir) and per signal (like RewardsManager) +export class RewardsTracker { + totalSupply = BigNumber.from(0) + lastUpdatedBlock = BigNumber.from(0) + lastPerSignalUpdatedBlock = BigNumber.from(0) + accumulated = BigNumber.from(0) + accumulatedPerSignal = BigNumber.from(0) + accumulatedAtLastPerSignalUpdatedBlock = BigNumber.from(0) + issuanceRate = BigNumber.from(0) + + static async create( + initialSupply: BigNumber, + issuanceRate: BigNumber, + updatedBlock?: BigNumber, + ): Promise { + const lastUpdatedBlock = updatedBlock || (await latestBlock()) + const tracker = new RewardsTracker(initialSupply, issuanceRate, lastUpdatedBlock) + return tracker + } + + constructor(initialSupply: BigNumber, issuanceRate: BigNumber, updatedBlock: BigNumber) { + this.issuanceRate = issuanceRate + this.totalSupply = initialSupply + this.lastUpdatedBlock = updatedBlock + this.lastPerSignalUpdatedBlock = updatedBlock + } + + async snapshotRewards(initialSupply?: BigNumber, atBlock?: BigNumber): Promise { + const newRewards = await this.newRewards(atBlock) + this.accumulated = this.accumulated.add(newRewards) + this.totalSupply = initialSupply || this.totalSupply.add(newRewards) + this.lastUpdatedBlock = atBlock || (await latestBlock()) + return this.accumulated + } + + async snapshotPerSignal(totalSignal: BigNumber, atBlock?: BigNumber): Promise { + this.accumulatedPerSignal = await this.accRewardsPerSignal(totalSignal, atBlock) + this.accumulatedAtLastPerSignalUpdatedBlock = await this.accRewards(atBlock) + this.lastPerSignalUpdatedBlock = atBlock || (await latestBlock()) + return this.accumulatedPerSignal + } + + async elapsedBlocks(): Promise { + const currentBlock = await latestBlock() + return currentBlock.sub(this.lastUpdatedBlock) + } + + async newRewardsPerSignal(totalSignal: BigNumber, atBlock?: BigNumber): Promise { + const accRewards = await this.accRewards(atBlock) + const diff = accRewards.sub(this.accumulatedAtLastPerSignalUpdatedBlock) + if (totalSignal.eq(0)) { + return BigNumber.from(0) + } + return diff.mul(toGRT(1)).div(totalSignal) + } + + async accRewardsPerSignal(totalSignal: BigNumber, atBlock?: BigNumber): Promise { + return this.accumulatedPerSignal.add(await this.newRewardsPerSignal(totalSignal, atBlock)) + } + + async newRewards(atBlock?: BigNumber): Promise { + if (!atBlock) { + atBlock = await latestBlock() + } + const nBlocks = atBlock.sub(this.lastUpdatedBlock) + return this.accruedByElapsed(nBlocks) + } + + async accRewards(atBlock?: BigNumber): Promise { + if (!atBlock) { + atBlock = await latestBlock() + } + return this.accumulated.add(await this.newRewards(atBlock)) + } + + async accruedByElapsed(nBlocks: BigNumber | number): Promise { + const n = getRewards( + new BN(this.totalSupply.toString()), + new BN(this.issuanceRate.toString()).div(1e18), + new BN(nBlocks.toString()), + ) + return BigNumber.from(n) + } +} + +// Adapted from: +// https://github.com/livepeer/arbitrum-lpt-bridge/blob/e1a81edda3594e434dbcaa4f1ebc95b7e67ecf2a/test/utils/messaging.ts#L5 +export async function getL2SignerFromL1(l1Address: string): Promise { + const l2Address = applyL1ToL2Alias(l1Address) + await provider().send('hardhat_impersonateAccount', [l2Address]) + const l2Signer = await hre.ethers.getSigner(l2Address) + + return l2Signer +} diff --git a/test/reservoir/l1Reservoir.test.ts b/test/reservoir/l1Reservoir.test.ts new file mode 100644 index 000000000..261a36c39 --- /dev/null +++ b/test/reservoir/l1Reservoir.test.ts @@ -0,0 +1,1197 @@ +import { expect } from 'chai' +import { BigNumber, constants } from 'ethers' + +import { defaults, deployContract, deployL1Reservoir } from '../lib/deployment' +import { ArbitrumL1Mocks, L1FixtureContracts, NetworkFixture } from '../lib/fixtures' + +import { GraphToken } from '../../build/types/GraphToken' +import { ReservoirMock } from '../../build/types/ReservoirMock' +import { BigNumber as BN } from 'bignumber.js' + +import { + advanceBlocks, + getAccounts, + latestBlock, + toBN, + toGRT, + formatGRT, + Account, + RewardsTracker, +} from '../lib/testHelpers' +import { L1Reservoir } from '../../build/types/L1Reservoir' +import { BridgeEscrow } from '../../build/types/BridgeEscrow' + +import path from 'path' +import { Artifacts } from 'hardhat/internal/artifacts' +import { Interface } from 'ethers/lib/utils' +import { L1GraphTokenGateway } from '../../build/types/L1GraphTokenGateway' +import { Controller } from '../../build/types/Controller' +import { GraphProxyAdmin } from '../../build/types/GraphProxyAdmin' +import { Staking } from '../../build/types/Staking' +const ARTIFACTS_PATH = path.resolve('build/contracts') +const artifacts = new Artifacts(ARTIFACTS_PATH) +const l2ReservoirAbi = artifacts.readArtifactSync('L2Reservoir').abi +const l2ReservoirIface = new Interface(l2ReservoirAbi) + +const { AddressZero } = constants +const toRound = (n: BigNumber) => formatGRT(n.add(toGRT('0.5'))).split('.')[0] + +const maxGas = toBN('1000000') +const maxSubmissionCost = toBN('7') +const gasPriceBid = toBN('2') +const defaultEthValue = maxSubmissionCost.add(maxGas.mul(gasPriceBid)) + +describe('L1Reservoir', () => { + let governor: Account + let testAccount1: Account + let testAccount2: Account + let testAccount3: Account + let mockRouter: Account + let mockL2GRT: Account + let mockL2Gateway: Account + let mockL2Reservoir: Account + let keeper: Account + let fixture: NetworkFixture + + let grt: GraphToken + let reservoirMock: ReservoirMock + let l1Reservoir: L1Reservoir + let bridgeEscrow: BridgeEscrow + let l1GraphTokenGateway: L1GraphTokenGateway + let controller: Controller + let proxyAdmin: GraphProxyAdmin + let staking: Staking + + let supplyBeforeDrip: BigNumber + let dripBlock: BigNumber + let fixtureContracts: L1FixtureContracts + let arbitrumMocks: ArbitrumL1Mocks + + const ISSUANCE_RATE_PERIODS = toBN(4) // blocks required to issue 0.05% rewards + const ISSUANCE_RATE_PER_BLOCK = toBN('1000122722344290393') // % increase every block + + // Test accumulated rewards after nBlocksToAdvance, + // asking for the value at blockToQuery + const shouldGetNewRewards = async ( + initialSupply: BigNumber, + nBlocksToAdvance: BigNumber = ISSUANCE_RATE_PERIODS, + blockToQuery?: BigNumber, + expectedValue?: BigNumber, + round = true, + ) => { + // -- t0 -- + const tracker = await RewardsTracker.create(initialSupply, ISSUANCE_RATE_PER_BLOCK) + const startAccrued = await l1Reservoir.getAccumulatedRewards(await latestBlock()) + // Jump + await advanceBlocks(nBlocksToAdvance) + + // -- t1 -- + + // Contract calculation + if (!blockToQuery) { + blockToQuery = await latestBlock() + } + const contractAccrued = await l1Reservoir.getAccumulatedRewards(blockToQuery) + // Local calculation + if (expectedValue == null) { + expectedValue = await tracker.newRewards(blockToQuery) + } + + // Check + if (round) { + expect(toRound(contractAccrued.sub(startAccrued))).eq(toRound(expectedValue)) + } else { + expect(contractAccrued.sub(startAccrued)).eq(expectedValue) + } + + return expectedValue + } + + const sequentialDoubleDrip = async ( + blocksToAdvance: BigNumber, + dripInterval = defaults.rewards.dripInterval, + ) => { + const supplyBeforeDrip = await grt.totalSupply() + const startAccrued = await l1Reservoir.getAccumulatedRewards(await latestBlock()) + expect(startAccrued).to.eq(0) + const dripBlock = (await latestBlock()).add(1) // We're gonna drip in the next transaction + const tracker = await RewardsTracker.create( + supplyBeforeDrip, + defaults.rewards.issuanceRate, + dripBlock, + ) + expect(await tracker.accRewards(dripBlock)).to.eq(0) + let expectedNextDeadline = dripBlock.add(dripInterval) + let expectedMintedAmount = await tracker.accRewards(expectedNextDeadline) + const tx1 = await l1Reservoir + .connect(keeper.signer) + ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), keeper.address) + const actualAmount = await grt.balanceOf(l1Reservoir.address) + expect(await latestBlock()).eq(dripBlock) + expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount)) + expect(await l1Reservoir.issuanceBase()).to.eq(supplyBeforeDrip) + await expect(tx1) + .emit(l1Reservoir, 'RewardsDripped') + .withArgs(actualAmount, toBN(0), expectedNextDeadline) + await expect(tx1).emit(grt, 'Transfer').withArgs(AddressZero, l1Reservoir.address, actualAmount) + await tracker.snapshotRewards() + + await advanceBlocks(blocksToAdvance) + + const tx2 = await l1Reservoir + .connect(keeper.signer) + ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), keeper.address) + const newAmount = (await grt.balanceOf(l1Reservoir.address)).sub(actualAmount) + expectedNextDeadline = (await latestBlock()).add(dripInterval) + const expectedSnapshottedSupply = supplyBeforeDrip.add(await tracker.accRewards()) + expectedMintedAmount = (await tracker.accRewards(expectedNextDeadline)).sub(actualAmount) + expect(toRound(newAmount)).to.eq(toRound(expectedMintedAmount)) + expect(toRound(await l1Reservoir.issuanceBase())).to.eq(toRound(expectedSnapshottedSupply)) + await expect(tx2) + .emit(l1Reservoir, 'RewardsDripped') + .withArgs(newAmount, toBN(0), expectedNextDeadline) + await expect(tx2).emit(grt, 'Transfer').withArgs(AddressZero, l1Reservoir.address, newAmount) + } + + before(async function () { + ;[ + governor, + testAccount1, + mockRouter, + mockL2GRT, + mockL2Gateway, + mockL2Reservoir, + keeper, + testAccount2, + testAccount3, + ] = await getAccounts() + + fixture = new NetworkFixture() + fixtureContracts = await fixture.load(governor.signer) + ;({ grt, l1Reservoir, bridgeEscrow, l1GraphTokenGateway, controller, proxyAdmin, staking } = + fixtureContracts) + + await l1Reservoir.connect(governor.signer).initialSnapshot(toBN(0)) + arbitrumMocks = await fixture.loadArbitrumL1Mocks(governor.signer) + await fixture.configureL1Bridge( + governor.signer, + arbitrumMocks, + fixtureContracts, + mockRouter.address, + mockL2GRT.address, + mockL2Gateway.address, + mockL2Reservoir.address, + ) + await l1Reservoir.connect(governor.signer).grantDripPermission(keeper.address) + reservoirMock = (await deployContract( + 'ReservoirMock', + governor.signer, + )) as unknown as ReservoirMock + }) + + beforeEach(async function () { + await fixture.setUp() + }) + + afterEach(async function () { + await fixture.tearDown() + }) + + describe('configuration', function () { + describe('initial snapshot', function () { + let reservoir: L1Reservoir + beforeEach(async function () { + // Deploy a new reservoir to avoid issues with initialSnapshot being called twice + reservoir = await deployL1Reservoir(governor.signer, controller.address, proxyAdmin) + await grt.connect(governor.signer).addMinter(reservoir.address) + }) + + it('rejects call if unauthorized', async function () { + const tx = reservoir.connect(testAccount1.signer).initialSnapshot(toGRT('1.025')) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + + it('snapshots the total GRT supply', async function () { + const tx = reservoir.connect(governor.signer).initialSnapshot(toGRT('0')) + const supply = await grt.totalSupply() + await expect(tx) + .emit(reservoir, 'InitialSnapshotTaken') + .withArgs(await latestBlock(), supply, toGRT('0')) + expect(await grt.balanceOf(reservoir.address)).to.eq(toGRT('0')) + expect(await reservoir.issuanceBase()).to.eq(supply) + expect(await reservoir.lastRewardsUpdateBlock()).to.eq(await latestBlock()) + }) + it('mints pending rewards and includes them in the snapshot', async function () { + const pending = toGRT('10000000') + const tx = reservoir.connect(governor.signer).initialSnapshot(pending) + const supply = await grt.totalSupply() + const expectedSupply = supply.add(pending) + await expect(tx) + .emit(reservoir, 'InitialSnapshotTaken') + .withArgs(await latestBlock(), expectedSupply, pending) + expect(await grt.balanceOf(reservoir.address)).to.eq(pending) + expect(await reservoir.issuanceBase()).to.eq(expectedSupply) + expect(await reservoir.lastRewardsUpdateBlock()).to.eq(await latestBlock()) + }) + it('cannot be called more than once', async function () { + let tx = reservoir.connect(governor.signer).initialSnapshot(toGRT('0')) + await expect(tx).emit(reservoir, 'InitialSnapshotTaken') + tx = reservoir.connect(governor.signer).initialSnapshot(toGRT('0')) + await expect(tx).revertedWith('Cannot call this function more than once') + }) + }) + describe('issuance rate update', function () { + it('rejects setting issuance rate if unauthorized', async function () { + const tx = l1Reservoir.connect(testAccount1.signer).setIssuanceRate(toGRT('1.025')) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + + it('rejects setting issuance rate to less than minimum allowed', async function () { + const newIssuanceRate = toGRT('0.1') // this get a bignumber with 1e17 + const tx = l1Reservoir.connect(governor.signer).setIssuanceRate(newIssuanceRate) + await expect(tx).revertedWith('Issuance rate under minimum allowed') + }) + + it('should set issuance rate to minimum allowed', async function () { + const newIssuanceRate = toGRT('1') // this get a bignumber with 1e18 + const tx = l1Reservoir.connect(governor.signer).setIssuanceRate(newIssuanceRate) + await expect(tx).emit(l1Reservoir, 'IssuanceRateStaged').withArgs(newIssuanceRate) + expect(await l1Reservoir.nextIssuanceRate()).eq(newIssuanceRate) + }) + + it('should set issuance rate to apply on next drip', async function () { + const newIssuanceRate = toGRT('1.00025') + let tx = l1Reservoir.connect(governor.signer).setIssuanceRate(newIssuanceRate) + await expect(tx).emit(l1Reservoir, 'IssuanceRateStaged').withArgs(newIssuanceRate) + expect(await l1Reservoir.issuanceRate()).eq(0) + expect(await l1Reservoir.nextIssuanceRate()).eq(newIssuanceRate) + tx = l1Reservoir + .connect(keeper.signer) + ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), keeper.address) + await expect(tx).emit(l1Reservoir, 'IssuanceRateUpdated').withArgs(newIssuanceRate) + expect(await l1Reservoir.issuanceRate()).eq(newIssuanceRate) + }) + }) + describe('drip interval update', function () { + it('rejects setting drip interval if unauthorized', async function () { + const tx = l1Reservoir.connect(testAccount1.signer).setDripInterval(toBN(40800)) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + + it('rejects setting drip interval to zero', async function () { + const tx = l1Reservoir.connect(governor.signer).setDripInterval(toBN(0)) + await expect(tx).revertedWith('Drip interval must be > 0') + }) + + it('updates the drip interval', async function () { + const newInterval = toBN(40800) + const tx = l1Reservoir.connect(governor.signer).setDripInterval(newInterval) + await expect(tx).emit(l1Reservoir, 'DripIntervalUpdated').withArgs(newInterval) + expect(await l1Reservoir.dripInterval()).eq(newInterval) + }) + }) + describe('L2 reservoir address update', function () { + it('rejects setting L2 reservoir address if unauthorized', async function () { + const tx = l1Reservoir + .connect(testAccount1.signer) + .setL2ReservoirAddress(testAccount1.address) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + + it('updates the L2 reservoir address', async function () { + const tx = l1Reservoir.connect(governor.signer).setL2ReservoirAddress(testAccount1.address) + await expect(tx) + .emit(l1Reservoir, 'L2ReservoirAddressUpdated') + .withArgs(testAccount1.address) + expect(await l1Reservoir.l2ReservoirAddress()).eq(testAccount1.address) + }) + }) + describe('L2 rewards fraction update', function () { + it('rejects setting L2 rewards fraction if unauthorized', async function () { + const tx = l1Reservoir.connect(testAccount1.signer).setL2RewardsFraction(toGRT('1.025')) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + + it('rejects setting L2 rewards fraction to more than 1', async function () { + const newValue = toGRT('1').add(1) + const tx = l1Reservoir.connect(governor.signer).setL2RewardsFraction(newValue) + await expect(tx).revertedWith('L2 Rewards fraction must be <= 1') + }) + + it('should set L2 rewards fraction to maximum allowed', async function () { + const newValue = toGRT('1') // this gets a bignumber with 1e18 + const tx = l1Reservoir.connect(governor.signer).setL2RewardsFraction(newValue) + await expect(tx).emit(l1Reservoir, 'L2RewardsFractionStaged').withArgs(newValue) + expect(await l1Reservoir.l2RewardsFraction()).eq(0) + expect(await l1Reservoir.nextL2RewardsFraction()).eq(newValue) + }) + + it('should set L2 rewards fraction to apply on next drip', async function () { + const newValue = toGRT('0.25') + let tx = l1Reservoir.connect(governor.signer).setL2RewardsFraction(newValue) + await expect(tx).emit(l1Reservoir, 'L2RewardsFractionStaged').withArgs(newValue) + expect(await l1Reservoir.nextL2RewardsFraction()).eq(newValue) + tx = l1Reservoir + .connect(keeper.signer) + ['drip(uint256,uint256,uint256,address)']( + maxGas, + gasPriceBid, + maxSubmissionCost, + keeper.address, + { value: defaultEthValue }, + ) + await expect(tx).emit(l1Reservoir, 'L2RewardsFractionUpdated').withArgs(newValue) + expect(await l1Reservoir.l2RewardsFraction()).eq(newValue) + }) + }) + describe('minimum drip interval update', function () { + it('rejects setting minimum drip interval if unauthorized', async function () { + const tx = l1Reservoir.connect(testAccount1.signer).setMinDripInterval(toBN('200')) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + it('rejects setting minimum drip interval if equal to dripInterval', async function () { + const tx = l1Reservoir + .connect(governor.signer) + .setMinDripInterval(await l1Reservoir.dripInterval()) + await expect(tx).revertedWith('MUST_BE_LT_DRIP_INTERVAL') + }) + it('rejects setting minimum drip interval if larger than dripInterval', async function () { + const tx = l1Reservoir + .connect(governor.signer) + .setMinDripInterval((await l1Reservoir.dripInterval()).add(1)) + await expect(tx).revertedWith('MUST_BE_LT_DRIP_INTERVAL') + }) + it('sets the minimum drip interval', async function () { + const newValue = toBN('200') + const tx = l1Reservoir.connect(governor.signer).setMinDripInterval(newValue) + await expect(tx).emit(l1Reservoir, 'MinDripIntervalUpdated').withArgs(newValue) + expect(await l1Reservoir.minDripInterval()).eq(newValue) + }) + }) + describe('allowed drippers whitelist', function () { + it('only allows the governor to add a dripper', async function () { + const tx = l1Reservoir + .connect(testAccount1.signer) + .grantDripPermission(testAccount1.address) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + it('only allows the governor to revoke a dripper', async function () { + const tx = l1Reservoir.connect(testAccount1.signer).revokeDripPermission(keeper.address) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + it('allows adding an address to the allowed drippers', async function () { + const tx = l1Reservoir.connect(governor.signer).grantDripPermission(testAccount1.address) + await expect(tx).emit(l1Reservoir, 'AllowedDripperAdded').withArgs(testAccount1.address) + expect(await l1Reservoir.allowedDrippers(testAccount1.address)).eq(true) + }) + it('allows removing an address from the allowed drippers', async function () { + await l1Reservoir.connect(governor.signer).grantDripPermission(testAccount1.address) + const tx = l1Reservoir.connect(governor.signer).revokeDripPermission(testAccount1.address) + await expect(tx).emit(l1Reservoir, 'AllowedDripperRevoked').withArgs(testAccount1.address) + expect(await l1Reservoir.allowedDrippers(testAccount1.address)).eq(false) + }) + }) + }) + + // TODO test that rewardsManager.updateAccRewardsPerSignal is called when + // issuanceRate or l2RewardsFraction is updated + describe('drip', function () { + it('cannot be called by an unauthorized address', async function () { + const tx = l1Reservoir + .connect(testAccount1.signer) + ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), testAccount1.address) + await expect(tx).revertedWith('UNAUTHORIZED') + }) + it('can be called by an indexer', async function () { + const stakedAmount = toGRT('100000') + await grt.connect(governor.signer).mint(testAccount1.address, stakedAmount) + await grt.connect(testAccount1.signer).approve(staking.address, stakedAmount) + await staking.connect(testAccount1.signer).stake(stakedAmount) + const tx = l1Reservoir + .connect(testAccount1.signer) + ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), testAccount1.address) + await expect(tx).emit(l1Reservoir, 'RewardsDripped') + }) + it('can be called by a whitelisted address', async function () { + await l1Reservoir.connect(governor.signer).grantDripPermission(testAccount1.address) + const tx = l1Reservoir + .connect(testAccount1.signer) + ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), testAccount1.address) + await expect(tx).emit(l1Reservoir, 'RewardsDripped') + }) + it('cannot be called with a zero address for the keeper reward beneficiary', async function () { + await l1Reservoir.connect(governor.signer).grantDripPermission(testAccount1.address) + const tx = l1Reservoir + .connect(testAccount1.signer) + ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), constants.AddressZero) + await expect(tx).revertedWith('INVALID_BENEFICIARY') + }) + it('(operator variant) cannot be called with an invalid indexer', async function () { + const tx = l1Reservoir + .connect(testAccount2.signer) + ['drip(uint256,uint256,uint256,address,address)']( + toBN(0), + toBN(0), + toBN(0), + testAccount1.address, + testAccount1.address, + ) + await expect(tx).revertedWith('UNAUTHORIZED_INVALID_INDEXER') + }) + it('(operator variant) cannot be called by someone who is not an operator for the right indexer', async function () { + const stakedAmount = toGRT('100000') + // testAccount1 is a valid indexer + await grt.connect(governor.signer).mint(testAccount1.address, stakedAmount) + await grt.connect(testAccount1.signer).approve(staking.address, stakedAmount) + await staking.connect(testAccount1.signer).stake(stakedAmount) + // testAccount2 is an operator for testAccount1's indexer + await staking.connect(testAccount1.signer).setOperator(testAccount2.address, true) + // testAccount3 is another valid indexer + await grt.connect(governor.signer).mint(testAccount3.address, stakedAmount) + await grt.connect(testAccount3.signer).approve(staking.address, stakedAmount) + await staking.connect(testAccount3.signer).stake(stakedAmount) + // But testAccount2 is not an operator for testAccount3's indexer + const tx = l1Reservoir + .connect(testAccount2.signer) + ['drip(uint256,uint256,uint256,address,address)']( + toBN(0), + toBN(0), + toBN(0), + testAccount1.address, + testAccount3.address, + ) + await expect(tx).revertedWith('UNAUTHORIZED_INVALID_OPERATOR') + }) + it('(operator variant) can be called by an indexer operator using an extra parameter', async function () { + const stakedAmount = toGRT('100000') + await grt.connect(governor.signer).mint(testAccount1.address, stakedAmount) + await grt.connect(testAccount1.signer).approve(staking.address, stakedAmount) + await staking.connect(testAccount1.signer).stake(stakedAmount) + await staking.connect(testAccount1.signer).setOperator(testAccount2.address, true) + const tx = l1Reservoir + .connect(testAccount2.signer) + ['drip(uint256,uint256,uint256,address,address)']( + toBN(0), + toBN(0), + toBN(0), + testAccount1.address, + testAccount1.address, + ) + await expect(tx).emit(l1Reservoir, 'RewardsDripped') + }) + it('mints rewards for the next week', async function () { + supplyBeforeDrip = await grt.totalSupply() + const startAccrued = await l1Reservoir.getAccumulatedRewards(await latestBlock()) + expect(startAccrued).to.eq(0) + const dripBlock = (await latestBlock()).add(1) // We're gonna drip in the next transaction + const tracker = await RewardsTracker.create( + supplyBeforeDrip, + defaults.rewards.issuanceRate, + dripBlock, + ) + expect(await tracker.accRewards(dripBlock)).to.eq(0) + const expectedNextDeadline = dripBlock.add(defaults.rewards.dripInterval) + const expectedMintedAmount = await tracker.accRewards(expectedNextDeadline) + const tx = await l1Reservoir + .connect(keeper.signer) + ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), keeper.address) + const actualAmount = await grt.balanceOf(l1Reservoir.address) + expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount)) + expect(await l1Reservoir.issuanceBase()).to.eq(supplyBeforeDrip) + await expect(tx) + .emit(l1Reservoir, 'RewardsDripped') + .withArgs(actualAmount, toBN(0), expectedNextDeadline) + }) + it('cannot be called more than once per minDripInterval', async function () { + supplyBeforeDrip = await grt.totalSupply() + const startAccrued = await l1Reservoir.getAccumulatedRewards(await latestBlock()) + expect(startAccrued).to.eq(0) + const dripBlock = (await latestBlock()).add(1) // We're gonna drip in the next transaction + const tracker = await RewardsTracker.create( + supplyBeforeDrip, + defaults.rewards.issuanceRate, + dripBlock, + ) + expect(await tracker.accRewards(dripBlock)).to.eq(0) + const expectedNextDeadline = dripBlock.add(defaults.rewards.dripInterval) + const expectedMintedAmount = await tracker.accRewards(expectedNextDeadline) + + const tx1 = await l1Reservoir + .connect(keeper.signer) + ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), keeper.address) + + const minInterval = toBN('200') + await l1Reservoir.connect(governor.signer).setMinDripInterval(minInterval) + + const actualAmount = await grt.balanceOf(l1Reservoir.address) + + expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount)) + await expect(tx1) + .emit(l1Reservoir, 'RewardsDripped') + .withArgs(actualAmount, toBN(0), expectedNextDeadline) + await expect(tx1) + .emit(grt, 'Transfer') + .withArgs(AddressZero, l1Reservoir.address, actualAmount) + + const tx2 = l1Reservoir + .connect(keeper.signer) + ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), keeper.address) + await expect(tx2).revertedWith('WAIT_FOR_MIN_INTERVAL') + + // We've had 1 block since the last drip so far, so we jump to one block before the interval is done + await advanceBlocks(minInterval.sub(2)) + const tx3 = l1Reservoir + .connect(keeper.signer) + ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), keeper.address) + await expect(tx3).revertedWith('WAIT_FOR_MIN_INTERVAL') + + await advanceBlocks(1) + // Now we're over the interval so we can drip again + const tx4 = l1Reservoir + .connect(keeper.signer) + ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), keeper.address) + await expect(tx4).emit(l1Reservoir, 'RewardsDripped') + }) + it('prevents locking eth in the contract if l2RewardsFraction is 0', async function () { + const tx = l1Reservoir + .connect(keeper.signer) + ['drip(uint256,uint256,uint256,address)']( + maxGas, + gasPriceBid, + maxSubmissionCost, + keeper.address, + { value: defaultEthValue }, + ) + await expect(tx).revertedWith('No eth value needed') + }) + it('mints only a few more tokens if called on the next block', async function () { + await sequentialDoubleDrip(toBN(0)) + }) + it('mints the right amount of tokens if called before the drip period is over', async function () { + const dripInterval = toBN('100') + await l1Reservoir.connect(governor.signer).setDripInterval(dripInterval) + await sequentialDoubleDrip(toBN('50'), dripInterval) + }) + it('mints the right amount of tokens filling the gap if called after the drip period is over', async function () { + const dripInterval = toBN('100') + await l1Reservoir.connect(governor.signer).setDripInterval(dripInterval) + await sequentialDoubleDrip(toBN('150'), dripInterval) + }) + it('sends the specified fraction of the rewards with a callhook to L2', async function () { + await l1Reservoir.connect(governor.signer).setL2RewardsFraction(toGRT('0.5')) + supplyBeforeDrip = await grt.totalSupply() + const startAccrued = await l1Reservoir.getAccumulatedRewards(await latestBlock()) + expect(startAccrued).to.eq(0) + const dripBlock = (await latestBlock()).add(1) // We're gonna drip in the next transaction + const tracker = await RewardsTracker.create( + supplyBeforeDrip, + defaults.rewards.issuanceRate, + dripBlock, + ) + expect(await tracker.accRewards(dripBlock)).to.eq(0) + const expectedNextDeadline = dripBlock.add(defaults.rewards.dripInterval) + const expectedMintedAmount = await tracker.accRewards(expectedNextDeadline) + const expectedSentToL2 = expectedMintedAmount.div(2) + const tx = await l1Reservoir + .connect(keeper.signer) + ['drip(uint256,uint256,uint256,address)']( + maxGas, + gasPriceBid, + maxSubmissionCost, + keeper.address, + { value: defaultEthValue }, + ) + const actualAmount = await grt.balanceOf(l1Reservoir.address) + const escrowedAmount = await grt.balanceOf(bridgeEscrow.address) + expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount.sub(expectedSentToL2))) + expect(toRound((await grt.totalSupply()).sub(supplyBeforeDrip))).to.eq( + toRound(expectedMintedAmount), + ) + expect(toRound(escrowedAmount)).to.eq(toRound(expectedSentToL2)) + await expect(tx) + .emit(l1Reservoir, 'RewardsDripped') + .withArgs(actualAmount.add(escrowedAmount), escrowedAmount, expectedNextDeadline) + + const l2IssuanceBase = (await l1Reservoir.issuanceBase()) + .mul(await l1Reservoir.l2RewardsFraction()) + .div(toGRT('1')) + const issuanceRate = await l1Reservoir.issuanceRate() + const expectedCallhookData = l2ReservoirIface.encodeFunctionData('receiveDrip', [ + l2IssuanceBase, + issuanceRate, + toBN('0'), + toBN('0'), + keeper.address, + ]) + const expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( + grt.address, + l1Reservoir.address, + mockL2Reservoir.address, + escrowedAmount, + expectedCallhookData, + ) + await expect(tx) + .emit(l1GraphTokenGateway, 'TxToL2') + .withArgs(l1Reservoir.address, mockL2Gateway.address, toBN(1), expectedL2Data) + }) + it('sends the specified fraction of the rewards with a keeper reward to L2', async function () { + await l1Reservoir.connect(governor.signer).setL2RewardsFraction(toGRT('0.5')) + await l1Reservoir.connect(governor.signer).setDripRewardPerBlock(toGRT('3')) + await l1Reservoir.connect(governor.signer).setMinDripInterval(toBN('2')) + + await advanceBlocks(toBN('4')) + + supplyBeforeDrip = await grt.totalSupply() + const issuanceBase = await l1Reservoir.issuanceBase() + const startAccrued = await l1Reservoir.getAccumulatedRewards(await latestBlock()) + expect(startAccrued).to.eq(0) + const dripBlock = (await latestBlock()).add(1) // We're gonna drip in the next transaction + const expectedKeeperReward = dripBlock + .sub(await l1Reservoir.lastRewardsUpdateBlock()) + .mul(toGRT('3')) + const tracker = await RewardsTracker.create( + issuanceBase, + defaults.rewards.issuanceRate, + dripBlock, + ) + expect(await tracker.accRewards(dripBlock)).to.eq(0) + const expectedNextDeadline = dripBlock.add(defaults.rewards.dripInterval) + const expectedMintedRewards = await tracker.accRewards(expectedNextDeadline) + const expectedMintedAmount = expectedMintedRewards.add(expectedKeeperReward) + const expectedSentToL2 = expectedMintedRewards.div(2).add(expectedKeeperReward) + const tx = await l1Reservoir + .connect(keeper.signer) + ['drip(uint256,uint256,uint256,address)']( + maxGas, + gasPriceBid, + maxSubmissionCost, + keeper.address, + { value: defaultEthValue }, + ) + const actualAmount = await grt.balanceOf(l1Reservoir.address) + const escrowedAmount = await grt.balanceOf(bridgeEscrow.address) + + expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount.sub(expectedSentToL2))) + expect(toRound((await grt.totalSupply()).sub(supplyBeforeDrip))).to.eq( + toRound(expectedMintedAmount), + ) + expect(toRound(escrowedAmount)).to.eq(toRound(expectedSentToL2)) + await expect(tx) + .emit(l1Reservoir, 'RewardsDripped') + .withArgs(actualAmount.add(escrowedAmount), escrowedAmount, expectedNextDeadline) + + const l2IssuanceBase = (await l1Reservoir.issuanceBase()) + .mul(await l1Reservoir.l2RewardsFraction()) + .div(toGRT('1')) + const issuanceRate = await l1Reservoir.issuanceRate() + const expectedCallhookData = l2ReservoirIface.encodeFunctionData('receiveDrip', [ + l2IssuanceBase, + issuanceRate, + toBN('0'), + expectedKeeperReward, + keeper.address, + ]) + const expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( + grt.address, + l1Reservoir.address, + mockL2Reservoir.address, + escrowedAmount, + expectedCallhookData, + ) + await expect(tx) + .emit(l1GraphTokenGateway, 'TxToL2') + .withArgs(l1Reservoir.address, mockL2Gateway.address, toBN(1), expectedL2Data) + }) + it('sends the outstanding amount if the L2 rewards fraction changes', async function () { + await l1Reservoir.connect(governor.signer).setL2RewardsFraction(toGRT('0.5')) + supplyBeforeDrip = await grt.totalSupply() + const startAccrued = await l1Reservoir.getAccumulatedRewards(await latestBlock()) + expect(startAccrued).to.eq(0) + const dripBlock = (await latestBlock()).add(1) // We're gonna drip in the next transaction + const tracker = await RewardsTracker.create( + supplyBeforeDrip, + defaults.rewards.issuanceRate, + dripBlock, + ) + expect(await tracker.accRewards(dripBlock)).to.eq(0) + const expectedNextDeadline = dripBlock.add(defaults.rewards.dripInterval) + const expectedMintedAmount = await tracker.accRewards(expectedNextDeadline) + const expectedSentToL2 = expectedMintedAmount.div(2) + const tx = await l1Reservoir + .connect(keeper.signer) + ['drip(uint256,uint256,uint256,address)']( + maxGas, + gasPriceBid, + maxSubmissionCost, + keeper.address, + { value: defaultEthValue }, + ) + const actualAmount = await grt.balanceOf(l1Reservoir.address) + const escrowedAmount = await grt.balanceOf(bridgeEscrow.address) + expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount.sub(expectedSentToL2))) + expect(toRound((await grt.totalSupply()).sub(supplyBeforeDrip))).to.eq( + toRound(expectedMintedAmount), + ) + expect(toRound(escrowedAmount)).to.eq(toRound(expectedSentToL2)) + await expect(tx) + .emit(l1Reservoir, 'RewardsDripped') + .withArgs(actualAmount.add(escrowedAmount), escrowedAmount, expectedNextDeadline) + + let l2IssuanceBase = (await l1Reservoir.issuanceBase()) + .mul(await l1Reservoir.l2RewardsFraction()) + .div(toGRT('1')) + const issuanceRate = await l1Reservoir.issuanceRate() + let expectedCallhookData = l2ReservoirIface.encodeFunctionData('receiveDrip', [ + l2IssuanceBase, + issuanceRate, + toBN('0'), + toBN('0'), + keeper.address, + ]) + let expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( + grt.address, + l1Reservoir.address, + mockL2Reservoir.address, + escrowedAmount, + expectedCallhookData, + ) + await expect(tx) + .emit(l1GraphTokenGateway, 'TxToL2') + .withArgs(l1Reservoir.address, mockL2Gateway.address, toBN(1), expectedL2Data) + + await tracker.snapshotRewards() + + await l1Reservoir.connect(governor.signer).setL2RewardsFraction(toGRT('0.8')) + supplyBeforeDrip = await grt.totalSupply() + const secondDripBlock = (await latestBlock()).add(1) + const expectedNewNextDeadline = secondDripBlock.add(defaults.rewards.dripInterval) + const rewardsUntilSecondDripBlock = await tracker.accRewards(secondDripBlock) + const expectedTotalRewards = await tracker.accRewards(expectedNewNextDeadline) + const expectedNewMintedAmount = expectedTotalRewards.sub(expectedMintedAmount) + // The amount sent to L2 should cover up to the new drip block with the old fraction, + // and from then onwards with the new fraction + const expectedNewTotalSentToL2 = rewardsUntilSecondDripBlock + .div(2) + .add(expectedTotalRewards.sub(rewardsUntilSecondDripBlock).mul(8).div(10)) + + const tx2 = await l1Reservoir + .connect(keeper.signer) + ['drip(uint256,uint256,uint256,address)']( + maxGas, + gasPriceBid, + maxSubmissionCost, + keeper.address, + { value: defaultEthValue }, + ) + const newActualAmount = await grt.balanceOf(l1Reservoir.address) + const newEscrowedAmount = await grt.balanceOf(bridgeEscrow.address) + expect(toRound(newActualAmount)).to.eq( + toRound(expectedTotalRewards.sub(expectedNewTotalSentToL2)), + ) + expect(toRound((await grt.totalSupply()).sub(supplyBeforeDrip))).to.eq( + toRound(expectedNewMintedAmount), + ) + expect(toRound(newEscrowedAmount)).to.eq(toRound(expectedNewTotalSentToL2)) + l2IssuanceBase = (await l1Reservoir.issuanceBase()) + .mul(await l1Reservoir.l2RewardsFraction()) + .div(toGRT('1')) + expectedCallhookData = l2ReservoirIface.encodeFunctionData('receiveDrip', [ + l2IssuanceBase, + issuanceRate, + toBN('1'), // Incremented nonce + toBN('0'), + keeper.address, + ]) + expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( + grt.address, + l1Reservoir.address, + mockL2Reservoir.address, + newEscrowedAmount.sub(escrowedAmount), + expectedCallhookData, + ) + await expect(tx2) + .emit(l1GraphTokenGateway, 'TxToL2') + .withArgs(l1Reservoir.address, mockL2Gateway.address, toBN(2), expectedL2Data) + await expect(tx2) + .emit(l1Reservoir, 'RewardsDripped') + .withArgs( + newActualAmount.add(newEscrowedAmount).sub(actualAmount.add(escrowedAmount)), + newEscrowedAmount.sub(escrowedAmount), + expectedNewNextDeadline, + ) + }) + it('sends the outstanding amount if the L2 rewards fraction stays constant', async function () { + await l1Reservoir.connect(governor.signer).setL2RewardsFraction(toGRT('0.5')) + supplyBeforeDrip = await grt.totalSupply() + const startAccrued = await l1Reservoir.getAccumulatedRewards(await latestBlock()) + expect(startAccrued).to.eq(0) + const dripBlock = (await latestBlock()).add(1) // We're gonna drip in the next transaction + const tracker = await RewardsTracker.create( + supplyBeforeDrip, + defaults.rewards.issuanceRate, + dripBlock, + ) + expect(await tracker.accRewards(dripBlock)).to.eq(0) + const expectedNextDeadline = dripBlock.add(defaults.rewards.dripInterval) + const expectedMintedAmount = await tracker.accRewards(expectedNextDeadline) + const expectedSentToL2 = expectedMintedAmount.div(2) + const tx = await l1Reservoir + .connect(keeper.signer) + ['drip(uint256,uint256,uint256,address)']( + maxGas, + gasPriceBid, + maxSubmissionCost, + keeper.address, + { value: defaultEthValue }, + ) + const actualAmount = await grt.balanceOf(l1Reservoir.address) + const escrowedAmount = await grt.balanceOf(bridgeEscrow.address) + expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount.sub(expectedSentToL2))) + expect(toRound((await grt.totalSupply()).sub(supplyBeforeDrip))).to.eq( + toRound(expectedMintedAmount), + ) + expect(toRound(escrowedAmount)).to.eq(toRound(expectedSentToL2)) + await expect(tx) + .emit(l1Reservoir, 'RewardsDripped') + .withArgs(actualAmount.add(escrowedAmount), escrowedAmount, expectedNextDeadline) + + let l2IssuanceBase = (await l1Reservoir.issuanceBase()) + .mul(await l1Reservoir.l2RewardsFraction()) + .div(toGRT('1')) + const issuanceRate = await l1Reservoir.issuanceRate() + let expectedCallhookData = l2ReservoirIface.encodeFunctionData('receiveDrip', [ + l2IssuanceBase, + issuanceRate, + toBN('0'), + toBN('0'), + keeper.address, + ]) + let expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( + grt.address, + l1Reservoir.address, + mockL2Reservoir.address, + escrowedAmount, + expectedCallhookData, + ) + await expect(tx) + .emit(l1GraphTokenGateway, 'TxToL2') + .withArgs(l1Reservoir.address, mockL2Gateway.address, toBN(1), expectedL2Data) + + await tracker.snapshotRewards() + + supplyBeforeDrip = await grt.totalSupply() + const secondDripBlock = (await latestBlock()).add(1) + const expectedNewNextDeadline = secondDripBlock.add(defaults.rewards.dripInterval) + const expectedTotalRewards = await tracker.accRewards(expectedNewNextDeadline) + const expectedNewMintedAmount = expectedTotalRewards.sub(expectedMintedAmount) + // The amount sent to L2 should cover up to the new drip block with the old fraction, + // and from then onwards with the new fraction + const expectedNewTotalSentToL2 = expectedTotalRewards.div(2) + + const tx2 = await l1Reservoir + .connect(keeper.signer) + ['drip(uint256,uint256,uint256,address)']( + maxGas, + gasPriceBid, + maxSubmissionCost, + keeper.address, + { value: defaultEthValue }, + ) + const newActualAmount = await grt.balanceOf(l1Reservoir.address) + const newEscrowedAmount = await grt.balanceOf(bridgeEscrow.address) + expect(toRound(newActualAmount)).to.eq( + toRound(expectedTotalRewards.sub(expectedNewTotalSentToL2)), + ) + expect(toRound((await grt.totalSupply()).sub(supplyBeforeDrip))).to.eq( + toRound(expectedNewMintedAmount), + ) + expect(toRound(newEscrowedAmount)).to.eq(toRound(expectedNewTotalSentToL2)) + l2IssuanceBase = (await l1Reservoir.issuanceBase()) + .mul(await l1Reservoir.l2RewardsFraction()) + .div(toGRT('1')) + expectedCallhookData = l2ReservoirIface.encodeFunctionData('receiveDrip', [ + l2IssuanceBase, + issuanceRate, + toBN('1'), // Incremented nonce + toBN('0'), + keeper.address, + ]) + expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( + grt.address, + l1Reservoir.address, + mockL2Reservoir.address, + newEscrowedAmount.sub(escrowedAmount), + expectedCallhookData, + ) + await expect(tx2) + .emit(l1GraphTokenGateway, 'TxToL2') + .withArgs(l1Reservoir.address, mockL2Gateway.address, toBN(2), expectedL2Data) + await expect(tx2) + .emit(l1Reservoir, 'RewardsDripped') + .withArgs( + newActualAmount.add(newEscrowedAmount).sub(actualAmount.add(escrowedAmount)), + newEscrowedAmount.sub(escrowedAmount), + expectedNewNextDeadline, + ) + }) + + it('reverts for a while but can be called again later if L2 fraction goes to zero', async function () { + await l1Reservoir.connect(governor.signer).setL2RewardsFraction(toGRT('0.5')) + + // First drip call, sending half the rewards to L2 + supplyBeforeDrip = await grt.totalSupply() + const startAccrued = await l1Reservoir.getAccumulatedRewards(await latestBlock()) + expect(startAccrued).to.eq(0) + const dripBlock = (await latestBlock()).add(1) // We're gonna drip in the next transaction + const tracker = await RewardsTracker.create( + supplyBeforeDrip, + defaults.rewards.issuanceRate, + dripBlock, + ) + expect(await tracker.accRewards(dripBlock)).to.eq(0) + const expectedNextDeadline = dripBlock.add(defaults.rewards.dripInterval) + const expectedMintedAmount = await tracker.accRewards(expectedNextDeadline) + const expectedSentToL2 = expectedMintedAmount.div(2) + const tx = await l1Reservoir + .connect(keeper.signer) + ['drip(uint256,uint256,uint256,address)']( + maxGas, + gasPriceBid, + maxSubmissionCost, + keeper.address, + { value: defaultEthValue }, + ) + const actualAmount = await grt.balanceOf(l1Reservoir.address) + const escrowedAmount = await grt.balanceOf(bridgeEscrow.address) + expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount.sub(expectedSentToL2))) + expect(toRound((await grt.totalSupply()).sub(supplyBeforeDrip))).to.eq( + toRound(expectedMintedAmount), + ) + expect(toRound(escrowedAmount)).to.eq(toRound(expectedSentToL2)) + await expect(tx) + .emit(l1Reservoir, 'RewardsDripped') + .withArgs(actualAmount.add(escrowedAmount), escrowedAmount, expectedNextDeadline) + + let l2IssuanceBase = (await l1Reservoir.issuanceBase()) + .mul(await l1Reservoir.l2RewardsFraction()) + .div(toGRT('1')) + const issuanceRate = await l1Reservoir.issuanceRate() + let expectedCallhookData = l2ReservoirIface.encodeFunctionData('receiveDrip', [ + l2IssuanceBase, + issuanceRate, + toBN('0'), + toBN('0'), + keeper.address, + ]) + let expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( + grt.address, + l1Reservoir.address, + mockL2Reservoir.address, + escrowedAmount, + expectedCallhookData, + ) + await expect(tx) + .emit(l1GraphTokenGateway, 'TxToL2') + .withArgs(l1Reservoir.address, mockL2Gateway.address, toBN(1), expectedL2Data) + + await tracker.snapshotRewards() + + await l1Reservoir.connect(governor.signer).setL2RewardsFraction(toGRT('0')) + + // Second attempt to drip immediately afterwards will revert, because we + // would have to send negative tokens to L2 to compensate + const tx2 = l1Reservoir + .connect(keeper.signer) + ['drip(uint256,uint256,uint256,address)']( + maxGas, + gasPriceBid, + maxSubmissionCost, + keeper.address, + { value: defaultEthValue }, + ) + await expect(tx2).revertedWith( + 'Negative amount would be sent to L2, wait before calling again', + ) + + await advanceBlocks(await l1Reservoir.dripInterval()) + + // Now we should be able to drip again, and a small amount will be sent to L2 + // to cover the few blocks since the drip interval was over + supplyBeforeDrip = await grt.totalSupply() + const secondDripBlock = (await latestBlock()).add(1) + const expectedNewNextDeadline = secondDripBlock.add(defaults.rewards.dripInterval) + const rewardsUntilSecondDripBlock = await tracker.accRewards(secondDripBlock) + const expectedTotalRewards = await tracker.accRewards(expectedNewNextDeadline) + const expectedNewMintedAmount = expectedTotalRewards.sub(expectedMintedAmount) + // The amount sent to L2 should cover up to the new drip block with the old fraction, + // and from then onwards with the new fraction, that is zero + const expectedNewTotalSentToL2 = rewardsUntilSecondDripBlock.div(2) + + const tx3 = await l1Reservoir + .connect(keeper.signer) + ['drip(uint256,uint256,uint256,address)']( + maxGas, + gasPriceBid, + maxSubmissionCost, + keeper.address, + { value: defaultEthValue }, + ) + const newActualAmount = await grt.balanceOf(l1Reservoir.address) + const newEscrowedAmount = await grt.balanceOf(bridgeEscrow.address) + expect(toRound(newActualAmount)).to.eq( + toRound(expectedTotalRewards.sub(expectedNewTotalSentToL2)), + ) + expect(toRound((await grt.totalSupply()).sub(supplyBeforeDrip))).to.eq( + toRound(expectedNewMintedAmount), + ) + expect(toRound(newEscrowedAmount)).to.eq(toRound(expectedNewTotalSentToL2)) + l2IssuanceBase = (await l1Reservoir.issuanceBase()) + .mul(await l1Reservoir.l2RewardsFraction()) + .div(toGRT('1')) + expectedCallhookData = l2ReservoirIface.encodeFunctionData('receiveDrip', [ + l2IssuanceBase, + issuanceRate, + toBN('1'), // Incremented nonce + toBN('0'), + keeper.address, + ]) + expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( + grt.address, + l1Reservoir.address, + mockL2Reservoir.address, + newEscrowedAmount.sub(escrowedAmount), + expectedCallhookData, + ) + await expect(tx3) + .emit(l1GraphTokenGateway, 'TxToL2') + .withArgs(l1Reservoir.address, mockL2Gateway.address, toBN(2), expectedL2Data) + await expect(tx3) + .emit(l1Reservoir, 'RewardsDripped') + .withArgs( + newActualAmount.add(newEscrowedAmount).sub(actualAmount.add(escrowedAmount)), + newEscrowedAmount.sub(escrowedAmount), + expectedNewNextDeadline, + ) + }) + }) + + context('calculating rewards', async function () { + beforeEach(async function () { + // 5% minute rate (4 blocks) + await l1Reservoir.connect(governor.signer).setIssuanceRate(ISSUANCE_RATE_PER_BLOCK) + supplyBeforeDrip = await grt.totalSupply() + await l1Reservoir + .connect(keeper.signer) + ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), keeper.address) + dripBlock = await latestBlock() + }) + + describe('getAccumulatedRewards', function () { + it('returns rewards accrued after some blocks', async function () { + await shouldGetNewRewards(supplyBeforeDrip) + }) + it('returns zero if evaluated at the block where reservoir had the first drip', async function () { + await shouldGetNewRewards( + supplyBeforeDrip, + ISSUANCE_RATE_PERIODS, + dripBlock, + toBN(0), + false, + ) + }) + it('returns the supply times issuance rate one block after the first drip', async function () { + const expectedVal = supplyBeforeDrip + .mul(ISSUANCE_RATE_PER_BLOCK.sub(toGRT(1))) + .div(toGRT(1)) + await shouldGetNewRewards( + supplyBeforeDrip, + ISSUANCE_RATE_PERIODS, + dripBlock.add(1), + expectedVal, + false, + ) + }) + it('returns the rewards for a block some time in the future', async function () { + await shouldGetNewRewards(supplyBeforeDrip, toBN(1), dripBlock.add(10000)) + }) + }) + describe('getNewRewards', function () { + const computeDelta = function (t1: BigNumber, t0: BigNumber, lambda = toBN(0)): BigNumber { + const deltaT = new BN(t1.toString()).minus(new BN(t0.toString())) + const rate = new BN(ISSUANCE_RATE_PER_BLOCK.toString()).div(1e18) + const supply = new BN(supplyBeforeDrip.toString()) + return toBN(supply.times(rate.pow(deltaT)).minus(supply).precision(18).toString(10)) + .mul(toGRT('1').sub(lambda)) + .div(toGRT('1')) + } + it('computes the rewards delta between the last drip block and the current block', async function () { + const t0 = dripBlock + const t1 = t0.add(200) + const expectedVal = computeDelta(t1, t0) + expect(toRound(await l1Reservoir.getNewRewards(t1))).to.eq(toRound(expectedVal)) + }) + it('returns zero rewards if the time delta is zero', async function () { + const t0 = dripBlock + const expectedVal = toBN('0') + expect(await l1Reservoir.getNewRewards(t0)).to.eq(expectedVal) + }) + it('computes the rewards delta between a past drip block and a future block', async function () { + await advanceBlocks(20) + const t0 = dripBlock + const t1 = t0.add(100) + const expectedVal = computeDelta(t1, t0) + expect(toRound(await l1Reservoir.getNewRewards(t1))).to.eq(toRound(expectedVal)) + }) + it('computes the rewards delta between a past drip block and the current block', async function () { + await advanceBlocks(20) + const t0 = dripBlock + const t1 = await latestBlock() + const expectedVal = computeDelta(t1, t0) + expect(toRound(await l1Reservoir.getNewRewards(t1))).to.eq(toRound(expectedVal)) + }) + it('computes the rewards delta considering the L2 rewards fraction', async function () { + const lambda = toGRT('0.32') + await l1Reservoir.connect(governor.signer).setL2RewardsFraction(lambda) + await l1Reservoir + .connect(keeper.signer) + ['drip(uint256,uint256,uint256,address)']( + maxGas, + gasPriceBid, + maxSubmissionCost, + keeper.address, + { value: defaultEthValue }, + ) + supplyBeforeDrip = await l1Reservoir.issuanceBase() // Has been updated accordingly + dripBlock = await latestBlock() + await advanceBlocks(20) + const t0 = dripBlock + const t1 = await latestBlock() + + const expectedVal = computeDelta(t1, t0, lambda) + expect(toRound(await l1Reservoir.getNewRewards(t1))).to.eq(toRound(expectedVal)) + }) + }) + }) + + describe('pow', function () { + it('exponentiation works under normal boundaries (annual rate from 1% to 700%, 90 days period)', async function () { + const baseRatio = toGRT('0.000000004641377923') // 1% annual rate + const timePeriods = (60 * 60 * 24 * 10) / 15 // 90 days in blocks + const powPrecision = 14 // Compare up to this amount of significant digits + BN.config({ POW_PRECISION: 100 }) + for (let i = 0; i < 50; i = i + 4) { + const r = baseRatio.mul(i * 4).add(toGRT('1')) + const h = await reservoirMock.pow(r, timePeriods, toGRT('1')) + console.log('\tr:', formatGRT(r), '=> c:', formatGRT(h)) + expect(new BN(h.toString()).precision(powPrecision).toString(10)).to.eq( + new BN(r.toString()) + .div(1e18) + .pow(timePeriods) + .times(1e18) + .precision(powPrecision) + .toString(10), + ) + } + }) + }) +}) diff --git a/test/rewards/rewards.test.ts b/test/rewards/rewards.test.ts index 2c9021d76..38639acc4 100644 --- a/test/rewards/rewards.test.ts +++ b/test/rewards/rewards.test.ts @@ -1,17 +1,16 @@ import { expect } from 'chai' -import { constants, BigNumber } from 'ethers' -import { BigNumber as BN } from 'bignumber.js' +import { constants, BigNumber, ContractReceipt } from 'ethers' -import { deployContract } from '../lib/deployment' import { NetworkFixture } from '../lib/fixtures' import { Curation } from '../../build/types/Curation' import { EpochManager } from '../../build/types/EpochManager' import { GraphToken } from '../../build/types/GraphToken' import { RewardsManager } from '../../build/types/RewardsManager' -import { RewardsManagerMock } from '../../build/types/RewardsManagerMock' import { Staking } from '../../build/types/Staking' +import { BigNumber as BN } from 'bignumber.js' + import { advanceBlocks, deriveChannelKey, @@ -24,7 +23,10 @@ import { Account, advanceToNextEpoch, provider, + RewardsTracker, } from '../lib/testHelpers' +import { L1Reservoir } from '../../build/types/L1Reservoir' +import { LogDescription } from 'ethers/lib/utils' const MAX_PPM = 1000000 @@ -40,6 +42,7 @@ describe('Rewards', () => { let indexer1: Account let indexer2: Account let oracle: Account + let keeper: Account let fixture: NetworkFixture @@ -48,7 +51,10 @@ describe('Rewards', () => { let epochManager: EpochManager let staking: Staking let rewardsManager: RewardsManager - let rewardsManagerMock: RewardsManagerMock + let l1Reservoir: L1Reservoir + + let supplyBeforeDrip: BigNumber + let dripBlock: BigNumber // Derive some channel keys for each indexer used to sign attestations const channelKey1 = deriveChannelKey() @@ -62,64 +68,18 @@ describe('Rewards', () => { const metadata = HashZero - const ISSUANCE_RATE_PERIODS = 4 // blocks required to issue 5% rewards - const ISSUANCE_RATE_PER_BLOCK = toBN('1012272234429039270') // % increase every block - - // Core formula that gets accumulated rewards per signal for a period of time - const getRewardsPerSignal = (p: BN, r: BN, t: BN, s: BN): string => { - if (s.eq(0)) { - return '0' - } - return p.times(r.pow(t)).minus(p).div(s).toPrecision(18).toString() - } - - // Tracks the accumulated rewards as totalSignalled or supply changes across snapshots - class RewardsTracker { - totalSupply = BigNumber.from(0) - totalSignalled = BigNumber.from(0) - lastUpdatedBlock = BigNumber.from(0) - accumulated = BigNumber.from(0) - - static async create() { - const tracker = new RewardsTracker() - await tracker.snapshot() - return tracker - } - - async snapshot() { - this.accumulated = this.accumulated.add(await this.accrued()) - this.totalSupply = await grt.totalSupply() - this.totalSignalled = await grt.balanceOf(curation.address) - this.lastUpdatedBlock = await latestBlock() - return this - } - - async elapsedBlocks() { - const currentBlock = await latestBlock() - return currentBlock.sub(this.lastUpdatedBlock) - } - - async accrued() { - const nBlocks = await this.elapsedBlocks() - return this.accruedByElapsed(nBlocks) - } - - async accruedByElapsed(nBlocks: BigNumber | number) { - const n = getRewardsPerSignal( - new BN(this.totalSupply.toString()), - new BN(ISSUANCE_RATE_PER_BLOCK.toString()).div(1e18), - new BN(nBlocks.toString()), - new BN(this.totalSignalled.toString()), - ) - return toGRT(n) - } - } + const ISSUANCE_RATE_PERIODS = 4 // blocks required to issue 0.05% rewards + const ISSUANCE_RATE_PER_BLOCK = toBN('1000122722344290393') // % increase every block // Test accumulated rewards per signal - const shouldGetNewRewardsPerSignal = async (nBlocks = ISSUANCE_RATE_PERIODS) => { + const shouldGetNewRewardsPerSignal = async ( + initialSupply: BigNumber, + nBlocks = ISSUANCE_RATE_PERIODS, + dripBlock?: BigNumber, + ) => { // -- t0 -- - const tracker = await RewardsTracker.create() - + const tracker = await RewardsTracker.create(initialSupply, ISSUANCE_RATE_PER_BLOCK, dripBlock) + await tracker.snapshotPerSignal(await grt.balanceOf(curation.address)) // Jump await advanceBlocks(nBlocks) @@ -128,35 +88,43 @@ describe('Rewards', () => { // Contract calculation const contractAccrued = await rewardsManager.getNewRewardsPerSignal() // Local calculation - const expectedAccrued = await tracker.accrued() + const expectedAccrued = await tracker.newRewardsPerSignal(await grt.balanceOf(curation.address)) // Check - expect(toRound(expectedAccrued)).eq(toRound(contractAccrued)) + expect(toRound(contractAccrued)).eq(toRound(expectedAccrued)) return expectedAccrued } + const findRewardsManagerEvents = (receipt: ContractReceipt): Array => { + return receipt.logs + .map((l) => { + try { + return rewardsManager.interface.parseLog(l) + } catch { + return null + } + }) + .filter((l) => !!l) + } + before(async function () { - ;[delegator, governor, curator1, curator2, indexer1, indexer2, oracle] = await getAccounts() + ;[delegator, governor, curator1, curator2, indexer1, indexer2, oracle, keeper] = + await getAccounts() fixture = new NetworkFixture() - ;({ grt, curation, epochManager, staking, rewardsManager } = await fixture.load( + ;({ grt, curation, epochManager, staking, rewardsManager, l1Reservoir } = await fixture.load( governor.signer, )) - rewardsManagerMock = (await deployContract( - 'RewardsManagerMock', - governor.signer, - )) as unknown as RewardsManagerMock - - // 5% minute rate (4 blocks) - await rewardsManager.connect(governor.signer).setIssuanceRate(ISSUANCE_RATE_PER_BLOCK) - // Distribute test funds for (const wallet of [indexer1, indexer2, curator1, curator2]) { await grt.connect(governor.signer).mint(wallet.address, toGRT('1000000')) await grt.connect(wallet.signer).approve(staking.address, toGRT('1000000')) await grt.connect(wallet.signer).approve(curation.address, toGRT('1000000')) } + await l1Reservoir.connect(governor.signer).grantDripPermission(keeper.address) + await l1Reservoir.connect(governor.signer).initialSnapshot(toBN(0)) + supplyBeforeDrip = await grt.totalSupply() }) beforeEach(async function () { @@ -168,32 +136,6 @@ describe('Rewards', () => { }) describe('configuration', function () { - describe('issuance rate update', function () { - it('reject set issuance rate if unauthorized', async function () { - const tx = rewardsManager.connect(indexer1.signer).setIssuanceRate(toGRT('1.025')) - await expect(tx).revertedWith('Caller must be Controller governor') - }) - - it('reject set issuance rate to less than minimum allowed', async function () { - const newIssuanceRate = toGRT('0.1') // this get a bignumber with 1e17 - const tx = rewardsManager.connect(governor.signer).setIssuanceRate(newIssuanceRate) - await expect(tx).revertedWith('Issuance rate under minimum allowed') - }) - - it('should set issuance rate to minimum allowed', async function () { - const newIssuanceRate = toGRT('1') // this get a bignumber with 1e18 - await rewardsManager.connect(governor.signer).setIssuanceRate(newIssuanceRate) - expect(await rewardsManager.issuanceRate()).eq(newIssuanceRate) - }) - - it('should set issuance rate', async function () { - const newIssuanceRate = toGRT('1.025') - await rewardsManager.connect(governor.signer).setIssuanceRate(newIssuanceRate) - expect(await rewardsManager.issuanceRate()).eq(newIssuanceRate) - expect(await rewardsManager.accRewardsPerSignalLastBlockUpdated()).eq(await latestBlock()) - }) - }) - describe('subgraph availability service', function () { it('reject set subgraph oracle if unauthorized', async function () { const tx = rewardsManager @@ -243,9 +185,104 @@ describe('Rewards', () => { }) context('issuing rewards', async function () { + interface DelegationParameters { + indexingRewardCut: BigNumber + queryFeeCut: BigNumber + cooldownBlocks: number + } + + async function setupIndexerAllocation() { + // Update total signalled + const signalled1 = toGRT('1500') + await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) + + // Allocate + const tokensToAllocate = toGRT('12500') + await staking.connect(indexer1.signer).stake(tokensToAllocate) + await staking + .connect(indexer1.signer) + .allocateFrom( + indexer1.address, + subgraphDeploymentID1, + tokensToAllocate, + allocationID1, + metadata, + await channelKey1.generateProof(indexer1.address), + ) + } + + async function setupIndexerAllocationWithDelegation( + tokensToDelegate: BigNumber, + delegationParams: DelegationParameters, + ) { + const tokensToAllocate = toGRT('12500') + + // Transfer some funds from the curator, I don't want to mint new tokens + await grt.connect(curator1.signer).transfer(delegator.address, tokensToDelegate) + await grt.connect(delegator.signer).approve(staking.address, tokensToDelegate) + + // Stake and set delegation parameters + await staking.connect(indexer1.signer).stake(tokensToAllocate) + await staking + .connect(indexer1.signer) + .setDelegationParameters( + delegationParams.indexingRewardCut, + delegationParams.queryFeeCut, + delegationParams.cooldownBlocks, + ) + + // Delegate + await staking.connect(delegator.signer).delegate(indexer1.address, tokensToDelegate) + + // Update total signalled + const signalled1 = toGRT('1500') + await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) + + // Allocate + await staking + .connect(indexer1.signer) + .allocateFrom( + indexer1.address, + subgraphDeploymentID1, + tokensToAllocate, + allocationID1, + metadata, + await channelKey1.generateProof(indexer1.address), + ) + } + + function calculatedExpectedRewards( + firstSnapshotBlocks: BN, + lastSnapshotBlocks: BN, + allocatedTokens: BN, + ): BigNumber { + const issuanceBase = new BN(10004000000) + const issuanceRate = new BN(ISSUANCE_RATE_PER_BLOCK.toString()).div(1e18) + // All the rewards in this subgraph go to this allocation. + // Rewards per token will be (issuanceBase * issuanceRate^nBlocks - issuanceBase) / allocatedTokens + // The first snapshot is after allocating, that is lastSnapshotBlocks blocks after dripBlock: + const startRewardsPerToken = issuanceBase + .times(issuanceRate.pow(firstSnapshotBlocks)) + .minus(issuanceBase) + .div(allocatedTokens) + // The final snapshot is when we close the allocation, that happens 8 blocks later: + const endRewardsPerToken = issuanceBase + .times(issuanceRate.pow(lastSnapshotBlocks)) + .minus(issuanceBase) + .div(allocatedTokens) + // Then our expected rewards are (endRewardsPerToken - startRewardsPerToken) * allocatedTokens. + return toGRT( + endRewardsPerToken.minus(startRewardsPerToken).times(allocatedTokens).toPrecision(18), + ) + } + beforeEach(async function () { // 5% minute rate (4 blocks) - await rewardsManager.connect(governor.signer).setIssuanceRate(ISSUANCE_RATE_PER_BLOCK) + await l1Reservoir.connect(governor.signer).setIssuanceRate(ISSUANCE_RATE_PER_BLOCK) + await l1Reservoir + .connect(keeper.signer) + ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), keeper.address) + dripBlock = await latestBlock() }) describe('getNewRewardsPerSignal', function () { @@ -262,7 +299,7 @@ describe('Rewards', () => { await curation.connect(curator1.signer).mint(subgraphDeploymentID1, tokensToSignal, 0) // Check - await shouldGetNewRewardsPerSignal() + await shouldGetNewRewardsPerSignal(supplyBeforeDrip, ISSUANCE_RATE_PERIODS, dripBlock) }) it('accrued per signal when signalled tokens w/ many subgraphs', async function () { @@ -270,78 +307,112 @@ describe('Rewards', () => { await curation.connect(curator1.signer).mint(subgraphDeploymentID1, toGRT('1000'), 0) // Check - await shouldGetNewRewardsPerSignal() + await shouldGetNewRewardsPerSignal(supplyBeforeDrip, ISSUANCE_RATE_PERIODS, dripBlock) // Update total signalled await curation.connect(curator2.signer).mint(subgraphDeploymentID2, toGRT('250'), 0) // Check - await shouldGetNewRewardsPerSignal() + await shouldGetNewRewardsPerSignal(supplyBeforeDrip, ISSUANCE_RATE_PERIODS, dripBlock) }) }) describe('updateAccRewardsPerSignal', function () { it('update the accumulated rewards per signal state', async function () { + const tracker = await RewardsTracker.create( + supplyBeforeDrip, + ISSUANCE_RATE_PER_BLOCK, + dripBlock, + ) + // Snapshot + const prevSignal = await grt.balanceOf(curation.address) // Update total signalled await curation.connect(curator1.signer).mint(subgraphDeploymentID1, toGRT('1000'), 0) - // Snapshot - const tracker = await RewardsTracker.create() + // Minting signal triggers onSubgraphSignalUpgrade before pulling the GRT, + // so we snapshot using the previous value + await tracker.snapshotPerSignal(prevSignal) // Update await rewardsManager.updateAccRewardsPerSignal() + await tracker.snapshotPerSignal(await grt.balanceOf(curation.address)) + const contractAccrued = await rewardsManager.accRewardsPerSignal() // Check - const expectedAccrued = await tracker.accrued() - expect(toRound(expectedAccrued)).eq(toRound(contractAccrued)) + const blockNum = await latestBlock() + const expectedAccrued = await tracker.accRewardsPerSignal( + await grt.balanceOf(curation.address), + blockNum, + ) + expect(toRound(contractAccrued)).eq(toRound(expectedAccrued)) }) it('update the accumulated rewards per signal state after many blocks', async function () { + const tracker = await RewardsTracker.create( + supplyBeforeDrip, + ISSUANCE_RATE_PER_BLOCK, + dripBlock, + ) + // Snapshot + const prevSignal = await grt.balanceOf(curation.address) // Update total signalled await curation.connect(curator1.signer).mint(subgraphDeploymentID1, toGRT('1000'), 0) - // Snapshot - const tracker = await RewardsTracker.create() + // Minting signal triggers onSubgraphSignalUpgrade before pulling the GRT, + // so we snapshot using the previous value + await tracker.snapshotPerSignal(prevSignal) // Jump await advanceBlocks(ISSUANCE_RATE_PERIODS) // Update await rewardsManager.updateAccRewardsPerSignal() + await tracker.snapshotPerSignal(await grt.balanceOf(curation.address)) const contractAccrued = await rewardsManager.accRewardsPerSignal() - // Check - const expectedAccrued = await tracker.accrued() - expect(toRound(expectedAccrued)).eq(toRound(contractAccrued)) + const blockNum = await latestBlock() + const expectedAccrued = await tracker.accRewardsPerSignal( + await grt.balanceOf(curation.address), + blockNum.add(0), + ) + expect(toRound(contractAccrued)).eq(toRound(expectedAccrued)) }) }) describe('getAccRewardsForSubgraph', function () { it('accrued for each subgraph', async function () { + const tracker = await RewardsTracker.create( + supplyBeforeDrip, + ISSUANCE_RATE_PER_BLOCK, + dripBlock, + ) + // Snapshot + let prevSignal = await grt.balanceOf(curation.address) // Curator1 - Update total signalled const signalled1 = toGRT('1500') await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) - const tracker1 = await RewardsTracker.create() + const sg1Snapshot = await tracker.snapshotPerSignal(prevSignal) // Curator2 - Update total signalled const signalled2 = toGRT('500') + prevSignal = await grt.balanceOf(curation.address) await curation.connect(curator2.signer).mint(subgraphDeploymentID2, signalled2, 0) - - // Snapshot - const tracker2 = await RewardsTracker.create() - await tracker1.snapshot() + const sg2Snapshot = await tracker.snapshotPerSignal(prevSignal) // Jump await advanceBlocks(ISSUANCE_RATE_PERIODS) - // Snapshot - await tracker1.snapshot() - await tracker2.snapshot() - // Calculate rewards - const rewardsPerSignal1 = await tracker1.accumulated - const rewardsPerSignal2 = await tracker2.accumulated - const expectedRewardsSG1 = rewardsPerSignal1.mul(signalled1).div(WeiPerEther) - const expectedRewardsSG2 = rewardsPerSignal2.mul(signalled2).div(WeiPerEther) + const rewardsPerSignal = await tracker.accRewardsPerSignal( + await grt.balanceOf(curation.address), + ) + const expectedRewardsSG1 = rewardsPerSignal + .sub(sg1Snapshot) + .mul(signalled1) + .div(WeiPerEther) + const expectedRewardsSG2 = rewardsPerSignal + .sub(sg2Snapshot) + .mul(signalled2) + .div(WeiPerEther) // Get rewards from contract const contractRewardsSG1 = await rewardsManager.getAccRewardsForSubgraph( @@ -359,27 +430,35 @@ describe('Rewards', () => { describe('onSubgraphSignalUpdate', function () { it('update the accumulated rewards for subgraph state', async function () { + const tracker = await RewardsTracker.create( + supplyBeforeDrip, + ISSUANCE_RATE_PER_BLOCK, + dripBlock, + ) + // Snapshot + const prevSignal = await grt.balanceOf(curation.address) // Update total signalled const signalled1 = toGRT('1500') await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) // Snapshot - const tracker1 = await RewardsTracker.create() + await tracker.snapshotPerSignal(prevSignal) // Jump await advanceBlocks(ISSUANCE_RATE_PERIODS) // Update await rewardsManager.onSubgraphSignalUpdate(subgraphDeploymentID1) - + const snapshot = await tracker.snapshotPerSignal(await grt.balanceOf(curation.address)) // Check const contractRewardsSG1 = (await rewardsManager.subgraphs(subgraphDeploymentID1)) .accRewardsForSubgraph - const rewardsPerSignal1 = await tracker1.accrued() - const expectedRewardsSG1 = rewardsPerSignal1.mul(signalled1).div(WeiPerEther) + const expectedRewardsSG1 = snapshot.mul(signalled1).div(WeiPerEther) expect(toRound(expectedRewardsSG1)).eq(toRound(contractRewardsSG1)) const contractAccrued = await rewardsManager.accRewardsPerSignal() - const expectedAccrued = await tracker1.accrued() + const expectedAccrued = await tracker.accRewardsPerSignal( + await grt.balanceOf(curation.address), + ) expect(toRound(expectedAccrued)).eq(toRound(contractAccrued)) const contractBlockUpdated = await rewardsManager.accRewardsPerSignalLastBlockUpdated() @@ -430,11 +509,14 @@ describe('Rewards', () => { it('update the accumulated rewards for allocated tokens state', async function () { // Update total signalled const signalled1 = toGRT('1500') + // block = dripBlock await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) + // block = dripBlock + 1 // Allocate const tokensToAllocate = toGRT('12500') await staking.connect(indexer1.signer).stake(tokensToAllocate) + // block = dripBlock + 2 await staking .connect(indexer1.signer) .allocateFrom( @@ -445,19 +527,28 @@ describe('Rewards', () => { metadata, await channelKey1.generateProof(indexer1.address), ) - + // block = dripBlock + 3 // Jump await advanceBlocks(ISSUANCE_RATE_PERIODS) - - // Prepare expected results - // NOTE: calculated the expected result manually as the above code has 1 off block difference - // replace with a RewardsManagerMock - const expectedSubgraphRewards = toGRT('891695470') - const expectedRewardsAT = toGRT('51571') + // block = dripBlock + 7 // Update await rewardsManager.onSubgraphAllocationUpdate(subgraphDeploymentID1) + // block = dripBlock + 8 + // Prepare expected results + // Expected total rewards: + // DeltaR_end = supplyBeforeDrip * r ^ 8 - supplyBeforeDrip + // DeltaR_end = 10004000000 GRT * (1000122722344290393 / 1e18)^8 - 10004000000 GRT = 9825934.397 + // The signal was minted at dripBlock + 1, so: + // DeltaR_start = supplyBeforeDrip * r ^ 1 - supplyBeforeDrip = 1227714.332 + + // And they all go to this subgraph, so subgraph rewards = DeltaR_end - DeltaR_start = 8598220.065 + const expectedSubgraphRewards = toGRT('8598220') + + // The allocation happened at dripBlock + 3, so rewards per allocated token are: + // ((supplyBeforeDrip * r ^ 8 - supplyBeforeDrip) - (supplyBeforeDrip * r ^ 3 - supplyBeforeDrip)) / 12500 = 491.387 + const expectedRewardsAT = toGRT('491') // Check on demand results saved const subgraph = await rewardsManager.subgraphs(subgraphDeploymentID1) const contractSubgraphRewards = await rewardsManager.getAccRewardsForSubgraph( @@ -507,84 +598,77 @@ describe('Rewards', () => { }) }) - describe('takeRewards', function () { - interface DelegationParameters { - indexingRewardCut: BigNumber - queryFeeCut: BigNumber - cooldownBlocks: number - } + describe('takeAndBurnRewards', function () { + it('should burn rewards on closed allocation with POI zero', async function () { + // Align with the epoch boundary + await epochManager.setEpochLength(10) + await advanceToNextEpoch(epochManager) - async function setupIndexerAllocation() { // Setup - await epochManager.setEpochLength(10) + await setupIndexerAllocation() + const firstSnapshotBlocks = new BN((await latestBlock()).sub(dripBlock).toString()) - // Update total signalled - const signalled1 = toGRT('1500') - await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) + // Jump + await advanceToNextEpoch(epochManager) - // Allocate - const tokensToAllocate = toGRT('12500') - await staking.connect(indexer1.signer).stake(tokensToAllocate) - await staking - .connect(indexer1.signer) - .allocateFrom( - indexer1.address, - subgraphDeploymentID1, - tokensToAllocate, - allocationID1, - metadata, - await channelKey1.generateProof(indexer1.address), - ) - } + // Before state + const beforeTokenSupply = await grt.totalSupply() + const beforeIndexer1Stake = await staking.getIndexerStakedTokens(indexer1.address) + const beforeIndexer1Balance = await grt.balanceOf(indexer1.address) + const beforeStakingBalance = await grt.balanceOf(staking.address) - async function setupIndexerAllocationWithDelegation( - tokensToDelegate: BigNumber, - delegationParams: DelegationParameters, - ) { - const tokensToAllocate = toGRT('12500') + // Close allocation with POI zero, which should burn the rewards + const tx = await staking.connect(indexer1.signer).closeAllocation(allocationID1, HashZero) + const receipt = await tx.wait() - // Setup - await epochManager.setEpochLength(10) + const lastSnapshotBlocks = new BN((await latestBlock()).sub(dripBlock).toString()) - // Transfer some funds from the curator, I don't want to mint new tokens - await grt.connect(curator1.signer).transfer(delegator.address, tokensToDelegate) - await grt.connect(delegator.signer).approve(staking.address, tokensToDelegate) + const expectedIndexingRewards = calculatedExpectedRewards( + firstSnapshotBlocks, + lastSnapshotBlocks, + new BN(12500), + ) - // Stake and set delegation parameters - await staking.connect(indexer1.signer).stake(tokensToAllocate) - await staking - .connect(indexer1.signer) - .setDelegationParameters( - delegationParams.indexingRewardCut, - delegationParams.queryFeeCut, - delegationParams.cooldownBlocks, - ) + const log = findRewardsManagerEvents(receipt)[0] + const event = log.args + expect(log.name).eq('RewardsBurned') + expect(event.indexer).eq(indexer1.address) + expect(event.allocationID).eq(allocationID1) + expect(event.epoch).eq(await epochManager.currentEpoch()) + expect(toRound(event.amount)).eq(toRound(expectedIndexingRewards)) - // Delegate - await staking.connect(delegator.signer).delegate(indexer1.address, tokensToDelegate) + // After state + const afterTokenSupply = await grt.totalSupply() + const afterIndexer1Stake = await staking.getIndexerStakedTokens(indexer1.address) + const afterIndexer1Balance = await grt.balanceOf(indexer1.address) + const afterStakingBalance = await grt.balanceOf(staking.address) - // Update total signalled - const signalled1 = toGRT('1500') - await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) + // Check that rewards are NOT put into indexer stake + const expectedIndexerStake = beforeIndexer1Stake - // Allocate - await staking - .connect(indexer1.signer) - .allocateFrom( - indexer1.address, - subgraphDeploymentID1, - tokensToAllocate, - allocationID1, - metadata, - await channelKey1.generateProof(indexer1.address), - ) - } + // Check stake should NOT have increased with the rewards staked + expect(toRound(afterIndexer1Stake)).eq(toRound(expectedIndexerStake)) + // Check indexer balance remains the same + expect(afterIndexer1Balance).eq(beforeIndexer1Balance) + // Check indexing rewards are kept in the staking contract + expect(toRound(afterStakingBalance)).eq(toRound(beforeStakingBalance)) + // Check that tokens have been burned + // We divide by 10 to accept numeric errors up to 10 GRT + expect(toRound(afterTokenSupply.div(10))).eq( + toRound(beforeTokenSupply.sub(expectedIndexingRewards).div(10)), + ) + }) + }) + describe('takeRewards', function () { it('should distribute rewards on closed allocation and stake', async function () { // Align with the epoch boundary + await epochManager.setEpochLength(10) await advanceToNextEpoch(epochManager) + // Setup await setupIndexerAllocation() + const firstSnapshotBlocks = new BN((await latestBlock()).sub(dripBlock).toString()) // Jump await advanceToNextEpoch(epochManager) @@ -595,21 +679,20 @@ describe('Rewards', () => { const beforeIndexer1Balance = await grt.balanceOf(indexer1.address) const beforeStakingBalance = await grt.balanceOf(staking.address) - // All the rewards in this subgraph go to this allocation. - // Rewards per token will be (totalSupply * issuanceRate^nBlocks - totalSupply) / allocatedTokens - // The first snapshot is after allocating, that is 2 blocks after the signal is minted: - // startRewardsPerToken = (10004000000 * 1.01227 ^ 2 - 10004000000) / 12500 = 122945.16 - // The final snapshot is when we close the allocation, that happens 9 blocks later: - // endRewardsPerToken = (10004000000 * 1.01227 ^ 9 - 10004000000) / 12500 = 92861.24 - // Then our expected rewards are (endRewardsPerToken - startRewardsPerToken) * 12500. - const expectedIndexingRewards = toGRT('913715958') - // Close allocation. At this point rewards should be collected for that indexer const tx = await staking .connect(indexer1.signer) .closeAllocation(allocationID1, randomHexBytes()) const receipt = await tx.wait() - const event = rewardsManager.interface.parseLog(receipt.logs[1]).args + + const lastSnapshotBlocks = new BN((await latestBlock()).sub(dripBlock).toString()) + const expectedIndexingRewards = calculatedExpectedRewards( + firstSnapshotBlocks, + lastSnapshotBlocks, + new BN(12500), + ) + + const event = findRewardsManagerEvents(receipt)[0].args expect(event.indexer).eq(indexer1.address) expect(event.allocationID).eq(allocationID1) expect(event.epoch).eq(await epochManager.currentEpoch()) @@ -623,7 +706,7 @@ describe('Rewards', () => { // Check that rewards are put into indexer stake const expectedIndexerStake = beforeIndexer1Stake.add(expectedIndexingRewards) - const expectedTokenSupply = beforeTokenSupply.add(expectedIndexingRewards) + // Check stake should have increased with the rewards staked expect(toRound(afterIndexer1Stake)).eq(toRound(expectedIndexerStake)) // Check indexer balance remains the same @@ -632,18 +715,20 @@ describe('Rewards', () => { expect(toRound(afterStakingBalance)).eq( toRound(beforeStakingBalance.add(expectedIndexingRewards)), ) - // Check that tokens have been minted - expect(toRound(afterTokenSupply)).eq(toRound(expectedTokenSupply)) + // Check that tokens have NOT been minted + expect(toRound(afterTokenSupply)).eq(toRound(beforeTokenSupply)) }) it('should distribute rewards on closed allocation and send to destination', async function () { const destinationAddress = randomHexBytes(20) await staking.connect(indexer1.signer).setRewardsDestination(destinationAddress) + await epochManager.setEpochLength(10) // Align with the epoch boundary await advanceToNextEpoch(epochManager) // Setup await setupIndexerAllocation() + const firstSnapshotBlocks = new BN((await latestBlock()).sub(dripBlock).toString()) // Jump await advanceToNextEpoch(epochManager) @@ -654,24 +739,22 @@ describe('Rewards', () => { const beforeDestinationBalance = await grt.balanceOf(destinationAddress) const beforeStakingBalance = await grt.balanceOf(staking.address) - // All the rewards in this subgraph go to this allocation. - // Rewards per token will be (totalSupply * issuanceRate^nBlocks - totalSupply) / allocatedTokens - // The first snapshot is after allocating, that is 2 blocks after the signal is minted: - // startRewardsPerToken = (10004000000 * 1.01227 ^ 2 - 10004000000) / 12500 = 122945.16 - // The final snapshot is when we close the allocation, that happens 9 blocks later: - // endRewardsPerToken = (10004000000 * 1.01227 ^ 9 - 10004000000) / 12500 = 92861.24 - // Then our expected rewards are (endRewardsPerToken - startRewardsPerToken) * 12500. - const expectedIndexingRewards = toGRT('913715958') - // Close allocation. At this point rewards should be collected for that indexer const tx = await staking .connect(indexer1.signer) .closeAllocation(allocationID1, randomHexBytes()) const receipt = await tx.wait() - const event = rewardsManager.interface.parseLog(receipt.logs[1]).args + const lastSnapshotBlocks = new BN((await latestBlock()).sub(dripBlock).toString()) + const event = findRewardsManagerEvents(receipt)[0].args expect(event.indexer).eq(indexer1.address) expect(event.allocationID).eq(allocationID1) expect(event.epoch).eq(await epochManager.currentEpoch()) + + const expectedIndexingRewards = calculatedExpectedRewards( + firstSnapshotBlocks, + lastSnapshotBlocks, + new BN(12500), + ) expect(toRound(event.amount)).eq(toRound(expectedIndexingRewards)) // After state @@ -682,7 +765,7 @@ describe('Rewards', () => { // Check that rewards are properly assigned const expectedIndexerStake = beforeIndexer1Stake - const expectedTokenSupply = beforeTokenSupply.add(expectedIndexingRewards) + // Check stake should not have changed expect(toRound(afterIndexer1Stake)).eq(toRound(expectedIndexerStake)) // Check indexing rewards are received by the rewards destination @@ -691,8 +774,8 @@ describe('Rewards', () => { ) // Check indexing rewards were not sent to the staking contract expect(afterStakingBalance).eq(beforeStakingBalance) - // Check that tokens have been minted - expect(toRound(afterTokenSupply)).eq(toRound(expectedTokenSupply)) + // Check that tokens have NOT been minted + expect(toRound(afterTokenSupply)).eq(toRound(beforeTokenSupply)) }) it('should distribute rewards on closed allocation w/delegators', async function () { @@ -703,14 +786,18 @@ describe('Rewards', () => { cooldownBlocks: 5, } const tokensToDelegate = toGRT('2000') + await epochManager.setEpochLength(10) // Align with the epoch boundary await advanceToNextEpoch(epochManager) + // Setup the allocation and delegators await setupIndexerAllocationWithDelegation(tokensToDelegate, delegationParams) + const firstSnapshotBlocks = new BN((await latestBlock()).sub(dripBlock).toString()) // Jump await advanceToNextEpoch(epochManager) + // dripBlock + 13 // Before state const beforeTokenSupply = await grt.totalSupply() @@ -719,6 +806,7 @@ describe('Rewards', () => { // Close allocation. At this point rewards should be collected for that indexer await staking.connect(indexer1.signer).closeAllocation(allocationID1, randomHexBytes()) + const lastSnapshotBlocks = new BN((await latestBlock()).sub(dripBlock).toString()) // After state const afterTokenSupply = await grt.totalSupply() @@ -728,14 +816,12 @@ describe('Rewards', () => { // Check that rewards are put into indexer stake (only indexer cut) // Check that rewards are put into delegators pool accordingly - // All the rewards in this subgraph go to this allocation. - // Rewards per token will be (totalSupply * issuanceRate^nBlocks - totalSupply) / allocatedTokens - // The first snapshot is after allocating, that is 2 blocks after the signal is minted: - // startRewardsPerToken = (10004000000 * 1.01227 ^ 2 - 10004000000) / 14500 = 8466.995 - // The final snapshot is when we close the allocation, that happens 4 blocks later: - // endRewardsPerToken = (10004000000 * 1.01227 ^ 4 - 10004000000) / 14500 = 34496.55 - // Then our expected rewards are (endRewardsPerToken - startRewardsPerToken) * 14500. - const expectedIndexingRewards = toGRT('377428566.77') + const expectedIndexingRewards = calculatedExpectedRewards( + firstSnapshotBlocks, + lastSnapshotBlocks, + new BN(14500), + ) + // Calculate delegators cut const indexerRewards = delegationParams.indexingRewardCut .mul(expectedIndexingRewards) @@ -745,101 +831,162 @@ describe('Rewards', () => { // Check const expectedIndexerStake = beforeIndexer1Stake.add(indexerRewards) const expectedDelegatorsPoolTokens = beforeDelegationPool.tokens.add(delegatorsRewards) - const expectedTokenSupply = beforeTokenSupply.add(expectedIndexingRewards) expect(toRound(afterIndexer1Stake)).eq(toRound(expectedIndexerStake)) expect(toRound(afterDelegationPool.tokens)).eq(toRound(expectedDelegatorsPoolTokens)) - // Check that tokens have been minted - expect(toRound(afterTokenSupply)).eq(toRound(expectedTokenSupply)) + // Check that tokens have NOT been minted + expect(toRound(afterTokenSupply)).eq(toRound(beforeTokenSupply)) }) - it('should deny rewards if subgraph on denylist', async function () { + it('should deny and burn rewards if subgraph on denylist', async function () { // Setup + // dripBlock (82) + await epochManager.setEpochLength(10) + // dripBlock + 1 await rewardsManager .connect(governor.signer) .setSubgraphAvailabilityOracle(governor.address) + // dripBlock + 2 await rewardsManager.connect(governor.signer).setDenied(subgraphDeploymentID1, true) + // dripBlock + 3 (epoch boundary!) + await advanceToNextEpoch(epochManager) + // dripBlock + 13 await setupIndexerAllocation() + const firstSnapshotBlocks = new BN((await latestBlock()).sub(dripBlock).toString()) // Jump await advanceToNextEpoch(epochManager) + // dripBlock + 23 + const supplyBefore = await grt.totalSupply() // Close allocation. At this point rewards should be collected for that indexer const tx = staking.connect(indexer1.signer).closeAllocation(allocationID1, randomHexBytes()) - await expect(tx) - .emit(rewardsManager, 'RewardsDenied') - .withArgs(indexer1.address, allocationID1, await epochManager.currentEpoch()) + await expect(tx).emit(rewardsManager, 'RewardsDenied') + const lastSnapshotBlocks = new BN((await latestBlock()).sub(dripBlock).toString()) + const receipt = await (await tx).wait() + const logs = findRewardsManagerEvents(receipt) + expect(logs.length).to.eq(1) + expect(logs[0].name).to.eq('RewardsDenied') + const ev = logs[0].args + expect(ev.indexer).to.eq(indexer1.address) + expect(ev.allocationID).to.eq(allocationID1) + expect(ev.epoch).to.eq(await epochManager.currentEpoch()) + + const expectedIndexingRewards = calculatedExpectedRewards( + firstSnapshotBlocks, + lastSnapshotBlocks, + new BN(12500), + ) + expect(toRound(ev.amount)).to.eq(toRound(expectedIndexingRewards)) + // Check that the rewards were burned + // We divide by 10 to accept numeric errors up to 10 GRT + expect(toRound((await grt.totalSupply()).div(10))).to.eq( + toRound(supplyBefore.sub(expectedIndexingRewards).div(10)), + ) }) }) - }) - describe('pow', function () { - it('exponentiation works under normal boundaries (annual rate from 1% to 700%, 90 days period)', async function () { - const baseRatio = toGRT('0.000000004641377923') // 1% annual rate - const timePeriods = (60 * 60 * 24 * 10) / 15 // 90 days in blocks - for (let i = 0; i < 50; i = i + 4) { - const r = baseRatio.mul(i * 4).add(toGRT('1')) - const h = await rewardsManagerMock.pow(r, timePeriods, toGRT('1')) - console.log('\tr:', formatGRT(r), '=> c:', formatGRT(h)) - } - }) - }) + describe('edge scenarios', function () { + it('close allocation on a subgraph that no longer have signal', async function () { + // Update total signalled + const signalled1 = toGRT('1500') + await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) - describe('edge scenarios', function () { - it('close allocation on a subgraph that no longer have signal', async function () { - // Update total signalled - const signalled1 = toGRT('1500') - await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) + // Allocate + const tokensToAllocate = toGRT('12500') + await staking.connect(indexer1.signer).stake(tokensToAllocate) + await staking + .connect(indexer1.signer) + .allocateFrom( + indexer1.address, + subgraphDeploymentID1, + tokensToAllocate, + allocationID1, + metadata, + await channelKey1.generateProof(indexer1.address), + ) - // Allocate - const tokensToAllocate = toGRT('12500') - await staking.connect(indexer1.signer).stake(tokensToAllocate) - await staking - .connect(indexer1.signer) - .allocateFrom( - indexer1.address, + // Jump + await advanceToNextEpoch(epochManager) + + // Remove all signal from the subgraph + const curatorShares = await curation.getCuratorSignal( + curator1.address, subgraphDeploymentID1, - tokensToAllocate, - allocationID1, - metadata, - await channelKey1.generateProof(indexer1.address), ) + await curation.connect(curator1.signer).burn(subgraphDeploymentID1, curatorShares, 0) - // Jump - await advanceToNextEpoch(epochManager) + // Close allocation. At this point rewards should be collected for that indexer + await staking.connect(indexer1.signer).closeAllocation(allocationID1, randomHexBytes()) + }) + }) - // Remove all signal from the subgraph - const curatorShares = await curation.getCuratorSignal(curator1.address, subgraphDeploymentID1) - await curation.connect(curator1.signer).burn(subgraphDeploymentID1, curatorShares, 0) + describe('multiple allocations', function () { + it('two allocations in the same block with a GRT burn in the middle should succeed', async function () { + // If rewards are not monotonically increasing, this can trigger + // a subtraction overflow error as seen in mainnet tx: + // 0xb6bf7bbc446720a7409c482d714aebac239dd62e671c3c94f7e93dd3a61835ab + await advanceToNextEpoch(epochManager) - // Close allocation. At this point rewards should be collected for that indexer - await staking.connect(indexer1.signer).closeAllocation(allocationID1, randomHexBytes()) - }) - }) + // Setup + await epochManager.setEpochLength(10) - describe('multiple allocations', function () { - it('two allocations in the same block with a GRT burn in the middle should succeed', async function () { - // If rewards are not monotonically increasing, this can trigger - // a subtraction overflow error as seen in mainnet tx: - // 0xb6bf7bbc446720a7409c482d714aebac239dd62e671c3c94f7e93dd3a61835ab - await advanceToNextEpoch(epochManager) + // Update total signalled + const signalled1 = toGRT('1500') + await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) - // Setup - await epochManager.setEpochLength(10) + // Stake + const tokensToStake = toGRT('12500') + await staking.connect(indexer1.signer).stake(tokensToStake) - // Update total signalled - const signalled1 = toGRT('1500') - await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) + // Allocate simultaneously, burning in the middle + const tokensToAlloc = toGRT('5000') + await provider().send('evm_setAutomine', [false]) + const tx1 = await staking + .connect(indexer1.signer) + .allocateFrom( + indexer1.address, + subgraphDeploymentID1, + tokensToAlloc, + allocationID1, + metadata, + await channelKey1.generateProof(indexer1.address), + ) + const tx2 = await grt.connect(indexer1.signer).burn(toGRT(1)) + const tx3 = await staking + .connect(indexer1.signer) + .allocateFrom( + indexer1.address, + subgraphDeploymentID1, + tokensToAlloc, + allocationID2, + metadata, + await channelKey2.generateProof(indexer1.address), + ) - // Stake - const tokensToStake = toGRT('12500') - await staking.connect(indexer1.signer).stake(tokensToStake) + await provider().send('evm_mine', []) + await provider().send('evm_setAutomine', [true]) - // Allocate simultaneously, burning in the middle - const tokensToAlloc = toGRT('5000') - await provider().send('evm_setAutomine', [false]) - const tx1 = await staking - .connect(indexer1.signer) - .allocateFrom( + await expect(tx1).emit(staking, 'AllocationCreated') + await expect(tx2).emit(grt, 'Transfer') + await expect(tx3).emit(staking, 'AllocationCreated') + }) + it('two simultanous-similar allocations should get same amount of rewards', async function () { + await advanceToNextEpoch(epochManager) + + // Setup + await epochManager.setEpochLength(10) + + // Update total signalled + const signalled1 = toGRT('1500') + await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) + + // Stake + const tokensToStake = toGRT('12500') + await staking.connect(indexer1.signer).stake(tokensToStake) + + // Allocate simultaneously + const tokensToAlloc = toGRT('5000') + const tx1 = await staking.populateTransaction.allocateFrom( indexer1.address, subgraphDeploymentID1, tokensToAlloc, @@ -847,10 +994,7 @@ describe('Rewards', () => { metadata, await channelKey1.generateProof(indexer1.address), ) - const tx2 = await grt.connect(indexer1.signer).burn(toGRT(1)) - const tx3 = await staking - .connect(indexer1.signer) - .allocateFrom( + const tx2 = await staking.populateTransaction.allocateFrom( indexer1.address, subgraphDeploymentID1, tokensToAlloc, @@ -858,61 +1002,31 @@ describe('Rewards', () => { metadata, await channelKey2.generateProof(indexer1.address), ) + await staking.connect(indexer1.signer).multicall([tx1.data, tx2.data]) - await provider().send('evm_mine', []) - await provider().send('evm_setAutomine', [true]) - - await expect(tx1).emit(staking, 'AllocationCreated') - await expect(tx2).emit(grt, 'Transfer') - await expect(tx3).emit(staking, 'AllocationCreated') - }) - it('two simultanous-similar allocations should get same amount of rewards', async function () { - await advanceToNextEpoch(epochManager) - - // Setup - await epochManager.setEpochLength(10) - - // Update total signalled - const signalled1 = toGRT('1500') - await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) - - // Stake - const tokensToStake = toGRT('12500') - await staking.connect(indexer1.signer).stake(tokensToStake) - - // Allocate simultaneously - const tokensToAlloc = toGRT('5000') - const tx1 = await staking.populateTransaction.allocateFrom( - indexer1.address, - subgraphDeploymentID1, - tokensToAlloc, - allocationID1, - metadata, - await channelKey1.generateProof(indexer1.address), - ) - const tx2 = await staking.populateTransaction.allocateFrom( - indexer1.address, - subgraphDeploymentID1, - tokensToAlloc, - allocationID2, - metadata, - await channelKey2.generateProof(indexer1.address), - ) - await staking.connect(indexer1.signer).multicall([tx1.data, tx2.data]) - - // Jump - await advanceToNextEpoch(epochManager) - - // Close allocations simultaneously - const tx3 = await staking.populateTransaction.closeAllocation(allocationID1, randomHexBytes()) - const tx4 = await staking.populateTransaction.closeAllocation(allocationID2, randomHexBytes()) - const tx5 = await staking.connect(indexer1.signer).multicall([tx3.data, tx4.data]) + // Jump + await advanceToNextEpoch(epochManager) - // Both allocations should receive the same amount of rewards - const receipt = await tx5.wait() - const event1 = rewardsManager.interface.parseLog(receipt.logs[1]).args - const event2 = rewardsManager.interface.parseLog(receipt.logs[5]).args - expect(event1.amount).eq(event2.amount) + // Close allocations simultaneously + const tx3 = await staking.populateTransaction.closeAllocation( + allocationID1, + randomHexBytes(), + ) + const tx4 = await staking.populateTransaction.closeAllocation( + allocationID2, + randomHexBytes(), + ) + const tx5 = await staking.connect(indexer1.signer).multicall([tx3.data, tx4.data]) + + // Both allocations should receive the same amount of rewards + const receipt = await tx5.wait() + const rewardsMgrEvents = findRewardsManagerEvents(receipt) + expect(rewardsMgrEvents.length).to.eq(2) + const event1 = rewardsMgrEvents[0].args + const event2 = rewardsMgrEvents[1].args + expect(event1.amount).to.not.eq(toBN(0)) + expect(event1.amount).to.eq(event2.amount) + }) }) }) }) diff --git a/test/staking/rebate.test.ts b/test/staking/rebate.test.ts index 2668b29f4..d6db5948b 100644 --- a/test/staking/rebate.test.ts +++ b/test/staking/rebate.test.ts @@ -4,7 +4,7 @@ import { BigNumber } from 'ethers' import { deployContract } from '../lib/deployment' import { RebatePoolMock } from '../../build/types/RebatePoolMock' -import { getAccounts, toBN, toGRT, formatGRT, Account } from '../lib/testHelpers' +import { getAccounts, toBN, toGRT, formatGRT, Account, initNetwork } from '../lib/testHelpers' const toFloat = (n: BigNumber) => parseFloat(formatGRT(n)) const toFixed = (n: number | BigNumber, precision = 12) => { @@ -194,6 +194,7 @@ describe('Staking:Rebate', () => { } beforeEach(async function () { + await initNetwork() ;[deployer] = await getAccounts() rebatePoolMock = (await deployContract( 'RebatePoolMock', diff --git a/yarn.lock b/yarn.lock index 798643bbe..eed2251bf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,22 @@ # 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== + dependencies: + "@ethersproject/address" "^5.0.8" + "@ethersproject/bignumber" "^5.1.1" + "@ethersproject/bytes" "^5.0.8" + "@typechain/ethers-v5" "9.0.0" + "@types/prompts" "^2.0.14" + "@types/yargs" "^17.0.9" + dotenv "^10.0.0" + ethers "^5.1.0" + ts-node "^10.2.1" + typechain "7.0.0" + "@babel/code-frame@7.12.11": version "7.12.11" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" @@ -175,6 +191,18 @@ dependencies: chalk "^4.0.0" +"@cspotcode/source-map-consumer@0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz#33bf4b7b39c178821606f669bbc447a6a629786b" + integrity sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg== + +"@cspotcode/source-map-support@0.7.0": + version "0.7.0" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz#4789840aa859e46d2f3173727ab707c66bf344f5" + integrity sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA== + dependencies: + "@cspotcode/source-map-consumer" "0.8.0" + "@cspotcode/source-map-support@^0.8.0": version "0.8.1" resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" @@ -191,6 +219,18 @@ enabled "2.0.x" kuler "^2.0.0" +"@defi-wonderland/smock@^2.0.7": + version "2.0.7" + resolved "https://registry.yarnpkg.com/@defi-wonderland/smock/-/smock-2.0.7.tgz#59d5fc93e175ad120c5dcdd8294e07525606c855" + integrity sha512-RVpODLKZ/Cr0C1bCbhJ2aXbAr2Ll/K2WO7hDL96tqhMzCsA7ToWdDIgiNpV5Vtqqvpftu5ddO7v3TAurQNSU0w== + dependencies: + "@nomiclabs/ethereumjs-vm" "^4.2.2" + diff "^5.0.0" + lodash.isequal "^4.5.0" + lodash.isequalwith "^4.4.0" + rxjs "^7.2.0" + semver "^7.3.5" + "@endemolshinegroup/cosmiconfig-typescript-loader@^3.0.2": version "3.0.2" resolved "https://registry.yarnpkg.com/@endemolshinegroup/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-3.0.2.tgz#eea4635828dde372838b0909693ebd9aafeec22d" @@ -295,6 +335,30 @@ ethereumjs-util "^7.1.5" merkle-patricia-tree "^4.2.4" +"@ethereumjs/block@^3.6.0": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@ethereumjs/block/-/block-3.6.2.tgz#63d1e26d0b7a7a3684fce920de6ebabec1e5b674" + integrity sha512-mOqYWwMlAZpYUEOEqt7EfMFuVL2eyLqWWIzcf4odn6QgXY8jBI2NhVuJncrMCKeMZrsJAe7/auaRRB6YcdH+Qw== + dependencies: + "@ethereumjs/common" "^2.6.3" + "@ethereumjs/tx" "^3.5.1" + ethereumjs-util "^7.1.4" + merkle-patricia-tree "^4.2.4" + +"@ethereumjs/blockchain@^5.5.0": + version "5.5.2" + resolved "https://registry.yarnpkg.com/@ethereumjs/blockchain/-/blockchain-5.5.2.tgz#1848abd9dc1ee56acf8cec4c84304d7f4667d027" + integrity sha512-Jz26iJmmsQtngerW6r5BDFaew/f2mObLrRZo3rskLOx1lmtMZ8+TX/vJexmivrnWgmAsTdNWhlKUYY4thPhPig== + dependencies: + "@ethereumjs/block" "^3.6.2" + "@ethereumjs/common" "^2.6.3" + "@ethereumjs/ethash" "^1.1.0" + debug "^4.3.3" + ethereumjs-util "^7.1.4" + level-mem "^5.0.1" + lru-cache "^5.1.1" + semaphore-async-await "^1.5.1" + "@ethereumjs/blockchain@^5.5.2", "@ethereumjs/blockchain@^5.5.3": version "5.5.3" resolved "https://registry.yarnpkg.com/@ethereumjs/blockchain/-/blockchain-5.5.3.tgz#aa49a6a04789da6b66b5bcbb0d0b98efc369f640" @@ -317,6 +381,14 @@ crc-32 "^1.2.0" ethereumjs-util "^7.1.5" +"@ethereumjs/common@^2.6.0", "@ethereumjs/common@^2.6.3": + version "2.6.3" + resolved "https://registry.yarnpkg.com/@ethereumjs/common/-/common-2.6.3.tgz#39ddece7300b336276bad6c02f6a9f1a082caa05" + integrity sha512-mQwPucDL7FDYIg9XQ8DL31CnIYZwGhU5hyOO5E+BMmT71G0+RHvIT5rIkLBirJEKxV6+Rcf9aEIY0kXInxUWpQ== + dependencies: + crc-32 "^1.2.0" + ethereumjs-util "^7.1.4" + "@ethereumjs/ethash@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@ethereumjs/ethash/-/ethash-1.1.0.tgz#7c5918ffcaa9cb9c1dc7d12f77ef038c11fb83fb" @@ -336,6 +408,32 @@ "@ethereumjs/common" "^2.6.4" ethereumjs-util "^7.1.5" +"@ethereumjs/tx@^3.4.0": + version "3.5.1" + resolved "https://registry.yarnpkg.com/@ethereumjs/tx/-/tx-3.5.1.tgz#8d941b83a602b4a89949c879615f7ea9a90e6671" + integrity sha512-xzDrTiu4sqZXUcaBxJ4n4W5FrppwxLxZB4ZDGVLtxSQR4lVuOnFR6RcUHdg1mpUhAPVrmnzLJpxaeXnPxIyhWA== + dependencies: + "@ethereumjs/common" "^2.6.3" + ethereumjs-util "^7.1.4" + +"@ethereumjs/vm@^5.6.0": + version "5.8.0" + resolved "https://registry.yarnpkg.com/@ethereumjs/vm/-/vm-5.8.0.tgz#c9055f96afc13dd7b72893b57fa20027effea6fe" + integrity sha512-mn2G2SX79QY4ckVvZUfxlNUpzwT2AEIkvgJI8aHoQaNYEHhH8rmdVDIaVVgz6//PjK52BZsK23afz+WvSR0Qqw== + dependencies: + "@ethereumjs/block" "^3.6.2" + "@ethereumjs/blockchain" "^5.5.2" + "@ethereumjs/common" "^2.6.3" + "@ethereumjs/tx" "^3.5.1" + async-eventemitter "^0.2.4" + core-js-pure "^3.0.1" + debug "^4.3.3" + ethereumjs-util "^7.1.4" + functional-red-black-tree "^1.0.1" + mcl-wasm "^0.7.1" + merkle-patricia-tree "^4.2.4" + rustbn.js "~0.2.0" + "@ethereumjs/vm@^5.9.0": version "5.9.3" resolved "https://registry.yarnpkg.com/@ethereumjs/vm/-/vm-5.9.3.tgz#6d69202e4c132a4a1e1628ac246e92062e230823" @@ -447,7 +545,7 @@ "@ethersproject/logger" "^5.6.0" "@ethersproject/properties" "^5.6.0" -"@ethersproject/address@5.6.0": +"@ethersproject/address@5.6.0", "@ethersproject/address@^5.0.8": version "5.6.0" resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.6.0.tgz#13c49836d73e7885fc148ad633afad729da25012" integrity sha512-6nvhYXjbXsHPS+30sHZ+U4VMagFC/9zAk6Gd/h3S21YW4+yfb0WfRtaAIZ4kfM4rrVwqiy284LP0GtL5HXGLxQ== @@ -499,7 +597,7 @@ "@ethersproject/bytes" "^5.6.1" "@ethersproject/properties" "^5.6.0" -"@ethersproject/bignumber@5.6.0": +"@ethersproject/bignumber@5.6.0", "@ethersproject/bignumber@^5.1.1": version "5.6.0" resolved "https://registry.yarnpkg.com/@ethersproject/bignumber/-/bignumber-5.6.0.tgz#116c81b075c57fa765a8f3822648cf718a8a0e26" integrity sha512-VziMaXIUHQlHJmkv1dlcd6GY2PmT0khtAqaMctCIDogxkrarMzA9L94KN1NeXqqOfFD6r0sJT3vCTOFSmZ07DA== @@ -517,7 +615,7 @@ "@ethersproject/logger" "^5.6.0" bn.js "^5.2.1" -"@ethersproject/bytes@5.6.1", "@ethersproject/bytes@>=5.0.0-beta.129", "@ethersproject/bytes@^5.6.0", "@ethersproject/bytes@^5.6.1": +"@ethersproject/bytes@5.6.1", "@ethersproject/bytes@>=5.0.0-beta.129", "@ethersproject/bytes@^5.0.8", "@ethersproject/bytes@^5.6.0", "@ethersproject/bytes@^5.6.1": version "5.6.1" resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.6.1.tgz#24f916e411f82a8a60412344bf4a813b917eefe7" integrity sha512-NwQt7cKn5+ZE4uDn+X5RAXLp46E1chXoaMmrxAyA0rblpxz8t58lVkrHXoRIn0lz1joQElQ8410GqhTqMOwc6g== @@ -570,13 +668,13 @@ "@ethersproject/properties" "^5.6.0" "@ethersproject/transactions" "^5.6.2" -"@ethersproject/experimental@^5.4.0": - version "5.6.3" - resolved "https://registry.yarnpkg.com/@ethersproject/experimental/-/experimental-5.6.3.tgz#d1cd8f3b886cbab86430fb2954eb65ddb7c75ffd" - integrity sha512-yMymv32XMr9sXvHc3S1On2wD0JMT6n4X9uKpfZ8jFFw5rEcI99yfovcCZ0tpUedh1b3IvReSain+RobeNQmmEg== +"@ethersproject/experimental@^5.6.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@ethersproject/experimental/-/experimental-5.6.0.tgz#c72ef00a79b746c522eb79736712169d71c55f64" + integrity sha512-lSEM/6t+BicbeyRxat5meoQhXZLoBEziVrxZqeCIhsPntvq4DlMobPBKXF0Iz3m0dMvl9uga7fHEO4YD9SgCgw== dependencies: - "@ethersproject/web" "^5.6.1" - ethers "^5.6.8" + "@ethersproject/web" "^5.6.0" + ethers "^5.6.0" scrypt-js "3.0.1" "@ethersproject/hash@5.6.0": @@ -1122,9 +1220,9 @@ "@jridgewell/sourcemap-codec" "^1.4.10" "@metamask/eth-sig-util@^4.0.0": - version "4.0.1" - resolved "https://registry.yarnpkg.com/@metamask/eth-sig-util/-/eth-sig-util-4.0.1.tgz#3ad61f6ea9ad73ba5b19db780d40d9aae5157088" - integrity sha512-tghyZKLHZjcdlDqCA3gNZmLeR0XvOE9U1qoQO9ohyAZT6Pya+H9vkBPcsyXytmYLNgVoin7CKCmweo/R43V+tQ== + version "4.0.0" + resolved "https://registry.yarnpkg.com/@metamask/eth-sig-util/-/eth-sig-util-4.0.0.tgz#11553ba06de0d1352332c1bde28c8edd00e0dcf6" + integrity sha512-LczOjjxY4A7XYloxzyxJIHONELmUxVZncpOLoClpEcTiebiVdM46KRPYXGuULro9oNNR2xdVx3yoKiQjdfWmoA== dependencies: ethereumjs-abi "^0.6.8" ethereumjs-util "^6.2.1" @@ -1168,6 +1266,27 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@nomiclabs/ethereumjs-vm@^4.2.2": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@nomiclabs/ethereumjs-vm/-/ethereumjs-vm-4.2.2.tgz#2f8817113ca0fb6c44c1b870d0a809f0e026a6cc" + integrity sha512-8WmX94mMcJaZ7/m7yBbyuS6B+wuOul+eF+RY9fBpGhNaUpyMR/vFIcDojqcWQ4Yafe1tMKY5LDu2yfT4NZgV4Q== + dependencies: + async "^2.1.2" + async-eventemitter "^0.2.2" + core-js-pure "^3.0.1" + ethereumjs-account "^3.0.0" + ethereumjs-block "^2.2.2" + ethereumjs-blockchain "^4.0.3" + ethereumjs-common "^1.5.0" + ethereumjs-tx "^2.1.2" + ethereumjs-util "^6.2.0" + fake-merkle-patricia-tree "^1.0.1" + functional-red-black-tree "^1.0.1" + merkle-patricia-tree "3.0.0" + rustbn.js "~0.2.0" + safe-buffer "^5.1.1" + util.promisify "^1.0.0" + "@nomiclabs/hardhat-ethers@^2.0.2", "@nomiclabs/hardhat-ethers@^2.0.6": version "2.1.0" resolved "https://registry.yarnpkg.com/@nomiclabs/hardhat-ethers/-/hardhat-ethers-2.1.0.tgz#9b7dc94d669ad9dc286b94f6f2f1513118c7027b" @@ -1429,6 +1548,14 @@ resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e" integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== +"@typechain/ethers-v5@9.0.0": + version "9.0.0" + resolved "https://registry.yarnpkg.com/@typechain/ethers-v5/-/ethers-v5-9.0.0.tgz#6aa93bea7425c0463bd8a61eea3643540ef851bd" + integrity sha512-bAanuPl1L2itaUdMvor/QvwnIH+TM/CmG00q17Ilv3ZZMeJ2j8HcarhgJUZ9pBY1teBb85P8cC03dz3mSSx+tQ== + dependencies: + lodash "^4.17.15" + ts-essentials "^7.0.1" + "@typechain/ethers-v5@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@typechain/ethers-v5/-/ethers-v5-2.0.0.tgz#cd3ca1590240d587ca301f4c029b67bfccd08810" @@ -1632,6 +1759,13 @@ resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.6.3.tgz#68ada76827b0010d0db071f739314fa429943d0a" integrity sha512-ymZk3LEC/fsut+/Q5qejp6R9O1rMxz3XaRHDV6kX8MrGAhOSPqVARbDi+EZvInBpw+BnCX3TD240byVkOfQsHg== +"@types/prompts@^2.0.14": + version "2.0.14" + resolved "https://registry.yarnpkg.com/@types/prompts/-/prompts-2.0.14.tgz#10cb8899844bb0771cabe57c1becaaaca9a3b521" + integrity sha512-HZBd99fKxRWpYCErtm2/yxUZv6/PBI9J7N4TNFffl5JbrYMHBwF25DjQGTW3b3jmXq+9P6/8fCIb2ee57BFfYA== + dependencies: + "@types/node" "*" + "@types/qs@^6.2.31", "@types/qs@^6.9.7": version "6.9.7" resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb" @@ -1715,6 +1849,13 @@ dependencies: "@types/yargs-parser" "*" +"@types/yargs@^17.0.9": + version "17.0.10" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.10.tgz#591522fce85d8739bca7b8bb90d048e4478d186a" + integrity sha512-gmEaFwpj/7f/ROdtIlci1R1VYU1J4j95m8T+Tj3iBgiBFKg1foE/PSl93bBd5T9LDXNPo8UlNN6W0qwD8O5OaA== + dependencies: + "@types/yargs-parser" "*" + "@typescript-eslint/eslint-plugin@^4.0.0": version "4.33.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.33.0.tgz#c24dc7c8069c7706bc40d99f6fa87edcb2005276" @@ -2083,6 +2224,13 @@ anymatch@~3.1.1, anymatch@~3.1.2: normalize-path "^3.0.0" picomatch "^2.0.4" +arbos-precompiles@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/arbos-precompiles/-/arbos-precompiles-1.0.2.tgz#7bebd5963aef972cd259eb41f3116ea065013ea6" + integrity sha512-1dOFYFJUN0kKoofh6buZJ8qCqTs+oLGSsGzHI0trA/Pka/TCERflCRsNVxez2lihOvK7MT/a2RA8AepKtBXdPQ== + dependencies: + hardhat "^2.6.4" + arg@^4.1.0: version "4.1.3" resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" @@ -2129,6 +2277,16 @@ array-back@^2.0.0: dependencies: typical "^2.6.1" +array-back@^3.0.1, array-back@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/array-back/-/array-back-3.1.0.tgz#b8859d7a508871c9a7b2cf42f99428f65e96bfb0" + integrity sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q== + +array-back@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/array-back/-/array-back-4.0.2.tgz#8004e999a6274586beeb27342168652fdb89fa1e" + integrity sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg== + array-flatten@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" @@ -3676,6 +3834,26 @@ command-line-args@^4.0.7: find-replace "^1.0.3" typical "^2.6.1" +command-line-args@^5.1.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/command-line-args/-/command-line-args-5.2.1.tgz#c44c32e437a57d7c51157696893c5909e9cec42e" + integrity sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg== + dependencies: + array-back "^3.1.0" + find-replace "^3.0.0" + lodash.camelcase "^4.3.0" + typical "^4.0.0" + +command-line-usage@^6.1.0: + version "6.1.2" + resolved "https://registry.yarnpkg.com/command-line-usage/-/command-line-usage-6.1.2.tgz#2b7ccd48a93fb19bd71ca8fe9900feab00e557b0" + integrity sha512-I+0XN613reAhpBQ6icsPOTwu9cvhc9NtLtUcY2fGYuwm9JZiWBzFDA8w0PHqQjru7Xth7fM/y9TJ13+VKdjh7Q== + dependencies: + array-back "^4.0.1" + chalk "^2.4.2" + table-layout "^1.0.1" + typical "^5.2.0" + commander@2.18.0: version "2.18.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.18.0.tgz#2bf063ddee7c7891176981a2cc798e5754bc6970" @@ -4011,6 +4189,13 @@ debug@4, debug@4.3.4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0, de dependencies: ms "2.1.2" +debug@4.3.3: + version "4.3.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" + integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== + dependencies: + ms "2.1.2" + debug@^3.1.0: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" @@ -4072,6 +4257,11 @@ deep-equal@~1.1.1: object-keys "^1.1.1" regexp.prototype.flags "^1.2.0" +deep-extend@~0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + deep-is@^0.1.3, deep-is@~0.1.3: version "0.1.4" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" @@ -4215,7 +4405,7 @@ diff@3.5.0: resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== -diff@5.0.0: +diff@5.0.0, diff@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== @@ -4274,6 +4464,11 @@ dotenv@*: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.1.tgz#8f8f9d94876c35dac989876a5d3a82a267fdce1d" integrity sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ== +dotenv@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81" + integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q== + dotenv@^9.0.0: version "9.0.2" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-9.0.2.tgz#dacc20160935a37dea6364aa1bef819fb9b6ab05" @@ -4921,7 +5116,7 @@ ethereum-common@^0.0.18: resolved "https://registry.yarnpkg.com/ethereum-common/-/ethereum-common-0.0.18.tgz#2fdc3576f232903358976eb39da783213ff9523f" integrity sha512-EoltVQTRNg2Uy4o84qpa2aXymXDJhxm7eos/ACOg0DG4baAbMjhbdAEsx9GeE8sC3XCxnYvrrzZDH8D8MtA2iQ== -ethereum-cryptography@^0.1.3: +ethereum-cryptography@^0.1.2, ethereum-cryptography@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/ethereum-cryptography/-/ethereum-cryptography-0.1.3.tgz#8d6143cfc3d74bf79bbd8edecdf29e4ae20dd191" integrity sha512-w8/4x1SGGzc+tO97TASLja6SLd3fRIK2tLVcV2Gx4IB21hE19atll5Cq9o3d0ZmAYC/8aw0ipieTSiekAea4SQ== @@ -5116,6 +5311,17 @@ ethereumjs-util@^7.0.10, ethereumjs-util@^7.0.2, ethereumjs-util@^7.0.3, ethereu ethereum-cryptography "^0.1.3" rlp "^2.2.4" +ethereumjs-util@^7.1.3: + version "7.1.4" + resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-7.1.4.tgz#a6885bcdd92045b06f596c7626c3e89ab3312458" + integrity sha512-p6KmuPCX4mZIqsQzXfmSx9Y0l2hqf+VkAiwSisW3UKUFdk8ZkAt+AYaor83z2nSi6CU2zSsXMlD80hAbNEGM0A== + dependencies: + "@types/bn.js" "^5.1.0" + bn.js "^5.1.2" + create-hash "^1.1.2" + ethereum-cryptography "^0.1.3" + rlp "^2.2.4" + ethereumjs-vm@4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/ethereumjs-vm/-/ethereumjs-vm-4.2.0.tgz#e885e861424e373dbc556278f7259ff3fca5edab" @@ -5169,7 +5375,7 @@ ethereumjs-wallet@0.6.5: utf8 "^3.0.0" uuid "^3.3.2" -ethers@5.6.2: +ethers@5.6.2, ethers@^5.1.0, ethers@^5.6.0: version "5.6.2" resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.6.2.tgz#e75bac7f038c5e0fdde667dba62fc223924143a2" integrity sha512-EzGCbns24/Yluu7+ToWnMca3SXJ1Jk1BvWB7CCmVNxyOeM4LLvw2OLuIHhlkhQk1dtOcj9UMsdkxUh8RiG1dxQ== @@ -5627,6 +5833,13 @@ find-replace@^1.0.3: array-back "^1.0.4" test-value "^2.1.0" +find-replace@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-replace/-/find-replace-3.0.0.tgz#3e7e23d3b05167a76f770c9fbd5258b0def68c38" + integrity sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ== + dependencies: + array-back "^3.0.1" + find-up@3.0.0, find-up@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" @@ -6344,6 +6557,60 @@ hardhat-tracer@^1.0.0-alpha.6: dependencies: ethers "^5.0.24" +hardhat@^2.6.4: + version "2.9.3" + resolved "https://registry.yarnpkg.com/hardhat/-/hardhat-2.9.3.tgz#4759dc3c468c7d15f34334ca1be7d59b04e47b1e" + integrity sha512-7Vw99RbYbMZ15UzegOR/nqIYIqddZXvLwJGaX5sX4G5bydILnbjmDU6g3jMKJSiArEixS3vHAEaOs5CW1JQ3hg== + dependencies: + "@ethereumjs/block" "^3.6.0" + "@ethereumjs/blockchain" "^5.5.0" + "@ethereumjs/common" "^2.6.0" + "@ethereumjs/tx" "^3.4.0" + "@ethereumjs/vm" "^5.6.0" + "@ethersproject/abi" "^5.1.2" + "@metamask/eth-sig-util" "^4.0.0" + "@sentry/node" "^5.18.1" + "@solidity-parser/parser" "^0.14.1" + "@types/bn.js" "^5.1.0" + "@types/lru-cache" "^5.1.0" + abort-controller "^3.0.0" + adm-zip "^0.4.16" + aggregate-error "^3.0.0" + ansi-escapes "^4.3.0" + chalk "^2.4.2" + chokidar "^3.4.0" + ci-info "^2.0.0" + debug "^4.1.1" + enquirer "^2.3.0" + env-paths "^2.2.0" + ethereum-cryptography "^0.1.2" + ethereumjs-abi "^0.6.8" + ethereumjs-util "^7.1.3" + find-up "^2.1.0" + fp-ts "1.19.3" + fs-extra "^7.0.1" + glob "^7.1.3" + immutable "^4.0.0-rc.12" + io-ts "1.10.4" + lodash "^4.17.11" + merkle-patricia-tree "^4.2.2" + mnemonist "^0.38.0" + mocha "^9.2.0" + p-map "^4.0.0" + qs "^6.7.0" + raw-body "^2.4.1" + resolve "1.17.0" + semver "^6.3.0" + slash "^3.0.0" + solc "0.7.3" + source-map-support "^0.5.13" + stacktrace-parser "^0.1.10" + "true-case-path" "^2.2.1" + tsort "0.0.1" + undici "^4.14.1" + uuid "^8.3.2" + ws "^7.4.6" + hardhat@^2.9.5: version "2.10.1" resolved "https://registry.yarnpkg.com/hardhat/-/hardhat-2.10.1.tgz#37fdc0c96d6a5d16b322269db2ad8f9f115c4046" @@ -8021,11 +8288,26 @@ lodash.assign@^4.0.3, lodash.assign@^4.0.6: resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7" integrity sha512-hFuH8TY+Yji7Eja3mGiuAxBqLagejScbG8GbG0j6o9vzn0YL14My+ktnqtZgFTosKymC9/44wP6s7xyuLfnClw== +lodash.camelcase@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" + integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY= + lodash.get@^4: version "4.4.2" resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ== +lodash.isequal@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA= + +lodash.isequalwith@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.isequalwith/-/lodash.isequalwith-4.4.0.tgz#266726ddd528f854f21f4ea98a065606e0fbc6b0" + integrity sha1-Jmcm3dUo+FTyH06pigZWBuD7xrA= + lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" @@ -8315,7 +8597,7 @@ merkle-patricia-tree@^2.1.2, merkle-patricia-tree@^2.3.2: rlp "^2.0.0" semaphore ">=1.0.1" -merkle-patricia-tree@^4.2.4: +merkle-patricia-tree@^4.2.2, merkle-patricia-tree@^4.2.4: version "4.2.4" resolved "https://registry.yarnpkg.com/merkle-patricia-tree/-/merkle-patricia-tree-4.2.4.tgz#ff988d045e2bf3dfa2239f7fabe2d59618d57413" integrity sha512-eHbf/BG6eGNsqqfbLED9rIqbsF4+sykEaBn6OLNs71tjclbMcMOk1tEPmJKcNcNCLkvbpY/lwyOlizWsqPNo8w== @@ -8442,6 +8724,13 @@ minimatch@3.0.4: dependencies: brace-expansion "^1.1.7" +minimatch@4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-4.2.1.tgz#40d9d511a46bdc4e563c22c3080cde9c0d8299b4" + integrity sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g== + dependencies: + brace-expansion "^1.1.7" + minimatch@5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.0.1.tgz#fb9022f7528125187c92bd9e9b6366be1cf3415b" @@ -8577,6 +8866,36 @@ mocha@^7.1.1: yargs-parser "13.1.2" yargs-unparser "1.6.0" +mocha@^9.2.0: + version "9.2.2" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-9.2.2.tgz#d70db46bdb93ca57402c809333e5a84977a88fb9" + integrity sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g== + dependencies: + "@ungap/promise-all-settled" "1.1.2" + ansi-colors "4.1.1" + browser-stdout "1.3.1" + chokidar "3.5.3" + debug "4.3.3" + diff "5.0.0" + escape-string-regexp "4.0.0" + find-up "5.0.0" + glob "7.2.0" + growl "1.10.5" + he "1.2.0" + js-yaml "4.1.0" + log-symbols "4.1.0" + minimatch "4.2.1" + ms "2.1.3" + nanoid "3.3.1" + serialize-javascript "6.0.0" + strip-json-comments "3.1.1" + supports-color "8.1.1" + which "2.0.2" + workerpool "6.2.0" + yargs "16.2.0" + yargs-parser "20.2.4" + yargs-unparser "2.0.0" + mock-fs@^4.1.0: version "4.14.0" resolved "https://registry.yarnpkg.com/mock-fs/-/mock-fs-4.14.0.tgz#ce5124d2c601421255985e6e94da80a7357b1b18" @@ -8781,6 +9100,11 @@ nano-json-stream-parser@^0.1.2: resolved "https://registry.yarnpkg.com/nano-json-stream-parser/-/nano-json-stream-parser-0.1.2.tgz#0cc8f6d0e2b622b479c40d499c46d64b755c6f5f" integrity sha512-9MqxMH/BSJC7dnLsEMPyfN5Dvoo49IsPFYMcHw3Bcfc2kN0lpHRBSzlMSVx4HGyJ7s9B31CyBTVehWJoQ8Ctew== +nanoid@3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.1.tgz#6347a18cac88af88f58af0b3594b723d5e99bb35" + integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw== + nanoid@3.3.3: version "3.3.3" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.3.tgz#fd8e8b7aa761fe807dba2d1b98fb7241bb724a25" @@ -10145,6 +10469,11 @@ redent@^3.0.0: indent-string "^4.0.0" strip-indent "^3.0.0" +reduce-flatten@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/reduce-flatten/-/reduce-flatten-2.0.0.tgz#734fd84e65f375d7ca4465c69798c25c9d10ae27" + integrity sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w== + regenerate@^1.2.1: version "1.4.2" resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" @@ -10485,6 +10814,13 @@ rxjs@^6.4.0: dependencies: tslib "^1.9.0" +rxjs@^7.2.0: + version "7.5.5" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.5.tgz#2ebad89af0f560f460ad5cc4213219e1f7dd4e9f" + integrity sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw== + dependencies: + tslib "^2.1.0" + rxjs@^7.5.1, rxjs@^7.5.5: version "7.5.6" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.6.tgz#0446577557862afd6903517ce7cae79ecb9662bc" @@ -11228,6 +11564,11 @@ string-argv@0.3.1: resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da" integrity sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg== +string-format@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/string-format/-/string-format-2.0.0.tgz#f2df2e7097440d3b65de31b6d40d54c96eaffb9b" + integrity sha512-bbEs3scLeYNXLecRRuk6uJxdXUSj6le/8rNPHChIJTn2V79aXVTR1EH2OH5zLKKoz0V02fOUKZZcw01pLUShZA== + string-width@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" @@ -11460,6 +11801,16 @@ sync-rpc@^1.2.1: dependencies: get-port "^3.1.0" +table-layout@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/table-layout/-/table-layout-1.0.2.tgz#c4038a1853b0136d63365a734b6931cf4fad4a04" + integrity sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A== + dependencies: + array-back "^4.0.1" + deep-extend "~0.6.0" + typical "^5.2.0" + wordwrapjs "^4.0.0" + table@^5.2.3: version "5.4.6" resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e" @@ -11713,6 +12064,16 @@ triple-beam@^1.3.0: resolved "https://registry.yarnpkg.com/true-case-path/-/true-case-path-2.2.1.tgz#c5bf04a5bbec3fd118be4084461b3a27c4d796bf" integrity sha512-0z3j8R7MCjy10kc/g+qg7Ln3alJTodw9aDuVWZa3uiWqfuBMKeAeP2ocWcxoyM3D73yz3Jt/Pu4qPr4wHSdB/Q== +ts-command-line-args@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ts-command-line-args/-/ts-command-line-args-2.2.1.tgz#fd6913e542099012c0ffb2496126a8f38305c7d6" + integrity sha512-mnK68QA86FYzQYTSA/rxIjT/8EpKsvQw9QkawPic8I8t0gjAOw3Oa509NIRoaY1FmH7hdrncMp7t7o+vYoceNQ== + dependencies: + chalk "^4.1.0" + command-line-args "^5.1.1" + command-line-usage "^6.1.0" + string-format "^2.0.0" + ts-essentials@^1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/ts-essentials/-/ts-essentials-1.0.4.tgz#ce3b5dade5f5d97cf69889c11bf7d2da8555b15a" @@ -11743,6 +12104,25 @@ ts-generator@^0.1.1: resolve "^1.8.1" ts-essentials "^1.0.0" +ts-node@^10.2.1: + version "10.7.0" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.7.0.tgz#35d503d0fab3e2baa672a0e94f4b40653c2463f5" + integrity sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A== + dependencies: + "@cspotcode/source-map-support" "0.7.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.0" + yn "3.1.1" + ts-node@^10.9.1: version "10.9.1" resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b" @@ -11885,6 +12265,22 @@ type@^2.5.0: resolved "https://registry.yarnpkg.com/type/-/type-2.6.0.tgz#3ca6099af5981d36ca86b78442973694278a219f" integrity sha512-eiDBDOmkih5pMbo9OqsqPRGMljLodLcwd5XD5JbtNB0o89xZAwynY9EdCDsJU7LtcVCClu9DvM7/0Ep1hYX3EQ== +typechain@7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/typechain/-/typechain-7.0.0.tgz#258ca136de1d451368bde01c318976a83062f110" + integrity sha512-ILfvBBFJ7j9aIk0crX03+N2GmzoDN1gtk32G1+XrasjuvXS0XAw2XxwQeQMMgKwlnxViJjIkG87sTMYXPkXA9g== + dependencies: + "@types/prettier" "^2.1.1" + debug "^4.1.1" + fs-extra "^7.0.0" + glob "^7.1.6" + js-sha3 "^0.8.0" + lodash "^4.17.15" + mkdirp "^1.0.4" + prettier "^2.1.2" + ts-command-line-args "^2.2.0" + ts-essentials "^7.0.1" + typechain@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/typechain/-/typechain-3.0.0.tgz#d5a47700831f238e43f7429b987b4bb54849b92e" @@ -11953,6 +12349,16 @@ typical@^2.6.0, typical@^2.6.1: resolved "https://registry.yarnpkg.com/typical/-/typical-2.6.1.tgz#5c080e5d661cbbe38259d2e70a3c7253e873881d" integrity sha512-ofhi8kjIje6npGozTip9Fr8iecmYfEbS06i0JnIg+rh51KakryWF4+jX8lLKZVhy6N+ID45WYSFCxPOdTWCzNg== +typical@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/typical/-/typical-4.0.0.tgz#cbeaff3b9d7ae1e2bbfaf5a4e6f11eccfde94fc4" + integrity sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw== + +typical@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/typical/-/typical-5.2.0.tgz#4daaac4f2b5315460804f0acf6cb69c52bb93066" + integrity sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg== + uglify-js@^3.1.4: version "3.16.2" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.16.2.tgz#0481e1dbeed343ad1c2ddf3c6d42e89b7a6d4def" @@ -12005,6 +12411,11 @@ underscore@^1.13.1: resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.4.tgz#7886b46bbdf07f768e0052f1828e1dcab40c0dee" integrity sha512-BQFnUDuAQ4Yf/cYY5LNrK9NCJFKriaRbD9uR1fTeXnBeoa97W0i41qkZfGO9pSo8I5KzjAcSY2XYtdf0oKd7KQ== +undici@^4.14.1: + version "4.16.0" + resolved "https://registry.yarnpkg.com/undici/-/undici-4.16.0.tgz#469bb87b3b918818d3d7843d91a1d08da357d5ff" + integrity sha512-tkZSECUYi+/T1i4u+4+lwZmQgLXd4BLGlrc7KZPcLIW7Jpq99+Xpc30ONv7nS6F5UNOxp/HBZSSL9MafUrvJbw== + undici@^5.4.0: version "5.8.0" resolved "https://registry.yarnpkg.com/undici/-/undici-5.8.0.tgz#dec9a8ccd90e5a1d81d43c0eab6503146d649a4f" @@ -12162,6 +12573,11 @@ uuid@^8.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== +v8-compile-cache-lib@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.0.tgz#0582bcb1c74f3a2ee46487ceecf372e46bce53e8" + integrity sha512-mpSYqfsFvASnSn5qMiwrr4VKfumbPyONLCOPmsR3A6pTY/r0+tSaVbgPWSAIuzbk3lCTa+FForeTiO+wBQGkjA== + v8-compile-cache-lib@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" @@ -12679,7 +13095,7 @@ web3-utils@1.2.11: underscore "1.9.1" utf8 "3.0.0" -web3-utils@1.7.4, web3-utils@^1.0.0-beta.31, web3-utils@^1.3.0: +web3-utils@1.7.4: version "1.7.4" resolved "https://registry.yarnpkg.com/web3-utils/-/web3-utils-1.7.4.tgz#eb6fa3706b058602747228234453811bbee017f5" integrity sha512-acBdm6Evd0TEZRnChM/MCvGsMwYKmSh7OaUfNf5OKG0CIeGWD/6gqLOWIwmwSnre/2WrA1nKGId5uW2e5EfluA== @@ -12692,6 +13108,19 @@ web3-utils@1.7.4, web3-utils@^1.0.0-beta.31, web3-utils@^1.3.0: randombytes "^2.1.0" utf8 "3.0.0" +web3-utils@^1.0.0-beta.31, web3-utils@^1.3.0: + version "1.7.1" + resolved "https://registry.yarnpkg.com/web3-utils/-/web3-utils-1.7.1.tgz#77d8bacaf426c66027d8aa4864d77f0ed211aacd" + integrity sha512-fef0EsqMGJUgiHPdX+KN9okVWshbIumyJPmR+btnD1HgvoXijKEkuKBv0OmUqjbeqmLKP2/N9EiXKJel5+E1Dw== + dependencies: + bn.js "^4.11.9" + ethereum-bloom-filters "^1.0.6" + ethereumjs-util "^7.1.0" + ethjs-unit "0.1.6" + number-to-bn "1.7.0" + randombytes "^2.1.0" + utf8 "3.0.0" + web3@1.2.11: version "1.2.11" resolved "https://registry.yarnpkg.com/web3/-/web3-1.2.11.tgz#50f458b2e8b11aa37302071c170ed61cff332975" @@ -12805,7 +13234,7 @@ which@1.3.1, which@^1.1.1, which@^1.2.9, which@^1.3.1: dependencies: isexe "^2.0.0" -which@^2.0.1: +which@2.0.2, which@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== @@ -12871,6 +13300,19 @@ wordwrap@^1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== +wordwrapjs@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/wordwrapjs/-/wordwrapjs-4.0.1.tgz#d9790bccfb110a0fc7836b5ebce0937b37a8b98f" + integrity sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA== + dependencies: + reduce-flatten "^2.0.0" + typical "^5.2.0" + +workerpool@6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.0.tgz#827d93c9ba23ee2019c3ffaff5c27fccea289e8b" + integrity sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A== + workerpool@6.2.1: version "6.2.1" resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343"