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
20 changes: 0 additions & 20 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,6 @@ import (
govkeeper "github.com/cosmos/cosmos-sdk/x/gov/keeper"
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
govv1beta1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1"
"github.com/cosmos/cosmos-sdk/x/group"
groupkeeper "github.com/cosmos/cosmos-sdk/x/group/keeper"
groupmodule "github.com/cosmos/cosmos-sdk/x/group/module"
"github.com/cosmos/cosmos-sdk/x/mint"
mintkeeper "github.com/cosmos/cosmos-sdk/x/mint/keeper"
minttypes "github.com/cosmos/cosmos-sdk/x/mint/types"
Expand Down Expand Up @@ -195,7 +192,6 @@ var (
capabilities = []string{
"iterator",
"staking",
"stargate",
"cosmwasm_1_1", "cosmwasm_1_2", "cosmwasm_1_3", "cosmwasm_1_4",
"token_factory",
}
Expand Down Expand Up @@ -312,7 +308,6 @@ type ChainApp struct {
AuthzKeeper authzkeeper.Keeper
EvidenceKeeper evidencekeeper.Keeper
FeeGrantKeeper feegrantkeeper.Keeper
GroupKeeper groupkeeper.Keeper
NFTKeeper nftkeeper.Keeper
ConsensusParamsKeeper consensusparamkeeper.Keeper
CircuitKeeper circuitkeeper.Keeper
Expand Down Expand Up @@ -433,7 +428,6 @@ func NewChainApp(
circuittypes.StoreKey,
authzkeeper.StoreKey,
nftkeeper.StoreKey,
group.StoreKey,
// non sdk store keys
ibcexported.StoreKey,
ibctransfertypes.StoreKey,
Expand Down Expand Up @@ -589,17 +583,6 @@ func NewChainApp(
app.AccountKeeper,
)

groupConfig := group.DefaultConfig()
groupConfig.MaxMetadataLen = 10000
app.GroupKeeper = groupkeeper.NewKeeper(
keys[group.StoreKey],
// runtime.NewKVStoreService(keys[group.StoreKey]),
appCodec,
app.MsgServiceRouter(),
app.AccountKeeper,
groupConfig,
)

// get skipUpgradeHeights from the app options
skipUpgradeHeights := map[int64]bool{}
for _, h := range cast.ToIntSlice(appOpts.Get(server.FlagUnsafeSkipUpgrades)) {
Expand Down Expand Up @@ -1026,7 +1009,6 @@ func NewChainApp(
evidence.NewAppModule(app.EvidenceKeeper),
params.NewAppModule(app.ParamsKeeper),
authzmodule.NewAppModule(appCodec, app.AuthzKeeper, app.AccountKeeper, app.BankKeeper, app.interfaceRegistry),
groupmodule.NewAppModule(appCodec, app.GroupKeeper, app.AccountKeeper, app.BankKeeper, app.interfaceRegistry),
nftmodule.NewAppModule(appCodec, app.NFTKeeper, app.AccountKeeper, app.BankKeeper, app.interfaceRegistry),
consensus.NewAppModule(appCodec, app.ConsensusParamsKeeper),
circuit.NewAppModule(appCodec, app.CircuitKeeper),
Expand Down Expand Up @@ -1111,7 +1093,6 @@ func NewChainApp(
stakingtypes.ModuleName,
genutiltypes.ModuleName,
feegrant.ModuleName,
group.ModuleName,
// additional non simd modules
evmtypes.ModuleName, erc20types.ModuleName, feemarkettypes.ModuleName,
ibctransfertypes.ModuleName,
Expand Down Expand Up @@ -1157,7 +1138,6 @@ func NewChainApp(
authz.ModuleName,
feegrant.ModuleName,
nft.ModuleName,
group.ModuleName,
paramstypes.ModuleName,
upgradetypes.ModuleName,
vestingtypes.ModuleName,
Expand Down
75 changes: 75 additions & 0 deletions app/nested_dispatch_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package app

import (
"testing"

"github.com/stretchr/testify/require"
)

// Regression tests for F-2026-18197 (nested message dispatch bypasses the EVM ante).
//
// Ethereum signature, nonce and gas checks live only in the EVM ante handler;
// x/vm's Keeper.EthereumTx assumes the ante already ran. Any module that unpacks
// and re-dispatches an embedded sdk.Msg therefore reaches the EVM executor with
// none of those checks applied. Push had two such dispatchers wired: x/group
// (MsgSubmitProposal/MsgExec) and the CosmWasm "stargate" capability
// (CosmosMsg::Any). Both are removed; these tests keep them removed.

// groupMsgTypeURLs are the x/group entry points that unpack and dispatch a
// nested sdk.Msg. They must not resolve or route.
var groupMsgTypeURLs = []string{
"/cosmos.group.v1.MsgSubmitProposal",
"/cosmos.group.v1.MsgExec",
"/cosmos.group.v1.MsgCreateGroup",
"/cosmos.group.v1.MsgCreateGroupWithPolicy",
"/cosmos.group.v1.MsgCreateGroupPolicy",
}

// TestGroupModuleNotWired asserts x/group is gone from every wiring point: the
// module manager, the store keys, the message router and the interface registry.
func TestGroupModuleNotWired(t *testing.T) {
// setup() constructs the app without InitChain, which is all these
// assertions need: the module manager, store keys, message routes and
// interface registry are populated by then. Setup() is avoided on purpose -
// it passes the "testing" chain ID and only works once another test has
// already initialised the global EVM configurator.
gapp, _ := setup(t, ChainID, false, 0)

t.Run("not in module manager", func(t *testing.T) {
_, ok := gapp.ModuleManager.Modules["group"]
require.False(t, ok, "x/group must not be registered in the module manager")
})

t.Run("no store key", func(t *testing.T) {
require.Nil(t, gapp.GetKey("group"), "x/group must not have a KV store key")
})

t.Run("msgs unroutable", func(t *testing.T) {
for _, typeURL := range groupMsgTypeURLs {
require.Nil(t, gapp.MsgServiceRouter().HandlerByTypeURL(typeURL),
"%s must have no handler on the msg service router", typeURL)
}
})

t.Run("msgs unresolvable", func(t *testing.T) {
for _, typeURL := range groupMsgTypeURLs {
_, err := gapp.InterfaceRegistry().Resolve(typeURL)
require.Error(t, err,
"%s must not resolve in the interface registry (tx decoding must fail)", typeURL)
}
})
}

// TestWasmStargateCapabilityDisabled asserts the "stargate" wasmvm capability is
// off for both wasm VMs. With it enabled, an uploaded contract may emit an
// arbitrary encoded sdk.Msg (CosmosMsg::Any / Stargate) that the wasm message
// handler forwards straight to the message router, after ante has already run.
func TestWasmStargateCapabilityDisabled(t *testing.T) {
t.Run("x/wasm", func(t *testing.T) {
require.NotContains(t, AllCapabilities(), "stargate")
})

t.Run("08-wasm light client", func(t *testing.T) {
require.NotContains(t, capabilities, "stargate")
})
}
54 changes: 54 additions & 0 deletions app/txpolicy/gasless_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package txpolicy_test

import (
"testing"

protov2 "google.golang.org/protobuf/proto"

codectypes "github.com/cosmos/cosmos-sdk/codec/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/authz"
evmtypes "github.com/cosmos/evm/x/vm/types"
"github.com/stretchr/testify/require"

"github.com/pushchain/push-chain-node/app/txpolicy"
uexecutortypes "github.com/pushchain/push-chain-node/x/uexecutor/types"
)

// msgsOnlyTx is the minimal sdk.Tx IsGaslessTx needs.
type msgsOnlyTx struct{ msgs []sdk.Msg }

func (t msgsOnlyTx) GetMsgs() []sdk.Msg { return t.msgs }
func (t msgsOnlyTx) GetMsgsV2() ([]protov2.Message, error) { return nil, nil }

// TestGaslessMsgTypesExcludeEthereumTx is one half of the F-2026-18197 invariant
// guard (see test/integration/uexecutor/gasless_module_sender_test.go for the
// other half).
//
// x/vm's Keeper.EthereumTx now rejects any MsgEthereumTx whose From is not the
// ECDSA signer of the raw transaction. A module account is derived from a name
// and has no key pair, so a module-signed MsgEthereumTx could never pass that
// check. Push's gasless flows are safe precisely because none of them is a
// MsgEthereumTx - they reach the EVM through CallEVM / DerivedEVMCall, which
// call ApplyMessageWithConfig directly. If a MsgEthereumTx were ever added to
// the gasless set, that flow would break 100% of the time; this test fails first.
func TestGaslessMsgTypesExcludeEthereumTx(t *testing.T) {
t.Run("MsgEthereumTx is not gasless", func(t *testing.T) {
tx := msgsOnlyTx{msgs: []sdk.Msg{&evmtypes.MsgEthereumTx{}}}
require.False(t, txpolicy.IsGaslessTx(tx),
"MsgEthereumTx must never be a gasless message type")
})

t.Run("MsgEthereumTx nested in authz is not gasless", func(t *testing.T) {
inner, err := codectypes.NewAnyWithValue(&evmtypes.MsgEthereumTx{})
require.NoError(t, err)
tx := msgsOnlyTx{msgs: []sdk.Msg{&authz.MsgExec{Msgs: []*codectypes.Any{inner}}}}
require.False(t, txpolicy.IsGaslessTx(tx),
"MsgEthereumTx nested in authz.MsgExec must never be a gasless message type")
})

t.Run("MsgExecutePayload stays gasless", func(t *testing.T) {
tx := msgsOnlyTx{msgs: []sdk.Msg{&uexecutortypes.MsgExecutePayload{}}}
require.True(t, txpolicy.IsGaslessTx(tx))
})
}
11 changes: 8 additions & 3 deletions app/wasm.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
package app

// AllCapabilities returns all capabilities available with the current wasmvm
// AllCapabilities returns the wasmvm capabilities enabled on this chain.
// See https://github.com/CosmWasm/cosmwasm/blob/main/docs/CAPABILITIES-BUILT-IN.md
// This functionality is going to be moved upstream: https://github.com/CosmWasm/wasmvm/issues/425
//
// NOTE: "stargate" is deliberately NOT enabled. It lets a contract emit an
// arbitrary encoded sdk.Msg (CosmosMsg::Any / Stargate), which reaches the
// message router without the tx ever passing through the ante handler. That is
// the nested-dispatch vector reported as F-2026-18197: an MsgEthereumTx routed
// that way skips the EVM ante entirely (signature, nonce and gas checks). No
// contract deployed on Push requires it.
func AllCapabilities() []string {
return []string{
"iterator",
"staking",
"stargate",
"cosmwasm_1_1",
"cosmwasm_1_2",
"cosmwasm_1_3",
Expand Down
130 changes: 130 additions & 0 deletions test/integration/uexecutor/gasless_module_sender_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package integrationtest

import (
"testing"

"cosmossdk.io/math"
sdkmath "cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"

utils "github.com/pushchain/push-chain-node/test/utils"
"github.com/pushchain/push-chain-node/types"
uexecutorkeeper "github.com/pushchain/push-chain-node/x/uexecutor/keeper"
uexecutortypes "github.com/pushchain/push-chain-node/x/uexecutor/types"
uregistrytypes "github.com/pushchain/push-chain-node/x/uregistry/types"
)

// TestGaslessExecutePayloadWithModuleSender is the invariant guard for
// F-2026-18197.
//
// The fix hardens x/vm's Keeper.EthereumTx to require that msg.From is the
// ECDSA signer of the raw transaction, so that an MsgEthereumTx smuggled in via
// a nested-message dispatcher can no longer execute as somebody else. Push's
// gasless / module-sender flows must be completely unaffected by that, and they
// are - because they never reach that msg server. MsgExecutePayload runs the
// payload through CallEVM / DerivedEVMCall, which go straight to
// ApplyMessageWithConfig; no MsgEthereumTx is ever constructed.
//
// This test pins that down end to end: a gasless MsgExecutePayload, whose EVM
// caller is the uexecutor module account, still executes successfully.
func TestGaslessExecutePayloadWithModuleSender(t *testing.T) {
app, ctx, _ := utils.SetAppWithValidators(t)

// The uexecutor module account is derived from a name, not from a key pair.
// It can never produce an ECDSA signature, so if a module operation ever
// routed through MsgEthereumTx the new VerifySender check would reject it
// 100% of the time. That is why module-driven EVM calls must keep using the
// ApplyMessage* path, and why this test exists.
moduleAcc := app.AccountKeeper.GetModuleAccount(ctx, uexecutortypes.ModuleName)
require.NotNil(t, moduleAcc)
require.Nil(t, moduleAcc.GetPubKey(),
"the uexecutor module account must have no public key - it cannot sign an MsgEthereumTx")

app.UregistryKeeper.AddChainConfig(ctx, &uregistrytypes.ChainConfig{
Chain: "eip155:11155111",
VmType: uregistrytypes.VmType_EVM,
PublicRpcUrl: "https://sepolia.drpc.org",
GatewayAddress: "0x28E0F09bE2321c1420Dc60Ee146aACbD68B335Fe",
BlockConfirmation: &uregistrytypes.BlockConfirmation{
FastInbound: 5,
StandardInbound: 12,
},
GatewayMethods: []*uregistrytypes.GatewayMethods{{
Name: "addFunds",
Identifier: "",
EventIdentifier: "0xb28f49668e7e76dc96d7aabe5b7f63fecfbd1c3574774c05e8204e749fd96fbd",
}},
Enabled: &uregistrytypes.ChainEnabled{
IsInboundEnabled: true,
IsOutboundEnabled: true,
},
})

params := app.FeeMarketKeeper.GetParams(ctx)
params.BaseFee = math.LegacyNewDec(1000000000)
app.FeeMarketKeeper.SetParams(ctx, params)

ms := uexecutorkeeper.NewMsgServerImpl(app.UexecutorKeeper)

universalAccount := &uexecutortypes.UniversalAccountId{
ChainNamespace: "eip155",
ChainId: "11155111",
Owner: "0x778d3206374f8ac265728e18e3fe2ae6b93e4ce4",
}
payload := &uexecutortypes.UniversalPayload{
To: "0x527F3692F5C53CfA83F7689885995606F93b6164",
Value: "0",
Data: "0x2ba2ed980000000000000000000000000000000000000000000000000000000000000312",
GasLimit: "21000000",
MaxFeePerGas: "1000000000",
MaxPriorityFeePerGas: "200000000",
Nonce: "1",
Deadline: "0",
VType: uexecutortypes.VerificationType(0),
}

evmFrom := common.HexToAddress("0x1000000000000000000000000000000000000001")

err := app.BankKeeper.MintCoins(
ctx,
uexecutortypes.ModuleName,
sdk.NewCoins(sdk.NewCoin(types.BaseDenom, sdkmath.NewInt(2_000_000_000_000_000))),
)
require.NoError(t, err)

err = app.BankKeeper.SendCoinsFromModuleToAccount(
ctx,
uexecutortypes.ModuleName,
sdk.AccAddress(evmFrom.Bytes()),
sdk.NewCoins(sdk.NewCoin(types.BaseDenom, sdkmath.NewInt(1_000_000_000_000_000))),
)
require.NoError(t, err)

_, err = app.UexecutorKeeper.DeployUEAV2(ctx, evmFrom, universalAccount)
require.NoError(t, err)

ueaAddr, _, err := app.UexecutorKeeper.CallFactoryToGetUEAAddressForOrigin(
ctx, evmFrom, utils.GetDefaultAddresses().FactoryAddr, universalAccount,
)
require.NoError(t, err)

err = app.BankKeeper.SendCoinsFromModuleToAccount(
ctx,
uexecutortypes.ModuleName,
sdk.AccAddress(ueaAddr.Bytes()),
sdk.NewCoins(sdk.NewCoin(types.BaseDenom, sdkmath.NewInt(1_000_000_000_000_000))),
)
require.NoError(t, err)

// The gasless message itself: signer is a relayer, the EVM caller is the
// uexecutor module. This must still succeed after the x/vm change.
_, err = ms.ExecutePayload(ctx, &uexecutortypes.MsgExecutePayload{
Signer: "cosmos1xpurwdecvsenyvpkxvmnge3cv93nyd34xuersef38pjnxen9xfsk2dnz8yek2drrv56qmn2ak9",
UniversalAccountId: universalAccount,
UniversalPayload: payload,
VerificationData: "0x91987784d56359fa91c3e3e0332f4f0cffedf9c081eb12874a63b41d5b5e5c660dc827947c2ae26e658d0551ad4b2d2aa073d62691429a0ae239d2cc58055bf11c",
})
require.NoError(t, err, "gasless module-sender MsgExecutePayload must still execute end to end")
}
Loading