Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .testcoverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ exclude:
paths:
- cmd
- docs
- app
- ^app/app\.go$
- tools
- tests
- \.pb\.go$
Expand All @@ -52,4 +52,4 @@ breakdown-file-name: ''
diff:
# File name of go-test-coverage breakdown file which will be used to
# report coverage difference.
base-breakdown-file-name: ''
base-breakdown-file-name: ''
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ lint-fix:
### Testing ###
###############################################################################
EXCLUDED_POA_PACKAGES=$(shell go list ./x/poa/... | grep -v /x/poa/testutil | grep -v /x/poa/client | grep -v /x/poa/simulation | grep -v /x/poa/types)
EXCLUDED_UNIT_PACKAGES=$(shell go list ./... | grep -v tests | grep -v testutil | grep -v tools | grep -v app | grep -v docs | grep -v cmd | grep -v /x/poa/testutil | grep -v /x/poa/client | grep -v /x/poa/simulation | grep -v /x/poa/types)
EXCLUDED_UNIT_PACKAGES=$(shell go list ./... | grep -v tests | grep -v testutil | grep -v tools | grep -v '/app$$' | grep -v docs | grep -v cmd | grep -v /x/poa/testutil | grep -v /x/poa/client | grep -v /x/poa/simulation | grep -v /x/poa/types)

mocks:
@echo "--> Installing mockgen"
Expand Down
59 changes: 32 additions & 27 deletions app/ante/ante.go
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not modify the ante handler since it is copy-pasted from cosmos/evm

Original file line number Diff line number Diff line change
Expand Up @@ -5,51 +5,56 @@ import (

errorsmod "cosmossdk.io/errors"

codectypes "github.com/cosmos/cosmos-sdk/codec/types"
sdk "github.com/cosmos/cosmos-sdk/types"
errortypes "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/cosmos/cosmos-sdk/x/auth/ante"
)

type anteHandlerFactory func(sdk.Context, baseevmante.HandlerOptions) sdk.AnteHandler

// NewAnteHandler returns an ante handler responsible for attempting to route an
// Ethereum or SDK transaction to an internal ante handler for performing
// transaction-level processing (e.g. fee payment, signature verification) before
// being passed onto it's respective handler.
func NewAnteHandler(options baseevmante.HandlerOptions) sdk.AnteHandler {
return newAnteHandler(options, newCosmosAnteHandler, newMonoEVMAnteHandler)
}

