standard-bridge.ts 11.1 KB
Newer Older
1
/* eslint-disable @typescript-eslint/no-unused-vars */
2 3 4 5 6 7 8 9
import {
  ethers,
  Contract,
  Overrides,
  Signer,
  BigNumber,
  CallOverrides,
} from 'ethers'
10 11 12 13 14
import {
  TransactionRequest,
  TransactionResponse,
  BlockTag,
} from '@ethersproject/abstract-provider'
15 16
import { predeploys } from '@eth-optimism/contracts'
import { getContractInterface } from '@eth-optimism/contracts-bedrock'
17 18
import { hexStringEquals } from '@eth-optimism/core-utils'

19
import { CrossChainMessenger } from '../cross-chain-messenger'
20 21 22 23 24 25 26 27 28 29 30 31 32
import {
  IBridgeAdapter,
  NumberLike,
  AddressLike,
  TokenBridgeMessage,
  MessageDirection,
} from '../interfaces'
import { toAddress } from '../utils'

/**
 * Bridge adapter for any token bridge that uses the standard token bridge interface.
 */
export class StandardBridgeAdapter implements IBridgeAdapter {
33
  public messenger: CrossChainMessenger
34 35 36 37 38 39 40
  public l1Bridge: Contract
  public l2Bridge: Contract

  /**
   * Creates a StandardBridgeAdapter instance.
   *
   * @param opts Options for the adapter.
41
   * @param opts.messenger Provider used to make queries related to cross-chain interactions.
42 43 44 45
   * @param opts.l1Bridge L1 bridge contract.
   * @param opts.l2Bridge L2 bridge contract.
   */
  constructor(opts: {
46
    messenger: CrossChainMessenger
47 48 49
    l1Bridge: AddressLike
    l2Bridge: AddressLike
  }) {
50
    this.messenger = opts.messenger
51 52 53
    this.l1Bridge = new Contract(
      toAddress(opts.l1Bridge),
      getContractInterface('L1StandardBridge'),
54
      this.messenger.l1Provider
55 56 57
    )
    this.l2Bridge = new Contract(
      toAddress(opts.l2Bridge),
58
      getContractInterface('L2StandardBridge'),
59
      this.messenger.l2Provider
60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
    )
  }

  public async getDepositsByAddress(
    address: AddressLike,
    opts?: {
      fromBlock?: BlockTag
      toBlock?: BlockTag
    }
  ): Promise<TokenBridgeMessage[]> {
    const events = await this.l1Bridge.queryFilter(
      this.l1Bridge.filters.ERC20DepositInitiated(
        undefined,
        undefined,
        address
      ),
      opts?.fromBlock,
      opts?.toBlock
    )

    return events
      .filter((event) => {
        // Specifically filter out ETH. ETH deposits and withdrawals are handled by the ETH bridge
        // adapter. Bridges that are not the ETH bridge should not be able to handle or even
        // present ETH deposits or withdrawals.
        return (
86 87
          !hexStringEquals(event.args.l1Token, ethers.constants.AddressZero) &&
          !hexStringEquals(event.args.l2Token, predeploys.OVM_ETH)
88 89 90 91 92
        )
      })
      .map((event) => {
        return {
          direction: MessageDirection.L1_TO_L2,
93 94 95 96 97 98
          from: event.args.from,
          to: event.args.to,
          l1Token: event.args.l1Token,
          l2Token: event.args.l2Token,
          amount: event.args.amount,
          data: event.args.extraData,
99 100 101 102 103
          logIndex: event.logIndex,
          blockNumber: event.blockNumber,
          transactionHash: event.transactionHash,
        }
      })
104 105 106 107
      .sort((a, b) => {
        // Sort descending by block number
        return b.blockNumber - a.blockNumber
      })
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128
  }

