• Mark Tyneway's avatar
    sdk: decouple from contracts-bedrock (#10258) · 53afefdd
    Mark Tyneway authored
    * sdk: decouple from contracts-bedrock
    
    Decouples the sdk from `contracts-bedrock` to enable
    it to easily be moved out of the monorepo.
    
    The foundry artifacts are build from 3fc229e3 and then
    copy/pasted into the sdk. There are no large breaking changes
    planned which is why this is safe to do. This sdk has already
    been replaced by viem and in the future a Go sdk can be broken
    out of the `op-e2e` package to serve as the reference implementation
    of a sdk.
    
    The typescript sdk will be subject to just work until new breaking
    changes are introduced but at that point we should have sufficient
    migration to the alternative solutions.
    
    * sdk: remove dead dep
    
    * deps: fix pnpm lock
    53afefdd
contracts.ts 9.88 KB
import { getContractInterface, predeploys } from '@eth-optimism/contracts'
import { ethers, Contract } from 'ethers'

import l1StandardBridge from '../forge-artifacts/L1StandardBridge.json'
import l2StandardBridge from '../forge-artifacts/L2StandardBridge.json'
import optimismMintableERC20 from '../forge-artifacts/OptimismMintableERC20.json'
import optimismPortal from '../forge-artifacts/OptimismPortal.json'
import l1CrossDomainMessenger from '../forge-artifacts/L1CrossDomainMessenger.json'
import l2CrossDomainMessenger from '../forge-artifacts/L2CrossDomainMessenger.json'
import optimismMintableERC20Factory from '../forge-artifacts/OptimismMintableERC20Factory.json'
import proxyAdmin from '../forge-artifacts/ProxyAdmin.json'
import l2OutputOracle from '../forge-artifacts/L2OutputOracle.json'
import l1ERC721Bridge from '../forge-artifacts/L1ERC721Bridge.json'
import l2ERC721Bridge from '../forge-artifacts/L2ERC721Bridge.json'
import l1Block from '../forge-artifacts/L1Block.json'
import l2ToL1MessagePasser from '../forge-artifacts/L2ToL1MessagePasser.json'
import gasPriceOracle from '../forge-artifacts/GasPriceOracle.json'
import disputeGameFactory from '../forge-artifacts/DisputeGameFactory.json'
import optimismPortal2 from '../forge-artifacts/OptimismPortal2.json'
import faultDisputeGame from '../forge-artifacts/FaultDisputeGame.json'
import { toAddress } from './coercion'
import { DeepPartial } from './type-utils'
import { CrossChainMessenger } from '../cross-chain-messenger'
import { StandardBridgeAdapter, ETHBridgeAdapter } from '../adapters'
import {
  CONTRACT_ADDRESSES,
  DEFAULT_L2_CONTRACT_ADDRESSES,
  BRIDGE_ADAPTER_DATA,
  IGNORABLE_CONTRACTS,
} from './chain-constants'
import {
  OEContracts,
  OEL1Contracts,
  OEL2Contracts,
  OEContractsLike,
  AddressLike,
  BridgeAdapters,
  BridgeAdapterData,
} from '../interfaces'

/**
 * We've changed some contract names in this SDK to be a bit nicer. Here we remap these nicer names
 * back to the original contract names so we can look them up.
 */
const NAME_REMAPPING = {
  AddressManager: 'Lib_AddressManager' as const,
  OVM_L1BlockNumber: 'iOVM_L1BlockNumber' as const,
  WETH: 'WETH9' as const,
  BedrockMessagePasser: 'L2ToL1MessagePasser' as const,
}

export const getContractInterfaceBedrock = (
  name: string
): ethers.utils.Interface => {
  let artifact: any = ''
  switch (name) {
    case 'Lib_AddressManager':
    case 'AddressManager':
      artifact = ''
      break
    case 'L1CrossDomainMessenger':
      artifact = l1CrossDomainMessenger
      break
    case 'L1ERC721Bridge':
      artifact = l1ERC721Bridge
      break
    case 'L2OutputOracle':
      artifact = l2OutputOracle
      break
    case 'OptimismMintableERC20Factory':
      artifact = optimismMintableERC20Factory
      break
    case 'ProxyAdmin':
      artifact = proxyAdmin
      break
    case 'L1StandardBridge':
      artifact = l1StandardBridge
      break
    case 'L2StandardBridge':
      artifact = l2StandardBridge
      break
    case 'OptimismPortal':
      artifact = optimismPortal
      break
    case 'L2CrossDomainMessenger':
      artifact = l2CrossDomainMessenger
      break
    case 'OptimismMintableERC20':
      artifact = optimismMintableERC20
      break
    case 'L2ERC721Bridge':
      artifact = l2ERC721Bridge
      break
    case 'L1Block':
      artifact = l1Block
      break
    case 'L2ToL1MessagePasser':
      artifact = l2ToL1MessagePasser
      break
    case 'GasPriceOracle':
      artifact = gasPriceOracle
      break
    case 'DisputeGameFactory':
      artifact = disputeGameFactory
      break
    case 'OptimismPortal2':
      artifact = optimismPortal2
      break
    case 'FaultDisputeGame':
      artifact = faultDisputeGame
      break
  }
  return new ethers.utils.Interface(artifact.abi)
}

/**
 * Returns an ethers.Contract object for the given name, connected to the appropriate address for
 * the given L2 chain ID. Users can also provide a custom address to connect the contract to
 * instead. If the chain ID is not known then the user MUST provide a custom address or this
 * function will throw an error.
 *
 * @param contractName Name of the contract to connect to.
 * @param l2ChainId Chain ID for the L2 network.
 * @param opts Additional options for connecting to the contract.
 * @param opts.address Custom address to connect to the contract.
 * @param opts.signerOrProvider Signer or provider to connect to the contract.
 * @returns An ethers.Contract object connected to the appropriate address and interface.
 */
export const getOEContract = (
  contractName: keyof OEL1Contracts | keyof OEL2Contracts,
  l2ChainId: number,
  opts: {
    address?: AddressLike
    signerOrProvider?: ethers.Signer | ethers.providers.Provider
  } = {}
): Contract => {
  // Generally we want to throw an error if a contract address is not provided but there are some
  // exceptions, particularly for contracts that are part of an upgrade that has not yet been
  // deployed. It's OK to not provide an address for these contracts with the caveat that this may
  // cause a runtime error if the contract does actually need to be used.
  const addresses = CONTRACT_ADDRESSES[l2ChainId]
  if (addresses === undefined && opts.address === undefined) {
    if (IGNORABLE_CONTRACTS.includes(contractName)) {
      return undefined
    } else {
      throw new Error(
        `cannot get contract ${contractName} for unknown L2 chain ID ${l2ChainId}, you must provide an address`
      )
    }
  }

  // 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]
    ),
    iface,
    opts.signerOrProvider
  )
}

