From 7ccd1c0281f889aac2290895859bea1e4143c324 Mon Sep 17 00:00:00 2001 From: Nikolas De Giorgis Date: Tue, 19 Nov 2024 09:16:09 +0000 Subject: [PATCH] Ibcmodulev2 transfer application spike (#7524) * chore: adding ibc module v2 and on recv implementation with hard coded sequence * chore: adding ack callback * chore: adding send packet * chore: adding transfer v2 module to app wiring * chore: adding happy path and basic failure for send and recv for transfer module * chore: adding ack test for transfer * chore: fix some linter errors * chore: adding timeout test for transfer v2 * chore: adding test case which ensures tokens can be transfered over both v1 and v2 * chore: full transfer flow from A - B - C - B - A * chore: separated test out into subtests * chore: add sequence as argument to OnRecvPacket * chore: adding TODOs for next steps * chore: adding transferv2 module to wasm simapp * chore: refactor OnTimeout to accept sequence * chore: refactor OnAck to accept sequence * chore: switch argument order * wip: mid merge with feature branch, build will be broken * Fix timeoutTimestamp for tests * linter * chore: removing duplicate imports in wasm simapp * chore: adding channelkeeperv2 query server * register grpc gateway routes * fix ack structure for v2 transfer tests * lint * make signature consistent --------- Co-authored-by: chatton Co-authored-by: bznein Co-authored-by: Gjermund Garaba --- modules/apps/transfer/keeper/export_test.go | 5 - modules/apps/transfer/keeper/forwarding.go | 10 +- modules/apps/transfer/keeper/genesis.go | 2 +- modules/apps/transfer/keeper/invariants.go | 2 +- modules/apps/transfer/keeper/keeper.go | 20 +- modules/apps/transfer/keeper/migrations.go | 6 +- modules/apps/transfer/keeper/msg_server.go | 4 +- modules/apps/transfer/keeper/relay.go | 44 +- modules/apps/transfer/types/packet.go | 2 + modules/apps/transfer/v2/ibc_module.go | 133 ++++ modules/apps/transfer/v2/keeper/keeper.go | 249 ++++++++ .../apps/transfer/v2/keeper/keeper_test.go | 58 ++ .../transfer/v2/keeper/msg_server_test.go | 597 ++++++++++++++++++ .../core/04-channel/v2/keeper/msg_server.go | 15 +- .../04-channel/v2/keeper/msg_server_test.go | 10 +- modules/core/04-channel/v2/keeper/packet.go | 7 +- modules/core/api/module.go | 11 +- .../08-wasm/testing/simapp/app.go | 12 +- testing/mock/v2/ibc_app.go | 8 +- testing/mock/v2/ibc_module.go | 12 +- testing/path.go | 7 + testing/simapp/app.go | 7 + 22 files changed, 1144 insertions(+), 77 deletions(-) create mode 100644 modules/apps/transfer/v2/ibc_module.go create mode 100644 modules/apps/transfer/v2/keeper/keeper.go create mode 100644 modules/apps/transfer/v2/keeper/keeper_test.go create mode 100644 modules/apps/transfer/v2/keeper/msg_server_test.go diff --git a/modules/apps/transfer/keeper/export_test.go b/modules/apps/transfer/keeper/export_test.go index 5efd6a85910..dedfd560b9b 100644 --- a/modules/apps/transfer/keeper/export_test.go +++ b/modules/apps/transfer/keeper/export_test.go @@ -54,11 +54,6 @@ func (k Keeper) GetAllForwardedPackets(ctx sdk.Context) []types.ForwardedPacket return k.getAllForwardedPackets(ctx) } -// IsBlockedAddr is a wrapper around isBlockedAddr for testing purposes -func (k Keeper) IsBlockedAddr(addr sdk.AccAddress) bool { - return k.isBlockedAddr(addr) -} - // CreatePacketDataBytesFromVersion is a wrapper around createPacketDataBytesFromVersion for testing purposes func CreatePacketDataBytesFromVersion(appVersion, sender, receiver, memo string, tokens types.Tokens, hops []types.Hop) ([]byte, error) { return createPacketDataBytesFromVersion(appVersion, sender, receiver, memo, tokens, hops) diff --git a/modules/apps/transfer/keeper/forwarding.go b/modules/apps/transfer/keeper/forwarding.go index cb68b9d919a..967fed6beff 100644 --- a/modules/apps/transfer/keeper/forwarding.go +++ b/modules/apps/transfer/keeper/forwarding.go @@ -22,7 +22,7 @@ func (k Keeper) forwardPacket(ctx context.Context, data types.FungibleTokenPacke } // sending from module account (used as a temporary forward escrow) to the original receiver address. - sender := k.authKeeper.GetModuleAddress(types.ModuleName) + sender := k.AuthKeeper.GetModuleAddress(types.ModuleName) msg := types.NewMsgTransfer( data.Forwarding.Hops[0].PortId, @@ -68,7 +68,7 @@ func (k Keeper) revertForwardedPacket(ctx context.Context, forwardedPacket chann 2. Burning voucher tokens if the funds are foreign */ - forwardingAddr := k.authKeeper.GetModuleAddress(types.ModuleName) + forwardingAddr := k.AuthKeeper.GetModuleAddress(types.ModuleName) escrow := types.GetEscrowAddress(forwardedPacket.DestinationPort, forwardedPacket.DestinationChannel) // we can iterate over the received tokens of forwardedPacket by iterating over the sent tokens of failedPacketData @@ -83,12 +83,12 @@ func (k Keeper) revertForwardedPacket(ctx context.Context, forwardedPacket chann // given that the packet is being reversed, we check the DestinationChannel and DestinationPort // of the forwardedPacket to see if a hop was added to the trace during the receive step if token.Denom.HasPrefix(forwardedPacket.DestinationPort, forwardedPacket.DestinationChannel) { - if err := k.bankKeeper.BurnCoins(ctx, types.ModuleName, sdk.NewCoins(coin)); err != nil { + if err := k.BankKeeper.BurnCoins(ctx, types.ModuleName, sdk.NewCoins(coin)); err != nil { return err } } else { // send it back to the escrow address - if err := k.escrowCoin(ctx, forwardingAddr, escrow, coin); err != nil { + if err := k.EscrowCoin(ctx, forwardingAddr, escrow, coin); err != nil { return err } } @@ -101,7 +101,7 @@ func (k Keeper) revertForwardedPacket(ctx context.Context, forwardedPacket chann func (k Keeper) getReceiverFromPacketData(data types.FungibleTokenPacketDataV2) (sdk.AccAddress, error) { if data.HasForwarding() { // since data.Receiver can potentially be a non-CosmosSDK AccAddress, we return early if the packet should be forwarded - return k.authKeeper.GetModuleAddress(types.ModuleName), nil + return k.AuthKeeper.GetModuleAddress(types.ModuleName), nil } receiver, err := sdk.AccAddressFromBech32(data.Receiver) diff --git a/modules/apps/transfer/keeper/genesis.go b/modules/apps/transfer/keeper/genesis.go index d9653ce3c6c..330c6f0360d 100644 --- a/modules/apps/transfer/keeper/genesis.go +++ b/modules/apps/transfer/keeper/genesis.go @@ -12,7 +12,7 @@ func (k Keeper) InitGenesis(ctx sdk.Context, state types.GenesisState) { for _, denom := range state.Denoms { k.SetDenom(ctx, denom) - k.setDenomMetadata(ctx, denom) + k.SetDenomMetadata(ctx, denom) } k.SetParams(ctx, state.Params) diff --git a/modules/apps/transfer/keeper/invariants.go b/modules/apps/transfer/keeper/invariants.go index dded2822ec9..3dc8f95b458 100644 --- a/modules/apps/transfer/keeper/invariants.go +++ b/modules/apps/transfer/keeper/invariants.go @@ -33,7 +33,7 @@ func TotalEscrowPerDenomInvariants(k *Keeper) sdk.Invariant { transferChannels := k.channelKeeper.GetAllChannelsWithPortPrefix(ctx, portID) for _, channel := range transferChannels { escrowAddress := types.GetEscrowAddress(portID, channel.ChannelId) - escrowBalances := k.bankKeeper.GetAllBalances(ctx, escrowAddress) + escrowBalances := k.BankKeeper.GetAllBalances(ctx, escrowAddress) actualTotalEscrowed = actualTotalEscrowed.Add(escrowBalances...) } diff --git a/modules/apps/transfer/keeper/keeper.go b/modules/apps/transfer/keeper/keeper.go index 1ebb14ace71..b9cb766ab48 100644 --- a/modules/apps/transfer/keeper/keeper.go +++ b/modules/apps/transfer/keeper/keeper.go @@ -34,8 +34,8 @@ type Keeper struct { ics4Wrapper porttypes.ICS4Wrapper channelKeeper types.ChannelKeeper - authKeeper types.AccountKeeper - bankKeeper types.BankKeeper + AuthKeeper types.AccountKeeper + BankKeeper types.BankKeeper // the address capable of executing a MsgUpdateParams message. Typically, this // should be the x/gov module account. @@ -68,8 +68,8 @@ func NewKeeper( legacySubspace: legacySubspace, ics4Wrapper: ics4Wrapper, channelKeeper: channelKeeper, - authKeeper: authKeeper, - bankKeeper: bankKeeper, + AuthKeeper: authKeeper, + BankKeeper: bankKeeper, authority: authority, } } @@ -195,8 +195,8 @@ func (k Keeper) IterateDenoms(ctx context.Context, cb func(denom types.Denom) bo } } -// setDenomMetadata sets an IBC token's denomination metadata -func (k Keeper) setDenomMetadata(ctx context.Context, denom types.Denom) { +// SetDenomMetadata sets an IBC token's denomination metadata +func (k Keeper) SetDenomMetadata(ctx context.Context, denom types.Denom) { metadata := banktypes.Metadata{ Description: fmt.Sprintf("IBC token from %s", denom.Path()), DenomUnits: []*banktypes.DenomUnit{ @@ -214,7 +214,7 @@ func (k Keeper) setDenomMetadata(ctx context.Context, denom types.Denom) { Symbol: strings.ToUpper(denom.Base), } - k.bankKeeper.SetDenomMetaData(ctx, metadata) + k.BankKeeper.SetDenomMetaData(ctx, metadata) } // GetTotalEscrowForDenom gets the total amount of source chain tokens that @@ -385,11 +385,11 @@ func (k Keeper) iterateForwardedPackets(ctx context.Context, cb func(packet type // IsBlockedAddr checks if the given address is allowed to send or receive tokens. // The module account is always allowed to send and receive tokens. -func (k Keeper) isBlockedAddr(addr sdk.AccAddress) bool { - moduleAddr := k.authKeeper.GetModuleAddress(types.ModuleName) +func (k Keeper) IsBlockedAddr(addr sdk.AccAddress) bool { + moduleAddr := k.AuthKeeper.GetModuleAddress(types.ModuleName) if addr.Equals(moduleAddr) { return false } - return k.bankKeeper.BlockedAddr(addr) + return k.BankKeeper.BlockedAddr(addr) } diff --git a/modules/apps/transfer/keeper/migrations.go b/modules/apps/transfer/keeper/migrations.go index ab1e526d4a2..bc82b83c21a 100644 --- a/modules/apps/transfer/keeper/migrations.go +++ b/modules/apps/transfer/keeper/migrations.go @@ -43,7 +43,7 @@ func (m Migrator) MigrateDenomMetadata(ctx sdk.Context) error { m.keeper.iterateDenomTraces(ctx, func(dt internaltypes.DenomTrace) (stop bool) { // check if the metadata for the given denom trace does not already exist - if !m.keeper.bankKeeper.HasDenomMetaData(ctx, dt.IBCDenom()) { + if !m.keeper.BankKeeper.HasDenomMetaData(ctx, dt.IBCDenom()) { m.keeper.setDenomMetadataWithDenomTrace(ctx, dt) } return false @@ -61,7 +61,7 @@ func (m Migrator) MigrateTotalEscrowForDenom(ctx sdk.Context) error { transferChannels := m.keeper.channelKeeper.GetAllChannelsWithPortPrefix(ctx, portID) for _, channel := range transferChannels { escrowAddress := types.GetEscrowAddress(portID, channel.ChannelId) - escrowBalances := m.keeper.bankKeeper.GetAllBalances(ctx, escrowAddress) + escrowBalances := m.keeper.BankKeeper.GetAllBalances(ctx, escrowAddress) totalEscrowed = totalEscrowed.Add(escrowBalances...) } @@ -164,5 +164,5 @@ func (k Keeper) setDenomMetadataWithDenomTrace(ctx sdk.Context, denomTrace inter Symbol: strings.ToUpper(denomTrace.BaseDenom), } - k.bankKeeper.SetDenomMetaData(ctx, metadata) + k.BankKeeper.SetDenomMetaData(ctx, metadata) } diff --git a/modules/apps/transfer/keeper/msg_server.go b/modules/apps/transfer/keeper/msg_server.go index b0cb57398b7..bf77a3241b0 100644 --- a/modules/apps/transfer/keeper/msg_server.go +++ b/modules/apps/transfer/keeper/msg_server.go @@ -29,11 +29,11 @@ func (k Keeper) Transfer(goCtx context.Context, msg *types.MsgTransfer) (*types. coins := msg.GetCoins() - if err := k.bankKeeper.IsSendEnabledCoins(ctx, coins...); err != nil { + if err := k.BankKeeper.IsSendEnabledCoins(ctx, coins...); err != nil { return nil, errorsmod.Wrapf(types.ErrSendDisabled, err.Error()) } - if k.isBlockedAddr(sender) { + if k.IsBlockedAddr(sender) { return nil, errorsmod.Wrapf(ibcerrors.ErrUnauthorized, "%s is not allowed to send funds", sender) } diff --git a/modules/apps/transfer/keeper/relay.go b/modules/apps/transfer/keeper/relay.go index 71ef740132c..a69ab8dbfff 100644 --- a/modules/apps/transfer/keeper/relay.go +++ b/modules/apps/transfer/keeper/relay.go @@ -96,7 +96,7 @@ func (k Keeper) sendTransfer( for _, coin := range coins { // Using types.UnboundedSpendLimit allows us to send the entire balance of a given denom. if coin.Amount.Equal(types.UnboundedSpendLimit()) { - coin.Amount = k.bankKeeper.GetBalance(ctx, sender, coin.Denom).Amount + coin.Amount = k.BankKeeper.GetBalance(ctx, sender, coin.Denom).Amount } token, err := k.tokenFromCoin(ctx, coin) @@ -112,13 +112,13 @@ func (k Keeper) sendTransfer( // the token, then we must be returning the token back to the chain they originated from if token.Denom.HasPrefix(sourcePort, sourceChannel) { // transfer the coins to the module account and burn them - if err := k.bankKeeper.SendCoinsFromAccountToModule( + if err := k.BankKeeper.SendCoinsFromAccountToModule( ctx, sender, types.ModuleName, sdk.NewCoins(coin), ); err != nil { return 0, err } - if err := k.bankKeeper.BurnCoins( + if err := k.BankKeeper.BurnCoins( ctx, types.ModuleName, sdk.NewCoins(coin), ); err != nil { // NOTE: should not happen as the module account was @@ -129,7 +129,7 @@ func (k Keeper) sendTransfer( } else { // obtain the escrow address for the source channel end escrowAddress := types.GetEscrowAddress(sourcePort, sourceChannel) - if err := k.escrowCoin(ctx, sender, escrowAddress, coin); err != nil { + if err := k.EscrowCoin(ctx, sender, escrowAddress, coin); err != nil { return 0, err } } @@ -178,7 +178,7 @@ func (k Keeper) OnRecvPacket(ctx context.Context, packet channeltypes.Packet, da return err } - if k.isBlockedAddr(receiver) { + if k.IsBlockedAddr(receiver) { return errorsmod.Wrapf(ibcerrors.ErrUnauthorized, "%s is not allowed to receive funds", receiver) } @@ -206,7 +206,7 @@ func (k Keeper) OnRecvPacket(ctx context.Context, packet channeltypes.Packet, da coin := sdk.NewCoin(token.Denom.IBCDenom(), transferAmount) escrowAddress := types.GetEscrowAddress(packet.GetDestPort(), packet.GetDestChannel()) - if err := k.unescrowCoin(ctx, escrowAddress, receiver, coin); err != nil { + if err := k.UnescrowCoin(ctx, escrowAddress, receiver, coin); err != nil { return err } @@ -224,8 +224,8 @@ func (k Keeper) OnRecvPacket(ctx context.Context, packet channeltypes.Packet, da } voucherDenom := token.Denom.IBCDenom() - if !k.bankKeeper.HasDenomMetaData(ctx, voucherDenom) { - k.setDenomMetadata(ctx, token.Denom) + if !k.BankKeeper.HasDenomMetaData(ctx, voucherDenom) { + k.SetDenomMetadata(ctx, token.Denom) } events.EmitDenomEvent(ctx, token) @@ -233,15 +233,15 @@ func (k Keeper) OnRecvPacket(ctx context.Context, packet channeltypes.Packet, da voucher := sdk.NewCoin(voucherDenom, transferAmount) // mint new tokens if the source of the transfer is the same chain - if err := k.bankKeeper.MintCoins( + if err := k.BankKeeper.MintCoins( ctx, types.ModuleName, sdk.NewCoins(voucher), ); err != nil { return errorsmod.Wrap(err, "failed to mint IBC tokens") } // send to receiver - moduleAddr := k.authKeeper.GetModuleAddress(types.ModuleName) - if err := k.bankKeeper.SendCoins( + moduleAddr := k.AuthKeeper.GetModuleAddress(types.ModuleName) + if err := k.BankKeeper.SendCoins( ctx, moduleAddr, receiver, sdk.NewCoins(voucher), ); err != nil { return errorsmod.Wrapf(err, "failed to send coins to receiver %s", receiver.String()) @@ -346,14 +346,14 @@ func (k Keeper) refundPacketTokens(ctx context.Context, packet channeltypes.Pack if err != nil { return err } - if k.isBlockedAddr(sender) { + if k.IsBlockedAddr(sender) { return errorsmod.Wrapf(ibcerrors.ErrUnauthorized, "%s is not allowed to receive funds", sender) } // escrow address for unescrowing tokens back to sender escrowAddress := types.GetEscrowAddress(packet.GetSourcePort(), packet.GetSourceChannel()) - moduleAccountAddr := k.authKeeper.GetModuleAddress(types.ModuleName) + moduleAccountAddr := k.AuthKeeper.GetModuleAddress(types.ModuleName) for _, token := range data.Tokens { coin, err := token.ToCoin() if err != nil { @@ -364,17 +364,17 @@ func (k Keeper) refundPacketTokens(ctx context.Context, packet channeltypes.Pack // then the tokens were burnt when the packet was sent and we must mint new tokens if token.Denom.HasPrefix(packet.GetSourcePort(), packet.GetSourceChannel()) { // mint vouchers back to sender - if err := k.bankKeeper.MintCoins( + if err := k.BankKeeper.MintCoins( ctx, types.ModuleName, sdk.NewCoins(coin), ); err != nil { return err } - if err := k.bankKeeper.SendCoins(ctx, moduleAccountAddr, sender, sdk.NewCoins(coin)); err != nil { + if err := k.BankKeeper.SendCoins(ctx, moduleAccountAddr, sender, sdk.NewCoins(coin)); err != nil { panic(fmt.Errorf("unable to send coins from module to account despite previously minting coins to module account: %v", err)) } } else { - if err := k.unescrowCoin(ctx, escrowAddress, sender, coin); err != nil { + if err := k.UnescrowCoin(ctx, escrowAddress, sender, coin); err != nil { return err } } @@ -383,10 +383,10 @@ func (k Keeper) refundPacketTokens(ctx context.Context, packet channeltypes.Pack return nil } -// escrowCoin will send the given coin from the provided sender to the escrow address. It will also +// EscrowCoin will send the given coin from the provided sender to the escrow address. It will also // update the total escrowed amount by adding the escrowed coin's amount to the current total escrow. -func (k Keeper) escrowCoin(ctx context.Context, sender, escrowAddress sdk.AccAddress, coin sdk.Coin) error { - if err := k.bankKeeper.SendCoins(ctx, sender, escrowAddress, sdk.NewCoins(coin)); err != nil { +func (k Keeper) EscrowCoin(ctx context.Context, sender, escrowAddress sdk.AccAddress, coin sdk.Coin) error { + if err := k.BankKeeper.SendCoins(ctx, sender, escrowAddress, sdk.NewCoins(coin)); err != nil { // failure is expected for insufficient balances return err } @@ -399,10 +399,10 @@ func (k Keeper) escrowCoin(ctx context.Context, sender, escrowAddress sdk.AccAdd return nil } -// unescrowCoin will send the given coin from the escrow address to the provided receiver. It will also +// UnescrowCoin will send the given coin from the escrow address to the provided receiver. It will also // update the total escrow by deducting the unescrowed coin's amount from the current total escrow. -func (k Keeper) unescrowCoin(ctx context.Context, escrowAddress, receiver sdk.AccAddress, coin sdk.Coin) error { - if err := k.bankKeeper.SendCoins(ctx, escrowAddress, receiver, sdk.NewCoins(coin)); err != nil { +func (k Keeper) UnescrowCoin(ctx context.Context, escrowAddress, receiver sdk.AccAddress, coin sdk.Coin) error { + if err := k.BankKeeper.SendCoins(ctx, escrowAddress, receiver, sdk.NewCoins(coin)); err != nil { // NOTE: this error is only expected to occur given an unexpected bug or a malicious // counterparty module. The bug may occur in bank or any part of the code that allows // the escrow address to be drained. A malicious counterparty module could drain the diff --git a/modules/apps/transfer/types/packet.go b/modules/apps/transfer/types/packet.go index 209c721d75b..98525dac769 100644 --- a/modules/apps/transfer/types/packet.go +++ b/modules/apps/transfer/types/packet.go @@ -210,6 +210,8 @@ func (ftpd FungibleTokenPacketDataV2) HasForwarding() bool { // UnmarshalPacketData attempts to unmarshal the provided packet data bytes into a FungibleTokenPacketDataV2. // The version of ics20 should be provided and should be either ics20-1 or ics20-2. func UnmarshalPacketData(bz []byte, ics20Version string) (FungibleTokenPacketDataV2, error) { + // TODO: in transfer ibc module V2, we need to respect he encoding value passed via the payload, some hard coded assumptions about + // encoding exist here based on the ics20 version passed in. switch ics20Version { case V1: var datav1 FungibleTokenPacketData diff --git a/modules/apps/transfer/v2/ibc_module.go b/modules/apps/transfer/v2/ibc_module.go new file mode 100644 index 00000000000..fcbedc6ef03 --- /dev/null +++ b/modules/apps/transfer/v2/ibc_module.go @@ -0,0 +1,133 @@ +package v2 + +import ( + "context" + "fmt" + + errorsmod "cosmossdk.io/errors" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/cosmos/ibc-go/v9/modules/apps/transfer/internal/events" + transfertypes "github.com/cosmos/ibc-go/v9/modules/apps/transfer/types" + "github.com/cosmos/ibc-go/v9/modules/apps/transfer/v2/keeper" + channeltypes "github.com/cosmos/ibc-go/v9/modules/core/04-channel/types" + "github.com/cosmos/ibc-go/v9/modules/core/04-channel/v2/types" + "github.com/cosmos/ibc-go/v9/modules/core/api" + ibcerrors "github.com/cosmos/ibc-go/v9/modules/core/errors" +) + +var _ api.IBCModule = (*IBCModule)(nil) + +// NewIBCModule creates a new IBCModule given the keeper +func NewIBCModule(k *keeper.Keeper) *IBCModule { + return &IBCModule{ + keeper: k, + } +} + +type IBCModule struct { + keeper *keeper.Keeper +} + +func (im *IBCModule) OnSendPacket(goCtx context.Context, sourceChannel string, destinationChannel string, sequence uint64, payload types.Payload, signer sdk.AccAddress) error { + ctx := sdk.UnwrapSDKContext(goCtx) + + if !im.keeper.GetParams(ctx).SendEnabled { + return transfertypes.ErrSendDisabled + } + + if im.keeper.IsBlockedAddr(signer) { + return errorsmod.Wrapf(ibcerrors.ErrUnauthorized, "%s is not allowed to send funds", signer) + } + + data, err := transfertypes.UnmarshalPacketData(payload.Value, payload.Version) + if err != nil { + return err + } + + return im.keeper.OnSendPacket(ctx, sourceChannel, payload, data, signer) +} + +func (im *IBCModule) OnRecvPacket(ctx context.Context, sourceChannel string, destinationChannel string, sequence uint64, payload types.Payload, relayer sdk.AccAddress) types.RecvPacketResult { + var ( + ackErr error + data transfertypes.FungibleTokenPacketDataV2 + ) + + ack := channeltypes.NewResultAcknowledgement([]byte{byte(1)}) + recvResult := types.RecvPacketResult{ + Status: types.PacketStatus_Success, + Acknowledgement: ack.Acknowledgement(), + } + // we are explicitly wrapping this emit event call in an anonymous function so that + // the packet data is evaluated after it has been assigned a value. + defer func() { + events.EmitOnRecvPacketEvent(ctx, data, ack, ackErr) + }() + + data, ackErr = transfertypes.UnmarshalPacketData(payload.Value, payload.Version) + if ackErr != nil { + ack = channeltypes.NewErrorAcknowledgement(ackErr) + im.keeper.Logger(ctx).Error(fmt.Sprintf("%s sequence %d", ackErr.Error(), sequence)) + return types.RecvPacketResult{ + Status: types.PacketStatus_Failure, + Acknowledgement: ack.Acknowledgement(), + } + } + + if ackErr = im.keeper.OnRecvPacket(ctx, sourceChannel, destinationChannel, payload, data); ackErr != nil { + ack = channeltypes.NewErrorAcknowledgement(ackErr) + im.keeper.Logger(ctx).Error(fmt.Sprintf("%s sequence %d", ackErr.Error(), sequence)) + return types.RecvPacketResult{ + Status: types.PacketStatus_Failure, + Acknowledgement: ack.Acknowledgement(), + } + } + + im.keeper.Logger(ctx).Info("successfully handled ICS-20 packet", "sequence", sequence) + + if data.HasForwarding() { + // NOTE: acknowledgement will be written asynchronously + return types.RecvPacketResult{ + Status: types.PacketStatus_Async, + } + } + + // NOTE: acknowledgement will be written synchronously during IBC handler execution. + return recvResult +} + +func (im *IBCModule) OnTimeoutPacket(ctx context.Context, sourceChannel string, destinationChannel string, sequence uint64, payload types.Payload, relayer sdk.AccAddress) error { + data, err := transfertypes.UnmarshalPacketData(payload.Value, payload.Version) + if err != nil { + return err + } + + // refund tokens + if err := im.keeper.OnTimeoutPacket(ctx, payload.SourcePort, sourceChannel, data); err != nil { + return err + } + + events.EmitOnTimeoutEvent(ctx, data) + return nil +} + +func (im *IBCModule) OnAcknowledgementPacket(ctx context.Context, sourceChannel string, destinationChannel string, sequence uint64, acknowledgement []byte, payload types.Payload, relayer sdk.AccAddress) error { + var ack channeltypes.Acknowledgement + if err := transfertypes.ModuleCdc.UnmarshalJSON(acknowledgement, &ack); err != nil { + return errorsmod.Wrapf(ibcerrors.ErrUnknownRequest, "cannot unmarshal ICS-20 transfer packet acknowledgement: %v", err) + } + + data, err := transfertypes.UnmarshalPacketData(payload.Value, payload.Version) + if err != nil { + return err + } + + if err := im.keeper.OnAcknowledgementPacket(ctx, payload.SourcePort, sourceChannel, data, ack); err != nil { + return err + } + + events.EmitOnAcknowledgementPacketEvent(ctx, data, ack) + return nil +} diff --git a/modules/apps/transfer/v2/keeper/keeper.go b/modules/apps/transfer/v2/keeper/keeper.go new file mode 100644 index 00000000000..6cf535b0cac --- /dev/null +++ b/modules/apps/transfer/v2/keeper/keeper.go @@ -0,0 +1,249 @@ +package keeper + +import ( + "context" + "fmt" + + errorsmod "cosmossdk.io/errors" + sdkmath "cosmossdk.io/math" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/cosmos/ibc-go/v9/modules/apps/transfer/internal/events" + transferkeeper "github.com/cosmos/ibc-go/v9/modules/apps/transfer/keeper" + "github.com/cosmos/ibc-go/v9/modules/apps/transfer/types" + channeltypes "github.com/cosmos/ibc-go/v9/modules/core/04-channel/types" + channelkeeperv2 "github.com/cosmos/ibc-go/v9/modules/core/04-channel/v2/keeper" + channeltypesv2 "github.com/cosmos/ibc-go/v9/modules/core/04-channel/v2/types" + ibcerrors "github.com/cosmos/ibc-go/v9/modules/core/errors" +) + +type Keeper struct { + transferkeeper.Keeper + channelKeeperV2 *channelkeeperv2.Keeper +} + +func NewKeeper(transferKeeper transferkeeper.Keeper, channelKeeperV2 *channelkeeperv2.Keeper) *Keeper { + return &Keeper{ + Keeper: transferKeeper, + channelKeeperV2: channelKeeperV2, + } +} + +func (k *Keeper) OnSendPacket(ctx context.Context, sourceChannel string, payload channeltypesv2.Payload, data types.FungibleTokenPacketDataV2, sender sdk.AccAddress) error { + for _, token := range data.Tokens { + coin, err := token.ToCoin() + if err != nil { + return err + } + + if coin.Amount.Equal(types.UnboundedSpendLimit()) { + coin.Amount = k.BankKeeper.GetBalance(ctx, sender, coin.Denom).Amount + } + + // NOTE: SendTransfer simply sends the denomination as it exists on its own + // chain inside the packet data. The receiving chain will perform denom + // prefixing as necessary. + + // if the denom is prefixed by the port and channel on which we are sending + // the token, then we must be returning the token back to the chain they originated from + if token.Denom.HasPrefix(payload.SourcePort, sourceChannel) { + // transfer the coins to the module account and burn them + if err := k.BankKeeper.SendCoinsFromAccountToModule( + ctx, sender, types.ModuleName, sdk.NewCoins(coin), + ); err != nil { + return err + } + + if err := k.BankKeeper.BurnCoins( + ctx, types.ModuleName, sdk.NewCoins(coin), + ); err != nil { + // NOTE: should not happen as the module account was + // retrieved on the step above and it has enough balance + // to burn. + panic(fmt.Errorf("cannot burn coins after a successful send to a module account: %v", err)) + } + } else { + // obtain the escrow address for the source channel end + escrowAddress := types.GetEscrowAddress(payload.SourcePort, sourceChannel) + if err := k.EscrowCoin(ctx, sender, escrowAddress, coin); err != nil { + return err + } + } + } + + // TODO: events + // events.EmitTransferEvent(ctx, sender.String(), receiver, tokens, memo, hops) + + // TODO: telemetry + // telemetry.ReportTransfer(sourcePort, sourceChannel, destinationPort, destinationChannel, tokens) + + return nil +} + +func (k *Keeper) OnRecvPacket(ctx context.Context, sourceChannel, destChannel string, payload channeltypesv2.Payload, data types.FungibleTokenPacketDataV2) error { + // validate packet data upon receiving + if err := data.ValidateBasic(); err != nil { + return errorsmod.Wrapf(err, "error validating ICS-20 transfer packet data") + } + + if !k.GetParams(ctx).ReceiveEnabled { + return types.ErrReceiveDisabled + } + + receiver, err := sdk.AccAddressFromBech32(data.Receiver) + if err != nil { + return errorsmod.Wrapf(ibcerrors.ErrInvalidAddress, "failed to decode receiver address %s: %v", data.Receiver, err) + } + + if k.IsBlockedAddr(receiver) { + return errorsmod.Wrapf(ibcerrors.ErrUnauthorized, "%s is not allowed to receive funds", receiver) + } + + receivedCoins := make(sdk.Coins, 0, len(data.Tokens)) + for _, token := range data.Tokens { + // parse the transfer amount + transferAmount, ok := sdkmath.NewIntFromString(token.Amount) + if !ok { + return errorsmod.Wrapf(types.ErrInvalidAmount, "unable to parse transfer amount: %s", token.Amount) + } + + // This is the prefix that would have been prefixed to the denomination + // on sender chain IF and only if the token originally came from the + // receiving chain. + // + // NOTE: We use SourcePort and SourceChannel here, because the counterparty + // chain would have prefixed with DestPort and DestChannel when originally + // receiving this token. + if token.Denom.HasPrefix(payload.SourcePort, sourceChannel) { + // sender chain is not the source, unescrow tokens + + // remove prefix added by sender chain + token.Denom.Trace = token.Denom.Trace[1:] + + coin := sdk.NewCoin(token.Denom.IBCDenom(), transferAmount) + + escrowAddress := types.GetEscrowAddress(payload.DestinationPort, destChannel) + if err := k.UnescrowCoin(ctx, escrowAddress, receiver, coin); err != nil { + return err + } + + // Appending token. The new denom has been computed + receivedCoins = append(receivedCoins, coin) + } else { + // sender chain is the source, mint vouchers + + // since SendPacket did not prefix the denomination, we must add the destination port and channel to the trace + trace := []types.Hop{types.NewHop(payload.DestinationPort, destChannel)} + token.Denom.Trace = append(trace, token.Denom.Trace...) + + if !k.HasDenom(ctx, token.Denom.Hash()) { + k.SetDenom(ctx, token.Denom) + } + + voucherDenom := token.Denom.IBCDenom() + if !k.BankKeeper.HasDenomMetaData(ctx, voucherDenom) { + k.SetDenomMetadata(ctx, token.Denom) + } + + events.EmitDenomEvent(ctx, token) + + voucher := sdk.NewCoin(voucherDenom, transferAmount) + + // mint new tokens if the source of the transfer is the same chain + if err := k.BankKeeper.MintCoins( + ctx, types.ModuleName, sdk.NewCoins(voucher), + ); err != nil { + return errorsmod.Wrap(err, "failed to mint IBC tokens") + } + + // send to receiver + moduleAddr := k.AuthKeeper.GetModuleAddress(types.ModuleName) + if err := k.BankKeeper.SendCoins( + ctx, moduleAddr, receiver, sdk.NewCoins(voucher), + ); err != nil { + return errorsmod.Wrapf(err, "failed to send coins to receiver %s", receiver.String()) + } + + receivedCoins = append(receivedCoins, voucher) + } + } + + _ = receivedCoins // TODO: remove this line when forwarding is implemented + // TODO: forwarding + // if data.HasForwarding() { + // // we are now sending from the forward escrow address to the final receiver address. + // TODO: inside this version of the function, we should fetch the packet that was stored in IBC core in order to set it for forwarding. + // if err := k.forwardPacket(ctx, data, packet, receivedCoins); err != nil { + // return err + // } + // } + + // TODO: telemetry + // telemetry.ReportOnRecvPacket(packet, data.Tokens) + + // The ibc_module.go module will return the proper ack. + return nil +} + +func (k *Keeper) OnAcknowledgementPacket(ctx context.Context, sourcePort, sourceChannel string, data types.FungibleTokenPacketDataV2, ack channeltypes.Acknowledgement) error { + switch ack.Response.(type) { + case *channeltypes.Acknowledgement_Result: + // the acknowledgement succeeded on the receiving chain so nothing + // needs to be executed and no error needs to be returned + return nil + case *channeltypes.Acknowledgement_Error: + // We refund the tokens from the escrow address to the sender + return k.refundPacketTokens(ctx, sourcePort, sourceChannel, data) + default: + return errorsmod.Wrapf(ibcerrors.ErrInvalidType, "expected one of [%T, %T], got %T", channeltypes.Acknowledgement_Result{}, channeltypes.Acknowledgement_Error{}, ack.Response) + } +} + +func (k *Keeper) OnTimeoutPacket(ctx context.Context, sourcePort, sourceChannel string, data types.FungibleTokenPacketDataV2) error { + return k.refundPacketTokens(ctx, sourcePort, sourceChannel, data) +} + +func (k Keeper) refundPacketTokens(ctx context.Context, sourcePort, sourceChannel string, data types.FungibleTokenPacketDataV2) error { + // NOTE: packet data type already checked in handler.go + + sender, err := sdk.AccAddressFromBech32(data.Sender) + if err != nil { + return err + } + if k.IsBlockedAddr(sender) { + return errorsmod.Wrapf(ibcerrors.ErrUnauthorized, "%s is not allowed to receive funds", sender) + } + + // escrow address for unescrowing tokens back to sender + escrowAddress := types.GetEscrowAddress(sourcePort, sourceChannel) + + moduleAccountAddr := k.AuthKeeper.GetModuleAddress(types.ModuleName) + for _, token := range data.Tokens { + coin, err := token.ToCoin() + if err != nil { + return err + } + + // if the token we must refund is prefixed by the source port and channel + // then the tokens were burnt when the packet was sent and we must mint new tokens + if token.Denom.HasPrefix(sourcePort, sourceChannel) { + // mint vouchers back to sender + if err := k.BankKeeper.MintCoins( + ctx, types.ModuleName, sdk.NewCoins(coin), + ); err != nil { + return err + } + + if err := k.BankKeeper.SendCoins(ctx, moduleAccountAddr, sender, sdk.NewCoins(coin)); err != nil { + panic(fmt.Errorf("unable to send coins from module to account despite previously minting coins to module account: %v", err)) + } + } else { + if err := k.UnescrowCoin(ctx, escrowAddress, sender, coin); err != nil { + return err + } + } + } + + return nil +} diff --git a/modules/apps/transfer/v2/keeper/keeper_test.go b/modules/apps/transfer/v2/keeper/keeper_test.go new file mode 100644 index 00000000000..69836b14120 --- /dev/null +++ b/modules/apps/transfer/v2/keeper/keeper_test.go @@ -0,0 +1,58 @@ +package keeper_test + +import ( + "fmt" + "testing" + + testifysuite "github.com/stretchr/testify/suite" + + sdkmath "cosmossdk.io/math" + + sdk "github.com/cosmos/cosmos-sdk/types" + + ibctesting "github.com/cosmos/ibc-go/v9/testing" +) + +type KeeperTestSuite struct { + testifysuite.Suite + + coordinator *ibctesting.Coordinator + + // testing chains used for convenience and readability + chainA *ibctesting.TestChain + chainB *ibctesting.TestChain + chainC *ibctesting.TestChain +} + +func (suite *KeeperTestSuite) SetupTest() { + suite.coordinator = ibctesting.NewCoordinator(suite.T(), 3) + suite.chainA = suite.coordinator.GetChain(ibctesting.GetChainID(1)) + suite.chainB = suite.coordinator.GetChain(ibctesting.GetChainID(2)) + suite.chainC = suite.coordinator.GetChain(ibctesting.GetChainID(3)) +} + +type amountType int + +const ( + escrow amountType = iota + balance +) + +func (suite *KeeperTestSuite) assertAmountOnChain(chain *ibctesting.TestChain, balanceType amountType, amount sdkmath.Int, denom string) { + var total sdk.Coin + switch balanceType { + case escrow: + total = chain.GetSimApp().TransferKeeper.GetTotalEscrowForDenom(chain.GetContext(), denom) + totalV2 := chain.GetSimApp().TransferKeeperV2.GetTotalEscrowForDenom(chain.GetContext(), denom) + suite.Require().Equal(total, totalV2, "escrow balance mismatch") + case balance: + total = chain.GetSimApp().BankKeeper.GetBalance(chain.GetContext(), chain.SenderAccounts[0].SenderAccount.GetAddress(), denom) + default: + suite.Fail("invalid amountType %s", balanceType) + } + suite.Require().Equal(amount, total.Amount, fmt.Sprintf("Chain %s: got balance of %s, wanted %s", chain.Name(), total.Amount.String(), amount.String())) +} + +func TestKeeperTestSuite(t *testing.T) { + testifysuite.Run(t, new(KeeperTestSuite)) +} diff --git a/modules/apps/transfer/v2/keeper/msg_server_test.go b/modules/apps/transfer/v2/keeper/msg_server_test.go new file mode 100644 index 00000000000..9dcaba67e89 --- /dev/null +++ b/modules/apps/transfer/v2/keeper/msg_server_test.go @@ -0,0 +1,597 @@ +package keeper_test + +import ( + "bytes" + "time" + + sdkmath "cosmossdk.io/math" + + sdk "github.com/cosmos/cosmos-sdk/types" + + transfertypes "github.com/cosmos/ibc-go/v9/modules/apps/transfer/types" + clienttypes "github.com/cosmos/ibc-go/v9/modules/core/02-client/types" + channeltypes "github.com/cosmos/ibc-go/v9/modules/core/04-channel/types" + channeltypesv2 "github.com/cosmos/ibc-go/v9/modules/core/04-channel/v2/types" + commitmenttypes "github.com/cosmos/ibc-go/v9/modules/core/23-commitment/types" + ibctesting "github.com/cosmos/ibc-go/v9/testing" + mockv2 "github.com/cosmos/ibc-go/v9/testing/mock/v2" +) + +// TestMsgSendPacketTransfer tests the MsgSendPacket rpc handler for the transfer v2 application. +func (suite *KeeperTestSuite) TestMsgSendPacketTransfer() { + var payload channeltypesv2.Payload + var path *ibctesting.Path + + testCases := []struct { + name string + malleate func() + expError error + }{ + { + "success", + func() {}, + nil, + }, + { + "failure: send transfers disabled", + func() { + suite.chainA.GetSimApp().TransferKeeperV2.SetParams(suite.chainA.GetContext(), + transfertypes.Params{ + SendEnabled: false, + }, + ) + }, + transfertypes.ErrSendDisabled, + }, + } + + for _, tc := range testCases { + tc := tc + + suite.Run(tc.name, func() { + suite.SetupTest() + + path = ibctesting.NewPath(suite.chainA, suite.chainB) + path.SetupV2() + + tokens := []transfertypes.Token{ + { + Denom: transfertypes.Denom{ + Base: sdk.DefaultBondDenom, + Trace: nil, + }, + Amount: ibctesting.DefaultCoinAmount.String(), + }, + } + + ftpd := transfertypes.NewFungibleTokenPacketDataV2(tokens, suite.chainA.SenderAccount.GetAddress().String(), suite.chainB.SenderAccount.GetAddress().String(), "", transfertypes.ForwardingPacketData{}) + bz := suite.chainA.Codec.MustMarshal(&ftpd) + + timestamp := suite.chainA.GetTimeoutTimestampSecs() + // TODO: note, encoding field currently not respected in the implementation. encoding is determined by the version. + // ics20-v1 == json + // ics20-v2 == proto + payload = channeltypesv2.NewPayload(transfertypes.ModuleName, transfertypes.ModuleName, transfertypes.V2, "json", bz) + + tc.malleate() + + packet, err := path.EndpointA.MsgSendPacket(timestamp, payload) + + expPass := tc.expError == nil + if expPass { + + // ensure every token sent is escrowed. + for _, t := range tokens { + escrowedAmount := suite.chainA.GetSimApp().TransferKeeperV2.GetTotalEscrowForDenom(suite.chainA.GetContext(), t.Denom.IBCDenom()) + expected, err := t.ToCoin() + suite.Require().NoError(err) + suite.Require().Equal(expected, escrowedAmount, "escrowed amount is not equal to expected amount") + } + suite.Require().NoError(err) + suite.Require().NotEmpty(packet) + } else { + ibctesting.RequireErrorIsOrContains(suite.T(), err, tc.expError, "expected error %q but got %q", tc.expError, err) + suite.Require().Empty(packet) + } + }) + } +} + +// TestMsgRecvPacketTransfer tests the MsgRecvPacket rpc handler for the transfer v2 application. +func (suite *KeeperTestSuite) TestMsgRecvPacketTransfer() { + var ( + path *ibctesting.Path + packet channeltypesv2.Packet + expectedAck channeltypesv2.Acknowledgement + ) + + testCases := []struct { + name string + malleate func() + expError error + }{ + { + "success", + func() {}, + nil, + }, + { + "failure: invalid destination channel on received packet", + func() { + packet.DestinationChannel = ibctesting.InvalidID + }, + channeltypesv2.ErrChannelNotFound, + }, + { + "failure: counter party channel does not match source channel", + func() { + packet.SourceChannel = ibctesting.InvalidID + }, + channeltypes.ErrInvalidChannelIdentifier, + }, + { + "failure: receive is disabled", + func() { + expectedAck.AppAcknowledgements[0] = channeltypes.NewErrorAcknowledgement(transfertypes.ErrReceiveDisabled).Acknowledgement() + suite.chainB.GetSimApp().TransferKeeperV2.SetParams(suite.chainB.GetContext(), + transfertypes.Params{ + ReceiveEnabled: false, + }) + }, + nil, + }, + // TODO: async tests + } + + for _, tc := range testCases { + tc := tc + + suite.Run(tc.name, func() { + suite.SetupTest() + + path = ibctesting.NewPath(suite.chainA, suite.chainB) + path.SetupV2() + + tokens := []transfertypes.Token{ + { + Denom: transfertypes.Denom{ + Base: sdk.DefaultBondDenom, + Trace: nil, + }, + Amount: ibctesting.DefaultCoinAmount.String(), + }, + } + + ftpd := transfertypes.NewFungibleTokenPacketDataV2(tokens, suite.chainA.SenderAccount.GetAddress().String(), suite.chainB.SenderAccount.GetAddress().String(), "", transfertypes.ForwardingPacketData{}) + bz := suite.chainA.Codec.MustMarshal(&ftpd) + + timestamp := suite.chainA.GetTimeoutTimestampSecs() + payload := channeltypesv2.NewPayload(transfertypes.ModuleName, transfertypes.ModuleName, transfertypes.V2, "json", bz) + var err error + packet, err = path.EndpointA.MsgSendPacket(timestamp, payload) + suite.Require().NoError(err) + + // by default, we assume a successful acknowledgement will be written. + ackBytes := channeltypes.NewResultAcknowledgement([]byte{byte(1)}).Acknowledgement() + expectedAck = channeltypesv2.Acknowledgement{AppAcknowledgements: [][]byte{ackBytes}} + tc.malleate() + + err = path.EndpointB.MsgRecvPacket(packet) + + expPass := tc.expError == nil + if expPass { + suite.Require().NoError(err) + + actualAckHash := suite.chainB.GetSimApp().IBCKeeper.ChannelKeeperV2.GetPacketAcknowledgement(suite.chainB.GetContext(), packet.DestinationChannel, packet.Sequence) + expectedHash := channeltypesv2.CommitAcknowledgement(expectedAck) + + suite.Require().Equal(expectedHash, actualAckHash) + + denom := transfertypes.Denom{ + Base: sdk.DefaultBondDenom, + Trace: []transfertypes.Hop{ + transfertypes.NewHop(payload.DestinationPort, packet.DestinationChannel), + }, + } + + actualBalance := path.EndpointB.Chain.GetSimApp().TransferKeeperV2.BankKeeper.GetBalance(suite.chainB.GetContext(), suite.chainB.SenderAccount.GetAddress(), denom.IBCDenom()) + + var expectedBalance sdk.Coin + // on a successful ack we expect the full amount to be transferred + if bytes.Equal(expectedAck.AppAcknowledgements[0], ackBytes) { + expectedBalance = sdk.NewCoin(denom.IBCDenom(), ibctesting.DefaultCoinAmount) + } else { + // otherwise the tokens do not make it to the address. + expectedBalance = sdk.NewCoin(denom.IBCDenom(), sdkmath.NewInt(0)) + } + + suite.Require().Equal(expectedBalance.Amount, actualBalance.Amount) + + } else { + ibctesting.RequireErrorIsOrContains(suite.T(), err, tc.expError, "expected error %q but got %q", tc.expError, err) + } + }) + } +} + +// TestMsgAckPacketTransfer tests the MsgAcknowledgePacket rpc handler for the transfer v2 application. +func (suite *KeeperTestSuite) TestMsgAckPacketTransfer() { + var ( + path *ibctesting.Path + packet channeltypesv2.Packet + expectedAck channeltypesv2.Acknowledgement + ) + + testCases := []struct { + name string + malleate func() + expError error + causeFailureOnRecv bool + }{ + { + "success", + func() {}, + nil, + false, + }, + { + "failure: proof verification failure", + func() { + expectedAck.AppAcknowledgements[0] = mockv2.MockFailRecvPacketResult.Acknowledgement + }, + commitmenttypes.ErrInvalidProof, + false, + }, + { + "failure: escrowed tokens are refunded", + func() { + expectedAck.AppAcknowledgements[0] = channeltypes.NewErrorAcknowledgement(transfertypes.ErrReceiveDisabled).Acknowledgement() + }, + nil, + true, + }, + } + + for _, tc := range testCases { + tc := tc + + suite.Run(tc.name, func() { + suite.SetupTest() + + path = ibctesting.NewPath(suite.chainA, suite.chainB) + path.SetupV2() + + tokens := []transfertypes.Token{ + { + Denom: transfertypes.Denom{ + Base: sdk.DefaultBondDenom, + Trace: nil, + }, + Amount: ibctesting.DefaultCoinAmount.String(), + }, + } + + ftpd := transfertypes.NewFungibleTokenPacketDataV2(tokens, suite.chainA.SenderAccount.GetAddress().String(), suite.chainB.SenderAccount.GetAddress().String(), "", transfertypes.ForwardingPacketData{}) + bz := suite.chainA.Codec.MustMarshal(&ftpd) + + timestamp := suite.chainA.GetTimeoutTimestampSecs() + payload := channeltypesv2.NewPayload(transfertypes.ModuleName, transfertypes.ModuleName, transfertypes.V2, "json", bz) + + var err error + packet, err = path.EndpointA.MsgSendPacket(timestamp, payload) + suite.Require().NoError(err) + + if tc.causeFailureOnRecv { + // ensure that the recv packet fails at the application level, but succeeds at the IBC handler level + // this will ensure that a failed ack will be written to state. + suite.chainB.GetSimApp().TransferKeeperV2.SetParams(suite.chainB.GetContext(), + transfertypes.Params{ + ReceiveEnabled: false, + }) + } + + err = path.EndpointB.MsgRecvPacket(packet) + suite.Require().NoError(err) + + ackBytes := channeltypes.NewResultAcknowledgement([]byte{byte(1)}).Acknowledgement() + expectedAck = channeltypesv2.Acknowledgement{AppAcknowledgements: [][]byte{ackBytes}} + tc.malleate() + + err = path.EndpointA.MsgAcknowledgePacket(packet, expectedAck) + + expPass := tc.expError == nil + if expPass { + suite.Require().NoError(err) + + if bytes.Equal(expectedAck.AppAcknowledgements[0], ackBytes) { + // tokens remain escrowed + for _, t := range tokens { + escrowedAmount := suite.chainA.GetSimApp().TransferKeeperV2.GetTotalEscrowForDenom(suite.chainA.GetContext(), t.Denom.IBCDenom()) + expected, err := t.ToCoin() + suite.Require().NoError(err) + suite.Require().Equal(expected, escrowedAmount, "escrowed amount is not equal to expected amount") + } + } else { + // tokens have been unescrowed + for _, t := range tokens { + escrowedAmount := suite.chainA.GetSimApp().TransferKeeperV2.GetTotalEscrowForDenom(suite.chainA.GetContext(), t.Denom.IBCDenom()) + suite.Require().Equal(sdk.NewCoin(t.Denom.IBCDenom(), sdkmath.NewInt(0)), escrowedAmount, "escrowed amount is not equal to expected amount") + } + } + } else { + ibctesting.RequireErrorIsOrContains(suite.T(), err, tc.expError, "expected error %q but got %q", tc.expError, err) + } + }) + } +} + +// TestMsgTimeoutPacketTransfer tests the MsgTimeoutPacket rpc handler for the transfer v2 application. +func (suite *KeeperTestSuite) TestMsgTimeoutPacketTransfer() { + var ( + path *ibctesting.Path + packet channeltypesv2.Packet + timeoutTimestamp uint64 + ) + + testCases := []struct { + name string + malleate func() + timeoutPacket bool + expError error + }{ + { + "success", + func() {}, + true, + nil, + }, + { + "failure: packet has not timed out", + func() {}, + false, + channeltypes.ErrTimeoutNotReached, + }, + } + + for _, tc := range testCases { + tc := tc + + suite.Run(tc.name, func() { + suite.SetupTest() + + path = ibctesting.NewPath(suite.chainA, suite.chainB) + path.SetupV2() + + tokens := []transfertypes.Token{ + { + Denom: transfertypes.Denom{ + Base: sdk.DefaultBondDenom, + Trace: nil, + }, + Amount: ibctesting.DefaultCoinAmount.String(), + }, + } + + ftpd := transfertypes.NewFungibleTokenPacketDataV2(tokens, suite.chainA.SenderAccount.GetAddress().String(), suite.chainB.SenderAccount.GetAddress().String(), "", transfertypes.ForwardingPacketData{}) + bz := suite.chainA.Codec.MustMarshal(&ftpd) + + timeoutTimestamp = uint64(suite.chainA.GetContext().BlockTime().Unix()) + uint64(time.Hour.Seconds()) + payload := channeltypesv2.NewPayload(transfertypes.ModuleName, transfertypes.ModuleName, transfertypes.V2, "json", bz) + + var err error + packet, err = path.EndpointA.MsgSendPacket(timeoutTimestamp, payload) + suite.Require().NoError(err) + + if tc.timeoutPacket { + suite.coordinator.IncrementTimeBy(time.Hour * 2) + } + + // ensure that chainA has an update to date client of chain B. + suite.Require().NoError(path.EndpointA.UpdateClient()) + + tc.malleate() + + err = path.EndpointA.MsgTimeoutPacket(packet) + + expPass := tc.expError == nil + if expPass { + suite.Require().NoError(err) + + // ensure funds are un-escrowed + for _, t := range tokens { + escrowedAmount := suite.chainA.GetSimApp().TransferKeeperV2.GetTotalEscrowForDenom(suite.chainA.GetContext(), t.Denom.IBCDenom()) + suite.Require().Equal(sdk.NewCoin(t.Denom.IBCDenom(), sdkmath.NewInt(0)), escrowedAmount, "escrowed amount is not equal to expected amount") + } + + } else { + ibctesting.RequireErrorIsOrContains(suite.T(), err, tc.expError, "expected error %q but got %q", tc.expError, err) + // tokens remain escrowed if there is a timeout failure + for _, t := range tokens { + escrowedAmount := suite.chainA.GetSimApp().TransferKeeperV2.GetTotalEscrowForDenom(suite.chainA.GetContext(), t.Denom.IBCDenom()) + expected, err := t.ToCoin() + suite.Require().NoError(err) + suite.Require().Equal(expected, escrowedAmount, "escrowed amount is not equal to expected amount") + } + } + }) + } +} + +func (suite *KeeperTestSuite) TestV2RetainsFungibility() { + suite.SetupTest() + + path := ibctesting.NewTransferPath(suite.chainA, suite.chainB) + path.Setup() + + pathv2 := ibctesting.NewPath(suite.chainB, suite.chainC) + pathv2.SetupV2() + + denomA := transfertypes.Denom{ + Base: sdk.DefaultBondDenom, + } + + denomAtoB := transfertypes.Denom{ + Base: sdk.DefaultBondDenom, + Trace: []transfertypes.Hop{ + transfertypes.NewHop(path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID), + }, + } + + denomBtoC := transfertypes.Denom{ + Base: sdk.DefaultBondDenom, + Trace: []transfertypes.Hop{ + transfertypes.NewHop(transfertypes.ModuleName, pathv2.EndpointB.ChannelID), + transfertypes.NewHop(path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID), + }, + } + + ackBytes := channeltypes.NewResultAcknowledgement([]byte{byte(1)}).Acknowledgement() + successfulAck := channeltypesv2.Acknowledgement{AppAcknowledgements: [][]byte{ackBytes}} + + originalAmount, ok := sdkmath.NewIntFromString(ibctesting.DefaultGenesisAccBalance) + suite.Require().True(ok) + + suite.Run("between A and B", func() { + var packet channeltypes.Packet + suite.Run("transfer packet", func() { + transferMsg := transfertypes.NewMsgTransfer( + path.EndpointA.ChannelConfig.PortID, + path.EndpointA.ChannelID, + sdk.NewCoins(sdk.NewCoin(denomA.IBCDenom(), ibctesting.TestCoin.Amount)), + suite.chainA.SenderAccount.GetAddress().String(), + suite.chainB.SenderAccount.GetAddress().String(), + clienttypes.ZeroHeight(), + suite.chainA.GetTimeoutTimestamp(), + "memo", + nil, + ) + + result, err := suite.chainA.SendMsgs(transferMsg) + suite.Require().NoError(err) // message committed + + remainingAmount := originalAmount.Sub(ibctesting.DefaultCoinAmount) + suite.assertAmountOnChain(suite.chainA, balance, remainingAmount, denomA.IBCDenom()) + + packet, err = ibctesting.ParsePacketFromEvents(result.Events) + suite.Require().NoError(err) + }) + + suite.Run("recv and ack packet", func() { + err := path.RelayPacket(packet) + suite.Require().NoError(err) + }) + }) + + suite.Run("between B and C", func() { + var packetV2 channeltypesv2.Packet + + suite.Run("send packet", func() { + tokens := []transfertypes.Token{ + { + Denom: denomAtoB, + Amount: ibctesting.DefaultCoinAmount.String(), + }, + } + + ftpd := transfertypes.NewFungibleTokenPacketDataV2(tokens, suite.chainB.SenderAccount.GetAddress().String(), suite.chainC.SenderAccount.GetAddress().String(), "", transfertypes.ForwardingPacketData{}) + bz := suite.chainB.Codec.MustMarshal(&ftpd) + + timeoutTimestamp := uint64(suite.chainB.GetContext().BlockTime().Unix()) + uint64(time.Hour.Seconds()) + payload := channeltypesv2.NewPayload(transfertypes.ModuleName, transfertypes.ModuleName, transfertypes.V2, "json", bz) + + var err error + packetV2, err = pathv2.EndpointA.MsgSendPacket(timeoutTimestamp, payload) + suite.Require().NoError(err) + // the escrow account on chain B should have escrowed the tokens after sending from B to C + suite.assertAmountOnChain(suite.chainB, escrow, ibctesting.DefaultCoinAmount, denomAtoB.IBCDenom()) + }) + + suite.Run("recv packet", func() { + err := pathv2.EndpointB.MsgRecvPacket(packetV2) + suite.Require().NoError(err) + + // the receiving chain should have received the tokens + suite.assertAmountOnChain(suite.chainC, balance, ibctesting.DefaultCoinAmount, denomBtoC.IBCDenom()) + }) + + suite.Run("ack packet", func() { + err := pathv2.EndpointA.MsgAcknowledgePacket(packetV2, successfulAck) + suite.Require().NoError(err) + }) + }) + + suite.Run("between C and B", func() { + var packetV2 channeltypesv2.Packet + + suite.Run("send packet", func() { + // send from C to B + tokens := []transfertypes.Token{ + { + Denom: denomBtoC, + Amount: ibctesting.DefaultCoinAmount.String(), + }, + } + + ftpd := transfertypes.NewFungibleTokenPacketDataV2(tokens, suite.chainC.SenderAccount.GetAddress().String(), suite.chainB.SenderAccount.GetAddress().String(), "", transfertypes.ForwardingPacketData{}) + bz := suite.chainC.Codec.MustMarshal(&ftpd) + + timeoutTimestamp := uint64(suite.chainC.GetContext().BlockTime().Unix()) + uint64(time.Hour.Seconds()) + payload := channeltypesv2.NewPayload(transfertypes.ModuleName, transfertypes.ModuleName, transfertypes.V2, "json", bz) + + var err error + packetV2, err = pathv2.EndpointB.MsgSendPacket(timeoutTimestamp, payload) + suite.Require().NoError(err) + + // tokens have been sent from chain C, and the balance is now empty. + suite.assertAmountOnChain(suite.chainC, balance, sdkmath.NewInt(0), denomBtoC.IBCDenom()) + }) + + suite.Run("recv packet", func() { + err := pathv2.EndpointA.MsgRecvPacket(packetV2) + suite.Require().NoError(err) + + // chain B should have received the tokens from chain C. + suite.assertAmountOnChain(suite.chainB, balance, ibctesting.DefaultCoinAmount, denomAtoB.IBCDenom()) + }) + + suite.Run("ack packet", func() { + err := pathv2.EndpointB.MsgAcknowledgePacket(packetV2, successfulAck) + suite.Require().NoError(err) + }) + }) + + suite.Run("between B and A", func() { + var packet channeltypes.Packet + + suite.Run("transfer packet", func() { + // send from B to A using MsgTransfer + transferMsg := transfertypes.NewMsgTransfer( + path.EndpointB.ChannelConfig.PortID, + path.EndpointB.ChannelID, + sdk.NewCoins(sdk.NewCoin(denomAtoB.IBCDenom(), ibctesting.TestCoin.Amount)), + suite.chainB.SenderAccount.GetAddress().String(), + suite.chainA.SenderAccount.GetAddress().String(), + clienttypes.ZeroHeight(), + suite.chainB.GetTimeoutTimestamp(), + "memo", + nil, + ) + + result, err := suite.chainB.SendMsgs(transferMsg) + suite.Require().NoError(err) // message committed + + suite.assertAmountOnChain(suite.chainB, balance, sdkmath.NewInt(0), denomAtoB.IBCDenom()) + + packet, err = ibctesting.ParsePacketFromEvents(result.Events) + suite.Require().NoError(err) + }) + suite.Run("recv and ack packet", func() { + // in order to recv in the other direction, we create a new path and recv + // on that with the endpoints reversed. + err := path.Reversed().RelayPacket(packet) + suite.Require().NoError(err) + + suite.assertAmountOnChain(suite.chainA, balance, originalAmount, denomA.IBCDenom()) + }) + }) +} diff --git a/modules/core/04-channel/v2/keeper/msg_server.go b/modules/core/04-channel/v2/keeper/msg_server.go index ba0fd05e17c..f4c2dc446a1 100644 --- a/modules/core/04-channel/v2/keeper/msg_server.go +++ b/modules/core/04-channel/v2/keeper/msg_server.go @@ -61,10 +61,9 @@ func (k *Keeper) RegisterCounterparty(goCtx context.Context, msg *types.MsgRegis return &types.MsgRegisterCounterpartyResponse{}, nil } -// SendPacket defines a rpc handler method for MsgSendPacket. +// SendPacket implements the PacketMsgServer SendPacket method. func (k *Keeper) SendPacket(ctx context.Context, msg *types.MsgSendPacket) (*types.MsgSendPacketResponse, error) { sdkCtx := sdk.UnwrapSDKContext(ctx) - sequence, destChannel, err := k.sendPacket(ctx, msg.SourceChannel, msg.TimeoutTimestamp, msg.Payloads) if err != nil { sdkCtx.Logger().Error("send packet failed", "source-channel", msg.SourceChannel, "error", errorsmod.Wrap(err, "send packet failed")) @@ -100,7 +99,8 @@ func (k *Keeper) SendPacket(ctx context.Context, msg *types.MsgSendPacket) (*typ return &types.MsgSendPacketResponse{Sequence: sequence}, nil } -// RecvPacket defines a rpc handler method for MsgRecvPacket. +// RecvPacket implements the PacketMsgServer RecvPacket method. + func (k *Keeper) RecvPacket(ctx context.Context, msg *types.MsgRecvPacket) (*types.MsgRecvPacketResponse, error) { sdkCtx := sdk.UnwrapSDKContext(ctx) @@ -139,7 +139,7 @@ func (k *Keeper) RecvPacket(ctx context.Context, msg *types.MsgRecvPacket) (*typ // Cache context so that we may discard state changes from callback if the acknowledgement is unsuccessful. cacheCtx, writeFn = sdkCtx.CacheContext() cb := k.Router.Route(pd.DestinationPort) - res := cb.OnRecvPacket(cacheCtx, msg.Packet.SourceChannel, msg.Packet.DestinationChannel, pd, signer) + res := cb.OnRecvPacket(cacheCtx, msg.Packet.SourceChannel, msg.Packet.DestinationChannel, msg.Packet.Sequence, pd, signer) if res.Status != types.PacketStatus_Failure { // write application state changes for asynchronous and successful acknowledgements @@ -178,6 +178,7 @@ func (k *Keeper) RecvPacket(ctx context.Context, msg *types.MsgRecvPacket) (*typ } } + // TODO: store the packet for async applications to access if required. defer telemetry.ReportRecvPacket(msg.Packet) sdkCtx.Logger().Info("receive packet callback succeeded", "source-channel", msg.Packet.SourceChannel, "dest-channel", msg.Packet.DestinationChannel, "result", types.SUCCESS.String()) @@ -211,7 +212,7 @@ func (k *Keeper) Acknowledgement(ctx context.Context, msg *types.MsgAcknowledgem for i, pd := range msg.Packet.Payloads { cbs := k.Router.Route(pd.SourcePort) ack := msg.Acknowledgement.AppAcknowledgements[i] - err := cbs.OnAcknowledgementPacket(ctx, msg.Packet.SourceChannel, msg.Packet.DestinationChannel, pd, ack, relayer) + err := cbs.OnAcknowledgementPacket(ctx, msg.Packet.SourceChannel, msg.Packet.DestinationChannel, msg.Packet.Sequence, ack, pd, relayer) if err != nil { return nil, errorsmod.Wrapf(err, "failed OnAcknowledgementPacket for source port %s, source channel %s, destination channel %s", pd.SourcePort, msg.Packet.SourceChannel, msg.Packet.DestinationChannel) } @@ -220,7 +221,7 @@ func (k *Keeper) Acknowledgement(ctx context.Context, msg *types.MsgAcknowledgem return &types.MsgAcknowledgementResponse{Result: types.SUCCESS}, nil } -// Timeout defines a rpc handler method for MsgTimeout. +// Timeout implements the PacketMsgServer Timeout method. func (k *Keeper) Timeout(ctx context.Context, timeout *types.MsgTimeout) (*types.MsgTimeoutResponse, error) { sdkCtx := sdk.UnwrapSDKContext(ctx) @@ -250,7 +251,7 @@ func (k *Keeper) Timeout(ctx context.Context, timeout *types.MsgTimeout) (*types for _, pd := range timeout.Packet.Payloads { cbs := k.Router.Route(pd.SourcePort) - err := cbs.OnTimeoutPacket(ctx, timeout.Packet.SourceChannel, timeout.Packet.DestinationChannel, pd, signer) + err := cbs.OnTimeoutPacket(ctx, timeout.Packet.SourceChannel, timeout.Packet.DestinationChannel, timeout.Packet.Sequence, pd, signer) if err != nil { return nil, errorsmod.Wrapf(err, "failed OnTimeoutPacket for source port %s, source channel %s, destination channel %s", pd.SourcePort, timeout.Packet.SourceChannel, timeout.Packet.DestinationChannel) } diff --git a/modules/core/04-channel/v2/keeper/msg_server_test.go b/modules/core/04-channel/v2/keeper/msg_server_test.go index 339c0f3d92f..072de3e77fb 100644 --- a/modules/core/04-channel/v2/keeper/msg_server_test.go +++ b/modules/core/04-channel/v2/keeper/msg_server_test.go @@ -316,7 +316,7 @@ func (suite *KeeperTestSuite) TestMsgRecvPacket() { expectedAck := types.Acknowledgement{AppAcknowledgements: [][]byte{expRecvRes.Acknowledgement}} // modify the callback to return the expected recv result. - path.EndpointB.Chain.GetSimApp().MockModuleV2B.IBCApp.OnRecvPacket = func(ctx context.Context, sourceChannel string, destinationChannel string, data types.Payload, relayer sdk.AccAddress) types.RecvPacketResult { + path.EndpointB.Chain.GetSimApp().MockModuleV2B.IBCApp.OnRecvPacket = func(ctx context.Context, sourceChannel string, destinationChannel string, sequence uint64, data types.Payload, relayer sdk.AccAddress) types.RecvPacketResult { return expRecvRes } @@ -377,7 +377,7 @@ func (suite *KeeperTestSuite) TestMsgAcknowledgement() { // Modify the callback to return an error. // This way, we can verify that the callback is not executed in a No-op case. - path.EndpointA.Chain.GetSimApp().MockModuleV2A.IBCApp.OnAcknowledgementPacket = func(context.Context, string, string, types.Payload, []byte, sdk.AccAddress) error { + path.EndpointA.Chain.GetSimApp().MockModuleV2A.IBCApp.OnAcknowledgementPacket = func(context.Context, string, string, uint64, types.Payload, []byte, sdk.AccAddress) error { return mock.MockApplicationCallbackError } }, @@ -385,7 +385,7 @@ func (suite *KeeperTestSuite) TestMsgAcknowledgement() { { name: "failure: callback fails", malleate: func() { - path.EndpointA.Chain.GetSimApp().MockModuleV2A.IBCApp.OnAcknowledgementPacket = func(context.Context, string, string, types.Payload, []byte, sdk.AccAddress) error { + path.EndpointA.Chain.GetSimApp().MockModuleV2A.IBCApp.OnAcknowledgementPacket = func(context.Context, string, string, uint64, types.Payload, []byte, sdk.AccAddress) error { return mock.MockApplicationCallbackError } }, @@ -471,7 +471,7 @@ func (suite *KeeperTestSuite) TestMsgTimeout() { // Modify the callback to return a different error. // This way, we can verify that the callback is not executed in a No-op case. - path.EndpointA.Chain.GetSimApp().MockModuleV2A.IBCApp.OnTimeoutPacket = func(context.Context, string, string, types.Payload, sdk.AccAddress) error { + path.EndpointA.Chain.GetSimApp().MockModuleV2A.IBCApp.OnTimeoutPacket = func(context.Context, string, string, uint64, types.Payload, sdk.AccAddress) error { return mock.MockApplicationCallbackError } }, @@ -480,7 +480,7 @@ func (suite *KeeperTestSuite) TestMsgTimeout() { { name: "failure: callback fails", malleate: func() { - path.EndpointA.Chain.GetSimApp().MockModuleV2A.IBCApp.OnTimeoutPacket = func(context.Context, string, string, types.Payload, sdk.AccAddress) error { + path.EndpointA.Chain.GetSimApp().MockModuleV2A.IBCApp.OnTimeoutPacket = func(context.Context, string, string, uint64, types.Payload, sdk.AccAddress) error { return mock.MockApplicationCallbackError } }, diff --git a/modules/core/04-channel/v2/keeper/packet.go b/modules/core/04-channel/v2/keeper/packet.go index d7e40d07fc2..967b87445f2 100644 --- a/modules/core/04-channel/v2/keeper/packet.go +++ b/modules/core/04-channel/v2/keeper/packet.go @@ -161,6 +161,7 @@ func (k *Keeper) recvPacket( } // WriteAcknowledgement writes the acknowledgement to the store. +// TODO: change this function to accept destPort, destChannel, sequence, ack func (k Keeper) WriteAcknowledgement( ctx context.Context, packet types.Packet, @@ -191,6 +192,7 @@ func (k Keeper) WriteAcknowledgement( // TODO: Validate Acknowledgment more thoroughly here after Issue #7472: https://github.com/cosmos/ibc-go/issues/7472 + // TODO: remove this check, maybe pull it up to the handler. if len(ack.AppAcknowledgements) != len(packet.Payloads) { return errorsmod.Wrapf(types.ErrInvalidAcknowledgement, "length of app acknowledgement %d does not match length of app payload %d", len(ack.AppAcknowledgements), len(packet.Payloads)) } @@ -203,7 +205,10 @@ func (k Keeper) WriteAcknowledgement( k.Logger(ctx).Info("acknowledgement written", "sequence", strconv.FormatUint(packet.Sequence, 10), "dest-channel", packet.DestinationChannel) - EmitWriteAcknowledgementEvents(ctx, packet, ack) + // TODO: decide how relayers will reconstruct the packet as it is not being passed. + // EmitWriteAcknowledgementEvents(ctx, packet, ack) + + // TODO: delete the packet that has been stored in ibc-core. return nil } diff --git a/modules/core/api/module.go b/modules/core/api/module.go index 1f86bfc5386..4d26947f42a 100644 --- a/modules/core/api/module.go +++ b/modules/core/api/module.go @@ -19,7 +19,7 @@ type IBCModule interface { sourceChannel string, destinationChannel string, sequence uint64, - data channeltypesv2.Payload, + payload channeltypesv2.Payload, signer sdk.AccAddress, ) error @@ -27,7 +27,8 @@ type IBCModule interface { ctx context.Context, sourceChannel string, destinationChannel string, - data channeltypesv2.Payload, + sequence uint64, + payload channeltypesv2.Payload, relayer sdk.AccAddress, ) channeltypesv2.RecvPacketResult @@ -36,7 +37,8 @@ type IBCModule interface { ctx context.Context, sourceChannel string, destinationChannel string, - data channeltypesv2.Payload, + sequence uint64, + payload channeltypesv2.Payload, relayer sdk.AccAddress, ) error @@ -45,8 +47,9 @@ type IBCModule interface { ctx context.Context, sourceChannel string, destinationChannel string, - data channeltypesv2.Payload, + sequence uint64, acknowledgement []byte, + payload channeltypesv2.Payload, relayer sdk.AccAddress, ) error } diff --git a/modules/light-clients/08-wasm/testing/simapp/app.go b/modules/light-clients/08-wasm/testing/simapp/app.go index e9d74cc88e2..8c5280db225 100644 --- a/modules/light-clients/08-wasm/testing/simapp/app.go +++ b/modules/light-clients/08-wasm/testing/simapp/app.go @@ -123,10 +123,13 @@ import ( "github.com/cosmos/ibc-go/v9/modules/apps/transfer" ibctransferkeeper "github.com/cosmos/ibc-go/v9/modules/apps/transfer/keeper" ibctransfertypes "github.com/cosmos/ibc-go/v9/modules/apps/transfer/types" + transferv2 "github.com/cosmos/ibc-go/v9/modules/apps/transfer/v2" + ibctransferkeeperv2 "github.com/cosmos/ibc-go/v9/modules/apps/transfer/v2/keeper" ibc "github.com/cosmos/ibc-go/v9/modules/core" ibcclienttypes "github.com/cosmos/ibc-go/v9/modules/core/02-client/types" ibcconnectiontypes "github.com/cosmos/ibc-go/v9/modules/core/03-connection/types" porttypes "github.com/cosmos/ibc-go/v9/modules/core/05-port/types" + ibcapi "github.com/cosmos/ibc-go/v9/modules/core/api" ibcexported "github.com/cosmos/ibc-go/v9/modules/core/exported" ibckeeper "github.com/cosmos/ibc-go/v9/modules/core/keeper" solomachine "github.com/cosmos/ibc-go/v9/modules/light-clients/06-solomachine" @@ -198,6 +201,7 @@ type SimApp struct { ICAHostKeeper icahostkeeper.Keeper EvidenceKeeper evidencekeeper.Keeper TransferKeeper ibctransferkeeper.Keeper + TransferKeeperV2 *ibctransferkeeperv2.Keeper WasmClientKeeper wasmkeeper.Keeper FeeGrantKeeper feegrantkeeper.Keeper GroupKeeper groupkeeper.Keeper @@ -477,6 +481,7 @@ func NewSimApp( // Create IBC Router ibcRouter := porttypes.NewRouter() + ibcRouterV2 := ibcapi.NewRouter() // Middleware Stacks @@ -563,8 +568,13 @@ func NewSimApp( feeWithMockModule := ibcfee.NewIBCMiddleware(feeMockModule, app.IBCFeeKeeper) ibcRouter.AddRoute(MockFeePort, feeWithMockModule) - // Seal the IBC Router + // register the transfer v2 module. + app.TransferKeeperV2 = ibctransferkeeperv2.NewKeeper(app.TransferKeeper, app.IBCKeeper.ChannelKeeperV2) + ibcRouterV2.AddRoute(ibctransfertypes.ModuleName, transferv2.NewIBCModule(app.TransferKeeperV2)) + + // Seal the IBC Routers. app.IBCKeeper.SetRouter(ibcRouter) + app.IBCKeeper.SetRouterV2(ibcRouterV2) clientKeeper := app.IBCKeeper.ClientKeeper storeProvider := app.IBCKeeper.ClientKeeper.GetStoreProvider() diff --git a/testing/mock/v2/ibc_app.go b/testing/mock/v2/ibc_app.go index a210285d40b..7b72f4bac30 100644 --- a/testing/mock/v2/ibc_app.go +++ b/testing/mock/v2/ibc_app.go @@ -9,8 +9,8 @@ import ( ) type IBCApp struct { - OnSendPacket func(ctx context.Context, sourceChannel string, destinationChannel string, sequence uint64, data channeltypesv2.Payload, signer sdk.AccAddress) error - OnRecvPacket func(ctx context.Context, sourceChannel string, destinationChannel string, data channeltypesv2.Payload, relayer sdk.AccAddress) channeltypesv2.RecvPacketResult - OnTimeoutPacket func(ctx context.Context, sourceChannel string, destinationChannel string, data channeltypesv2.Payload, relayer sdk.AccAddress) error - OnAcknowledgementPacket func(ctx context.Context, sourceChannel string, destinationChannel string, data channeltypesv2.Payload, acknowledgement []byte, relayer sdk.AccAddress) error + OnSendPacket func(goCtx context.Context, sourceChannel string, destinationChannel string, sequence uint64, payload channeltypesv2.Payload, signer sdk.AccAddress) error + OnRecvPacket func(goCtx context.Context, sourceChannel string, destinationChannel string, sequence uint64, payload channeltypesv2.Payload, relayer sdk.AccAddress) channeltypesv2.RecvPacketResult + OnTimeoutPacket func(goCtx context.Context, sourceChannel string, destinationChannel string, sequence uint64, payload channeltypesv2.Payload, relayer sdk.AccAddress) error + OnAcknowledgementPacket func(goCtx context.Context, sourceChannel string, destinationChannel string, sequence uint64, payload channeltypesv2.Payload, acknowledgement []byte, relayer sdk.AccAddress) error } diff --git a/testing/mock/v2/ibc_module.go b/testing/mock/v2/ibc_module.go index 2f44dad7350..8ea9fad704a 100644 --- a/testing/mock/v2/ibc_module.go +++ b/testing/mock/v2/ibc_module.go @@ -38,23 +38,23 @@ func (im IBCModule) OnSendPacket(ctx context.Context, sourceChannel string, dest return nil } -func (im IBCModule) OnRecvPacket(ctx context.Context, sourceChannel string, destinationChannel string, data channeltypesv2.Payload, relayer sdk.AccAddress) channeltypesv2.RecvPacketResult { +func (im IBCModule) OnRecvPacket(ctx context.Context, sourceChannel string, destinationChannel string, sequence uint64, data channeltypesv2.Payload, relayer sdk.AccAddress) channeltypesv2.RecvPacketResult { if im.IBCApp.OnRecvPacket != nil { - return im.IBCApp.OnRecvPacket(ctx, sourceChannel, destinationChannel, data, relayer) + return im.IBCApp.OnRecvPacket(ctx, sourceChannel, destinationChannel, sequence, data, relayer) } return MockRecvPacketResult } -func (im IBCModule) OnAcknowledgementPacket(ctx context.Context, sourceChannel string, destinationChannel string, data channeltypesv2.Payload, acknowledgement []byte, relayer sdk.AccAddress) error { +func (im IBCModule) OnAcknowledgementPacket(ctx context.Context, sourceChannel string, destinationChannel string, sequence uint64, acknowledgement []byte, payload channeltypesv2.Payload, relayer sdk.AccAddress) error { if im.IBCApp.OnAcknowledgementPacket != nil { - return im.IBCApp.OnAcknowledgementPacket(ctx, sourceChannel, destinationChannel, data, acknowledgement, relayer) + return im.IBCApp.OnAcknowledgementPacket(ctx, sourceChannel, destinationChannel, sequence, payload, acknowledgement, relayer) } return nil } -func (im IBCModule) OnTimeoutPacket(ctx context.Context, sourceChannel string, destinationChannel string, data channeltypesv2.Payload, relayer sdk.AccAddress) error { +func (im IBCModule) OnTimeoutPacket(ctx context.Context, sourceChannel string, destinationChannel string, sequence uint64, payload channeltypesv2.Payload, relayer sdk.AccAddress) error { if im.IBCApp.OnTimeoutPacket != nil { - return im.IBCApp.OnTimeoutPacket(ctx, sourceChannel, destinationChannel, data, relayer) + return im.IBCApp.OnTimeoutPacket(ctx, sourceChannel, destinationChannel, sequence, payload, relayer) } return nil } diff --git a/testing/path.go b/testing/path.go index b31dec57ed9..cc2949158e7 100644 --- a/testing/path.go +++ b/testing/path.go @@ -139,6 +139,13 @@ func (path *Path) RelayPacketWithResults(packet channeltypes.Packet) (*abci.Exec return nil, nil, errors.New("packet commitment does not exist on either endpoint for provided packet") } +// Reversed returns a new path with endpoints reversed. +func (path *Path) Reversed() *Path { + reversedPath := *path + reversedPath.EndpointA, reversedPath.EndpointB = path.EndpointB, path.EndpointA + return &reversedPath +} + // Setup constructs a TM client, connection, and channel on both chains provided. It will // fail if any error occurs. func (path *Path) Setup() { diff --git a/testing/simapp/app.go b/testing/simapp/app.go index 320d5957b83..869b31ba649 100644 --- a/testing/simapp/app.go +++ b/testing/simapp/app.go @@ -96,6 +96,8 @@ import ( "github.com/cosmos/ibc-go/v9/modules/apps/transfer" ibctransferkeeper "github.com/cosmos/ibc-go/v9/modules/apps/transfer/keeper" ibctransfertypes "github.com/cosmos/ibc-go/v9/modules/apps/transfer/types" + transferv2 "github.com/cosmos/ibc-go/v9/modules/apps/transfer/v2" + ibctransferkeeperv2 "github.com/cosmos/ibc-go/v9/modules/apps/transfer/v2/keeper" ibc "github.com/cosmos/ibc-go/v9/modules/core" ibcclienttypes "github.com/cosmos/ibc-go/v9/modules/core/02-client/types" ibcconnectiontypes "github.com/cosmos/ibc-go/v9/modules/core/03-connection/types" @@ -168,6 +170,7 @@ type SimApp struct { ICAControllerKeeper icacontrollerkeeper.Keeper ICAHostKeeper icahostkeeper.Keeper TransferKeeper ibctransferkeeper.Keeper + TransferKeeperV2 *ibctransferkeeperv2.Keeper ConsensusParamsKeeper consensusparamkeeper.Keeper // make IBC modules public for test purposes @@ -484,6 +487,10 @@ func NewSimApp( ibcRouterV2.AddRoute(mockv2.ModuleNameB, mockV2B) app.MockModuleV2B = mockV2B + // register the transfer v2 module. + app.TransferKeeperV2 = ibctransferkeeperv2.NewKeeper(app.TransferKeeper, app.IBCKeeper.ChannelKeeperV2) + ibcRouterV2.AddRoute(ibctransfertypes.ModuleName, transferv2.NewIBCModule(app.TransferKeeperV2)) + // Seal the IBC Router app.IBCKeeper.SetRouter(ibcRouter) app.IBCKeeper.SetRouterV2(ibcRouterV2)