Skip to content

Commit

Permalink
Sync: l2-testnet > pcv/l2-bridge (#704)
Browse files Browse the repository at this point in the history
Signed-off-by: Tomás Migone <[email protected]>
Co-authored-by: Pablo Carranza Vélez <[email protected]>
Co-authored-by: Ariel Barmat <[email protected]>
  • Loading branch information
3 people authored Sep 22, 2022
1 parent b510d8e commit 8c8f6aa
Show file tree
Hide file tree
Showing 43 changed files with 1,146 additions and 231 deletions.
24 changes: 23 additions & 1 deletion TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,26 @@ Scenarios are defined by an optional script and a test file:
- They run before the test file.
- Test file
- Should be named e2e/scenarios/{scenario-name}.test.ts.
- Standard chai/mocha/hardhat/ethers test file.
- Standard chai/mocha/hardhat/ethers test file.

## Setting up Arbitrum's testnodes

Arbitrum provides a quick way of setting up L1 and L2 testnodes for local development and testing. The following steps will guide you through the process of setting them up. Note that a local installation of Docker and Docker Compose is required.

```bash
git clone https://github.com/offchainlabs/nitro
cd nitro
git submodule update --init --recursive

# Apply any changes you might want, see below for more info, and then start the testnodes
./test-node.bash --init
```

**Useful information**
- L1 RPC: [http://localhost:8545](http://localhost:8545/)
- L2 RPC: [http://localhost:8547](http://localhost:8547/)
- Blockscout explorer (L2 only): [http://localhost:4000/](http://localhost:4000/)
- Prefunded genesis key (L1 and L2): `e887f7d17d07cc7b8004053fb8826f6657084e88904bb61590e498ca04704cf2`

**Enable automine on L1**
In `docker-compose.yml` file edit the `geth` service command by removing the `--dev.period 1` flag.
25 changes: 11 additions & 14 deletions cli/commands/bridge/to-l1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,14 @@ import { loadEnv, CLIArgs, CLIEnvironment } from '../../env'
import { logger } from '../../logging'
import { getAddressBook } from '../../address-book'
import { getProvider, sendTransaction, toGRT } from '../../network'
import { chainIdIsL2 } from '../../utils'
import { chainIdIsL2 } from '../../cross-chain'
import { loadAddressBookContract } from '../../contracts'
import {
L2TransactionReceipt,
getL2Network,
L2ToL1MessageStatus,
L2ToL1MessageWriter,
} from '@arbitrum/sdk'
import { L2TransactionReceipt, L2ToL1MessageStatus, L2ToL1MessageWriter } from '@arbitrum/sdk'
import { L2GraphTokenGateway } from '../../../build/types/L2GraphTokenGateway'
import { BigNumber } from 'ethers'
import { JsonRpcProvider } from '@ethersproject/providers'
import { providers } from 'ethers'
import { L2GraphToken } from '../../../build/types/L2GraphToken'

const FOURTEEN_DAYS_IN_SECONDS = 24 * 3600 * 14

Expand Down Expand Up @@ -82,12 +78,17 @@ export const startSendToL1 = async (cli: CLIEnvironment, cliArgs: CLIArgs): Prom
const l2AddressBook = getAddressBook(cliArgs.addressBook, l2ChainId.toString())

const gateway = loadAddressBookContract('L2GraphTokenGateway', l2AddressBook, l2Wallet)
const l2GRT = loadAddressBookContract('L2GraphToken', l2AddressBook, l2Wallet)
const l2GRT = loadAddressBookContract('L2GraphToken', l2AddressBook, l2Wallet) as L2GraphToken

const l1Gateway = cli.contracts['L1GraphTokenGateway']
logger.info(`Will send ${cliArgs.amount} GRT to ${recipient}`)
logger.info(`Using L2 gateway ${gateway.address} and L1 gateway ${l1Gateway.address}`)

const senderBalance = await l2GRT.balanceOf(cli.wallet.address)
if (senderBalance.lt(amount)) {
throw new Error('Sender balance is insufficient for the transfer')
}

const params = [l1GRTAddress, recipient, amount, '0x']
logger.info('Approving token transfer')
await sendTransaction(l2Wallet, l2GRT, 'approve', [gateway.address, amount])
Expand All @@ -99,9 +100,7 @@ export const startSendToL1 = async (cli: CLIEnvironment, cliArgs: CLIArgs): Prom
params,
)
const l2Receipt = new L2TransactionReceipt(receipt)
const l2ToL1Message = (
await l2Receipt.getL2ToL1Messages(cli.wallet, await getL2Network(l2Provider))
)[0]
const l2ToL1Message = (await l2Receipt.getL2ToL1Messages(cli.wallet))[0]

logger.info(`The transaction generated an L2 to L1 message in outbox with eth block number:`)
logger.info(l2ToL1Message.event.ethBlockNum.toString())
Expand Down Expand Up @@ -157,9 +156,7 @@ export const finishSendToL1 = async (

const l2Receipt = new L2TransactionReceipt(receipt)
logger.info(`Getting L2 to L1 message...`)
const l2ToL1Message = (
await l2Receipt.getL2ToL1Messages(cli.wallet, await getL2Network(l2Provider))
)[0]
const l2ToL1Message = (await l2Receipt.getL2ToL1Messages(cli.wallet))[0]

if (wait) {
const retryDelayMs = cliArgs.retryDelaySeconds ? cliArgs.retryDelaySeconds * 1000 : 60000
Expand Down
152 changes: 93 additions & 59 deletions cli/commands/bridge/to-l2.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
import { Argv } from 'yargs'
import { utils } from 'ethers'
import { L1TransactionReceipt, L1ToL2MessageStatus, L1ToL2MessageWriter } from '@arbitrum/sdk'

import { loadEnv, CLIArgs, CLIEnvironment } from '../../env'
import { logger } from '../../logging'
import { getProvider, sendTransaction, toGRT } from '../../network'
import { utils } from 'ethers'
import { parseEther } from '@ethersproject/units'
import {
L1TransactionReceipt,
L1ToL2MessageStatus,
L1ToL2MessageWriter,
L1ToL2MessageGasEstimator,
} from '@arbitrum/sdk'
import { chainIdIsL2 } from '../../utils'
import { getProvider, sendTransaction, toGRT, ensureAllowance, toBN } from '../../network'
import { chainIdIsL2, estimateRetryableTxGas } from '../../cross-chain'

const logAutoRedeemReason = (autoRedeemRec) => {
if (autoRedeemRec == null) {
Expand All @@ -32,7 +28,12 @@ const checkAndRedeemMessage = async (l1ToL2Message: L1ToL2MessageWriter) => {
logAutoRedeemReason(autoRedeemRec)
logger.info('Attempting to redeem...')
await l1ToL2Message.redeem()
l2TxHash = (await l1ToL2Message.getSuccessfulRedeem()).transactionHash
const redeemAttempt = await l1ToL2Message.getSuccessfulRedeem()
if (redeemAttempt.status == L1ToL2MessageStatus.REDEEMED) {
l2TxHash = redeemAttempt.l2TxReceipt ? redeemAttempt.l2TxReceipt.transactionHash : 'null'
} else {
throw new Error(`Unexpected L1ToL2MessageStatus after redeem attempt: ${res.status}`)
}
} else if (res.status != L1ToL2MessageStatus.REDEEMED) {
throw new Error(`Unexpected L1ToL2MessageStatus ${res.status}`)
}
Expand All @@ -41,75 +42,78 @@ const checkAndRedeemMessage = async (l1ToL2Message: L1ToL2MessageWriter) => {

export const sendToL2 = async (cli: CLIEnvironment, cliArgs: CLIArgs): Promise<void> => {
logger.info(`>>> Sending tokens to L2 <<<\n`)

// parse provider
const l1Provider = cli.wallet.provider
const l2Provider = getProvider(cliArgs.l2ProviderUrl)
const l1ChainId = cli.chainId
const l2ChainId = (await l2Provider.getNetwork()).chainId

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

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

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

// estimate L2 ticket
// See https://github.com/OffchainLabs/arbitrum/blob/master/packages/arb-ts/src/lib/bridge.ts
const depositCalldata = await gateway.getOutboundCalldata(
l1GRTAddress,
const depositCalldata = await l1Gateway.getOutboundCalldata(
l1GRT.address,
cli.wallet.address,
recipient,
amount,
'0x',
calldata,
)

// Comment from Offchain Labs' implementation:
// we add a 0.05 ether "deposit" buffer to pay for execution in the gas estimation
logger.info('Estimating retryable ticket gas:')
const baseFee = (await cli.wallet.provider.getBlock('latest')).baseFeePerGas
const gasEstimator = new L1ToL2MessageGasEstimator(l2Provider)
const gasParams = await gasEstimator.estimateMessage(
gateway.address,
l2Dest,
const { maxGas, gasPriceBid, maxSubmissionCost } = await estimateRetryableTxGas(
l1Provider,
l2Provider,
l1GatewayAddress,
l2GatewayAddress,
depositCalldata,
parseEther('0'),
baseFee,
gateway.address,
gateway.address,
{
maxGas: cliArgs.maxGas,
gasPriceBid: cliArgs.gasPriceBid,
maxSubmissionCost: cliArgs.maxSubmissionCost,
},
)
const maxGas = gasParams.maxGasBid
const gasPriceBid = gasParams.maxGasPriceBid
const maxSubmissionPrice = gasParams.maxSubmissionPriceBid
const ethValue = maxSubmissionCost.add(gasPriceBid.mul(maxGas))
logger.info(
`Using max gas: ${maxGas}, gas price bid: ${gasPriceBid}, max submission price: ${maxSubmissionPrice}`,
`Using maxGas:${maxGas}, gasPriceBid:${gasPriceBid}, maxSubmissionCost:${maxSubmissionCost} = tx value: ${ethValue}`,
)

const ethValue = maxSubmissionPrice.add(gasPriceBid.mul(maxGas))
logger.info(`tx value: ${ethValue}`)
const data = utils.defaultAbiCoder.encode(['uint256', 'bytes'], [maxSubmissionPrice, '0x'])

const params = [l1GRTAddress, recipient, amount, maxGas, gasPriceBid, data]
logger.info('Approving token transfer')
await sendTransaction(cli.wallet, l1GRT, 'approve', [gateway.address, amount])
// build transaction
logger.info('Sending outbound transfer transaction')
const receipt = await sendTransaction(cli.wallet, gateway, 'outboundTransfer', params, {
const txData = utils.defaultAbiCoder.encode(['uint256', 'bytes'], [maxSubmissionCost, calldata])
const txParams = [l1GRT.address, recipient, amount, maxGas, gasPriceBid, txData]
const txReceipt = await sendTransaction(cli.wallet, l1Gateway, 'outboundTransfer', txParams, {
value: ethValue,
})
const l1Receipt = new L1TransactionReceipt(receipt)
const l1ToL2Message = await l1Receipt.getL1ToL2Message(cli.wallet.connect(l2Provider))

logger.info('Waiting for message to propagate to L2...')
try {
await checkAndRedeemMessage(l1ToL2Message)
} catch (e) {
logger.error('Auto redeem failed')
logger.error(e)
logger.error('You can re-attempt using redeem-send-to-l2 with the following txHash:')
logger.error(receipt.transactionHash)
// get l2 ticket status
if (txReceipt.status == 1) {
logger.info('Waiting for message to propagate to L2...')
const l1Receipt = new L1TransactionReceipt(txReceipt)
const l1ToL2Messages = await l1Receipt.getL1ToL2Messages(cli.wallet.connect(l2Provider))
const l1ToL2Message = l1ToL2Messages[0]
try {
await checkAndRedeemMessage(l1ToL2Message)
} catch (e) {
logger.error('Auto redeem failed')
logger.error(e)
logger.error('You can re-attempt using redeem-send-to-l2 with the following txHash:')
logger.error(txReceipt.transactionHash)
}
}
}

Expand All @@ -135,8 +139,38 @@ export const redeemSendToL2 = async (cli: CLIEnvironment, cliArgs: CLIArgs): Pro
}

export const sendToL2Command = {
command: 'send-to-l2 <amount> [recipient]',
command: 'send-to-l2 <amount> [recipient] [calldata]',
describe: 'Perform an L1-to-L2 Graph Token transaction',
builder: (yargs: Argv): Argv => {
return yargs
.option('max-gas', {
description: 'Max gas for the L2 redemption attempt',
requiresArg: true,
type: 'string',
})
.option('gas-price-bid', {
description: 'Gas price for the L2 redemption attempt',
requiresArg: true,
type: 'string',
})
.option('max-submission-cost', {
description: 'Max submission cost for the retryable ticket',
requiresArg: true,
type: 'string',
})
.positional('amount', { description: 'Amount to send (will be converted to wei)' })
.positional('recipient', {
description: 'Receiving address in L2. Same to L1 address if empty',
})
.positional('calldata', {
description: 'Calldata to pass to the recipient. Must be whitelisted in the bridge',
})
.coerce({
maxGas: toBN,
gasPriceBid: toBN,
maxSubmissionCost: toBN,
})
},
handler: async (argv: CLIArgs): Promise<void> => {
return sendToL2(await loadEnv(argv), argv)
},
Expand Down
14 changes: 11 additions & 3 deletions cli/commands/migrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import {
sendTransaction,
} from '../network'
import { loadEnv, CLIArgs, CLIEnvironment } from '../env'
import { chainIdIsL2 } from '../cross-chain'
import { confirm } from '../helpers'
import { chainIdIsL2 } from '../utils'

const { EtherSymbol } = constants
const { formatEther } = utils
Expand Down Expand Up @@ -73,8 +73,8 @@ export const migrate = async (
if (!sure) return

if (chainId == 1337) {
await (cli.wallet.provider as providers.JsonRpcProvider).send('evm_setAutomine', [true])
allContracts = ['EthereumDIDRegistry', ...allContracts]
await setAutoMine(cli.wallet.provider as providers.JsonRpcProvider, true)
} else if (chainIdIsL2(chainId)) {
allContracts = l2Contracts
}
Expand Down Expand Up @@ -169,7 +169,15 @@ export const migrate = async (
logger.info(`Sent ${nTx} transaction${nTx === 1 ? '' : 's'} & spent ${EtherSymbol} ${spent}`)

if (chainId == 1337) {
await (cli.wallet.provider as providers.JsonRpcProvider).send('evm_setAutomine', [autoMine])
await setAutoMine(cli.wallet.provider as providers.JsonRpcProvider, autoMine)
}
}

const setAutoMine = async (provider: providers.JsonRpcProvider, automine: boolean) => {
try {
await provider.send('evm_setAutomine', [automine])
} catch (error) {
logger.warn('The method evm_setAutomine does not exist/is not available!')
}
}

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 @@ -25,7 +26,6 @@ import { L1GraphTokenGateway } from '../build/types/L1GraphTokenGateway'
import { L2GraphToken } from '../build/types/L2GraphToken'
import { L2GraphTokenGateway } from '../build/types/L2GraphTokenGateway'
import { BridgeEscrow } from '../build/types/BridgeEscrow'
import { chainIdIsL2 } from './utils'

export interface NetworkContracts {
EpochManager: EpochManager
Expand Down
Loading

0 comments on commit 8c8f6aa

Please sign in to comment.