@hyperlane-xyz/core
Version:
Core solidity contracts for Hyperlane
565 lines (489 loc) • 18.5 kB
text/typescript
/* Imports: External */
import { expectApprox, getChainId, sleep } from '@eth-optimism/core-utils'
import { Wallet, BigNumber, Contract, ContractFactory, constants } from 'ethers'
import { serialize } from '@ethersproject/transactions'
import { ethers } from 'hardhat'
import {
TransactionReceipt,
TransactionRequest,
} from '@ethersproject/providers'
/* Imports: Internal */
import { expect } from './shared/setup'
import {
defaultTransactionFactory,
fundUser,
L2_CHAINID,
gasPriceForL2,
isHardhat,
hardhatTest,
envConfig,
gasPriceOracleWallet,
} from './shared/utils'
import { OptimismEnv } from './shared/env'
describe('Basic RPC tests', () => {
let env: OptimismEnv
let wallet: Wallet
let Reverter: Contract
let ValueContext: Contract
let revertMessage: string
let revertingTx: TransactionRequest
let revertingDeployTx: TransactionRequest
before(async () => {
env = await OptimismEnv.new()
wallet = env.l2Wallet
const Factory__Reverter = await ethers.getContractFactory(
'Reverter',
wallet
)
Reverter = await Factory__Reverter.connect(env.l2Wallet).deploy()
await Reverter.deployTransaction.wait()
revertMessage = await Reverter.revertMessage()
revertingTx = {
to: Reverter.address,
data: Reverter.interface.encodeFunctionData('doRevert'),
}
const Factory__ConstructorReverter = await ethers.getContractFactory(
'ConstructorReverter',
wallet
)
revertingDeployTx = {
data: Factory__ConstructorReverter.bytecode,
}
// Deploy a contract to check msg.value of the call
const Factory__ValueContext: ContractFactory =
await ethers.getContractFactory('ValueContext', wallet)
ValueContext = await Factory__ValueContext.deploy()
await ValueContext.deployTransaction.wait()
})
describe('eth_sendRawTransaction', () => {
it('should correctly process a valid transaction', async () => {
const tx = defaultTransactionFactory()
tx.gasPrice = await gasPriceForL2()
const nonce = await wallet.getTransactionCount()
const result = await wallet.sendTransaction(tx)
expect(result.from).to.equal(wallet.address)
expect(result.nonce).to.equal(nonce)
expect(result.gasLimit.toNumber()).to.equal(tx.gasLimit)
expect(result.gasPrice.toNumber()).to.equal(tx.gasPrice)
expect(result.data).to.equal(tx.data)
})
it('should not accept a transaction with the wrong chain ID', async () => {
const tx = {
...defaultTransactionFactory(),
gasPrice: await gasPriceForL2(),
chainId: (await wallet.getChainId()) + 1,
}
await expect(
env.l2Provider.sendTransaction(await wallet.signTransaction(tx))
).to.be.rejectedWith('invalid transaction: invalid sender')
})
it('should accept a transaction without a chain ID', async () => {
const tx = {
...defaultTransactionFactory(),
nonce: await wallet.getTransactionCount(),
gasPrice: await gasPriceForL2(),
chainId: null, // Disables EIP155 transaction signing.
}
const signed = await wallet.signTransaction(tx)
const response = await env.l2Provider.sendTransaction(signed)
expect(response.chainId).to.equal(0)
const v = response.v
expect(v === 27 || v === 28).to.be.true
})
it('should accept a transaction with a value', async () => {
const tx = {
...defaultTransactionFactory(),
gasPrice: await gasPriceForL2(),
chainId: await env.l2Wallet.getChainId(),
data: '0x',
value: ethers.utils.parseEther('0.1'),
}
const balanceBefore = await env.l2Provider.getBalance(
env.l2Wallet.address
)
const result = await env.l2Wallet.sendTransaction(tx)
const receipt = await result.wait()
expect(receipt.status).to.deep.equal(1)
const balAfter = await env.l2Provider.getBalance(env.l2Wallet.address)
expect(balAfter.lte(balanceBefore.sub(ethers.utils.parseEther('0.1')))).to
.be.true
})
it('should reject a transaction with higher value than user balance', async () => {
const balance = await env.l2Wallet.getBalance()
const tx = {
...defaultTransactionFactory(),
gasPrice: await gasPriceForL2(),
chainId: await env.l2Wallet.getChainId(),
data: '0x',
value: balance.add(ethers.utils.parseEther('1')),
}
await expect(env.l2Wallet.sendTransaction(tx)).to.be.rejectedWith(
'invalid transaction: insufficient funds for gas * price + value'
)
})
it('should correctly report OOG for contract creations', async () => {
const factory = await ethers.getContractFactory('TestOOGInConstructor')
await expect(factory.connect(wallet).deploy()).to.be.rejectedWith(
'gas required exceeds allowance'
)
})
it('should reject a transaction with too low of a fee', async () => {
const isHH = await isHardhat()
let gasPrice
if (isHH) {
gasPrice = await env.messenger.contracts.l2.OVM_GasPriceOracle.connect(
gasPriceOracleWallet
).gasPrice()
await env.messenger.contracts.l2.OVM_GasPriceOracle.connect(
gasPriceOracleWallet
).setGasPrice(1000)
}
const tx = {
...defaultTransactionFactory(),
gasPrice: 1,
}
await expect(env.l2Wallet.sendTransaction(tx)).to.be.rejectedWith(
/gas price too low: 1 wei, use at least tx\.gasPrice = \d+ wei/
)
if (isHH) {
// Reset the gas price to its original price
await env.messenger.contracts.l2.OVM_GasPriceOracle.connect(
gasPriceOracleWallet
).setGasPrice(gasPrice)
}
})
it('should reject a transaction with too high of a fee', async () => {
const gasPrice =
await env.messenger.contracts.l2.OVM_GasPriceOracle.connect(
gasPriceOracleWallet
).gasPrice()
const largeGasPrice = gasPrice.mul(10)
const tx = {
...defaultTransactionFactory(),
gasPrice: largeGasPrice,
}
await expect(env.l2Wallet.sendTransaction(tx)).to.be.rejectedWith(
`gas price too high: ${largeGasPrice.toString()} wei, use at most ` +
`tx.gasPrice = ${gasPrice.toString()} wei`
)
})
})
describe('eth_call', () => {
it('should correctly identify call out-of-gas', async () => {
await expect(
env.l2Provider.call({
...revertingTx,
gasLimit: 1,
})
).to.be.rejectedWith('out of gas')
})
it('should correctly return solidity revert data from a call', async () => {
await expect(env.l2Provider.call(revertingTx)).to.be.revertedWith(
revertMessage
)
})
it('should produce error when called from ethers', async () => {
await expect(Reverter.doRevert()).to.be.revertedWith(revertMessage)
})
it('should correctly return revert data from contract creation', async () => {
await expect(env.l2Provider.call(revertingDeployTx)).to.be.revertedWith(
revertMessage
)
})
it('should correctly identify contract creation out of gas', async () => {
await expect(
env.l2Provider.call({
...revertingDeployTx,
gasLimit: 1,
})
).to.be.rejectedWith('out of gas')
})
it('should allow eth_calls with nonzero value', async () => {
// Fund account to call from
const from = wallet.address
const value = 15
await fundUser(env.messenger, value, from)
// Do the call and check msg.value
const data = ValueContext.interface.encodeFunctionData('getCallValue')
const res = await env.l2Provider.call({
to: ValueContext.address,
from,
data,
value,
})
expect(res).to.eq(BigNumber.from(value))
})
// https://github.com/ethereum-optimism/optimism/issues/1998
it('should use address(0) as the default "from" value', async () => {
// Do the call and check msg.sender
const data = ValueContext.interface.encodeFunctionData('getCaller')
const res = await env.l2Provider.call({
to: ValueContext.address,
data,
})
const [paddedRes] = ValueContext.interface.decodeFunctionResult(
'getCaller',
res
)
expect(paddedRes).to.eq(constants.AddressZero)
})
it('should correctly use the "from" value', async () => {
const from = wallet.address
// Do the call and check msg.sender
const data = ValueContext.interface.encodeFunctionData('getCaller')
const res = await env.l2Provider.call({
to: ValueContext.address,
from,
data,
})
const [paddedRes] = ValueContext.interface.decodeFunctionResult(
'getCaller',
res
)
expect(paddedRes).to.eq(from)
})
it('should be deterministic', async () => {
let res = await ValueContext.callStatic.getSelfBalance()
for (let i = 0; i < 10; i++) {
const next = await ValueContext.callStatic.getSelfBalance()
expect(res.toNumber()).to.deep.eq(next.toNumber())
res = next
}
})
})
describe('eth_getTransactionReceipt', () => {
it('correctly exposes revert data for contract calls', async () => {
const req: TransactionRequest = {
...revertingTx,
gasLimit: 8_000_000, // override gas estimation
}
const tx = await wallet.sendTransaction(req)
let errored = false
try {
await tx.wait()
} catch (e) {
errored = true
}
expect(errored).to.be.true
const receipt: TransactionReceipt =
await env.l2Provider.getTransactionReceipt(tx.hash)
expect(receipt.status).to.eq(0)
})
it('correctly exposes revert data for contract creations', async () => {
const req: TransactionRequest = {
...revertingDeployTx,
gasLimit: 8_000_000, // override gas estimation
}
const tx = await wallet.sendTransaction(req)
let errored = false
try {
await tx.wait()
} catch (e) {
errored = true
}
expect(errored).to.be.true
const receipt: TransactionReceipt =
await env.l2Provider.getTransactionReceipt(tx.hash)
expect(receipt.status).to.eq(0)
})
// Optimism special fields on the receipt
it('includes L1 gas price and L1 gas used', async () => {
const tx = await env.l2Wallet.populateTransaction({
to: env.l2Wallet.address,
gasPrice: await gasPriceForL2(),
})
const raw = serialize({
nonce: parseInt(tx.nonce.toString(), 10),
to: tx.to,
gasLimit: tx.gasLimit,
gasPrice: tx.gasPrice,
type: tx.type,
data: tx.data,
})
const l1Fee = await env.messenger.contracts.l2.OVM_GasPriceOracle.connect(
gasPriceOracleWallet
).getL1Fee(raw)
const l1GasPrice =
await env.messenger.contracts.l2.OVM_GasPriceOracle.connect(
gasPriceOracleWallet
).l1BaseFee()
const l1GasUsed =
await env.messenger.contracts.l2.OVM_GasPriceOracle.connect(
gasPriceOracleWallet
).getL1GasUsed(raw)
const scalar =
await env.messenger.contracts.l2.OVM_GasPriceOracle.connect(
gasPriceOracleWallet
).scalar()
const decimals =
await env.messenger.contracts.l2.OVM_GasPriceOracle.connect(
gasPriceOracleWallet
).decimals()
const scaled = scalar.toNumber() / 10 ** decimals.toNumber()
const res = await env.l2Wallet.sendTransaction(tx)
await res.wait()
const json = await env.l2Provider.send('eth_getTransactionReceipt', [
res.hash,
])
expect(l1GasUsed).to.deep.equal(BigNumber.from(json.l1GasUsed))
expect(l1GasPrice).to.deep.equal(BigNumber.from(json.l1GasPrice))
expect(scaled.toString()).to.deep.equal(json.l1FeeScalar)
expect(l1Fee).to.deep.equal(BigNumber.from(json.l1Fee))
})
})
describe('eth_getTransactionByHash', () => {
it('should be able to get all relevant l1/l2 transaction data', async () => {
const tx = defaultTransactionFactory()
tx.gasPrice = await gasPriceForL2()
const result = await wallet.sendTransaction(tx)
await result.wait()
const transaction = (await env.l2Provider.getTransaction(
result.hash
)) as any
expect(transaction.queueOrigin).to.equal('sequencer')
expect(transaction.transactionIndex).to.be.eq(0)
expect(transaction.gasLimit).to.be.deep.eq(BigNumber.from(tx.gasLimit))
})
})
describe('eth_getBlockByHash', () => {
it('should return the block and all included transactions', async () => {
// Send a transaction and wait for it to be mined.
const tx = defaultTransactionFactory()
tx.gasPrice = await gasPriceForL2()
const result = await wallet.sendTransaction(tx)
const receipt = await result.wait()
const block = (await env.l2Provider.getBlockWithTransactions(
receipt.blockHash
)) as any
expect(block.number).to.not.equal(0)
expect(typeof block.stateRoot).to.equal('string')
expect(block.transactions.length).to.equal(1)
expect(block.transactions[0].queueOrigin).to.equal('sequencer')
expect(block.transactions[0].l1TxOrigin).to.equal(null)
})
})
describe('eth_getBlockByNumber', () => {
// There was a bug that causes transactions to be reingested over
// and over again only when a single transaction was in the
// canonical transaction chain. This test catches this by
// querying for the latest block and then waits and then queries
// the latest block again and then asserts that they are the same.
//
// Needs to be skipped on Prod networks because this test doesn't work when
// other people are sending transactions to the Sequencer at the same time
// as this test is running.
hardhatTest(
'should return the same result when new transactions are not applied',
async () => {
// Get latest block once to start.
const prev = await env.l2Provider.getBlockWithTransactions('latest')
// set wait to null to allow a deep object comparison
prev.transactions[0].wait = null
// Over ten seconds, repeatedly check the latest block to make sure nothing has changed.
for (let i = 0; i < 5; i++) {
const latest = await env.l2Provider.getBlockWithTransactions('latest')
latest.transactions[0].wait = null
// Check each key of the transaction individually
// for easy debugging if one field changes
for (const [key, value] of Object.entries(latest.transactions[0])) {
expect(value).to.deep.equal(
prev.transactions[0][key],
`mismatch ${key}`
)
}
expect(latest).to.deep.equal(prev)
await sleep(2000)
}
}
)
})
describe('eth_getBalance', () => {
it('should get the OVM_ETH balance', async () => {
const rpcBalance = await env.l2Provider.getBalance(env.l2Wallet.address)
const contractBalance =
await env.messenger.contracts.l2.OVM_ETH.balanceOf(env.l2Wallet.address)
expect(rpcBalance).to.be.deep.eq(contractBalance)
})
})
describe('eth_chainId', () => {
it('should get the correct chainid', async () => {
expect(await getChainId(env.l2Provider)).to.be.eq(L2_CHAINID)
})
})
describe('eth_estimateGas', () => {
it('simple send gas estimation is deterministic', async () => {
let lastEstimate: BigNumber
for (let i = 0; i < 10; i++) {
const estimate = await env.l2Provider.estimateGas({
to: defaultTransactionFactory().to,
value: 0,
})
if (i > 0) {
expect(lastEstimate).to.be.eq(estimate)
}
lastEstimate = estimate
}
})
it('deterministic gas estimation for evm execution', async () => {
let res = await ValueContext.estimateGas.getSelfBalance()
for (let i = 0; i < 10; i++) {
const next = await ValueContext.estimateGas.getSelfBalance()
expect(res.toNumber()).to.deep.eq(next.toNumber())
res = next
}
})
it('should return a gas estimate for txs with empty data', async () => {
const estimate = await env.l2Provider.estimateGas({
to: defaultTransactionFactory().to,
value: 0,
})
// Expect gas to be less than or equal to the target plus 1%
expectApprox(estimate, 21000, { percentUpperDeviation: 1 })
})
it('should fail for a reverting call transaction', async () => {
await expect(env.l2Provider.send('eth_estimateGas', [revertingTx])).to.be
.reverted
})
it('should fail for a reverting deploy transaction', async () => {
await expect(env.l2Provider.send('eth_estimateGas', [revertingDeployTx]))
.to.be.reverted
})
})
describe('debug_traceTransaction', () => {
before(async function () {
if (!envConfig.RUN_DEBUG_TRACE_TESTS) {
this.skip()
}
})
it('should match debug_traceBlock', async () => {
const storage = await ethers.getContractFactory(
'SimpleStorage',
env.l2Wallet
)
const tx = (await storage.deploy()).deployTransaction
const receipt = await tx.wait()
const txTrace = await env.l2Provider.send('debug_traceTransaction', [
receipt.transactionHash,
])
const blockTrace = await env.l2Provider.send('debug_traceBlockByHash', [
receipt.blockHash,
])
expect(txTrace).to.deep.equal(blockTrace[0].result)
})
})
describe('rollup_gasPrices', () => {
it('should return the L1 and L2 gas prices', async () => {
const result = await env.l2Provider.send('rollup_gasPrices', [])
const l1GasPrice =
await env.messenger.contracts.l2.OVM_GasPriceOracle.connect(
gasPriceOracleWallet
).l1BaseFee()
const l2GasPrice =
await env.messenger.contracts.l2.OVM_GasPriceOracle.connect(
gasPriceOracleWallet
).gasPrice()
expect(BigNumber.from(result.l1GasPrice)).to.deep.eq(l1GasPrice)
expect(BigNumber.from(result.l2GasPrice)).to.deep.eq(l2GasPrice)
})
})
})