From dac4a9f066d81df95fec336066900e317904c7d1 Mon Sep 17 00:00:00 2001 From: smartcontracts Date: Tue, 2 Aug 2022 18:47:02 +0200 Subject: [PATCH] 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: Mark Tyneway --- .changeset/wise-ads-try.md | 7 + .circleci/config.yml | 4 + .../deploy-config/devnetL1.ts | 2 +- packages/core-utils/src/optimism/hashing.ts | 29 ++ packages/sdk/hardhat.config.ts | 22 + packages/sdk/package.json | 2 + packages/sdk/src/adapters/eth-bridge.ts | 34 +- packages/sdk/src/adapters/standard-bridge.ts | 45 +- packages/sdk/src/cross-chain-messenger.ts | 385 ++++++++++++++++-- .../src/interfaces/cross-chain-messenger.ts | 32 ++ packages/sdk/src/interfaces/types.ts | 11 +- packages/sdk/src/utils/chain-constants.ts | 2 + packages/sdk/src/utils/contracts.ts | 43 +- packages/sdk/src/utils/index.ts | 1 - packages/sdk/src/utils/merkle-utils.ts | 15 +- packages/sdk/src/utils/message-encoding.ts | 37 -- packages/sdk/tasks/deposit.ts | 214 ++++++++++ packages/sdk/test/contracts/MockBridge.sol | 6 +- packages/sdk/test/contracts/MockMessenger.sol | 5 +- .../sdk/test/cross-chain-messenger.spec.ts | 193 +++++---- packages/sdk/test/helpers/constants.ts | 14 +- .../sdk/test/utils/message-encoding.spec.ts | 72 ---- packages/sdk/tsconfig.json | 2 - 23 files changed, 890 insertions(+), 287 deletions(-) create mode 100644 .changeset/wise-ads-try.md delete mode 100644 packages/sdk/src/utils/message-encoding.ts create mode 100644 packages/sdk/tasks/deposit.ts delete mode 100644 packages/sdk/test/utils/message-encoding.spec.ts diff --git a/.changeset/wise-ads-try.md b/.changeset/wise-ads-try.md new file mode 100644 index 000000000000..53231cd7a9e1 --- /dev/null +++ b/.changeset/wise-ads-try.md @@ -0,0 +1,7 @@ +--- +'@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. diff --git a/.circleci/config.yml b/.circleci/config.yml index bf03fe70da26..a636b3919707 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -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 diff --git a/packages/contracts-bedrock/deploy-config/devnetL1.ts b/packages/contracts-bedrock/deploy-config/devnetL1.ts index 183398b51331..8a8df23833c0 100644 --- a/packages/contracts-bedrock/deploy-config/devnetL1.ts +++ b/packages/contracts-bedrock/deploy-config/devnetL1.ts @@ -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', diff --git a/packages/core-utils/src/optimism/hashing.ts b/packages/core-utils/src/optimism/hashing.ts index d639d18366a5..8a48b7e2852f 100644 --- a/packages/core-utils/src/optimism/hashing.ts +++ b/packages/core-utils/src/optimism/hashing.ts @@ -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. * diff --git a/packages/sdk/hardhat.config.ts b/packages/sdk/hardhat.config.ts index aeea1c1de447..6d3290f54cf1 100644 --- a/packages/sdk/hardhat.config.ts +++ b/packages/sdk/hardhat.config.ts @@ -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 diff --git a/packages/sdk/package.json b/packages/sdk/package.json index f9be264de176..51584ec2f566 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -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" diff --git a/packages/sdk/src/adapters/eth-bridge.ts b/packages/sdk/src/adapters/eth-bridge.ts index 028535bfcfe3..1eb15702275c 100644 --- a/packages/sdk/src/adapters/eth-bridge.ts +++ b/packages/sdk/src/adapters/eth-bridge.ts @@ -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, + } ) } }, diff --git a/packages/sdk/src/adapters/standard-bridge.ts b/packages/sdk/src/adapters/standard-bridge.ts index 63db67de3571..22d9a7d562cd 100644 --- a/packages/sdk/src/adapters/standard-bridge.ts +++ b/packages/sdk/src/adapters/standard-bridge.ts @@ -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 ) diff --git a/packages/sdk/src/cross-chain-messenger.ts b/packages/sdk/src/cross-chain-messenger.ts index f4346793c67c..816c3a223cf8 100644 --- a/packages/sdk/src/cross-chain-messenger.ts +++ b/packages/sdk/src/cross-chain-messenger.ts @@ -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 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 { + 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,20 +425,32 @@ export class CrossChainMessenger implements ICrossChainMessenger { } } else { if (receipt === null) { - const stateRoot = await this.getMessageStateRoot(resolved) - if (stateRoot === null) { - return MessageStatus.STATE_ROOT_NOT_PUBLISHED + 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 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) { - return MessageStatus.IN_CHALLENGE_PERIOD - } else { - return MessageStatus.READY_FOR_RELAY + const stateRoot = await this.getMessageStateRoot(resolved) + if (stateRoot === null) { + return MessageStatus.STATE_ROOT_NOT_PUBLISHED } + + const bn = stateRoot.batch.blockNumber + const block = await this.l1Provider.getBlock(bn) + timestamp = block.timestamp + } + + const challengePeriod = await this.getChallengePeriodSeconds() + const latestBlock = await this.l1Provider.getBlock('latest') + + if (timestamp + challengePeriod > latestBlock.timestamp) { + return MessageStatus.IN_CHALLENGE_PERIOD + } else { + return MessageStatus.READY_FOR_RELAY } } else { if (receipt.receiptStatus === MessageReceiptStatus.RELAYED_SUCCEEDED) { @@ -402,7 +466,14 @@ export class CrossChainMessenger implements ICrossChainMessenger { message: MessageLike ): Promise { 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 { - 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 { + 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 { @@ -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( - resolved.target, - resolved.sender, - resolved.message, - resolved.messageNonce, - resolved.gasLimit, - messageGasLimit, - opts?.overrides || {} - ) + 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.minGasLimit, + messageGasLimit, + opts?.overrides || {} + ) + } }, finalizeMessage: async ( @@ -1055,15 +1323,48 @@ export class CrossChainMessenger implements ICrossChainMessenger { throw new Error(`cannot finalize L1 to L2 message`) } - const proof = await this.getMessageProof(resolved) - return this.contracts.l1.L1CrossDomainMessenger.populateTransaction.relayMessage( - resolved.target, - resolved.sender, - resolved.message, - resolved.messageNonce, - proof, - opts?.overrides || {} - ) + 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) + const legacyL1XDM = new ethers.Contract( + this.contracts.l1.L1CrossDomainMessenger.address, + getContractInterface('L1CrossDomainMessenger'), + this.l1SignerOrProvider + ) + return legacyL1XDM.populateTransaction.relayMessage( + resolved.target, + resolved.sender, + resolved.message, + resolved.messageNonce, + proof, + opts?.overrides || {} + ) + } }, depositETH: async ( diff --git a/packages/sdk/src/interfaces/cross-chain-messenger.ts b/packages/sdk/src/interfaces/cross-chain-messenger.ts index 6cc1a9942b19..2c598215d149 100644 --- a/packages/sdk/src/interfaces/cross-chain-messenger.ts +++ b/packages/sdk/src/interfaces/cross-chain-messenger.ts @@ -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 + /** + * 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 + /** * 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 + /** + * 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. diff --git a/packages/sdk/src/interfaces/types.ts b/packages/sdk/src/interfaces/types.ts index 93b45a41ce24..a70795b7f79a 100644 --- a/packages/sdk/src/interfaces/types.ts +++ b/packages/sdk/src/interfaces/types.ts @@ -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 diff --git a/packages/sdk/src/utils/chain-constants.ts b/packages/sdk/src/utils/chain-constants.ts index d7d290aa92a8..73144ab784d1 100644 --- a/packages/sdk/src/utils/chain-constants.ts +++ b/packages/sdk/src/utils/chain-constants.ts @@ -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, } diff --git a/packages/sdk/src/utils/contracts.ts b/packages/sdk/src/utils/contracts.ts index 0fa405640633..57a68f11997e 100644 --- a/packages/sdk/src/utils/contracts.ts +++ b/packages/sdk/src/utils/contracts.ts @@ -1,4 +1,5 @@ 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, } diff --git a/packages/sdk/src/utils/index.ts b/packages/sdk/src/utils/index.ts index 41dcf3a1375e..eed51741f042 100644 --- a/packages/sdk/src/utils/index.ts +++ b/packages/sdk/src/utils/index.ts @@ -1,6 +1,5 @@ export * from './coercion' export * from './contracts' -export * from './message-encoding' export * from './type-utils' export * from './misc-utils' export * from './merkle-utils' diff --git a/packages/sdk/src/utils/merkle-utils.ts b/packages/sdk/src/utils/merkle-utils.ts index 5e47edfcdc0b..50244328ddff 100644 --- a/packages/sdk/src/utils/merkle-utils.ts +++ b/packages/sdk/src/utils/merkle-utils.ts @@ -1,12 +1,11 @@ /* 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, } } diff --git a/packages/sdk/src/utils/message-encoding.ts b/packages/sdk/src/utils/message-encoding.ts deleted file mode 100644 index 491594692851..000000000000 --- a/packages/sdk/src/utils/message-encoding.ts +++ /dev/null @@ -1,37 +0,0 @@ -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)] - ) -} diff --git a/packages/sdk/tasks/deposit.ts b/packages/sdk/tasks/deposit.ts new file mode 100644 index 000000000000..59dab27efc7d --- /dev/null +++ b/packages/sdk/tasks/deposit.ts @@ -0,0 +1,214 @@ +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') + }) diff --git a/packages/sdk/test/contracts/MockBridge.sol b/packages/sdk/test/contracts/MockBridge.sol index fd46d11537ca..7f3b5155cfb1 100644 --- a/packages/sdk/test/contracts/MockBridge.sol +++ b/packages/sdk/test/contracts/MockBridge.sol @@ -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 ) ); } diff --git a/packages/sdk/test/contracts/MockMessenger.sol b/packages/sdk/test/contracts/MockMessenger.sol index c23360f427b7..724d90ed6778 100644 --- a/packages/sdk/test/contracts/MockMessenger.sol +++ b/packages/sdk/test/contracts/MockMessenger.sol @@ -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 ); } diff --git a/packages/sdk/test/cross-chain-messenger.spec.ts b/packages/sdk/test/cross-chain-messenger.spec.ts index 4b2a78d23c33..91537431a1fb 100644 --- a/packages/sdk/test/cross-chain-messenger.spec.ts +++ b/packages/sdk/test/cross-chain-messenger.spec.ts @@ -1,5 +1,5 @@ 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( diff --git a/packages/sdk/test/helpers/constants.ts b/packages/sdk/test/helpers/constants.ts index eccdf7a848cd..8a13a005c808 100644 --- a/packages/sdk/test/helpers/constants.ts +++ b/packages/sdk/test/helpers/constants.ts @@ -1,7 +1,17 @@ +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), } diff --git a/packages/sdk/test/utils/message-encoding.spec.ts b/packages/sdk/test/utils/message-encoding.spec.ts deleted file mode 100644 index 232cefb22aaf..000000000000 --- a/packages/sdk/test/utils/message-encoding.spec.ts +++ /dev/null @@ -1,72 +0,0 @@ -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) - }) - }) -}) diff --git a/packages/sdk/tsconfig.json b/packages/sdk/tsconfig.json index a58b672ec1f6..5cb4fda3c546 100644 --- a/packages/sdk/tsconfig.json +++ b/packages/sdk/tsconfig.json @@ -1,11 +1,9 @@ { "extends": "../../tsconfig.json", - "compilerOptions": { "rootDir": "./src", "outDir": "./dist" }, - "include": [ "src/**/*" ]