diff --git a/app/app.go b/app/app.go index 73c50bf77..95d17eb27 100644 --- a/app/app.go +++ b/app/app.go @@ -114,6 +114,8 @@ import ( feeburnertypes "github.com/neutron-org/neutron/x/feeburner/types" "github.com/neutron-org/neutron/x/feerefunder" feekeeper "github.com/neutron-org/neutron/x/feerefunder/keeper" + ibchooks "github.com/neutron-org/neutron/x/ibc-hooks" + ibchookstypes "github.com/neutron-org/neutron/x/ibc-hooks/types" "github.com/neutron-org/neutron/x/interchainqueries" interchainqueriesmodulekeeper "github.com/neutron-org/neutron/x/interchainqueries/keeper" interchainqueriesmoduletypes "github.com/neutron-org/neutron/x/interchainqueries/types" @@ -211,6 +213,7 @@ var ( upgraderest.ProposalCancelRESTHandler, ), ), + ibchooks.AppModuleBasic{}, router.AppModuleBasic{}, ) @@ -289,6 +292,9 @@ type App struct { RouterModule router.AppModule + HooksTransferIBCModule *ibchooks.IBCMiddleware + HooksICS4Wrapper ibchooks.ICS4Middleware + // make scoped keepers public for test purposes ScopedIBCKeeper capabilitykeeper.ScopedKeeper ScopedTransferKeeper capabilitykeeper.ScopedKeeper @@ -340,7 +346,7 @@ func New( icahosttypes.StoreKey, capabilitytypes.StoreKey, interchainqueriesmoduletypes.StoreKey, contractmanagermoduletypes.StoreKey, interchaintxstypes.StoreKey, wasm.StoreKey, feetypes.StoreKey, feeburnertypes.StoreKey, adminmodulemoduletypes.StoreKey, ccvconsumertypes.StoreKey, tokenfactorytypes.StoreKey, routertypes.StoreKey, - crontypes.StoreKey, + crontypes.StoreKey, ibchookstypes.StoreKey, ) tkeys := sdk.NewTransientStoreKeys(paramstypes.TStoreKey) memKeys := sdk.NewMemoryStoreKeys(capabilitytypes.MemStoreKey, feetypes.MemStoreKey) @@ -449,12 +455,25 @@ func New( app.BankKeeper, app.IBCKeeper.ChannelKeeper, ) + wasmHooks := ibchooks.NewWasmHooks(nil, sdk.GetConfig().GetBech32AccountAddrPrefix()) // The contract keeper needs to be set later + app.HooksICS4Wrapper = ibchooks.NewICS4Middleware( + app.RouterKeeper, + &wasmHooks, + ) // Create Transfer Keepers app.TransferKeeper = wrapkeeper.NewKeeper( - appCodec, keys[ibctransfertypes.StoreKey], app.GetSubspace(ibctransfertypes.ModuleName), - app.RouterKeeper, app.IBCKeeper.ChannelKeeper, &app.IBCKeeper.PortKeeper, - app.AccountKeeper, app.BankKeeper, scopedTransferKeeper, app.FeeKeeper, app.ContractManagerKeeper, + appCodec, + keys[ibctransfertypes.StoreKey], + app.GetSubspace(ibctransfertypes.ModuleName), + app.HooksICS4Wrapper, // essentially still app.IBCKeeper.ChannelKeeper under the hood because no hook overrides + app.IBCKeeper.ChannelKeeper, + &app.IBCKeeper.PortKeeper, + app.AccountKeeper, + app.BankKeeper, + scopedTransferKeeper, + app.FeeKeeper, + app.ContractManagerKeeper, ) app.RouterKeeper.SetTransferKeeper(app.TransferKeeper.Keeper) @@ -568,6 +587,7 @@ func New( supportedFeatures, wasmOpts..., ) + wasmHooks.ContractKeeper = wasmkeeper.NewDefaultPermissionKeeper(app.WasmKeeper) app.CronKeeper.WasmMsgServer = wasmkeeper.NewMsgServerImpl(wasmkeeper.NewDefaultPermissionKeeper(app.WasmKeeper)) cronModule := cron.NewAppModule(appCodec, &app.CronKeeper) @@ -576,6 +596,9 @@ func New( app.AdminmoduleKeeper.Router().AddRoute(wasm.RouterKey, wasm.NewWasmProposalHandler(app.WasmKeeper, enabledProposals)) } transferIBCModule := transferSudo.NewIBCModule(app.TransferKeeper) + // receive call order: wasmHooks#OnRecvPacketOverride(transferIbcModule#OnRecvPacket()) + ibcHooksMiddleware := ibchooks.NewIBCMiddleware(&transferIBCModule, &app.HooksICS4Wrapper) + app.HooksTransferIBCModule = &ibcHooksMiddleware // Create static IBC router, add transfer route, then set and seal it ibcRouter := ibcporttypes.NewRouter() @@ -592,11 +615,12 @@ func New( interchainQueriesModule := interchainqueries.NewAppModule(appCodec, app.InterchainQueriesKeeper, app.AccountKeeper, app.BankKeeper) interchainTxsModule := interchaintxs.NewAppModule(appCodec, app.InterchainTxsKeeper, app.AccountKeeper, app.BankKeeper) contractManagerModule := contractmanager.NewAppModule(appCodec, app.ContractManagerKeeper) + ibcHooksModule := ibchooks.NewAppModule(app.AccountKeeper) app.RouterModule = router.NewAppModule(app.RouterKeeper) ibcStack := router.NewIBCMiddleware( - transferIBCModule, + app.HooksTransferIBCModule, app.RouterKeeper, 0, routerkeeper.DefaultForwardTransferPacketTimeoutTimestamp, @@ -644,6 +668,7 @@ func New( feeBurnerModule, contractManagerModule, adminModule, + ibcHooksModule, tokenfactory.NewAppModule(appCodec, *app.TokenFactoryKeeper, app.AccountKeeper, app.BankKeeper), cronModule, ) @@ -676,6 +701,7 @@ func New( feetypes.ModuleName, feeburnertypes.ModuleName, adminmodulemoduletypes.ModuleName, + ibchookstypes.ModuleName, routertypes.ModuleName, crontypes.ModuleName, ) @@ -704,6 +730,7 @@ func New( feetypes.ModuleName, feeburnertypes.ModuleName, adminmodulemoduletypes.ModuleName, + ibchookstypes.ModuleName, routertypes.ModuleName, crontypes.ModuleName, ) @@ -737,6 +764,7 @@ func New( feetypes.ModuleName, feeburnertypes.ModuleName, adminmodulemoduletypes.ModuleName, + ibchookstypes.ModuleName, // after auth keeper routertypes.ModuleName, crontypes.ModuleName, ) diff --git a/go.mod b/go.mod index 0b128af4a..f32015478 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/CosmWasm/wasmvm v1.1.1 github.com/confio/ics23/go v0.9.0 github.com/cosmos/admin-module v0.0.0-00010101000000-000000000000 + github.com/cosmos/cosmos-proto v1.0.0-alpha8 github.com/cosmos/cosmos-sdk v0.45.13 github.com/cosmos/ibc-go/v4 v4.3.0 github.com/cosmos/interchain-security v1.0.0-rc3 @@ -17,7 +18,6 @@ require ( github.com/grpc-ecosystem/grpc-gateway v1.16.0 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.14.0 - github.com/regen-network/cosmos-proto v0.3.1 github.com/spf13/cast v1.5.0 github.com/spf13/cobra v1.6.1 github.com/strangelove-ventures/packet-forward-middleware/v4 v4.0.5 @@ -44,7 +44,6 @@ require ( github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/coinbase/rosetta-sdk-go v0.7.9 // indirect github.com/cosmos/btcutil v1.0.4 // indirect - github.com/cosmos/cosmos-proto v1.0.0-alpha8 // indirect github.com/cosmos/go-bip39 v1.0.0 // indirect github.com/cosmos/gogoproto v1.4.3 // indirect github.com/cosmos/gorocksdb v1.2.0 // indirect @@ -62,7 +61,6 @@ require ( github.com/dvsekhvalnov/jose2go v1.5.0 // indirect github.com/felixge/httpsnoop v1.0.2 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect - github.com/ghodss/yaml v1.0.0 // indirect github.com/go-kit/kit v0.12.0 // indirect github.com/go-kit/log v0.2.1 // indirect github.com/go-logfmt/logfmt v0.5.1 // indirect @@ -111,6 +109,7 @@ require ( github.com/prometheus/procfs v0.8.0 // indirect github.com/rakyll/statik v0.1.7 // indirect github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect + github.com/regen-network/cosmos-proto v0.3.1 // indirect github.com/rs/cors v1.8.2 // indirect github.com/rs/zerolog v1.27.0 // indirect github.com/sasha-s/go-deadlock v0.3.1 // indirect @@ -135,7 +134,7 @@ require ( google.golang.org/protobuf v1.28.2-0.20220831092852-f930b1dc76e8 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - nhooyr.io/websocket v1.8.6 // indirect + nhooyr.io/websocket v1.8.7 // indirect ) replace ( diff --git a/go.sum b/go.sum index 8ad2d7267..fd42a049b 100644 --- a/go.sum +++ b/go.sum @@ -944,7 +944,6 @@ github.com/getkin/kin-openapi v0.61.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= @@ -3525,8 +3524,9 @@ mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jC mvdan.cc/unparam v0.0.0-20190720180237-d51796306d8f/go.mod h1:4G1h5nDURzA3bwVMZIVpwbkw+04kSxk3rAtzlimaUJw= mvdan.cc/unparam v0.0.0-20200501210554-b37ab49443f7/go.mod h1:HGC5lll35J70Y5v7vCGb9oLhHoScFwkHDJm/05RdSTc= mvdan.cc/unparam v0.0.0-20220706161116-678bad134442/go.mod h1:F/Cxw/6mVrNKqrR2YjFf5CaW0Bw4RL8RfbEf4GRggJk= -nhooyr.io/websocket v1.8.6 h1:s+C3xAMLwGmlI31Nyn/eAehUlZPwfYZu2JXM621Q5/k= nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= +nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= +nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= pack.ag/amqp v0.11.2/go.mod h1:4/cbmt4EJXSKlG6LCfWHoqmN0uFdy5i/+YFz+fTfhV4= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/testutil/test_helpers.go b/testutil/test_helpers.go index b9ff5d890..7c7187f19 100644 --- a/testutil/test_helpers.go +++ b/testutil/test_helpers.go @@ -25,7 +25,7 @@ import ( clienttypes "github.com/cosmos/ibc-go/v4/modules/core/02-client/types" appProvider "github.com/cosmos/interchain-security/app/provider" - e2e "github.com/cosmos/interchain-security/testutil/e2e" + "github.com/cosmos/interchain-security/testutil/e2e" "github.com/cosmos/interchain-security/x/ccv/utils" tmtypes "github.com/tendermint/tendermint/types" @@ -413,13 +413,12 @@ func NewTransferPath(chainA, chainB, chainProvider *ibctesting.TestChain) *ibcte // SetupTransferPath func SetupTransferPath(path *ibctesting.Path) error { - ctx := path.EndpointA.Chain.GetContext() - - channelSequence := path.EndpointA.Chain.App.GetIBCKeeper().ChannelKeeper.GetNextChannelSequence(ctx) + channelSequence := path.EndpointA.Chain.App.GetIBCKeeper().ChannelKeeper.GetNextChannelSequence(path.EndpointA.Chain.GetContext()) + channelSequenceB := path.EndpointB.Chain.App.GetIBCKeeper().ChannelKeeper.GetNextChannelSequence(path.EndpointB.Chain.GetContext()) // update port/channel ids path.EndpointA.ChannelID = channeltypes.FormatChannelIdentifier(channelSequence) - path.EndpointA.ChannelConfig.PortID = types.PortID + path.EndpointB.ChannelID = channeltypes.FormatChannelIdentifier(channelSequenceB) if err := path.EndpointA.ChanOpenInit(); err != nil { return err diff --git a/x/ibc-hooks/README.md b/x/ibc-hooks/README.md new file mode 100644 index 000000000..b8b9d49f0 --- /dev/null +++ b/x/ibc-hooks/README.md @@ -0,0 +1,127 @@ +# IBC-hooks + +Taken from [osmosis](https://github.com/osmosis-labs/osmosis) `v14.0.0-rc1` (commit `26e2fad8e7b3eb7c33965360b31a593b392d7d75`) + +Removed `ibc_callback` functionality since we already have similar [sudo callback mechanism](https://docs.neutron.org/neutron/transfer/overview#ibc-transfer-results-handover) in Transfer module. + +Module https://github.com/osmosis-labs/osmosis/tree/v14.0.0-rc1/x/ibc-hooks + +## Wasm Hooks + +The wasm hook is an IBC middleware which is used to allow ICS-20 token transfers to initiate contract calls. +This allows cross-chain contract calls, that involve token movement. +This is useful for a variety of usecases. +One of primary importance is cross-chain swaps, which is an extremely powerful primitive. + +The mechanism enabling this is a `memo` field on every ICS20 transfer packet as of [IBC v3.4.0](https://medium.com/the-interchain-foundation/moving-beyond-simple-token-transfers-d42b2b1dc29b). +Wasm hooks is an IBC middleware that parses an ICS20 transfer, and if the `memo` field is of a particular form, executes a wasm contract call. We now detail the `memo` format for `wasm` contract calls, and the execution guarantees provided. + +### Cosmwasm Contract Execution Format + +Before we dive into the IBC metadata format, we show the cosmwasm execute message format, so the reader has a sense of what are the fields we need to be setting in. +The cosmwasm `MsgExecuteContract` is defined [here](https://github.com/CosmWasm/wasmd/blob/4fe2fbc8f322efdaf187e2e5c99ce32fd1df06f0/x/wasm/types/tx.pb.go#L340-L349 +) as the following type: + +```go +type MsgExecuteContract struct { +// Sender is the that actor that signed the messages +Sender string +// Contract is the address of the smart contract +Contract string +// Msg json encoded message to be passed to the contract +Msg RawContractMessage +// Funds coins that are transferred to the contract on execution +Funds sdk.Coins +} +``` + +So we detail where we want to get each of these fields from: + +* Sender: We cannot trust the sender of an IBC packet, the counterparty chain has full ability to lie about it. + We cannot risk this sender being confused for a particular user or module address on Neutron. + So we replace the sender with an account to represent the sender prefixed by the channel and a wasm module prefix. + This is done by setting the sender to `Bech32(Hash("ibc-wasm-hook-intermediaryg" || channelID || sender))`, where the channelId is the channel id on the local chain. +* Contract: This field should be directly obtained from the ICS-20 packet metadata +* Msg: This field should be directly obtained from the ICS-20 packet metadata. +* Funds: This field is set to the amount of funds being sent over in the ICS 20 packet. One detail is that the denom in the packet is the counterparty chains representation of the denom, so we have to translate it to Neutron' representation. + +So our constructed cosmwasm message that we execute will look like: + +```go +msg := MsgExecuteContract{ + // Sender is the that actor that signed the messages + Sender: "ntrn-hash-of-channel-and-sender", + // Contract is the address of the smart contract + Contract: packet.data.memo["wasm"]["ContractAddress"], + // Msg json encoded message to be passed to the contract + Msg: packet.data.memo["wasm"]["Msg"], + // Funds coins that are transferred to the contract on execution + Funds: sdk.NewCoin{Denom: ibc.ConvertSenderDenomToLocalDenom(packet.data.Denom), Amount: packet.data.Amount} +} +``` + +### ICS20 packet structure + +So given the details above, we propagate the implied ICS20 packet data structure. +ICS20 is JSON native, so we use JSON for the memo format. + +```json +{ + //... other ibc fields that we don't care about + "data":{ + "denom": "denom on counterparty chain (e.g. uatom)", // will be transformed to the local denom (ibc/...) + "amount": "1000", + "sender": "addr on counterparty chain", // will be transformed + "receiver": "contract addr or blank", + "memo": { + "wasm": { + "contract": "ntrnContractAddr", + "msg": { + "raw_message_fields": "raw_message_data", + } + } + } + } +} +``` + +An ICS20 packet is formatted correctly for wasmhooks iff the following all hold: + +* `memo` is not blank +* `memo` is valid JSON +* `memo` has at least one key, with value `"wasm"` +* `memo["wasm"]` has exactly two entries, `"contract"` and `"msg"` +* `memo["wasm"]["msg"]` is a valid JSON object +* `receiver == "" || receiver == memo["wasm"]["contract"]` + +We consider an ICS20 packet as directed towards wasmhooks iff all of the following hold: + +* `memo` is not blank +* `memo` is valid JSON +* `memo` has at least one key, with name `"wasm"` + +If an ICS20 packet is not directed towards wasmhooks, wasmhooks doesn't do anything. +If an ICS20 packet is directed towards wasmhooks, and is formatted incorrectly, then wasmhooks returns an error. + +### Execution flow + +Pre wasm hooks: + +* Ensure the incoming IBC packet is cryptographically valid +* Ensure the incoming IBC packet is not timed out. + +In Wasm hooks, pre packet execution: + +* Ensure the packet is correctly formatted (as defined above) +* Edit the receiver to be the hardcoded IBC module account + +In wasm hooks, post packet execution: + +* Construct wasm message as defined before +* Execute wasm message +* if wasm message has error, return ErrAck +* otherwise continue through middleware + +# Testing strategy + +See go tests. diff --git a/x/ibc-hooks/bytecode/README.md b/x/ibc-hooks/bytecode/README.md new file mode 100644 index 000000000..13b499c34 --- /dev/null +++ b/x/ibc-hooks/bytecode/README.md @@ -0,0 +1,4 @@ +# Description +This is a place for bytecode to test ibc-hooks. + +- echo.wasm is an echo contract compiled from https://github.com/neutron-org/neutron-dev-contracts/ diff --git a/x/ibc-hooks/bytecode/echo.wasm b/x/ibc-hooks/bytecode/echo.wasm new file mode 100644 index 000000000..1c23bc083 Binary files /dev/null and b/x/ibc-hooks/bytecode/echo.wasm differ diff --git a/x/ibc-hooks/client/cli/query.go b/x/ibc-hooks/client/cli/query.go new file mode 100644 index 000000000..c695830fc --- /dev/null +++ b/x/ibc-hooks/client/cli/query.go @@ -0,0 +1,66 @@ +package cli + +import ( + "fmt" + "strings" + + "github.com/neutron-org/neutron/x/ibc-hooks/utils" + + "github.com/cosmos/cosmos-sdk/client" + + "github.com/neutron-org/neutron/x/ibc-hooks/types" + + "github.com/cosmos/cosmos-sdk/client/flags" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/version" + "github.com/spf13/cobra" +) + +// GetQueryCmd returns the cli query commands for this module. +func GetQueryCmd() *cobra.Command { + // Group queries under a subcommand + cmd := &cobra.Command{ + Use: types.ModuleName, + Short: fmt.Sprintf("Querying commands for the %s module", types.ModuleName), + DisableFlagParsing: true, + SuggestionsMinimumDistance: 2, + RunE: client.ValidateCmd, + } + cmd.AddCommand( + GetCmdWasmSender(), + ) + return cmd +} + +// GetCmdPoolParams return pool params. +func GetCmdWasmSender() *cobra.Command { + cmd := &cobra.Command{ + Use: "wasm-sender ", + Short: "Generate the local address for a wasm hooks sender", + Long: strings.TrimSpace( + fmt.Sprintf(`Generate the local address for a wasm hooks sender. +Example: +$ %s query ibc-hooks wasm-hooks-sender channel-42 juno12smx2wdlyttvyzvzg54y2vnqwq2qjatezqwqxu +`, + version.AppName, + ), + ), + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + channelID := args[0] + originalSender := args[1] + // TODO: Make this flexible as an arg + prefix := sdk.GetConfig().GetBech32AccountAddrPrefix() + senderBech32, err := utils.DeriveIntermediateSender(channelID, originalSender, prefix) + if err != nil { + return err + } + fmt.Println(senderBech32) + return nil + }, + } + + flags.AddQueryFlagsToCmd(cmd) + + return cmd +} diff --git a/x/ibc-hooks/hooks.go b/x/ibc-hooks/hooks.go new file mode 100644 index 000000000..8b6d4d0a9 --- /dev/null +++ b/x/ibc-hooks/hooks.go @@ -0,0 +1,144 @@ +package ibchooks + +import ( + // external libraries + sdk "github.com/cosmos/cosmos-sdk/types" + capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" + + // ibc-go + channeltypes "github.com/cosmos/ibc-go/v4/modules/core/04-channel/types" + ibcexported "github.com/cosmos/ibc-go/v4/modules/core/exported" +) + +type Hooks interface{} + +type OnChanOpenInitOverrideHooks interface { + OnChanOpenInitOverride(im IBCMiddleware, ctx sdk.Context, order channeltypes.Order, connectionHops []string, portID string, channelID string, channelCap *capabilitytypes.Capability, counterparty channeltypes.Counterparty, version string) (string, error) +} +type OnChanOpenInitBeforeHooks interface { + OnChanOpenInitBeforeHook(ctx sdk.Context, order channeltypes.Order, connectionHops []string, portID string, channelID string, channelCap *capabilitytypes.Capability, counterparty channeltypes.Counterparty, version string) +} +type OnChanOpenInitAfterHooks interface { + OnChanOpenInitAfterHook(ctx sdk.Context, order channeltypes.Order, connectionHops []string, portID string, channelID string, channelCap *capabilitytypes.Capability, counterparty channeltypes.Counterparty, version string, finalVersion string, err error) +} + +// OnChanOpenTry Hooks +type OnChanOpenTryOverrideHooks interface { + OnChanOpenTryOverride(im IBCMiddleware, ctx sdk.Context, order channeltypes.Order, connectionHops []string, portID, channelID string, channelCap *capabilitytypes.Capability, counterparty channeltypes.Counterparty, counterpartyVersion string) (string, error) +} +type OnChanOpenTryBeforeHooks interface { + OnChanOpenTryBeforeHook(ctx sdk.Context, order channeltypes.Order, connectionHops []string, portID, channelID string, channelCap *capabilitytypes.Capability, counterparty channeltypes.Counterparty, counterpartyVersion string) +} +type OnChanOpenTryAfterHooks interface { + OnChanOpenTryAfterHook(ctx sdk.Context, order channeltypes.Order, connectionHops []string, portID, channelID string, channelCap *capabilitytypes.Capability, counterparty channeltypes.Counterparty, counterpartyVersion string, version string, err error) +} + +// OnChanOpenAck Hooks +type OnChanOpenAckOverrideHooks interface { + OnChanOpenAckOverride(im IBCMiddleware, ctx sdk.Context, portID, channelID string, counterpartyChannelID string, counterpartyVersion string) error +} +type OnChanOpenAckBeforeHooks interface { + OnChanOpenAckBeforeHook(ctx sdk.Context, portID, channelID string, counterpartyChannelID string, counterpartyVersion string) +} +type OnChanOpenAckAfterHooks interface { + OnChanOpenAckAfterHook(ctx sdk.Context, portID, channelID string, counterpartyChannelID string, counterpartyVersion string, err error) +} + +// OnChanOpenConfirm Hooks +type OnChanOpenConfirmOverrideHooks interface { + OnChanOpenConfirmOverride(im IBCMiddleware, ctx sdk.Context, portID, channelID string) error +} +type OnChanOpenConfirmBeforeHooks interface { + OnChanOpenConfirmBeforeHook(ctx sdk.Context, portID, channelID string) +} +type OnChanOpenConfirmAfterHooks interface { + OnChanOpenConfirmAfterHook(ctx sdk.Context, portID, channelID string, err error) +} + +// OnChanCloseInit Hooks +type OnChanCloseInitOverrideHooks interface { + OnChanCloseInitOverride(im IBCMiddleware, ctx sdk.Context, portID, channelID string) error +} +type OnChanCloseInitBeforeHooks interface { + OnChanCloseInitBeforeHook(ctx sdk.Context, portID, channelID string) +} +type OnChanCloseInitAfterHooks interface { + OnChanCloseInitAfterHook(ctx sdk.Context, portID, channelID string, err error) +} + +// OnChanCloseConfirm Hooks +type OnChanCloseConfirmOverrideHooks interface { + OnChanCloseConfirmOverride(im IBCMiddleware, ctx sdk.Context, portID, channelID string) error +} +type OnChanCloseConfirmBeforeHooks interface { + OnChanCloseConfirmBeforeHook(ctx sdk.Context, portID, channelID string) +} +type OnChanCloseConfirmAfterHooks interface { + OnChanCloseConfirmAfterHook(ctx sdk.Context, portID, channelID string, err error) +} + +// OnRecvPacket Hooks +type OnRecvPacketOverrideHooks interface { + OnRecvPacketOverride(im IBCMiddleware, ctx sdk.Context, packet channeltypes.Packet, relayer sdk.AccAddress) ibcexported.Acknowledgement +} +type OnRecvPacketBeforeHooks interface { + OnRecvPacketBeforeHook(ctx sdk.Context, packet channeltypes.Packet, relayer sdk.AccAddress) +} +type OnRecvPacketAfterHooks interface { + OnRecvPacketAfterHook(ctx sdk.Context, packet channeltypes.Packet, relayer sdk.AccAddress, ack ibcexported.Acknowledgement) +} + +// OnAcknowledgementPacket Hooks +type OnAcknowledgementPacketOverrideHooks interface { + OnAcknowledgementPacketOverride(im IBCMiddleware, ctx sdk.Context, packet channeltypes.Packet, acknowledgement []byte, relayer sdk.AccAddress) error +} +type OnAcknowledgementPacketBeforeHooks interface { + OnAcknowledgementPacketBeforeHook(ctx sdk.Context, packet channeltypes.Packet, acknowledgement []byte, relayer sdk.AccAddress) +} +type OnAcknowledgementPacketAfterHooks interface { + OnAcknowledgementPacketAfterHook(ctx sdk.Context, packet channeltypes.Packet, acknowledgement []byte, relayer sdk.AccAddress, err error) +} + +// OnTimeoutPacket Hooks +type OnTimeoutPacketOverrideHooks interface { + OnTimeoutPacketOverride(im IBCMiddleware, ctx sdk.Context, packet channeltypes.Packet, relayer sdk.AccAddress) error +} +type OnTimeoutPacketBeforeHooks interface { + OnTimeoutPacketBeforeHook(ctx sdk.Context, packet channeltypes.Packet, relayer sdk.AccAddress) +} +type OnTimeoutPacketAfterHooks interface { + OnTimeoutPacketAfterHook(ctx sdk.Context, packet channeltypes.Packet, relayer sdk.AccAddress, err error) +} + +// SendPacket Hooks +type SendPacketOverrideHooks interface { + SendPacketOverride(i ICS4Middleware, ctx sdk.Context, chanCap *capabilitytypes.Capability, packet ibcexported.PacketI) error +} +type SendPacketBeforeHooks interface { + SendPacketBeforeHook(ctx sdk.Context, chanCap *capabilitytypes.Capability, packet ibcexported.PacketI) +} +type SendPacketAfterHooks interface { + SendPacketAfterHook(ctx sdk.Context, chanCap *capabilitytypes.Capability, packet ibcexported.PacketI, err error) +} + +// WriteAcknowledgement Hooks +type WriteAcknowledgementOverrideHooks interface { + WriteAcknowledgementOverride(i ICS4Middleware, ctx sdk.Context, chanCap *capabilitytypes.Capability, packet ibcexported.PacketI, ack ibcexported.Acknowledgement) error +} +type WriteAcknowledgementBeforeHooks interface { + WriteAcknowledgementBeforeHook(ctx sdk.Context, chanCap *capabilitytypes.Capability, packet ibcexported.PacketI, ack ibcexported.Acknowledgement) +} +type WriteAcknowledgementAfterHooks interface { + WriteAcknowledgementAfterHook(ctx sdk.Context, chanCap *capabilitytypes.Capability, packet ibcexported.PacketI, ack ibcexported.Acknowledgement, err error) +} + +// GetAppVersion Hooks +type GetAppVersionOverrideHooks interface { + GetAppVersionOverride(i ICS4Middleware, ctx sdk.Context, portID, channelID string) (string, bool) +} +type GetAppVersionBeforeHooks interface { + GetAppVersionBeforeHook(ctx sdk.Context, portID, channelID string) +} +type GetAppVersionAfterHooks interface { + GetAppVersionAfterHook(ctx sdk.Context, portID, channelID string, result string, success bool) +} diff --git a/x/ibc-hooks/ibc_middleware_test.go b/x/ibc-hooks/ibc_middleware_test.go new file mode 100644 index 000000000..322e5cfc6 --- /dev/null +++ b/x/ibc-hooks/ibc_middleware_test.go @@ -0,0 +1,358 @@ +package ibchooks_test + +import ( + "encoding/json" + "fmt" + "os" + "testing" + + wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" + wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" + ibctesting "github.com/cosmos/interchain-security/legacy_ibc_testing/testing" + "github.com/neutron-org/neutron/app/params" + "github.com/neutron-org/neutron/testutil" + "github.com/neutron-org/neutron/x/ibc-hooks/testutils" + "github.com/neutron-org/neutron/x/ibc-hooks/utils" + "github.com/stretchr/testify/suite" + + sdk "github.com/cosmos/cosmos-sdk/types" + + transfertypes "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" +) + +type HooksTestSuite struct { + testutil.IBCConnectionTestSuite +} + +func TestIBCHooksTestSuite(t *testing.T) { + suite.Run(t, new(HooksTestSuite)) +} + +func (suite *HooksTestSuite) TestOnRecvPacketHooks() { + var ( + trace transfertypes.DenomTrace + amount sdk.Int + receiver string + status testutils.Status + ) + + testCases := []struct { + msg string + malleate func(*testutils.Status) + expPass bool + }{ + {"override", func(status *testutils.Status) { + suite.GetNeutronZoneApp(suite.ChainB).HooksTransferIBCModule. + ICS4Middleware.Hooks = testutils.TestRecvOverrideHooks{Status: status} + }, true}, + {"before and after", func(status *testutils.Status) { + suite.GetNeutronZoneApp(suite.ChainB).HooksTransferIBCModule. + ICS4Middleware.Hooks = testutils.TestRecvBeforeAfterHooks{Status: status} + }, true}, + } + + for _, tc := range testCases { + tc := tc + suite.Run(tc.msg, func() { + suite.SetupTest() // reset + + suite.ConfigureTransferChannel() + receiver = suite.ChainB.SenderAccount.GetAddress().String() // must be explicitly changed in malleate + status = testutils.Status{} + + amount = sdk.NewInt(100) // must be explicitly changed in malleate + seq := uint64(1) + + trace = transfertypes.ParseDenomTrace(params.DefaultDenom) + + // send coin from chainA to chainB + transferMsg := transfertypes.NewMsgTransfer( + suite.TransferPath.EndpointA.ChannelConfig.PortID, + suite.TransferPath.EndpointA.ChannelID, + sdk.NewCoin(trace.IBCDenom(), amount), + suite.ChainA.SenderAccount.GetAddress().String(), + receiver, + clienttypes.NewHeight(1, 110), + 0) + _, err := suite.ChainA.SendMsgs(transferMsg) + suite.Require().NoError(err) // message committed + + tc.malleate(&status) + + data := transfertypes.NewFungibleTokenPacketData(trace.GetFullDenomPath(), amount.String(), suite.ChainA.SenderAccount.GetAddress().String(), receiver) + packet := channeltypes.NewPacket(data.GetBytes(), seq, suite.TransferPath.EndpointA.ChannelConfig.PortID, suite.TransferPath.EndpointA.ChannelID, suite.TransferPath.EndpointB.ChannelConfig.PortID, suite.TransferPath.EndpointB.ChannelID, clienttypes.NewHeight(1, 100), 0) + + ack := suite.GetNeutronZoneApp(suite.ChainB).HooksTransferIBCModule. + OnRecvPacket(suite.ChainB.GetContext(), packet, suite.ChainA.SenderAccount.GetAddress()) + + if tc.expPass { + suite.Require().True(ack.Success()) + } else { + suite.Require().False(ack.Success()) + } + + if _, ok := suite.GetNeutronZoneApp(suite.ChainB).HooksTransferIBCModule. + ICS4Middleware.Hooks.(testutils.TestRecvOverrideHooks); ok { + suite.Require().True(status.OverrideRan) + suite.Require().False(status.BeforeRan) + suite.Require().False(status.AfterRan) + } + + if _, ok := suite.GetNeutronZoneApp(suite.ChainB).HooksTransferIBCModule. + ICS4Middleware.Hooks.(testutils.TestRecvBeforeAfterHooks); ok { + suite.Require().False(status.OverrideRan) + suite.Require().True(status.BeforeRan) + suite.Require().True(status.AfterRan) + } + }) + } +} + +func (suite *HooksTestSuite) makeMockPacket(receiver, memo string, prevSequence uint64) channeltypes.Packet { + packetData := transfertypes.FungibleTokenPacketData{ + Denom: sdk.DefaultBondDenom, + Amount: "1", + Sender: suite.ChainB.SenderAccount.GetAddress().String(), + Receiver: receiver, + Memo: memo, + } + + return channeltypes.NewPacket( + packetData.GetBytes(), + prevSequence+1, + suite.TransferPath.EndpointB.ChannelConfig.PortID, + suite.TransferPath.EndpointB.ChannelID, + suite.TransferPath.EndpointA.ChannelConfig.PortID, + suite.TransferPath.EndpointA.ChannelID, + clienttypes.NewHeight(0, 150), + 0, + ) +} + +func (suite *HooksTestSuite) receivePacket(receiver, memo string) []byte { + return suite.receivePacketWithSequence(receiver, memo, 0) +} + +func (suite *HooksTestSuite) receivePacketWithSequence(receiver, memo string, prevSequence uint64) []byte { + channelCap := suite.ChainB.GetChannelCapability( + suite.TransferPath.EndpointB.ChannelConfig.PortID, + suite.TransferPath.EndpointB.ChannelID) + + packet := suite.makeMockPacket(receiver, memo, prevSequence) + + err := suite.GetNeutronZoneApp(suite.ChainB).HooksICS4Wrapper.SendPacket( + suite.ChainB.GetContext(), channelCap, packet) + suite.Require().NoError(err, "IBC send failed. Expected success. %s", err) + + // Update both clients + err = suite.TransferPath.EndpointB.UpdateClient() + suite.Require().NoError(err) + err = suite.TransferPath.EndpointA.UpdateClient() + suite.Require().NoError(err) + + // recv in chain a + res, err := suite.TransferPath.EndpointA.RecvPacketWithResult(packet) + + // get the ack from the chain a's response + ack, err := ibctesting.ParseAckFromEvents(res.GetEvents()) + suite.Require().NoError(err) + + // manually send the acknowledgement to chain b + err = suite.TransferPath.EndpointA.AcknowledgePacket(packet, ack) + suite.Require().NoError(err) + return ack +} + +func (suite *HooksTestSuite) TestRecvTransferWithMetadata() { + suite.ConfigureTransferChannel() + + // Setup contract + codeId := suite.StoreContractCode(suite.ChainA, sdk.MustAccAddressFromBech32(testutil.TestOwnerAddress), "./bytecode/echo.wasm") + addr := suite.InstantiateContract(suite.ChainA, sdk.MustAccAddressFromBech32(testutil.TestOwnerAddress), codeId, "{}") + + ackBytes := suite.receivePacket(addr.String(), fmt.Sprintf(`{"wasm": {"contract": "%s", "msg": {"echo": {"msg": "test"} } } }`, addr)) + ackStr := string(ackBytes) + fmt.Println(ackStr) + var ack map[string]string // This can't be unmarshalled to Acknowledgement because it's fetched from the events + err := json.Unmarshal(ackBytes, &ack) + suite.Require().NoError(err) + suite.Require().NotContains(ack, "error") + suite.Require().Equal(ack["result"], "eyJjb250cmFjdF9yZXN1bHQiOiJkR2hwY3lCemFHOTFiR1FnWldOb2J3PT0iLCJpYmNfYWNrIjoiZXlKeVpYTjFiSFFpT2lKQlVUMDlJbjA9In0=") +} + +// After successfully executing a wasm call, the contract should have the funds sent via IBC +func (suite *HooksTestSuite) TestFundsAreTransferredToTheContract() { + suite.ConfigureTransferChannel() + + // Setup contract + codeId := suite.StoreContractCode(suite.ChainA, sdk.MustAccAddressFromBech32(testutil.TestOwnerAddress), "./bytecode/echo.wasm") + addr := suite.InstantiateContract(suite.ChainA, sdk.MustAccAddressFromBech32(testutil.TestOwnerAddress), codeId, "{}") + + // Check that the contract has no funds + localDenom := utils.MustExtractDenomFromPacketOnRecv(suite.makeMockPacket("", "", 0)) + balance := suite.GetNeutronZoneApp(suite.ChainA).BankKeeper.GetBalance(suite.ChainA.GetContext(), addr, localDenom) + suite.Require().Equal(sdk.NewInt(0), balance.Amount) + + // Execute the contract via IBC + ackBytes := suite.receivePacket(addr.String(), fmt.Sprintf(`{"wasm": {"contract": "%s", "msg": {"echo": {"msg": "test"} } } }`, addr)) + ackStr := string(ackBytes) + fmt.Println(ackStr) + var ack map[string]string // This can't be unmarshalled to Acknowledgement because it's fetched from the events + err := json.Unmarshal(ackBytes, &ack) + suite.Require().NoError(err) + suite.Require().NotContains(ack, "error") + suite.Require().Equal(ack["result"], "eyJjb250cmFjdF9yZXN1bHQiOiJkR2hwY3lCemFHOTFiR1FnWldOb2J3PT0iLCJpYmNfYWNrIjoiZXlKeVpYTjFiSFFpT2lKQlVUMDlJbjA9In0=") + + // Check that the token has now been transferred to the contract + balance = suite.GetNeutronZoneApp(suite.ChainA).BankKeeper.GetBalance(suite.ChainA.GetContext(), addr, localDenom) + suite.Require().Equal(sdk.NewInt(1), balance.Amount) +} + +// If the wasm call wails, the contract acknowledgement should be an error and the funds returned +func (suite *HooksTestSuite) TestFundsAreReturnedOnFailedContractExec() { + suite.ConfigureTransferChannel() + + // Setup contract + codeId := suite.StoreContractCode(suite.ChainA, sdk.MustAccAddressFromBech32(testutil.TestOwnerAddress), "./bytecode/echo.wasm") + addr := suite.InstantiateContract(suite.ChainA, sdk.MustAccAddressFromBech32(testutil.TestOwnerAddress), codeId, "{}") + + // Check that the contract has no funds + localDenom := utils.MustExtractDenomFromPacketOnRecv(suite.makeMockPacket("", "", 0)) + balance := suite.GetNeutronZoneApp(suite.ChainA).BankKeeper.GetBalance(suite.ChainA.GetContext(), addr, localDenom) + suite.Require().Equal(sdk.NewInt(0), balance.Amount) + + // Execute the contract via IBC with a message that the contract will reject + ackBytes := suite.receivePacket(addr.String(), fmt.Sprintf(`{"wasm": {"contract": "%s", "msg": {"not_echo": {"msg": "test"} } } }`, addr)) + ackStr := string(ackBytes) + fmt.Println(ackStr) + var ack map[string]string // This can't be unmarshalled to Acknowledgement because it's fetched from the events + err := json.Unmarshal(ackBytes, &ack) + suite.Require().NoError(err) + suite.Require().Contains(ack, "error") + + // Check that the token has now been transferred to the contract + balance = suite.GetNeutronZoneApp(suite.ChainA).BankKeeper.GetBalance(suite.ChainA.GetContext(), addr, localDenom) + fmt.Println(balance) + suite.Require().Equal(sdk.NewInt(0), balance.Amount) +} + +func (suite *HooksTestSuite) TestPacketsThatShouldBeSkipped() { + suite.ConfigureTransferChannel() + + var sequence uint64 + receiver := suite.ChainB.SenderAccount.GetAddress().String() + + testCases := []struct { + memo string + expPassthrough bool + }{ + {"", true}, + {"{01]", true}, // bad json + {"{}", true}, + {`{"something": ""}`, true}, + {`{"wasm": "test"}`, false}, + {`{"wasm": []`, true}, // invalid top level JSON + {`{"wasm": {}`, true}, // invalid top level JSON + {`{"wasm": []}`, false}, + {`{"wasm": {}}`, false}, + {`{"wasm": {"contract": "something"}}`, false}, + {`{"wasm": {"contract": "cosmos1clpqr4nrk4khgkxj78fcwwh6dl3uw4epasmvnj"}}`, false}, + {`{"wasm": {"msg": "something"}}`, false}, + // invalid receiver + {`{"wasm": {"contract": "cosmos1clpqr4nrk4khgkxj78fcwwh6dl3uw4epasmvnj", "msg": {}}}`, false}, + // msg not an object + {fmt.Sprintf(`{"wasm": {"contract": "%s", "msg": 1}}`, receiver), false}, + } + + for _, tc := range testCases { + ackBytes := suite.receivePacketWithSequence(receiver, tc.memo, sequence) + ackStr := string(ackBytes) + // fmt.Println(ackStr) + var ack map[string]string // This can't be unmarshalled to Acknowledgement because it's fetched from the events + err := json.Unmarshal(ackBytes, &ack) + suite.Require().NoError(err) + if tc.expPassthrough { + suite.Require().Equal("AQ==", ack["result"], tc.memo) + } else { + suite.Require().Contains(ackStr, "error", tc.memo) + } + sequence += 1 + } +} + +type Direction int64 + +const ( + AtoB Direction = iota + BtoA +) + +func (suite *HooksTestSuite) GetEndpoints(direction Direction) (sender *ibctesting.Endpoint, receiver *ibctesting.Endpoint) { + switch direction { + case AtoB: + sender = suite.TransferPath.EndpointA + receiver = suite.TransferPath.EndpointB + case BtoA: + sender = suite.TransferPath.EndpointB + receiver = suite.TransferPath.EndpointA + } + return sender, receiver +} + +func (suite *HooksTestSuite) RelayPacket(packet channeltypes.Packet, direction Direction) (*sdk.Result, []byte) { + sender, receiver := suite.GetEndpoints(direction) + + err := receiver.UpdateClient() + suite.Require().NoError(err) + + // receiver Receives + receiveResult, err := receiver.RecvPacketWithResult(packet) + suite.Require().NoError(err) + + ack, err := ibctesting.ParseAckFromEvents(receiveResult.GetEvents()) + suite.Require().NoError(err) + + // sender Acknowledges + err = sender.AcknowledgePacket(packet, ack) + suite.Require().NoError(err) + + err = sender.UpdateClient() + suite.Require().NoError(err) + err = receiver.UpdateClient() + suite.Require().NoError(err) + + return receiveResult, ack +} + +func (suite *HooksTestSuite) StoreContractCode(chain *ibctesting.TestChain, addr sdk.AccAddress, path string) uint64 { + wasmCode, err := os.ReadFile(path) + if err != nil { + panic(err) + } + + codeID, _, err := wasmkeeper.NewDefaultPermissionKeeper(suite.GetNeutronZoneApp(chain).WasmKeeper).Create(chain.GetContext(), addr, wasmCode, &wasmtypes.AccessConfig{Permission: wasmtypes.AccessTypeEverybody, Address: ""}) + if err != nil { + panic(err) + } + + return codeID +} + +func (suite *HooksTestSuite) InstantiateContract(chain *ibctesting.TestChain, funder sdk.AccAddress, codeID uint64, initMsg string) sdk.AccAddress { + contractKeeper := wasmkeeper.NewDefaultPermissionKeeper(suite.GetNeutronZoneApp(chain).WasmKeeper) + addr, _, err := contractKeeper.Instantiate(chain.GetContext(), codeID, funder, funder, []byte(initMsg), "demo contract", nil) + if err != nil { + panic(err) + } + return addr +} + +func (suite *HooksTestSuite) QueryContract(chain *ibctesting.TestChain, contract sdk.AccAddress, req []byte) string { + state, err := suite.GetNeutronZoneApp(chain).WasmKeeper.QuerySmart(chain.GetContext(), contract, req) + if err != nil { + panic(err) + } + return string(state) +} diff --git a/x/ibc-hooks/ibc_module.go b/x/ibc-hooks/ibc_module.go new file mode 100644 index 000000000..b60193fda --- /dev/null +++ b/x/ibc-hooks/ibc_module.go @@ -0,0 +1,257 @@ +package ibchooks + +import ( + // external libraries + sdk "github.com/cosmos/cosmos-sdk/types" + capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" + + // ibc-go + channeltypes "github.com/cosmos/ibc-go/v4/modules/core/04-channel/types" + porttypes "github.com/cosmos/ibc-go/v4/modules/core/05-port/types" + ibcexported "github.com/cosmos/ibc-go/v4/modules/core/exported" +) + +var _ porttypes.Middleware = &IBCMiddleware{} + +type IBCMiddleware struct { + App porttypes.IBCModule + ICS4Middleware *ICS4Middleware +} + +func NewIBCMiddleware(app porttypes.IBCModule, ics4 *ICS4Middleware) IBCMiddleware { + return IBCMiddleware{ + App: app, + ICS4Middleware: ics4, + } +} + +// OnChanOpenInit implements the IBCMiddleware interface +func (im IBCMiddleware) OnChanOpenInit( + ctx sdk.Context, + order channeltypes.Order, + connectionHops []string, + portID string, + channelID string, + channelCap *capabilitytypes.Capability, + counterparty channeltypes.Counterparty, + version string, +) (string, error) { + if hook, ok := im.ICS4Middleware.Hooks.(OnChanOpenInitOverrideHooks); ok { + return hook.OnChanOpenInitOverride(im, ctx, order, connectionHops, portID, channelID, channelCap, counterparty, version) + } + + if hook, ok := im.ICS4Middleware.Hooks.(OnChanOpenInitBeforeHooks); ok { + hook.OnChanOpenInitBeforeHook(ctx, order, connectionHops, portID, channelID, channelCap, counterparty, version) + } + + finalVersion, err := im.App.OnChanOpenInit(ctx, order, connectionHops, portID, channelID, channelCap, counterparty, version) + + if hook, ok := im.ICS4Middleware.Hooks.(OnChanOpenInitAfterHooks); ok { + hook.OnChanOpenInitAfterHook(ctx, order, connectionHops, portID, channelID, channelCap, counterparty, version, finalVersion, err) + } + return version, err +} + +// OnChanOpenTry implements the IBCMiddleware interface +func (im IBCMiddleware) OnChanOpenTry( + ctx sdk.Context, + order channeltypes.Order, + connectionHops []string, + portID, + channelID string, + channelCap *capabilitytypes.Capability, + counterparty channeltypes.Counterparty, + counterpartyVersion string, +) (string, error) { + if hook, ok := im.ICS4Middleware.Hooks.(OnChanOpenTryOverrideHooks); ok { + return hook.OnChanOpenTryOverride(im, ctx, order, connectionHops, portID, channelID, channelCap, counterparty, counterpartyVersion) + } + + if hook, ok := im.ICS4Middleware.Hooks.(OnChanOpenTryBeforeHooks); ok { + hook.OnChanOpenTryBeforeHook(ctx, order, connectionHops, portID, channelID, channelCap, counterparty, counterpartyVersion) + } + + version, err := im.App.OnChanOpenTry(ctx, order, connectionHops, portID, channelID, channelCap, counterparty, counterpartyVersion) + + if hook, ok := im.ICS4Middleware.Hooks.(OnChanOpenTryAfterHooks); ok { + hook.OnChanOpenTryAfterHook(ctx, order, connectionHops, portID, channelID, channelCap, counterparty, counterpartyVersion, version, err) + } + return version, err +} + +// OnChanOpenAck implements the IBCMiddleware interface +func (im IBCMiddleware) OnChanOpenAck( + ctx sdk.Context, + portID, + channelID string, + counterpartyChannelID string, + counterpartyVersion string, +) error { + if hook, ok := im.ICS4Middleware.Hooks.(OnChanOpenAckOverrideHooks); ok { + return hook.OnChanOpenAckOverride(im, ctx, portID, channelID, counterpartyChannelID, counterpartyVersion) + } + + if hook, ok := im.ICS4Middleware.Hooks.(OnChanOpenAckBeforeHooks); ok { + hook.OnChanOpenAckBeforeHook(ctx, portID, channelID, counterpartyChannelID, counterpartyVersion) + } + err := im.App.OnChanOpenAck(ctx, portID, channelID, counterpartyChannelID, counterpartyVersion) + if hook, ok := im.ICS4Middleware.Hooks.(OnChanOpenAckAfterHooks); ok { + hook.OnChanOpenAckAfterHook(ctx, portID, channelID, counterpartyChannelID, counterpartyVersion, err) + } + + return err +} + +// OnChanOpenConfirm implements the IBCMiddleware interface +func (im IBCMiddleware) OnChanOpenConfirm( + ctx sdk.Context, + portID, + channelID string, +) error { + if hook, ok := im.ICS4Middleware.Hooks.(OnChanOpenConfirmOverrideHooks); ok { + return hook.OnChanOpenConfirmOverride(im, ctx, portID, channelID) + } + + if hook, ok := im.ICS4Middleware.Hooks.(OnChanOpenConfirmBeforeHooks); ok { + hook.OnChanOpenConfirmBeforeHook(ctx, portID, channelID) + } + err := im.App.OnChanOpenConfirm(ctx, portID, channelID) + if hook, ok := im.ICS4Middleware.Hooks.(OnChanOpenConfirmAfterHooks); ok { + hook.OnChanOpenConfirmAfterHook(ctx, portID, channelID, err) + } + return err +} + +// OnChanCloseInit implements the IBCMiddleware interface +func (im IBCMiddleware) OnChanCloseInit( + ctx sdk.Context, + portID, + channelID string, +) error { + // Here we can remove the limits when a new channel is closed. For now, they can remove them manually on the contract + if hook, ok := im.ICS4Middleware.Hooks.(OnChanCloseInitOverrideHooks); ok { + return hook.OnChanCloseInitOverride(im, ctx, portID, channelID) + } + + if hook, ok := im.ICS4Middleware.Hooks.(OnChanCloseInitBeforeHooks); ok { + hook.OnChanCloseInitBeforeHook(ctx, portID, channelID) + } + err := im.App.OnChanCloseInit(ctx, portID, channelID) + if hook, ok := im.ICS4Middleware.Hooks.(OnChanCloseInitAfterHooks); ok { + hook.OnChanCloseInitAfterHook(ctx, portID, channelID, err) + } + + return err +} + +// OnChanCloseConfirm implements the IBCMiddleware interface +func (im IBCMiddleware) OnChanCloseConfirm( + ctx sdk.Context, + portID, + channelID string, +) error { + // Here we can remove the limits when a new channel is closed. For now, they can remove them manually on the contract + if hook, ok := im.ICS4Middleware.Hooks.(OnChanCloseConfirmOverrideHooks); ok { + return hook.OnChanCloseConfirmOverride(im, ctx, portID, channelID) + } + + if hook, ok := im.ICS4Middleware.Hooks.(OnChanCloseConfirmBeforeHooks); ok { + hook.OnChanCloseConfirmBeforeHook(ctx, portID, channelID) + } + err := im.App.OnChanCloseConfirm(ctx, portID, channelID) + if hook, ok := im.ICS4Middleware.Hooks.(OnChanCloseConfirmAfterHooks); ok { + hook.OnChanCloseConfirmAfterHook(ctx, portID, channelID, err) + } + + return err +} + +// OnRecvPacket implements the IBCMiddleware interface +func (im IBCMiddleware) OnRecvPacket( + ctx sdk.Context, + packet channeltypes.Packet, + relayer sdk.AccAddress, +) ibcexported.Acknowledgement { + if hook, ok := im.ICS4Middleware.Hooks.(OnRecvPacketOverrideHooks); ok { + return hook.OnRecvPacketOverride(im, ctx, packet, relayer) + } + + if hook, ok := im.ICS4Middleware.Hooks.(OnRecvPacketBeforeHooks); ok { + hook.OnRecvPacketBeforeHook(ctx, packet, relayer) + } + + ack := im.App.OnRecvPacket(ctx, packet, relayer) + + if hook, ok := im.ICS4Middleware.Hooks.(OnRecvPacketAfterHooks); ok { + hook.OnRecvPacketAfterHook(ctx, packet, relayer, ack) + } + + return ack +} + +// OnAcknowledgementPacket implements the IBCMiddleware interface +func (im IBCMiddleware) OnAcknowledgementPacket( + ctx sdk.Context, + packet channeltypes.Packet, + acknowledgement []byte, + relayer sdk.AccAddress, +) error { + if hook, ok := im.ICS4Middleware.Hooks.(OnAcknowledgementPacketOverrideHooks); ok { + return hook.OnAcknowledgementPacketOverride(im, ctx, packet, acknowledgement, relayer) + } + if hook, ok := im.ICS4Middleware.Hooks.(OnAcknowledgementPacketBeforeHooks); ok { + hook.OnAcknowledgementPacketBeforeHook(ctx, packet, acknowledgement, relayer) + } + + err := im.App.OnAcknowledgementPacket(ctx, packet, acknowledgement, relayer) + + if hook, ok := im.ICS4Middleware.Hooks.(OnAcknowledgementPacketAfterHooks); ok { + hook.OnAcknowledgementPacketAfterHook(ctx, packet, acknowledgement, relayer, err) + } + + return err +} + +// OnTimeoutPacket implements the IBCMiddleware interface +func (im IBCMiddleware) OnTimeoutPacket( + ctx sdk.Context, + packet channeltypes.Packet, + relayer sdk.AccAddress, +) error { + if hook, ok := im.ICS4Middleware.Hooks.(OnTimeoutPacketOverrideHooks); ok { + return hook.OnTimeoutPacketOverride(im, ctx, packet, relayer) + } + + if hook, ok := im.ICS4Middleware.Hooks.(OnTimeoutPacketBeforeHooks); ok { + hook.OnTimeoutPacketBeforeHook(ctx, packet, relayer) + } + err := im.App.OnTimeoutPacket(ctx, packet, relayer) + if hook, ok := im.ICS4Middleware.Hooks.(OnTimeoutPacketAfterHooks); ok { + hook.OnTimeoutPacketAfterHook(ctx, packet, relayer, err) + } + + return err +} + +// SendPacket implements the ICS4 Wrapper interface +func (im IBCMiddleware) SendPacket( + ctx sdk.Context, + chanCap *capabilitytypes.Capability, + packet ibcexported.PacketI, +) error { + return im.ICS4Middleware.SendPacket(ctx, chanCap, packet) +} + +// WriteAcknowledgement implements the ICS4 Wrapper interface +func (im IBCMiddleware) WriteAcknowledgement( + ctx sdk.Context, + chanCap *capabilitytypes.Capability, + packet ibcexported.PacketI, + ack ibcexported.Acknowledgement, +) error { + return im.ICS4Middleware.WriteAcknowledgement(ctx, chanCap, packet, ack) +} + +func (im IBCMiddleware) GetAppVersion(ctx sdk.Context, portID, channelID string) (string, bool) { + return im.ICS4Middleware.GetAppVersion(ctx, portID, channelID) +} diff --git a/x/ibc-hooks/ics4_middleware.go b/x/ibc-hooks/ics4_middleware.go new file mode 100644 index 000000000..19ef100f6 --- /dev/null +++ b/x/ibc-hooks/ics4_middleware.go @@ -0,0 +1,77 @@ +package ibchooks + +import ( + // external libraries + sdk "github.com/cosmos/cosmos-sdk/types" + capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" + + // ibc-go + porttypes "github.com/cosmos/ibc-go/v4/modules/core/05-port/types" + ibcexported "github.com/cosmos/ibc-go/v4/modules/core/exported" +) + +var _ porttypes.ICS4Wrapper = &ICS4Middleware{} + +type ICS4Middleware struct { + channel porttypes.ICS4Wrapper + + // Hooks + Hooks Hooks +} + +func NewICS4Middleware(channel porttypes.ICS4Wrapper, hooks Hooks) ICS4Middleware { + return ICS4Middleware{ + channel: channel, + Hooks: hooks, + } +} + +func (i ICS4Middleware) SendPacket(ctx sdk.Context, channelCap *capabilitytypes.Capability, packet ibcexported.PacketI) error { + if hook, ok := i.Hooks.(SendPacketOverrideHooks); ok { + return hook.SendPacketOverride(i, ctx, channelCap, packet) + } + + if hook, ok := i.Hooks.(SendPacketBeforeHooks); ok { + hook.SendPacketBeforeHook(ctx, channelCap, packet) + } + + err := i.channel.SendPacket(ctx, channelCap, packet) + + if hook, ok := i.Hooks.(SendPacketAfterHooks); ok { + hook.SendPacketAfterHook(ctx, channelCap, packet, err) + } + + return err +} + +func (i ICS4Middleware) WriteAcknowledgement(ctx sdk.Context, chanCap *capabilitytypes.Capability, packet ibcexported.PacketI, ack ibcexported.Acknowledgement) error { + if hook, ok := i.Hooks.(WriteAcknowledgementOverrideHooks); ok { + return hook.WriteAcknowledgementOverride(i, ctx, chanCap, packet, ack) + } + + if hook, ok := i.Hooks.(WriteAcknowledgementBeforeHooks); ok { + hook.WriteAcknowledgementBeforeHook(ctx, chanCap, packet, ack) + } + err := i.channel.WriteAcknowledgement(ctx, chanCap, packet, ack) + if hook, ok := i.Hooks.(WriteAcknowledgementAfterHooks); ok { + hook.WriteAcknowledgementAfterHook(ctx, chanCap, packet, ack, err) + } + + return err +} + +func (i ICS4Middleware) GetAppVersion(ctx sdk.Context, portID, channelID string) (string, bool) { + if hook, ok := i.Hooks.(GetAppVersionOverrideHooks); ok { + return hook.GetAppVersionOverride(i, ctx, portID, channelID) + } + + if hook, ok := i.Hooks.(GetAppVersionBeforeHooks); ok { + hook.GetAppVersionBeforeHook(ctx, portID, channelID) + } + version, err := i.channel.GetAppVersion(ctx, portID, channelID) + if hook, ok := i.Hooks.(GetAppVersionAfterHooks); ok { + hook.GetAppVersionAfterHook(ctx, portID, channelID, version, err) + } + + return version, err +} diff --git a/x/ibc-hooks/sdkmodule.go b/x/ibc-hooks/sdkmodule.go new file mode 100644 index 000000000..f2be6ecec --- /dev/null +++ b/x/ibc-hooks/sdkmodule.go @@ -0,0 +1,136 @@ +package ibchooks + +import ( + "encoding/json" + "fmt" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/types/module" + "github.com/gorilla/mux" + "github.com/grpc-ecosystem/grpc-gateway/runtime" + "github.com/spf13/cobra" + + "github.com/neutron-org/neutron/x/ibc-hooks/client/cli" + "github.com/neutron-org/neutron/x/ibc-hooks/types" + + cdctypes "github.com/cosmos/cosmos-sdk/codec/types" + + sdk "github.com/cosmos/cosmos-sdk/types" + abci "github.com/tendermint/tendermint/abci/types" +) + +var ( + _ module.AppModule = AppModule{} + _ module.AppModuleBasic = AppModuleBasic{} +) + +// AppModuleBasic defines the basic application module used by the ibc-hooks module. +type AppModuleBasic struct{} + +var _ module.AppModuleBasic = AppModuleBasic{} + +// Name returns the ibc-hooks module's name. +func (AppModuleBasic) Name() string { + return types.ModuleName +} + +// RegisterLegacyAminoCodec registers the ibc-hooks module's types on the given LegacyAmino codec. +func (AppModuleBasic) RegisterLegacyAminoCodec(_ *codec.LegacyAmino) {} + +// RegisterInterfaces registers the module's interface types. +func (b AppModuleBasic) RegisterInterfaces(_ cdctypes.InterfaceRegistry) {} + +// DefaultGenesis returns default genesis state as raw bytes for the +// module. +func (AppModuleBasic) DefaultGenesis(_ codec.JSONCodec) json.RawMessage { + emptyString := "{}" + return []byte(emptyString) +} + +// ValidateGenesis performs genesis state validation for the ibc-hooks module. +func (AppModuleBasic) ValidateGenesis(_ codec.JSONCodec, _ client.TxEncodingConfig, _ json.RawMessage) error { + return nil +} + +// RegisterRESTRoutes registers the REST routes for the ibc-hooks module. +func (AppModuleBasic) RegisterRESTRoutes(_ client.Context, _ *mux.Router) {} + +// RegisterGRPCGatewayRoutes registers the gRPC Gateway routes for the ibc-hooks module. +func (AppModuleBasic) RegisterGRPCGatewayRoutes(_ client.Context, _ *runtime.ServeMux) {} + +// GetTxCmd returns no root tx command for the ibc-hooks module. +func (AppModuleBasic) GetTxCmd() *cobra.Command { return nil } + +// GetQueryCmd returns the root query command for the ibc-hooks module. +func (AppModuleBasic) GetQueryCmd() *cobra.Command { + return cli.GetQueryCmd() +} + +// ___________________________________________________________________________ + +// AppModule implements an application module for the ibc-hooks module. +type AppModule struct { + AppModuleBasic + + authKeeper types.AccountKeeper +} + +// NewAppModule creates a new AppModule object. +func NewAppModule(ak types.AccountKeeper) AppModule { + return AppModule{ + AppModuleBasic: AppModuleBasic{}, + authKeeper: ak, + } +} + +// Name returns the ibc-hooks module's name. +func (AppModule) Name() string { + return types.ModuleName +} + +// RegisterInvariants registers the ibc-hooks module invariants. +func (am AppModule) RegisterInvariants(_ sdk.InvariantRegistry) {} + +// Route returns the message routing key for the ibc-hooks module. +func (AppModule) Route() sdk.Route { return sdk.Route{} } + +// QuerierRoute returns the module's querier route name. +func (AppModule) QuerierRoute() string { + return "" +} + +// LegacyQuerierHandler returns the x/ibc-hooks module's sdk.Querier. +func (am AppModule) LegacyQuerierHandler(_ *codec.LegacyAmino) sdk.Querier { + return func(sdk.Context, []string, abci.RequestQuery) ([]byte, error) { + return nil, fmt.Errorf("legacy querier not supported for the x/%s module", types.ModuleName) + } +} + +// RegisterServices registers a gRPC query service to respond to the +// module-specific gRPC queries. +func (am AppModule) RegisterServices(_ module.Configurator) { +} + +// InitGenesis performs genesis initialization for the ibc-hooks module. It returns +// no validator updates. +func (am AppModule) InitGenesis(_ sdk.Context, _ codec.JSONCodec, _ json.RawMessage) []abci.ValidatorUpdate { + return []abci.ValidatorUpdate{} +} + +func (am AppModule) ExportGenesis(_ sdk.Context, _ codec.JSONCodec) json.RawMessage { + return []byte("{}") +} + +// BeginBlock returns the begin blocker for the ibc-hooks module. +func (am AppModule) BeginBlock(_ sdk.Context, _ abci.RequestBeginBlock) { +} + +// EndBlock returns the end blocker for the ibc-hooks module. It returns no validator +// updates. +func (AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { + return []abci.ValidatorUpdate{} +} + +// ConsensusVersion implements AppModule/ConsensusVersion. +func (AppModule) ConsensusVersion() uint64 { return 1 } diff --git a/x/ibc-hooks/testutils/testing_hooks.go b/x/ibc-hooks/testutils/testing_hooks.go new file mode 100644 index 000000000..a5cda30f5 --- /dev/null +++ b/x/ibc-hooks/testutils/testing_hooks.go @@ -0,0 +1,42 @@ +package testutils + +import ( + // external libraries + sdk "github.com/cosmos/cosmos-sdk/types" + + // ibc-go + channeltypes "github.com/cosmos/ibc-go/v4/modules/core/04-channel/types" + ibcexported "github.com/cosmos/ibc-go/v4/modules/core/exported" + + ibchooks "github.com/neutron-org/neutron/x/ibc-hooks" +) + +var ( + _ ibchooks.Hooks = TestRecvOverrideHooks{} + _ ibchooks.Hooks = TestRecvBeforeAfterHooks{} +) + +type Status struct { + OverrideRan bool + BeforeRan bool + AfterRan bool +} + +// Recv +type TestRecvOverrideHooks struct{ Status *Status } + +func (t TestRecvOverrideHooks) OnRecvPacketOverride(im ibchooks.IBCMiddleware, ctx sdk.Context, packet channeltypes.Packet, relayer sdk.AccAddress) ibcexported.Acknowledgement { + t.Status.OverrideRan = true + ack := im.App.OnRecvPacket(ctx, packet, relayer) + return ack +} + +type TestRecvBeforeAfterHooks struct{ Status *Status } + +func (t TestRecvBeforeAfterHooks) OnRecvPacketBeforeHook(_ sdk.Context, _ channeltypes.Packet, _ sdk.AccAddress) { + t.Status.BeforeRan = true +} + +func (t TestRecvBeforeAfterHooks) OnRecvPacketAfterHook(_ sdk.Context, _ channeltypes.Packet, _ sdk.AccAddress, _ ibcexported.Acknowledgement) { + t.Status.AfterRan = true +} diff --git a/x/ibc-hooks/types/errors.go b/x/ibc-hooks/types/errors.go new file mode 100644 index 000000000..9683d397f --- /dev/null +++ b/x/ibc-hooks/types/errors.go @@ -0,0 +1,15 @@ +package types + +import sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + +var ( + ErrBadMetadataFormatMsg = "wasm metadata not properly formatted for: '%v'. %s" + ErrBadExecutionMsg = "cannot execute contract: %v" + + ErrMsgValidation = sdkerrors.Register("wasm-hooks", 2, "error in wasmhook message validation") + ErrMarshaling = sdkerrors.Register("wasm-hooks", 3, "cannot marshal the ICS20 packet") + ErrInvalidPacket = sdkerrors.Register("wasm-hooks", 4, "invalid packet data") + ErrBadResponse = sdkerrors.Register("wasm-hooks", 5, "cannot create response") + ErrWasmError = sdkerrors.Register("wasm-hooks", 6, "wasm error") + ErrBadSender = sdkerrors.Register("wasm-hooks", 7, "bad sender") +) diff --git a/x/ibc-hooks/types/expected_keepers.go b/x/ibc-hooks/types/expected_keepers.go new file mode 100644 index 000000000..308853fc7 --- /dev/null +++ b/x/ibc-hooks/types/expected_keepers.go @@ -0,0 +1,13 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" +) + +type AccountKeeper interface { + NewAccount(sdk.Context, authtypes.AccountI) authtypes.AccountI + + GetAccount(ctx sdk.Context, addr sdk.AccAddress) authtypes.AccountI + SetAccount(ctx sdk.Context, acc authtypes.AccountI) +} diff --git a/x/ibc-hooks/types/keys.go b/x/ibc-hooks/types/keys.go new file mode 100644 index 000000000..9a3e7ded1 --- /dev/null +++ b/x/ibc-hooks/types/keys.go @@ -0,0 +1,8 @@ +package types + +const ( + ModuleName = "ibchooks" + StoreKey = "hooks-for-ibc" // not using the module name because of collisions with key "ibc" + IBCCallbackKey = "ibc_callback" + SenderPrefix = "ibc-wasm-hook-intermediary" +) diff --git a/x/ibc-hooks/utils/utils.go b/x/ibc-hooks/utils/utils.go new file mode 100644 index 000000000..aaaa81d0c --- /dev/null +++ b/x/ibc-hooks/utils/utils.go @@ -0,0 +1,72 @@ +package utils + +import ( + "encoding/json" + "fmt" + + "github.com/cosmos/cosmos-sdk/types/address" + "github.com/neutron-org/neutron/x/ibc-hooks/types" + + sdk "github.com/cosmos/cosmos-sdk/types" + transfertypes "github.com/cosmos/ibc-go/v4/modules/apps/transfer/types" + channeltypes "github.com/cosmos/ibc-go/v4/modules/core/04-channel/types" + ibcexported "github.com/cosmos/ibc-go/v4/modules/core/exported" +) + +// NewEmitErrorAcknowledgement creates a new error acknowledgement after having emitted an event with the +// details of the error. +func NewEmitErrorAcknowledgement(ctx sdk.Context, err error, errorContexts ...string) channeltypes.Acknowledgement { + attributes := make([]sdk.Attribute, len(errorContexts)+1) + attributes[0] = sdk.NewAttribute("error", err.Error()) + for i, s := range errorContexts { + attributes[i+1] = sdk.NewAttribute("error-context", s) + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + "ibc-acknowledgement-error", + attributes..., + ), + }) + + return channeltypes.NewErrorAcknowledgement(err) +} + +// MustExtractDenomFromPacketOnRecv takes a packet with a valid ICS20 token data in the Data field and returns the +// denom as represented in the local chain. +// If the data cannot be unmarshalled this function will panic +func MustExtractDenomFromPacketOnRecv(packet ibcexported.PacketI) string { + var data transfertypes.FungibleTokenPacketData + if err := json.Unmarshal(packet.GetData(), &data); err != nil { + panic("unable to unmarshal ICS20 packet data") + } + + var denom string + if transfertypes.ReceiverChainIsSource(packet.GetSourcePort(), packet.GetSourceChannel(), data.Denom) { + // remove prefix added by sender chain + voucherPrefix := transfertypes.GetDenomPrefix(packet.GetSourcePort(), packet.GetSourceChannel()) + + unprefixedDenom := data.Denom[len(voucherPrefix):] + + // coin denomination used in sending from the escrow address + denom = unprefixedDenom + + // The denomination used to send the coins is either the native denom or the hash of the path + // if the denomination is not native. + denomTrace := transfertypes.ParseDenomTrace(unprefixedDenom) + if denomTrace.Path != "" { + denom = denomTrace.IBCDenom() + } + } else { + prefixedDenom := transfertypes.GetDenomPrefix(packet.GetDestPort(), packet.GetDestChannel()) + data.Denom + denom = transfertypes.ParseDenomTrace(prefixedDenom).IBCDenom() + } + return denom +} + +func DeriveIntermediateSender(channel, originalSender, bech32Prefix string) (string, error) { + senderStr := fmt.Sprintf("%s/%s", channel, originalSender) + senderHash32 := address.Hash(types.SenderPrefix, []byte(senderStr)) + sender := sdk.AccAddress(senderHash32) + return sdk.Bech32ifyAddressBytes(bech32Prefix, sender) +} diff --git a/x/ibc-hooks/wasm_hook.go b/x/ibc-hooks/wasm_hook.go new file mode 100644 index 000000000..a3a115bc9 --- /dev/null +++ b/x/ibc-hooks/wasm_hook.go @@ -0,0 +1,221 @@ +package ibchooks + +import ( + "encoding/json" + "fmt" + + "github.com/neutron-org/neutron/x/ibc-hooks/utils" + + wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" + + wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" + sdk "github.com/cosmos/cosmos-sdk/types" + transfertypes "github.com/cosmos/ibc-go/v4/modules/apps/transfer/types" + channeltypes "github.com/cosmos/ibc-go/v4/modules/core/04-channel/types" + ibcexported "github.com/cosmos/ibc-go/v4/modules/core/exported" + + "github.com/neutron-org/neutron/x/ibc-hooks/types" +) + +type ContractAck struct { + ContractResult []byte `json:"contract_result"` + IbcAck []byte `json:"ibc_ack"` +} + +type WasmHooks struct { + ContractKeeper *wasmkeeper.PermissionedKeeper + bech32PrefixAccAddr string +} + +func NewWasmHooks(contractKeeper *wasmkeeper.PermissionedKeeper, bech32PrefixAccAddr string) WasmHooks { + return WasmHooks{ + ContractKeeper: contractKeeper, + bech32PrefixAccAddr: bech32PrefixAccAddr, + } +} + +func (h WasmHooks) ProperlyConfigured() bool { + return h.ContractKeeper != nil +} + +func (h WasmHooks) OnRecvPacketOverride(im IBCMiddleware, ctx sdk.Context, packet channeltypes.Packet, relayer sdk.AccAddress) ibcexported.Acknowledgement { + if !h.ProperlyConfigured() { + // Not configured + return im.App.OnRecvPacket(ctx, packet, relayer) + } + isIcs20, data := isIcs20Packet(packet) + if !isIcs20 { + return im.App.OnRecvPacket(ctx, packet, relayer) + } + + // Validate the memo + isWasmRouted, contractAddr, msgBytes, err := validateAndParseMemo(data.GetMemo(), data.Receiver) + if !isWasmRouted { + return im.App.OnRecvPacket(ctx, packet, relayer) + } + if err != nil { + return utils.NewEmitErrorAcknowledgement(ctx, types.ErrMsgValidation, err.Error()) + } + if msgBytes == nil || contractAddr == nil { // This should never happen + return utils.NewEmitErrorAcknowledgement(ctx, types.ErrMsgValidation) + } + + // Calculate the receiver / contract caller based on the packet's channel and sender + channel := packet.GetDestChannel() + sender := data.GetSender() + senderBech32, err := utils.DeriveIntermediateSender(channel, sender, h.bech32PrefixAccAddr) + if err != nil { + return utils.NewEmitErrorAcknowledgement(ctx, types.ErrBadSender, fmt.Sprintf("cannot convert sender address %s/%s to bech32: %s", channel, sender, err.Error())) + } + + // The funds sent on this packet need to be transferred to the intermediary account for the sender. + // For this, we override the ICS20 packet's Receiver (essentially hijacking the funds to this new address) + // and execute the underlying OnRecvPacket() call (which should eventually land on the transfer app's + // relay.go and send the funds to the intermediary account. + // + // If that succeeds, we make the contract call + data.Receiver = senderBech32 + bz, err := json.Marshal(data) + if err != nil { + return utils.NewEmitErrorAcknowledgement(ctx, types.ErrMarshaling, err.Error()) + } + packet.Data = bz + + // Execute the receive + ack := im.App.OnRecvPacket(ctx, packet, relayer) + if !ack.Success() { + return ack + } + + amount, ok := sdk.NewIntFromString(data.GetAmount()) + if !ok { + // This should never happen, as it should've been caught in the underlaying call to OnRecvPacket, + // but returning here for completeness + return utils.NewEmitErrorAcknowledgement(ctx, types.ErrInvalidPacket, "Amount is not an int") + } + + // The packet's denom is the denom in the sender chain. This needs to be converted to the local denom. + denom := utils.MustExtractDenomFromPacketOnRecv(packet) + funds := sdk.NewCoins(sdk.NewCoin(denom, amount)) + + // Execute the contract + execMsg := wasmtypes.MsgExecuteContract{ + Sender: senderBech32, + Contract: contractAddr.String(), + Msg: msgBytes, + Funds: funds, + } + response, err := h.execWasmMsg(ctx, &execMsg) + if err != nil { + return utils.NewEmitErrorAcknowledgement(ctx, types.ErrWasmError, err.Error()) + } + + fullAck := ContractAck{ContractResult: response.Data, IbcAck: ack.Acknowledgement()} + bz, err = json.Marshal(fullAck) + if err != nil { + return utils.NewEmitErrorAcknowledgement(ctx, types.ErrBadResponse, err.Error()) + } + + return channeltypes.NewResultAcknowledgement(bz) +} + +func (h WasmHooks) execWasmMsg(ctx sdk.Context, execMsg *wasmtypes.MsgExecuteContract) (*wasmtypes.MsgExecuteContractResponse, error) { + if err := execMsg.ValidateBasic(); err != nil { + return nil, fmt.Errorf(types.ErrBadExecutionMsg, err.Error()) + } + wasmMsgServer := wasmkeeper.NewMsgServerImpl(h.ContractKeeper) + return wasmMsgServer.ExecuteContract(sdk.WrapSDKContext(ctx), execMsg) +} + +func isIcs20Packet(packet channeltypes.Packet) (isIcs20 bool, ics20data transfertypes.FungibleTokenPacketData) { + var data transfertypes.FungibleTokenPacketData + if err := json.Unmarshal(packet.GetData(), &data); err != nil { + return false, data + } + return true, data +} + +// jsonStringHasKey parses the memo as a json object and checks if it contains the key. +func jsonStringHasKey(memo, key string) (found bool, jsonObject map[string]interface{}) { + jsonObject = make(map[string]interface{}) + + // If there is no memo, the packet was either sent with an earlier version of IBC, or the memo was + // intentionally left blank. Nothing to do here. Ignore the packet and pass it down the stack. + if len(memo) == 0 { + return false, jsonObject + } + + // the jsonObject must be a valid JSON object + err := json.Unmarshal([]byte(memo), &jsonObject) + if err != nil { + return false, jsonObject + } + + // If the key doesn't exist, there's nothing to do on this hook. Continue by passing the packet + // down the stack + _, ok := jsonObject[key] + if !ok { + return false, jsonObject + } + + return true, jsonObject +} + +func validateAndParseMemo(memo string, receiver string) (isWasmRouted bool, contractAddr sdk.AccAddress, msgBytes []byte, err error) { + isWasmRouted, metadata := jsonStringHasKey(memo, "wasm") + if !isWasmRouted { + return isWasmRouted, sdk.AccAddress{}, nil, nil + } + + wasmRaw := metadata["wasm"] + + // Make sure the wasm key is a map. If it isn't, ignore this packet + wasm, ok := wasmRaw.(map[string]interface{}) + if !ok { + return isWasmRouted, sdk.AccAddress{}, nil, + fmt.Errorf(types.ErrBadMetadataFormatMsg, memo, "wasm metadata is not a valid JSON map object") + } + + // Get the contract + contract, ok := wasm["contract"].(string) + if !ok { + // The tokens will be returned + return isWasmRouted, sdk.AccAddress{}, nil, + fmt.Errorf(types.ErrBadMetadataFormatMsg, memo, `Could not find key wasm["contract"]`) + } + + contractAddr, err = sdk.AccAddressFromBech32(contract) + if err != nil { + return isWasmRouted, sdk.AccAddress{}, nil, + fmt.Errorf(types.ErrBadMetadataFormatMsg, memo, `wasm["contract"] is not a valid bech32 address`) + } + + // The contract and the receiver should be the same for the packet to be valid + if contract != receiver { + return isWasmRouted, sdk.AccAddress{}, nil, + fmt.Errorf(types.ErrBadMetadataFormatMsg, memo, `wasm["contract"] should be the same as the receiver of the packet`) + } + + // Ensure the message key is provided + if wasm["msg"] == nil { + return isWasmRouted, sdk.AccAddress{}, nil, + fmt.Errorf(types.ErrBadMetadataFormatMsg, memo, `Could not find key wasm["msg"]`) + } + + // Make sure the msg key is a map. If it isn't, return an error + _, ok = wasm["msg"].(map[string]interface{}) + if !ok { + return isWasmRouted, sdk.AccAddress{}, nil, + fmt.Errorf(types.ErrBadMetadataFormatMsg, memo, `wasm["msg"] is not a map object`) + } + + // Get the message string by serializing the map + msgBytes, err = json.Marshal(wasm["msg"]) + if err != nil { + // The tokens will be returned + return isWasmRouted, sdk.AccAddress{}, nil, + fmt.Errorf(types.ErrBadMetadataFormatMsg, memo, err.Error()) + } + + return isWasmRouted, contractAddr, msgBytes, nil +}