Commit dac4a9f0 authored by smartcontracts's avatar smartcontracts Committed by GitHub

feat(sdk): add Bedrock support to SDK (#3086)

* core-utils: add bedrock types

* sdk: implement bedrock functionality

* tests: update for bedrock

* sdk: add hardhat deposit task

* circleci: run deposit task in ci

* tsconfig: cleanup

* contracts-bedrock: make commitment interval larger

* changeset: add
Co-authored-by: default avatarMark Tyneway <mark.tyneway@gmail.com>
parent 28649d64
---
'@eth-optimism/sdk': minor
'@eth-optimism/contracts-bedrock': patch
'@eth-optimism/core-utils': patch
---
Updates the SDK to be compatible with Bedrock (via the "bedrock: true" constructor param). Updates the build pipeline for contracts-bedrock to export a properly formatted dist folder that matches our other packages.
......@@ -457,6 +457,10 @@ jobs:
--private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \
--network devnetL1
working_directory: packages/contracts-bedrock
- run:
name: Deposit ERC20 through the bridge
command: npx hardhat deposit --network devnetL1
working_directory: packages/sdk
- run:
name: Check the status
command: npx hardhat check-op-node
......
......@@ -8,7 +8,7 @@ const l1GenesisTimestamp =
: Math.floor(Date.now() / 1000)
const config = {
submissionInterval: 6,
submissionInterval: 20,
genesisOutput: ethers.constants.HashZero,
historicalBlocks: 0,
l1StartingBlockTag: 'earliest',
......
......@@ -9,6 +9,18 @@ import {
big1,
} from './encoding'
/**
* Bedrock output oracle data.
*/
export interface BedrockOutputData {
outputRoot: string
l1Timestamp: number
l2BlockNumber: number
}
/**
* Bedrock state commitment
*/
export interface OutputRootProof {
version: string
stateRoot: string
......@@ -16,6 +28,23 @@ export interface OutputRootProof {
latestBlockhash: string
}
/**
* Bedrock proof data required to finalize an L2 to L1 message.
*/
export interface BedrockCrossChainMessageProof {
outputRootProof: OutputRootProof
withdrawalProof: string
}
/**
* Parameters that govern the L2OutputOracle.
*/
export type L2OutputOracleParameters = {
submissionInterval: number
startingBlockNumber: number
l2BlockTime: number
}
/**
* Hahses a cross domain message.
*
......
......@@ -2,6 +2,9 @@ import { HardhatUserConfig } from 'hardhat/types'
import '@nomiclabs/hardhat-ethers'
import '@nomiclabs/hardhat-waffle'
import 'hardhat-deploy'
import './tasks/deposit'
const config: HardhatUserConfig = {
solidity: {
......@@ -10,6 +13,25 @@ const config: HardhatUserConfig = {
paths: {
sources: './test/contracts',
},
networks: {
devnetL1: {
url: 'http://localhost:8545',
accounts: [
'ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80',
],
},
},
external: {
contracts: [
{
artifacts: '../contracts-bedrock/artifacts',
},
],
deployments: {
devnetL1: ['../contracts-bedrock/deployments/devnetL1'],
goerli: ['../contracts-bedrock/deployments/goerli'],
},
},
}
export default config
......@@ -41,6 +41,7 @@
"ethereum-waffle": "^3.4.0",
"ethers": "^5.6.8",
"hardhat": "^2.9.6",
"hardhat-deploy": "^0.11.4",
"nyc": "^15.1.0",
"typedoc": "^0.22.13",
"mocha": "^10.0.0"
......@@ -48,6 +49,7 @@
"dependencies": {
"@eth-optimism/contracts": "0.5.31",
"@eth-optimism/core-utils": "0.9.2",
"@eth-optimism/contracts-bedrock": "0.5.2",
"lodash": "^4.17.21",
"merkletreejs": "^0.2.27",
"rlp": "^2.2.7"
......
......@@ -42,12 +42,12 @@ export class ETHBridgeAdapter extends StandardBridgeAdapter {
.map((event) => {
return {
direction: MessageDirection.L1_TO_L2,
from: event.args._from,
to: event.args._to,
from: event.args.from,
to: event.args.to,
l1Token: ethers.constants.AddressZero,
l2Token: predeploys.OVM_ETH,
amount: event.args._amount,
data: event.args._data,
amount: event.args.amount,
data: event.args.extraData,
logIndex: event.logIndex,
blockNumber: event.blockNumber,
transactionHash: event.transactionHash,
......@@ -76,19 +76,19 @@ export class ETHBridgeAdapter extends StandardBridgeAdapter {
.filter((event) => {
// Only find ETH withdrawals.
return (
hexStringEquals(event.args._l1Token, ethers.constants.AddressZero) &&
hexStringEquals(event.args._l2Token, predeploys.OVM_ETH)
hexStringEquals(event.args.l1Token, ethers.constants.AddressZero) &&
hexStringEquals(event.args.l2Token, predeploys.OVM_ETH)
)
})
.map((event) => {
return {
direction: MessageDirection.L2_TO_L1,
from: event.args._from,
to: event.args._to,
l1Token: event.args._l1Token,
l2Token: event.args._l2Token,
amount: event.args._amount,
data: event.args._data,
from: event.args.from,
to: event.args.to,
l1Token: event.args.l1Token,
l2Token: event.args.l2Token,
amount: event.args.amount,
data: event.args.extraData,
logIndex: event.logIndex,
blockNumber: event.blockNumber,
transactionHash: event.transactionHash,
......@@ -178,7 +178,10 @@ export class ETHBridgeAdapter extends StandardBridgeAdapter {
amount,
0, // L1 gas not required.
'0x', // No data.
opts?.overrides || {}
{
...omit(opts?.overrides || {}, 'value'),
value: this.messenger.bedrock ? amount : 0,
}
)
} else {
return this.l2Bridge.populateTransaction.withdrawTo(
......@@ -187,7 +190,10 @@ export class ETHBridgeAdapter extends StandardBridgeAdapter {
amount,
0, // L1 gas not required.
'0x', // No data.
opts?.overrides || {}
{
...omit(opts?.overrides || {}, 'value'),
value: this.messenger.bedrock ? amount : 0,
}
)
}
},
......
......@@ -12,7 +12,8 @@ import {
TransactionResponse,
BlockTag,
} from '@ethersproject/abstract-provider'
import { getContractInterface, predeploys } from '@eth-optimism/contracts'
import { predeploys } from '@eth-optimism/contracts'
import { getContractInterface } from '@eth-optimism/contracts-bedrock'
import { hexStringEquals } from '@eth-optimism/core-utils'
import {
......@@ -54,7 +55,7 @@ export class StandardBridgeAdapter implements IBridgeAdapter {
)
this.l2Bridge = new Contract(
toAddress(opts.l2Bridge),
getContractInterface('IL2ERC20Bridge'),
getContractInterface('L2StandardBridge'),
this.messenger.l2Provider
)
}
......@@ -82,19 +83,19 @@ export class StandardBridgeAdapter implements IBridgeAdapter {
// adapter. Bridges that are not the ETH bridge should not be able to handle or even
// present ETH deposits or withdrawals.
return (
!hexStringEquals(event.args._l1Token, ethers.constants.AddressZero) &&
!hexStringEquals(event.args._l2Token, predeploys.OVM_ETH)
!hexStringEquals(event.args.l1Token, ethers.constants.AddressZero) &&
!hexStringEquals(event.args.l2Token, predeploys.OVM_ETH)
)
})
.map((event) => {
return {
direction: MessageDirection.L1_TO_L2,
from: event.args._from,
to: event.args._to,
l1Token: event.args._l1Token,
l2Token: event.args._l2Token,
amount: event.args._amount,
data: event.args._data,
from: event.args.from,
to: event.args.to,
l1Token: event.args.l1Token,
l2Token: event.args.l2Token,
amount: event.args.amount,
data: event.args.extraData,
logIndex: event.logIndex,
blockNumber: event.blockNumber,
transactionHash: event.transactionHash,
......@@ -125,19 +126,19 @@ export class StandardBridgeAdapter implements IBridgeAdapter {
// adapter. Bridges that are not the ETH bridge should not be able to handle or even
// present ETH deposits or withdrawals.
return (
!hexStringEquals(event.args._l1Token, ethers.constants.AddressZero) &&
!hexStringEquals(event.args._l2Token, predeploys.OVM_ETH)
!hexStringEquals(event.args.l1Token, ethers.constants.AddressZero) &&
!hexStringEquals(event.args.l2Token, predeploys.OVM_ETH)
)
})
.map((event) => {
return {
direction: MessageDirection.L2_TO_L1,
from: event.args._from,
to: event.args._to,
l1Token: event.args._l1Token,
l2Token: event.args._l2Token,
amount: event.args._amount,
data: event.args._data,
from: event.args.from,
to: event.args.to,
l1Token: event.args.l1Token,
l2Token: event.args.l2Token,
amount: event.args.amount,
data: event.args.extraData,
logIndex: event.logIndex,
blockNumber: event.blockNumber,
transactionHash: event.transactionHash,
......@@ -156,10 +157,9 @@ export class StandardBridgeAdapter implements IBridgeAdapter {
try {
const contract = new Contract(
toAddress(l2Token),
getContractInterface('L2StandardERC20'),
getContractInterface('OptimismMintableERC20'),
this.messenger.l2Provider
)
// Don't support ETH deposits or withdrawals via this bridge.
if (
hexStringEquals(toAddress(l1Token), ethers.constants.AddressZero) ||
......@@ -170,6 +170,7 @@ export class StandardBridgeAdapter implements IBridgeAdapter {
// Make sure the L1 token matches.
const remoteL1Token = await contract.l1Token()
if (!hexStringEquals(remoteL1Token, toAddress(l1Token))) {
return false
}
......@@ -203,7 +204,7 @@ export class StandardBridgeAdapter implements IBridgeAdapter {
const token = new Contract(
toAddress(l1Token),
getContractInterface('L2StandardERC20'), // Any ERC20 will do
getContractInterface('OptimismMintableERC20'), // Any ERC20 will do
this.messenger.l1Provider
)
......@@ -270,7 +271,7 @@ export class StandardBridgeAdapter implements IBridgeAdapter {
const token = new Contract(
toAddress(l1Token),
getContractInterface('L2StandardERC20'), // Any ERC20 will do
getContractInterface('OptimismMintableERC20'), // Any ERC20 will do
this.messenger.l1Provider
)
......
......@@ -5,13 +5,27 @@ import {
TransactionReceipt,
TransactionResponse,
TransactionRequest,
Log,
} from '@ethersproject/abstract-provider'
import { Signer } from '@ethersproject/abstract-signer'
import { ethers, BigNumber, Overrides, CallOverrides } from 'ethers'
import { sleep, remove0x } from '@eth-optimism/core-utils'
import { predeploys } from '@eth-optimism/contracts'
import {
sleep,
remove0x,
toHexString,
toRpcHexString,
hashWithdrawal,
encodeCrossDomainMessageV0,
hashCrossDomainMessage,
L2OutputOracleParameters,
BedrockOutputData,
BedrockCrossChainMessageProof,
} from '@eth-optimism/core-utils'
import { getContractInterface, predeploys } from '@eth-optimism/contracts'
import * as rlp from 'rlp'
import {
CoreCrossChainMessage,
ICrossChainMessenger,
OEContracts,
OEContractsLike,
......@@ -42,10 +56,8 @@ import {
DeepPartial,
getAllOEContracts,
getBridgeAdapters,
hashCrossChainMessage,
makeMerkleTreeProof,
makeStateTrieProof,
encodeCrossChainMessage,
DEPOSIT_CONFIRMATION_BLOCKS,
CHAIN_BLOCK_TIMES,
} from './utils'
......@@ -59,6 +71,9 @@ export class CrossChainMessenger implements ICrossChainMessenger {
public bridges: BridgeAdapters
public depositConfirmationBlocks: number
public l1BlockTimeSeconds: number
public bedrock: boolean
private _l2OutputOracleParameters: L2OutputOracleParameters
/**
* Creates a new CrossChainProvider instance.
......@@ -72,6 +87,7 @@ export class CrossChainMessenger implements ICrossChainMessenger {
* @param opts.l1BlockTimeSeconds Optional estimated block time in seconds for the L1 chain.
* @param opts.contracts Optional contract address overrides.
* @param opts.bridges Optional bridge address list.
* @param opts.bedrock Whether or not to enable Bedrock compatibility.
*/
constructor(opts: {
l1SignerOrProvider: SignerOrProviderLike
......@@ -82,7 +98,9 @@ export class CrossChainMessenger implements ICrossChainMessenger {
l1BlockTimeSeconds?: NumberLike
contracts?: DeepPartial<OEContractsLike>
bridges?: BridgeAdapterData
bedrock?: boolean
}) {
this.bedrock = opts.bedrock ?? false
this.l1SignerOrProvider = toSignerOrProvider(opts.l1SignerOrProvider)
this.l2SignerOrProvider = toSignerOrProvider(opts.l2SignerOrProvider)
......@@ -151,6 +169,26 @@ export class CrossChainMessenger implements ICrossChainMessenger {
}
}
public async getL2OutputOracleParameters(): Promise<L2OutputOracleParameters> {
if (this._l2OutputOracleParameters) {
return this._l2OutputOracleParameters
}
this._l2OutputOracleParameters = {
submissionInterval: (
await this.contracts.l1.L2OutputOracle.SUBMISSION_INTERVAL()
).toNumber(),
startingBlockNumber: (
await this.contracts.l1.L2OutputOracle.STARTING_BLOCK_NUMBER()
).toNumber(),
l2BlockTime: (
await this.contracts.l1.L2OutputOracle.L2_BLOCK_TIME()
).toNumber(),
}
return this._l2OutputOracleParameters
}
public async getMessagesByTransaction(
transaction: TransactionLike,
opts: {
......@@ -203,6 +241,19 @@ export class CrossChainMessenger implements ICrossChainMessenger {
return parsed.name === 'SentMessage'
})
.map((log) => {
// Try to pull out the value field, but only if the very next log is a SentMessageExtraData
// event which was introduced in the Bedrock upgrade.
let value = ethers.BigNumber.from(0)
if (receipt.logs.length > log.logIndex + 1) {
const next = receipt.logs[log.logIndex + 1]
if (next.address === messenger.address) {
const nextParsed = messenger.interface.parseLog(next)
if (nextParsed.name === 'SentMessageExtension1') {
value = nextParsed.args.value
}
}
}
// Convert each SentMessage log into a message object
const parsed = messenger.interface.parseLog(log)
return {
......@@ -211,7 +262,8 @@ export class CrossChainMessenger implements ICrossChainMessenger {
sender: parsed.args.sender,
message: parsed.args.message,
messageNonce: parsed.args.messageNonce,
gasLimit: parsed.args.gasLimit,
value,
minGasLimit: parsed.args.gasLimit,
logIndex: log.logIndex,
blockNumber: log.blockNumber,
transactionHash: log.transactionHash,
......@@ -373,21 +425,33 @@ export class CrossChainMessenger implements ICrossChainMessenger {
}
} else {
if (receipt === null) {
let timestamp: number
if (this.bedrock) {
const output = await this.getMessageBedrockOutput(resolved)
if (output === null) {
return MessageStatus.STATE_ROOT_NOT_PUBLISHED
}
timestamp = output.l1Timestamp
} else {
const stateRoot = await this.getMessageStateRoot(resolved)
if (stateRoot === null) {
return MessageStatus.STATE_ROOT_NOT_PUBLISHED
} else {
}
const bn = stateRoot.batch.blockNumber
const block = await this.l1Provider.getBlock(bn)
timestamp = block.timestamp
}
const challengePeriod = await this.getChallengePeriodSeconds()
const targetBlock = await this.l1Provider.getBlock(
stateRoot.batch.blockNumber
)
const latestBlock = await this.l1Provider.getBlock('latest')
if (targetBlock.timestamp + challengePeriod > latestBlock.timestamp) {
if (timestamp + challengePeriod > latestBlock.timestamp) {
return MessageStatus.IN_CHALLENGE_PERIOD
} else {
return MessageStatus.READY_FOR_RELAY
}
}
} else {
if (receipt.receiptStatus === MessageReceiptStatus.RELAYED_SUCCEEDED) {
return MessageStatus.RELAYED
......@@ -402,7 +466,14 @@ export class CrossChainMessenger implements ICrossChainMessenger {
message: MessageLike
): Promise<MessageReceipt> {
const resolved = await this.toCrossChainMessage(message)
const messageHash = hashCrossChainMessage(resolved)
const messageHash = hashCrossDomainMessage(
resolved.messageNonce,
resolved.sender,
resolved.target,
resolved.value,
resolved.minGasLimit,
resolved.message
)
// Here we want the messenger that will receive the message, not the one that sent it.
const messenger =
......@@ -642,11 +713,56 @@ export class CrossChainMessenger implements ICrossChainMessenger {
}
public async getChallengePeriodSeconds(): Promise<number> {
const challengePeriod =
await this.contracts.l1.StateCommitmentChain.FRAUD_PROOF_WINDOW()
const challengePeriod = this.bedrock
? await this.contracts.l1.OptimismPortal.FINALIZATION_PERIOD_SECONDS()
: await this.contracts.l1.StateCommitmentChain.FRAUD_PROOF_WINDOW()
return challengePeriod.toNumber()
}
public async getMessageBedrockOutput(
message: MessageLike
): Promise<BedrockOutputData | null> {
const resolved = await this.toCrossChainMessage(message)
// Outputs are only a thing for L2 to L1 messages.
if (resolved.direction === MessageDirection.L1_TO_L2) {
throw new Error(`cannot get a state root for an L1 to L2 message`)
}
const l2OutputOracleParameters = await this.getL2OutputOracleParameters()
// TODO: better way to do this
let number =
resolved.blockNumber - l2OutputOracleParameters.startingBlockNumber
while (number % l2OutputOracleParameters.submissionInterval !== 0) {
number++
}
// TODO: Handle old messages from before Bedrock upgrade.
const events = await this.contracts.l1.L2OutputOracle.queryFilter(
this.contracts.l1.L2OutputOracle.filters.OutputProposed(
undefined,
undefined,
number
)
)
if (events.length === 0) {
return null
}
// Should not happen
if (events.length > 1) {
throw new Error(`multiple output roots found for message`)
}
return {
outputRoot: events[0].args.l2Output,
l1Timestamp: events[0].args.l1Timestamp.toNumber(),
l2BlockNumber: events[0].args.l2BlockNumber.toNumber(),
}
}
public async getMessageStateRoot(
message: MessageLike
): Promise<StateRoot | null> {
......@@ -826,8 +942,12 @@ export class CrossChainMessenger implements ICrossChainMessenger {
// https://docs.soliditylang.org/en/v0.8.4/internals/layout_in_storage.html#mappings-and-dynamic-arrays
const messageSlot = ethers.utils.keccak256(
ethers.utils.keccak256(
encodeCrossChainMessage(resolved) +
remove0x(this.contracts.l2.L2CrossDomainMessenger.address)
encodeCrossDomainMessageV0(
resolved.target,
resolved.sender,
resolved.message,
resolved.messageNonce
) + remove0x(this.contracts.l2.L2CrossDomainMessenger.address)
) + '00'.repeat(32)
)
......@@ -848,9 +968,142 @@ export class CrossChainMessenger implements ICrossChainMessenger {
stateRoot.stateRootIndexInBatch
),
},
stateTrieWitness: stateTrieProof.accountProof,
storageTrieWitness: stateTrieProof.storageProof,
stateTrieWitness: toHexString(rlp.encode(stateTrieProof.accountProof)),
storageTrieWitness: toHexString(rlp.encode(stateTrieProof.storageProof)),
}
}
public async getBedrockMessageProof(
message: MessageLike
): Promise<
[BedrockCrossChainMessageProof, BedrockOutputData, CoreCrossChainMessage]
> {
const resolved = await this.toCrossChainMessage(message)
if (resolved.direction === MessageDirection.L1_TO_L2) {
throw new Error(`can only generate proofs for L2 to L1 messages`)
}
const output = await this.getMessageBedrockOutput(resolved)
if (output === null) {
throw new Error(`state root for message not yet published`)
}
const receipt = await this.l2Provider.getTransactionReceipt(
resolved.transactionHash
)
interface WithdrawalEntry {
withdrawalInitiated: any
withdrawalInitiatedExtension1: any
}
// Handle multiple withdrawals in the same tx and be backwards
// compatible without WithdrawalInitiatedExtension1
const logs: Partial<{ number: WithdrawalEntry }> = {}
for (const [i, log] of Object.entries(receipt.logs)) {
if (log.address === predeploys.OVM_L2ToL1MessagePasser) {
const decoded =
this.contracts.l2.L2ToL1MessagePasser.interface.parseLog(log)
// Find the withdrawal initiated events
if (decoded.name === 'WithdrawalInitiated') {
logs[log.logIndex] = {
withdrawalInitiated: decoded.args,
withdrawalInitiatedExtension1: null,
}
if (receipt.logs[i + 1]) {
const next =
this.contracts.l2.L2ToL1MessagePasser.interface.parseLog(
receipt.logs[i + 1]
)
if (next.name === 'WithdrawalInitiatedExtension1') {
logs[log.logIndex].withdrawalInitiatedExtension1 = next.args
}
}
}
}
}
// TODO(tynes): be able to handle transactions that do multiple withdrawals
// in a single transaction. Right now just go for the first one.
const withdrawal = Object.values(logs)[0]
if (!withdrawal) {
throw new Error(
`Cannot find withdrawal logs for ${resolved.transactionHash}`
)
}
const withdrawalHash = hashWithdrawal(
withdrawal.withdrawalInitiated.nonce,
withdrawal.withdrawalInitiated.sender,
withdrawal.withdrawalInitiated.target,
withdrawal.withdrawalInitiated.value,
withdrawal.withdrawalInitiated.gasLimit,
withdrawal.withdrawalInitiated.data
)
// Sanity check
if (withdrawal.withdrawalInitiatedExtension1) {
if (withdrawal.withdrawalInitiatedExtension1.hash !== withdrawalHash) {
throw new Error(`Mismatched withdrawal hashes`)
}
}
// TODO: turn into util
const preimage = ethers.utils.defaultAbiCoder.encode(
['bytes32', 'uint256'],
[withdrawalHash, ethers.constants.HashZero]
)
const isMessageSent =
await this.contracts.l2.L2ToL1MessagePasser.sentMessages(withdrawalHash)
if (!isMessageSent) {
throw new Error(`Withdrawal not initiated on L2`)
}
const messageSlot = ethers.utils.keccak256(preimage)
const stateTrieProof = await makeStateTrieProof(
this.l2Provider as ethers.providers.JsonRpcProvider,
output.l2BlockNumber,
this.contracts.l2.OVM_L2ToL1MessagePasser.address,
messageSlot
)
// Sanity check that the value is set to 1 in the state
if (!stateTrieProof.storageValue.eq(1)) {
throw new Error(`Withdrawal hash ${withdrawalHash} is not set in state`)
}
const block = await (
this.l2Provider as ethers.providers.JsonRpcProvider
).send('eth_getBlockByNumber', [
toRpcHexString(output.l2BlockNumber),
false,
])
return [
{
outputRootProof: {
// TODO: Handle multiple versions in the future
version: ethers.constants.HashZero,
stateRoot: block.stateRoot,
withdrawerStorageRoot: stateTrieProof.storageRoot,
latestBlockhash: block.hash,
},
withdrawalProof: ethers.utils.RLP.encode(stateTrieProof.storageProof),
// withdrawalProof: toHexString(rlp.encode(stateTrieProof.storageProof)),
},
output,
// TODO(tynes): use better type, typechain?
{
messageNonce: withdrawal.withdrawalInitiated.nonce,
sender: withdrawal.withdrawalInitiated.sender,
target: withdrawal.withdrawalInitiated.target,
value: withdrawal.withdrawalInitiated.value,
minGasLimit: withdrawal.withdrawalInitiated.gasLimit,
message: withdrawal.withdrawalInitiated.data,
},
]
}
public async sendMessage(
......@@ -1033,15 +1286,30 @@ export class CrossChainMessenger implements ICrossChainMessenger {
throw new Error(`cannot resend L2 to L1 message`)
}
return this.contracts.l1.L1CrossDomainMessenger.populateTransaction.replayMessage(
if (this.bedrock) {
return this.populateTransaction.finalizeMessage(resolved, {
...(opts || {}),
overrides: {
...opts?.overrides,
gasLimit: messageGasLimit,
},
})
} else {
const legacyL1XDM = new ethers.Contract(
this.contracts.l1.L1CrossDomainMessenger.address,
getContractInterface('L1CrossDomainMessenger'),
this.l1SignerOrProvider
)
return legacyL1XDM.populateTransaction.replayMessage(
resolved.target,
resolved.sender,
resolved.message,
resolved.messageNonce,
resolved.gasLimit,
resolved.minGasLimit,
messageGasLimit,
opts?.overrides || {}
)
}
},
finalizeMessage: async (
......@@ -1055,8 +1323,40 @@ export class CrossChainMessenger implements ICrossChainMessenger {
throw new Error(`cannot finalize L1 to L2 message`)
}
if (this.bedrock) {
const [proof, output, withdrawalTx] = await this.getBedrockMessageProof(
message
)
return this.contracts.l1.OptimismPortal.populateTransaction.finalizeWithdrawalTransaction(
[
withdrawalTx.messageNonce,
withdrawalTx.sender,
withdrawalTx.target,
withdrawalTx.value,
withdrawalTx.minGasLimit,
withdrawalTx.message,
],
output.l2BlockNumber,
[
proof.outputRootProof.version,
proof.outputRootProof.stateRoot,
proof.outputRootProof.withdrawerStorageRoot,
proof.outputRootProof.latestBlockhash,
],
proof.withdrawalProof
)
} else {
// L1CrossDomainMessenger relayMessage is the only method that isn't fully backwards
// compatible, so we need to use the legacy interface. When we fully upgrade to Bedrock we
// should be able to remove this code.
const proof = await this.getMessageProof(resolved)
return this.contracts.l1.L1CrossDomainMessenger.populateTransaction.relayMessage(
const legacyL1XDM = new ethers.Contract(
this.contracts.l1.L1CrossDomainMessenger.address,
getContractInterface('L1CrossDomainMessenger'),
this.l1SignerOrProvider
)
return legacyL1XDM.populateTransaction.relayMessage(
resolved.target,
resolved.sender,
resolved.message,
......@@ -1064,6 +1364,7 @@ export class CrossChainMessenger implements ICrossChainMessenger {
proof,
opts?.overrides || {}
)
}
},
depositETH: async (
......
......@@ -6,8 +6,13 @@ import {
TransactionResponse,
} from '@ethersproject/abstract-provider'
import { Signer } from '@ethersproject/abstract-signer'
import {
BedrockCrossChainMessageProof,
BedrockOutputData,
} from '@eth-optimism/core-utils'
import {
CoreCrossChainMessage,
MessageLike,
MessageRequestLike,
TransactionLike,
......@@ -91,6 +96,11 @@ export interface ICrossChainMessenger {
*/
l1BlockTimeSeconds: number
/**
* Whether or not Bedrock compatibility is enabled.
*/
bedrock: boolean
/**
* Retrieves all cross chain messages sent within a given transaction.
*
......@@ -291,6 +301,16 @@ export interface ICrossChainMessenger {
*/
getChallengePeriodSeconds(): Promise<number>
/**
* Returns the Bedrock output root that corresponds to the given message.
*
* @param message Message to get the Bedrock output root for.
* @returns Bedrock output root.
*/
getMessageBedrockOutput(
message: MessageLike
): Promise<BedrockOutputData | null>
/**
* Returns the state root that corresponds to a given message. This is the state root for the
* block in which the transaction was included, as published to the StateCommitmentChain. If the
......@@ -342,6 +362,18 @@ export interface ICrossChainMessenger {
*/
getMessageProof(message: MessageLike): Promise<CrossChainMessageProof>
/**
* Generates the bedrock proof required to finalize an L2 to L1 message.
*
* @param message Message to generate a proof for.
* @returns Proof that can be used to finalize the message.
*/
getBedrockMessageProof(
message: MessageLike
): Promise<
[BedrockCrossChainMessageProof, BedrockOutputData, CoreCrossChainMessage]
>
/**
* Sends a given cross chain message. Where the message is sent depends on the direction attached
* to the message itself.
......
......@@ -17,6 +17,7 @@ export enum L1ChainID {
GOERLI = 5,
KOVAN = 42,
HARDHAT_LOCAL = 31337,
BEDROCK_LOCAL_DEVNET = 900,
}
/**
......@@ -28,6 +29,7 @@ export enum L2ChainID {
OPTIMISM_KOVAN = 69,
OPTIMISM_HARDHAT_LOCAL = 31337,
OPTIMISM_HARDHAT_DEVNET = 17,
OPTIMISM_BEDROCK_LOCAL_DEVNET = 901,
}
/**
......@@ -40,6 +42,9 @@ export interface OEL1Contracts {
StateCommitmentChain: Contract
CanonicalTransactionChain: Contract
BondManager: Contract
// Bedrock
OptimismPortal: Contract
L2OutputOracle: Contract
}
/**
......@@ -48,6 +53,7 @@ export interface OEL1Contracts {
export interface OEL2Contracts {
L2CrossDomainMessenger: Contract
L2StandardBridge: Contract
L2ToL1MessagePasser: Contract
OVM_L1BlockNumber: Contract
OVM_L2ToL1MessagePasser: Contract
OVM_DeployerWhitelist: Contract
......@@ -174,7 +180,9 @@ export interface CoreCrossChainMessage {
sender: string
target: string
message: string
messageNonce: number
messageNonce: BigNumber
value: BigNumber
minGasLimit: BigNumber
}
/**
......@@ -183,7 +191,6 @@ export interface CoreCrossChainMessage {
*/
export interface CrossChainMessage extends CoreCrossChainMessage {
direction: MessageDirection
gasLimit: number
logIndex: number
blockNumber: number
transactionHash: string
......
......@@ -8,6 +8,7 @@ export const DEPOSIT_CONFIRMATION_BLOCKS: {
[L2ChainID.OPTIMISM_KOVAN]: 12 as const,
[L2ChainID.OPTIMISM_HARDHAT_LOCAL]: 2 as const,
[L2ChainID.OPTIMISM_HARDHAT_DEVNET]: 2 as const,
[L2ChainID.OPTIMISM_BEDROCK_LOCAL_DEVNET]: 2 as const,
}
export const CHAIN_BLOCK_TIMES: {
......@@ -17,4 +18,5 @@ export const CHAIN_BLOCK_TIMES: {
[L1ChainID.GOERLI]: 15 as const,
[L1ChainID.KOVAN]: 4 as const,
[L1ChainID.HARDHAT_LOCAL]: 1 as const,
[L1ChainID.BEDROCK_LOCAL_DEVNET]: 15 as const,
}
import { getContractInterface, predeploys } from '@eth-optimism/contracts'
import { getContractInterface as getContractInterfaceBedrock } from '@eth-optimism/contracts-bedrock'
import { ethers, Contract } from 'ethers'
import { toAddress } from './coercion'
......@@ -23,9 +24,11 @@ import {
/**
* Full list of default L2 contract addresses.
* TODO(tynes): migrate to predeploys from contracts-bedrock
*/
export const DEFAULT_L2_CONTRACT_ADDRESSES: OEL2ContractsLike = {
L2CrossDomainMessenger: predeploys.L2CrossDomainMessenger,
L2ToL1MessagePasser: predeploys.OVM_L2ToL1MessagePasser,
L2StandardBridge: predeploys.L2StandardBridge,
OVM_L1BlockNumber: predeploys.OVM_L1BlockNumber,
OVM_L2ToL1MessagePasser: predeploys.OVM_L2ToL1MessagePasser,
......@@ -65,6 +68,8 @@ export const CONTRACT_ADDRESSES: {
CanonicalTransactionChain:
'0x5E4e65926BA27467555EB562121fac00D24E9dD2' as const,
BondManager: '0xcd626E1328b41fCF24737F137BcD4CE0c32bc8d1' as const,
OptimismPortal: '0x0000000000000000000000000000000000000000' as const,
L2OutputOracle: '0x0000000000000000000000000000000000000000' as const,
},
l2: DEFAULT_L2_CONTRACT_ADDRESSES,
},
......@@ -79,6 +84,8 @@ export const CONTRACT_ADDRESSES: {
CanonicalTransactionChain:
'0xf7B88A133202d41Fe5E2Ab22e6309a1A4D50AF74' as const,
BondManager: '0xc5a603d273E28185c18Ba4d26A0024B2d2F42740' as const,
OptimismPortal: '0x0000000000000000000000000000000000000000' as const,
L2OutputOracle: '0x0000000000000000000000000000000000000000' as const,
},
l2: DEFAULT_L2_CONTRACT_ADDRESSES,
},
......@@ -93,6 +100,8 @@ export const CONTRACT_ADDRESSES: {
CanonicalTransactionChain:
'0x607F755149cFEB3a14E1Dc3A4E2450Cde7dfb04D' as const,
BondManager: '0xfC2ab6987C578218f99E85d61Dcf4814A26637Bd' as const,
OptimismPortal: '0x0000000000000000000000000000000000000000' as const,
L2OutputOracle: '0x0000000000000000000000000000000000000000' as const,
},
l2: DEFAULT_L2_CONTRACT_ADDRESSES,
},
......@@ -107,6 +116,8 @@ export const CONTRACT_ADDRESSES: {
CanonicalTransactionChain:
'0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9' as const,
BondManager: '0x5FC8d32690cc91D4c39d9d3abcBD16989F875707' as const,
OptimismPortal: '0x0000000000000000000000000000000000000000' as const,
L2OutputOracle: '0x0000000000000000000000000000000000000000' as const,
},
l2: DEFAULT_L2_CONTRACT_ADDRESSES,
},
......@@ -121,6 +132,24 @@ export const CONTRACT_ADDRESSES: {
CanonicalTransactionChain:
'0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9' as const,
BondManager: '0x5FC8d32690cc91D4c39d9d3abcBD16989F875707' as const,
OptimismPortal: '0x0000000000000000000000000000000000000000' as const,
L2OutputOracle: '0x0000000000000000000000000000000000000000' as const,
},
l2: DEFAULT_L2_CONTRACT_ADDRESSES,
},
[L2ChainID.OPTIMISM_BEDROCK_LOCAL_DEVNET]: {
l1: {
AddressManager: '0x5FbDB2315678afecb367f032d93F642f64180aa3' as const,
L1CrossDomainMessenger:
'0x0165878A594ca255338adfa4d48449f69242Eb8F' as const,
L1StandardBridge: '0x8A791620dd6260079BF849Dc5567aDC3F2FdC318' as const,
StateCommitmentChain:
'0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9' as const,
CanonicalTransactionChain:
'0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9' as const,
BondManager: '0x5FC8d32690cc91D4c39d9d3abcBD16989F875707' as const,
OptimismPortal: '0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9' as const,
L2OutputOracle: '0x5FbDB2315678afecb367f032d93F642f64180aa3' as const,
},
l2: DEFAULT_L2_CONTRACT_ADDRESSES,
},
......@@ -196,11 +225,21 @@ export const getOEContract = (
)
}
// Bedrock interfaces are backwards compatible. We can prefer Bedrock interfaces over legacy
// interfaces if they exist.
const name = NAME_REMAPPING[contractName] || contractName
let iface: ethers.utils.Interface
try {
iface = getContractInterfaceBedrock(name)
} catch (err) {
iface = getContractInterface(name)
}
return new Contract(
toAddress(
opts.address || addresses.l1[contractName] || addresses.l2[contractName]
),
getContractInterface(NAME_REMAPPING[contractName] || contractName),
iface,
opts.signerOrProvider
)
}
......@@ -235,6 +274,8 @@ export const getAllOEContracts = (
StateCommitmentChain: undefined,
CanonicalTransactionChain: undefined,
BondManager: undefined,
OptimismPortal: undefined,
L2OutputOracle: undefined,
},
l2: DEFAULT_L2_CONTRACT_ADDRESSES,
}
......
export * from './coercion'
export * from './contracts'
export * from './message-encoding'
export * from './type-utils'
export * from './misc-utils'
export * from './merkle-utils'
......
/* Imports: External */
import { ethers } from 'ethers'
import { ethers, BigNumber } from 'ethers'
import {
fromHexString,
toHexString,
toRpcHexString,
} from '@eth-optimism/core-utils'
import { MerkleTree } from 'merkletreejs'
import * as rlp from 'rlp'
/**
* Generates a Merkle proof (using the particular scheme we use within Lib_MerkleTree).
......@@ -60,8 +59,10 @@ export const makeStateTrieProof = async (
address: string,
slot: string
): Promise<{
accountProof: string
storageProof: string
accountProof: string[]
storageProof: string[]
storageValue: BigNumber
storageRoot: string
}> => {
const proof = await provider.send('eth_getProof', [
address,
......@@ -70,7 +71,9 @@ export const makeStateTrieProof = async (
])
return {
accountProof: toHexString(rlp.encode(proof.accountProof)),
storageProof: toHexString(rlp.encode(proof.storageProof[0].proof)),
accountProof: proof.accountProof,
storageProof: proof.storageProof[0].proof,
storageValue: BigNumber.from(proof.storageProof[0].value),
storageRoot: proof.storageHash,
}
}
import { getContractInterface } from '@eth-optimism/contracts'
import { ethers } from 'ethers'
import { CoreCrossChainMessage } from '../interfaces'
/**
* Returns the canonical encoding of a cross chain message. This encoding is used in various
* locations within the Optimism smart contracts.
*
* @param message Cross chain message to encode.
* @returns Canonical encoding of the message.
*/
export const encodeCrossChainMessage = (
message: CoreCrossChainMessage
): string => {
return getContractInterface('L2CrossDomainMessenger').encodeFunctionData(
'relayMessage',
[message.target, message.sender, message.message, message.messageNonce]
)
}
/**
* Returns the canonical hash of a cross chain message. This hash is used in various locations
* within the Optimism smart contracts and is the keccak256 hash of the result of
* encodeCrossChainMessage.
*
* @param message Cross chain message to hash.
* @returns Canonical hash of the message.
*/
export const hashCrossChainMessage = (
message: CoreCrossChainMessage
): string => {
return ethers.utils.solidityKeccak256(
['bytes'],
[encodeCrossChainMessage(message)]
)
}
import { task, types } from 'hardhat/config'
import '@nomiclabs/hardhat-ethers'
import 'hardhat-deploy'
import {
predeploys,
getContractInterface,
} from '@eth-optimism/contracts-bedrock'
import { Event } from 'ethers'
import {
CrossChainMessenger,
StandardBridgeAdapter,
MessageStatus,
} from '../src'
// TODO(tynes): this task could be modularized in the future
// so that it can deposit an arbitrary token. Right now it
// deploys a WETH9 contract, mints some WETH9 and then
// deposits that into L2 through the StandardBridge
task('deposit', 'Deposits WETH9 onto L2.')
.addParam(
'l2ProviderUrl',
'L2 provider URL.',
'http://localhost:9545',
types.string
)
.addParam(
'opNodeProviderUrl',
'op-node provider URL',
'http://localhost:7545',
types.string
)
.setAction(async (args, hre) => {
const { utils } = hre.ethers
const signers = await hre.ethers.getSigners()
if (signers.length === 0) {
throw new Error('No configured signers')
}
// Use the first configured signer for simplicity
const signer = signers[0]
const address = await signer.getAddress()
console.log(`Using signer ${address}`)
// Ensure that the signer has a balance before trying to
// do anything
const balance = await signer.getBalance()
if (balance.eq(0)) {
throw new Error('Signer has no balance')
}
const l2Provider = new hre.ethers.providers.StaticJsonRpcProvider(
args.l2ProviderUrl
)
const Deployment__L2OutputOracleProxy = await hre.deployments.get(
'L2OutputOracleProxy'
)
const l2Signer = new hre.ethers.Wallet(
hre.network.config.accounts[0],
l2Provider
)
const Artifact__WETH9 = await hre.deployments.getArtifact('WETH9')
const Factory__WETH9 = new hre.ethers.ContractFactory(
Artifact__WETH9.abi,
Artifact__WETH9.bytecode,
signer
)
const Deployment__OptimismMintableERC20TokenFactory =
await hre.deployments.get('OptimismMintableERC20Factory')
const Deployment__OptimismPortalProxy = await hre.deployments.get(
'OptimismPortalProxy'
)
const Deployment__L1StandardBridgeProxy = await hre.deployments.get(
'L1StandardBridgeProxy'
)
const Deployment__L1CrossDomainMessengerProxy = await hre.deployments.get(
'L1CrossDomainMessengerProxy'
)
const messenger = new CrossChainMessenger({
l1SignerOrProvider: signer,
l2SignerOrProvider: l2Signer,
l1ChainId: await signer.getChainId(),
l2ChainId: await l2Signer.getChainId(),
bridges: {
Standard: {
Adapter: StandardBridgeAdapter,
l1Bridge: Deployment__L1StandardBridgeProxy.address,
l2Bridge: predeploys.L2StandardBridge,
},
},
contracts: {
l1: {
L1StandardBridge: Deployment__L1StandardBridgeProxy.address,
L1CrossDomainMessenger:
Deployment__L1CrossDomainMessengerProxy.address,
L2OutputOracle: Deployment__L2OutputOracleProxy.address,
OptimismPortal: Deployment__OptimismPortalProxy.address,
},
},
bedrock: true,
})
const OptimismMintableERC20TokenFactory = await hre.ethers.getContractAt(
Deployment__OptimismMintableERC20TokenFactory.abi,
predeploys.OptimismMintableERC20Factory,
l2Signer
)
console.log('Deploying WETH9 to L1')
const WETH9 = await Factory__WETH9.deploy()
await WETH9.deployTransaction.wait()
console.log(`Deployed to ${WETH9.address}`)
console.log('Creating L2 WETH9')
const deployTx =
await OptimismMintableERC20TokenFactory.createOptimismMintableERC20(
WETH9.address,
'L2 Wrapped Ether',
'L2-WETH9'
)
const receipt = await deployTx.wait()
const event = receipt.events.find(
(e: Event) => e.event === 'OptimismMintableERC20Created'
)
if (!event) {
throw new Error('Unable to find OptimismMintableERC20Created event')
}
// TODO(tynes): may need to be updated based on
// https://github.com/ethereum-optimism/optimism/pull/3104
const l2WethAddress = event.args.remoteToken
console.log(`Deployed to ${l2WethAddress}`)
console.log('Wrapping ETH')
const deposit = await signer.sendTransaction({
value: utils.parseEther('1'),
to: WETH9.address,
})
await deposit.wait()
console.log('ETH wrapped')
console.log(`Approving WETH9 for deposit`)
const approvalTx = await messenger.approveERC20(
WETH9.address,
l2WethAddress,
hre.ethers.constants.MaxUint256
)
await approvalTx.wait()
console.log('WETH9 approved')
console.log('Depositing WETH9 to L2')
const depositTx = await messenger.depositERC20(
WETH9.address,
l2WethAddress,
utils.parseEther('1')
)
await depositTx.wait()
console.log('ERC20 deposited')
const messageReceipt = await messenger.waitForMessageReceipt(depositTx)
if (messageReceipt.receiptStatus !== 1) {
throw new Error('deposit failed')
}
const L2WETH9 = new hre.ethers.Contract(
l2WethAddress,
getContractInterface('OptimismMintableERC20'),
l2Signer
)
const l2Balance = await L2WETH9.balanceOf(await signer.getAddress())
if (l2Balance.lt(utils.parseEther('1'))) {
throw new Error('bad deposit')
}
console.log('Deposit success')
console.log('Starting withdrawal')
const preBalance = await WETH9.balanceOf(signer.address)
const tx = await messenger.withdrawERC20(
WETH9.address,
l2WethAddress,
utils.parseEther('1')
)
await tx.wait()
setInterval(async () => {
const currentStatus = await messenger.getMessageStatus(tx)
console.log(`Message status: ${MessageStatus[currentStatus]}`)
}, 3000)
const now = Math.floor(Date.now() / 1000)
console.log('Waiting for message to be able to be relayed')
await messenger.waitForMessageStatus(tx, MessageStatus.READY_FOR_RELAY)
const finalize = await messenger.finalizeMessage(tx)
await finalize.wait()
console.log(`Took ${Math.floor(Date.now() / 1000) - now} seconds`)
const postBalance = await WETH9.balanceOf(signer.address)
const expectedBalance = preBalance.add(utils.parseEther('1'))
if (!expectedBalance.eq(postBalance)) {
throw new Error('Balance mismatch')
}
console.log('Withdrawal success')
})
......@@ -80,7 +80,8 @@ contract MockBridge {
address(0),
hex"1234",
1234,
12345678
12345678,
0
)
);
}
......@@ -101,7 +102,8 @@ contract MockBridge {
address(0),
hex"1234",
1234,
12345678
12345678,
0
)
);
}
......
......@@ -48,7 +48,8 @@ contract MockMessenger is ICrossDomainMessenger {
address sender;
bytes message;
uint256 messageNonce;
uint256 gasLimit;
uint256 minGasLimit;
uint256 value;
}
function doNothing() public {
......@@ -63,7 +64,7 @@ contract MockMessenger is ICrossDomainMessenger {
_params.sender,
_params.message,
_params.messageNonce,
_params.gasLimit
_params.minGasLimit
);
}
......
import { Provider } from '@ethersproject/abstract-provider'
import { expectApprox } from '@eth-optimism/core-utils'
import { expectApprox, hashCrossDomainMessage } from '@eth-optimism/core-utils'
import { predeploys } from '@eth-optimism/contracts'
import { Contract } from 'ethers'
import { ethers } from 'hardhat'
......@@ -8,7 +8,6 @@ import { expect } from './setup'
import {
MessageDirection,
CONTRACT_ADDRESSES,
hashCrossChainMessage,
omit,
MessageStatus,
CrossChainMessage,
......@@ -18,7 +17,7 @@ import {
L1ChainID,
L2ChainID,
} from '../src'
import { DUMMY_MESSAGE } from './helpers'
import { DUMMY_MESSAGE, DUMMY_EXTENDED_MESSAGE } from './helpers'
describe('CrossChainMessenger', () => {
let l1Signer: any
......@@ -202,6 +201,8 @@ describe('CrossChainMessenger', () => {
StateCommitmentChain: '0x' + '14'.repeat(20),
CanonicalTransactionChain: '0x' + '15'.repeat(20),
BondManager: '0x' + '16'.repeat(20),
OptimismPortal: '0x' + '17'.repeat(20),
L2OutputOracle: '0x' + '18'.repeat(20),
},
l2: {
L2CrossDomainMessenger: '0x' + '22'.repeat(20),
......@@ -318,7 +319,8 @@ describe('CrossChainMessenger', () => {
target: message.target,
message: message.message,
messageNonce: ethers.BigNumber.from(message.messageNonce),
gasLimit: ethers.BigNumber.from(message.gasLimit),
minGasLimit: ethers.BigNumber.from(message.minGasLimit),
value: ethers.BigNumber.from(message.value),
logIndex: i,
blockNumber: tx.blockNumber,
transactionHash: tx.hash,
......@@ -370,7 +372,8 @@ describe('CrossChainMessenger', () => {
target: message.target,
message: message.message,
messageNonce: ethers.BigNumber.from(message.messageNonce),
gasLimit: ethers.BigNumber.from(message.gasLimit),
minGasLimit: ethers.BigNumber.from(message.minGasLimit),
value: ethers.BigNumber.from(message.value),
logIndex: i,
blockNumber: tx.blockNumber,
transactionHash: tx.hash,
......@@ -497,15 +500,8 @@ describe('CrossChainMessenger', () => {
describe('when the input is a CrossChainMessage', () => {
it('should return the input', async () => {
const message = {
...DUMMY_EXTENDED_MESSAGE,
direction: MessageDirection.L1_TO_L2,
target: '0x' + '11'.repeat(20),
sender: '0x' + '22'.repeat(20),
message: '0x' + '33'.repeat(64),
messageNonce: 1234,
gasLimit: 0,
logIndex: 0,
blockNumber: 1234,
transactionHash: '0x' + '44'.repeat(32),
}
expect(await messenger.toCrossChainMessage(message)).to.deep.equal(
......@@ -655,7 +651,14 @@ describe('CrossChainMessenger', () => {
)
await l2Messenger.triggerRelayedMessageEvents([
hashCrossChainMessage(message),
hashCrossDomainMessage(
message.messageNonce,
message.sender,
message.target,
message.value,
message.minGasLimit,
message.message
),
])
expect(await messenger.getMessageStatus(message)).to.equal(
......@@ -671,7 +674,14 @@ describe('CrossChainMessenger', () => {
)
await l2Messenger.triggerFailedRelayedMessageEvents([
hashCrossChainMessage(message),
hashCrossDomainMessage(
message.messageNonce,
message.sender,
message.target,
message.value,
message.minGasLimit,
message.message
),
])
expect(await messenger.getMessageStatus(message)).to.equal(
......@@ -722,7 +732,14 @@ describe('CrossChainMessenger', () => {
ethers.provider.send('evm_mine', [])
await l1Messenger.triggerRelayedMessageEvents([
hashCrossChainMessage(message),
hashCrossDomainMessage(
message.messageNonce,
message.sender,
message.target,
message.value,
message.minGasLimit,
message.message
),
])
expect(await messenger.getMessageStatus(message)).to.equal(
......@@ -744,7 +761,14 @@ describe('CrossChainMessenger', () => {
ethers.provider.send('evm_mine', [])
await l1Messenger.triggerFailedRelayedMessageEvents([
hashCrossChainMessage(message),
hashCrossDomainMessage(
message.messageNonce,
message.sender,
message.target,
message.value,
message.minGasLimit,
message.message
),
])
expect(await messenger.getMessageStatus(message)).to.equal(
......@@ -821,19 +845,19 @@ describe('CrossChainMessenger', () => {
describe('when the relay was successful', () => {
it('should return the receipt of the transaction that relayed the message', async () => {
const message = {
...DUMMY_EXTENDED_MESSAGE,
direction: MessageDirection.L1_TO_L2,
target: '0x' + '11'.repeat(20),
sender: '0x' + '22'.repeat(20),
message: '0x' + '33'.repeat(64),
messageNonce: 1234,
gasLimit: 0,
logIndex: 0,
blockNumber: 1234,
transactionHash: '0x' + '44'.repeat(32),
}
const tx = await l2Messenger.triggerRelayedMessageEvents([
hashCrossChainMessage(message),
hashCrossDomainMessage(
message.messageNonce,
message.sender,
message.target,
message.value,
message.minGasLimit,
message.message
),
])
const messageReceipt = await messenger.getMessageReceipt(message)
......@@ -852,19 +876,19 @@ describe('CrossChainMessenger', () => {
describe('when the relay failed', () => {
it('should return the receipt of the transaction that attempted to relay the message', async () => {
const message = {
...DUMMY_EXTENDED_MESSAGE,
direction: MessageDirection.L1_TO_L2,
target: '0x' + '11'.repeat(20),
sender: '0x' + '22'.repeat(20),
message: '0x' + '33'.repeat(64),
messageNonce: 1234,
gasLimit: 0,
logIndex: 0,
blockNumber: 1234,
transactionHash: '0x' + '44'.repeat(32),
}
const tx = await l2Messenger.triggerFailedRelayedMessageEvents([
hashCrossChainMessage(message),
hashCrossDomainMessage(
message.messageNonce,
message.sender,
message.target,
message.value,
message.minGasLimit,
message.message
),
])
const messageReceipt = await messenger.getMessageReceipt(message)
......@@ -883,23 +907,30 @@ describe('CrossChainMessenger', () => {
describe('when the relay failed more than once', () => {
it('should return the receipt of the last transaction that attempted to relay the message', async () => {
const message = {
...DUMMY_EXTENDED_MESSAGE,
direction: MessageDirection.L1_TO_L2,
target: '0x' + '11'.repeat(20),
sender: '0x' + '22'.repeat(20),
message: '0x' + '33'.repeat(64),
messageNonce: 1234,
gasLimit: 0,
logIndex: 0,
blockNumber: 1234,
transactionHash: '0x' + '44'.repeat(32),
}
await l2Messenger.triggerFailedRelayedMessageEvents([
hashCrossChainMessage(message),
hashCrossDomainMessage(
message.messageNonce,
message.sender,
message.target,
message.value,
message.minGasLimit,
message.message
),
])
const tx = await l2Messenger.triggerFailedRelayedMessageEvents([
hashCrossChainMessage(message),
hashCrossDomainMessage(
message.messageNonce,
message.sender,
message.target,
message.value,
message.minGasLimit,
message.message
),
])
const messageReceipt = await messenger.getMessageReceipt(message)
......@@ -919,15 +950,8 @@ describe('CrossChainMessenger', () => {
describe('when the message has not been relayed', () => {
it('should return null', async () => {
const message = {
...DUMMY_EXTENDED_MESSAGE,
direction: MessageDirection.L1_TO_L2,
target: '0x' + '11'.repeat(20),
sender: '0x' + '22'.repeat(20),
message: '0x' + '33'.repeat(64),
messageNonce: 1234,
gasLimit: 0,
logIndex: 0,
blockNumber: 1234,
transactionHash: '0x' + '44'.repeat(32),
}
await l2Messenger.doNothing()
......@@ -965,19 +989,19 @@ describe('CrossChainMessenger', () => {
describe('when the message receipt already exists', () => {
it('should immediately return the receipt', async () => {
const message = {
...DUMMY_EXTENDED_MESSAGE,
direction: MessageDirection.L1_TO_L2,
target: '0x' + '11'.repeat(20),
sender: '0x' + '22'.repeat(20),
message: '0x' + '33'.repeat(64),
messageNonce: 1234,
gasLimit: 0,
logIndex: 0,
blockNumber: 1234,
transactionHash: '0x' + '44'.repeat(32),
}
const tx = await l2Messenger.triggerRelayedMessageEvents([
hashCrossChainMessage(message),
hashCrossDomainMessage(
message.messageNonce,
message.sender,
message.target,
message.value,
message.minGasLimit,
message.message
),
])
const messageReceipt = await messenger.waitForMessageReceipt(message)
......@@ -997,20 +1021,20 @@ describe('CrossChainMessenger', () => {
describe('when no extra options are provided', () => {
it('should wait for the receipt to be published', async () => {
const message = {
...DUMMY_EXTENDED_MESSAGE,
direction: MessageDirection.L1_TO_L2,
target: '0x' + '11'.repeat(20),
sender: '0x' + '22'.repeat(20),
message: '0x' + '33'.repeat(64),
messageNonce: 1234,
gasLimit: 0,
logIndex: 0,
blockNumber: 1234,
transactionHash: '0x' + '44'.repeat(32),
}
setTimeout(async () => {
await l2Messenger.triggerRelayedMessageEvents([
hashCrossChainMessage(message),
hashCrossDomainMessage(
message.messageNonce,
message.sender,
message.target,
message.value,
message.minGasLimit,
message.message
),
])
}, 5000)
......@@ -1030,15 +1054,8 @@ describe('CrossChainMessenger', () => {
describe('when a timeout is provided', () => {
it('should throw an error if the timeout is reached', async () => {
const message = {
...DUMMY_EXTENDED_MESSAGE,
direction: MessageDirection.L1_TO_L2,
target: '0x' + '11'.repeat(20),
sender: '0x' + '22'.repeat(20),
message: '0x' + '33'.repeat(64),
messageNonce: 1234,
gasLimit: 0,
logIndex: 0,
blockNumber: 1234,
transactionHash: '0x' + '44'.repeat(32),
}
await expect(
......@@ -1222,7 +1239,14 @@ describe('CrossChainMessenger', () => {
await l1Messenger.triggerSentMessageEvents([message])
await l2Messenger.triggerRelayedMessageEvents([
hashCrossChainMessage(message),
hashCrossDomainMessage(
message.messageNonce,
message.sender,
message.target,
message.value,
message.minGasLimit,
message.message
),
])
expect(
......@@ -1294,7 +1318,14 @@ describe('CrossChainMessenger', () => {
await l2Messenger.triggerSentMessageEvents([message])
await l1Messenger.triggerRelayedMessageEvents([
hashCrossChainMessage(message),
hashCrossDomainMessage(
message.messageNonce,
message.sender,
message.target,
message.value,
message.minGasLimit,
message.message
),
])
expect(
......
import { ethers } from 'ethers'
export const DUMMY_MESSAGE = {
target: '0x' + '11'.repeat(20),
sender: '0x' + '22'.repeat(20),
message: '0x' + '33'.repeat(64),
messageNonce: 1234,
gasLimit: 100000,
messageNonce: ethers.BigNumber.from(1234),
value: ethers.BigNumber.from(0),
minGasLimit: ethers.BigNumber.from(5678),
}
export const DUMMY_EXTENDED_MESSAGE = {
...DUMMY_MESSAGE,
logIndex: 0,
blockNumber: 1234,
transactionHash: '0x' + '44'.repeat(32),
}
import { Contract, Signer } from 'ethers'
import { ethers } from 'hardhat'
import { getContractFactory } from '@eth-optimism/contracts'
import { expect } from '../setup'
import {
CoreCrossChainMessage,
encodeCrossChainMessage,
hashCrossChainMessage,
} from '../../src'
describe('message encoding utils', () => {
let signers: Signer[]
before(async () => {
signers = (await ethers.getSigners()) as any
})
describe('encodeCrossChainMessage', () => {
let Lib_CrossDomainUtils: Contract
before(async () => {
Lib_CrossDomainUtils = (await getContractFactory(
'TestLib_CrossDomainUtils',
signers[0]
).deploy()) as any
})
it('should properly encode a message', async () => {
const message: CoreCrossChainMessage = {
target: '0x' + '11'.repeat(20),
sender: '0x' + '22'.repeat(20),
message: '0x' + '1234'.repeat(32),
messageNonce: 1234,
}
const actual = encodeCrossChainMessage(message)
const expected = await Lib_CrossDomainUtils.encodeXDomainCalldata(
message.target,
message.sender,
message.message,
message.messageNonce
)
expect(actual).to.equal(expected)
})
})
describe('hashCrossChainMessage', () => {
let MessageEncodingHelper: Contract
before(async () => {
MessageEncodingHelper = (await (
await ethers.getContractFactory('MessageEncodingHelper')
).deploy()) as any
})
it('should properly hash a message', async () => {
const message: CoreCrossChainMessage = {
target: '0x' + '11'.repeat(20),
sender: '0x' + '22'.repeat(20),
message: '0x' + '1234'.repeat(32),
messageNonce: 1234,
}
const actual = hashCrossChainMessage(message)
const expected = await MessageEncodingHelper.hashXDomainCalldata(
message.target,
message.sender,
message.message,
message.messageNonce
)
expect(actual).to.equal(expected)
})
})
})
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist"
},
"include": [
"src/**/*"
]
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment