diff --git a/cli/commands/bridge/to-l1.ts b/cli/commands/bridge/to-l1.ts index 1d6506a31..42f319eb4 100644 --- a/cli/commands/bridge/to-l1.ts +++ b/cli/commands/bridge/to-l1.ts @@ -2,7 +2,7 @@ import { loadEnv, CLIArgs, CLIEnvironment } from '../../env' import { logger } from '../../logging' import { getAddressBook } from '../../address-book' import { getProvider, sendTransaction, toGRT } from '../../network' -import { chainIdIsL2 } from '../../utils' +import { chainIdIsL2 } from '../../cross-chain' import { loadAddressBookContract } from '../../contracts' import { L2TransactionReceipt, L2ToL1MessageStatus, L2ToL1MessageWriter } from '@arbitrum/sdk' import { L2GraphTokenGateway } from '../../../build/types/L2GraphTokenGateway' diff --git a/cli/commands/bridge/to-l2.ts b/cli/commands/bridge/to-l2.ts index 32b8aef74..1caf008d9 100644 --- a/cli/commands/bridge/to-l2.ts +++ b/cli/commands/bridge/to-l2.ts @@ -1,16 +1,11 @@ +import { Argv } from 'yargs' +import { utils } from 'ethers' +import { L1TransactionReceipt, L1ToL2MessageStatus, L1ToL2MessageWriter } from '@arbitrum/sdk' + import { loadEnv, CLIArgs, CLIEnvironment } from '../../env' import { logger } from '../../logging' -import { getProvider, sendTransaction, toGRT } from '../../network' -import { BigNumber, utils } from 'ethers' -import { parseEther } from '@ethersproject/units' -import { - L1TransactionReceipt, - L1ToL2MessageStatus, - L1ToL2MessageWriter, - L1ToL2MessageGasEstimator, -} from '@arbitrum/sdk' -import { chainIdIsL2 } from '../../utils' -import { Argv } from 'yargs' +import { getProvider, sendTransaction, toGRT, ensureAllowance, toBN } from '../../network' +import { chainIdIsL2, estimateRetryableTxGas } from '../../cross-chain' const logAutoRedeemReason = (autoRedeemRec) => { if (autoRedeemRec == null) { @@ -49,95 +44,75 @@ export const sendToL2 = async (cli: CLIEnvironment, cliArgs: CLIArgs): Promise>> Sending tokens to L2 <<<\n`) // parse provider + const l1Provider = cli.wallet.provider const l2Provider = getProvider(cliArgs.l2ProviderUrl) + const l1ChainId = cli.chainId const l2ChainId = (await l2Provider.getNetwork()).chainId - if (chainIdIsL2(cli.chainId) || !chainIdIsL2(l2ChainId)) { + if (chainIdIsL2(l1ChainId) || !chainIdIsL2(l2ChainId)) { throw new Error( 'Please use an L1 provider in --provider-url, and an L2 provider in --l2-provider-url', ) } - const gateway = cli.contracts['L1GraphTokenGateway'] - const l1GRT = cli.contracts['GraphToken'] - const l1GRTAddress = l1GRT.address + + // parse params + const { L1GraphTokenGateway: l1Gateway, GraphToken: l1GRT } = cli.contracts const amount = toGRT(cliArgs.amount) - const recipient = cliArgs.recipient ? cliArgs.recipient : cli.wallet.address - const l2Dest = await gateway.l2Counterpart() + const recipient = cliArgs.recipient ?? cli.wallet.address + const l1GatewayAddress = l1Gateway.address + const l2GatewayAddress = await l1Gateway.l2Counterpart() const calldata = cliArgs.calldata ?? '0x' + // transport tokens logger.info(`Will send ${cliArgs.amount} GRT to ${recipient}`) - logger.info(`Using L1 gateway ${gateway.address} and L2 gateway ${l2Dest}`) + logger.info(`Using L1 gateway ${l1GatewayAddress} and L2 gateway ${l2GatewayAddress}`) + await ensureAllowance(cli.wallet, l1GatewayAddress, l1GRT, amount) + + // estimate L2 ticket // See https://github.com/OffchainLabs/arbitrum/blob/master/packages/arb-ts/src/lib/bridge.ts - const depositCalldata = await gateway.getOutboundCalldata( - l1GRTAddress, + const depositCalldata = await l1Gateway.getOutboundCalldata( + l1GRT.address, cli.wallet.address, recipient, amount, calldata, ) - - const senderBalance = await l1GRT.balanceOf(cli.wallet.address) - if (senderBalance.lt(amount)) { - throw new Error('Sender balance is insufficient for the transfer') - } - logger.info('Approving token transfer') - await sendTransaction(cli.wallet, l1GRT, 'approve', [gateway.address, amount]) - - let maxGas: BigNumber - let gasPriceBid: BigNumber - let maxSubmissionPrice: BigNumber - - if (!cliArgs.maxGas || !cliArgs.gasPrice || !cliArgs.maxSubmissionCost) { - // 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.estimateAll( - gateway.address, - l2Dest, - depositCalldata, - parseEther('0'), - baseFee as BigNumber, - gateway.address, - gateway.address, - cli.wallet.provider, - ) - maxGas = cliArgs.maxGas ? BigNumber.from(cliArgs.maxGas) : gasParams.gasLimit - gasPriceBid = cliArgs.gasPrice ? BigNumber.from(cliArgs.gasPrice) : gasParams.maxFeePerGas - maxSubmissionPrice = cliArgs.maxSubmissionCost - ? BigNumber.from(cliArgs.maxSubmissionCost) - : gasParams.maxSubmissionFee - } else { - maxGas = BigNumber.from(cliArgs.maxGas) - gasPriceBid = BigNumber.from(cliArgs.gasPrice) - maxSubmissionPrice = BigNumber.from(cliArgs.maxSubmissionCost) - } - + const { maxGas, gasPriceBid, maxSubmissionCost } = await estimateRetryableTxGas( + l1Provider, + l2Provider, + l1GatewayAddress, + l2GatewayAddress, + depositCalldata, + { + maxGas: cliArgs.maxGas, + gasPriceBid: cliArgs.gasPriceBid, + maxSubmissionCost: cliArgs.maxSubmissionCost, + }, + ) + const ethValue = maxSubmissionCost.add(gasPriceBid.mul(maxGas)) logger.info( - `Using max gas: ${maxGas}, gas price bid: ${gasPriceBid}, max submission price: ${maxSubmissionPrice}`, + `Using maxGas:${maxGas}, gasPriceBid:${gasPriceBid}, maxSubmissionCost:${maxSubmissionCost} = tx value: ${ethValue}`, ) - const ethValue = maxSubmissionPrice.add(gasPriceBid.mul(maxGas)) - logger.info(`tx value: ${ethValue}`) - const data = utils.defaultAbiCoder.encode(['uint256', 'bytes'], [maxSubmissionPrice, calldata]) - - const params = [l1GRTAddress, recipient, amount, maxGas, gasPriceBid, data] + // build transaction logger.info('Sending outbound transfer transaction') - const receipt = await sendTransaction(cli.wallet, gateway, 'outboundTransfer', params, { - value: ethValue, - }) - const l1Receipt = new L1TransactionReceipt(receipt) - const l1ToL2Messages = await l1Receipt.getL1ToL2Messages(cli.wallet.connect(l2Provider)) - const l1ToL2Message = l1ToL2Messages[0] - - 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) + const txData = utils.defaultAbiCoder.encode(['uint256', 'bytes'], [maxSubmissionCost, calldata]) + const txParams = [l1GRT.address, recipient, amount, maxGas, gasPriceBid, txData] + const txReceipt = await sendTransaction(cli.wallet, l1Gateway, 'outboundTransfer', txParams) + + // get l2 ticket status + if (txReceipt.status == 1) { + logger.info('Waiting for message to propagate to L2...') + const l1Receipt = new L1TransactionReceipt(txReceipt) + const l1ToL2Messages = await l1Receipt.getL1ToL2Messages(cli.wallet.connect(l2Provider)) + const l1ToL2Message = l1ToL2Messages[0] + try { + await checkAndRedeemMessage(l1ToL2Message) + } catch (e) { + logger.error('Auto redeem failed') + logger.error(e) + logger.error('You can re-attempt using redeem-send-to-l2 with the following txHash:') + logger.error(txReceipt.transactionHash) + } } } @@ -172,7 +147,7 @@ export const sendToL2Command = { requiresArg: true, type: 'string', }) - .option('gas-price', { + .option('gas-price-bid', { description: 'Gas price for the L2 redemption attempt', requiresArg: true, type: 'string', @@ -189,6 +164,11 @@ export const sendToL2Command = { .positional('calldata', { description: 'Calldata to pass to the recipient. Must be whitelisted in the bridge', }) + .coerce({ + maxGas: toBN, + gasPriceBid: toBN, + maxSubmissionCost: toBN, + }) }, handler: async (argv: CLIArgs): Promise => { return sendToL2(await loadEnv(argv), argv) diff --git a/cli/commands/migrate.ts b/cli/commands/migrate.ts index 20934d518..3b5c86272 100644 --- a/cli/commands/migrate.ts +++ b/cli/commands/migrate.ts @@ -11,7 +11,7 @@ import { sendTransaction, } from '../network' import { loadEnv, CLIArgs, CLIEnvironment } from '../env' -import { chainIdIsL2 } from '../utils' +import { chainIdIsL2 } from '../cross-chain' import { confirm } from '../helpers' const { EtherSymbol } = constants diff --git a/cli/commands/protocol/configure-bridge.ts b/cli/commands/protocol/configure-bridge.ts index d96d462a5..057ebc653 100644 --- a/cli/commands/protocol/configure-bridge.ts +++ b/cli/commands/protocol/configure-bridge.ts @@ -2,7 +2,7 @@ import { loadEnv, CLIArgs, CLIEnvironment } from '../../env' import { logger } from '../../logging' import { getAddressBook } from '../../address-book' import { sendTransaction } from '../../network' -import { chainIdIsL2, l1ToL2ChainIdMap, l2ToL1ChainIdMap } from '../../utils' +import { chainIdIsL2, l1ToL2ChainIdMap, l2ToL1ChainIdMap } from '../../cross-chain' export const configureL1Bridge = async (cli: CLIEnvironment, cliArgs: CLIArgs): Promise => { logger.info(`>>> Setting L1 Bridge Configuration <<<\n`) diff --git a/cli/contracts.ts b/cli/contracts.ts index 0657f764a..f8ed22277 100644 --- a/cli/contracts.ts +++ b/cli/contracts.ts @@ -1,6 +1,7 @@ import { BaseContract, providers, Signer } from 'ethers' import { AddressBook } from './address-book' +import { chainIdIsL2 } from './cross-chain' import { logger } from './logging' import { getContractAt } from './network' @@ -22,7 +23,6 @@ import { L1GraphTokenGateway } from '../build/types/L1GraphTokenGateway' import { L2GraphToken } from '../build/types/L2GraphToken' import { L2GraphTokenGateway } from '../build/types/L2GraphTokenGateway' import { BridgeEscrow } from '../build/types/BridgeEscrow' -import { chainIdIsL2 } from './utils' import { SubgraphNFT } from '../build/types/SubgraphNFT' import { GraphCurationToken } from '../build/types/GraphCurationToken' import { SubgraphNFTDescriptor } from '../build/types/SubgraphNFTDescriptor' diff --git a/cli/cross-chain.ts b/cli/cross-chain.ts new file mode 100644 index 000000000..ea3b081ac --- /dev/null +++ b/cli/cross-chain.ts @@ -0,0 +1,63 @@ +import { L1ToL2MessageGasEstimator } from '@arbitrum/sdk' +import { BigNumber, providers } from 'ethers' +import { parseEther } from 'ethers/lib/utils' + +import { logger } from './logging' + +export const l1ToL2ChainIdMap = { + '1': '42161', + '4': '421611', + '5': '421613', + '1337': '412346', +} + +export const l2ChainIds = Object.values(l1ToL2ChainIdMap).map(Number) +export const l2ToL1ChainIdMap = Object.fromEntries( + Object.entries(l1ToL2ChainIdMap).map(([k, v]) => [v, k]), +) + +export const chainIdIsL2 = (chainId: number | string): boolean => { + return l2ChainIds.includes(Number(chainId)) +} + +interface L2GasParams { + maxGas: BigNumber + gasPriceBid: BigNumber + maxSubmissionCost: BigNumber +} + +export const estimateRetryableTxGas = async ( + l1Provider: providers.Provider, + l2Provider: providers.Provider, + gatewayAddress: string, + l2Dest: string, + depositCalldata: string, + opts: L2GasParams, +): Promise => { + const autoEstimate = opts && (!opts.maxGas || !opts.gasPriceBid || !opts.maxSubmissionCost) + if (!autoEstimate) { + return opts + } + + // Comment from Offchain Labs' implementation: + // we add a 0.05 ether "deposit" buffer to pay for execution in the gas estimation + logger.info('Estimating retryable ticket gas:') + const baseFee = (await l1Provider.getBlock('latest')).baseFeePerGas + const gasEstimator = new L1ToL2MessageGasEstimator(l2Provider) + const gasParams = await gasEstimator.estimateAll( + gatewayAddress, + l2Dest, + depositCalldata, + parseEther('0'), + baseFee as BigNumber, + gatewayAddress, + gatewayAddress, + l1Provider, + ) + // override fixed values + return { + maxGas: opts.maxGas ?? gasParams.gasLimit, + gasPriceBid: opts.gasPriceBid ?? gasParams.maxFeePerGas, + maxSubmissionCost: opts.maxSubmissionCost ?? gasParams.maxSubmissionFee, + } +} diff --git a/cli/env.ts b/cli/env.ts index ab89c9d85..d607e4be5 100644 --- a/cli/env.ts +++ b/cli/env.ts @@ -4,7 +4,7 @@ import { Argv } from 'yargs' import { logger } from './logging' import { getAddressBook, AddressBook } from './address-book' import { defaultOverrides } from './defaults' -import { getProvider } from './utils' +import { getProvider } from './network' import { loadContracts, NetworkContracts } from './contracts' const { formatEther } = utils diff --git a/cli/network.ts b/cli/network.ts index 3dc2e69c1..6667a960d 100644 --- a/cli/network.ts +++ b/cli/network.ts @@ -10,16 +10,19 @@ import { Overrides, BigNumber, PayableOverrides, + Wallet, } from 'ethers' import { logger } from './logging' import { AddressBook } from './address-book' import { loadArtifact } from './artifacts' import { defaultOverrides } from './defaults' +import { GraphToken } from '../build/types/GraphToken' const { keccak256, randomBytes, parseUnits, hexlify } = utils export const randomHexBytes = (n = 32): string => hexlify(randomBytes(n)) +export const toBN = (value: string | number | BigNumber): BigNumber => BigNumber.from(value) export const toGRT = (value: string | number): BigNumber => { return parseUnits(typeof value === 'number' ? value.toString() : value, '18') } @@ -368,3 +371,26 @@ export const linkLibraries = ( } return bytecode } + +export const ensureAllowance = async ( + sender: Wallet, + spenderAddress: string, + token: GraphToken, + amount: BigNumber, +) => { + // check balance + const senderBalance = await token.balanceOf(sender.address) + if (senderBalance.lt(amount)) { + throw new Error('Sender balance is insufficient for the transfer') + } + + // check allowance + const allowance = await token.allowance(sender.address, spenderAddress) + if (allowance.gte(amount)) { + return + } + + // approve + logger.info('Approving token transfer') + return sendTransaction(sender, token, 'approve', [spenderAddress, amount]) +} diff --git a/cli/utils.ts b/cli/utils.ts deleted file mode 100644 index 6fda0d611..000000000 --- a/cli/utils.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Contract, Wallet, providers } from 'ethers' - -import { loadArtifact } from './artifacts' - -export const l1ToL2ChainIdMap = { - '1': '42161', - '4': '421611', - '5': '421613', - '1337': '412346', -} - -export const l2ChainIds = Object.values(l1ToL2ChainIdMap).map(Number) -export const l2ToL1ChainIdMap = Object.fromEntries( - Object.entries(l1ToL2ChainIdMap).map(([k, v]) => [v, k]), -) - -export const contractAt = ( - contractName: string, - contractAddress: string, - wallet: Wallet, -): Contract => { - return new Contract(contractAddress, loadArtifact(contractName).abi, wallet.provider) -} - -export const getProvider = (providerUrl: string, network?: number): providers.JsonRpcProvider => - new providers.JsonRpcProvider(providerUrl, network) - -export const chainIdIsL2 = (chainId: number | string): boolean => { - return l2ChainIds.includes(Number(chainId)) -} diff --git a/tasks/deployment/sync.ts b/tasks/deployment/sync.ts index 27e668f00..29cebd7bd 100644 --- a/tasks/deployment/sync.ts +++ b/tasks/deployment/sync.ts @@ -1,7 +1,7 @@ import { ContractTransaction } from 'ethers' import { task } from 'hardhat/config' import { cliOpts } from '../../cli/defaults' -import { chainIdIsL2 } from '../../cli/utils' +import { chainIdIsL2 } from '../../cli/cross-chain' task('migrate:sync', 'Sync controller contracts') .addParam('addressBook', cliOpts.addressBook.description, cliOpts.addressBook.default)