diff --git a/app/app.go b/app/app.go index 19f459b9..881a79cf 100644 --- a/app/app.go +++ b/app/app.go @@ -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" @@ -195,7 +192,6 @@ var ( capabilities = []string{ "iterator", "staking", - "stargate", "cosmwasm_1_1", "cosmwasm_1_2", "cosmwasm_1_3", "cosmwasm_1_4", "token_factory", } @@ -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 @@ -433,7 +428,6 @@ func NewChainApp( circuittypes.StoreKey, authzkeeper.StoreKey, nftkeeper.StoreKey, - group.StoreKey, // non sdk store keys ibcexported.StoreKey, ibctransfertypes.StoreKey, @@ -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)) { @@ -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), @@ -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, @@ -1157,7 +1138,6 @@ func NewChainApp( authz.ModuleName, feegrant.ModuleName, nft.ModuleName, - group.ModuleName, paramstypes.ModuleName, upgradetypes.ModuleName, vestingtypes.ModuleName, diff --git a/app/nested_dispatch_test.go b/app/nested_dispatch_test.go new file mode 100644 index 00000000..0ac59d9a --- /dev/null +++ b/app/nested_dispatch_test.go @@ -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") + }) +} diff --git a/app/txpolicy/gasless_test.go b/app/txpolicy/gasless_test.go new file mode 100644 index 00000000..78f2c57e --- /dev/null +++ b/app/txpolicy/gasless_test.go @@ -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)) + }) +} diff --git a/app/wasm.go b/app/wasm.go index 70f811bc..1facc0d0 100755 --- a/app/wasm.go +++ b/app/wasm.go @@ -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", diff --git a/test/integration/uexecutor/gasless_module_sender_test.go b/test/integration/uexecutor/gasless_module_sender_test.go new file mode 100644 index 00000000..0cbe3078 --- /dev/null +++ b/test/integration/uexecutor/gasless_module_sender_test.go @@ -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") +}