func newAnteHandler(
options baseevmante.HandlerOptions,
cosmosHandler anteHandlerFactory,
evmHandler anteHandlerFactory,
) sdk.AnteHandler {
return func(
ctx sdk.Context, tx sdk.Tx, sim bool,
) (newCtx sdk.Context, err error) {
var anteHandler sdk.AnteHandler
if tx == nil {
return ctx, errorsmod.Wrap(errortypes.ErrUnknownRequest, "tx is nil")
}

txWithExtensions, ok := tx.(ante.HasExtensionOptionsTx)
if ok {
opts := txWithExtensions.GetExtensionOptions()
if len(opts) > 0 {
switch typeURL := opts[0].GetTypeUrl(); typeURL {
case "/cosmos.evm.vm.v1.ExtensionOptionsEthereumTx":
// handle as *evmtypes.MsgEthereumTx
anteHandler = newMonoEVMAnteHandler(ctx, options)
case "/cosmos.evm.ante.v1.ExtensionOptionDynamicFeeTx":
// cosmos-sdk tx with dynamic fee extension
anteHandler = newCosmosAnteHandler(ctx, options)
default:
return ctx, errorsmod.Wrapf(
errortypes.ErrUnknownExtensionOptions,
"rejecting tx with unsupported extension option: %s", typeURL,
)
}

return anteHandler(ctx, tx, sim)
}
var opts []*codectypes.Any
if txExt, ok := tx.(ante.HasExtensionOptionsTx); ok {
opts = txExt.GetExtensionOptions()
}
if len(opts) == 0 {
return cosmosHandler(ctx, options)(ctx, tx, sim)
}

// handle as totally normal Cosmos SDK tx
switch tx.(type) {
case sdk.Tx:
anteHandler = newCosmosAnteHandler(ctx, options)
var anteHandler sdk.AnteHandler
switch typeURL := opts[0].GetTypeUrl(); typeURL {
case "/cosmos.evm.vm.v1.ExtensionOptionsEthereumTx":
// handle as *evmtypes.MsgEthereumTx
anteHandler = evmHandler(ctx, options)
case "/cosmos.evm.ante.v1.ExtensionOptionDynamicFeeTx":
// cosmos-sdk tx with dynamic fee extension
anteHandler = cosmosHandler(ctx, options)
default:
return ctx, errorsmod.Wrapf(errortypes.ErrUnknownRequest, "invalid transaction type: %T", tx)
return ctx, errorsmod.Wrapf(
errortypes.ErrUnknownExtensionOptions,
"rejecting tx with unsupported extension option: %s", typeURL,
)
Comment on lines +45 to +56
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

For Cosmos SDK v0.53.x, can a transaction include multiple TxBody.extension_options, and what is the expected validation behavior for unknown options beyond index 0?

💡 Result:

Yes, a transaction can include multiple TxBody.extension_options. The TxBody protobuf message defines extension_options as a repeated google.protobuf.Any extension_options = 1023;, allowing zero or more elements. For validation of unknown options: extension_options (field 1023) are critical (bit 11 not set, as 1023 & 2048 == 0). Unknown fields in critical ranges, including unknown Any types packed into extension_options, cause the transaction to be rejected during decoding via the TxDecoder, which uses unknown field rejection. The comment in the proto states: "If any of these are present and can’t be handled, the transaction will be rejected". non_critical_extension_options (field 2047) are for options that can be safely ignored if unknown (bit 11 set). The query specifies extension_options, which are the critical ones requiring handling or rejection. This behavior is consistent in v0.53.x, as the proto definition remains the same from v0.50 to v0.53, and unknown field rejection is enforced in TxDecoder and ante handlers.

Citations:


🏁 Script executed:

# First, let's see the full function context around lines 45-56
cat -n app/ante/ante.go | head -100

Repository: xrplevm/node

Length of output: 2412


🏁 Script executed:

# Also check if there are any guards or length checks before line 45
cat -n app/ante/ante.go | grep -A 50 "opts\[0\]" | head -60

Repository: xrplevm/node

Length of output: 709


🏁 Script executed:

# Check test file for coverage patterns
cat -n app/ante/ante_test.go | head -150

Repository: xrplevm/node

Length of output: 4323


🏁 Script executed:

# Search for how evmHandler and cosmosHandler handle extension options
rg -A 20 "func newMonoEVMAnteHandler|func newCosmosAnteHandler" app/ante/ante.go

Repository: xrplevm/node

Length of output: 38


🏁 Script executed:

# Check if there's any validation of extension options in handlers
rg "GetExtensionOptions|extension_options" app/ante/ -A 3

Repository: xrplevm/node

Length of output: 424


🏁 Script executed:

# Look for any nil pointer protection patterns in the codebase
rg "opts\[0\].*nil|== nil" app/ante/

Repository: xrplevm/node

Length of output: 90


🏁 Script executed:

# Check what files exist in app/ante
ls -la app/ante/

Repository: xrplevm/node

Length of output: 418


🏁 Script executed:

# Search for handler definitions in the entire codebase
find . -name "*.go" -type f -exec grep -l "newMonoEVMAnteHandler\|newCosmosAnteHandler" {} \;

Repository: xrplevm/node

Length of output: 130


🏁 Script executed:

# Check if these handlers are external (imported from baseevmante)
rg "newMonoEVMAnteHandler|newCosmosAnteHandler" --type go

Repository: xrplevm/node

Length of output: 622


🏁 Script executed:

# Check the evm_handler to see if it validates extension options
cat -n app/ante/evm_handler.go

Repository: xrplevm/node

Length of output: 1025


🏁 Script executed:

# Check the cosmos_handler to see if it validates extension options
cat -n app/ante/cosmos_handler.go

Repository: xrplevm/node

Length of output: 2943


🏁 Script executed:

# Check what ExtensionOptionsDecorator does - search for its usage pattern
rg "ExtensionOptionsDecorator" --type go -B 2 -A 5

Repository: xrplevm/node

Length of output: 630


🏁 Script executed:

# Verify the imports to understand where ExtensionOptionChecker comes from
rg "ExtensionOptionChecker" --type go -B 2 -A 2

Repository: xrplevm/node

Length of output: 1034


🏁 Script executed:

# Check what HasDynamicFeeExtensionOption does
rg "HasDynamicFeeExtensionOption" --type go -B 3 -A 10

Repository: xrplevm/node

Length of output: 1750


🏁 Script executed:

# Look for the antetypes package
find . -name "*.go" -type go -exec grep -l "HasDynamicFeeExtensionOption" {} \;

Repository: xrplevm/node

Length of output: 91


🏁 Script executed:

# Check the protobuf definition to understand if pointers in a slice can be nil after len check
rg "codectypes.Any" --type go | head -20

Repository: xrplevm/node

Length of output: 770


🏁 Script executed:

# Check if there's a way opts[0] can be nil after len check
# Understand the structure better - is it possible for a pointer in a slice to be nil?
python3 << 'EOF'
# In Go, []*T is a slice of pointers. len() checks slice length, not pointer validity.
# Example: make([]*int, 1) creates a slice with one element that is nil by default
print("In Go, a slice like []*codectypes.Any after len(opts) == 0 check")
print("can still have nil pointers at individual indices.")
print("")
print("So opts[0] CAN be nil even when len(opts) > 0")
print("This would cause a panic on opts[0].GetTypeUrl()")
EOF

Repository: xrplevm/node

Length of output: 268


🏁 Script executed:

# Check if there are actual scenarios with multiple extension options
rg "extension_options.*=" --type go -B 2 -A 2 | head -40

Repository: xrplevm/node

Length of output: 38


🏁 Script executed:

# Final verification: check if EVM handler validates extension options via any decorator
cat -n app/ante/evm_handler.go

Repository: xrplevm/node

Length of output: 1025


Validate all extension options before routing and guard nil pointer dereference.

On Line 45, opts[0] is dereferenced without a nil check, risking a panic. More critically, the routing logic only validates the first extension option—any unsupported options at indices 1+ bypass validation entirely. Since the EVM handler chain does not include an ExtensionOptionsDecorator, selecting it leaves non-first options unvalidated. Per Cosmos SDK spec, unknown critical extension options must be rejected, but options beyond index 0 silently pass through.

Suggested fix
 		var anteHandler sdk.AnteHandler
+		if opts[0] == nil {
+			return ctx, errorsmod.Wrap(
+				errortypes.ErrUnknownExtensionOptions,
+				"rejecting tx with nil extension option",
+			)
+		}
+
+		// Validate all extension options before routing.
+		for _, opt := range opts {
+			switch opt.GetTypeUrl() {
+			case "/cosmos.evm.vm.v1.ExtensionOptionsEthereumTx", "/cosmos.evm.ante.v1.ExtensionOptionDynamicFeeTx":
+				// supported
+			default:
+				return ctx, errorsmod.Wrapf(
+					errortypes.ErrUnknownExtensionOptions,
+					"rejecting tx with unsupported extension option: %s", opt.GetTypeUrl(),
+				)
+			}
+		}
+
 		switch typeURL := opts[0].GetTypeUrl(); typeURL {
 		case "/cosmos.evm.vm.v1.ExtensionOptionsEthereumTx":
 			// handle as *evmtypes.MsgEthereumTx
 			anteHandler = evmHandler(ctx, options)
 		case "/cosmos.evm.ante.v1.ExtensionOptionDynamicFeeTx":
 			// cosmos-sdk tx with dynamic fee extension
 			anteHandler = cosmosHandler(ctx, options)
-		default:
-			return ctx, errorsmod.Wrapf(
-				errortypes.ErrUnknownExtensionOptions,
-				"rejecting tx with unsupported extension option: %s", typeURL,
-			)
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/ante/ante.go` around lines 45 - 56, Validate the entire opts slice and
guard against nil before accessing opts[0]: first ensure opts != nil and
len(opts) > 0 (or return ctx with ErrUnknownExtensionOptions), then iterate over
all extension options (for _, o := range opts) and call o.GetTypeUrl() to
validate each type URL, rejecting any unknown/critical option using
errorsmod.Wrapf with errortypes.ErrUnknownExtensionOptions; only after full
validation decide routing (use the first known type URL to pick between
evmHandler(ctx, options) and cosmosHandler(ctx, options) as the existing logic
does) so no non-first options can bypass validation and no nil deref occurs.

}

return anteHandler(ctx, tx, sim)
}
}
123 changes: 123 additions & 0 deletions app/ante/ante_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package ante

import (
"errors"
"testing"

codectypes "github.com/cosmos/cosmos-sdk/codec/types"
sdk "github.com/cosmos/cosmos-sdk/types"
errortypes "github.com/cosmos/cosmos-sdk/types/errors"
authante "github.com/cosmos/cosmos-sdk/x/auth/ante"
baseevmante "github.com/cosmos/evm/ante"
antetypes "github.com/cosmos/evm/ante/types"
evmtypes "github.com/cosmos/evm/x/vm/types"
"github.com/stretchr/testify/require"
protov2 "google.golang.org/protobuf/proto"
)

// Sentinel errors returned by the stub ante handlers to signal which branch
// the router selected, so tests can assert routing without a real ante chain.
var (
errRoutedToCosmosAnte = errors.New("routed to cosmos ante")
errRoutedToEVMAnte = errors.New("routed to evm ante")
)

type routingTx struct {
msgs []sdk.Msg
extensionOptions []*codectypes.Any
}

var (
_ sdk.Tx = routingTx{}
_ authante.HasExtensionOptionsTx = routingTx{}
)

func (tx routingTx) GetMsgs() []sdk.Msg {
return tx.msgs
}

func (tx routingTx) GetMsgsV2() ([]protov2.Message, error) {
return nil, nil
}

func (tx routingTx) GetExtensionOptions() []*codectypes.Any {
return tx.extensionOptions
}

func (tx routingTx) GetNonCriticalExtensionOptions() []*codectypes.Any {
return nil
}

func stubAnteHandlerFactory(routeErr error) anteHandlerFactory {
return func(sdk.Context, baseevmante.HandlerOptions) sdk.AnteHandler {
return func(ctx sdk.Context, _ sdk.Tx, _ bool) (sdk.Context, error) {
return ctx, routeErr
}
}
}

func TestNewAnteHandlerRouting(t *testing.T) {
ethExtension, err := codectypes.NewAnyWithValue(&evmtypes.ExtensionOptionsEthereumTx{})
require.NoError(t, err)
dynamicFeeExtension, err := codectypes.NewAnyWithValue(&antetypes.ExtensionOptionDynamicFeeTx{})
require.NoError(t, err)

cases := []struct {
name string
tx sdk.Tx
wantErr error
wantErrContains string
}{
{
name: "ethereum extension routes to evm ante",
tx: routingTx{
msgs: []sdk.Msg{&evmtypes.MsgEthereumTx{}},
extensionOptions: []*codectypes.Any{ethExtension},
},
wantErr: errRoutedToEVMAnte,
},
{
name: "dynamic fee extension routes to cosmos ante",
tx: routingTx{extensionOptions: []*codectypes.Any{dynamicFeeExtension}},
wantErr: errRoutedToCosmosAnte,
},
{
name: "no extension routes to cosmos ante",
tx: routingTx{},
wantErr: errRoutedToCosmosAnte,
},
{
name: "unknown extension is rejected",
tx: routingTx{
extensionOptions: []*codectypes.Any{{
TypeUrl: "/cosmos.evm.test.v1.UnknownExtensionOption",
}},
},
wantErr: errortypes.ErrUnknownExtensionOptions,
wantErrContains: "rejecting tx with unsupported extension option: /cosmos.evm.test.v1.UnknownExtensionOption",
},
{
name: "nil tx is rejected",
tx: nil,
wantErr: errortypes.ErrUnknownRequest,
wantErrContains: "tx is nil",
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
handler := newAnteHandler(
baseevmante.HandlerOptions{},
stubAnteHandlerFactory(errRoutedToCosmosAnte),
stubAnteHandlerFactory(errRoutedToEVMAnte),
)

_, err := handler(sdk.Context{}, tc.tx, false)

require.ErrorIs(t, err, tc.wantErr)
if tc.wantErrContains != "" {
require.ErrorContains(t, err, tc.wantErrContains)
}
})
}
}
53 changes: 53 additions & 0 deletions app/upgrades/v10/upgrades_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package v10

import (
"testing"

"cosmossdk.io/log"
upgradetypes "cosmossdk.io/x/upgrade/types"
cmtproto "github.com/cometbft/cometbft/proto/tendermint/types"
"github.com/cosmos/cosmos-sdk/codec"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module"
evmtypes "github.com/cosmos/evm/x/vm/types"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
)

var _ EvmKeeper = (*mockEvmKeeper)(nil)

type mockEvmKeeper struct {
initErr error
initCalled bool
}

func (m *mockEvmKeeper) GetParams(sdk.Context) evmtypes.Params { return evmtypes.Params{} }
func (m *mockEvmKeeper) SetParams(sdk.Context, evmtypes.Params) error { return nil }
func (m *mockEvmKeeper) SetCodeHash(sdk.Context, []byte, []byte) {}
func (m *mockEvmKeeper) InitEvmCoinInfo(sdk.Context) error {
m.initCalled = true
return m.initErr
}

func TestCreateUpgradeHandlerInitializesEvmCoinInfo(t *testing.T) {
ctx := sdk.NewContext(nil, cmtproto.Header{}, false, log.NewNopLogger())
moduleManager := module.NewManager()
configurator := module.NewConfigurator(
codec.NewProtoCodec(codectypes.NewInterfaceRegistry()),
grpc.NewServer(),
grpc.NewServer(),
)
keeper := &mockEvmKeeper{}
versions := module.VersionMap{"bank": 1}

updatedVersions, err := CreateUpgradeHandler(moduleManager, configurator, keeper)(
ctx,
upgradetypes.Plan{Name: UpgradeName},
versions,
)

require.NoError(t, err)
require.True(t, keeper.initCalled)
require.Empty(t, updatedVersions)
}
35 changes: 35 additions & 0 deletions app/upgrades/v5/upgrades_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package v5

import (
"testing"

"cosmossdk.io/log"
upgradetypes "cosmossdk.io/x/upgrade/types"
cmtproto "github.com/cometbft/cometbft/proto/tendermint/types"
"github.com/cosmos/cosmos-sdk/codec"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
)

func TestCreateUpgradeHandlerCompletes(t *testing.T) {
ctx := sdk.NewContext(nil, cmtproto.Header{}, false, log.NewNopLogger())
moduleManager := module.NewManager()
configurator := module.NewConfigurator(
codec.NewProtoCodec(codectypes.NewInterfaceRegistry()),
grpc.NewServer(),
grpc.NewServer(),
)
versions := module.VersionMap{"bank": 1}

updatedVersions, err := CreateUpgradeHandler(moduleManager, configurator)(
ctx,
upgradetypes.Plan{Name: UpgradeName},
versions,
)

require.NoError(t, err)
require.Empty(t, updatedVersions)
}
35 changes: 35 additions & 0 deletions app/upgrades/v6/upgrades_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package v6

import (
"testing"

"cosmossdk.io/log"
upgradetypes "cosmossdk.io/x/upgrade/types"
cmtproto "github.com/cometbft/cometbft/proto/tendermint/types"
"github.com/cosmos/cosmos-sdk/codec"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
)

func TestCreateUpgradeHandlerCompletes(t *testing.T) {
ctx := sdk.NewContext(nil, cmtproto.Header{}, false, log.NewNopLogger())
moduleManager := module.NewManager()
configurator := module.NewConfigurator(
codec.NewProtoCodec(codectypes.NewInterfaceRegistry()),
grpc.NewServer(),
grpc.NewServer(),
)
versions := module.VersionMap{"bank": 1}

updatedVersions, err := CreateUpgradeHandler(moduleManager, configurator)(
ctx,
upgradetypes.Plan{Name: UpgradeName},
versions,
)

require.NoError(t, err)
require.Empty(t, updatedVersions)
}
35 changes: 35 additions & 0 deletions app/upgrades/v7/upgrades_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package v7

import (
"testing"

"cosmossdk.io/log"
upgradetypes "cosmossdk.io/x/upgrade/types"
cmtproto "github.com/cometbft/cometbft/proto/tendermint/types"
"github.com/cosmos/cosmos-sdk/codec"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
)

func TestCreateUpgradeHandlerCompletes(t *testing.T) {
ctx := sdk.NewContext(nil, cmtproto.Header{}, false, log.NewNopLogger())
moduleManager := module.NewManager()
configurator := module.NewConfigurator(
codec.NewProtoCodec(codectypes.NewInterfaceRegistry()),
grpc.NewServer(),
grpc.NewServer(),
)
versions := module.VersionMap{"bank": 1}

updatedVersions, err := CreateUpgradeHandler(moduleManager, configurator)(
ctx,
upgradetypes.Plan{Name: UpgradeName},
versions,
)

require.NoError(t, err)
require.Empty(t, updatedVersions)
}
Loading
Loading