From 1467436b3aa3bc2f454503fe4d2a9ea68762c27a Mon Sep 17 00:00:00 2001
From: Silas Lenihan <sjl@lenihan.net>
Date: Thu, 5 Sep 2024 11:30:39 -0400
Subject: [PATCH] Added ChainWriter to ChainComponents interface tests

---
 .changeset/loud-months-act.md                 |   5 +
 core/scripts/go.mod                           |   2 +-
 core/scripts/go.sum                           |   4 +-
 ...eader_test.go => chain_components_test.go} | 132 +++++++--
 ..._reader_historical_client_wrapper_test.go} |  17 +-
 .../chain_writer_historical_wrapper_test.go   |  39 +++
 ...o => chain_components_interface_tester.go} | 279 +++++++++---------
 .../relay/evm/evmtesting/run_tests.go         |  68 +++--
 go.mod                                        |   2 +-
 go.sum                                        |   4 +-
 integration-tests/go.mod                      |   2 +-
 integration-tests/go.sum                      |   4 +-
 integration-tests/load/go.mod                 |   2 +-
 integration-tests/load/go.sum                 |   4 +-
 14 files changed, 360 insertions(+), 204 deletions(-)
 create mode 100644 .changeset/loud-months-act.md
 rename core/services/relay/evm/{chain_reader_test.go => chain_components_test.go} (60%)
 rename core/services/relay/evm/{evmtesting/chain_reader_historical_client_wrapper.go => chain_reader_historical_client_wrapper_test.go} (89%)
 create mode 100644 core/services/relay/evm/chain_writer_historical_wrapper_test.go
 rename core/services/relay/evm/evmtesting/{chain_reader_interface_tester.go => chain_components_interface_tester.go} (59%)

