From 3bc03dc7e574abde2892ee3caaf73228938cf8e4 Mon Sep 17 00:00:00 2001 From: Will Cory Date: Fri, 16 Jun 2023 18:30:54 -0700 Subject: [PATCH] fix(sdk): Fix multicall3 support for sdk --- packages/sdk/src/cross-chain-messenger.ts | 421 ++++++++++++++-------- 1 file changed, 268 insertions(+), 153 deletions(-) diff --git a/packages/sdk/src/cross-chain-messenger.ts b/packages/sdk/src/cross-chain-messenger.ts index 764d9b6337c4..8c22fcae4ffe 100644 --- a/packages/sdk/src/cross-chain-messenger.ts +++ b/packages/sdk/src/cross-chain-messenger.ts @@ -329,9 +329,16 @@ export class CrossChainMessenger { * @returns Bedrock representation of the message. */ public async toBedrockCrossChainMessage( - message: MessageLike + message: MessageLike, + /** + * The index of the withdrawal if multiple are made with multicall + */ + multiWithdrawalIndex = 0 ): Promise { - const resolved = await this.toCrossChainMessage(message) + const resolved = await this.toCrossChainMessage( + message, + multiWithdrawalIndex + ) // Bedrock messages are already in the correct format. const { version } = decodeVersionedNonce(resolved.messageNonce) @@ -367,6 +374,25 @@ export class CrossChainMessenger { } } + public async getWithdrawalsFromMessage(message: MessageLike) { + const resolved = await this.toCrossChainMessage(message) + const receipt = await this.l2Provider.getTransactionReceipt( + resolved.transactionHash + ) + + const withdrawals: any[] = [] + for (const log of receipt.logs) { + if (log.address === this.contracts.l2.BedrockMessagePasser.address) { + const decoded = + this.contracts.l2.L2ToL1MessagePasser.interface.parseLog(log) + if (decoded.name === 'MessagePassed') { + withdrawals.push(decoded.args) + } + } + } + return withdrawals + } + /** * Transforms a CrossChainMessenger message into its low-level representation inside the * L2ToL1MessagePasser contract on L2. @@ -375,9 +401,16 @@ export class CrossChainMessenger { * @return Transformed message. */ public async toLowLevelMessage( - message: MessageLike + message: MessageLike, + /** + * The index of the withdrawal if multiple are made with multicall + */ + multiWithdrawalIndex = 0 ): Promise { - const resolved = await this.toCrossChainMessage(message) + const resolved = await this.toCrossChainMessage( + message, + multiWithdrawalIndex + ) if (resolved.direction === MessageDirection.L1_TO_L2) { throw new Error(`can only convert L2 to L1 messages to low level`) } @@ -386,7 +419,10 @@ export class CrossChainMessenger { const { version } = decodeVersionedNonce(resolved.messageNonce) let updated: CrossChainMessage if (version.eq(0)) { - updated = await this.toBedrockCrossChainMessage(resolved) + updated = await this.toBedrockCrossChainMessage( + resolved, + multiWithdrawalIndex + ) } else { updated = resolved } @@ -411,32 +447,17 @@ export class CrossChainMessenger { gasLimit = migratedWithdrawalGasLimit(encoded, chainID) messageNonce = resolved.messageNonce } else { - const receipt = await this.l2Provider.getTransactionReceipt( - resolved.transactionHash - ) - - const withdrawals: any[] = [] - for (const log of receipt.logs) { - if (log.address === this.contracts.l2.BedrockMessagePasser.address) { - const decoded = - this.contracts.l2.L2ToL1MessagePasser.interface.parseLog(log) - if (decoded.name === 'MessagePassed') { - withdrawals.push(decoded.args) - } - } - } + const withdrawals = + await this.getWithdrawalsFromMessage( + message + ) // Should not happen. if (withdrawals.length === 0) { throw new Error(`no withdrawals found in receipt`) } - // TODO: Add support for multiple withdrawals. - if (withdrawals.length > 1) { - throw new Error(`multiple withdrawals found in receipt`) - } - - const withdrawal = withdrawals[0] + const withdrawal = withdrawals[multiWithdrawalIndex] messageNonce = withdrawal.nonce gasLimit = withdrawal.gasLimit } @@ -577,7 +598,11 @@ export class CrossChainMessenger { * @returns Message coerced into a CrossChainMessage. */ public async toCrossChainMessage( - message: MessageLike + message: MessageLike, + /** + * The index of the withdrawal if multiple are made with multicall + */ + multiWithdrawalIndex = 0 ): Promise { if (!message) { throw new Error('message is undefined') @@ -621,14 +646,7 @@ export class CrossChainMessenger { message as TransactionLike ) - // We only want to treat TransactionLike objects as MessageLike if they only emit a single - // message (very common). It's unintuitive to treat a TransactionLike as a MessageLike if - // they emit more than one message (which message do you pick?), so we throw an error. - if (messages.length !== 1) { - throw new Error(`expected 1 message, got ${messages.length}`) - } - - return messages[0] + return messages[multiWithdrawalIndex] } } @@ -638,9 +656,19 @@ export class CrossChainMessenger { * @param message Cross chain message to check the status of. * @returns Status of the message. */ - public async getMessageStatus(message: MessageLike): Promise { - const resolved = await this.toCrossChainMessage(message) - const receipt = await this.getMessageReceipt(resolved) + public async getMessageStatus( + message: MessageLike, + + /** + * The index of the withdrawal if multiple are made with multicall + */ + multiWithdrawalIndex = 0 + ): Promise { + const resolved = await this.toCrossChainMessage( + message, + multiWithdrawalIndex + ) + const receipt = await this.getMessageReceipt(resolved, multiWithdrawalIndex) if (resolved.direction === MessageDirection.L1_TO_L2) { if (receipt === null) { @@ -656,13 +684,19 @@ export class CrossChainMessenger { if (receipt === null) { let timestamp: number if (this.bedrock) { - const output = await this.getMessageBedrockOutput(resolved) + const output = await this.getMessageBedrockOutput( + resolved, + multiWithdrawalIndex + ) if (output === null) { return MessageStatus.STATE_ROOT_NOT_PUBLISHED } // Convert the message to the low level message that was proven. - const withdrawal = await this.toLowLevelMessage(resolved) + const withdrawal = await this.toLowLevelMessage( + resolved, + multiWithdrawalIndex + ) // Attempt to fetch the proven withdrawal. const provenWithdrawal = @@ -679,7 +713,10 @@ export class CrossChainMessenger { // Set the timestamp to the provenWithdrawal's timestamp timestamp = provenWithdrawal.timestamp.toNumber() } else { - const stateRoot = await this.getMessageStateRoot(resolved) + const stateRoot = await this.getMessageStateRoot( + resolved, + multiWithdrawalIndex + ) if (stateRoot === null) { return MessageStatus.STATE_ROOT_NOT_PUBLISHED } @@ -715,9 +752,17 @@ export class CrossChainMessenger { * given message. */ public async getMessageReceipt( - message: MessageLike + message: MessageLike, + + /** + * The index of the withdrawal if multiple are made with multicall + */ + multiWithdrawalIndex = 0 ): Promise { - const resolved = await this.toCrossChainMessage(message) + const resolved = await this.toCrossChainMessage( + message, + multiWithdrawalIndex + ) // legacy withdrawals relayed prebedrock are v1 const messageHashV0 = hashCrossDomainMessagev0( resolved.target, @@ -819,15 +864,26 @@ export class CrossChainMessenger { confirmations?: number pollIntervalMs?: number timeoutMs?: number - } = {} + } = {}, + + /** + * The index of the withdrawal if multiple are made with multicall + */ + multiWithdrawalIndex = 0 ): Promise { // Resolving once up-front is slightly more efficient. - const resolved = await this.toCrossChainMessage(message) + const resolved = await this.toCrossChainMessage( + message, + multiWithdrawalIndex + ) let totalTimeMs = 0 while (totalTimeMs < (opts.timeoutMs || Infinity)) { const tick = Date.now() - const receipt = await this.getMessageReceipt(resolved) + const receipt = await this.getMessageReceipt( + resolved, + multiWithdrawalIndex + ) if (receipt !== null) { return receipt } else { @@ -857,15 +913,26 @@ export class CrossChainMessenger { opts: { pollIntervalMs?: number timeoutMs?: number - } = {} + } = {}, + + /** + * The index of the withdrawal if multiple are made with multicall + */ + multiWithdrawalIndex = 0 ): Promise { // Resolving once up-front is slightly more efficient. - const resolved = await this.toCrossChainMessage(message) + const resolved = await this.toCrossChainMessage( + message, + multiWithdrawalIndex + ) let totalTimeMs = 0 while (totalTimeMs < (opts.timeoutMs || Infinity)) { const tick = Date.now() - const currentStatus = await this.getMessageStatus(resolved) + const currentStatus = await this.getMessageStatus( + resolved, + multiWithdrawalIndex + ) // Handle special cases for L1 to L2 messages. if (resolved.direction === MessageDirection.L1_TO_L2) { @@ -933,7 +1000,8 @@ export class CrossChainMessenger { opts?: { bufferPercent?: number from?: string - } + }, + multiWithdrawalIndex = 0 ): Promise { let resolved: CrossChainMessage | CrossChainMessageRequest let from: string @@ -941,7 +1009,10 @@ export class CrossChainMessenger { resolved = message as CrossChainMessageRequest from = opts?.from } else { - resolved = await this.toCrossChainMessage(message as MessageLike) + resolved = await this.toCrossChainMessage( + message as MessageLike, + multiWithdrawalIndex + ) from = opts?.from || (resolved as CrossChainMessage).sender } @@ -971,10 +1042,18 @@ export class CrossChainMessenger { * @returns Estimated amount of time remaining (in seconds) before the message can be executed. */ public async estimateMessageWaitTimeSeconds( - message: MessageLike + message: MessageLike, + + /** + * The index of the withdrawal if multiple are made with multicall + */ + multiWithdrawalIndex = 0 ): Promise { - const resolved = await this.toCrossChainMessage(message) - const status = await this.getMessageStatus(resolved) + const resolved = await this.toCrossChainMessage( + message, + multiWithdrawalIndex + ) + const status = await this.getMessageStatus(resolved, multiWithdrawalIndex) if (resolved.direction === MessageDirection.L1_TO_L2) { if ( status === MessageStatus.RELAYED || @@ -1012,7 +1091,10 @@ export class CrossChainMessenger { // If the message is still within the challenge period, then we need to estimate exactly // the amount of time left until the challenge period expires. The challenge period starts // when the state root is published. - const stateRoot = await this.getMessageStateRoot(resolved) + const stateRoot = await this.getMessageStateRoot( + resolved, + multiWithdrawalIndex + ) const challengePeriod = await this.getChallengePeriodSeconds() const targetBlock = await this.l1Provider.getBlock( stateRoot.batch.blockNumber @@ -1045,13 +1127,13 @@ export class CrossChainMessenger { const challengePeriod = oracleVersion === '1.0.0' ? // The ABI in the SDK does not contain FINALIZATION_PERIOD_SECONDS - // in OptimismPortal, so making an explicit call instead. - BigNumber.from( - await this.contracts.l1.OptimismPortal.provider.call({ - to: this.contracts.l1.OptimismPortal.address, - data: '0xf4daa291', // FINALIZATION_PERIOD_SECONDS - }) - ) + // in OptimismPortal, so making an explicit call instead. + BigNumber.from( + await this.contracts.l1.OptimismPortal.provider.call({ + to: this.contracts.l1.OptimismPortal.address, + data: '0xf4daa291', // FINALIZATION_PERIOD_SECONDS + }) + ) : await this.contracts.l1.L2OutputOracle.FINALIZATION_PERIOD_SECONDS() return challengePeriod.toNumber() } @@ -1082,9 +1164,17 @@ export class CrossChainMessenger { * @returns Bedrock output root. */ public async getMessageBedrockOutput( - message: MessageLike + message: MessageLike, + + /** + * The index of the withdrawal if multiple are made with multicall + */ + multiWithdrawalIndex = 0 ): Promise { - const resolved = await this.toCrossChainMessage(message) + const resolved = await this.toCrossChainMessage( + message, + multiWithdrawalIndex + ) // Outputs are only a thing for L2 to L1 messages. if (resolved.direction === MessageDirection.L1_TO_L2) { @@ -1099,7 +1189,7 @@ export class CrossChainMessenger { try { l2OutputIndex = await this.contracts.l1.L2OutputOracle.getL2OutputIndexAfter( - resolved.blockNumber + resolved.blockNumber, ) } catch (err) { if (err.message.includes('L2OutputOracle: cannot get output')) { @@ -1112,7 +1202,7 @@ export class CrossChainMessenger { // Now pull the proposal out given the output index. Should always work as long as the above // codepath completed successfully. const proposal = await this.contracts.l1.L2OutputOracle.getL2Output( - l2OutputIndex + l2OutputIndex, ) // Format everything and return it nicely. @@ -1133,9 +1223,17 @@ export class CrossChainMessenger { * @returns State root for the block in which the message was created. */ public async getMessageStateRoot( - message: MessageLike + message: MessageLike, + + /** + * The index of the withdrawal if multiple are made with multicall + */ + multiWithdrawalIndex = 0 ): Promise { - const resolved = await this.toCrossChainMessage(message) + const resolved = await this.toCrossChainMessage( + message, + multiWithdrawalIndex + ) // State roots are only a thing for L2 to L1 messages. if (resolved.direction === MessageDirection.L1_TO_L2) { @@ -1318,14 +1416,25 @@ export class CrossChainMessenger { * @returns Proof that can be used to finalize the message. */ public async getMessageProof( - message: MessageLike + message: MessageLike, + + /** + * The index of the withdrawal if multiple are made with multicall + */ + multiWithdrawalIndex = 0 ): Promise { - const resolved = await this.toCrossChainMessage(message) + const resolved = await this.toCrossChainMessage( + message, + multiWithdrawalIndex + ) if (resolved.direction === MessageDirection.L1_TO_L2) { throw new Error(`can only generate proofs for L2 to L1 messages`) } - const stateRoot = await this.getMessageStateRoot(resolved) + const stateRoot = await this.getMessageStateRoot( + resolved, + multiWithdrawalIndex + ) if (stateRoot === null) { throw new Error(`state root for message not yet published`) } @@ -1376,19 +1485,33 @@ export class CrossChainMessenger { * @returns Proof that can be used to finalize the message. */ public async getBedrockMessageProof( - message: MessageLike + message: MessageLike, + + /** + * The index of the withdrawal if multiple are made with multicall + */ + multiWithdrawalIndex = 0 ): Promise { - const resolved = await this.toCrossChainMessage(message) + const resolved = await this.toCrossChainMessage( + message, + multiWithdrawalIndex + ) 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) + const output = await this.getMessageBedrockOutput( + resolved, + multiWithdrawalIndex + ) if (output === null) { throw new Error(`state root for message not yet published`) } - const withdrawal = await this.toLowLevelMessage(resolved) + const withdrawal = await this.toLowLevelMessage( + resolved, + multiWithdrawalIndex + ) const hash = hashLowLevelMessage(withdrawal) const messageSlot = hashMessageHash(hash) @@ -1462,7 +1585,7 @@ export class CrossChainMessenger { opts?: { signer?: Signer overrides?: Overrides - } + }, ): Promise { return (opts?.signer || this.l1Signer).sendTransaction( await this.populateTransaction.resendMessage( @@ -1488,7 +1611,7 @@ export class CrossChainMessenger { opts?: { signer?: Signer overrides?: Overrides - } + }, ): Promise { const tx = await this.populateTransaction.proveMessage(message, opts) return (opts?.signer || this.l1Signer).sendTransaction(tx) @@ -1509,7 +1632,7 @@ export class CrossChainMessenger { opts?: { signer?: Signer overrides?: PayableOverrides - } + }, ): Promise { return (opts?.signer || this.l1Signer).sendTransaction( await this.populateTransaction.finalizeMessage(message, opts) @@ -1635,7 +1758,7 @@ export class CrossChainMessenger { recipient?: AddressLike signer?: Signer l2GasLimit?: NumberLike - overrides?: CallOverrides + overrides?: Overrides } ): Promise { return (opts?.signer || this.l1Signer).sendTransaction( @@ -1734,9 +1857,17 @@ export class CrossChainMessenger { messageGasLimit: NumberLike, opts?: { overrides?: Overrides - } + }, + + /** + * The index of the withdrawal if multiple are made with multicall + */ + multiWithdrawalIndex = 0 ): Promise => { - const resolved = await this.toCrossChainMessage(message) + const resolved = await this.toCrossChainMessage( + message, + multiWithdrawalIndex + ) if (resolved.direction === MessageDirection.L2_TO_L1) { throw new Error(`cannot resend L2 to L1 message`) } @@ -1780,9 +1911,17 @@ export class CrossChainMessenger { message: MessageLike, opts?: { overrides?: PayableOverrides - } + }, + + /** + * The index of the withdrawal if multiple are made with multicall + */ + multiWithdrawalIndex = 0 ): Promise => { - const resolved = await this.toCrossChainMessage(message) + const resolved = await this.toCrossChainMessage( + message, + multiWithdrawalIndex + ) if (resolved.direction === MessageDirection.L1_TO_L2) { throw new Error('cannot finalize L1 to L2 message') } @@ -1793,8 +1932,14 @@ export class CrossChainMessenger { ) } - const withdrawal = await this.toLowLevelMessage(resolved) - const proof = await this.getBedrockMessageProof(resolved) + const withdrawal = await this.toLowLevelMessage( + resolved, + multiWithdrawalIndex + ) + const proof = await this.getBedrockMessageProof( + resolved, + multiWithdrawalIndex + ) const args = [ [ @@ -1835,15 +1980,26 @@ export class CrossChainMessenger { message: MessageLike, opts?: { overrides?: PayableOverrides - } + }, + + /** + * The index of the withdrawal if multiple are made with multicall + */ + multiWithdrawalIndex = 0 ): Promise => { - const resolved = await this.toCrossChainMessage(message) + const resolved = await this.toCrossChainMessage( + message, + multiWithdrawalIndex + ) if (resolved.direction === MessageDirection.L1_TO_L2) { throw new Error(`cannot finalize L1 to L2 message`) } if (this.bedrock) { - const withdrawal = await this.toLowLevelMessage(resolved) + const withdrawal = await this.toLowLevelMessage( + resolved, + multiWithdrawalIndex + ) return this.contracts.l1.OptimismPortal.populateTransaction.finalizeWithdrawalTransaction( [ withdrawal.messageNonce, @@ -1859,7 +2015,7 @@ export class CrossChainMessenger { // 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 proof = await this.getMessageProof(resolved, multiWithdrawalIndex) const legacyL1XDM = new ethers.Contract( this.contracts.l1.L1CrossDomainMessenger.address, getContractInterface('L1CrossDomainMessenger'), @@ -1892,27 +2048,13 @@ export class CrossChainMessenger { recipient?: AddressLike l2GasLimit?: NumberLike overrides?: PayableOverrides - }, - isEstimatingGas: boolean = false - ): Promise => { - const getOpts = async () => { - if (isEstimatingGas) { - return opts - } - const gasEstimation = await this.estimateGas.depositETH(amount, opts) - return { - ...opts, - overrides: { - ...opts?.overrides, - gasLimit: gasEstimation.add(gasEstimation.div(2)), - }, - } } + ): Promise => { return this.bridges.ETH.populateTransaction.deposit( ethers.constants.AddressZero, predeploys.OVM_ETH, amount, - await getOpts() + opts ) }, @@ -1981,48 +2123,11 @@ export class CrossChainMessenger { opts?: { recipient?: AddressLike l2GasLimit?: NumberLike - overrides?: CallOverrides - }, - isEstimatingGas: boolean = false + overrides?: Overrides + } ): Promise => { const bridge = await this.getBridgeForTokenPair(l1Token, l2Token) - // we need extra buffer for gas limit - const getOpts = async () => { - if (isEstimatingGas) { - return opts - } - // if we don't include the users address the estimation will fail from lack of allowance - if (!ethers.Signer.isSigner(this.l1SignerOrProvider)) { - throw new Error('unable to deposit without an l1 signer') - } - const from = (this.l1SignerOrProvider as Signer).getAddress() - const gasEstimation = await this.estimateGas.depositERC20( - l1Token, - l2Token, - amount, - { - ...opts, - overrides: { - ...opts?.overrides, - from: opts?.overrides?.from ?? from, - }, - } - ) - return { - ...opts, - overrides: { - ...opts?.overrides, - gasLimit: gasEstimation.add(gasEstimation.div(2)), - from: opts?.overrides?.from ?? from, - }, - } - } - return bridge.populateTransaction.deposit( - l1Token, - l2Token, - amount, - await getOpts() - ) + return bridge.populateTransaction.deposit(l1Token, l2Token, amount, opts) }, /** @@ -2093,13 +2198,13 @@ export class CrossChainMessenger { messageGasLimit: NumberLike, opts?: { overrides?: CallOverrides - } + }, ): Promise => { return this.l1Provider.estimateGas( await this.populateTransaction.resendMessage( message, messageGasLimit, - opts + opts, ) ) }, @@ -2116,10 +2221,18 @@ export class CrossChainMessenger { message: MessageLike, opts?: { overrides?: CallOverrides - } + }, + /** + * The index of the withdrawal if multiple are made with multicall + */ + multiWithdrawalIndex = 0 ): Promise => { return this.l1Provider.estimateGas( - await this.populateTransaction.proveMessage(message, opts) + await this.populateTransaction.proveMessage( + message, + opts, + multiWithdrawalIndex + ) ) }, @@ -2135,10 +2248,13 @@ export class CrossChainMessenger { message: MessageLike, opts?: { overrides?: CallOverrides - } + }, ): Promise => { return this.l1Provider.estimateGas( - await this.populateTransaction.finalizeMessage(message, opts) + await this.populateTransaction.finalizeMessage( + message, + opts, + ) ) }, @@ -2161,7 +2277,7 @@ export class CrossChainMessenger { } ): Promise => { return this.l1Provider.estimateGas( - await this.populateTransaction.depositETH(amount, opts, true) + await this.populateTransaction.depositETH(amount, opts) ) }, @@ -2241,8 +2357,7 @@ export class CrossChainMessenger { l1Token, l2Token, amount, - opts, - true + opts ) ) },