diff --git a/proto/ibc/transfer/transfer.proto b/proto/ibc/transfer/transfer.proto index 2867273539..555ad4c36d 100644 --- a/proto/ibc/transfer/transfer.proto +++ b/proto/ibc/transfer/transfer.proto @@ -6,26 +6,19 @@ option go_package = "github.com/cosmos/cosmos-sdk/x/ibc-transfer/types"; import "gogoproto/gogo.proto"; import "cosmos/cosmos.proto"; -// MsgTransfer defines a msg to transfer fungible tokens (i.e Coins) between ICS20 enabled chains. -// See ICS Spec here: https://github.com/cosmos/ics/tree/master/spec/ics-020-fungible-token-transfer#data-structures +// MsgTransfer defines a msg to transfer fungible tokens (i.e Coins) between +// ICS20 enabled chains. See ICS Spec here: +// https://github.com/cosmos/ics/tree/master/spec/ics-020-fungible-token-transfer#data-structures message MsgTransfer { // the port on which the packet will be sent - string source_port = 1 [ - (gogoproto.moretags) = "yaml:\"source_port\"" - ]; + string source_port = 1 [(gogoproto.moretags) = "yaml:\"source_port\""]; // the channel by which the packet will be sent - string source_channel = 2 [ - (gogoproto.moretags) = "yaml:\"source_channel\"" - ]; + string source_channel = 2 [(gogoproto.moretags) = "yaml:\"source_channel\""]; // the tokens to be transferred - repeated cosmos.Coin amount = 3 [ - (gogoproto.nullable) = false, - (gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins" - ]; + cosmos.Coin token = 3 [(gogoproto.nullable) = false]; // the sender address - bytes sender = 4 [ - (gogoproto.casttype) = "github.com/cosmos/cosmos-sdk/types.AccAddress" - ]; + bytes sender = 4 + [(gogoproto.casttype) = "github.com/cosmos/cosmos-sdk/types.AccAddress"]; // the recipient address on the destination chain string receiver = 5; // Timeout height relative to the current block height. @@ -33,27 +26,29 @@ message MsgTransfer { uint64 timeout_height = 6 [(gogoproto.moretags) = "yaml:\"timeout_height\""]; // Timeout timestamp (in nanoseconds) relative to the current block timestamp. // The timeout is disabled when set to 0. - uint64 timeout_timestamp = 7 [(gogoproto.moretags) = "yaml:\"timeout_timestamp\""]; + uint64 timeout_timestamp = 7 + [(gogoproto.moretags) = "yaml:\"timeout_timestamp\""]; } // FungibleTokenPacketData defines a struct for the packet payload -// See FungibleTokenPacketData spec: https://github.com/cosmos/ics/tree/master/spec/ics-020-fungible-token-transfer#data-structures +// See FungibleTokenPacketData spec: +// https://github.com/cosmos/ics/tree/master/spec/ics-020-fungible-token-transfer#data-structures message FungibleTokenPacketData { - // the tokens to be transferred - repeated cosmos.Coin amount = 1 [ - (gogoproto.nullable) = false, - (gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins" - ]; + // the token denomination to be transferred + string denom = 1; + // the token amount to be transferred + uint64 amount = 2; // the sender address - string sender = 2; + string sender = 3; // the recipient address on the destination chain - string receiver = 3; + string receiver = 4; } -// FungibleTokenPacketAcknowledgement contains a boolean success flag and an optional error msg -// error msg is empty string on success -// See spec for onAcknowledgePacket: https://github.com/cosmos/ics/tree/master/spec/ics-020-fungible-token-transfer#packet-relay +// FungibleTokenPacketAcknowledgement contains a boolean success flag and an +// optional error msg error msg is empty string on success See spec for +// onAcknowledgePacket: +// https://github.com/cosmos/ics/tree/master/spec/ics-020-fungible-token-transfer#packet-relay message FungibleTokenPacketAcknowledgement { - bool success = 1; - string error = 2; + bool success = 1; + string error = 2; } diff --git a/x/ibc-transfer/client/cli/tx.go b/x/ibc-transfer/client/cli/tx.go index 451245d74d..f3b3a11b42 100644 --- a/x/ibc-transfer/client/cli/tx.go +++ b/x/ibc-transfer/client/cli/tx.go @@ -44,7 +44,7 @@ to the counterparty channel. Any timeout set to 0 is disabled.`), srcChannel := args[1] receiver := args[2] - coins, err := sdk.ParseCoins(args[3]) + coin, err := sdk.ParseCoin(args[3]) if err != nil { return err } @@ -82,7 +82,7 @@ to the counterparty channel. Any timeout set to 0 is disabled.`), } msg := types.NewMsgTransfer( - srcPort, srcChannel, coins, sender, receiver, timeoutHeight, timeoutTimestamp, + srcPort, srcChannel, coin, sender, receiver, timeoutHeight, timeoutTimestamp, ) if err := msg.ValidateBasic(); err != nil { return err diff --git a/x/ibc-transfer/client/rest/rest.go b/x/ibc-transfer/client/rest/rest.go index 4a498ba3bb..6829f6b3c5 100644 --- a/x/ibc-transfer/client/rest/rest.go +++ b/x/ibc-transfer/client/rest/rest.go @@ -21,7 +21,7 @@ func RegisterRoutes(clientCtx client.Context, r *mux.Router) { // TransferTxReq defines the properties of a transfer tx request's body. type TransferTxReq struct { BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` - Amount sdk.Coins `json:"amount" yaml:"amount"` + Amount sdk.Coin `json:"amount" yaml:"amount"` Receiver string `json:"receiver" yaml:"receiver"` TimeoutHeight uint64 `json:"timeout_height" yaml:"timeout_height"` TimeoutTimestamp uint64 `json:"timeout_timestamp" yaml:"timeout_timestamp"` diff --git a/x/ibc-transfer/handler.go b/x/ibc-transfer/handler.go index 08c3df9dd4..dd8cabb54e 100644 --- a/x/ibc-transfer/handler.go +++ b/x/ibc-transfer/handler.go @@ -24,12 +24,12 @@ func NewHandler(k keeper.Keeper) sdk.Handler { // See createOutgoingPacket in spec:https://github.com/cosmos/ics/tree/master/spec/ics-020-fungible-token-transfer#packet-relay func handleMsgTransfer(ctx sdk.Context, k keeper.Keeper, msg *types.MsgTransfer) (*sdk.Result, error) { if err := k.SendTransfer( - ctx, msg.SourcePort, msg.SourceChannel, msg.Amount, msg.Sender, msg.Receiver, msg.TimeoutHeight, msg.TimeoutTimestamp, + ctx, msg.SourcePort, msg.SourceChannel, msg.Token, msg.Sender, msg.Receiver, msg.TimeoutHeight, msg.TimeoutTimestamp, ); err != nil { return nil, err } - k.Logger(ctx).Info("IBC transfer: %s from %s to %s", msg.Amount, msg.Sender, msg.Receiver) + k.Logger(ctx).Info("IBC transfer: %s from %s to %s", msg.Token, msg.Sender, msg.Receiver) ctx.EventManager().EmitEvents(sdk.Events{ sdk.NewEvent( diff --git a/x/ibc-transfer/handler_test.go b/x/ibc-transfer/handler_test.go index 71d48182ef..6545a5884f 100644 --- a/x/ibc-transfer/handler_test.go +++ b/x/ibc-transfer/handler_test.go @@ -1,14 +1,13 @@ package transfer_test import ( - "fmt" "testing" "github.com/stretchr/testify/suite" sdk "github.com/cosmos/cosmos-sdk/types" - ibctransfer "github.com/cosmos/cosmos-sdk/x/ibc-transfer" "github.com/cosmos/cosmos-sdk/x/ibc-transfer/types" + channeltypes "github.com/cosmos/cosmos-sdk/x/ibc/04-channel/types" ibctesting "github.com/cosmos/cosmos-sdk/x/ibc/testing" ) @@ -29,41 +28,54 @@ func (suite *HandlerTestSuite) SetupTest() { } // constructs a send from chainA to chainB on the established channel/connection -// and sends the coins back from chainB to chainA. -// FIX: this test currently passes because source is incorrectly determined -// by the ibc-transfer module, so what actually occurs is chainA and chainB -// send coins to each other, but no coins are ever sent back. This can be -// fixed by receving and acknowledeging the send on the counterparty chain. -// https://github.com/cosmos/cosmos-sdk/issues/6827 +// and sends the same coin back from chainB to chainA. func (suite *HandlerTestSuite) TestHandleMsgTransfer() { clientA, clientB, _, _, channelA, channelB := suite.coordinator.Setup(suite.chainA, suite.chainB) - handlerA := ibctransfer.NewHandler(suite.chainA.App.TransferKeeper) + originalBalance := suite.chainA.App.BankKeeper.GetBalance(suite.chainA.GetContext(), suite.chainA.SenderAccount.GetAddress(), sdk.DefaultBondDenom) - coinToSendToB := sdk.NewCoins(sdk.NewCoin(fmt.Sprintf("%s/%s/%s", channelB.PortID, channelB.ID, sdk.DefaultBondDenom), sdk.NewInt(100))) + coinToSendToB := sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(100)) // send from chainA to chainB msg := types.NewMsgTransfer(channelA.PortID, channelA.ID, coinToSendToB, suite.chainA.SenderAccount.GetAddress(), suite.chainB.SenderAccount.GetAddress().String(), 110, 0) - res, err := handlerA(suite.chainA.GetContext(), msg) - suite.Require().NoError(err) - suite.Require().NotNil(res, "%+v", res) // successfully executed - - err = suite.coordinator.SendMsgs(suite.chainA, suite.chainB, clientB, msg) + err := suite.coordinator.SendMsgs(suite.chainA, suite.chainB, clientB, msg) suite.Require().NoError(err) // message committed - handlerB := ibctransfer.NewHandler(suite.chainB.App.TransferKeeper) + // relay send + fungibleTokenPacket := types.NewFungibleTokenPacketData(coinToSendToB.Denom, coinToSendToB.Amount.Uint64(), suite.chainA.SenderAccount.GetAddress().String(), suite.chainB.SenderAccount.GetAddress().String()) + packet := channeltypes.NewPacket(fungibleTokenPacket.GetBytes(), 1, channelA.PortID, channelA.ID, channelB.PortID, channelB.ID, 110, 0) + ack := types.FungibleTokenPacketAcknowledgement{Success: true} + err = suite.coordinator.RelayPacket(suite.chainA, suite.chainB, clientA, clientB, packet, ack.GetBytes()) + suite.Require().NoError(err) // relay committed - coinToSendBackToA := sdk.NewCoins(sdk.NewCoin(fmt.Sprintf("%s/%s/%s", channelA.PortID, channelA.ID, sdk.DefaultBondDenom), sdk.NewInt(100))) + // check that voucher exists on chain + voucherDenom := types.GetPrefixedDenom(packet.GetDestPort(), packet.GetDestChannel(), sdk.DefaultBondDenom) + balance := suite.chainB.App.BankKeeper.GetBalance(suite.chainB.GetContext(), suite.chainB.SenderAccount.GetAddress(), voucherDenom) + + coinToSendBackToA := types.GetTransferCoin(channelB.PortID, channelB.ID, sdk.DefaultBondDenom, 100) + suite.Require().Equal(coinToSendBackToA, balance) // send from chainB back to chainA - msg = types.NewMsgTransfer(channelA.PortID, channelA.ID, coinToSendBackToA, suite.chainB.SenderAccount.GetAddress(), suite.chainA.SenderAccount.GetAddress().String(), 110, 0) - - res, err = handlerB(suite.chainB.GetContext(), msg) - suite.Require().NoError(err) - suite.Require().NotNil(res, "%+v", res) // successfully executed + msg = types.NewMsgTransfer(channelB.PortID, channelB.ID, coinToSendBackToA, suite.chainB.SenderAccount.GetAddress(), suite.chainA.SenderAccount.GetAddress().String(), 110, 0) err = suite.coordinator.SendMsgs(suite.chainB, suite.chainA, clientA, msg) suite.Require().NoError(err) // message committed + + // relay send + fungibleTokenPacket = types.NewFungibleTokenPacketData(coinToSendBackToA.Denom, coinToSendBackToA.Amount.Uint64(), suite.chainB.SenderAccount.GetAddress().String(), suite.chainA.SenderAccount.GetAddress().String()) + packet = channeltypes.NewPacket(fungibleTokenPacket.GetBytes(), 1, channelB.PortID, channelB.ID, channelA.PortID, channelA.ID, 110, 0) + err = suite.coordinator.RelayPacket(suite.chainB, suite.chainA, clientB, clientA, packet, ack.GetBytes()) + suite.Require().NoError(err) // relay committed + + balance = suite.chainA.App.BankKeeper.GetBalance(suite.chainA.GetContext(), suite.chainA.SenderAccount.GetAddress(), sdk.DefaultBondDenom) + + // check that the balance on chainA returned back to the original state + suite.Require().Equal(originalBalance, balance) + + // check that module account escrow address is empty + escrowAddress := types.GetEscrowAddress(packet.GetDestPort(), packet.GetDestChannel()) + balance = suite.chainA.App.BankKeeper.GetBalance(suite.chainA.GetContext(), escrowAddress, sdk.DefaultBondDenom) + suite.Require().Equal(sdk.NewCoin(sdk.DefaultBondDenom, sdk.ZeroInt()), balance) } func TestHandlerTestSuite(t *testing.T) { diff --git a/x/ibc-transfer/keeper/relay.go b/x/ibc-transfer/keeper/relay.go index e5b89fe594..1b7f6dd332 100644 --- a/x/ibc-transfer/keeper/relay.go +++ b/x/ibc-transfer/keeper/relay.go @@ -1,8 +1,6 @@ package keeper import ( - "strings" - sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/cosmos/cosmos-sdk/x/ibc-transfer/types" @@ -12,19 +10,42 @@ import ( // SendTransfer handles transfer sending logic. There are 2 possible cases: // -// 1. Sender chain is the source chain of the coins (i.e where they were minted): the coins -// are transferred to an escrow address (i.e locked) on the sender chain and then -// transferred to the destination chain (i.e not the source chain) via a packet -// with the corresponding fungible token data. +// 1. Sender chain is acting as the source zone. The coins are transferred +// to an escrow address (i.e locked) on the sender chain and then transferred +// to the receiving chain through IBC TAO logic. It is expected that the +// receiving chain will mint vouchers to the receiving address. // -// 2. Coins are not native from the sender chain (i.e tokens sent where transferred over -// through IBC already): the coins are burned and then a packet is sent to the -// source chain of the tokens. +// 2. Sender chain is acting as the sink zone. The coins (vouchers) are burned +// on the sender chain and then transferred to the receiving chain though IBC +// TAO logic. It is expected that the receiving chain, which had previously +// sent the original denomination, will unescrow the fungible token and send +// it to the receiving address. +// +// Another way of thinking of source and sink zones is through the token's +// timeline. Each send to any chain other than the one it was previously +// received from is a movement forwards in the token's timeline. This causes +// trace to be added to the token's history and the destination port and +// destination channel to be prefixed to the denomination. In these instances +// the sender chain is acting as the source zone. When the token is sent back +// to the chain it previously received from, the prefix is removed. This is +// a backwards movement in the token's timeline and the sender chain +// is acting as the sink zone. +// +// Example: +// These steps of transfer occur: A -> B -> C -> A -> C -> B -> A +// +// 1. A -> B : sender chain is source zone. Denom upon receiving: 'B/denom' +// 2. B -> C : sender chain is source zone. Denom upon receiving: 'C/B/denom' +// 3. C -> A : sender chain is source zone. Denom upon receiving: 'A/C/B/denom' +// 4. A -> C : sender chain is sink zone. Denom upon receiving: 'C/B/denom' +// 5. C -> B : sender chain is sink zone. Denom upon receiving: 'B/denom' +// 6. B -> A : sender chain is sink zone. Denom upon receiving: 'denom' + func (k Keeper) SendTransfer( ctx sdk.Context, sourcePort, sourceChannel string, - amount sdk.Coins, + token sdk.Coin, sender sdk.AccAddress, receiver string, timeoutHeight, @@ -47,81 +68,39 @@ func (k Keeper) SendTransfer( ) } - return k.createOutgoingPacket( - ctx, sequence, sourcePort, sourceChannel, destinationPort, destinationChannel, - amount, sender, receiver, timeoutHeight, timeoutTimestamp, - ) -} + // begin createOutgoingPacket logic + // See spec for this logic: https://github.com/cosmos/ics/tree/master/spec/ics-020-fungible-token-transfer#packet-relay -// See spec for this function: https://github.com/cosmos/ics/tree/master/spec/ics-020-fungible-token-transfer#packet-relay -func (k Keeper) createOutgoingPacket( - ctx sdk.Context, - seq uint64, - sourcePort, sourceChannel, - destinationPort, destinationChannel string, - amount sdk.Coins, - sender sdk.AccAddress, - receiver string, - timeoutHeight, timeoutTimestamp uint64, -) error { channelCap, ok := k.scopedKeeper.GetCapability(ctx, host.ChannelCapabilityPath(sourcePort, sourceChannel)) if !ok { return sdkerrors.Wrap(channeltypes.ErrChannelCapabilityNotFound, "module does not own channel capability") } - // NOTE: - // - Coins transferred from the destination chain should have their denomination - // prefixed with source port and channel IDs. - // - Coins transferred from the source chain can have their denomination - // clear from prefixes when transferred to the escrow account (i.e when they are - // locked) BUT MUST have the destination port and channel ID when constructing - // the packet data. - if len(amount) != 1 { - return sdkerrors.Wrapf(types.ErrOnlyOneDenomAllowed, "%d denoms included", len(amount)) - } - prefix := types.GetDenomPrefix(destinationPort, destinationChannel) - source := strings.HasPrefix(amount[0].Denom, prefix) + // 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 source { - // clear the denomination from the prefix to send the coins to the escrow account - coins := make(sdk.Coins, len(amount)) - for i, coin := range amount { - if strings.HasPrefix(coin.Denom, prefix) { - coins[i] = sdk.NewCoin(coin.Denom[len(prefix):], coin.Amount) - } else { - coins[i] = coin - } - } - - // escrow tokens if the destination chain is the same as the sender's + if types.SenderChainIsSource(sourcePort, sourceChannel, token.Denom) { + // create the escrow address for the tokens escrowAddress := types.GetEscrowAddress(sourcePort, sourceChannel) // escrow source tokens. It fails if balance insufficient. if err := k.bankKeeper.SendCoins( - ctx, sender, escrowAddress, coins, + ctx, sender, escrowAddress, sdk.NewCoins(token), ); err != nil { return err } } else { - // build the receiving denomination prefix if it's not present - prefix = types.GetDenomPrefix(sourcePort, sourceChannel) - for _, coin := range amount { - if !strings.HasPrefix(coin.Denom, prefix) { - return sdkerrors.Wrapf(types.ErrInvalidDenomForTransfer, "denom was: %s", coin.Denom) - } - } - // transfer the coins to the module account and burn them if err := k.bankKeeper.SendCoinsFromAccountToModule( - ctx, sender, types.ModuleName, amount, + ctx, sender, types.ModuleName, sdk.NewCoins(token), ); err != nil { return err } - // burn vouchers from the sender's balance if the source is from another chain if err := k.bankKeeper.BurnCoins( - ctx, types.ModuleName, amount, + ctx, types.ModuleName, sdk.NewCoins(token), ); err != nil { // NOTE: should not happen as the module account was // retrieved on the step above and it has enough balace @@ -131,12 +110,12 @@ func (k Keeper) createOutgoingPacket( } packetData := types.NewFungibleTokenPacketData( - amount, sender.String(), receiver, + token.Denom, token.Amount.Uint64(), sender.String(), receiver, ) packet := channeltypes.NewPacket( packetData.GetBytes(), - seq, + sequence, sourcePort, sourceChannel, destinationPort, @@ -148,76 +127,87 @@ func (k Keeper) createOutgoingPacket( return k.channelKeeper.SendPacket(ctx, channelCap, packet) } +// OnRecvPacket processes a cross chain fungible token transfer. If the +// sender chain is the source of minted tokens then vouchers will be minted +// and sent to the receiving address. Otherwise if the sender chain is sending +// back tokens this chain originally transferred to it, the tokens are +// unescrowed and sent to the receiving address. func (k Keeper) OnRecvPacket(ctx sdk.Context, packet channeltypes.Packet, data types.FungibleTokenPacketData) error { // NOTE: packet data type already checked in handler.go - if len(data.Amount) != 1 { - return sdkerrors.Wrapf(types.ErrOnlyOneDenomAllowed, "%d denoms included", len(data.Amount)) - } - - prefix := types.GetDenomPrefix(packet.GetDestPort(), packet.GetDestChannel()) - source := strings.HasPrefix(data.Amount[0].Denom, prefix) - // decode the receiver address receiver, err := sdk.AccAddressFromBech32(data.Receiver) if err != nil { return err } - if source { + // 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 coin as seen in the "sender chain is the source" condition. - // mint new tokens if the source of the transfer is the same chain - if err := k.bankKeeper.MintCoins( - ctx, types.ModuleName, data.Amount, - ); err != nil { - return err - } + if types.ReceiverChainIsSource(packet.GetSourcePort(), packet.GetSourceChannel(), data.Denom) { + // sender chain is not the source, unescrow tokens - // send to receiver - return k.bankKeeper.SendCoinsFromModuleToAccount( - ctx, types.ModuleName, receiver, data.Amount, - ) + // remove prefix added by sender chain + voucherPrefix := types.GetDenomPrefix(packet.GetSourcePort(), packet.GetSourceChannel()) + unprefixedDenom := data.Denom[len(voucherPrefix):] + token := sdk.NewCoin(unprefixedDenom, sdk.NewIntFromUint64(data.Amount)) + + // unescrow tokens + escrowAddress := types.GetEscrowAddress(packet.GetDestPort(), packet.GetDestChannel()) + return k.bankKeeper.SendCoins(ctx, escrowAddress, receiver, sdk.NewCoins(token)) } - // check the denom prefix - prefix = types.GetDenomPrefix(packet.GetSourcePort(), packet.GetSourceChannel()) - coins := make(sdk.Coins, len(data.Amount)) - for i, coin := range data.Amount { - if !strings.HasPrefix(coin.Denom, prefix) { - return sdkerrors.Wrapf( - sdkerrors.ErrInvalidCoins, - "%s doesn't contain the prefix '%s'", coin.Denom, prefix, - ) - } - coins[i] = sdk.NewCoin(coin.Denom[len(prefix):], coin.Amount) + // sender chain is the source, mint vouchers + + // since SendPacket did not prefix the denomination, we must prefix denomination here + sourcePrefix := types.GetDenomPrefix(packet.GetDestPort(), packet.GetDestChannel()) + // NOTE: sourcePrefix contains the trailing "/" + prefixedDenom := sourcePrefix + data.Denom + voucher := sdk.NewCoin(prefixedDenom, sdk.NewIntFromUint64(data.Amount)) + + // 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 err } - // unescrow tokens - escrowAddress := types.GetEscrowAddress(packet.GetDestPort(), packet.GetDestChannel()) - return k.bankKeeper.SendCoins(ctx, escrowAddress, receiver, coins) + // send to receiver + return k.bankKeeper.SendCoinsFromModuleToAccount( + ctx, types.ModuleName, receiver, sdk.NewCoins(voucher), + ) } +// OnAcknowledgementPacket responds to the the success or failure of a packet +// acknowledgement written on the receiving chain. If the acknowledgement +// was a success then nothing occurs. If the acknowledgement failed, then +// the sender is refunded their tokens using the refundPacketToken function. func (k Keeper) OnAcknowledgementPacket(ctx sdk.Context, packet channeltypes.Packet, data types.FungibleTokenPacketData, ack types.FungibleTokenPacketAcknowledgement) error { if !ack.Success { - return k.refundPacketAmount(ctx, packet, data) + return k.refundPacketToken(ctx, packet, data) } return nil } +// OnTimeoutPacket refunds the sender since the original packet sent was +// never received and has been timed out. func (k Keeper) OnTimeoutPacket(ctx sdk.Context, packet channeltypes.Packet, data types.FungibleTokenPacketData) error { - return k.refundPacketAmount(ctx, packet, data) + return k.refundPacketToken(ctx, packet, data) } -func (k Keeper) refundPacketAmount(ctx sdk.Context, packet channeltypes.Packet, data types.FungibleTokenPacketData) error { +// refundPacketToken will unescrow and send back the tokens back to sender +// if the sending chain was the source chain. Otherwise, the sent tokens +// were burnt in the original send so new tokens are minted and sent to +// the sending address. +func (k Keeper) refundPacketToken(ctx sdk.Context, packet channeltypes.Packet, data types.FungibleTokenPacketData) error { // NOTE: packet data type already checked in handler.go - if len(data.Amount) != 1 { - return sdkerrors.Wrapf(types.ErrOnlyOneDenomAllowed, "%d denoms included", len(data.Amount)) - } - - // check the denom prefix - prefix := types.GetDenomPrefix(packet.GetDestPort(), packet.GetDestChannel()) - source := strings.HasPrefix(data.Amount[0].Denom, prefix) + token := sdk.NewCoin(data.Denom, sdk.NewIntFromUint64(data.Amount)) // decode the sender address sender, err := sdk.AccAddressFromBech32(data.Sender) @@ -225,27 +215,18 @@ func (k Keeper) refundPacketAmount(ctx sdk.Context, packet channeltypes.Packet, return err } - if source { - coins := make(sdk.Coins, len(data.Amount)) - for i, coin := range data.Amount { - coin := coin - if !strings.HasPrefix(coin.Denom, prefix) { - return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "%s doesn't contain the prefix '%s'", coin.Denom, prefix) - } - coins[i] = sdk.NewCoin(coin.Denom[len(prefix):], coin.Amount) - } - + if types.SenderChainIsSource(packet.GetSourcePort(), packet.GetSourceChannel(), token.Denom) { // unescrow tokens back to sender escrowAddress := types.GetEscrowAddress(packet.GetSourcePort(), packet.GetSourceChannel()) - return k.bankKeeper.SendCoins(ctx, escrowAddress, sender, coins) + return k.bankKeeper.SendCoins(ctx, escrowAddress, sender, sdk.NewCoins(token)) } // mint vouchers back to sender if err := k.bankKeeper.MintCoins( - ctx, types.ModuleName, data.Amount, + ctx, types.ModuleName, sdk.NewCoins(token), ); err != nil { return err } - return k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, sender, data.Amount) + return k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, sender, sdk.NewCoins(token)) } diff --git a/x/ibc-transfer/keeper/relay_test.go b/x/ibc-transfer/keeper/relay_test.go index 6e29207d36..984701a4ac 100644 --- a/x/ibc-transfer/keeper/relay_test.go +++ b/x/ibc-transfer/keeper/relay_test.go @@ -35,20 +35,20 @@ func (suite *KeeperTestSuite) TestSendTransfer() { {"successful transfer from source chain", func() { _, _, _, _, channelA, channelB = suite.coordinator.Setup(suite.chainA, suite.chainB) - amount = ibctesting.NewTransferCoins(channelB, sdk.DefaultBondDenom, 100) + amount = types.GetTransferCoin(channelB, sdk.DefaultBondDenom, 100) }, true, true}, {"successful transfer with coins from counterparty chain", func() { // send coins from chainA back to chainB _, _, _, _, channelA, channelB = suite.coordinator.Setup(suite.chainA, suite.chainB) - amount = ibctesting.NewTransferCoins(channelA, sdk.DefaultBondDenom, 100) + amount = types.GetTransferCoin(channelA, sdk.DefaultBondDenom, 100) }, false, true}, {"source channel not found", func() { // channel references wrong ID _, _, _, _, channelA, channelB = suite.coordinator.Setup(suite.chainA, suite.chainB) channelA.ID = ibctesting.InvalidID - amount = ibctesting.NewTransferCoins(channelB, sdk.DefaultBondDenom, 100) + amount = types.GetTransferCoin(channelB, sdk.DefaultBondDenom, 100) }, true, false}, {"next seq send not found", func() { @@ -62,7 +62,7 @@ func (suite *KeeperTestSuite) TestSendTransfer() { channeltypes.NewChannel(channeltypes.OPEN, channeltypes.ORDERED, channeltypes.NewCounterparty(channelB.PortID, channelB.ID), []string{connA.ID}, ibctesting.ChannelVersion), ) suite.chainA.CreateChannelCapability(channelA.PortID, channelA.ID) - amount = ibctesting.NewTransferCoins(channelB, sdk.DefaultBondDenom, 100) + amount = types.GetTransferCoin(channelB, sdk.DefaultBondDenom, 100) }, true, false}, // createOutgoingPacket tests @@ -70,13 +70,13 @@ func (suite *KeeperTestSuite) TestSendTransfer() { {"send coins failed", func() { _, _, _, _, channelA, channelB = suite.coordinator.Setup(suite.chainA, suite.chainB) - amount = ibctesting.NewTransferCoins(channelB, "randomdenom", 100) + amount = types.GetTransferCoin(channelB, "randomdenom", 100) }, true, false}, // - receiving chain {"send from module account failed", func() { _, _, _, _, channelA, channelB = suite.coordinator.Setup(suite.chainA, suite.chainB) - amount = ibctesting.NewTransferCoins(channelA, "randomdenom", 100) + amount = types.GetTransferCoin(channelA, "randomdenom", 100) }, false, false}, {"channel capability not found", func() { @@ -98,7 +98,7 @@ func (suite *KeeperTestSuite) TestSendTransfer() { if !tc.source { // send coins from chainB to chainA - coinFromBToA := ibctesting.NewTransferCoins(channelA, sdk.DefaultBondDenom, 100) + coinFromBToA := types.GetTransferCoin(channelA, sdk.DefaultBondDenom, 100) transferMsg := types.NewMsgTransfer(channelB.PortID, channelB.ID, coinFromBToA, suite.chainB.SenderAccount.GetAddress(), suite.chainA.SenderAccount.GetAddress().String(), 110, 0) err = suite.coordinator.SendMsgs(suite.chainB, suite.chainA, channelA.ClientID, transferMsg) suite.Require().NoError(err) // message committed @@ -163,12 +163,12 @@ func (suite *KeeperTestSuite) TestOnRecvPacket() { // onRecvPacket // - coins from source chain (chainA) {"failure: mint zero coins", func() { - coins = ibctesting.NewTransferCoins(channelB, sdk.DefaultBondDenom, 0) + coins = types.GetTransferCoin(channelB, sdk.DefaultBondDenom, 0) }, true, false}, // - coins being sent back to original chain (chainB) {"tries to unescrow more tokens than allowed", func() { - coins = ibctesting.NewTransferCoins(channelA, sdk.DefaultBondDenom, 1000000) + coins = types.GetTransferCoin(channelA, sdk.DefaultBondDenom, 1000000) }, false, false}, } @@ -184,38 +184,23 @@ func (suite *KeeperTestSuite) TestOnRecvPacket() { if !tc.source { // send coins from chainB to chainA, receive them, acknowledge them, and send back to chainB - coinFromBToA := ibctesting.NewTransferCoins(channelA, sdk.DefaultBondDenom, 100) + coinFromBToA := types.GetTransferCoin(channelA, sdk.DefaultBondDenom, 100) transferMsg := types.NewMsgTransfer(channelB.PortID, channelB.ID, coinFromBToA, suite.chainB.SenderAccount.GetAddress(), suite.chainA.SenderAccount.GetAddress().String(), 110, 0) err := suite.coordinator.SendMsgs(suite.chainB, suite.chainA, channelA.ClientID, transferMsg) suite.Require().NoError(err) // message committed - // receive coins on chainA from chainB + // relay send packet fungibleTokenPacket := types.NewFungibleTokenPacketData(coinFromBToA, suite.chainB.SenderAccount.GetAddress().String(), suite.chainA.SenderAccount.GetAddress().String()) packet := channeltypes.NewPacket(fungibleTokenPacket.GetBytes(), 1, channelB.PortID, channelB.ID, channelA.PortID, channelA.ID, 110, 0) - - // get proof of packet commitment from chainB - packetKey := host.KeyPacketCommitment(packet.GetSourcePort(), packet.GetSourceChannel(), packet.GetSequence()) - proof, proofHeight := suite.chainB.QueryProof(packetKey) - - recvMsg := channeltypes.NewMsgRecvPacket(packet, proof, proofHeight, suite.chainA.SenderAccount.GetAddress()) - err = suite.coordinator.SendMsgs(suite.chainA, suite.chainB, channelB.ClientID, recvMsg) - suite.Require().NoError(err) // message committed - - // get proof of acknowledgement on chainA - packetKey = host.KeyPacketAcknowledgement(packet.GetDestPort(), packet.GetDestChannel(), packet.GetSequence()) - proof, proofHeight = suite.chainA.QueryProof(packetKey) - - // acknowledge on chainB the receive that happened on chainA - ack := types.FungibleTokenPacketAcknowledgement{true, ""} - ackMsg := channeltypes.NewMsgAcknowledgement(packet, ack.GetBytes(), proof, proofHeight, suite.chainB.SenderAccount.GetAddress()) - err = suite.coordinator.SendMsgs(suite.chainB, suite.chainA, channelA.ClientID, ackMsg) - suite.Require().NoError(err) // message committed + ack := types.FungibleTokenPacketAcknowledgement{Success: true} + err = suite.coordinator.RelayPacket(suite.chainB, suite.chainA, clientB, clientA, packet, ack.GetBytes()) + suite.Require().NoError(err) // relay committed seq++ // NOTE: coins must be explicitly changed in malleate to test invalid cases - coins = ibctesting.NewTransferCoins(channelA, sdk.DefaultBondDenom, 100) + coins = types.GetTransferCoin(channelA, sdk.DefaultBondDenom, 100) } else { - coins = ibctesting.NewTransferCoins(channelB, sdk.DefaultBondDenom, 100) + coins = types.GetTransferCoin(channelB, sdk.DefaultBondDenom, 100) } // send coins from chainA to chainB @@ -264,7 +249,7 @@ func (suite *KeeperTestSuite) TestOnAcknowledgementPacket() { success bool // success of ack }{ {"success ack causes no-op", successAck, func() { - coins = ibctesting.NewTransferCoins(channelB, sdk.DefaultBondDenom, 100) + coins = types.GetTransferCoin(channelB, sdk.DefaultBondDenom, 100) }, true, true}, {"successful refund from source chain", failedAck, func() { @@ -272,11 +257,11 @@ func (suite *KeeperTestSuite) TestOnAcknowledgementPacket() { _, err := suite.chainA.App.BankKeeper.AddCoins(suite.chainA.GetContext(), escrow, sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(100)))) suite.Require().NoError(err) - coins = ibctesting.NewTransferCoins(channelB, sdk.DefaultBondDenom, 100) + coins = types.GetTransferCoin(channelB, sdk.DefaultBondDenom, 100) }, true, false}, {"successful refund from external chain", failedAck, func() { - coins = ibctesting.NewTransferCoins(channelA, sdk.DefaultBondDenom, 100) + coins = types.GetTransferCoin(channelA, sdk.DefaultBondDenom, 100) }, false, false}, } @@ -339,11 +324,11 @@ func (suite *KeeperTestSuite) TestOnTimeoutPacket() { _, err := suite.chainA.App.BankKeeper.AddCoins(suite.chainA.GetContext(), escrow, sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(100)))) suite.Require().NoError(err) - coins = ibctesting.NewTransferCoins(channelB, sdk.DefaultBondDenom, 100) + coins = types.GetTransferCoin(channelB, sdk.DefaultBondDenom, 100) }, true, true}, {"successful timeout from external chain", func() { - coins = ibctesting.NewTransferCoins(channelA, sdk.DefaultBondDenom, 100) + coins = types.GetTransferCoin(channelA, sdk.DefaultBondDenom, 100) }, false, true}, {"no source prefix on coin denom", func() { @@ -351,11 +336,11 @@ func (suite *KeeperTestSuite) TestOnTimeoutPacket() { }, false, false}, {"unescrow failed", func() { - coins = ibctesting.NewTransferCoins(channelB, sdk.DefaultBondDenom, 100) + coins = types.GetTransferCoin(channelB, sdk.DefaultBondDenom, 100) }, true, false}, {"mint failed", func() { - coins = ibctesting.NewTransferCoins(channelA, sdk.DefaultBondDenom, 0) + coins = types.GetTransferCoin(channelA, sdk.DefaultBondDenom, 0) }, true, false}, } diff --git a/x/ibc-transfer/module.go b/x/ibc-transfer/module.go index 1b1b72da29..632783c9e5 100644 --- a/x/ibc-transfer/module.go +++ b/x/ibc-transfer/module.go @@ -306,7 +306,8 @@ func (am AppModule) OnRecvPacket( types.EventTypePacket, sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), sdk.NewAttribute(types.AttributeKeyReceiver, data.Receiver), - sdk.NewAttribute(types.AttributeKeyValue, data.Amount.String()), + sdk.NewAttribute(types.AttributeKeyDenom, data.Denom), + sdk.NewAttribute(types.AttributeKeyAmount, fmt.Sprintf("%d", data.Amount)), ), ) @@ -338,7 +339,8 @@ func (am AppModule) OnAcknowledgementPacket( types.EventTypePacket, sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), sdk.NewAttribute(types.AttributeKeyReceiver, data.Receiver), - sdk.NewAttribute(types.AttributeKeyValue, data.Amount.String()), + sdk.NewAttribute(types.AttributeKeyDenom, data.Denom), + sdk.NewAttribute(types.AttributeKeyAmount, fmt.Sprintf("%d", data.Amount)), sdk.NewAttribute(types.AttributeKeyAckSuccess, fmt.Sprintf("%t", ack.Success)), ), ) @@ -375,7 +377,8 @@ func (am AppModule) OnTimeoutPacket( types.EventTypeTimeout, sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), sdk.NewAttribute(types.AttributeKeyRefundReceiver, data.Sender), - sdk.NewAttribute(types.AttributeKeyRefundValue, data.Amount.String()), + sdk.NewAttribute(types.AttributeKeyRefundDenom, data.Denom), + sdk.NewAttribute(types.AttributeKeyRefundAmount, fmt.Sprintf("%d", data.Amount)), ), ) diff --git a/x/ibc-transfer/spec/04_messages.md b/x/ibc-transfer/spec/04_messages.md index f644c71a62..1f81a0001b 100644 --- a/x/ibc-transfer/spec/04_messages.md +++ b/x/ibc-transfer/spec/04_messages.md @@ -3,3 +3,38 @@ order: 4 --> # Messages + +### MsgTransfer + +A fungible token cross chain transfer is achieved by using the `MsgTransfer`: + +```go +type MsgTransfer struct { + SourcePort string + SourceChannel string + Token sdk.Coin + Sender sdk.AccAddress + Receiver string + TimeoutHeight uint64 + TimeoutTimestamp uint64 +} +``` + +This message is expected to fail if: + +- `SourcePort` is invalid (see 24-host naming requirements) +- `SourceChannel` is invalid (see 24-host naming requirements) +- `Token` is invalid (denom is invalid or amount is negative) +- `Token.Amount` is not positive +- `Sender` is empty +- `Receiver` is empty +- `TimeoutHeight` and `TimeoutTimestamp` are both zero + +This message will send a fungible token to the counterparty chain represented +by the counterparty Channel End connected to the Channel End with the identifiers +`SourcePort` and `SourceChannel`. + +The denomination provided for transfer should correspond to the same denomination +represented on this chain. The prefixes will be added as necessary upon by the +receiving chain. + diff --git a/x/ibc-transfer/spec/05_events.md b/x/ibc-transfer/spec/05_events.md index 8c4fa93f67..15a694e5a2 100644 --- a/x/ibc-transfer/spec/05_events.md +++ b/x/ibc-transfer/spec/05_events.md @@ -19,7 +19,8 @@ order: 5 |-----------------------|---------------|-----------------| | fungible_token_packet | module | transfer | | fungible_token_packet | receiver | {receiver} | -| fungible_token_packet | value | {amount} | +| fungible_token_packet | denom | {denom} | +| fungible_token_packet | amount | {amount} | ## OnAcknowledgePacket callback @@ -27,7 +28,8 @@ order: 5 |-----------------------|---------------|-----------------| | fungible_token_packet | module | transfer | | fungible_token_packet | receiver | {receiver} | -| fungible_token_packet | value | {amount} | +| fungible_token_packet | denom | {denom} | +| fungible_token_packet | amount | {amount} | | fungible_token_packet | success | {ackSuccess} | ## OnTimeoutPacket callback @@ -36,8 +38,8 @@ order: 5 |-----------------------|-----------------|-----------------| | fungible_token_packet | module | transfer | | fungible_token_packet | refund_receiver | {receiver} | -| fungible_token_packet | value | {amount} | - +| fungible_token_packet | denom | {denom} | +| fungible_token_packet | amount | {amount} | diff --git a/x/ibc-transfer/types/coin.go b/x/ibc-transfer/types/coin.go new file mode 100644 index 0000000000..65b4e91d19 --- /dev/null +++ b/x/ibc-transfer/types/coin.go @@ -0,0 +1,52 @@ +package types + +import ( + "fmt" + "strings" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// SenderChainIsSource returns false if the denomination originally came +// from the receiving chain and true otherwise. +func SenderChainIsSource(sourcePort, sourceChannel, denom string) bool { + // 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. + + return !ReceiverChainIsSource(sourcePort, sourceChannel, denom) +} + +// ReceiverChainIsSource returns true if the denomination originally came +// from the receiving chain and false otherwise. +func ReceiverChainIsSource(sourcePort, sourceChannel, denom string) bool { + // The prefix passed in should contain the SourcePort and SourceChannel. + // If the receiver chain originally sent the token to the sender chain + // the denom will have the sender's SourcePort and SourceChannel as the + // prefix. + + voucherPrefix := GetDenomPrefix(sourcePort, sourceChannel) + return strings.HasPrefix(denom, voucherPrefix) + +} + +// GetDenomPrefix returns the receiving denomination prefix +func GetDenomPrefix(portID, channelID string) string { + return fmt.Sprintf("%s/%s/", portID, channelID) +} + +// GetPrefixedDenom returns the denomination with the portID and channelID prefixed +func GetPrefixedDenom(portID, channelID, baseDenom string) string { + return fmt.Sprintf("%s/%s/%s", portID, channelID, baseDenom) +} + +// GetPrefixedCoin creates a copy of the given coin with the prefixed denom +func GetPrefixedCoin(portID, channelID string, coin sdk.Coin) sdk.Coin { + return sdk.NewCoin(GetPrefixedDenom(portID, channelID, coin.Denom), coin.Amount) +} + +// GetTransferCoin creates a transfer coin with the port ID and channel ID +// prefixed to the base denom. +func GetTransferCoin(portID, channelID, baseDenom string, amount int64) sdk.Coin { + return sdk.NewInt64Coin(GetPrefixedDenom(portID, channelID, baseDenom), amount) +} diff --git a/x/ibc-transfer/types/errors.go b/x/ibc-transfer/types/errors.go index 625f5cc776..6c1da2fb46 100644 --- a/x/ibc-transfer/types/errors.go +++ b/x/ibc-transfer/types/errors.go @@ -7,7 +7,7 @@ import ( // IBC channel sentinel errors var ( ErrInvalidPacketTimeout = sdkerrors.Register(ModuleName, 2, "invalid packet timeout") - ErrOnlyOneDenomAllowed = sdkerrors.Register(ModuleName, 3, "only one denom allowed") - ErrInvalidDenomForTransfer = sdkerrors.Register(ModuleName, 4, "invalid denomination for cross-chain transfer") - ErrInvalidVersion = sdkerrors.Register(ModuleName, 5, "invalid ICS20 version") + ErrInvalidDenomForTransfer = sdkerrors.Register(ModuleName, 3, "invalid denomination for cross-chain transfer") + ErrInvalidVersion = sdkerrors.Register(ModuleName, 4, "invalid ICS20 version") + ErrInvalidAmount = sdkerrors.Register(ModuleName, 5, "invalid token amount") ) diff --git a/x/ibc-transfer/types/events.go b/x/ibc-transfer/types/events.go index f9f27f4f2f..900460a505 100644 --- a/x/ibc-transfer/types/events.go +++ b/x/ibc-transfer/types/events.go @@ -8,9 +8,11 @@ const ( EventTypeChannelClose = "channel_closed" AttributeKeyReceiver = "receiver" - AttributeKeyValue = "value" + AttributeKeyDenom = "denom" + AttributeKeyAmount = "amount" AttributeKeyRefundReceiver = "refund_receiver" - AttributeKeyRefundValue = "refund_value" + AttributeKeyRefundDenom = "refund_denom" + AttributeKeyRefundAmount = "refund_amount" AttributeKeyAckSuccess = "success" AttributeKeyAckError = "error" ) diff --git a/x/ibc-transfer/types/keys.go b/x/ibc-transfer/types/keys.go index bc66ebcad1..ebeb2d9fa2 100644 --- a/x/ibc-transfer/types/keys.go +++ b/x/ibc-transfer/types/keys.go @@ -1,8 +1,6 @@ package types import ( - "fmt" - "github.com/tendermint/tendermint/crypto" sdk "github.com/cosmos/cosmos-sdk/types" @@ -40,17 +38,3 @@ var PortKey = []byte{0x01} func GetEscrowAddress(portID, channelID string) sdk.AccAddress { return sdk.AccAddress(crypto.AddressHash([]byte(portID + channelID))) } - -// GetDenomPrefix returns the receiving denomination prefix -func GetDenomPrefix(portID, channelID string) string { - return fmt.Sprintf("%s/%s/", portID, channelID) -} - -// GetPrefixedCoins creates a copy of the given coins with the denom updated with the prefix. -func GetPrefixedCoins(portID, channelID string, coins ...sdk.Coin) sdk.Coins { - prefixedCoins := make(sdk.Coins, len(coins)) - for i := range coins { - prefixedCoins[i] = sdk.NewCoin(GetDenomPrefix(portID, channelID)+coins[i].Denom, coins[i].Amount) - } - return prefixedCoins -} diff --git a/x/ibc-transfer/types/msgs.go b/x/ibc-transfer/types/msgs.go index ad352a2c81..dc6473d868 100644 --- a/x/ibc-transfer/types/msgs.go +++ b/x/ibc-transfer/types/msgs.go @@ -14,13 +14,13 @@ const ( // NewMsgTransfer creates a new MsgTransfer instance func NewMsgTransfer( sourcePort, sourceChannel string, - amount sdk.Coins, sender sdk.AccAddress, receiver string, + token sdk.Coin, sender sdk.AccAddress, receiver string, timeoutHeight, timeoutTimestamp uint64, ) *MsgTransfer { return &MsgTransfer{ SourcePort: sourcePort, SourceChannel: sourceChannel, - Amount: amount, + Token: token, Sender: sender, Receiver: receiver, TimeoutHeight: timeoutHeight, @@ -39,7 +39,7 @@ func (MsgTransfer) Type() string { } // ValidateBasic performs a basic check of the MsgTransfer fields. -// NOTE: timeout height and timestamp values can be 0 to disable the timeout. +// NOTE: timeout height or timestamp values can be 0 to disable the timeout. func (msg MsgTransfer) ValidateBasic() error { if err := host.PortIdentifierValidator(msg.SourcePort); err != nil { return sdkerrors.Wrap(err, "invalid source port ID") @@ -47,11 +47,11 @@ func (msg MsgTransfer) ValidateBasic() error { if err := host.ChannelIdentifierValidator(msg.SourceChannel); err != nil { return sdkerrors.Wrap(err, "invalid source channel ID") } - if !msg.Amount.IsAllPositive() { - return sdkerrors.Wrap(sdkerrors.ErrInsufficientFunds, msg.Amount.String()) + if !msg.Token.IsValid() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, msg.Token.String()) } - if !msg.Amount.IsValid() { - return sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, msg.Amount.String()) + if !msg.Token.IsPositive() { + return sdkerrors.Wrap(sdkerrors.ErrInsufficientFunds, msg.Token.String()) } if msg.Sender.Empty() { return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "missing sender address") @@ -59,7 +59,12 @@ func (msg MsgTransfer) ValidateBasic() error { if msg.Receiver == "" { return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "missing recipient address") } - return nil + + // sanity check that validate basic on fungible token packet passes + // NOTE: this should always pass since validation checks should be the + // same. Please open an issue if you encounter an error on this line. + packet := NewFungibleTokenPacketData(msg.Token.Denom, msg.Token.Amount.Uint64(), msg.Sender.String(), msg.Receiver) + return packet.ValidateBasic() } // GetSignBytes implements sdk.Msg diff --git a/x/ibc-transfer/types/msgs_test.go b/x/ibc-transfer/types/msgs_test.go index ed20aac257..954c3f6ef1 100644 --- a/x/ibc-transfer/types/msgs_test.go +++ b/x/ibc-transfer/types/msgs_test.go @@ -27,82 +27,68 @@ var ( addr2 = sdk.AccAddress("testaddr2").String() emptyAddr sdk.AccAddress - coins, _ = sdk.ParseCoins("100atom") - invalidDenomCoins = sdk.Coins{sdk.Coin{Denom: "ato-m", Amount: sdk.NewInt(100)}} - negativeCoins = sdk.Coins{sdk.Coin{Denom: "atom", Amount: sdk.NewInt(100)}, sdk.Coin{Denom: "atoms", Amount: sdk.NewInt(-100)}} + coin = sdk.NewCoin("atom", sdk.NewInt(100)) + invalidDenomCoin = sdk.Coin{Denom: "ato-m", Amount: sdk.NewInt(100)} + negativeCoin = sdk.Coin{Denom: "atoms", Amount: sdk.NewInt(-100)} ) // TestMsgTransferRoute tests Route for MsgTransfer func TestMsgTransferRoute(t *testing.T) { - msg := NewMsgTransfer(validPort, validChannel, coins, addr1, addr2, 10, 0) + msg := NewMsgTransfer(validPort, validChannel, coin, addr1, addr2, 10, 0) require.Equal(t, RouterKey, msg.Route()) } // TestMsgTransferType tests Type for MsgTransfer func TestMsgTransferType(t *testing.T) { - msg := NewMsgTransfer(validPort, validChannel, coins, addr1, addr2, 10, 0) + msg := NewMsgTransfer(validPort, validChannel, coin, addr1, addr2, 10, 0) require.Equal(t, "transfer", msg.Type()) } // TestMsgTransferValidation tests ValidateBasic for MsgTransfer func TestMsgTransferValidation(t *testing.T) { - testMsgs := []*MsgTransfer{ - NewMsgTransfer(validPort, validChannel, coins, addr1, addr2, 10, 0), // valid msg - NewMsgTransfer(invalidShortPort, validChannel, coins, addr1, addr2, 10, 0), // too short port id - NewMsgTransfer(invalidLongPort, validChannel, coins, addr1, addr2, 10, 0), // too long port id - NewMsgTransfer(invalidPort, validChannel, coins, addr1, addr2, 10, 0), // port id contains non-alpha - NewMsgTransfer(validPort, invalidShortChannel, coins, addr1, addr2, 10, 0), // too short channel id - NewMsgTransfer(validPort, invalidLongChannel, coins, addr1, addr2, 10, 0), // too long channel id - NewMsgTransfer(validPort, invalidChannel, coins, addr1, addr2, 10, 0), // channel id contains non-alpha - NewMsgTransfer(validPort, validChannel, invalidDenomCoins, addr1, addr2, 10, 0), // invalid amount - NewMsgTransfer(validPort, validChannel, negativeCoins, addr1, addr2, 10, 0), // amount contains negative coin - NewMsgTransfer(validPort, validChannel, coins, emptyAddr, addr2, 10, 0), // missing sender address - NewMsgTransfer(validPort, validChannel, coins, addr1, "", 10, 0), // missing recipient address - NewMsgTransfer(validPort, validChannel, sdk.Coins{}, addr1, addr2, 10, 0), // not possitive coin - } - testCases := []struct { + name string msg *MsgTransfer expPass bool - errMsg string }{ - {testMsgs[0], true, ""}, - {testMsgs[1], false, "too short port id"}, - {testMsgs[2], false, "too long port id"}, - {testMsgs[3], false, "port id contains non-alpha"}, - {testMsgs[4], false, "too short channel id"}, - {testMsgs[5], false, "too long channel id"}, - {testMsgs[6], false, "channel id contains non-alpha"}, - {testMsgs[7], false, "invalid amount"}, - {testMsgs[8], false, "amount contains negative coin"}, - {testMsgs[9], false, "missing sender address"}, - {testMsgs[10], false, "missing recipient address"}, + {"valid msg", NewMsgTransfer(validPort, validChannel, coin, addr1, addr2, 10, 0), true}, + {"too short port id", NewMsgTransfer(invalidShortPort, validChannel, coin, addr1, addr2, 10, 0), false}, + {"too long port id", NewMsgTransfer(invalidLongPort, validChannel, coin, addr1, addr2, 10, 0), false}, + {"port id contains non-alpha", NewMsgTransfer(invalidPort, validChannel, coin, addr1, addr2, 10, 0), false}, + {"too short channel id", NewMsgTransfer(validPort, invalidShortChannel, coin, addr1, addr2, 10, 0), false}, + {"too long channel id", NewMsgTransfer(validPort, invalidLongChannel, coin, addr1, addr2, 10, 0), false}, + {"channel id contains non-alpha", NewMsgTransfer(validPort, invalidChannel, coin, addr1, addr2, 10, 0), false}, + {"invalid amount", NewMsgTransfer(validPort, validChannel, invalidDenomCoin, addr1, addr2, 10, 0), false}, + {"negative coin", NewMsgTransfer(validPort, validChannel, negativeCoin, addr1, addr2, 10, 0), false}, + {"missing sender address", NewMsgTransfer(validPort, validChannel, coin, emptyAddr, addr2, 10, 0), false}, + {"missing recipient address", NewMsgTransfer(validPort, validChannel, coin, addr1, "", 10, 0), false}, + {"empty coin", NewMsgTransfer(validPort, validChannel, sdk.Coin{}, addr1, addr2, 10, 0), false}, } for i, tc := range testCases { err := tc.msg.ValidateBasic() if tc.expPass { - require.NoError(t, err, "Msg %d failed: %v", i, err) + require.NoError(t, err, "valid test case %d failed: %v", i, err) } else { - require.Error(t, err, "Invalid Msg %d passed: %s", i, tc.errMsg) + require.Error(t, err, "invalid test case %d passed: %s", i, tc.name) } } } // TestMsgTransferGetSignBytes tests GetSignBytes for MsgTransfer func TestMsgTransferGetSignBytes(t *testing.T) { - msg := NewMsgTransfer(validPort, validChannel, coins, addr1, addr2, 110, 10) + msg := NewMsgTransfer(validPort, validChannel, coin, addr1, addr2, 110, 10) res := msg.GetSignBytes() - expected := `{"type":"cosmos-sdk/MsgTransfer","value":{"amount":[{"amount":"100","denom":"atom"}],"receiver":"cosmos1w3jhxarpv3j8yvs7f9y7g","sender":"cosmos1w3jhxarpv3j8yvg4ufs4x","source_channel":"testchannel","source_port":"testportid","timeout_height":"110","timeout_timestamp":"10"}}` + expected := `{"type":"cosmos-sdk/MsgTransfer","value":{"receiver":"cosmos1w3jhxarpv3j8yvs7f9y7g","sender":"cosmos1w3jhxarpv3j8yvg4ufs4x","source_channel":"testchannel","source_port":"testportid","timeout_height":"110","timeout_timestamp":"10","token":{"amount":"100","denom":"atom"}}}` require.Equal(t, expected, string(res)) } // TestMsgTransferGetSigners tests GetSigners for MsgTransfer func TestMsgTransferGetSigners(t *testing.T) { - msg := NewMsgTransfer(validPort, validChannel, coins, addr1, addr2, 10, 0) + msg := NewMsgTransfer(validPort, validChannel, coin, addr1, addr2, 10, 0) res := msg.GetSigners() expected := "[746573746164647231]" diff --git a/x/ibc-transfer/types/packet.go b/x/ibc-transfer/types/packet.go index 9386a5bf54..da40286e98 100644 --- a/x/ibc-transfer/types/packet.go +++ b/x/ibc-transfer/types/packet.go @@ -23,8 +23,11 @@ var ( // NewFungibleTokenPacketData contructs a new FungibleTokenPacketData instance func NewFungibleTokenPacketData( - amount sdk.Coins, sender, receiver string) FungibleTokenPacketData { + denom string, amount uint64, + sender, receiver string, +) FungibleTokenPacketData { return FungibleTokenPacketData{ + Denom: denom, Amount: amount, Sender: sender, Receiver: receiver, @@ -33,11 +36,11 @@ func NewFungibleTokenPacketData( // ValidateBasic is used for validating the token transfer func (ftpd FungibleTokenPacketData) ValidateBasic() error { - if !ftpd.Amount.IsAllPositive() { - return sdkerrors.Wrap(sdkerrors.ErrInsufficientFunds, ftpd.Amount.String()) + if strings.TrimSpace(ftpd.Denom) == "" { + return sdkerrors.Wrap(ErrInvalidDenomForTransfer, "denom cannot be empty") } - if !ftpd.Amount.IsValid() { - return sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, ftpd.Amount.String()) + if ftpd.Amount == 0 { + return sdkerrors.Wrap(ErrInvalidAmount, "amount cannot be 0") } if strings.TrimSpace(ftpd.Sender) == "" { return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "sender address cannot be blank") diff --git a/x/ibc-transfer/types/packet_test.go b/x/ibc-transfer/types/packet_test.go index 8602dff7d0..1edcb093d3 100644 --- a/x/ibc-transfer/types/packet_test.go +++ b/x/ibc-transfer/types/packet_test.go @@ -6,34 +6,31 @@ import ( "github.com/stretchr/testify/require" ) +const ( + denom = "transfer/gaiachannel/atom" + amount = uint64(100) +) + // TestFungibleTokenPacketDataValidateBasic tests ValidateBasic for FungibleTokenPacketData func TestFungibleTokenPacketDataValidateBasic(t *testing.T) { - testPacketDataTransfer := []FungibleTokenPacketData{ - NewFungibleTokenPacketData(coins, addr1.String(), addr2), // valid msg - NewFungibleTokenPacketData(invalidDenomCoins, addr1.String(), addr2), // invalid amount - NewFungibleTokenPacketData(negativeCoins, addr1.String(), addr2), // amount contains negative coin - NewFungibleTokenPacketData(coins, emptyAddr.String(), addr2), // missing sender address - NewFungibleTokenPacketData(coins, addr1.String(), emptyAddr.String()), // missing recipient address - } - testCases := []struct { + name string packetData FungibleTokenPacketData expPass bool - errMsg string }{ - {testPacketDataTransfer[0], true, ""}, - {testPacketDataTransfer[1], false, "invalid amount"}, - {testPacketDataTransfer[2], false, "amount contains negative coin"}, - {testPacketDataTransfer[3], false, "missing sender address"}, - {testPacketDataTransfer[4], false, "missing recipient address"}, + {"valid packet", NewFungibleTokenPacketData(denom, amount, addr1.String(), addr2), true}, + {"invalid denom", NewFungibleTokenPacketData("", amount, addr1.String(), addr2), false}, + {"invalid amount", NewFungibleTokenPacketData(denom, 0, addr1.String(), addr2), false}, + {"missing sender address", NewFungibleTokenPacketData(denom, amount, emptyAddr.String(), addr2), false}, + {"missing recipient address", NewFungibleTokenPacketData(denom, amount, addr1.String(), emptyAddr.String()), false}, } for i, tc := range testCases { err := tc.packetData.ValidateBasic() if tc.expPass { - require.NoError(t, err, "PacketDataTransfer %d failed: %v", i, err) + require.NoError(t, err, "valid test case %d failed: %v", i, err) } else { - require.Error(t, err, "Invalid PacketDataTransfer %d passed: %s", i, tc.errMsg) + require.Error(t, err, "invalid test case %d passed: %s", i, tc.name) } } } diff --git a/x/ibc-transfer/types/transfer.pb.go b/x/ibc-transfer/types/transfer.pb.go index 6df49844da..c7c6ac6acc 100644 --- a/x/ibc-transfer/types/transfer.pb.go +++ b/x/ibc-transfer/types/transfer.pb.go @@ -25,15 +25,16 @@ var _ = math.Inf // proto package needs to be updated. const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package -// MsgTransfer defines a msg to transfer fungible tokens (i.e Coins) between ICS20 enabled chains. -// See ICS Spec here: https://github.com/cosmos/ics/tree/master/spec/ics-020-fungible-token-transfer#data-structures +// MsgTransfer defines a msg to transfer fungible tokens (i.e Coins) between +// ICS20 enabled chains. See ICS Spec here: +// https://github.com/cosmos/ics/tree/master/spec/ics-020-fungible-token-transfer#data-structures type MsgTransfer struct { // the port on which the packet will be sent SourcePort string `protobuf:"bytes,1,opt,name=source_port,json=sourcePort,proto3" json:"source_port,omitempty" yaml:"source_port"` // the channel by which the packet will be sent SourceChannel string `protobuf:"bytes,2,opt,name=source_channel,json=sourceChannel,proto3" json:"source_channel,omitempty" yaml:"source_channel"` // the tokens to be transferred - Amount github_com_cosmos_cosmos_sdk_types.Coins `protobuf:"bytes,3,rep,name=amount,proto3,castrepeated=github.com/cosmos/cosmos-sdk/types.Coins" json:"amount"` + Token types.Coin `protobuf:"bytes,3,opt,name=token,proto3" json:"token"` // the sender address Sender github_com_cosmos_cosmos_sdk_types.AccAddress `protobuf:"bytes,4,opt,name=sender,proto3,casttype=github.com/cosmos/cosmos-sdk/types.AccAddress" json:"sender,omitempty"` // the recipient address on the destination chain @@ -93,11 +94,11 @@ func (m *MsgTransfer) GetSourceChannel() string { return "" } -func (m *MsgTransfer) GetAmount() github_com_cosmos_cosmos_sdk_types.Coins { +func (m *MsgTransfer) GetToken() types.Coin { if m != nil { - return m.Amount + return m.Token } - return nil + return types.Coin{} } func (m *MsgTransfer) GetSender() github_com_cosmos_cosmos_sdk_types.AccAddress { @@ -129,14 +130,17 @@ func (m *MsgTransfer) GetTimeoutTimestamp() uint64 { } // FungibleTokenPacketData defines a struct for the packet payload -// See FungibleTokenPacketData spec: https://github.com/cosmos/ics/tree/master/spec/ics-020-fungible-token-transfer#data-structures +// See FungibleTokenPacketData spec: +// https://github.com/cosmos/ics/tree/master/spec/ics-020-fungible-token-transfer#data-structures type FungibleTokenPacketData struct { - // the tokens to be transferred - Amount github_com_cosmos_cosmos_sdk_types.Coins `protobuf:"bytes,1,rep,name=amount,proto3,castrepeated=github.com/cosmos/cosmos-sdk/types.Coins" json:"amount"` + // the token denomination to be transferred + Denom string `protobuf:"bytes,1,opt,name=denom,proto3" json:"denom,omitempty"` + // the token amount to be transferred + Amount uint64 `protobuf:"varint,2,opt,name=amount,proto3" json:"amount,omitempty"` // the sender address - Sender string `protobuf:"bytes,2,opt,name=sender,proto3" json:"sender,omitempty"` + Sender string `protobuf:"bytes,3,opt,name=sender,proto3" json:"sender,omitempty"` // the recipient address on the destination chain - Receiver string `protobuf:"bytes,3,opt,name=receiver,proto3" json:"receiver,omitempty"` + Receiver string `protobuf:"bytes,4,opt,name=receiver,proto3" json:"receiver,omitempty"` } func (m *FungibleTokenPacketData) Reset() { *m = FungibleTokenPacketData{} } @@ -172,11 +176,18 @@ func (m *FungibleTokenPacketData) XXX_DiscardUnknown() { var xxx_messageInfo_FungibleTokenPacketData proto.InternalMessageInfo -func (m *FungibleTokenPacketData) GetAmount() github_com_cosmos_cosmos_sdk_types.Coins { +func (m *FungibleTokenPacketData) GetDenom() string { + if m != nil { + return m.Denom + } + return "" +} + +func (m *FungibleTokenPacketData) GetAmount() uint64 { if m != nil { return m.Amount } - return nil + return 0 } func (m *FungibleTokenPacketData) GetSender() string { @@ -193,9 +204,10 @@ func (m *FungibleTokenPacketData) GetReceiver() string { return "" } -// FungibleTokenPacketAcknowledgement contains a boolean success flag and an optional error msg -// error msg is empty string on success -// See spec for onAcknowledgePacket: https://github.com/cosmos/ics/tree/master/spec/ics-020-fungible-token-transfer#packet-relay +// FungibleTokenPacketAcknowledgement contains a boolean success flag and an +// optional error msg error msg is empty string on success See spec for +// onAcknowledgePacket: +// https://github.com/cosmos/ics/tree/master/spec/ics-020-fungible-token-transfer#packet-relay type FungibleTokenPacketAcknowledgement struct { Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"` @@ -257,38 +269,38 @@ func init() { func init() { proto.RegisterFile("ibc/transfer/transfer.proto", fileDescriptor_08134a70fd29e656) } var fileDescriptor_08134a70fd29e656 = []byte{ - // 484 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x53, 0x3f, 0x6f, 0xd3, 0x40, - 0x1c, 0x8d, 0x49, 0x9b, 0x96, 0x4b, 0x8a, 0xe0, 0x28, 0xe5, 0x08, 0xc8, 0x8e, 0x3c, 0x79, 0x49, - 0x4c, 0x61, 0x40, 0x62, 0x22, 0x29, 0x42, 0x54, 0x08, 0xa9, 0xb2, 0x22, 0x06, 0x96, 0xca, 0x3e, - 0xff, 0x70, 0x4e, 0x89, 0xef, 0xa2, 0xbb, 0x33, 0xd0, 0x6f, 0xc1, 0x97, 0x60, 0xe1, 0x93, 0x74, - 0xec, 0xc0, 0xc0, 0x64, 0x50, 0xf2, 0x0d, 0x32, 0x32, 0x21, 0xff, 0x89, 0x89, 0xa5, 0x08, 0x31, - 0x74, 0xba, 0x7b, 0xef, 0xfd, 0x7e, 0xcf, 0xf7, 0x7b, 0xbe, 0x43, 0x0f, 0x59, 0x40, 0x5d, 0x2d, - 0x7d, 0xae, 0x3e, 0x80, 0xac, 0x36, 0x83, 0xb9, 0x14, 0x5a, 0xe0, 0x0e, 0x0b, 0xe8, 0x60, 0xcd, - 0x75, 0x0f, 0x23, 0x11, 0x89, 0x5c, 0x70, 0xb3, 0x5d, 0x51, 0xd3, 0xbd, 0x4b, 0x85, 0x8a, 0x85, - 0x72, 0x8b, 0xa5, 0x20, 0xed, 0xef, 0x4d, 0xd4, 0x7e, 0xab, 0xa2, 0x71, 0xd9, 0x8a, 0x9f, 0xa1, - 0xb6, 0x12, 0x89, 0xa4, 0x70, 0x3e, 0x17, 0x52, 0x13, 0xa3, 0x67, 0x38, 0x37, 0x47, 0x47, 0xab, - 0xd4, 0xc2, 0x17, 0x7e, 0x3c, 0x7b, 0x6e, 0x6f, 0x88, 0xb6, 0x87, 0x0a, 0x74, 0x26, 0xa4, 0xc6, - 0x2f, 0xd0, 0xad, 0x52, 0xa3, 0x13, 0x9f, 0x73, 0x98, 0x91, 0x1b, 0x79, 0xef, 0x83, 0x55, 0x6a, - 0xdd, 0xab, 0xf5, 0x96, 0xba, 0xed, 0x1d, 0x14, 0xc4, 0x49, 0x81, 0xf1, 0x3b, 0xd4, 0xf2, 0x63, - 0x91, 0x70, 0x4d, 0x9a, 0xbd, 0xa6, 0xd3, 0x7e, 0xd2, 0x19, 0x94, 0x27, 0x3d, 0x11, 0x8c, 0x8f, - 0x1e, 0x5f, 0xa6, 0x56, 0xe3, 0xdb, 0x4f, 0xcb, 0x89, 0x98, 0x9e, 0x24, 0xc1, 0x80, 0x8a, 0xd8, - 0xad, 0x0d, 0xd4, 0x57, 0xe1, 0xd4, 0xd5, 0x17, 0x73, 0x28, 0x1a, 0x94, 0x57, 0xba, 0xe1, 0x53, - 0xd4, 0x52, 0xc0, 0x43, 0x90, 0x64, 0xa7, 0x67, 0x38, 0x9d, 0xd1, 0xf1, 0xef, 0xd4, 0xea, 0xff, - 0x87, 0xcb, 0x90, 0xd2, 0x61, 0x18, 0x4a, 0x50, 0xca, 0x2b, 0x0d, 0x70, 0x17, 0xed, 0x4b, 0xa0, - 0xc0, 0x3e, 0x82, 0x24, 0xbb, 0xd9, 0x78, 0x5e, 0x85, 0xb3, 0x00, 0x34, 0x8b, 0x41, 0x24, 0xfa, - 0x7c, 0x02, 0x2c, 0x9a, 0x68, 0xd2, 0xea, 0x19, 0xce, 0xce, 0x66, 0x00, 0x75, 0xdd, 0xf6, 0x0e, - 0x4a, 0xe2, 0x75, 0x8e, 0xf1, 0x29, 0xba, 0xb3, 0xae, 0xc8, 0x56, 0xa5, 0xfd, 0x78, 0x4e, 0xf6, - 0x72, 0x93, 0x47, 0xab, 0xd4, 0x22, 0x75, 0x93, 0xaa, 0xc4, 0xf6, 0x6e, 0x97, 0xdc, 0xb8, 0xa2, - 0xbe, 0x1a, 0xe8, 0xfe, 0xab, 0x84, 0x47, 0x2c, 0x98, 0xc1, 0x58, 0x4c, 0x81, 0x9f, 0xf9, 0x74, - 0x0a, 0xfa, 0xa5, 0xaf, 0xfd, 0x8d, 0x9c, 0x8d, 0x6b, 0xcd, 0xf9, 0xa8, 0xca, 0x39, 0xff, 0xf3, - 0x5b, 0x43, 0x6b, 0xd6, 0x43, 0xb3, 0xc7, 0xc8, 0xde, 0x72, 0xcc, 0x21, 0x9d, 0x72, 0xf1, 0x69, - 0x06, 0x61, 0x04, 0x31, 0x70, 0x8d, 0x09, 0xda, 0x53, 0x09, 0xa5, 0xa0, 0x54, 0x7e, 0x21, 0xf7, - 0xbd, 0x35, 0xc4, 0x87, 0x68, 0x17, 0xa4, 0x14, 0xeb, 0x4f, 0x16, 0x60, 0xf4, 0xe6, 0x72, 0x61, - 0x1a, 0x57, 0x0b, 0xd3, 0xf8, 0xb5, 0x30, 0x8d, 0x2f, 0x4b, 0xb3, 0x71, 0xb5, 0x34, 0x1b, 0x3f, - 0x96, 0x66, 0xe3, 0xfd, 0xf1, 0x3f, 0xa7, 0xfa, 0xec, 0xb2, 0x80, 0xf6, 0xff, 0xbe, 0xb1, 0x6c, - 0xc8, 0xa0, 0x95, 0x3f, 0x94, 0xa7, 0x7f, 0x02, 0x00, 0x00, 0xff, 0xff, 0x6a, 0x95, 0x43, 0xad, - 0x80, 0x03, 0x00, 0x00, + // 485 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x53, 0xcf, 0x6e, 0xd3, 0x30, + 0x18, 0x6f, 0x68, 0xda, 0x0d, 0xb7, 0x43, 0x60, 0xc6, 0x08, 0x05, 0x25, 0x55, 0x4e, 0xb9, 0x34, + 0xd1, 0xe0, 0x80, 0xc4, 0x89, 0x76, 0x08, 0x31, 0x21, 0xa4, 0xc9, 0xea, 0x89, 0xcb, 0x94, 0x38, + 0x1f, 0x69, 0xd4, 0xc6, 0xae, 0x6c, 0x07, 0x98, 0x78, 0x09, 0x9e, 0x82, 0x67, 0xd9, 0x71, 0x47, + 0x4e, 0x11, 0x6a, 0xdf, 0xa0, 0x47, 0x4e, 0x28, 0x71, 0x5a, 0x16, 0xa9, 0xda, 0xc9, 0xfe, 0xfd, + 0xf9, 0xbe, 0x7c, 0x7f, 0x1c, 0xf4, 0x3c, 0x8d, 0x68, 0xa0, 0x44, 0xc8, 0xe4, 0x17, 0x10, 0xbb, + 0x8b, 0xbf, 0x14, 0x5c, 0x71, 0xdc, 0x4f, 0x23, 0xea, 0x6f, 0xb9, 0xc1, 0x71, 0xc2, 0x13, 0x5e, + 0x09, 0x41, 0x79, 0xd3, 0x9e, 0xc1, 0x63, 0xca, 0x65, 0xc6, 0x65, 0xa0, 0x0f, 0x4d, 0xba, 0xbf, + 0xda, 0xa8, 0xf7, 0x49, 0x26, 0xd3, 0x3a, 0x14, 0xbf, 0x46, 0x3d, 0xc9, 0x73, 0x41, 0xe1, 0x72, + 0xc9, 0x85, 0xb2, 0x8c, 0xa1, 0xe1, 0xdd, 0x9f, 0x9c, 0x6c, 0x0a, 0x07, 0x5f, 0x85, 0xd9, 0xe2, + 0x8d, 0x7b, 0x4b, 0x74, 0x09, 0xd2, 0xe8, 0x82, 0x0b, 0x85, 0xdf, 0xa2, 0x07, 0xb5, 0x46, 0x67, + 0x21, 0x63, 0xb0, 0xb0, 0xee, 0x55, 0xb1, 0xcf, 0x36, 0x85, 0xf3, 0xa4, 0x11, 0x5b, 0xeb, 0x2e, + 0x39, 0xd2, 0xc4, 0x99, 0xc6, 0xd8, 0x43, 0x1d, 0xc5, 0xe7, 0xc0, 0xac, 0xf6, 0xd0, 0xf0, 0x7a, + 0x2f, 0xfb, 0x7e, 0x5d, 0xe8, 0x19, 0x4f, 0xd9, 0xc4, 0xbc, 0x2e, 0x9c, 0x16, 0xd1, 0x06, 0x7c, + 0x8e, 0xba, 0x12, 0x58, 0x0c, 0xc2, 0x32, 0x87, 0x86, 0xd7, 0x9f, 0x9c, 0xfe, 0x2d, 0x9c, 0x51, + 0x92, 0xaa, 0x59, 0x1e, 0xf9, 0x94, 0x67, 0x41, 0xa3, 0xd1, 0x91, 0x8c, 0xe7, 0x81, 0xba, 0x5a, + 0x82, 0xf4, 0xc7, 0x94, 0x8e, 0xe3, 0x58, 0x80, 0x94, 0xa4, 0x4e, 0x80, 0x07, 0xe8, 0x50, 0x00, + 0x85, 0xf4, 0x2b, 0x08, 0xab, 0x53, 0x16, 0x4c, 0x76, 0xb8, 0x6c, 0x49, 0xa5, 0x19, 0xf0, 0x5c, + 0x5d, 0xce, 0x20, 0x4d, 0x66, 0xca, 0xea, 0x0e, 0x0d, 0xcf, 0xbc, 0xdd, 0x52, 0x53, 0x77, 0xc9, + 0x51, 0x4d, 0x7c, 0xa8, 0x30, 0x3e, 0x47, 0x8f, 0xb6, 0x8e, 0xf2, 0x94, 0x2a, 0xcc, 0x96, 0xd6, + 0x41, 0x95, 0xe4, 0xc5, 0xa6, 0x70, 0xac, 0x66, 0x92, 0x9d, 0xc5, 0x25, 0x0f, 0x6b, 0x6e, 0xba, + 0xa3, 0x7e, 0xa0, 0xa7, 0xef, 0x73, 0x96, 0xa4, 0xd1, 0x02, 0xa6, 0xe5, 0x10, 0x2e, 0x42, 0x3a, + 0x07, 0xf5, 0x2e, 0x54, 0x21, 0x3e, 0x46, 0x9d, 0x18, 0x18, 0xcf, 0xf4, 0xb6, 0x88, 0x06, 0xf8, + 0x04, 0x75, 0xc3, 0x8c, 0xe7, 0x4c, 0x55, 0x8b, 0x30, 0x49, 0x8d, 0x4a, 0xbe, 0x1e, 0x5e, 0xbb, + 0xb2, 0xef, 0x9b, 0x84, 0xd9, 0x9c, 0x84, 0x3b, 0x45, 0xee, 0x9e, 0x8f, 0x8f, 0xe9, 0x9c, 0xf1, + 0x6f, 0x0b, 0x88, 0x13, 0xc8, 0x80, 0x29, 0x6c, 0xa1, 0x03, 0x99, 0x53, 0x0a, 0x52, 0x56, 0x95, + 0x1c, 0x92, 0x2d, 0x2c, 0x2b, 0x04, 0x21, 0xb8, 0xd0, 0x6f, 0x82, 0x68, 0x30, 0xf9, 0x78, 0xbd, + 0xb2, 0x8d, 0x9b, 0x95, 0x6d, 0xfc, 0x59, 0xd9, 0xc6, 0xcf, 0xb5, 0xdd, 0xba, 0x59, 0xdb, 0xad, + 0xdf, 0x6b, 0xbb, 0xf5, 0xf9, 0xf4, 0xce, 0x65, 0x7e, 0x0f, 0xd2, 0x88, 0x8e, 0xfe, 0xff, 0x0a, + 0xe5, 0x6e, 0xa3, 0x6e, 0xf5, 0x9e, 0x5f, 0xfd, 0x0b, 0x00, 0x00, 0xff, 0xff, 0x1b, 0xc4, 0xe2, + 0x00, 0x27, 0x03, 0x00, 0x00, } func (m *MsgTransfer) Marshal() (dAtA []byte, err error) { @@ -335,20 +347,16 @@ func (m *MsgTransfer) MarshalToSizedBuffer(dAtA []byte) (int, error) { i-- dAtA[i] = 0x22 } - if len(m.Amount) > 0 { - for iNdEx := len(m.Amount) - 1; iNdEx >= 0; iNdEx-- { - { - size, err := m.Amount[iNdEx].MarshalToSizedBuffer(dAtA[:i]) - if err != nil { - return 0, err - } - i -= size - i = encodeVarintTransfer(dAtA, i, uint64(size)) - } - i-- - dAtA[i] = 0x1a + { + size, err := m.Token.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err } + i -= size + i = encodeVarintTransfer(dAtA, i, uint64(size)) } + i-- + dAtA[i] = 0x1a if len(m.SourceChannel) > 0 { i -= len(m.SourceChannel) copy(dAtA[i:], m.SourceChannel) @@ -391,28 +399,26 @@ func (m *FungibleTokenPacketData) MarshalToSizedBuffer(dAtA []byte) (int, error) copy(dAtA[i:], m.Receiver) i = encodeVarintTransfer(dAtA, i, uint64(len(m.Receiver))) i-- - dAtA[i] = 0x1a + dAtA[i] = 0x22 } if len(m.Sender) > 0 { i -= len(m.Sender) copy(dAtA[i:], m.Sender) i = encodeVarintTransfer(dAtA, i, uint64(len(m.Sender))) i-- - dAtA[i] = 0x12 + dAtA[i] = 0x1a } - if len(m.Amount) > 0 { - for iNdEx := len(m.Amount) - 1; iNdEx >= 0; iNdEx-- { - { - size, err := m.Amount[iNdEx].MarshalToSizedBuffer(dAtA[:i]) - if err != nil { - return 0, err - } - i -= size - i = encodeVarintTransfer(dAtA, i, uint64(size)) - } - i-- - dAtA[i] = 0xa - } + if m.Amount != 0 { + i = encodeVarintTransfer(dAtA, i, uint64(m.Amount)) + i-- + dAtA[i] = 0x10 + } + if len(m.Denom) > 0 { + i -= len(m.Denom) + copy(dAtA[i:], m.Denom) + i = encodeVarintTransfer(dAtA, i, uint64(len(m.Denom))) + i-- + dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -482,12 +488,8 @@ func (m *MsgTransfer) Size() (n int) { if l > 0 { n += 1 + l + sovTransfer(uint64(l)) } - if len(m.Amount) > 0 { - for _, e := range m.Amount { - l = e.Size() - n += 1 + l + sovTransfer(uint64(l)) - } - } + l = m.Token.Size() + n += 1 + l + sovTransfer(uint64(l)) l = len(m.Sender) if l > 0 { n += 1 + l + sovTransfer(uint64(l)) @@ -511,11 +513,12 @@ func (m *FungibleTokenPacketData) Size() (n int) { } var l int _ = l - if len(m.Amount) > 0 { - for _, e := range m.Amount { - l = e.Size() - n += 1 + l + sovTransfer(uint64(l)) - } + l = len(m.Denom) + if l > 0 { + n += 1 + l + sovTransfer(uint64(l)) + } + if m.Amount != 0 { + n += 1 + sovTransfer(uint64(m.Amount)) } l = len(m.Sender) if l > 0 { @@ -645,7 +648,7 @@ func (m *MsgTransfer) Unmarshal(dAtA []byte) error { iNdEx = postIndex case 3: if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Amount", wireType) + return fmt.Errorf("proto: wrong wireType = %d for field Token", wireType) } var msglen int for shift := uint(0); ; shift += 7 { @@ -672,8 +675,7 @@ func (m *MsgTransfer) Unmarshal(dAtA []byte) error { if postIndex > l { return io.ErrUnexpectedEOF } - m.Amount = append(m.Amount, types.Coin{}) - if err := m.Amount[len(m.Amount)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + if err := m.Token.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex @@ -836,9 +838,9 @@ func (m *FungibleTokenPacketData) Unmarshal(dAtA []byte) error { switch fieldNum { case 1: if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Amount", wireType) + return fmt.Errorf("proto: wrong wireType = %d for field Denom", wireType) } - var msglen int + var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowTransfer @@ -848,27 +850,44 @@ func (m *FungibleTokenPacketData) Unmarshal(dAtA []byte) error { } b := dAtA[iNdEx] iNdEx++ - msglen |= int(b&0x7F) << shift + stringLen |= uint64(b&0x7F) << shift if b < 0x80 { break } } - if msglen < 0 { + intStringLen := int(stringLen) + if intStringLen < 0 { return ErrInvalidLengthTransfer } - postIndex := iNdEx + msglen + postIndex := iNdEx + intStringLen if postIndex < 0 { return ErrInvalidLengthTransfer } if postIndex > l { return io.ErrUnexpectedEOF } - m.Amount = append(m.Amount, types.Coin{}) - if err := m.Amount[len(m.Amount)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { - return err - } + m.Denom = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex case 2: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Amount", wireType) + } + m.Amount = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTransfer + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Amount |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 3: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Sender", wireType) } @@ -900,7 +919,7 @@ func (m *FungibleTokenPacketData) Unmarshal(dAtA []byte) error { } m.Sender = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex - case 3: + case 4: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Receiver", wireType) } diff --git a/x/ibc/handler_test.go b/x/ibc/handler_test.go index c7482cccb2..8db8cd829e 100644 --- a/x/ibc/handler_test.go +++ b/x/ibc/handler_test.go @@ -326,8 +326,11 @@ func (suite *HandlerTestSuite) TestHandleTimeoutPacket() { clientA, clientB, connA, connB := suite.coordinator.SetupClientConnections(suite.chainA, suite.chainB, clientexported.Tendermint) channelA, channelB := suite.coordinator.CreateChannel(suite.chainA, suite.chainB, connA, connB, channeltypes.ORDERED) packet = channeltypes.NewPacket(suite.chainA.GetPacketData(suite.chainB), 1, channelA.PortID, channelA.ID, channelB.PortID, channelB.ID, uint64(suite.chainB.GetContext().BlockHeight()), uint64(suite.chainB.GetContext().BlockTime().UnixNano())) - err := suite.coordinator.SendPacket(suite.chainA, suite.chainB, packet, clientB) - suite.Require().NoError(err) + + // send from chainA to chainB + msg := ibctransfertypes.NewMsgTransfer(channelA.PortID, channelA.ID, ibctesting.TestCoin, suite.chainA.SenderAccount.GetAddress(), suite.chainB.SenderAccount.GetAddress().String(), packet.GetTimeoutHeight(), packet.GetTimeoutTimestamp()) + err := suite.coordinator.SendMsgs(suite.chainA, suite.chainB, clientB, msg) + suite.Require().NoError(err) // message committed // need to update chainA client to prove missing ack suite.coordinator.UpdateClient(suite.chainA, suite.chainB, clientA, clientexported.Tendermint) @@ -337,11 +340,15 @@ func (suite *HandlerTestSuite) TestHandleTimeoutPacket() { {"success: UNORDERED", func() { clientA, clientB, _, _, channelA, channelB := suite.coordinator.Setup(suite.chainA, suite.chainB) packet = channeltypes.NewPacket(suite.chainA.GetPacketData(suite.chainB), 1, channelA.PortID, channelA.ID, channelB.PortID, channelB.ID, uint64(suite.chainB.GetContext().BlockHeight()), uint64(suite.chainB.GetContext().BlockTime().UnixNano())) - err := suite.coordinator.SendPacket(suite.chainA, suite.chainB, packet, clientB) - suite.Require().NoError(err) + + // send from chainA to chainB + msg := ibctransfertypes.NewMsgTransfer(channelA.PortID, channelA.ID, ibctesting.TestCoin, suite.chainA.SenderAccount.GetAddress(), suite.chainB.SenderAccount.GetAddress().String(), packet.GetTimeoutHeight(), packet.GetTimeoutTimestamp()) + err := suite.coordinator.SendMsgs(suite.chainA, suite.chainB, clientB, msg) + suite.Require().NoError(err) // message committed // need to update chainA client to prove missing ack suite.coordinator.UpdateClient(suite.chainA, suite.chainB, clientA, clientexported.Tendermint) + packetKey = host.KeyPacketAcknowledgement(packet.GetDestPort(), packet.GetDestChannel(), packet.GetSequence()) }, true}, {"success: UNORDERED timeout out of order packet", func() { @@ -352,8 +359,10 @@ func (suite *HandlerTestSuite) TestHandleTimeoutPacket() { for i := uint64(1); i < 10; i++ { packet = channeltypes.NewPacket(suite.chainA.GetPacketData(suite.chainB), i, channelA.PortID, channelA.ID, channelB.PortID, channelB.ID, uint64(suite.chainB.GetContext().BlockHeight()), 0) - err := suite.coordinator.SendPacket(suite.chainA, suite.chainB, packet, clientB) - suite.Require().NoError(err) + // send from chainA to chainB + msg := ibctransfertypes.NewMsgTransfer(channelA.PortID, channelA.ID, ibctesting.TestCoin, suite.chainA.SenderAccount.GetAddress(), suite.chainB.SenderAccount.GetAddress().String(), packet.GetTimeoutHeight(), packet.GetTimeoutTimestamp()) + err := suite.coordinator.SendMsgs(suite.chainA, suite.chainB, clientB, msg) + suite.Require().NoError(err) // message committed } // need to update chainA client to prove missing ack suite.coordinator.UpdateClient(suite.chainA, suite.chainB, clientA, clientexported.Tendermint) @@ -368,8 +377,10 @@ func (suite *HandlerTestSuite) TestHandleTimeoutPacket() { for i := uint64(1); i < 10; i++ { packet = channeltypes.NewPacket(suite.chainA.GetPacketData(suite.chainB), i, channelA.PortID, channelA.ID, channelB.PortID, channelB.ID, uint64(suite.chainB.GetContext().BlockHeight()), 0) - err := suite.coordinator.SendPacket(suite.chainA, suite.chainB, packet, clientB) - suite.Require().NoError(err) + // send from chainA to chainB + msg := ibctransfertypes.NewMsgTransfer(channelA.PortID, channelA.ID, ibctesting.TestCoin, suite.chainA.SenderAccount.GetAddress(), suite.chainB.SenderAccount.GetAddress().String(), packet.GetTimeoutHeight(), packet.GetTimeoutTimestamp()) + err := suite.coordinator.SendMsgs(suite.chainA, suite.chainB, clientB, msg) + suite.Require().NoError(err) // message committed } // need to update chainA client to prove missing ack suite.coordinator.UpdateClient(suite.chainA, suite.chainB, clientA, clientexported.Tendermint) diff --git a/x/ibc/testing/chain.go b/x/ibc/testing/chain.go index d79954990c..f8b4e78dfe 100644 --- a/x/ibc/testing/chain.go +++ b/x/ibc/testing/chain.go @@ -54,6 +54,7 @@ const ( var ( DefaultTrustLevel tmmath.Fraction = lite.DefaultTrustLevel TestHash = []byte("TESTING HASH") + TestCoin = sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(100)) ConnectionVersion = connectiontypes.GetCompatibleEncodedVersions()[0] ) @@ -594,9 +595,14 @@ func (chain *TestChain) ChanCloseInit( } // GetPacketData returns a ibc-transfer marshalled packet to be used for -// callback testing +// callback testing. func (chain *TestChain) GetPacketData(counterparty *TestChain) []byte { - packet := ibctransfertypes.NewFungibleTokenPacketData(sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(100))), chain.SenderAccount.GetAddress().String(), counterparty.SenderAccount.GetAddress().String()) + packet := ibctransfertypes.FungibleTokenPacketData{ + Denom: TestCoin.Denom, + Amount: TestCoin.Amount.Uint64(), + Sender: chain.SenderAccount.GetAddress().String(), + Receiver: counterparty.SenderAccount.GetAddress().String(), + } return packet.GetBytes() } diff --git a/x/ibc/testing/coordinator.go b/x/ibc/testing/coordinator.go index b0dc236f98..713fb581a0 100644 --- a/x/ibc/testing/coordinator.go +++ b/x/ibc/testing/coordinator.go @@ -13,6 +13,7 @@ import ( clientexported "github.com/cosmos/cosmos-sdk/x/ibc/02-client/exported" channelexported "github.com/cosmos/cosmos-sdk/x/ibc/04-channel/exported" channeltypes "github.com/cosmos/cosmos-sdk/x/ibc/04-channel/types" + host "github.com/cosmos/cosmos-sdk/x/ibc/24-host" ) var ( @@ -244,6 +245,55 @@ func (coord *Coordinator) AcknowledgementExecuted( ) } +// RecvPacket receives a channel packet on the counterparty chain and updates +// the client on the source chain representing the counterparty. +func (coord *Coordinator) RecvPacket( + source, counterparty *TestChain, + sourceClient string, + packet channeltypes.Packet, +) error { + // get proof of packet commitment on source + packetKey := host.KeyPacketCommitment(packet.GetSourcePort(), packet.GetSourceChannel(), packet.GetSequence()) + proof, proofHeight := source.QueryProof(packetKey) + + recvMsg := channeltypes.NewMsgRecvPacket(packet, proof, proofHeight, counterparty.SenderAccount.GetAddress()) + + // receive on counterparty and update source client + return coord.SendMsgs(counterparty, source, sourceClient, recvMsg) +} + +// AcknowledgePacket acknowledges on the source chain the packet received on +// the counterparty chain and updates the client on the counterparty representing +// the source chain. +// TODO: add a query for the acknowledgement by events +// - https://github.com/cosmos/cosmos-sdk/issues/6509 +func (coord *Coordinator) AcknowledgePacket( + source, counterparty *TestChain, + counterpartyClient string, + packet channeltypes.Packet, ack []byte, +) error { + // get proof of acknowledgement on counterparty + packetKey := host.KeyPacketAcknowledgement(packet.GetDestPort(), packet.GetDestChannel(), packet.GetSequence()) + proof, proofHeight := counterparty.QueryProof(packetKey) + + ackMsg := channeltypes.NewMsgAcknowledgement(packet, ack, proof, proofHeight, source.SenderAccount.GetAddress()) + return coord.SendMsgs(source, counterparty, counterpartyClient, ackMsg) +} + +// RelayPacket receives a channel packet on counterparty, queries the ack +// and acknowledges the packet on source. The clients are updated as needed. +func (coord *Coordinator) RelayPacket( + source, counterparty *TestChain, + sourceClient, counterpartyClient string, + packet channeltypes.Packet, ack []byte, +) error { + if err := coord.RecvPacket(source, counterparty, sourceClient, packet); err != nil { + return err + } + + return coord.AcknowledgePacket(source, counterparty, counterpartyClient, packet, ack) +} + // IncrementTime iterates through all the TestChain's and increments their current header time // by 5 seconds. // diff --git a/x/ibc/testing/transfer.go b/x/ibc/testing/transfer.go deleted file mode 100644 index 76367b06b9..0000000000 --- a/x/ibc/testing/transfer.go +++ /dev/null @@ -1,11 +0,0 @@ -package testing - -import ( - "fmt" - - sdk "github.com/cosmos/cosmos-sdk/types" -) - -func NewTransferCoins(dst TestChannel, denom string, amount int64) sdk.Coins { - return sdk.NewCoins(sdk.NewCoin(fmt.Sprintf("%s/%s/%s", dst.PortID, dst.ID, denom), sdk.NewInt(amount))) -}