diff --git a/CHANGELOG.md b/CHANGELOG.md index 58c927f0ae9..2aae0efca69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,13 +46,8 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Features -<<<<<<< HEAD -======= -* (apps/transfer) [\#6492](https://github.com/cosmos/ibc-go/pull/6492) Added new `Tokens` field to `MsgTransfer` to enable sending of multiple denoms, and deprecated the `Token` field. -* (apps/transfer) [\#6693](https://github.com/cosmos/ibc-go/pull/6693) Added new `Forwarding` field to `MsgTransfer` to enable forwarding tokens through multiple intermediary chains with a single transaction. This also enables automatic unwinding of tokens to their native chain. `x/authz` support for transfer allows granters to specify a set of possible forwarding hops that are allowed for grantees. * (apps/transfer) [\#6877](https://github.com/cosmos/ibc-go/pull/6877) Added the possibility to transfer the entire user balance of a particular denomination by using [`UnboundedSpendLimit`](https://github.com/cosmos/ibc-go/blob/715f00eef8727da41db25fdd4763b709bdbba07e/modules/apps/transfer/types/transfer_authorization.go#L253-L255) as the token amount. ->>>>>>> 92e1f387 ((feat) Add possibility to transfer entire balance. (#6877)) ### Bug Fixes ## [v8.3.2](https://github.com/cosmos/ibc-go/releases/tag/v8.3.2) - 2024-06-20 diff --git a/docs/docs/02-apps/01-transfer/04-messages.md b/docs/docs/02-apps/01-transfer/04-messages.md deleted file mode 100644 index 4d121985fdc..00000000000 --- a/docs/docs/02-apps/01-transfer/04-messages.md +++ /dev/null @@ -1,86 +0,0 @@ ---- -title: Messages -sidebar_label: Messages -sidebar_position: 4 -slug: /apps/transfer/messages ---- - -# Messages - -## `MsgTransfer` - -A fungible token cross chain transfer is achieved by using the `MsgTransfer`: - -```go -type MsgTransfer struct { - SourcePort string - SourceChannel string - // Deprecated: Use Tokens instead. - Token sdk.Coin - Sender string - Receiver string - TimeoutHeight ibcexported.Height - TimeoutTimestamp uint64 - Memo string - Tokens []sdk.Coin - Forwarding *Forwarding -} - -type Forwarding struct { - Unwind bool - Hops []Hop -} - -type Hop struct { - PortId string - ChannelId string -} -``` - -:::info -Multi-denom token transfers and token forwarding are features supported only on ICS20 v2 transfer channels. -::: - -If `Forwarding` is `nil`, this message is expected to fail if: - -- `SourcePort` is invalid (see [24-host naming requirements](https://github.com/cosmos/ibc/blob/master/spec/core/ics-024-host-requirements/README.md#paths-identifiers-separators). -- `SourceChannel` is invalid (see [24-host naming requirements](https://github.com/cosmos/ibc/blob/master/spec/core/ics-024-host-requirements/README.md#paths-identifiers-separators)). -- `Tokens` must not be empty. -- Each `Coin` in `Tokens` must satisfy the following: - - `Amount` must be positive. - - `Denom` must be a valid IBC denomination, as defined in [ADR 001 - Coin Source Tracing](/architecture/adr-001-coin-source-tracing). -- `Sender` is empty. -- `Receiver` is empty or contains more than 2048 bytes. -- `Memo` contains more than 32768 bytes. -- `TimeoutHeight` and `TimeoutTimestamp` are both zero. - -If `Forwarding` is not `nil`, then to use forwarding you must either set `Unwind` to true or provide a non-empty list of `Hops`. Setting both `Unwind` to true and providing a non-empty list of `Hops` is allowed, but the total number of hops that is formed as a combination of the hops needed to unwind the tokens and the hops to forward them afterwards to the final destination must not exceed 8. When using forwarding, timeout must be specified using only `TimeoutTimestamp` (i.e. `TimeoutHeight` must be zero). Please note that the timeout timestamp must take into account the time that it may take tokens to be forwarded through the intermediary chains. Additionally, please note that the `MsgTransfer` will fail if: - -- `Hops` is not empty, and the number of elements of `Hops` is greater than 8, or either the `PortId` or `ChannelId` of any of the `Hops` is not a valid identifier. -- `Unwind` is true, and either the coins to be transferred have different denomination traces, or `SourcePort` and `SourceChannel` are not empty strings (they must be empty because they are set by the transfer module, since it has access to the denomination trace information and is thus able to know the source port ID, channel ID to use in order to unwind the tokens). If `Unwind` is true, the transfer module expects the tokens in `MsgTransfer` to not be native to the sending chain (i.e. they must be IBC vouchers). - -Please note that the `Token` field is deprecated and users should now use `Tokens` instead. If `Token` is used then `Tokens` must be empty. Similarly, if `Tokens` is used then `Token` should be left empty. 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. - -If the `Amount` is set to the maximum value for a 256-bit unsigned integer (i.e. 2^256 - 1), then the whole balance of the corrsponding denomination will be transferred. The helper function `UnboundedSpendLimit` in the `types` package of the `transfer` module provides the sentinel value that can be used. - -### Memo - -The memo field was added to allow applications and users to attach metadata to transfer packets. The field is optional and may be left empty. When it is used to attach metadata for a particular middleware, the memo field should be represented as a json object where different middlewares use different json keys. - -For example, the following memo field is used by the [callbacks middleware](../../04-middleware/02-callbacks/01-overview.md) to attach a source callback to a transfer packet: - -```jsonc -{ - "src_callback": { - "address": "callbackAddressString", - // optional - "gas_limit": "userDefinedGasLimitString", - } -} -``` - -You can find more information about other applications that use the memo field in the [chain registry](https://github.com/cosmos/chain-registry/blob/master/_memo_keys/ICS20_memo_keys.json). - -Please note that the memo field is always meant to be consumed only on the final destination chain. This means that the transfer module will guarantee that the memo field in the intermediary chains is empty. diff --git a/docs/docs/02-apps/01-transfer/10-ICS20-v1/04-messages.md b/docs/docs/02-apps/01-transfer/10-ICS20-v1/04-messages.md deleted file mode 100644 index 08fc05e88c3..00000000000 --- a/docs/docs/02-apps/01-transfer/10-ICS20-v1/04-messages.md +++ /dev/null @@ -1,65 +0,0 @@ ---- -title: Messages -sidebar_label: Messages -sidebar_position: 4 -slug: /apps/transfer/ics20-v1/messages ---- - -:::warning -This document is relevant only for fungible token transfers over channels on v1 of the ICS-20 protocol. -::: - -# 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 string - Receiver string - TimeoutHeight ibcexported.Height - TimeoutTimestamp uint64 - Memo string -} -``` - -This message is expected to fail if: - -- `SourcePort` is invalid (see [24-host naming requirements](https://github.com/cosmos/ibc/blob/master/spec/core/ics-024-host-requirements/README.md#paths-identifiers-separators). -- `SourceChannel` is invalid (see [24-host naming requirements](https://github.com/cosmos/ibc/blob/master/spec/core/ics-024-host-requirements/README.md#paths-identifiers-separators)). -- `Token` is invalid: - - `Amount` is not positive. - - `Denom` is not a valid IBC denomination as per [ADR 001 - Coin Source Tracing](/architecture/adr-001-coin-source-tracing). -- `Sender` is empty. -- `Receiver` is empty or contains more than 2048 bytes. -- `Memo` contains more than 32768 bytes. -- `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. - -If the `Amount` is set to the maximum value for a 256-bit unsigned integer (i.e. 2^256 - 1), then the whole balance of the corrsponding denomination will be transferred. The helper function `UnboundedSpendLimit` in the `types` package of the `transfer` module provides the sentinel value that can be used. - -### Memo - -The memo field was added to allow applications and users to attach metadata to transfer packets. The field is optional and may be left empty. When it is used to attach metadata for a particular middleware, the memo field should be represented as a json object where different middlewares use different json keys. - -For example, the following memo field is used by the [callbacks middleware](../../../04-middleware/02-callbacks/01-overview.md) to attach a source callback to a transfer packet: - -```jsonc -{ - "src_callback": { - "address": "callbackAddressString", - // optional - "gas_limit": "userDefinedGasLimitString", - } -} -``` - -You can find more information about other applications that use the memo field in the [chain registry](https://github.com/cosmos/chain-registry/blob/master/_memo_keys/ICS20_memo_keys.json). diff --git a/e2e/tests/transfer/base_test.go b/e2e/tests/transfer/base_test.go deleted file mode 100644 index 745a0d57f07..00000000000 --- a/e2e/tests/transfer/base_test.go +++ /dev/null @@ -1,644 +0,0 @@ -//go:build !test_e2e - -package transfer - -import ( - "context" - "testing" - "time" - - "github.com/strangelove-ventures/interchaintest/v8/ibc" - test "github.com/strangelove-ventures/interchaintest/v8/testutil" - testifysuite "github.com/stretchr/testify/suite" - - sdkmath "cosmossdk.io/math" - - sdk "github.com/cosmos/cosmos-sdk/types" - - "github.com/cosmos/ibc-go/e2e/testsuite" - "github.com/cosmos/ibc-go/e2e/testsuite/query" - "github.com/cosmos/ibc-go/e2e/testvalues" - transfertypes "github.com/cosmos/ibc-go/v9/modules/apps/transfer/types" -) - -func TestTransferTestSuite(t *testing.T) { - testifysuite.Run(t, new(TransferTestSuite)) -} - -type TransferTestSuite struct { - transferTester -} - -// transferTester defines some helper functions that can be used in various test suites -// that test transfer functionality. -type transferTester struct { - testsuite.E2ETestSuite -} - -// QueryTransferParams queries the on-chain send enabled param for the transfer module -func (s *transferTester) QueryTransferParams(ctx context.Context, chain ibc.Chain) transfertypes.Params { - res, err := query.GRPCQuery[transfertypes.QueryParamsResponse](ctx, chain, &transfertypes.QueryParamsRequest{}) - s.Require().NoError(err) - return *res.Params -} - -// CreateTransferPath sets up a path between chainA and chainB with a transfer channel and returns the relayer wired -// up to watch the channel and port IDs created. -func (s *transferTester) CreateTransferPath(testName string) (ibc.Relayer, ibc.ChannelOutput) { - relayer, channel := s.CreatePaths(ibc.DefaultClientOpts(), s.TransferChannelOptions(), testName), s.GetChainAChannelForTest(testName) - s.T().Logf("test %s running on portID %s channelID %s", testName, channel.PortID, channel.ChannelID) - return relayer, channel -} - -// TestMsgTransfer_Succeeds_Nonincentivized will test sending successful IBC transfers from chainA to chainB. -// The transfer will occur over a basic transfer channel (non incentivized) and both native and non-native tokens -// will be sent forwards and backwards in the IBC transfer timeline (both chains will act as source and receiver chains). -func (s *TransferTestSuite) TestMsgTransfer_Succeeds_Nonincentivized() { - t := s.T() - ctx := context.TODO() - - testName := t.Name() - - // NOTE: t.Parallel() should be called before SetupPath in all tests. - // t.Name() must be stored in a variable before t.Parallel() otherwise t.Name() is not - // deterministic. - t.Parallel() - - relayer, channelA := s.CreateTransferPath(testName) - - chainA, chainB := s.GetChains() - - chainBVersion := chainB.Config().Images[0].Version - chainADenom := chainA.Config().Denom - - chainAWallet := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) - chainAAddress := chainAWallet.FormattedAddress() - - chainBWallet := s.CreateUserOnChainB(ctx, testvalues.StartingTokenAmount) - chainBAddress := chainBWallet.FormattedAddress() - - s.Require().NoError(test.WaitForBlocks(ctx, 1, chainA, chainB), "failed to wait for blocks") - // TODO: https://github.com/cosmos/ibc-go/issues/6743 - // t.Run("ensure capability module BeginBlock is executed", func(t *testing.T) { - // // by restarting the chain we ensure that the capability module's BeginBlocker is executed. - // s.Require().NoError(chainA.(*cosmos.CosmosChain).StopAllNodes(ctx)) - // s.Require().NoError(chainA.(*cosmos.CosmosChain).StartAllNodes(ctx)) - // s.Require().NoError(test.WaitForBlocks(ctx, 5, chainA), "failed to wait for blocks") - // }) - - t.Run("native IBC token transfer from chainA to chainB, sender is source of tokens", func(t *testing.T) { - transferTxResp := s.Transfer(ctx, chainA, chainAWallet, channelA.PortID, channelA.ChannelID, testvalues.DefaultTransferCoins(chainADenom), chainAAddress, chainBAddress, s.GetTimeoutHeight(ctx, chainB), 0, "", nil) - s.AssertTxSuccess(transferTxResp) - }) - - t.Run("tokens are escrowed", func(t *testing.T) { - actualBalance, err := s.GetChainANativeBalance(ctx, chainAWallet) - s.Require().NoError(err) - - expected := testvalues.StartingTokenAmount - testvalues.IBCTransferAmount - s.Require().Equal(expected, actualBalance) - - // TODO: cannot query total escrow if tests in parallel are using the same denom. - // if testvalues.TotalEscrowFeatureReleases.IsSupported(chainAVersion) { - // actualTotalEscrow, err := query.TotalEscrowForDenom(ctx, chainA, chainADenom) - // s.Require().NoError(err) - // - // expectedTotalEscrow := sdk.NewCoin(chainADenom, sdkmath.NewInt(testvalues.IBCTransferAmount)) - // s.Require().Equal(expectedTotalEscrow, actualTotalEscrow) - // } - }) - - t.Run("start relayer", func(t *testing.T) { - s.StartRelayer(relayer, testName) - }) - - chainBIBCToken := testsuite.GetIBCToken(chainADenom, channelA.Counterparty.PortID, channelA.Counterparty.ChannelID) - - t.Run("packets are relayed", func(t *testing.T) { - s.AssertPacketRelayed(ctx, chainA, channelA.PortID, channelA.ChannelID, 1) - - actualBalance, err := query.Balance(ctx, chainB, chainBAddress, chainBIBCToken.IBCDenom()) - s.Require().NoError(err) - - expected := testvalues.IBCTransferAmount - s.Require().Equal(expected, actualBalance.Int64()) - }) - - if testvalues.TokenMetadataFeatureReleases.IsSupported(chainBVersion) { - t.Run("metadata for IBC denomination exists on chainB", func(t *testing.T) { - s.AssertHumanReadableDenom(ctx, chainB, chainADenom, channelA) - }) - } - - t.Run("non-native IBC token transfer from chainB to chainA, receiver is source of tokens", func(t *testing.T) { - transferTxResp := s.Transfer(ctx, chainB, chainBWallet, channelA.Counterparty.PortID, channelA.Counterparty.ChannelID, testvalues.DefaultTransferCoins(chainBIBCToken.IBCDenom()), chainBAddress, chainAAddress, s.GetTimeoutHeight(ctx, chainA), 0, "", nil) - s.AssertTxSuccess(transferTxResp) - }) - - t.Run("tokens are escrowed", func(t *testing.T) { - actualBalance, err := query.Balance(ctx, chainB, chainBAddress, chainBIBCToken.IBCDenom()) - s.Require().NoError(err) - - s.Require().Equal(sdkmath.ZeroInt(), actualBalance) - - // https://github.com/cosmos/ibc-go/issues/6742 - // if testvalues.TotalEscrowFeatureReleases.IsSupported(chainBVersion) { - // actualTotalEscrow, err := query.TotalEscrowForDenom(ctx, chainB, chainBIBCToken.IBCDenom()) - // s.Require().NoError(err) - // s.Require().Equal(sdk.NewCoin(chainBIBCToken.IBCDenom(), sdkmath.NewInt(0)), actualTotalEscrow) // total escrow is zero because sending chain is not source for tokens - // } - }) - - s.Require().NoError(test.WaitForBlocks(ctx, 5, chainA, chainB), "failed to wait for blocks") - - t.Run("packets are relayed", func(t *testing.T) { - s.AssertPacketRelayed(ctx, chainB, channelA.Counterparty.PortID, channelA.Counterparty.ChannelID, 1) - - actualBalance, err := s.GetChainANativeBalance(ctx, chainAWallet) - s.Require().NoError(err) - - expected := testvalues.StartingTokenAmount - s.Require().Equal(expected, actualBalance) - }) - - // https://github.com/cosmos/ibc-go/issues/6742 - // if testvalues.TotalEscrowFeatureReleases.IsSupported(chainAVersion) { - // t.Run("tokens are un-escrowed", func(t *testing.T) { - // actualTotalEscrow, err := query.TotalEscrowForDenom(ctx, chainA, chainADenom) - // s.Require().NoError(err) - // s.Require().Equal(sdk.NewCoin(chainADenom, sdkmath.NewInt(0)), actualTotalEscrow) // total escrow is zero because tokens have come back - // }) - // } -} - -// TestMsgTransfer_Succeeds_MultiDenom will test sending successful IBC transfers from chainA to chainB. -// A multidenom transfer with native chainB tokens and IBC tokens from chainA is executed from chainB to chainA. -func (s *TransferTestSuite) TestMsgTransfer_Succeeds_Nonincentivized_MultiDenom() { - t := s.T() - ctx := context.TODO() - - testName := t.Name() - t.Parallel() - relayer, channelA := s.CreateTransferPath(testName) - - chainA, chainB := s.GetChains() - - chainADenom := chainA.Config().Denom - chainBDenom := chainB.Config().Denom - - chainAWallet := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) - chainAAddress := chainAWallet.FormattedAddress() - - chainBWallet := s.CreateUserOnChainB(ctx, testvalues.StartingTokenAmount) - chainBAddress := chainBWallet.FormattedAddress() - - chainBIBCToken := testsuite.GetIBCToken(chainADenom, channelA.Counterparty.PortID, channelA.Counterparty.ChannelID) - chainAIBCToken := testsuite.GetIBCToken(chainBDenom, channelA.PortID, channelA.ChannelID) - - t.Run("native IBC token transfer from chainA to chainB, sender is source of tokens", func(t *testing.T) { - transferTxResp := s.Transfer(ctx, chainA, chainAWallet, channelA.PortID, channelA.ChannelID, sdk.NewCoins(testvalues.DefaultTransferAmount(chainADenom)), chainAAddress, chainBAddress, s.GetTimeoutHeight(ctx, chainB), 0, "", nil) - s.AssertTxSuccess(transferTxResp) - }) - - s.Require().NoError(test.WaitForBlocks(ctx, 5, chainA, chainB), "failed to wait for blocks") - - t.Run("native chainA tokens are escrowed", func(t *testing.T) { - actualBalance, err := s.GetChainANativeBalance(ctx, chainAWallet) - s.Require().NoError(err) - - expected := testvalues.StartingTokenAmount - testvalues.IBCTransferAmount - s.Require().Equal(expected, actualBalance) - - // https://github.com/cosmos/ibc-go/issues/6742 - // actualTotalEscrow, err := query.TotalEscrowForDenom(ctx, chainA, chainADenom) - // s.Require().NoError(err) - // - // expectedTotalEscrow := sdk.NewCoin(chainADenom, sdkmath.NewInt(testvalues.IBCTransferAmount)) - // s.Require().Equal(expectedTotalEscrow, actualTotalEscrow) - }) - - t.Run("start relayer", func(t *testing.T) { - s.StartRelayer(relayer, testName) - }) - - t.Run("packets are relayed", func(t *testing.T) { - s.AssertPacketRelayed(ctx, chainA, channelA.PortID, channelA.ChannelID, 1) - - actualBalance, err := query.Balance(ctx, chainB, chainBAddress, chainBIBCToken.IBCDenom()) - s.Require().NoError(err) - - expected := testvalues.IBCTransferAmount - s.Require().Equal(expected, actualBalance.Int64()) - }) - - t.Run("metadata for IBC denomination exists on chainB", func(t *testing.T) { - s.AssertHumanReadableDenom(ctx, chainB, chainADenom, channelA) - }) - - // send the native chainB denom and also the ibc token from chainA - transferCoins := []sdk.Coin{ - testvalues.DefaultTransferAmount(chainBIBCToken.IBCDenom()), - testvalues.DefaultTransferAmount(chainBDenom), - } - - t.Run("native token from chain B and non-native IBC token from chainA, both to chainA", func(t *testing.T) { - transferTxResp := s.Transfer(ctx, chainB, chainBWallet, channelA.Counterparty.PortID, channelA.Counterparty.ChannelID, transferCoins, chainBAddress, chainAAddress, s.GetTimeoutHeight(ctx, chainA), 0, "", nil) - s.AssertTxSuccess(transferTxResp) - }) - - s.Require().NoError(test.WaitForBlocks(ctx, 5, chainA, chainB), "failed to wait for blocks") - - t.Run("packets are relayed", func(t *testing.T) { - s.AssertPacketRelayed(ctx, chainB, channelA.Counterparty.PortID, channelA.Counterparty.ChannelID, 1) - - t.Run("chain A native denom", func(t *testing.T) { - actualBalance, err := s.GetChainANativeBalance(ctx, chainAWallet) - s.Require().NoError(err) - - expected := testvalues.StartingTokenAmount - s.Require().Equal(expected, actualBalance) - }) - - t.Run("chain B IBC denom", func(t *testing.T) { - actualBalance, err := query.Balance(ctx, chainA, chainAAddress, chainAIBCToken.IBCDenom()) - s.Require().NoError(err) - - expected := testvalues.IBCTransferAmount - s.Require().Equal(expected, actualBalance.Int64()) - }) - }) - - // https://github.com/cosmos/ibc-go/issues/6742 - // t.Run("native chainA tokens are un-escrowed", func(t *testing.T) { - // actualTotalEscrow, err := query.TotalEscrowForDenom(ctx, chainA, chainADenom) - // s.Require().NoError(err) - // s.Require().Equal(sdk.NewCoin(chainADenom, sdkmath.NewInt(0)), actualTotalEscrow) // total escrow is zero because tokens have come back - // }) -} - -// TestMsgTransfer_Fails_InvalidAddress_MultiDenom attempts to send a multidenom IBC transfer -// to an invalid address and ensures that the tokens on the sending chain are returned to the sender. -func (s *TransferTestSuite) TestMsgTransfer_Fails_InvalidAddress_MultiDenom() { - t := s.T() - ctx := context.TODO() - - testName := t.Name() - t.Parallel() - relayer, channelA := s.CreateTransferPath(testName) - - chainA, chainB := s.GetChains() - - chainADenom := chainA.Config().Denom - chainBDenom := chainB.Config().Denom - - chainAWallet := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) - chainAAddress := chainAWallet.FormattedAddress() - - chainBWallet := s.CreateUserOnChainB(ctx, testvalues.StartingTokenAmount) - chainBAddress := chainBWallet.FormattedAddress() - - chainBIBCToken := testsuite.GetIBCToken(chainADenom, channelA.Counterparty.PortID, channelA.Counterparty.ChannelID) - - t.Run("native IBC token transfer from chainA to chainB, sender is source of tokens", func(t *testing.T) { - transferTxResp := s.Transfer(ctx, chainA, chainAWallet, channelA.PortID, channelA.ChannelID, sdk.NewCoins(testvalues.DefaultTransferAmount(chainADenom)), chainAAddress, chainBAddress, s.GetTimeoutHeight(ctx, chainB), 0, "", nil) - s.AssertTxSuccess(transferTxResp) - }) - - s.Require().NoError(test.WaitForBlocks(ctx, 5, chainA, chainB), "failed to wait for blocks") - - t.Run("native chainA tokens are escrowed", func(t *testing.T) { - actualBalance, err := s.GetChainANativeBalance(ctx, chainAWallet) - s.Require().NoError(err) - - expected := testvalues.StartingTokenAmount - testvalues.IBCTransferAmount - s.Require().Equal(expected, actualBalance) - - // https://github.com/cosmos/ibc-go/issues/6742 - // actualTotalEscrow, err := query.TotalEscrowForDenom(ctx, chainA, chainADenom) - // s.Require().NoError(err) - // - // expectedTotalEscrow := sdk.NewCoin(chainADenom, sdkmath.NewInt(testvalues.IBCTransferAmount)) - // s.Require().Equal(expectedTotalEscrow, actualTotalEscrow) - }) - - t.Run("start relayer", func(t *testing.T) { - s.StartRelayer(relayer, testName) - }) - - t.Run("packets are relayed", func(t *testing.T) { - s.AssertPacketRelayed(ctx, chainA, channelA.PortID, channelA.ChannelID, 1) - - actualBalance, err := query.Balance(ctx, chainB, chainBAddress, chainBIBCToken.IBCDenom()) - s.Require().NoError(err) - - expected := testvalues.IBCTransferAmount - s.Require().Equal(expected, actualBalance.Int64()) - }) - - t.Run("metadata for IBC denomination exists on chainB", func(t *testing.T) { - s.AssertHumanReadableDenom(ctx, chainB, chainADenom, channelA) - }) - - // send the native chainB denom and also the ibc token from chainA - transferCoins := []sdk.Coin{ - testvalues.DefaultTransferAmount(chainBIBCToken.IBCDenom()), - testvalues.DefaultTransferAmount(chainBDenom), - } - - t.Run("stop relayer", func(t *testing.T) { - s.StopRelayer(ctx, relayer) - }) - - t.Run("native token from chain B and non-native IBC token from chainA, both to chainA", func(t *testing.T) { - transferTxResp := s.Transfer(ctx, chainB, chainBWallet, channelA.Counterparty.PortID, channelA.Counterparty.ChannelID, transferCoins, chainBAddress, testvalues.InvalidAddress, s.GetTimeoutHeight(ctx, chainA), 0, "", nil) - s.AssertTxSuccess(transferTxResp) - }) - - s.Require().NoError(test.WaitForBlocks(ctx, 5, chainA, chainB), "failed to wait for blocks") - - t.Run("tokens are sent from chain B", func(t *testing.T) { - t.Run("native chainB tokens are escrowed", func(t *testing.T) { - actualBalance, err := s.GetChainBNativeBalance(ctx, chainBWallet) - s.Require().NoError(err) - - expected := testvalues.StartingTokenAmount - testvalues.IBCTransferAmount - s.Require().Equal(expected, actualBalance) - }) - - t.Run("non-native chainA IBC denom are burned", func(t *testing.T) { - actualBalance, err := query.Balance(ctx, chainB, chainBAddress, chainBIBCToken.IBCDenom()) - s.Require().NoError(err) - s.Require().Equal(int64(0), actualBalance.Int64()) - }) - }) - - t.Run("start relayer", func(t *testing.T) { - s.StartRelayer(relayer, testName) - }) - - t.Run("packets are relayed", func(t *testing.T) { - s.AssertPacketRelayed(ctx, chainB, channelA.Counterparty.PortID, channelA.Counterparty.ChannelID, 1) - }) - - t.Run("tokens are returned to sender on chainB", func(t *testing.T) { - t.Run("native chainB denom", func(t *testing.T) { - actualBalance, err := s.GetChainBNativeBalance(ctx, chainBWallet) - s.Require().NoError(err) - - expected := testvalues.StartingTokenAmount - s.Require().Equal(expected, actualBalance) - }) - - t.Run("non-native chainA IBC denom", func(t *testing.T) { - actualBalance, err := query.Balance(ctx, chainB, chainBAddress, chainBIBCToken.IBCDenom()) - s.Require().NoError(err) - - expected := testvalues.IBCTransferAmount - s.Require().Equal(expected, actualBalance.Int64()) - }) - }) -} - -// TestMsgTransfer_Fails_InvalidAddress attempts to send an IBC transfer to an invalid address and ensures -// that the tokens on the sending chain are unescrowed. -func (s *TransferTestSuite) TestMsgTransfer_Fails_InvalidAddress() { - t := s.T() - ctx := context.TODO() - - testName := t.Name() - t.Parallel() - relayer, channelA := s.CreateTransferPath(testName) - - chainA, chainB := s.GetChains() - - chainADenom := chainA.Config().Denom - - chainAWallet := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) - chainAAddress := chainAWallet.FormattedAddress() - - s.Require().NoError(test.WaitForBlocks(ctx, 1, chainA, chainB), "failed to wait for blocks") - - t.Run("native IBC token transfer from chainA to invalid address", func(t *testing.T) { - transferTxResp := s.Transfer(ctx, chainA, chainAWallet, channelA.PortID, channelA.ChannelID, testvalues.DefaultTransferCoins(chainADenom), chainAAddress, testvalues.InvalidAddress, s.GetTimeoutHeight(ctx, chainB), 0, "", nil) - s.AssertTxSuccess(transferTxResp) - }) - - t.Run("tokens are escrowed", func(t *testing.T) { - actualBalance, err := s.GetChainANativeBalance(ctx, chainAWallet) - s.Require().NoError(err) - - expected := testvalues.StartingTokenAmount - testvalues.IBCTransferAmount - s.Require().Equal(expected, actualBalance) - }) - - t.Run("start relayer", func(t *testing.T) { - s.StartRelayer(relayer, testName) - }) - - t.Run("packets are relayed", func(t *testing.T) { - s.AssertPacketRelayed(ctx, chainA, channelA.PortID, channelA.ChannelID, 1) - }) - - t.Run("token transfer amount unescrowed", func(t *testing.T) { - actualBalance, err := s.GetChainANativeBalance(ctx, chainAWallet) - s.Require().NoError(err) - - expected := testvalues.StartingTokenAmount - s.Require().Equal(expected, actualBalance) - }) -} - -func (s *TransferTestSuite) TestMsgTransfer_Timeout_Nonincentivized() { - t := s.T() - ctx := context.TODO() - - testName := t.Name() - t.Parallel() - relayer, channelA := s.CreateTransferPath(testName) - - chainA, _ := s.GetChains() - - chainAWallet := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) - chainBWallet := s.CreateUserOnChainB(ctx, testvalues.StartingTokenAmount) - - chainBWalletAmount := ibc.WalletAmount{ - Address: chainBWallet.FormattedAddress(), // destination address - Denom: chainA.Config().Denom, - Amount: sdkmath.NewInt(testvalues.IBCTransferAmount), - } - - t.Run("IBC transfer packet timesout", func(t *testing.T) { - tx, err := chainA.SendIBCTransfer(ctx, channelA.ChannelID, chainAWallet.KeyName(), chainBWalletAmount, ibc.TransferOptions{Timeout: testvalues.ImmediatelyTimeout()}) - s.Require().NoError(err) - s.Require().NoError(tx.Validate(), "source ibc transfer tx is invalid") - time.Sleep(time.Nanosecond * 1) // want it to timeout immediately - }) - - t.Run("tokens are escrowed", func(t *testing.T) { - actualBalance, err := s.GetChainANativeBalance(ctx, chainAWallet) - s.Require().NoError(err) - - expected := testvalues.StartingTokenAmount - testvalues.IBCTransferAmount - s.Require().Equal(expected, actualBalance) - }) - - t.Run("start relayer", func(t *testing.T) { - s.StartRelayer(relayer, testName) - }) - - t.Run("ensure escrowed tokens have been refunded to sender due to timeout", func(t *testing.T) { - // ensure destination address did not receive any tokens - bal, err := s.GetChainBNativeBalance(ctx, chainBWallet) - s.Require().NoError(err) - s.Require().Equal(testvalues.StartingTokenAmount, bal) - - // ensure that the sender address has been successfully refunded the full amount - bal, err = s.GetChainANativeBalance(ctx, chainAWallet) - s.Require().NoError(err) - s.Require().Equal(testvalues.StartingTokenAmount, bal) - }) -} - -// This can be used to test sending with a transfer packet with a memo given different combinations of -// ibc-go versions. -// -// TestMsgTransfer_WithMemo will test sending IBC transfers from chainA to chainB -// If the chains contain a version of FungibleTokenPacketData with memo, both send and receive should succeed. -// If one of the chains contains a version of FungibleTokenPacketData without memo, then receiving a packet with -// memo should fail in that chain -func (s *TransferTestSuite) TestMsgTransfer_WithMemo() { - t := s.T() - ctx := context.TODO() - - testName := t.Name() - t.Parallel() - relayer, channelA := s.CreateTransferPath(testName) - - chainA, chainB := s.GetChains() - - chainADenom := chainA.Config().Denom - - chainAWallet := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) - chainAAddress := chainAWallet.FormattedAddress() - - chainBWallet := s.CreateUserOnChainB(ctx, testvalues.StartingTokenAmount) - chainBAddress := chainBWallet.FormattedAddress() - - s.Require().NoError(test.WaitForBlocks(ctx, 1, chainA, chainB), "failed to wait for blocks") - - t.Run("IBC token transfer with memo from chainA to chainB", func(t *testing.T) { - transferTxResp := s.Transfer(ctx, chainA, chainAWallet, channelA.PortID, channelA.ChannelID, testvalues.DefaultTransferCoins(chainADenom), chainAAddress, chainBAddress, s.GetTimeoutHeight(ctx, chainB), 0, "memo", nil) - s.AssertTxSuccess(transferTxResp) - }) - - t.Run("tokens are escrowed", func(t *testing.T) { - actualBalance, err := s.GetChainANativeBalance(ctx, chainAWallet) - s.Require().NoError(err) - - expected := testvalues.StartingTokenAmount - testvalues.IBCTransferAmount - s.Require().Equal(expected, actualBalance) - }) - - t.Run("start relayer", func(t *testing.T) { - s.StartRelayer(relayer, testName) - }) - - chainBIBCToken := testsuite.GetIBCToken(chainADenom, channelA.Counterparty.PortID, channelA.Counterparty.ChannelID) - - t.Run("packets relayed", func(t *testing.T) { - s.AssertPacketRelayed(ctx, chainA, channelA.PortID, channelA.ChannelID, 1) - actualBalance, err := query.Balance(ctx, chainB, chainBAddress, chainBIBCToken.IBCDenom()) - - s.Require().NoError(err) - s.Require().Equal(testvalues.IBCTransferAmount, actualBalance.Int64()) - }) -} - -// TestMsgTransfer_EntireBalance tests that it is possible to transfer the entire balance -// of a given denom by using types.UnboundedSpendLimit as the amount. -func (s *TransferTestSuite) TestMsgTransfer_EntireBalance() { - t := s.T() - ctx := context.TODO() - - testName := t.Name() - t.Parallel() - relayer, channelA := s.CreateTransferPath(testName) - - chainA, chainB := s.GetChains() - - chainADenom := chainA.Config().Denom - - chainAWallet := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) - chainAAddress := chainAWallet.FormattedAddress() - - chainBWallet := s.CreateUserOnChainB(ctx, testvalues.StartingTokenAmount) - chainBAddress := chainBWallet.FormattedAddress() - - coinFromA := testvalues.DefaultTransferAmount(chainADenom) - - s.Require().NoError(test.WaitForBlocks(ctx, 1, chainA, chainB), "failed to wait for blocks") - - t.Run("IBC token transfer from chainA to chainB", func(t *testing.T) { - transferTxResp := s.Transfer(ctx, chainA, chainAWallet, channelA.PortID, channelA.ChannelID, sdk.NewCoins(coinFromA), chainAAddress, chainBAddress, s.GetTimeoutHeight(ctx, chainA), 0, "", nil) - s.AssertTxSuccess(transferTxResp) - }) - - t.Run("tokens are escrowed", func(t *testing.T) { - actualBalance, err := s.GetChainANativeBalance(ctx, chainAWallet) - s.Require().NoError(err) - - s.Require().Equal(testvalues.StartingTokenAmount-coinFromA.Amount.Int64(), actualBalance) - }) - - t.Run("start relayer", func(t *testing.T) { - s.StartRelayer(relayer, testName) - }) - - chainBIBCToken := testsuite.GetIBCToken(chainADenom, channelA.Counterparty.PortID, channelA.Counterparty.ChannelID) - - t.Run("packets relayed", func(t *testing.T) { - s.AssertPacketRelayed(ctx, chainA, channelA.PortID, channelA.ChannelID, 1) - actualBalance, err := query.Balance(ctx, chainB, chainBAddress, chainBIBCToken.IBCDenom()) - - s.Require().NoError(err) - s.Require().Equal(coinFromA.Amount.Int64(), actualBalance.Int64()) - - actualBalance, err = query.Balance(ctx, chainA, chainAAddress, chainADenom) - - s.Require().NoError(err) - s.Require().Equal(testvalues.StartingTokenAmount-coinFromA.Amount.Int64(), actualBalance.Int64()) - }) - - t.Run("send entire balance from B to A", func(t *testing.T) { - transferCoins := sdk.NewCoins((sdk.NewCoin(chainBIBCToken.IBCDenom(), transfertypes.UnboundedSpendLimit())), sdk.NewCoin(chainB.Config().Denom, transfertypes.UnboundedSpendLimit())) - transferTxResp := s.Transfer(ctx, chainB, chainBWallet, channelA.Counterparty.PortID, channelA.Counterparty.ChannelID, transferCoins, chainBAddress, chainAAddress, s.GetTimeoutHeight(ctx, chainB), 0, "", nil) - s.AssertTxSuccess(transferTxResp) - }) - - chainAIBCToken := testsuite.GetIBCToken(chainB.Config().Denom, channelA.PortID, channelA.ChannelID) - t.Run("packets relayed", func(t *testing.T) { - // test that chainA has the entire balance back of its native token. - s.AssertPacketRelayed(ctx, chainA, channelA.PortID, channelA.ChannelID, 1) - actualBalance, err := query.Balance(ctx, chainA, chainAAddress, chainADenom) - - s.Require().NoError(err) - s.Require().Equal(testvalues.StartingTokenAmount, actualBalance.Int64()) - - // test that chainA has the entirety of chainB's token IBC denom. - actualBalance, err = query.Balance(ctx, chainA, chainAAddress, chainAIBCToken.IBCDenom()) - - s.Require().NoError(err) - s.Require().Equal(testvalues.StartingTokenAmount, actualBalance.Int64()) - - // Tests that chainB has a zero balance for both. - actualBalance, err = query.Balance(ctx, chainB, chainBAddress, chainBIBCToken.IBCDenom()) - - s.Require().NoError(err) - s.Require().Zero(actualBalance.Int64()) - - actualBalance, err = query.Balance(ctx, chainB, chainBAddress, chainB.Config().Denom) - - s.Require().NoError(err) - s.Require().Zero(actualBalance.Int64()) - }) -} diff --git a/modules/apps/transfer/keeper/relay.go b/modules/apps/transfer/keeper/relay.go index eee3ecb403b..a6e662bce69 100644 --- a/modules/apps/transfer/keeper/relay.go +++ b/modules/apps/transfer/keeper/relay.go @@ -81,22 +81,12 @@ func (k Keeper) sendTransfer( // NOTE: denomination and hex hash correctness checked during msg.ValidateBasic fullDenomPath := token.Denom -<<<<<<< HEAD var err error // deconstruct the token denomination into the denomination trace info // to determine if the sender is the source chain if strings.HasPrefix(token.Denom, "ibc/") { fullDenomPath, err = k.DenomPathFromHash(ctx, token.Denom) -======= - 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 - } - - token, err := k.tokenFromCoin(ctx, coin) ->>>>>>> 92e1f387 ((feat) Add possibility to transfer entire balance. (#6877)) if err != nil { return 0, err } @@ -107,6 +97,11 @@ func (k Keeper) sendTransfer( telemetry.NewLabel(coretypes.LabelDestinationChannel, destinationChannel), } + // Using types.UnboundedSpendLimit allows us to send the entire balance of a given denom. + if token.Amount.Equal(types.UnboundedSpendLimit()) { + token.Amount = k.bankKeeper.GetBalance(ctx, sender, token.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. diff --git a/modules/apps/transfer/keeper/relay_test.go b/modules/apps/transfer/keeper/relay_test.go index e9a7add5beb..9d1311f4122 100644 --- a/modules/apps/transfer/keeper/relay_test.go +++ b/modules/apps/transfer/keeper/relay_test.go @@ -62,45 +62,16 @@ func (suite *KeeperTestSuite) TestSendTransfer() { }, true, }, { -<<<<<<< HEAD - "source channel not found", -======= - "successful transfer of native token with ics20-1", - func() { - coins = sdk.NewCoins(coins[0]) - - // Set version to isc20-1. - path.EndpointA.UpdateChannel(func(channel *channeltypes.Channel) { - channel.Version = types.V1 - }) - }, - nil, - }, - { - "successful transfer with empty forwarding hops and ics20-1", - func() { - coins = sdk.NewCoins(coins[0]) - - // Set version to isc20-1. - path.EndpointA.UpdateChannel(func(channel *channeltypes.Channel) { - channel.Version = types.V1 - }) - }, - nil, - }, - { "successful transfer of entire balance", func() { - coins = sdk.NewCoins(sdk.NewCoin(coins[0].Denom, types.UnboundedSpendLimit())) + coin.Amount = types.UnboundedSpendLimit() var ok bool - expEscrowAmounts[0], ok = sdkmath.NewIntFromString(ibctesting.DefaultGenesisAccBalance) + expEscrowAmount, ok = sdkmath.NewIntFromString(ibctesting.DefaultGenesisAccBalance) suite.Require().True(ok) - }, - nil, + }, true, }, { - "failure: source channel not found", ->>>>>>> 92e1f387 ((feat) Add possibility to transfer entire balance. (#6877)) + "source channel not found", func() { // channel references wrong ID path.EndpointA.ChannelID = ibctesting.InvalidID diff --git a/modules/apps/transfer/types/coin.go b/modules/apps/transfer/types/coin.go index fad6532af81..6cd9c5f1252 100644 --- a/modules/apps/transfer/types/coin.go +++ b/modules/apps/transfer/types/coin.go @@ -2,6 +2,7 @@ package types import ( "fmt" + "math/big" "strings" sdkmath "cosmossdk.io/math" @@ -9,6 +10,9 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) +// maxUint256 is the maximum value for a 256 bit unsigned integer. +var maxUint256 = new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 256), big.NewInt(1)) + // SenderChainIsSource returns false if the denomination originally came // from the receiving chain and true otherwise. func SenderChainIsSource(sourcePort, sourceChannel, denom string) bool { @@ -47,3 +51,12 @@ func GetTransferCoin(portID, channelID, baseDenom string, amount sdkmath.Int) sd denomTrace := ParseDenomTrace(GetPrefixedDenom(portID, channelID, baseDenom)) return sdk.NewCoin(denomTrace.IBCDenom(), amount) } + +// UnboundedSpendLimit returns the sentinel value that can be used +// as the amount for a denomination's spend limit for which spend limit updating +// should be disabled. Please note that using this sentinel value means that a grantee +// will be granted the privilege to do ICS20 token transfers for the total amount +// of the denomination available at the granter's account. +func UnboundedSpendLimit() sdkmath.Int { + return sdkmath.NewIntFromBigInt(maxUint256) +} diff --git a/modules/apps/transfer/types/token.go b/modules/apps/transfer/types/token.go deleted file mode 100644 index d30a765f0f2..00000000000 --- a/modules/apps/transfer/types/token.go +++ /dev/null @@ -1,58 +0,0 @@ -package types - -import ( - "math/big" - - errorsmod "cosmossdk.io/errors" - sdkmath "cosmossdk.io/math" - - sdk "github.com/cosmos/cosmos-sdk/types" -) - -// Tokens is a slice of Tokens -type Tokens []Token - -// maxUint256 is the maximum value for a 256 bit unsigned integer. -var maxUint256 = new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 256), big.NewInt(1)) - -// Validate validates a token denomination and amount. -func (t Token) Validate() error { - if err := t.Denom.Validate(); err != nil { - return errorsmod.Wrap(err, "invalid token denom") - } - - amount, ok := sdkmath.NewIntFromString(t.Amount) - if !ok { - return errorsmod.Wrapf(ErrInvalidAmount, "unable to parse transfer amount (%s) into math.Int", t.Amount) - } - - if !amount.IsPositive() { - return errorsmod.Wrapf(ErrInvalidAmount, "amount must be strictly positive: got %d", amount) - } - - return nil -} - -// ToCoin converts a Token to an sdk.Coin. -// -// The function parses the Amount field of the Token into an sdkmath.Int and returns a new sdk.Coin with -// the IBCDenom of the Token's Denom field and the parsed Amount. -// If the Amount cannot be parsed, an error is returned with a wrapped error message. -func (t Token) ToCoin() (sdk.Coin, error) { - transferAmount, ok := sdkmath.NewIntFromString(t.Amount) - if !ok { - return sdk.Coin{}, errorsmod.Wrapf(ErrInvalidAmount, "unable to parse transfer amount (%s) into math.Int", transferAmount) - } - - coin := sdk.NewCoin(t.Denom.IBCDenom(), transferAmount) - return coin, nil -} - -// UnboundedSpendLimit returns the sentinel value that can be used -// as the amount for a denomination's spend limit for which spend limit updating -// should be disabled. Please note that using this sentinel value means that a grantee -// will be granted the privilege to do ICS20 token transfers for the total amount -// of the denomination available at the granter's account. -func UnboundedSpendLimit() sdkmath.Int { - return sdkmath.NewIntFromBigInt(maxUint256) -} diff --git a/modules/apps/transfer/types/transfer_authorization.go b/modules/apps/transfer/types/transfer_authorization.go index 03080bf42b2..be27a340afd 100644 --- a/modules/apps/transfer/types/transfer_authorization.go +++ b/modules/apps/transfer/types/transfer_authorization.go @@ -19,14 +19,9 @@ import ( var _ authz.Authorization = (*TransferAuthorization)(nil) -<<<<<<< HEAD -// maxUint256 is the maximum value for a 256 bit unsigned integer. -var maxUint256 = new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 256), big.NewInt(1)) -======= const ( allocationNotFound = -1 ) ->>>>>>> 92e1f387 ((feat) Add possibility to transfer entire balance. (#6877)) // NewTransferAuthorization creates a new TransferAuthorization object. func NewTransferAuthorization(allocations ...Allocation) *TransferAuthorization { @@ -188,15 +183,6 @@ func validateMemo(ctx sdk.Context, memo string, allowedMemos []string) error { return nil } -<<<<<<< HEAD -// UnboundedSpendLimit returns the sentinel value that can be used -// as the amount for a denomination's spend limit for which spend limit updating -// should be disabled. Please note that using this sentinel value means that a grantee -// will be granted the privilege to do ICS20 token transfers for the total amount -// of the denomination available at the granter's account. -func UnboundedSpendLimit() sdkmath.Int { - return sdkmath.NewIntFromBigInt(maxUint256) -======= // getAllocationIndex ranges through a set of allocations, and returns the index of the allocation if found. If not, returns -1. func getAllocationIndex(msg MsgTransfer, allocations []Allocation) int { for index, allocation := range allocations { @@ -205,5 +191,4 @@ func getAllocationIndex(msg MsgTransfer, allocations []Allocation) int { } } return allocationNotFound ->>>>>>> 92e1f387 ((feat) Add possibility to transfer entire balance. (#6877)) }