diff --git a/app/app.go b/app/app.go index b3f552b9..caf2bb9b 100644 --- a/app/app.go +++ b/app/app.go @@ -9,6 +9,9 @@ import ( "sort" "strings" + ibchooks "github.com/White-Whale-Defi-Platform/migaloo-chain/v2/x/ibchooks" + ibchookskeeper "github.com/White-Whale-Defi-Platform/migaloo-chain/v2/x/ibchooks/keeper" + ibchookstypes "github.com/White-Whale-Defi-Platform/migaloo-chain/v2/x/ibchooks/types" "github.com/cosmos/cosmos-sdk/baseapp" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/grpc/tmservice" @@ -281,6 +284,7 @@ type MigalooApp struct { EvidenceKeeper evidencekeeper.Keeper IBCKeeper *ibckeeper.Keeper // IBC Keeper must be a pointer in the app, so we can SetRouter on it correctly IBCFeeKeeper ibcfeekeeper.Keeper + IBCHooksKeeper *ibchookskeeper.Keeper ICAControllerKeeper icacontrollerkeeper.Keeper ICAHostKeeper icahostkeeper.Keeper InterTxKeeper intertxkeeper.Keeper @@ -289,6 +293,7 @@ type MigalooApp struct { FeeGrantKeeper feegrantkeeper.Keeper AuthzKeeper authzkeeper.Keeper WasmKeeper wasm.Keeper + ContractKeeper *wasmkeeper.PermissionedKeeper RouterKeeper routerkeeper.Keeper ScopedIBCKeeper capabilitykeeper.ScopedKeeper @@ -299,6 +304,10 @@ type MigalooApp struct { ScopedIBCFeeKeeper capabilitykeeper.ScopedKeeper ScopedWasmKeeper capabilitykeeper.ScopedKeeper + // Middleware wrapper + Ics20WasmHooks *ibchooks.WasmHooks + HooksICS4Wrapper ibchooks.ICS4Middleware + // the module manager mm *module.Manager @@ -338,7 +347,7 @@ func NewMigalooApp( evidencetypes.StoreKey, ibctransfertypes.StoreKey, capabilitytypes.StoreKey, feegrant.StoreKey, authzkeeper.StoreKey, wasm.StoreKey, icahosttypes.StoreKey, icacontrollertypes.StoreKey, intertxtypes.StoreKey, ibcfeetypes.StoreKey, tokenfactorytypes.StoreKey, - alliancemoduletypes.StoreKey, + alliancemoduletypes.StoreKey, ibchookstypes.StoreKey, ) tkeys := sdk.NewTransientStoreKeys(paramstypes.TStoreKey) memKeys := sdk.NewMemoryStoreKeys(capabilitytypes.MemStoreKey) @@ -503,6 +512,21 @@ func NewMigalooApp( AddRoute(ibcclienttypes.RouterKey, ibcclient.NewClientProposalHandler(app.IBCKeeper.ClientKeeper)). AddRoute(alliancemoduletypes.RouterKey, alliancemodule.NewAllianceProposalHandler(app.AllianceKeeper)) + // Configure the hooks keeper + hooksKeeper := ibchookskeeper.NewKeeper( + app.keys[ibchookstypes.StoreKey], + ) + app.IBCHooksKeeper = &hooksKeeper + + migalooPrefix := sdk.GetConfig().GetBech32AccountAddrPrefix() + wasmHooks := ibchooks.NewWasmHooks(app.IBCHooksKeeper, nil, migalooPrefix) // The contract keeper needs to be set later + + app.Ics20WasmHooks = &wasmHooks + app.HooksICS4Wrapper = ibchooks.NewICS4Middleware( + app.IBCKeeper.ChannelKeeper, + app.Ics20WasmHooks, + ) + // RouterKeeper must be created before TransferKeeper app.RouterKeeper = *routerkeeper.NewKeeper( appCodec, @@ -512,9 +536,8 @@ func NewMigalooApp( app.IBCKeeper.ChannelKeeper, app.DistrKeeper, app.BankKeeper, - app.IBCKeeper.ChannelKeeper, + app.HooksICS4Wrapper, ) - // IBC Fee Module keeper app.IBCFeeKeeper = ibcfeekeeper.NewKeeper( appCodec, keys[ibcfeetypes.StoreKey], @@ -606,6 +629,14 @@ func NewMigalooApp( var transferStack porttypes.IBCModule transferStack = transfer.NewIBCModule(app.TransferKeeper) transferStack = ibcfee.NewIBCMiddleware(transferStack, app.IBCFeeKeeper) + transferStack = router.NewIBCMiddleware( + transferStack, + &app.RouterKeeper, + 0, + routerkeeper.DefaultForwardTransferPacketTimeoutTimestamp, + routerkeeper.DefaultRefundTransferPacketTimeoutTimestamp, + ) + transferStack = ibchooks.NewIBCMiddleware(transferStack, &app.HooksICS4Wrapper) // Create Interchain Accounts Stack // SendPacket, since it is originating from the application to core IBC: @@ -705,6 +736,7 @@ func NewMigalooApp( tokenfactory.NewAppModule(app.TokenFactoryKeeper, app.AccountKeeper, app.BankKeeper), router.NewAppModule(&app.RouterKeeper), crisis.NewAppModule(&app.CrisisKeeper, skipGenesisInvariants), // always be last to make sure that it checks for all invariants and not only part of them + ibchooks.NewAppModule(app.AccountKeeper), ) // During begin block slashing happens after distr.BeginBlocker so that @@ -738,6 +770,7 @@ func NewMigalooApp( wasm.ModuleName, tokenfactorytypes.ModuleName, alliancemoduletypes.ModuleName, + ibchookstypes.ModuleName, ) app.mm.SetOrderEndBlockers( @@ -767,6 +800,7 @@ func NewMigalooApp( wasm.ModuleName, tokenfactorytypes.ModuleName, alliancemoduletypes.ModuleName, + ibchookstypes.ModuleName, ) // NOTE: The genutils module must occur after staking so that pools are @@ -805,6 +839,7 @@ func NewMigalooApp( wasm.ModuleName, routertypes.ModuleName, alliancemoduletypes.ModuleName, + ibchookstypes.ModuleName, ) // Uncomment if you want to set a custom migration order here. @@ -891,6 +926,10 @@ func NewMigalooApp( app.ScopedICAControllerKeeper = scopedICAControllerKeeper app.ScopedInterTxKeeper = scopedInterTxKeeper + // set the contract keeper for the Ics20WasmHooks + app.ContractKeeper = wasmkeeper.NewDefaultPermissionKeeper(app.WasmKeeper) + app.Ics20WasmHooks.ContractKeeper = app.ContractKeeper + if loadLatest { if err := app.LoadLatestVersion(); err != nil { tmos.Exit(fmt.Sprintf("failed to load latest version: %s", err)) @@ -1044,6 +1083,7 @@ func RegisterSwaggerAPI(rtr *mux.Router) { func (app *MigalooApp) setupUpgradeHandlers(cfg module.Configurator) { for _, upgrade := range Upgrades { upgradeInfo, err := app.UpgradeKeeper.ReadUpgradeInfoFromDisk() + upgrade := upgrade if err != nil { panic(fmt.Sprintf("failed to read upgrade info from disk %s", err)) } diff --git a/go.mod b/go.mod index 6c12ab12..cdfa06a7 100644 --- a/go.mod +++ b/go.mod @@ -3,12 +3,14 @@ module github.com/White-Whale-Defi-Platform/migaloo-chain/v2 go 1.20 require ( + cosmossdk.io/errors v1.0.0-beta.7 cosmossdk.io/math v1.0.1 github.com/CosmWasm/wasmd v0.30.0 github.com/cosmos/cosmos-sdk v0.46.13 github.com/cosmos/ibc-go/v6 v6.2.0 github.com/cosmos/interchain-accounts v0.4.3 github.com/gorilla/mux v1.8.0 + github.com/grpc-ecosystem/grpc-gateway v1.16.0 github.com/prometheus/client_golang v1.14.0 github.com/rakyll/statik v0.1.7 github.com/spf13/cast v1.5.0 @@ -27,7 +29,6 @@ require ( cloud.google.com/go/compute/metadata v0.2.3 // indirect cloud.google.com/go/iam v0.12.0 // indirect cloud.google.com/go/storage v1.28.1 // indirect - cosmossdk.io/errors v1.0.0-beta.7 // indirect filippo.io/edwards25519 v1.0.0-rc.1 // indirect github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect github.com/99designs/keyring v1.2.1 // indirect @@ -90,7 +91,6 @@ require ( github.com/gorilla/handlers v1.5.1 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect - github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect github.com/gtank/merlin v0.1.1 // indirect github.com/gtank/ristretto255 v0.1.2 // indirect diff --git a/osmosis-types/osmoutils/ibc.go b/osmosis-types/osmoutils/ibc.go new file mode 100644 index 00000000..0d4eafd1 --- /dev/null +++ b/osmosis-types/osmoutils/ibc.go @@ -0,0 +1,72 @@ +package osmoutils + +import ( + "encoding/json" + + transfertypes "github.com/cosmos/ibc-go/v6/modules/apps/transfer/types" + channeltypes "github.com/cosmos/ibc-go/v6/modules/core/04-channel/types" + ibcexported "github.com/cosmos/ibc-go/v6/modules/core/exported" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// 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 +} + +// IsAckError checks an IBC acknowledgement to see if it's an error. +// This is a replacement for ack.Success() which is currently not working on some circumstances +func IsAckError(acknowledgement []byte) bool { + var ackErr channeltypes.Acknowledgement_Error + if err := json.Unmarshal(acknowledgement, &ackErr); err == nil && len(ackErr.Error) > 0 { + return true + } + return false +} diff --git a/x/ibchooks/README.md b/x/ibchooks/README.md new file mode 100644 index 00000000..1955c925 --- /dev/null +++ b/x/ibchooks/README.md @@ -0,0 +1,185 @@ +# 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 Osmosis. +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-intermediary" || 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 Osmosis' 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: "osmo1-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 propogate 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": "osmo1contractAddr", + "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 formated incorrectly, then wasmhooks returns an error. + +### Execution flow + +Pre wasm hooks: + +* Ensure the incoming IBC packet is cryptogaphically 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 + +## Ack callbacks + +A contract that sends an IBC transfer, may need to listen for the ACK from that packet. To allow +contracts to listen on the ack of specific packets, we provide Ack callbacks. + +### Design + +The sender of an IBC transfer packet may specify a callback for when the ack of that packet is received in the memo +field of the transfer packet. + +Crucially, _only_ the IBC packet sender can set the callback. + +### Use case + +The crosschain swaps implementation sends an IBC transfer. If the transfer were to fail, we want to allow the sender +to be able to retrieve their funds (which would otherwise be stuck in the contract). To do this, we allow users to +retrieve the funds after the timeout has passed, but without the ack information, we cannot guarantee that the send +hasn't failed (i.e.: returned an error ack notifying that the receiving change didn't accept it) + +### Implementation + +#### Callback information in memo + +For the callback to be processed, the transfer packet's memo should contain the following in its JSON: + +`{"ibc_callback": "osmo1contractAddr"}` + +The wasm hooks will keep the mapping from the packet's channel and sequence to the contract in storage. When an ack is +received, it will notify the specified contract via a sudo message. + +#### Interface for receiving the Acks and Timeouts + +The contract that awaits the callback should implement the following interface for a sudo message: + +```rust +#[cw_serde] +pub enum IBCLifecycleComplete { + #[serde(rename = "ibc_ack")] + IBCAck { + /// The source channel (osmosis side) of the IBC packet + channel: String, + /// The sequence number that the packet was sent with + sequence: u64, + /// String encoded version of the ack as seen by OnAcknowledgementPacket(..) + ack: String, + /// Weather an ack is a success of failure according to the transfer spec + success: bool, + }, + #[serde(rename = "ibc_timeout")] + IBCTimeout { + /// The source channel (osmosis side) of the IBC packet + channel: String, + /// The sequence number that the packet was sent with + sequence: u64, + }, +} + +/// Message type for `sudo` entry_point +#[cw_serde] +pub enum SudoMsg { + #[serde(rename = "ibc_lifecycle_complete")] + IBCLifecycleComplete(IBCLifecycleComplete), +} +``` + +# Testing strategy + +See go tests. \ No newline at end of file diff --git a/x/ibchooks/client/cli/query.go b/x/ibchooks/client/cli/query.go new file mode 100644 index 00000000..f98806ac --- /dev/null +++ b/x/ibchooks/client/cli/query.go @@ -0,0 +1,77 @@ +package cli + +import ( + "fmt" + "strings" + + "github.com/spf13/cobra" + + "github.com/cosmos/cosmos-sdk/client/flags" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/version" + + "github.com/White-Whale-Defi-Platform/migaloo-chain/v2/x/ibchooks/keeper" + "github.com/White-Whale-Defi-Platform/migaloo-chain/v2/x/ibchooks/types" +) + +func indexRunCmd(cmd *cobra.Command, _ []string) error { + usageTemplate := `Usage:{{if .HasAvailableSubCommands}} + {{.CommandPath}} [command]{{end}} + +{{if .HasAvailableSubCommands}}Available Commands:{{range .Commands}}{{if .IsAvailableCommand}} + {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}} + +Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}} +` + cmd.SetUsageTemplate(usageTemplate) + return cmd.Help() +} + +// GetQueryCmd returns the cli query commands for this module. +func GetQueryCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: types.ModuleName, + Short: fmt.Sprintf("Querying commands for the %s module", types.ModuleName), + DisableFlagParsing: true, + SuggestionsMinimumDistance: 2, + RunE: indexRunCmd, + } + + 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 := keeper.DeriveIntermediateSender(channelID, originalSender, prefix) + if err != nil { + return err + } + fmt.Println(senderBech32) + return nil + }, + } + + flags.AddQueryFlagsToCmd(cmd) + + return cmd +} diff --git a/x/ibchooks/hooks.go b/x/ibchooks/hooks.go new file mode 100644 index 00000000..58bde6dd --- /dev/null +++ b/x/ibchooks/hooks.go @@ -0,0 +1,146 @@ +package ibchooks + +import ( + clienttypes "github.com/cosmos/ibc-go/v6/modules/core/02-client/types" + // ibc-go + channeltypes "github.com/cosmos/ibc-go/v6/modules/core/04-channel/types" + ibcexported "github.com/cosmos/ibc-go/v6/modules/core/exported" + + // external libraries + sdk "github.com/cosmos/cosmos-sdk/types" + capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" +) + +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, sourcePort string, sourceChannel string, timeoutHeight clienttypes.Height, timeoutTimestamp uint64, data []byte) (uint64, error) +} +type SendPacketBeforeHooks interface { + SendPacketBeforeHook(ctx sdk.Context, chanCap *capabilitytypes.Capability, sourcePort string, sourceChannel string, timeoutHeight clienttypes.Height, timeoutTimestamp uint64, data []byte) +} +type SendPacketAfterHooks interface { + SendPacketAfterHook(ctx sdk.Context, chanCap *capabilitytypes.Capability, sourcePort string, sourceChannel string, timeoutHeight clienttypes.Height, timeoutTimestamp uint64, data []byte, 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/ibchooks/ibc_module.go b/x/ibchooks/ibc_module.go new file mode 100644 index 00000000..2a45219c --- /dev/null +++ b/x/ibchooks/ibc_module.go @@ -0,0 +1,262 @@ +package ibchooks + +import ( + // ibc-go + clienttypes "github.com/cosmos/ibc-go/v6/modules/core/02-client/types" + channeltypes "github.com/cosmos/ibc-go/v6/modules/core/04-channel/types" + porttypes "github.com/cosmos/ibc-go/v6/modules/core/05-port/types" + ibcexported "github.com/cosmos/ibc-go/v6/modules/core/exported" + + // external libraries + sdk "github.com/cosmos/cosmos-sdk/types" + capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" +) + +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, + sourcePort string, + sourceChannel string, + timeoutHeight clienttypes.Height, + timeoutTimestamp uint64, + data []byte, +) (uint64, error) { + return im.ICS4Middleware.SendPacket(ctx, chanCap, sourcePort, sourceChannel, timeoutHeight, timeoutTimestamp, data) +} + +// 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/ibchooks/ics4_middleware.go b/x/ibchooks/ics4_middleware.go new file mode 100644 index 00000000..2baa5c7d --- /dev/null +++ b/x/ibchooks/ics4_middleware.go @@ -0,0 +1,85 @@ +package ibchooks + +import ( + // ibc-go + clienttypes "github.com/cosmos/ibc-go/v6/modules/core/02-client/types" + porttypes "github.com/cosmos/ibc-go/v6/modules/core/05-port/types" + ibcexported "github.com/cosmos/ibc-go/v6/modules/core/exported" + + // external libraries + sdk "github.com/cosmos/cosmos-sdk/types" + capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" +) + +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, + sourcePort string, + sourceChannel string, + timeoutHeight clienttypes.Height, + timeoutTimestamp uint64, + data []byte, +) (sequence uint64, err error) { + if hook, ok := i.Hooks.(SendPacketOverrideHooks); ok { + return hook.SendPacketOverride(i, ctx, channelCap, sourcePort, sourceChannel, timeoutHeight, timeoutTimestamp, data) + } + + if hook, ok := i.Hooks.(SendPacketBeforeHooks); ok { + hook.SendPacketBeforeHook(ctx, channelCap, sourcePort, sourceChannel, timeoutHeight, timeoutTimestamp, data) + } + + sequence, err = i.channel.SendPacket(ctx, channelCap, sourcePort, sourceChannel, timeoutHeight, timeoutTimestamp, data) + + if hook, ok := i.Hooks.(SendPacketAfterHooks); ok { + hook.SendPacketAfterHook(ctx, channelCap, sourcePort, sourceChannel, timeoutHeight, timeoutTimestamp, data, err) + } + + return sequence, 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/ibchooks/keeper/keeper.go b/x/ibchooks/keeper/keeper.go new file mode 100644 index 00000000..0f1e5303 --- /dev/null +++ b/x/ibchooks/keeper/keeper.go @@ -0,0 +1,62 @@ +package keeper + +import ( + "fmt" + + storetypes "github.com/cosmos/cosmos-sdk/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/address" + + "github.com/tendermint/tendermint/libs/log" + + "github.com/White-Whale-Defi-Platform/migaloo-chain/v2/x/ibchooks/types" +) + +type ( + Keeper struct { + storeKey storetypes.StoreKey + } +) + +// NewKeeper returns a new instance of the x/ibchooks keeper +func NewKeeper( + storeKey storetypes.StoreKey, +) Keeper { + return Keeper{ + storeKey: storeKey, + } +} + +// Logger returns a logger for the x/tokenfactory module +func (k Keeper) Logger(ctx sdk.Context) log.Logger { + return ctx.Logger().With("module", fmt.Sprintf("x/%s", types.ModuleName)) +} + +func GetPacketKey(channel string, packetSequence uint64) []byte { + return []byte(fmt.Sprintf("%s::%d", channel, packetSequence)) +} + +// StorePacketCallback stores which contract will be listening for the ack or timeout of a packet +func (k Keeper) StorePacketCallback(ctx sdk.Context, channel string, packetSequence uint64, contract string) { + store := ctx.KVStore(k.storeKey) + store.Set(GetPacketKey(channel, packetSequence), []byte(contract)) +} + +// GetPacketCallback returns the bech32 addr of the contract that is expecting a callback from a packet +func (k Keeper) GetPacketCallback(ctx sdk.Context, channel string, packetSequence uint64) string { + store := ctx.KVStore(k.storeKey) + return string(store.Get(GetPacketKey(channel, packetSequence))) +} + +// DeletePacketCallback deletes the callback from storage once it has been processed +func (k Keeper) DeletePacketCallback(ctx sdk.Context, channel string, packetSequence uint64) { + store := ctx.KVStore(k.storeKey) + store.Delete(GetPacketKey(channel, packetSequence)) +} + +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/ibchooks/module.go b/x/ibchooks/module.go new file mode 100644 index 00000000..39846460 --- /dev/null +++ b/x/ibchooks/module.go @@ -0,0 +1,132 @@ +package ibchooks + +import ( + "encoding/json" + + "github.com/gorilla/mux" + "github.com/grpc-ecosystem/grpc-gateway/runtime" + "github.com/spf13/cobra" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/codec" + cdctypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" + + abci "github.com/tendermint/tendermint/abci/types" + + "github.com/White-Whale-Defi-Platform/migaloo-chain/v2/x/ibchooks/client/cli" + "github.com/White-Whale-Defi-Platform/migaloo-chain/v2/x/ibchooks/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 authkeeper.AccountKeeper +} + +// NewAppModule creates a new AppModule object. +func NewAppModule(ak authkeeper.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) {} + +// QuerierRoute returns the module's querier route name. +func (AppModule) QuerierRoute() string { + return "" +} + +// Route empty module message route +func (am AppModule) Route() sdk.Route { return sdk.Route{} } + +// LegacyQuerierHandler returns an empty module querier +func (am AppModule) LegacyQuerierHandler(*codec.LegacyAmino) sdk.Querier { return nil } + +// 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 json.RawMessage([]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/ibchooks/types/errors.go b/x/ibchooks/types/errors.go new file mode 100644 index 00000000..f2ba928b --- /dev/null +++ b/x/ibchooks/types/errors.go @@ -0,0 +1,15 @@ +package types + +import sdkerrors "cosmossdk.io/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/ibchooks/types/keys.go b/x/ibchooks/types/keys.go new file mode 100644 index 00000000..9a3e7ded --- /dev/null +++ b/x/ibchooks/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/ibchooks/wasm_hook.go b/x/ibchooks/wasm_hook.go new file mode 100644 index 00000000..446c3787 --- /dev/null +++ b/x/ibchooks/wasm_hook.go @@ -0,0 +1,419 @@ +package ibchooks + +import ( + "encoding/json" + "fmt" + + wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" + wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" + transfertypes "github.com/cosmos/ibc-go/v6/modules/apps/transfer/types" + clienttypes "github.com/cosmos/ibc-go/v6/modules/core/02-client/types" + channeltypes "github.com/cosmos/ibc-go/v6/modules/core/04-channel/types" + ibcexported "github.com/cosmos/ibc-go/v6/modules/core/exported" + + errorsmod "cosmossdk.io/errors" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" + + "github.com/White-Whale-Defi-Platform/migaloo-chain/v2/osmosis-types/osmoutils" + "github.com/White-Whale-Defi-Platform/migaloo-chain/v2/x/ibchooks/keeper" + "github.com/White-Whale-Defi-Platform/migaloo-chain/v2/x/ibchooks/types" +) + +type ContractAck struct { + ContractResult []byte `json:"contract_result"` + IbcAck []byte `json:"ibc_ack"` +} + +type WasmHooks struct { + ContractKeeper *wasmkeeper.PermissionedKeeper + ibcHooksKeeper *keeper.Keeper + bech32PrefixAccAddr string +} + +func NewWasmHooks(ibcHooksKeeper *keeper.Keeper, contractKeeper *wasmkeeper.PermissionedKeeper, bech32PrefixAccAddr string) WasmHooks { + return WasmHooks{ + ContractKeeper: contractKeeper, + ibcHooksKeeper: ibcHooksKeeper, + bech32PrefixAccAddr: bech32PrefixAccAddr, + } +} + +func (h WasmHooks) ProperlyConfigured() bool { + return h.ContractKeeper != nil && h.ibcHooksKeeper != 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 osmoutils.NewEmitErrorAcknowledgement(ctx, types.ErrMsgValidation, err.Error()) + } + if msgBytes == nil || contractAddr == nil { // This should never happen + return osmoutils.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 := keeper.DeriveIntermediateSender(channel, sender, h.bech32PrefixAccAddr) + if err != nil { + return osmoutils.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 sunds to the intermediary account. + // + // If that succeeds, we make the contract call + data.Receiver = senderBech32 + bz, err := json.Marshal(data) + if err != nil { + return osmoutils.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 osmoutils.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 := osmoutils.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 osmoutils.NewEmitErrorAcknowledgement(ctx, types.ErrWasmError, err.Error()) + } + + fullAck := ContractAck{ContractResult: response.Data, IbcAck: ack.Acknowledgement()} + bz, err = json.Marshal(fullAck) + if err != nil { + return osmoutils.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 +} + +func (h WasmHooks) SendPacketOverride(i ICS4Middleware, ctx sdk.Context, chanCap *capabilitytypes.Capability, packet ibcexported.PacketI) error { + height := clienttypes.Height{ + RevisionNumber: packet.GetTimeoutHeight().GetRevisionHeight(), + RevisionHeight: packet.GetTimeoutHeight().GetRevisionHeight(), + } + + concretePacket, ok := packet.(channeltypes.Packet) + if !ok { + if _, err := i.channel.SendPacket(ctx, chanCap, + packet.GetSourcePort(), + packet.GetSourceChannel(), + height, + packet.GetTimeoutTimestamp(), + packet.GetData()); err != nil { + return err + } + } + + isIcs20, data := isIcs20Packet(concretePacket) + if !isIcs20 { + if _, err := i.channel.SendPacket(ctx, chanCap, + packet.GetSourcePort(), + packet.GetSourceChannel(), + height, + packet.GetTimeoutTimestamp(), + packet.GetData()); err != nil { + return err + } + } + + isCallbackRouted, metadata := jsonStringHasKey(data.GetMemo(), types.IBCCallbackKey) + if !isCallbackRouted { + if _, err := i.channel.SendPacket(ctx, chanCap, + packet.GetSourcePort(), + packet.GetSourceChannel(), + height, + packet.GetTimeoutTimestamp(), + packet.GetData()); err != nil { + return err + } + } + + // We remove the callback metadata from the memo as it has already been processed. + + // If the only available key in the memo is the callback, we should remove the memo + // from the data completely so the packet is sent without it. + // This way receiver chains that are on old versions of IBC will be able to process the packet + + callbackRaw := metadata[types.IBCCallbackKey] // This will be used later. + delete(metadata, types.IBCCallbackKey) + bzMetadata, err := json.Marshal(metadata) + if err != nil { + return errorsmod.Wrap(err, "Send packet with callback error") + } + stringMetadata := string(bzMetadata) + if stringMetadata == "{}" { + data.Memo = "" + } else { + data.Memo = stringMetadata + } + dataBytes, err := json.Marshal(data) + if err != nil { + return errorsmod.Wrap(err, "Send packet with callback error") + } + + packetWithoutCallbackMemo := channeltypes.Packet{ + Sequence: concretePacket.Sequence, + SourcePort: concretePacket.SourcePort, + SourceChannel: concretePacket.SourceChannel, + DestinationPort: concretePacket.DestinationPort, + DestinationChannel: concretePacket.DestinationChannel, + Data: dataBytes, + TimeoutTimestamp: concretePacket.TimeoutTimestamp, + TimeoutHeight: concretePacket.TimeoutHeight, + } + + _, err = i.channel.SendPacket(ctx, chanCap, + packetWithoutCallbackMemo.GetSourcePort(), + packetWithoutCallbackMemo.GetSourceChannel(), + height, + packetWithoutCallbackMemo.GetTimeoutTimestamp(), + packetWithoutCallbackMemo.GetData(), + ) + if err != nil { + return err + } + + // Make sure the callback contract is a string and a valid bech32 addr. If it isn't, ignore this packet + contract, ok := callbackRaw.(string) + if !ok { + return nil + } + _, err = sdk.AccAddressFromBech32(contract) + if err != nil { + return nil + } + + h.ibcHooksKeeper.StorePacketCallback(ctx, packet.GetSourceChannel(), packet.GetSequence(), contract) + return nil +} + +func (h WasmHooks) OnAcknowledgementPacketOverride(im IBCMiddleware, ctx sdk.Context, packet channeltypes.Packet, acknowledgement []byte, relayer sdk.AccAddress) error { + err := im.App.OnAcknowledgementPacket(ctx, packet, acknowledgement, relayer) + if err != nil { + return err + } + + if !h.ProperlyConfigured() { + // Not configured. Return from the underlying implementation + return nil + } + + contract := h.ibcHooksKeeper.GetPacketCallback(ctx, packet.GetSourceChannel(), packet.GetSequence()) + if contract == "" { + // No callback configured + return nil + } + + contractAddr, err := sdk.AccAddressFromBech32(contract) + if err != nil { + return sdkerrors.Wrap(err, "Ack callback error") // The callback configured is not a bech32. Error out + } + + success := "false" + if !osmoutils.IsAckError(acknowledgement) { + success = "true" + } + + // Notify the sender that the ack has been received + ackAsJSON, err := json.Marshal(acknowledgement) + if err != nil { + // If the ack is not a json object, error + return err + } + + sudoMsg := []byte(fmt.Sprintf( + `{"ibc_lifecycle_complete": {"ibc_ack": {"channel": "%s", "sequence": %d, "ack": %s, "success": %s}}}`, + packet.SourceChannel, packet.Sequence, ackAsJSON, success)) + _, err = h.ContractKeeper.Sudo(ctx, contractAddr, sudoMsg) + if err != nil { + // error processing the callback + // ToDo: Open Question: Should we also delete the callback here? + return sdkerrors.Wrap(err, "Ack callback error") + } + h.ibcHooksKeeper.DeletePacketCallback(ctx, packet.GetSourceChannel(), packet.GetSequence()) + return nil +} + +func (h WasmHooks) OnTimeoutPacketOverride(im IBCMiddleware, ctx sdk.Context, packet channeltypes.Packet, relayer sdk.AccAddress) error { + err := im.App.OnTimeoutPacket(ctx, packet, relayer) + if err != nil { + return err + } + + if !h.ProperlyConfigured() { + // Not configured. Return from the underlying implementation + return nil + } + + contract := h.ibcHooksKeeper.GetPacketCallback(ctx, packet.GetSourceChannel(), packet.GetSequence()) + if contract == "" { + // No callback configured + return nil + } + + contractAddr, err := sdk.AccAddressFromBech32(contract) + if err != nil { + return sdkerrors.Wrap(err, "Timeout callback error") // The callback configured is not a bech32. Error out + } + + sudoMsg := []byte(fmt.Sprintf( + `{"ibc_lifecycle_complete": {"ibc_timeout": {"channel": "%s", "sequence": %d}}}`, + packet.SourceChannel, packet.Sequence)) + _, err = h.ContractKeeper.Sudo(ctx, contractAddr, sudoMsg) + if err != nil { + // error processing the callback. This could be because the contract doesn't implement the message type to + // process the callback. Retrying this will not help, so we can delete the callback from storage. + // Since the packet has timed out, we don't expect any other responses that may trigger the callback. + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + "ibc-timeout-callback-error", + sdk.NewAttribute("contract", contractAddr.String()), + sdk.NewAttribute("message", string(sudoMsg)), + sdk.NewAttribute("error", err.Error()), + ), + }) + } + h.ibcHooksKeeper.DeletePacketCallback(ctx, packet.GetSourceChannel(), packet.GetSequence()) + return nil +}