/**
 * Automatically connects to all contract addresses, both L1 and L2, for the given L2 chain ID. The
 * user can provide custom contract address overrides for L1 or L2 contracts. If the given chain ID
 * is not known then the user MUST provide custom contract addresses for ALL L1 contracts or this
 * function will throw an error.
 *
 * @param l2ChainId Chain ID for the L2 network.
 * @param opts Additional options for connecting to the contracts.
 * @param opts.l1SignerOrProvider: Signer or provider to connect to the L1 contracts.
 * @param opts.l2SignerOrProvider: Signer or provider to connect to the L2 contracts.
 * @param opts.overrides Custom contract address overrides for L1 or L2 contracts.
 * @returns An object containing ethers.Contract objects connected to the appropriate addresses on
 * both L1 and L2.
 */
export const getAllOEContracts = (
  l2ChainId: number,
  opts: {
    l1SignerOrProvider?: ethers.Signer | ethers.providers.Provider
    l2SignerOrProvider?: ethers.Signer | ethers.providers.Provider
    overrides?: DeepPartial<OEContractsLike>
  } = {}
): OEContracts => {
  const addresses = CONTRACT_ADDRESSES[l2ChainId] || {
    l1: {
      AddressManager: undefined,
      L1CrossDomainMessenger: undefined,
      L1StandardBridge: undefined,
      StateCommitmentChain: undefined,
      CanonicalTransactionChain: undefined,
      BondManager: undefined,
      OptimismPortal: undefined,
      L2OutputOracle: undefined,
      DisputeGameFactory: undefined,
      OptimismPortal2: undefined,
    },
    l2: DEFAULT_L2_CONTRACT_ADDRESSES,
  }

  // Attach all L1 contracts.
  const l1Contracts = {} as OEL1Contracts
  for (const [contractName, contractAddress] of Object.entries(addresses.l1)) {
    l1Contracts[contractName] = getOEContract(
      contractName as keyof OEL1Contracts,
      l2ChainId,
      {
        address: opts.overrides?.l1?.[contractName] || contractAddress,
        signerOrProvider: opts.l1SignerOrProvider,
      }
    )
  }

  // Attach all L2 contracts.
  const l2Contracts = {} as OEL2Contracts
  for (const [contractName, contractAddress] of Object.entries(addresses.l2)) {
    l2Contracts[contractName] = getOEContract(
      contractName as keyof OEL2Contracts,
      l2ChainId,
      {
        address: opts.overrides?.l2?.[contractName] || contractAddress,
        signerOrProvider: opts.l2SignerOrProvider,
      }
    )
  }

  return {
    l1: l1Contracts,
    l2: l2Contracts,
  }
}

/**
 * Gets a series of bridge adapters for the given L2 chain ID.
 *
 * @param l2ChainId Chain ID for the L2 network.
 * @param messenger Cross chain messenger to connect to the bridge adapters
 * @param opts Additional options for connecting to the custom bridges.
 * @param opts.overrides Custom bridge adapters.
 * @returns An object containing all bridge adapters
 */
export const getBridgeAdapters = (
  l2ChainId: number,
  messenger: CrossChainMessenger,
  opts?: {
    overrides?: BridgeAdapterData
    contracts?: DeepPartial<OEContractsLike>
  }
): BridgeAdapters => {
  const adapterData: BridgeAdapterData = {
    ...(CONTRACT_ADDRESSES[l2ChainId] || opts?.contracts?.l1?.L1StandardBridge
      ? {
          Standard: {
            Adapter: StandardBridgeAdapter,
            l1Bridge:
              opts?.contracts?.l1?.L1StandardBridge ||
              CONTRACT_ADDRESSES[l2ChainId].l1.L1StandardBridge,
            l2Bridge: predeploys.L2StandardBridge,
          },
          ETH: {
            Adapter: ETHBridgeAdapter,
            l1Bridge:
              opts?.contracts?.l1?.L1StandardBridge ||
              CONTRACT_ADDRESSES[l2ChainId].l1.L1StandardBridge,
            l2Bridge: predeploys.L2StandardBridge,
          },
        }
      : {}),
    ...(BRIDGE_ADAPTER_DATA[l2ChainId] || {}),
    ...(opts?.overrides || {}),
  }

  const adapters: BridgeAdapters = {}
  for (const [bridgeName, bridgeData] of Object.entries(adapterData)) {
    adapters[bridgeName] = new bridgeData.Adapter({
      messenger,
      l1Bridge: bridgeData.l1Bridge,
      l2Bridge: bridgeData.l2Bridge,
    })
  }

  return adapters
}