diff --git a/app/app.go b/app/app.go index fc3c5167..b6ad5fe3 100644 --- a/app/app.go +++ b/app/app.go @@ -437,6 +437,9 @@ func NewLinkApp( panic("error while reading wasm config: " + err.Error()) } + // change wasm's StargateMsgEncoder to filtered encoder + wasmOpts = append(wasmOpts, wasmkeeper.WithMessageEncoders(filteredStargateMsgEncoders(appCodec))) + // The last arguments can contain custom message handlers, and custom query handlers, // if we want to allow any custom callbacks availableCapabilities := "iterator,staking,stargate,cosmwasm_1_1" diff --git a/app/wasm_filter.go b/app/wasm_filter.go new file mode 100644 index 00000000..e5cff9f7 --- /dev/null +++ b/app/wasm_filter.go @@ -0,0 +1,35 @@ +package app + +import ( + "strings" + + "github.com/Finschia/finschia-sdk/codec" + codectypes "github.com/Finschia/finschia-sdk/codec/types" + sdk "github.com/Finschia/finschia-sdk/types" + sdkerrors "github.com/Finschia/finschia-sdk/types/errors" + wasmkeeper "github.com/Finschia/wasmd/x/wasm/keeper" + wasmtypes "github.com/Finschia/wasmd/x/wasm/types" + wasmvmtypes "github.com/Finschia/wasmvm/types" +) + +// deniedModulesInStargateMsg is deny list in StargateMsg of wasm +var deniedModulesInStargateMsg = []string{"/lbm.fswap.v1", "/lbm.fbridge.v1"} + +func filteredStargateMsgEncoders(cdc codec.Codec) *wasmkeeper.MessageEncoders { + return &wasmkeeper.MessageEncoders{ + Stargate: wasmFilteredEncodeStargateMsg(cdc), + } +} + +func wasmFilteredEncodeStargateMsg(unpakcer codectypes.AnyUnpacker) wasmkeeper.StargateEncoder { + stargateMsgEncoder := wasmkeeper.EncodeStargateMsg(unpakcer) + return func(sender sdk.AccAddress, msg *wasmvmtypes.StargateMsg) ([]sdk.Msg, error) { + for _, moduleName := range deniedModulesInStargateMsg { + if strings.HasPrefix(msg.TypeURL, moduleName) { + return nil, sdkerrors.Wrap(wasmtypes.ErrUnsupportedForContract, moduleName+" not supported by Stargate") + } + } + + return stargateMsgEncoder(sender, msg) + } +} diff --git a/app/wasm_filter_test.go b/app/wasm_filter_test.go new file mode 100644 index 00000000..0cd57b7f --- /dev/null +++ b/app/wasm_filter_test.go @@ -0,0 +1,603 @@ +package app + +import ( + "testing" + + "github.com/golang/protobuf/proto" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + codectypes "github.com/Finschia/finschia-sdk/codec/types" + sdk "github.com/Finschia/finschia-sdk/types" + banktypes "github.com/Finschia/finschia-sdk/x/bank/types" + distributiontypes "github.com/Finschia/finschia-sdk/x/distribution/types" + govtypes "github.com/Finschia/finschia-sdk/x/gov/types" + stakingtypes "github.com/Finschia/finschia-sdk/x/staking/types" + wasmkeeper "github.com/Finschia/wasmd/x/wasm/keeper" + "github.com/Finschia/wasmd/x/wasm/keeper/wasmtesting" + wasmtypes "github.com/Finschia/wasmd/x/wasm/types" + wasmvmtypes "github.com/Finschia/wasmvm/types" + ibctransfertypes "github.com/cosmos/ibc-go/v4/modules/apps/transfer/types" + clienttypes "github.com/cosmos/ibc-go/v4/modules/core/02-client/types" + channeltypes "github.com/cosmos/ibc-go/v4/modules/core/04-channel/types" +) + +func TestFilteredStargateMsgEncoders(t *testing.T) { + var ( + addr1 = wasmkeeper.RandomAccountAddress(t) + addr2 = wasmkeeper.RandomAccountAddress(t) + addr3 = wasmkeeper.RandomAccountAddress(t) + invalidAddr = "xrnd1d02kd90n38qvr3qb9qof83fn2d2" + ) + valAddr := make(sdk.ValAddress, wasmtypes.SDKAddrLen) + valAddr[0] = 12 + valAddr2 := make(sdk.ValAddress, wasmtypes.SDKAddrLen) + valAddr2[1] = 123 + + jsonMsg := wasmtypes.RawContractMessage(`{"foo": 123}`) + + bankMsg := &banktypes.MsgSend{ + FromAddress: addr2.String(), + ToAddress: addr1.String(), + Amount: sdk.Coins{ + sdk.NewInt64Coin("uatom", 12345), + sdk.NewInt64Coin("utgd", 54321), + }, + } + bankMsgBin, err := proto.Marshal(bankMsg) + require.NoError(t, err) + + content, err := codectypes.NewAnyWithValue(wasmtypes.StoreCodeProposalFixture()) + require.NoError(t, err) + + proposalMsg := &govtypes.MsgSubmitProposal{ + Proposer: addr1.String(), + InitialDeposit: sdk.NewCoins(sdk.NewInt64Coin("uatom", 12345)), + Content: content, + } + proposalMsgBin, err := proto.Marshal(proposalMsg) + require.NoError(t, err) + + cases := map[string]struct { + sender sdk.AccAddress + srcMsg wasmvmtypes.CosmosMsg + srcContractIBCPort string + transferPortSource wasmtypes.ICS20TransferPortSource + // set if valid + output []sdk.Msg + // set if invalid + isError bool + }{ + "simple send": { + sender: addr1, + srcMsg: wasmvmtypes.CosmosMsg{ + Bank: &wasmvmtypes.BankMsg{ + Send: &wasmvmtypes.SendMsg{ + ToAddress: addr2.String(), + Amount: []wasmvmtypes.Coin{ + { + Denom: "uatom", + Amount: "12345", + }, + { + Denom: "usdt", + Amount: "54321", + }, + }, + }, + }, + }, + output: []sdk.Msg{ + &banktypes.MsgSend{ + FromAddress: addr1.String(), + ToAddress: addr2.String(), + Amount: sdk.Coins{ + sdk.NewInt64Coin("uatom", 12345), + sdk.NewInt64Coin("usdt", 54321), + }, + }, + }, + }, + "invalid send amount": { + sender: addr1, + srcMsg: wasmvmtypes.CosmosMsg{ + Bank: &wasmvmtypes.BankMsg{ + Send: &wasmvmtypes.SendMsg{ + ToAddress: addr2.String(), + Amount: []wasmvmtypes.Coin{ + { + Denom: "uatom", + Amount: "123.456", + }, + }, + }, + }, + }, + isError: true, + }, + "invalid address": { + sender: addr1, + srcMsg: wasmvmtypes.CosmosMsg{ + Bank: &wasmvmtypes.BankMsg{ + Send: &wasmvmtypes.SendMsg{ + ToAddress: invalidAddr, + Amount: []wasmvmtypes.Coin{ + { + Denom: "uatom", + Amount: "7890", + }, + }, + }, + }, + }, + isError: false, // addresses are checked in the handler + output: []sdk.Msg{ + &banktypes.MsgSend{ + FromAddress: addr1.String(), + ToAddress: invalidAddr, + Amount: sdk.Coins{ + sdk.NewInt64Coin("uatom", 7890), + }, + }, + }, + }, + "wasm execute": { + sender: addr1, + srcMsg: wasmvmtypes.CosmosMsg{ + Wasm: &wasmvmtypes.WasmMsg{ + Execute: &wasmvmtypes.ExecuteMsg{ + ContractAddr: addr2.String(), + Msg: jsonMsg, + Funds: []wasmvmtypes.Coin{ + wasmvmtypes.NewCoin(12, "eth"), + }, + }, + }, + }, + output: []sdk.Msg{ + &wasmtypes.MsgExecuteContract{ + Sender: addr1.String(), + Contract: addr2.String(), + Msg: jsonMsg, + Funds: sdk.NewCoins(sdk.NewInt64Coin("eth", 12)), + }, + }, + }, + "wasm instantiate": { + sender: addr1, + srcMsg: wasmvmtypes.CosmosMsg{ + Wasm: &wasmvmtypes.WasmMsg{ + Instantiate: &wasmvmtypes.InstantiateMsg{ + CodeID: 7, + Msg: jsonMsg, + Funds: []wasmvmtypes.Coin{ + wasmvmtypes.NewCoin(123, "eth"), + }, + Label: "myLabel", + Admin: addr2.String(), + }, + }, + }, + output: []sdk.Msg{ + &wasmtypes.MsgInstantiateContract{ + Sender: addr1.String(), + CodeID: 7, + Label: "myLabel", + Msg: jsonMsg, + Funds: sdk.NewCoins(sdk.NewInt64Coin("eth", 123)), + Admin: addr2.String(), + }, + }, + }, + "wasm migrate": { + sender: addr2, + srcMsg: wasmvmtypes.CosmosMsg{ + Wasm: &wasmvmtypes.WasmMsg{ + Migrate: &wasmvmtypes.MigrateMsg{ + ContractAddr: addr1.String(), + NewCodeID: 12, + Msg: jsonMsg, + }, + }, + }, + output: []sdk.Msg{ + &wasmtypes.MsgMigrateContract{ + Sender: addr2.String(), + Contract: addr1.String(), + CodeID: 12, + Msg: jsonMsg, + }, + }, + }, + "wasm update admin": { + sender: addr2, + srcMsg: wasmvmtypes.CosmosMsg{ + Wasm: &wasmvmtypes.WasmMsg{ + UpdateAdmin: &wasmvmtypes.UpdateAdminMsg{ + ContractAddr: addr1.String(), + Admin: addr3.String(), + }, + }, + }, + output: []sdk.Msg{ + &wasmtypes.MsgUpdateAdmin{ + Sender: addr2.String(), + Contract: addr1.String(), + NewAdmin: addr3.String(), + }, + }, + }, + "wasm clear admin": { + sender: addr2, + srcMsg: wasmvmtypes.CosmosMsg{ + Wasm: &wasmvmtypes.WasmMsg{ + ClearAdmin: &wasmvmtypes.ClearAdminMsg{ + ContractAddr: addr1.String(), + }, + }, + }, + output: []sdk.Msg{ + &wasmtypes.MsgClearAdmin{ + Sender: addr2.String(), + Contract: addr1.String(), + }, + }, + }, + "staking delegate": { + sender: addr1, + srcMsg: wasmvmtypes.CosmosMsg{ + Staking: &wasmvmtypes.StakingMsg{ + Delegate: &wasmvmtypes.DelegateMsg{ + Validator: valAddr.String(), + Amount: wasmvmtypes.NewCoin(777, "stake"), + }, + }, + }, + output: []sdk.Msg{ + &stakingtypes.MsgDelegate{ + DelegatorAddress: addr1.String(), + ValidatorAddress: valAddr.String(), + Amount: sdk.NewInt64Coin("stake", 777), + }, + }, + }, + "staking delegate to non-validator": { + sender: addr1, + srcMsg: wasmvmtypes.CosmosMsg{ + Staking: &wasmvmtypes.StakingMsg{ + Delegate: &wasmvmtypes.DelegateMsg{ + Validator: addr2.String(), + Amount: wasmvmtypes.NewCoin(777, "stake"), + }, + }, + }, + isError: false, // fails in the handler + output: []sdk.Msg{ + &stakingtypes.MsgDelegate{ + DelegatorAddress: addr1.String(), + ValidatorAddress: addr2.String(), + Amount: sdk.NewInt64Coin("stake", 777), + }, + }, + }, + "staking undelegate": { + sender: addr1, + srcMsg: wasmvmtypes.CosmosMsg{ + Staking: &wasmvmtypes.StakingMsg{ + Undelegate: &wasmvmtypes.UndelegateMsg{ + Validator: valAddr.String(), + Amount: wasmvmtypes.NewCoin(555, "stake"), + }, + }, + }, + output: []sdk.Msg{ + &stakingtypes.MsgUndelegate{ + DelegatorAddress: addr1.String(), + ValidatorAddress: valAddr.String(), + Amount: sdk.NewInt64Coin("stake", 555), + }, + }, + }, + "staking redelegate": { + sender: addr1, + srcMsg: wasmvmtypes.CosmosMsg{ + Staking: &wasmvmtypes.StakingMsg{ + Redelegate: &wasmvmtypes.RedelegateMsg{ + SrcValidator: valAddr.String(), + DstValidator: valAddr2.String(), + Amount: wasmvmtypes.NewCoin(222, "stake"), + }, + }, + }, + output: []sdk.Msg{ + &stakingtypes.MsgBeginRedelegate{ + DelegatorAddress: addr1.String(), + ValidatorSrcAddress: valAddr.String(), + ValidatorDstAddress: valAddr2.String(), + Amount: sdk.NewInt64Coin("stake", 222), + }, + }, + }, + "staking withdraw (explicit recipient)": { + sender: addr1, + srcMsg: wasmvmtypes.CosmosMsg{ + Distribution: &wasmvmtypes.DistributionMsg{ + WithdrawDelegatorReward: &wasmvmtypes.WithdrawDelegatorRewardMsg{ + Validator: valAddr2.String(), + }, + }, + }, + output: []sdk.Msg{ + &distributiontypes.MsgWithdrawDelegatorReward{ + DelegatorAddress: addr1.String(), + ValidatorAddress: valAddr2.String(), + }, + }, + }, + "staking set withdraw address": { + sender: addr1, + srcMsg: wasmvmtypes.CosmosMsg{ + Distribution: &wasmvmtypes.DistributionMsg{ + SetWithdrawAddress: &wasmvmtypes.SetWithdrawAddressMsg{ + Address: addr2.String(), + }, + }, + }, + output: []sdk.Msg{ + &distributiontypes.MsgSetWithdrawAddress{ + DelegatorAddress: addr1.String(), + WithdrawAddress: addr2.String(), + }, + }, + }, + "stargate encoded bank msg": { + sender: addr2, + srcMsg: wasmvmtypes.CosmosMsg{ + Stargate: &wasmvmtypes.StargateMsg{ + TypeURL: "/cosmos.bank.v1beta1.MsgSend", + Value: bankMsgBin, + }, + }, + output: []sdk.Msg{bankMsg}, + }, + "stargate encoded msg with any type": { + sender: addr2, + srcMsg: wasmvmtypes.CosmosMsg{ + Stargate: &wasmvmtypes.StargateMsg{ + TypeURL: "/cosmos.gov.v1beta1.MsgSubmitProposal", + Value: proposalMsgBin, + }, + }, + output: []sdk.Msg{proposalMsg}, + }, + "stargate encoded invalid typeUrl": { + sender: addr2, + srcMsg: wasmvmtypes.CosmosMsg{ + Stargate: &wasmvmtypes.StargateMsg{ + TypeURL: "/cosmos.bank.v2.MsgSend", + Value: bankMsgBin, + }, + }, + isError: true, + }, + "IBC transfer with block timeout": { + sender: addr1, + srcContractIBCPort: "myIBCPort", + srcMsg: wasmvmtypes.CosmosMsg{ + IBC: &wasmvmtypes.IBCMsg{ + Transfer: &wasmvmtypes.TransferMsg{ + ChannelID: "myChanID", + ToAddress: addr2.String(), + Amount: wasmvmtypes.Coin{ + Denom: "ALX", + Amount: "1", + }, + Timeout: wasmvmtypes.IBCTimeout{ + Block: &wasmvmtypes.IBCTimeoutBlock{Revision: 1, Height: 2}, + }, + }, + }, + }, + transferPortSource: wasmtesting.MockIBCTransferKeeper{GetPortFn: func(ctx sdk.Context) string { + return "myTransferPort" + }}, + output: []sdk.Msg{ + &ibctransfertypes.MsgTransfer{ + SourcePort: "myTransferPort", + SourceChannel: "myChanID", + Token: sdk.Coin{ + Denom: "ALX", + Amount: sdk.NewInt(1), + }, + Sender: addr1.String(), + Receiver: addr2.String(), + TimeoutHeight: clienttypes.Height{RevisionNumber: 1, RevisionHeight: 2}, + }, + }, + }, + "IBC transfer with time timeout": { + sender: addr1, + srcContractIBCPort: "myIBCPort", + srcMsg: wasmvmtypes.CosmosMsg{ + IBC: &wasmvmtypes.IBCMsg{ + Transfer: &wasmvmtypes.TransferMsg{ + ChannelID: "myChanID", + ToAddress: addr2.String(), + Amount: wasmvmtypes.Coin{ + Denom: "ALX", + Amount: "1", + }, + Timeout: wasmvmtypes.IBCTimeout{Timestamp: 100}, + }, + }, + }, + transferPortSource: wasmtesting.MockIBCTransferKeeper{GetPortFn: func(ctx sdk.Context) string { + return "transfer" + }}, + output: []sdk.Msg{ + &ibctransfertypes.MsgTransfer{ + SourcePort: "transfer", + SourceChannel: "myChanID", + Token: sdk.Coin{ + Denom: "ALX", + Amount: sdk.NewInt(1), + }, + Sender: addr1.String(), + Receiver: addr2.String(), + TimeoutTimestamp: 100, + }, + }, + }, + "IBC transfer with time and height timeout": { + sender: addr1, + srcContractIBCPort: "myIBCPort", + srcMsg: wasmvmtypes.CosmosMsg{ + IBC: &wasmvmtypes.IBCMsg{ + Transfer: &wasmvmtypes.TransferMsg{ + ChannelID: "myChanID", + ToAddress: addr2.String(), + Amount: wasmvmtypes.Coin{ + Denom: "ALX", + Amount: "1", + }, + Timeout: wasmvmtypes.IBCTimeout{Timestamp: 100, Block: &wasmvmtypes.IBCTimeoutBlock{Height: 1, Revision: 2}}, + }, + }, + }, + transferPortSource: wasmtesting.MockIBCTransferKeeper{GetPortFn: func(ctx sdk.Context) string { + return "transfer" + }}, + output: []sdk.Msg{ + &ibctransfertypes.MsgTransfer{ + SourcePort: "transfer", + SourceChannel: "myChanID", + Token: sdk.Coin{ + Denom: "ALX", + Amount: sdk.NewInt(1), + }, + Sender: addr1.String(), + Receiver: addr2.String(), + TimeoutTimestamp: 100, + TimeoutHeight: clienttypes.NewHeight(2, 1), + }, + }, + }, + "IBC close channel": { + sender: addr1, + srcContractIBCPort: "myIBCPort", + srcMsg: wasmvmtypes.CosmosMsg{ + IBC: &wasmvmtypes.IBCMsg{ + CloseChannel: &wasmvmtypes.CloseChannelMsg{ + ChannelID: "channel-1", + }, + }, + }, + output: []sdk.Msg{ + &channeltypes.MsgChannelCloseInit{ + PortId: "wasm." + addr1.String(), + ChannelId: "channel-1", + Signer: addr1.String(), + }, + }, + }, + "Gov vote: yes": { + sender: addr1, + srcContractIBCPort: "myIBCPort", + srcMsg: wasmvmtypes.CosmosMsg{ + Gov: &wasmvmtypes.GovMsg{ + Vote: &wasmvmtypes.VoteMsg{ProposalId: 1, Vote: wasmvmtypes.Yes}, + }, + }, + output: []sdk.Msg{ + &govtypes.MsgVote{ + ProposalId: 1, + Voter: addr1.String(), + Option: govtypes.OptionYes, + }, + }, + }, + "Gov vote: No": { + sender: addr1, + srcContractIBCPort: "myIBCPort", + srcMsg: wasmvmtypes.CosmosMsg{ + Gov: &wasmvmtypes.GovMsg{ + Vote: &wasmvmtypes.VoteMsg{ProposalId: 1, Vote: wasmvmtypes.No}, + }, + }, + output: []sdk.Msg{ + &govtypes.MsgVote{ + ProposalId: 1, + Voter: addr1.String(), + Option: govtypes.OptionNo, + }, + }, + }, + "Gov vote: Abstain": { + sender: addr1, + srcContractIBCPort: "myIBCPort", + srcMsg: wasmvmtypes.CosmosMsg{ + Gov: &wasmvmtypes.GovMsg{ + Vote: &wasmvmtypes.VoteMsg{ProposalId: 10, Vote: wasmvmtypes.Abstain}, + }, + }, + output: []sdk.Msg{ + &govtypes.MsgVote{ + ProposalId: 10, + Voter: addr1.String(), + Option: govtypes.OptionAbstain, + }, + }, + }, + "Gov vote: No with veto": { + sender: addr1, + srcContractIBCPort: "myIBCPort", + srcMsg: wasmvmtypes.CosmosMsg{ + Gov: &wasmvmtypes.GovMsg{ + Vote: &wasmvmtypes.VoteMsg{ProposalId: 1, Vote: wasmvmtypes.NoWithVeto}, + }, + }, + output: []sdk.Msg{ + &govtypes.MsgVote{ + ProposalId: 1, + Voter: addr1.String(), + Option: govtypes.OptionNoWithVeto, + }, + }, + }, + "stargate encoded filtered (fswap)": { + sender: addr1, + srcMsg: wasmvmtypes.CosmosMsg{ + Stargate: &wasmvmtypes.StargateMsg{ + TypeURL: "/lbm.fswap.v1.MsgSwap", + Value: nil, + }, + }, + isError: true, + }, + "stargate encoded filtered (fbridge)": { + sender: addr1, + srcMsg: wasmvmtypes.CosmosMsg{ + Stargate: &wasmvmtypes.StargateMsg{ + TypeURL: "/lbm.fbridge.v1.MsgTransfer", + Value: nil, + }, + }, + isError: true, + }, + } + encodingConfig := MakeEncodingConfig() + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + var ctx sdk.Context + encoder := wasmkeeper.DefaultEncoders(encodingConfig.Marshaler, tc.transferPortSource) + encoder.Merge(filteredStargateMsgEncoders(encodingConfig.Marshaler)) + res, err := encoder.Encode(ctx, tc.sender, tc.srcContractIBCPort, tc.srcMsg) + if tc.isError { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tc.output, res) + } + }) + } +}