  public async getWithdrawalsByAddress(
    address: AddressLike,
    opts?: {
      fromBlock?: BlockTag
      toBlock?: BlockTag
    }
  ): Promise<TokenBridgeMessage[]> {
    const events = await this.l2Bridge.queryFilter(
      this.l2Bridge.filters.WithdrawalInitiated(undefined, undefined, address),
      opts?.fromBlock,
      opts?.toBlock
    )

    return events
      .filter((event) => {
        // Specifically filter out ETH. ETH deposits and withdrawals are handled by the ETH bridge
        // adapter. Bridges that are not the ETH bridge should not be able to handle or even
        // present ETH deposits or withdrawals.
        return (
129 130
          !hexStringEquals(event.args.l1Token, ethers.constants.AddressZero) &&
          !hexStringEquals(event.args.l2Token, predeploys.OVM_ETH)
131 132 133 134 135
        )
      })
      .map((event) => {
        return {
          direction: MessageDirection.L2_TO_L1,
136 137 138 139 140 141
          from: event.args.from,
          to: event.args.to,
          l1Token: event.args.l1Token,
          l2Token: event.args.l2Token,
          amount: event.args.amount,
          data: event.args.extraData,
142 143 144 145 146
          logIndex: event.logIndex,
          blockNumber: event.blockNumber,
          transactionHash: event.transactionHash,
        }
      })
147 148 149 150
      .sort((a, b) => {
        // Sort descending by block number
        return b.blockNumber - a.blockNumber
      })
151 152 153 154 155 156 157 158 159
  }

  public async supportsTokenPair(
    l1Token: AddressLike,
    l2Token: AddressLike
  ): Promise<boolean> {
    try {
      const contract = new Contract(
        toAddress(l2Token),
160
        getContractInterface('OptimismMintableERC20'),
161
        this.messenger.l2Provider
162 163 164 165 166 167 168 169 170 171 172
      )
      // Don't support ETH deposits or withdrawals via this bridge.
      if (
        hexStringEquals(toAddress(l1Token), ethers.constants.AddressZero) ||
        hexStringEquals(toAddress(l2Token), predeploys.OVM_ETH)
      ) {
        return false
      }

      // Make sure the L1 token matches.
      const remoteL1Token = await contract.l1Token()
173

174 175 176 177 178 179 180 181 182 183 184 185 186
      if (!hexStringEquals(remoteL1Token, toAddress(l1Token))) {
        return false
      }

      // Make sure the L2 bridge matches.
      const remoteL2Bridge = await contract.l2Bridge()
      if (!hexStringEquals(remoteL2Bridge, this.l2Bridge.address)) {
        return false
      }

      return true
    } catch (err) {
      // If the L2 token is not an L2StandardERC20, it may throw an error. If there's a call
187 188
      // exception then we assume that the token is not supported. Other errors are thrown. Since
      // the JSON-RPC API is not well-specified, we need to handle multiple possible error codes.
tre's avatar
tre committed
189 190 191 192
      if (
        !err?.message?.toString().includes('CALL_EXCEPTION') &&
        !err?.stack?.toString().includes('execution reverted')
      ) {
tre's avatar
tre committed
193
        console.error('Unexpected error when checking bridge', err)
194
      }
195
      return false
196 197 198
    }
  }

199 200 201 202 203 204 205 206 207 208 209
  public async approval(
    l1Token: AddressLike,
    l2Token: AddressLike,
    signer: ethers.Signer
  ): Promise<BigNumber> {
    if (!(await this.supportsTokenPair(l1Token, l2Token))) {
      throw new Error(`token pair not supported by bridge`)
    }

    const token = new Contract(
      toAddress(l1Token),
210
      getContractInterface('OptimismMintableERC20'), // Any ERC20 will do
211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230
      this.messenger.l1Provider
    )

    return token.allowance(await signer.getAddress(), this.l1Bridge.address)
  }

  public async approve(
    l1Token: AddressLike,
    l2Token: AddressLike,
    amount: NumberLike,
    signer: Signer,
    opts?: {
      overrides?: Overrides
    }
  ): Promise<TransactionResponse> {
    return signer.sendTransaction(
      await this.populateTransaction.approve(l1Token, l2Token, amount, opts)
    )
  }

231 232 233 234 235 236
  public async deposit(
    l1Token: AddressLike,
    l2Token: AddressLike,
    amount: NumberLike,
    signer: Signer,
    opts?: {
237
      recipient?: AddressLike
238 239 240 241 242 243 244 245 246 247 248 249 250 251 252
      l2GasLimit?: NumberLike
      overrides?: Overrides
    }
  ): Promise<TransactionResponse> {
    return signer.sendTransaction(
      await this.populateTransaction.deposit(l1Token, l2Token, amount, opts)
    )
  }

  public async withdraw(
    l1Token: AddressLike,
    l2Token: AddressLike,
    amount: NumberLike,
    signer: Signer,
    opts?: {
253
      recipient?: AddressLike
254 255 256 257 258 259 260 261 262
      overrides?: Overrides
    }
  ): Promise<TransactionResponse> {
    return signer.sendTransaction(
      await this.populateTransaction.withdraw(l1Token, l2Token, amount, opts)
    )
  }

