Skip to content

Commit 25c219e

Browse files
fix(evm): Truncate leaked SDK events on VM error failed EVM txs (#2543)
* fix(evm): Truncate leaked SDK events on VM error failed EVM txs * ci: Use pinned version, v1.64.0, for Buf CLI
1 parent 28ef258 commit 25c219e

File tree

7 files changed

+149
-0
lines changed

7 files changed

+149
-0
lines changed

internal/cosmos-sdk/types/events.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,28 @@ func NewEventManager() *EventManager {
3232

3333
func (em *EventManager) Events() Events { return em.events }
3434

35+
// Len returns the number of events currently stored in the manager.
36+
func (em *EventManager) Len() int { return len(em.events) }
37+
38+
// TruncateEvents keeps only the first "mark" events and drops all later events.
39+
//
40+
// Bounds behavior:
41+
// - mark <= 0: clear all events
42+
// - mark >= Len(): no-op
43+
//
44+
// The return value is the resulting event length after truncation.
45+
func (em *EventManager) TruncateEvents(mark int) int {
46+
switch {
47+
case mark <= 0:
48+
em.events = EmptyEvents()
49+
case mark >= len(em.events):
50+
// no-op
51+
default:
52+
em.events = em.events[:mark]
53+
}
54+
return len(em.events)
55+
}
56+
3557
// EmitEvent stores a single Event object.
3658
// Deprecated: Use EmitTypedEvent
3759
func (em *EventManager) EmitEvent(event Event) {

internal/cosmos-sdk/types/events_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,38 @@ func (s *eventsTestSuite) TestEventManager() {
8686
s.Require().Equal(em.Events(), events.AppendEvent(event))
8787
}
8888

89+
func (s *eventsTestSuite) TestEventManagerLenAndTruncateEvents() {
90+
em := sdk.NewEventManager()
91+
e1 := sdk.NewEvent("e1", sdk.NewAttribute("k", "v1"))
92+
e2 := sdk.NewEvent("e2", sdk.NewAttribute("k", "v2"))
93+
e3 := sdk.NewEvent("e3", sdk.NewAttribute("k", "v3"))
94+
em.EmitEvents(sdk.Events{e1, e2, e3})
95+
96+
s.Require().Equal(3, em.Len())
97+
98+
newLen := em.TruncateEvents(2)
99+
s.Require().Equal(2, newLen)
100+
s.Require().Equal(sdk.Events{e1, e2}, em.Events())
101+
s.Require().Equal(2, em.Len())
102+
103+
// Out-of-range marks above Len are no-op.
104+
newLen = em.TruncateEvents(100)
105+
s.Require().Equal(2, newLen)
106+
s.Require().Equal(sdk.Events{e1, e2}, em.Events())
107+
108+
// Full clear on zero.
109+
newLen = em.TruncateEvents(0)
110+
s.Require().Equal(0, newLen)
111+
s.Require().Equal(0, em.Len())
112+
s.Require().Equal(sdk.EmptyEvents(), em.Events())
113+
114+
// Negative marks are treated as clear-all for safety.
115+
em.EmitEvent(e1)
116+
newLen = em.TruncateEvents(-1)
117+
s.Require().Equal(0, newLen)
118+
s.Require().Equal(0, em.Len())
119+
}
120+
89121
func (s *eventsTestSuite) TestEmitTypedEvent() {
90122
s.Run("deterministic key-value order", func() {
91123
for i := 0; i < 10; i++ {

x/evm/const.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ const (
4343
CtxKeyEvmSimulation contextKey = "evm_simulation"
4444
CtxKeyGasEstimateZeroTolerance contextKey = "gas_estimate_zero_tolerance"
4545
CtxKeyZeroGasMeta contextKey = "zero_gas_meta"
46+
CtxKeyEvmEventTruncationMark contextKey = "evm_event_truncation_mark"
4647
)
4748

4849
// GetZeroGasMeta returns the ZeroGasMeta stored under CtxKeyZeroGasMeta, or nil if not set or type assertion fails.
@@ -56,6 +57,12 @@ func IsZeroGasEthTx(ctx sdk.Context) bool {
5657
return GetZeroGasMeta(ctx) != nil
5758
}
5859

60+
// GetEvmEventTruncationMark returns the mark to use for truncating events.
61+
func GetEvmEventTruncationMark(ctx sdk.Context) (mark int, ok bool) {
62+
mark, ok = ctx.Value(CtxKeyEvmEventTruncationMark).(int)
63+
return mark, ok
64+
}
65+
5966
// BASE_FEE_MICRONIBI is the global base fee value for the network. It has a
6067
// constant value of 1 unibi (micronibi) == 10^12 wei.
6168
var (

x/evm/evmante/evmante_emit_event.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,17 @@ func AnteStepEmitPendingEvent(
3737
),
3838
)
3939

40+
// Mark where execution-time events begin. On VM-level failure, EthereumTx
41+
// uses this mark (see MaybeTruncateEventsForFailedEvmTx) to truncate
42+
// module/precompile side-effect events emitted after ante.
43+
//
44+
// Context for this behavior:
45+
// https://github.com/NibiruChain/nibiru/issues/2542
46+
sdb.SetCtx(
47+
sdb.Ctx().WithValue(
48+
evm.CtxKeyEvmEventTruncationMark,
49+
sdb.Ctx().EventManager().Len(),
50+
))
51+
4052
return nil
4153
}

x/evm/evmante/evmante_emit_event_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ func (s *Suite) TestEthAnteEmitPendingEvent() {
3333
attr, ok = event.GetAttribute(evm.PendingEthereumTxEventAttrIndex)
3434
s.Require().True(ok, "tx index attribute not found")
3535
s.Require().Equal("0", attr.Value)
36+
37+
// Ante stores an event truncation mark for EthereumTx failure cleanup.
38+
mark, ok := evm.GetEvmEventTruncationMark(sdb.Ctx())
39+
s.Require().True(ok, "evm event truncation mark not found")
40+
s.Require().Equal(len(events), mark)
3641
},
3742
},
3843
}

x/evm/evmstate/msg_ethereum_tx_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121

2222
abci "github.com/cometbft/cometbft/abci/types"
2323

24+
"github.com/NibiruChain/nibiru/v2/x/evm/evmstate"
2425
"github.com/NibiruChain/nibiru/v2/x/evm/evmtest"
2526
)
2627

@@ -240,6 +241,50 @@ func (s *Suite) TestMsgEthereumTx_SimpleTransfer() {
240241
}
241242
}
242243

244+
func (s *Suite) TestMaybeTruncateEventsForFailedEvmTx() {
245+
mkDeps := func() evmtest.TestDeps {
246+
deps := evmtest.NewTestDeps()
247+
deps.SetCtx(deps.Ctx().WithEventManager(sdk.NewEventManager()))
248+
deps.Ctx().EventManager().EmitEvents(sdk.Events{
249+
sdk.NewEvent("e1"),
250+
sdk.NewEvent("e2"),
251+
sdk.NewEvent("e3"),
252+
})
253+
deps.SetCtx(deps.Ctx().WithValue(evm.CtxKeyEvmEventTruncationMark, 2))
254+
return deps
255+
}
256+
257+
testCases := []struct {
258+
name string
259+
evmResp *evm.MsgEthereumTxResponse
260+
wantTypes []string
261+
}{
262+
{
263+
name: "vm error truncates to mark",
264+
evmResp: &evm.MsgEthereumTxResponse{VmError: "execution reverted"},
265+
wantTypes: []string{"e1", "e2"},
266+
},
267+
{
268+
name: "success keeps all events",
269+
evmResp: &evm.MsgEthereumTxResponse{},
270+
wantTypes: []string{"e1", "e2", "e3"},
271+
},
272+
}
273+
274+
for _, tc := range testCases {
275+
s.Run(tc.name, func() {
276+
deps := mkDeps()
277+
evmstate.MaybeTruncateEventsForFailedEvmTx(deps.Ctx(), tc.evmResp)
278+
279+
events := deps.Ctx().EventManager().Events()
280+
s.Require().Len(events, len(tc.wantTypes))
281+
for i, wantType := range tc.wantTypes {
282+
s.Require().Equal(wantType, events[i].Type)
283+
}
284+
})
285+
}
286+
}
287+
243288
// The following zero-gas tests exercise only the EthereumTx msg_server. They do not
244289
// run the ante handler. The context is given the same zero-gas marker (CtxKeyZeroGasMeta)
245290
// that the ante would set in production. We verify that the msg_server skips RefundGas

x/evm/evmstate/msg_server.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ func (k *Keeper) EthereumTx(
175175
}
176176

177177
stage = "post_execution_events_and_tx_index"
178+
MaybeTruncateEventsForFailedEvmTx(rootCtxGasless, evmResp)
178179
txEvents := k.GetEvmTxEvents(sdb.Ctx(), coreTx.To(), coreTx.Type(), *evmMsg, evmResp)
179180

180181
err = txEvents.EmitEvents(rootCtxGasless)
@@ -201,6 +202,31 @@ func (k *Keeper) EthereumTx(
201202
return evmResp, nil
202203
}
203204

205+
// MaybeTruncateEventsForFailedEvmTx truncates the current tx event stream to an
206+
// ante-computed mark when the EVM response indicates VM-level failure.
207+
//
208+
// This is part of the mitigation for leaked non-EVM module/precompile events on
209+
// failed EVM transactions:
210+
// https://github.com/NibiruChain/nibiru/issues/2542
211+
func MaybeTruncateEventsForFailedEvmTx(
212+
ctx sdk.Context,
213+
evmResp *evm.MsgEthereumTxResponse,
214+
) {
215+
if evmResp == nil || !evmResp.Failed() {
216+
return
217+
}
218+
219+
mark, ok := evm.GetEvmEventTruncationMark(ctx)
220+
if !ok {
221+
return
222+
}
223+
224+
// Keep only ante-phase events (up to mark) when the EVM execution fails.
225+
// Canonical EVM metadata events are emitted immediately after this call in
226+
// EthereumTx via txEvents.EmitEvents and EventTxLog.
227+
ctx.EventManager().TruncateEvents(mark)
228+
}
229+
204230
// NewEVM generates a go-ethereum VM.
205231
//
206232
// Args:

0 commit comments

Comments
 (0)