Skip to content

CLI support for Arbitrum bridge + L2 rewards distribution #556

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

Merged
merged 40 commits into from
Jul 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
00e7d3a
feat: add commands to configure and use the Arbitrum bridge on the CLI
pcarranzav Apr 5, 2022
497a7d8
fix: Take a snapshot of GRT supply when signal is updated
pcarranzav May 4, 2022
1bf6515
test: add a test for rewards distribution accrual with multiple alloc…
abarmat May 15, 2022
ebf51fe
test: Add a test for two simultaneous allocations with a GRT burn in …
pcarranzav May 16, 2022
dbeaa7c
fix: Typo fix in tests
pcarranzav May 16, 2022
403be94
feat: implement distribution of rewards to L1 and L2 using a Reservoir
pcarranzav May 17, 2022
57dbbb8
fix: document potential drip reverts if issuance rate is updated [L-01]
pcarranzav Jul 11, 2022
b5967ac
fix: document drip revert when l2RewardsFraction changed [L-02]
pcarranzav Jul 11, 2022
d5fa0a8
fix: rename variables related to supply to issuanceBase to make it cl…
pcarranzav Jul 11, 2022
494df53
fix: use issuanceBase check to prevent calling initialSnapshot twice …
pcarranzav Jul 11, 2022
a15bc1d
test: fix tests after not allowing initialSnapshot to be called twice
pcarranzav Jul 12, 2022
2f41f81
fix: document the need for drip after a param update [L-06]
pcarranzav Jul 13, 2022
e8544e8
fix: validate L2Reservoir address on L1Reservoir [L-07]
pcarranzav Jul 12, 2022
0b51643
fix: rename normalizedSupply to l2IssuanceBase in L2 message to avoid…
pcarranzav Jul 13, 2022
f561a34
fix: remove silent failure if rewardsManager is not set [L-12]
pcarranzav Jul 13, 2022
88fac68
Merge branch 'pcv/552-m02-document-non-expiring-permits' into pcv/571…
pcarranzav Jul 13, 2022
f2641f1
test: remove unneeded L2 callhook whitelist entry
pcarranzav Jul 13, 2022
2582b10
fix: document the need for providing the gateways with allowance [N-01]
pcarranzav Jul 13, 2022
bb7f2e7
fix: remove unused event in L2GraphTokenGateway [N-02]
pcarranzav Jul 13, 2022
0ea0d30
fix: move nonce change to reduce gas on reverted permit call [N-03]
pcarranzav Jul 13, 2022
31e07c4
fix: general code improvements [N-05]
pcarranzav Jul 13, 2022
14ada45
fix: add some missing parameters in docstrings [N-06]
pcarranzav Jul 13, 2022
8742cc8
fix: use internal function to consistently set dripInterval [N-07]
pcarranzav Jul 13, 2022
3002d3e
fix: remove named returns [N-08]
pcarranzav Jul 13, 2022
5c1f877
fix: update some outdated docstrings and comments [N-09]
pcarranzav Jul 13, 2022
bd23616
fix: document why some variables are not set during initialization [N…
pcarranzav Jul 13, 2022
0f5fc9b
fix: add missing getters to Managed [N-11]
pcarranzav Jul 13, 2022
46a8444
fix: use Arbitrum's AddressAliasHelper instead of reimplementing it […
pcarranzav Jul 13, 2022
505a204
fix: separate contracts into different files [N-13]
pcarranzav Jul 14, 2022
b7b2a6c
fix: rename some variables for clarity [N-14]
pcarranzav Jul 14, 2022
1f798d8
fix: document the need to retry tickets if drip is received out-of-or…
pcarranzav Jul 14, 2022
012e3d2
fix: various typos [N-16]
pcarranzav Jul 14, 2022
1e171db
fix: remove unneeded ERC20Upgradeable inheritance [N-17]
pcarranzav Jul 14, 2022
65829af
fix: remove some unnecessary bits of code [N-19]
pcarranzav Jul 14, 2022
a11501f
fix: replace MAX_UINT256 with type().max [N-18] [N-20]
pcarranzav Jul 14, 2022
960cc04
fix: remove an unused import [N-21]
pcarranzav Jul 14, 2022
3a6c3b5
fix: check before adding/removing whitelisted addresses [N-23]
pcarranzav Jul 14, 2022
37d70a5
test: remove repeated addToCallhookWhitelist
pcarranzav Jul 15, 2022
9e7686f
fix: use SafeMath more consistently
pcarranzav Jul 15, 2022
8426b4c
fix: add more docs on the design behind callhook reverts [L-13]
pcarranzav Jul 26, 2022
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
42 changes: 42 additions & 0 deletions arbitrum-addresses.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"source": "https://github.com/OffchainLabs/arbitrum/tree/f54baf10871ee86aedca4880796342ef9bd0b0ab/packages/",
"1": {
"L1GatewayRouter": {
"address": "0x72Ce9c846789fdB6fC1f34aC4AD25Dd9ef7031ef"
},
"IInbox": {
"address": "0x4Dbd4fc535Ac27206064B68FfCf827b0A60BAB3f"
}
},
"4": {
"L1GatewayRouter": {
"address": "0x70C143928eCfFaf9F5b406f7f4fC28Dc43d68380"
},
"IInbox": {
"address": "0x578BAde599406A8fE3d24Fd7f7211c0911F5B29e"
}
},
"5": {
"L1GatewayRouter": {
"address": "0x8BDFa67ace22cE2BFb2fFebe72f0c91CDA694d4b"
},
"IInbox": {
"address": "0x1FdBBcC914e84aF593884bf8e8Dd6877c29035A2"
}
},
"42161": {
"L2GatewayRouter": {
"address": "0x5288c571Fd7aD117beA99bF60FE0846C4E84F933"
}
},
"421611": {
"L2GatewayRouter": {
"address": "0x9413AD42910c1eA60c737dB5f58d1C504498a3cD"
}
},
"421612": {
"L2GatewayRouter": {
"address": "0xC502Ded1EE1d616B43F7f20Ebde83Be1A275ca3c"
}
}
}
2 changes: 1 addition & 1 deletion cli/address-book.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -27,11 +28,13 @@ yargs
.option('m', cliOpts.mnemonic)
.option('p', cliOpts.providerUrl)
.option('n', cliOpts.accountNumber)
.option('r', cliOpts.arbitrumAddressBook)
.command(deployCommand)
.command(migrateCommand)
.command(proxyCommand)
.command(protocolCommand)
.command(contractsCommand)
.command(airdropCommand)
.command(bridgeCommand)
.demandCommand(1, 'Choose a command from the above list')
.help().argv
22 changes: 22 additions & 0 deletions cli/commands/bridge/index.ts
Original file line number Diff line number Diff line change
@@ -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()
},
}
217 changes: 217 additions & 0 deletions cli/commands/bridge/to-l1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
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'