  populateTransaction = {
263 264 265 266 267 268 269 270 271 272 273 274 275 276
    approve: async (
      l1Token: AddressLike,
      l2Token: AddressLike,
      amount: NumberLike,
      opts?: {
        overrides?: Overrides
      }
    ): Promise<TransactionRequest> => {
      if (!(await this.supportsTokenPair(l1Token, l2Token))) {
        throw new Error(`token pair not supported by bridge`)
      }

      const token = new Contract(
        toAddress(l1Token),
277
        getContractInterface('OptimismMintableERC20'), // Any ERC20 will do
278 279 280 281 282 283 284 285 286 287
        this.messenger.l1Provider
      )

      return token.populateTransaction.approve(
        this.l1Bridge.address,
        amount,
        opts?.overrides || {}
      )
    },

288 289 290 291 292
    deposit: async (
      l1Token: AddressLike,
      l2Token: AddressLike,
      amount: NumberLike,
      opts?: {
293
        recipient?: AddressLike
294 295 296 297 298 299 300 301
        l2GasLimit?: NumberLike
        overrides?: Overrides
      }
    ): Promise<TransactionRequest> => {
      if (!(await this.supportsTokenPair(l1Token, l2Token))) {
        throw new Error(`token pair not supported by bridge`)
      }

302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321
      if (opts?.recipient === undefined) {
        return this.l1Bridge.populateTransaction.depositERC20(
          toAddress(l1Token),
          toAddress(l2Token),
          amount,
          opts?.l2GasLimit || 200_000, // Default to 200k gas limit.
          '0x', // No data.
          opts?.overrides || {}
        )
      } else {
        return this.l1Bridge.populateTransaction.depositERC20To(
          toAddress(l1Token),
          toAddress(l2Token),
          toAddress(opts.recipient),
          amount,
          opts?.l2GasLimit || 200_000, // Default to 200k gas limit.
          '0x', // No data.
          opts?.overrides || {}
        )
      }
322 323 324 325 326 327 328
    },

    withdraw: async (
      l1Token: AddressLike,
      l2Token: AddressLike,
      amount: NumberLike,
      opts?: {
329
        recipient?: AddressLike
330 331 332 333 334 335 336
        overrides?: Overrides
      }
    ): Promise<TransactionRequest> => {
      if (!(await this.supportsTokenPair(l1Token, l2Token))) {
        throw new Error(`token pair not supported by bridge`)
      }

337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354
      if (opts?.recipient === undefined) {
        return this.l2Bridge.populateTransaction.withdraw(
          toAddress(l2Token),
          amount,
          0, // L1 gas not required.
          '0x', // No data.
          opts?.overrides || {}
        )
      } else {
        return this.l2Bridge.populateTransaction.withdrawTo(
          toAddress(l2Token),
          toAddress(opts.recipient),
          amount,
          0, // L1 gas not required.
          '0x', // No data.
          opts?.overrides || {}
        )
      }
355 356 357 358
    },
  }

  estimateGas = {
359 360 361 362 363
    approve: async (
      l1Token: AddressLike,
      l2Token: AddressLike,
      amount: NumberLike,
      opts?: {
364
        overrides?: CallOverrides
365 366 367 368 369 370 371
      }
    ): Promise<BigNumber> => {
      return this.messenger.l1Provider.estimateGas(
        await this.populateTransaction.approve(l1Token, l2Token, amount, opts)
      )
    },

372 373 374 375 376
    deposit: async (
      l1Token: AddressLike,
      l2Token: AddressLike,
      amount: NumberLike,
      opts?: {
377
        recipient?: AddressLike
378
        l2GasLimit?: NumberLike
379
        overrides?: CallOverrides
380 381
      }
    ): Promise<BigNumber> => {
382
      return this.messenger.l1Provider.estimateGas(
383 384 385 386 387 388 389 390 391
        await this.populateTransaction.deposit(l1Token, l2Token, amount, opts)
      )
    },

    withdraw: async (
      l1Token: AddressLike,
      l2Token: AddressLike,
      amount: NumberLike,
      opts?: {
392
        recipient?: AddressLike
393
        overrides?: CallOverrides
394 395
      }
    ): Promise<BigNumber> => {
396
      return this.messenger.l2Provider.estimateGas(
397 398 399 400 401
        await this.populateTransaction.withdraw(l1Token, l2Token, amount, opts)
      )
    },
  }
}