diff --git a/.github/compatibility-test-matrices/main/transfer-v2-multidenom-chain-a.json b/.github/compatibility-test-matrices/main/transfer-v2-multidenom-chain-a.json new file mode 100644 index 00000000000..fb04ec8ba4d --- /dev/null +++ b/.github/compatibility-test-matrices/main/transfer-v2-multidenom-chain-a.json @@ -0,0 +1,19 @@ +{ + "chain-a": [ + "main" + ], + "chain-b": [ + "main" + ], + "entrypoint": [ + "TestTransferTestSuite" + ], + "test": [ + "TestMsgTransfer_Succeeds_Nonincentivized_MultiDenom", + "TestMsgTransfer_EntireBalance", + "TestMsgTransfer_Fails_InvalidAddress_MultiDenom" + ], + "relayer-type": [ + "hermes" + ] +} \ No newline at end of file diff --git a/.github/compatibility-test-matrices/release-v9.0.x/transfer-v2-multidenom-chain-a.json b/.github/compatibility-test-matrices/release-v9.0.x/transfer-v2-multidenom-chain-a.json new file mode 100644 index 00000000000..e75a7b6c73e --- /dev/null +++ b/.github/compatibility-test-matrices/release-v9.0.x/transfer-v2-multidenom-chain-a.json @@ -0,0 +1,19 @@ +{ + "chain-a": [ + "release-v9.0.x" + ], + "chain-b": [ + "release-v9.0.x" + ], + "entrypoint": [ + "TestTransferTestSuite" + ], + "test": [ + "TestMsgTransfer_Succeeds_Nonincentivized_MultiDenom", + "TestMsgTransfer_EntireBalance", + "TestMsgTransfer_Fails_InvalidAddress_MultiDenom" + ], + "relayer-type": [ + "hermes" + ] +} \ No newline at end of file diff --git a/.github/compatibility-test-matrices/unreleased/transfer-v2-multidenom.json b/.github/compatibility-test-matrices/unreleased/transfer-v2-multidenom.json new file mode 100644 index 00000000000..da1134e3fa6 --- /dev/null +++ b/.github/compatibility-test-matrices/unreleased/transfer-v2-multidenom.json @@ -0,0 +1,19 @@ +{ + "chain-a": [ + "release-v9.0.x" + ], + "chain-b": [ + "release-v9.0.x" + ], + "entrypoint": [ + "TestTransferTestSuite" + ], + "test": [ + "TestMsgTransfer_Succeeds_Nonincentivized_MultiDenom", + "TestMsgTransfer_EntireBalance", + "TestMsgTransfer_Fails_InvalidAddress_MultiDenom" + ], + "relayer-type": [ + "hermes" + ] +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 2205df42599..7b2f57f7b61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,8 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Features +* (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. + ### Bug Fixes ## [v7.6.0](https://github.com/cosmos/ibc-go/releases/tag/v7.6.0) - 2024-06-20 diff --git a/modules/apps/transfer/keeper/relay.go b/modules/apps/transfer/keeper/relay.go index 29ea46a6b57..3a36b6c6580 100644 --- a/modules/apps/transfer/keeper/relay.go +++ b/modules/apps/transfer/keeper/relay.go @@ -93,6 +93,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 e5eeb9a8ef6..c1ea80166ef 100644 --- a/modules/apps/transfer/keeper/relay_test.go +++ b/modules/apps/transfer/keeper/relay_test.go @@ -58,6 +58,15 @@ func (suite *KeeperTestSuite) TestSendTransfer() { memo = "memo" }, true, }, + { + "successful transfer of entire balance", + func() { + coin.Amount = types.UnboundedSpendLimit() + var ok bool + expEscrowAmount, ok = sdk.NewIntFromString(ibctesting.DefaultGenesisAccBalance) + suite.Require().True(ok) + }, true, + }, { "source channel not found", func() { diff --git a/modules/apps/transfer/types/coin.go b/modules/apps/transfer/types/coin.go index 6abc5367cbe..a8cebdb62c2 100644 --- a/modules/apps/transfer/types/coin.go +++ b/modules/apps/transfer/types/coin.go @@ -2,12 +2,16 @@ package types import ( "fmt" + "math/big" "strings" - "cosmossdk.io/math" + sdkmath "cosmossdk.io/math" 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 { @@ -42,7 +46,16 @@ func GetPrefixedDenom(portID, channelID, baseDenom string) string { // GetTransferCoin creates a transfer coin with the port ID and channel ID // prefixed to the base denom. -func GetTransferCoin(portID, channelID, baseDenom string, amount math.Int) sdk.Coin { +func GetTransferCoin(portID, channelID, baseDenom string, amount sdkmath.Int) sdk.Coin { 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/transfer_authorization.go b/modules/apps/transfer/types/transfer_authorization.go index 1af6f9c9c43..8676c6fb679 100644 --- a/modules/apps/transfer/types/transfer_authorization.go +++ b/modules/apps/transfer/types/transfer_authorization.go @@ -1,10 +1,8 @@ package types import ( - "math/big" "strings" - sdkmath "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/cosmos/cosmos-sdk/x/authz" @@ -15,9 +13,6 @@ import ( var _ authz.Authorization = &TransferAuthorization{} -// 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)) - // NewTransferAuthorization creates a new TransferAuthorization object. func NewTransferAuthorization(allocations ...Allocation) *TransferAuthorization { return &TransferAuthorization{ @@ -174,12 +169,3 @@ func validateMemo(ctx sdk.Context, memo string, allowedMemos []string) error { return sdkerrors.Wrapf(ErrInvalidAuthorization, "not allowed memo: %s", memo) } - -// 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 sdk.NewIntFromBigInt(maxUint256) -} diff --git a/testing/chain.go b/testing/chain.go index b34003adc2d..7ad257b3a80 100644 --- a/testing/chain.go +++ b/testing/chain.go @@ -42,6 +42,10 @@ type SenderAccount struct { SenderAccount authtypes.AccountI } +const ( + DefaultGenesisAccBalance = "10000000000000000000" +) + // TestChain is a testing struct that wraps a simapp with the last TM Header, the current ABCI // header and the validators of the TestChain. It also contains a field called ChainID. This // is the clientID that *other* chains use to refer to this TestChain. The SenderAccount @@ -104,7 +108,7 @@ func NewTestChainWithValSet(t *testing.T, coord *Coordinator, chainID string, va for i := 0; i < MaxAccounts; i++ { senderPrivKey := secp256k1.GenPrivKey() acc := authtypes.NewBaseAccount(senderPrivKey.PubKey().Address().Bytes(), senderPrivKey.PubKey(), uint64(i), 0) - amount, ok := sdk.NewIntFromString("10000000000000000000") + amount, ok := sdk.NewIntFromString(DefaultGenesisAccBalance) require.True(t, ok) // add sender account