From 1f9d2ca4a9db222ee34317177bafc2e88be5f965 Mon Sep 17 00:00:00 2001 From: Arya Lanjewar <102943033+AryaLanjewar3005@users.noreply.github.com> Date: Mon, 8 Jun 2026 15:41:45 +0530 Subject: [PATCH 1/4] fix(vm): wire commit flag through DerivedEVMCallWithData to prevent unintended state persistence --- x/vm/keeper/call_evm.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/x/vm/keeper/call_evm.go b/x/vm/keeper/call_evm.go index 341c30d359..c7f97b9d73 100644 --- a/x/vm/keeper/call_evm.go +++ b/x/vm/keeper/call_evm.go @@ -239,13 +239,12 @@ func (k Keeper) DerivedEVMCallWithData( // thus restricted to be used only inside `ApplyMessage`. tmpCtx, commitState := ctx.CacheContext() - // pass true to commit the StateDB - res, err := k.ApplyMessageWithConfig(tmpCtx, msg, nil, true, cfg, txConfig) + res, err := k.ApplyMessageWithConfig(tmpCtx, msg, nil, commit, cfg, txConfig) if err != nil { return nil, err } - if !res.Failed() { + if commit && !res.Failed() { commitState() } From 7cac8239441cfc91b29aa0d1f9449b1ad21954a5 Mon Sep 17 00:00:00 2001 From: Arya Lanjewar <102943033+AryaLanjewar3005@users.noreply.github.com> Date: Mon, 8 Jun 2026 15:59:55 +0530 Subject: [PATCH 2/4] fix(vm): gate tx log events and block bloom updates on successful derived EVM execution --- x/vm/keeper/call_evm.go | 49 +++++++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/x/vm/keeper/call_evm.go b/x/vm/keeper/call_evm.go index c7f97b9d73..fef91b5bb5 100644 --- a/x/vm/keeper/call_evm.go +++ b/x/vm/keeper/call_evm.go @@ -274,16 +274,6 @@ func (k Keeper) DerivedEVMCallWithData( attrs = append(attrs, sdk.NewAttribute(types.AttributeKeyEthereumTxFailed, res.VmError)) } - txLogAttrs := make([]sdk.Attribute, len(res.Logs)) - for i, log := range res.Logs { - log.TxHash = ethTxHash - value, err := json.Marshal(log) - if err != nil { - return nil, errorsmod.Wrap(err, "failed to encode log") - } - txLogAttrs[i] = sdk.NewAttribute(types.AttributeKeyTxLog, string(value)) - } - // adding txData for more info in rpc methods in order to parse derived txs attrs = append(attrs, sdk.NewAttribute(types.AttributeKeyTxData, hexutil.Encode(msg.Data()))) // adding nonce for more info in rpc methods in order to parse derived txs @@ -294,10 +284,6 @@ func (k Keeper) DerivedEVMCallWithData( types.EventTypeEthereumTx, attrs..., ), - sdk.NewEvent( - types.EventTypeTxLog, - txLogAttrs..., - ), sdk.NewEvent( sdk.EventTypeMessage, sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), @@ -306,14 +292,33 @@ func (k Keeper) DerivedEVMCallWithData( ), }) - logs := types.LogsToEthereum(res.Logs) - var bloomReceipt ethtypes.Bloom - if len(logs) > 0 { - bloom := k.GetBlockBloomTransient(ctx) - bloom.Or(bloom, big.NewInt(0).SetBytes(ethtypes.LogsBloom(logs))) - bloomReceipt = ethtypes.BytesToBloom(bloom.Bytes()) - k.SetBlockBloomTransient(ctx, bloomReceipt.Big()) - k.SetLogSizeTransient(ctx, (k.GetLogSizeTransient(ctx))+uint64(len(logs))) + if !res.Failed() { + txLogAttrs := make([]sdk.Attribute, len(res.Logs)) + for i, log := range res.Logs { + log.TxHash = ethTxHash + value, err := json.Marshal(log) + if err != nil { + return nil, errorsmod.Wrap(err, "failed to encode log") + } + txLogAttrs[i] = sdk.NewAttribute(types.AttributeKeyTxLog, string(value)) + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeTxLog, + txLogAttrs..., + ), + }) + + logs := types.LogsToEthereum(res.Logs) + var bloomReceipt ethtypes.Bloom + if len(logs) > 0 { + bloom := k.GetBlockBloomTransient(ctx) + bloom.Or(bloom, big.NewInt(0).SetBytes(ethtypes.LogsBloom(logs))) + bloomReceipt = ethtypes.BytesToBloom(bloom.Bytes()) + k.SetBlockBloomTransient(ctx, bloomReceipt.Big()) + k.SetLogSizeTransient(ctx, (k.GetLogSizeTransient(ctx))+uint64(len(logs))) + } } } From 03590f0ca040ff843c4a121d3594c394c19dd27a Mon Sep 17 00:00:00 2001 From: Nilesh Gupta Date: Thu, 11 Jun 2026 10:40:50 +0530 Subject: [PATCH 3/4] fix(vm): keep ethereum_tx/tx_log paired on failed derived execution (F-2026-17738) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A reverted derived call must not contribute logs or bloom for uncommitted state, but it must still emit a tx_log event paired with its ethereum_tx. The JSON-RPC log builder (TxLogsFromEvents) matches logs to txs positionally — the Nth tx_log belongs to the Nth eth tx — so dropping the tx_log on failure desyncs logs across the other derived txs in the same block (wrong logs on one receipt, missing logs on another). Always emit the ethereum_tx + tx_log + message triple under commit; on a revert res.Logs is empty so the tx_log carries no logs, and the bloom/log-size transients are guarded behind !res.Failed(). The failed tx therefore shows a status-0 receipt with empty logs and no bloom side effects. Adds regression tests: - EthTxLogEventsStayPaired: interleaved success/failure derived calls keep the ethereum_tx and tx_log event counts equal (fails on the drop-tx_log approach). - FailedExecutionNoBloomSideEffect: a reverted call leaves bloom/log-size unchanged while still emitting the ethereum_tx + empty tx_log pair. --- x/vm/keeper/call_evm.go | 43 +++++++++------- x/vm/keeper/call_evm_test.go | 96 ++++++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 19 deletions(-) diff --git a/x/vm/keeper/call_evm.go b/x/vm/keeper/call_evm.go index fef91b5bb5..727f83a9b0 100644 --- a/x/vm/keeper/call_evm.go +++ b/x/vm/keeper/call_evm.go @@ -279,11 +279,32 @@ func (k Keeper) DerivedEVMCallWithData( // adding nonce for more info in rpc methods in order to parse derived txs attrs = append(attrs, sdk.NewAttribute(types.AttributeKeyTxNonce, strconv.FormatUint(nonce, 10))) attrs = append(attrs, sdk.NewAttribute(types.AttributeKeyTxGasLimit, strconv.FormatUint(gasCap, 10))) + // Build the tx_log attributes. On a reverted execution res.Logs is empty, + // so txLogAttrs ends up empty — but the tx_log event is still emitted below. + // The JSON-RPC log builder (TxLogsFromEvents) matches logs to txs by + // position: the Nth tx_log event belongs to the Nth ethereum_tx. So every + // ethereum_tx must be paired with exactly one tx_log event — an empty one on + // failure — otherwise logs get misattributed across derived txs in the same + // block. The failed tx therefore shows a status-0 receipt with no logs. + txLogAttrs := make([]sdk.Attribute, len(res.Logs)) + for i, log := range res.Logs { + log.TxHash = ethTxHash + value, err := json.Marshal(log) + if err != nil { + return nil, errorsmod.Wrap(err, "failed to encode log") + } + txLogAttrs[i] = sdk.NewAttribute(types.AttributeKeyTxLog, string(value)) + } + ctx.EventManager().EmitEvents(sdk.Events{ sdk.NewEvent( types.EventTypeEthereumTx, attrs..., ), + sdk.NewEvent( + types.EventTypeTxLog, + txLogAttrs..., + ), sdk.NewEvent( sdk.EventTypeMessage, sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), @@ -292,30 +313,14 @@ func (k Keeper) DerivedEVMCallWithData( ), }) + // Only successful executions contribute to the block bloom / log size. + // res.Logs is empty on a revert, so a failed tx never touches the bloom. if !res.Failed() { - txLogAttrs := make([]sdk.Attribute, len(res.Logs)) - for i, log := range res.Logs { - log.TxHash = ethTxHash - value, err := json.Marshal(log) - if err != nil { - return nil, errorsmod.Wrap(err, "failed to encode log") - } - txLogAttrs[i] = sdk.NewAttribute(types.AttributeKeyTxLog, string(value)) - } - - ctx.EventManager().EmitEvents(sdk.Events{ - sdk.NewEvent( - types.EventTypeTxLog, - txLogAttrs..., - ), - }) - logs := types.LogsToEthereum(res.Logs) - var bloomReceipt ethtypes.Bloom if len(logs) > 0 { bloom := k.GetBlockBloomTransient(ctx) bloom.Or(bloom, big.NewInt(0).SetBytes(ethtypes.LogsBloom(logs))) - bloomReceipt = ethtypes.BytesToBloom(bloom.Bytes()) + bloomReceipt := ethtypes.BytesToBloom(bloom.Bytes()) k.SetBlockBloomTransient(ctx, bloomReceipt.Big()) k.SetLogSizeTransient(ctx, (k.GetLogSizeTransient(ctx))+uint64(len(logs))) } diff --git a/x/vm/keeper/call_evm_test.go b/x/vm/keeper/call_evm_test.go index c334824162..8a97441bbc 100644 --- a/x/vm/keeper/call_evm_test.go +++ b/x/vm/keeper/call_evm_test.go @@ -2,13 +2,16 @@ package keeper_test import ( "fmt" + "math/big" "github.com/ethereum/go-ethereum/common" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/evm/contracts" testconstants "github.com/cosmos/evm/testutil/constants" utiltx "github.com/cosmos/evm/testutil/tx" "github.com/cosmos/evm/x/erc20/types" + "github.com/cosmos/evm/x/vm/keeper/testdata" evmtypes "github.com/cosmos/evm/x/vm/types" ) @@ -148,3 +151,96 @@ func (suite *KeeperTestSuite) TestCallEVMWithData() { }) } } + +// derivedTransfer issues a single ERC20 transfer through DerivedEVMCall (commit=true) +// on the shared ctx and returns the resulting error (non-nil when the call reverts). +func (suite *KeeperTestSuite) derivedTransfer(ctx sdk.Context, from, contract, recipient common.Address) error { + erc20Contract, err := testdata.LoadERC20Contract() + suite.Require().NoError(err) + _, err = suite.network.App.EVMKeeper.DerivedEVMCall( + ctx, + erc20Contract.ABI, + from, + contract, + big.NewInt(0), // value + big.NewInt(200000), // gasLimit (explicit so a reverting call still reaches + // execution + event emission instead of failing in gas estimation) + true, // commit + false, // gasless + false, // isModuleSender + nil, // manualNonce + "transfer", + recipient, big.NewInt(100), + ) + return err +} + +// countEthTxAndLogEvents returns how many ethereum_tx and tx_log events are present. +func countEthTxAndLogEvents(events []sdk.Event) (ethTx, txLog int) { + for _, e := range events { + switch e.Type { + case evmtypes.EventTypeEthereumTx: + ethTx++ + case evmtypes.EventTypeTxLog: + txLog++ + } + } + return ethTx, txLog +} + +// TestDerivedEVMCallEthTxLogEventsStayPaired is a regression test for F-2026-17738. +// Every derived ethereum_tx event must be paired with exactly one tx_log event — +// even on failure, where the tx_log is empty. The JSON-RPC log builder matches logs +// to txs positionally, so a missing tx_log on a failed derived tx would desync logs +// across the other derived txs in the same block. +func (suite *KeeperTestSuite) TestDerivedEVMCallEthTxLogEventsStayPaired() { + suite.SetupTest() + + owner := suite.keyring.GetAddr(0) // holds the supply + broke := suite.keyring.GetAddr(1) // holds 0 tokens -> transfer reverts + recipient := utiltx.GenerateAddress() + + contractAddr := suite.DeployTestContract(suite.T(), suite.network.GetContext(), owner, big.NewInt(1_000_000)) + // Fresh event manager so only the calls below are counted (not the deploy). + ctx := suite.network.GetContext().WithEventManager(sdk.NewEventManager()) + + // Interleave success / failure / success so a dropped tx_log on the middle + // (failed) call would leave the counts unequal. + suite.Require().NoError(suite.derivedTransfer(ctx, owner, contractAddr, recipient)) + suite.Require().Error(suite.derivedTransfer(ctx, broke, contractAddr, recipient)) + suite.Require().NoError(suite.derivedTransfer(ctx, owner, contractAddr, recipient)) + + ethTx, txLog := countEthTxAndLogEvents(ctx.EventManager().Events()) + suite.Require().Equal(3, ethTx, "each derived call must emit exactly one ethereum_tx event") + suite.Require().Equal(ethTx, txLog, + "every ethereum_tx must be paired with a tx_log event (empty on failure) to preserve positional log alignment") +} + +// TestDerivedEVMCallFailedExecutionNoBloomSideEffect is a regression test for +// F-2026-17738: a reverted derived execution must not contribute to the block bloom +// or log size, while still emitting the ethereum_tx + (empty) tx_log pair. +func (suite *KeeperTestSuite) TestDerivedEVMCallFailedExecutionNoBloomSideEffect() { + suite.SetupTest() + + owner := suite.keyring.GetAddr(0) + broke := suite.keyring.GetAddr(1) // 0 tokens -> transfer reverts + recipient := utiltx.GenerateAddress() + + contractAddr := suite.DeployTestContract(suite.T(), suite.network.GetContext(), owner, big.NewInt(1_000_000)) + ctx := suite.network.GetContext().WithEventManager(sdk.NewEventManager()) + + bloomBefore := new(big.Int).Set(suite.network.App.EVMKeeper.GetBlockBloomTransient(ctx)) + logSizeBefore := suite.network.App.EVMKeeper.GetLogSizeTransient(ctx) + + // reverting transfer (broke has no tokens) + suite.Require().Error(suite.derivedTransfer(ctx, broke, contractAddr, recipient)) + + suite.Require().Equal(0, bloomBefore.Cmp(suite.network.App.EVMKeeper.GetBlockBloomTransient(ctx)), + "failed derived tx must not mutate the block bloom") + suite.Require().Equal(logSizeBefore, suite.network.App.EVMKeeper.GetLogSizeTransient(ctx), + "failed derived tx must not mutate the log size") + + ethTx, txLog := countEthTxAndLogEvents(ctx.EventManager().Events()) + suite.Require().Equal(1, ethTx, "failed derived tx still emits its ethereum_tx receipt") + suite.Require().Equal(1, txLog, "failed derived tx still emits an (empty) tx_log to preserve alignment") +} From 151b020689e6f2601f3eca792ee6218ed82265bd Mon Sep 17 00:00:00 2001 From: Nilesh Gupta Date: Thu, 11 Jun 2026 10:47:36 +0530 Subject: [PATCH 4/4] test(vm): assert reverted derived tx emits empty tx_log, no bloom (F-2026-17738) --- x/vm/keeper/call_evm_test.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/x/vm/keeper/call_evm_test.go b/x/vm/keeper/call_evm_test.go index 8a97441bbc..d36a072849 100644 --- a/x/vm/keeper/call_evm_test.go +++ b/x/vm/keeper/call_evm_test.go @@ -243,4 +243,27 @@ func (suite *KeeperTestSuite) TestDerivedEVMCallFailedExecutionNoBloomSideEffect ethTx, txLog := countEthTxAndLogEvents(ctx.EventManager().Events()) suite.Require().Equal(1, ethTx, "failed derived tx still emits its ethereum_tx receipt") suite.Require().Equal(1, txLog, "failed derived tx still emits an (empty) tx_log to preserve alignment") + + // The tx_log emitted on failure MUST carry no log attributes — otherwise the fix + // would publish phantom logs for state that was never committed. + suite.Require().Equal(0, txLogAttrCount(ctx.EventManager().Events()), + "a reverted derived tx must not emit any log attributes") + + // Sanity: a successful transfer DOES produce a non-empty tx_log (ERC20 Transfer + // event), so the empty-on-failure result above is not trivially always-empty. + okCtx := suite.network.GetContext().WithEventManager(sdk.NewEventManager()) + suite.Require().NoError(suite.derivedTransfer(okCtx, owner, contractAddr, recipient)) + suite.Require().Positive(txLogAttrCount(okCtx.EventManager().Events()), + "a successful derived tx must emit its logs") +} + +// txLogAttrCount returns the total number of tx_log attributes across all tx_log events. +func txLogAttrCount(events []sdk.Event) int { + n := 0 + for _, e := range events { + if e.Type == evmtypes.EventTypeTxLog { + n += len(e.Attributes) + } + } + return n }