From de14541ad4a99854c42ea4663901e219622e7b51 Mon Sep 17 00:00:00 2001
From: Kelvin Fichter <kelvin@optimism.io>
Date: Mon, 24 Jan 2022 12:29:42 -0500
Subject: [PATCH] feat(sdk): implement L2 message gas estimation

---
 packages/sdk/src/cross-chain-provider.ts      | 22 ++++-
 .../src/interfaces/cross-chain-provider.ts    |  9 +-
 .../sdk/test/cross-chain-provider.spec.ts     | 89 ++++++++++++++++++-
 3 files changed, 116 insertions(+), 4 deletions(-)

diff --git a/packages/sdk/src/cross-chain-provider.ts b/packages/sdk/src/cross-chain-provider.ts
index 455f1ac1b..8acc1ecf5 100644
--- a/packages/sdk/src/cross-chain-provider.ts
+++ b/packages/sdk/src/cross-chain-provider.ts
@@ -421,9 +421,27 @@ export class CrossChainProvider implements ICrossChainProvider {
   }
 
   public async estimateL2MessageGasLimit(
-    message: MessageLike
+    message: MessageLike,
+    opts?: {
+      bufferPercent?: number
+    }
   ): Promise<BigNumber> {
-    throw new Error('Not implemented')
+    const resolved = await this.toCrossChainMessage(message)
+
+    // L2 message gas estimation is only used for L1 => L2 messages.
+    if (resolved.direction === MessageDirection.L2_TO_L1) {
+      throw new Error(`cannot estimate gas limit for L2 => L1 message`)
+    }
+
+    const estimate = await this.l2Provider.estimateGas({
+      from: resolved.sender,
+      to: resolved.target,
+      data: resolved.message,
+    })
+
+    // Return the estimate plus a buffer of 20% just in case.
+    const bufferPercent = opts?.bufferPercent || 20
+    return estimate.mul(100 + bufferPercent).div(100)
   }
 
   public async estimateMessageWaitTimeSeconds(
diff --git a/packages/sdk/src/interfaces/cross-chain-provider.ts b/packages/sdk/src/interfaces/cross-chain-provider.ts
index fb75a751e..5fe4541f7 100644
--- a/packages/sdk/src/interfaces/cross-chain-provider.ts
+++ b/packages/sdk/src/interfaces/cross-chain-provider.ts
@@ -201,9 +201,16 @@ export interface ICrossChainProvider {
    * L1 => L2 messages. You would supply this gas limit when sending the message to L2.
    *
    * @param message Message get a gas estimate for.
+   * @param opts Options object.
+   * @param opts.bufferPercent Percentage of gas to add to the estimate. Defaults to 20.
    * @returns Estimates L2 gas limit.
    */
-  estimateL2MessageGasLimit(message: MessageLike): Promise<BigNumber>
+  estimateL2MessageGasLimit(
+    message: MessageLike,
+    opts?: {
+      bufferPercent?: number
+    }
+  ): Promise<BigNumber>
 
   /**
    * Returns the estimated amount of time before the message can be executed. When this is a
diff --git a/packages/sdk/test/cross-chain-provider.spec.ts b/packages/sdk/test/cross-chain-provider.spec.ts
index a4adc58cc..8d8fc34d1 100644
--- a/packages/sdk/test/cross-chain-provider.spec.ts
+++ b/packages/sdk/test/cross-chain-provider.spec.ts
@@ -1,4 +1,5 @@
 import { Provider } from '@ethersproject/abstract-provider'
+import { expectApprox } from '@eth-optimism/core-utils'
 import { Contract } from 'ethers'
 import { ethers } from 'hardhat'
 
@@ -1091,7 +1092,93 @@ describe('CrossChainProvider', () => {
   })
 
   describe('estimateL2MessageGasLimit', () => {
-    it('should perform a gas estimation of the L2 action')
+    let provider: CrossChainProvider
+    beforeEach(async () => {
+      provider = new CrossChainProvider({
+        l1Provider: ethers.provider,
+        l2Provider: ethers.provider,
+        l1ChainId: 31337,
+      })
+    })
+
+    describe('when the message is an L1 to L2 message', () => {
+      it('should return an accurate gas estimate plus a ~20% buffer', async () => {
+        const message = {
+          direction: MessageDirection.L1_TO_L2,
+          target: '0x' + '11'.repeat(20),
+          sender: '0x' + '22'.repeat(20),
+          message: '0x' + '33'.repeat(64),
+          messageNonce: 1234,
+          logIndex: 0,
+          blockNumber: 1234,
+          transactionHash: '0x' + '44'.repeat(32),
+        }
+
+        const estimate = await ethers.provider.estimateGas({
+          to: message.target,
+          from: message.sender,
+          data: message.message,
+        })
+
+        // Approximately 20% greater than the estimate, +/- 1%.
+        expectApprox(
+          await provider.estimateL2MessageGasLimit(message),
+          estimate.mul(120).div(100),
+          {
+            percentUpperDeviation: 1,
+            percentLowerDeviation: 1,
+          }
+        )
+      })
+
+      it('should return an accurate gas estimate when a custom buffer is provided', async () => {
+        const message = {
+          direction: MessageDirection.L1_TO_L2,
+          target: '0x' + '11'.repeat(20),
+          sender: '0x' + '22'.repeat(20),
+          message: '0x' + '33'.repeat(64),
+          messageNonce: 1234,
+          logIndex: 0,
+          blockNumber: 1234,
+          transactionHash: '0x' + '44'.repeat(32),
+        }
+
+        const estimate = await ethers.provider.estimateGas({
+          to: message.target,
+          from: message.sender,
+          data: message.message,
+        })
+
+        // Approximately 30% greater than the estimate, +/- 1%.
+        expectApprox(
+          await provider.estimateL2MessageGasLimit(message, {
+            bufferPercent: 30,
+          }),
+          estimate.mul(130).div(100),
+          {
+            percentUpperDeviation: 1,
+            percentLowerDeviation: 1,
+          }
+        )
+      })
+    })
+
+    describe('when the message is an L2 to L1 message', () => {
+      it('should throw an error', async () => {
+        const message = {
+          direction: MessageDirection.L2_TO_L1,
+          target: '0x' + '11'.repeat(20),
+          sender: '0x' + '22'.repeat(20),
+          message: '0x' + '33'.repeat(64),
+          messageNonce: 1234,
+          logIndex: 0,
+          blockNumber: 1234,
+          transactionHash: '0x' + '44'.repeat(32),
+        }
+
+        await expect(provider.estimateL2MessageGasLimit(message)).to.be.rejected
+      })
+    })
   })
 
   describe('estimateMessageWaitTimeBlocks', () => {
-- 
2.23.0