diff --git a/app/app.go b/app/app.go index fb7de81b5..3c84a62d6 100644 --- a/app/app.go +++ b/app/app.go @@ -101,15 +101,18 @@ import ( ibctestingtypes "github.com/cosmos/ibc-go/v7/testing/types" + ibc_hooks "github.com/notional-labs/centauri/v3/x/ibc-hooks" + ibchookstypes "github.com/notional-labs/centauri/v3/x/ibc-hooks/types" + "github.com/CosmWasm/wasmd/x/wasm" wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" ) const ( - Name = "centauri" - dirName = "banksy" - ForkHeight = 244008 + Name = "centauri" + dirName = "banksy" + ForkHeight = 244008 ) var ( @@ -191,6 +194,7 @@ var ( wasm08.AppModuleBasic{}, wasm.AppModuleBasic{}, router.AppModuleBasic{}, + ibc_hooks.AppModuleBasic{}, transfermiddleware.AppModuleBasic{}, consensus.AppModuleBasic{}, alliancemodule.AppModuleBasic{}, @@ -305,6 +309,7 @@ func NewCentauriApp( routerModule := router.NewAppModule(app.RouterKeeper) transfermiddlewareModule := transfermiddleware.NewAppModule(&app.TransferMiddlewareKeeper) icqModule := icq.NewAppModule(app.ICQKeeper) + ibcHooksModule := ibc_hooks.NewAppModule() /**** Module Options ****/ // NOTE: we may consider parsing `appOpts` inside module constructors. For the moment @@ -337,6 +342,7 @@ func NewCentauriApp( params.NewAppModule(app.ParamsKeeper), transferModule, icqModule, + ibcHooksModule, consensus.NewAppModule(appCodec, app.ConsensusParamsKeeper), wasm08.NewAppModule(app.Wasm08Keeper), wasm.NewAppModule(appCodec, &app.WasmKeeper, app.StakingKeeper, app.AccountKeeper, app.BankKeeper, app.MsgServiceRouter(), app.GetSubspace(wasmtypes.ModuleName)), @@ -363,6 +369,7 @@ func NewCentauriApp( ibctransfertypes.ModuleName, routertypes.ModuleName, transfermiddlewaretypes.ModuleName, + ibchookstypes.ModuleName, icqtypes.ModuleName, authtypes.ModuleName, banktypes.ModuleName, @@ -399,6 +406,7 @@ func NewCentauriApp( ibchost.ModuleName, routertypes.ModuleName, transfermiddlewaretypes.ModuleName, + ibchookstypes.ModuleName, ibctransfertypes.ModuleName, icqtypes.ModuleName, consensusparamtypes.ModuleName, @@ -432,6 +440,7 @@ func NewCentauriApp( icqtypes.ModuleName, routertypes.ModuleName, transfermiddlewaretypes.ModuleName, + ibchookstypes.ModuleName, feegrant.ModuleName, group.ModuleName, consensusparamtypes.ModuleName, diff --git a/app/ibctesting/chain.go b/app/ibctesting/chain.go index 586d67821..df4981dcf 100644 --- a/app/ibctesting/chain.go +++ b/app/ibctesting/chain.go @@ -2,7 +2,9 @@ package ibctesting import ( "bytes" + "crypto/sha256" "fmt" + "os" "testing" "time" @@ -19,12 +21,20 @@ import ( "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" sdk "github.com/cosmos/cosmos-sdk/types" + authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + govkeeper "github.com/cosmos/cosmos-sdk/x/gov/keeper" + v1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" + "github.com/CosmWasm/wasmd/x/wasm" + wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" + wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" capabilitykeeper "github.com/cosmos/cosmos-sdk/x/capability/keeper" capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" teststaking "github.com/cosmos/cosmos-sdk/x/staking/testutil" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" clienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" @@ -43,6 +53,7 @@ import ( "github.com/notional-labs/centauri/v3/app/ibctesting/simapp" routerKeeper "github.com/notional-labs/centauri/v3/x/transfermiddleware/keeper" "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" ) // TestChain is a testing struct that wraps a simapp with the last TM Header, the current ABCI @@ -621,6 +632,73 @@ func (chain TestChain) GetTestSupport() *centauri.TestSupport { return chain.App.(*TestingAppDecorator).TestSupport() } +func (chain *TestChain) QueryContract(suite *suite.Suite, contract sdk.AccAddress, key []byte) string { + wasmKeeper := chain.GetTestSupport().WasmdKeeper() + state, err := wasmKeeper.QuerySmart(chain.GetContext(), contract, key) + suite.Require().NoError(err) + return string(state) +} + +func (chain *TestChain) StoreContractCode(suite *suite.Suite, path string) { + govModuleAddress := chain.GetTestSupport().AccountKeeper().GetModuleAddress(govtypes.ModuleName) + wasmCode, err := os.ReadFile(path) + suite.Require().NoError(err) + + src := wasmtypes.StoreCodeProposalFixture(func(p *wasmtypes.StoreCodeProposal) { + p.RunAs = govModuleAddress.String() + p.WASMByteCode = wasmCode + checksum := sha256.Sum256(wasmCode) + p.CodeHash = checksum[:] + }) + + govKeeper := chain.GetTestSupport().GovKeeper() + // when + mustSubmitAndExecuteLegacyProposal(suite.T(), chain.GetContext(), src, chain.SenderAccount.GetAddress().String(), &govKeeper, govModuleAddress.String()) + suite.Require().NoError(err) +} + +func (chain *TestChain) InstantiateContract(suite *suite.Suite, msg string, codeID uint64) sdk.AccAddress { + wasmKeeper := chain.GetTestSupport().WasmdKeeper() + govModuleAddress := chain.GetTestSupport().AccountKeeper().GetModuleAddress(govtypes.ModuleName) + + contractKeeper := wasmkeeper.NewDefaultPermissionKeeper(wasmKeeper) + addr, _, err := contractKeeper.Instantiate(chain.GetContext(), codeID, govModuleAddress, govModuleAddress, []byte(msg), "contract", nil) + suite.Require().NoError(err) + return addr +} + +func mustSubmitAndExecuteLegacyProposal(t *testing.T, ctx sdk.Context, content v1beta1.Content, myActorAddress string, govKeeper *govkeeper.Keeper, authority string) { + t.Helper() + msgServer := govkeeper.NewMsgServerImpl(govKeeper) + // ignore all submit events + contentMsg, err := submitLegacyProposal(t, ctx.WithEventManager(sdk.NewEventManager()), content, myActorAddress, authority, msgServer) + require.NoError(t, err) + + _, err = msgServer.ExecLegacyContent(sdk.WrapSDKContext(ctx), v1.NewMsgExecLegacyContent(contentMsg.Content, authority)) + require.NoError(t, err) +} + +// does not fail on submit proposal +func submitLegacyProposal(t *testing.T, ctx sdk.Context, content v1beta1.Content, myActorAddress string, govAuthority string, msgServer v1.MsgServer) (*v1.MsgExecLegacyContent, error) { + t.Helper() + contentMsg, err := v1.NewLegacyContent(content, govAuthority) + require.NoError(t, err) + + proposal, err := v1.NewMsgSubmitProposal( + []sdk.Msg{contentMsg}, + sdk.Coins{}, + myActorAddress, + "", + "my title", + "my description", + ) + require.NoError(t, err) + + // when stored + _, err = msgServer.SubmitProposal(sdk.WrapSDKContext(ctx), proposal) + return contentMsg, err +} + var _ ibctesting.TestingApp = TestingAppDecorator{} type TestingAppDecorator struct { @@ -641,6 +719,14 @@ func (a TestingAppDecorator) GetStakingKeeper() ibctestingtypes.StakingKeeper { return a.TestSupport().StakingKeeper() } +func (a TestingAppDecorator) GetAccountKeeper() authkeeper.AccountKeeper { + return a.TestSupport().AccountKeeper() +} + +func (a TestingAppDecorator) GetGovKeeper() govkeeper.Keeper { + return a.TestSupport().GovKeeper() +} + func (a TestingAppDecorator) GetBankKeeper() bankkeeper.Keeper { return a.TestSupport().BankKeeper() } @@ -661,6 +747,10 @@ func (a TestingAppDecorator) TestSupport() *centauri.TestSupport { return centauri.NewTestSupport(a.t, a.CentauriApp) } +func (a TestingAppDecorator) GetWasmdKeeper() wasm.Keeper { + return a.TestSupport().WasmdKeeper() +} + func (a TestingAppDecorator) GetWasmKeeper() wasm08.Keeper { return a.TestSupport().Wasm08Keeper() } diff --git a/app/ibctesting/event_utils.go b/app/ibctesting/event_utils.go index a801402c8..e910ab69e 100644 --- a/app/ibctesting/event_utils.go +++ b/app/ibctesting/event_utils.go @@ -1,10 +1,12 @@ package ibctesting import ( + "fmt" "strconv" "strings" abci "github.com/cometbft/cometbft/abci/types" + sdk "github.com/cosmos/cosmos-sdk/types" clienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" channeltypes "github.com/cosmos/ibc-go/v7/modules/core/04-channel/types" ) @@ -56,6 +58,64 @@ func parsePacketFromEvent(evt abci.Event) channeltypes.Packet { } } +// ParsePacketFromEvents parses events emitted from a MsgRecvPacket and returns the +// acknowledgement. +func ParsePacketFromEvents(events sdk.Events) (channeltypes.Packet, error) { + for _, ev := range events { + if ev.Type == channeltypes.EventTypeSendPacket { + packet := channeltypes.Packet{} + for _, attr := range ev.Attributes { + switch string(attr.Key) { + case channeltypes.AttributeKeyData: + packet.Data = []byte(attr.Value) + + case channeltypes.AttributeKeySequence: + seq, err := strconv.ParseUint(string(attr.Value), 10, 64) + if err != nil { + return channeltypes.Packet{}, err + } + + packet.Sequence = seq + + case channeltypes.AttributeKeySrcPort: + packet.SourcePort = string(attr.Value) + + case channeltypes.AttributeKeySrcChannel: + packet.SourceChannel = string(attr.Value) + + case channeltypes.AttributeKeyDstPort: + packet.DestinationPort = string(attr.Value) + + case channeltypes.AttributeKeyDstChannel: + packet.DestinationChannel = string(attr.Value) + + case channeltypes.AttributeKeyTimeoutHeight: + height, err := clienttypes.ParseHeight(string(attr.Value)) + if err != nil { + return channeltypes.Packet{}, err + } + + packet.TimeoutHeight = height + + case channeltypes.AttributeKeyTimeoutTimestamp: + timestamp, err := strconv.ParseUint(string(attr.Value), 10, 64) + if err != nil { + return channeltypes.Packet{}, err + } + + packet.TimeoutTimestamp = timestamp + + default: + continue + } + } + + return packet, nil + } + } + return channeltypes.Packet{}, fmt.Errorf("acknowledgement event attribute not found") +} + // return the value for the attribute with the given name func getField(evt abci.Event, key string) string { for _, attr := range evt.Attributes { diff --git a/app/keepers/keepers.go b/app/keepers/keepers.go index d9c013b27..7262dfefc 100644 --- a/app/keepers/keepers.go +++ b/app/keepers/keepers.go @@ -7,6 +7,7 @@ import ( "github.com/cosmos/cosmos-sdk/baseapp" "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" porttypes "github.com/cosmos/ibc-go/v7/modules/core/05-port/types" authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" @@ -87,6 +88,10 @@ import ( storetypes "github.com/cosmos/cosmos-sdk/store/types" wasm08Keeper "github.com/cosmos/ibc-go/v7/modules/light-clients/08-wasm/keeper" wasmtypes "github.com/cosmos/ibc-go/v7/modules/light-clients/08-wasm/types" + + ibc_hooks "github.com/notional-labs/centauri/v3/x/ibc-hooks" + ibchookskeeper "github.com/notional-labs/centauri/v3/x/ibc-hooks/keeper" + ibchookstypes "github.com/notional-labs/centauri/v3/x/ibc-hooks/types" ) const ( @@ -120,6 +125,9 @@ type AppKeepers struct { GroupKeeper groupkeeper.Keeper Wasm08Keeper wasm08Keeper.Keeper // TODO: use this name ? WasmKeeper wasm.Keeper + IBCHooksKeeper *ibchookskeeper.Keeper + Ics20WasmHooks *ibc_hooks.WasmHooks + HooksICS4Wrapper ibc_hooks.ICS4Middleware // make scoped keepers public for test purposes ScopedIBCKeeper capabilitykeeper.ScopedKeeper ScopedTransferKeeper capabilitykeeper.ScopedKeeper @@ -214,12 +222,26 @@ func (appKeepers *AppKeepers) InitNormalKeepers( ) appKeepers.Wasm08Keeper = wasm08Keeper.NewKeeper(appCodec, appKeepers.keys[wasmtypes.StoreKey], authorityAddress, homePath) + // Create Transfer Keepers + hooksKeeper := ibchookskeeper.NewKeeper( + appKeepers.keys[ibchookstypes.StoreKey], + ) + appKeepers.IBCHooksKeeper = &hooksKeeper + + centauriPrefix := sdk.GetConfig().GetBech32AccountAddrPrefix() + wasmHooks := ibc_hooks.NewWasmHooks(&hooksKeeper, nil, centauriPrefix) // The contract keeper needs to be set later + appKeepers.Ics20WasmHooks = &wasmHooks + appKeepers.HooksICS4Wrapper = ibc_hooks.NewICS4Middleware( + appKeepers.IBCKeeper.ChannelKeeper, + appKeepers.Ics20WasmHooks, + ) + appKeepers.TransferMiddlewareKeeper = transfermiddlewarekeeper.NewKeeper( appKeepers.keys[transfermiddlewaretypes.StoreKey], appKeepers.GetSubspace(transfermiddlewaretypes.ModuleName), appCodec, - appKeepers.IBCKeeper.ChannelKeeper, + &appKeepers.HooksICS4Wrapper, &appKeepers.TransferKeeper, appKeepers.BankKeeper, authorityAddress, @@ -272,6 +294,8 @@ func (appKeepers *AppKeepers) InitNormalKeepers( routerkeeper.DefaultRefundTransferPacketTimeoutTimestamp, ) + hooksTransferMiddleware := ibc_hooks.NewIBCMiddleware(ibcMiddlewareStack, &appKeepers.HooksICS4Wrapper) + // Create evidence Keeper for to register the IBC light client misbehaviour evidence route evidenceKeeper := evidencekeeper.NewKeeper( appCodec, appKeepers.keys[evidencetypes.StoreKey], appKeepers.StakingKeeper, appKeepers.SlashingKeeper, @@ -309,6 +333,8 @@ func (appKeepers *AppKeepers) InitNormalKeepers( wasmOpts..., ) + appKeepers.Ics20WasmHooks.ContractKeeper = &appKeepers.WasmKeeper + // Register Gov (must be registered after stakeibc) govRouter := govtypesv1beta1.NewRouter() govRouter.AddRoute(govtypes.RouterKey, govtypesv1beta1.ProposalHandler). @@ -337,7 +363,7 @@ func (appKeepers *AppKeepers) InitNormalKeepers( ) ibcRouter := porttypes.NewRouter() - ibcRouter.AddRoute(ibctransfertypes.ModuleName, ibcMiddlewareStack) + ibcRouter.AddRoute(ibctransfertypes.ModuleName, hooksTransferMiddleware) ibcRouter.AddRoute(icqtypes.ModuleName, icqIBCModule) ibcRouter.AddRoute(wasm.ModuleName, wasm.NewIBCHandler(appKeepers.WasmKeeper, appKeepers.IBCKeeper.ChannelKeeper, appKeepers.IBCKeeper.ChannelKeeper)) diff --git a/app/keepers/keys.go b/app/keepers/keys.go index 61b08c1ed..df7cf600b 100644 --- a/app/keepers/keys.go +++ b/app/keepers/keys.go @@ -12,8 +12,8 @@ import ( evidencetypes "github.com/cosmos/cosmos-sdk/x/evidence/types" govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" - "github.com/cosmos/cosmos-sdk/x/group" "github.com/cosmos/cosmos-sdk/x/feegrant" + "github.com/cosmos/cosmos-sdk/x/group" paramstypes "github.com/cosmos/cosmos-sdk/x/params/types" slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types" @@ -26,12 +26,13 @@ import ( routertypes "github.com/strangelove-ventures/packet-forward-middleware/v7/router/types" alliancemoduletypes "github.com/terra-money/alliance/x/alliance/types" + ibchookstypes "github.com/notional-labs/centauri/v3/x/ibc-hooks/types" transfermiddlewaretypes "github.com/notional-labs/centauri/v3/x/transfermiddleware/types" consensusparamtypes "github.com/cosmos/cosmos-sdk/x/consensus/types" - minttypes "github.com/notional-labs/centauri/v3/x/mint/types" storetypes "github.com/cosmos/cosmos-sdk/store/types" + minttypes "github.com/notional-labs/centauri/v3/x/mint/types" "github.com/CosmWasm/wasmd/x/wasm" wasm08types "github.com/cosmos/ibc-go/v7/modules/light-clients/08-wasm/types" @@ -45,7 +46,7 @@ func (appKeepers *AppKeepers) GenerateKeys() { authtypes.StoreKey, banktypes.StoreKey, stakingtypes.StoreKey, distrtypes.StoreKey, slashingtypes.StoreKey, govtypes.StoreKey, paramstypes.StoreKey, ibchost.StoreKey, upgradetypes.StoreKey, feegrant.StoreKey, evidencetypes.StoreKey, ibctransfertypes.StoreKey, icqtypes.StoreKey, capabilitytypes.StoreKey, consensusparamtypes.StoreKey, wasm08types.StoreKey, - crisistypes.StoreKey, routertypes.StoreKey, transfermiddlewaretypes.StoreKey, group.StoreKey, minttypes.StoreKey, alliancemoduletypes.StoreKey, wasm.StoreKey, + crisistypes.StoreKey, routertypes.StoreKey, transfermiddlewaretypes.StoreKey, group.StoreKey, minttypes.StoreKey, alliancemoduletypes.StoreKey, wasm.StoreKey, ibchookstypes.StoreKey, ) // Define transient store keys diff --git a/app/test_access.go b/app/test_access.go index 6e32fcbf0..bd348cb82 100644 --- a/app/test_access.go +++ b/app/test_access.go @@ -3,12 +3,15 @@ package app import ( "testing" + "github.com/CosmWasm/wasmd/x/wasm" "github.com/cosmos/cosmos-sdk/baseapp" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/codec" + authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" capabilitykeeper "github.com/cosmos/cosmos-sdk/x/capability/keeper" + govkeeper "github.com/cosmos/cosmos-sdk/x/gov/keeper" stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" ibctransferkeeper "github.com/cosmos/ibc-go/v7/modules/apps/transfer/keeper" ibckeeper "github.com/cosmos/ibc-go/v7/modules/core/keeper" @@ -46,10 +49,18 @@ func (s TestSupport) StakingKeeper() *stakingkeeper.Keeper { return s.app.StakingKeeper } +func (s TestSupport) AccountKeeper() authkeeper.AccountKeeper { + return s.app.AccountKeeper +} + func (s TestSupport) BankKeeper() bankkeeper.Keeper { return s.app.BankKeeper } +func (s TestSupport) GovKeeper() govkeeper.Keeper { + return s.app.GovKeeper +} + func (s TestSupport) TransferKeeper() ibctransferkeeper.Keeper { return s.app.TransferKeeper } @@ -58,6 +69,10 @@ func (s TestSupport) Wasm08Keeper() wasm08.Keeper { return s.app.Wasm08Keeper } +func (s TestSupport) WasmdKeeper() wasm.Keeper { + return s.app.WasmKeeper +} + func (s TestSupport) GetBaseApp() *baseapp.BaseApp { return s.app.BaseApp } diff --git a/tests/ibc-hooks/bytecode/counter.wasm b/tests/ibc-hooks/bytecode/counter.wasm new file mode 100644 index 000000000..938171ef8 Binary files /dev/null and b/tests/ibc-hooks/bytecode/counter.wasm differ diff --git a/tests/ibc-hooks/bytecode/echo.wasm b/tests/ibc-hooks/bytecode/echo.wasm new file mode 100644 index 000000000..4feb4554e Binary files /dev/null and b/tests/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..fe9140878 --- /dev/null +++ b/x/ibc-hooks/client/cli/query.go @@ -0,0 +1,76 @@ +package cli + +import ( + "fmt" + "strings" + + "github.com/cosmos/cosmos-sdk/client/flags" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/version" + "github.com/notional-labs/centauri/v3/x/ibc-hooks/keeper" + "github.com/spf13/cobra" + + "github.com/notional-labs/centauri/v3/x/ibc-hooks/types" +) + +func indexRunCmd(cmd *cobra.Command, args []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/ibc-hooks/hooks.go b/x/ibc-hooks/hooks.go new file mode 100644 index 000000000..2a2fc2cd3 --- /dev/null +++ b/x/ibc-hooks/hooks.go @@ -0,0 +1,171 @@ +package ibc_hooks + +import ( + // external libraries + sdk "github.com/cosmos/cosmos-sdk/types" + capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" + + // ibc-go + clienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" + channeltypes "github.com/cosmos/ibc-go/v7/modules/core/04-channel/types" + ibcexported "github.com/cosmos/ibc-go/v7/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, + sourcePort string, + sourceChannel string, + timeoutHeight clienttypes.Height, + timeoutTimestamp uint64, + data []byte, + ) (sequence uint64, err 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/ibc-hooks/ibc_module.go b/x/ibc-hooks/ibc_module.go new file mode 100644 index 000000000..0d876f891 --- /dev/null +++ b/x/ibc-hooks/ibc_module.go @@ -0,0 +1,262 @@ +package ibc_hooks + +import ( + // external libraries + + sdk "github.com/cosmos/cosmos-sdk/types" + capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" + + // ibc-go + clienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" + channeltypes "github.com/cosmos/ibc-go/v7/modules/core/04-channel/types" + porttypes "github.com/cosmos/ibc-go/v7/modules/core/05-port/types" + ibcexported "github.com/cosmos/ibc-go/v7/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, + sourcePort, sourceChannel string, + timeoutHeight clienttypes.Height, + timeoutTimestamp uint64, + data []byte, +) (sequence uint64, err 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/ibc-hooks/ics4_middleware.go b/x/ibc-hooks/ics4_middleware.go new file mode 100644 index 000000000..ab45dcb7e --- /dev/null +++ b/x/ibc-hooks/ics4_middleware.go @@ -0,0 +1,86 @@ +package ibc_hooks + +import ( + // external libraries + + sdk "github.com/cosmos/cosmos-sdk/types" + capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" + + // ibc-go + clienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" + porttypes "github.com/cosmos/ibc-go/v7/modules/core/05-port/types" + ibcexported "github.com/cosmos/ibc-go/v7/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, + chanCap *capabilitytypes.Capability, + sourcePort, 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, chanCap, sourcePort, sourceChannel, timeoutHeight, timeoutTimestamp, data) + } + + if hook, ok := i.Hooks.(SendPacketBeforeHooks); ok { + hook.SendPacketBeforeHook(ctx, chanCap, sourcePort, sourceChannel, timeoutHeight, timeoutTimestamp, data) + } + + seq, err := i.channel.SendPacket(ctx, chanCap, sourcePort, sourceChannel, timeoutHeight, timeoutTimestamp, data) + + if hook, ok := i.Hooks.(SendPacketAfterHooks); ok { + hook.SendPacketAfterHook(ctx, chanCap, sourcePort, sourceChannel, timeoutHeight, timeoutTimestamp, data, err) + } + + return seq, 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/keeper/keeper.go b/x/ibc-hooks/keeper/keeper.go new file mode 100644 index 000000000..cc02434d0 --- /dev/null +++ b/x/ibc-hooks/keeper/keeper.go @@ -0,0 +1,60 @@ +package keeper + +import ( + "fmt" + + "github.com/cometbft/cometbft/libs/log" + storetypes "github.com/cosmos/cosmos-sdk/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/address" + "github.com/notional-labs/centauri/v3/x/ibc-hooks/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/ibc-hooks/module.go b/x/ibc-hooks/module.go new file mode 100644 index 000000000..82aa83727 --- /dev/null +++ b/x/ibc-hooks/module.go @@ -0,0 +1,122 @@ +package ibc_hooks + +import ( + "encoding/json" + + "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/notional-labs/centauri/v3/x/ibc-hooks/client/cli" + "github.com/notional-labs/centauri/v3/x/ibc-hooks/types" + + cdctypes "github.com/cosmos/cosmos-sdk/codec/types" + + abci "github.com/cometbft/cometbft/abci/types" + sdk "github.com/cosmos/cosmos-sdk/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(cdc *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(cdc codec.JSONCodec) json.RawMessage { + emptyString := "{}" + return []byte(emptyString) +} + +// ValidateGenesis performs genesis state validation for the ibc-hooks module. +func (AppModuleBasic) ValidateGenesis(cdc codec.JSONCodec, config client.TxEncodingConfig, bz json.RawMessage) error { + return nil +} + +// RegisterRESTRoutes registers the REST routes for the ibc-hooks module. +func (AppModuleBasic) RegisterRESTRoutes(clientCtx client.Context, rtr *mux.Router) {} + +// RegisterGRPCGatewayRoutes registers the gRPC Gateway routes for the ibc-hooks module. +func (AppModuleBasic) RegisterGRPCGatewayRoutes(clientCtx client.Context, mux *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 +} + +// NewAppModule creates a new AppModule object. +func NewAppModule() AppModule { + return AppModule{ + AppModuleBasic: AppModuleBasic{}, + } +} + +// 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 "" +} + +// RegisterServices registers a gRPC query service to respond to the +// module-specific gRPC queries. +func (am AppModule) RegisterServices(cfg module.Configurator) { +} + +// InitGenesis performs genesis initialization for the ibc-hooks module. It returns +// no validator updates. +func (am AppModule) InitGenesis(ctx sdk.Context, cdc codec.JSONCodec, data json.RawMessage) []abci.ValidatorUpdate { + return []abci.ValidatorUpdate{} +} + +func (am AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONCodec) json.RawMessage { + return json.RawMessage([]byte("{}")) +} + +// BeginBlock returns the begin blocker for the ibc-hooks module. +func (am AppModule) BeginBlock(ctx 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/relay_test.go b/x/ibc-hooks/relay_test.go new file mode 100644 index 000000000..32ea3ab01 --- /dev/null +++ b/x/ibc-hooks/relay_test.go @@ -0,0 +1,236 @@ +package ibc_hooks_test + +import ( + "fmt" + "testing" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + transfertypes "github.com/cosmos/ibc-go/v7/modules/apps/transfer/types" + clienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" + customibctesting "github.com/notional-labs/centauri/v3/app/ibctesting" + ibchookskeeper "github.com/notional-labs/centauri/v3/x/ibc-hooks/keeper" + "github.com/stretchr/testify/suite" +) + +// TODO: use testsuite here. +type IBCHooksTestSuite struct { + suite.Suite + + coordinator *customibctesting.Coordinator + + // testing chains used for convenience and readability + chainA *customibctesting.TestChain + chainB *customibctesting.TestChain + chainC *customibctesting.TestChain +} + +func (suite *IBCHooksTestSuite) SetupTest() { + suite.coordinator = customibctesting.NewCoordinator(suite.T(), 4) + suite.chainA = suite.coordinator.GetChain(customibctesting.GetChainID(1)) + suite.chainB = suite.coordinator.GetChain(customibctesting.GetChainID(2)) + suite.chainC = suite.coordinator.GetChain(customibctesting.GetChainID(3)) +} + +func NewTransferPath(chainA, chainB *customibctesting.TestChain) *customibctesting.Path { + path := customibctesting.NewPath(chainA, chainB) + path.EndpointA.ChannelConfig.PortID = customibctesting.TransferPort + path.EndpointB.ChannelConfig.PortID = customibctesting.TransferPort + path.EndpointA.ChannelConfig.Version = transfertypes.Version + path.EndpointB.ChannelConfig.Version = transfertypes.Version + + return path +} + +func TestKeeperTestSuite(t *testing.T) { + suite.Run(t, new(IBCHooksTestSuite)) +} + +func (suite *IBCHooksTestSuite) TestRecvHooks() { + var ( + transferAmount = sdk.NewInt(1000000000) + timeoutHeight = clienttypes.NewHeight(1, 110) + // when transfer via sdk transfer from A (module) -> B (contract) + // nativeTokenSendOnChainA = sdk.NewCoin(sdk.DefaultBondDenom, transferAmount) + ) + + suite.SetupTest() // reset + + path := NewTransferPath(suite.chainA, suite.chainB) + suite.coordinator.Setup(path) + + // store code + suite.chainB.StoreContractCode(&suite.Suite, "../../tests/ibc-hooks/bytecode/counter.wasm") + // instancetiate contract + addr := suite.chainB.InstantiateContract(&suite.Suite, `{"count": 0}`, 1) + suite.Require().NotEmpty(addr) + + memo := fmt.Sprintf(`{"wasm": {"contract": "%s", "msg": {"increment": {} } } }`, addr) + + msg := transfertypes.NewMsgTransfer( + path.EndpointA.ChannelConfig.PortID, + path.EndpointA.ChannelID, + sdk.NewCoin(sdk.DefaultBondDenom, transferAmount), + suite.chainA.SenderAccount.GetAddress().String(), + addr.String(), + timeoutHeight, + 0, + memo, + ) + _, err := suite.chainA.SendMsgs(msg) + suite.Require().NoError(err) + suite.Require().NoError(err, path.EndpointB.UpdateClient()) + + // then + suite.Require().Equal(1, len(suite.chainA.PendingSendPackets)) + suite.Require().Equal(0, len(suite.chainB.PendingSendPackets)) + + // and when relay to chain B and handle Ack on chain A + err = suite.coordinator.RelayAndAckPendingPackets(path) + suite.Require().NoError(err) + + // then + suite.Require().Equal(0, len(suite.chainA.PendingSendPackets)) + suite.Require().Equal(0, len(suite.chainB.PendingSendPackets)) + + senderLocalAcc, err := ibchookskeeper.DeriveIntermediateSender("channel-0", suite.chainA.SenderAccount.GetAddress().String(), "cosmos") + suite.Require().NoError(err) + + state := suite.chainB.QueryContract(&suite.Suite, addr, []byte(fmt.Sprintf(`{"get_count": {"addr": "%s"}}`, senderLocalAcc))) + suite.Require().Equal(`{"count":0}`, state) + + state = suite.chainB.QueryContract(&suite.Suite, addr, []byte(fmt.Sprintf(`{"get_total_funds": {"addr": "%s"}}`, senderLocalAcc))) + suite.Require().Equal(`{"total_funds":[{"denom":"ibc/C053D637CCA2A2BA030E2C5EE1B28A16F71CCB0E45E8BE52766DC1B241B77878","amount":"1000000000"}]}`, state) +} + +func (suite *IBCHooksTestSuite) TestAckHooks() { + var ( + transferAmount = sdk.NewInt(1000000000) + timeoutHeight = clienttypes.NewHeight(0, 110) + // when transfer via sdk transfer from A (module) -> B (contract) + // nativeTokenSendOnChainA = sdk.NewCoin(sdk.DefaultBondDenom, transferAmount) + ) + + suite.SetupTest() // reset + + path := NewTransferPath(suite.chainA, suite.chainB) + suite.coordinator.Setup(path) + + // store code + suite.chainA.StoreContractCode(&suite.Suite, "../../tests/ibc-hooks/bytecode/counter.wasm") + // instancetiate contract + addr := suite.chainA.InstantiateContract(&suite.Suite, `{"count": 0}`, 1) + suite.Require().NotEmpty(addr) + + fmt.Println(addr.String()) + + // Generate swap instructions for the contract + callbackMemo := fmt.Sprintf(`{"ibc_callback":"%s"}`, addr) + + msg := transfertypes.NewMsgTransfer( + path.EndpointA.ChannelConfig.PortID, + path.EndpointA.ChannelID, + sdk.NewCoin(sdk.DefaultBondDenom, transferAmount), + suite.chainA.SenderAccount.GetAddress().String(), + addr.String(), + timeoutHeight, + 0, + callbackMemo, + ) + _, err := suite.chainA.SendMsgs(msg) + suite.Require().NoError(err) + suite.Require().NoError(err, path.EndpointB.UpdateClient()) + + // then + suite.Require().Equal(1, len(suite.chainA.PendingSendPackets)) + suite.Require().Equal(0, len(suite.chainB.PendingSendPackets)) + + // and when relay to chain B and handle Ack on chain A + err = suite.coordinator.RelayAndAckPendingPackets(path) + suite.Require().NoError(err) + + // then + suite.Require().Equal(0, len(suite.chainA.PendingSendPackets)) + suite.Require().Equal(0, len(suite.chainB.PendingSendPackets)) + + state := suite.chainA.QueryContract( + &suite.Suite, addr, + []byte(fmt.Sprintf(`{"get_count": {"addr": "%s"}}`, addr))) + suite.Require().Equal(`{"count":1}`, state) + + _, err = suite.chainA.SendMsgs(msg) + suite.Require().NoError(err) + suite.Require().NoError(err, path.EndpointB.UpdateClient()) + + // then + suite.Require().Equal(1, len(suite.chainA.PendingSendPackets)) + suite.Require().Equal(0, len(suite.chainB.PendingSendPackets)) + + // and when relay to chain B and handle Ack on chain A + err = suite.coordinator.RelayAndAckPendingPackets(path) + suite.Require().NoError(err) + + // then + suite.Require().Equal(0, len(suite.chainA.PendingSendPackets)) + suite.Require().Equal(0, len(suite.chainB.PendingSendPackets)) + + state = suite.chainA.QueryContract( + &suite.Suite, addr, + []byte(fmt.Sprintf(`{"get_count": {"addr": "%s"}}`, addr))) + suite.Require().Equal(`{"count":2}`, state) +} + +func (suite *IBCHooksTestSuite) TestTimeoutHooks() { + var ( + transferAmount = sdk.NewInt(1000000000) + timeoutHeight = clienttypes.NewHeight(0, 500) + // when transfer via sdk transfer from A (module) -> B (contract) + // nativeTokenSendOnChainA = sdk.NewCoin(sdk.DefaultBondDenom, transferAmount) + ) + + suite.SetupTest() // reset + + path := NewTransferPath(suite.chainA, suite.chainB) + suite.coordinator.Setup(path) + + // store code + suite.chainA.StoreContractCode(&suite.Suite, "../../tests/ibc-hooks/bytecode/counter.wasm") + // instancetiate contract + addr := suite.chainA.InstantiateContract(&suite.Suite, `{"count": 0}`, 1) + suite.Require().NotEmpty(addr) + + fmt.Println(addr.String()) + + // Generate swap instructions for the contract + callbackMemo := fmt.Sprintf(`{"ibc_callback":"%s"}`, addr) + + msg := transfertypes.NewMsgTransfer( + path.EndpointA.ChannelConfig.PortID, + path.EndpointA.ChannelID, + sdk.NewCoin(sdk.DefaultBondDenom, transferAmount), + suite.chainA.SenderAccount.GetAddress().String(), + addr.String(), + timeoutHeight, + uint64(suite.coordinator.CurrentTime.Add(time.Minute).UnixNano()), + callbackMemo, + ) + sdkResult, err := suite.chainA.SendMsgs(msg) + suite.Require().NoError(err) + + packet, err := customibctesting.ParsePacketFromEvents(sdkResult.GetEvents()) + suite.Require().NoError(err) + + // Move chainB forward one block + suite.chainB.NextBlock() + // One month later + suite.coordinator.IncrementTimeBy(time.Hour) + err = path.EndpointA.UpdateClient() + suite.Require().NoError(err) + + err = path.EndpointA.TimeoutPacket(packet) + suite.Require().NoError(err) + + // The test contract will increment the counter for itself by 10 when a packet times out + state := suite.chainA.QueryContract(&suite.Suite, addr, []byte(fmt.Sprintf(`{"get_count": {"addr": "%s"}}`, addr))) + suite.Require().Equal(`{"count":10}`, state) +} diff --git a/x/ibc-hooks/types/errors.go b/x/ibc-hooks/types/errors.go new file mode 100644 index 000000000..717e33bdd --- /dev/null +++ b/x/ibc-hooks/types/errors.go @@ -0,0 +1,17 @@ +package types + +import ( + errorsmod "cosmossdk.io/errors" +) + +var ( + ErrBadMetadataFormatMsg = "wasm metadata not properly formatted for: '%v'. %s" + ErrBadExecutionMsg = "cannot execute contract: %v" + + ErrMsgValidation = errorsmod.Register("wasm-hooks", 2, "error in wasmhook message validation") + ErrMarshaling = errorsmod.Register("wasm-hooks", 3, "cannot marshal the ICS20 packet") + ErrInvalidPacket = errorsmod.Register("wasm-hooks", 4, "invalid packet data") + ErrBadResponse = errorsmod.Register("wasm-hooks", 5, "cannot create response") + ErrWasmError = errorsmod.Register("wasm-hooks", 6, "wasm error") + ErrBadSender = errorsmod.Register("wasm-hooks", 7, "bad sender") +) 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.go b/x/ibc-hooks/utils.go new file mode 100644 index 000000000..e8c084e60 --- /dev/null +++ b/x/ibc-hooks/utils.go @@ -0,0 +1,77 @@ +package ibc_hooks + +import ( + "encoding/json" + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + transfertypes "github.com/cosmos/ibc-go/v7/modules/apps/transfer/types" + channeltypes "github.com/cosmos/ibc-go/v7/modules/core/04-channel/types" + ibcexported "github.com/cosmos/ibc-go/v7/modules/core/exported" +) + +const IbcAcknowledgementErrorType = "ibc-acknowledgement-error" + +// 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 { + logger := ctx.Logger().With("module", IbcAcknowledgementErrorType) + + 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) + logger.Error(fmt.Sprintf("error-context: %v", s)) + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + IbcAcknowledgementErrorType, + 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/ibc-hooks/wasm_hook.go b/x/ibc-hooks/wasm_hook.go new file mode 100644 index 000000000..f68b28082 --- /dev/null +++ b/x/ibc-hooks/wasm_hook.go @@ -0,0 +1,375 @@ +package ibc_hooks + +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/v7/modules/apps/transfer/types" + clienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" + channeltypes "github.com/cosmos/ibc-go/v7/modules/core/04-channel/types" + ibcexported "github.com/cosmos/ibc-go/v7/modules/core/exported" + "github.com/notional-labs/centauri/v3/x/ibc-hooks/keeper" + "github.com/notional-labs/centauri/v3/x/ibc-hooks/types" + + errorsmod "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" + capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" +) + +type ContractAck struct { + ContractResult []byte `json:"contract_result"` + IbcAck []byte `json:"ibc_ack"` +} + +type WasmHooks struct { + ContractKeeper *wasmkeeper.Keeper + ibcHooksKeeper *keeper.Keeper + bech32PrefixAccAddr string +} + +func NewWasmHooks(ibcHooksKeeper *keeper.Keeper, contractKeeper *wasmkeeper.Keeper, 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.GetData()) + 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 NewEmitErrorAcknowledgement(ctx, types.ErrMsgValidation, err.Error()) + } + if msgBytes == nil || contractAddr == nil { // This should never happen + return 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 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 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 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 := 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 NewEmitErrorAcknowledgement(ctx, types.ErrWasmError, err.Error()) + } + + fullAck := ContractAck{ContractResult: response.Data, IbcAck: ack.Acknowledgement()} + bz, err = json.Marshal(fullAck) + if err != nil { + return NewEmitErrorAcknowledgement(ctx, types.ErrBadResponse, err.Error()) + } + return channeltypes.NewResultAcknowledgement(bz) +} + +func (h WasmHooks) SendPacketOverride( + i ICS4Middleware, + ctx sdk.Context, + chanCap *capabilitytypes.Capability, + sourcePort string, + sourceChannel string, + timeoutHeight clienttypes.Height, + timeoutTimestamp uint64, + data []byte, +) (sequence uint64, err error) { + isIcs20, Ics20PacketData := isIcs20Packet(data) + if !isIcs20 { + return i.channel.SendPacket(ctx, chanCap, sourcePort, sourceChannel, timeoutHeight, timeoutTimestamp, data) // continue + } + + isCallbackRouted, metadata := jsonStringHasKey(Ics20PacketData.GetMemo(), types.IBCCallbackKey) + if !isCallbackRouted { + return i.channel.SendPacket(ctx, chanCap, sourcePort, sourceChannel, timeoutHeight, timeoutTimestamp, data) // continue + } + + // 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 0, errorsmod.Wrap(err, "Send packet with callback error") + } + stringMetadata := string(bzMetadata) + if stringMetadata == "{}" { + Ics20PacketData.Memo = "" + } else { + Ics20PacketData.Memo = stringMetadata + } + dataBytes, err := json.Marshal(Ics20PacketData) + if err != nil { + return 0, errorsmod.Wrap(err, "Send packet with callback error") + } + + seq, err := i.channel.SendPacket(ctx, chanCap, sourcePort, sourceChannel, timeoutHeight, timeoutTimestamp, dataBytes) + if err != nil { + return seq, 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 seq, nil + } + _, err = sdk.AccAddressFromBech32(contract) + if err != nil { + return seq, nil + } + h.ibcHooksKeeper.StorePacketCallback(ctx, sourceChannel, seq, contract) + return seq, 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 errorsmod.Wrap(err, "Ack callback error") // The callback configured is not a bech32. Error out + } + + success := "false" + if !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 errorsmod.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 errorsmod.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 +} + +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 isIcs20Packet(data []byte) (isIcs20 bool, ics20data transfertypes.FungibleTokenPacketData) { + var packetData transfertypes.FungibleTokenPacketData + if err := json.Unmarshal(data, &packetData); err != nil { + return false, packetData + } + return true, packetData +} + +// 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 (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) +}