const FOURTEEN_DAYS_IN_SECONDS = 24 * 3600 * 14

const BLOCK_SEARCH_THRESHOLD = 6 * 3600
const searchForArbBlockByTimestamp = async (
l2Provider: JsonRpcProvider,
timestamp: number,
): Promise<number> => {
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<void> => {
return new Promise((res) => setTimeout(res, ms))
}

const waitUntilOutboxEntryCreatedWithCb = async (
msg: L2ToL1MessageWriter,
retryDelay: number,
callback: () => void,
) => {
let done = false
while (!done) {
const status = await msg.status(null)
if (status == L2ToL1MessageStatus.CONFIRMED) {
done = true
} else {
callback()
await wait(retryDelay)
}
}
}

export const startSendToL1 = async (cli: CLIEnvironment, cliArgs: CLIArgs): Promise<void> => {
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 outbox message with batch number ${l2ToL1Message.batchNumber}`,
)
logger.info(`and index in batch ${l2ToL1Message.indexInBatch}.`)
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<void> => {
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, retryDelayMs, () => {
logger.info('Still waiting...')
})
} else {
logger.info('Checking if L2 to L1 message is confirmed...')
const status = await l2ToL1Message.status(null)
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('Getting proof to execute message')
const proofInfo = await l2ToL1Message.tryGetProof(l2Provider)

if (await l2ToL1Message.hasExecuted(proofInfo)) {
throw new Error('Message already executed!')
}

logger.info('Executing outbox transaction')
const tx = await l2ToL1Message.execute(proofInfo)
const outboxExecuteReceipt = await tx.wait()
logger.info('Transaction succeeded! tx hash:')
logger.info(outboxExecuteReceipt.transactionHash)
}

export const startSendToL1Command = {
command: 'start-send-to-l1 <amount> [recipient]',
describe: 'Start an L2-to-L1 Graph Token transaction',
handler: async (argv: CLIArgs): Promise<void> => {
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<void> => {
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<void> => {
return finishSendToL1(await loadEnv(argv), argv, true)
},
}
Loading