Skip to content

Commit e0e2a88

Browse files
committed
feat: add Arbitrum GRT bridge
Deployment commands in the CLI are also updated to include an L2 deployment. Configuration and address book entries for Arbitrum are added as well.
1 parent 69dcc9a commit e0e2a88

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+5798
-1392
lines changed

.solcover.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const skipFiles = ['bancor', 'ens', 'erc1056']
1+
const skipFiles = ['bancor', 'ens', 'erc1056', 'arbitrum', 'tests/arbitrum']
22

33
module.exports = {
44
providerOptions: {

arbitrum-addresses.json

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{
2+
"source": "https://developer.offchainlabs.com/docs/useful_addresses",
3+
"1": {
4+
"L1GatewayRouter": {
5+
"address": "0x72Ce9c846789fdB6fC1f34aC4AD25Dd9ef7031ef"
6+
},
7+
"IInbox": {
8+
"address": "0x4Dbd4fc535Ac27206064B68FfCf827b0A60BAB3f"
9+
}
10+
},
11+
"4": {
12+
"L1GatewayRouter": {
13+
"address": "0x70C143928eCfFaf9F5b406f7f4fC28Dc43d68380"
14+
},
15+
"IInbox": {
16+
"address": "0x578BAde599406A8fE3d24Fd7f7211c0911F5B29e"
17+
}
18+
},
19+
"5": {
20+
"L1GatewayRouter": {
21+
"address": "0x4c7708168395aEa569453Fc36862D2ffcDaC588c"
22+
},
23+
"IInbox": {
24+
"address": "0x6BEbC4925716945D46F0Ec336D5C2564F419682C"
25+
}
26+
},
27+
"42161": {
28+
"L2GatewayRouter": {
29+
"address": "0x5288c571Fd7aD117beA99bF60FE0846C4E84F933"
30+
}
31+
},
32+
"421611": {
33+
"L2GatewayRouter": {
34+
"address": "0x9413AD42910c1eA60c737dB5f58d1C504498a3cD"
35+
}
36+
},
37+
"421613": {
38+
"L2GatewayRouter": {
39+
"address": "0xE5B9d8d42d656d1DcB8065A6c012FE3780246041"
40+
}
41+
}
42+
}

cli/address-book.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export interface AddressBook {
2828
}
2929

3030
export const getAddressBook = (path: string, chainId: string): AddressBook => {
31-
if (!path) throw new Error(`A path the the address book file is required.`)
31+
if (!path) throw new Error(`A path to the address book file is required.`)
3232
if (!chainId) throw new Error(`A chainId is required.`)
3333

3434
const addressBook = JSON.parse(fs.readFileSync(path, 'utf8') || '{}') as AddressBookJson

cli/cli.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { proxyCommand } from './commands/proxy'
88
import { protocolCommand } from './commands/protocol'
99
import { contractsCommand } from './commands/contracts'
1010
import { airdropCommand } from './commands/airdrop'
11+
import { bridgeCommand } from './commands/bridge'
1112

1213
import { cliOpts } from './defaults'
1314

@@ -28,11 +29,13 @@ yargs
2829
.option('p', cliOpts.providerUrl)
2930
.option('n', cliOpts.accountNumber)
3031
.option('s', cliOpts.skipConfirmation)
32+
.option('r', cliOpts.arbitrumAddressBook)
3133
.command(deployCommand)
3234
.command(migrateCommand)
3335
.command(proxyCommand)
3436
.command(protocolCommand)
3537
.command(contractsCommand)
3638
.command(airdropCommand)
39+
.command(bridgeCommand)
3740
.demandCommand(1, 'Choose a command from the above list')
3841
.help().argv

cli/commands/bridge/index.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import yargs, { Argv } from 'yargs'
2+
3+
import { redeemSendToL2Command, sendToL2Command } from './to-l2'
4+
import { startSendToL1Command, finishSendToL1Command, waitFinishSendToL1Command } from './to-l1'
5+
import { cliOpts } from '../../defaults'
6+
7+
export const bridgeCommand = {
8+
command: 'bridge',
9+
describe: 'Graph token bridge actions.',
10+
builder: (yargs: Argv): yargs.Argv => {
11+
return yargs
12+
.option('-l', cliOpts.l2ProviderUrl)
13+
.command(sendToL2Command)
14+
.command(redeemSendToL2Command)
15+
.command(startSendToL1Command)
16+
.command(finishSendToL1Command)
17+
.command(waitFinishSendToL1Command)
18+
},
19+
handler: (): void => {
20+
yargs.showHelp()
21+
},
22+
}

cli/commands/bridge/to-l1.ts

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
import { loadEnv, CLIArgs, CLIEnvironment } from '../../env'
2+
import { logger } from '../../logging'
3+
import { getAddressBook } from '../../address-book'
4+
import { getProvider, sendTransaction, toGRT } from '../../network'
5+
import { chainIdIsL2 } from '../../utils'
6+
import { loadAddressBookContract } from '../../contracts'
7+
import {
8+
L2TransactionReceipt,
9+
getL2Network,
10+
L2ToL1MessageStatus,
11+
L2ToL1MessageWriter,
12+
} from '@arbitrum/sdk'
13+
import { L2GraphTokenGateway } from '../../../build/types/L2GraphTokenGateway'
14+
import { BigNumber } from 'ethers'
15+
import { JsonRpcProvider } from '@ethersproject/providers'
16+
import { providers } from 'ethers'
17+
18+
const FOURTEEN_DAYS_IN_SECONDS = 24 * 3600 * 14
19+
20+
const BLOCK_SEARCH_THRESHOLD = 6 * 3600
21+
const searchForArbBlockByTimestamp = async (
22+
l2Provider: JsonRpcProvider,
23+
timestamp: number,
24+
): Promise<number> => {
25+
let step = 131072
26+
let block = await l2Provider.getBlock('latest')
27+
while (block.timestamp > timestamp) {
28+
while (block.number - step < 0) {
29+
step = Math.round(step / 2)
30+
}
31+
block = await l2Provider.getBlock(block.number - step)
32+
}
33+
while (step > 1 && Math.abs(block.timestamp - timestamp) > BLOCK_SEARCH_THRESHOLD) {
34+
step = Math.round(step / 2)
35+
if (block.timestamp - timestamp > 0) {
36+
block = await l2Provider.getBlock(block.number - step)
37+
} else {
38+
block = await l2Provider.getBlock(block.number + step)
39+
}
40+
}
41+
return block.number
42+
}
43+
44+
const wait = (ms: number): Promise<void> => {
45+
return new Promise((res) => setTimeout(res, ms))
46+
}
47+
48+
const waitUntilOutboxEntryCreatedWithCb = async (
49+
msg: L2ToL1MessageWriter,
50+
provider: providers.Provider,
51+
retryDelay: number,
52+
callback: () => void,
53+
) => {
54+
let done = false
55+
while (!done) {
56+
const status = await msg.status(provider)
57+
if (status == L2ToL1MessageStatus.CONFIRMED || status == L2ToL1MessageStatus.EXECUTED) {
58+
done = true
59+
} else {
60+
callback()
61+
await wait(retryDelay)
62+
}
63+
}
64+
}
65+
66+
export const startSendToL1 = async (cli: CLIEnvironment, cliArgs: CLIArgs): Promise<void> => {
67+
logger.info(`>>> Sending tokens to L1 <<<\n`)
68+
const l2Provider = getProvider(cliArgs.l2ProviderUrl)
69+
const l2ChainId = (await l2Provider.getNetwork()).chainId
70+
71+
if (chainIdIsL2(cli.chainId) || !chainIdIsL2(l2ChainId)) {
72+
throw new Error(
73+
'Please use an L1 provider in --provider-url, and an L2 provider in --l2-provider-url',
74+
)
75+
}
76+
77+
const l1GRT = cli.contracts['GraphToken']
78+
const l1GRTAddress = l1GRT.address
79+
const amount = toGRT(cliArgs.amount)
80+
const recipient = cliArgs.recipient ? cliArgs.recipient : cli.wallet.address
81+
const l2Wallet = cli.wallet.connect(l2Provider)
82+
const l2AddressBook = getAddressBook(cliArgs.addressBook, l2ChainId.toString())
83+
84+
const gateway = loadAddressBookContract('L2GraphTokenGateway', l2AddressBook, l2Wallet)
85+
const l2GRT = loadAddressBookContract('L2GraphToken', l2AddressBook, l2Wallet)
86+
87+
const l1Gateway = cli.contracts['L1GraphTokenGateway']
88+
logger.info(`Will send ${cliArgs.amount} GRT to ${recipient}`)
89+
logger.info(`Using L2 gateway ${gateway.address} and L1 gateway ${l1Gateway.address}`)
90+
91+
const params = [l1GRTAddress, recipient, amount, '0x']
92+
logger.info('Approving token transfer')
93+
await sendTransaction(l2Wallet, l2GRT, 'approve', [gateway.address, amount])
94+
logger.info('Sending outbound transfer transaction')
95+
const receipt = await sendTransaction(
96+
l2Wallet,
97+
gateway,
98+
'outboundTransfer(address,address,uint256,bytes)',
99+
params,
100+
)
101+
const l2Receipt = new L2TransactionReceipt(receipt)
102+
const l2ToL1Message = (
103+
await l2Receipt.getL2ToL1Messages(cli.wallet, await getL2Network(l2Provider))
104+
)[0]
105+
106+
logger.info(`The transaction generated an L2 to L1 message in outbox with eth block number:`)
107+
logger.info(l2ToL1Message.event.ethBlockNum.toString())
108+
logger.info(
109+
`After the dispute period is finalized (in ~1 week), you can finalize this by calling`,
110+
)
111+
logger.info(`finish-send-to-l1 with the following txhash:`)
112+
logger.info(l2Receipt.transactionHash)
113+
}
114+
115+
export const finishSendToL1 = async (
116+
cli: CLIEnvironment,
117+
cliArgs: CLIArgs,
118+
wait: boolean,
119+
): Promise<void> => {
120+
logger.info(`>>> Finishing transaction sending tokens to L1 <<<\n`)
121+
const l2Provider = getProvider(cliArgs.l2ProviderUrl)
122+
const l2ChainId = (await l2Provider.getNetwork()).chainId
123+
124+
if (chainIdIsL2(cli.chainId) || !chainIdIsL2(l2ChainId)) {
125+
throw new Error(
126+
'Please use an L1 provider in --provider-url, and an L2 provider in --l2-provider-url',
127+
)
128+
}
129+
130+
const l2AddressBook = getAddressBook(cliArgs.addressBook, l2ChainId.toString())
131+
132+
const gateway = loadAddressBookContract(
133+
'L2GraphTokenGateway',
134+
l2AddressBook,
135+
l2Provider,
136+
) as L2GraphTokenGateway
137+
let txHash: string
138+
if (cliArgs.txHash) {
139+
txHash = cliArgs.txHash
140+
} else {
141+
logger.info(
142+
`Looking for withdrawals initiated by ${cli.wallet.address} in roughly the last 14 days`,
143+
)
144+
const fromBlock = await searchForArbBlockByTimestamp(
145+
l2Provider,
146+
Math.round(Date.now() / 1000) - FOURTEEN_DAYS_IN_SECONDS,
147+
)
148+
const filt = gateway.filters.WithdrawalInitiated(null, cli.wallet.address)
149+
const allEvents = await gateway.queryFilter(filt, BigNumber.from(fromBlock).toHexString())
150+
if (allEvents.length == 0) {
151+
throw new Error('No withdrawals found')
152+
}
153+
txHash = allEvents[allEvents.length - 1].transactionHash
154+
}
155+
logger.info(`Getting receipt from transaction ${txHash}`)
156+
const receipt = await l2Provider.getTransactionReceipt(txHash)
157+
158+
const l2Receipt = new L2TransactionReceipt(receipt)
159+
logger.info(`Getting L2 to L1 message...`)
160+
const l2ToL1Message = (
161+
await l2Receipt.getL2ToL1Messages(cli.wallet, await getL2Network(l2Provider))
162+
)[0]
163+
164+
if (wait) {
165+
const retryDelayMs = cliArgs.retryDelaySeconds ? cliArgs.retryDelaySeconds * 1000 : 60000
166+
logger.info('Waiting for outbox entry to be created, this can take a full week...')
167+
await waitUntilOutboxEntryCreatedWithCb(l2ToL1Message, l2Provider, retryDelayMs, () => {
168+
logger.info('Still waiting...')
169+
})
170+
} else {
171+
const status = await l2ToL1Message.status(l2Provider)
172+
if (status == L2ToL1MessageStatus.EXECUTED) {
173+
throw new Error('Message already executed!')
174+
} else if (status != L2ToL1MessageStatus.CONFIRMED) {
175+
throw new Error(
176+
`Transaction is not confirmed, status is ${status} when it should be ${L2ToL1MessageStatus.CONFIRMED}. Has the dispute period passed?`,
177+
)
178+
}
179+
}
180+
181+
logger.info('Executing outbox transaction')
182+
const tx = await l2ToL1Message.execute(l2Provider)
183+
const outboxExecuteReceipt = await tx.wait()
184+
logger.info('Transaction succeeded! tx hash:')
185+
logger.info(outboxExecuteReceipt.transactionHash)
186+
}
187+
188+
export const startSendToL1Command = {
189+
command: 'start-send-to-l1 <amount> [recipient]',
190+
describe: 'Start an L2-to-L1 Graph Token transaction',
191+
handler: async (argv: CLIArgs): Promise<void> => {
192+
return startSendToL1(await loadEnv(argv), argv)
193+
},
194+
}
195+
196+
export const finishSendToL1Command = {
197+
command: 'finish-send-to-l1 [txHash]',
198+
describe:
199+
'Finish an L2-to-L1 Graph Token transaction. L2 dispute period must have completed. ' +
200+
'If txHash is not specified, the last withdrawal from the main account in the past 14 days will be redeemed.',
201+
handler: async (argv: CLIArgs): Promise<void> => {
202+
return finishSendToL1(await loadEnv(argv), argv, false)
203+
},
204+
}
205+
206+
export const waitFinishSendToL1Command = {
207+
command: 'wait-finish-send-to-l1 [txHash] [retryDelaySeconds]',
208+
describe:
209+
"Wait for an L2-to-L1 Graph Token transaction's dispute period to complete (which takes about a week), and then finalize it. " +
210+
'If txHash is not specified, the last withdrawal from the main account in the past 14 days will be redeemed.',
211+
handler: async (argv: CLIArgs): Promise<void> => {
212+
return finishSendToL1(await loadEnv(argv), argv, true)
213+
},
214+
}

0 commit comments

Comments
 (0)