diff --git a/CHANGELOG.md b/CHANGELOG.md index eccf51dc4..8ce46175e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,7 @@ JSON encoding for the `EIP55Addr` struct was not following the Go conventions an needed to include double quotes around the hexadecimal string. - [#2156](https://github.com/NibiruChain/nibiru/pull/2156) - test(evm-e2e): add E2E test using the Nibiru Oracle's ChainLink impl - [#2157](https://github.com/NibiruChain/nibiru/pull/2157) - fix(evm): Fix unit inconsistency related to AuthInfo.Fee and txData.Fee using effective fee +- [#2159](https://github.com/NibiruChain/nibiru/pull/2159) - chore(evm): Augment the Wasm msg handler so that wasm contracts cannot send MsgEthereumTx #### Nibiru EVM | Before Audit 2 - 2024-12-06 diff --git a/app/app.go b/app/app.go index 2bc2618de..e2ecc8013 100644 --- a/app/app.go +++ b/app/app.go @@ -120,7 +120,11 @@ func init() { } // GetWasmOpts build wasm options -func GetWasmOpts(nibiru NibiruApp, appOpts servertypes.AppOptions) []wasmkeeper.Option { +func GetWasmOpts( + nibiru NibiruApp, + appOpts servertypes.AppOptions, + wasmMsgHandlerArgs wasmext.MsgHandlerArgs, +) []wasmkeeper.Option { var wasmOpts []wasmkeeper.Option if cast.ToBool(appOpts.Get("telemetry.enabled")) { wasmOpts = append(wasmOpts, wasmkeeper.WithVMCacheMetrics(prometheus.DefaultRegisterer)) @@ -129,6 +133,7 @@ func GetWasmOpts(nibiru NibiruApp, appOpts servertypes.AppOptions) []wasmkeeper. return append(wasmOpts, wasmext.NibiruWasmOptions( nibiru.GRPCQueryRouter(), nibiru.appCodec, + wasmMsgHandlerArgs, )...) } diff --git a/app/keepers.go b/app/keepers.go index 1b7a09021..1138bc540 100644 --- a/app/keepers.go +++ b/app/keepers.go @@ -108,6 +108,7 @@ import ( // Nibiru Custom Modules "github.com/NibiruChain/nibiru/v2/app/keepers" + "github.com/NibiruChain/nibiru/v2/app/wasmext" "github.com/NibiruChain/nibiru/v2/eth" "github.com/NibiruChain/nibiru/v2/x/common" "github.com/NibiruChain/nibiru/v2/x/devgas/v1" @@ -465,6 +466,16 @@ func (app *NibiruApp) InitKeepers( panic(err) } + wmha := wasmext.MsgHandlerArgs{ + Router: app.MsgServiceRouter(), + Ics4Wrapper: app.ibcFeeKeeper, + ChannelKeeper: app.ibcKeeper.ChannelKeeper, + CapabilityKeeper: app.ScopedWasmKeeper, + BankKeeper: app.BankKeeper, + Unpacker: appCodec, + PortSource: app.ibcTransferKeeper, + } + app.WasmMsgHandlerArgs = wmha app.WasmKeeper = wasmkeeper.NewKeeper( appCodec, keys[wasmtypes.StoreKey], @@ -472,18 +483,18 @@ func (app *NibiruApp) InitKeepers( app.BankKeeper, app.StakingKeeper, distrkeeper.NewQuerier(app.DistrKeeper), - app.ibcFeeKeeper, // ISC4 Wrapper: fee IBC middleware - app.ibcKeeper.ChannelKeeper, + wmha.Ics4Wrapper, // ISC4 Wrapper: fee IBC middleware + wmha.ChannelKeeper, &app.ibcKeeper.PortKeeper, - app.ScopedWasmKeeper, - app.ibcTransferKeeper, - app.MsgServiceRouter(), + wmha.CapabilityKeeper, + wmha.PortSource, + wmha.Router, app.GRPCQueryRouter(), wasmDir, wasmConfig, supportedFeatures, govModuleAddr, - append(GetWasmOpts(*app, appOpts), wasmkeeper.WithWasmEngine(wasmVM))..., + append(GetWasmOpts(*app, appOpts, wmha), wasmkeeper.WithWasmEngine(wasmVM))..., ) app.WasmClientKeeper = ibcwasmkeeper.NewKeeperWithVM( diff --git a/app/keepers/all_keepers.go b/app/keepers/all_keepers.go index 0ebc82afd..06d08655c 100644 --- a/app/keepers/all_keepers.go +++ b/app/keepers/all_keepers.go @@ -22,6 +22,7 @@ import ( // --------------------------------------------------------------- // Nibiru Custom Modules + "github.com/NibiruChain/nibiru/v2/app/wasmext" devgaskeeper "github.com/NibiruChain/nibiru/v2/x/devgas/v1/keeper" epochskeeper "github.com/NibiruChain/nibiru/v2/x/epochs/keeper" evmkeeper "github.com/NibiruChain/nibiru/v2/x/evm/keeper" @@ -67,7 +68,9 @@ type PublicKeepers struct { EvmKeeper *evmkeeper.Keeper // WASM keepers - WasmKeeper wasmkeeper.Keeper + WasmKeeper wasmkeeper.Keeper + WasmMsgHandlerArgs wasmext.MsgHandlerArgs + ScopedWasmKeeper capabilitykeeper.ScopedKeeper WasmClientKeeper ibcwasmkeeper.Keeper } diff --git a/app/wasmext/stargate_query_test.go b/app/wasmext/stargate_query_test.go index 948310790..1b8f981f1 100644 --- a/app/wasmext/stargate_query_test.go +++ b/app/wasmext/stargate_query_test.go @@ -3,7 +3,6 @@ package wasmext_test import ( "fmt" "strings" - "testing" "github.com/cosmos/gogoproto/proto" "github.com/stretchr/testify/assert" @@ -36,7 +35,8 @@ Given only the `PB_MSG.PACKAGE` and the `PB_MSG.NAME` of either the query request or response, we should know the `QueryRequest::Stargate.path` deterministically. */ -func TestWasmAcceptedStargateQueries(t *testing.T) { +func (s *Suite) TestWasmAcceptedStargateQueries() { + t := s.T() t.Log("stargateQueryPaths: Add nibiru query paths from GRPC service descriptions") queryServiceDescriptions := []grpc.ServiceDesc{ epochs.GrpcQueryServiceDesc(), diff --git a/app/wasmext/wasm.go b/app/wasmext/wasm.go index c71c38a23..6ccfa2329 100644 --- a/app/wasmext/wasm.go +++ b/app/wasmext/wasm.go @@ -1,9 +1,17 @@ package wasmext import ( + "github.com/NibiruChain/nibiru/v2/x/evm" + + "cosmossdk.io/errors" wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" + wasm "github.com/CosmWasm/wasmd/x/wasm/types" + wasmvmtypes "github.com/CosmWasm/wasmvm/types" "github.com/cosmos/cosmos-sdk/baseapp" "github.com/cosmos/cosmos-sdk/codec" + sdkcodec "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" ) // NibiruWasmOptions: Wasm Options are extension points to instantiate the Wasm @@ -11,6 +19,7 @@ import ( func NibiruWasmOptions( grpcQueryRouter *baseapp.GRPCQueryRouter, appCodec codec.Codec, + msgHandlerArgs MsgHandlerArgs, ) []wasmkeeper.Option { wasmQueryOption := wasmkeeper.WithQueryPlugins(&wasmkeeper.QueryPlugins{ Stargate: wasmkeeper.AcceptListStargateQuerier( @@ -20,5 +29,110 @@ func NibiruWasmOptions( ), }) - return []wasmkeeper.Option{wasmQueryOption} + wasmMsgHandlerOption := wasmkeeper.WithMessageHandler(WasmMessageHandler(msgHandlerArgs)) + + return []wasmkeeper.Option{ + wasmQueryOption, + wasmMsgHandlerOption, + } +} + +func (h SDKMessageHandler) handleSdkMessage(ctx sdk.Context, contractAddr sdk.Address, msg sdk.Msg) (*sdk.Result, error) { + if err := msg.ValidateBasic(); err != nil { + return nil, err + } + + // make sure this account can send it + for _, acct := range msg.GetSigners() { + if !acct.Equals(contractAddr) { + return nil, errors.Wrap(sdkerrors.ErrUnauthorized, "contract doesn't have permission") + } + } + + msgTypeUrl := sdk.MsgTypeURL(msg) + if msgTypeUrl == sdk.MsgTypeURL(new(evm.MsgEthereumTx)) { + return nil, errors.Wrap(sdkerrors.ErrUnauthorized, "Wasm VM to EVM call pattern is not yet supported") + } + + // find the handler and execute it + if handler := h.router.Handler(msg); handler != nil { + // ADR 031 request type routing + msgResult, err := handler(ctx, msg) + return msgResult, err + } + // legacy sdk.Msg routing + // Assuming that the app developer has migrated all their Msgs to + // proto messages and has registered all `Msg services`, then this + // path should never be called, because all those Msgs should be + // registered within the `msgServiceRouter` already. + return nil, errors.Wrapf(sdkerrors.ErrUnknownRequest, "can't route message %+v", msg) +} + +type MsgHandlerArgs struct { + Router MessageRouter + Ics4Wrapper wasm.ICS4Wrapper + ChannelKeeper wasm.ChannelKeeper + CapabilityKeeper wasm.CapabilityKeeper + BankKeeper wasm.Burner + Unpacker sdkcodec.AnyUnpacker + PortSource wasm.ICS20TransferPortSource +} + +// SDKMessageHandler can handles messages that can be encoded into sdk.Message types and routed. +type SDKMessageHandler struct { + router MessageRouter + encoders msgEncoder +} + +// MessageRouter ADR 031 request type routing +type MessageRouter interface { + Handler(msg sdk.Msg) baseapp.MsgServiceHandler +} + +// msgEncoder is an extension point to customize encodings +type msgEncoder interface { + // Encode converts wasmvm message to n cosmos message types + Encode(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) ([]sdk.Msg, error) +} + +// WasmMessageHandler is a replacement constructor for +// [wasmkeeper.NewDefaultMessageHandler] inside of [wasmkeeper.NewKeeper]. +func WasmMessageHandler( + args MsgHandlerArgs, +) wasmkeeper.Messenger { + encoders := wasmkeeper.DefaultEncoders(args.Unpacker, args.PortSource) + return wasmkeeper.NewMessageHandlerChain( + NewSDKMessageHandler(args.Router, encoders), + wasmkeeper.NewIBCRawPacketHandler(args.Ics4Wrapper, args.ChannelKeeper, args.CapabilityKeeper), + wasmkeeper.NewBurnCoinMessageHandler(args.BankKeeper), + ) +} + +func NewSDKMessageHandler(router MessageRouter, encoders msgEncoder) SDKMessageHandler { + return SDKMessageHandler{ + router: router, + encoders: encoders, + } +} + +func (h SDKMessageHandler) DispatchMsg(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) { + sdkMsgs, err := h.encoders.Encode(ctx, contractAddr, contractIBCPortID, msg) + if err != nil { + return nil, nil, err + } + for _, sdkMsg := range sdkMsgs { + res, err := h.handleSdkMessage(ctx, contractAddr, sdkMsg) + if err != nil { + return nil, nil, err + } + // append data + data = append(data, res.Data) + // append events + sdkEvents := make([]sdk.Event, len(res.Events)) + for i := range res.Events { + sdkEvents[i] = sdk.Event(res.Events[i]) + } + events = append(events, sdkEvents...) + } + return } diff --git a/app/wasmext/wasmext_test.go b/app/wasmext/wasmext_test.go new file mode 100644 index 000000000..de230c637 --- /dev/null +++ b/app/wasmext/wasmext_test.go @@ -0,0 +1,73 @@ +package wasmext_test + +import ( + "math/big" + "testing" + + wasmvm "github.com/CosmWasm/wasmvm/types" + sdkcodec "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/suite" + + "github.com/NibiruChain/nibiru/v2/app/wasmext" + "github.com/NibiruChain/nibiru/v2/x/evm" + "github.com/NibiruChain/nibiru/v2/x/evm/evmtest" +) + +type Suite struct { + suite.Suite +} + +func TestWasmExtSuite(t *testing.T) { + suite.Run(t, new(Suite)) +} + +// WasmVM to EVM call pattern is not yet supported. This test verifies the +// Nibiru's [wasmkeeper.Option] function as expected. +func (s *Suite) TestEvmFilter() { + deps := evmtest.NewTestDeps() + // wk := wasmkeeper.NewDefaultPermissionKeeper(deps.App.WasmKeeper) + wasmMsgHandler := wasmext.WasmMessageHandler(deps.App.WasmMsgHandlerArgs) + + s.T().Log("Create a valid Ethereum tx msg") + + to := evmtest.NewEthPrivAcc() + ethTxMsg, err := evmtest.TxTransferWei{ + Deps: &deps, + To: to.EthAddr, + AmountWei: evm.NativeToWei(big.NewInt(420)), + }.Build() + s.NoError(err) + + s.T().Log("Validate Eth tx msg proto encoding as wasmvm.StargateMsg") + wasmContractAddr := deps.Sender.NibiruAddr + protoValueBz, err := deps.EncCfg.Codec.Marshal(ethTxMsg) + s.Require().NoError(err, "expect ethTxMsg to proto marshal", protoValueBz) + + _, ok := deps.EncCfg.Codec.(sdkcodec.AnyUnpacker) + s.Require().True(ok, "codec must be an AnyUnpacker") + + pbAny, err := sdkcodec.NewAnyWithValue(ethTxMsg) + s.NoError(err) + pbAnyBz, err := pbAny.Marshal() + s.NoError(err, pbAnyBz) + + var sdkMsg sdk.Msg + err = deps.EncCfg.Codec.UnpackAny(pbAny, &sdkMsg) + s.Require().NoError(err) + s.Equal("/eth.evm.v1.MsgEthereumTx", sdk.MsgTypeURL(sdkMsg)) + + s.T().Log("Dispatch the Eth tx msg from Wasm (unsuccessfully)") + _, _, err = wasmMsgHandler.DispatchMsg( + deps.Ctx, + wasmContractAddr, + "ibcport-unused", + wasmvm.CosmosMsg{ + Stargate: &wasmvm.StargateMsg{ + TypeURL: sdk.MsgTypeURL(ethTxMsg), + Value: protoValueBz, + }, + }, + ) + s.Require().ErrorContains(err, "Wasm VM to EVM call pattern is not yet supported") +}