@hyperlane-xyz/core
Version:
Core solidity contracts for Hyperlane
367 lines (321 loc) • 11 kB
text/typescript
/* Imports: External */
import { Wallet, utils, BigNumber } from 'ethers'
import { serialize } from '@ethersproject/transactions'
import { predeploys } from '@eth-optimism/contracts'
import { expectApprox } from '@eth-optimism/core-utils'
/* Imports: Internal */
import { expect } from './shared/setup'
import {
DEFAULT_TEST_GAS_L1,
DEFAULT_TEST_GAS_L2,
envConfig,
withdrawalTest,
gasPriceOracleWallet,
} from './shared/utils'
import { OptimismEnv } from './shared/env'
// TX size enforced by CTC:
const MAX_ROLLUP_TX_SIZE = 50_000
describe('Native ETH Integration Tests', async () => {
let env: OptimismEnv
let l1Bob: Wallet
let l2Bob: Wallet
const getBalances = async (_env: OptimismEnv) => {
const l1UserBalance = await _env.l1Wallet.getBalance()
const l2UserBalance = await _env.l2Wallet.getBalance()
const l1BobBalance = await l1Bob.getBalance()
const l2BobBalance = await l2Bob.getBalance()
const l1BridgeBalance = await _env.l1Wallet.provider.getBalance(
_env.messenger.contracts.l1.L1StandardBridge.address
)
return {
l1UserBalance,
l2UserBalance,
l1BobBalance,
l2BobBalance,
l1BridgeBalance,
}
}
before(async () => {
env = await OptimismEnv.new()
l1Bob = Wallet.createRandom().connect(env.l1Wallet.provider)
l2Bob = l1Bob.connect(env.l2Wallet.provider)
})
describe('estimateGas', () => {
it('Should estimate gas for ETH withdraw', async () => {
const amount = utils.parseEther('0.0000001')
const gas =
await env.messenger.contracts.l2.L2StandardBridge.estimateGas.withdraw(
predeploys.OVM_ETH,
amount,
0,
'0xFFFF'
)
// Expect gas to be less than or equal to the target plus 1%
expectApprox(gas, 6700060, { absoluteUpperDeviation: 1000 })
})
})
it('receive', async () => {
const depositAmount = 10
const preBalances = await getBalances(env)
const { tx, receipt } = await env.waitForXDomainTransaction(
env.l1Wallet.sendTransaction({
to: env.messenger.contracts.l1.L1StandardBridge.address,
value: depositAmount,
gasLimit: DEFAULT_TEST_GAS_L1,
})
)
const l1FeePaid = receipt.gasUsed.mul(tx.gasPrice)
const postBalances = await getBalances(env)
expect(postBalances.l1BridgeBalance).to.deep.eq(
preBalances.l1BridgeBalance.add(depositAmount)
)
expect(postBalances.l2UserBalance).to.deep.eq(
preBalances.l2UserBalance.add(depositAmount)
)
expect(postBalances.l1UserBalance).to.deep.eq(
preBalances.l1UserBalance.sub(l1FeePaid.add(depositAmount))
)
})
it('depositETH', async () => {
const depositAmount = 10
const preBalances = await getBalances(env)
const { tx, receipt } = await env.waitForXDomainTransaction(
env.messenger.contracts.l1.L1StandardBridge.depositETH(
DEFAULT_TEST_GAS_L2,
'0xFFFF',
{
value: depositAmount,
gasLimit: DEFAULT_TEST_GAS_L1,
}
)
)
const l1FeePaid = receipt.gasUsed.mul(tx.gasPrice)
const postBalances = await getBalances(env)
expect(postBalances.l1BridgeBalance).to.deep.eq(
preBalances.l1BridgeBalance.add(depositAmount)
)
expect(postBalances.l2UserBalance).to.deep.eq(
preBalances.l2UserBalance.add(depositAmount)
)
expect(postBalances.l1UserBalance).to.deep.eq(
preBalances.l1UserBalance.sub(l1FeePaid.add(depositAmount))
)
})
it('depositETHTo', async () => {
const depositAmount = 10
const preBalances = await getBalances(env)
const depositReceipts = await env.waitForXDomainTransaction(
env.messenger.contracts.l1.L1StandardBridge.depositETHTo(
l2Bob.address,
DEFAULT_TEST_GAS_L2,
'0xFFFF',
{
value: depositAmount,
gasLimit: DEFAULT_TEST_GAS_L1,
}
)
)
const l1FeePaid = depositReceipts.receipt.gasUsed.mul(
depositReceipts.tx.gasPrice
)
const postBalances = await getBalances(env)
expect(postBalances.l1BridgeBalance).to.deep.eq(
preBalances.l1BridgeBalance.add(depositAmount)
)
expect(postBalances.l2BobBalance).to.deep.eq(
preBalances.l2BobBalance.add(depositAmount)
)
expect(postBalances.l1UserBalance).to.deep.eq(
preBalances.l1UserBalance.sub(l1FeePaid.add(depositAmount))
)
})
it('deposit passes with a large data argument', async () => {
const ASSUMED_L2_GAS_LIMIT = 8_000_000
const depositAmount = 10
const preBalances = await getBalances(env)
// Set data length slightly less than MAX_ROLLUP_TX_SIZE
// to allow for encoding and other arguments
const data = `0x` + 'ab'.repeat(MAX_ROLLUP_TX_SIZE - 500)
const { tx, receipt } = await env.waitForXDomainTransaction(
env.messenger.contracts.l1.L1StandardBridge.depositETH(
ASSUMED_L2_GAS_LIMIT,
data,
{
value: depositAmount,
gasLimit: 4_000_000,
}
)
)
const l1FeePaid = receipt.gasUsed.mul(tx.gasPrice)
const postBalances = await getBalances(env)
expect(postBalances.l1BridgeBalance).to.deep.eq(
preBalances.l1BridgeBalance.add(depositAmount)
)
expect(postBalances.l2UserBalance).to.deep.eq(
preBalances.l2UserBalance.add(depositAmount)
)
expect(postBalances.l1UserBalance).to.deep.eq(
preBalances.l1UserBalance.sub(l1FeePaid.add(depositAmount))
)
})
it('depositETH fails with a TOO large data argument', async () => {
const depositAmount = 10
const data = `0x` + 'ab'.repeat(MAX_ROLLUP_TX_SIZE + 1)
await expect(
env.messenger.contracts.l1.L1StandardBridge.depositETH(
DEFAULT_TEST_GAS_L2,
data,
{
value: depositAmount,
}
)
).to.be.reverted
})
withdrawalTest('withdraw', async () => {
const withdrawAmount = BigNumber.from(3)
const preBalances = await getBalances(env)
expect(
preBalances.l2UserBalance.gt(0),
'Cannot run withdrawal test before any deposits...'
)
const transaction =
await env.messenger.contracts.l2.L2StandardBridge.withdraw(
predeploys.OVM_ETH,
withdrawAmount,
DEFAULT_TEST_GAS_L2,
'0xFFFF'
)
await transaction.wait()
await env.relayXDomainMessages(transaction)
const receipts = await env.waitForXDomainTransaction(transaction)
const fee = receipts.tx.gasLimit.mul(receipts.tx.gasPrice)
const postBalances = await getBalances(env)
// Approximate because there's a fee related to relaying the L2 => L1 message and it throws off the math.
expectApprox(
postBalances.l1BridgeBalance,
preBalances.l1BridgeBalance.sub(withdrawAmount),
{ percentUpperDeviation: 1 }
)
expectApprox(
postBalances.l2UserBalance,
preBalances.l2UserBalance.sub(withdrawAmount.add(fee)),
{ percentUpperDeviation: 1 }
)
expectApprox(
postBalances.l1UserBalance,
preBalances.l1UserBalance.add(withdrawAmount),
{ percentUpperDeviation: 1 }
)
})
withdrawalTest('withdrawTo', async () => {
const withdrawAmount = BigNumber.from(3)
const preBalances = await getBalances(env)
expect(
preBalances.l2UserBalance.gt(0),
'Cannot run withdrawal test before any deposits...'
)
const transaction =
await env.messenger.contracts.l2.L2StandardBridge.withdrawTo(
predeploys.OVM_ETH,
l1Bob.address,
withdrawAmount,
DEFAULT_TEST_GAS_L2,
'0xFFFF'
)
await transaction.wait()
await env.relayXDomainMessages(transaction)
const receipts = await env.waitForXDomainTransaction(transaction)
const l2Fee = receipts.tx.gasPrice.mul(receipts.receipt.gasUsed)
// Calculate the L1 portion of the fee
const raw = serialize({
nonce: transaction.nonce,
value: transaction.value,
gasPrice: transaction.gasPrice,
gasLimit: transaction.gasLimit,
to: transaction.to,
data: transaction.data,
})
const l1Fee = await env.messenger.contracts.l2.OVM_GasPriceOracle.connect(
gasPriceOracleWallet
).getL1Fee(raw)
const fee = l2Fee.add(l1Fee)
const postBalances = await getBalances(env)
expect(postBalances.l1BridgeBalance).to.deep.eq(
preBalances.l1BridgeBalance.sub(withdrawAmount),
'L1 Bridge Balance Mismatch'
)
expect(postBalances.l2UserBalance).to.deep.eq(
preBalances.l2UserBalance.sub(withdrawAmount.add(fee)),
'L2 User Balance Mismatch'
)
expect(postBalances.l1BobBalance).to.deep.eq(
preBalances.l1BobBalance.add(withdrawAmount),
'L1 User Balance Mismatch'
)
})
withdrawalTest(
'deposit, transfer, withdraw',
async () => {
// 1. deposit
const amount = utils.parseEther('1')
await env.waitForXDomainTransaction(
env.messenger.contracts.l1.L1StandardBridge.depositETH(
DEFAULT_TEST_GAS_L2,
'0xFFFF',
{
value: amount,
gasLimit: DEFAULT_TEST_GAS_L1,
}
)
)
// 2. transfer to another address
const other = Wallet.createRandom().connect(env.l2Wallet.provider)
const tx = await env.l2Wallet.sendTransaction({
to: other.address,
value: amount,
})
await tx.wait()
const l1BalanceBefore = await other
.connect(env.l1Wallet.provider)
.getBalance()
// 3. do withdrawal
const withdrawnAmount = utils.parseEther('0.95')
const transaction =
await env.messenger.contracts.l2.L2StandardBridge.connect(
other
).withdraw(
predeploys.OVM_ETH,
withdrawnAmount,
DEFAULT_TEST_GAS_L1,
'0xFFFF'
)
await transaction.wait()
await env.relayXDomainMessages(transaction)
const receipts = await env.waitForXDomainTransaction(transaction)
// Compute the L1 portion of the fee
const l1Fee =
await await env.messenger.contracts.l2.OVM_GasPriceOracle.connect(
gasPriceOracleWallet
).getL1Fee(
serialize({
nonce: transaction.nonce,
value: transaction.value,
gasPrice: transaction.gasPrice,
gasLimit: transaction.gasLimit,
to: transaction.to,
data: transaction.data,
})
)
// check that correct amount was withdrawn and that fee was charged
const l2Fee = receipts.tx.gasPrice.mul(receipts.receipt.gasUsed)
const fee = l1Fee.add(l2Fee)
const l1BalanceAfter = await other
.connect(env.l1Wallet.provider)
.getBalance()
const l2BalanceAfter = await other.getBalance()
expect(l1BalanceAfter).to.deep.eq(l1BalanceBefore.add(withdrawnAmount))
expect(l2BalanceAfter).to.deep.eq(amount.sub(withdrawnAmount).sub(fee))
},
envConfig.MOCHA_TIMEOUT * 3
)
})