Skip to content

refactor: send tokens to L2 #687

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cli/commands/bridge/to-l1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
142 changes: 61 additions & 81 deletions cli/commands/bridge/to-l2.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down Expand Up @@ -49,95 +44,75 @@ export const sendToL2 = async (cli: CLIEnvironment, cliArgs: CLIArgs): Promise<v
logger.info(`>>> Sending tokens to L2 <<<\n`)

// parse provider
const l1Provider = cli.wallet.provider
const l2Provider = getProvider(cliArgs.l2ProviderUrl)
const l1ChainId = cli.chainId
const l2ChainId = (await l2Provider.getNetwork()).chainId
if (chainIdIsL2(cli.chainId) || !chainIdIsL2(l2ChainId)) {
if (chainIdIsL2(l1ChainId) || !chainIdIsL2(l2ChainId)) {
throw new Error(
'Please use an L1 provider in --provider-url, and an L2 provider in --l2-provider-url',
)
}
const gateway = cli.contracts['L1GraphTokenGateway']
const l1GRT = cli.contracts['GraphToken']
const l1GRTAddress = l1GRT.address

// parse params
const { L1GraphTokenGateway: l1Gateway, GraphToken: l1GRT } = cli.contracts
const amount = toGRT(cliArgs.amount)
const recipient = cliArgs.recipient ? cliArgs.recipient : cli.wallet.address
const l2Dest = await gateway.l2Counterpart()
const recipient = cliArgs.recipient ?? cli.wallet.address
const l1GatewayAddress = l1Gateway.address
const l2GatewayAddress = await l1Gateway.l2Counterpart()
const calldata = cliArgs.calldata ?? '0x'

// transport tokens
logger.info(`Will send ${cliArgs.amount} GRT to ${recipient}`)
logger.info(`Using L1 gateway ${gateway.address} and L2 gateway ${l2Dest}`)
logger.info(`Using L1 gateway ${l1GatewayAddress} and L2 gateway ${l2GatewayAddress}`)
await ensureAllowance(cli.wallet, l1GatewayAddress, l1GRT, amount)

// estimate L2 ticket
// See https://github.com/OffchainLabs/arbitrum/blob/master/packages/arb-ts/src/lib/bridge.ts
const depositCalldata = await gateway.getOutboundCalldata(
l1GRTAddress,
const depositCalldata = await l1Gateway.getOutboundCalldata(
l1GRT.address,
cli.wallet.address,
recipient,
amount,
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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we fail more noisily if txReceipt.status is not 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)
}
}
}

Expand Down Expand Up @@ -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',
Expand All @@ -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<void> => {
return sendToL2(await loadEnv(argv), argv)
Expand Down
2 changes: 1 addition & 1 deletion cli/commands/migrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion cli/commands/protocol/configure-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> => {
logger.info(`>>> Setting L1 Bridge Configuration <<<\n`)
Expand Down
2 changes: 1 addition & 1 deletion cli/contracts.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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'
Expand Down
63 changes: 63 additions & 0 deletions cli/cross-chain.ts
Original file line number Diff line number Diff line change
@@ -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<L2GasParams> => {
const autoEstimate = opts && (!opts.maxGas || !opts.gasPriceBid || !opts.maxSubmissionCost)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the opts && needed here? I think typescript won't allow it to be null anyways (unless you mark it as optional in the function definition), but in any case if opts is nullish we would want to auto-estimate, wouldn't we?

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,
}
}
2 changes: 1 addition & 1 deletion cli/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 26 additions & 0 deletions cli/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
}
Expand Down Expand Up @@ -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])
}
30 changes: 0 additions & 30 deletions cli/utils.ts

This file was deleted.

2 changes: 1 addition & 1 deletion tasks/deployment/sync.ts
Original file line number Diff line number Diff line change
@@ -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)
Expand Down