From d0290dbab02560d8d5d3801a91c71ca3892b7ee5 Mon Sep 17 00:00:00 2001 From: psy2848048 Date: Tue, 22 Sep 2026 15:14:37 +0900 Subject: [PATCH 1/2] feat(vm): add composable application-level EVM tracers --- .../x/vm/test_application_tracing.go | 216 +++++++++ .../integration/x/vm/test_state_transition.go | 86 ++++ x/vm/keeper/call_evm.go | 45 +- x/vm/keeper/call_evm_tracing.go | 39 ++ x/vm/keeper/call_evm_tracing_test.go | 115 +++++ x/vm/keeper/grpc_query.go | 3 +- x/vm/keeper/keeper.go | 3 + x/vm/keeper/state_transition.go | 7 +- x/vm/keeper/tracer_set.go | 50 +++ x/vm/keeper/tracer_set_test.go | 86 ++++ x/vm/statedb/tracing.go | 16 + x/vm/statedb/tracing_test.go | 55 +++ x/vm/tracer/application.go | 27 ++ x/vm/tracer/application/collector.go | 257 +++++++++++ x/vm/tracer/application/collector_test.go | 110 +++++ x/vm/tracer/application/doc.go | 13 + x/vm/tracer/application/tx_trace.go | 45 ++ x/vm/tracer/application/tx_trace_test.go | 77 ++++ x/vm/tracer/context.go | 22 + x/vm/tracer/docs.go | 17 + x/vm/tracer/manager.go | 418 ++++++++++++++++++ x/vm/tracer/manager_test.go | 171 +++++++ x/vm/tracer/tracer.go | 10 + 23 files changed, 1884 insertions(+), 4 deletions(-) create mode 100644 tests/integration/x/vm/test_application_tracing.go create mode 100644 x/vm/keeper/call_evm_tracing.go create mode 100644 x/vm/keeper/call_evm_tracing_test.go create mode 100644 x/vm/keeper/tracer_set.go create mode 100644 x/vm/keeper/tracer_set_test.go create mode 100644 x/vm/statedb/tracing.go create mode 100644 x/vm/statedb/tracing_test.go create mode 100644 x/vm/tracer/application.go create mode 100644 x/vm/tracer/application/collector.go create mode 100644 x/vm/tracer/application/collector_test.go create mode 100644 x/vm/tracer/application/doc.go create mode 100644 x/vm/tracer/application/tx_trace.go create mode 100644 x/vm/tracer/application/tx_trace_test.go create mode 100644 x/vm/tracer/context.go create mode 100644 x/vm/tracer/docs.go create mode 100644 x/vm/tracer/manager.go create mode 100644 x/vm/tracer/manager_test.go create mode 100644 x/vm/tracer/tracer.go diff --git a/tests/integration/x/vm/test_application_tracing.go b/tests/integration/x/vm/test_application_tracing.go new file mode 100644 index 000000000..93ceaf663 --- /dev/null +++ b/tests/integration/x/vm/test_application_tracing.go @@ -0,0 +1,216 @@ +package vm + +import ( + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/tracing" + ethtypes "github.com/ethereum/go-ethereum/core/types" + gethvm "github.com/ethereum/go-ethereum/core/vm" + + precompilecommon "github.com/cosmos/evm/precompiles/common" + vmkeeper "github.com/cosmos/evm/x/vm/keeper" + "github.com/cosmos/evm/x/vm/statedb" + vmtracer "github.com/cosmos/evm/x/vm/tracer" + evmtypes "github.com/cosmos/evm/x/vm/types" + + storetypes "github.com/cosmos/cosmos-sdk/store/v2/types" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +type applicationHooksTracer struct { + hooks *tracing.Hooks +} + +func (t *applicationHooksTracer) Hooks() *tracing.Hooks { return t.hooks } + +type nestedCallEVMPrecompile struct { + precompilecommon.Precompile + keeper *vmkeeper.Keeper + target common.Address + manager *vmtracer.Manager +} + +type viewCallEVMPrecompile struct { + precompilecommon.Precompile + keeper *vmkeeper.Keeper + target common.Address + parentManager *vmtracer.Manager +} + +func (p *viewCallEVMPrecompile) Name() string { return "view-call-evm-tracing-test" } + +func (p *viewCallEVMPrecompile) RequiredGas(_ []byte) uint64 { return 0 } + +func (p *viewCallEVMPrecompile) Run(evm *gethvm.EVM, contract *gethvm.Contract, _ bool) ([]byte, error) { + return p.RunNativeAction(evm, contract, func(ctx sdk.Context) ([]byte, error) { + p.parentManager, _ = vmtracer.FromContext(ctx) + response, err := p.keeper.CallEVMViewWithData( + ctx, + p.Address(), + &p.target, + nil, + new(big.Int).SetUint64(contract.Gas), + ) + if err != nil { + return nil, err + } + return response.Ret, nil + }) +} + +func (p *nestedCallEVMPrecompile) Name() string { return "nested-call-evm-tracing-test" } + +func (p *nestedCallEVMPrecompile) RequiredGas(_ []byte) uint64 { return 0 } + +func (p *nestedCallEVMPrecompile) Run(evm *gethvm.EVM, contract *gethvm.Contract, _ bool) ([]byte, error) { + return p.RunNativeAction(evm, contract, func(ctx sdk.Context) ([]byte, error) { + p.manager, _ = vmtracer.FromContext(ctx) + stateDB, ok := evm.StateDB.(*statedb.StateDB) + if !ok { + return nil, fmt.Errorf("unexpected StateDB type %T", evm.StateDB) + } + response, err := p.keeper.CallEVMWithData( + ctx, + stateDB, + p.Address(), + &p.target, + nil, + true, + true, + new(big.Int).SetUint64(contract.Gas), + ) + if err != nil { + return nil, err + } + return response.Ret, nil + }) +} + +func (s *KeeperTestSuite) TestNestedCallEVMWithDataReusesApplicationTracing() { + s.SetupTest() + + ctx := s.Network.GetContext() + evmKeeper := s.Network.App.GetEVMKeeper() + precompileAddress := common.HexToAddress("0x000000000000000000000000000000000000fffe") + precompile := &nestedCallEVMPrecompile{ + Precompile: precompilecommon.Precompile{ + KvGasConfig: ctx.KVGasConfig(), + TransientKVGasConfig: ctx.TransientKVGasConfig(), + ContractAddress: precompileAddress, + }, + keeper: evmKeeper, + target: s.Keyring.GetAddr(1), + } + evmKeeper.RegisterStaticPrecompile(precompileAddress, precompile) + params := evmKeeper.GetParams(ctx) + params.ActiveStaticPrecompiles = append(params.ActiveStaticPrecompiles, precompileAddress.String()) + s.Require().NoError(evmKeeper.SetParams(ctx, params)) + + type tracerCounts struct { + txStarts int + txEnds int + enters int + } + var factoryCalls []string + var factoryManagers []*vmtracer.Manager + first, second := &tracerCounts{}, &tracerCounts{} + newFactory := func(name string, counts *tracerCounts) vmtracer.ApplicationTracerFactory { + return func(factoryCtx sdk.Context, _ vmtracer.ExecutionInfo) (sdk.Context, vmtracer.Tracer) { + factoryCalls = append(factoryCalls, name) + manager, _ := vmtracer.FromContext(factoryCtx) + factoryManagers = append(factoryManagers, manager) + return factoryCtx, &applicationHooksTracer{hooks: &tracing.Hooks{ + OnTxStart: func(_ *tracing.VMContext, _ *ethtypes.Transaction, _ common.Address) { counts.txStarts++ }, + OnTxEnd: func(_ *ethtypes.Receipt, _ error) { counts.txEnds++ }, + OnEnter: func(_ int, _ byte, _, _ common.Address, _ []byte, _ uint64, _ *big.Int) { counts.enters++ }, + }} + } + } + evmKeeper.SetApplicationTracerFactories( + newFactory("application-1", first), + newFactory("application-2", second), + ) + + tx, err := s.Factory.GenerateSignedEthTx(s.Keyring.GetPrivKey(0), evmtypes.EvmTxArgs{ + To: &precompileAddress, + GasLimit: 500_000, + GasPrice: big.NewInt(0), + }) + s.Require().NoError(err) + response, err := evmKeeper.ApplyTransaction( + ctx.WithGasMeter(storetypes.NewGasMeter(1_000_000)), + tx.GetMsgs()[0].(*evmtypes.MsgEthereumTx).AsTransaction(), + ) + + s.Require().NoError(err) + s.Require().False(response.Failed()) + s.Require().Equal([]string{"application-1", "application-2"}, factoryCalls) + s.Require().Len(factoryManagers, 2) + s.Require().Same(factoryManagers[0], factoryManagers[1]) + for _, counts := range []*tracerCounts{first, second} { + s.Require().Equal(1, counts.txStarts) + s.Require().Equal(1, counts.txEnds) + s.Require().GreaterOrEqual(counts.enters, 2) + } + s.Require().NotNil(factoryManagers[0]) + s.Require().Same(factoryManagers[0], precompile.manager) +} + +func (s *KeeperTestSuite) TestCallEVMViewWithDataStartsIndependentApplicationTracing() { + s.SetupTest() + + ctx := s.Network.GetContext() + evmKeeper := s.Network.App.GetEVMKeeper() + precompileAddress := common.HexToAddress("0x000000000000000000000000000000000000fffd") + target := s.Keyring.GetAddr(1) + precompile := &viewCallEVMPrecompile{ + Precompile: precompilecommon.Precompile{ + KvGasConfig: ctx.KVGasConfig(), + TransientKVGasConfig: ctx.TransientKVGasConfig(), + ContractAddress: precompileAddress, + }, + keeper: evmKeeper, + target: target, + } + evmKeeper.RegisterStaticPrecompile(precompileAddress, precompile) + params := evmKeeper.GetParams(ctx) + params.ActiveStaticPrecompiles = append(params.ActiveStaticPrecompiles, precompileAddress.String()) + s.Require().NoError(evmKeeper.SetParams(ctx, params)) + + var managers []*vmtracer.Manager + var entered [][]common.Address + evmKeeper.SetApplicationTracerFactories(func(factoryCtx sdk.Context, _ vmtracer.ExecutionInfo) (sdk.Context, vmtracer.Tracer) { + manager, ok := vmtracer.FromContext(factoryCtx) + s.Require().True(ok) + managers = append(managers, manager) + index := len(entered) + entered = append(entered, nil) + return factoryCtx, &applicationHooksTracer{hooks: &tracing.Hooks{ + OnEnter: func(_ int, _ byte, _, to common.Address, _ []byte, _ uint64, _ *big.Int) { + entered[index] = append(entered[index], to) + }, + }} + }) + + tx, err := s.Factory.GenerateSignedEthTx(s.Keyring.GetPrivKey(0), evmtypes.EvmTxArgs{ + To: &precompileAddress, + GasLimit: 500_000, + GasPrice: big.NewInt(0), + }) + s.Require().NoError(err) + response, err := evmKeeper.ApplyTransaction( + ctx.WithGasMeter(storetypes.NewGasMeter(1_000_000)), + tx.GetMsgs()[0].(*evmtypes.MsgEthereumTx).AsTransaction(), + ) + + s.Require().NoError(err) + s.Require().False(response.Failed()) + s.Require().Len(managers, 2) + s.Require().Same(managers[0], precompile.parentManager) + s.Require().NotSame(managers[0], managers[1]) + s.Require().Contains(entered[0], precompileAddress) + s.Require().NotContains(entered[0], target) + s.Require().Contains(entered[1], target) +} diff --git a/tests/integration/x/vm/test_state_transition.go b/tests/integration/x/vm/test_state_transition.go index d6ff66235..28accdb42 100644 --- a/tests/integration/x/vm/test_state_transition.go +++ b/tests/integration/x/vm/test_state_transition.go @@ -9,6 +9,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/tracing" gethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/params" "github.com/holiman/uint256" @@ -27,6 +28,8 @@ import ( feemarkettypes "github.com/cosmos/evm/x/feemarket/types" "github.com/cosmos/evm/x/vm/keeper" "github.com/cosmos/evm/x/vm/statedb" + vmtracer "github.com/cosmos/evm/x/vm/tracer" + applicationtracer "github.com/cosmos/evm/x/vm/tracer/application" "github.com/cosmos/evm/x/vm/types" sdkmath "cosmossdk.io/math" @@ -676,6 +679,89 @@ func (h *testHooks) PostTxProcessing(ctx sdk.Context, sender common.Address, msg return h.postProcessing(ctx, sender, msg, receipt) } +func (s *KeeperTestSuite) TestApplicationTracerAvailableToPostTxHook() { + s.SetupTest() + + var sawTrace bool + evmKeeper := s.Network.App.GetEVMKeeper() + evmKeeper.SetApplicationTracerFactories(applicationtracer.TxTraceFactory) + evmKeeper.SetHooks(keeper.NewMultiEvmHooks(&testHooks{ + postProcessing: func(ctx sdk.Context, _ common.Address, _ core.Message, receipt *gethtypes.Receipt) error { + touches, transfers, erc20Transfers := applicationtracer.GetTxTrace(ctx, uint64(receipt.TransactionIndex)) + s.Require().NotEmpty(touches) + s.Require().Len(transfers, 1) + s.Require().Equal(big.NewInt(100), transfers[0].Value) + s.Require().Empty(erc20Transfers) + sawTrace = true + return ctx.Context().Err() + }, + })) + + ctx := s.Network.GetContext().WithBlockGasMeter(storetypes.NewGasMeter(1e6)) + err := s.Network.App.GetBankKeeper().MintCoins(ctx, "mint", sdk.NewCoins(sdk.NewCoin("aatom", sdkmath.NewInt(3e18)))) + s.Require().NoError(err) + err = s.Network.App.GetBankKeeper().SendCoinsFromModuleToModule(ctx, "mint", "fee_collector", sdk.NewCoins(sdk.NewCoin("aatom", sdkmath.NewInt(3e18)))) + s.Require().NoError(err) + + recipient := s.Keyring.GetAddr(1) + tx, err := s.Factory.GenerateSignedEthTx(s.Keyring.GetPrivKey(0), types.EvmTxArgs{ + To: &recipient, + Amount: big.NewInt(100), + }) + s.Require().NoError(err) + + ethMsg := tx.GetMsgs()[0].(*types.MsgEthereumTx) + res, err := evmKeeper.ApplyTransaction(ctx, ethMsg.AsTransaction()) + s.Require().NoError(err) + s.Require().False(res.Failed()) + s.Require().True(sawTrace) +} + +func (s *KeeperTestSuite) TestFailedTransactionRetainsApplicationTracingContext() { + s.SetupTest() + + type factoryContextKey struct{} + marker := &struct{}{} + var sawManager, sawFactoryValue bool + + evmKeeper := s.Network.App.GetEVMKeeper() + evmKeeper.SetApplicationTracerFactories( + func(ctx sdk.Context, _ vmtracer.ExecutionInfo) (sdk.Context, vmtracer.Tracer) { + return ctx.WithValue(factoryContextKey{}, marker), &applicationHooksTracer{hooks: &tracing.Hooks{}} + }, + ) + evmKeeper.SetHooks(keeper.NewMultiEvmHooks(&testHooks{ + postProcessing: func(ctx sdk.Context, _ common.Address, _ core.Message, receipt *gethtypes.Receipt) error { + if receipt.Status == gethtypes.ReceiptStatusFailed { + _, sawManager = vmtracer.FromContext(ctx) + sawFactoryValue = ctx.Value(factoryContextKey{}) == marker + } + return ctx.Context().Err() + }, + })) + + ctx := s.Network.GetContext().WithBlockGasMeter(storetypes.NewGasMeter(1e6)) + err := s.Network.App.GetBankKeeper().MintCoins(ctx, "mint", sdk.NewCoins(sdk.NewCoin("aatom", sdkmath.NewInt(3e18)))) + s.Require().NoError(err) + err = s.Network.App.GetBankKeeper().SendCoinsFromModuleToModule(ctx, "mint", "fee_collector", sdk.NewCoins(sdk.NewCoin("aatom", sdkmath.NewInt(3e18)))) + s.Require().NoError(err) + + senderBalance := s.Network.App.GetBankKeeper().GetBalance(ctx, s.Keyring.GetAccAddr(0), types.GetEVMCoinDenom()).Amount + recipient := s.Keyring.GetAddr(1) + tx, err := s.Factory.GenerateSignedEthTx(s.Keyring.GetPrivKey(0), types.EvmTxArgs{ + To: &recipient, + Amount: senderBalance.AddRaw(100).BigInt(), + }) + s.Require().NoError(err) + + ethMsg := tx.GetMsgs()[0].(*types.MsgEthereumTx) + res, err := evmKeeper.ApplyTransaction(ctx, ethMsg.AsTransaction()) + s.Require().NoError(err) + s.Require().True(res.Failed()) + s.Require().True(sawManager) + s.Require().True(sawFactoryValue) +} + func (s *KeeperTestSuite) TestApplyTransactionWithTxPostProcessing() { s.EnableFeemarket = true defer func() { s.EnableFeemarket = false }() diff --git a/x/vm/keeper/call_evm.go b/x/vm/keeper/call_evm.go index 82d4dfcce..0b6b454f6 100644 --- a/x/vm/keeper/call_evm.go +++ b/x/vm/keeper/call_evm.go @@ -1,23 +1,27 @@ package keeper import ( + "errors" "math/big" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" "github.com/cosmos/evm/server/config" evmtrace "github.com/cosmos/evm/trace" "github.com/cosmos/evm/x/vm/statedb" + vmtracer "github.com/cosmos/evm/x/vm/tracer" "github.com/cosmos/evm/x/vm/types" errorsmod "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" ) // CallEVM performs a smart contract method call using given args. @@ -88,8 +92,10 @@ func (k Keeper) CallEVMWithData(ctx sdk.Context, stateDB *statedb.StateDB, from Data: data, AccessList: ethtypes.AccessList{}, } + txConfig := statedb.NewEmptyTxConfig() + ctx, tracingHooks := k.prepareCallEVMTracing(ctx, stateDB, msg, txConfig, commit) - res, err := k.ApplyMessage(ctx, stateDB, msg, nil, commit, callFromPrecompile, true) + res, err := k.ApplyMessage(ctx, stateDB, msg, tracingHooks, commit, callFromPrecompile, true) if err != nil { return nil, err } @@ -103,3 +109,40 @@ func (k Keeper) CallEVMWithData(ctx sdk.Context, stateDB *statedb.StateDB, from return res, nil } + +// CallEVMViewWithData performs an isolated, non-committing EVM call and caps the +// execution at the caller's remaining gas. The isolated call starts its own +// application-tracing lifecycle instead of inheriting the caller's manager. +func (k *Keeper) CallEVMViewWithData(ctx sdk.Context, from common.Address, contract *common.Address, data []byte, gasCap *big.Int) (_ *types.MsgEthereumTxResponse, err error) { + remainingGas := ctx.GasMeter().GasRemaining() + if remainingGas == 0 { + return nil, errorsmod.Wrap(sdkerrors.ErrOutOfGas, "no gas remaining for EVM view call") + } + + effectiveGasCap := new(big.Int).SetUint64(min(remainingGas, config.DefaultGasCap)) + if gasCap != nil && gasCap.Sign() > 0 && gasCap.Cmp(effectiveGasCap) < 0 { + effectiveGasCap.Set(gasCap) + } + limitedByParent := effectiveGasCap.IsUint64() && effectiveGasCap.Uint64() == remainingGas + + execCtx := vmtracer.WithoutManager(buildTraceCtx(ctx, remainingGas)) + stateDB := statedb.New(execCtx, k, statedb.NewEmptyTxConfig()) + + res, err := k.CallEVMWithData(execCtx, stateDB, from, contract, data, false, false, effectiveGasCap) + if err != nil { + parentOutOfGas := limitedByParent && + (res != nil && res.VmError == vm.ErrOutOfGas.Error() || + res == nil && (errors.Is(err, core.ErrIntrinsicGas) || errors.Is(err, core.ErrFloorDataGas))) + if parentOutOfGas { + ctx.GasMeter().ConsumeGas(remainingGas, "apply evm message") + return res, errorsmod.Wrap(sdkerrors.ErrOutOfGas, err.Error()) + } + if res != nil { + ctx.GasMeter().ConsumeGas(res.GasUsed, "apply evm message") + } + return res, err + } + + ctx.GasMeter().ConsumeGas(res.GasUsed, "apply evm message") + return res, nil +} diff --git a/x/vm/keeper/call_evm_tracing.go b/x/vm/keeper/call_evm_tracing.go new file mode 100644 index 000000000..fd071a77c --- /dev/null +++ b/x/vm/keeper/call_evm_tracing.go @@ -0,0 +1,39 @@ +package keeper + +import ( + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/tracing" + + "github.com/cosmos/evm/x/vm/statedb" + vmtracer "github.com/cosmos/evm/x/vm/tracer" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// prepareCallEVMTracing reuses the lifecycle of an enclosing EVM execution or +// prepares a new lifecycle for a top-level native EVM call. +func (k Keeper) prepareCallEVMTracing( + ctx sdk.Context, + stateDB *statedb.StateDB, + msg core.Message, + txConfig statedb.TxConfig, + commit bool, +) (sdk.Context, *tracing.Hooks) { + if stateDB == nil { + return ctx, nil + } + if manager, ok := vmtracer.FromContext(stateDB.GetContext()); ok { + ctx = vmtracer.WithManager(ctx, manager) + return ctx, manager.NestedHooks() + } + + if manager, ok := vmtracer.FromContext(ctx); ok { + stateDB.AttachTracerManager(manager) + return ctx, manager.NestedHooks() + } + + ctx, hooks := k.prepareTracing(ctx, msg, txConfig, commit) + manager, _ := vmtracer.FromContext(ctx) + stateDB.AttachTracerManager(manager) + return ctx, hooks +} diff --git a/x/vm/keeper/call_evm_tracing_test.go b/x/vm/keeper/call_evm_tracing_test.go new file mode 100644 index 000000000..566538a8b --- /dev/null +++ b/x/vm/keeper/call_evm_tracing_test.go @@ -0,0 +1,115 @@ +package keeper + +import ( + "context" + "testing" + + "github.com/ethereum/go-ethereum/core" + "github.com/stretchr/testify/require" + + "github.com/cosmos/evm/x/vm/statedb" + vmtracer "github.com/cosmos/evm/x/vm/tracer" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func TestPrepareCallEVMTracingReusesContextManager(t *testing.T) { + k := Keeper{} + ctx := sdk.Context{}.WithContext(context.Background()).WithEventManager(sdk.NewEventManager()) + stateDB := statedb.New(ctx, nil, statedb.NewEmptyTxConfig()) + manager := vmtracer.New(nil) + ctx = vmtracer.WithManager(ctx, manager) + factoryCalls := 0 + k.SetApplicationTracerFactories(func(factoryCtx sdk.Context, _ vmtracer.ExecutionInfo) (sdk.Context, vmtracer.Tracer) { + factoryCalls++ + return factoryCtx, nil + }) + + actualCtx, hooks := k.prepareCallEVMTracing(ctx, stateDB, core.Message{}, statedb.NewEmptyTxConfig(), true) + + actualManager, ok := vmtracer.FromContext(actualCtx) + require.True(t, ok) + require.Same(t, manager, actualManager) + require.Same(t, manager.NestedHooks(), hooks) + stateDBManager, ok := vmtracer.FromContext(stateDB.GetContext()) + require.True(t, ok) + require.Same(t, manager, stateDBManager) + require.Zero(t, factoryCalls) +} + +func TestPrepareCallEVMTracingPrefersStateDBManager(t *testing.T) { + type contextKey string + const callValueKey contextKey = "call-value" + callValue := &struct{}{} + + k := Keeper{} + stateDBManager := vmtracer.New(nil) + stateDBCtx := vmtracer.WithManager( + sdk.Context{}.WithContext(context.Background()).WithEventManager(sdk.NewEventManager()), + stateDBManager, + ) + stateDB := statedb.New(stateDBCtx, nil, statedb.NewEmptyTxConfig()) + + callManager := vmtracer.New(nil) + callCtx := vmtracer.WithManager( + sdk.Context{}.WithContext(context.Background()).WithValue(callValueKey, callValue), + callManager, + ) + factoryCalls := 0 + k.SetApplicationTracerFactories(func(factoryCtx sdk.Context, _ vmtracer.ExecutionInfo) (sdk.Context, vmtracer.Tracer) { + factoryCalls++ + return factoryCtx, nil + }) + + actualCtx, hooks := k.prepareCallEVMTracing(callCtx, stateDB, core.Message{}, statedb.NewEmptyTxConfig(), true) + + actualManager, ok := vmtracer.FromContext(actualCtx) + require.True(t, ok) + require.Same(t, stateDBManager, actualManager) + require.NotSame(t, callManager, actualManager) + require.Same(t, stateDBManager.NestedHooks(), hooks) + require.Same(t, callValue, actualCtx.Value(callValueKey)) + require.Zero(t, factoryCalls) +} + +func TestPrepareCallEVMTracingReusesManagerWithoutCommit(t *testing.T) { + k := Keeper{} + manager := vmtracer.New(nil) + ctx := vmtracer.WithManager( + sdk.Context{}.WithContext(context.Background()).WithEventManager(sdk.NewEventManager()), + manager, + ) + stateDB := statedb.New(ctx, nil, statedb.NewEmptyTxConfig()) + factoryCalls := 0 + k.SetApplicationTracerFactories(func(factoryCtx sdk.Context, _ vmtracer.ExecutionInfo) (sdk.Context, vmtracer.Tracer) { + factoryCalls++ + return factoryCtx, nil + }) + + _, hooks := k.prepareCallEVMTracing(ctx, stateDB, core.Message{}, statedb.NewEmptyTxConfig(), false) + + require.Same(t, manager.NestedHooks(), hooks) + require.Zero(t, factoryCalls) +} + +func TestPrepareCallEVMTracingCreatesManagerWithoutCommit(t *testing.T) { + k := Keeper{} + ctx := sdk.Context{}.WithContext(context.Background()).WithEventManager(sdk.NewEventManager()) + stateDB := statedb.New(ctx, nil, statedb.NewEmptyTxConfig()) + factoryCalls := 0 + k.SetApplicationTracerFactories(func(factoryCtx sdk.Context, execution vmtracer.ExecutionInfo) (sdk.Context, vmtracer.Tracer) { + factoryCalls++ + require.False(t, execution.Commit) + return factoryCtx, nil + }) + + actualCtx, hooks := k.prepareCallEVMTracing(ctx, stateDB, core.Message{}, statedb.NewEmptyTxConfig(), false) + + require.NotNil(t, hooks) + require.Equal(t, 1, factoryCalls) + manager, ok := vmtracer.FromContext(actualCtx) + require.True(t, ok) + stateDBManager, ok := vmtracer.FromContext(stateDB.GetContext()) + require.True(t, ok) + require.Same(t, manager, stateDBManager) +} diff --git a/x/vm/keeper/grpc_query.go b/x/vm/keeper/grpc_query.go index 8f8ff567f..cb6783c4d 100644 --- a/x/vm/keeper/grpc_query.go +++ b/x/vm/keeper/grpc_query.go @@ -289,10 +289,11 @@ func (k Keeper) EthCall(c context.Context, req *types.EthCallRequest) (_ *types. msg := args.ToMessage(cfg.BaseFee, false, false) txConfig := statedb.NewEmptyTxConfig() + ctx, tracingHooks := k.prepareTracing(ctx, *msg, txConfig, false) // pass false to not commit StateDB stateDB := statedb.New(ctx, &k, txConfig) - res, err := k.ApplyMessageWithConfig(ctx, stateDB, *msg, nil, false, false, cfg, txConfig, false, overrides) + res, err := k.ApplyMessageWithConfig(ctx, stateDB, *msg, tracingHooks, false, false, cfg, txConfig, false, overrides) if err != nil { return nil, status.Error(codes.Internal, err.Error()) } diff --git a/x/vm/keeper/keeper.go b/x/vm/keeper/keeper.go index 6b7b95643..71a963c7d 100644 --- a/x/vm/keeper/keeper.go +++ b/x/vm/keeper/keeper.go @@ -18,6 +18,7 @@ import ( evmtrace "github.com/cosmos/evm/trace" "github.com/cosmos/evm/utils" "github.com/cosmos/evm/x/vm/statedb" + vmtracer "github.com/cosmos/evm/x/vm/tracer" "github.com/cosmos/evm/x/vm/types" "github.com/cosmos/evm/x/vm/wrappers" @@ -74,6 +75,8 @@ type Keeper struct { // Tracer used to collect execution traces from the EVM transaction execution tracer string + // applicationTracerFactories create application-provided, per-execution tracers. + applicationTracerFactories []vmtracer.ApplicationTracerFactory hooks types.EvmHooks // EVM Hooks for tx post-processing diff --git a/x/vm/keeper/state_transition.go b/x/vm/keeper/state_transition.go index fb6c6b6da..cbd19cb6a 100644 --- a/x/vm/keeper/state_transition.go +++ b/x/vm/keeper/state_transition.go @@ -230,10 +230,11 @@ func (k *Keeper) ApplyTransaction(ctx sdk.Context, tx *ethtypes.Transaction) (_ // Didn't use `Snapshot` because the context stack has exponential complexity on certain operations, // thus restricted to be used only inside `ApplyMessage`. tmpCtx, commitFn := ctx.CacheContext() + tmpCtx, tracingHooks := k.prepareTracing(tmpCtx, *msg, txConfig, true) // pass true to commit the StateDB stateDB := statedb.New(tmpCtx, k, txConfig) - res, err := k.ApplyMessageWithConfig(tmpCtx, stateDB, *msg, nil, true, false, cfg, txConfig, false, nil) + res, err := k.ApplyMessageWithConfig(tmpCtx, stateDB, *msg, tracingHooks, true, false, cfg, txConfig, false, nil) if err != nil { // when a transaction contains multiple msg, as long as one of the msg fails // all gas will be deducted. so is not msg.Gas() @@ -270,7 +271,9 @@ func (k *Keeper) ApplyTransaction(ctx sdk.Context, tx *ethtypes.Transaction) (_ // If the tx failed we discard the old context and create a new one, so // PostTxProcessing can persist data even if the tx fails. - tmpCtx, commitFn = ctx.CacheContext() + failedCtx, failedCommitFn := ctx.CacheContext() + tmpCtx = failedCtx.WithContext(tmpCtx.Context()) + commitFn = failedCommitFn } else { receipt.Status = ethtypes.ReceiptStatusSuccessful } diff --git a/x/vm/keeper/tracer_set.go b/x/vm/keeper/tracer_set.go new file mode 100644 index 000000000..119b9a371 --- /dev/null +++ b/x/vm/keeper/tracer_set.go @@ -0,0 +1,50 @@ +package keeper + +import ( + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/tracing" + + "github.com/cosmos/evm/x/vm/statedb" + vmtracer "github.com/cosmos/evm/x/vm/tracer" + "github.com/cosmos/evm/x/vm/types" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// SetApplicationTracerFactories configures application-provided tracers for +// every top-level EVM execution. Cosmos EVM installs no application tracer +// unless the embedding application calls this method. +func (k *Keeper) SetApplicationTracerFactories(factories ...vmtracer.ApplicationTracerFactory) *Keeper { + k.applicationTracerFactories = append([]vmtracer.ApplicationTracerFactory(nil), factories...) + return k +} + +func (k Keeper) prepareTracing( + ctx sdk.Context, + msg core.Message, + txConfig statedb.TxConfig, + commit bool, +) (sdk.Context, *tracing.Hooks) { + var base *tracing.Hooks + if k.tracer != "" { + base = k.Tracer(ctx, msg, types.GetEthChainConfig()) + } + + manager := vmtracer.New(base) + ctx = vmtracer.WithManager(ctx, manager) + execution := vmtracer.ExecutionInfo{ + Message: msg, + TxHash: txConfig.TxHash, + TxIndex: uint64(txConfig.TxIndex), + Commit: commit, + } + for _, factory := range k.applicationTracerFactories { + if factory == nil { + continue + } + var applicationTracer vmtracer.Tracer + ctx, applicationTracer = factory(ctx, execution) + manager.InstallApplication(applicationTracer) + } + return ctx, manager.Hooks() +} diff --git a/x/vm/keeper/tracer_set_test.go b/x/vm/keeper/tracer_set_test.go new file mode 100644 index 000000000..a8280d554 --- /dev/null +++ b/x/vm/keeper/tracer_set_test.go @@ -0,0 +1,86 @@ +package keeper + +import ( + "context" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/tracing" + "github.com/stretchr/testify/require" + + "github.com/cosmos/evm/x/vm/statedb" + vmtracer "github.com/cosmos/evm/x/vm/tracer" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +type testExecutionTracer struct { + hooks *tracing.Hooks +} + +func (t *testExecutionTracer) Hooks() *tracing.Hooks { return t.hooks } + +func TestPrepareTracingCreatesManagerWithoutApplicationFactories(t *testing.T) { + k := Keeper{} + ctx := sdk.Context{}.WithContext(context.Background()) + + actualCtx, hooks := k.prepareTracing(ctx, core.Message{}, statedb.NewEmptyTxConfig(), false) + require.NotNil(t, hooks) + _, ok := vmtracer.FromContext(actualCtx) + require.True(t, ok) +} + +func TestPrepareTracingInstallsApplicationFactories(t *testing.T) { + k := Keeper{} + ctx := sdk.Context{}.WithContext(context.Background()) + txConfig := statedb.NewTxConfig(common.HexToHash("0x1"), 7) + calls := 0 + + k.SetApplicationTracerFactories(func(factoryCtx sdk.Context, execution vmtracer.ExecutionInfo) (sdk.Context, vmtracer.Tracer) { + manager, ok := vmtracer.FromContext(factoryCtx) + require.True(t, ok) + require.NotNil(t, manager) + require.Equal(t, txConfig.TxHash, execution.TxHash) + require.Equal(t, uint64(txConfig.TxIndex), execution.TxIndex) + require.True(t, execution.Commit) + return factoryCtx, &testExecutionTracer{hooks: &tracing.Hooks{OnEnter: func(_ int, _ byte, _, _ common.Address, _ []byte, _ uint64, _ *big.Int) { + calls++ + }}} + }) + + actualCtx, hooks := k.prepareTracing(ctx, core.Message{}, txConfig, true) + require.NotNil(t, hooks) + _, ok := vmtracer.FromContext(actualCtx) + require.True(t, ok) + + hooks.OnEnter(0, 0, common.Address{}, common.Address{}, nil, 0, nil) + require.Equal(t, 1, calls) +} + +func TestPrepareTracingComposesMultipleApplicationTracers(t *testing.T) { + k := Keeper{} + ctx := sdk.Context{}.WithContext(context.Background()) + var factoryCalls, hookCalls []string + k.SetApplicationTracerFactories( + func(factoryCtx sdk.Context, _ vmtracer.ExecutionInfo) (sdk.Context, vmtracer.Tracer) { + factoryCalls = append(factoryCalls, "application-1") + return factoryCtx, &testExecutionTracer{hooks: &tracing.Hooks{OnEnter: func(_ int, _ byte, _, _ common.Address, _ []byte, _ uint64, _ *big.Int) { + hookCalls = append(hookCalls, "application-1") + }}} + }, + nil, + func(factoryCtx sdk.Context, _ vmtracer.ExecutionInfo) (sdk.Context, vmtracer.Tracer) { + factoryCalls = append(factoryCalls, "application-2") + return factoryCtx, &testExecutionTracer{hooks: &tracing.Hooks{OnEnter: func(_ int, _ byte, _, _ common.Address, _ []byte, _ uint64, _ *big.Int) { + hookCalls = append(hookCalls, "application-2") + }}} + }, + ) + + _, hooks := k.prepareTracing(ctx, core.Message{}, statedb.NewEmptyTxConfig(), false) + require.Equal(t, []string{"application-1", "application-2"}, factoryCalls) + hooks.OnEnter(0, 0, common.Address{}, common.Address{}, nil, 0, nil) + require.Equal(t, []string{"application-1", "application-2"}, hookCalls) +} diff --git a/x/vm/statedb/tracing.go b/x/vm/statedb/tracing.go new file mode 100644 index 000000000..daaf2f535 --- /dev/null +++ b/x/vm/statedb/tracing.go @@ -0,0 +1,16 @@ +package statedb + +import vmtracer "github.com/cosmos/evm/x/vm/tracer" + +// AttachTracerManager makes manager available to precompiles without replacing +// other execution-scoped values in the StateDB contexts. If the cache context +// has already been created, it is updated as well. +func (s *StateDB) AttachTracerManager(manager *vmtracer.Manager) { + if manager == nil { + return + } + s.ctx = vmtracer.WithManager(s.ctx, manager) + if s.writeCache != nil { + s.cacheCtx = vmtracer.WithManager(s.cacheCtx, manager) + } +} diff --git a/x/vm/statedb/tracing_test.go b/x/vm/statedb/tracing_test.go new file mode 100644 index 000000000..01f77d119 --- /dev/null +++ b/x/vm/statedb/tracing_test.go @@ -0,0 +1,55 @@ +package statedb_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/cosmos/evm/x/vm/statedb" + vmtracer "github.com/cosmos/evm/x/vm/tracer" + + storetypes "github.com/cosmos/cosmos-sdk/store/v2/types" + sdktestutil "github.com/cosmos/cosmos-sdk/testutil" +) + +func TestAttachTracerManager(t *testing.T) { + type contextKey string + const baseValueKey contextKey = "base-value" + baseValue := &struct{}{} + + key := storetypes.NewKVStoreKey("execution-context") + tkey := storetypes.NewTransientStoreKey("execution-context-transient") + ctx := sdktestutil.DefaultContext(key, tkey).WithValue(baseValueKey, baseValue) + manager := vmtracer.New(nil) + + db := statedb.New(ctx, NewMockKeeper(), emptyTxConfig) + baseStore := db.GetContext().MultiStore() + baseEvents := db.GetContext().EventManager() + baseGasMeter := db.GetContext().GasMeter() + + cacheCtx, err := db.GetCacheContext() + require.NoError(t, err) + cacheStore := cacheCtx.MultiStore() + cacheEvents := cacheCtx.EventManager() + cacheGasMeter := cacheCtx.GasMeter() + + db.AttachTracerManager(manager) + + actualManager, ok := vmtracer.FromContext(db.GetContext()) + require.True(t, ok) + require.Same(t, manager, actualManager) + require.Same(t, baseValue, db.GetContext().Value(baseValueKey)) + require.Same(t, baseStore, db.GetContext().MultiStore()) + require.Same(t, baseEvents, db.GetContext().EventManager()) + require.Same(t, baseGasMeter, db.GetContext().GasMeter()) + + cacheCtx, err = db.GetCacheContext() + require.NoError(t, err) + actualManager, ok = vmtracer.FromContext(cacheCtx) + require.True(t, ok) + require.Same(t, manager, actualManager) + require.Same(t, baseValue, cacheCtx.Value(baseValueKey)) + require.Same(t, cacheStore, cacheCtx.MultiStore()) + require.Same(t, cacheEvents, cacheCtx.EventManager()) + require.Same(t, cacheGasMeter, cacheCtx.GasMeter()) +} diff --git a/x/vm/tracer/application.go b/x/vm/tracer/application.go new file mode 100644 index 000000000..040f6c742 --- /dev/null +++ b/x/vm/tracer/application.go @@ -0,0 +1,27 @@ +package tracer + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// ExecutionInfo identifies one top-level EVM execution. +type ExecutionInfo struct { + Message core.Message + TxHash common.Hash + TxIndex uint64 + Commit bool +} + +// ApplicationTracerFactory creates a tracer for one top-level EVM execution. +// It may return a derived context to expose per-execution state to post-tx +// hooks. A new tracer is created for every traced execution. +type ApplicationTracerFactory func(ctx sdk.Context, execution ExecutionInfo) (sdk.Context, Tracer) + +// InstallApplication installs a tracer that observes the whole EVM execution. +// Application tracers normally live until the Manager is discarded. +func (m *Manager) InstallApplication(tracer Tracer) func() { + return m.install(tracer) +} diff --git a/x/vm/tracer/application/collector.go b/x/vm/tracer/application/collector.go new file mode 100644 index 000000000..84714e23d --- /dev/null +++ b/x/vm/tracer/application/collector.go @@ -0,0 +1,257 @@ +package application + +import ( + "bytes" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/tracing" + "github.com/ethereum/go-ethereum/core/vm" + + vmtracer "github.com/cosmos/evm/x/vm/tracer" +) + +// TxCallTouch records a call frame observed during EVM execution. +type TxCallTouch struct { + From common.Address + To common.Address + Depth int + CallType byte +} + +// TxValueTransfer records a native balance transfer observed during EVM execution. +type TxValueTransfer struct { + From common.Address + To common.Address + Depth int + CallType byte + Value *big.Int +} + +// TxERC20Transfer records a successful standard ERC20 transfer or transferFrom +// call. It represents the call arguments, not a token-contract-specific +// balance delta. +type TxERC20Transfer struct { + Caller common.Address + Token common.Address + From common.Address + To common.Address + Value *big.Int +} + +// Collector is the standard transaction-trace contract. Applications can +// provide their own collection policy while retaining GetTxTrace compatibility. +type Collector interface { + vmtracer.Tracer + Touches() []TxCallTouch + Transfers() []TxValueTransfer + ERC20Transfers() []TxERC20Transfer +} + +// txTraceCollector records transaction-wide call touches, successful native +// balance transfers, and successful standard ERC20 transfer calls. Entries +// created inside reverted frames are discarded. +type txTraceCollector struct { + touches []TxCallTouch + transfers []TxValueTransfer + erc20Transfers []TxERC20Transfer + + touchesFrameStart []int + transfersFrameStart []int + erc20Frames []erc20Frame +} + +type erc20Frame struct { + start int + transfer *TxERC20Transfer +} + +var ( + erc20TransferSelector = []byte{0xa9, 0x05, 0x9c, 0xbb} + erc20TransferFromSelector = []byte{0x23, 0xb8, 0x72, 0xdd} + zeroABIWord [common.HashLength]byte +) + +// NewCollector creates the default transaction-trace collector. +func NewCollector() Collector { + return &txTraceCollector{} +} + +// Hooks returns the EVM callbacks used by the collector. +func (c *txTraceCollector) Hooks() *tracing.Hooks { + if c == nil { + return nil + } + return &tracing.Hooks{ + OnEnter: c.onEnter, + OnExit: c.onExit, + } +} + +// Touches returns a copy of the collected call touches. +func (c *txTraceCollector) Touches() []TxCallTouch { + if c == nil { + return nil + } + return append([]TxCallTouch(nil), c.touches...) +} + +// Transfers returns a deep copy of the collected native value transfers. +func (c *txTraceCollector) Transfers() []TxValueTransfer { + if c == nil { + return nil + } + transfers := make([]TxValueTransfer, len(c.transfers)) + for i, transfer := range c.transfers { + transfers[i] = transfer + if transfer.Value != nil { + transfers[i].Value = new(big.Int).Set(transfer.Value) + } + } + return transfers +} + +// ERC20Transfers returns a deep copy of collected standard ERC20 transfer calls. +func (c *txTraceCollector) ERC20Transfers() []TxERC20Transfer { + if c == nil { + return nil + } + transfers := make([]TxERC20Transfer, len(c.erc20Transfers)) + for i, transfer := range c.erc20Transfers { + transfers[i] = transfer + if transfer.Value != nil { + transfers[i].Value = new(big.Int).Set(transfer.Value) + } + } + return transfers +} + +func isNativeBalanceTransfer(typ byte, value *big.Int) bool { + if value == nil || value.Sign() <= 0 { + return false + } + // Only CALL/CREATE/CREATE2 can move native balance between accounts. + // DELEGATECALL, STATICCALL, and CALLCODE can surface a value in tracing + // callbacks without transferring it to the callee. + return typ == byte(vm.CALL) || typ == byte(vm.CREATE) || typ == byte(vm.CREATE2) +} + +func (c *txTraceCollector) onEnter(depth int, typ byte, from, to common.Address, input []byte, _ uint64, value *big.Int) { + c.touchesFrameStart = append(c.touchesFrameStart, len(c.touches)) + c.transfersFrameStart = append(c.transfersFrameStart, len(c.transfers)) + c.erc20Frames = append(c.erc20Frames, erc20Frame{ + start: len(c.erc20Transfers), + transfer: parseERC20Transfer(typ, from, to, input), + }) + + c.touches = append(c.touches, TxCallTouch{ + From: from, + To: to, + Depth: depth, + CallType: typ, + }) + + if isNativeBalanceTransfer(typ, value) { + c.transfers = append(c.transfers, TxValueTransfer{ + From: from, + To: to, + Depth: depth, + CallType: typ, + Value: new(big.Int).Set(value), + }) + } +} + +func (c *txTraceCollector) onExit(_ int, output []byte, _ uint64, _ error, reverted bool) { + if reverted { + if n := len(c.touchesFrameStart); n > 0 { + start := c.touchesFrameStart[n-1] + if start >= 0 && start <= len(c.touches) { + c.touches = c.touches[:start] + } + } + if n := len(c.transfersFrameStart); n > 0 { + start := c.transfersFrameStart[n-1] + if start >= 0 && start <= len(c.transfers) { + c.transfers = c.transfers[:start] + } + } + if n := len(c.erc20Frames); n > 0 { + start := c.erc20Frames[n-1].start + if start >= 0 && start <= len(c.erc20Transfers) { + c.erc20Transfers = c.erc20Transfers[:start] + } + } + } else if n := len(c.erc20Frames); n > 0 { + frame := c.erc20Frames[n-1] + if frame.transfer != nil && !isFalseERC20Return(output) { + c.erc20Transfers = append(c.erc20Transfers, *frame.transfer) + } + } + + if n := len(c.touchesFrameStart); n > 0 { + c.touchesFrameStart = c.touchesFrameStart[:n-1] + } + if n := len(c.transfersFrameStart); n > 0 { + c.transfersFrameStart = c.transfersFrameStart[:n-1] + } + if n := len(c.erc20Frames); n > 0 { + c.erc20Frames = c.erc20Frames[:n-1] + } +} + +func parseERC20Transfer(typ byte, caller, token common.Address, input []byte) *TxERC20Transfer { + if typ != byte(vm.CALL) || len(input) < 4 { + return nil + } + + switch { + case bytes.Equal(input[:4], erc20TransferSelector): + if len(input) < 4+32+32 { + return nil + } + to, ok := abiAddress(input[4 : 4+32]) + if !ok { + return nil + } + return &TxERC20Transfer{ + Caller: caller, + Token: token, + From: caller, + To: to, + Value: new(big.Int).SetBytes(input[4+32 : 4+64]), + } + case bytes.Equal(input[:4], erc20TransferFromSelector): + if len(input) < 4+32+32+32 { + return nil + } + from, ok := abiAddress(input[4 : 4+32]) + if !ok { + return nil + } + to, ok := abiAddress(input[4+32 : 4+64]) + if !ok { + return nil + } + return &TxERC20Transfer{ + Caller: caller, + Token: token, + From: from, + To: to, + Value: new(big.Int).SetBytes(input[4+64 : 4+96]), + } + default: + return nil + } +} + +func abiAddress(word []byte) (common.Address, bool) { + if len(word) != common.HashLength || !bytes.Equal(word[:12], zeroABIWord[:12]) { + return common.Address{}, false + } + return common.BytesToAddress(word[12:]), true +} + +func isFalseERC20Return(output []byte) bool { + return len(output) >= common.HashLength && bytes.Equal(output[:common.HashLength], zeroABIWord[:]) +} diff --git a/x/vm/tracer/application/collector_test.go b/x/vm/tracer/application/collector_test.go new file mode 100644 index 000000000..2a9ddd942 --- /dev/null +++ b/x/vm/tracer/application/collector_test.go @@ -0,0 +1,110 @@ +package application + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/stretchr/testify/require" +) + +func erc20Input(selector []byte, words ...[]byte) []byte { + input := append([]byte(nil), selector...) + for _, word := range words { + input = append(input, common.LeftPadBytes(word, common.HashLength)...) + } + return input +} + +func TestCollectorCollectsSuccessfulTouchesAndTransfers(t *testing.T) { + collector := NewCollector() + hooks := collector.Hooks() + from := common.HexToAddress("0x1") + to := common.HexToAddress("0x2") + rootValue := big.NewInt(10) + + hooks.OnEnter(0, byte(vm.CALL), from, to, nil, 0, rootValue) + rootValue.SetInt64(99) + + hooks.OnEnter(1, byte(vm.DELEGATECALL), to, from, nil, 0, big.NewInt(7)) + hooks.OnExit(1, nil, 0, nil, false) + + hooks.OnEnter(1, byte(vm.CREATE), to, common.Address{}, nil, 0, big.NewInt(3)) + hooks.OnEnter(2, byte(vm.CALL), to, from, nil, 0, big.NewInt(4)) + hooks.OnExit(2, nil, 0, nil, false) + hooks.OnExit(1, nil, 0, nil, true) + hooks.OnExit(0, nil, 0, nil, false) + + touches := collector.Touches() + require.Len(t, touches, 2) + require.Equal(t, byte(vm.CALL), touches[0].CallType) + require.Equal(t, byte(vm.DELEGATECALL), touches[1].CallType) + + transfers := collector.Transfers() + require.Len(t, transfers, 1) + require.Equal(t, int64(10), transfers[0].Value.Int64()) + + transfers[0].Value.SetInt64(123) + require.Equal(t, int64(10), collector.Transfers()[0].Value.Int64()) + touches[0].Depth = 42 + require.Equal(t, 0, collector.Touches()[0].Depth) +} + +func TestCollectorDropsRevertedRoot(t *testing.T) { + collector := NewCollector() + hooks := collector.Hooks() + + hooks.OnEnter(0, byte(vm.CALL), common.Address{}, common.Address{}, nil, 0, big.NewInt(1)) + hooks.OnEnter(1, byte(vm.STATICCALL), common.Address{}, common.Address{}, nil, 0, nil) + hooks.OnExit(1, nil, 0, nil, false) + hooks.OnExit(0, nil, 0, nil, true) + + require.Empty(t, collector.Touches()) + require.Empty(t, collector.Transfers()) + require.Empty(t, collector.ERC20Transfers()) +} + +func TestCollectorCollectsSuccessfulERC20Calls(t *testing.T) { + collector := NewCollector() + hooks := collector.Hooks() + caller := common.HexToAddress("0x1001") + token := common.HexToAddress("0x2002") + from := common.HexToAddress("0x3003") + to := common.HexToAddress("0x4004") + input := erc20Input(erc20TransferFromSelector, from.Bytes(), to.Bytes(), big.NewInt(9).Bytes()) + + // A false ABI return does not represent a successful transfer. + hooks.OnEnter(0, byte(vm.CALL), caller, token, input, 0, big.NewInt(0)) + hooks.OnExit(0, make([]byte, common.HashLength), 0, nil, false) + require.Empty(t, collector.ERC20Transfers()) + + // A successful nested call is discarded when the enclosing frame reverts. + hooks.OnEnter(0, byte(vm.CALL), caller, common.HexToAddress("0x5005"), nil, 0, big.NewInt(0)) + hooks.OnEnter(1, byte(vm.CALL), caller, token, input, 0, big.NewInt(0)) + hooks.OnExit(1, nil, 0, nil, false) + hooks.OnExit(0, nil, 0, nil, true) + require.Empty(t, collector.ERC20Transfers()) + + // Empty return data is accepted for ERC20 implementations that do not return a bool. + hooks.OnEnter(0, byte(vm.CALL), caller, token, input, 0, big.NewInt(0)) + hooks.OnExit(0, nil, 0, nil, false) + transfers := collector.ERC20Transfers() + require.Len(t, transfers, 1) + require.Equal(t, caller, transfers[0].Caller) + require.Equal(t, token, transfers[0].Token) + require.Equal(t, from, transfers[0].From) + require.Equal(t, to, transfers[0].To) + require.Equal(t, int64(9), transfers[0].Value.Int64()) + + transfers[0].Value.SetInt64(99) + require.Equal(t, int64(9), collector.ERC20Transfers()[0].Value.Int64()) +} + +func TestCollectorNilReceiver(t *testing.T) { + var collector *txTraceCollector + require.Nil(t, collector.Hooks()) + require.Nil(t, collector.Touches()) + require.Nil(t, collector.Transfers()) + require.Nil(t, collector.ERC20Transfers()) +} diff --git a/x/vm/tracer/application/doc.go b/x/vm/tracer/application/doc.go new file mode 100644 index 000000000..92ed08b34 --- /dev/null +++ b/x/vm/tracer/application/doc.go @@ -0,0 +1,13 @@ +// Package application provides an optional transaction-wide collector for +// call touches, native value transfers, and standard ERC-20 transfer calls. +// +// Register the collector while wiring the application: +// +// evmKeeper.SetApplicationTracerFactories(application.TxTraceFactory) +// +// Its result is available from the execution context in PostTxProcessing: +// +// touches, transfers, erc20Transfers := application.GetTxTrace( +// ctx, uint64(receipt.TransactionIndex), +// ) +package application diff --git a/x/vm/tracer/application/tx_trace.go b/x/vm/tracer/application/tx_trace.go new file mode 100644 index 000000000..1808b2e9c --- /dev/null +++ b/x/vm/tracer/application/tx_trace.go @@ -0,0 +1,45 @@ +package application + +import ( + vmtracer "github.com/cosmos/evm/x/vm/tracer" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +type txTraceContextKey struct{} + +type txTraceContext struct { + txIndex uint64 + collector Collector +} + +// TxTraceFactory creates a standard transaction-wide collector. Applications +// can pass this function to keeper.SetApplicationTracerFactories to opt in to +// call-touch, native-transfer, and standard ERC20 transfer-call collection. +func TxTraceFactory(ctx sdk.Context, execution vmtracer.ExecutionInfo) (sdk.Context, vmtracer.Tracer) { + collector := NewCollector() + return WithCollector(ctx, execution.TxIndex, collector), collector +} + +// WithCollector stores a collector for the current EVM transaction in ctx. The +// value is available only through the returned execution context and contexts +// derived from it; committing an SDK cache context does not copy it to the +// parent context. +func WithCollector(ctx sdk.Context, txIndex uint64, collector Collector) sdk.Context { + return ctx.WithValue(txTraceContextKey{}, txTraceContext{ + txIndex: txIndex, + collector: collector, + }) +} + +// GetTxTrace returns call touches, native value transfers, and standard ERC20 +// transfer calls collected for txIndex in the current EVM execution context. +// It returns nil slices when the context has no trace or belongs to a different +// transaction. +func GetTxTrace(ctx sdk.Context, txIndex uint64) (touches []TxCallTouch, transfers []TxValueTransfer, erc20Transfers []TxERC20Transfer) { + trace, ok := ctx.Value(txTraceContextKey{}).(txTraceContext) + if !ok || trace.txIndex != txIndex || trace.collector == nil { + return nil, nil, nil + } + return trace.collector.Touches(), trace.collector.Transfers(), trace.collector.ERC20Transfers() +} diff --git a/x/vm/tracer/application/tx_trace_test.go b/x/vm/tracer/application/tx_trace_test.go new file mode 100644 index 000000000..48b7e157c --- /dev/null +++ b/x/vm/tracer/application/tx_trace_test.go @@ -0,0 +1,77 @@ +package application + +import ( + "context" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/stretchr/testify/require" + + vmtracer "github.com/cosmos/evm/x/vm/tracer" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func TestTxTraceContext(t *testing.T) { + parent := sdk.Context{}.WithContext(context.Background()) + collector := NewCollector() + ctx := WithCollector(parent, 7, collector) + + collector.Hooks().OnEnter( + 0, + byte(vm.CALL), + common.HexToAddress("0x1"), + common.HexToAddress("0x2"), + nil, + 0, + big.NewInt(10), + ) + + touches, transfers, erc20Transfers := GetTxTrace(ctx, 7) + require.Len(t, touches, 1) + require.Len(t, transfers, 1) + require.Empty(t, erc20Transfers) + + touches, transfers, erc20Transfers = GetTxTrace(ctx, 8) + require.Nil(t, touches) + require.Nil(t, transfers) + require.Nil(t, erc20Transfers) + + touches, transfers, erc20Transfers = GetTxTrace(parent, 7) + require.Nil(t, touches) + require.Nil(t, transfers) + require.Nil(t, erc20Transfers) +} + +func TestTxTraceContextWithNilCollector(t *testing.T) { + ctx := sdk.Context{}.WithContext(context.Background()) + ctx = WithCollector(ctx, 0, nil) + + touches, transfers, erc20Transfers := GetTxTrace(ctx, 0) + require.Nil(t, touches) + require.Nil(t, transfers) + require.Nil(t, erc20Transfers) +} + +func TestTxTraceFactory(t *testing.T) { + ctx := sdk.Context{}.WithContext(context.Background()) + ctx, tracer := TxTraceFactory(ctx, vmtracer.ExecutionInfo{TxIndex: 3}) + require.NotNil(t, tracer) + + tracer.Hooks().OnEnter( + 0, + byte(vm.CALL), + common.HexToAddress("0x1"), + common.HexToAddress("0x2"), + nil, + 0, + big.NewInt(1), + ) + + touches, transfers, erc20Transfers := GetTxTrace(ctx, 3) + require.Len(t, touches, 1) + require.Len(t, transfers, 1) + require.Empty(t, erc20Transfers) +} diff --git a/x/vm/tracer/context.go b/x/vm/tracer/context.go new file mode 100644 index 000000000..87e3ce4df --- /dev/null +++ b/x/vm/tracer/context.go @@ -0,0 +1,22 @@ +package tracer + +import sdk "github.com/cosmos/cosmos-sdk/types" + +type managerContextKey struct{} + +// WithManager stores the manager in an SDK context so nested native EVM calls +// can reuse the enclosing execution's tracing lifecycle. +func WithManager(ctx sdk.Context, manager *Manager) sdk.Context { + return ctx.WithValue(managerContextKey{}, manager) +} + +// WithoutManager returns a context that does not inherit its parent's manager. +func WithoutManager(ctx sdk.Context) sdk.Context { + return ctx.WithValue(managerContextKey{}, (*Manager)(nil)) +} + +// FromContext returns the tracing manager associated with an EVM execution. +func FromContext(ctx sdk.Context) (*Manager, bool) { + manager, ok := ctx.Value(managerContextKey{}).(*Manager) + return manager, ok && manager != nil +} diff --git a/x/vm/tracer/docs.go b/x/vm/tracer/docs.go new file mode 100644 index 000000000..0ef494ddb --- /dev/null +++ b/x/vm/tracer/docs.go @@ -0,0 +1,17 @@ +// Package tracer composes geth tracing hooks with application-level tracers. +// +// Applications register one or more factories while wiring the keeper: +// +// evmKeeper.SetApplicationTracerFactories( +// application.TxTraceFactory, +// myPolicyTracerFactory, +// ) +// +// Each top-level EVM execution receives a new Manager. Nested CallEVM calls +// reuse that manager, while CallEVMViewWithData starts an isolated lifecycle. +// A post-transaction hook can read the built-in collector with: +// +// touches, transfers, erc20Transfers := application.GetTxTrace( +// ctx, uint64(receipt.TransactionIndex), +// ) +package tracer diff --git a/x/vm/tracer/manager.go b/x/vm/tracer/manager.go new file mode 100644 index 000000000..6212fd2e1 --- /dev/null +++ b/x/vm/tracer/manager.go @@ -0,0 +1,418 @@ +package tracer + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/tracing" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/params" +) + +// Manager dispatches EVM tracing events to a base tracer and application-level +// tracers. A Manager belongs to one EVM execution and is not safe for concurrent +// use, matching vm.EVM's concurrency guarantees. +type Manager struct { + hooks *tracing.Hooks + nestedHooks *tracing.Hooks + base *tracing.Hooks + currentDepth int + nestedDepthOffsets []int + + nextID uint64 + applications []installedHooks +} + +type installedHooks struct { + id uint64 + hooks tracing.Hooks +} + +// New creates a tracing manager. base is invoked before application tracers and +// may be nil. The returned Hooks pointers remain stable for the manager's +// lifetime. +func New(base *tracing.Hooks) *Manager { + m := &Manager{ + hooks: &tracing.Hooks{}, + nestedHooks: &tracing.Hooks{}, + currentDepth: -1, + } + if base != nil { + baseCopy := *base + m.base = &baseCopy + } + m.rebuild() + return m +} + +// Hooks returns the stable dispatcher hooks to install in vm.Config.Tracer. +func (m *Manager) Hooks() *tracing.Hooks { + if m == nil { + return nil + } + return m.hooks +} + +// NestedHooks returns a stable dispatcher for a nested EVM execution. It +// dispatches the same call, opcode, and state events as Hooks, but omits the +// transaction boundary owned by the enclosing execution. +func (m *Manager) NestedHooks() *tracing.Hooks { + if m == nil { + return nil + } + return m.nestedHooks +} + +func (m *Manager) install(tracer Tracer) func() { + if m == nil || tracer == nil { + return func() {} + } + + hooks := tracer.Hooks() + if hooks == nil { + return func() {} + } + + m.nextID++ + entry := installedHooks{id: m.nextID, hooks: *hooks} + m.applications = append(m.applications, entry) + m.rebuild() + + closed := false + return func() { + if closed { + return + } + closed = true + m.remove(entry.id) + } +} + +func (m *Manager) remove(id uint64) { + for i := range m.applications { + if m.applications[i].id != id { + continue + } + copy(m.applications[i:], m.applications[i+1:]) + m.applications = m.applications[:len(m.applications)-1] + m.rebuild() + return + } +} + +func (m *Manager) rebuild() { + ordered := make([]tracing.Hooks, 0, 1+len(m.applications)) + if m.base != nil { + ordered = append(ordered, *m.base) + } + for _, entry := range m.applications { + ordered = append(ordered, entry.hooks) + } + + composed := compose(ordered) + *m.hooks = m.withDepthTracking(composed) + composed.OnTxStart = nil + composed.OnTxEnd = nil + *m.nestedHooks = m.withNestedDepthOffset(composed) +} + +func (m *Manager) withDepthTracking(hooks tracing.Hooks) tracing.Hooks { + out := hooks + out.OnEnter = func(depth int, typ byte, from, to common.Address, input []byte, gas uint64, value *big.Int) { + m.currentDepth = depth + if hooks.OnEnter != nil { + hooks.OnEnter(depth, typ, from, to, input, gas, value) + } + } + out.OnExit = func(depth int, output []byte, gasUsed uint64, err error, reverted bool) { + if hooks.OnExit != nil { + hooks.OnExit(depth, output, gasUsed, err, reverted) + } + m.currentDepth = depth - 1 + } + return out +} + +func (m *Manager) withNestedDepthOffset(hooks tracing.Hooks) tracing.Hooks { + out := hooks + out.OnEnter = func(depth int, typ byte, from, to common.Address, input []byte, gas uint64, value *big.Int) { + if depth == 0 { + m.nestedDepthOffsets = append(m.nestedDepthOffsets, m.currentDepth+1) + } + depth += m.nestedDepthOffset() + m.currentDepth = depth + if hooks.OnEnter != nil { + hooks.OnEnter(depth, typ, from, to, input, gas, value) + } + } + out.OnExit = func(depth int, output []byte, gasUsed uint64, err error, reverted bool) { + nestedRoot := depth == 0 + depth += m.nestedDepthOffset() + if hooks.OnExit != nil { + hooks.OnExit(depth, output, gasUsed, err, reverted) + } + m.currentDepth = depth - 1 + if nestedRoot && len(m.nestedDepthOffsets) > 0 { + m.nestedDepthOffsets = m.nestedDepthOffsets[:len(m.nestedDepthOffsets)-1] + } + } + if hooks.OnOpcode != nil { + out.OnOpcode = func(pc uint64, op byte, gas, cost uint64, scope tracing.OpContext, returnData []byte, depth int, err error) { + hooks.OnOpcode(pc, op, gas, cost, scope, returnData, depth+m.nestedDepthOffset(), err) + } + } + if hooks.OnFault != nil { + out.OnFault = func(pc uint64, op byte, gas, cost uint64, scope tracing.OpContext, depth int, err error) { + hooks.OnFault(pc, op, gas, cost, scope, depth+m.nestedDepthOffset(), err) + } + } + return out +} + +func (m *Manager) nestedDepthOffset() int { + if len(m.nestedDepthOffsets) == 0 { + return 0 + } + return m.nestedDepthOffsets[len(m.nestedDepthOffsets)-1] +} + +func compose(hooks []tracing.Hooks) tracing.Hooks { + var out tracing.Hooks + + if anyHook(hooks, func(h tracing.Hooks) bool { return h.OnTxStart != nil }) { + out.OnTxStart = func(vm *tracing.VMContext, tx *ethtypes.Transaction, from common.Address) { + for i := range hooks { + if fn := hooks[i].OnTxStart; fn != nil { + fn(vm, tx, from) + } + } + } + } + if anyHook(hooks, func(h tracing.Hooks) bool { return h.OnTxEnd != nil }) { + out.OnTxEnd = func(receipt *ethtypes.Receipt, err error) { + for i := range hooks { + if fn := hooks[i].OnTxEnd; fn != nil { + fn(receipt, err) + } + } + } + } + if anyHook(hooks, func(h tracing.Hooks) bool { return h.OnEnter != nil }) { + out.OnEnter = func(depth int, typ byte, from, to common.Address, input []byte, gas uint64, value *big.Int) { + for i := range hooks { + if fn := hooks[i].OnEnter; fn != nil { + fn(depth, typ, from, to, input, gas, value) + } + } + } + } + if anyHook(hooks, func(h tracing.Hooks) bool { return h.OnExit != nil }) { + out.OnExit = func(depth int, output []byte, gasUsed uint64, err error, reverted bool) { + for i := range hooks { + if fn := hooks[i].OnExit; fn != nil { + fn(depth, output, gasUsed, err, reverted) + } + } + } + } + if anyHook(hooks, func(h tracing.Hooks) bool { return h.OnOpcode != nil }) { + out.OnOpcode = func(pc uint64, op byte, gas, cost uint64, scope tracing.OpContext, returnData []byte, depth int, err error) { + for i := range hooks { + if fn := hooks[i].OnOpcode; fn != nil { + fn(pc, op, gas, cost, scope, returnData, depth, err) + } + } + } + } + if anyHook(hooks, func(h tracing.Hooks) bool { return h.OnFault != nil }) { + out.OnFault = func(pc uint64, op byte, gas, cost uint64, scope tracing.OpContext, depth int, err error) { + for i := range hooks { + if fn := hooks[i].OnFault; fn != nil { + fn(pc, op, gas, cost, scope, depth, err) + } + } + } + } + if anyHook(hooks, func(h tracing.Hooks) bool { return h.OnGasChange != nil }) { + out.OnGasChange = func(oldGas, newGas uint64, reason tracing.GasChangeReason) { + for i := range hooks { + if fn := hooks[i].OnGasChange; fn != nil { + fn(oldGas, newGas, reason) + } + } + } + } + if anyHook(hooks, func(h tracing.Hooks) bool { return h.OnBlockchainInit != nil }) { + out.OnBlockchainInit = func(config *params.ChainConfig) { + for i := range hooks { + if fn := hooks[i].OnBlockchainInit; fn != nil { + fn(config) + } + } + } + } + if anyHook(hooks, func(h tracing.Hooks) bool { return h.OnClose != nil }) { + out.OnClose = func() { + for i := range hooks { + if fn := hooks[i].OnClose; fn != nil { + fn() + } + } + } + } + if anyHook(hooks, func(h tracing.Hooks) bool { return h.OnBlockStart != nil }) { + out.OnBlockStart = func(event tracing.BlockEvent) { + for i := range hooks { + if fn := hooks[i].OnBlockStart; fn != nil { + fn(event) + } + } + } + } + if anyHook(hooks, func(h tracing.Hooks) bool { return h.OnBlockEnd != nil }) { + out.OnBlockEnd = func(err error) { + for i := range hooks { + if fn := hooks[i].OnBlockEnd; fn != nil { + fn(err) + } + } + } + } + if anyHook(hooks, func(h tracing.Hooks) bool { return h.OnSkippedBlock != nil }) { + out.OnSkippedBlock = func(event tracing.BlockEvent) { + for i := range hooks { + if fn := hooks[i].OnSkippedBlock; fn != nil { + fn(event) + } + } + } + } + if anyHook(hooks, func(h tracing.Hooks) bool { return h.OnGenesisBlock != nil }) { + out.OnGenesisBlock = func(genesis *ethtypes.Block, alloc ethtypes.GenesisAlloc) { + for i := range hooks { + if fn := hooks[i].OnGenesisBlock; fn != nil { + fn(genesis, alloc) + } + } + } + } + if anyHook(hooks, func(h tracing.Hooks) bool { return h.OnSystemCallStart != nil }) { + out.OnSystemCallStart = func() { + for i := range hooks { + if fn := hooks[i].OnSystemCallStart; fn != nil { + fn() + } + } + } + } + if anyHook(hooks, func(h tracing.Hooks) bool { return h.OnSystemCallStartV2 != nil }) { + out.OnSystemCallStartV2 = func(vm *tracing.VMContext) { + for i := range hooks { + if fn := hooks[i].OnSystemCallStartV2; fn != nil { + fn(vm) + } + } + } + } + if anyHook(hooks, func(h tracing.Hooks) bool { return h.OnSystemCallEnd != nil }) { + out.OnSystemCallEnd = func() { + for i := range hooks { + if fn := hooks[i].OnSystemCallEnd; fn != nil { + fn() + } + } + } + } + if anyHook(hooks, func(h tracing.Hooks) bool { return h.OnStateUpdate != nil }) { + out.OnStateUpdate = func(update *tracing.StateUpdate) { + for i := range hooks { + if fn := hooks[i].OnStateUpdate; fn != nil { + fn(update) + } + } + } + } + if anyHook(hooks, func(h tracing.Hooks) bool { return h.OnBalanceChange != nil }) { + out.OnBalanceChange = func(address common.Address, previous, current *big.Int, reason tracing.BalanceChangeReason) { + for i := range hooks { + if fn := hooks[i].OnBalanceChange; fn != nil { + fn(address, previous, current, reason) + } + } + } + } + if anyHook(hooks, func(h tracing.Hooks) bool { return h.OnNonceChange != nil }) { + out.OnNonceChange = func(address common.Address, previous, current uint64) { + for i := range hooks { + if fn := hooks[i].OnNonceChange; fn != nil { + fn(address, previous, current) + } + } + } + } + if anyHook(hooks, func(h tracing.Hooks) bool { return h.OnNonceChangeV2 != nil }) { + out.OnNonceChangeV2 = func(address common.Address, previous, current uint64, reason tracing.NonceChangeReason) { + for i := range hooks { + if fn := hooks[i].OnNonceChangeV2; fn != nil { + fn(address, previous, current, reason) + } + } + } + } + if anyHook(hooks, func(h tracing.Hooks) bool { return h.OnCodeChange != nil }) { + out.OnCodeChange = func(address common.Address, previousHash common.Hash, previousCode []byte, hash common.Hash, code []byte) { + for i := range hooks { + if fn := hooks[i].OnCodeChange; fn != nil { + fn(address, previousHash, previousCode, hash, code) + } + } + } + } + if anyHook(hooks, func(h tracing.Hooks) bool { return h.OnCodeChangeV2 != nil }) { + out.OnCodeChangeV2 = func(address common.Address, previousHash common.Hash, previousCode []byte, hash common.Hash, code []byte, reason tracing.CodeChangeReason) { + for i := range hooks { + if fn := hooks[i].OnCodeChangeV2; fn != nil { + fn(address, previousHash, previousCode, hash, code, reason) + } + } + } + } + if anyHook(hooks, func(h tracing.Hooks) bool { return h.OnStorageChange != nil }) { + out.OnStorageChange = func(address common.Address, slot, previous, current common.Hash) { + for i := range hooks { + if fn := hooks[i].OnStorageChange; fn != nil { + fn(address, slot, previous, current) + } + } + } + } + if anyHook(hooks, func(h tracing.Hooks) bool { return h.OnLog != nil }) { + out.OnLog = func(log *ethtypes.Log) { + for i := range hooks { + if fn := hooks[i].OnLog; fn != nil { + fn(log) + } + } + } + } + if anyHook(hooks, func(h tracing.Hooks) bool { return h.OnBlockHashRead != nil }) { + out.OnBlockHashRead = func(blockNumber uint64, hash common.Hash) { + for i := range hooks { + if fn := hooks[i].OnBlockHashRead; fn != nil { + fn(blockNumber, hash) + } + } + } + } + + return out +} + +func anyHook(hooks []tracing.Hooks, predicate func(tracing.Hooks) bool) bool { + for i := range hooks { + if predicate(hooks[i]) { + return true + } + } + return false +} diff --git a/x/vm/tracer/manager_test.go b/x/vm/tracer/manager_test.go new file mode 100644 index 000000000..6d8a0c69a --- /dev/null +++ b/x/vm/tracer/manager_test.go @@ -0,0 +1,171 @@ +package tracer + +import ( + "context" + "math/big" + "reflect" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/tracing" + ethtypes "github.com/ethereum/go-ethereum/core/types" + gethvm "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/eth/tracers" + "github.com/ethereum/go-ethereum/params" + "github.com/stretchr/testify/require" + + // Force-load native tracer engines so callTracer is registered. + _ "github.com/ethereum/go-ethereum/eth/tracers/native" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +type testTracer struct { + hooks *tracing.Hooks +} + +func (t *testTracer) Hooks() *tracing.Hooks { return t.hooks } + +func TestManagerDispatchesBaseAndApplicationTracers(t *testing.T) { + var calls []string + manager := New(&tracing.Hooks{OnEnter: func(_ int, _ byte, _, _ common.Address, _ []byte, _ uint64, _ *big.Int) { + calls = append(calls, "base") + }}) + dispatcher := manager.Hooks() + manager.InstallApplication(&testTracer{hooks: &tracing.Hooks{OnEnter: func(_ int, _ byte, _, _ common.Address, _ []byte, _ uint64, _ *big.Int) { + calls = append(calls, "application-1") + }}}) + manager.InstallApplication(&testTracer{hooks: &tracing.Hooks{OnEnter: func(_ int, _ byte, _, _ common.Address, _ []byte, _ uint64, _ *big.Int) { + calls = append(calls, "application-2") + }}}) + + require.Same(t, dispatcher, manager.Hooks()) + dispatcher.OnEnter(0, 0, common.Address{}, common.Address{}, nil, 0, nil) + require.Equal(t, []string{"base", "application-1", "application-2"}, calls) +} + +func TestManagerUninstallsApplicationTracer(t *testing.T) { + manager := New(nil) + dispatcher := manager.Hooks() + calls := 0 + uninstall := manager.InstallApplication(&testTracer{hooks: &tracing.Hooks{ + OnEnter: func(_ int, _ byte, _, _ common.Address, _ []byte, _ uint64, _ *big.Int) { calls++ }, + }}) + + dispatcher.OnEnter(0, 0, common.Address{}, common.Address{}, nil, 0, nil) + require.Equal(t, 1, calls) + + uninstall() + uninstall() + require.Same(t, dispatcher, manager.Hooks()) + dispatcher.OnEnter(0, 0, common.Address{}, common.Address{}, nil, 0, nil) + require.Equal(t, 1, calls) +} + +func TestManagerNestedHooksOmitTransactionBoundary(t *testing.T) { + var txStarts, txEnds, enters int + manager := New(&tracing.Hooks{ + OnTxStart: func(_ *tracing.VMContext, _ *ethtypes.Transaction, _ common.Address) { txStarts++ }, + OnTxEnd: func(_ *ethtypes.Receipt, _ error) { txEnds++ }, + }) + manager.InstallApplication(&testTracer{hooks: &tracing.Hooks{OnEnter: func(_ int, _ byte, _, _ common.Address, _ []byte, _ uint64, _ *big.Int) { + enters++ + }}}) + + nested := manager.NestedHooks() + require.Nil(t, nested.OnTxStart) + require.Nil(t, nested.OnTxEnd) + + manager.Hooks().OnTxStart(nil, nil, common.Address{}) + manager.Hooks().OnTxEnd(nil, nil) + nested.OnEnter(0, 0, common.Address{}, common.Address{}, nil, 0, nil) + require.Equal(t, 1, txStarts) + require.Equal(t, 1, txEnds) + require.Equal(t, 1, enters) +} + +func TestManagerNestedHooksAdjustDepthRelativeToParentFrame(t *testing.T) { + var enters, exits, opcodes, faults []int + manager := New(&tracing.Hooks{ + OnEnter: func(depth int, _ byte, _, _ common.Address, _ []byte, _ uint64, _ *big.Int) { + enters = append(enters, depth) + }, + OnExit: func(depth int, _ []byte, _ uint64, _ error, _ bool) { exits = append(exits, depth) }, + OnOpcode: func(_ uint64, _ byte, _, _ uint64, _ tracing.OpContext, _ []byte, depth int, _ error) { + opcodes = append(opcodes, depth) + }, + OnFault: func(_ uint64, _ byte, _, _ uint64, _ tracing.OpContext, depth int, _ error) { + faults = append(faults, depth) + }, + }) + + root, nested := manager.Hooks(), manager.NestedHooks() + root.OnEnter(0, 0, common.Address{}, common.Address{}, nil, 0, nil) + root.OnEnter(1, 0, common.Address{}, common.Address{}, nil, 0, nil) + nested.OnEnter(0, 0, common.Address{}, common.Address{}, nil, 0, nil) + nested.OnOpcode(0, 0, 0, 0, nil, nil, 0, nil) + nested.OnFault(0, 0, 0, 0, nil, 1, nil) + nested.OnEnter(1, 0, common.Address{}, common.Address{}, nil, 0, nil) + nested.OnEnter(0, 0, common.Address{}, common.Address{}, nil, 0, nil) + nested.OnExit(0, nil, 0, nil, false) + nested.OnExit(1, nil, 0, nil, false) + nested.OnExit(0, nil, 0, nil, false) + root.OnExit(1, nil, 0, nil, false) + root.OnExit(0, nil, 0, nil, false) + + require.Equal(t, []int{0, 1, 2, 3, 4}, enters) + require.Equal(t, []int{4, 3, 2, 1, 0}, exits) + require.Equal(t, []int{2}, opcodes) + require.Equal(t, []int{3}, faults) +} + +func TestManagerNestedHooksPreserveCallTracerStack(t *testing.T) { + callTracer, err := tracers.DefaultDirectory.New("callTracer", &tracers.Context{}, nil, params.MainnetChainConfig) + require.NoError(t, err) + + manager := New(callTracer.Hooks) + root, nested := manager.Hooks(), manager.NestedHooks() + from, to := common.HexToAddress("0x1"), common.HexToAddress("0x2") + tx := ethtypes.NewTx(ðtypes.LegacyTx{To: &to, Gas: 100_000}) + + root.OnTxStart(nil, tx, from) + root.OnEnter(0, byte(gethvm.CALL), from, to, nil, 100_000, big.NewInt(0)) + root.OnEnter(1, byte(gethvm.CALL), to, to, nil, 90_000, big.NewInt(0)) + nested.OnEnter(0, byte(gethvm.CALL), to, to, nil, 80_000, big.NewInt(0)) + nested.OnExit(0, nil, 1_000, nil, false) + root.OnExit(1, nil, 2_000, nil, false) + root.OnExit(0, nil, 3_000, nil, false) + root.OnTxEnd(ðtypes.Receipt{GasUsed: 3_000}, nil) + + _, err = callTracer.GetResult() + require.NoError(t, err) +} + +func TestManagerPreservesEveryHookField(t *testing.T) { + hooksType := reflect.TypeOf(tracing.Hooks{}) + hooksValue := reflect.New(hooksType).Elem() + for i := 0; i < hooksType.NumField(); i++ { + hooksValue.Field(i).Set(reflect.MakeFunc(hooksType.Field(i).Type, func(_ []reflect.Value) []reflect.Value { return nil })) + } + + dispatcher := reflect.ValueOf(New(hooksValue.Addr().Interface().(*tracing.Hooks)).Hooks()).Elem() + for i := 0; i < hooksType.NumField(); i++ { + require.Falsef(t, dispatcher.Field(i).IsNil(), "hook %s was not composed", hooksType.Field(i).Name) + } +} + +func TestManagerContextRoundTrip(t *testing.T) { + ctx := sdk.Context{}.WithContext(context.Background()) + _, ok := FromContext(ctx) + require.False(t, ok) + + manager := New(nil) + ctx = WithManager(ctx, manager) + actual, ok := FromContext(ctx) + require.True(t, ok) + require.Same(t, manager, actual) + + ctx = WithoutManager(ctx) + _, ok = FromContext(ctx) + require.False(t, ok) +} diff --git a/x/vm/tracer/tracer.go b/x/vm/tracer/tracer.go new file mode 100644 index 000000000..273efea60 --- /dev/null +++ b/x/vm/tracer/tracer.go @@ -0,0 +1,10 @@ +package tracer + +import "github.com/ethereum/go-ethereum/core/tracing" + +// Tracer is the contract for application-level tracers managed during one EVM +// execution. Concrete implementations own their collection policy and expose +// only the geth hooks needed by Manager. +type Tracer interface { + Hooks() *tracing.Hooks +} From 6c09966fa601df97ade287a21d30451d52d0bccc Mon Sep 17 00:00:00 2001 From: psy2848048 Date: Tue, 22 Sep 2026 15:16:09 +0900 Subject: [PATCH 2/2] fix(erc20): isolate read-only EVM calls from parent traces --- tests/integration/x/erc20/test_convert.go | 11 +-- tests/integration/x/erc20/test_evm.go | 85 +++++++++---------- .../integration/x/erc20/test_ibc_callback.go | 9 +- tests/integration/x/erc20/test_msg_server.go | 8 +- tests/integration/x/erc20/test_proposals.go | 2 +- x/erc20/keeper/evm.go | 22 ++--- x/erc20/types/interfaces.go | 1 + x/erc20/types/mocks/EVMKeeper.go | 45 ++++++++-- 8 files changed, 103 insertions(+), 80 deletions(-) diff --git a/tests/integration/x/erc20/test_convert.go b/tests/integration/x/erc20/test_convert.go index 5d10d0c1e..8f714101d 100644 --- a/tests/integration/x/erc20/test_convert.go +++ b/tests/integration/x/erc20/test_convert.go @@ -140,8 +140,7 @@ func (s *KeeperTestSuite) TestConvertERC20IntoCoinsForNativeToken() { existingAcc := &statedb.Account{Nonce: uint64(1), Balance: uint256.NewInt(1)} balance := make([]uint8, 32) mockEVMKeeper.On("EstimateGasInternal", mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.EstimateGasResponse{Gas: uint64(200)}, nil) - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, - mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil).Once() + expectViewCallEVM(mockEVMKeeper, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil).Once() mockEVMKeeper.On("CallEVMWithData", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("forced ApplyMessage error")) mockEVMKeeper.On("GetAccountWithoutBalance", mock.Anything, mock.Anything).Return(existingAcc, nil) @@ -171,7 +170,7 @@ func (s *KeeperTestSuite) TestConvertERC20IntoCoinsForNativeToken() { balance := make([]uint8, 32) balance[31] = uint8(1) mockEVMKeeper.On("EstimateGasInternal", mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.EstimateGasResponse{Gas: uint64(200)}, nil) - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil).Twice() + expectViewCallEVM(mockEVMKeeper, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil).Twice() mockEVMKeeper.On("CallEVMWithData", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("forced balance error")) mockEVMKeeper.On("GetAccountWithoutBalance", mock.Anything, mock.Anything).Return(existingAcc, nil) mockEVMKeeper.On("IsContract", mock.Anything, mock.Anything).Return(true) @@ -199,8 +198,7 @@ func (s *KeeperTestSuite) TestConvertERC20IntoCoinsForNativeToken() { existingAcc := &statedb.Account{Nonce: uint64(1), Balance: uint256.NewInt(1)} balance := make([]uint8, 32) mockEVMKeeper.On("EstimateGasInternal", mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.EstimateGasResponse{Gas: uint64(200)}, nil) - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, - mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil).Once() + expectViewCallEVM(mockEVMKeeper, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil).Once() mockEVMKeeper.On("CallEVMWithData", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{}, nil) mockEVMKeeper.On("GetAccountWithoutBalance", mock.Anything, mock.Anything).Return(existingAcc, nil) mockEVMKeeper.On("IsContract", mock.Anything, mock.Anything).Return(true) @@ -228,8 +226,7 @@ func (s *KeeperTestSuite) TestConvertERC20IntoCoinsForNativeToken() { existingAcc := &statedb.Account{Nonce: uint64(1), Balance: uint256.NewInt(1)} balance := make([]uint8, 32) mockEVMKeeper.On("EstimateGasInternal", mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.EstimateGasResponse{Gas: uint64(200)}, nil) - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, - mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil).Once() + expectViewCallEVM(mockEVMKeeper, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil).Once() mockEVMKeeper.On("CallEVMWithData", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil) mockEVMKeeper.On("GetAccountWithoutBalance", mock.Anything, mock.Anything).Return(existingAcc, nil) diff --git a/tests/integration/x/erc20/test_evm.go b/tests/integration/x/erc20/test_evm.go index 25bf2317d..753b6f3e4 100644 --- a/tests/integration/x/erc20/test_evm.go +++ b/tests/integration/x/erc20/test_evm.go @@ -19,6 +19,17 @@ import ( govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" ) +func expectViewCallEVM(mockEVMKeeper *erc20mocks.EVMKeeper, _ interface{}, _ ...interface{}) *mock.Call { + return mockEVMKeeper.On( + "CallEVMViewWithData", + mock.Anything, // context + mock.Anything, // from + mock.Anything, // contract + mock.Anything, // ABI-encoded data + mock.Anything, // gas cap + ) +} + func (s *KeeperTestSuite) TestQueryERC20() { var ( contract common.Address @@ -84,8 +95,7 @@ func (s *KeeperTestSuite) TestBalanceOf() { { "Failed to call Evm", func() { - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, - mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("forced ApplyMessage error")) + expectViewCallEVM(mockEVMKeeper, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("forced ApplyMessage error")) }, int64(0), false, @@ -93,8 +103,7 @@ func (s *KeeperTestSuite) TestBalanceOf() { { "Incorrect res", func() { - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, - mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: []uint8{0, 0}}, nil).Once() + expectViewCallEVM(mockEVMKeeper, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: []uint8{0, 0}}, nil).Once() }, int64(0), false, @@ -104,8 +113,7 @@ func (s *KeeperTestSuite) TestBalanceOf() { func() { balance := make([]uint8, 32) balance[31] = uint8(10) - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, - mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil).Once() + expectViewCallEVM(mockEVMKeeper, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil).Once() }, int64(10), true, @@ -147,16 +155,14 @@ func (s *KeeperTestSuite) TestQueryERC20ForceFail() { { "Failed to call Evm", func() { - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, - mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("forced ApplyMessage error")) + expectViewCallEVM(mockEVMKeeper, mock.Anything).Return(nil, fmt.Errorf("forced ApplyMessage error")) }, false, }, { "Incorrect res", func() { - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, - mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: []uint8{0, 0}}, nil).Once() + expectViewCallEVM(mockEVMKeeper, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: []uint8{0, 0}}, nil).Once() }, false, }, @@ -165,8 +171,7 @@ func (s *KeeperTestSuite) TestQueryERC20ForceFail() { func() { ret := []uint8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 67, 111, 105, 110, 32, 84, 111, 107, 101, 110, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} mockEVMKeeper.On("ApplyMessage", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: ret}, nil).Once() - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, - mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{VmError: "Error"}, nil).Once() + expectViewCallEVM(mockEVMKeeper, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{VmError: "Error"}, nil).Once() }, false, }, @@ -174,10 +179,8 @@ func (s *KeeperTestSuite) TestQueryERC20ForceFail() { "incorrect symbol res", func() { ret := []uint8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 67, 111, 105, 110, 32, 84, 111, 107, 101, 110, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, - mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: ret}, nil).Once() - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, - mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: []uint8{0, 0}}, nil).Once() + expectViewCallEVM(mockEVMKeeper, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: ret}, nil).Once() + expectViewCallEVM(mockEVMKeeper, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: []uint8{0, 0}}, nil).Once() }, false, }, @@ -186,12 +189,9 @@ func (s *KeeperTestSuite) TestQueryERC20ForceFail() { func() { ret := []uint8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 67, 111, 105, 110, 32, 84, 111, 107, 101, 110, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} retSymbol := []uint8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 67, 84, 75, 78, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, - mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: ret}, nil).Once() - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, - mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: retSymbol}, nil).Once() - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, - mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{VmError: "Error"}, nil).Once() + expectViewCallEVM(mockEVMKeeper, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: ret}, nil).Once() + expectViewCallEVM(mockEVMKeeper, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: retSymbol}, nil).Once() + expectViewCallEVM(mockEVMKeeper, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{VmError: "Error"}, nil).Once() }, false, }, @@ -200,12 +200,9 @@ func (s *KeeperTestSuite) TestQueryERC20ForceFail() { func() { ret := []uint8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 67, 111, 105, 110, 32, 84, 111, 107, 101, 110, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} retSymbol := []uint8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 67, 84, 75, 78, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, - mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: ret}, nil).Once() - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, - mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: retSymbol}, nil).Once() - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, - mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: []uint8{0, 0}}, nil).Once() + expectViewCallEVM(mockEVMKeeper, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: ret}, nil).Once() + expectViewCallEVM(mockEVMKeeper, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: retSymbol}, nil).Once() + expectViewCallEVM(mockEVMKeeper, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: []uint8{0, 0}}, nil).Once() }, false, }, @@ -287,11 +284,11 @@ func (s *KeeperTestSuite) TestQueryERC20Bytes32Fallback() { symbolData := createStringData("MKR") decimalsData := []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18} - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, "name"). + expectViewCallEVM(mockEVMKeeper, "name"). Return(&evmtypes.MsgEthereumTxResponse{Ret: nameData}, nil).Once() - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, "symbol"). + expectViewCallEVM(mockEVMKeeper, "symbol"). Return(&evmtypes.MsgEthereumTxResponse{Ret: symbolData}, nil).Once() - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, "decimals"). + expectViewCallEVM(mockEVMKeeper, "decimals"). Return(&evmtypes.MsgEthereumTxResponse{Ret: decimalsData}, nil).Once() }, types.ERC20Data{Name: "Maker", Symbol: "MKR", Decimals: 18}, @@ -305,11 +302,11 @@ func (s *KeeperTestSuite) TestQueryERC20Bytes32Fallback() { decimalsData := []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18} // First call tries string unpacking (will fail), then tries bytes32 (will succeed) - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, "name"). + expectViewCallEVM(mockEVMKeeper, "name"). Return(&evmtypes.MsgEthereumTxResponse{Ret: nameData}, nil).Once() - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, "symbol"). + expectViewCallEVM(mockEVMKeeper, "symbol"). Return(&evmtypes.MsgEthereumTxResponse{Ret: symbolData}, nil).Once() - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, "decimals"). + expectViewCallEVM(mockEVMKeeper, "decimals"). Return(&evmtypes.MsgEthereumTxResponse{Ret: decimalsData}, nil).Once() }, types.ERC20Data{Name: "Maker", Symbol: "MKR", Decimals: 18}, @@ -322,11 +319,11 @@ func (s *KeeperTestSuite) TestQueryERC20Bytes32Fallback() { symbolData := createBytes32Data("MKR") decimalsData := []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18} - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, "name"). + expectViewCallEVM(mockEVMKeeper, "name"). Return(&evmtypes.MsgEthereumTxResponse{Ret: nameData}, nil).Once() - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, "symbol"). + expectViewCallEVM(mockEVMKeeper, "symbol"). Return(&evmtypes.MsgEthereumTxResponse{Ret: symbolData}, nil).Once() - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, "decimals"). + expectViewCallEVM(mockEVMKeeper, "decimals"). Return(&evmtypes.MsgEthereumTxResponse{Ret: decimalsData}, nil).Once() }, types.ERC20Data{Name: "Maker", Symbol: "MKR", Decimals: 18}, @@ -346,11 +343,11 @@ func (s *KeeperTestSuite) TestQueryERC20Bytes32Fallback() { decimalsData := []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18} - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, "name"). + expectViewCallEVM(mockEVMKeeper, "name"). Return(&evmtypes.MsgEthereumTxResponse{Ret: nameData}, nil).Once() - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, "symbol"). + expectViewCallEVM(mockEVMKeeper, "symbol"). Return(&evmtypes.MsgEthereumTxResponse{Ret: symbolData}, nil).Once() - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, "decimals"). + expectViewCallEVM(mockEVMKeeper, "decimals"). Return(&evmtypes.MsgEthereumTxResponse{Ret: decimalsData}, nil).Once() }, types.ERC20Data{Name: "Maker", Symbol: "MKR", Decimals: 18}, @@ -359,7 +356,7 @@ func (s *KeeperTestSuite) TestQueryERC20Bytes32Fallback() { { "EVM call fails for name", func() { - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, "name"). + expectViewCallEVM(mockEVMKeeper, "name"). Return(nil, fmt.Errorf("EVM call failed")).Once() }, types.ERC20Data{}, @@ -370,7 +367,7 @@ func (s *KeeperTestSuite) TestQueryERC20Bytes32Fallback() { func() { invalidData := []byte{0xFF, 0xFF} // Invalid data that will fail both unpacking methods - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, "name"). + expectViewCallEVM(mockEVMKeeper, "name"). Return(&evmtypes.MsgEthereumTxResponse{Ret: invalidData}, nil).Once() }, types.ERC20Data{}, @@ -381,9 +378,9 @@ func (s *KeeperTestSuite) TestQueryERC20Bytes32Fallback() { func() { nameData := createStringData("Maker") - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, "name"). + expectViewCallEVM(mockEVMKeeper, "name"). Return(&evmtypes.MsgEthereumTxResponse{Ret: nameData}, nil).Once() - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, "symbol"). + expectViewCallEVM(mockEVMKeeper, "symbol"). Return(nil, fmt.Errorf("EVM call failed")).Once() }, types.ERC20Data{}, diff --git a/tests/integration/x/erc20/test_ibc_callback.go b/tests/integration/x/erc20/test_ibc_callback.go index 88c28164a..2cd481c36 100644 --- a/tests/integration/x/erc20/test_ibc_callback.go +++ b/tests/integration/x/erc20/test_ibc_callback.go @@ -492,10 +492,7 @@ func (s *KeeperTestSuite) TestConvertCoinToERC20FromPacket_GasConfigPreserved() ctx = ctx.WithKVGasConfig(kvGas).WithTransientKVGasConfig(transientGas) var capturedCtx sdk.Context - evmMock.On("CallEVM", - mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, - mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, - ).Run(func(args mock.Arguments) { + expectViewCallEVM(evmMock, mock.Anything, mock.Anything).Run(func(args mock.Arguments) { capturedCtx = args.Get(0).(sdk.Context) }).Return((*evmtypes.MsgEthereumTxResponse)(nil), errors.New("mock evm error")) evmMock.On("KVStoreKeys").Return(map[string]storetypes.StoreKey{}).Maybe() @@ -505,8 +502,8 @@ func (s *KeeperTestSuite) TestConvertCoinToERC20FromPacket_GasConfigPreserved() s.Require().NoError(customKeeper.ConvertCoinToERC20FromPacket(ctx, data)) - // capturedCtx is only set if CallEVM was reached. A zeroed GasConfig would not - // equal kvGas, so these assertions also verify that CallEVM was called. + // capturedCtx is only set if the non-committing CallEVM was reached. A zeroed + // GasConfig would not equal kvGas, so these assertions also verify the view call. s.Require().Equal(kvGas, capturedCtx.KVGasConfig(), "KV gas config must not be zeroed before EVM call") s.Require().Equal(transientGas, capturedCtx.TransientKVGasConfig(), "transient KV gas config must not be zeroed before EVM call") } diff --git a/tests/integration/x/erc20/test_msg_server.go b/tests/integration/x/erc20/test_msg_server.go index 884f24018..1afb1ca29 100644 --- a/tests/integration/x/erc20/test_msg_server.go +++ b/tests/integration/x/erc20/test_msg_server.go @@ -206,8 +206,7 @@ func (s *KeeperTestSuite) TestConvertNativeERC20ToEVMERC20() { existingAcc := &statedb.Account{Nonce: uint64(1), Balance: uint256.NewInt(1)} balance := make([]uint8, 32) mockEVMKeeper.On("EstimateGasInternal", mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.EstimateGasResponse{Gas: uint64(200)}, nil) - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, - mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, fmt.Errorf("forced ApplyMessage error")).Once() + expectViewCallEVM(mockEVMKeeper, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, fmt.Errorf("forced ApplyMessage error")).Once() mockEVMKeeper.On("CallEVMWithData", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("forced ApplyMessage error")) mockEVMKeeper.On("GetAccountWithoutBalance", mock.Anything, mock.Anything).Return(existingAcc, nil) @@ -237,8 +236,7 @@ func (s *KeeperTestSuite) TestConvertNativeERC20ToEVMERC20() { balance := make([]uint8, 32) balance[31] = uint8(1) mockEVMKeeper.On("EstimateGasInternal", mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.EstimateGasResponse{Gas: uint64(200)}, nil) - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, - mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil).Times(3) + expectViewCallEVM(mockEVMKeeper, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil).Times(3) mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil).Maybe() mockEVMKeeper.On("CallEVMWithData", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("forced balance error")) @@ -268,6 +266,7 @@ func (s *KeeperTestSuite) TestConvertNativeERC20ToEVMERC20() { existingAcc := &statedb.Account{Nonce: uint64(1), Balance: uint256.NewInt(1)} balance := make([]uint8, 32) mockEVMKeeper.On("EstimateGasInternal", mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.EstimateGasResponse{Gas: uint64(200)}, nil) + expectViewCallEVM(mockEVMKeeper, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil).Maybe() mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil).Twice() mockEVMKeeper.On("CallEVMWithData", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{}, nil) @@ -298,6 +297,7 @@ func (s *KeeperTestSuite) TestConvertNativeERC20ToEVMERC20() { existingAcc := &statedb.Account{Nonce: uint64(1), Balance: uint256.NewInt(1)} balance := make([]uint8, 32) mockEVMKeeper.On("EstimateGasInternal", mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.EstimateGasResponse{Gas: uint64(200)}, nil) + expectViewCallEVM(mockEVMKeeper, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil).Maybe() mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.MsgEthereumTxResponse{Ret: balance}, nil).Twice() mockEVMKeeper.On("CallEVMWithData", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, diff --git a/tests/integration/x/erc20/test_proposals.go b/tests/integration/x/erc20/test_proposals.go index 2ce7cd5fe..ff528e70d 100644 --- a/tests/integration/x/erc20/test_proposals.go +++ b/tests/integration/x/erc20/test_proposals.go @@ -168,7 +168,7 @@ func (s *KeeperTestSuite) TestRegisterERC20() { s.network.App.SetErc20Keeper(erc20Keeper) mockEVMKeeper.On("EstimateGasInternal", mock.Anything, mock.Anything, mock.Anything).Return(&evmtypes.EstimateGasResponse{Gas: uint64(200)}, nil) - mockEVMKeeper.On("CallEVM", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("forced CallEVM error")) + expectViewCallEVM(mockEVMKeeper, mock.Anything).Return(nil, fmt.Errorf("forced CallEVM error")) mockEVMKeeper.On("ApplyMessage", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("forced ApplyMessage error")) }, s.keyring.GetAccAddr(0).String(), diff --git a/x/erc20/keeper/evm.go b/x/erc20/keeper/evm.go index 1e14d2754..96975d7c5 100644 --- a/x/erc20/keeper/evm.go +++ b/x/erc20/keeper/evm.go @@ -10,7 +10,7 @@ import ( "github.com/cosmos/evm/contracts" "github.com/cosmos/evm/utils" "github.com/cosmos/evm/x/erc20/types" - "github.com/cosmos/evm/x/vm/statedb" + evmtypes "github.com/cosmos/evm/x/vm/types" errorsmod "cosmossdk.io/errors" @@ -45,9 +45,7 @@ func (k Keeper) QueryERC20( } // Decimals - standard uint8, no fallback needed - stateDB := statedb.New(ctx, k.evmKeeper, statedb.NewEmptyTxConfig()) - // Okay to assume we're not calling from a precompile, as queries will just revert state changes. - res, err := k.evmKeeper.CallEVM(ctx, stateDB, erc20, types.ModuleAddress, contract, false, false, nil, "decimals") + res, err := k.callEVMView(ctx, erc20, contract, "decimals") if err != nil { return types.ERC20Data{}, err } @@ -70,9 +68,7 @@ func (k Keeper) queryERC20String( method string, ) (string, error) { // 1) Call into the EVM - stateDB := statedb.New(ctx, k.evmKeeper, statedb.NewEmptyTxConfig()) - // Okay to assume we're not calling from a precompile, as queries will just revert state changes. - res, err := k.evmKeeper.CallEVM(ctx, stateDB, erc20, types.ModuleAddress, contract, false, false, nil, method) + res, err := k.callEVMView(ctx, erc20, contract, method) if err != nil { return "", err } @@ -105,9 +101,7 @@ func (k Keeper) BalanceOf( abi abi.ABI, contract, account common.Address, ) *big.Int { - stateDB := statedb.New(ctx, k.evmKeeper, statedb.NewEmptyTxConfig()) - // Okay to assume we're not calling from a precompile, as queries will just revert state changes. - res, err := k.evmKeeper.CallEVM(ctx, stateDB, abi, types.ModuleAddress, contract, false, false, nil, "balanceOf", account) + res, err := k.callEVMView(ctx, abi, contract, "balanceOf", account) if err != nil { return nil } @@ -124,3 +118,11 @@ func (k Keeper) BalanceOf( return balance } + +func (k Keeper) callEVMView(ctx sdk.Context, contractABI abi.ABI, contract common.Address, method string, args ...interface{}) (*evmtypes.MsgEthereumTxResponse, error) { + data, err := contractABI.Pack(method, args...) + if err != nil { + return nil, errorsmod.Wrap(types.ErrABIPack, err.Error()) + } + return k.evmKeeper.CallEVMViewWithData(ctx, types.ModuleAddress, &contract, data, nil) +} diff --git a/x/erc20/types/interfaces.go b/x/erc20/types/interfaces.go index 446a5801b..e196075b9 100644 --- a/x/erc20/types/interfaces.go +++ b/x/erc20/types/interfaces.go @@ -47,6 +47,7 @@ type EVMKeeper interface { IsAvailableStaticPrecompile(params *evmtypes.Params, address common.Address) bool CallEVM(ctx sdk.Context, stateDB *statedb.StateDB, abi abi.ABI, from, contract common.Address, commit, callFromPrecompile bool, gasCap *big.Int, method string, args ...interface{}) (*evmtypes.MsgEthereumTxResponse, error) CallEVMWithData(ctx sdk.Context, stateDB *statedb.StateDB, from common.Address, contract *common.Address, data []byte, commit bool, callFromPrecompile bool, gasCap *big.Int) (*evmtypes.MsgEthereumTxResponse, error) + CallEVMViewWithData(ctx sdk.Context, from common.Address, contract *common.Address, data []byte, gasCap *big.Int) (*evmtypes.MsgEthereumTxResponse, error) GetCode(ctx sdk.Context, hash common.Hash) []byte SetCode(ctx sdk.Context, hash []byte, bytecode []byte) SetAccount(ctx sdk.Context, address common.Address, account statedb.Account) error diff --git a/x/erc20/types/mocks/EVMKeeper.go b/x/erc20/types/mocks/EVMKeeper.go index f29367848..81539ad63 100644 --- a/x/erc20/types/mocks/EVMKeeper.go +++ b/x/erc20/types/mocks/EVMKeeper.go @@ -17,12 +17,12 @@ import ( statedb "github.com/cosmos/evm/x/vm/statedb" - storetypes "github.com/cosmos/cosmos-sdk/store/v2/types" - tracing "github.com/ethereum/go-ethereum/core/tracing" types "github.com/cosmos/cosmos-sdk/types" + v2types "github.com/cosmos/cosmos-sdk/store/v2/types" + vmtypes "github.com/cosmos/evm/x/vm/types" ) @@ -94,6 +94,36 @@ func (_m *EVMKeeper) CallEVM(ctx types.Context, stateDB *statedb.StateDB, _a2 ab return r0, r1 } +// CallEVMViewWithData provides a mock function with given fields: ctx, from, contract, data, gasCap +func (_m *EVMKeeper) CallEVMViewWithData(ctx types.Context, from common.Address, contract *common.Address, data []byte, gasCap *big.Int) (*vmtypes.MsgEthereumTxResponse, error) { + ret := _m.Called(ctx, from, contract, data, gasCap) + + if len(ret) == 0 { + panic("no return value specified for CallEVMViewWithData") + } + + var r0 *vmtypes.MsgEthereumTxResponse + var r1 error + if rf, ok := ret.Get(0).(func(types.Context, common.Address, *common.Address, []byte, *big.Int) (*vmtypes.MsgEthereumTxResponse, error)); ok { + return rf(ctx, from, contract, data, gasCap) + } + if rf, ok := ret.Get(0).(func(types.Context, common.Address, *common.Address, []byte, *big.Int) *vmtypes.MsgEthereumTxResponse); ok { + r0 = rf(ctx, from, contract, data, gasCap) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*vmtypes.MsgEthereumTxResponse) + } + } + + if rf, ok := ret.Get(1).(func(types.Context, common.Address, *common.Address, []byte, *big.Int) error); ok { + r1 = rf(ctx, from, contract, data, gasCap) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // CallEVMWithData provides a mock function with given fields: ctx, stateDB, from, contract, data, commit, callFromPrecompile, gasCap func (_m *EVMKeeper) CallEVMWithData(ctx types.Context, stateDB *statedb.StateDB, from common.Address, contract *common.Address, data []byte, commit bool, callFromPrecompile bool, gasCap *big.Int) (*vmtypes.MsgEthereumTxResponse, error) { ret := _m.Called(ctx, stateDB, from, contract, data, commit, callFromPrecompile, gasCap) @@ -342,19 +372,19 @@ func (_m *EVMKeeper) IsContract(ctx types.Context, address common.Address) bool } // KVStoreKeys provides a mock function with no fields -func (_m *EVMKeeper) KVStoreKeys() map[string]storetypes.StoreKey { +func (_m *EVMKeeper) KVStoreKeys() map[string]v2types.StoreKey { ret := _m.Called() if len(ret) == 0 { panic("no return value specified for KVStoreKeys") } - var r0 map[string]storetypes.StoreKey - if rf, ok := ret.Get(0).(func() map[string]storetypes.StoreKey); ok { + var r0 map[string]v2types.StoreKey + if rf, ok := ret.Get(0).(func() map[string]v2types.StoreKey); ok { r0 = rf() } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(map[string]storetypes.StoreKey) + r0 = ret.Get(0).(map[string]v2types.StoreKey) } } @@ -394,8 +424,7 @@ func (_m *EVMKeeper) SetState(ctx types.Context, addr common.Address, key common func NewEVMKeeper(t interface { mock.TestingT Cleanup(func()) -}, -) *EVMKeeper { +}) *EVMKeeper { mock := &EVMKeeper{} mock.Mock.Test(t)