diff --git a/.changeset/loud-months-act.md b/.changeset/loud-months-act.md
new file mode 100644
index 00000000000..d50254acb7a
--- /dev/null
+++ b/.changeset/loud-months-act.md
@@ -0,0 +1,5 @@
+---
+"chainlink": minor
+---
+
+#internal Added ChainWriter to ChainReader tests
diff --git a/core/scripts/go.mod b/core/scripts/go.mod
index 93e4b2dacaa..aa532448922 100644
--- a/core/scripts/go.mod
+++ b/core/scripts/go.mod
@@ -22,7 +22,7 @@ require (
 	github.com/prometheus/client_golang v1.17.0
 	github.com/shopspring/decimal v1.4.0
 	github.com/smartcontractkit/chainlink-automation v1.0.4
-	github.com/smartcontractkit/chainlink-common v0.2.2-0.20240903184200-6488292a85e3
+	github.com/smartcontractkit/chainlink-common v0.2.2-0.20240905145927-2ff0f9628f4d
 	github.com/smartcontractkit/chainlink/v2 v2.0.0-00010101000000-000000000000
 	github.com/smartcontractkit/libocr v0.0.0-20240717100443-f6226e09bee7
 	github.com/spf13/cobra v1.8.0
diff --git a/core/scripts/go.sum b/core/scripts/go.sum
index 0bf25bc5c25..61e784345fa 100644
--- a/core/scripts/go.sum
+++ b/core/scripts/go.sum
@@ -1190,8 +1190,8 @@ github.com/smartcontractkit/chainlink-automation v1.0.4 h1:iyW181JjKHLNMnDleI8um
 github.com/smartcontractkit/chainlink-automation v1.0.4/go.mod h1:u4NbPZKJ5XiayfKHD/v3z3iflQWqvtdhj13jVZXj/cM=
 github.com/smartcontractkit/chainlink-ccip v0.0.0-20240905130411-ebd9328c9870 h1:/NYLKZOQhOAPFszrq86gPBD47Rt6TF69GnZ8Vf7qdVQ=
 github.com/smartcontractkit/chainlink-ccip v0.0.0-20240905130411-ebd9328c9870/go.mod h1:Z9lQ5t20kRk28pzRLnqAJZUVOw8E6/siA3P3MLyKqoM=
-github.com/smartcontractkit/chainlink-common v0.2.2-0.20240903184200-6488292a85e3 h1:fkfOoAPviqO2rN8ngvejsDa7WKcw4paGEFA4/Znu0L0=
-github.com/smartcontractkit/chainlink-common v0.2.2-0.20240903184200-6488292a85e3/go.mod h1:D/qaCoq0SxXzg5NRN5FtBRv98VBf+D2NOC++RbvvuOc=
+github.com/smartcontractkit/chainlink-common v0.2.2-0.20240905145927-2ff0f9628f4d h1:VVtgseTBEJN0/NcewMcka1qwslKhY1HPXs4EEpZa7ek=
+github.com/smartcontractkit/chainlink-common v0.2.2-0.20240905145927-2ff0f9628f4d/go.mod h1:D/qaCoq0SxXzg5NRN5FtBRv98VBf+D2NOC++RbvvuOc=
 github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240710121324-3ed288aa9b45 h1:NBQLtqk8zsyY4qTJs+NElI3aDFTcAo83JHvqD04EvB0=
 github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240710121324-3ed288aa9b45/go.mod h1:LV0h7QBQUpoC2UUi6TcUvcIFm1xjP/DtEcqV8+qeLUs=
 github.com/smartcontractkit/chainlink-data-streams v0.0.0-20240904093355-e40169857652 h1:0aZ3HiEz2bMM5ywHAyKlFMN95qTzpNDn7uvnHLrFX6s=
diff --git a/core/services/relay/evm/chain_reader_test.go b/core/services/relay/evm/chain_components_test.go
similarity index 60%
rename from core/services/relay/evm/chain_reader_test.go
rename to core/services/relay/evm/chain_components_test.go
index f30bba0e449..841e2d0a7e0 100644
--- a/core/services/relay/evm/chain_reader_test.go
+++ b/core/services/relay/evm/chain_components_test.go
@@ -2,6 +2,7 @@ package evm_test
 
 import (
 	"context"
+	"crypto/ecdsa"
 	"fmt"
 	"math"
 	"math/big"
@@ -18,26 +19,33 @@ import (
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 
+	commonconfig "github.com/smartcontractkit/chainlink-common/pkg/config"
 	clcommontypes "github.com/smartcontractkit/chainlink-common/pkg/types"
 	. "github.com/smartcontractkit/chainlink-common/pkg/types/interfacetests" //nolint common practice to import test mods with .
-	"github.com/smartcontractkit/chainlink/v2/core/internal/cltest"
 	"github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/ethkey"
 
 	"github.com/smartcontractkit/chainlink/v2/core/logger"
+	"github.com/smartcontractkit/chainlink/v2/core/services/chainlink"
 	"github.com/smartcontractkit/chainlink/v2/core/services/relay/evm"
 	"github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types"
 
 	commontestutils "github.com/smartcontractkit/chainlink-common/pkg/loop/testutils"
 
+	"github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets"
 	"github.com/smartcontractkit/chainlink/v2/core/chains/evm/client"
+	"github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller"
+	evmtxmgr "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txmgr"
+	"github.com/smartcontractkit/chainlink/v2/core/internal/cltest"
 	"github.com/smartcontractkit/chainlink/v2/core/internal/testutils"
+	"github.com/smartcontractkit/chainlink/v2/core/internal/testutils/configtest"
 	"github.com/smartcontractkit/chainlink/v2/core/internal/testutils/pgtest"
+	keytypes "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/ethkey"
 	. "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/evmtesting" //nolint common practice to import test mods with .
 )
 
 const commonGasLimitOnEvms = uint64(4712388)
 
-func TestChainReaderEventsInitValidation(t *testing.T) {
+func TestContractReaderEventsInitValidation(t *testing.T) {
 	tests := []struct {
 		name                 string
 		chainContractReaders map[string]types.ChainContractReader
@@ -143,43 +151,77 @@ func TestChainReaderEventsInitValidation(t *testing.T) {
 	}
 }
 
-func TestChainReader(t *testing.T) {
+func TestChainComponents(t *testing.T) {
 	t.Parallel()
-	it := &EVMChainReaderInterfaceTester[*testing.T]{Helper: &helper{}}
+	it := &EVMChainComponentsInterfaceTester[*testing.T]{Helper: &helper{}}
+
+	it.Helper.Init(t)
+
 	// add new subtests here so that it can be run on real chains too
-	RunChainReaderEvmTests(t, it)
-	RunChainReaderInterfaceTests[*testing.T](t, commontestutils.WrapChainReaderTesterForLoop(it))
+	RunChainComponentsEvmTests(t, it)
+	RunContractReaderInterfaceTests[*testing.T](t, commontestutils.WrapContractReaderTesterForLoop(it), false)
 }
 
 type helper struct {
-	sim  *backends.SimulatedBackend
-	auth *bind.TransactOpts
+	sim         *backends.SimulatedBackend
+	accounts    []*bind.TransactOpts
+	deployerKey *ecdsa.PrivateKey
+	senderKey   *ecdsa.PrivateKey
+	txm         evmtxmgr.TxManager
+	client      client.Client
+	db          *sqlx.DB
 }
 
-func (h *helper) MustGenerateRandomKey(t *testing.T) ethkey.KeyV2 {
-	return cltest.MustGenerateRandomKey(t)
+func (h *helper) Init(t *testing.T) {
+	h.SetupKeys(t)
+
+	h.accounts = h.Accounts(t)
+
+	h.db = pgtest.NewSqlxDB(t)
+
+	h.Backend()
+	h.client = h.Client(t)
+
+	h.txm = h.TXM(t, h.client)
+	h.Commit()
 }
 
-func (h *helper) GasPriceBufferPercent() int64 {
-	return 0
+func (h *helper) SetupKeys(t *testing.T) {
+	deployerPkey, err := crypto.GenerateKey()
+	require.NoError(t, err)
+	h.deployerKey = deployerPkey
+
+	senderPkey, err := crypto.GenerateKey()
+	require.NoError(t, err)
+	h.senderKey = senderPkey
 }
 
-func (h *helper) SetupAuth(t *testing.T) *bind.TransactOpts {
-	privateKey, err := crypto.GenerateKey()
+func (h *helper) Accounts(t *testing.T) []*bind.TransactOpts {
+	if h.accounts != nil {
+		return h.accounts
+	}
+	deployer, err := bind.NewKeyedTransactorWithChainID(h.deployerKey, big.NewInt(1337))
 	require.NoError(t, err)
 
-	h.auth, err = bind.NewKeyedTransactorWithChainID(privateKey, big.NewInt(1337))
+	sender, err := bind.NewKeyedTransactorWithChainID(h.senderKey, big.NewInt(1337))
 	require.NoError(t, err)
 
-	h.Backend()
-	h.Commit()
-	return h.auth
+	return []*bind.TransactOpts{deployer, sender}
+}
+
+func (h *helper) MustGenerateRandomKey(t *testing.T) ethkey.KeyV2 {
+	return cltest.MustGenerateRandomKey(t)
+}
+
+func (h *helper) GasPriceBufferPercent() int64 {
+	return 0
 }
 
 func (h *helper) Backend() bind.ContractBackend {
 	if h.sim == nil {
 		h.sim = backends.NewSimulatedBackend(
-			core.GenesisAlloc{h.auth.From: {Balance: big.NewInt(math.MaxInt64)}}, commonGasLimitOnEvms*5000)
+			core.GenesisAlloc{h.accounts[0].From: {Balance: big.NewInt(math.MaxInt64)}, h.accounts[1].From: {Balance: big.NewInt(math.MaxInt64)}}, commonGasLimitOnEvms*5000)
+		cltest.Mine(h.sim, 1*time.Second)
 	}
 
 	return h.sim
@@ -190,6 +232,9 @@ func (h *helper) Commit() {
 }
 
 func (h *helper) Client(t *testing.T) client.Client {
+	if h.client != nil {
+		return h.client
+	}
 	return client.NewSimulatedBackendClient(t, h.sim, big.NewInt(1337))
 }
 
@@ -205,6 +250,18 @@ func (h *helper) Context(t *testing.T) context.Context {
 	return testutils.Context(t)
 }
 
+func (h *helper) ChainReaderEVMClient(ctx context.Context, t *testing.T, ht logpoller.HeadTracker, conf types.ChainReaderConfig) client.Client {
+	// wrap the client so that we can mock historical contract state
+	cwh := &evm.ClientWithContractHistory{Client: h.Client(t), HT: ht}
+	require.NoError(t, cwh.Init(ctx, conf))
+	return cwh
+}
+
+func (h *helper) WrappedChainWriter(cw clcommontypes.ChainWriter, client client.Client) clcommontypes.ChainWriter {
+	cwhw := evm.NewChainWriterHistoricalWrapper(cw, client.(*evm.ClientWithContractHistory))
+	return cwhw
+}
+
 func (h *helper) MaxWaitTimeForEvents() time.Duration {
 	// From trial and error, when running on CI, sometimes the boxes get slow
 	maxWaitTime := time.Second * 30
@@ -216,6 +273,41 @@ func (h *helper) MaxWaitTimeForEvents() time.Duration {
 		}
 		maxWaitTime = time.Second * time.Duration(waitS)
 	}
-
 	return maxWaitTime
 }
+
+func (h *helper) TXM(t *testing.T, client client.Client) evmtxmgr.TxManager {
+	if h.txm != nil {
+		return h.txm
+	}
+	db := h.db
+
+	clconfig := configtest.NewGeneralConfigSimulated(t, func(c *chainlink.Config, s *chainlink.Secrets) {
+		c.Database.Listener.FallbackPollInterval = commonconfig.MustNewDuration(100 * time.Millisecond)
+		c.EVM[0].GasEstimator.EIP1559DynamicFees = ptr(true)
+	})
+
+	clconfig.EVMConfigs()[0].GasEstimator.PriceMax = assets.GWei(100)
+
+	app := cltest.NewApplicationWithConfigV2AndKeyOnSimulatedBlockchain(t, clconfig, h.sim, db, client)
+	err := app.Start(h.Context(t))
+	require.NoError(t, err)
+
+	keyStore := app.KeyStore.Eth()
+
+	keyStore.XXXTestingOnlyAdd(h.Context(t), keytypes.FromPrivateKey(h.deployerKey))
+	require.NoError(t, keyStore.Add(h.Context(t), h.accounts[0].From, h.ChainID()))
+	require.NoError(t, keyStore.Enable(h.Context(t), h.accounts[0].From, h.ChainID()))
+
+	keyStore.XXXTestingOnlyAdd(h.Context(t), keytypes.FromPrivateKey(h.senderKey))
+	require.NoError(t, keyStore.Add(h.Context(t), h.accounts[1].From, h.ChainID()))
+	require.NoError(t, keyStore.Enable(h.Context(t), h.accounts[1].From, h.ChainID()))
+
+	chain, err := app.GetRelayers().LegacyEVMChains().Get((h.ChainID()).String())
+	require.NoError(t, err)
+
+	h.txm = chain.TxManager()
+	return h.txm
+}
+
+func ptr[T any](v T) *T { return &v }
diff --git a/core/services/relay/evm/evmtesting/chain_reader_historical_client_wrapper.go b/core/services/relay/evm/chain_reader_historical_client_wrapper_test.go
similarity index 89%
rename from core/services/relay/evm/evmtesting/chain_reader_historical_client_wrapper.go
rename to core/services/relay/evm/chain_reader_historical_client_wrapper_test.go
index b3d73be28f8..a3ea97650ec 100644
--- a/core/services/relay/evm/evmtesting/chain_reader_historical_client_wrapper.go
+++ b/core/services/relay/evm/chain_reader_historical_client_wrapper_test.go
@@ -1,4 +1,4 @@
-package evmtesting
+package evm
 
 import (
 	"context"
@@ -14,9 +14,9 @@ import (
 
 	clcommontypes "github.com/smartcontractkit/chainlink-common/pkg/types"
 	. "github.com/smartcontractkit/chainlink-common/pkg/types/interfacetests" //nolint common practice to import test mods with .
+
 	"github.com/smartcontractkit/chainlink/v2/core/chains/evm/client"
 	"github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller"
-	"github.com/smartcontractkit/chainlink/v2/core/services/relay/evm"
 	"github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types"
 )
 
@@ -30,7 +30,7 @@ type ClientWithContractHistory struct {
 
 func (cwh *ClientWithContractHistory) Init(_ context.Context, config types.ChainReaderConfig) error {
 	cwh.valsWithCall = make(map[int64]valWithCall)
-	parsedTypes := evm.ParsedTypes{
+	parsedTypes := ParsedTypes{
 		EncoderDefs: make(map[string]types.CodecEntry),
 		DecoderDefs: make(map[string]types.CodecEntry),
 	}
@@ -47,12 +47,12 @@ func (cwh *ClientWithContractHistory) Init(_ context.Context, config types.Chain
 				continue
 			}
 
-			inputMod, err := readDef.InputModifications.ToModifier(evm.DecoderHooks...)
+			inputMod, err := readDef.InputModifications.ToModifier(DecoderHooks...)
 			if err != nil {
 				return err
 			}
 
-			outputMod, err := readDef.OutputModifications.ToModifier(evm.DecoderHooks...)
+			outputMod, err := readDef.OutputModifications.ToModifier(DecoderHooks...)
 			if err != nil {
 				return err
 			}
@@ -67,8 +67,8 @@ func (cwh *ClientWithContractHistory) Init(_ context.Context, config types.Chain
 				return err
 			}
 
-			parsedTypes.EncoderDefs[evm.WrapItemType(contractName, genericName, true)] = input
-			parsedTypes.DecoderDefs[evm.WrapItemType(contractName, genericName, false)] = output
+			parsedTypes.EncoderDefs[WrapItemType(contractName, genericName, true)] = input
+			parsedTypes.DecoderDefs[WrapItemType(contractName, genericName, false)] = output
 		}
 	}
 
@@ -90,7 +90,6 @@ func (cwh *ClientWithContractHistory) SetUintLatestValue(ctx context.Context, va
 		ExpectedGetLatestValueArgs: forCall,
 		val:                        val,
 	}
-
 	return nil
 }
 
@@ -123,7 +122,7 @@ func (cwh *ClientWithContractHistory) CallContract(ctx context.Context, msg ethe
 	}
 
 	// encode the expected call to compare with the actual call
-	dataToCmp, err := cwh.codec.Encode(ctx, valAndCall.Params, evm.WrapItemType(valAndCall.ContractName, valAndCall.ReadName, true))
+	dataToCmp, err := cwh.codec.Encode(ctx, valAndCall.Params, WrapItemType(valAndCall.ContractName, valAndCall.ReadName, true))
 	if err != nil {
 		return nil, err
 	}
diff --git a/core/services/relay/evm/chain_writer_historical_wrapper_test.go b/core/services/relay/evm/chain_writer_historical_wrapper_test.go
new file mode 100644
index 00000000000..c849d1f3d57
--- /dev/null
+++ b/core/services/relay/evm/chain_writer_historical_wrapper_test.go
@@ -0,0 +1,39 @@
+package evm
+
+import (
+	"context"
+	"math/big"
+
+	commontypes "github.com/smartcontractkit/chainlink-common/pkg/types"
+	interfacetesttypes "github.com/smartcontractkit/chainlink-common/pkg/types/interfacetests"
+	primitives "github.com/smartcontractkit/chainlink-common/pkg/types/query/primitives"
+)
+
+// This wrapper is required to enable the ChainReader to access historical data
+// Since the geth simulated backend doesn't support historical data, we use this
+// thin wrapper.
+type ChainWriterHistoricalWrapper struct {
+	commontypes.ChainWriter
+	cwh *ClientWithContractHistory
+}
+
+func NewChainWriterHistoricalWrapper(cw commontypes.ChainWriter, cwh *ClientWithContractHistory) *ChainWriterHistoricalWrapper {
+	return &ChainWriterHistoricalWrapper{ChainWriter: cw, cwh: cwh}
+}
+
+func (cwhw *ChainWriterHistoricalWrapper) SubmitTransaction(ctx context.Context, contractName, method string, args any, transactionID string, toAddress string, meta *commontypes.TxMeta, value *big.Int) error {
+	if primArgs, ok := args.(interfacetesttypes.PrimitiveArgs); ok {
+		callArgs := interfacetesttypes.ExpectedGetLatestValueArgs{
+			ContractName:    contractName,
+			ReadName:        "GetAlterablePrimitiveValue",
+			ConfidenceLevel: primitives.Unconfirmed,
+			Params:          nil,
+			ReturnVal:       nil,
+		}
+		err := cwhw.cwh.SetUintLatestValue(ctx, primArgs.Value, callArgs)
+		if err != nil {
+			return err
+		}
+	}
+	return cwhw.ChainWriter.SubmitTransaction(ctx, contractName, method, args, transactionID, toAddress, meta, value)
+}
diff --git a/core/services/relay/evm/evmtesting/chain_reader_interface_tester.go b/core/services/relay/evm/evmtesting/chain_components_interface_tester.go
similarity index 59%
rename from core/services/relay/evm/evmtesting/chain_reader_interface_tester.go
rename to core/services/relay/evm/evmtesting/chain_components_interface_tester.go
index 7812ab202b1..c877f48784e 100644
--- a/core/services/relay/evm/evmtesting/chain_reader_interface_tester.go
+++ b/core/services/relay/evm/evmtesting/chain_components_interface_tester.go
@@ -3,7 +3,6 @@ package evmtesting
 import (
 	"context"
 	"encoding/json"
-	"fmt"
 	"math/big"
 	"time"
 
@@ -16,10 +15,14 @@ import (
 	"github.com/smartcontractkit/chainlink-common/pkg/codec"
 	clcommontypes "github.com/smartcontractkit/chainlink-common/pkg/types"
 	. "github.com/smartcontractkit/chainlink-common/pkg/types/interfacetests" //nolint common practice to import test mods with .
-	"github.com/smartcontractkit/chainlink-common/pkg/types/query/primitives"
+	primitives "github.com/smartcontractkit/chainlink-common/pkg/types/query/primitives"
+
+	"github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets"
 	"github.com/smartcontractkit/chainlink/v2/core/chains/evm/client"
+	"github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas"
 	"github.com/smartcontractkit/chainlink/v2/core/chains/evm/headtracker"
 	"github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller"
+	evmtxmgr "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txmgr"
 	evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types"
 	"github.com/smartcontractkit/chainlink/v2/core/gethwrappers/generated/chain_reader_tester"
 	_ "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/pgtest" // force binding for tx type
@@ -38,8 +41,8 @@ const (
 	finalityDepth                  = 4
 )
 
-type EVMChainReaderInterfaceTesterHelper[T TestingT[T]] interface {
-	SetupAuth(t T) *bind.TransactOpts
+type EVMChainComponentsInterfaceTesterHelper[T TestingT[T]] interface {
+	Init(t T)
 	Client(t T) client.Client
 	Commit()
 	Backend() bind.ContractBackend
@@ -48,21 +51,31 @@ type EVMChainReaderInterfaceTesterHelper[T TestingT[T]] interface {
 	NewSqlxDB(t T) *sqlx.DB
 	MaxWaitTimeForEvents() time.Duration
 	GasPriceBufferPercent() int64
+	Accounts(t T) []*bind.TransactOpts
+	TXM(T, client.Client) evmtxmgr.TxManager
+	// To enable the historical wrappers required for Simulated Backend tests.
+	ChainReaderEVMClient(ctx context.Context, t T, ht logpoller.HeadTracker, conf types.ChainReaderConfig) client.Client
+	WrappedChainWriter(cw clcommontypes.ChainWriter, client client.Client) clcommontypes.ChainWriter
 }
 
-type EVMChainReaderInterfaceTester[T TestingT[T]] struct {
-	Helper          EVMChainReaderInterfaceTesterHelper[T]
-	client          client.Client
-	address         string
-	address2        string
-	contractTesters map[string]*chain_reader_tester.ChainReaderTester
-	chainConfig     types.ChainReaderConfig
-	auth            *bind.TransactOpts
-	cr              evm.ChainReaderService
-	dirtyContracts  bool
+type EVMChainComponentsInterfaceTester[T TestingT[T]] struct {
+	Helper            EVMChainComponentsInterfaceTesterHelper[T]
+	client            client.Client
+	address           string
+	address2          string
+	contractTesters   map[string]*chain_reader_tester.ChainReaderTester
+	chainReaderConfig types.ChainReaderConfig
+	chainWriterConfig types.ChainWriterConfig
+	deployerAuth      *bind.TransactOpts
+	senderAuth        *bind.TransactOpts
+	cr                evm.ChainReaderService
+	cw                evm.ChainWriterService
+	dirtyContracts    bool
+	txm               evmtxmgr.TxManager
+	gasEstimator      gas.EvmFeeEstimator
 }
 
-func (it *EVMChainReaderInterfaceTester[T]) Setup(t T) {
+func (it *EVMChainComponentsInterfaceTester[T]) Setup(t T) {
 	t.Cleanup(func() {
 		// DB may be closed by the test already, ignore errors
 		if it.cr != nil {
@@ -73,6 +86,11 @@ func (it *EVMChainReaderInterfaceTester[T]) Setup(t T) {
 		if it.dirtyContracts {
 			it.contractTesters = nil
 		}
+
+		if it.cw != nil {
+			_ = it.cw.Close()
+		}
+		it.cw = nil
 	})
 
 	// can re-use the same chain for tests, just make new contract for each test
@@ -81,7 +99,11 @@ func (it *EVMChainReaderInterfaceTester[T]) Setup(t T) {
 		return
 	}
 
-	it.auth = it.Helper.SetupAuth(t)
+	// Need to separate accounts to ensure the nonce doesn't get misaligned after the
+	// contract deployments.
+	accounts := it.Helper.Accounts(t)
+	it.deployerAuth = accounts[0]
+	it.senderAuth = accounts[1]
 
 	testStruct := CreateTestStruct[T](0, it)
 
@@ -92,7 +114,7 @@ func (it *EVMChainReaderInterfaceTester[T]) Setup(t T) {
 		},
 	}
 
-	it.chainConfig = types.ChainReaderConfig{
+	it.chainReaderConfig = types.ChainReaderConfig{
 		Contracts: map[string]types.ChainContractReader{
 			AnyContractName: {
 				ContractABI: chain_reader_tester.ChainReaderTesterMetaData.ABI,
@@ -182,23 +204,90 @@ func (it *EVMChainReaderInterfaceTester[T]) Setup(t T) {
 			},
 		},
 	}
-	it.client = it.Helper.Client(t)
+	it.GetChainReader(t)
+	it.txm = it.Helper.TXM(t, it.client)
 
+	it.chainWriterConfig = types.ChainWriterConfig{
+		Contracts: map[string]*types.ContractConfig{
+			AnyContractName: {
+				ContractABI: chain_reader_tester.ChainReaderTesterMetaData.ABI,
+				Configs: map[string]*types.ChainWriterDefinition{
+					"addTestStruct": {
+						ChainSpecificName: "addTestStruct",
+						FromAddress:       it.Helper.Accounts(t)[1].From,
+						GasLimit:          2_000_000,
+						Checker:           "simulate",
+						InputModifications: codec.ModifiersConfig{
+							&codec.RenameModifierConfig{Fields: map[string]string{"NestedStruct.Inner.IntVal": "I"}},
+						},
+					},
+					"setAlterablePrimitiveValue": {
+						ChainSpecificName: "setAlterablePrimitiveValue",
+						FromAddress:       it.Helper.Accounts(t)[1].From,
+						GasLimit:          2_000_000,
+						Checker:           "simulate",
+					},
+					"triggerEvent": {
+						ChainSpecificName: "triggerEvent",
+						FromAddress:       it.Helper.Accounts(t)[1].From,
+						GasLimit:          2_000_000,
+						Checker:           "simulate",
+						InputModifications: codec.ModifiersConfig{
+							&codec.RenameModifierConfig{Fields: map[string]string{"NestedStruct.Inner.IntVal": "I"}},
+						},
+					},
+					"triggerEventWithDynamicTopic": {
+						ChainSpecificName: "triggerEventWithDynamicTopic",
+						FromAddress:       it.Helper.Accounts(t)[1].From,
+						GasLimit:          2_000_000,
+						Checker:           "simulate",
+					},
+					"triggerWithFourTopics": {
+						ChainSpecificName: "triggerWithFourTopics",
+						FromAddress:       it.Helper.Accounts(t)[1].From,
+						GasLimit:          2_000_000,
+						Checker:           "simulate",
+					},
+					"triggerWithFourTopicsWithHashed": {
+						ChainSpecificName: "triggerWithFourTopicsWithHashed",
+						FromAddress:       it.Helper.Accounts(t)[1].From,
+						GasLimit:          2_000_000,
+						Checker:           "simulate",
+					},
+				},
+			},
+			AnySecondContractName: {
+				ContractABI: chain_reader_tester.ChainReaderTesterMetaData.ABI,
+				Configs: map[string]*types.ChainWriterDefinition{
+					"addTestStruct": {
+						ChainSpecificName: "addTestStruct",
+						FromAddress:       it.Helper.Accounts(t)[1].From,
+						GasLimit:          2_000_000,
+						Checker:           "simulate",
+						InputModifications: codec.ModifiersConfig{
+							&codec.RenameModifierConfig{Fields: map[string]string{"NestedStruct.Inner.IntVal": "I"}},
+						},
+					},
+				},
+			},
+		},
+		MaxGasPrice: assets.NewWei(big.NewInt(1000000000000000000)),
+	}
 	it.deployNewContracts(t)
 }
 
-func (it *EVMChainReaderInterfaceTester[T]) Name() string {
+func (it *EVMChainComponentsInterfaceTester[T]) Name() string {
 	return "EVM"
 }
 
-func (it *EVMChainReaderInterfaceTester[T]) GetAccountBytes(i int) []byte {
+func (it *EVMChainComponentsInterfaceTester[T]) GetAccountBytes(i int) []byte {
 	account := [20]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
 	account[i%20] += byte(i)
 	account[(i+3)%20] += byte(i + 3)
 	return account[:]
 }
 
-func (it *EVMChainReaderInterfaceTester[T]) GetChainReader(t T) clcommontypes.ContractReader {
+func (it *EVMChainComponentsInterfaceTester[T]) GetChainReader(t T) clcommontypes.ContractReader {
 	ctx := it.Helper.Context(t)
 	if it.cr != nil {
 		return it.cr
@@ -213,20 +302,18 @@ func (it *EVMChainReaderInterfaceTester[T]) GetChainReader(t T) clcommontypes.Co
 		RpcBatchSize:             1,
 		KeepFinalizedBlocksDepth: 10000,
 	}
-	ht := headtracker.NewSimulatedHeadTracker(it.client, lpOpts.UseFinalityTag, lpOpts.FinalityDepth)
-	lp := logpoller.NewLogPoller(logpoller.NewORM(it.Helper.ChainID(), db, lggr), it.client, lggr, ht, lpOpts)
+	ht := headtracker.NewSimulatedHeadTracker(it.Helper.Client(t), lpOpts.UseFinalityTag, lpOpts.FinalityDepth)
+	lp := logpoller.NewLogPoller(logpoller.NewORM(it.Helper.ChainID(), db, lggr), it.Helper.Client(t), lggr, ht, lpOpts)
 	require.NoError(t, lp.Start(ctx))
 
 	// encode and decode the config to ensure the test covers type issues
-	confBytes, err := json.Marshal(it.chainConfig)
+	confBytes, err := json.Marshal(it.chainReaderConfig)
 	require.NoError(t, err)
 
 	conf, err := types.ChainReaderConfigFromBytes(confBytes)
 	require.NoError(t, err)
 
-	// wrap the client so that we can mock historical contract state
-	cwh := &ClientWithContractHistory{Client: it.Helper.Client(t), HT: ht}
-	require.NoError(t, cwh.Init(ctx, conf))
+	cwh := it.Helper.ChainReaderEVMClient(ctx, t, ht, conf)
 	it.client = cwh
 
 	cr, err := evm.NewChainReaderService(ctx, lggr, lp, ht, it.client, conf)
@@ -236,136 +323,61 @@ func (it *EVMChainReaderInterfaceTester[T]) GetChainReader(t T) clcommontypes.Co
 	return cr
 }
 
-func (it *EVMChainReaderInterfaceTester[T]) SetTestStructLatestValue(t T, testStruct *TestStruct) {
-	it.sendTxWithTestStruct(t, it.address, testStruct, (*chain_reader_tester.ChainReaderTesterTransactor).AddTestStruct)
+// This function is no longer necessary for Simulated Backend or Testnet tests.
+func (it *EVMChainComponentsInterfaceTester[T]) GenerateBlocksTillConfidenceLevel(t T, contractName, readName string, confidenceLevel primitives.ConfidenceLevel) {
 }
 
-func (it *EVMChainReaderInterfaceTester[T]) SetBatchLatestValues(t T, batchCallEntry BatchCallEntry) {
-	nameToAddress := make(map[string]string)
-	boundContracts := it.GetBindings(t)
-	for _, bc := range boundContracts {
-		nameToAddress[bc.Name] = bc.Address
-	}
-
-	for contractName, contractBatch := range batchCallEntry {
-		require.Contains(t, nameToAddress, contractName)
-		for _, readEntry := range contractBatch {
-			val, isOk := readEntry.ReturnValue.(*TestStruct)
-			if !isOk {
-				require.Fail(t, "expected *TestStruct for contract: %s read: %s, but received %T", contractName, readEntry.Name, readEntry.ReturnValue)
-			}
-			it.sendTxWithTestStruct(t, nameToAddress[contractName], val, (*chain_reader_tester.ChainReaderTesterTransactor).AddTestStruct)
-		}
-	}
-}
-
-// SetUintLatestValue is supposed to be used for testing confidence levels, but geth simulated backend doesn't support calling past state
-func (it *EVMChainReaderInterfaceTester[T]) SetUintLatestValue(t T, val uint64, forCall ExpectedGetLatestValueArgs) {
-	cw, ok := it.client.(*ClientWithContractHistory)
-	if !ok {
-		require.True(t, ok, "SetUintLatestValue should always be used for tests involving finality")
+func (it *EVMChainComponentsInterfaceTester[T]) GetChainWriter(t T) clcommontypes.ChainWriter {
+	ctx := it.Helper.Context(t)
+	if it.cw != nil {
+		return it.cw
 	}
 
-	it.sendTxWithUintVal(t, it.address, val, (*chain_reader_tester.ChainReaderTesterTransactor).SetAlterablePrimitiveValue)
-	require.NoError(t, cw.SetUintLatestValue(it.Helper.Context(t), val, forCall))
-}
-
-func (it *EVMChainReaderInterfaceTester[T]) TriggerEvent(t T, testStruct *TestStruct) {
-	it.sendTxWithTestStruct(t, it.address, testStruct, (*chain_reader_tester.ChainReaderTesterTransactor).TriggerEvent)
-}
+	cw, err := evm.NewChainWriterService(logger.NullLogger, it.client, it.txm, it.gasEstimator, it.chainWriterConfig)
+	require.NoError(t, err)
+	it.cw = it.Helper.WrappedChainWriter(cw, it.client)
 
-// GenerateBlocksTillConfidenceLevel is supposed to be used for testing confidence levels, but geth simulated backend doesn't support calling past state
-func (it *EVMChainReaderInterfaceTester[T]) GenerateBlocksTillConfidenceLevel(t T, contractName, readName string, confidenceLevel primitives.ConfidenceLevel) {
-	contractCfg, ok := it.chainConfig.Contracts[contractName]
-	if !ok {
-		t.Errorf("contract %s not found", contractName)
-		return
-	}
-	readCfg, ok := contractCfg.Configs[readName]
-	require.True(t, ok, fmt.Sprintf("readName: %s not found for contract: %s", readName, contractName))
-	toEvmConf, err := evm.ConfirmationsFromConfig(readCfg.ConfidenceConfirmations)
-	require.True(t, ok, fmt.Errorf("failed to parse confidence level mapping:%s not found for contract: %s readName: %s, err:%w", confidenceLevel, readName, contractName, err))
-	confirmations, ok := toEvmConf[confidenceLevel]
-	require.True(t, ok, fmt.Sprintf("confidence level mapping:%s not found for contract: %s readName: %s", confidenceLevel, readName, contractName))
-
-	if confirmations == evmtypes.Finalized {
-		for i := 0; i < finalityDepth; i++ {
-			it.Helper.Commit()
-		}
-	}
+	require.NoError(t, err)
+	require.NoError(t, cw.Start(ctx))
+	return it.cw
 }
 
-func (it *EVMChainReaderInterfaceTester[T]) GetBindings(_ T) []clcommontypes.BoundContract {
+func (it *EVMChainComponentsInterfaceTester[T]) GetBindings(_ T) []clcommontypes.BoundContract {
 	return []clcommontypes.BoundContract{
 		{Name: AnyContractName, Address: it.address},
 		{Name: AnySecondContractName, Address: it.address2},
 	}
 }
 
-type uintFn = func(*chain_reader_tester.ChainReaderTesterTransactor, *bind.TransactOpts, uint64) (*gethtypes.Transaction, error)
-
-// sendTxWithUintVal is supposed to be used for testing confidence levels, but geth simulated backend doesn't support calling past state
-func (it *EVMChainReaderInterfaceTester[T]) sendTxWithUintVal(t T, contractAddress string, val uint64, fn uintFn) {
-	tx, err := fn(
-		&it.contractTesters[contractAddress].ChainReaderTesterTransactor,
-		it.GetAuthWithGasSet(t),
-		val,
-	)
-
-	require.NoError(t, err)
-	it.Helper.Commit()
-	it.IncNonce()
-	it.AwaitTx(t, tx)
-	it.dirtyContracts = true
-}
-
-type testStructFn = func(*chain_reader_tester.ChainReaderTesterTransactor, *bind.TransactOpts, int32, string, uint8, [32]uint8, common.Address, []common.Address, *big.Int, chain_reader_tester.MidLevelTestStruct) (*gethtypes.Transaction, error)
-
-func (it *EVMChainReaderInterfaceTester[T]) sendTxWithTestStruct(t T, contractAddress string, testStruct *TestStruct, fn testStructFn) {
-	tx, err := fn(
-		&it.contractTesters[contractAddress].ChainReaderTesterTransactor,
-		it.GetAuthWithGasSet(t),
-		*testStruct.Field,
-		testStruct.DifferentField,
-		uint8(testStruct.OracleID),
-		OracleIdsToBytes(testStruct.OracleIDs),
-		common.Address(testStruct.Account),
-		ConvertAccounts(testStruct.Accounts),
-		testStruct.BigField,
-		MidToInternalType(testStruct.NestedStruct),
-	)
-	require.NoError(t, err)
-	it.Helper.Commit()
-	it.IncNonce()
-	it.AwaitTx(t, tx)
+func (it *EVMChainComponentsInterfaceTester[T]) DirtyContracts() {
 	it.dirtyContracts = true
 }
 
-func (it *EVMChainReaderInterfaceTester[T]) GetAuthWithGasSet(t T) *bind.TransactOpts {
+func (it *EVMChainComponentsInterfaceTester[T]) GetAuthWithGasSet(t T) *bind.TransactOpts {
 	gasPrice, err := it.client.SuggestGasPrice(it.Helper.Context(t))
 	require.NoError(t, err)
 	extra := new(big.Int).Mul(gasPrice, big.NewInt(it.Helper.GasPriceBufferPercent()))
 	extra = extra.Div(extra, big.NewInt(100))
-	it.auth.GasPrice = gasPrice.Add(gasPrice, extra)
-	return it.auth
+	it.deployerAuth.GasPrice = gasPrice.Add(gasPrice, extra)
+	return it.deployerAuth
 }
 
-func (it *EVMChainReaderInterfaceTester[T]) IncNonce() {
-	if it.auth.Nonce == nil {
-		it.auth.Nonce = big.NewInt(1)
+func (it *EVMChainComponentsInterfaceTester[T]) IncNonce() {
+	if it.deployerAuth.Nonce == nil {
+		it.deployerAuth.Nonce = big.NewInt(1)
 	} else {
-		it.auth.Nonce = it.auth.Nonce.Add(it.auth.Nonce, big.NewInt(1))
+		it.deployerAuth.Nonce = it.deployerAuth.Nonce.Add(it.deployerAuth.Nonce, big.NewInt(1))
 	}
 }
 
-func (it *EVMChainReaderInterfaceTester[T]) AwaitTx(t T, tx *gethtypes.Transaction) {
+func (it *EVMChainComponentsInterfaceTester[T]) AwaitTx(t T, tx *gethtypes.Transaction) {
 	ctx := it.Helper.Context(t)
 	receipt, err := bind.WaitMined(ctx, it.client, tx)
 	require.NoError(t, err)
 	require.Equal(t, gethtypes.ReceiptStatusSuccessful, receipt.Status)
 }
 
-func (it *EVMChainReaderInterfaceTester[T]) deployNewContracts(t T) {
+func (it *EVMChainComponentsInterfaceTester[T]) deployNewContracts(t T) {
 	// First test deploy both contracts, otherwise only deploy contracts if cleanup decides that we need to.
 	if it.address == "" || it.contractTesters == nil {
 		it.contractTesters = make(map[string]*chain_reader_tester.ChainReaderTester, 2)
@@ -374,13 +386,14 @@ func (it *EVMChainReaderInterfaceTester[T]) deployNewContracts(t T) {
 		it.address, it.address2 = address, address2
 		it.contractTesters[it.address] = ts1
 		it.contractTesters[it.address2] = ts2
+		it.dirtyContracts = false
 	}
 }
 
-func (it *EVMChainReaderInterfaceTester[T]) deployNewContract(t T) (string, *chain_reader_tester.ChainReaderTester) {
+func (it *EVMChainComponentsInterfaceTester[T]) deployNewContract(t T) (string, *chain_reader_tester.ChainReaderTester) {
 	// 105528 was in the error: gas too low: have 0, want 105528
 	// Not sure if there's a better way to get it.
-	it.auth.GasLimit = 10552800
+	it.deployerAuth.GasLimit = 10552800
 
 	address, tx, ts, err := chain_reader_tester.DeployChainReaderTester(it.GetAuthWithGasSet(t), it.Helper.Backend())
 	require.NoError(t, err)
@@ -391,16 +404,16 @@ func (it *EVMChainReaderInterfaceTester[T]) deployNewContract(t T) (string, *cha
 	return address.String(), ts
 }
 
-func (it *EVMChainReaderInterfaceTester[T]) MaxWaitTimeForEvents() time.Duration {
+func (it *EVMChainComponentsInterfaceTester[T]) MaxWaitTimeForEvents() time.Duration {
 	return it.Helper.MaxWaitTimeForEvents()
 }
 
-func OracleIdsToBytes(oracleIDs [32]commontypes.OracleID) [32]byte {
-	convertedIds := [32]byte{}
+func OracleIDsToBytes(oracleIDs [32]commontypes.OracleID) [32]byte {
+	convertedIDs := [32]byte{}
 	for i, id := range oracleIDs {
-		convertedIds[i] = byte(id)
+		convertedIDs[i] = byte(id)
 	}
-	return convertedIds
+	return convertedIDs
 }
 
 func ConvertAccounts(accounts [][]byte) []common.Address {
@@ -416,7 +429,7 @@ func ToInternalType(testStruct TestStruct) chain_reader_tester.TestStruct {
 		Field:          *testStruct.Field,
 		DifferentField: testStruct.DifferentField,
 		OracleId:       byte(testStruct.OracleID),
-		OracleIds:      OracleIdsToBytes(testStruct.OracleIDs),
+		OracleIds:      OracleIDsToBytes(testStruct.OracleIDs),
 		Account:        common.Address(testStruct.Account),
 		Accounts:       ConvertAccounts(testStruct.Accounts),
 		BigField:       testStruct.BigField,
diff --git a/core/services/relay/evm/evmtesting/run_tests.go b/core/services/relay/evm/evmtesting/run_tests.go
index caa24e8ae2c..8aba7c1b05d 100644
--- a/core/services/relay/evm/evmtesting/run_tests.go
+++ b/core/services/relay/evm/evmtesting/run_tests.go
@@ -10,6 +10,7 @@ import (
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 
+	"github.com/smartcontractkit/chainlink-common/pkg/types"
 	clcommontypes "github.com/smartcontractkit/chainlink-common/pkg/types"
 	"github.com/smartcontractkit/chainlink-common/pkg/types/query/primitives"
 	"github.com/smartcontractkit/chainlink/v2/core/services/relay/evm"
@@ -17,23 +18,27 @@ import (
 	. "github.com/smartcontractkit/chainlink-common/pkg/types/interfacetests" //nolint common practice to import test mods with .
 )
 
-func RunChainReaderEvmTests[T TestingT[T]](t T, it *EVMChainReaderInterfaceTester[T]) {
-	RunChainReaderInterfaceTests[T](t, it)
+func RunChainComponentsEvmTests[T TestingT[T]](t T, it *EVMChainComponentsInterfaceTester[T]) {
+	RunContractReaderEvmTests[T](t, it)
+	// Add ChainWriter tests here
+}
+
+func RunContractReaderEvmTests[T TestingT[T]](t T, it *EVMChainComponentsInterfaceTester[T]) {
+	RunContractReaderInterfaceTests[T](t, it, false)
 
 	t.Run("Dynamically typed topics can be used to filter and have type correct in return", func(t T) {
 		it.Setup(t)
 
 		anyString := "foo"
-		it.dirtyContracts = true
-		tx, err := it.contractTesters[it.address].ChainReaderTesterTransactor.TriggerEventWithDynamicTopic(it.GetAuthWithGasSet(t), anyString)
-		require.NoError(t, err)
-		it.Helper.Commit()
-		it.IncNonce()
-		it.AwaitTx(t, tx)
 		ctx := it.Helper.Context(t)
 
 		cr := it.GetChainReader(t)
 		require.NoError(t, cr.Bind(ctx, it.GetBindings(t)))
+		contracts := it.GetBindings(t)
+		type DynamicEvent struct {
+			Field string
+		}
+		SubmitTransactionToCW(t, it, "triggerEventWithDynamicTopic", DynamicEvent{Field: anyString}, contracts[0], types.Unconfirmed)
 
 		input := struct{ Field string }{Field: anyString}
 		tp := cr.(clcommontypes.ContractTypeProvider)
@@ -53,15 +58,15 @@ func RunChainReaderEvmTests[T TestingT[T]](t T, it *EVMChainReaderInterfaceTeste
 
 	t.Run("Multiple topics can filter together", func(t T) {
 		it.Setup(t)
-		it.dirtyContracts = true
+		ctx := it.Helper.Context(t)
+		cr := it.GetChainReader(t)
+		require.NoError(t, cr.Bind(ctx, it.GetBindings(t)))
+
 		triggerFourTopics(t, it, int32(1), int32(2), int32(3))
 		triggerFourTopics(t, it, int32(2), int32(2), int32(3))
 		triggerFourTopics(t, it, int32(1), int32(3), int32(3))
 		triggerFourTopics(t, it, int32(1), int32(2), int32(4))
 
-		ctx := it.Helper.Context(t)
-		cr := it.GetChainReader(t)
-		require.NoError(t, cr.Bind(ctx, it.GetBindings(t)))
 		var latest struct{ Field1, Field2, Field3 int32 }
 		params := struct{ Field1, Field2, Field3 int32 }{Field1: 1, Field2: 2, Field3: 3}
 
@@ -75,14 +80,15 @@ func RunChainReaderEvmTests[T TestingT[T]](t T, it *EVMChainReaderInterfaceTeste
 
 	t.Run("Filtering can be done on indexed topics that get hashed", func(t T) {
 		it.Setup(t)
-		it.dirtyContracts = true
+
+		cr := it.GetChainReader(t)
+		ctx := it.Helper.Context(t)
+		require.NoError(t, cr.Bind(ctx, it.GetBindings(t)))
+
 		triggerFourTopicsWithHashed(t, it, "1", [32]uint8{2}, [32]byte{5})
 		triggerFourTopicsWithHashed(t, it, "2", [32]uint8{2}, [32]byte{3})
 		triggerFourTopicsWithHashed(t, it, "1", [32]uint8{3}, [32]byte{3})
 
-		ctx := it.Helper.Context(t)
-		cr := it.GetChainReader(t)
-		require.NoError(t, cr.Bind(ctx, it.GetBindings(t)))
 		var latest struct {
 			Field3 [32]byte
 		}
@@ -111,20 +117,22 @@ func RunChainReaderEvmTests[T TestingT[T]](t T, it *EVMChainReaderInterfaceTeste
 	})
 }
 
-func triggerFourTopics[T TestingT[T]](t T, it *EVMChainReaderInterfaceTester[T], i1, i2, i3 int32) {
-	tx, err := it.contractTesters[it.address].ChainReaderTesterTransactor.TriggerWithFourTopics(it.GetAuthWithGasSet(t), i1, i2, i3)
-	require.NoError(t, err)
-	require.NoError(t, err)
-	it.Helper.Commit()
-	it.IncNonce()
-	it.AwaitTx(t, tx)
+func triggerFourTopics[T TestingT[T]](t T, it *EVMChainComponentsInterfaceTester[T], i1, i2, i3 int32) {
+	type DynamicEvent struct {
+		Field1 int32
+		Field2 int32
+		Field3 int32
+	}
+	contracts := it.GetBindings(t)
+	SubmitTransactionToCW(t, it, "triggerWithFourTopics", DynamicEvent{Field1: i1, Field2: i2, Field3: i3}, contracts[0], types.Unconfirmed)
 }
 
-func triggerFourTopicsWithHashed[T TestingT[T]](t T, it *EVMChainReaderInterfaceTester[T], i1 string, i2 [32]uint8, i3 [32]byte) {
-	tx, err := it.contractTesters[it.address].ChainReaderTesterTransactor.TriggerWithFourTopicsWithHashed(it.GetAuthWithGasSet(t), i1, i2, i3)
-	require.NoError(t, err)
-	require.NoError(t, err)
-	it.Helper.Commit()
-	it.IncNonce()
-	it.AwaitTx(t, tx)
+func triggerFourTopicsWithHashed[T TestingT[T]](t T, it *EVMChainComponentsInterfaceTester[T], i1 string, i2 [32]uint8, i3 [32]byte) {
+	type DynamicEvent struct {
+		Field1 string
+		Field2 [32]uint8
+		Field3 [32]byte
+	}
+	contracts := it.GetBindings(t)
+	SubmitTransactionToCW(t, it, "triggerWithFourTopicsWithHashed", DynamicEvent{Field1: i1, Field2: i2, Field3: i3}, contracts[0], types.Unconfirmed)
 }
diff --git a/go.mod b/go.mod
index 4045bbcc87a..fa8b665d974 100644
--- a/go.mod
+++ b/go.mod
@@ -75,7 +75,7 @@ require (
 	github.com/smartcontractkit/chain-selectors v1.0.21
 	github.com/smartcontractkit/chainlink-automation v1.0.4
 	github.com/smartcontractkit/chainlink-ccip v0.0.0-20240905130411-ebd9328c9870
-	github.com/smartcontractkit/chainlink-common v0.2.2-0.20240903184200-6488292a85e3
+	github.com/smartcontractkit/chainlink-common v0.2.2-0.20240905145927-2ff0f9628f4d
 	github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240710121324-3ed288aa9b45
 	github.com/smartcontractkit/chainlink-data-streams v0.0.0-20240904093355-e40169857652
 	github.com/smartcontractkit/chainlink-feeds v0.0.0-20240710170203-5b41615da827
diff --git a/go.sum b/go.sum
index 106ac0c579a..70600a58981 100644
--- a/go.sum
+++ b/go.sum
@@ -1147,8 +1147,8 @@ github.com/smartcontractkit/chainlink-automation v1.0.4 h1:iyW181JjKHLNMnDleI8um
 github.com/smartcontractkit/chainlink-automation v1.0.4/go.mod h1:u4NbPZKJ5XiayfKHD/v3z3iflQWqvtdhj13jVZXj/cM=
 github.com/smartcontractkit/chainlink-ccip v0.0.0-20240905130411-ebd9328c9870 h1:/NYLKZOQhOAPFszrq86gPBD47Rt6TF69GnZ8Vf7qdVQ=
 github.com/smartcontractkit/chainlink-ccip v0.0.0-20240905130411-ebd9328c9870/go.mod h1:Z9lQ5t20kRk28pzRLnqAJZUVOw8E6/siA3P3MLyKqoM=
-github.com/smartcontractkit/chainlink-common v0.2.2-0.20240903184200-6488292a85e3 h1:fkfOoAPviqO2rN8ngvejsDa7WKcw4paGEFA4/Znu0L0=
-github.com/smartcontractkit/chainlink-common v0.2.2-0.20240903184200-6488292a85e3/go.mod h1:D/qaCoq0SxXzg5NRN5FtBRv98VBf+D2NOC++RbvvuOc=
+github.com/smartcontractkit/chainlink-common v0.2.2-0.20240905145927-2ff0f9628f4d h1:VVtgseTBEJN0/NcewMcka1qwslKhY1HPXs4EEpZa7ek=
+github.com/smartcontractkit/chainlink-common v0.2.2-0.20240905145927-2ff0f9628f4d/go.mod h1:D/qaCoq0SxXzg5NRN5FtBRv98VBf+D2NOC++RbvvuOc=
 github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240710121324-3ed288aa9b45 h1:NBQLtqk8zsyY4qTJs+NElI3aDFTcAo83JHvqD04EvB0=
 github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240710121324-3ed288aa9b45/go.mod h1:LV0h7QBQUpoC2UUi6TcUvcIFm1xjP/DtEcqV8+qeLUs=
 github.com/smartcontractkit/chainlink-data-streams v0.0.0-20240904093355-e40169857652 h1:0aZ3HiEz2bMM5ywHAyKlFMN95qTzpNDn7uvnHLrFX6s=
diff --git a/integration-tests/go.mod b/integration-tests/go.mod
index 849a1928e87..4c4ec200b9a 100644
--- a/integration-tests/go.mod
+++ b/integration-tests/go.mod
@@ -37,7 +37,7 @@ require (
 	github.com/smartcontractkit/chain-selectors v1.0.21
 	github.com/smartcontractkit/chainlink-automation v1.0.4
 	github.com/smartcontractkit/chainlink-ccip v0.0.0-20240905130411-ebd9328c9870
-	github.com/smartcontractkit/chainlink-common v0.2.2-0.20240903184200-6488292a85e3
+	github.com/smartcontractkit/chainlink-common v0.2.2-0.20240905145927-2ff0f9628f4d
 	github.com/smartcontractkit/chainlink-testing-framework/havoc v1.50.0
 	github.com/smartcontractkit/chainlink-testing-framework/lib v1.50.0
 	github.com/smartcontractkit/chainlink-testing-framework/lib/grafana v1.50.0
diff --git a/integration-tests/go.sum b/integration-tests/go.sum
index 9bd0fadcb6c..1682e416822 100644
--- a/integration-tests/go.sum
+++ b/integration-tests/go.sum
@@ -1425,8 +1425,8 @@ github.com/smartcontractkit/chainlink-automation v1.0.4 h1:iyW181JjKHLNMnDleI8um
 github.com/smartcontractkit/chainlink-automation v1.0.4/go.mod h1:u4NbPZKJ5XiayfKHD/v3z3iflQWqvtdhj13jVZXj/cM=
 github.com/smartcontractkit/chainlink-ccip v0.0.0-20240905130411-ebd9328c9870 h1:/NYLKZOQhOAPFszrq86gPBD47Rt6TF69GnZ8Vf7qdVQ=
 github.com/smartcontractkit/chainlink-ccip v0.0.0-20240905130411-ebd9328c9870/go.mod h1:Z9lQ5t20kRk28pzRLnqAJZUVOw8E6/siA3P3MLyKqoM=
-github.com/smartcontractkit/chainlink-common v0.2.2-0.20240903184200-6488292a85e3 h1:fkfOoAPviqO2rN8ngvejsDa7WKcw4paGEFA4/Znu0L0=
-github.com/smartcontractkit/chainlink-common v0.2.2-0.20240903184200-6488292a85e3/go.mod h1:D/qaCoq0SxXzg5NRN5FtBRv98VBf+D2NOC++RbvvuOc=
+github.com/smartcontractkit/chainlink-common v0.2.2-0.20240905145927-2ff0f9628f4d h1:VVtgseTBEJN0/NcewMcka1qwslKhY1HPXs4EEpZa7ek=
+github.com/smartcontractkit/chainlink-common v0.2.2-0.20240905145927-2ff0f9628f4d/go.mod h1:D/qaCoq0SxXzg5NRN5FtBRv98VBf+D2NOC++RbvvuOc=
 github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240710121324-3ed288aa9b45 h1:NBQLtqk8zsyY4qTJs+NElI3aDFTcAo83JHvqD04EvB0=
 github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240710121324-3ed288aa9b45/go.mod h1:LV0h7QBQUpoC2UUi6TcUvcIFm1xjP/DtEcqV8+qeLUs=
 github.com/smartcontractkit/chainlink-data-streams v0.0.0-20240904093355-e40169857652 h1:0aZ3HiEz2bMM5ywHAyKlFMN95qTzpNDn7uvnHLrFX6s=
diff --git a/integration-tests/load/go.mod b/integration-tests/load/go.mod
index 264e14140ee..f60e0b50635 100644
--- a/integration-tests/load/go.mod
+++ b/integration-tests/load/go.mod
@@ -16,7 +16,7 @@ require (
 	github.com/rs/zerolog v1.33.0
 	github.com/slack-go/slack v0.12.2
 	github.com/smartcontractkit/chainlink-automation v1.0.4
-	github.com/smartcontractkit/chainlink-common v0.2.2-0.20240903184200-6488292a85e3
+	github.com/smartcontractkit/chainlink-common v0.2.2-0.20240905145927-2ff0f9628f4d
 	github.com/smartcontractkit/chainlink-testing-framework/lib v1.50.0
 	github.com/smartcontractkit/chainlink-testing-framework/seth v1.50.1
 	github.com/smartcontractkit/chainlink-testing-framework/wasp v1.50.0
diff --git a/integration-tests/load/go.sum b/integration-tests/load/go.sum
index 7c5c580d1ee..6e674b834d2 100644
--- a/integration-tests/load/go.sum
+++ b/integration-tests/load/go.sum
@@ -1395,8 +1395,8 @@ github.com/smartcontractkit/chainlink-automation v1.0.4 h1:iyW181JjKHLNMnDleI8um
 github.com/smartcontractkit/chainlink-automation v1.0.4/go.mod h1:u4NbPZKJ5XiayfKHD/v3z3iflQWqvtdhj13jVZXj/cM=
 github.com/smartcontractkit/chainlink-ccip v0.0.0-20240905130411-ebd9328c9870 h1:/NYLKZOQhOAPFszrq86gPBD47Rt6TF69GnZ8Vf7qdVQ=
 github.com/smartcontractkit/chainlink-ccip v0.0.0-20240905130411-ebd9328c9870/go.mod h1:Z9lQ5t20kRk28pzRLnqAJZUVOw8E6/siA3P3MLyKqoM=
-github.com/smartcontractkit/chainlink-common v0.2.2-0.20240903184200-6488292a85e3 h1:fkfOoAPviqO2rN8ngvejsDa7WKcw4paGEFA4/Znu0L0=
-github.com/smartcontractkit/chainlink-common v0.2.2-0.20240903184200-6488292a85e3/go.mod h1:D/qaCoq0SxXzg5NRN5FtBRv98VBf+D2NOC++RbvvuOc=
+github.com/smartcontractkit/chainlink-common v0.2.2-0.20240905145927-2ff0f9628f4d h1:VVtgseTBEJN0/NcewMcka1qwslKhY1HPXs4EEpZa7ek=
+github.com/smartcontractkit/chainlink-common v0.2.2-0.20240905145927-2ff0f9628f4d/go.mod h1:D/qaCoq0SxXzg5NRN5FtBRv98VBf+D2NOC++RbvvuOc=
 github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240710121324-3ed288aa9b45 h1:NBQLtqk8zsyY4qTJs+NElI3aDFTcAo83JHvqD04EvB0=
 github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240710121324-3ed288aa9b45/go.mod h1:LV0h7QBQUpoC2UUi6TcUvcIFm1xjP/DtEcqV8+qeLUs=
 github.com/smartcontractkit/chainlink-data-streams v0.0.0-20240904093355-e40169857652 h1:0aZ3HiEz2bMM5ywHAyKlFMN95qTzpNDn7uvnHLrFX6s=