Repository: ethereum-optimism/optimism
Pull request #7206 (2023-09-12 19:04:59)

feat(indexer): explicity state all contracts of interest for L1/L2 prior to launch
Even though the primary use case is bridging, we list out all optimism contracts
that emit events for the indexer to process.

Until we have backfill support, it's important to hardcode these lists. Any addition
of a new contract will require resync until the we can dynamically backfill new contracts.
For this reason, we explicitly configure `L1Contracts` and `L2Contracts` that must be fully
specified, otherwise error out



Patch:
diff --git a/indexer/config/config.go b/indexer/config/config.go
index 69eb82284..d370c0eda 100644
--- a/indexer/config/config.go
+++ b/indexer/config/config.go
@@ -6,6 +6,7 @@ import (
 	"reflect"
 
 	"github.com/BurntSushi/toml"
+	"github.com/ethereum-optimism/optimism/op-bindings/predeploys"
 	"github.com/ethereum/go-ethereum/common"
 	"github.com/ethereum/go-ethereum/log"
 )
@@ -16,7 +17,7 @@ const (
 	defaultHeaderBufferSize = 500
 )
 
-// in future presets can just be onchain config and fetched on initialization
+// In the future, presets can just be onchain config and fetched on initialization
 
 // Config represents the `indexer.toml` file used to configure the indexer
 type Config struct {
@@ -27,50 +28,80 @@ type Config struct {
 	MetricsServer ServerConfig `toml:"metrics"`
 }
 
-// fetch this via onchain config from RPCsConfig and remove from config in future
+// L1Contracts configures deployed contracts
 type L1Contracts struct {
-	OptimismPortalProxy         common.Address `toml:"optimism-portal"`
-	L2OutputOracleProxy         common.Address `toml:"l2-output-oracle"`
+	// administrative
+	AddressManager    common.Address `toml:"address-manager"`
+	SystemConfigProxy common.Address `toml:"system-config"`
+
+	// rollup state
+	OptimismPortalProxy common.Address `toml:"optimism-portal"`
+	L2OutputOracleProxy common.Address `toml:"l2-output-oracle"`
+
+	// bridging
 	L1CrossDomainMessengerProxy common.Address `toml:"l1-cross-domain-messenger"`
 	L1StandardBridgeProxy       common.Address `toml:"l1-standard-bridge"`
+	L1ERC721BridgeProxy         common.Address `toml:"l1-erc721-bridge"`
+
+	// IGNORE: legacy contracts (only settable via presets)
+	LegacyCanonicalTransactionChain common.Address `toml:"-"`
+	LegacyStateCommitmentChain      common.Address `toml:"-"`
+}
 
-	// Pre-Bedrock Legacy Contracts
-	LegacyCanonicalTransactionChain common.Address `toml:"l1-canonical-transaction-chain"`
+func (c L1Contracts) ForEach(cb func(string, common.Address) error) error {
+	contracts := reflect.ValueOf(c)
+	fields := reflect.VisibleFields(reflect.TypeOf(c))
+	for _, field := range fields {
+		// ruleid: unsafe-reflect-by-name
+		addr := (contracts.FieldByName(field.Name).Interface()).(common.Address)
+		if err := cb(field.Name, addr); err != nil {
+			return err
+		}
+	}
 
-	// Some more contracts -- L1ERC721Bridge, ProxyAdmin, SystemConfig, etc
-	// Ignore the auxiliary contracts?
+	return nil
+}
 
-	// Legacy contracts? We'll add this in to index the legacy chain.
-	// Remove afterwards?
+// L2Contracts configures core predeploy contracts. We explicitly specify
+// fields until we can detect and backfill new addresses
+type L2Contracts struct {
+	L2ToL1MessagePasser    common.Address
+	L2CrossDomainMessenger common.Address
+	L2StandardBridge       common.Address
+	L2ERC721Bridge         common.Address
 }
 
-// converts struct of to a slice of addresses for easy iteration
-// also validates that all fields are addresses
-func (c *L1Contracts) AsSlice() ([]common.Address, error) {
-	clone := *c
-	contractValue := reflect.ValueOf(clone)
-	fields := reflect.VisibleFields(reflect.TypeOf(clone))
-	l1Contracts := make([]common.Address, len(fields))
-	for i, field := range fields {
+func L2ContractsFromPredeploys() L2Contracts {
+	return L2Contracts{
+		L2ToL1MessagePasser:    predeploys.L2ToL1MessagePasserAddr,
+		L2CrossDomainMessenger: predeploys.L2CrossDomainMessengerAddr,
+		L2StandardBridge:       predeploys.L2StandardBridgeAddr,
+		L2ERC721Bridge:         predeploys.L2ERC721BridgeAddr,
+	}
+}
+
+func (c L2Contracts) ForEach(cb func(string, common.Address) error) error {
+	contracts := reflect.ValueOf(c)
+	fields := reflect.VisibleFields(reflect.TypeOf(c))
+	for _, field := range fields {
 		// ruleid: unsafe-reflect-by-name
-		addr, ok := (contractValue.FieldByName(field.Name).Interface()).(common.Address)
-		if !ok {
-			return nil, fmt.Errorf("non-address found in L1Contracts: %s", field.Name)
+		addr := (contracts.FieldByName(field.Name).Interface()).(common.Address)
+		if err := cb(field.Name, addr); err != nil {
+			return err
 		}
-
-		l1Contracts[i] = addr
 	}
 
-	return l1Contracts, nil
+	return nil
 }
 
 // ChainConfig configures of the chain being indexed
 type ChainConfig struct {
 	// Configure known chains with the l2 chain id
-	Preset int
+	Preset           int
+	L1StartingHeight uint `toml:"l1-starting-height"`
 
-	L1Contracts      L1Contracts `toml:"l1-contracts"`
-	L1StartingHeight uint        `toml:"l1-starting-height"`
+	L1Contracts L1Contracts `toml:"l1-contracts"`
+	L2Contracts L2Contracts `toml:"-"`
 
 	// Bedrock starting heights only applicable for OP-Mainnet & OP-Goerli
 	L1BedrockStartingHeight uint `toml:"-"`
@@ -127,18 +158,24 @@ func LoadConfig(log log.Logger, path string) (Config, error) {
 	}
 
 	if conf.Chain.Preset != 0 {
-		knownPreset, ok := presetConfigs[conf.Chain.Preset]
+		preset, ok := Presets[conf.Chain.Preset]
 		if !ok {
 			return conf, fmt.Errorf("unknown preset: %d", conf.Chain.Preset)
 		}
 
-		conf.Chain.L1Contracts = knownPreset.L1Contracts
-		conf.Chain.L1StartingHeight = knownPreset.L1StartingHeight
-		conf.Chain.L1BedrockStartingHeight = knownPreset.L1BedrockStartingHeight
-		conf.Chain.L2BedrockStartingHeight = knownPreset.L1BedrockStartingHeight
+		log.Info("detected preset", "preset", conf.Chain.Preset, "name", preset.Name)
+		log.Info("setting L1 information from preset")
+		conf.Chain.L1Contracts = preset.ChainConfig.L1Contracts
+		conf.Chain.L1StartingHeight = preset.ChainConfig.L1StartingHeight
+		conf.Chain.L1BedrockStartingHeight = preset.ChainConfig.L1BedrockStartingHeight
+		conf.Chain.L2BedrockStartingHeight = preset.ChainConfig.L1BedrockStartingHeight
 	}
 
-	// Set polling defaults if not set
+	// Setup L2Contracts from predeploys
+	conf.Chain.L2Contracts = L2ContractsFromPredeploys()
+
+	// Setup defaults for some unset options
+
 	if conf.Chain.L1PollingInterval == 0 {
 		log.Info("setting default L1 polling interval", "interval", defaultLoopInterval)
 		conf.Chain.L1PollingInterval = defaultLoopInterval
diff --git a/indexer/config/presets.go b/indexer/config/presets.go
index c4acec6fd..a455b8e85 100644
--- a/indexer/config/presets.go
+++ b/indexer/config/presets.go
@@ -4,73 +4,115 @@ import (
 	"github.com/ethereum/go-ethereum/common"
 )
 
-// in future presets can just be onchain config and fetched on initialization
-// Mapping of l2 chain ids to their preset chain configurations
-var presetConfigs = map[int]ChainConfig{
-	// OP Mainnet
+type Preset struct {
+	Name        string
+	ChainConfig ChainConfig
+}
+
+// In the future, presets can just be onchain config and fetched on initialization
+
+// Mapping of L2 chain ids to their preset chain configurations
+var Presets = map[int]Preset{
 	10: {
-		L1Contracts: L1Contracts{
-			OptimismPortalProxy:             common.HexToAddress("0xbEb5Fc579115071764c7423A4f12eDde41f106Ed"),
-			L2OutputOracleProxy:             common.HexToAddress("0xdfe97868233d1aa22e815a266982f2cf17685a27"),
-			L1CrossDomainMessengerProxy:     common.HexToAddress("0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1"),
-			L1StandardBridgeProxy:           common.HexToAddress("0x99C9fc46f92E8a1c0deC1b1747d010903E884bE1"),
-			LegacyCanonicalTransactionChain: common.HexToAddress("0x5e4e65926ba27467555eb562121fac00d24e9dd2"),
+		Name: "Optimism",
+		ChainConfig: ChainConfig{
+			L1Contracts: L1Contracts{
+				AddressManager:              common.HexToAddress("0xdE1FCfB0851916CA5101820A69b13a4E276bd81F"),
+				SystemConfigProxy:           common.HexToAddress("0x229047fed2591dbec1eF1118d64F7aF3dB9EB290"),
+				OptimismPortalProxy:         common.HexToAddress("0xbEb5Fc579115071764c7423A4f12eDde41f106Ed"),
+				L2OutputOracleProxy:         common.HexToAddress("0xdfe97868233d1aa22e815a266982f2cf17685a27"),
+				L1CrossDomainMessengerProxy: common.HexToAddress("0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1"),
+				L1StandardBridgeProxy:       common.HexToAddress("0x99C9fc46f92E8a1c0deC1b1747d010903E884bE1"),
+				L1ERC721BridgeProxy:         common.HexToAddress("0x5a7749f83b81B301cAb5f48EB8516B986DAef23D"),
+
+				// pre-bedrock
+				LegacyCanonicalTransactionChain: common.HexToAddress("0x5e4e65926ba27467555eb562121fac00d24e9dd2"),
+				LegacyStateCommitmentChain:      common.HexToAddress("0xBe5dAb4A2e9cd0F27300dB4aB94BeE3A233AEB19"),
+			},
+			L1StartingHeight:        13596466,
+			L1BedrockStartingHeight: 17422590,
+			L2BedrockStartingHeight: 105235063,
 		},
-		L1StartingHeight:        13596466,
-		L1BedrockStartingHeight: 17422590,
-		L2BedrockStartingHeight: 105235063,
 	},
-	// OP Goerli
 	420: {
-		L1Contracts: L1Contracts{
-			OptimismPortalProxy:             common.HexToAddress("0x5b47E1A08Ea6d985D6649300584e6722Ec4B1383"),
-			L2OutputOracleProxy:             common.HexToAddress("0xE6Dfba0953616Bacab0c9A8ecb3a9BBa77FC15c0"),
-			L1CrossDomainMessengerProxy:     common.HexToAddress("0x5086d1eEF304eb5284A0f6720f79403b4e9bE294"),
-			L1StandardBridgeProxy:           common.HexToAddress("0x636Af16bf2f682dD3109e60102b8E1A089FedAa8"),
-			LegacyCanonicalTransactionChain: common.HexToAddress("0x607F755149cFEB3a14E1Dc3A4E2450Cde7dfb04D"),
+		Name: "Optimism Goerli",
+		ChainConfig: ChainConfig{
+			L1Contracts: L1Contracts{
+				AddressManager:              common.HexToAddress("0xa6f73589243a6A7a9023b1Fa0651b1d89c177111"),
+				SystemConfigProxy:           common.HexToAddress("0xAe851f927Ee40dE99aaBb7461C00f9622ab91d60"),
+				OptimismPortalProxy:         common.HexToAddress("0x5b47E1A08Ea6d985D6649300584e6722Ec4B1383"),
+				L2OutputOracleProxy:         common.HexToAddress("0xE6Dfba0953616Bacab0c9A8ecb3a9BBa77FC15c0"),
+				L1CrossDomainMessengerProxy: common.HexToAddress("0x5086d1eEF304eb5284A0f6720f79403b4e9bE294"),
+				L1StandardBridgeProxy:       common.HexToAddress("0x636Af16bf2f682dD3109e60102b8E1A089FedAa8"),
+				L1ERC721BridgeProxy:         common.HexToAddress("0x8DD330DdE8D9898d43b4dc840Da27A07dF91b3c9"),
+
+				// pre-bedrock
+				LegacyCanonicalTransactionChain: common.HexToAddress("0x607F755149cFEB3a14E1Dc3A4E2450Cde7dfb04D"),
+				LegacyStateCommitmentChain:      common.HexToAddress("0x9c945aC97Baf48cB784AbBB61399beB71aF7A378"),
+			},
+			L1StartingHeight:        7017096,
+			L1BedrockStartingHeight: 8300214,
+			L2BedrockStartingHeight: 4061224,
 		},
-		L1StartingHeight:        7017096,
-		L1BedrockStartingHeight: 8300214,
-		L2BedrockStartingHeight: 4061224,
 	},
-	// Base Mainnet
 	8453: {
-		L1Contracts: L1Contracts{
-			OptimismPortalProxy:         common.HexToAddress("0x49048044D57e1C92A77f79988d21Fa8fAF74E97e"),
-			L2OutputOracleProxy:         common.HexToAddress("0x56315b90c40730925ec5485cf004d835058518A0"),
-			L1CrossDomainMessengerProxy: common.HexToAddress("0x866E82a600A1414e583f7F13623F1aC5d58b0Afa"),
-			L1StandardBridgeProxy:       common.HexToAddress("0x3154Cf16ccdb4C6d922629664174b904d80F2C35"),
+		Name: "Base",
+		ChainConfig: ChainConfig{
+			L1Contracts: L1Contracts{
+				AddressManager:              common.HexToAddress("0x8EfB6B5c4767B09Dc9AA6Af4eAA89F749522BaE2"),
+				SystemConfigProxy:           common.HexToAddress("0x73a79Fab69143498Ed3712e519A88a918e1f4072"),
+				OptimismPortalProxy:         common.HexToAddress("0x49048044D57e1C92A77f79988d21Fa8fAF74E97e"),
+				L2OutputOracleProxy:         common.HexToAddress("0x56315b90c40730925ec5485cf004d835058518A0"),
+				L1CrossDomainMessengerProxy: common.HexToAddress("0x866E82a600A1414e583f7F13623F1aC5d58b0Afa"),
+				L1StandardBridgeProxy:       common.HexToAddress("0x3154Cf16ccdb4C6d922629664174b904d80F2C35"),
+				L1ERC721BridgeProxy:         common.HexToAddress("0x608d94945A64503E642E6370Ec598e519a2C1E53"),
+			},
+			L1StartingHeight: 17481768,
 		},
-		L1StartingHeight: 17481768,
 	},
-	// Base Goerli
 	84531: {
-		L1Contracts: L1Contracts{
-			OptimismPortalProxy:         common.HexToAddress("0xe93c8cD0D409341205A592f8c4Ac1A5fe5585cfA"),
-			L2OutputOracleProxy:         common.HexToAddress("0x2A35891ff30313CcFa6CE88dcf3858bb075A2298"),
-			L1CrossDomainMessengerProxy: common.HexToAddress("0x8e5693140eA606bcEB98761d9beB1BC87383706D"),
-			L1StandardBridgeProxy:       common.HexToAddress("0xfA6D8Ee5BE770F84FC001D098C4bD604Fe01284a"),
+		Name: "Base Goerli",
+		ChainConfig: ChainConfig{
+			L1Contracts: L1Contracts{
+				AddressManager:              common.HexToAddress("0x4Cf6b56b14c6CFcB72A75611080514F94624c54e"),
+				SystemConfigProxy:           common.HexToAddress("0xb15eea247eCE011C68a614e4a77AD648ff495bc1"),
+				OptimismPortalProxy:         common.HexToAddress("0xe93c8cD0D409341205A592f8c4Ac1A5fe5585cfA"),
+				L2OutputOracleProxy:         common.HexToAddress("0x2A35891ff30313CcFa6CE88dcf3858bb075A2298"),
+				L1CrossDomainMessengerProxy: common.HexToAddress("0x8e5693140eA606bcEB98761d9beB1BC87383706D"),
+				L1StandardBridgeProxy:       common.HexToAddress("0xfA6D8Ee5BE770F84FC001D098C4bD604Fe01284a"),
+				L1ERC721BridgeProxy:         common.HexToAddress("0x5E0c967457347D5175bF82E8CCCC6480FCD7e568"),
+			},
+			L1StartingHeight: 8410981,
 		},
-		L1StartingHeight: 8410981,
 	},
-	// Zora mainnet
 	7777777: {
-		L1Contracts: L1Contracts{
-			OptimismPortalProxy:         common.HexToAddress("0x1a0ad011913A150f69f6A19DF447A0CfD9551054"),
-			L2OutputOracleProxy:         common.HexToAddress("0x9E6204F750cD866b299594e2aC9eA824E2e5f95c"),
-			L1CrossDomainMessengerProxy: common.HexToAddress("0xdC40a14d9abd6F410226f1E6de71aE03441ca506"),
-			L1StandardBridgeProxy:       common.HexToAddress("0x3e2Ea9B92B7E48A52296fD261dc26fd995284631"),
+		Name: "Zora",
+		ChainConfig: ChainConfig{
+			L1Contracts: L1Contracts{
+				AddressManager:              common.HexToAddress("0xEF8115F2733fb2033a7c756402Fc1deaa56550Ef"),
+				SystemConfigProxy:           common.HexToAddress("0xA3cAB0126d5F504B071b81a3e8A2BBBF17930d86"),
+				OptimismPortalProxy:         common.HexToAddress("0x1a0ad011913A150f69f6A19DF447A0CfD9551054"),
+				L2OutputOracleProxy:         common.HexToAddress("0x9E6204F750cD866b299594e2aC9eA824E2e5f95c"),
+				L1CrossDomainMessengerProxy: common.HexToAddress("0xdC40a14d9abd6F410226f1E6de71aE03441ca506"),
+				L1StandardBridgeProxy:       common.HexToAddress("0x3e2Ea9B92B7E48A52296fD261dc26fd995284631"),
+				L1ERC721BridgeProxy:         common.HexToAddress("0x83A4521A3573Ca87f3a971B169C5A0E1d34481c3"),
+			},
+			L1StartingHeight: 17473923,
 		},
-		L1StartingHeight: 17473923,
 	},
-	// Zora goerli
 	999: {
-		L1Contracts: L1Contracts{
-			OptimismPortalProxy:         common.HexToAddress("0xDb9F51790365e7dc196e7D072728df39Be958ACe"),
-			L2OutputOracleProxy:         common.HexToAddress("0xdD292C9eEd00f6A32Ff5245d0BCd7f2a15f24e00"),
-			L1CrossDomainMessengerProxy: common.HexToAddress("0xD87342e16352D33170557A7dA1e5fB966a60FafC"),
-			L1StandardBridgeProxy:       common.HexToAddress("0x7CC09AC2452D6555d5e0C213Ab9E2d44eFbFc956"),
+		Name: "Zora Goerli",
+		ChainConfig: ChainConfig{
+			L1Contracts: L1Contracts{
+				AddressManager:              common.HexToAddress("0x54f4676203dEDA6C08E0D40557A119c602bFA246"),
+				SystemConfigProxy:           common.HexToAddress("0xF66C9A5E4fE1A8a9bc44a4aF80505a4C3620Ee64"),
+				OptimismPortalProxy:         common.HexToAddress("0xDb9F51790365e7dc196e7D072728df39Be958ACe"),
+				L2OutputOracleProxy:         common.HexToAddress("0xdD292C9eEd00f6A32Ff5245d0BCd7f2a15f24e00"),
+				L1CrossDomainMessengerProxy: common.HexToAddress("0xD87342e16352D33170557A7dA1e5fB966a60FafC"),
+				L1StandardBridgeProxy:       common.HexToAddress("0x7CC09AC2452D6555d5e0C213Ab9E2d44eFbFc956"),
+				L1ERC721BridgeProxy:         common.HexToAddress("0x57C1C6b596ce90C0e010c358DD4Aa052404bB70F"),
+			},
+			L1StartingHeight: 8942381,
 		},
-		L1StartingHeight: 8942381,
 	},
 }
diff --git a/indexer/etl/l1_etl.go b/indexer/etl/l1_etl.go
index 1ab44de6f..a4613928b 100644
--- a/indexer/etl/l1_etl.go
+++ b/indexer/etl/l1_etl.go
@@ -2,7 +2,9 @@ package etl
 
 import (
 	"context"
+	"errors"
 	"fmt"
+	"strings"
 	"sync"
 	"time"
 
@@ -10,6 +12,7 @@ import (
 	"github.com/ethereum-optimism/optimism/indexer/database"
 	"github.com/ethereum-optimism/optimism/indexer/node"
 	"github.com/ethereum-optimism/optimism/op-service/retry"
+	"github.com/ethereum/go-ethereum/common"
 	"github.com/ethereum/go-ethereum/core/types"
 	"github.com/ethereum/go-ethereum/log"
 )
@@ -27,12 +30,25 @@ type L1ETL struct {
 func NewL1ETL(cfg Config, log log.Logger, db *database.DB, metrics Metricer, client node.EthClient, contracts config.L1Contracts) (*L1ETL, error) {
 	log = log.New("etl", "l1")
 
-	latestHeader, err := db.Blocks.L1LatestBlockHeader()
-	if err != nil {
+	zeroAddr := common.Address{}
+	l1Contracts := []common.Address{}
+	if err := contracts.ForEach(func(name string, addr common.Address) error {
+		// Since we dont have backfill support yet, we want to make sure all expected
+		// contracts are specified to ensure consistent behavior. Once backfill support
+		// is ready, we can relax this requirement.
+		if addr == zeroAddr && !strings.HasPrefix(name, "Legacy") {
+			log.Error("address not configured", "name", name)
+			return errors.New("all L1Contracts must be configured")
+		}
+
+		log.Info("configured contract", "name", name, "addr", addr)
+		l1Contracts = append(l1Contracts, addr)
+		return nil
+	}); err != nil {
 		return nil, err
 	}
 
-	cSlice, err := contracts.AsSlice()
+	latestHeader, err := db.Blocks.L1LatestBlockHeader()
 	if err != nil {
 		return nil, err
 	}
@@ -42,7 +58,6 @@ func NewL1ETL(cfg Config, log log.Logger, db *database.DB, metrics Metricer, cli
 	if latestHeader != nil {
 		log.Info("detected last indexed block", "number", latestHeader.Number, "hash", latestHeader.Hash)
 		fromHeader = latestHeader.RLPHeader.Header()
-
 	} else if cfg.StartHeight.BitLen() > 0 {
 		log.Info("no indexed state starting from supplied L1 height", "height", cfg.StartHeight.String())
 		header, err := client.BlockHeaderByNumber(cfg.StartHeight)
@@ -51,7 +66,6 @@ func NewL1ETL(cfg Config, log log.Logger, db *database.DB, metrics Metricer, cli
 		}
 
 		fromHeader = header
-
 	} else {
 		log.Info("no indexed state, starting from genesis")
 	}
@@ -66,7 +80,7 @@ func NewL1ETL(cfg Config, log log.Logger, db *database.DB, metrics Metricer, cli
 		log:             log,
 		metrics:         metrics,
 		headerTraversal: node.NewHeaderTraversal(client, fromHeader, cfg.ConfirmationDepth),
-		contracts:       cSlice,
+		contracts:       l1Contracts,
 		etlBatches:      etlBatches,
 
 		EthClient: client,
diff --git a/indexer/etl/l2_etl.go b/indexer/etl/l2_etl.go
index ce2559e5d..72dfff8c7 100644
--- a/indexer/etl/l2_etl.go
+++ b/indexer/etl/l2_etl.go
@@ -2,11 +2,12 @@ package etl
 
 import (
 	"context"
+	"errors"
 	"time"
 
+	"github.com/ethereum-optimism/optimism/indexer/config"
 	"github.com/ethereum-optimism/optimism/indexer/database"
 	"github.com/ethereum-optimism/optimism/indexer/node"
-	"github.com/ethereum-optimism/optimism/op-bindings/predeploys"
 	"github.com/ethereum-optimism/optimism/op-service/retry"
 	"github.com/ethereum/go-ethereum/common"
 	"github.com/ethereum/go-ethereum/core/types"
@@ -19,14 +20,25 @@ type L2ETL struct {
 	db *database.DB
 }
 
-func NewL2ETL(cfg Config, log log.Logger, db *database.DB, metrics Metricer, client node.EthClient) (*L2ETL, error) {
+func NewL2ETL(cfg Config, log log.Logger, db *database.DB, metrics Metricer, client node.EthClient, contracts config.L2Contracts) (*L2ETL, error) {
 	log = log.New("etl", "l2")
 
-	// allow predeploys to be overridable
+	zeroAddr := common.Address{}
 	l2Contracts := []common.Address{}
-	for name, addr := range predeploys.Predeploys {
+	if err := contracts.ForEach(func(name string, addr common.Address) error {
+		// Since we dont have backfill support yet, we want to make sure all expected
+		// contracts are specified to ensure consistent behavior. Once backfill support
+		// is ready, we can relax this requirement.
+		if addr == zeroAddr {
+			log.Error("address not configured", "name", name)
+			return errors.New("all L2Contracts must be configured")
+		}
+
 		log.Info("configured contract", "name", name, "addr", addr)
-		l2Contracts = append(l2Contracts, *addr)
+		l2Contracts = append(l2Contracts, addr)
+		return nil
+	}); err != nil {
+		return nil, err
 	}
 
 	latestHeader, err := db.Blocks.L2LatestBlockHeader()
diff --git a/indexer/indexer.go b/indexer/indexer.go
index 8f652fc5d..f3718ebbd 100644
--- a/indexer/indexer.go
+++ b/indexer/indexer.go
@@ -76,7 +76,7 @@ func NewIndexer(
 		HeaderBufferSize:  chainConfig.L2HeaderBufferSize,
 		ConfirmationDepth: big.NewInt(int64(chainConfig.L2ConfirmationDepth)),
 	}
-	l2Etl, err := etl.NewL2ETL(l2Cfg, log, db, etl.NewMetrics(metricsRegistry, "l2"), l2EthClient)
+	l2Etl, err := etl.NewL2ETL(l2Cfg, log, db, etl.NewMetrics(metricsRegistry, "l2"), l2EthClient, chainConfig.L2Contracts)
 	if err != nil {
 		return nil, err
 	}
diff --git a/indexer/migrations/20230523_create_schema.sql b/indexer/migrations/20230523_create_schema.sql
index e205db9f2..86c67a8ce 100644
--- a/indexer/migrations/20230523_create_schema.sql
+++ b/indexer/migrations/20230523_create_schema.sql
@@ -57,6 +57,7 @@ CREATE TABLE IF NOT EXISTS l1_contract_events (
 CREATE INDEX IF NOT EXISTS l1_contract_events_timestamp ON l1_contract_events(timestamp);
 CREATE INDEX IF NOT EXISTS l1_contract_events_block_hash ON l1_contract_events(block_hash);
 CREATE INDEX IF NOT EXISTS l1_contract_events_event_signature ON l1_contract_events(event_signature);
+CREATE INDEX IF NOT EXISTS l1_contract_events_contract_address ON l1_contract_events(contract_address);
 
 CREATE TABLE IF NOT EXISTS l2_contract_events (
     -- Searchable fields
@@ -74,6 +75,7 @@ CREATE TABLE IF NOT EXISTS l2_contract_events (
 CREATE INDEX IF NOT EXISTS l2_contract_events_timestamp ON l2_contract_events(timestamp);
 CREATE INDEX IF NOT EXISTS l2_contract_events_block_hash ON l2_contract_events(block_hash);
 CREATE INDEX IF NOT EXISTS l2_contract_events_event_signature ON l2_contract_events(event_signature);
+CREATE INDEX IF NOT EXISTS l2_contract_events_contract_address ON l2_contract_events(contract_address);
 
 -- Tables that index finalization markers for L2 blocks.
 
diff --git a/indexer/processors/bridge.go b/indexer/processors/bridge.go
index 992928498..07d11bdda 100644
--- a/indexer/processors/bridge.go
+++ b/indexer/processors/bridge.go
@@ -155,14 +155,14 @@ func (b *BridgeProcessor) Start(ctx context.Context) error {
 				for _, group := range l1BlockGroups {
 					log := l1BridgeLog.New("from_l1_block_number", group.Start, "to_l1_block_number", group.End)
 					log.Info("scanning for initiated bridge events")
-					if err := bridge.LegacyL1ProcessInitiatedBridgeEvents(log, tx, b.chainConfig, group.Start, group.End); err != nil {
+					if err := bridge.LegacyL1ProcessInitiatedBridgeEvents(log, tx, b.chainConfig.L1Contracts, group.Start, group.End); err != nil {
 						return err
 					}
 				}
 				for _, group := range l2BlockGroups {
 					log := l2BridgeLog.New("from_l2_block_number", group.Start, "to_l2_block_number", group.End)
 					log.Info("scanning for initiated bridge events")
-					if err := bridge.LegacyL2ProcessInitiatedBridgeEvents(log, tx, group.Start, group.End); err != nil {
+					if err := bridge.LegacyL2ProcessInitiatedBridgeEvents(log, tx, b.chainConfig.L2Contracts, group.Start, group.End); err != nil {
 						return err
 					}
 				}
@@ -171,14 +171,14 @@ func (b *BridgeProcessor) Start(ctx context.Context) error {
 				for _, group := range l1BlockGroups {
 					log := l1BridgeLog.New("from_l1_block_number", group.Start, "to_l1_block_number", group.End)
 					log.Info("scanning for finalized bridge events")
-					if err := bridge.LegacyL1ProcessFinalizedBridgeEvents(log, tx, b.l1Etl.EthClient, b.chainConfig, group.Start, group.End); err != nil {
+					if err := bridge.LegacyL1ProcessFinalizedBridgeEvents(log, tx, b.l1Etl.EthClient, b.chainConfig.L1Contracts, group.Start, group.End); err != nil {
 						return err
 					}
 				}
 				for _, group := range l2BlockGroups {
 					log := l2BridgeLog.New("from_l2_block_number", group.Start, "to_l2_block_number", group.End)
 					log.Info("scanning for finalized bridge events")
-					if err := bridge.LegacyL2ProcessFinalizedBridgeEvents(log, tx, group.Start, group.End); err != nil {
+					if err := bridge.LegacyL2ProcessFinalizedBridgeEvents(log, tx, b.chainConfig.L2Contracts, group.Start, group.End); err != nil {
 						return err
 					}
 				}
@@ -199,14 +199,14 @@ func (b *BridgeProcessor) Start(ctx context.Context) error {
 			for _, group := range l1BlockGroups {
 				log := l1BridgeLog.New("from_block_number", group.Start, "to_block_number", group.End)
 				log.Info("scanning for initiated bridge events")
-				if err := bridge.L1ProcessInitiatedBridgeEvents(log, tx, b.chainConfig, group.Start, group.End); err != nil {
+				if err := bridge.L1ProcessInitiatedBridgeEvents(log, tx, b.chainConfig.L1Contracts, group.Start, group.End); err != nil {
 					return err
 				}
 			}
 			for _, group := range l2BlockGroups {
 				log := l2BridgeLog.New("from_block_number", group.Start, "to_block_number", group.End)
 				log.Info("scanning for initiated bridge events")
-				if err := bridge.L2ProcessInitiatedBridgeEvents(log, tx, group.Start, group.End); err != nil {
+				if err := bridge.L2ProcessInitiatedBridgeEvents(log, tx, b.chainConfig.L2Contracts, group.Start, group.End); err != nil {
 					return err
 				}
 			}
@@ -215,14 +215,14 @@ func (b *BridgeProcessor) Start(ctx context.Context) error {
 			for _, group := range l1BlockGroups {
 				log := l1BridgeLog.New("from_block_number", group.Start, "to_block_number", group.End)
 				log.Info("scanning for finalized bridge events")
-				if err := bridge.L1ProcessFinalizedBridgeEvents(log, tx, b.chainConfig, group.Start, group.End); err != nil {
+				if err := bridge.L1ProcessFinalizedBridgeEvents(log, tx, b.chainConfig.L1Contracts, group.Start, group.End); err != nil {
 					return err
 				}
 			}
 			for _, group := range l2BlockGroups {
 				log := l2BridgeLog.New("from_block_number", group.Start, "to_block_number", group.End)
 				log.Info("scanning for finalized bridge events")
-				if err := bridge.L2ProcessFinalizedBridgeEvents(log, tx, group.Start, group.End); err != nil {
+				if err := bridge.L2ProcessFinalizedBridgeEvents(log, tx, b.chainConfig.L2Contracts, group.Start, group.End); err != nil {
 					return err
 				}
 			}
diff --git a/indexer/processors/bridge/l1_bridge_processor.go b/indexer/processors/bridge/l1_bridge_processor.go
index a35611fad..51baf9d6f 100644
--- a/indexer/processors/bridge/l1_bridge_processor.go
+++ b/indexer/processors/bridge/l1_bridge_processor.go
@@ -17,9 +17,9 @@ import (
 //  1. OptimismPortal
 //  2. L1CrossDomainMessenger
 //  3. L1StandardBridge
-func L1ProcessInitiatedBridgeEvents(log log.Logger, db *database.DB, chainConfig config.ChainConfig, fromHeight *big.Int, toHeight *big.Int) error {
+func L1ProcessInitiatedBridgeEvents(log log.Logger, db *database.DB, l1Contracts config.L1Contracts, fromHeight, toHeight *big.Int) error {
 	// (1) OptimismPortal
-	optimismPortalTxDeposits, err := contracts.OptimismPortalTransactionDepositEvents(chainConfig.L1Contracts.OptimismPortalProxy, db, fromHeight, toHeight)
+	optimismPortalTxDeposits, err := contracts.OptimismPortalTransactionDepositEvents(l1Contracts.OptimismPortalProxy, db, fromHeight, toHeight)
 	if err != nil {
 		return err
 	}
@@ -47,7 +47,7 @@ func L1ProcessInitiatedBridgeEvents(log log.Logger, db *database.DB, chainConfig
 	}
 
 	// (2) L1CrossDomainMessenger
-	crossDomainSentMessages, err := contracts.CrossDomainMessengerSentMessageEvents("l1", chainConfig.L1Contracts.L1CrossDomainMessengerProxy, db, fromHeight, toHeight)
+	crossDomainSentMessages, err := contracts.CrossDomainMessengerSentMessageEvents("l1", l1Contracts.L1CrossDomainMessengerProxy, db, fromHeight, toHeight)
 	if err != nil {
 		return err
 	}
@@ -77,7 +77,7 @@ func L1ProcessInitiatedBridgeEvents(log log.Logger, db *database.DB, chainConfig
 	}
 
 	// (3) L1StandardBridge
-	initiatedBridges, err := contracts.StandardBridgeInitiatedEvents("l1", chainConfig.L1Contracts.L1StandardBridgeProxy, db, fromHeight, toHeight)
+	initiatedBridges, err := contracts.StandardBridgeInitiatedEvents("l1", l1Contracts.L1StandardBridgeProxy, db, fromHeight, toHeight)
 	if err != nil {
 		return err
 	}
@@ -121,9 +121,9 @@ func L1ProcessInitiatedBridgeEvents(log log.Logger, db *database.DB, chainConfig
 //  1. OptimismPortal (Bedrock prove & finalize steps)
 //  2. L1CrossDomainMessenger (relayMessage marker)
 //  3. L1StandardBridge (no-op, since this is simply a wrapper over the L1CrossDomainMessenger)
-func L1ProcessFinalizedBridgeEvents(log log.Logger, db *database.DB, chainConfig config.ChainConfig, fromHeight *big.Int, toHeight *big.Int) error {
+func L1ProcessFinalizedBridgeEvents(log log.Logger, db *database.DB, l1Contracts config.L1Contracts, fromHeight, toHeight *big.Int) error {
 	// (1) OptimismPortal (proven withdrawals)
-	provenWithdrawals, err := contracts.OptimismPortalWithdrawalProvenEvents(chainConfig.L1Contracts.OptimismPortalProxy, db, fromHeight, toHeight)
+	provenWithdrawals, err := contracts.OptimismPortalWithdrawalProvenEvents(l1Contracts.OptimismPortalProxy, db, fromHeight, toHeight)
 	if err != nil {
 		return err
 	}
@@ -148,7 +148,7 @@ func L1ProcessFinalizedBridgeEvents(log log.Logger, db *database.DB, chainConfig
 	}
 
 	// (2) OptimismPortal (finalized withdrawals)
-	finalizedWithdrawals, err := contracts.OptimismPortalWithdrawalFinalizedEvents(chainConfig.L1Contracts.OptimismPortalProxy, db, fromHeight, toHeight)
+	finalizedWithdrawals, err := contracts.OptimismPortalWithdrawalFinalizedEvents(l1Contracts.OptimismPortalProxy, db, fromHeight, toHeight)
 	if err != nil {
 		return err
 	}
@@ -173,7 +173,7 @@ func L1ProcessFinalizedBridgeEvents(log log.Logger, db *database.DB, chainConfig
 	}
 
 	// (3) L1CrossDomainMessenger
-	crossDomainRelayedMessages, err := contracts.CrossDomainMessengerRelayedMessageEvents("l1", chainConfig.L1Contracts.L1CrossDomainMessengerProxy, db, fromHeight, toHeight)
+	crossDomainRelayedMessages, err := contracts.CrossDomainMessengerRelayedMessageEvents("l1", l1Contracts.L1CrossDomainMessengerProxy, db, fromHeight, toHeight)
 	if err != nil {
 		return err
 	}
@@ -200,7 +200,7 @@ func L1ProcessFinalizedBridgeEvents(log log.Logger, db *database.DB, chainConfig
 	}
 
 	// (4) L1StandardBridge
-	finalizedBridges, err := contracts.StandardBridgeFinalizedEvents("l1", chainConfig.L1Contracts.L1StandardBridgeProxy, db, fromHeight, toHeight)
+	finalizedBridges, err := contracts.StandardBridgeFinalizedEvents("l1", l1Contracts.L1StandardBridgeProxy, db, fromHeight, toHeight)
 	if err != nil {
 		return err
 	}
diff --git a/indexer/processors/bridge/l2_bridge_processor.go b/indexer/processors/bridge/l2_bridge_processor.go
index 790faede6..0229bf291 100644
--- a/indexer/processors/bridge/l2_bridge_processor.go
+++ b/indexer/processors/bridge/l2_bridge_processor.go
@@ -5,9 +5,9 @@ import (
 	"fmt"
 	"math/big"
 
+	"github.com/ethereum-optimism/optimism/indexer/config"
 	"github.com/ethereum-optimism/optimism/indexer/database"
 	"github.com/ethereum-optimism/optimism/indexer/processors/contracts"
-	"github.com/ethereum-optimism/optimism/op-bindings/predeploys"
 
 	"github.com/ethereum/go-ethereum/log"
 )
@@ -17,9 +17,9 @@ import (
 //  1. OptimismPortal
 //  2. L2CrossDomainMessenger
 //  3. L2StandardBridge
-func L2ProcessInitiatedBridgeEvents(log log.Logger, db *database.DB, fromHeight *big.Int, toHeight *big.Int) error {
+func L2ProcessInitiatedBridgeEvents(log log.Logger, db *database.DB, l2Contracts config.L2Contracts, fromHeight, toHeight *big.Int) error {
 	// (1) L2ToL1MessagePasser
-	l2ToL1MPMessagesPassed, err := contracts.L2ToL1MessagePasserMessagePassedEvents(predeploys.L2ToL1MessagePasserAddr, db, fromHeight, toHeight)
+	l2ToL1MPMessagesPassed, err := contracts.L2ToL1MessagePasserMessagePassedEvents(l2Contracts.L2ToL1MessagePasser, db, fromHeight, toHeight)
 	if err != nil {
 		return err
 	}
@@ -47,7 +47,7 @@ func L2ProcessInitiatedBridgeEvents(log log.Logger, db *database.DB, fromHeight
 	}
 
 	// (2) L2CrossDomainMessenger
-	crossDomainSentMessages, err := contracts.CrossDomainMessengerSentMessageEvents("l2", predeploys.L2CrossDomainMessengerAddr, db, fromHeight, toHeight)
+	crossDomainSentMessages, err := contracts.CrossDomainMessengerSentMessageEvents("l2", l2Contracts.L2CrossDomainMessenger, db, fromHeight, toHeight)
 	if err != nil {
 		return err
 	}
@@ -78,7 +78,7 @@ func L2ProcessInitiatedBridgeEvents(log log.Logger, db *database.DB, fromHeight
 	}
 
 	// (3) L2StandardBridge
-	initiatedBridges, err := contracts.StandardBridgeInitiatedEvents("l2", predeploys.L2StandardBridgeAddr, db, fromHeight, toHeight)
+	initiatedBridges, err := contracts.StandardBridgeInitiatedEvents("l2", l2Contracts.L2StandardBridge, db, fromHeight, toHeight)
 	if err != nil {
 		return err
 	}
@@ -122,9 +122,9 @@ func L2ProcessInitiatedBridgeEvents(log log.Logger, db *database.DB, fromHeight
 //  2. L2StandardBridge (no-op, since this is simply a wrapper over the L2CrossDomainMEssenger)
 //
 // NOTE: Unlike L1, there's no L2ToL1MessagePasser stage since transaction deposits are apart of the block derivation process.
-func L2ProcessFinalizedBridgeEvents(log log.Logger, db *database.DB, fromHeight *big.Int, toHeight *big.Int) error {
+func L2ProcessFinalizedBridgeEvents(log log.Logger, db *database.DB, l2Contracts config.L2Contracts, fromHeight, toHeight *big.Int) error {
 	// (1) L2CrossDomainMessenger
-	crossDomainRelayedMessages, err := contracts.CrossDomainMessengerRelayedMessageEvents("l2", predeploys.L2CrossDomainMessengerAddr, db, fromHeight, toHeight)
+	crossDomainRelayedMessages, err := contracts.CrossDomainMessengerRelayedMessageEvents("l2", l2Contracts.L2CrossDomainMessenger, db, fromHeight, toHeight)
 	if err != nil {
 		return err
 	}
@@ -151,7 +151,7 @@ func L2ProcessFinalizedBridgeEvents(log log.Logger, db *database.DB, fromHeight
 	}
 
 	// (2) L2StandardBridge
-	finalizedBridges, err := contracts.StandardBridgeFinalizedEvents("l2", predeploys.L2StandardBridgeAddr, db, fromHeight, toHeight)
+	finalizedBridges, err := contracts.StandardBridgeFinalizedEvents("l2", l2Contracts.L2StandardBridge, db, fromHeight, toHeight)
 	if err != nil {
 		return err
 	}
diff --git a/indexer/processors/bridge/legacy_bridge_processor.go b/indexer/processors/bridge/legacy_bridge_processor.go
index f78513959..6c60fe34f 100644
--- a/indexer/processors/bridge/legacy_bridge_processor.go
+++ b/indexer/processors/bridge/legacy_bridge_processor.go
@@ -11,7 +11,6 @@ import (
 	"github.com/ethereum-optimism/optimism/indexer/database"
 	"github.com/ethereum-optimism/optimism/indexer/node"
 	"github.com/ethereum-optimism/optimism/indexer/processors/contracts"
-	"github.com/ethereum-optimism/optimism/op-bindings/predeploys"
 )
 
 // Legacy Bridge Initiation
@@ -21,9 +20,9 @@ import (
 //  1. CanonicalTransactionChain
 //  2. L1CrossDomainMessenger
 //  3. L1StandardBridge
-func LegacyL1ProcessInitiatedBridgeEvents(log log.Logger, db *database.DB, chainConfig config.ChainConfig, fromHeight *big.Int, toHeight *big.Int) error {
+func LegacyL1ProcessInitiatedBridgeEvents(log log.Logger, db *database.DB, l1Contracts config.L1Contracts, fromHeight, toHeight *big.Int) error {
 	// (1) CanonicalTransactionChain
-	ctcTxDepositEvents, err := contracts.LegacyCTCDepositEvents(chainConfig.L1Contracts.LegacyCanonicalTransactionChain, db, fromHeight, toHeight)
+	ctcTxDepositEvents, err := contracts.LegacyCTCDepositEvents(l1Contracts.LegacyCanonicalTransactionChain, db, fromHeight, toHeight)
 	if err != nil {
 		return err
 	}
@@ -54,7 +53,7 @@ func LegacyL1ProcessInitiatedBridgeEvents(log log.Logger, db *database.DB, chain
 	}
 
 	// (2) L1CrossDomainMessenger
-	crossDomainSentMessages, err := contracts.CrossDomainMessengerSentMessageEvents("l1", chainConfig.L1Contracts.L1CrossDomainMessengerProxy, db, fromHeight, toHeight)
+	crossDomainSentMessages, err := contracts.CrossDomainMessengerSentMessageEvents("l1", l1Contracts.L1CrossDomainMessengerProxy, db, fromHeight, toHeight)
 	if err != nil {
 		return err
 	}
@@ -84,7 +83,7 @@ func LegacyL1ProcessInitiatedBridgeEvents(log log.Logger, db *database.DB, chain
 	}
 
 	// (3) L1StandardBridge
-	initiatedBridges, err := contracts.L1StandardBridgeLegacyDepositInitiatedEvents(chainConfig.L1Contracts.L1StandardBridgeProxy, db, fromHeight, toHeight)
+	initiatedBridges, err := contracts.L1StandardBridgeLegacyDepositInitiatedEvents(l1Contracts.L1StandardBridgeProxy, db, fromHeight, toHeight)
 	if err != nil {
 		return err
 	}
@@ -131,9 +130,9 @@ func LegacyL1ProcessInitiatedBridgeEvents(log log.Logger, db *database.DB, chain
 //  1. L2CrossDomainMessenger - The LegacyMessagePasser contract cannot be used as entrypoint to bridge transactions from L2. The protocol
 //     only allows the L2CrossDomainMessenger as the sole sender when relaying a bridged message.
 //  2. L2StandardBridge
-func LegacyL2ProcessInitiatedBridgeEvents(log log.Logger, db *database.DB, fromHeight *big.Int, toHeight *big.Int) error {
+func LegacyL2ProcessInitiatedBridgeEvents(log log.Logger, db *database.DB, l2Contracts config.L2Contracts, fromHeight, toHeight *big.Int) error {
 	// (1) L2CrossDomainMessenger
-	crossDomainSentMessages, err := contracts.CrossDomainMessengerSentMessageEvents("l2", predeploys.L2CrossDomainMessengerAddr, db, fromHeight, toHeight)
+	crossDomainSentMessages, err := contracts.CrossDomainMessengerSentMessageEvents("l2", l2Contracts.L2CrossDomainMessenger, db, fromHeight, toHeight)
 	if err != nil {
 		return err
 	}
@@ -151,7 +150,7 @@ func LegacyL2ProcessInitiatedBridgeEvents(log log.Logger, db *database.DB, fromH
 
 		// To ensure consistency in the schema, we duplicate this as the "root" transaction withdrawal. The storage key in the message
 		// passer contract is sha3(calldata + sender). The sender always being the L2CrossDomainMessenger pre-bedrock.
-		withdrawalHash := crypto.Keccak256Hash(append(sentMessage.MessageCalldata, predeploys.L2CrossDomainMessengerAddr[:]...))
+		withdrawalHash := crypto.Keccak256Hash(append(sentMessage.MessageCalldata, l2Contracts.L2CrossDomainMessenger[:]...))
 		l2TransactionWithdrawals[i] = database.L2TransactionWithdrawal{
 			WithdrawalHash:       withdrawalHash,
 			InitiatedL2EventGUID: sentMessage.Event.GUID,
@@ -182,7 +181,7 @@ func LegacyL2ProcessInitiatedBridgeEvents(log log.Logger, db *database.DB, fromH
 	}
 
 	// (2) L2StandardBridge
-	initiatedBridges, err := contracts.L2StandardBridgeLegacyWithdrawalInitiatedEvents(predeploys.L2StandardBridgeAddr, db, fromHeight, toHeight)
+	initiatedBridges, err := contracts.L2StandardBridgeLegacyWithdrawalInitiatedEvents(l2Contracts.L2StandardBridge, db, fromHeight, toHeight)
 	if err != nil {
 		return err
 	}
@@ -225,10 +224,10 @@ func LegacyL2ProcessInitiatedBridgeEvents(log log.Logger, db *database.DB, fromH
 // according to the pre-bedrock protocol. This follows:
 //  1. L1CrossDomainMessenger
 //  2. L1StandardBridge
-func LegacyL1ProcessFinalizedBridgeEvents(log log.Logger, db *database.DB, l1Client node.EthClient, chainConfig config.ChainConfig, fromHeight *big.Int, toHeight *big.Int) error {
+func LegacyL1ProcessFinalizedBridgeEvents(log log.Logger, db *database.DB, l1Client node.EthClient, l1Contracts config.L1Contracts, fromHeight, toHeight *big.Int) error {
 	// (1) L1CrossDomainMessenger -> This is the root-most contract from which bridge events are finalized since withdrawals must be initiated from the
 	// L2CrossDomainMessenger. Since there's no two-step withdrawal process, we mark the transaction as proven/finalized in the same step
-	crossDomainRelayedMessages, err := contracts.CrossDomainMessengerRelayedMessageEvents("l1", chainConfig.L1Contracts.L1CrossDomainMessengerProxy, db, fromHeight, toHeight)
+	crossDomainRelayedMessages, err := contracts.CrossDomainMessengerRelayedMessageEvents("l1", l1Contracts.L1CrossDomainMessengerProxy, db, fromHeight, toHeight)
 	if err != nil {
 		return err
 	}
@@ -305,9 +304,9 @@ func LegacyL1ProcessFinalizedBridgeEvents(log log.Logger, db *database.DB, l1Cli
 // according to the pre-bedrock protocol. This follows:
 //  1. L2CrossDomainMessenger
 //  2. L2StandardBridge
-func LegacyL2ProcessFinalizedBridgeEvents(log log.Logger, db *database.DB, fromHeight *big.Int, toHeight *big.Int) error {
+func LegacyL2ProcessFinalizedBridgeEvents(log log.Logger, db *database.DB, l2Contracts config.L2Contracts, fromHeight, toHeight *big.Int) error {
 	// (1) L2CrossDomainMessenger
-	crossDomainRelayedMessages, err := contracts.CrossDomainMessengerRelayedMessageEvents("l2", predeploys.L2CrossDomainMessengerAddr, db, fromHeight, toHeight)
+	crossDomainRelayedMessages, err := contracts.CrossDomainMessengerRelayedMessageEvents("l2", l2Contracts.L2CrossDomainMessenger, db, fromHeight, toHeight)
 	if err != nil {
 		return err
 	}


Test patch:
diff --git a/indexer/config/config_test.go b/indexer/config/config_test.go
index 681fd3c8a..07a9716c3 100644
--- a/indexer/config/config_test.go
+++ b/indexer/config/config_test.go
@@ -54,10 +54,10 @@ func TestLoadConfig(t *testing.T) {
 	require.NoError(t, err)
 
 	require.Equal(t, conf.Chain.Preset, 420)
-	require.Equal(t, conf.Chain.L1Contracts.OptimismPortalProxy.String(), presetConfigs[420].L1Contracts.OptimismPortalProxy.String())
-	require.Equal(t, conf.Chain.L1Contracts.L1CrossDomainMessengerProxy.String(), presetConfigs[420].L1Contracts.L1CrossDomainMessengerProxy.String())
-	require.Equal(t, conf.Chain.L1Contracts.L1StandardBridgeProxy.String(), presetConfigs[420].L1Contracts.L1StandardBridgeProxy.String())
-	require.Equal(t, conf.Chain.L1Contracts.L2OutputOracleProxy.String(), presetConfigs[420].L1Contracts.L2OutputOracleProxy.String())
+	require.Equal(t, conf.Chain.L1Contracts.OptimismPortalProxy.String(), Presets[420].ChainConfig.L1Contracts.OptimismPortalProxy.String())
+	require.Equal(t, conf.Chain.L1Contracts.L1CrossDomainMessengerProxy.String(), Presets[420].ChainConfig.L1Contracts.L1CrossDomainMessengerProxy.String())
+	require.Equal(t, conf.Chain.L1Contracts.L1StandardBridgeProxy.String(), Presets[420].ChainConfig.L1Contracts.L1StandardBridgeProxy.String())
+	require.Equal(t, conf.Chain.L1Contracts.L2OutputOracleProxy.String(), Presets[420].ChainConfig.L1Contracts.L2OutputOracleProxy.String())
 	require.Equal(t, conf.RPCs.L1RPC, "https://l1.example.com")
 	require.Equal(t, conf.RPCs.L2RPC, "https://l2.example.com")
 	require.Equal(t, conf.DB.Host, "127.0.0.1")
@@ -71,7 +71,7 @@ func TestLoadConfig(t *testing.T) {
 	require.Equal(t, conf.MetricsServer.Port, 7300)
 }
 
-func TestLoadConfig_WithoutPreset(t *testing.T) {
+func TestLoadConfigWithoutPreset(t *testing.T) {
 	tmpfile, err := os.CreateTemp("", "test_without_preset.toml")
 	require.NoError(t, err)
 	defer os.Remove(tmpfile.Name())
@@ -103,7 +103,6 @@ func TestLoadConfig_WithoutPreset(t *testing.T) {
 	conf, err := LoadConfig(logger, tmpfile.Name())
 	require.NoError(t, err)
 
-	// Enforce default values
 	require.Equal(t, conf.Chain.L1Contracts.OptimismPortalProxy.String(), common.HexToAddress("0x4205Fc579115071764c7423A4f12eDde41f106Ed").String())
 	require.Equal(t, conf.Chain.L1Contracts.L2OutputOracleProxy.String(), common.HexToAddress("0x42097868233d1aa22e815a266982f2cf17685a27").String())
 	require.Equal(t, conf.Chain.L1Contracts.L1CrossDomainMessengerProxy.String(), common.HexToAddress("0x420ce71c97B33Cc4729CF772ae268934F7ab5fA1").String())
@@ -117,7 +116,7 @@ func TestLoadConfig_WithoutPreset(t *testing.T) {
 	require.Equal(t, conf.Chain.L2HeaderBufferSize, uint(500))
 }
 
-func TestLoadConfig_WithUnknownPreset(t *testing.T) {
+func TestLoadConfigWithUnknownPreset(t *testing.T) {
 	tmpfile, err := os.CreateTemp("", "test_bad_preset.toml")
 	require.NoError(t, err)
 	defer os.Remove(tmpfile.Name())
@@ -148,7 +147,7 @@ func TestLoadConfig_WithUnknownPreset(t *testing.T) {
 	require.Equal(t, fmt.Sprintf("unknown preset: %d", faultyPreset), err.Error())
 }
 
-func Test_LoadConfig_PollingValues(t *testing.T) {
+func TestLoadConfigPollingValues(t *testing.T) {
 	tmpfile, err := os.CreateTemp("", "test_user_values.toml")
 	require.NoError(t, err)
 	defer os.Remove(tmpfile.Name())
@@ -178,25 +177,3 @@ func Test_LoadConfig_PollingValues(t *testing.T) {
 	require.Equal(t, conf.Chain.L1HeaderBufferSize, uint(100))
 	require.Equal(t, conf.Chain.L2HeaderBufferSize, uint(105))
 }
-
-func Test_AsSliceSuccess(t *testing.T) {
-	// error cases are intentionally ignored for testing since they can only be
-	// generated when the L1Contracts struct is developer modified to hold a non-address var field
-
-	testCfg := &L1Contracts{
-		OptimismPortalProxy:         common.HexToAddress("0x4205Fc579115071764c7423A4f12eDde41f106Ed"),
-		L2OutputOracleProxy:         common.HexToAddress("0x42097868233d1aa22e815a266982f2cf17685a27"),
-		L1CrossDomainMessengerProxy: common.HexToAddress("0x420ce71c97B33Cc4729CF772ae268934F7ab5fA1"),
-		L1StandardBridgeProxy:       common.HexToAddress("0x4209fc46f92E8a1c0deC1b1747d010903E884bE1"),
-	}
-
-	slice, err := testCfg.AsSlice()
-	require.NoError(t, err)
-	require.Equal(t, len(slice), 5)
-	require.Equal(t, slice[0].String(), testCfg.OptimismPortalProxy.String())
-	require.Equal(t, slice[1].String(), testCfg.L2OutputOracleProxy.String())
-	require.Equal(t, slice[2].String(), testCfg.L1CrossDomainMessengerProxy.String())
-	require.Equal(t, slice[3].String(), testCfg.L1StandardBridgeProxy.String())
-
-	// LegacyCanonicalTransactionChain is the 4th slot
-}
diff --git a/indexer/e2e_tests/setup.go b/indexer/e2e_tests/setup.go
index 20e660ee5..c282a94d5 100644
--- a/indexer/e2e_tests/setup.go
+++ b/indexer/e2e_tests/setup.go
@@ -71,15 +71,17 @@ func createE2ETestSuite(t *testing.T) E2ETestSuite {
 			L2RPC: opSys.EthInstances["sequencer"].HTTPEndpoint(),
 		},
 		Chain: config.ChainConfig{
-			L1PollingInterval:   uint(opCfg.DeployConfig.L1BlockTime) * 1000,
-			L1ConfirmationDepth: 0,
-			L2PollingInterval:   uint(opCfg.DeployConfig.L2BlockTime) * 1000,
-			L2ConfirmationDepth: 0,
+			L1PollingInterval: uint(opCfg.DeployConfig.L1BlockTime) * 1000,
+			L2PollingInterval: uint(opCfg.DeployConfig.L2BlockTime) * 1000,
+			L2Contracts:       config.L2ContractsFromPredeploys(),
 			L1Contracts: config.L1Contracts{
+				AddressManager:              opCfg.L1Deployments.AddressManager,
+				SystemConfigProxy:           opCfg.L1Deployments.SystemConfigProxy,
 				OptimismPortalProxy:         opCfg.L1Deployments.OptimismPortalProxy,
 				L2OutputOracleProxy:         opCfg.L1Deployments.L2OutputOracleProxy,
 				L1CrossDomainMessengerProxy: opCfg.L1Deployments.L1CrossDomainMessengerProxy,
 				L1StandardBridgeProxy:       opCfg.L1Deployments.L1StandardBridgeProxy,
+				L1ERC721BridgeProxy:         opCfg.L1Deployments.L1ERC721BridgeProxy,
 			},
 		},
 		HTTPServer:    config.ServerConfig{Host: "127.0.0.1", Port: 0},
diff --git a/indexer/etl/l1_etl_test.go b/indexer/etl/l1_etl_test.go
index 343d48bbc..b9673162b 100644
--- a/indexer/etl/l1_etl_test.go
+++ b/indexer/etl/l1_etl_test.go
@@ -51,10 +51,12 @@ func TestL1ETLConstruction(t *testing.T) {
 				client.On("GethEthClient").Return(nil)
 
 				return &testSuite{
-					db:        db,
-					client:    client,
-					start:     testStart,
-					contracts: config.L1Contracts{},
+					db:     db,
+					client: client,
+					start:  testStart,
+
+					// utilize sample l1 contract configuration (optimism)
+					contracts: config.Presets[10].ChainConfig.L1Contracts,
 				}
 			},
 			assertion: func(etl *L1ETL, err error) {
@@ -81,10 +83,12 @@ func TestL1ETLConstruction(t *testing.T) {
 				client.On("GethEthClient").Return(nil)
 
 				return &testSuite{
-					db:        db,
-					client:    client,
-					start:     testStart,
-					contracts: config.L1Contracts{},
+					db:     db,
+					client: client,
+					start:  testStart,
+
+					// utilize sample l1 contract configuration (optimism)
+					contracts: config.Presets[10].ChainConfig.L1Contracts,
 				}
 			},
 			assertion: func(etl *L1ETL, err error) {