UNPKG

@hyperlane-xyz/core

Version:

Core solidity contracts for Hyperlane

406 lines (352 loc) 13.4 kB
import { L1ToL2MessageGasEstimator, L1ToL2MessageStatus, L1TransactionReceipt, L2Network, L2TransactionReceipt, } from '@arbitrum/sdk' import { getBaseFee } from '@arbitrum/sdk/dist/lib/utils/lib' import { JsonRpcProvider } from '@ethersproject/providers' import { expect } from 'chai' import { ethers, Wallet } from '@arbitrum/sdk/node_modules/ethers' import { ArbSys__factory, ERC20, ERC20Bridge__factory, ERC20Inbox, ERC20Inbox__factory, ERC20__factory, EthVault__factory, RollupCore__factory, } from '../../build/types' import { setupNetworks, sleep } from '../../scripts/testSetup' import { applyAlias } from '../contract/utils' export const config = { arbUrl: 'http://localhost:8547', ethUrl: 'http://localhost:8545', } let l1Provider: JsonRpcProvider let l2Provider: JsonRpcProvider let _l2Network: L2Network & { nativeToken: string } let userL1Wallet: Wallet let userL2Wallet: Wallet let token: ERC20 let inbox: ERC20Inbox const excessFeeRefundAddress = Wallet.createRandom().address const callValueRefundAddress = Wallet.createRandom().address describe('ArbERC20Rollup', () => { // setup providers and connect deployed contracts before(async function () { const { l2Network } = await setupNetworks(config.ethUrl, config.arbUrl) _l2Network = l2Network l1Provider = new JsonRpcProvider(config.ethUrl) l2Provider = new JsonRpcProvider(config.arbUrl) userL1Wallet = new ethers.Wallet( ethers.utils.sha256(ethers.utils.toUtf8Bytes('user_l1user')), l1Provider ) userL2Wallet = new ethers.Wallet(userL1Wallet.privateKey, l2Provider) token = ERC20__factory.connect(_l2Network.nativeToken, l1Provider) inbox = ERC20Inbox__factory.connect(_l2Network.ethBridge.inbox, l1Provider) }) it('should have deployed bridge contracts', async function () { // get rollup as entry point const rollup = RollupCore__factory.connect( _l2Network.ethBridge.rollup, l1Provider ) // check contract refs are properly set expect(rollup.address).to.be.eq(_l2Network.ethBridge.rollup) expect((await rollup.sequencerInbox()).toLowerCase()).to.be.eq( _l2Network.ethBridge.sequencerInbox ) expect(await rollup.outbox()).to.be.eq(_l2Network.ethBridge.outbox) expect((await rollup.inbox()).toLowerCase()).to.be.eq( _l2Network.ethBridge.inbox ) const erc20Bridge = ERC20Bridge__factory.connect( await rollup.bridge(), l1Provider ) expect(erc20Bridge.address.toLowerCase()).to.be.eq( _l2Network.ethBridge.bridge ) expect((await erc20Bridge.nativeToken()).toLowerCase()).to.be.eq( _l2Network.nativeToken ) }) it('can deposit native token to L2', async function () { // snapshot state before deposit const userL1TokenBalance = await token.balanceOf(userL1Wallet.address) const userL2Balance = await l2Provider.getBalance(userL2Wallet.address) const bridgeL1TokenBalance = await token.balanceOf( _l2Network.ethBridge.bridge ) /// deposit 60 tokens const amountToDeposit = ethers.utils.parseEther('60') await ( await token .connect(userL1Wallet) .approve(_l2Network.ethBridge.inbox, amountToDeposit) ).wait() const depositTx = await inbox .connect(userL1Wallet) .depositERC20(amountToDeposit) // wait for deposit to be processed const depositRec = await L1TransactionReceipt.monkeyPatchEthDepositWait( depositTx ).wait() const l2Result = await depositRec.waitForL2(l2Provider) expect(l2Result.complete).to.be.true // check user balance increased on L2 and decreased on L1 const userL1TokenBalanceAfter = await token.balanceOf(userL1Wallet.address) expect(userL1TokenBalance.sub(userL1TokenBalanceAfter)).to.be.eq( amountToDeposit ) const userL2BalanceAfter = await l2Provider.getBalance(userL2Wallet.address) expect(userL2BalanceAfter.sub(userL2Balance)).to.be.eq(amountToDeposit) const bridgeL1TokenBalanceAfter = await token.balanceOf( _l2Network.ethBridge.bridge ) // bridge escrow increased expect(bridgeL1TokenBalanceAfter.sub(bridgeL1TokenBalance)).to.be.eq( amountToDeposit ) }) it('can issue retryable ticket (no calldata)', async function () { // snapshot state before issuing retryable const userL1TokenBalance = await token.balanceOf(userL1Wallet.address) const userL2Balance = await l2Provider.getBalance(userL2Wallet.address) const aliasL2Balance = await l2Provider.getBalance( applyAlias(userL2Wallet.address) ) const bridgeL1TokenBalance = await token.balanceOf( _l2Network.ethBridge.bridge ) const excessFeeReceiverBalance = await l2Provider.getBalance( excessFeeRefundAddress ) const callValueRefundReceiverBalance = await l2Provider.getBalance( callValueRefundAddress ) //// retryables params const to = userL1Wallet.address const l2CallValue = ethers.utils.parseEther('37') const data = '0x' const l1ToL2MessageGasEstimate = new L1ToL2MessageGasEstimator(l2Provider) const retryableParams = await l1ToL2MessageGasEstimate.estimateAll( { from: userL1Wallet.address, to: to, l2CallValue: l2CallValue, excessFeeRefundAddress: excessFeeRefundAddress, callValueRefundAddress: callValueRefundAddress, data: data, }, await getBaseFee(l1Provider), l1Provider ) const tokenTotalFeeAmount = retryableParams.deposit const gasLimit = retryableParams.gasLimit const maxFeePerGas = retryableParams.maxFeePerGas const maxSubmissionCost = retryableParams.maxSubmissionCost /// deposit 37 tokens using retryable await ( await token .connect(userL1Wallet) .approve(_l2Network.ethBridge.inbox, tokenTotalFeeAmount) ).wait() const retryableTx = await inbox .connect(userL1Wallet) .createRetryableTicket( to, l2CallValue, maxSubmissionCost, excessFeeRefundAddress, callValueRefundAddress, gasLimit, maxFeePerGas, tokenTotalFeeAmount, data ) // wait for L2 msg to be executed await waitOnL2Msg(retryableTx) // check balances after retryable is processed const userL1TokenAfter = await token.balanceOf(userL1Wallet.address) expect(userL1TokenBalance.sub(userL1TokenAfter)).to.be.eq( tokenTotalFeeAmount ) const userL2After = await l2Provider.getBalance(userL2Wallet.address) expect(userL2After.sub(userL2Balance)).to.be.eq(l2CallValue) const aliasL2BalanceAfter = await l2Provider.getBalance( applyAlias(userL2Wallet.address) ) expect(aliasL2BalanceAfter).to.be.eq(aliasL2Balance) const excessFeeReceiverBalanceAfter = await l2Provider.getBalance( excessFeeRefundAddress ) expect(excessFeeReceiverBalanceAfter).to.be.gte(excessFeeReceiverBalance) const callValueRefundReceiverBalanceAfter = await l2Provider.getBalance( callValueRefundAddress ) expect(callValueRefundReceiverBalanceAfter).to.be.eq( callValueRefundReceiverBalance ) const bridgeL1TokenAfter = await token.balanceOf( _l2Network.ethBridge.bridge ) expect(bridgeL1TokenAfter.sub(bridgeL1TokenBalance)).to.be.eq( tokenTotalFeeAmount ) }) it('can issue retryable ticket', async function () { // deploy contract on L2 which will be retryable's target const ethVaultContract = await new EthVault__factory( userL2Wallet.connect(l2Provider) ).deploy() await ethVaultContract.deployed() // snapshot state before retryable const userL1TokenBalance = await token.balanceOf(userL1Wallet.address) const userL2Balance = await l2Provider.getBalance(userL2Wallet.address) const aliasL2Balance = await l2Provider.getBalance( applyAlias(userL2Wallet.address) ) const bridgeL1TokenBalance = await token.balanceOf( _l2Network.ethBridge.bridge ) const excessFeeReceiverBalance = await l2Provider.getBalance( excessFeeRefundAddress ) const callValueRefundReceiverBalance = await l2Provider.getBalance( callValueRefundAddress ) //// retryables params const to = ethVaultContract.address const l2CallValue = ethers.utils.parseEther('45') // calldata -> change 'version' field to 11 const newValue = 11 const data = new ethers.utils.Interface([ 'function setVersion(uint256 _version)', ]).encodeFunctionData('setVersion', [newValue]) const l1ToL2MessageGasEstimate = new L1ToL2MessageGasEstimator(l2Provider) const retryableParams = await l1ToL2MessageGasEstimate.estimateAll( { from: userL1Wallet.address, to: to, l2CallValue: l2CallValue, excessFeeRefundAddress: excessFeeRefundAddress, callValueRefundAddress: callValueRefundAddress, data: data, }, await getBaseFee(l1Provider), l1Provider ) const tokenTotalFeeAmount = retryableParams.deposit const gasLimit = retryableParams.gasLimit const maxFeePerGas = retryableParams.maxFeePerGas const maxSubmissionCost = retryableParams.maxSubmissionCost /// execute retryable await ( await token .connect(userL1Wallet) .approve(_l2Network.ethBridge.inbox, tokenTotalFeeAmount) ).wait() const retryableTx = await inbox .connect(userL1Wallet) .createRetryableTicket( to, l2CallValue, maxSubmissionCost, excessFeeRefundAddress, callValueRefundAddress, gasLimit, maxFeePerGas, tokenTotalFeeAmount, data ) // wait for L2 msg to be executed await waitOnL2Msg(retryableTx) // check balances after retryable is processed const userL1TokenAfter = await token.balanceOf(userL2Wallet.address) expect(userL1TokenBalance.sub(userL1TokenAfter)).to.be.eq( tokenTotalFeeAmount ) const userL2After = await l2Provider.getBalance(userL2Wallet.address) expect(userL2After).to.be.eq(userL2Balance) const ethVaultBalanceAfter = await l2Provider.getBalance( ethVaultContract.address ) expect(ethVaultBalanceAfter).to.be.eq(l2CallValue) const ethVaultVersion = await ethVaultContract.version() expect(ethVaultVersion).to.be.eq(newValue) const aliasL2BalanceAfter = await l2Provider.getBalance( applyAlias(userL1Wallet.address) ) expect(aliasL2BalanceAfter).to.be.eq(aliasL2Balance) const excessFeeReceiverBalanceAfter = await l2Provider.getBalance( excessFeeRefundAddress ) expect(excessFeeReceiverBalanceAfter).to.be.gte(excessFeeReceiverBalance) const callValueRefundReceiverBalanceAfter = await l2Provider.getBalance( callValueRefundAddress ) expect(callValueRefundReceiverBalanceAfter).to.be.eq( callValueRefundReceiverBalance ) const bridgeL1TokenAfter = await token.balanceOf( _l2Network.ethBridge.bridge ) expect(bridgeL1TokenAfter.sub(bridgeL1TokenBalance)).to.be.eq( tokenTotalFeeAmount ) }) it('can withdraw funds from L2 to L1', async function () { // snapshot state before issuing retryable const userL1TokenBalance = await token.balanceOf(userL1Wallet.address) const userL2Balance = await l2Provider.getBalance(userL2Wallet.address) const bridgeL1TokenBalance = await token.balanceOf( _l2Network.ethBridge.bridge ) /// send L2 to L1 TX const arbSys = ArbSys__factory.connect( '0x0000000000000000000000000000000000000064', l2Provider ) const withdrawAmount = ethers.utils.parseEther('3') const withdrawTx = await arbSys .connect(userL2Wallet) .sendTxToL1(userL1Wallet.address, '0x', { value: withdrawAmount, }) const withdrawReceipt = await withdrawTx.wait() const l2Receipt = new L2TransactionReceipt(withdrawReceipt) // wait until dispute period passes and withdrawal is ready for execution await sleep(5 * 1000) const messages = await l2Receipt.getL2ToL1Messages(userL1Wallet) const l2ToL1Msg = messages[0] const timeToWaitMs = 60 * 1000 await l2ToL1Msg.waitUntilReadyToExecute(l2Provider, timeToWaitMs) // execute await (await l2ToL1Msg.execute(l2Provider)).wait() // check balances after withdrawal is processed const userL1TokenAfter = await token.balanceOf(userL2Wallet.address) expect(userL1TokenAfter.sub(userL1TokenBalance)).to.be.eq(withdrawAmount) const userL2BalanceAfter = await l2Provider.getBalance(userL2Wallet.address) expect(userL2BalanceAfter).to.be.lte(userL2Balance.sub(withdrawAmount)) const bridgeL1TokenAfter = await token.balanceOf( _l2Network.ethBridge.bridge ) expect(bridgeL1TokenBalance.sub(bridgeL1TokenAfter)).to.be.eq( withdrawAmount ) }) }) async function waitOnL2Msg(tx: ethers.ContractTransaction) { const retryableReceipt = await tx.wait() const l1TxReceipt = new L1TransactionReceipt(retryableReceipt) const messages = await l1TxReceipt.getL1ToL2Messages(l2Provider) // 1 msg expected const messageResult = await messages[0].waitForStatus() const status = messageResult.status expect(status).to.be.eq(L1ToL2MessageStatus.REDEEMED) }