@hyperlane-xyz/core
Version:
Core solidity contracts for Hyperlane
406 lines (352 loc) • 13.4 kB
text/typescript
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)
}