diff --git a/CHANGELOG.md b/CHANGELOG.md index 87f8373c29b..74834f51b28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,7 +40,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ * [\#6193](https://github.com/cosmos/ibc-go/pull/6193) Bump Cosmos SDK to v0.50.7. * [\#6193](https://github.com/cosmos/ibc-go/pull/6193) Bump `cosmossdk.io/store` to v1.1.0. -* [\#6239](https://github.com/cosmos/ibc-go/pull/6239) Bump CometBFT to v0.38.7. +* [\#6720](https://github.com/cosmos/ibc-go/pull/6720) Bump CometBFT to v0.38.8. * [\#6380](https://github.com/cosmos/ibc-go/pull/6380) Bump go to v1.22. ### API Breaking @@ -58,6 +58,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ * (apps/27-interchain-accounts) [\#6433](https://github.com/cosmos/ibc-go/pull/6433) Use UNORDERED as the default ordering for new ICA channels. * (apps/transfer) [\#6440](https://github.com/cosmos/ibc-go/pull/6440) Remove `GetPrefixedDenom`. * (apps/transfer) [\#6508](https://github.com/cosmos/ibc-go/pull/6508) Remove the `DenomTrace` type. +* (apps/27-interchain-accounts) [\#6598](https://github.com/cosmos/ibc-go/pull/6598) Mark the `requests` repeated field of `MsgModuleQuerySafe` non-nullable. * (23-commmitment) [\#6633](https://github.com/cosmos/ibc-go/pull/6633) MerklePath has been changed to use `repeated bytes` in favour of `repeated strings`. ### State Machine Breaking @@ -73,6 +74,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Features * (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. ### Bug Fixes diff --git a/docs/docs/01-ibc/13-best-practices.md b/docs/docs/01-ibc/13-best-practices.md new file mode 100644 index 00000000000..91093a1d984 --- /dev/null +++ b/docs/docs/01-ibc/13-best-practices.md @@ -0,0 +1,25 @@ +--- +title: Best Practices +sidebar_label: Best Practices +sidebar_position: 12 +slug: /ibc/best-practices +--- + +# Best practices + +## Identifying legitimate channels + +Identifying which channel to use can be difficult as it requires verifying information about the chains you want to connect to. +Channels are based on a light client. A chain can be uniquely identified by its chain ID, validator set pairing. It is unsafe to rely only on the chain ID. +Any user can create a client with any chain ID, but only the chain with correct validator set and chain ID can produce headers which would update that client. + +Which channel to use is based on social consensus. The desired channel should have the following properities: + +- based on a valid client (can only be updated by the chain it connects to) +- has sizable activity +- the underlying client is active + +To verify if a client is valid. You will need to obtain a header from the chain you want to connect to. This can be done by running a full node for that chain or relying on a trusted rpc address. +Then you should query the light client you want to verify and obtain its latest consensus state. All consensus state fields must match the header queried for at same height as the consensus state (root, timestamp, next validator set hash). + +Explorers and wallets are highly encouraged to follow this practice. It is unsafe to algorithmically add new channels without following this process. diff --git a/docs/docs/02-apps/02-interchain-accounts/05-messages.md b/docs/docs/02-apps/02-interchain-accounts/05-messages.md index 69599f03538..fa5833bd976 100644 --- a/docs/docs/02-apps/02-interchain-accounts/05-messages.md +++ b/docs/docs/02-apps/02-interchain-accounts/05-messages.md @@ -130,7 +130,7 @@ balanceQuery := banktypes.NewQueryBalanceRequest("cosmos1...", "uatom") queryBz, err := balanceQuery.Marshal() // signer of message must be the interchain account on the host -queryMsg := icahosttypes.NewMsgModuleQuerySafe("cosmos2...", []*icahosttypes.QueryRequest{ +queryMsg := icahosttypes.NewMsgModuleQuerySafe("cosmos2...", []icahosttypes.QueryRequest{ { Path: "/cosmos.bank.v1beta1.Query/Balance", Data: queryBz, diff --git a/docs/docs/05-migrations/13-v8-to-v9.md b/docs/docs/05-migrations/13-v8-to-v9.md index ebeb98b477b..c7cef378655 100644 --- a/docs/docs/05-migrations/13-v8-to-v9.md +++ b/docs/docs/05-migrations/13-v8-to-v9.md @@ -69,6 +69,10 @@ func AssertEvents( - `UnmarshalPacketData` now takes in the context, portID, and channelID. This allows the packet data to be unmarshaled based on the channel version. - `Router` reference has been removed from IBC core keeper: [#6138](https://github.com/cosmos/ibc-go/pull/6138) +### ICS20 - Transfer + +The functions [`SenderChainIsSource` and `ReceiverChainIsSource`](https://github.com/cosmos/ibc-go/blob/v8.0.0/modules/apps/transfer/types/coin.go#L12-L32) have been replaced with the function `HasPrefix` of the newly added `Denom` type. + ### ICS27 - Interchain Accounts In [#5785](https://github.com/cosmos/ibc-go/pull/5785) the list of arguments of the `NewKeeper` constructor function of the host submodule was extended with an extra argument for the gRPC query router that the submodule uses when executing a [`MsgModuleQuerySafe`](https://github.com/cosmos/ibc-go/blob/eecfa5c09a4c38a5c9f2cc2a322d2286f45911da/proto/ibc/applications/interchain_accounts/host/v1/tx.proto#L41-L51) to perform queries that are module safe: @@ -95,6 +99,16 @@ func (k Keeper) RegisterInterchainAccount( ) error { ``` +The `requests` repeated field of `MsgModuleQuerySafe` has been marked non-nullable, and therefore the signature of the constructor function `NewMsgModuleQuerySafe` has been updated: + +```diff +func NewMsgModuleQuerySafe( + signer string, +- requests []*QueryRequest, ++ requests []QueryRequest, +) *MsgModuleQuerySafe { +``` + ## Relayers - Renaming of event attribute keys in [#5603](https://github.com/cosmos/ibc-go/pull/5603). diff --git a/e2e/go.mod b/e2e/go.mod index ca8df0d619d..5346914afbc 100644 --- a/e2e/go.mod +++ b/e2e/go.mod @@ -10,7 +10,7 @@ require ( cosmossdk.io/errors v1.0.1 cosmossdk.io/math v1.3.0 cosmossdk.io/x/upgrade v0.1.3 - github.com/cometbft/cometbft v0.38.7 + github.com/cometbft/cometbft v0.38.8 github.com/cosmos/cosmos-sdk v0.50.7 github.com/cosmos/gogoproto v1.5.0 github.com/cosmos/ibc-go/modules/light-clients/08-wasm v0.0.0-00010101000000-000000000000 @@ -143,6 +143,7 @@ require ( github.com/hashicorp/go-safetemp v1.0.0 // indirect github.com/hashicorp/go-version v1.6.0 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/yamux v0.1.1 // indirect github.com/hdevalence/ed25519consensus v0.1.0 // indirect diff --git a/e2e/go.sum b/e2e/go.sum index cce26c78aef..f9712b6d68f 100644 --- a/e2e/go.sum +++ b/e2e/go.sum @@ -358,8 +358,8 @@ github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZ github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= -github.com/cometbft/cometbft v0.38.7 h1:ULhIOJ9+LgSy6nLekhq9ae3juX3NnQUMMPyVdhZV6Hk= -github.com/cometbft/cometbft v0.38.7/go.mod h1:HIyf811dFMI73IE0F7RrnY/Fr+d1+HuJAgtkEpQjCMY= +github.com/cometbft/cometbft v0.38.8 h1:XyJ9Cu3xqap6xtNxiemrO8roXZ+KS2Zlu7qQ0w1trvU= +github.com/cometbft/cometbft v0.38.8/go.mod h1:xOoGZrtUT+A5izWfHSJgl0gYZUE7lu7Z2XIS1vWG/QQ= github.com/cometbft/cometbft-db v0.9.1 h1:MIhVX5ja5bXNHF8EYrThkG9F7r9kSfv8BX4LWaxWJ4M= github.com/cometbft/cometbft-db v0.9.1/go.mod h1:iliyWaoV0mRwBJoizElCwwRA9Tf7jZJOURcRZF9m60U= github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg= @@ -712,6 +712,8 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= diff --git a/e2e/relayer/relayer.go b/e2e/relayer/relayer.go index 3cae393b52a..27e63dcaf4d 100644 --- a/e2e/relayer/relayer.go +++ b/e2e/relayer/relayer.go @@ -20,7 +20,7 @@ const ( Hyperspace = "hyperspace" HermesRelayerRepository = "ghcr.io/informalsystems/hermes" - hermesRelayerUser = "1000:1000" + hermesRelayerUser = "2000:2000" RlyRelayerRepository = "ghcr.io/cosmos/relayer" rlyRelayerUser = "100:1000" diff --git a/e2e/tests/core/03-connection/connection_test.go b/e2e/tests/core/03-connection/connection_test.go index 9caeb4a302b..93dbcd98004 100644 --- a/e2e/tests/core/03-connection/connection_test.go +++ b/e2e/tests/core/03-connection/connection_test.go @@ -34,7 +34,7 @@ type ConnectionTestSuite struct { } func (s *ConnectionTestSuite) SetupTest() { - s.SetupPath(ibc.DefaultClientOpts(), s.TransferChannelOptions()) + s.SetupPaths(ibc.DefaultClientOpts(), s.TransferChannelOptions()) } // QueryMaxExpectedTimePerBlockParam queries the on-chain max expected time per block param for 03-connection diff --git a/e2e/tests/interchain_accounts/query_test.go b/e2e/tests/interchain_accounts/query_test.go index 7c864bf70c7..cd648b19455 100644 --- a/e2e/tests/interchain_accounts/query_test.go +++ b/e2e/tests/interchain_accounts/query_test.go @@ -82,7 +82,7 @@ func (s *InterchainAccountsQueryTestSuite) TestInterchainAccountsQuery() { queryBz, err := balanceQuery.Marshal() s.Require().NoError(err) - queryMsg := icahosttypes.NewMsgModuleQuerySafe(hostAccount, []*icahosttypes.QueryRequest{ + queryMsg := icahosttypes.NewMsgModuleQuerySafe(hostAccount, []icahosttypes.QueryRequest{ { Path: "/cosmos.bank.v1beta1.Query/Balance", Data: queryBz, diff --git a/e2e/tests/transfer/authz_test.go b/e2e/tests/transfer/authz_test.go index 3e3f5389588..2a1d9b7c1fc 100644 --- a/e2e/tests/transfer/authz_test.go +++ b/e2e/tests/transfer/authz_test.go @@ -33,7 +33,7 @@ type AuthzTransferTestSuite struct { } func (suite *AuthzTransferTestSuite) SetupTest() { - suite.SetupPath(ibc.DefaultClientOpts(), suite.TransferChannelOptions()) + suite.SetupPaths(ibc.DefaultClientOpts(), suite.TransferChannelOptions()) } // QueryGranterGrants returns all GrantAuthorizations for the given granterAddress. @@ -131,6 +131,7 @@ func (suite *AuthzTransferTestSuite) TestAuthz_MsgTransfer_Succeeds() { suite.GetTimeoutHeight(ctx, chainB), 0, "", + transfertypes.Forwarding{}, ) protoAny, err := codectypes.NewAnyWithValue(transferMsg) @@ -191,6 +192,7 @@ func (suite *AuthzTransferTestSuite) TestAuthz_MsgTransfer_Succeeds() { suite.GetTimeoutHeight(ctx, chainB), 0, "", + transfertypes.Forwarding{}, ) protoAny, err := codectypes.NewAnyWithValue(transferMsg) @@ -274,6 +276,7 @@ func (suite *AuthzTransferTestSuite) TestAuthz_InvalidTransferAuthorizations() { suite.GetTimeoutHeight(ctx, chainB), 0, "", + transfertypes.Forwarding{}, ) protoAny, err := codectypes.NewAnyWithValue(transferMsg) @@ -334,6 +337,7 @@ func (suite *AuthzTransferTestSuite) TestAuthz_InvalidTransferAuthorizations() { suite.GetTimeoutHeight(ctx, chainB), 0, "", + transfertypes.Forwarding{}, ) protoAny, err := codectypes.NewAnyWithValue(transferMsg) diff --git a/e2e/tests/transfer/base_test.go b/e2e/tests/transfer/base_test.go index 5993f92862e..7acafea833f 100644 --- a/e2e/tests/transfer/base_test.go +++ b/e2e/tests/transfer/base_test.go @@ -34,7 +34,7 @@ type TransferTestSuite struct { } func (s *TransferTestSuite) SetupTest() { - s.SetupPath(ibc.DefaultClientOpts(), s.TransferChannelOptions()) + s.SetupPaths(ibc.DefaultClientOpts(), s.TransferChannelOptions()) } // QueryTransferParams queries the on-chain send enabled param for the transfer module diff --git a/e2e/tests/transfer/forwarding_test.go b/e2e/tests/transfer/forwarding_test.go index a104d69edcd..d038a134d73 100644 --- a/e2e/tests/transfer/forwarding_test.go +++ b/e2e/tests/transfer/forwarding_test.go @@ -5,12 +5,16 @@ package transfer import ( "context" "testing" + "time" + "github.com/strangelove-ventures/interchaintest/v8/ibc" testifysuite "github.com/stretchr/testify/suite" "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/v8/modules/apps/transfer/types" + clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" ) func TestTransferForwardingTestSuite(t *testing.T) { @@ -26,10 +30,9 @@ func (s *TransferForwardingTestSuite) SetupSuite() { s.SetupChains(context.TODO(), nil, testsuite.ThreeChainSetup()) } -// TODO: replace this with actual tests https://github.com/cosmos/ibc-go/issues/6578 -// this test verifies that three chains can be set up, and the relayer will relay -// packets between them as configured in the newInterchain function. -func (s *TransferForwardingTestSuite) TestThreeChainSetup() { +// TestForwarding_WithLastChainBeingICS20v1_Succeeds tests the case where a token is forwarded and successfully +// received on a destination chain that is on ics20-v1 version. +func (s *TransferForwardingTestSuite) TestForwarding_WithLastChainBeingICS20v1_Succeeds() { ctx := context.TODO() t := s.T() @@ -37,61 +40,54 @@ func (s *TransferForwardingTestSuite) TestThreeChainSetup() { chainA, chainB, chainC := chains[0], chains[1], chains[2] - chainAChannels, err := relayer.GetChannels(ctx, s.GetRelayerExecReporter(), chainA.Config().ChainID) - s.Require().NoError(err) - chainBChannels, err := relayer.GetChannels(ctx, s.GetRelayerExecReporter(), chainB.Config().ChainID) - s.Require().NoError(err) - chainCChannels, err := relayer.GetChannels(ctx, s.GetRelayerExecReporter(), chainC.Config().ChainID) - s.Require().NoError(err) + channelAtoB := s.GetChainAChannel() - s.Require().Len(chainAChannels, 1, "expected 1 channels on chain A") - s.Require().Len(chainBChannels, 2, "expected 2 channels on chain B") - s.Require().Len(chainCChannels, 1, "expected 1 channels on chain C") - - channelAToB := chainAChannels[0] - channelBToC := chainBChannels[1] + // Creating a new path between chain B and chain C with a ICS20-v1 channel + opts := s.TransferChannelOptions() + opts.Version = transfertypes.V1 + channelBtoC, _ := s.CreatePath(ctx, chainB, chainC, ibc.DefaultClientOpts(), opts) + s.Require().Equal(transfertypes.V1, channelBtoC.Version, "the channel version is not ics20-1") chainAWallet := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) chainAAddress := chainAWallet.FormattedAddress() chainADenom := chainA.Config().Denom - chainBWallet := s.CreateUserOnChainB(ctx, testvalues.StartingTokenAmount) - chainBAddress := chainBWallet.FormattedAddress() - chainBDenom := chainB.Config().Denom - chainCWallet := s.CreateUserOnChainC(ctx, testvalues.StartingTokenAmount) chainCAddress := chainCWallet.FormattedAddress() - t.Run("IBC transfer from A to B", func(t *testing.T) { - transferTxResp := s.Transfer(ctx, chainA, chainAWallet, channelAToB.PortID, channelAToB.ChannelID, testvalues.DefaultTransferCoins(chainADenom), chainAAddress, chainBAddress, s.GetTimeoutHeight(ctx, chainB), 0, "") - s.AssertTxSuccess(transferTxResp) - }) - - t.Run("IBC transfer from B to C", func(t *testing.T) { - transferTxResp := s.Transfer(ctx, chainB, chainBWallet, channelBToC.PortID, channelBToC.ChannelID, testvalues.DefaultTransferCoins(chainBDenom), chainBAddress, chainCAddress, s.GetTimeoutHeight(ctx, chainC), 0, "") - s.AssertTxSuccess(transferTxResp) + t.Run("IBC transfer from A to C with forwarding through B", func(t *testing.T) { + inFiveMinutes := time.Now().Add(5 * time.Minute).UnixNano() + forwarding := transfertypes.NewForwarding(false, transfertypes.NewHop(channelBtoC.PortID, channelBtoC.ChannelID)) + + msgTransfer := testsuite.GetMsgTransfer( + channelAtoB.PortID, + channelAtoB.ChannelID, + transfertypes.V2, + testvalues.DefaultTransferCoins(chainADenom), + chainAAddress, + chainCAddress, + clienttypes.ZeroHeight(), + uint64(inFiveMinutes), + "", + forwarding) + resp := s.BroadcastMessages(ctx, chainA, chainAWallet, msgTransfer) + s.AssertTxSuccess(resp) }) t.Run("start relayer", func(t *testing.T) { s.StartRelayer(relayer) }) - chainBIBCToken := testsuite.GetIBCToken(chainADenom, channelAToB.Counterparty.PortID, channelAToB.Counterparty.ChannelID) - t.Run("packets are relayed from A to B", func(t *testing.T) { - s.AssertPacketRelayed(ctx, chainA, channelAToB.PortID, channelAToB.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("packets are relayed from A to B to C", func(t *testing.T) { + chainCDenom := transfertypes.NewDenom(chainADenom, + transfertypes.NewTrace(channelBtoC.Counterparty.PortID, channelBtoC.Counterparty.ChannelID), + transfertypes.NewTrace(channelAtoB.Counterparty.PortID, channelAtoB.Counterparty.ChannelID), + ) - chainCIBCToken := testsuite.GetIBCToken(chainBDenom, channelBToC.Counterparty.PortID, channelBToC.Counterparty.ChannelID) - t.Run("packets are relayed from B to C", func(t *testing.T) { - s.AssertPacketRelayed(ctx, chainB, channelBToC.PortID, channelBToC.ChannelID, 1) + s.AssertPacketRelayed(ctx, chainA, channelAtoB.PortID, channelAtoB.ChannelID, 1) + s.AssertPacketRelayed(ctx, chainB, channelBtoC.PortID, channelBtoC.ChannelID, 1) - actualBalance, err := query.Balance(ctx, chainC, chainCAddress, chainCIBCToken.IBCDenom()) + actualBalance, err := query.Balance(ctx, chainC, chainCAddress, chainCDenom.IBCDenom()) s.Require().NoError(err) expected := testvalues.IBCTransferAmount diff --git a/e2e/tests/transfer/incentivized_test.go b/e2e/tests/transfer/incentivized_test.go index d57a8b2c0b8..350d0ccaa37 100644 --- a/e2e/tests/transfer/incentivized_test.go +++ b/e2e/tests/transfer/incentivized_test.go @@ -20,6 +20,7 @@ import ( "github.com/cosmos/ibc-go/e2e/testsuite/query" "github.com/cosmos/ibc-go/e2e/testvalues" feetypes "github.com/cosmos/ibc-go/v8/modules/apps/29-fee/types" + transfertypes "github.com/cosmos/ibc-go/v8/modules/apps/transfer/types" channeltypes "github.com/cosmos/ibc-go/v8/modules/core/04-channel/types" ) @@ -38,7 +39,7 @@ func TestIncentivizedTransferTestSuite(t *testing.T) { // SetupTest explicitly enables fee middleware in the channel options. func (s *IncentivizedTransferTestSuite) SetupTest() { - s.SetupPath(ibc.DefaultClientOpts(), s.FeeTransferChannelOptions()) + s.SetupPaths(ibc.DefaultClientOpts(), s.FeeTransferChannelOptions()) } func (s *IncentivizedTransferTestSuite) TestMsgPayPacketFee_AsyncSingleSender_Succeeds() { @@ -216,6 +217,7 @@ func (s *IncentivizedTransferTestSuite) TestMsgPayPacketFee_InvalidReceiverAccou s.GetTimeoutHeight(ctx, chainB), 0, "", + transfertypes.Forwarding{}, ) txResp := s.BroadcastMessages(ctx, chainA, chainAWallet, msgTransfer) // this message should be successful, as receiver account is not validated on the sending chain. @@ -354,6 +356,7 @@ func (s *IncentivizedTransferTestSuite) TestMultiMsg_MsgPayPacketFeeSingleSender s.GetTimeoutHeight(ctx, chainB), 0, "", + transfertypes.Forwarding{}, ) resp := s.BroadcastMessages(ctx, chainA, chainAWallet, msgPayPacketFee, msgTransfer) s.AssertTxSuccess(resp) diff --git a/e2e/tests/transfer/upgradesv1_test.go b/e2e/tests/transfer/upgradesv1_test.go index 254b288127f..31bcaf7faa2 100644 --- a/e2e/tests/transfer/upgradesv1_test.go +++ b/e2e/tests/transfer/upgradesv1_test.go @@ -33,7 +33,7 @@ type TransferChannelUpgradesV1TestSuite struct { func (s *TransferChannelUpgradesV1TestSuite) SetupTest() { opts := s.TransferChannelOptions() opts.Version = transfertypes.V1 - s.SetupPath(ibc.DefaultClientOpts(), opts) + s.SetupPaths(ibc.DefaultClientOpts(), opts) } // TestChannelUpgrade_WithICS20v2_Succeeds tests upgrading a transfer channel to ICS20 v2. @@ -306,6 +306,7 @@ func (s *TransferChannelUpgradesV1TestSuite) TestChannelUpgrade_WithFeeMiddlewar s.GetTimeoutHeight(ctx, chainB), 0, "", + transfertypes.Forwarding{}, ) resp := s.BroadcastMessages(ctx, chainA, chainAWallet, msgPayPacketFee, msgTransfer) s.AssertTxSuccess(resp) diff --git a/e2e/tests/transfer/upgradesv2_test.go b/e2e/tests/transfer/upgradesv2_test.go index b35d38d5dba..96033742f64 100644 --- a/e2e/tests/transfer/upgradesv2_test.go +++ b/e2e/tests/transfer/upgradesv2_test.go @@ -32,7 +32,7 @@ type TransferChannelUpgradesTestSuite struct { } func (s *TransferChannelUpgradesTestSuite) SetupTest() { - s.SetupPath(ibc.DefaultClientOpts(), s.TransferChannelOptions()) + s.SetupPaths(ibc.DefaultClientOpts(), s.TransferChannelOptions()) } // TestChannelUpgrade_WithFeeMiddleware_Succeeds tests upgrading a transfer channel to wire up fee middleware @@ -213,6 +213,7 @@ func (s *TransferChannelUpgradesTestSuite) TestChannelUpgrade_WithFeeMiddleware_ s.GetTimeoutHeight(ctx, chainB), 0, "", + transfertypes.Forwarding{}, ) resp := s.BroadcastMessages(ctx, chainA, chainAWallet, msgPayPacketFee, msgTransfer) s.AssertTxSuccess(resp) @@ -370,10 +371,6 @@ func (s *TransferChannelUpgradesTestSuite) TestChannelUpgrade_WithFeeMiddleware_ s.AssertTxSuccess(transferTxResp) }) - t.Run("start relayer", func(t *testing.T) { - s.StartRelayer(relayer) - }) - t.Run("execute gov proposals to initiate channel upgrade on chain A and chain B", func(t *testing.T) { var wg sync.WaitGroup @@ -398,6 +395,10 @@ func (s *TransferChannelUpgradesTestSuite) TestChannelUpgrade_WithFeeMiddleware_ s.Require().NoError(test.WaitForBlocks(ctx, 10, chainA, chainB), "failed to wait for blocks") + t.Run("start relayer", func(t *testing.T) { + s.StartRelayer(relayer) + }) + t.Run("packets are relayed between chain A and chain B", func(t *testing.T) { // packet from chain A to chain B s.AssertPacketRelayed(ctx, chainA, channelA.PortID, channelA.ChannelID, 1) diff --git a/e2e/tests/upgrades/upgrade_test.go b/e2e/tests/upgrades/upgrade_test.go index 0e6b7ad56aa..bbfa2567915 100644 --- a/e2e/tests/upgrades/upgrade_test.go +++ b/e2e/tests/upgrades/upgrade_test.go @@ -29,6 +29,7 @@ import ( "github.com/cosmos/ibc-go/e2e/testsuite/query" "github.com/cosmos/ibc-go/e2e/testvalues" feetypes "github.com/cosmos/ibc-go/v8/modules/apps/29-fee/types" + transfertypes "github.com/cosmos/ibc-go/v8/modules/apps/transfer/types" v7migrations "github.com/cosmos/ibc-go/v8/modules/core/02-client/migrations/v7" clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" connectiontypes "github.com/cosmos/ibc-go/v8/modules/core/03-connection/types" @@ -62,7 +63,7 @@ func (s *UpgradeTestSuite) SetupTest() { if strings.HasSuffix(s.T().Name(), "TestV8ToV8_1ChainUpgrade") { channelOpts = s.FeeTransferChannelOptions() } - s.SetupPath(ibc.DefaultClientOpts(), channelOpts) + s.SetupPaths(ibc.DefaultClientOpts(), channelOpts) } // UpgradeChain upgrades a chain to a specific version using the planName provided. @@ -978,6 +979,7 @@ func (s *UpgradeTestSuite) TestV8ToV8_1ChainUpgrade_ChannelUpgrades() { s.GetTimeoutHeight(ctx, chainB), 0, "", + transfertypes.Forwarding{}, ) resp := s.BroadcastMessages(ctx, chainA, chainAWallet, msgPayPacketFee, msgTransfer) s.AssertTxSuccess(resp) diff --git a/e2e/tests/wasm/grandpa_test.go b/e2e/tests/wasm/grandpa_test.go index 845a5c7bfe8..8543fb5d4cb 100644 --- a/e2e/tests/wasm/grandpa_test.go +++ b/e2e/tests/wasm/grandpa_test.go @@ -154,7 +154,7 @@ func (s *GrandpaTestSuite) SetupTest() { channelOpts := ibc.DefaultChannelOpts() channelOpts.Version = transfertypes.V1 - s.SetupPath(ibc.DefaultClientOpts(), channelOpts) + s.SetupPaths(ibc.DefaultClientOpts(), channelOpts) } // TestMsgTransfer_Succeeds_GrandpaContract features diff --git a/e2e/testsuite/testconfig.go b/e2e/testsuite/testconfig.go index 74ad3fc6d89..f32deb7d938 100644 --- a/e2e/testsuite/testconfig.go +++ b/e2e/testsuite/testconfig.go @@ -62,7 +62,7 @@ const ( // TODO: https://github.com/cosmos/ibc-go/issues/4965 defaultHyperspaceTag = "20231122v39" // defaultHermesTag is the tag that will be used if no relayer tag is specified for hermes. - defaultHermesTag = "sean-channel-upgradability" + defaultHermesTag = "1.10.0" // defaultChainTag is the tag that will be used for the chains if none is specified. defaultChainTag = "main" // defaultConfigFileName is the default filename for the config file that can be used to configure diff --git a/e2e/testsuite/testsuite.go b/e2e/testsuite/testsuite.go index 4015f01f4c7..831f0ad5d2e 100644 --- a/e2e/testsuite/testsuite.go +++ b/e2e/testsuite/testsuite.go @@ -191,57 +191,71 @@ func (s *E2ETestSuite) SetupChains(ctx context.Context, channelOptionsModifier C // SetupTest will by default use the default channel options to create a path between the chains. // if non default channel options are required, the test suite must override the `SetupTest` function. func (s *E2ETestSuite) SetupTest() { - s.SetupPath(ibc.DefaultClientOpts(), defaultChannelOpts(s.GetAllChains())) + s.SetupPaths(ibc.DefaultClientOpts(), defaultChannelOpts(s.GetAllChains())) } -// SetupPath creates a path between the chains using the provided client and channel options. -func (s *E2ETestSuite) SetupPath(clientOpts ibc.CreateClientOptions, channelOpts ibc.CreateChannelOptions) { +// SetupPaths creates paths between the chains using the provided client and channel options. +// The paths are created such that ChainA is connected to ChainB, ChainB is connected to ChainC etc. +func (s *E2ETestSuite) SetupPaths(clientOpts ibc.CreateClientOptions, channelOpts ibc.CreateChannelOptions) { s.T().Logf("Setting up path for: %s", s.T().Name()) - r := s.GetRelayer() - - if s.channels[s.T().Name()] == nil { - s.channels[s.T().Name()] = make(map[ibc.Chain][]ibc.ChannelOutput) - } ctx := context.TODO() allChains := s.GetAllChains() for i := 0; i < len(allChains)-1; i++ { chainA, chainB := allChains[i], allChains[i+1] - pathName := s.generatePathName() - s.T().Logf("establishing path between %s and %s on path %s", chainA.Config().ChainID, chainB.Config().ChainID, pathName) + _, _ = s.CreatePath(ctx, chainA, chainB, clientOpts, channelOpts) + } +} + +// CreatePath creates a path between chainA and chainB using the provided client and channel options. +func (s *E2ETestSuite) CreatePath( + ctx context.Context, + chainA ibc.Chain, + chainB ibc.Chain, + clientOpts ibc.CreateClientOptions, + channelOpts ibc.CreateChannelOptions, +) (chainAChannel ibc.ChannelOutput, chainBChannel ibc.ChannelOutput) { + r := s.GetRelayer() - err := r.GeneratePath(ctx, s.GetRelayerExecReporter(), chainA.Config().ChainID, chainB.Config().ChainID, pathName) - s.Require().NoError(err) + pathName := s.generatePathName() + s.T().Logf("establishing path between %s and %s on path %s", chainA.Config().ChainID, chainB.Config().ChainID, pathName) - // Create new clients - err = r.CreateClients(ctx, s.GetRelayerExecReporter(), pathName, clientOpts) - s.Require().NoError(err) - err = test.WaitForBlocks(ctx, 1, chainA, chainB) - s.Require().NoError(err) + err := r.GeneratePath(ctx, s.GetRelayerExecReporter(), chainA.Config().ChainID, chainB.Config().ChainID, pathName) + s.Require().NoError(err) - err = r.CreateConnections(ctx, s.GetRelayerExecReporter(), pathName) - s.Require().NoError(err) - err = test.WaitForBlocks(ctx, 1, chainA, chainB) - s.Require().NoError(err) + // Create new clients + err = r.CreateClients(ctx, s.GetRelayerExecReporter(), pathName, clientOpts) + s.Require().NoError(err) + err = test.WaitForBlocks(ctx, 1, chainA, chainB) + s.Require().NoError(err) - err = r.CreateChannel(ctx, s.GetRelayerExecReporter(), pathName, channelOpts) - s.Require().NoError(err) - err = test.WaitForBlocks(ctx, 1, chainA, chainB) - s.Require().NoError(err) + err = r.CreateConnections(ctx, s.GetRelayerExecReporter(), pathName) + s.Require().NoError(err) + err = test.WaitForBlocks(ctx, 1, chainA, chainB) + s.Require().NoError(err) - s.testPaths[s.T().Name()] = append(s.testPaths[s.T().Name()], pathName) + err = r.CreateChannel(ctx, s.GetRelayerExecReporter(), pathName, channelOpts) + s.Require().NoError(err) + err = test.WaitForBlocks(ctx, 1, chainA, chainB) + s.Require().NoError(err) - for _, c := range []ibc.Chain{chainA, chainB} { - channels, err := r.GetChannels(ctx, s.GetRelayerExecReporter(), c.Config().ChainID) - s.Require().NoError(err) + channelsA, err := r.GetChannels(ctx, s.GetRelayerExecReporter(), chainA.Config().ChainID) + s.Require().NoError(err) - // only the most recent channel is relevant. - s.channels[s.T().Name()][c] = []ibc.ChannelOutput{channels[len(channels)-1]} + channelsB, err := r.GetChannels(ctx, s.GetRelayerExecReporter(), chainB.Config().ChainID) + s.Require().NoError(err) - err = relayer.ApplyPacketFilter(ctx, s.T(), r, c.Config().ChainID, channels) - s.Require().NoError(err, "failed to watch port and channel on chain: %s", c.Config().ChainID) - } + if s.channels[s.T().Name()] == nil { + s.channels[s.T().Name()] = make(map[ibc.Chain][]ibc.ChannelOutput) } + + // keep track of channels associated with a given chain for access within the tests. + s.channels[s.T().Name()][chainA] = channelsA + s.channels[s.T().Name()][chainB] = channelsB + + s.testPaths[s.T().Name()] = append(s.testPaths[s.T().Name()], pathName) + + return channelsA[len(channelsA)-1], channelsB[len(channelsB)-1] } // GetChainAChannel returns the ibc.ChannelOutput for the current test. @@ -692,7 +706,7 @@ func getValidatorsAndFullNodes(chainIdx int) (int, int) { } // GetMsgTransfer returns a MsgTransfer that is constructed based on the channel version -func GetMsgTransfer(portID, channelID, version string, tokens sdk.Coins, sender, receiver string, timeoutHeight clienttypes.Height, timeoutTimestamp uint64, memo string) *transfertypes.MsgTransfer { +func GetMsgTransfer(portID, channelID, version string, tokens sdk.Coins, sender, receiver string, timeoutHeight clienttypes.Height, timeoutTimestamp uint64, memo string, forwarding transfertypes.Forwarding) *transfertypes.MsgTransfer { if len(tokens) == 0 { panic(errors.New("tokens cannot be empty")) } @@ -712,7 +726,7 @@ func GetMsgTransfer(portID, channelID, version string, tokens sdk.Coins, sender, Tokens: sdk.NewCoins(), } case transfertypes.V2: - msg = transfertypes.NewMsgTransfer(portID, channelID, tokens, sender, receiver, timeoutHeight, timeoutTimestamp, memo) + msg = transfertypes.NewMsgTransfer(portID, channelID, tokens, sender, receiver, timeoutHeight, timeoutTimestamp, memo, forwarding) default: panic(fmt.Errorf("unsupported transfer version: %s", version)) } diff --git a/e2e/testsuite/tx.go b/e2e/testsuite/tx.go index 3033d128e91..535870b3c85 100644 --- a/e2e/testsuite/tx.go +++ b/e2e/testsuite/tx.go @@ -30,6 +30,7 @@ import ( "github.com/cosmos/ibc-go/e2e/testsuite/sanitize" "github.com/cosmos/ibc-go/e2e/testvalues" feetypes "github.com/cosmos/ibc-go/v8/modules/apps/29-fee/types" + transfertypes "github.com/cosmos/ibc-go/v8/modules/apps/transfer/types" clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" channeltypes "github.com/cosmos/ibc-go/v8/modules/core/04-channel/types" ) @@ -299,7 +300,7 @@ func (s *E2ETestSuite) Transfer(ctx context.Context, chain ibc.Chain, user ibc.W transferVersion = version.AppVersion } - msg := GetMsgTransfer(portID, channelID, transferVersion, tokens, sender, receiver, timeoutHeight, timeoutTimestamp, memo) + msg := GetMsgTransfer(portID, channelID, transferVersion, tokens, sender, receiver, timeoutHeight, timeoutTimestamp, memo, transfertypes.Forwarding{}) return s.BroadcastMessages(ctx, chain, user, msg) } diff --git a/go.mod b/go.mod index e565a95bbc6..1bc37d90aac 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( cosmossdk.io/x/feegrant v0.1.1 cosmossdk.io/x/tx v0.13.3 cosmossdk.io/x/upgrade v0.1.3 - github.com/cometbft/cometbft v0.38.7 + github.com/cometbft/cometbft v0.38.8 github.com/cosmos/cosmos-db v1.0.2 github.com/cosmos/cosmos-proto v1.0.0-beta.5 github.com/cosmos/cosmos-sdk v0.50.7 @@ -121,6 +121,7 @@ require ( github.com/hashicorp/go-safetemp v1.0.0 // indirect github.com/hashicorp/go-version v1.6.0 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/yamux v0.1.1 // indirect github.com/hdevalence/ed25519consensus v0.1.0 // indirect diff --git a/go.sum b/go.sum index 37c262e81b9..b6984a289ec 100644 --- a/go.sum +++ b/go.sum @@ -333,8 +333,8 @@ github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZ github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= -github.com/cometbft/cometbft v0.38.7 h1:ULhIOJ9+LgSy6nLekhq9ae3juX3NnQUMMPyVdhZV6Hk= -github.com/cometbft/cometbft v0.38.7/go.mod h1:HIyf811dFMI73IE0F7RrnY/Fr+d1+HuJAgtkEpQjCMY= +github.com/cometbft/cometbft v0.38.8 h1:XyJ9Cu3xqap6xtNxiemrO8roXZ+KS2Zlu7qQ0w1trvU= +github.com/cometbft/cometbft v0.38.8/go.mod h1:xOoGZrtUT+A5izWfHSJgl0gYZUE7lu7Z2XIS1vWG/QQ= github.com/cometbft/cometbft-db v0.9.1 h1:MIhVX5ja5bXNHF8EYrThkG9F7r9kSfv8BX4LWaxWJ4M= github.com/cometbft/cometbft-db v0.9.1/go.mod h1:iliyWaoV0mRwBJoizElCwwRA9Tf7jZJOURcRZF9m60U= github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg= @@ -681,6 +681,8 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= diff --git a/modules/apps/27-interchain-accounts/controller/keeper/msg_server_test.go b/modules/apps/27-interchain-accounts/controller/keeper/msg_server_test.go index 462bc422a8f..c137aad908c 100644 --- a/modules/apps/27-interchain-accounts/controller/keeper/msg_server_test.go +++ b/modules/apps/27-interchain-accounts/controller/keeper/msg_server_test.go @@ -5,8 +5,6 @@ import ( "github.com/cosmos/gogoproto/proto" - sdkmath "cosmossdk.io/math" - sdk "github.com/cosmos/cosmos-sdk/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" @@ -191,7 +189,7 @@ func (suite *KeeperTestSuite) TestSubmitTx() { icaMsg := &banktypes.MsgSend{ FromAddress: interchainAccountAddr, ToAddress: suite.chainB.SenderAccount.GetAddress().String(), - Amount: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(100))), + Amount: sdk.NewCoins(ibctesting.TestCoin), } data, err := icatypes.SerializeCosmosTx(suite.chainA.GetSimApp().AppCodec(), []proto.Message{icaMsg}, icatypes.EncodingProtobuf) diff --git a/modules/apps/27-interchain-accounts/controller/keeper/relay_test.go b/modules/apps/27-interchain-accounts/controller/keeper/relay_test.go index 910e3bf9173..7292f73538a 100644 --- a/modules/apps/27-interchain-accounts/controller/keeper/relay_test.go +++ b/modules/apps/27-interchain-accounts/controller/keeper/relay_test.go @@ -3,8 +3,6 @@ package keeper_test import ( "github.com/cosmos/gogoproto/proto" - sdkmath "cosmossdk.io/math" - sdk "github.com/cosmos/cosmos-sdk/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" @@ -36,7 +34,7 @@ func (suite *KeeperTestSuite) TestSendTx() { msg := &banktypes.MsgSend{ FromAddress: interchainAccountAddr, ToAddress: suite.chainB.SenderAccount.GetAddress().String(), - Amount: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(100))), + Amount: sdk.NewCoins(ibctesting.TestCoin), } data, err := icatypes.SerializeCosmosTx(suite.chainB.GetSimApp().AppCodec(), []proto.Message{msg}, icatypes.EncodingProtobuf) @@ -59,12 +57,12 @@ func (suite *KeeperTestSuite) TestSendTx() { &banktypes.MsgSend{ FromAddress: interchainAccountAddr, ToAddress: suite.chainB.SenderAccount.GetAddress().String(), - Amount: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(100))), + Amount: sdk.NewCoins(ibctesting.TestCoin), }, &banktypes.MsgSend{ FromAddress: interchainAccountAddr, ToAddress: suite.chainB.SenderAccount.GetAddress().String(), - Amount: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(100))), + Amount: sdk.NewCoins(ibctesting.TestCoin), }, } @@ -125,7 +123,7 @@ func (suite *KeeperTestSuite) TestSendTx() { msg := &banktypes.MsgSend{ FromAddress: interchainAccountAddr, ToAddress: suite.chainB.SenderAccount.GetAddress().String(), - Amount: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(100))), + Amount: sdk.NewCoins(ibctesting.TestCoin), } data, err := icatypes.SerializeCosmosTx(suite.chainB.GetSimApp().AppCodec(), []proto.Message{msg}, icatypes.EncodingProtobuf) diff --git a/modules/apps/27-interchain-accounts/host/keeper/msg_server_test.go b/modules/apps/27-interchain-accounts/host/keeper/msg_server_test.go index d358303fc12..54d95d7c02b 100644 --- a/modules/apps/27-interchain-accounts/host/keeper/msg_server_test.go +++ b/modules/apps/27-interchain-accounts/host/keeper/msg_server_test.go @@ -32,7 +32,7 @@ func (suite *KeeperTestSuite) TestModuleQuerySafe() { Data: balanceQueryBz, } - msg = types.NewMsgModuleQuerySafe(suite.chainA.GetSimApp().ICAHostKeeper.GetAuthority(), []*types.QueryRequest{&queryReq}) + msg = types.NewMsgModuleQuerySafe(suite.chainA.GetSimApp().ICAHostKeeper.GetAuthority(), []types.QueryRequest{queryReq}) balance := suite.chainA.GetSimApp().BankKeeper.GetBalance(suite.chainA.GetContext(), suite.chainA.SenderAccount.GetAddress(), sdk.DefaultBondDenom) @@ -64,7 +64,7 @@ func (suite *KeeperTestSuite) TestModuleQuerySafe() { Data: paramsQueryBz, } - msg = types.NewMsgModuleQuerySafe(suite.chainA.GetSimApp().ICAHostKeeper.GetAuthority(), []*types.QueryRequest{&queryReq, ¶msQueryReq}) + msg = types.NewMsgModuleQuerySafe(suite.chainA.GetSimApp().ICAHostKeeper.GetAuthority(), []types.QueryRequest{queryReq, paramsQueryReq}) balance := suite.chainA.GetSimApp().BankKeeper.GetBalance(suite.chainA.GetContext(), suite.chainA.SenderAccount.GetAddress(), sdk.DefaultBondDenom) @@ -102,7 +102,7 @@ func (suite *KeeperTestSuite) TestModuleQuerySafe() { Data: paramsQueryBz, } - msg = types.NewMsgModuleQuerySafe(suite.chainA.GetSimApp().ICAHostKeeper.GetAuthority(), []*types.QueryRequest{&queryReq, ¶msQueryReq}) + msg = types.NewMsgModuleQuerySafe(suite.chainA.GetSimApp().ICAHostKeeper.GetAuthority(), []types.QueryRequest{queryReq, paramsQueryReq}) }, ibcerrors.ErrInvalidRequest, }, @@ -117,7 +117,7 @@ func (suite *KeeperTestSuite) TestModuleQuerySafe() { Data: balanceQueryBz, } - msg = types.NewMsgModuleQuerySafe(suite.chainA.GetSimApp().ICAHostKeeper.GetAuthority(), []*types.QueryRequest{&queryReq}) + msg = types.NewMsgModuleQuerySafe(suite.chainA.GetSimApp().ICAHostKeeper.GetAuthority(), []types.QueryRequest{queryReq}) }, ibcerrors.ErrInvalidRequest, }, diff --git a/modules/apps/27-interchain-accounts/host/keeper/relay_test.go b/modules/apps/27-interchain-accounts/host/keeper/relay_test.go index 28c71b36d73..719ae8047f5 100644 --- a/modules/apps/27-interchain-accounts/host/keeper/relay_test.go +++ b/modules/apps/27-interchain-accounts/host/keeper/relay_test.go @@ -91,7 +91,7 @@ func (suite *KeeperTestSuite) TestOnRecvPacket() { msg := &banktypes.MsgSend{ FromAddress: interchainAccountAddr, ToAddress: suite.chainB.SenderAccount.GetAddress().String(), - Amount: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(100))), + Amount: sdk.NewCoins(ibctesting.TestCoin), } data, err := icatypes.SerializeCosmosTx(suite.chainA.GetSimApp().AppCodec(), []proto.Message{msg}, encoding) @@ -286,7 +286,7 @@ func (suite *KeeperTestSuite) TestOnRecvPacket() { queryBz, err := balanceQuery.Marshal() suite.Require().NoError(err) - msg := types.NewMsgModuleQuerySafe(interchainAccountAddr, []*types.QueryRequest{ + msg := types.NewMsgModuleQuerySafe(interchainAccountAddr, []types.QueryRequest{ { Path: "/cosmos.bank.v1beta1.Query/Balance", Data: queryBz, @@ -347,12 +347,13 @@ func (suite *KeeperTestSuite) TestOnRecvPacket() { msg := transfertypes.NewMsgTransfer( transferPath.EndpointA.ChannelConfig.PortID, transferPath.EndpointA.ChannelID, - sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(100))), + sdk.NewCoins(ibctesting.TestCoin), interchainAccountAddr, suite.chainA.SenderAccount.GetAddress().String(), suite.chainB.GetTimeoutHeight(), 0, "", + transfertypes.Forwarding{}, ) data, err := icatypes.SerializeCosmosTx(suite.chainA.GetSimApp().AppCodec(), []proto.Message{msg}, encoding) @@ -382,12 +383,13 @@ func (suite *KeeperTestSuite) TestOnRecvPacket() { msg := transfertypes.NewMsgTransfer( transferPath.EndpointA.ChannelConfig.PortID, transferPath.EndpointA.ChannelID, - sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(100))), + sdk.NewCoins(ibctesting.TestCoin), interchainAccountAddr, "", suite.chainB.GetTimeoutHeight(), 0, "", + transfertypes.Forwarding{}, ) data, err := icatypes.SerializeCosmosTx(suite.chainA.GetSimApp().AppCodec(), []proto.Message{msg}, encoding) @@ -484,7 +486,7 @@ func (suite *KeeperTestSuite) TestOnRecvPacket() { msg := &banktypes.MsgSend{ FromAddress: suite.chainB.SenderAccount.GetAddress().String(), ToAddress: suite.chainB.SenderAccount.GetAddress().String(), - Amount: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(100))), + Amount: sdk.NewCoins(ibctesting.TestCoin), } data, err := icatypes.SerializeCosmosTx(suite.chainA.GetSimApp().AppCodec(), []proto.Message{msg}, encoding) @@ -505,7 +507,7 @@ func (suite *KeeperTestSuite) TestOnRecvPacket() { msg := &banktypes.MsgSend{ FromAddress: suite.chainB.SenderAccount.GetAddress().String(), // unexpected signer ToAddress: suite.chainB.SenderAccount.GetAddress().String(), - Amount: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(100))), + Amount: sdk.NewCoins(ibctesting.TestCoin), } data, err := icatypes.SerializeCosmosTx(suite.chainA.GetSimApp().AppCodec(), []proto.Message{msg}, encoding) @@ -790,11 +792,13 @@ func (suite *KeeperTestSuite) TestJSONOnRecvPacket() { "@type": "/ibc.applications.transfer.v1.MsgTransfer", "source_port": "transfer", "source_channel": "channel-1", - "token": { "denom": "stake", "amount": "100" }, + "tokens": [{ "denom": "stake", "amount": "100" }], "sender": "` + icaAddress + `", "receiver": "cosmos15ulrf36d4wdtrtqzkgaan9ylwuhs7k7qz753uk", "timeout_height": { "revision_number": 1, "revision_height": 100 }, - "timeout_timestamp": 0 + "timeout_timestamp": 0, + "memo": "", + "forwarding": { "hops": [], "unwind": false } } ] }`) diff --git a/modules/apps/27-interchain-accounts/host/types/msgs.go b/modules/apps/27-interchain-accounts/host/types/msgs.go index 092ff1df850..5757dc77fc8 100644 --- a/modules/apps/27-interchain-accounts/host/types/msgs.go +++ b/modules/apps/27-interchain-accounts/host/types/msgs.go @@ -35,7 +35,7 @@ func (msg MsgUpdateParams) ValidateBasic() error { } // NewMsgModuleQuerySafe creates a new MsgModuleQuerySafe instance -func NewMsgModuleQuerySafe(signer string, requests []*QueryRequest) *MsgModuleQuerySafe { +func NewMsgModuleQuerySafe(signer string, requests []QueryRequest) *MsgModuleQuerySafe { return &MsgModuleQuerySafe{ Signer: signer, Requests: requests, diff --git a/modules/apps/27-interchain-accounts/host/types/msgs_test.go b/modules/apps/27-interchain-accounts/host/types/msgs_test.go index eb088d7c597..784f3646ce1 100644 --- a/modules/apps/27-interchain-accounts/host/types/msgs_test.go +++ b/modules/apps/27-interchain-accounts/host/types/msgs_test.go @@ -77,7 +77,7 @@ func TestMsgUpdateParamsGetSigners(t *testing.T) { } func TestMsgModuleQuerySafeValidateBasic(t *testing.T) { - queryRequest := &types.QueryRequest{ + queryRequest := types.QueryRequest{ Path: "/cosmos.bank.v1beta1.Query/Balance", Data: []byte{}, } @@ -89,17 +89,17 @@ func TestMsgModuleQuerySafeValidateBasic(t *testing.T) { }{ { "success: valid signer address", - types.NewMsgModuleQuerySafe(sdk.AccAddress(ibctesting.TestAccAddress).String(), []*types.QueryRequest{queryRequest}), + types.NewMsgModuleQuerySafe(sdk.AccAddress(ibctesting.TestAccAddress).String(), []types.QueryRequest{queryRequest}), nil, }, { "failure: invalid signer address", - types.NewMsgModuleQuerySafe("signer", []*types.QueryRequest{queryRequest}), + types.NewMsgModuleQuerySafe("signer", []types.QueryRequest{queryRequest}), ibcerrors.ErrInvalidAddress, }, { "failure: empty query requests", - types.NewMsgModuleQuerySafe(sdk.AccAddress(ibctesting.TestAccAddress).String(), []*types.QueryRequest{}), + types.NewMsgModuleQuerySafe(sdk.AccAddress(ibctesting.TestAccAddress).String(), []types.QueryRequest{}), ibcerrors.ErrInvalidRequest, }, } @@ -135,7 +135,7 @@ func TestMsgModuleQuerySafeGetSigners(t *testing.T) { tc := tc t.Run(tc.name, func(t *testing.T) { - msg := types.NewMsgModuleQuerySafe(tc.address.String(), []*types.QueryRequest{}) + msg := types.NewMsgModuleQuerySafe(tc.address.String(), []types.QueryRequest{}) encodingCfg := moduletestutil.MakeTestEncodingConfig(ica.AppModuleBasic{}) signers, _, err := encodingCfg.Codec.GetMsgV1Signers(msg) if tc.expPass { diff --git a/modules/apps/27-interchain-accounts/host/types/tx.pb.go b/modules/apps/27-interchain-accounts/host/types/tx.pb.go index 8663234557f..fd74a84741f 100644 --- a/modules/apps/27-interchain-accounts/host/types/tx.pb.go +++ b/modules/apps/27-interchain-accounts/host/types/tx.pb.go @@ -114,7 +114,7 @@ type MsgModuleQuerySafe struct { // signer address Signer string `protobuf:"bytes,1,opt,name=signer,proto3" json:"signer,omitempty"` // requests defines the module safe queries to execute. - Requests []*QueryRequest `protobuf:"bytes,2,rep,name=requests,proto3" json:"requests,omitempty"` + Requests []QueryRequest `protobuf:"bytes,2,rep,name=requests,proto3" json:"requests"` } func (m *MsgModuleQuerySafe) Reset() { *m = MsgModuleQuerySafe{} } @@ -217,36 +217,36 @@ func init() { } var fileDescriptor_fa437afde7f1e7ae = []byte{ - // 456 bytes of a gzipped FileDescriptorProto + // 457 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xa4, 0x93, 0x41, 0x6b, 0xd4, 0x40, 0x14, 0xc7, 0x33, 0x6d, 0x5d, 0xec, 0xb4, 0x50, 0x08, 0x62, 0x6b, 0x90, 0xb4, 0xec, 0x69, 0x29, - 0xee, 0x0c, 0x5d, 0x95, 0x4a, 0x41, 0x90, 0x82, 0x20, 0x42, 0x40, 0x47, 0xf4, 0xe0, 0x45, 0x26, - 0xb3, 0xe3, 0x64, 0xa0, 0xc9, 0xc4, 0xbc, 0xc9, 0x62, 0x6f, 0xe2, 0xc9, 0x93, 0x78, 0xd0, 0xa3, - 0xe0, 0x47, 0xe8, 0x77, 0xf0, 0xd2, 0x63, 0x8f, 0x9e, 0x44, 0x76, 0x0f, 0xfd, 0x1a, 0x92, 0x49, - 0xda, 0xda, 0xd4, 0x1e, 0x42, 0x6f, 0x99, 0x64, 0xfe, 0xbf, 0xf7, 0x7b, 0x99, 0x79, 0xf8, 0xbe, - 0x8e, 0x05, 0xe5, 0x79, 0xbe, 0xa7, 0x05, 0xb7, 0xda, 0x64, 0x40, 0x75, 0x66, 0x65, 0x21, 0x12, - 0xae, 0xb3, 0x37, 0x5c, 0x08, 0x53, 0x66, 0x16, 0x68, 0x62, 0xc0, 0xd2, 0xc9, 0x16, 0xb5, 0xef, - 0x49, 0x5e, 0x18, 0x6b, 0xfc, 0x3b, 0x3a, 0x16, 0xe4, 0xdf, 0x18, 0xf9, 0x4f, 0x8c, 0x54, 0x31, - 0x32, 0xd9, 0x0a, 0x6e, 0x28, 0xa3, 0x8c, 0x0b, 0xd2, 0xea, 0xa9, 0x66, 0x04, 0xab, 0xc2, 0x40, - 0x6a, 0x80, 0xa6, 0xa0, 0x2a, 0x76, 0x0a, 0xaa, 0xf9, 0xb0, 0xdd, 0xc9, 0xc9, 0x15, 0x71, 0xc1, - 0xfe, 0x67, 0x84, 0x57, 0x22, 0x50, 0x2f, 0xf3, 0x31, 0xb7, 0xf2, 0x19, 0x2f, 0x78, 0x0a, 0xfe, - 0x4d, 0xdc, 0x03, 0xad, 0x32, 0x59, 0xac, 0xa1, 0x0d, 0x34, 0x58, 0x64, 0xcd, 0xca, 0x67, 0xb8, - 0x97, 0xbb, 0x1d, 0x6b, 0x73, 0x1b, 0x68, 0xb0, 0x34, 0xba, 0x47, 0xba, 0xb4, 0x44, 0x6a, 0xfa, - 0xee, 0xc2, 0xe1, 0xef, 0x75, 0x8f, 0x35, 0xa4, 0x9d, 0x95, 0x4f, 0x3f, 0xd6, 0xbd, 0x8f, 0xc7, - 0x07, 0x9b, 0x4d, 0x91, 0xfe, 0x2d, 0xbc, 0xda, 0xf2, 0x61, 0x12, 0x72, 0x93, 0x81, 0xec, 0x7f, - 0x43, 0xd8, 0x8f, 0x40, 0x45, 0x66, 0x5c, 0xee, 0xc9, 0xe7, 0xa5, 0x2c, 0xf6, 0x5f, 0xf0, 0xb7, - 0xf2, 0x52, 0xdd, 0x57, 0xf8, 0x7a, 0x21, 0xdf, 0x95, 0x12, 0x6c, 0x25, 0x3c, 0x3f, 0x58, 0x1a, - 0xed, 0x74, 0x13, 0x76, 0x25, 0x58, 0x8d, 0x60, 0xa7, 0xac, 0x8b, 0xca, 0x0c, 0x07, 0x17, 0xb5, - 0x4e, 0xac, 0x2b, 0xbd, 0x44, 0x6a, 0x95, 0x58, 0xa7, 0xb7, 0xc0, 0x9a, 0x95, 0x7f, 0x1b, 0x2f, - 0x16, 0xcd, 0x9e, 0xda, 0x6f, 0x99, 0x9d, 0xbd, 0x18, 0xfd, 0x9c, 0xc3, 0xf3, 0x11, 0x28, 0xff, - 0x2b, 0xc2, 0xcb, 0xe7, 0x0e, 0xe7, 0x61, 0xb7, 0x1e, 0x5a, 0xff, 0x32, 0x78, 0x7c, 0xa5, 0xf8, - 0x69, 0x53, 0xdf, 0xab, 0x6b, 0xd3, 0x3a, 0x87, 0x47, 0x9d, 0xd1, 0x2d, 0x42, 0xf0, 0xe4, 0xaa, - 0x84, 0x13, 0xbf, 0xe0, 0xda, 0x87, 0xe3, 0x83, 0x4d, 0xb4, 0x3b, 0x3e, 0x9c, 0x86, 0xe8, 0x68, - 0x1a, 0xa2, 0x3f, 0xd3, 0x10, 0x7d, 0x99, 0x85, 0xde, 0xd1, 0x2c, 0xf4, 0x7e, 0xcd, 0x42, 0xef, - 0xf5, 0x53, 0xa5, 0x6d, 0x52, 0xc6, 0x44, 0x98, 0x94, 0x36, 0x43, 0xa5, 0x63, 0x31, 0x54, 0x86, - 0x4e, 0x1e, 0xd0, 0xd4, 0x51, 0xa1, 0x1a, 0x28, 0xa0, 0xa3, 0xed, 0xe1, 0x99, 0xc4, 0xf0, 0xfc, - 0x2c, 0xd9, 0xfd, 0x5c, 0x42, 0xdc, 0x73, 0xa3, 0x74, 0xf7, 0x6f, 0x00, 0x00, 0x00, 0xff, 0xff, - 0x49, 0x5c, 0x48, 0xd7, 0x19, 0x04, 0x00, 0x00, + 0xee, 0x0c, 0x5d, 0x95, 0x4a, 0x41, 0x90, 0x82, 0x20, 0xc2, 0x82, 0x8e, 0x78, 0x11, 0x41, 0x26, + 0xb3, 0xe3, 0x64, 0xa0, 0xc9, 0xc4, 0xbc, 0xc9, 0x62, 0x6f, 0xe2, 0xc9, 0x93, 0x78, 0xf0, 0x26, + 0x82, 0x1f, 0xa1, 0xdf, 0xc1, 0x4b, 0x8f, 0x3d, 0x7a, 0x12, 0xd9, 0x3d, 0xf4, 0x6b, 0x48, 0x66, + 0xa7, 0xad, 0x4d, 0xed, 0x21, 0xf4, 0x96, 0x49, 0xe6, 0xff, 0x7b, 0xbf, 0x97, 0x99, 0x87, 0xef, + 0xeb, 0x44, 0x50, 0x5e, 0x14, 0x7b, 0x5a, 0x70, 0xab, 0x4d, 0x0e, 0x54, 0xe7, 0x56, 0x96, 0x22, + 0xe5, 0x3a, 0x7f, 0xc3, 0x85, 0x30, 0x55, 0x6e, 0x81, 0xa6, 0x06, 0x2c, 0x1d, 0x6f, 0x51, 0xfb, + 0x9e, 0x14, 0xa5, 0xb1, 0x26, 0xbc, 0xa3, 0x13, 0x41, 0xfe, 0x8d, 0x91, 0xff, 0xc4, 0x48, 0x1d, + 0x23, 0xe3, 0xad, 0xe8, 0x86, 0x32, 0xca, 0xb8, 0x20, 0xad, 0x9f, 0x66, 0x8c, 0x68, 0x55, 0x18, + 0xc8, 0x0c, 0xd0, 0x0c, 0x54, 0xcd, 0xce, 0x40, 0xf9, 0x0f, 0xdb, 0xad, 0x9c, 0x5c, 0x11, 0x17, + 0xec, 0x7e, 0x46, 0x78, 0x65, 0x08, 0xea, 0x65, 0x31, 0xe2, 0x56, 0x3e, 0xe3, 0x25, 0xcf, 0x20, + 0xbc, 0x89, 0x3b, 0xa0, 0x55, 0x2e, 0xcb, 0x35, 0xb4, 0x81, 0x7a, 0x8b, 0xcc, 0xaf, 0x42, 0x86, + 0x3b, 0x85, 0xdb, 0xb1, 0x36, 0xb7, 0x81, 0x7a, 0x4b, 0x83, 0x7b, 0xa4, 0x4d, 0x4b, 0x64, 0x46, + 0xdf, 0x5d, 0x38, 0xfc, 0xbd, 0x1e, 0x30, 0x4f, 0xda, 0x59, 0xf9, 0xf4, 0x63, 0x3d, 0xf8, 0x78, + 0x7c, 0xb0, 0xe9, 0x8b, 0x74, 0x6f, 0xe1, 0xd5, 0x86, 0x0f, 0x93, 0x50, 0x98, 0x1c, 0x64, 0xf7, + 0x1b, 0xc2, 0xe1, 0x10, 0xd4, 0xd0, 0x8c, 0xaa, 0x3d, 0xf9, 0xbc, 0x92, 0xe5, 0xfe, 0x0b, 0xfe, + 0x56, 0x5e, 0xaa, 0xfb, 0x1a, 0x5f, 0x2f, 0xe5, 0xbb, 0x4a, 0x82, 0xad, 0x85, 0xe7, 0x7b, 0x4b, + 0x83, 0x9d, 0x76, 0xc2, 0xae, 0x04, 0x9b, 0x21, 0xbc, 0xf6, 0x29, 0xf1, 0xa2, 0x38, 0xc3, 0xd1, + 0x45, 0xb9, 0x13, 0xf7, 0x5a, 0x32, 0x95, 0x5a, 0xa5, 0xd6, 0x49, 0x2e, 0x30, 0xbf, 0x0a, 0x6f, + 0xe3, 0xc5, 0xd2, 0xef, 0x99, 0x59, 0x2e, 0xb3, 0xb3, 0x17, 0x83, 0x9f, 0x73, 0x78, 0x7e, 0x08, + 0x2a, 0xfc, 0x8a, 0xf0, 0xf2, 0xb9, 0x23, 0x7a, 0xd8, 0xae, 0x93, 0xc6, 0x1f, 0x8d, 0x1e, 0x5f, + 0x29, 0x7e, 0xda, 0xd4, 0xf7, 0xfa, 0xf2, 0x34, 0x4e, 0xe3, 0x51, 0x6b, 0x74, 0x83, 0x10, 0x3d, + 0xb9, 0x2a, 0xe1, 0xc4, 0x2f, 0xba, 0xf6, 0xe1, 0xf8, 0x60, 0x13, 0xed, 0x8e, 0x0e, 0x27, 0x31, + 0x3a, 0x9a, 0xc4, 0xe8, 0xcf, 0x24, 0x46, 0x5f, 0xa6, 0x71, 0x70, 0x34, 0x8d, 0x83, 0x5f, 0xd3, + 0x38, 0x78, 0xf5, 0x54, 0x69, 0x9b, 0x56, 0x09, 0x11, 0x26, 0xa3, 0x7e, 0xb4, 0x74, 0x22, 0xfa, + 0xca, 0xd0, 0xf1, 0x03, 0x9a, 0x39, 0x2a, 0xd4, 0x63, 0x05, 0x74, 0xb0, 0xdd, 0x3f, 0x93, 0xe8, + 0x9f, 0x9f, 0x28, 0xbb, 0x5f, 0x48, 0x48, 0x3a, 0x6e, 0xa0, 0xee, 0xfe, 0x0d, 0x00, 0x00, 0xff, + 0xff, 0xd3, 0x2f, 0xef, 0x8d, 0x1f, 0x04, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -846,7 +846,7 @@ func (m *MsgModuleQuerySafe) Unmarshal(dAtA []byte) error { if postIndex > l { return io.ErrUnexpectedEOF } - m.Requests = append(m.Requests, &QueryRequest{}) + m.Requests = append(m.Requests, QueryRequest{}) if err := m.Requests[len(m.Requests)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } diff --git a/modules/apps/29-fee/ibc_middleware_test.go b/modules/apps/29-fee/ibc_middleware_test.go index b0a4a7301a9..b8bbd2eac52 100644 --- a/modules/apps/29-fee/ibc_middleware_test.go +++ b/modules/apps/29-fee/ibc_middleware_test.go @@ -644,7 +644,7 @@ func (suite *FeeTestSuite) TestOnAcknowledgementPacket() { "success: some refunds", func() { // set timeout_fee > recv_fee + ack_fee - packetFee.Fee.TimeoutFee = packetFee.Fee.Total().Add(sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(100)))...) + packetFee.Fee.TimeoutFee = packetFee.Fee.Total().Add(sdk.NewCoins(ibctesting.TestCoin)...) escrowAmount = packetFee.Fee.Total() @@ -895,7 +895,7 @@ func (suite *FeeTestSuite) TestOnTimeoutPacket() { "success: refund (recv_fee + ack_fee) - timeout_fee", func() { // set recv_fee + ack_fee > timeout_fee - packetFee.Fee.RecvFee = packetFee.Fee.Total().Add(sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(100)))...) + packetFee.Fee.RecvFee = packetFee.Fee.Total().Add(sdk.NewCoins(ibctesting.TestCoin)...) escrowAmount = packetFee.Fee.Total() diff --git a/modules/apps/29-fee/keeper/escrow_test.go b/modules/apps/29-fee/keeper/escrow_test.go index 8aa0840784c..6669ca7809b 100644 --- a/modules/apps/29-fee/keeper/escrow_test.go +++ b/modules/apps/29-fee/keeper/escrow_test.go @@ -10,6 +10,7 @@ import ( "github.com/cosmos/ibc-go/v8/modules/apps/29-fee/types" transfertypes "github.com/cosmos/ibc-go/v8/modules/apps/transfer/types" channeltypes "github.com/cosmos/ibc-go/v8/modules/core/04-channel/types" + ibctesting "github.com/cosmos/ibc-go/v8/testing" "github.com/cosmos/ibc-go/v8/testing/mock" ) @@ -69,7 +70,7 @@ func (suite *KeeperTestSuite) TestDistributeFee() { "success: refund timeout_fee - (recv_fee + ack_fee)", func() { // set the timeout fee to be greater than recv + ack fee so that the refund amount is non-zero - fee.TimeoutFee = fee.Total().Add(sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(100))) + fee.TimeoutFee = fee.Total().Add(ibctesting.TestCoin) packetFee = types.NewPacketFee(fee, refundAcc.String(), []string{}) packetFees = []types.PacketFee{packetFee, packetFee} @@ -107,7 +108,7 @@ func (suite *KeeperTestSuite) TestDistributeFee() { "success: refund account is module account", func() { // set the timeout fee to be greater than recv + ack fee so that the refund amount is non-zero - fee.TimeoutFee = fee.Total().Add(sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(100))) + fee.TimeoutFee = fee.Total().Add(ibctesting.TestCoin) refundAcc = suite.chainA.GetSimApp().AccountKeeper.GetModuleAddress(mock.ModuleName) @@ -195,7 +196,7 @@ func (suite *KeeperTestSuite) TestDistributeFee() { "invalid refund address: no-op, timeout_fee - (recv_fee + ack_fee) remains in escrow", func() { // set the timeout fee to be greater than recv + ack fee so that the refund amount is non-zero - fee.TimeoutFee = fee.Total().Add(sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(100))) + fee.TimeoutFee = fee.Total().Add(ibctesting.TestCoin) packetFee = types.NewPacketFee(fee, refundAcc.String(), []string{}) packetFees = []types.PacketFee{packetFee, packetFee} @@ -286,7 +287,7 @@ func (suite *KeeperTestSuite) TestDistributePacketFeesOnTimeout() { "success: refund (recv_fee + ack_fee) - timeout_fee", func() { // set the recv + ack fee to be greater than timeout fee so that the refund amount is non-zero - fee.RecvFee = fee.RecvFee.Add(sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(100))) + fee.RecvFee = fee.RecvFee.Add(ibctesting.TestCoin) packetFee = types.NewPacketFee(fee, refundAcc.String(), []string{}) packetFees = []types.PacketFee{packetFee, packetFee} }, @@ -340,7 +341,7 @@ func (suite *KeeperTestSuite) TestDistributePacketFeesOnTimeout() { "invalid refund address: no-op, (recv_fee + ack_fee) - timeout_fee remain in escrow", func() { // set the recv + ack fee to be greater than timeout fee so that the refund amount is non-zero - fee.RecvFee = fee.RecvFee.Add(sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(100))) + fee.RecvFee = fee.RecvFee.Add(ibctesting.TestCoin) packetFee = types.NewPacketFee(fee, refundAcc.String(), []string{}) packetFees = []types.PacketFee{packetFee, packetFee} diff --git a/modules/apps/29-fee/keeper/events_test.go b/modules/apps/29-fee/keeper/events_test.go index 38b64cadd80..e480cebcf28 100644 --- a/modules/apps/29-fee/keeper/events_test.go +++ b/modules/apps/29-fee/keeper/events_test.go @@ -1,8 +1,6 @@ package keeper_test import ( - sdkmath "cosmossdk.io/math" - sdk "github.com/cosmos/cosmos-sdk/types" abcitypes "github.com/cometbft/cometbft/abci/types" @@ -113,8 +111,9 @@ func (suite *KeeperTestSuite) TestDistributeFeeEvent() { msgTransfer := transfertypes.NewMsgTransfer( path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, - sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(100))), suite.chainA.SenderAccount.GetAddress().String(), suite.chainB.SenderAccount.GetAddress().String(), + sdk.NewCoins(ibctesting.TestCoin), suite.chainA.SenderAccount.GetAddress().String(), suite.chainB.SenderAccount.GetAddress().String(), clienttypes.NewHeight(1, 100), 0, "", + transfertypes.Forwarding{}, ) res, err := suite.chainA.SendMsgs(msgPayPacketFee, msgTransfer) diff --git a/modules/apps/29-fee/transfer_test.go b/modules/apps/29-fee/transfer_test.go index c9c83d8211b..3d2beb18a49 100644 --- a/modules/apps/29-fee/transfer_test.go +++ b/modules/apps/29-fee/transfer_test.go @@ -46,7 +46,7 @@ func (suite *FeeTestSuite) TestFeeTransfer() { msgs := []sdk.Msg{ types.NewMsgPayPacketFee(fee, path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, suite.chainA.SenderAccount.GetAddress().String(), nil), - transfertypes.NewMsgTransfer(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, tc.coinsToTransfer, suite.chainA.SenderAccount.GetAddress().String(), suite.chainB.SenderAccount.GetAddress().String(), clienttypes.NewHeight(1, 100), 0, ""), + transfertypes.NewMsgTransfer(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, tc.coinsToTransfer, suite.chainA.SenderAccount.GetAddress().String(), suite.chainB.SenderAccount.GetAddress().String(), clienttypes.NewHeight(1, 100), 0, "", transfertypes.Forwarding{}), } res, err := suite.chainA.SendMsgs(msgs...) @@ -157,7 +157,7 @@ func (suite *FeeTestSuite) TestTransferFeeUpgrade() { fee := types.NewFee(defaultRecvFee, defaultAckFee, defaultTimeoutFee) msgs := []sdk.Msg{ types.NewMsgPayPacketFee(fee, path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, suite.chainA.SenderAccount.GetAddress().String(), nil), - transfertypes.NewMsgTransfer(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, sdk.NewCoins(ibctesting.TestCoin), suite.chainA.SenderAccount.GetAddress().String(), suite.chainB.SenderAccount.GetAddress().String(), clienttypes.NewHeight(1, 100), 0, ""), + transfertypes.NewMsgTransfer(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, sdk.NewCoins(ibctesting.TestCoin), suite.chainA.SenderAccount.GetAddress().String(), suite.chainB.SenderAccount.GetAddress().String(), clienttypes.NewHeight(1, 100), 0, "", transfertypes.Forwarding{}), } res, err := suite.chainA.SendMsgs(msgs...) diff --git a/modules/apps/callbacks/go.mod b/modules/apps/callbacks/go.mod index 3baad5a3ee4..2ec0d448ee1 100644 --- a/modules/apps/callbacks/go.mod +++ b/modules/apps/callbacks/go.mod @@ -22,7 +22,7 @@ require ( cosmossdk.io/x/feegrant v0.1.1 cosmossdk.io/x/tx v0.13.3 cosmossdk.io/x/upgrade v0.1.3 - github.com/cometbft/cometbft v0.38.7 + github.com/cometbft/cometbft v0.38.8 github.com/cosmos/cosmos-db v1.0.2 github.com/cosmos/cosmos-sdk v0.50.7 github.com/cosmos/gogoproto v1.5.0 @@ -122,6 +122,7 @@ require ( github.com/hashicorp/go-safetemp v1.0.0 // indirect github.com/hashicorp/go-version v1.6.0 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/yamux v0.1.1 // indirect github.com/hdevalence/ed25519consensus v0.1.0 // indirect diff --git a/modules/apps/callbacks/go.sum b/modules/apps/callbacks/go.sum index 37c262e81b9..b6984a289ec 100644 --- a/modules/apps/callbacks/go.sum +++ b/modules/apps/callbacks/go.sum @@ -333,8 +333,8 @@ github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZ github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= -github.com/cometbft/cometbft v0.38.7 h1:ULhIOJ9+LgSy6nLekhq9ae3juX3NnQUMMPyVdhZV6Hk= -github.com/cometbft/cometbft v0.38.7/go.mod h1:HIyf811dFMI73IE0F7RrnY/Fr+d1+HuJAgtkEpQjCMY= +github.com/cometbft/cometbft v0.38.8 h1:XyJ9Cu3xqap6xtNxiemrO8roXZ+KS2Zlu7qQ0w1trvU= +github.com/cometbft/cometbft v0.38.8/go.mod h1:xOoGZrtUT+A5izWfHSJgl0gYZUE7lu7Z2XIS1vWG/QQ= github.com/cometbft/cometbft-db v0.9.1 h1:MIhVX5ja5bXNHF8EYrThkG9F7r9kSfv8BX4LWaxWJ4M= github.com/cometbft/cometbft-db v0.9.1/go.mod h1:iliyWaoV0mRwBJoizElCwwRA9Tf7jZJOURcRZF9m60U= github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg= @@ -681,6 +681,8 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= diff --git a/modules/apps/callbacks/ibc_middleware_test.go b/modules/apps/callbacks/ibc_middleware_test.go index a818116e01c..d0d7505d514 100644 --- a/modules/apps/callbacks/ibc_middleware_test.go +++ b/modules/apps/callbacks/ibc_middleware_test.go @@ -24,6 +24,8 @@ import ( ibcmock "github.com/cosmos/ibc-go/v8/testing/mock" ) +var emptyForwardingPacketData = transfertypes.ForwardingPacketData{} + func (s *CallbacksTestSuite) TestNewIBCMiddleware() { testCases := []struct { name string @@ -186,6 +188,7 @@ func (s *CallbacksTestSuite) TestSendPacket() { ibctesting.TestAccAddress, ibctesting.TestAccAddress, fmt.Sprintf(`{"src_callback": {"address": "%s"}}`, simapp.SuccessContract), + emptyForwardingPacketData, ) chanCap := s.path.EndpointA.Chain.GetChannelCapability(s.path.EndpointA.ChannelConfig.PortID, s.path.EndpointA.ChannelID) @@ -327,6 +330,7 @@ func (s *CallbacksTestSuite) TestOnAcknowledgementPacket() { ibctesting.TestAccAddress, ibctesting.TestAccAddress, fmt.Sprintf(`{"src_callback": {"address":"%s", "gas_limit":"%d"}}`, simapp.SuccessContract, userGasLimit), + emptyForwardingPacketData, ) packet = channeltypes.Packet{ @@ -492,6 +496,7 @@ func (s *CallbacksTestSuite) TestOnTimeoutPacket() { sdk.NewCoins(ibctesting.TestCoin), s.chainA.SenderAccount.GetAddress().String(), s.chainB.SenderAccount.GetAddress().String(), clienttypes.ZeroHeight(), timeoutTimestamp, fmt.Sprintf(`{"src_callback": {"address":"%s", "gas_limit":"%d"}}`, ibctesting.TestAccAddress, userGasLimit), // set user gas limit above panic level in mock contract keeper + transfertypes.Forwarding{}, ) res, err := s.chainA.SendMsgs(msg) @@ -659,6 +664,7 @@ func (s *CallbacksTestSuite) TestOnRecvPacket() { ibctesting.TestAccAddress, s.chainB.SenderAccount.GetAddress().String(), fmt.Sprintf(`{"dest_callback": {"address":"%s", "gas_limit":"%d"}}`, ibctesting.TestAccAddress, userGasLimit), + emptyForwardingPacketData, ) packet = channeltypes.Packet{ @@ -790,6 +796,7 @@ func (s *CallbacksTestSuite) TestWriteAcknowledgement() { ibctesting.TestAccAddress, s.chainB.SenderAccount.GetAddress().String(), fmt.Sprintf(`{"dest_callback": {"address":"%s", "gas_limit":"600000"}}`, ibctesting.TestAccAddress), + emptyForwardingPacketData, ) packet = channeltypes.Packet{ @@ -1010,9 +1017,10 @@ func (s *CallbacksTestSuite) TestUnmarshalPacketDataV1() { Amount: ibctesting.TestCoin.Amount.String(), }, }, - Sender: ibctesting.TestAccAddress, - Receiver: ibctesting.TestAccAddress, - Memo: fmt.Sprintf(`{"src_callback": {"address": "%s"}, "dest_callback": {"address":"%s"}}`, ibctesting.TestAccAddress, ibctesting.TestAccAddress), + Sender: ibctesting.TestAccAddress, + Receiver: ibctesting.TestAccAddress, + Memo: fmt.Sprintf(`{"src_callback": {"address": "%s"}, "dest_callback": {"address":"%s"}}`, ibctesting.TestAccAddress, ibctesting.TestAccAddress), + Forwarding: emptyForwardingPacketData, } portID := s.path.EndpointA.ChannelConfig.PortID diff --git a/modules/apps/callbacks/replay_test.go b/modules/apps/callbacks/replay_test.go index 7730d67989a..4c619346597 100644 --- a/modules/apps/callbacks/replay_test.go +++ b/modules/apps/callbacks/replay_test.go @@ -330,6 +330,7 @@ func (s *CallbacksTestSuite) ExecuteFailedTransfer(memo string) { s.chainA.SenderAccount.GetAddress().String(), s.chainB.SenderAccount.GetAddress().String(), clienttypes.NewHeight(1, 100), 0, memo, + transfertypes.Forwarding{}, ) res, err := s.chainA.SendMsgs(msg) diff --git a/modules/apps/callbacks/transfer_test.go b/modules/apps/callbacks/transfer_test.go index 498e91c9d6a..c1e935e204d 100644 --- a/modules/apps/callbacks/transfer_test.go +++ b/modules/apps/callbacks/transfer_test.go @@ -193,6 +193,7 @@ func (s *CallbacksTestSuite) ExecuteTransfer(memo string) { s.chainA.SenderAccount.GetAddress().String(), s.chainB.SenderAccount.GetAddress().String(), clienttypes.NewHeight(1, 100), 0, memo, + transfertypes.Forwarding{}, ) res, err := s.chainA.SendMsgs(msg) @@ -227,6 +228,7 @@ func (s *CallbacksTestSuite) ExecuteTransferTimeout(memo string) { s.chainA.SenderAccount.GetAddress().String(), s.chainB.SenderAccount.GetAddress().String(), timeoutHeight, timeoutTimestamp, memo, + transfertypes.Forwarding{}, ) res, err := s.chainA.SendMsgs(msg) diff --git a/modules/apps/transfer/client/cli/tx.go b/modules/apps/transfer/client/cli/tx.go index e3cc4be4366..2a42012a082 100644 --- a/modules/apps/transfer/client/cli/tx.go +++ b/modules/apps/transfer/client/cli/tx.go @@ -23,6 +23,8 @@ const ( flagPacketTimeoutTimestamp = "packet-timeout-timestamp" flagAbsoluteTimeouts = "absolute-timeouts" flagMemo = "memo" + flagForwarding = "forwarding" + flagUnwind = "unwind" ) // defaultRelativePacketTimeoutTimestamp is the default packet timeout timestamp (in nanoseconds) @@ -40,7 +42,11 @@ func NewTransferTxCmd() *cobra.Command { packet if the coins list is a comma-separated string (e.g. 100uatom,100uosmo). Timeouts can be specified as absolute using the {absolute-timeouts} flag. Timeout height can be set by passing in the height string in the form {revision}-{height} using the {packet-timeout-height} flag. Note, relative timeout height is not supported. Relative timeout timestamp is added to the value of the user's local system clock time -using the {packet-timeout-timestamp} flag. If no timeout value is set then a default relative timeout value of 10 minutes is used.`), +using the {packet-timeout-timestamp} flag. If no timeout value is set then a default relative timeout value of 10 minutes is used. IBC tokens +can be automatically unwound to their native chain using the {unwind} flag. Please note that if the {unwind} flag is used, then the transfer should contain only +a single token. Tokens can also be automatically forwarded through multiple chains using the {fowarding} flag and specifying +a comma-separated list of source portID/channelID pairs for each intermediary chain. {unwind} and {forwarding} flags can be used together +to first unwind IBC tokens to their native chain and then forward them to the final destination.`), Example: fmt.Sprintf("%s tx ibc-transfer transfer [src-port] [src-channel] [receiver] [coins]", version.AppName), Args: cobra.ExactArgs(4), RunE: func(cmd *cobra.Command, args []string) error { @@ -90,6 +96,11 @@ using the {packet-timeout-timestamp} flag. If no timeout value is set then a def return err } + forwarding, err := parseForwarding(cmd) + if err != nil { + return err + } + // NOTE: relative timeouts using block height are not supported. // if the timeouts are not absolute, CLI users rely solely on local clock time in order to calculate relative timestamps. if !absoluteTimeouts { @@ -111,8 +122,9 @@ using the {packet-timeout-timestamp} flag. If no timeout value is set then a def } msg := types.NewMsgTransfer( - srcPort, srcChannel, coins, sender, receiver, timeoutHeight, timeoutTimestamp, memo, + srcPort, srcChannel, coins, sender, receiver, timeoutHeight, timeoutTimestamp, memo, forwarding, ) + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) }, } @@ -121,7 +133,42 @@ using the {packet-timeout-timestamp} flag. If no timeout value is set then a def cmd.Flags().Uint64(flagPacketTimeoutTimestamp, defaultRelativePacketTimeoutTimestamp, "Packet timeout timestamp in nanoseconds from now. Default is 10 minutes. The timeout is disabled when set to 0.") cmd.Flags().Bool(flagAbsoluteTimeouts, false, "Timeout flags are used as absolute timeouts.") cmd.Flags().String(flagMemo, "", "Memo to be sent along with the packet.") + cmd.Flags().String(flagForwarding, "", "Forwarding information in the form of a comma separated list of portID/channelID pairs.") + cmd.Flags().Bool(flagUnwind, false, "Flag to indicate if the coin should be unwound to its native chain before forwarding.") + flags.AddTxFlagsToCmd(cmd) return cmd } + +// parseForwarding parses the forwarding flag into a Forwarding object or nil if the flag is not specified. If the flag cannot +// be parsed or the hops aren't in the portID/channelID format an error is returned. +func parseForwarding(cmd *cobra.Command) (types.Forwarding, error) { + var hops []types.Hop + + forwardingString, err := cmd.Flags().GetString(flagForwarding) + if err != nil { + return types.Forwarding{}, err + } + if strings.TrimSpace(forwardingString) == "" { + return types.Forwarding{}, nil + } + + pairs := strings.Split(forwardingString, ",") + for _, pair := range pairs { + pairSplit := strings.Split(pair, "/") + if len(pairSplit) != 2 { + return types.Forwarding{}, fmt.Errorf("expected a portID/channelID pair, found %s", pair) + } + + hop := types.NewHop(pairSplit[0], pairSplit[1]) + hops = append(hops, hop) + } + + unwind, err := cmd.Flags().GetBool(flagUnwind) + if err != nil { + return types.Forwarding{}, err + } + + return types.NewForwarding(unwind, hops...), nil +} diff --git a/modules/apps/transfer/ibc_module.go b/modules/apps/transfer/ibc_module.go index 299b17f735e..3748383f4ae 100644 --- a/modules/apps/transfer/ibc_module.go +++ b/modules/apps/transfer/ibc_module.go @@ -178,34 +178,45 @@ func (IBCModule) OnChanCloseConfirm( // OnRecvPacket implements the IBCModule interface. A successful acknowledgement // is returned if the packet data is successfully decoded and the receive application // logic returns without error. +// A nil acknowledgement may be returned when using the packet forwarding feature. This signals to core IBC that the acknowledgement will be written asynchronously. func (im IBCModule) OnRecvPacket( ctx sdk.Context, packet channeltypes.Packet, relayer sdk.AccAddress, ) ibcexported.Acknowledgement { + var ( + ackErr error + data types.FungibleTokenPacketDataV2 + ) + ack := channeltypes.NewResultAcknowledgement([]byte{byte(1)}) - data, ackErr := im.getICS20PacketData(ctx, packet.GetData(), packet.GetDestPort(), packet.GetDestChannel()) + // we are explicitly wrapping this emit event call in an anonymous function so that + // the packet data is evaluated after it has been assigned a value. + defer func() { + events.EmitOnRecvPacketEvent(ctx, data, ack, ackErr) + }() + + data, ackErr = im.getICS20PacketData(ctx, packet.GetData(), packet.GetDestPort(), packet.GetDestChannel()) if ackErr != nil { ackErr = errorsmod.Wrapf(ibcerrors.ErrInvalidType, ackErr.Error()) - im.keeper.Logger(ctx).Error(fmt.Sprintf("%s sequence %d", ackErr.Error(), packet.Sequence)) ack = channeltypes.NewErrorAcknowledgement(ackErr) + im.keeper.Logger(ctx).Error(fmt.Sprintf("%s sequence %d", ackErr.Error(), packet.Sequence)) + return ack } - // only attempt the application logic if the packet data - // was successfully decoded - if ack.Success() { - err := im.keeper.OnRecvPacket(ctx, packet, data) - if err != nil { - ack = channeltypes.NewErrorAcknowledgement(err) - ackErr = err - im.keeper.Logger(ctx).Error(fmt.Sprintf("%s sequence %d", ackErr.Error(), packet.Sequence)) - } else { - im.keeper.Logger(ctx).Info("successfully handled ICS-20 packet", "sequence", packet.Sequence) - } + if ackErr = im.keeper.OnRecvPacket(ctx, packet, data); ackErr != nil { + ack = channeltypes.NewErrorAcknowledgement(ackErr) + im.keeper.Logger(ctx).Error(fmt.Sprintf("%s sequence %d", ackErr.Error(), packet.Sequence)) + return ack } - events.EmitOnRecvPacketEvent(ctx, data, ack, ackErr) + im.keeper.Logger(ctx).Info("successfully handled ICS-20 packet", "sequence", packet.Sequence) + + if data.HasForwarding() { + // NOTE: acknowledgement will be written asynchronously + return nil + } // NOTE: acknowledgement will be written synchronously during IBC handler execution. return ack diff --git a/modules/apps/transfer/ibc_module_test.go b/modules/apps/transfer/ibc_module_test.go index 2abcbafbfb9..4cc72c970b7 100644 --- a/modules/apps/transfer/ibc_module_test.go +++ b/modules/apps/transfer/ibc_module_test.go @@ -1,6 +1,7 @@ package transfer_test import ( + "encoding/json" "errors" "math" @@ -12,11 +13,13 @@ import ( capabilitytypes "github.com/cosmos/ibc-go/modules/capability/types" "github.com/cosmos/ibc-go/v8/modules/apps/transfer" "github.com/cosmos/ibc-go/v8/modules/apps/transfer/types" + clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" connectiontypes "github.com/cosmos/ibc-go/v8/modules/core/03-connection/types" channeltypes "github.com/cosmos/ibc-go/v8/modules/core/04-channel/types" porttypes "github.com/cosmos/ibc-go/v8/modules/core/05-port/types" host "github.com/cosmos/ibc-go/v8/modules/core/24-host" ibcerrors "github.com/cosmos/ibc-go/v8/modules/core/errors" + "github.com/cosmos/ibc-go/v8/modules/core/exported" ibctesting "github.com/cosmos/ibc-go/v8/testing" ) @@ -273,6 +276,148 @@ func (suite *TransferTestSuite) TestOnChanOpenAck() { } } +func (suite *TransferTestSuite) TestOnRecvPacket() { + // This test suite mostly covers the top-level logic of the ibc module OnRecvPacket function + // The core logic is covered in keeper OnRecvPacket + var packet channeltypes.Packet + var expectedAttributes []sdk.Attribute + testCases := []struct { + name string + malleate func() + expAck exported.Acknowledgement + expEventErrorMsg string + }{ + { + "success", func() {}, channeltypes.NewResultAcknowledgement([]byte{byte(1)}), "", + }, + { + "success: async aknowledgment with forwarding path", + func() { + packetData := types.NewFungibleTokenPacketDataV2( + []types.Token{ + { + Denom: types.NewDenom(sdk.DefaultBondDenom), + Amount: sdkmath.NewInt(100).String(), + }, + }, + suite.chainA.SenderAccount.GetAddress().String(), + suite.chainB.SenderAccount.GetAddress().String(), + "", + types.NewForwardingPacketData("", types.NewHop("transfer", "channel-0")), + ) + packet.Data = packetData.GetBytes() + + forwardingHopsBz, err := json.Marshal(packetData.Forwarding.Hops) + suite.Require().NoError(err) + for i, attr := range expectedAttributes { + if attr.Key == types.AttributeKeyForwardingHops { + expectedAttributes[i].Value = string(forwardingHopsBz) + break + } + } + }, + nil, + "", + }, + { + "failure: invalid packet data bytes", + func() { + packet.Data = []byte("invalid data") + + // Override expected attributes because this fails on unmarshaling packet data (so can't get the attributes) + expectedAttributes = []sdk.Attribute{ + sdk.NewAttribute(types.AttributeKeySender, ""), + sdk.NewAttribute(types.AttributeKeyReceiver, ""), + sdk.NewAttribute(types.AttributeKeyTokens, "null"), + sdk.NewAttribute(types.AttributeKeyMemo, ""), + sdk.NewAttribute(types.AttributeKeyForwardingHops, "null"), + sdk.NewAttribute(types.AttributeKeyAckSuccess, "false"), + sdk.NewAttribute(types.AttributeKeyAckError, "cannot unmarshal ICS20-V2 transfer packet data: invalid character 'i' looking for beginning of value: invalid type: invalid type"), + } + }, + channeltypes.NewErrorAcknowledgement(ibcerrors.ErrInvalidType), + "cannot unmarshal ICS20-V2 transfer packet data: invalid character 'i' looking for beginning of value: invalid type: invalid type", + }, + { + "failure: receive disabled", + func() { + suite.chainA.GetSimApp().TransferKeeper.SetParams(suite.chainA.GetContext(), types.Params{SendEnabled: false}) + }, + channeltypes.NewErrorAcknowledgement(types.ErrReceiveDisabled), + "fungible token transfers to this chain are disabled", + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + suite.SetupTest() // reset + + path := ibctesting.NewTransferPath(suite.chainA, suite.chainB) + path.Setup() + + packetData := types.NewFungibleTokenPacketDataV2( + []types.Token{ + { + Denom: types.NewDenom(sdk.DefaultBondDenom), + Amount: sdkmath.NewInt(100).String(), + }, + }, + suite.chainA.SenderAccount.GetAddress().String(), + suite.chainB.SenderAccount.GetAddress().String(), + "", + types.ForwardingPacketData{}, + ) + + tokensBz, err := json.Marshal(packetData.Tokens) + suite.Require().NoError(err) + forwardingHopsBz, err := json.Marshal(packetData.Forwarding.Hops) + suite.Require().NoError(err) + + expectedAttributes = []sdk.Attribute{ + sdk.NewAttribute(types.AttributeKeySender, packetData.Sender), + sdk.NewAttribute(types.AttributeKeyReceiver, packetData.Receiver), + sdk.NewAttribute(types.AttributeKeyTokens, string(tokensBz)), + sdk.NewAttribute(types.AttributeKeyMemo, packetData.Memo), + sdk.NewAttribute(types.AttributeKeyForwardingHops, string(forwardingHopsBz)), + } + if tc.expAck == nil || tc.expAck.Success() { + expectedAttributes = append(expectedAttributes, sdk.NewAttribute(types.AttributeKeyAckSuccess, "true")) + } else { + expectedAttributes = append(expectedAttributes, + sdk.NewAttribute(types.AttributeKeyAckSuccess, "false"), + sdk.NewAttribute(types.AttributeKeyAckError, tc.expEventErrorMsg), + ) + } + + seq := uint64(1) + packet = channeltypes.NewPacket(packetData.GetBytes(), seq, path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID, clienttypes.ZeroHeight(), suite.chainA.GetTimeoutTimestamp()) + + module, _, err := suite.chainA.App.GetIBCKeeper().PortKeeper.LookupModuleByPort(suite.chainA.GetContext(), ibctesting.TransferPort) + suite.Require().NoError(err) + + cbs, ok := suite.chainA.App.GetIBCKeeper().PortKeeper.Route(module) + suite.Require().True(ok) + + tc.malleate() // change fields in packet + + ctx := suite.chainA.GetContext() + ack := cbs.OnRecvPacket(ctx, packet, suite.chainA.SenderAccount.GetAddress()) + + suite.Require().Equal(tc.expAck, ack) + + expectedEvents := sdk.Events{ + sdk.NewEvent( + types.EventTypePacket, + expectedAttributes..., + ), + }.ToABCIEvents() + + expectedEvents = sdk.MarkEventsToIndex(expectedEvents, map[string]struct{}{}) + ibctesting.AssertEvents(&suite.Suite, expectedEvents, ctx.EventManager().Events().ToABCIEvents()) + }) + } +} + func (suite *TransferTestSuite) TestOnTimeoutPacket() { var path *ibctesting.Path var packet channeltypes.Packet @@ -344,7 +489,9 @@ func (suite *TransferTestSuite) TestOnTimeoutPacket() { suite.chainB.SenderAccount.GetAddress().String(), timeoutHeight, 0, - "") + "", + types.Forwarding{}, + ) res, err := suite.chainA.SendMsgs(msg) suite.Require().NoError(err) // message committed diff --git a/modules/apps/transfer/internal/events/events.go b/modules/apps/transfer/internal/events/events.go index c636819a796..bb17431bdd0 100644 --- a/modules/apps/transfer/internal/events/events.go +++ b/modules/apps/transfer/internal/events/events.go @@ -11,16 +11,18 @@ import ( ) // EmitTransferEvent emits a ibc transfer event on successful transfers. -func EmitTransferEvent(ctx sdk.Context, sender, receiver string, tokens types.Tokens, memo string) { - jsonTokens := mustMarshalType[types.Tokens](tokens) +func EmitTransferEvent(ctx sdk.Context, sender, receiver string, tokens types.Tokens, memo string, forwardingHops []types.Hop) { + tokensStr := mustMarshalJSON(tokens) + forwardingHopsStr := mustMarshalJSON(forwardingHops) ctx.EventManager().EmitEvents(sdk.Events{ sdk.NewEvent( types.EventTypeTransfer, sdk.NewAttribute(types.AttributeKeySender, sender), sdk.NewAttribute(types.AttributeKeyReceiver, receiver), - sdk.NewAttribute(types.AttributeKeyTokens, jsonTokens), + sdk.NewAttribute(types.AttributeKeyTokens, tokensStr), sdk.NewAttribute(types.AttributeKeyMemo, memo), + sdk.NewAttribute(types.AttributeKeyForwardingHops, forwardingHopsStr), ), sdk.NewEvent( sdk.EventTypeMessage, @@ -31,13 +33,15 @@ func EmitTransferEvent(ctx sdk.Context, sender, receiver string, tokens types.To // EmitOnRecvPacketEvent emits a fungible token packet event in the OnRecvPacket callback func EmitOnRecvPacketEvent(ctx sdk.Context, packetData types.FungibleTokenPacketDataV2, ack channeltypes.Acknowledgement, ackErr error) { - jsonTokens := mustMarshalType[types.Tokens](types.Tokens(packetData.Tokens)) + tokensStr := mustMarshalJSON(packetData.Tokens) + forwardingHopStr := mustMarshalJSON(packetData.Forwarding.Hops) eventAttributes := []sdk.Attribute{ sdk.NewAttribute(types.AttributeKeySender, packetData.Sender), sdk.NewAttribute(types.AttributeKeyReceiver, packetData.Receiver), - sdk.NewAttribute(types.AttributeKeyTokens, jsonTokens), + sdk.NewAttribute(types.AttributeKeyTokens, tokensStr), sdk.NewAttribute(types.AttributeKeyMemo, packetData.Memo), + sdk.NewAttribute(types.AttributeKeyForwardingHops, forwardingHopStr), sdk.NewAttribute(types.AttributeKeyAckSuccess, strconv.FormatBool(ack.Success())), } @@ -59,15 +63,17 @@ func EmitOnRecvPacketEvent(ctx sdk.Context, packetData types.FungibleTokenPacket // EmitOnAcknowledgementPacketEvent emits a fungible token packet event in the OnAcknowledgementPacket callback func EmitOnAcknowledgementPacketEvent(ctx sdk.Context, packetData types.FungibleTokenPacketDataV2, ack channeltypes.Acknowledgement) { - jsonTokens := mustMarshalType[types.Tokens](types.Tokens(packetData.Tokens)) + tokensStr := mustMarshalJSON(packetData.Tokens) + forwardingHopsStr := mustMarshalJSON(packetData.Forwarding.Hops) ctx.EventManager().EmitEvents(sdk.Events{ sdk.NewEvent( types.EventTypePacket, sdk.NewAttribute(sdk.AttributeKeySender, packetData.Sender), sdk.NewAttribute(types.AttributeKeyReceiver, packetData.Receiver), - sdk.NewAttribute(types.AttributeKeyTokens, jsonTokens), + sdk.NewAttribute(types.AttributeKeyTokens, tokensStr), sdk.NewAttribute(types.AttributeKeyMemo, packetData.Memo), + sdk.NewAttribute(types.AttributeKeyForwardingHops, forwardingHopsStr), sdk.NewAttribute(types.AttributeKeyAck, ack.String()), ), sdk.NewEvent( @@ -96,14 +102,16 @@ func EmitOnAcknowledgementPacketEvent(ctx sdk.Context, packetData types.Fungible // EmitOnTimeoutEvent emits a fungible token packet event in the OnTimeoutPacket callback func EmitOnTimeoutEvent(ctx sdk.Context, packetData types.FungibleTokenPacketDataV2) { - jsonTokens := mustMarshalType[types.Tokens](types.Tokens(packetData.Tokens)) + tokensStr := mustMarshalJSON(packetData.Tokens) + forwardingHopsStr := mustMarshalJSON(packetData.Forwarding.Hops) ctx.EventManager().EmitEvents(sdk.Events{ sdk.NewEvent( types.EventTypeTimeout, sdk.NewAttribute(types.AttributeKeyReceiver, packetData.Sender), - sdk.NewAttribute(types.AttributeKeyRefundTokens, jsonTokens), + sdk.NewAttribute(types.AttributeKeyRefundTokens, tokensStr), sdk.NewAttribute(types.AttributeKeyMemo, packetData.Memo), + sdk.NewAttribute(types.AttributeKeyForwardingHops, forwardingHopsStr), ), sdk.NewEvent( sdk.EventTypeMessage, @@ -114,19 +122,19 @@ func EmitOnTimeoutEvent(ctx sdk.Context, packetData types.FungibleTokenPacketDat // EmitDenomEvent emits a denomination event in the OnRecv callback. func EmitDenomEvent(ctx sdk.Context, token types.Token) { - jsonDenom := mustMarshalType[types.Denom](token.Denom) + denomStr := mustMarshalJSON(token.Denom) ctx.EventManager().EmitEvent( sdk.NewEvent( types.EventTypeDenom, sdk.NewAttribute(types.AttributeKeyDenomHash, token.Denom.Hash().String()), - sdk.NewAttribute(types.AttributeKeyDenom, jsonDenom), + sdk.NewAttribute(types.AttributeKeyDenom, denomStr), ), ) } // mustMarshalType json marshals the given type and panics on failure. -func mustMarshalType[T any](v T) string { +func mustMarshalJSON(v any) string { bz, err := json.Marshal(v) if err != nil { panic(err) diff --git a/modules/apps/transfer/internal/packet.go b/modules/apps/transfer/internal/packet.go index 0d5c7a9456e..cd6a2638954 100644 --- a/modules/apps/transfer/internal/packet.go +++ b/modules/apps/transfer/internal/packet.go @@ -51,8 +51,9 @@ func packetDataV1ToV2(packetData types.FungibleTokenPacketData) (types.FungibleT Amount: packetData.Amount, }, }, - Sender: packetData.Sender, - Receiver: packetData.Receiver, - Memo: packetData.Memo, + Sender: packetData.Sender, + Receiver: packetData.Receiver, + Memo: packetData.Memo, + Forwarding: types.ForwardingPacketData{}, }, nil } diff --git a/modules/apps/transfer/internal/packet_test.go b/modules/apps/transfer/internal/packet_test.go index 72d57476fc2..ab56c593111 100644 --- a/modules/apps/transfer/internal/packet_test.go +++ b/modules/apps/transfer/internal/packet_test.go @@ -10,6 +10,8 @@ import ( "github.com/cosmos/ibc-go/v8/modules/apps/transfer/types" ) +var emptyForwardingPacketData = types.ForwardingPacketData{} + func TestUnmarshalPacketData(t *testing.T) { var ( packetDataBz []byte @@ -35,7 +37,7 @@ func TestUnmarshalPacketData(t *testing.T) { Denom: types.NewDenom("atom", types.NewTrace("transfer", "channel-0")), Amount: "1000", }, - }, "sender", "receiver", "") + }, "sender", "receiver", "", emptyForwardingPacketData) packetDataBz = packetData.GetBytes() version = types.V2 @@ -92,7 +94,7 @@ func TestPacketV1ToPacketV2(t *testing.T) { Denom: types.NewDenom("atom", types.NewTrace("transfer", "channel-0")), Amount: "1000", }, - }, sender, receiver, ""), + }, sender, receiver, "", emptyForwardingPacketData), nil, }, { @@ -104,7 +106,7 @@ func TestPacketV1ToPacketV2(t *testing.T) { Denom: types.NewDenom("atom"), Amount: "1000", }, - }, sender, receiver, ""), + }, sender, receiver, "", emptyForwardingPacketData), nil, }, { @@ -116,7 +118,7 @@ func TestPacketV1ToPacketV2(t *testing.T) { Denom: types.NewDenom("atom/withslash", types.NewTrace("transfer", "channel-0")), Amount: "1000", }, - }, sender, receiver, ""), + }, sender, receiver, "", emptyForwardingPacketData), nil, }, { @@ -128,7 +130,7 @@ func TestPacketV1ToPacketV2(t *testing.T) { Denom: types.NewDenom("atom/", types.NewTrace("transfer", "channel-0")), Amount: "1000", }, - }, sender, receiver, ""), + }, sender, receiver, "", emptyForwardingPacketData), nil, }, { @@ -140,7 +142,7 @@ func TestPacketV1ToPacketV2(t *testing.T) { Denom: types.NewDenom("atom/pool", types.NewTrace("transfer", "channel-0"), types.NewTrace("transfer", "channel-1")), Amount: "1000", }, - }, sender, receiver, ""), + }, sender, receiver, "", emptyForwardingPacketData), nil, }, { @@ -152,7 +154,7 @@ func TestPacketV1ToPacketV2(t *testing.T) { Denom: types.NewDenom("atom", types.NewTrace("transfer", "channel-0"), types.NewTrace("transfer", "channel-1"), types.NewTrace("transfer-custom", "channel-2")), Amount: "1000", }, - }, sender, receiver, ""), + }, sender, receiver, "", emptyForwardingPacketData), nil, }, { @@ -164,7 +166,7 @@ func TestPacketV1ToPacketV2(t *testing.T) { Denom: types.NewDenom("atom/pool", types.NewTrace("transfer", "channel-0"), types.NewTrace("transfer", "channel-1"), types.NewTrace("transfer-custom", "channel-2")), Amount: "1000", }, - }, sender, receiver, ""), + }, sender, receiver, "", emptyForwardingPacketData), nil, }, { diff --git a/modules/apps/transfer/keeper/export_test.go b/modules/apps/transfer/keeper/export_test.go index f3f66b3c33e..cda19c83814 100644 --- a/modules/apps/transfer/keeper/export_test.go +++ b/modules/apps/transfer/keeper/export_test.go @@ -5,6 +5,7 @@ import ( internaltypes "github.com/cosmos/ibc-go/v8/modules/apps/transfer/internal/types" "github.com/cosmos/ibc-go/v8/modules/apps/transfer/types" + channeltypes "github.com/cosmos/ibc-go/v8/modules/core/04-channel/types" ) // SetDenomTraces is a wrapper around iterateDenomTraces for testing purposes. @@ -33,7 +34,22 @@ func (k Keeper) TokenFromCoin(ctx sdk.Context, coin sdk.Coin) (types.Token, erro return k.tokenFromCoin(ctx, coin) } +// UnwindHops is a wrapper around unwindToken for testing purposes. +func (k Keeper) UnwindHops(ctx sdk.Context, msg *types.MsgTransfer) (*types.MsgTransfer, error) { + return k.unwindHops(ctx, msg) +} + +// UnwindHops is a wrapper around unwindToken for testing purposes. +func (k Keeper) GetForwardedPacket(ctx sdk.Context, portID, channelID string, sequence uint64) (channeltypes.Packet, bool) { + return k.getForwardedPacket(ctx, portID, channelID, sequence) +} + +// IsBlockedAddr is a wrapper around isBlockedAddr for testing purposes +func (k Keeper) IsBlockedAddr(addr sdk.AccAddress) bool { + return k.isBlockedAddr(addr) +} + // CreatePacketDataBytesFromVersion is a wrapper around createPacketDataBytesFromVersion for testing purposes -func CreatePacketDataBytesFromVersion(appVersion, sender, receiver, memo string, tokens types.Tokens) []byte { - return createPacketDataBytesFromVersion(appVersion, sender, receiver, memo, tokens) +func CreatePacketDataBytesFromVersion(appVersion, sender, receiver, memo string, tokens types.Tokens, hops []types.Hop) ([]byte, error) { + return createPacketDataBytesFromVersion(appVersion, sender, receiver, memo, tokens, hops) } diff --git a/modules/apps/transfer/keeper/forwarding.go b/modules/apps/transfer/keeper/forwarding.go new file mode 100644 index 00000000000..b99f3b06713 --- /dev/null +++ b/modules/apps/transfer/keeper/forwarding.go @@ -0,0 +1,146 @@ +package keeper + +import ( + errorsmod "cosmossdk.io/errors" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/cosmos/ibc-go/v8/modules/apps/transfer/types" + clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" + channeltypes "github.com/cosmos/ibc-go/v8/modules/core/04-channel/types" + host "github.com/cosmos/ibc-go/v8/modules/core/24-host" + ibcerrors "github.com/cosmos/ibc-go/v8/modules/core/errors" +) + +// forwardPacket forwards a fungible FungibleTokenPacketDataV2 to the next hop in the forwarding path. +func (k Keeper) forwardPacket(ctx sdk.Context, data types.FungibleTokenPacketDataV2, packet channeltypes.Packet, receivedCoins sdk.Coins) error { + var nextForwardingPath types.Forwarding + if len(data.Forwarding.Hops) > 1 { + // remove the first hop since we are going to send to the first hop now and we want to propagate the rest of the hops to the receiver + nextForwardingPath = types.NewForwarding(false, data.Forwarding.Hops[1:]...) + } + + // sending from module account (used as a temporary forward escrow) to the original receiver address. + sender := k.authKeeper.GetModuleAddress(types.ModuleName) + + msg := types.NewMsgTransfer( + data.Forwarding.Hops[0].PortId, + data.Forwarding.Hops[0].ChannelId, + receivedCoins, + sender.String(), + data.Receiver, + clienttypes.ZeroHeight(), + packet.TimeoutTimestamp, + data.Forwarding.DestinationMemo, + nextForwardingPath, + ) + + resp, err := k.Transfer(ctx, msg) + if err != nil { + return err + } + + k.setForwardedPacket(ctx, data.Forwarding.Hops[0].PortId, data.Forwarding.Hops[0].ChannelId, resp.Sequence, packet) + return nil +} + +// ackForwardPacketSuccess writes a successful async acknowledgement for the prevPacket +func (k Keeper) ackForwardPacketSuccess(ctx sdk.Context, prevPacket, forwardedPacket channeltypes.Packet) error { + forwardAck := channeltypes.NewResultAcknowledgement([]byte{byte(1)}) + return k.acknowledgeForwardedPacket(ctx, prevPacket, forwardedPacket, forwardAck) +} + +// ackForwardPacketError reverts the receive packet logic that occurs in the middle chain and writes the async ack for the prevPacket +func (k Keeper) ackForwardPacketError(ctx sdk.Context, prevPacket, forwardedPacket channeltypes.Packet, failedPacketData types.FungibleTokenPacketDataV2) error { + // the forwarded packet has failed, thus the funds have been refunded to the intermediate address. + // we must revert the changes that came from successfully receiving the tokens on our chain + // before propagating the error acknowledgement back to original sender chain + if err := k.revertForwardedPacket(ctx, prevPacket, failedPacketData); err != nil { + return err + } + + forwardAck := channeltypes.NewErrorAcknowledgement(types.ErrForwardedPacketFailed) + return k.acknowledgeForwardedPacket(ctx, prevPacket, forwardedPacket, forwardAck) +} + +// ackForwardPacketTimeout reverts the receive packet logic that occurs in the middle chain and writes a failed async ack for the prevPacket +func (k Keeper) ackForwardPacketTimeout(ctx sdk.Context, prevPacket, forwardedPacket channeltypes.Packet, timeoutPacketData types.FungibleTokenPacketDataV2) error { + if err := k.revertForwardedPacket(ctx, prevPacket, timeoutPacketData); err != nil { + return err + } + + forwardAck := channeltypes.NewErrorAcknowledgement(types.ErrForwardedPacketTimedOut) + return k.acknowledgeForwardedPacket(ctx, prevPacket, forwardedPacket, forwardAck) +} + +// acknowledgeForwardedPacket writes the async acknowledgement for packet +func (k Keeper) acknowledgeForwardedPacket(ctx sdk.Context, packet, forwardedPacket channeltypes.Packet, ack channeltypes.Acknowledgement) error { + capability, ok := k.scopedKeeper.GetCapability(ctx, host.ChannelCapabilityPath(packet.DestinationPort, packet.DestinationChannel)) + if !ok { + return errorsmod.Wrap(channeltypes.ErrChannelCapabilityNotFound, "module does not own channel capability") + } + + if err := k.ics4Wrapper.WriteAcknowledgement(ctx, capability, packet, ack); err != nil { + return err + } + + k.deleteForwardedPacket(ctx, forwardedPacket.SourcePort, forwardedPacket.SourceChannel, forwardedPacket.Sequence) + return nil +} + +// revertForwardedPacket reverts the logic of receive packet that occurs in the middle chains during a packet forwarding. +// If the packet fails to be forwarded all the way to the final destination, the state changes on this chain must be reverted +// before sending back the error acknowledgement to ensure atomic packet forwarding. +func (k Keeper) revertForwardedPacket(ctx sdk.Context, prevPacket channeltypes.Packet, failedPacketData types.FungibleTokenPacketDataV2) error { + /* + Recall that RecvPacket handles an incoming packet depending on the denom of the received funds: + 1. If the funds are native, then the amount is sent to the receiver from the escrow. + 2. If the funds are foreign, then a voucher token is minted. + We revert it in this function by: + 1. Sending funds back to escrow if the funds are native. + 2. Burning voucher tokens if the funds are foreign + */ + + forwardingAddr := k.authKeeper.GetModuleAddress(types.ModuleName) + escrow := types.GetEscrowAddress(prevPacket.DestinationPort, prevPacket.DestinationChannel) + + // we can iterate over the received tokens of prevPacket by iterating over the sent tokens of failedPacketData + for _, token := range failedPacketData.Tokens { + // parse the transfer amount + coin, err := token.ToCoin() + if err != nil { + return err + } + + // check if the token we received originated on the sender + // given that the packet is being reversed, we check the DestinationChannel and DestinationPort + // of the prevPacket to see if a hop was added to the trace during the receive step + if token.Denom.HasPrefix(prevPacket.DestinationPort, prevPacket.DestinationChannel) { + if err := k.bankKeeper.BurnCoins(ctx, types.ModuleName, sdk.NewCoins(coin)); err != nil { + return err + } + } else { + // send it back to the escrow address + if err := k.escrowCoin(ctx, forwardingAddr, escrow, coin); err != nil { + return err + } + } + } + return nil +} + +// getReceiverFromPacketData returns either the sender specified in the packet data or the forwarding address +// if there are still hops left to perform. +func (k Keeper) getReceiverFromPacketData(data types.FungibleTokenPacketDataV2) (sdk.AccAddress, error) { + if data.HasForwarding() { + // since data.Receiver can potentially be a non-CosmosSDK AccAddress, we return early if the packet should be forwarded + return k.authKeeper.GetModuleAddress(types.ModuleName), nil + } + + receiver, err := sdk.AccAddressFromBech32(data.Receiver) + if err != nil { + return nil, errorsmod.Wrapf(ibcerrors.ErrInvalidAddress, "failed to decode receiver address %s: %v", data.Receiver, err) + } + + return receiver, nil +} diff --git a/modules/apps/transfer/keeper/invariants_test.go b/modules/apps/transfer/keeper/invariants_test.go index 668d5c17c59..61474cfafbc 100644 --- a/modules/apps/transfer/keeper/invariants_test.go +++ b/modules/apps/transfer/keeper/invariants_test.go @@ -56,6 +56,7 @@ func (suite *KeeperTestSuite) TestTotalEscrowPerDenomInvariant() { suite.chainA.SenderAccount.GetAddress().String(), suite.chainB.SenderAccount.GetAddress().String(), suite.chainA.GetTimeoutHeight(), 0, "", + types.Forwarding{}, ) res, err := suite.chainA.SendMsgs(msg) diff --git a/modules/apps/transfer/keeper/keeper.go b/modules/apps/transfer/keeper/keeper.go index 255abffe4ef..a5adf27b975 100644 --- a/modules/apps/transfer/keeper/keeper.go +++ b/modules/apps/transfer/keeper/keeper.go @@ -18,6 +18,7 @@ import ( capabilitytypes "github.com/cosmos/ibc-go/modules/capability/types" "github.com/cosmos/ibc-go/v8/modules/apps/transfer/types" + channeltypes "github.com/cosmos/ibc-go/v8/modules/core/04-channel/types" porttypes "github.com/cosmos/ibc-go/v8/modules/core/05-port/types" host "github.com/cosmos/ibc-go/v8/modules/core/24-host" "github.com/cosmos/ibc-go/v8/modules/core/exported" @@ -306,3 +307,43 @@ func (k Keeper) AuthenticateCapability(ctx sdk.Context, cap *capabilitytypes.Cap func (k Keeper) ClaimCapability(ctx sdk.Context, cap *capabilitytypes.Capability, name string) error { return k.scopedKeeper.ClaimCapability(ctx, cap, name) } + +// setForwardedPacket sets the forwarded packet in the store. +func (k Keeper) setForwardedPacket(ctx sdk.Context, portID, channelID string, sequence uint64, packet channeltypes.Packet) { + store := ctx.KVStore(k.storeKey) + bz := k.cdc.MustMarshal(&packet) + store.Set(types.PacketForwardKey(portID, channelID, sequence), bz) +} + +// getForwardedPacket gets the forwarded packet from the store. +func (k Keeper) getForwardedPacket(ctx sdk.Context, portID, channelID string, sequence uint64) (channeltypes.Packet, bool) { + store := ctx.KVStore(k.storeKey) + bz := store.Get(types.PacketForwardKey(portID, channelID, sequence)) + if bz == nil { + return channeltypes.Packet{}, false + } + + var storedPacket channeltypes.Packet + k.cdc.MustUnmarshal(bz, &storedPacket) + + return storedPacket, true +} + +// deleteForwardedPacket deletes the forwarded packet from the store. +func (k Keeper) deleteForwardedPacket(ctx sdk.Context, portID, channelID string, sequence uint64) { + store := ctx.KVStore(k.storeKey) + packetKey := types.PacketForwardKey(portID, channelID, sequence) + + store.Delete(packetKey) +} + +// IsBlockedAddr checks if the given address is allowed to send or receive tokens. +// The module account is always allowed to send and receive tokens. +func (k Keeper) isBlockedAddr(addr sdk.AccAddress) bool { + moduleAddr := k.authKeeper.GetModuleAddress(types.ModuleName) + if addr.Equals(moduleAddr) { + return false + } + + return k.bankKeeper.BlockedAddr(addr) +} diff --git a/modules/apps/transfer/keeper/keeper_test.go b/modules/apps/transfer/keeper/keeper_test.go index f9f0e658f15..f816dbe4740 100644 --- a/modules/apps/transfer/keeper/keeper_test.go +++ b/modules/apps/transfer/keeper/keeper_test.go @@ -13,6 +13,7 @@ import ( "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" + minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" "github.com/cosmos/ibc-go/v8/modules/apps/transfer/keeper" "github.com/cosmos/ibc-go/v8/modules/apps/transfer/types" @@ -29,13 +30,15 @@ type KeeperTestSuite struct { chainA *ibctesting.TestChain chainB *ibctesting.TestChain chainC *ibctesting.TestChain + chainD *ibctesting.TestChain } func (suite *KeeperTestSuite) SetupTest() { - suite.coordinator = ibctesting.NewCoordinator(suite.T(), 3) + suite.coordinator = ibctesting.NewCoordinator(suite.T(), 4) suite.chainA = suite.coordinator.GetChain(ibctesting.GetChainID(1)) suite.chainB = suite.coordinator.GetChain(ibctesting.GetChainID(2)) suite.chainC = suite.coordinator.GetChain(ibctesting.GetChainID(3)) + suite.chainD = suite.coordinator.GetChain(ibctesting.GetChainID(4)) queryHelper := baseapp.NewQueryServerTestHelper(suite.chainA.GetContext(), suite.chainA.GetSimApp().InterfaceRegistry()) types.RegisterQueryServer(queryHelper, suite.chainA.GetSimApp().TransferKeeper) @@ -350,3 +353,37 @@ func (suite *KeeperTestSuite) TestWithICS4Wrapper() { suite.Require().IsType((*channelkeeper.Keeper)(nil), ics4Wrapper) } + +func (suite *KeeperTestSuite) TestIsBlockedAddr() { + suite.SetupTest() + + testCases := []struct { + name string + addr sdk.AccAddress + expBlock bool + }{ + { + "transfer module account address", + suite.chainA.GetSimApp().AccountKeeper.GetModuleAddress(types.ModuleName), + false, + }, + { + "regular address", + suite.chainA.SenderAccount.GetAddress(), + false, + }, + { + "blocked address", + suite.chainA.GetSimApp().AccountKeeper.GetModuleAddress(minttypes.ModuleName), + true, + }, + } + + for _, tc := range testCases { + tc := tc + + suite.Run(tc.name, func() { + suite.Require().Equal(tc.expBlock, suite.chainA.GetSimApp().TransferKeeper.IsBlockedAddr(tc.addr)) + }) + } +} diff --git a/modules/apps/transfer/keeper/mbt_relay_test.go b/modules/apps/transfer/keeper/mbt_relay_test.go index 585fc596364..33eeab9bee7 100644 --- a/modules/apps/transfer/keeper/mbt_relay_test.go +++ b/modules/apps/transfer/keeper/mbt_relay_test.go @@ -159,6 +159,7 @@ func FungibleTokenPacketFromTla(packet TlaFungibleTokenPacket) FungibleTokenPack AddressFromString(packet.Data.Sender), AddressFromString(packet.Data.Receiver), "", + types.ForwardingPacketData{}, ), } } @@ -348,6 +349,7 @@ func (suite *KeeperTestSuite) TestModelBasedRelay() { if !ok { panic(errors.New("MBT failed to parse amount from string")) } + msg := types.NewMsgTransfer( tc.packet.SourcePort, tc.packet.SourceChannel, @@ -356,6 +358,7 @@ func (suite *KeeperTestSuite) TestModelBasedRelay() { tc.packet.Data.Receiver, suite.chainA.GetTimeoutHeight(), 0, // only use timeout height "", + types.Forwarding{}, ) _, err = suite.chainB.GetSimApp().TransferKeeper.Transfer(suite.chainB.GetContext(), msg) @@ -363,9 +366,11 @@ func (suite *KeeperTestSuite) TestModelBasedRelay() { } case "OnRecvPacket": err = suite.chainB.GetSimApp().TransferKeeper.OnRecvPacket(suite.chainB.GetContext(), packet, tc.packet.Data) + case "OnTimeoutPacket": registerDenomFn() err = suite.chainB.GetSimApp().TransferKeeper.OnTimeoutPacket(suite.chainB.GetContext(), packet, tc.packet.Data) + case "OnRecvAcknowledgementResult": err = suite.chainB.GetSimApp().TransferKeeper.OnAcknowledgementPacket( suite.chainB.GetContext(), packet, tc.packet.Data, diff --git a/modules/apps/transfer/keeper/migrations_test.go b/modules/apps/transfer/keeper/migrations_test.go index c5e922688dc..24fd65bfef6 100644 --- a/modules/apps/transfer/keeper/migrations_test.go +++ b/modules/apps/transfer/keeper/migrations_test.go @@ -243,7 +243,7 @@ func (suite *KeeperTestSuite) TestMigrateTotalEscrowForDenom() { func() { denom = sdk.DefaultBondDenom escrowAddress := transfertypes.GetEscrowAddress(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID) - coin := sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(100)) + coin := ibctesting.TestCoin // funds the escrow account to have balance suite.Require().NoError(banktestutil.FundAccount(suite.chainA.GetContext(), suite.chainA.GetSimApp().BankKeeper, escrowAddress, sdk.NewCoins(coin))) @@ -259,8 +259,8 @@ func (suite *KeeperTestSuite) TestMigrateTotalEscrowForDenom() { escrowAddress1 := transfertypes.GetEscrowAddress(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID) escrowAddress2 := transfertypes.GetEscrowAddress(extraPath.EndpointA.ChannelConfig.PortID, extraPath.EndpointA.ChannelID) - coin1 := sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(100)) - coin2 := sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(100)) + coin1 := ibctesting.TestCoin + coin2 := ibctesting.TestCoin // funds the escrow accounts to have balance suite.Require().NoError(banktestutil.FundAccount(suite.chainA.GetContext(), suite.chainA.GetSimApp().BankKeeper, escrowAddress1, sdk.NewCoins(coin1))) diff --git a/modules/apps/transfer/keeper/msg_server.go b/modules/apps/transfer/keeper/msg_server.go index c8cc0f5b0eb..b8712975df4 100644 --- a/modules/apps/transfer/keeper/msg_server.go +++ b/modules/apps/transfer/keeper/msg_server.go @@ -32,13 +32,20 @@ func (k Keeper) Transfer(goCtx context.Context, msg *types.MsgTransfer) (*types. return nil, errorsmod.Wrapf(types.ErrSendDisabled, err.Error()) } - if k.bankKeeper.BlockedAddr(sender) { + if k.isBlockedAddr(sender) { return nil, errorsmod.Wrapf(ibcerrors.ErrUnauthorized, "%s is not allowed to send funds", sender) } + if msg.Forwarding.Unwind { + msg, err = k.unwindHops(ctx, msg) + if err != nil { + return nil, err + } + } + sequence, err := k.sendTransfer( ctx, msg.SourcePort, msg.SourceChannel, coins, sender, msg.Receiver, msg.TimeoutHeight, msg.TimeoutTimestamp, - msg.Memo) + msg.Memo, msg.Forwarding.Hops) if err != nil { return nil, err } @@ -59,3 +66,34 @@ func (k Keeper) UpdateParams(goCtx context.Context, msg *types.MsgUpdateParams) return &types.MsgUpdateParamsResponse{}, nil } + +// unwindHops unwinds the hops present in the tokens denomination and returns the message modified to reflect +// the unwound path to take. It assumes that only a single token is present (as this is verified in ValidateBasic) +// in the tokens list and ensures that the token is not native to the chain. +func (k Keeper) unwindHops(ctx sdk.Context, msg *types.MsgTransfer) (*types.MsgTransfer, error) { + coins := msg.GetCoins() + token, err := k.tokenFromCoin(ctx, coins[0]) + if err != nil { + return nil, err + } + + if token.Denom.IsNative() { + return nil, errorsmod.Wrap(types.ErrInvalidForwarding, "cannot unwind a native token") + } + var unwindHops []types.Hop + // remove the first hop in denom as it is the current port/channel on this chain + for _, trace := range token.Denom.Trace[1:] { + unwindHops = append(unwindHops, types.NewHop(trace.PortId, trace.ChannelId)) //nolint: gosimple + } + + // Update message fields. + msg.SourcePort, msg.SourceChannel = token.Denom.Trace[0].PortId, token.Denom.Trace[0].ChannelId + msg.Forwarding.Hops = append(unwindHops, msg.Forwarding.Hops...) + msg.Forwarding.Unwind = false + + // Message is validate again, this would only fail if hops now exceeds maximum allowed. + if err := msg.ValidateBasic(); err != nil { + return nil, err + } + return msg, nil +} diff --git a/modules/apps/transfer/keeper/msg_server_test.go b/modules/apps/transfer/keeper/msg_server_test.go index 02242c703a8..83449069878 100644 --- a/modules/apps/transfer/keeper/msg_server_test.go +++ b/modules/apps/transfer/keeper/msg_server_test.go @@ -9,10 +9,12 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" abci "github.com/cometbft/cometbft/abci/types" "github.com/cosmos/ibc-go/v8/modules/apps/transfer/types" + clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" channeltypes "github.com/cosmos/ibc-go/v8/modules/core/04-channel/types" ibcerrors "github.com/cosmos/ibc-go/v8/modules/core/errors" ibctesting "github.com/cosmos/ibc-go/v8/testing" @@ -79,7 +81,7 @@ func (suite *KeeperTestSuite) TestMsgTransfer() { { "failure: sender is a blocked address", func() { - msg.Sender = suite.chainA.GetSimApp().AccountKeeper.GetModuleAddress(types.ModuleName).String() + msg.Sender = suite.chainA.GetSimApp().AccountKeeper.GetModuleAddress(minttypes.ModuleName).String() }, ibcerrors.ErrUnauthorized, }, @@ -110,6 +112,14 @@ func (suite *KeeperTestSuite) TestMsgTransfer() { }, ibcerrors.ErrInvalidRequest, }, + { + "failure: cannot unwind native tokens", + func() { + msg.Forwarding = types.NewForwarding(true) + msg.Tokens = []sdk.Coin{ibctesting.TestCoin} + }, + types.ErrInvalidForwarding, + }, } for _, tc := range testCases { @@ -129,6 +139,7 @@ func (suite *KeeperTestSuite) TestMsgTransfer() { suite.chainB.SenderAccount.GetAddress().String(), suite.chainB.GetTimeoutHeight(), 0, // only use timeout height "memo", + types.Forwarding{}, ) // send some coins of the second denom from bank module to the sender account as well @@ -148,7 +159,10 @@ func (suite *KeeperTestSuite) TestMsgTransfer() { tokens = append(tokens, token) } - jsonTokens, err := json.Marshal(types.Tokens(tokens)) + tokensBz, err := json.Marshal(types.Tokens(tokens)) + suite.Require().NoError(err) + + forwardingHopsBz, err := json.Marshal(msg.Forwarding.Hops) suite.Require().NoError(err) res, err := suite.chainA.GetSimApp().TransferKeeper.Transfer(ctx, msg) @@ -161,8 +175,9 @@ func (suite *KeeperTestSuite) TestMsgTransfer() { sdk.NewEvent(types.EventTypeTransfer, sdk.NewAttribute(types.AttributeKeySender, msg.Sender), sdk.NewAttribute(types.AttributeKeyReceiver, msg.Receiver), - sdk.NewAttribute(types.AttributeKeyTokens, string(jsonTokens)), + sdk.NewAttribute(types.AttributeKeyTokens, string(tokensBz)), sdk.NewAttribute(types.AttributeKeyMemo, msg.Memo), + sdk.NewAttribute(types.AttributeKeyForwardingHops, string(forwardingHopsBz)), ), sdk.NewEvent( sdk.EventTypeMessage, @@ -233,3 +248,122 @@ func (suite *KeeperTestSuite) TestUpdateParams() { }) } } + +func (suite *KeeperTestSuite) TestUnwindHops() { + var msg *types.MsgTransfer + var path *ibctesting.Path + denom := types.NewDenom(ibctesting.TestCoin.Denom, types.NewTrace(ibctesting.MockPort, "channel-0"), types.NewTrace(ibctesting.MockPort, "channel-1")) + coins := sdk.NewCoins(sdk.NewCoin(denom.IBCDenom(), ibctesting.TestCoin.Amount)) + testCases := []struct { + name string + malleate func() + assertResult func(modified *types.MsgTransfer, err error) + }{ + { + "success", + func() { + suite.chainA.GetSimApp().TransferKeeper.SetDenom(suite.chainA.GetContext(), denom) + }, + func(modified *types.MsgTransfer, err error) { + suite.Require().NoError(err, "got unexpected error from unwindHops") + msg.SourceChannel = denom.Trace[0].PortId + msg.SourcePort = denom.Trace[0].ChannelId + msg.Forwarding = types.NewForwarding(false, types.NewHop(denom.Trace[1].PortId, denom.Trace[1].ChannelId)) + suite.Require().Equal(*msg, *modified, "expected msg and modified msg are different") + }, + }, + { + "success: multiple unwind hops", + func() { + denom.Trace = append(denom.Trace, types.NewTrace(ibctesting.MockPort, "channel-2"), types.NewTrace(ibctesting.MockPort, "channel-3")) + coins = sdk.NewCoins(sdk.NewCoin(denom.IBCDenom(), ibctesting.TestCoin.Amount)) + suite.chainA.GetSimApp().TransferKeeper.SetDenom(suite.chainA.GetContext(), denom) + msg.Tokens = coins + }, + func(modified *types.MsgTransfer, err error) { + suite.Require().NoError(err, "got unexpected error from unwindHops") + msg.SourceChannel = denom.Trace[0].PortId + msg.SourcePort = denom.Trace[0].ChannelId + msg.Forwarding = types.NewForwarding(false, + types.NewHop(denom.Trace[3].PortId, denom.Trace[3].ChannelId), + types.NewHop(denom.Trace[2].PortId, denom.Trace[2].ChannelId), + types.NewHop(denom.Trace[1].PortId, denom.Trace[1].ChannelId), + ) + suite.Require().Equal(*msg, *modified, "expected msg and modified msg are different") + }, + }, + { + "success - unwind hops are added to existing hops", + func() { + suite.chainA.GetSimApp().TransferKeeper.SetDenom(suite.chainA.GetContext(), denom) + msg.Forwarding = types.NewForwarding(true, types.NewHop(ibctesting.MockPort, "channel-2")) + }, + func(modified *types.MsgTransfer, err error) { + suite.Require().NoError(err, "got unexpected error from unwindHops") + msg.SourceChannel = denom.Trace[0].PortId + msg.SourcePort = denom.Trace[0].ChannelId + msg.Forwarding = types.NewForwarding(false, + types.NewHop(denom.Trace[1].PortId, denom.Trace[1].ChannelId), + types.NewHop(ibctesting.MockPort, "channel-2"), + ) + suite.Require().Equal(*msg, *modified, "expected msg and modified msg are different") + }, + }, + { + "failure: no denom set on keeper", + func() {}, + func(modified *types.MsgTransfer, err error) { + suite.Require().ErrorIs(err, types.ErrDenomNotFound) + }, + }, + { + "failure: validateBasic() fails due to invalid channelID", + func() { + denom.Trace[0].ChannelId = "channel/0" + coins = sdk.NewCoins(sdk.NewCoin(denom.IBCDenom(), ibctesting.TestCoin.Amount)) + msg.Tokens = coins + suite.chainA.GetSimApp().TransferKeeper.SetDenom(suite.chainA.GetContext(), denom) + }, + func(modified *types.MsgTransfer, err error) { + suite.Require().ErrorContains(err, "invalid source channel ID") + }, + }, + { + "failure: denom is native", + func() { + denom.Trace = nil + coins = sdk.NewCoins(sdk.NewCoin(denom.IBCDenom(), ibctesting.TestCoin.Amount)) + msg.Tokens = coins + suite.chainA.GetSimApp().TransferKeeper.SetDenom(suite.chainA.GetContext(), denom) + }, + func(modified *types.MsgTransfer, err error) { + suite.Require().ErrorIs(err, types.ErrInvalidForwarding) + }, + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + suite.SetupTest() + + path = ibctesting.NewTransferPath(suite.chainA, suite.chainB) + path.Setup() + + msg = types.NewMsgTransfer( + path.EndpointA.ChannelConfig.PortID, + path.EndpointA.ChannelID, + coins, + suite.chainA.SenderAccount.GetAddress().String(), + suite.chainB.SenderAccount.GetAddress().String(), + clienttypes.ZeroHeight(), + suite.chainA.GetTimeoutTimestamp(), + "memo", + types.NewForwarding(true), + ) + + tc.malleate() + gotMsg, err := suite.chainA.GetSimApp().TransferKeeper.UnwindHops(suite.chainA.GetContext(), msg) + tc.assertResult(gotMsg, err) + }) + } +} diff --git a/modules/apps/transfer/keeper/relay.go b/modules/apps/transfer/keeper/relay.go index 60c01a836b1..b3bd61c3cad 100644 --- a/modules/apps/transfer/keeper/relay.go +++ b/modules/apps/transfer/keeper/relay.go @@ -11,6 +11,7 @@ import ( "github.com/cosmos/cosmos-sdk/telemetry" sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/cosmos/ibc-go/v8/modules/apps/transfer/internal/events" internaltelemetry "github.com/cosmos/ibc-go/v8/modules/apps/transfer/internal/telemetry" @@ -64,6 +65,7 @@ func (k Keeper) sendTransfer( timeoutHeight clienttypes.Height, timeoutTimestamp uint64, memo string, + hops []types.Hop, ) (uint64, error) { channel, found := k.channelKeeper.GetChannel(ctx, sourcePort, sourceChannel) if !found { @@ -75,9 +77,16 @@ func (k Keeper) sendTransfer( return 0, errorsmod.Wrapf(ibcerrors.ErrInvalidRequest, "application version not found for source port: %s and source channel: %s", sourcePort, sourceChannel) } - // ics20-1 only supports a single coin, so if that is the current version, we must only process a single coin. - if appVersion == types.V1 && len(coins) > 1 { - return 0, errorsmod.Wrapf(ibcerrors.ErrInvalidRequest, "cannot transfer multiple coins with ics20-1") + if appVersion == types.V1 { + // ics20-1 only supports a single coin, so if that is the current version, we must only process a single coin. + if len(coins) > 1 { + return 0, errorsmod.Wrapf(ibcerrors.ErrInvalidRequest, "cannot transfer multiple coins with %s", types.V1) + } + + // ics20-1 does not support forwarding, so if that is the current version, we must reject the transfer. + if len(hops) > 0 { + return 0, errorsmod.Wrapf(ibcerrors.ErrInvalidRequest, "cannot forward coins with %s", types.V1) + } } destinationPort := channel.Counterparty.PortId @@ -107,16 +116,9 @@ func (k Keeper) sendTransfer( // chain inside the packet data. The receiving chain will perform denom // prefixing as necessary. - if token.Denom.SenderChainIsSource(sourcePort, sourceChannel) { - labels = append(labels, telemetry.NewLabel(coretypes.LabelSource, "true")) - - // obtain the escrow address for the source channel end - escrowAddress := types.GetEscrowAddress(sourcePort, sourceChannel) - if err := k.escrowCoin(ctx, sender, escrowAddress, coin); err != nil { - return 0, err - } - - } else { + // if the denom is prefixed by the port and channel on which we are sending + // the token, then we must be returning the token back to the chain they originated from + if token.Denom.HasPrefix(sourcePort, sourceChannel) { labels = append(labels, telemetry.NewLabel(coretypes.LabelSource, "false")) // transfer the coins to the module account and burn them @@ -134,19 +136,30 @@ func (k Keeper) sendTransfer( // to burn. panic(fmt.Errorf("cannot burn coins after a successful send to a module account: %v", err)) } + } else { + labels = append(labels, telemetry.NewLabel(coretypes.LabelSource, "true")) + + // obtain the escrow address for the source channel end + escrowAddress := types.GetEscrowAddress(sourcePort, sourceChannel) + if err := k.escrowCoin(ctx, sender, escrowAddress, coin); err != nil { + return 0, err + } } tokens = append(tokens, token) } - packetDataBytes := createPacketDataBytesFromVersion(appVersion, sender.String(), receiver, memo, tokens) + packetDataBytes, err := createPacketDataBytesFromVersion(appVersion, sender.String(), receiver, memo, tokens, hops) + if err != nil { + return 0, err + } sequence, err := k.ics4Wrapper.SendPacket(ctx, channelCap, sourcePort, sourceChannel, timeoutHeight, timeoutTimestamp, packetDataBytes) if err != nil { return 0, err } - events.EmitTransferEvent(ctx, sender.String(), receiver, tokens, memo) + events.EmitTransferEvent(ctx, sender.String(), receiver, tokens, memo, hops) defer internaltelemetry.ReportTransferTelemetry(tokens, labels) @@ -168,12 +181,12 @@ func (k Keeper) OnRecvPacket(ctx sdk.Context, packet channeltypes.Packet, data t return types.ErrReceiveDisabled } - // decode the receiver address - receiver, err := sdk.AccAddressFromBech32(data.Receiver) + receiver, err := k.getReceiverFromPacketData(data) if err != nil { - return errorsmod.Wrapf(err, "failed to decode receiver address: %s", data.Receiver) + return err } + receivedCoins := make(sdk.Coins, 0, len(data.Tokens)) for _, token := range data.Tokens { labels := []metrics.Label{ telemetry.NewLabel(coretypes.LabelSourcePort, packet.GetSourcePort()), @@ -192,8 +205,8 @@ func (k Keeper) OnRecvPacket(ctx sdk.Context, packet channeltypes.Packet, data t // // 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. - if token.Denom.ReceiverChainIsSource(packet.GetSourcePort(), packet.GetSourceChannel()) { + // receiving this token. + if token.Denom.HasPrefix(packet.GetSourcePort(), packet.GetSourceChannel()) { // sender chain is not the source, unescrow tokens // remove prefix added by sender chain @@ -201,7 +214,7 @@ func (k Keeper) OnRecvPacket(ctx sdk.Context, packet channeltypes.Packet, data t coin := sdk.NewCoin(token.Denom.IBCDenom(), transferAmount) - if k.bankKeeper.BlockedAddr(receiver) { + if k.isBlockedAddr(receiver) { return errorsmod.Wrapf(ibcerrors.ErrUnauthorized, "%s is not allowed to receive funds", receiver) } @@ -214,72 +227,115 @@ func (k Keeper) OnRecvPacket(ctx sdk.Context, packet channeltypes.Packet, data t labels = append(labels, telemetry.NewLabel(coretypes.LabelSource, "true")) defer internaltelemetry.ReportOnRecvPacketTelemetry(transferAmount, denomPath, labels) - // Continue processing rest of tokens in packet data. - continue - } + // Appending token. The new denom has been computed + receivedCoins = append(receivedCoins, coin) + } else { + // sender chain is the source, mint vouchers - // sender chain is the source, mint vouchers + // since SendPacket did not prefix the denomination, we must add the destination port and channel to the trace + trace := []types.Trace{types.NewTrace(packet.DestinationPort, packet.DestinationChannel)} + token.Denom.Trace = append(trace, token.Denom.Trace...) - // since SendPacket did not prefix the denomination, we must add the destination port and channel to the trace - trace := []types.Trace{types.NewTrace(packet.DestinationPort, packet.DestinationChannel)} - token.Denom.Trace = append(trace, token.Denom.Trace...) + if !k.HasDenom(ctx, token.Denom.Hash()) { + k.SetDenom(ctx, token.Denom) + } - if !k.HasDenom(ctx, token.Denom.Hash()) { - k.SetDenom(ctx, token.Denom) - } + voucherDenom := token.Denom.IBCDenom() + if !k.bankKeeper.HasDenomMetaData(ctx, voucherDenom) { + k.setDenomMetadata(ctx, token.Denom) + } - voucherDenom := token.Denom.IBCDenom() - if !k.bankKeeper.HasDenomMetaData(ctx, voucherDenom) { - k.setDenomMetadata(ctx, token.Denom) - } + events.EmitDenomEvent(ctx, token) - events.EmitDenomEvent(ctx, token) + voucher := sdk.NewCoin(voucherDenom, transferAmount) - voucher := sdk.NewCoin(voucherDenom, transferAmount) + // mint new tokens if the source of the transfer is the same chain + if err := k.bankKeeper.MintCoins( + ctx, types.ModuleName, sdk.NewCoins(voucher), + ); err != nil { + return errorsmod.Wrap(err, "failed to mint IBC tokens") + } - // mint new tokens if the source of the transfer is the same chain - if err := k.bankKeeper.MintCoins( - ctx, types.ModuleName, sdk.NewCoins(voucher), - ); err != nil { - return errorsmod.Wrap(err, "failed to mint IBC tokens") - } + // send to receiver + if k.isBlockedAddr(receiver) { + return errorsmod.Wrapf(ibcerrors.ErrUnauthorized, "%s is not allowed to receive funds", receiver) + } + moduleAddr := k.authKeeper.GetModuleAddress(types.ModuleName) + if moduleAddr == nil { + return errorsmod.Wrapf(sdkerrors.ErrUnknownAddress, "module account %s does not exist", types.ModuleName) + } + if err := k.bankKeeper.SendCoins( + ctx, moduleAddr, receiver, sdk.NewCoins(voucher), + ); err != nil { + return errorsmod.Wrapf(err, "failed to send coins to receiver %s", receiver.String()) + } - // send to receiver - if err := k.bankKeeper.SendCoinsFromModuleToAccount( - ctx, types.ModuleName, receiver, sdk.NewCoins(voucher), - ); err != nil { - return errorsmod.Wrapf(err, "failed to send coins to receiver %s", receiver.String()) + denomPath := token.Denom.Path() + labels = append(labels, telemetry.NewLabel(coretypes.LabelSource, "false")) + defer internaltelemetry.ReportOnRecvPacketTelemetry(transferAmount, denomPath, labels) + + receivedCoins = append(receivedCoins, voucher) } + } - denomPath := token.Denom.Path() - labels = append(labels, telemetry.NewLabel(coretypes.LabelSource, "false")) - defer internaltelemetry.ReportOnRecvPacketTelemetry(transferAmount, denomPath, labels) + if data.HasForwarding() { + // we are now sending from the forward escrow address to the final receiver address. + if err := k.forwardPacket(ctx, data, packet, receivedCoins); err != nil { + return err + } } + // The ibc_module.go module will return the proper ack. return nil } -// OnAcknowledgementPacket responds to 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 refundPacketTokens function. +// OnAcknowledgementPacket either reverts the state changes executed in receive +// and send packet if the chain acted as a middle hop on a multihop transfer; or +// responds to the success or failure of a packet acknowledgement written on the +// final receiving chain, if it acted as the original sender 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.FungibleTokenPacketDataV2, ack channeltypes.Acknowledgement) error { + prevPacket, isForwarded := k.getForwardedPacket(ctx, packet.SourcePort, packet.SourceChannel, packet.Sequence) + switch ack.Response.(type) { case *channeltypes.Acknowledgement_Result: + if isForwarded { + return k.ackForwardPacketSuccess(ctx, prevPacket, packet) + } + // the acknowledgement succeeded on the receiving chain so nothing // needs to be executed and no error needs to be returned return nil case *channeltypes.Acknowledgement_Error: - return k.refundPacketTokens(ctx, packet, data) + // We refund the tokens from the escrow address to the sender + if err := k.refundPacketTokens(ctx, packet, data); err != nil { + return err + } + if isForwarded { + return k.ackForwardPacketError(ctx, prevPacket, packet, data) + } + + return nil default: return errorsmod.Wrapf(ibcerrors.ErrInvalidType, "expected one of [%T, %T], got %T", channeltypes.Acknowledgement_Result{}, channeltypes.Acknowledgement_Error{}, ack.Response) } } -// OnTimeoutPacket refunds the sender since the original packet sent was -// never received and has been timed out. +// OnTimeoutPacket either reverts the state changes executed in receive and send +// packet if the chain acted as a middle hop on a multihop transfer; or refunds +// the sender if the original packet sent was never received and has been timed out. func (k Keeper) OnTimeoutPacket(ctx sdk.Context, packet channeltypes.Packet, data types.FungibleTokenPacketDataV2) error { - return k.refundPacketTokens(ctx, packet, data) + if err := k.refundPacketTokens(ctx, packet, data); err != nil { + return err + } + + prevPacket, isForwarded := k.getForwardedPacket(ctx, packet.SourcePort, packet.SourceChannel, packet.Sequence) + if isForwarded { + return k.ackForwardPacketTimeout(ctx, prevPacket, packet, data) + } + + return nil } // refundPacketTokens will unescrow and send back the tokens back to sender @@ -289,39 +345,40 @@ func (k Keeper) OnTimeoutPacket(ctx sdk.Context, packet channeltypes.Packet, dat func (k Keeper) refundPacketTokens(ctx sdk.Context, packet channeltypes.Packet, data types.FungibleTokenPacketDataV2) error { // NOTE: packet data type already checked in handler.go + moduleAccountAddr := k.authKeeper.GetModuleAddress(types.ModuleName) for _, token := range data.Tokens { - transferAmount, ok := sdkmath.NewIntFromString(token.Amount) - if !ok { - return errorsmod.Wrapf(types.ErrInvalidAmount, "unable to parse transfer amount (%s) into math.Int", transferAmount) + coin, err := token.ToCoin() + if err != nil { + return err } - coin := sdk.NewCoin(token.Denom.IBCDenom(), transferAmount) - sender, err := sdk.AccAddressFromBech32(data.Sender) if err != nil { return err } - if token.Denom.SenderChainIsSource(packet.GetSourcePort(), packet.GetSourceChannel()) { + // if the token we must refund is prefixed by the source port and channel + // then the tokens were burnt when the packet was sent and we must mint new tokens + if token.Denom.HasPrefix(packet.GetSourcePort(), packet.GetSourceChannel()) { + // mint vouchers back to sender + if err := k.bankKeeper.MintCoins( + ctx, types.ModuleName, sdk.NewCoins(coin), + ); err != nil { + return err + } + + if k.isBlockedAddr(sender) { + return errorsmod.Wrapf(ibcerrors.ErrUnauthorized, "%s is not allowed to send funds", sender) + } + if err := k.bankKeeper.SendCoins(ctx, moduleAccountAddr, sender, sdk.NewCoins(coin)); err != nil { + panic(fmt.Errorf("unable to send coins from module to account despite previously minting coins to module account: %v", err)) + } + } else { // unescrow tokens back to sender escrowAddress := types.GetEscrowAddress(packet.GetSourcePort(), packet.GetSourceChannel()) if err := k.unescrowCoin(ctx, escrowAddress, sender, coin); err != nil { return err } - - // Continue processing rest of tokens in packet data. - continue - } - - // mint vouchers back to sender - if err := k.bankKeeper.MintCoins( - ctx, types.ModuleName, sdk.NewCoins(coin), - ); err != nil { - return err - } - - if err := k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, sender, sdk.NewCoins(coin)); err != nil { - panic(fmt.Errorf("unable to send coins from module to account despite previously minting coins to module account: %v", err)) } } @@ -393,8 +450,7 @@ func (k Keeper) tokenFromCoin(ctx sdk.Context, coin sdk.Coin) (types.Token, erro } // createPacketDataBytesFromVersion creates the packet data bytes to be sent based on the application version. -func createPacketDataBytesFromVersion(appVersion, sender, receiver, memo string, tokens types.Tokens) []byte { - var packetDataBytes []byte +func createPacketDataBytesFromVersion(appVersion, sender, receiver, memo string, tokens types.Tokens, hops []types.Hop) ([]byte, error) { switch appVersion { case types.V1: // Sanity check, tokens must always be of length 1 if using app version V1. @@ -404,13 +460,28 @@ func createPacketDataBytesFromVersion(appVersion, sender, receiver, memo string, token := tokens[0] packetData := types.NewFungibleTokenPacketData(token.Denom.Path(), token.Amount, sender, receiver, memo) - packetDataBytes = packetData.GetBytes() + + if err := packetData.ValidateBasic(); err != nil { + return nil, errorsmod.Wrapf(err, "failed to validate %s packet data", types.V1) + } + + return packetData.GetBytes(), nil case types.V2: - packetData := types.NewFungibleTokenPacketDataV2(tokens, sender, receiver, memo) - packetDataBytes = packetData.GetBytes() + // If forwarding is needed, move memo to forwarding packet data and set packet.Memo to empty string. + var forwardingPacketData types.ForwardingPacketData + if len(hops) > 0 { + forwardingPacketData = types.NewForwardingPacketData(memo, hops...) + memo = "" + } + + packetData := types.NewFungibleTokenPacketDataV2(tokens, sender, receiver, memo, forwardingPacketData) + + if err := packetData.ValidateBasic(); err != nil { + return nil, errorsmod.Wrapf(err, "failed to validate %s packet data", types.V2) + } + + return packetData.GetBytes(), nil default: panic(fmt.Errorf("app version must be one of %s", types.SupportedVersions)) } - - return packetDataBytes } diff --git a/modules/apps/transfer/keeper/relay_forwarding_test.go b/modules/apps/transfer/keeper/relay_forwarding_test.go new file mode 100644 index 00000000000..05f18aadee5 --- /dev/null +++ b/modules/apps/transfer/keeper/relay_forwarding_test.go @@ -0,0 +1,1141 @@ +package keeper_test + +import ( + "fmt" + "time" + + sdkmath "cosmossdk.io/math" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/cosmos/ibc-go/v8/modules/apps/transfer/types" + clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" + channeltypes "github.com/cosmos/ibc-go/v8/modules/core/04-channel/types" + ibctesting "github.com/cosmos/ibc-go/v8/testing" +) + +func (suite *KeeperTestSuite) setupForwardingPaths() (pathAtoB, pathBtoC *ibctesting.Path) { + pathAtoB = ibctesting.NewTransferPath(suite.chainA, suite.chainB) + pathBtoC = ibctesting.NewTransferPath(suite.chainB, suite.chainC) + pathAtoB.Setup() + pathBtoC.Setup() + return pathAtoB, pathBtoC +} + +type amountType int + +const ( + escrow amountType = iota + balance +) + +func (suite *KeeperTestSuite) assertAmountOnChain(chain *ibctesting.TestChain, balanceType amountType, amount sdkmath.Int, denom string) { + var total sdk.Coin + switch balanceType { + case escrow: + total = chain.GetSimApp().TransferKeeper.GetTotalEscrowForDenom(chain.GetContext(), denom) + case balance: + total = chain.GetSimApp().BankKeeper.GetBalance(chain.GetContext(), chain.SenderAccounts[0].SenderAccount.GetAddress(), denom) + default: + suite.Fail("invalid amountType %s", balanceType) + } + suite.Require().Equal(amount, total.Amount, fmt.Sprintf("Chain %s: got balance of %d, wanted %d", chain.Name(), total.Amount, amount)) +} + +// TestStoredForwardedPacketAndEscrowAfterFirstHop tests that the forwarded packet +// from chain A to chain B is stored after when the packet is received on chain B +// and then forwarded to chain C, and checks the balance of the escrow accounts +// in chain A nad B. +func (suite *KeeperTestSuite) TestStoredForwardedPacketAndEscrowAfterFirstHop() { + /* + Given the following topology: + chain A (channel 0) -> (channel-0) chain B (channel-1) -> (channel-0) chain A + stake transfer/channel-0/stake transfer/channel-0/transfer/channel-0/stake + We want to trigger: + 1. A sends B over channel-0. + 2. Receive on B. + At this point we want to assert: + A: escrowA = amount,stake AND packet A to B is stored in forwarded packet + B: escrowB = amount,transfer/channel-0/stake + */ + + amount := sdkmath.NewInt(100) + pathAtoB, pathBtoC := suite.setupForwardingPaths() + + coin := ibctesting.TestCoin + sender := suite.chainA.SenderAccounts[0].SenderAccount + receiver := suite.chainC.SenderAccounts[0].SenderAccount + forwarding := types.NewForwarding(false, types.NewHop( + pathBtoC.EndpointA.ChannelConfig.PortID, + pathBtoC.EndpointA.ChannelID, + )) + + transferMsg := types.NewMsgTransfer( + pathAtoB.EndpointA.ChannelConfig.PortID, + pathAtoB.EndpointA.ChannelID, + sdk.NewCoins(coin), + sender.GetAddress().String(), + receiver.GetAddress().String(), + clienttypes.ZeroHeight(), + suite.chainA.GetTimeoutTimestamp(), "", + forwarding, + ) + result, err := suite.chainA.SendMsgs(transferMsg) + suite.Require().NoError(err) // message committed + + // parse the packet from result events and recv packet on chainB + packet, err := ibctesting.ParsePacketFromEvents(result.Events) + suite.Require().NoError(err) + suite.Require().NotNil(packet) + + err = pathAtoB.EndpointB.UpdateClient() + suite.Require().NoError(err) + + result, err = pathAtoB.EndpointB.RecvPacketWithResult(packet) + suite.Require().NoError(err) + suite.Require().NotNil(result) + + forwardedPacket, found := suite.chainB.GetSimApp().TransferKeeper.GetForwardedPacket(suite.chainB.GetContext(), pathBtoC.EndpointA.ChannelConfig.PortID, pathBtoC.EndpointA.ChannelID, packet.Sequence) + suite.Require().True(found) + suite.Require().Equal(packet, forwardedPacket) + + suite.assertAmountOnChain(suite.chainA, escrow, amount, sdk.DefaultBondDenom) + + // denom path: transfer/channel-0 + denom := types.NewDenom(sdk.DefaultBondDenom, types.NewTrace(pathAtoB.EndpointB.ChannelConfig.PortID, pathAtoB.EndpointB.ChannelID)) + suite.assertAmountOnChain(suite.chainB, escrow, amount, denom.IBCDenom()) +} + +// TestSuccessfulForward tests a successful transfer from A to C through B. +func (suite *KeeperTestSuite) TestSuccessfulForward() { + /* + Given the following topology: + chain A (channel 0) -> (channel-0) chain B (channel-1) -> (channel-0) chain C + stake transfer/channel-0/stake transfer/channel-0/transfer/channel-0/stake + We want to trigger: + 1. A sends B over channel-0. + 2. Receive on B. + 2.1 B sends C over channel-1 + 3. Receive on C. + At this point we want to assert: + A: escrowA = amount,stake + B: escrowB = amount,transfer/channel-0/denom + C: finalReceiver = amount,transfer/channel-0/transfer/channel-0/denom + */ + + amount := sdkmath.NewInt(100) + + pathAtoB, pathBtoC := suite.setupForwardingPaths() + + coinOnA := ibctesting.TestCoin + sender := suite.chainA.SenderAccounts[0].SenderAccount + receiver := suite.chainC.SenderAccounts[0].SenderAccount + forwarding := types.NewForwarding(false, types.NewHop( + pathBtoC.EndpointA.ChannelConfig.PortID, + pathBtoC.EndpointA.ChannelID, + )) + + transferMsg := types.NewMsgTransfer( + pathAtoB.EndpointA.ChannelConfig.PortID, + pathAtoB.EndpointA.ChannelID, + sdk.NewCoins(coinOnA), + sender.GetAddress().String(), + receiver.GetAddress().String(), + clienttypes.ZeroHeight(), + suite.chainA.GetTimeoutTimestamp(), "", + forwarding, + ) + + result, err := suite.chainA.SendMsgs(transferMsg) + suite.Require().NoError(err) // message committed + + // parse the packet from result events and recv packet on chainB + packetFromAtoB, err := ibctesting.ParsePacketFromEvents(result.Events) + suite.Require().NoError(err) + suite.Require().NotNil(packetFromAtoB) + + err = pathAtoB.EndpointB.UpdateClient() + suite.Require().NoError(err) + + result, err = pathAtoB.EndpointB.RecvPacketWithResult(packetFromAtoB) + suite.Require().NoError(err) + suite.Require().NotNil(result) + + // Check that Escrow A has amount + suite.assertAmountOnChain(suite.chainA, escrow, amount, sdk.DefaultBondDenom) + + // denom path: transfer/channel-0 + denom := types.NewDenom(sdk.DefaultBondDenom, types.NewTrace(pathAtoB.EndpointB.ChannelConfig.PortID, pathAtoB.EndpointB.ChannelID)) + suite.assertAmountOnChain(suite.chainB, escrow, amount, denom.IBCDenom()) + + packetFromBtoC, err := ibctesting.ParsePacketFromEvents(result.Events) + suite.Require().NoError(err) + suite.Require().NotNil(packetFromBtoC) + + err = pathBtoC.EndpointA.UpdateClient() + suite.Require().NoError(err) + + err = pathBtoC.EndpointB.UpdateClient() + suite.Require().NoError(err) + + // B should have stored the forwarded packet. + _, found := suite.chainB.GetSimApp().TransferKeeper.GetForwardedPacket(suite.chainB.GetContext(), packetFromBtoC.SourcePort, packetFromBtoC.SourceChannel, packetFromBtoC.Sequence) + suite.Require().True(found, "Chain B should have stored the forwarded packet") + + result, err = pathBtoC.EndpointB.RecvPacketWithResult(packetFromBtoC) + suite.Require().NoError(err) + suite.Require().NotNil(result) + + // transfer/channel-0/transfer/channel-0/denom + // Check that the final receiver has received the expected tokens. + denomABC := types.NewDenom(sdk.DefaultBondDenom, types.NewTrace(pathBtoC.EndpointB.ChannelConfig.PortID, pathBtoC.EndpointB.ChannelID), types.NewTrace(pathAtoB.EndpointB.ChannelConfig.PortID, pathAtoB.EndpointB.ChannelID)) + // Check that the final receiver has received the expected tokens. + suite.assertAmountOnChain(suite.chainC, balance, amount, denomABC.IBCDenom()) + + successAck := channeltypes.NewResultAcknowledgement([]byte{byte(1)}) + successAckBz := channeltypes.CommitAcknowledgement(successAck.Acknowledgement()) + ackOnC := suite.chainC.GetAcknowledgement(packetFromBtoC) + suite.Require().Equal(successAckBz, ackOnC) + + // Ack back to B + err = pathBtoC.EndpointB.UpdateClient() + suite.Require().NoError(err) + + err = pathBtoC.EndpointA.AcknowledgePacket(packetFromBtoC, successAck.Acknowledgement()) + suite.Require().NoError(err) + + ackOnB := suite.chainB.GetAcknowledgement(packetFromAtoB) + suite.Require().Equal(successAckBz, ackOnB) + + // B should now have deleted the forwarded packet. + _, found = suite.chainB.GetSimApp().TransferKeeper.GetForwardedPacket(suite.chainB.GetContext(), packetFromBtoC.SourcePort, packetFromBtoC.SourceChannel, packetFromBtoC.Sequence) + suite.Require().False(found, "Chain B should have deleted the forwarded packet") + + // Ack back to A + err = pathAtoB.EndpointA.UpdateClient() + suite.Require().NoError(err) + + err = pathAtoB.EndpointA.AcknowledgePacket(packetFromAtoB, successAck.Acknowledgement()) + suite.Require().NoError(err) +} + +// TestSuccessfulForwardWithMemo tests a successful transfer from A to C through B with a memo that should arrive at C. +func (suite *KeeperTestSuite) TestSuccessfulForwardWithMemo() { + /* + Given the following topology: + chain A (channel 0) -> (channel-0) chain B (channel-1) -> (channel-0) chain C + stake transfer/channel-0/stake transfer/channel-0/transfer/channel-0/stake + We want to trigger: + 1. A sends B over channel-0. + 2. Receive on B. + 2.1 B sends C over channel-1 + 3. Receive on C. + At this point we want to assert: + A: escrowA = amount,stake + B: escrowB = amount,transfer/channel-0/denom + C: finalReceiver = amount,transfer/channel-0/transfer/channel-0/denom,memo + */ + + amount := sdkmath.NewInt(100) + testMemo := "test forwarding memo" + + pathAtoB, pathBtoC := suite.setupForwardingPaths() + + coinOnA := ibctesting.TestCoin + sender := suite.chainA.SenderAccounts[0].SenderAccount + receiver := suite.chainC.SenderAccounts[0].SenderAccount + forwarding := types.NewForwarding(false, types.NewHop( + pathBtoC.EndpointA.ChannelConfig.PortID, + pathBtoC.EndpointA.ChannelID, + )) + + transferMsg := types.NewMsgTransfer( + pathAtoB.EndpointA.ChannelConfig.PortID, + pathAtoB.EndpointA.ChannelID, + sdk.NewCoins(coinOnA), + sender.GetAddress().String(), + receiver.GetAddress().String(), + clienttypes.ZeroHeight(), + suite.chainA.GetTimeoutTimestamp(), + testMemo, + forwarding, + ) + + result, err := suite.chainA.SendMsgs(transferMsg) + suite.Require().NoError(err) // message committed + + // parse the packet from result events and recv packet on chainB + packetFromAtoB, err := ibctesting.ParsePacketFromEvents(result.Events) + suite.Require().NoError(err) + suite.Require().NotNil(packetFromAtoB) + + err = pathAtoB.EndpointB.UpdateClient() + suite.Require().NoError(err) + + // Check that the memo is stored correctly in the packet sent from A + var tokenPacketOnA types.FungibleTokenPacketDataV2 + err = suite.chainA.Codec.UnmarshalJSON(packetFromAtoB.Data, &tokenPacketOnA) + suite.Require().NoError(err) + suite.Require().Equal("", tokenPacketOnA.Memo) + suite.Require().Equal(testMemo, tokenPacketOnA.Forwarding.DestinationMemo) + + result, err = pathAtoB.EndpointB.RecvPacketWithResult(packetFromAtoB) + suite.Require().NoError(err) + suite.Require().NotNil(result) + + // Check that Escrow A has amount + suite.assertAmountOnChain(suite.chainA, escrow, amount, sdk.DefaultBondDenom) + + // denom path: transfer/channel-0 + denom := types.NewDenom(sdk.DefaultBondDenom, types.NewTrace(pathAtoB.EndpointB.ChannelConfig.PortID, pathAtoB.EndpointB.ChannelID)) + suite.assertAmountOnChain(suite.chainB, escrow, amount, denom.IBCDenom()) + + packetFromBtoC, err := ibctesting.ParsePacketFromEvents(result.Events) + suite.Require().NoError(err) + suite.Require().NotNil(packetFromBtoC) + + // Check that the memo is stored correctly in the packet sent from B + var tokenPacketOnB types.FungibleTokenPacketDataV2 + err = suite.chainB.Codec.UnmarshalJSON(packetFromBtoC.Data, &tokenPacketOnB) + suite.Require().NoError(err) + suite.Require().Equal(testMemo, tokenPacketOnB.Memo) + suite.Require().Equal("", tokenPacketOnB.Forwarding.DestinationMemo) + + err = pathBtoC.EndpointA.UpdateClient() + suite.Require().NoError(err) + + err = pathBtoC.EndpointB.UpdateClient() + suite.Require().NoError(err) + + // B should have stored the forwarded packet. + _, found := suite.chainB.GetSimApp().TransferKeeper.GetForwardedPacket(suite.chainB.GetContext(), packetFromBtoC.SourcePort, packetFromBtoC.SourceChannel, packetFromBtoC.Sequence) + suite.Require().True(found, "Chain B should have stored the forwarded packet") + + result, err = pathBtoC.EndpointB.RecvPacketWithResult(packetFromBtoC) + suite.Require().NoError(err) + suite.Require().NotNil(result) + + packetOnC, err := ibctesting.ParseRecvPacketFromEvents(result.Events) + suite.Require().NoError(err) + suite.Require().NotNil(packetOnC) + + // Check that the memo is stored directly in the memo field on C + var tokenPacketOnC types.FungibleTokenPacketDataV2 + err = suite.chainC.Codec.UnmarshalJSON(packetOnC.Data, &tokenPacketOnC) + suite.Require().NoError(err) + suite.Require().Equal("", tokenPacketOnC.Forwarding.DestinationMemo) + suite.Require().Equal(testMemo, tokenPacketOnC.Memo) + + // transfer/channel-0/transfer/channel-0/denom + // Check that the final receiver has received the expected tokens. + denomABC := types.NewDenom(sdk.DefaultBondDenom, types.NewTrace(pathBtoC.EndpointB.ChannelConfig.PortID, pathBtoC.EndpointB.ChannelID), types.NewTrace(pathAtoB.EndpointB.ChannelConfig.PortID, pathAtoB.EndpointB.ChannelID)) + // Check that the final receiver has received the expected tokens. + suite.assertAmountOnChain(suite.chainC, balance, amount, denomABC.IBCDenom()) + + successAck := channeltypes.NewResultAcknowledgement([]byte{byte(1)}) + successAckBz := channeltypes.CommitAcknowledgement(successAck.Acknowledgement()) + ackOnC := suite.chainC.GetAcknowledgement(packetFromBtoC) + suite.Require().Equal(successAckBz, ackOnC) + + // Ack back to B + err = pathBtoC.EndpointB.UpdateClient() + suite.Require().NoError(err) + + err = pathBtoC.EndpointA.AcknowledgePacket(packetFromBtoC, successAck.Acknowledgement()) + suite.Require().NoError(err) + + ackOnB := suite.chainB.GetAcknowledgement(packetFromAtoB) + suite.Require().Equal(successAckBz, ackOnB) + + // B should now have deleted the forwarded packet. + _, found = suite.chainB.GetSimApp().TransferKeeper.GetForwardedPacket(suite.chainB.GetContext(), packetFromBtoC.SourcePort, packetFromBtoC.SourceChannel, packetFromBtoC.Sequence) + suite.Require().False(found, "Chain B should have deleted the forwarded packet") + + // Ack back to A + err = pathAtoB.EndpointA.UpdateClient() + suite.Require().NoError(err) + + err = pathAtoB.EndpointA.AcknowledgePacket(packetFromAtoB, successAck.Acknowledgement()) + suite.Require().NoError(err) +} + +// TestSuccessfulForwardWithNonCosmosAccAddress tests that a packet is successfully forwarded with a non-Cosmos account address. +// The test stops before verifying the final receive, because we don't have a non-cosmos chain to test with. +func (suite *KeeperTestSuite) TestSuccessfulForwardWithNonCosmosAccAddress() { + /* + Given the following topology: + chain A (channel 0) -> (channel-0) chain B (channel-1) -> (channel-0) chain C + stake transfer/channel-0/stake transfer/channel-0/transfer/channel-0/stake + We want to trigger: + 1. A sends B over channel-0. + 2. Receive on B. + 2.1 B sends C over channel-1 + 3. Receive on C. + At this point we want to assert: + A: packet gets relayed properly with final receiver intact + B: packet arrives and is forwarded with final receiver intact + C: no assertions as we don't have a non-cosmos chain to test with, so this would fail here + */ + + amount := sdkmath.NewInt(100) + + pathAtoB, pathBtoC := suite.setupForwardingPaths() + + sender := suite.chainA.SenderAccounts[0].SenderAccount + nonCosmosReceiver := "0x42069163Ac5919fD49e6A67e6c211E0C86397fa2" + forwarding := types.NewForwarding(false, types.NewHop( + pathBtoC.EndpointA.ChannelConfig.PortID, + pathBtoC.EndpointA.ChannelID, + )) + + transferMsg := types.NewMsgTransfer( + pathAtoB.EndpointA.ChannelConfig.PortID, + pathAtoB.EndpointA.ChannelID, + sdk.NewCoins(ibctesting.TestCoin), + sender.GetAddress().String(), + nonCosmosReceiver, + clienttypes.ZeroHeight(), + suite.chainA.GetTimeoutTimestamp(), "", + forwarding, + ) + + result, err := suite.chainA.SendMsgs(transferMsg) + suite.Require().NoError(err) // message committed + + // parse the packet from result events and recv packet on chainB + packetFromAtoB, err := ibctesting.ParsePacketFromEvents(result.Events) + suite.Require().NoError(err) + suite.Require().NotNil(packetFromAtoB) + + // Check that the token sent from A has final receiver intact + var tokenPacketOnA types.FungibleTokenPacketDataV2 + err = suite.chainA.Codec.UnmarshalJSON(packetFromAtoB.Data, &tokenPacketOnA) + suite.Require().NoError(err) + suite.Require().Equal(nonCosmosReceiver, tokenPacketOnA.Receiver) + + err = pathAtoB.EndpointB.UpdateClient() + suite.Require().NoError(err) + + result, err = pathAtoB.EndpointB.RecvPacketWithResult(packetFromAtoB) + suite.Require().NoError(err) + suite.Require().NotNil(result) + + // Check that Escrow A has amount + suite.assertAmountOnChain(suite.chainA, escrow, amount, sdk.DefaultBondDenom) + + packetFromBtoC, err := ibctesting.ParsePacketFromEvents(result.Events) + suite.Require().NoError(err) + suite.Require().NotNil(packetFromBtoC) + + // Check that the token sent from B has final receiver intact + var tokenPacketOnB types.FungibleTokenPacketDataV2 + err = suite.chainB.Codec.UnmarshalJSON(packetFromBtoC.Data, &tokenPacketOnB) + suite.Require().NoError(err) + suite.Require().Equal(nonCosmosReceiver, tokenPacketOnB.Receiver) + + // B should have stored the forwarded packet. + _, found := suite.chainB.GetSimApp().TransferKeeper.GetForwardedPacket(suite.chainB.GetContext(), packetFromBtoC.SourcePort, packetFromBtoC.SourceChannel, packetFromBtoC.Sequence) + suite.Require().True(found, "Chain B should have stored the forwarded packet") +} + +// TestSuccessfulUnwind tests unwinding of tokens sent from A -> B -> C by +// forwarding the tokens back from C -> B -> A. +func (suite *KeeperTestSuite) TestSuccessfulUnwind() { + /* + Given the following topolgy: + chain A (channel 0) -> (channel-0) chain B (channel-1) -> (channel-0) chain C + stake transfer/channel-0/stake transfer/channel-0/transfer/channel-0/stake + We want to trigger: + 1. Send vouchers from C to B. + 2. Receive on B. + 2.1 B sends B over channel-0 + 3. Receive on A. + At this point we want to assert: + - escrow on B and C is zero + - receiver on A has amount,stake + */ + + amount := sdkmath.NewInt(100) + + pathAtoB, pathBtoC := suite.setupForwardingPaths() + + sender := suite.chainC.SenderAccount + receiver := suite.chainA.SenderAccount + + // set sender and escrow accounts with the right balances to set up an initial state + // that should have been the same as sending token from A -> B -> C + denomA := types.NewDenom(sdk.DefaultBondDenom) + denomAB := types.NewDenom(sdk.DefaultBondDenom, types.NewTrace(pathAtoB.EndpointB.ChannelConfig.PortID, pathAtoB.EndpointB.ChannelID)) + denomABC := types.NewDenom(sdk.DefaultBondDenom, append([]types.Trace{types.NewTrace(pathBtoC.EndpointB.ChannelConfig.PortID, pathBtoC.EndpointB.ChannelID)}, denomAB.Trace...)...) + + coinOnA := sdk.NewCoin(denomA.IBCDenom(), amount) + err := suite.chainA.GetSimApp().BankKeeper.MintCoins(suite.chainA.GetContext(), types.ModuleName, sdk.NewCoins(coinOnA)) + suite.Require().NoError(err) + escrowAddressAtoB := types.GetEscrowAddress(pathAtoB.EndpointA.ChannelConfig.PortID, pathAtoB.EndpointA.ChannelID) + err = suite.chainA.GetSimApp().BankKeeper.MintCoins(suite.chainA.GetContext(), types.ModuleName, sdk.NewCoins(coinOnA)) + suite.Require().NoError(err) + err = suite.chainA.GetSimApp().BankKeeper.SendCoinsFromModuleToAccount(suite.chainA.GetContext(), types.ModuleName, escrowAddressAtoB, sdk.NewCoins(coinOnA)) + suite.Require().NoError(err) + suite.chainA.GetSimApp().TransferKeeper.SetTotalEscrowForDenom(suite.chainA.GetContext(), coinOnA) + + coinOnB := sdk.NewCoin(denomAB.IBCDenom(), amount) + err = suite.chainB.GetSimApp().BankKeeper.MintCoins(suite.chainB.GetContext(), types.ModuleName, sdk.NewCoins(coinOnB)) + suite.Require().NoError(err) + escrowAddressBtoC := types.GetEscrowAddress(pathBtoC.EndpointA.ChannelConfig.PortID, pathBtoC.EndpointA.ChannelID) + err = suite.chainB.GetSimApp().BankKeeper.MintCoins(suite.chainB.GetContext(), types.ModuleName, sdk.NewCoins(coinOnB)) + suite.Require().NoError(err) + err = suite.chainB.GetSimApp().BankKeeper.SendCoinsFromModuleToAccount(suite.chainB.GetContext(), types.ModuleName, escrowAddressBtoC, sdk.NewCoins(coinOnB)) + suite.Require().NoError(err) + suite.chainB.GetSimApp().TransferKeeper.SetTotalEscrowForDenom(suite.chainB.GetContext(), coinOnB) + suite.chainB.GetSimApp().TransferKeeper.SetDenom(suite.chainB.GetContext(), denomAB) + + coinOnC := sdk.NewCoin(denomABC.IBCDenom(), amount) + err = suite.chainC.GetSimApp().BankKeeper.MintCoins(suite.chainC.GetContext(), types.ModuleName, sdk.NewCoins(coinOnC)) + suite.Require().NoError(err) + err = suite.chainC.GetSimApp().BankKeeper.SendCoinsFromModuleToAccount(suite.chainC.GetContext(), types.ModuleName, sender.GetAddress(), sdk.NewCoins(coinOnC)) + suite.Require().NoError(err) + suite.chainC.GetSimApp().TransferKeeper.SetDenom(suite.chainC.GetContext(), denomABC) + + originalABalance := suite.chainA.GetSimApp().BankKeeper.GetBalance(suite.chainA.GetContext(), receiver.GetAddress(), coinOnA.Denom) + + forwarding := types.NewForwarding(false, types.NewHop( + pathAtoB.EndpointB.ChannelConfig.PortID, + pathAtoB.EndpointB.ChannelID, + )) + + transferMsg := types.NewMsgTransfer( + pathBtoC.EndpointB.ChannelConfig.PortID, + pathBtoC.EndpointB.ChannelID, + sdk.NewCoins(coinOnC), + sender.GetAddress().String(), + receiver.GetAddress().String(), + clienttypes.ZeroHeight(), + suite.chainC.GetTimeoutTimestamp(), "", + forwarding, + ) + + result, err := suite.chainC.SendMsgs(transferMsg) + suite.Require().NoError(err) // message committed + + // Sender's balance for vouchers is 0 + suite.assertAmountOnChain(suite.chainC, balance, sdkmath.NewInt(0), denomABC.IBCDenom()) + + // parse the packet from result events and recv packet on chainB + packetFromCtoB, err := ibctesting.ParsePacketFromEvents(result.Events) + suite.Require().NoError(err) + suite.Require().NotNil(packetFromCtoB) + + err = pathBtoC.EndpointA.UpdateClient() + suite.Require().NoError(err) + + result, err = pathBtoC.EndpointA.RecvPacketWithResult(packetFromCtoB) + suite.Require().NoError(err) + suite.Require().NotNil(result) + + // Vouchers have been burned on chain B + suite.assertAmountOnChain(suite.chainB, escrow, sdkmath.NewInt(0), denomAB.IBCDenom()) + + // parse the packet from result events and recv packet on chainA + packetFromBtoA, err := ibctesting.ParsePacketFromEvents(result.Events) + suite.Require().NoError(err) + suite.Require().NotNil(packetFromBtoA) + + err = pathAtoB.EndpointA.UpdateClient() + suite.Require().NoError(err) + + // B should have stored the forwarded packet. + forwardedPacket, found := suite.chainB.GetSimApp().TransferKeeper.GetForwardedPacket(suite.chainB.GetContext(), packetFromBtoA.SourcePort, packetFromBtoA.SourceChannel, packetFromBtoA.Sequence) + suite.Require().True(found) + suite.Require().Equal(packetFromCtoB, forwardedPacket) + + result, err = pathAtoB.EndpointA.RecvPacketWithResult(packetFromBtoA) + suite.Require().NoError(err) + suite.Require().NotNil(result) + + successAck := channeltypes.NewResultAcknowledgement([]byte{byte(1)}) + successAckBz := channeltypes.CommitAcknowledgement(successAck.Acknowledgement()) + + // Ack back to B + err = pathAtoB.EndpointB.UpdateClient() + suite.Require().NoError(err) + + err = pathAtoB.EndpointB.AcknowledgePacket(packetFromBtoA, successAck.Acknowledgement()) + suite.Require().NoError(err) + + ackOnB := suite.chainB.GetAcknowledgement(packetFromCtoB) + suite.Require().Equal(successAckBz, ackOnB) + + // Ack back to C + err = pathBtoC.EndpointB.UpdateClient() + suite.Require().NoError(err) + + err = pathBtoC.EndpointB.AcknowledgePacket(packetFromCtoB, successAck.Acknowledgement()) + suite.Require().NoError(err) + + // Check that B deleted the forwarded packet. + _, found = suite.chainB.GetSimApp().TransferKeeper.GetForwardedPacket(suite.chainB.GetContext(), packetFromBtoA.SourcePort, packetFromBtoA.SourceChannel, packetFromBtoA.Sequence) + suite.Require().False(found, "chain B should have deleted the forwarded packet mapping") + + // Check that tokens have been unescrowed and receiver got the tokens + suite.assertAmountOnChain(suite.chainA, escrow, sdkmath.NewInt(0), denomA.IBCDenom()) + suite.assertAmountOnChain(suite.chainA, balance, originalABalance.Amount.Add(amount), denomA.IBCDenom()) +} + +// TestAcknowledgementFailureWithMiddleChainAsNativeTokenSource tests a failure in the last hop where the +// middle chain is native source when receiving and sending the packet. In other words, the middle chain's native +// token has been sent to chain C, and the multi-hop transfer from C -> B -> A has chain B being the source of +// the token both when receiving and forwarding (sending). +func (suite *KeeperTestSuite) TestAcknowledgementFailureWithMiddleChainAsNativeTokenSource() { + /* + Given the following topology: + chain A (channel 0) -> (channel-0) chain B (channel-1) -> (channel-0) chain C + stake transfer/channel-0/stake transfer/channel-0/transfer/channel-0/stake + We want to trigger: + 1. Transfer from B to C + 2. Single transfer forwarding token from C -> B -> A + 2.1 The ack fails on the last hop (chain A) + 2.2 Propagate the error back to C + 3. Verify all the balances are updated as expected + */ + + amount := sdkmath.NewInt(100) + + pathAtoB, pathBtoC := suite.setupForwardingPaths() + + coinOnB := ibctesting.TestCoin + setupSender := suite.chainB.SenderAccounts[0].SenderAccount + setupReceiver := suite.chainC.SenderAccounts[0].SenderAccount + + setupTransferMsg := types.NewMsgTransfer( + pathBtoC.EndpointA.ChannelConfig.PortID, + pathBtoC.EndpointA.ChannelID, + sdk.NewCoins(coinOnB), + setupSender.GetAddress().String(), + setupReceiver.GetAddress().String(), + suite.chainB.GetTimeoutHeight(), + 0, "", + types.Forwarding{}, + ) + + result, err := suite.chainB.SendMsgs(setupTransferMsg) + suite.Require().NoError(err) // message committed + + // parse the packet from result events and recv packet on chainC + packetFromBToC, err := ibctesting.ParsePacketFromEvents(result.Events) + suite.Require().NoError(err) + suite.Require().NotNil(packetFromBToC) + + err = pathBtoC.EndpointB.UpdateClient() + suite.Require().NoError(err) + + result, err = pathBtoC.EndpointB.RecvPacketWithResult(packetFromBToC) + suite.Require().NoError(err) + suite.Require().NotNil(result) + + // Check that EscrowBtoC has amount + escrowAddressBtoC := types.GetEscrowAddress(pathBtoC.EndpointA.ChannelConfig.PortID, pathBtoC.EndpointA.ChannelID) + escrowBalancBtoC := suite.chainB.GetSimApp().BankKeeper.GetBalance(suite.chainB.GetContext(), escrowAddressBtoC, coinOnB.GetDenom()) + suite.Require().Equal(amount, escrowBalancBtoC.Amount) + + // Check that receiver has the expected tokens + denomOnC := types.NewDenom(sdk.DefaultBondDenom, types.NewTrace(pathBtoC.EndpointB.ChannelConfig.PortID, pathBtoC.EndpointB.ChannelID)) + coinOnC := sdk.NewCoin(denomOnC.IBCDenom(), amount) + balanceOnC := suite.chainC.GetSimApp().BankKeeper.GetBalance(suite.chainC.GetContext(), setupReceiver.GetAddress(), coinOnC.GetDenom()) + suite.Require().Equal(amount, balanceOnC.Amount) + + // Now we start the transfer from C -> B -> A + sender := suite.chainC.SenderAccounts[0].SenderAccount + receiver := suite.chainA.SenderAccounts[0].SenderAccount + + forwarding := types.NewForwarding(false, types.NewHop( + pathAtoB.EndpointB.ChannelConfig.PortID, + pathAtoB.EndpointB.ChannelID, + )) + + forwardTransfer := types.NewMsgTransfer( + pathBtoC.EndpointB.ChannelConfig.PortID, + pathBtoC.EndpointB.ChannelID, + sdk.NewCoins(coinOnC), + sender.GetAddress().String(), + receiver.GetAddress().String(), + clienttypes.ZeroHeight(), + suite.chainA.GetTimeoutTimestamp(), + "", + forwarding, + ) + + result, err = suite.chainC.SendMsgs(forwardTransfer) + suite.Require().NoError(err) // message committed + + // Check that Escrow C has unescrowed the amount + totalEscrowChainC := suite.chainC.GetSimApp().TransferKeeper.GetTotalEscrowForDenom(suite.chainC.GetContext(), coinOnC.GetDenom()) + suite.Require().Equal(sdkmath.NewInt(0), totalEscrowChainC.Amount) + + // parse the packet from result events and recv packet on chainB + packetFromCtoB, err := ibctesting.ParsePacketFromEvents(result.Events) + suite.Require().NoError(err) + suite.Require().NotNil(packetFromCtoB) + + err = pathBtoC.EndpointA.UpdateClient() + suite.Require().NoError(err) + + result, err = pathBtoC.EndpointA.RecvPacketWithResult(packetFromCtoB) + suite.Require().NoError(err) + suite.Require().NotNil(result) + + // Check that escrow has been moved from EscrowBtoC to EscrowBtoA + escrowBalancBtoC = suite.chainB.GetSimApp().BankKeeper.GetBalance(suite.chainB.GetContext(), escrowAddressBtoC, coinOnB.GetDenom()) + suite.Require().Equal(sdkmath.NewInt(0), escrowBalancBtoC.Amount) + + escrowAddressBtoA := types.GetEscrowAddress(pathAtoB.EndpointB.ChannelConfig.PortID, pathAtoB.EndpointB.ChannelID) + escrowBalanceBtoA := suite.chainB.GetSimApp().BankKeeper.GetBalance(suite.chainB.GetContext(), escrowAddressBtoA, coinOnB.GetDenom()) + suite.Require().Equal(amount, escrowBalanceBtoA.Amount) + + forwardedPacket, found := suite.chainB.GetSimApp().TransferKeeper.GetForwardedPacket(suite.chainB.GetContext(), pathAtoB.EndpointB.ChannelConfig.PortID, pathAtoB.EndpointB.ChannelID, packetFromCtoB.Sequence) + suite.Require().True(found) + suite.Require().Equal(packetFromCtoB, forwardedPacket) + + // Now we can receive the packet on A where we want to trigger an error + packetFromBtoA, err := ibctesting.ParsePacketFromEvents(result.Events) + suite.Require().NoError(err) + suite.Require().NotNil(packetFromBtoA) + + // turn off receive on chain A to trigger an error + suite.chainA.GetSimApp().TransferKeeper.SetParams(suite.chainA.GetContext(), types.Params{ + SendEnabled: true, + ReceiveEnabled: false, + }) + + err = pathAtoB.EndpointA.UpdateClient() + suite.Require().NoError(err) + + result, err = pathAtoB.EndpointA.RecvPacketWithResult(packetFromBtoA) + suite.Require().NoError(err) + suite.Require().NotNil(result) + + // An error ack is now written on chainA + // Now we need to propagate the error to B and C + errorAckOnA := channeltypes.NewErrorAcknowledgement(types.ErrReceiveDisabled) + errorAckCommitmentOnA := channeltypes.CommitAcknowledgement(errorAckOnA.Acknowledgement()) + ackOnA := suite.chainA.GetAcknowledgement(packetFromBtoA) + suite.Require().Equal(errorAckCommitmentOnA, ackOnA) + + err = pathAtoB.EndpointB.UpdateClient() + suite.Require().NoError(err) + + err = pathAtoB.EndpointB.AcknowledgePacket(packetFromBtoA, errorAckOnA.Acknowledgement()) + suite.Require().NoError(err) + + errorAckOnB := channeltypes.NewErrorAcknowledgement(types.ErrForwardedPacketFailed) + errorAckCommitmentOnB := channeltypes.CommitAcknowledgement(errorAckOnB.Acknowledgement()) + ackOnB := suite.chainB.GetAcknowledgement(packetFromCtoB) + suite.Require().Equal(errorAckCommitmentOnB, ackOnB) + + // Check that escrow has been moved back from EscrowBtoA to EscrowBtoC + escrowBalanceBtoA = suite.chainB.GetSimApp().BankKeeper.GetBalance(suite.chainB.GetContext(), escrowAddressBtoA, coinOnB.GetDenom()) + suite.Require().Equal(sdkmath.NewInt(0), escrowBalanceBtoA.Amount) + + escrowBalancBtoC = suite.chainB.GetSimApp().BankKeeper.GetBalance(suite.chainB.GetContext(), escrowAddressBtoC, coinOnB.GetDenom()) + suite.Require().Equal(amount, escrowBalancBtoC.Amount) + + // Check the status of account on chain C before executing ack. + balanceOnC = suite.chainC.GetSimApp().BankKeeper.GetBalance(suite.chainC.GetContext(), setupReceiver.GetAddress(), coinOnC.GetDenom()) + suite.Require().Equal(sdkmath.NewInt(0), balanceOnC.Amount) + + // Propagate the error to C + err = pathBtoC.EndpointB.UpdateClient() + suite.Require().NoError(err) + + err = pathBtoC.EndpointB.AcknowledgePacket(packetFromCtoB, errorAckOnB.Acknowledgement()) + suite.Require().NoError(err) + + // Check that everything has been reverted + // + // Check the vouchers have been refunded on C + balanceOnC = suite.chainC.GetSimApp().BankKeeper.GetBalance(suite.chainC.GetContext(), setupReceiver.GetAddress(), coinOnC.GetDenom()) + suite.Require().Equal(amount, balanceOnC.Amount, "final receiver balance has not increased") +} + +// TestAcknowledgementFailureWithMiddleChainAsNotBeingTokenSource tests a failure in the last hop where the middle chain +// is not source of the token when receiving or sending the packet. In other words, the middle chain's is sent +// (and forwarding) someone else's native token (in this case chain C). +func (suite *KeeperTestSuite) TestAcknowledgementFailureWithMiddleChainAsNotBeingTokenSource() { + /* + Given the following topology: + chain A (channel 0) <- (channel-0) chain B (channel-1) <- (channel-0) chain C + transfer/channel-0/transfer/channel-1/stake transfer/channel-1/stake stake + We want to trigger: + 1. Single transfer forwarding token from C -> B -> A + 1.1 The ack fails on the last hop + 1.2 Propagate the error back to C + 2. Verify all the balances are updated as expected + */ + + amount := sdkmath.NewInt(100) + + pathAtoB, pathBtoC := suite.setupForwardingPaths() + + // Now we start the transfer from C -> B -> A + coinOnC := ibctesting.TestCoin + sender := suite.chainC.SenderAccounts[0].SenderAccount + receiver := suite.chainA.SenderAccounts[0].SenderAccount + + forwarding := types.NewForwarding(false, types.NewHop( + pathAtoB.EndpointB.ChannelConfig.PortID, + pathAtoB.EndpointB.ChannelID, + )) + + forwardTransfer := types.NewMsgTransfer( + pathBtoC.EndpointB.ChannelConfig.PortID, + pathBtoC.EndpointB.ChannelID, + sdk.NewCoins(coinOnC), + sender.GetAddress().String(), + receiver.GetAddress().String(), + clienttypes.ZeroHeight(), + suite.chainA.GetTimeoutTimestamp(), + "", + forwarding, + ) + + balanceOnCBefore := suite.chainC.GetSimApp().BankKeeper.GetBalance(suite.chainC.GetContext(), sender.GetAddress(), coinOnC.GetDenom()) + + result, err := suite.chainC.SendMsgs(forwardTransfer) + suite.Require().NoError(err) // message committed + + // Check that Escrow C has amount + totalEscrowChainC := suite.chainC.GetSimApp().TransferKeeper.GetTotalEscrowForDenom(suite.chainC.GetContext(), coinOnC.GetDenom()) + suite.Require().Equal(amount, totalEscrowChainC.Amount) + + packetFromCtoB, err := ibctesting.ParsePacketFromEvents(result.Events) + suite.Require().NoError(err) + suite.Require().NotNil(packetFromCtoB) + + err = pathBtoC.EndpointA.UpdateClient() + suite.Require().NoError(err) + + result, err = pathBtoC.EndpointA.RecvPacketWithResult(packetFromCtoB) + suite.Require().NoError(err) + suite.Require().NotNil(result) + + // Check that Escrow B has amount + denomOnB := types.NewDenom(sdk.DefaultBondDenom, types.NewTrace(pathBtoC.EndpointA.ChannelConfig.PortID, pathBtoC.EndpointA.ChannelID)) + suite.assertAmountOnChain(suite.chainB, escrow, amount, denomOnB.IBCDenom()) + + forwardedPacket, found := suite.chainB.GetSimApp().TransferKeeper.GetForwardedPacket(suite.chainB.GetContext(), pathAtoB.EndpointB.ChannelConfig.PortID, pathAtoB.EndpointB.ChannelID, packetFromCtoB.Sequence) + suite.Require().True(found) + suite.Require().Equal(packetFromCtoB, forwardedPacket) + + // Now we can receive the packet on A where we want to trigger an error + packetFromBtoA, err := ibctesting.ParsePacketFromEvents(result.Events) + suite.Require().NoError(err) + suite.Require().NotNil(packetFromBtoA) + + // turn off receive on chain A to trigger an error + suite.chainA.GetSimApp().TransferKeeper.SetParams(suite.chainA.GetContext(), types.Params{ + SendEnabled: true, + ReceiveEnabled: false, + }) + + err = pathAtoB.EndpointA.UpdateClient() + suite.Require().NoError(err) + + result, err = pathAtoB.EndpointA.RecvPacketWithResult(packetFromBtoA) + suite.Require().NoError(err) + suite.Require().NotNil(result) + + // An error ack is now written on chainA + // Now we need to propagate the error to B and C + errorAckOnA := channeltypes.NewErrorAcknowledgement(types.ErrReceiveDisabled) + errorAckCommitmentOnA := channeltypes.CommitAcknowledgement(errorAckOnA.Acknowledgement()) + ackOnA := suite.chainA.GetAcknowledgement(packetFromBtoA) + suite.Require().Equal(errorAckCommitmentOnA, ackOnA) + + err = pathAtoB.EndpointB.UpdateClient() + suite.Require().NoError(err) + + err = pathAtoB.EndpointB.AcknowledgePacket(packetFromBtoA, errorAckOnA.Acknowledgement()) + suite.Require().NoError(err) + + errorAckOnB := channeltypes.NewErrorAcknowledgement(types.ErrForwardedPacketFailed) + errorAckCommitmentOnB := channeltypes.CommitAcknowledgement(errorAckOnB.Acknowledgement()) + ackOnB := suite.chainB.GetAcknowledgement(packetFromCtoB) + suite.Require().Equal(errorAckCommitmentOnB, ackOnB) + + // Check that escrow has been burnt on B + suite.assertAmountOnChain(suite.chainB, escrow, sdkmath.NewInt(0), denomOnB.IBCDenom()) + + // Check the status of account on chain C before executing ack. + balanceOnC := suite.chainC.GetSimApp().BankKeeper.GetBalance(suite.chainC.GetContext(), sender.GetAddress(), coinOnC.GetDenom()) + suite.Require().Equal(balanceOnCBefore.SubAmount(amount).Amount, balanceOnC.Amount) + + // Propagate the error to C + err = pathBtoC.EndpointB.UpdateClient() + suite.Require().NoError(err) + + err = pathBtoC.EndpointB.AcknowledgePacket(packetFromCtoB, errorAckOnB.Acknowledgement()) + suite.Require().NoError(err) + + // Check that everything has been reverted + // + // Check the token has been returned to the sender on C + suite.assertAmountOnChain(suite.chainC, escrow, sdkmath.NewInt(0), coinOnC.GetDenom()) + suite.assertAmountOnChain(suite.chainC, balance, balanceOnCBefore.Amount, coinOnC.GetDenom()) +} + +// TestOnTimeoutPacketForwarding tests the scenario in which a packet goes from +// A to C, using B as a forwarding hop. The packet times out when going to C +// from B and we verify that funds are properly returned to A. +func (suite *KeeperTestSuite) TestOnTimeoutPacketForwarding() { + pathAtoB, pathBtoC := suite.setupForwardingPaths() + + amount := sdkmath.NewInt(100) + coin := ibctesting.TestCoin + sender := suite.chainA.SenderAccounts[0].SenderAccount + receiver := suite.chainC.SenderAccounts[0].SenderAccount + + denomA := types.NewDenom(coin.Denom) + denomAB := types.NewDenom(coin.Denom, types.NewTrace(pathAtoB.EndpointB.ChannelConfig.PortID, pathAtoB.EndpointB.ChannelID)) + denomABC := types.NewDenom(coin.Denom, append([]types.Trace{types.NewTrace(pathBtoC.EndpointB.ChannelConfig.PortID, pathBtoC.EndpointB.ChannelID)}, denomAB.Trace...)...) + + originalABalance := suite.chainA.GetSimApp().BankKeeper.GetBalance(suite.chainA.GetContext(), sender.GetAddress(), coin.Denom) + + forwarding := types.Forwarding{ + Hops: []types.Hop{types.NewHop(pathBtoC.EndpointA.ChannelConfig.PortID, pathBtoC.EndpointA.ChannelID)}, + } + + transferMsg := types.NewMsgTransfer( + pathAtoB.EndpointA.ChannelConfig.PortID, + pathAtoB.EndpointA.ChannelID, + sdk.NewCoins(coin), + sender.GetAddress().String(), + receiver.GetAddress().String(), + clienttypes.ZeroHeight(), + uint64(suite.chainA.GetContext().BlockTime().Add(time.Minute*5).UnixNano()), + "", + forwarding, + ) + + result, err := suite.chainA.SendMsgs(transferMsg) + suite.Require().NoError(err) // message committed + + // parse the packet from result events and recv packet on chainB + packet, err := ibctesting.ParsePacketFromEvents(result.Events) + suite.Require().NoError(err) + suite.Require().NotNil(packet) + + err = pathAtoB.EndpointB.UpdateClient() + suite.Require().NoError(err) + + // Receive packet on B. + result, err = pathAtoB.EndpointB.RecvPacketWithResult(packet) + suite.Require().NoError(err) + suite.Require().NotNil(result) + + err = pathBtoC.EndpointA.UpdateClient() + suite.Require().NoError(err) + + err = pathBtoC.EndpointB.UpdateClient() + suite.Require().NoError(err) + + // Make sure funds went from A to B's escrow account. + suite.assertAmountOnChain(suite.chainA, balance, originalABalance.Amount.Sub(amount), denomA.IBCDenom()) + suite.assertAmountOnChain(suite.chainB, escrow, amount, denomAB.IBCDenom()) + + // Check that forwarded packet exists + forwardedPacket, found := suite.chainB.GetSimApp().TransferKeeper.GetForwardedPacket(suite.chainB.GetContext(), pathBtoC.EndpointA.ChannelConfig.PortID, pathBtoC.EndpointA.ChannelID, packet.Sequence) + suite.Require().True(found, "Chain B has no forwarded packet") + suite.Require().Equal(packet, forwardedPacket, "ForwardedPacket stored in ChainB is not the same that was sent") + + address := suite.chainB.GetSimApp().AccountKeeper.GetModuleAddress(types.ModuleName).String() + data := types.NewFungibleTokenPacketDataV2( + []types.Token{ + { + Denom: types.NewDenom(sdk.DefaultBondDenom, types.NewTrace(pathAtoB.EndpointA.ChannelConfig.PortID, pathAtoB.EndpointA.ChannelID)), + Amount: "100", + }, + }, + address, + receiver.GetAddress().String(), + "", types.ForwardingPacketData{}, + ) + + packet = channeltypes.NewPacket( + data.GetBytes(), + 1, + pathBtoC.EndpointA.ChannelConfig.PortID, + pathBtoC.EndpointA.ChannelID, + pathBtoC.EndpointB.ChannelConfig.PortID, + pathBtoC.EndpointB.ChannelID, + packet.TimeoutHeight, + packet.TimeoutTimestamp) + + // retrieve module callbacks + module, _, err := suite.chainB.App.GetIBCKeeper().PortKeeper.LookupModuleByPort(suite.chainB.GetContext(), pathBtoC.EndpointA.ChannelConfig.PortID) + suite.Require().NoError(err) + + cbs, ok := suite.chainB.App.GetIBCKeeper().PortKeeper.Route(module) + suite.Require().True(ok) + + // Trigger OnTimeoutPacket for chainB + err = cbs.OnTimeoutPacket(suite.chainB.GetContext(), packet, nil) + suite.Require().NoError(err) + + // Ensure that chainB has an ack. + storedAck, found := suite.chainB.App.GetIBCKeeper().ChannelKeeper.GetPacketAcknowledgement(suite.chainB.GetContext(), packet.GetDestPort(), packet.GetDestChannel(), packet.GetSequence()) + suite.Require().True(found, "chainB does not have an ack") + + // And that this ack is of the type we expect (Error due to time out) + ack := channeltypes.NewErrorAcknowledgement(types.ErrForwardedPacketTimedOut) + ackbytes := channeltypes.CommitAcknowledgement(ack.Acknowledgement()) + suite.Require().Equal(ackbytes, storedAck) + + forwardingPacketData := types.NewForwardingPacketData("", forwarding.Hops...) + data = types.NewFungibleTokenPacketDataV2( + []types.Token{ + { + Denom: types.NewDenom(sdk.DefaultBondDenom), + Amount: "100", + }, + }, + sender.GetAddress().String(), + receiver.GetAddress().String(), + "", forwardingPacketData, + ) + + packet = channeltypes.NewPacket( + data.GetBytes(), + 1, + pathAtoB.EndpointA.ChannelConfig.PortID, + pathAtoB.EndpointA.ChannelID, + pathAtoB.EndpointB.ChannelConfig.PortID, + pathAtoB.EndpointB.ChannelID, + packet.TimeoutHeight, + packet.TimeoutTimestamp) + + // Send the ack to chain A. + err = suite.chainA.GetSimApp().TransferKeeper.OnAcknowledgementPacket(suite.chainA.GetContext(), packet, data, ack) + suite.Require().NoError(err) + + // Finally, check that A,B, and C escrow accounts do not have fund. + suite.assertAmountOnChain(suite.chainC, escrow, sdkmath.NewInt(0), denomABC.IBCDenom()) + suite.assertAmountOnChain(suite.chainB, escrow, sdkmath.NewInt(0), denomAB.IBCDenom()) + suite.assertAmountOnChain(suite.chainA, escrow, sdkmath.NewInt(0), denomA.IBCDenom()) + + // And that A has its original balance back. + suite.assertAmountOnChain(suite.chainA, balance, originalABalance.Amount, coin.Denom) +} + +// TestForwardingWithMoreThanOneHop tests the scenario in which we +// forward with more than one forwarding hop. +func (suite *KeeperTestSuite) TestForwardingWithMoreThanOneHop() { + // Setup A->B->C->D + coinOnA := ibctesting.TestCoin + + pathAtoB := ibctesting.NewTransferPath(suite.chainA, suite.chainB) + pathAtoB.Setup() + + pathBtoC := ibctesting.NewTransferPath(suite.chainB, suite.chainC) + pathBtoC.Setup() + + pathCtoD := ibctesting.NewTransferPath(suite.chainC, suite.chainD) + pathCtoD.Setup() + + sender := suite.chainA.SenderAccounts[0].SenderAccount + receiver := suite.chainD.SenderAccounts[0].SenderAccount + + forwarding := types.NewForwarding(false, + types.NewHop(pathBtoC.EndpointA.ChannelConfig.PortID, pathBtoC.EndpointA.ChannelID), + types.NewHop(pathCtoD.EndpointA.ChannelConfig.PortID, pathCtoD.EndpointA.ChannelID), + ) + + transferMsg := types.NewMsgTransfer( + pathAtoB.EndpointA.ChannelConfig.PortID, + pathAtoB.EndpointA.ChannelID, + sdk.NewCoins(coinOnA), + sender.GetAddress().String(), + receiver.GetAddress().String(), + clienttypes.ZeroHeight(), + suite.chainA.GetTimeoutTimestamp(), + "", + forwarding) + + // Send message to A and verify. + result, err := suite.chainA.SendMsgs(transferMsg) + suite.Require().NoError(err) + + packetFromAtoB, err := ibctesting.ParsePacketFromEvents(result.Events) + suite.Require().NoError(err) + suite.Require().NotNil(packetFromAtoB) + + err = pathAtoB.EndpointB.UpdateClient() + suite.Require().NoError(err) + + // Receive from B and verify. + result, err = pathAtoB.EndpointB.RecvPacketWithResult(packetFromAtoB) + suite.Require().NoError(err) + suite.Require().NotNil(result) + + // Check that Escrow A has amount + suite.assertAmountOnChain(suite.chainA, escrow, coinOnA.Amount, coinOnA.Denom) + + // Check that Escrow B has amount + denomTrace := types.NewDenom(sdk.DefaultBondDenom, types.NewTrace(pathAtoB.EndpointB.ChannelConfig.PortID, pathAtoB.EndpointB.ChannelID)) + suite.assertAmountOnChain(suite.chainB, escrow, coinOnA.Amount, denomTrace.IBCDenom()) + + // Receive on C the packet sent from B, verify amount. + packetFromBtoC, err := ibctesting.ParsePacketFromEvents(result.Events) + suite.Require().NoError(err) + suite.Require().NotNil(packetFromBtoC) + + err = pathBtoC.EndpointA.UpdateClient() + suite.Require().NoError(err) + + err = pathBtoC.EndpointB.UpdateClient() + suite.Require().NoError(err) + + result, err = pathBtoC.EndpointB.RecvPacketWithResult(packetFromBtoC) + suite.Require().NoError(err) + suite.Require().NotNil(result) + + // Check that Escrow C has amount + denomTraceABC := types.NewDenom(denomTrace.Base, append([]types.Trace{types.NewTrace(pathBtoC.EndpointB.ChannelConfig.PortID, pathBtoC.EndpointB.ChannelID)}, denomTrace.Trace...)...) + suite.assertAmountOnChain(suite.chainC, escrow, coinOnA.Amount, denomTraceABC.IBCDenom()) + + // Finally, receive on D and verify that D has the desired amount. + packetFromCtoD, err := ibctesting.ParsePacketFromEvents(result.Events) + suite.Require().NoError(err) + suite.Require().NotNil(packetFromCtoD) + + err = pathCtoD.EndpointA.UpdateClient() + suite.Require().NoError(err) + + err = pathCtoD.EndpointB.UpdateClient() + suite.Require().NoError(err) + + result, err = pathCtoD.EndpointB.RecvPacketWithResult(packetFromCtoD) + suite.Require().NoError(err) + suite.Require().NotNil(result) + + denomTraceABCD := types.NewDenom(denomTraceABC.Base, append([]types.Trace{types.NewTrace(pathCtoD.EndpointB.ChannelConfig.PortID, pathCtoD.EndpointB.ChannelID)}, denomTraceABC.Trace...)...) + suite.assertAmountOnChain(suite.chainD, balance, coinOnA.Amount, denomTraceABCD.IBCDenom()) + + // Propagate the ack back from D to A. + ack, err := ibctesting.ParseAckFromEvents(result.Events) + suite.Require().NoError(err) + suite.Require().NotNil(ack) + + err = pathCtoD.EndpointA.AcknowledgePacket(packetFromCtoD, ack) + suite.Require().NoError(err) + + err = pathBtoC.EndpointA.UpdateClient() + suite.Require().NoError(err) + + err = pathBtoC.EndpointA.AcknowledgePacket(packetFromBtoC, ack) + suite.Require().NoError(err) + + err = pathAtoB.EndpointA.UpdateClient() + suite.Require().NoError(err) + + err = pathAtoB.EndpointA.AcknowledgePacket(packetFromAtoB, ack) + suite.Require().NoError(err) +} diff --git a/modules/apps/transfer/keeper/relay_test.go b/modules/apps/transfer/keeper/relay_test.go index 5fcd6c59c13..4b3c23b6d28 100644 --- a/modules/apps/transfer/keeper/relay_test.go +++ b/modules/apps/transfer/keeper/relay_test.go @@ -11,6 +11,7 @@ import ( sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" banktestutil "github.com/cosmos/cosmos-sdk/x/bank/testutil" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" transferkeeper "github.com/cosmos/ibc-go/v8/modules/apps/transfer/keeper" "github.com/cosmos/ibc-go/v8/modules/apps/transfer/types" @@ -21,6 +22,11 @@ import ( ibcmock "github.com/cosmos/ibc-go/v8/testing/mock" ) +var ( + emptyForwarding = types.Forwarding{} + emptyForwardingPacketData = types.ForwardingPacketData{} +) + // TestSendTransfer tests sending from chainA to chainB using both coin // that originate on chainA and coin that originate on chainB. func (suite *KeeperTestSuite) TestSendTransfer() { @@ -30,6 +36,7 @@ func (suite *KeeperTestSuite) TestSendTransfer() { sender sdk.AccAddress timeoutHeight clienttypes.Height memo string + forwarding types.Forwarding expEscrowAmount sdkmath.Int // total amount in escrow for denom on receiving chain ) @@ -74,6 +81,29 @@ func (suite *KeeperTestSuite) TestSendTransfer() { }, nil, }, + { + "successful transfer with empty forwarding hops and ics20-1", + func() { + expEscrowAmount = sdkmath.NewInt(100) + + // Set version to isc20-1. + path.EndpointA.UpdateChannel(func(channel *channeltypes.Channel) { + channel.Version = types.V1 + }) + }, + nil, + }, + { + "successful transfer with non-empty forwarding hops and ics20-2", + func() { + expEscrowAmount = sdkmath.NewInt(100) + forwarding = types.NewForwarding(false, types.NewHop( + path.EndpointA.ChannelConfig.PortID, + path.EndpointA.ChannelID, + )) + }, + nil, + }, { "successful transfer of IBC token with memo", func() { @@ -95,7 +125,7 @@ func (suite *KeeperTestSuite) TestSendTransfer() { { "failure: sender account is blocked", func() { - sender = suite.chainA.GetSimApp().AccountKeeper.GetModuleAddress(types.ModuleName) + sender = suite.chainA.GetSimApp().AccountKeeper.GetModuleAddress(minttypes.ModuleName) }, ibcerrors.ErrUnauthorized, }, @@ -140,6 +170,21 @@ func (suite *KeeperTestSuite) TestSendTransfer() { }, channeltypes.ErrInvalidPacket, }, + { + "failure: forwarding hops is not empty with ics20-1", + func() { + // Set version to isc20-1. + path.EndpointA.UpdateChannel(func(channel *channeltypes.Channel) { + channel.Version = types.V1 + }) + + forwarding = types.NewForwarding(false, types.NewHop( + path.EndpointA.ChannelConfig.PortID, + path.EndpointA.ChannelID, + )) + }, + ibcerrors.ErrInvalidRequest, + }, } for _, tc := range testCases { @@ -151,14 +196,15 @@ func (suite *KeeperTestSuite) TestSendTransfer() { path = ibctesting.NewTransferPath(suite.chainA, suite.chainB) path.Setup() - coin = sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(100)) + coin = ibctesting.TestCoin sender = suite.chainA.SenderAccount.GetAddress() memo = "" timeoutHeight = suite.chainB.GetTimeoutHeight() expEscrowAmount = sdkmath.ZeroInt() + forwarding = emptyForwarding // create IBC token on chainA - transferMsg := types.NewMsgTransfer(path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID, sdk.NewCoins(coin), suite.chainB.SenderAccount.GetAddress().String(), suite.chainA.SenderAccount.GetAddress().String(), suite.chainA.GetTimeoutHeight(), 0, "") + transferMsg := types.NewMsgTransfer(path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID, sdk.NewCoins(coin), suite.chainB.SenderAccount.GetAddress().String(), suite.chainA.SenderAccount.GetAddress().String(), suite.chainA.GetTimeoutHeight(), 0, "", emptyForwarding) result, err := suite.chainB.SendMsgs(transferMsg) suite.Require().NoError(err) // message committed @@ -178,6 +224,7 @@ func (suite *KeeperTestSuite) TestSendTransfer() { suite.chainB.SenderAccount.GetAddress().String(), timeoutHeight, 0, // only use timeout height memo, + forwarding, ) res, err := suite.chainA.GetSimApp().TransferKeeper.Transfer(suite.chainA.GetContext(), msg) @@ -238,7 +285,7 @@ func (suite *KeeperTestSuite) TestSendTransferSetsTotalEscrowAmountForSourceIBCT path2.Setup() // create IBC token on chain B with denom trace "transfer/channel-0/stake" - coin := sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(100)) + coin := ibctesting.TestCoin transferMsg := types.NewMsgTransfer( path1.EndpointA.ChannelConfig.PortID, path1.EndpointA.ChannelID, @@ -246,6 +293,7 @@ func (suite *KeeperTestSuite) TestSendTransferSetsTotalEscrowAmountForSourceIBCT suite.chainA.SenderAccount.GetAddress().String(), suite.chainB.SenderAccount.GetAddress().String(), suite.chainB.GetTimeoutHeight(), 0, "", + emptyForwarding, ) result, err := suite.chainA.SendMsgs(transferMsg) suite.Require().NoError(err) // message committed @@ -266,6 +314,7 @@ func (suite *KeeperTestSuite) TestSendTransferSetsTotalEscrowAmountForSourceIBCT suite.chainB.SenderAccount.GetAddress().String(), suite.chainA.SenderAccount.GetAddress().String(), suite.chainA.GetTimeoutHeight(), 0, "", + emptyForwarding, ) res, err := suite.chainB.GetSimApp().TransferKeeper.Transfer(suite.chainB.GetContext(), msg) @@ -316,9 +365,16 @@ func (suite *KeeperTestSuite) TestOnRecvPacket_ReceiverIsNotSource() { { "failure: receiver is module account", func() { - receiver = suite.chainB.GetSimApp().AccountKeeper.GetModuleAddress(types.ModuleName).String() + receiver = suite.chainB.GetSimApp().AccountKeeper.GetModuleAddress(minttypes.ModuleName).String() + }, + ibcerrors.ErrUnauthorized, + }, + { + "failure: receiver is invalid", + func() { + receiver = "invalid-address" }, - sdkerrors.ErrUnauthorized, + ibcerrors.ErrInvalidAddress, }, { "failure: receive is disabled", @@ -364,7 +420,7 @@ func (suite *KeeperTestSuite) TestOnRecvPacket_ReceiverIsNotSource() { // send coin from chainA to chainB coin := sdk.NewCoin(sdk.DefaultBondDenom, amount) - transferMsg := types.NewMsgTransfer(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, sdk.NewCoins(coin), suite.chainA.SenderAccount.GetAddress().String(), receiver, clienttypes.NewHeight(1, 110), 0, memo) + transferMsg := types.NewMsgTransfer(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, sdk.NewCoins(coin), suite.chainA.SenderAccount.GetAddress().String(), receiver, clienttypes.NewHeight(1, 110), 0, memo, emptyForwarding) _, err := suite.chainA.SendMsgs(transferMsg) suite.Require().NoError(err) // message committed @@ -377,7 +433,7 @@ func (suite *KeeperTestSuite) TestOnRecvPacket_ReceiverIsNotSource() { Denom: types.NewDenom(sdk.DefaultBondDenom, []types.Trace{}...), Amount: amount.String(), }, - }, suite.chainA.SenderAccount.GetAddress().String(), receiver, memo) + }, suite.chainA.SenderAccount.GetAddress().String(), receiver, memo, emptyForwardingPacketData) packet := channeltypes.NewPacket(data.GetBytes(), seq, path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID, clienttypes.NewHeight(1, 100), 0) err = suite.chainB.GetSimApp().TransferKeeper.OnRecvPacket(suite.chainB.GetContext(), packet, data) @@ -474,7 +530,7 @@ func (suite *KeeperTestSuite) TestOnRecvPacket_ReceiverIsSource() { { "failure: receiver is module account", func() { - receiver = suite.chainB.GetSimApp().AccountKeeper.GetModuleAddress(types.ModuleName).String() + receiver = suite.chainB.GetSimApp().AccountKeeper.GetModuleAddress(minttypes.ModuleName).String() expEscrowAmount = sdkmath.NewInt(100) }, ibcerrors.ErrUnauthorized, @@ -498,8 +554,8 @@ func (suite *KeeperTestSuite) TestOnRecvPacket_ReceiverIsSource() { seq := uint64(1) // send coin from chainB to chainA, receive them, acknowledge them - coin := sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(100)) - transferMsg := types.NewMsgTransfer(path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID, sdk.NewCoins(coin), suite.chainB.SenderAccount.GetAddress().String(), suite.chainA.SenderAccount.GetAddress().String(), clienttypes.NewHeight(1, 110), 0, memo) + coin := ibctesting.TestCoin + transferMsg := types.NewMsgTransfer(path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID, sdk.NewCoins(coin), suite.chainB.SenderAccount.GetAddress().String(), suite.chainA.SenderAccount.GetAddress().String(), clienttypes.NewHeight(1, 110), 0, memo, emptyForwarding) res, err := suite.chainB.SendMsgs(transferMsg) suite.Require().NoError(err) // message committed @@ -516,7 +572,7 @@ func (suite *KeeperTestSuite) TestOnRecvPacket_ReceiverIsSource() { // send coin back from chainA to chainB coin = sdk.NewCoin(denom.IBCDenom(), amount) - transferMsg = types.NewMsgTransfer(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, sdk.NewCoins(coin), suite.chainA.SenderAccount.GetAddress().String(), receiver, clienttypes.NewHeight(1, 110), 0, memo) + transferMsg = types.NewMsgTransfer(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, sdk.NewCoins(coin), suite.chainA.SenderAccount.GetAddress().String(), receiver, clienttypes.NewHeight(1, 110), 0, memo, emptyForwarding) _, err = suite.chainA.SendMsgs(transferMsg) suite.Require().NoError(err) // message committed @@ -528,7 +584,7 @@ func (suite *KeeperTestSuite) TestOnRecvPacket_ReceiverIsSource() { Denom: denom, Amount: amount.String(), }, - }, suite.chainA.SenderAccount.GetAddress().String(), receiver, memo) + }, suite.chainA.SenderAccount.GetAddress().String(), receiver, memo, emptyForwardingPacketData) packet = channeltypes.NewPacket(data.GetBytes(), seq, path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID, clienttypes.NewHeight(1, 100), 0) err = suite.chainB.GetSimApp().TransferKeeper.OnRecvPacket(suite.chainB.GetContext(), packet, data) @@ -606,8 +662,7 @@ func (suite *KeeperTestSuite) TestOnRecvPacketSetsTotalEscrowAmountForSourceIBCT Denom: denom, Amount: amount.String(), }, - }, suite.chainA.SenderAccount.GetAddress().String(), suite.chainB.SenderAccount.GetAddress().String(), "") - + }, suite.chainA.SenderAccount.GetAddress().String(), suite.chainB.SenderAccount.GetAddress().String(), "", emptyForwardingPacketData) packet := channeltypes.NewPacket( data.GetBytes(), seq, @@ -738,7 +793,7 @@ func (suite *KeeperTestSuite) TestOnAcknowledgementPacket() { Denom: denom, Amount: amount.String(), }, - }, suite.chainA.SenderAccount.GetAddress().String(), suite.chainB.SenderAccount.GetAddress().String(), "") + }, suite.chainA.SenderAccount.GetAddress().String(), suite.chainB.SenderAccount.GetAddress().String(), "", emptyForwardingPacketData) packet := channeltypes.NewPacket(data.GetBytes(), 1, path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID, clienttypes.NewHeight(1, 100), 0) preAcknowledgementBalance := suite.chainA.GetSimApp().BankKeeper.GetBalance(suite.chainA.GetContext(), suite.chainA.SenderAccount.GetAddress(), denom.IBCDenom()) @@ -833,6 +888,7 @@ func (suite *KeeperTestSuite) TestOnAcknowledgementPacketSetsTotalEscrowAmountFo suite.chainB.SenderAccount.GetAddress().String(), suite.chainA.SenderAccount.GetAddress().String(), "", + emptyForwardingPacketData, ) packet := channeltypes.NewPacket( data.GetBytes(), @@ -972,7 +1028,7 @@ func (suite *KeeperTestSuite) TestOnTimeoutPacket() { Denom: denom, Amount: amount, }, - }, sender, suite.chainB.SenderAccount.GetAddress().String(), "") + }, sender, suite.chainB.SenderAccount.GetAddress().String(), "", emptyForwardingPacketData) packet := channeltypes.NewPacket(data.GetBytes(), 1, path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID, clienttypes.NewHeight(1, 100), 0) preTimeoutBalance := suite.chainA.GetSimApp().BankKeeper.GetBalance(suite.chainA.GetContext(), suite.chainA.SenderAccount.GetAddress(), denom.IBCDenom()) @@ -1058,7 +1114,7 @@ func (suite *KeeperTestSuite) TestOnTimeoutPacketSetsTotalEscrowAmountForSourceI Denom: denom, Amount: amount.String(), }, - }, suite.chainB.SenderAccount.GetAddress().String(), suite.chainA.SenderAccount.GetAddress().String(), "") + }, suite.chainB.SenderAccount.GetAddress().String(), suite.chainA.SenderAccount.GetAddress().String(), "", emptyForwardingPacketData) packet := channeltypes.NewPacket( data.GetBytes(), seq, @@ -1239,24 +1295,26 @@ func (suite *KeeperTestSuite) TestPacketForwardsCompatibility() { func (suite *KeeperTestSuite) TestCreatePacketDataBytesFromVersion() { var ( - bz []byte - tokens types.Tokens + bz []byte + tokens types.Tokens + sender, receiver string ) testCases := []struct { name string appVersion string malleate func() - expResult func(bz []byte) + expResult func(bz []byte, err error) expPanicErr error }{ { "success", types.V1, func() {}, - func(bz []byte) { - expPacketData := types.NewFungibleTokenPacketData("", "", "", "", "") + func(bz []byte, err error) { + expPacketData := types.NewFungibleTokenPacketData(ibctesting.TestCoin.Denom, ibctesting.TestCoin.Amount.String(), sender, receiver, "") suite.Require().Equal(bz, expPacketData.GetBytes()) + suite.Require().NoError(err) }, nil, }, @@ -1264,9 +1322,34 @@ func (suite *KeeperTestSuite) TestCreatePacketDataBytesFromVersion() { "success: version 2", types.V2, func() {}, - func(bz []byte) { - expPacketData := types.NewFungibleTokenPacketDataV2(types.Tokens{types.Token{}}, "", "", "") + func(bz []byte, err error) { + expPacketData := types.NewFungibleTokenPacketDataV2(tokens, sender, receiver, "", emptyForwardingPacketData) suite.Require().Equal(bz, expPacketData.GetBytes()) + suite.Require().NoError(err) + }, + nil, + }, + { + "failure: fails v1 validation", + types.V1, + func() { + sender = "" + }, + func(bz []byte, err error) { + suite.Require().Nil(bz) + suite.Require().ErrorIs(err, ibcerrors.ErrInvalidAddress) + }, + nil, + }, + { + "failure: fails v2 validation", + types.V2, + func() { + sender = "" + }, + func(bz []byte, err error) { + suite.Require().Nil(bz) + suite.Require().ErrorIs(err, ibcerrors.ErrInvalidAddress) }, nil, }, @@ -1290,12 +1373,26 @@ func (suite *KeeperTestSuite) TestCreatePacketDataBytesFromVersion() { for _, tc := range testCases { suite.Run(tc.name, func() { - tokens = types.Tokens{types.Token{}} + suite.SetupTest() + + path := ibctesting.NewTransferPath(suite.chainA, suite.chainB) + path.Setup() + + tokens = types.Tokens{ + { + Amount: ibctesting.TestCoin.Amount.String(), + Denom: types.NewDenom(ibctesting.TestCoin.Denom), + }, + } + + sender = suite.chainA.SenderAccount.GetAddress().String() + receiver = suite.chainB.SenderAccount.GetAddress().String() tc.malleate() + var err error createFunc := func() { - bz = transferkeeper.CreatePacketDataBytesFromVersion(tc.appVersion, "", "", "", tokens) + bz, err = transferkeeper.CreatePacketDataBytesFromVersion(tc.appVersion, sender, receiver, "", tokens, nil) } expPanic := tc.expPanicErr != nil @@ -1303,7 +1400,7 @@ func (suite *KeeperTestSuite) TestCreatePacketDataBytesFromVersion() { suite.Require().PanicsWithError(tc.expPanicErr.Error(), createFunc) } else { createFunc() - tc.expResult(bz) + tc.expResult(bz, err) } }) } diff --git a/modules/apps/transfer/transfer_test.go b/modules/apps/transfer/transfer_test.go index 63145ae77a9..b3198fc8cc1 100644 --- a/modules/apps/transfer/transfer_test.go +++ b/modules/apps/transfer/transfer_test.go @@ -80,7 +80,7 @@ func (suite *TransferTestSuite) TestHandleMsgTransfer() { } // send from chainA to chainB - msg := types.NewMsgTransfer(pathAToB.EndpointA.ChannelConfig.PortID, pathAToB.EndpointA.ChannelID, originalCoins, suite.chainA.SenderAccount.GetAddress().String(), suite.chainB.SenderAccount.GetAddress().String(), timeoutHeight, 0, "") + msg := types.NewMsgTransfer(pathAToB.EndpointA.ChannelConfig.PortID, pathAToB.EndpointA.ChannelID, originalCoins, suite.chainA.SenderAccount.GetAddress().String(), suite.chainB.SenderAccount.GetAddress().String(), timeoutHeight, 0, "", types.Forwarding{}) res, err := suite.chainA.SendMsgs(msg) suite.Require().NoError(err) // message committed @@ -120,7 +120,7 @@ func (suite *TransferTestSuite) TestHandleMsgTransfer() { traceBToC := types.NewTrace(pathBToC.EndpointB.ChannelConfig.PortID, pathBToC.EndpointB.ChannelID) // send from chainB to chainC - msg = types.NewMsgTransfer(pathBToC.EndpointA.ChannelConfig.PortID, pathBToC.EndpointA.ChannelID, coinsSentFromAToB, suite.chainB.SenderAccount.GetAddress().String(), suite.chainC.SenderAccount.GetAddress().String(), timeoutHeight, 0, "") + msg = types.NewMsgTransfer(pathBToC.EndpointA.ChannelConfig.PortID, pathBToC.EndpointA.ChannelID, coinsSentFromAToB, suite.chainB.SenderAccount.GetAddress().String(), suite.chainC.SenderAccount.GetAddress().String(), timeoutHeight, 0, "", types.Forwarding{}) res, err = suite.chainB.SendMsgs(msg) suite.Require().NoError(err) // message committed @@ -149,7 +149,7 @@ func (suite *TransferTestSuite) TestHandleMsgTransfer() { } // send from chainC back to chainB - msg = types.NewMsgTransfer(pathBToC.EndpointB.ChannelConfig.PortID, pathBToC.EndpointB.ChannelID, coinsSentFromBToC, suite.chainC.SenderAccount.GetAddress().String(), suite.chainB.SenderAccount.GetAddress().String(), timeoutHeight, 0, "") + msg = types.NewMsgTransfer(pathBToC.EndpointB.ChannelConfig.PortID, pathBToC.EndpointB.ChannelID, coinsSentFromBToC, suite.chainC.SenderAccount.GetAddress().String(), suite.chainB.SenderAccount.GetAddress().String(), timeoutHeight, 0, "", types.Forwarding{}) res, err = suite.chainC.SendMsgs(msg) suite.Require().NoError(err) // message committed diff --git a/modules/apps/transfer/types/authz.pb.go b/modules/apps/transfer/types/authz.pb.go index 01928af5ac8..e71fcc8dc6f 100644 --- a/modules/apps/transfer/types/authz.pb.go +++ b/modules/apps/transfer/types/authz.pb.go @@ -39,6 +39,8 @@ type Allocation struct { // allow list of memo strings, an empty list prohibits all memo strings; // a list only with "*" permits any memo string AllowedPacketData []string `protobuf:"bytes,5,rep,name=allowed_packet_data,json=allowedPacketData,proto3" json:"allowed_packet_data,omitempty"` + // Forwarding options that are allowed. + AllowedForwarding []AllowedForwarding `protobuf:"bytes,6,rep,name=allowed_forwarding,json=allowedForwarding,proto3" json:"allowed_forwarding"` } func (m *Allocation) Reset() { *m = Allocation{} } @@ -109,6 +111,60 @@ func (m *Allocation) GetAllowedPacketData() []string { return nil } +func (m *Allocation) GetAllowedForwarding() []AllowedForwarding { + if m != nil { + return m.AllowedForwarding + } + return nil +} + +// AllowedForwarding defines which options are allowed for forwarding. +type AllowedForwarding struct { + // a list of allowed source port ID/channel ID pairs through which the packet is allowed to be forwarded until final + // destination + Hops []Hop `protobuf:"bytes,1,rep,name=hops,proto3" json:"hops"` +} + +func (m *AllowedForwarding) Reset() { *m = AllowedForwarding{} } +func (m *AllowedForwarding) String() string { return proto.CompactTextString(m) } +func (*AllowedForwarding) ProtoMessage() {} +func (*AllowedForwarding) Descriptor() ([]byte, []int) { + return fileDescriptor_b1a28b55d17325aa, []int{1} +} +func (m *AllowedForwarding) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *AllowedForwarding) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_AllowedForwarding.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *AllowedForwarding) XXX_Merge(src proto.Message) { + xxx_messageInfo_AllowedForwarding.Merge(m, src) +} +func (m *AllowedForwarding) XXX_Size() int { + return m.Size() +} +func (m *AllowedForwarding) XXX_DiscardUnknown() { + xxx_messageInfo_AllowedForwarding.DiscardUnknown(m) +} + +var xxx_messageInfo_AllowedForwarding proto.InternalMessageInfo + +func (m *AllowedForwarding) GetHops() []Hop { + if m != nil { + return m.Hops + } + return nil +} + // TransferAuthorization allows the grantee to spend up to spend_limit coins from // the granter's account for ibc transfer on a specific channel type TransferAuthorization struct { @@ -120,7 +176,7 @@ func (m *TransferAuthorization) Reset() { *m = TransferAuthorization{} } func (m *TransferAuthorization) String() string { return proto.CompactTextString(m) } func (*TransferAuthorization) ProtoMessage() {} func (*TransferAuthorization) Descriptor() ([]byte, []int) { - return fileDescriptor_b1a28b55d17325aa, []int{1} + return fileDescriptor_b1a28b55d17325aa, []int{2} } func (m *TransferAuthorization) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -158,6 +214,7 @@ func (m *TransferAuthorization) GetAllocations() []Allocation { func init() { proto.RegisterType((*Allocation)(nil), "ibc.applications.transfer.v1.Allocation") + proto.RegisterType((*AllowedForwarding)(nil), "ibc.applications.transfer.v1.AllowedForwarding") proto.RegisterType((*TransferAuthorization)(nil), "ibc.applications.transfer.v1.TransferAuthorization") } @@ -166,35 +223,38 @@ func init() { } var fileDescriptor_b1a28b55d17325aa = []byte{ - // 436 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x92, 0xcf, 0x8e, 0xd3, 0x30, - 0x10, 0xc6, 0x9b, 0xed, 0x82, 0x54, 0x57, 0x20, 0x11, 0x40, 0xca, 0xae, 0x20, 0xad, 0x2a, 0x81, - 0x72, 0xa9, 0x4d, 0xe1, 0x00, 0xe2, 0xb6, 0x5d, 0x8e, 0x7b, 0x28, 0x15, 0x27, 0x2e, 0x91, 0xe3, - 0x98, 0xd6, 0x5a, 0x27, 0x13, 0xc5, 0x93, 0x20, 0xf6, 0x29, 0xd8, 0xd7, 0xe0, 0xcc, 0x43, 0xac, - 0x38, 0xed, 0x91, 0x13, 0xa0, 0xf6, 0x45, 0x50, 0x6c, 0x17, 0x8a, 0x90, 0xf6, 0x94, 0xf8, 0x9b, - 0xcf, 0xf3, 0xe7, 0xe7, 0x21, 0x89, 0xca, 0x04, 0xe3, 0x55, 0xa5, 0x95, 0xe0, 0xa8, 0xa0, 0x34, - 0x0c, 0x6b, 0x5e, 0x9a, 0x0f, 0xb2, 0x66, 0xed, 0x8c, 0xf1, 0x06, 0xd7, 0x17, 0xb4, 0xaa, 0x01, - 0x21, 0x7c, 0xa4, 0x32, 0x41, 0xf7, 0x9d, 0x74, 0xe7, 0xa4, 0xed, 0xec, 0xf8, 0x48, 0x80, 0x29, - 0xc0, 0xa4, 0xd6, 0xcb, 0xdc, 0xc1, 0x5d, 0x3c, 0x7e, 0xb0, 0x82, 0x15, 0x38, 0xbd, 0xfb, 0xf3, - 0x6a, 0xec, 0x3c, 0x2c, 0xe3, 0x46, 0xb2, 0x76, 0x96, 0x49, 0xe4, 0x33, 0x26, 0x40, 0x95, 0x2e, - 0x3e, 0xb9, 0x3c, 0x20, 0xe4, 0x44, 0x6b, 0x70, 0xc5, 0xc2, 0x11, 0x19, 0x1a, 0x68, 0x6a, 0x21, - 0xd3, 0x0a, 0x6a, 0x8c, 0x82, 0x71, 0x90, 0x0c, 0x96, 0xc4, 0x49, 0x0b, 0xa8, 0x31, 0x7c, 0x42, - 0xee, 0x7a, 0x83, 0x58, 0xf3, 0xb2, 0x94, 0x3a, 0x3a, 0xb0, 0x9e, 0x3b, 0x4e, 0x3d, 0x75, 0x62, - 0xa8, 0xc9, 0xd0, 0x54, 0xb2, 0xcc, 0x53, 0xad, 0x0a, 0x85, 0x51, 0x7f, 0xdc, 0x4f, 0x86, 0xcf, - 0x8f, 0xa8, 0x6f, 0xb8, 0x6b, 0x86, 0xfa, 0x66, 0xe8, 0x29, 0xa8, 0x72, 0xfe, 0xec, 0xea, 0xc7, - 0xa8, 0xf7, 0xe5, 0xe7, 0x28, 0x59, 0x29, 0x5c, 0x37, 0x19, 0x15, 0x50, 0xf8, 0xe9, 0xfc, 0x67, - 0x6a, 0xf2, 0x73, 0x86, 0x9f, 0x2a, 0x69, 0xec, 0x05, 0xb3, 0x24, 0x36, 0xff, 0x59, 0x97, 0x3e, - 0x7c, 0x4c, 0x08, 0xd7, 0x1a, 0x3e, 0xa6, 0x5a, 0x19, 0x8c, 0x0e, 0xc7, 0xfd, 0x64, 0xb0, 0x1c, - 0x58, 0xe5, 0x4c, 0x19, 0x0c, 0x29, 0xb9, 0x6f, 0x0f, 0x32, 0x4f, 0x2b, 0x2e, 0xce, 0x25, 0xa6, - 0x39, 0x47, 0x1e, 0xdd, 0xb2, 0xbe, 0x7b, 0x3e, 0xb4, 0xb0, 0x91, 0x37, 0x1c, 0xf9, 0xe4, 0x32, - 0x20, 0x0f, 0xdf, 0x79, 0xe8, 0x27, 0x0d, 0xae, 0xa1, 0x56, 0x17, 0x0e, 0xcf, 0x82, 0x0c, 0xf9, - 0x1f, 0x58, 0x26, 0x0a, 0xec, 0x58, 0x09, 0xbd, 0xe9, 0xc9, 0xe8, 0x5f, 0xba, 0xf3, 0xc3, 0x6e, - 0xca, 0xe5, 0x7e, 0x8a, 0xd7, 0x4f, 0xbf, 0x7d, 0x9d, 0x4e, 0x3c, 0x16, 0xb7, 0x06, 0x3b, 0x2e, - 0xff, 0x54, 0x9e, 0xbf, 0xbd, 0xda, 0xc4, 0xc1, 0xf5, 0x26, 0x0e, 0x7e, 0x6d, 0xe2, 0xe0, 0xf3, - 0x36, 0xee, 0x5d, 0x6f, 0xe3, 0xde, 0xf7, 0x6d, 0xdc, 0x7b, 0xff, 0xf2, 0x7f, 0x64, 0x2a, 0x13, - 0xd3, 0x15, 0xb0, 0xf6, 0x15, 0x2b, 0x20, 0x6f, 0xb4, 0x34, 0xdd, 0xea, 0xed, 0xad, 0x9c, 0xe5, - 0x98, 0xdd, 0xb6, 0x1b, 0xf0, 0xe2, 0x77, 0x00, 0x00, 0x00, 0xff, 0xff, 0xcc, 0xb7, 0x0f, 0x97, - 0x9c, 0x02, 0x00, 0x00, + // 496 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x84, 0x92, 0x4f, 0x8b, 0xd3, 0x40, + 0x18, 0xc6, 0x9b, 0x6d, 0x5d, 0xe8, 0x14, 0x85, 0x8d, 0x0a, 0xd9, 0x45, 0xd3, 0x5a, 0x50, 0x02, + 0xd2, 0x19, 0xab, 0x07, 0x45, 0x4f, 0xed, 0x8a, 0x78, 0xd8, 0x43, 0x2d, 0x9e, 0xbc, 0x84, 0xc9, + 0x64, 0xb6, 0x19, 0x76, 0x9a, 0x37, 0x64, 0x26, 0x5d, 0xdc, 0x4f, 0xa1, 0x5f, 0xc3, 0xb3, 0x1f, + 0x62, 0xf1, 0xb4, 0x47, 0x4f, 0x2a, 0xed, 0x87, 0xf0, 0x2a, 0x99, 0x99, 0xd6, 0xca, 0x42, 0x3d, + 0xb5, 0xf3, 0xbc, 0xbf, 0xf7, 0x5f, 0x9e, 0x17, 0x45, 0x22, 0x61, 0x84, 0x16, 0x85, 0x14, 0x8c, + 0x6a, 0x01, 0xb9, 0x22, 0xba, 0xa4, 0xb9, 0x3a, 0xe5, 0x25, 0x59, 0x0c, 0x09, 0xad, 0x74, 0x76, + 0x81, 0x8b, 0x12, 0x34, 0xf8, 0xf7, 0x44, 0xc2, 0xf0, 0x36, 0x89, 0xd7, 0x24, 0x5e, 0x0c, 0x8f, + 0x0e, 0x19, 0xa8, 0x39, 0xa8, 0xd8, 0xb0, 0xc4, 0x3e, 0x6c, 0xe2, 0xd1, 0x9d, 0x19, 0xcc, 0xc0, + 0xea, 0xf5, 0x3f, 0xa7, 0x86, 0x96, 0x21, 0x09, 0x55, 0x9c, 0x2c, 0x86, 0x09, 0xd7, 0x74, 0x48, + 0x18, 0x88, 0xdc, 0xc5, 0x1f, 0xef, 0x1c, 0x6c, 0xd3, 0xda, 0xc0, 0xfd, 0xdf, 0x7b, 0x08, 0x8d, + 0xa4, 0x04, 0x8b, 0xfa, 0x5d, 0xd4, 0x51, 0x50, 0x95, 0x8c, 0xc7, 0x05, 0x94, 0x3a, 0xf0, 0x7a, + 0x5e, 0xd4, 0x9e, 0x22, 0x2b, 0x4d, 0xa0, 0xd4, 0xfe, 0x43, 0x74, 0xcb, 0x01, 0x2c, 0xa3, 0x79, + 0xce, 0x65, 0xb0, 0x67, 0x98, 0x9b, 0x56, 0x3d, 0xb6, 0xa2, 0x2f, 0x51, 0x47, 0x15, 0x3c, 0x4f, + 0x63, 0x29, 0xe6, 0x42, 0x07, 0xcd, 0x5e, 0x33, 0xea, 0x3c, 0x3d, 0xc4, 0x6e, 0xbb, 0x7a, 0x72, + 0xec, 0x26, 0xc7, 0xc7, 0x20, 0xf2, 0xf1, 0x93, 0xcb, 0x1f, 0xdd, 0xc6, 0x97, 0x9f, 0xdd, 0x68, + 0x26, 0x74, 0x56, 0x25, 0x98, 0xc1, 0xdc, 0x7d, 0x0a, 0xf7, 0x33, 0x50, 0xe9, 0x19, 0xd1, 0x1f, + 0x0b, 0xae, 0x4c, 0x82, 0x9a, 0x22, 0x53, 0xff, 0xa4, 0x2e, 0xef, 0xdf, 0x47, 0x88, 0x4a, 0x09, + 0xe7, 0xb1, 0x14, 0x4a, 0x07, 0xad, 0x5e, 0x33, 0x6a, 0x4f, 0xdb, 0x46, 0x39, 0x11, 0x4a, 0xfb, + 0x18, 0xdd, 0x36, 0x0f, 0x9e, 0xc6, 0x05, 0x65, 0x67, 0x5c, 0xc7, 0x29, 0xd5, 0x34, 0xb8, 0x61, + 0xb8, 0x03, 0x17, 0x9a, 0x98, 0xc8, 0x6b, 0xaa, 0xa9, 0x9f, 0x22, 0x7f, 0xcd, 0x9f, 0x42, 0x79, + 0x4e, 0xcb, 0x54, 0xe4, 0xb3, 0x60, 0xdf, 0xec, 0x40, 0xf0, 0x2e, 0x33, 0xf1, 0xc8, 0xe6, 0xbd, + 0xd9, 0xa4, 0x8d, 0x5b, 0xf5, 0x66, 0x9b, 0x2e, 0x7f, 0x03, 0xfd, 0x09, 0x3a, 0xb8, 0x46, 0xfb, + 0xaf, 0x50, 0x2b, 0x83, 0x42, 0x05, 0x9e, 0x69, 0xf6, 0x60, 0x77, 0xb3, 0xb7, 0x50, 0xb8, 0xf2, + 0x26, 0xa9, 0xff, 0xd9, 0x43, 0x77, 0xdf, 0xbb, 0xf8, 0xa8, 0xd2, 0x19, 0x94, 0xe2, 0xc2, 0xda, + 0x3a, 0x41, 0x1d, 0xba, 0x31, 0x79, 0x5d, 0x3d, 0xfa, 0xff, 0x2a, 0x56, 0x77, 0x4d, 0xb6, 0x4b, + 0xbc, 0x7c, 0xf4, 0xed, 0xeb, 0xa0, 0xef, 0xec, 0xb4, 0xb7, 0xbe, 0xf6, 0xf3, 0x9f, 0xce, 0xe3, + 0x77, 0x97, 0xcb, 0xd0, 0xbb, 0x5a, 0x86, 0xde, 0xaf, 0x65, 0xe8, 0x7d, 0x5a, 0x85, 0x8d, 0xab, + 0x55, 0xd8, 0xf8, 0xbe, 0x0a, 0x1b, 0x1f, 0x9e, 0x5f, 0xb7, 0x5a, 0x24, 0x6c, 0x30, 0x03, 0xb2, + 0x78, 0x41, 0xe6, 0x90, 0x56, 0x92, 0xab, 0xfa, 0x8c, 0xb7, 0xce, 0xd7, 0xf8, 0x9f, 0xec, 0x9b, + 0xcb, 0x7d, 0xf6, 0x27, 0x00, 0x00, 0xff, 0xff, 0x79, 0x59, 0xb5, 0xa3, 0x81, 0x03, 0x00, 0x00, } func (m *Allocation) Marshal() (dAtA []byte, err error) { @@ -217,6 +277,20 @@ func (m *Allocation) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if len(m.AllowedForwarding) > 0 { + for iNdEx := len(m.AllowedForwarding) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.AllowedForwarding[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintAuthz(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x32 + } + } if len(m.AllowedPacketData) > 0 { for iNdEx := len(m.AllowedPacketData) - 1; iNdEx >= 0; iNdEx-- { i -= len(m.AllowedPacketData[iNdEx]) @@ -266,6 +340,43 @@ func (m *Allocation) MarshalToSizedBuffer(dAtA []byte) (int, error) { return len(dAtA) - i, nil } +func (m *AllowedForwarding) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *AllowedForwarding) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *AllowedForwarding) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.Hops) > 0 { + for iNdEx := len(m.Hops) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.Hops[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintAuthz(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0xa + } + } + return len(dAtA) - i, nil +} + func (m *TransferAuthorization) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) @@ -346,6 +457,27 @@ func (m *Allocation) Size() (n int) { n += 1 + l + sovAuthz(uint64(l)) } } + if len(m.AllowedForwarding) > 0 { + for _, e := range m.AllowedForwarding { + l = e.Size() + n += 1 + l + sovAuthz(uint64(l)) + } + } + return n +} + +func (m *AllowedForwarding) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if len(m.Hops) > 0 { + for _, e := range m.Hops { + l = e.Size() + n += 1 + l + sovAuthz(uint64(l)) + } + } return n } @@ -561,6 +693,124 @@ func (m *Allocation) Unmarshal(dAtA []byte) error { } m.AllowedPacketData = append(m.AllowedPacketData, string(dAtA[iNdEx:postIndex])) iNdEx = postIndex + case 6: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field AllowedForwarding", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowAuthz + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthAuthz + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthAuthz + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.AllowedForwarding = append(m.AllowedForwarding, AllowedForwarding{}) + if err := m.AllowedForwarding[len(m.AllowedForwarding)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipAuthz(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthAuthz + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *AllowedForwarding) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowAuthz + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: AllowedForwarding: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: AllowedForwarding: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Hops", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowAuthz + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthAuthz + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthAuthz + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Hops = append(m.Hops, Hop{}) + if err := m.Hops[len(m.Hops)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipAuthz(dAtA[iNdEx:]) diff --git a/modules/apps/transfer/types/denom.go b/modules/apps/transfer/types/denom.go index f6a8a6abdfb..c8fce741b6e 100644 --- a/modules/apps/transfer/types/denom.go +++ b/modules/apps/transfer/types/denom.go @@ -76,24 +76,15 @@ func (d Denom) IsNative() bool { return len(d.Trace) == 0 } -// SenderChainIsSource returns false if the denomination originally came -// from the receiving chain and true otherwise. -func (d Denom) SenderChainIsSource(sourcePort, sourceChannel string) bool { - // sender chain is source, if the receiver is not source - return !d.ReceiverChainIsSource(sourcePort, sourceChannel) -} - -// ReceiverChainIsSource returns true if the denomination originally came -// from the receiving chain and false otherwise. -func (d Denom) ReceiverChainIsSource(sourcePort, sourceChannel string) bool { - // if the denom is native, then the receiver chain cannot be source +// HasPrefix returns true if the first element of the trace of the denom +// matches the provided portId and channelId. +func (d Denom) HasPrefix(portID, channelID string) bool { + // if the denom is native, then it is not prefixed by any port/channel pair if d.IsNative() { return false } - // if the receiver chain originally sent the token to the sender chain, then the first element of - // the denom's trace (latest trace) will contain the SourcePort and SourceChannel. - return d.Trace[0].PortId == sourcePort && d.Trace[0].ChannelId == sourceChannel + return d.Trace[0].PortId == portID && d.Trace[0].ChannelId == channelID } // Denoms defines a wrapper type for a slice of Denom. diff --git a/modules/apps/transfer/types/denom_test.go b/modules/apps/transfer/types/denom_test.go index 70dcd94dfed..86c7d9b686a 100644 --- a/modules/apps/transfer/types/denom_test.go +++ b/modules/apps/transfer/types/denom_test.go @@ -172,11 +172,11 @@ func (suite *TypesTestSuite) TestSort() { func (suite *TypesTestSuite) TestDenomChainSource() { testCases := []struct { - name string - denom types.Denom - sourcePort string - sourceChannel string - expReceiverChainIsSource bool + name string + denom types.Denom + sourcePort string + sourceChannel string + expHasPrefix bool }{ { "sender chain is source: empty trace", @@ -235,8 +235,7 @@ func (suite *TypesTestSuite) TestDenomChainSource() { for _, tc := range testCases { tc := tc suite.Run(tc.name, func() { - suite.Require().Equal(tc.expReceiverChainIsSource, tc.denom.ReceiverChainIsSource(tc.sourcePort, tc.sourceChannel)) - suite.Require().Equal(!tc.expReceiverChainIsSource, tc.denom.SenderChainIsSource(tc.sourcePort, tc.sourceChannel)) + suite.Require().Equal(tc.expHasPrefix, tc.denom.HasPrefix(tc.sourcePort, tc.sourceChannel)) }) } } diff --git a/modules/apps/transfer/types/errors.go b/modules/apps/transfer/types/errors.go index 2a7d90c0fbe..fc6b0dad5c2 100644 --- a/modules/apps/transfer/types/errors.go +++ b/modules/apps/transfer/types/errors.go @@ -16,4 +16,7 @@ var ( ErrMaxTransferChannels = errorsmod.Register(ModuleName, 9, "max transfer channels") ErrInvalidAuthorization = errorsmod.Register(ModuleName, 10, "invalid transfer authorization") ErrInvalidMemo = errorsmod.Register(ModuleName, 11, "invalid memo") + ErrInvalidForwarding = errorsmod.Register(ModuleName, 12, "invalid token forwarding") + ErrForwardedPacketTimedOut = errorsmod.Register(ModuleName, 13, "forwarded packet timed out") + ErrForwardedPacketFailed = errorsmod.Register(ModuleName, 14, "forwarded packet failed") ) diff --git a/modules/apps/transfer/types/events.go b/modules/apps/transfer/types/events.go index d982298c12b..f5ad7210431 100644 --- a/modules/apps/transfer/types/events.go +++ b/modules/apps/transfer/types/events.go @@ -19,4 +19,5 @@ const ( AttributeKeyAck = "acknowledgement" AttributeKeyAckError = "error" AttributeKeyMemo = "memo" + AttributeKeyForwardingHops = "forwarding_hops" ) diff --git a/modules/apps/transfer/types/forwarding.go b/modules/apps/transfer/types/forwarding.go new file mode 100644 index 00000000000..9db76e10ad3 --- /dev/null +++ b/modules/apps/transfer/types/forwarding.go @@ -0,0 +1,85 @@ +package types + +import ( + errorsmod "cosmossdk.io/errors" + + host "github.com/cosmos/ibc-go/v8/modules/core/24-host" +) + +const MaximumNumberOfForwardingHops = 8 // denotes the maximum number of forwarding hops allowed + +// NewForwarding creates a new Forwarding instance given an unwind value and a variable number of hops. +func NewForwarding(unwind bool, hops ...Hop) Forwarding { + return Forwarding{ + Unwind: unwind, + Hops: hops, + } +} + +// Validate performs a basic validation of the Forwarding fields. +func (f Forwarding) Validate() error { + if err := validateHops(f.Hops); err != nil { + return errorsmod.Wrapf(ErrInvalidForwarding, "invalid hops in forwarding") + } + + return nil +} + +// NewForwardingPacketData creates a new ForwardingPacketData instance given a memo and a variable number of hops. +func NewForwardingPacketData(destinationMemo string, hops ...Hop) ForwardingPacketData { + return ForwardingPacketData{ + DestinationMemo: destinationMemo, + Hops: hops, + } +} + +// Validate performs a basic validation of the ForwardingPacketData fields. +func (fpd ForwardingPacketData) Validate() error { + if err := validateHops(fpd.Hops); err != nil { + return errorsmod.Wrapf(ErrInvalidForwarding, "invalid hops in forwarding packet data") + } + + if len(fpd.DestinationMemo) > MaximumMemoLength { + return errorsmod.Wrapf(ErrInvalidMemo, "memo length cannot exceed %d", MaximumMemoLength) + } + + if len(fpd.Hops) == 0 && fpd.DestinationMemo != "" { + return errorsmod.Wrap(ErrInvalidForwarding, "memo specified when forwarding packet data hops is empty") + } + + return nil +} + +// NewHop creates a Hop with the given port ID and channel ID. +func NewHop(portID, channelID string) Hop { + return Hop{portID, channelID} +} + +// Validate performs a basic validation of the Hop fields. +func (h Hop) Validate() error { + if err := host.PortIdentifierValidator(h.PortId); err != nil { + return errorsmod.Wrapf(err, "invalid hop source port ID %s", h.PortId) + } + if err := host.ChannelIdentifierValidator(h.ChannelId); err != nil { + return errorsmod.Wrapf(err, "invalid source channel ID %s", h.ChannelId) + } + + return nil +} + +// validateHops performs a basic validation of the hops. +// It checks that the number of hops does not exceed the maximum allowed and that each hop is valid. +// It will not return any errors if hops is empty. +func validateHops(hops []Hop) error { + if len(hops) > MaximumNumberOfForwardingHops { + return errorsmod.Wrapf(ErrInvalidForwarding, "number of hops cannot exceed %d", MaximumNumberOfForwardingHops) + } + + for _, hop := range hops { + if err := hop.Validate(); err != nil { + return err + } + } + + return nil +} diff --git a/modules/apps/transfer/types/forwarding_test.go b/modules/apps/transfer/types/forwarding_test.go new file mode 100644 index 00000000000..07deaa62527 --- /dev/null +++ b/modules/apps/transfer/types/forwarding_test.go @@ -0,0 +1,282 @@ +package types_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/cosmos/ibc-go/v8/modules/apps/transfer/types" + host "github.com/cosmos/ibc-go/v8/modules/core/24-host" + ibctesting "github.com/cosmos/ibc-go/v8/testing" +) + +var validHop = types.NewHop(types.PortID, ibctesting.FirstChannelID) + +func TestForwarding_Validate(t *testing.T) { + tests := []struct { + name string + forwarding types.Forwarding + expError error + }{ + { + "valid forwarding with no hops", + types.NewForwarding(false), + nil, + }, + { + "valid forwarding with hops", + types.NewForwarding(false, validHop), + nil, + }, + { + "valid forwarding with max hops", + types.NewForwarding(false, generateHops(types.MaximumNumberOfForwardingHops)...), + nil, + }, + { + "invalid forwarding with too many hops", + types.NewForwarding(false, generateHops(types.MaximumNumberOfForwardingHops+1)...), + types.ErrInvalidForwarding, + }, + { + "invalid forwarding with too short hop port ID", + types.NewForwarding( + false, + types.NewHop(invalidShortPort, ibctesting.FirstChannelID), + ), + types.ErrInvalidForwarding, + }, + { + "invalid forwarding with too long hop port ID", + types.NewForwarding( + false, + types.NewHop(invalidLongPort, ibctesting.FirstChannelID), + ), + types.ErrInvalidForwarding, + }, + { + "invalid forwarding with non-alpha hop port ID", + types.NewForwarding( + false, + types.NewHop(invalidPort, ibctesting.FirstChannelID), + ), + types.ErrInvalidForwarding, + }, + { + "invalid forwarding with too long hop channel ID", + types.NewForwarding( + false, + types.NewHop(types.PortID, invalidLongChannel), + ), + types.ErrInvalidForwarding, + }, + { + "invalid forwarding with too short hop channel ID", + types.NewForwarding( + false, + types.NewHop(types.PortID, invalidShortChannel), + ), + types.ErrInvalidForwarding, + }, + { + "invalid forwarding with non-alpha hop channel ID", + types.NewForwarding( + false, + types.NewHop(types.PortID, invalidChannel), + ), + types.ErrInvalidForwarding, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + tc := tc + + err := tc.forwarding.Validate() + + expPass := tc.expError == nil + if expPass { + require.NoError(t, err) + } else { + require.ErrorIs(t, err, tc.expError) + } + }) + } +} + +func TestForwardingPacketData_Validate(t *testing.T) { + tests := []struct { + name string + forwarding types.ForwardingPacketData + expError error + }{ + { + "valid forwarding with no hops", + types.NewForwardingPacketData(""), + nil, + }, + { + "valid forwarding with hops", + types.NewForwardingPacketData("", validHop), + nil, + }, + { + "valid forwarding with memo", + types.NewForwardingPacketData(testMemo1, validHop, validHop), + nil, + }, + { + "valid forwarding with max hops", + types.NewForwardingPacketData("", generateHops(types.MaximumNumberOfForwardingHops)...), + nil, + }, + { + "valid forwarding with max memo length", + types.NewForwardingPacketData(ibctesting.GenerateString(types.MaximumMemoLength), validHop), + nil, + }, + { + "invalid forwarding with too many hops", + types.NewForwardingPacketData("", generateHops(types.MaximumNumberOfForwardingHops+1)...), + types.ErrInvalidForwarding, + }, + { + "invalid forwarding with too long memo", + types.NewForwardingPacketData(ibctesting.GenerateString(types.MaximumMemoLength+1), validHop), + types.ErrInvalidMemo, + }, + { + "invalid forwarding with empty hops and specified memo", + types.NewForwardingPacketData("memo"), + types.ErrInvalidForwarding, + }, + { + "invalid forwarding with too short hop port ID", + types.NewForwardingPacketData( + "", + types.NewHop(invalidShortPort, ibctesting.FirstChannelID), + ), + types.ErrInvalidForwarding, + }, + { + "invalid forwarding with too long hop port ID", + types.NewForwardingPacketData( + "", + types.NewHop(invalidLongPort, ibctesting.FirstChannelID), + ), + types.ErrInvalidForwarding, + }, + { + "invalid forwarding with non-alpha hop port ID", + types.NewForwardingPacketData( + "", + types.NewHop(invalidPort, ibctesting.FirstChannelID), + ), + types.ErrInvalidForwarding, + }, + { + "invalid forwarding with too long hop channel ID", + types.NewForwardingPacketData( + "", + types.NewHop(types.PortID, invalidLongChannel), + ), + types.ErrInvalidForwarding, + }, + { + "invalid forwarding with too short hop channel ID", + types.NewForwardingPacketData( + "", + types.NewHop(types.PortID, invalidShortChannel), + ), + types.ErrInvalidForwarding, + }, + { + "invalid forwarding with non-alpha hop channel ID", + types.NewForwardingPacketData( + "", + types.NewHop(types.PortID, invalidChannel), + ), + types.ErrInvalidForwarding, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + tc := tc + + err := tc.forwarding.Validate() + + expPass := tc.expError == nil + if expPass { + require.NoError(t, err) + } else { + require.ErrorIs(t, err, tc.expError) + } + }) + } +} + +func TestValidateHop(t *testing.T) { + tests := []struct { + name string + hop types.Hop + expError error + }{ + { + "valid hop", + validHop, + nil, + }, + { + "invalid hop with too short port ID", + types.NewHop(invalidShortPort, ibctesting.FirstChannelID), + host.ErrInvalidID, + }, + { + "invalid hop with too long port ID", + types.NewHop(invalidLongPort, ibctesting.FirstChannelID), + host.ErrInvalidID, + }, + { + "invalid hop with non-alpha port ID", + types.NewHop(invalidPort, ibctesting.FirstChannelID), + host.ErrInvalidID, + }, + { + "invalid hop with too long channel ID", + types.NewHop(types.PortID, invalidLongChannel), + host.ErrInvalidID, + }, + { + "invalid hop with too short channel ID", + types.NewHop(types.PortID, invalidShortChannel), + host.ErrInvalidID, + }, + { + "invalid hop with non-alpha channel ID", + types.NewHop(types.PortID, invalidChannel), + host.ErrInvalidID, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + tc := tc + + err := tc.hop.Validate() + + expPass := tc.expError == nil + if expPass { + require.NoError(t, err) + } else { + require.ErrorIs(t, err, tc.expError) + } + }) + } +} + +// generateHops generates a slice of n correctly initialized hops. +func generateHops(n int) []types.Hop { + hops := make([]types.Hop, n) + for i := 0; i < n; i++ { + hops[i] = types.NewHop(types.PortID, ibctesting.FirstChannelID) + } + return hops +} diff --git a/modules/apps/transfer/types/keys.go b/modules/apps/transfer/types/keys.go index 6ab7fbbb3b2..1ec121d45f0 100644 --- a/modules/apps/transfer/types/keys.go +++ b/modules/apps/transfer/types/keys.go @@ -32,6 +32,8 @@ const ( KeyTotalEscrowPrefix = "totalEscrowForDenom" ParamsKey = "params" + + KeyPacketForwardPrefix = "forwardedPacket" ) const ( @@ -81,3 +83,9 @@ func GetEscrowAddress(portID, channelID string) sdk.AccAddress { func TotalEscrowForDenomKey(denom string) []byte { return []byte(fmt.Sprintf("%s/%s", KeyTotalEscrowPrefix, denom)) } + +// PacketForwardKey returns the store key under which the forwarded packet is stored +// for the provided portID, channelID, and packet sequence. +func PacketForwardKey(portID, channelID string, sequence uint64) []byte { + return []byte(fmt.Sprintf("%s/%s/%s/%d", KeyPacketForwardPrefix, portID, channelID, sequence)) +} diff --git a/modules/apps/transfer/types/msgs.go b/modules/apps/transfer/types/msgs.go index c5fd206d2c4..6b036e491c3 100644 --- a/modules/apps/transfer/types/msgs.go +++ b/modules/apps/transfer/types/msgs.go @@ -49,6 +49,7 @@ func NewMsgTransfer( tokens sdk.Coins, sender, receiver string, timeoutHeight clienttypes.Height, timeoutTimestamp uint64, memo string, + forwarding Forwarding, ) *MsgTransfer { return &MsgTransfer{ SourcePort: sourcePort, @@ -59,6 +60,7 @@ func NewMsgTransfer( TimeoutTimestamp: timeoutTimestamp, Memo: memo, Tokens: tokens, + Forwarding: forwarding, } } @@ -67,13 +69,22 @@ func NewMsgTransfer( // NOTE: The recipient addresses format is not validated as the format defined by // the chain is not known to IBC. func (msg MsgTransfer) ValidateBasic() error { - if err := host.PortIdentifierValidator(msg.SourcePort); err != nil { - return errorsmod.Wrap(err, "invalid source port ID") - } - if err := host.ChannelIdentifierValidator(msg.SourceChannel); err != nil { - return errorsmod.Wrap(err, "invalid source channel ID") + if err := msg.validateForwarding(); err != nil { + return err } + if !msg.Forwarding.Unwind { + // We verify that portID and channelID are valid IDs only if + // we are not setting unwind to true. + // In that case, validation that they are empty is performed in + // validateForwarding(). + if err := host.PortIdentifierValidator(msg.SourcePort); err != nil { + return errorsmod.Wrap(err, "invalid source port ID") + } + if err := host.ChannelIdentifierValidator(msg.SourceChannel); err != nil { + return errorsmod.Wrap(err, "invalid source channel ID") + } + } if len(msg.Tokens) == 0 && !isValidIBCCoin(msg.Token) { return errorsmod.Wrap(ibcerrors.ErrInvalidCoins, "either token or token array must be filled") } @@ -109,6 +120,36 @@ func (msg MsgTransfer) ValidateBasic() error { return nil } +// validateForwarding ensures that forwarding is set up correctly. +func (msg MsgTransfer) validateForwarding() error { + if !msg.HasForwarding() { + return nil + } + if err := msg.Forwarding.Validate(); err != nil { + return err + } + + if !msg.TimeoutHeight.IsZero() { + // when forwarding, the timeout height must not be set + return errorsmod.Wrapf(ErrInvalidPacketTimeout, "timeout height must be zero if forwarding path hops is not empty: %s, %s", msg.TimeoutHeight, msg.Forwarding.Hops) + } + + if msg.Forwarding.Unwind { + if msg.SourcePort != "" { + return errorsmod.Wrapf(ErrInvalidForwarding, "source port must be empty when unwind is set, got %s instead", msg.SourcePort) + } + if msg.SourceChannel != "" { + return errorsmod.Wrapf(ErrInvalidForwarding, "source channel must be empty when unwind is set, got %s instead", msg.SourceChannel) + } + if len(msg.GetCoins()) > 1 { + // When unwinding, we must have at most one token. + return errorsmod.Wrap(ibcerrors.ErrInvalidCoins, "cannot unwind more than one token") + } + } + + return nil +} + // GetCoins returns the tokens which will be transferred. // If MsgTransfer is populated in the Token field, only that field // will be returned in the coin array. @@ -120,6 +161,11 @@ func (msg MsgTransfer) GetCoins() sdk.Coins { return coins } +// HasForwarding determines if the transfer should be forwarded to the next hop. +func (msg MsgTransfer) HasForwarding() bool { + return len(msg.Forwarding.Hops) > 0 || msg.Forwarding.Unwind +} + // isValidIBCCoin returns true if the token provided is valid, // and should be used to transfer tokens. func isValidIBCCoin(coin sdk.Coin) bool { diff --git a/modules/apps/transfer/types/msgs_test.go b/modules/apps/transfer/types/msgs_test.go index 9c1d84ceea8..4d300f3f5ac 100644 --- a/modules/apps/transfer/types/msgs_test.go +++ b/modules/apps/transfer/types/msgs_test.go @@ -47,7 +47,8 @@ var ( invalidDenomCoins = []sdk.Coin{{Denom: "0atom", Amount: sdkmath.NewInt(100)}} zeroCoins = []sdk.Coin{{Denom: "atoms", Amount: sdkmath.NewInt(0)}} - timeoutHeight = clienttypes.NewHeight(0, 10) + timeoutHeight = clienttypes.NewHeight(0, 10) + emptyForwarding = types.Forwarding{} ) // TestMsgTransferValidation tests ValidateBasic for MsgTransfer @@ -57,48 +58,60 @@ func TestMsgTransferValidation(t *testing.T) { msg *types.MsgTransfer expError error }{ - {"valid msg with base denom", types.NewMsgTransfer(validPort, validChannel, coins, sender, receiver, timeoutHeight, 0, ""), nil}, - {"valid msg with trace hash", types.NewMsgTransfer(validPort, validChannel, ibcCoins, sender, receiver, timeoutHeight, 0, ""), nil}, - {"multidenom", types.NewMsgTransfer(validPort, validChannel, coins.Add(ibcCoins...), sender, receiver, timeoutHeight, 0, ""), nil}, - {"invalid ibc denom", types.NewMsgTransfer(validPort, validChannel, invalidIBCCoins, sender, receiver, timeoutHeight, 0, ""), ibcerrors.ErrInvalidCoins}, - {"too short port id", types.NewMsgTransfer(invalidShortPort, validChannel, coins, sender, receiver, timeoutHeight, 0, ""), host.ErrInvalidID}, - {"too long port id", types.NewMsgTransfer(invalidLongPort, validChannel, coins, sender, receiver, timeoutHeight, 0, ""), host.ErrInvalidID}, - {"port id contains non-alpha", types.NewMsgTransfer(invalidPort, validChannel, coins, sender, receiver, timeoutHeight, 0, ""), host.ErrInvalidID}, - {"too short channel id", types.NewMsgTransfer(validPort, invalidShortChannel, coins, sender, receiver, timeoutHeight, 0, ""), host.ErrInvalidID}, - {"too long channel id", types.NewMsgTransfer(validPort, invalidLongChannel, coins, sender, receiver, timeoutHeight, 0, ""), host.ErrInvalidID}, - {"too long memo", types.NewMsgTransfer(validPort, validChannel, coins, sender, receiver, timeoutHeight, 0, ibctesting.GenerateString(types.MaximumMemoLength+1)), types.ErrInvalidMemo}, - {"channel id contains non-alpha", types.NewMsgTransfer(validPort, invalidChannel, coins, sender, receiver, timeoutHeight, 0, ""), host.ErrInvalidID}, - {"invalid denom", types.NewMsgTransfer(validPort, validChannel, invalidDenomCoins, sender, receiver, timeoutHeight, 0, ""), ibcerrors.ErrInvalidCoins}, - {"zero coins", types.NewMsgTransfer(validPort, validChannel, zeroCoins, sender, receiver, timeoutHeight, 0, ""), ibcerrors.ErrInvalidCoins}, - {"missing sender address", types.NewMsgTransfer(validPort, validChannel, coins, emptyAddr, receiver, timeoutHeight, 0, ""), ibcerrors.ErrInvalidAddress}, - {"missing recipient address", types.NewMsgTransfer(validPort, validChannel, coins, sender, "", timeoutHeight, 0, ""), ibcerrors.ErrInvalidAddress}, - {"too long recipient address", types.NewMsgTransfer(validPort, validChannel, coins, sender, ibctesting.GenerateString(types.MaximumReceiverLength+1), timeoutHeight, 0, ""), ibcerrors.ErrInvalidAddress}, - {"empty coins", types.NewMsgTransfer(validPort, validChannel, sdk.NewCoins(), sender, receiver, timeoutHeight, 0, ""), ibcerrors.ErrInvalidCoins}, - {"multidenom: invalid denom", types.NewMsgTransfer(validPort, validChannel, coins.Add(invalidDenomCoins...), sender, receiver, timeoutHeight, 0, ""), ibcerrors.ErrInvalidCoins}, - {"multidenom: invalid ibc denom", types.NewMsgTransfer(validPort, validChannel, coins.Add(invalidIBCCoins...), sender, receiver, timeoutHeight, 0, ""), ibcerrors.ErrInvalidCoins}, - {"multidenom: zero coins", types.NewMsgTransfer(validPort, validChannel, zeroCoins, sender, receiver, timeoutHeight, 0, ""), ibcerrors.ErrInvalidCoins}, - {"multidenom: too many coins", types.NewMsgTransfer(validPort, validChannel, make([]sdk.Coin, types.MaximumTokensLength+1), sender, receiver, timeoutHeight, 0, ""), ibcerrors.ErrInvalidCoins}, - {"multidenom: both token and tokens are set", &types.MsgTransfer{validPort, validChannel, coin, sender, receiver, timeoutHeight, 0, "", coins}, ibcerrors.ErrInvalidCoins}, + {"valid msg with base denom", types.NewMsgTransfer(validPort, validChannel, coins, sender, receiver, clienttypes.ZeroHeight(), 100, "", emptyForwarding), nil}, + {"valid msg with unwind", types.NewMsgTransfer("", "", coins, sender, receiver, clienttypes.ZeroHeight(), 100, "", types.NewForwarding(true)), nil}, + {"valid msg with trace hash", types.NewMsgTransfer(validPort, validChannel, ibcCoins, sender, receiver, clienttypes.ZeroHeight(), 100, "", emptyForwarding), nil}, + {"multidenom", types.NewMsgTransfer(validPort, validChannel, coins.Add(ibcCoins...), sender, receiver, clienttypes.ZeroHeight(), 100, "", emptyForwarding), nil}, + {"memo with forwarding path hops not empty", types.NewMsgTransfer(validPort, validChannel, coins, sender, receiver, clienttypes.ZeroHeight(), 100, "memo", types.NewForwarding(false, validHop)), nil}, + {"memo with forwarding unwind set to true", types.NewMsgTransfer("", "", coins, sender, receiver, clienttypes.ZeroHeight(), 100, "memo", types.NewForwarding(true)), nil}, + {"invalid ibc denom", types.NewMsgTransfer(validPort, validChannel, invalidIBCCoins, sender, receiver, clienttypes.ZeroHeight(), 100, "", emptyForwarding), ibcerrors.ErrInvalidCoins}, + {"too short port id", types.NewMsgTransfer(invalidShortPort, validChannel, coins, sender, receiver, clienttypes.ZeroHeight(), 100, "", emptyForwarding), host.ErrInvalidID}, + {"too long port id", types.NewMsgTransfer(invalidLongPort, validChannel, coins, sender, receiver, clienttypes.ZeroHeight(), 100, "", emptyForwarding), host.ErrInvalidID}, + {"port id contains non-alpha", types.NewMsgTransfer(invalidPort, validChannel, coins, sender, receiver, clienttypes.ZeroHeight(), 100, "", emptyForwarding), host.ErrInvalidID}, + {"too short channel id", types.NewMsgTransfer(validPort, invalidShortChannel, coins, sender, receiver, clienttypes.ZeroHeight(), 100, "", emptyForwarding), host.ErrInvalidID}, + {"too long channel id", types.NewMsgTransfer(validPort, invalidLongChannel, coins, sender, receiver, clienttypes.ZeroHeight(), 100, "", emptyForwarding), host.ErrInvalidID}, + {"too long memo", types.NewMsgTransfer(validPort, validChannel, coins, sender, receiver, clienttypes.ZeroHeight(), 100, ibctesting.GenerateString(types.MaximumMemoLength+1), emptyForwarding), types.ErrInvalidMemo}, + {"channel id contains non-alpha", types.NewMsgTransfer(validPort, invalidChannel, coins, sender, receiver, clienttypes.ZeroHeight(), 100, "", emptyForwarding), host.ErrInvalidID}, + {"invalid denom", types.NewMsgTransfer(validPort, validChannel, invalidDenomCoins, sender, receiver, clienttypes.ZeroHeight(), 100, "", emptyForwarding), ibcerrors.ErrInvalidCoins}, + {"zero coins", types.NewMsgTransfer(validPort, validChannel, zeroCoins, sender, receiver, clienttypes.ZeroHeight(), 100, "", emptyForwarding), ibcerrors.ErrInvalidCoins}, + {"missing sender address", types.NewMsgTransfer(validPort, validChannel, coins, emptyAddr, receiver, clienttypes.ZeroHeight(), 100, "", emptyForwarding), ibcerrors.ErrInvalidAddress}, + {"missing recipient address", types.NewMsgTransfer(validPort, validChannel, coins, sender, "", clienttypes.ZeroHeight(), 100, "", emptyForwarding), ibcerrors.ErrInvalidAddress}, + {"too long recipient address", types.NewMsgTransfer(validPort, validChannel, coins, sender, ibctesting.GenerateString(types.MaximumReceiverLength+1), clienttypes.ZeroHeight(), 100, "", emptyForwarding), ibcerrors.ErrInvalidAddress}, + {"empty coins", types.NewMsgTransfer(validPort, validChannel, sdk.NewCoins(), sender, receiver, clienttypes.ZeroHeight(), 100, "", emptyForwarding), ibcerrors.ErrInvalidCoins}, + {"multidenom: invalid denom", types.NewMsgTransfer(validPort, validChannel, coins.Add(invalidDenomCoins...), sender, receiver, clienttypes.ZeroHeight(), 100, "", emptyForwarding), ibcerrors.ErrInvalidCoins}, + {"multidenom: invalid ibc denom", types.NewMsgTransfer(validPort, validChannel, coins.Add(invalidIBCCoins...), sender, receiver, clienttypes.ZeroHeight(), 100, "", emptyForwarding), ibcerrors.ErrInvalidCoins}, + {"multidenom: zero coins", types.NewMsgTransfer(validPort, validChannel, zeroCoins, sender, receiver, clienttypes.ZeroHeight(), 100, "", emptyForwarding), ibcerrors.ErrInvalidCoins}, + {"multidenom: too many coins", types.NewMsgTransfer(validPort, validChannel, make([]sdk.Coin, types.MaximumTokensLength+1), sender, receiver, clienttypes.ZeroHeight(), 100, "", emptyForwarding), ibcerrors.ErrInvalidCoins}, + {"multidenom: both token and tokens are set", &types.MsgTransfer{validPort, validChannel, coin, sender, receiver, clienttypes.ZeroHeight(), 100, "", coins, emptyForwarding}, ibcerrors.ErrInvalidCoins}, + {"timeout height must be zero if forwarding path hops is not empty", types.NewMsgTransfer(validPort, validChannel, coins, sender, receiver, timeoutHeight, 100, "memo", types.NewForwarding(false, validHop)), types.ErrInvalidPacketTimeout}, + {"invalid forwarding info port", types.NewMsgTransfer(validPort, validChannel, coins, sender, receiver, clienttypes.ZeroHeight(), 100, "", types.NewForwarding(false, types.NewHop(invalidPort, validChannel))), types.ErrInvalidForwarding}, + {"invalid forwarding info channel", types.NewMsgTransfer(validPort, validChannel, coins, sender, receiver, clienttypes.ZeroHeight(), 100, "", types.NewForwarding(false, types.NewHop(validPort, invalidChannel))), types.ErrInvalidForwarding}, + {"invalid forwarding info too many hops", types.NewMsgTransfer(validPort, validChannel, coins, sender, receiver, clienttypes.ZeroHeight(), 100, "", types.NewForwarding(false, generateHops(types.MaximumNumberOfForwardingHops+1)...)), types.ErrInvalidForwarding}, + {"invalid portID when forwarding is set but unwind is not", types.NewMsgTransfer("", validChannel, coins, sender, receiver, clienttypes.ZeroHeight(), 100, "", types.NewForwarding(false, validHop)), host.ErrInvalidID}, + {"invalid channelID when forwarding is set but unwind is not", types.NewMsgTransfer(validPort, "", coins, sender, receiver, clienttypes.ZeroHeight(), 100, "", types.NewForwarding(false, validHop)), host.ErrInvalidID}, + {"unwind specified but source port is not empty", types.NewMsgTransfer(validPort, "", coins, sender, receiver, clienttypes.ZeroHeight(), 100, "", types.NewForwarding(true)), types.ErrInvalidForwarding}, + {"unwind specified but source channel is not empty", types.NewMsgTransfer("", validChannel, coins, sender, receiver, clienttypes.ZeroHeight(), 100, "", types.NewForwarding(true)), types.ErrInvalidForwarding}, + {"unwind specified but more than one coin in the message", types.NewMsgTransfer("", "", coins.Add(sdk.NewCoin("atom", ibctesting.TestCoin.Amount)), sender, receiver, clienttypes.ZeroHeight(), 100, "", types.NewForwarding(true)), ibcerrors.ErrInvalidCoins}, } for _, tc := range testCases { - tc := tc - - err := tc.msg.ValidateBasic() - - expPass := tc.expError == nil - if expPass { - require.NoError(t, err) - } else { - require.ErrorIs(t, err, tc.expError) - } + t.Run(tc.name, func(t *testing.T) { + err := tc.msg.ValidateBasic() + + expPass := tc.expError == nil + if expPass { + require.NoError(t, err) + } else { + require.ErrorIs(t, err, tc.expError) + } + }) } } // TestMsgTransferGetSigners tests GetSigners for MsgTransfer func TestMsgTransferGetSigners(t *testing.T) { addr := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) - msg := types.NewMsgTransfer(validPort, validChannel, coins, addr.String(), receiver, timeoutHeight, 0, "") + msg := types.NewMsgTransfer(validPort, validChannel, coins, addr.String(), receiver, timeoutHeight, 0, "", emptyForwarding) encodingCfg := moduletestutil.MakeTestEncodingConfig(transfer.AppModuleBasic{}) signers, _, err := encodingCfg.Codec.GetMsgV1Signers(msg) @@ -119,16 +132,16 @@ func TestMsgUpdateParamsValidateBasic(t *testing.T) { } for _, tc := range testCases { - tc := tc - - err := tc.msg.ValidateBasic() - - expPass := tc.expError == nil - if expPass { - require.NoError(t, err) - } else { - require.ErrorIs(t, err, tc.expError) - } + t.Run(tc.name, func(t *testing.T) { + err := tc.msg.ValidateBasic() + + expPass := tc.expError == nil + if expPass { + require.NoError(t, err) + } else { + require.ErrorIs(t, err, tc.expError) + } + }) } } @@ -144,21 +157,21 @@ func TestMsgUpdateParamsGetSigners(t *testing.T) { } for _, tc := range testCases { - tc := tc - - msg := types.MsgUpdateParams{ - Signer: tc.address.String(), - Params: types.DefaultParams(), - } - - encodingCfg := moduletestutil.MakeTestEncodingConfig(transfer.AppModuleBasic{}) - signers, _, err := encodingCfg.Codec.GetMsgV1Signers(&msg) - - if tc.expPass { - require.NoError(t, err) - require.Equal(t, tc.address.Bytes(), signers[0]) - } else { - require.Error(t, err) - } + t.Run(tc.name, func(t *testing.T) { + msg := types.MsgUpdateParams{ + Signer: tc.address.String(), + Params: types.DefaultParams(), + } + + encodingCfg := moduletestutil.MakeTestEncodingConfig(transfer.AppModuleBasic{}) + signers, _, err := encodingCfg.Codec.GetMsgV1Signers(&msg) + + if tc.expPass { + require.NoError(t, err) + require.Equal(t, tc.address.Bytes(), signers[0]) + } else { + require.Error(t, err) + } + }) } } diff --git a/modules/apps/transfer/types/packet.go b/modules/apps/transfer/types/packet.go index e55b1d67ee5..7a182b8598e 100644 --- a/modules/apps/transfer/types/packet.go +++ b/modules/apps/transfer/types/packet.go @@ -103,12 +103,14 @@ func NewFungibleTokenPacketDataV2( tokens []Token, sender, receiver string, memo string, + forwarding ForwardingPacketData, ) FungibleTokenPacketDataV2 { return FungibleTokenPacketDataV2{ - Tokens: tokens, - Sender: sender, - Receiver: receiver, - Memo: memo, + Tokens: tokens, + Sender: sender, + Receiver: receiver, + Memo: memo, + Forwarding: forwarding, } } @@ -138,6 +140,15 @@ func (ftpd FungibleTokenPacketDataV2) ValidateBasic() error { return errorsmod.Wrapf(ErrInvalidMemo, "memo must not exceed %d bytes", MaximumMemoLength) } + if err := ftpd.Forwarding.Validate(); err != nil { + return err + } + + // We cannot have non-empty memo and non-empty forwarding path hops at the same time. + if ftpd.HasForwarding() && ftpd.Memo != "" { + return errorsmod.Wrapf(ErrInvalidMemo, "memo must be empty if forwarding path hops is not empty: %s, %s", ftpd.Memo, ftpd.Forwarding.Hops) + } + return nil } @@ -183,3 +194,8 @@ func (ftpd FungibleTokenPacketDataV2) GetCustomPacketData(key string) interface{ func (ftpd FungibleTokenPacketDataV2) GetPacketSender(sourcePortID string) string { return ftpd.Sender } + +// HasForwarding determines if the packet should be forwarded to the next hop. +func (ftpd FungibleTokenPacketDataV2) HasForwarding() bool { + return len(ftpd.Forwarding.Hops) > 0 +} diff --git a/modules/apps/transfer/types/packet.pb.go b/modules/apps/transfer/types/packet.pb.go index 6ad8ed05699..2622fc514bd 100644 --- a/modules/apps/transfer/types/packet.pb.go +++ b/modules/apps/transfer/types/packet.pb.go @@ -119,6 +119,8 @@ type FungibleTokenPacketDataV2 struct { Receiver string `protobuf:"bytes,3,opt,name=receiver,proto3" json:"receiver,omitempty"` // optional memo Memo string `protobuf:"bytes,4,opt,name=memo,proto3" json:"memo,omitempty"` + // optional forwarding information + Forwarding ForwardingPacketData `protobuf:"bytes,5,opt,name=forwarding,proto3" json:"forwarding"` } func (m *FungibleTokenPacketDataV2) Reset() { *m = FungibleTokenPacketDataV2{} } @@ -182,9 +184,74 @@ func (m *FungibleTokenPacketDataV2) GetMemo() string { return "" } +func (m *FungibleTokenPacketDataV2) GetForwarding() ForwardingPacketData { + if m != nil { + return m.Forwarding + } + return ForwardingPacketData{} +} + +// ForwardingPacketData defines a list of port ID, channel ID pairs determining the path +// through which a packet must be forwarded, and the destination memo string to be used in the +// final destination of the tokens. +type ForwardingPacketData struct { + // optional memo consumed by final destination chain + DestinationMemo string `protobuf:"bytes,1,opt,name=destination_memo,json=destinationMemo,proto3" json:"destination_memo,omitempty"` + // optional intermediate path through which packet will be forwarded. + Hops []Hop `protobuf:"bytes,2,rep,name=hops,proto3" json:"hops"` +} + +func (m *ForwardingPacketData) Reset() { *m = ForwardingPacketData{} } +func (m *ForwardingPacketData) String() string { return proto.CompactTextString(m) } +func (*ForwardingPacketData) ProtoMessage() {} +func (*ForwardingPacketData) Descriptor() ([]byte, []int) { + return fileDescriptor_653ca2ce9a5ca313, []int{2} +} +func (m *ForwardingPacketData) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *ForwardingPacketData) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_ForwardingPacketData.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *ForwardingPacketData) XXX_Merge(src proto.Message) { + xxx_messageInfo_ForwardingPacketData.Merge(m, src) +} +func (m *ForwardingPacketData) XXX_Size() int { + return m.Size() +} +func (m *ForwardingPacketData) XXX_DiscardUnknown() { + xxx_messageInfo_ForwardingPacketData.DiscardUnknown(m) +} + +var xxx_messageInfo_ForwardingPacketData proto.InternalMessageInfo + +func (m *ForwardingPacketData) GetDestinationMemo() string { + if m != nil { + return m.DestinationMemo + } + return "" +} + +func (m *ForwardingPacketData) GetHops() []Hop { + if m != nil { + return m.Hops + } + return nil +} + func init() { proto.RegisterType((*FungibleTokenPacketData)(nil), "ibc.applications.transfer.v2.FungibleTokenPacketData") proto.RegisterType((*FungibleTokenPacketDataV2)(nil), "ibc.applications.transfer.v2.FungibleTokenPacketDataV2") + proto.RegisterType((*ForwardingPacketData)(nil), "ibc.applications.transfer.v2.ForwardingPacketData") } func init() { @@ -192,28 +259,34 @@ func init() { } var fileDescriptor_653ca2ce9a5ca313 = []byte{ - // 329 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x51, 0x31, 0x4f, 0xeb, 0x30, - 0x18, 0x8c, 0xdb, 0xb4, 0x7a, 0xcf, 0x6f, 0xb3, 0xaa, 0xf7, 0xf2, 0x2a, 0x14, 0xaa, 0xb2, 0x94, - 0x01, 0x5b, 0x0a, 0x03, 0xac, 0x54, 0x88, 0x19, 0x2a, 0xc4, 0xc0, 0xe6, 0xb8, 0x26, 0x58, 0xad, - 0xfd, 0x45, 0xb1, 0x13, 0x89, 0x5f, 0x01, 0xbf, 0x82, 0xdf, 0xd2, 0xb1, 0x23, 0x13, 0x42, 0xed, - 0x1f, 0x41, 0x71, 0x0a, 0x74, 0x69, 0xb7, 0xbb, 0xcb, 0xe5, 0x7c, 0xf6, 0xe1, 0x63, 0x95, 0x0a, - 0xc6, 0xf3, 0x7c, 0xae, 0x04, 0x77, 0x0a, 0x8c, 0x65, 0xae, 0xe0, 0xc6, 0x3e, 0xc8, 0x82, 0x55, - 0x09, 0xcb, 0xb9, 0x98, 0x49, 0x47, 0xf3, 0x02, 0x1c, 0x90, 0x03, 0x95, 0x0a, 0xba, 0x6d, 0xa5, - 0x5f, 0x56, 0x5a, 0x25, 0xfd, 0xd1, 0xde, 0x20, 0x07, 0x33, 0x69, 0x9a, 0x9c, 0x7e, 0x2f, 0x83, - 0x0c, 0x3c, 0x64, 0x35, 0x6a, 0xd4, 0xe1, 0x33, 0xc2, 0xff, 0xae, 0x4a, 0x93, 0xa9, 0x74, 0x2e, - 0x6f, 0x6b, 0xf7, 0xb5, 0x3f, 0xfb, 0x92, 0x3b, 0x4e, 0x7a, 0xb8, 0x33, 0x95, 0x06, 0x74, 0x84, - 0x06, 0x68, 0xf4, 0x7b, 0xd2, 0x10, 0xf2, 0x17, 0x77, 0xb9, 0x86, 0xd2, 0xb8, 0xa8, 0xe5, 0xe5, - 0x0d, 0xab, 0x75, 0x2b, 0xcd, 0x54, 0x16, 0x51, 0xbb, 0xd1, 0x1b, 0x46, 0xfa, 0xf8, 0x57, 0x21, - 0x85, 0x54, 0x95, 0x2c, 0xa2, 0xd0, 0x7f, 0xf9, 0xe6, 0x84, 0xe0, 0x50, 0x4b, 0x0d, 0x51, 0xc7, - 0xeb, 0x1e, 0x0f, 0x5f, 0x11, 0xfe, 0xbf, 0xa3, 0xd1, 0x5d, 0x42, 0x2e, 0x70, 0xd7, 0x5f, 0xca, - 0x46, 0x68, 0xd0, 0x1e, 0xfd, 0x49, 0x8e, 0xe8, 0xbe, 0xe7, 0xa1, 0x3e, 0x60, 0x1c, 0x2e, 0xde, - 0x0f, 0x83, 0xc9, 0xe6, 0xc7, 0xad, 0xa2, 0xad, 0x9d, 0x45, 0xdb, 0x3b, 0x8a, 0x86, 0x3f, 0x45, - 0xc7, 0x37, 0x8b, 0x55, 0x8c, 0x96, 0xab, 0x18, 0x7d, 0xac, 0x62, 0xf4, 0xb2, 0x8e, 0x83, 0xe5, - 0x3a, 0x0e, 0xde, 0xd6, 0x71, 0x70, 0x7f, 0x96, 0x29, 0xf7, 0x58, 0xa6, 0x54, 0x80, 0x66, 0x02, - 0xac, 0x06, 0xcb, 0x54, 0x2a, 0x4e, 0x32, 0x60, 0xd5, 0x39, 0xd3, 0x30, 0x2d, 0xe7, 0xd2, 0xd6, - 0xa3, 0x6d, 0x8d, 0xe5, 0x9e, 0x72, 0x69, 0xd3, 0xae, 0x1f, 0xe5, 0xf4, 0x33, 0x00, 0x00, 0xff, - 0xff, 0x26, 0xe5, 0xca, 0x7c, 0x1f, 0x02, 0x00, 0x00, + // 418 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x92, 0x41, 0x8b, 0xd3, 0x40, + 0x14, 0xc7, 0x93, 0x34, 0x5b, 0x74, 0xf6, 0xa0, 0x0c, 0x45, 0x63, 0x91, 0xb8, 0xd6, 0x4b, 0x17, + 0x71, 0x86, 0x8d, 0x07, 0x05, 0x4f, 0x2e, 0xb2, 0x78, 0x11, 0x74, 0x11, 0x11, 0x2f, 0x32, 0x99, + 0xcc, 0x66, 0x87, 0x36, 0xf3, 0xc2, 0xcc, 0x24, 0xe2, 0x45, 0xfc, 0x06, 0xfa, 0xb1, 0x7a, 0xec, + 0xd1, 0x93, 0x48, 0xfb, 0x45, 0x24, 0x93, 0xd8, 0xe6, 0x60, 0x73, 0x7b, 0xef, 0x9f, 0xff, 0xfb, + 0xf3, 0x9b, 0x97, 0x87, 0x4e, 0x65, 0xca, 0x29, 0x2b, 0xcb, 0xa5, 0xe4, 0xcc, 0x4a, 0x50, 0x86, + 0x5a, 0xcd, 0x94, 0xb9, 0x12, 0x9a, 0xd6, 0x09, 0x2d, 0x19, 0x5f, 0x08, 0x4b, 0x4a, 0x0d, 0x16, + 0xf0, 0x7d, 0x99, 0x72, 0xd2, 0xb7, 0x92, 0x7f, 0x56, 0x52, 0x27, 0xd3, 0xf9, 0x60, 0x90, 0x85, + 0x85, 0x50, 0x6d, 0xce, 0x74, 0x92, 0x43, 0x0e, 0xae, 0xa4, 0x4d, 0xd5, 0xa9, 0x8f, 0x07, 0xe6, + 0xcf, 0x76, 0x75, 0x6b, 0x9e, 0xfd, 0xf0, 0xd1, 0xdd, 0x8b, 0x4a, 0xe5, 0x32, 0x5d, 0x8a, 0xf7, + 0x4d, 0xf4, 0x5b, 0x07, 0xfa, 0x8a, 0x59, 0x86, 0x27, 0xe8, 0x28, 0x13, 0x0a, 0x8a, 0xc8, 0x3f, + 0xf1, 0xe7, 0x37, 0x2f, 0xdb, 0x06, 0xdf, 0x41, 0x63, 0x56, 0x40, 0xa5, 0x6c, 0x14, 0x38, 0xb9, + 0xeb, 0x1a, 0xdd, 0x08, 0x95, 0x09, 0x1d, 0x8d, 0x5a, 0xbd, 0xed, 0xf0, 0x14, 0xdd, 0xd0, 0x82, + 0x0b, 0x59, 0x0b, 0x1d, 0x85, 0xee, 0xcb, 0xae, 0xc7, 0x18, 0x85, 0x85, 0x28, 0x20, 0x3a, 0x72, + 0xba, 0xab, 0x67, 0xdf, 0x03, 0x74, 0xef, 0x00, 0xd1, 0x87, 0x04, 0xbf, 0x44, 0x63, 0xb7, 0x01, + 0x13, 0xf9, 0x27, 0xa3, 0xf9, 0x71, 0xf2, 0x88, 0x0c, 0xed, 0x92, 0xb8, 0x80, 0xf3, 0x70, 0xf5, + 0xfb, 0x81, 0x77, 0xd9, 0x0d, 0xf6, 0x40, 0x83, 0x83, 0xa0, 0xa3, 0x03, 0xa0, 0xe1, 0x1e, 0x14, + 0x7f, 0x44, 0xe8, 0x0a, 0xf4, 0x17, 0xa6, 0x33, 0xa9, 0x72, 0xf7, 0x84, 0xe3, 0x24, 0x19, 0xc6, + 0xb9, 0xd8, 0xf9, 0xf7, 0x8f, 0xea, 0xe8, 0x7a, 0x59, 0xb3, 0x6f, 0x68, 0xf2, 0x3f, 0x27, 0x3e, + 0x45, 0xb7, 0x33, 0x61, 0xac, 0x54, 0x2e, 0xfa, 0xb3, 0x23, 0x6a, 0xff, 0xcd, 0xad, 0x9e, 0xfe, + 0xa6, 0x81, 0x7b, 0x81, 0xc2, 0x6b, 0x28, 0x4d, 0x14, 0xb8, 0x2d, 0x3d, 0x1c, 0xc2, 0x3a, 0x23, + 0xaf, 0xa1, 0xec, 0x28, 0xdc, 0xd0, 0xf9, 0xbb, 0xd5, 0x26, 0xf6, 0xd7, 0x9b, 0xd8, 0xff, 0xb3, + 0x89, 0xfd, 0x9f, 0xdb, 0xd8, 0x5b, 0x6f, 0x63, 0xef, 0xd7, 0x36, 0xf6, 0x3e, 0x3d, 0xcb, 0xa5, + 0xbd, 0xae, 0x52, 0xc2, 0xa1, 0xa0, 0x1c, 0x4c, 0x01, 0x86, 0xca, 0x94, 0x3f, 0xc9, 0x81, 0xd6, + 0xcf, 0x69, 0x01, 0x59, 0xb5, 0x14, 0xa6, 0xb9, 0xbd, 0xde, 0xcd, 0xd9, 0xaf, 0xa5, 0x30, 0xe9, + 0xd8, 0x9d, 0xdb, 0xd3, 0xbf, 0x01, 0x00, 0x00, 0xff, 0xff, 0xf8, 0x64, 0x17, 0xb6, 0x26, 0x03, + 0x00, 0x00, } func (m *FungibleTokenPacketData) Marshal() (dAtA []byte, err error) { @@ -294,6 +367,16 @@ func (m *FungibleTokenPacketDataV2) MarshalToSizedBuffer(dAtA []byte) (int, erro _ = i var l int _ = l + { + size, err := m.Forwarding.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintPacket(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x2a if len(m.Memo) > 0 { i -= len(m.Memo) copy(dAtA[i:], m.Memo) @@ -332,6 +415,50 @@ func (m *FungibleTokenPacketDataV2) MarshalToSizedBuffer(dAtA []byte) (int, erro return len(dAtA) - i, nil } +func (m *ForwardingPacketData) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *ForwardingPacketData) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *ForwardingPacketData) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.Hops) > 0 { + for iNdEx := len(m.Hops) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.Hops[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintPacket(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x12 + } + } + if len(m.DestinationMemo) > 0 { + i -= len(m.DestinationMemo) + copy(dAtA[i:], m.DestinationMemo) + i = encodeVarintPacket(dAtA, i, uint64(len(m.DestinationMemo))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + func encodeVarintPacket(dAtA []byte, offset int, v uint64) int { offset -= sovPacket(v) base := offset @@ -396,6 +523,27 @@ func (m *FungibleTokenPacketDataV2) Size() (n int) { if l > 0 { n += 1 + l + sovPacket(uint64(l)) } + l = m.Forwarding.Size() + n += 1 + l + sovPacket(uint64(l)) + return n +} + +func (m *ForwardingPacketData) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.DestinationMemo) + if l > 0 { + n += 1 + l + sovPacket(uint64(l)) + } + if len(m.Hops) > 0 { + for _, e := range m.Hops { + l = e.Size() + n += 1 + l + sovPacket(uint64(l)) + } + } return n } @@ -774,6 +922,155 @@ func (m *FungibleTokenPacketDataV2) Unmarshal(dAtA []byte) error { } m.Memo = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex + case 5: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Forwarding", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPacket + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthPacket + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthPacket + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.Forwarding.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipPacket(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthPacket + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *ForwardingPacketData) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPacket + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: ForwardingPacketData: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: ForwardingPacketData: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field DestinationMemo", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPacket + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthPacket + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthPacket + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.DestinationMemo = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Hops", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPacket + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthPacket + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthPacket + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Hops = append(m.Hops, Hop{}) + if err := m.Hops[len(m.Hops)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipPacket(dAtA[iNdEx:]) diff --git a/modules/apps/transfer/types/packet_test.go b/modules/apps/transfer/types/packet_test.go index d16461aaad8..ad348cfe450 100644 --- a/modules/apps/transfer/types/packet_test.go +++ b/modules/apps/transfer/types/packet_test.go @@ -19,6 +19,8 @@ const ( invalidLargeAmount = "115792089237316195423570985008687907853269984665640564039457584007913129639936" // 2^256 ) +var emptyForwardingPacketData = types.ForwardingPacketData{} + // TestFungibleTokenPacketDataValidateBasic tests ValidateBasic for FungibleTokenPacketData func TestFungibleTokenPacketDataValidateBasic(t *testing.T) { testCases := []struct { @@ -182,6 +184,7 @@ func TestFungibleTokenPacketDataV2ValidateBasic(t *testing.T) { sender, receiver, "", + emptyForwardingPacketData, ), nil, }, @@ -197,6 +200,7 @@ func TestFungibleTokenPacketDataV2ValidateBasic(t *testing.T) { sender, receiver, "memo", + emptyForwardingPacketData, ), nil, }, @@ -212,6 +216,39 @@ func TestFungibleTokenPacketDataV2ValidateBasic(t *testing.T) { sender, receiver, "memo", + emptyForwardingPacketData, + ), + nil, + }, + { + "success: valid packet with forwarding path hops", + types.NewFungibleTokenPacketDataV2( + []types.Token{ + { + Denom: types.NewDenom(denom, types.NewTrace("transfer", "channel-0"), types.NewTrace("transfer", "channel-1")), + Amount: amount, + }, + }, + sender, + receiver, + "", + types.NewForwardingPacketData("", validHop, validHop), + ), + nil, + }, + { + "success: valid packet with forwarding path hops with memo", + types.NewFungibleTokenPacketDataV2( + []types.Token{ + { + Denom: types.NewDenom(denom, types.NewTrace("transfer", "channel-0"), types.NewTrace("transfer", "channel-1")), + Amount: amount, + }, + }, + sender, + receiver, + "", + types.NewForwardingPacketData("memo", validHop), ), nil, }, @@ -227,6 +264,7 @@ func TestFungibleTokenPacketDataV2ValidateBasic(t *testing.T) { sender, receiver, "", + emptyForwardingPacketData, ), types.ErrInvalidDenomForTransfer, }, @@ -242,6 +280,7 @@ func TestFungibleTokenPacketDataV2ValidateBasic(t *testing.T) { sender, receiver, "", + emptyForwardingPacketData, ), types.ErrInvalidAmount, }, @@ -252,6 +291,7 @@ func TestFungibleTokenPacketDataV2ValidateBasic(t *testing.T) { sender, receiver, "", + emptyForwardingPacketData, ), types.ErrInvalidAmount, }, @@ -267,6 +307,7 @@ func TestFungibleTokenPacketDataV2ValidateBasic(t *testing.T) { sender, receiver, "", + emptyForwardingPacketData, ), types.ErrInvalidAmount, }, @@ -282,6 +323,7 @@ func TestFungibleTokenPacketDataV2ValidateBasic(t *testing.T) { sender, receiver, "", + emptyForwardingPacketData, ), types.ErrInvalidAmount, }, @@ -297,6 +339,7 @@ func TestFungibleTokenPacketDataV2ValidateBasic(t *testing.T) { sender, receiver, "memo", + emptyForwardingPacketData, ), types.ErrInvalidAmount, }, @@ -312,6 +355,7 @@ func TestFungibleTokenPacketDataV2ValidateBasic(t *testing.T) { "", receiver, "memo", + emptyForwardingPacketData, ), ibcerrors.ErrInvalidAddress, }, @@ -327,6 +371,7 @@ func TestFungibleTokenPacketDataV2ValidateBasic(t *testing.T) { sender, "", "", + emptyForwardingPacketData, ), ibcerrors.ErrInvalidAddress, }, @@ -342,6 +387,93 @@ func TestFungibleTokenPacketDataV2ValidateBasic(t *testing.T) { sender, receiver, ibctesting.GenerateString(types.MaximumMemoLength+1), + emptyForwardingPacketData, + ), + types.ErrInvalidMemo, + }, + { + "failure: memo must be empty if forwarding path hops is not empty", + types.NewFungibleTokenPacketDataV2( + []types.Token{ + { + Denom: types.NewDenom(denom, types.NewTrace("transfer", "channel-0"), types.NewTrace("transfer", "channel-1")), + Amount: amount, + }, + }, + sender, + receiver, + "memo", + types.NewForwardingPacketData("", validHop), + ), + types.ErrInvalidMemo, + }, + { + "failure: invalid forwarding path port ID", + types.NewFungibleTokenPacketDataV2( + []types.Token{ + { + Denom: types.NewDenom(denom, types.NewTrace("transfer", "channel-0"), types.NewTrace("transfer", "channel-1")), + Amount: amount, + }, + }, + sender, + receiver, + "", + types.NewForwardingPacketData( + "", + types.NewHop(invalidPort, "channel-1"), + ), + ), + types.ErrInvalidForwarding, + }, + { + "failure: invalid forwarding path channel ID", + types.NewFungibleTokenPacketDataV2( + []types.Token{ + { + Denom: types.NewDenom(denom, types.NewTrace("transfer", "channel-0"), types.NewTrace("transfer", "channel-1")), + Amount: amount, + }, + }, + sender, + receiver, + "", + types.NewForwardingPacketData( + "", + types.NewHop("transfer", invalidChannel), + ), + ), + types.ErrInvalidForwarding, + }, + { + "failure: invalid forwarding path too many hops", + types.NewFungibleTokenPacketDataV2( + []types.Token{ + { + Denom: types.NewDenom(denom, types.NewTrace("transfer", "channel-0"), types.NewTrace("transfer", "channel-1")), + Amount: amount, + }, + }, + sender, + receiver, + "", + types.NewForwardingPacketData("", generateHops(types.MaximumNumberOfForwardingHops+1)...), + ), + types.ErrInvalidForwarding, + }, + { + "failure: invalid forwarding path too long memo", + types.NewFungibleTokenPacketDataV2( + []types.Token{ + { + Denom: types.NewDenom(denom, types.NewTrace("transfer", "channel-0"), types.NewTrace("transfer", "channel-1")), + Amount: amount, + }, + }, + sender, + receiver, + "", + types.NewForwardingPacketData(ibctesting.GenerateString(types.MaximumMemoLength+1), validHop), ), types.ErrInvalidMemo, }, @@ -379,6 +511,7 @@ func TestGetPacketSender(t *testing.T) { sender, receiver, "", + emptyForwardingPacketData, ), sender, }, @@ -394,6 +527,7 @@ func TestGetPacketSender(t *testing.T) { "", receiver, "abc", + emptyForwardingPacketData, ), "", }, @@ -423,7 +557,9 @@ func TestPacketDataProvider(t *testing.T) { }, sender, receiver, - fmt.Sprintf(`{"src_callback": {"address": "%s"}}`, receiver)), + fmt.Sprintf(`{"src_callback": {"address": "%s"}}`, receiver), + emptyForwardingPacketData, + ), map[string]interface{}{ "address": receiver, @@ -440,7 +576,9 @@ func TestPacketDataProvider(t *testing.T) { }, sender, receiver, - fmt.Sprintf(`{"src_callback": {"address": "%s", "gas_limit": "200000"}}`, receiver)), + fmt.Sprintf(`{"src_callback": {"address": "%s", "gas_limit": "200000"}}`, receiver), + emptyForwardingPacketData, + ), map[string]interface{}{ "address": receiver, "gas_limit": "200000", @@ -457,7 +595,9 @@ func TestPacketDataProvider(t *testing.T) { }, sender, receiver, - `{"src_callback": "string"}`), + `{"src_callback": "string"}`, + emptyForwardingPacketData, + ), "string", }, { @@ -471,7 +611,9 @@ func TestPacketDataProvider(t *testing.T) { }, sender, receiver, - fmt.Sprintf(`{"dest_callback": {"address": "%s", "min_gas": "200000"}}`, receiver)), + fmt.Sprintf(`{"dest_callback": {"address": "%s", "min_gas": "200000"}}`, receiver), + emptyForwardingPacketData, + ), nil, }, { @@ -485,7 +627,9 @@ func TestPacketDataProvider(t *testing.T) { }, sender, receiver, - ""), + "", + emptyForwardingPacketData, + ), nil, }, { @@ -499,7 +643,9 @@ func TestPacketDataProvider(t *testing.T) { }, sender, receiver, - "invalid"), + "invalid", + emptyForwardingPacketData, + ), nil, }, } @@ -530,6 +676,7 @@ func TestFungibleTokenPacketDataOmitEmpty(t *testing.T) { sender, receiver, "", + emptyForwardingPacketData, ), false, }, @@ -545,6 +692,7 @@ func TestFungibleTokenPacketDataOmitEmpty(t *testing.T) { sender, receiver, "abc", + emptyForwardingPacketData, ), true, }, diff --git a/modules/apps/transfer/types/token.go b/modules/apps/transfer/types/token.go index 58a1eec402f..0f8b361516d 100644 --- a/modules/apps/transfer/types/token.go +++ b/modules/apps/transfer/types/token.go @@ -3,6 +3,8 @@ package types import ( errorsmod "cosmossdk.io/errors" sdkmath "cosmossdk.io/math" + + sdk "github.com/cosmos/cosmos-sdk/types" ) // Tokens is a slice of Tokens @@ -25,3 +27,18 @@ func (t Token) Validate() error { 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 +} diff --git a/modules/apps/transfer/types/token_test.go b/modules/apps/transfer/types/token_test.go index 85f529a2e50..3c9a2f550d6 100644 --- a/modules/apps/transfer/types/token_test.go +++ b/modules/apps/transfer/types/token_test.go @@ -5,6 +5,10 @@ import ( "testing" "github.com/stretchr/testify/require" + + sdkmath "cosmossdk.io/math" + + sdk "github.com/cosmos/cosmos-sdk/types" ) const ( @@ -149,3 +153,52 @@ func TestValidate(t *testing.T) { }) } } + +func TestToCoin(t *testing.T) { + testCases := []struct { + name string + token Token + expCoin sdk.Coin + expError error + }{ + { + "success: convert token to coin", + Token{ + Denom: Denom{ + Base: denom, + Trace: []Trace{}, + }, + Amount: amount, + }, + sdk.NewCoin(denom, sdkmath.NewInt(100)), + nil, + }, + { + "failure: invalid amount string", + Token{ + Denom: Denom{ + Base: denom, + Trace: []Trace{}, + }, + Amount: "value", + }, + sdk.Coin{}, + ErrInvalidAmount, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + coin, err := tc.token.ToCoin() + + require.Equal(t, tc.expCoin, coin, tc.name) + + expPass := tc.expError == nil + if expPass { + require.NoError(t, err, tc.name) + } else { + require.ErrorContains(t, err, tc.expError.Error(), tc.name) + } + }) + } +} diff --git a/modules/apps/transfer/types/transfer.pb.go b/modules/apps/transfer/types/transfer.pb.go index 18086ea38a4..f2825d90088 100644 --- a/modules/apps/transfer/types/transfer.pb.go +++ b/modules/apps/transfer/types/transfer.pb.go @@ -5,6 +5,7 @@ package types import ( fmt "fmt" + _ "github.com/cosmos/gogoproto/gogoproto" proto "github.com/cosmos/gogoproto/proto" io "io" math "math" @@ -82,8 +83,121 @@ func (m *Params) GetReceiveEnabled() bool { return false } +// Forwarding defines a list of port ID, channel ID pairs determining the path +// through which a packet must be forwarded, and an unwind boolean indicating if +// the coin should be unwinded to its native chain before forwarding. +type Forwarding struct { + // optional unwinding for the token transfered + Unwind bool `protobuf:"varint,1,opt,name=unwind,proto3" json:"unwind,omitempty"` + // optional intermediate path through which packet will be forwarded + Hops []Hop `protobuf:"bytes,2,rep,name=hops,proto3" json:"hops"` +} + +func (m *Forwarding) Reset() { *m = Forwarding{} } +func (m *Forwarding) String() string { return proto.CompactTextString(m) } +func (*Forwarding) ProtoMessage() {} +func (*Forwarding) Descriptor() ([]byte, []int) { + return fileDescriptor_5041673e96e97901, []int{1} +} +func (m *Forwarding) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *Forwarding) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_Forwarding.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *Forwarding) XXX_Merge(src proto.Message) { + xxx_messageInfo_Forwarding.Merge(m, src) +} +func (m *Forwarding) XXX_Size() int { + return m.Size() +} +func (m *Forwarding) XXX_DiscardUnknown() { + xxx_messageInfo_Forwarding.DiscardUnknown(m) +} + +var xxx_messageInfo_Forwarding proto.InternalMessageInfo + +func (m *Forwarding) GetUnwind() bool { + if m != nil { + return m.Unwind + } + return false +} + +func (m *Forwarding) GetHops() []Hop { + if m != nil { + return m.Hops + } + return nil +} + +// Hop defines a port ID, channel ID pair specifying where tokens must be forwarded +// next in a multihop transfer. +type Hop struct { + PortId string `protobuf:"bytes,1,opt,name=port_id,json=portId,proto3" json:"port_id,omitempty"` + ChannelId string `protobuf:"bytes,2,opt,name=channel_id,json=channelId,proto3" json:"channel_id,omitempty"` +} + +func (m *Hop) Reset() { *m = Hop{} } +func (m *Hop) String() string { return proto.CompactTextString(m) } +func (*Hop) ProtoMessage() {} +func (*Hop) Descriptor() ([]byte, []int) { + return fileDescriptor_5041673e96e97901, []int{2} +} +func (m *Hop) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *Hop) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_Hop.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *Hop) XXX_Merge(src proto.Message) { + xxx_messageInfo_Hop.Merge(m, src) +} +func (m *Hop) XXX_Size() int { + return m.Size() +} +func (m *Hop) XXX_DiscardUnknown() { + xxx_messageInfo_Hop.DiscardUnknown(m) +} + +var xxx_messageInfo_Hop proto.InternalMessageInfo + +func (m *Hop) GetPortId() string { + if m != nil { + return m.PortId + } + return "" +} + +func (m *Hop) GetChannelId() string { + if m != nil { + return m.ChannelId + } + return "" +} + func init() { proto.RegisterType((*Params)(nil), "ibc.applications.transfer.v1.Params") + proto.RegisterType((*Forwarding)(nil), "ibc.applications.transfer.v1.Forwarding") + proto.RegisterType((*Hop)(nil), "ibc.applications.transfer.v1.Hop") } func init() { @@ -91,21 +205,28 @@ func init() { } var fileDescriptor_5041673e96e97901 = []byte{ - // 214 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xd2, 0xce, 0x4c, 0x4a, 0xd6, - 0x4f, 0x2c, 0x28, 0xc8, 0xc9, 0x4c, 0x4e, 0x2c, 0xc9, 0xcc, 0xcf, 0x2b, 0xd6, 0x2f, 0x29, 0x4a, - 0xcc, 0x2b, 0x4e, 0x4b, 0x2d, 0xd2, 0x2f, 0x33, 0x84, 0xb3, 0xf5, 0x0a, 0x8a, 0xf2, 0x4b, 0xf2, - 0x85, 0x64, 0x32, 0x93, 0x92, 0xf5, 0x90, 0x15, 0xeb, 0xc1, 0x15, 0x94, 0x19, 0x2a, 0x85, 0x70, - 0xb1, 0x05, 0x24, 0x16, 0x25, 0xe6, 0x16, 0x0b, 0x29, 0x72, 0xf1, 0x14, 0xa7, 0xe6, 0xa5, 0xc4, - 0xa7, 0xe6, 0x25, 0x26, 0xe5, 0xa4, 0xa6, 0x48, 0x30, 0x2a, 0x30, 0x6a, 0x70, 0x04, 0x71, 0x83, - 0xc4, 0x5c, 0x21, 0x42, 0x42, 0xea, 0x5c, 0xfc, 0x45, 0xa9, 0xc9, 0xa9, 0x99, 0x65, 0xa9, 0x70, - 0x55, 0x4c, 0x60, 0x55, 0x7c, 0x50, 0x61, 0xa8, 0x42, 0xa7, 0xc0, 0x13, 0x8f, 0xe4, 0x18, 0x2f, - 0x3c, 0x92, 0x63, 0x7c, 0xf0, 0x48, 0x8e, 0x71, 0xc2, 0x63, 0x39, 0x86, 0x0b, 0x8f, 0xe5, 0x18, - 0x6e, 0x3c, 0x96, 0x63, 0x88, 0x32, 0x4f, 0xcf, 0x2c, 0xc9, 0x28, 0x4d, 0xd2, 0x4b, 0xce, 0xcf, - 0xd5, 0x4f, 0xce, 0x2f, 0xce, 0xcd, 0x2f, 0xd6, 0xcf, 0x4c, 0x4a, 0xd6, 0x4d, 0xcf, 0xd7, 0x2f, - 0xb3, 0xd0, 0xcf, 0xcd, 0x4f, 0x29, 0xcd, 0x49, 0x2d, 0x06, 0x79, 0x0d, 0xc9, 0x4b, 0x25, 0x95, - 0x05, 0xa9, 0xc5, 0x49, 0x6c, 0x60, 0xdf, 0x18, 0x03, 0x02, 0x00, 0x00, 0xff, 0xff, 0xc7, 0x93, - 0x43, 0xf8, 0xfc, 0x00, 0x00, 0x00, + // 323 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x91, 0x41, 0x4b, 0xc3, 0x30, + 0x14, 0xc7, 0xdb, 0x6d, 0x54, 0x97, 0x89, 0x42, 0x10, 0x1d, 0xa2, 0x75, 0xdb, 0xc5, 0x81, 0xd8, + 0x30, 0x3d, 0x28, 0x88, 0x97, 0x81, 0xb2, 0xdd, 0x74, 0x78, 0xf2, 0x32, 0xd2, 0x34, 0x76, 0x81, + 0x36, 0x2f, 0x24, 0x59, 0x87, 0xdf, 0xc2, 0x8f, 0xb5, 0xe3, 0x8e, 0x9e, 0x44, 0xb6, 0x2f, 0x22, + 0xed, 0xea, 0xd8, 0xc9, 0xdb, 0x7b, 0xbf, 0xf7, 0x7b, 0x8f, 0x90, 0x3f, 0xba, 0x14, 0x21, 0x23, + 0x54, 0xa9, 0x44, 0x30, 0x6a, 0x05, 0x48, 0x43, 0xac, 0xa6, 0xd2, 0xbc, 0x73, 0x4d, 0xb2, 0xde, + 0xa6, 0x0e, 0x94, 0x06, 0x0b, 0xf8, 0x54, 0x84, 0x2c, 0xd8, 0x96, 0x83, 0x8d, 0x90, 0xf5, 0x4e, + 0x0e, 0x63, 0x88, 0xa1, 0x10, 0x49, 0x5e, 0xad, 0x77, 0x3a, 0xaf, 0xc8, 0x7b, 0xa6, 0x9a, 0xa6, + 0x06, 0xb7, 0xd1, 0x9e, 0xe1, 0x32, 0x1a, 0x73, 0x49, 0xc3, 0x84, 0x47, 0x4d, 0xb7, 0xe5, 0x76, + 0x77, 0x47, 0x8d, 0x9c, 0x3d, 0xae, 0x11, 0xbe, 0x40, 0x07, 0x9a, 0x33, 0x2e, 0x32, 0xbe, 0xb1, + 0x2a, 0x85, 0xb5, 0x5f, 0xe2, 0x52, 0xec, 0x50, 0x84, 0x9e, 0x40, 0xcf, 0xa8, 0x8e, 0x84, 0x8c, + 0xf1, 0x11, 0xf2, 0xa6, 0x72, 0x26, 0xe4, 0xdf, 0xcd, 0xb2, 0xc3, 0xf7, 0xa8, 0x36, 0x01, 0x65, + 0x9a, 0x95, 0x56, 0xb5, 0xdb, 0xb8, 0x6e, 0x07, 0xff, 0x3d, 0x3f, 0x18, 0x80, 0xea, 0xd7, 0xe6, + 0xdf, 0xe7, 0xce, 0xa8, 0x58, 0xea, 0x3c, 0xa0, 0xea, 0x00, 0x14, 0x3e, 0x46, 0x3b, 0x0a, 0xb4, + 0x1d, 0x8b, 0xf5, 0xf1, 0xfa, 0xc8, 0xcb, 0xdb, 0x61, 0x84, 0xcf, 0x10, 0x62, 0x13, 0x2a, 0x25, + 0x4f, 0xf2, 0x59, 0xa5, 0x98, 0xd5, 0x4b, 0x32, 0x8c, 0xfa, 0x2f, 0xf3, 0xa5, 0xef, 0x2e, 0x96, + 0xbe, 0xfb, 0xb3, 0xf4, 0xdd, 0xcf, 0x95, 0xef, 0x2c, 0x56, 0xbe, 0xf3, 0xb5, 0xf2, 0x9d, 0xb7, + 0xdb, 0x58, 0xd8, 0xc9, 0x34, 0x0c, 0x18, 0xa4, 0x84, 0x81, 0x49, 0xc1, 0x10, 0x11, 0xb2, 0xab, + 0x18, 0x48, 0x76, 0x47, 0x52, 0x88, 0xa6, 0x09, 0x37, 0x79, 0x24, 0x5b, 0x51, 0xd8, 0x0f, 0xc5, + 0x4d, 0xe8, 0x15, 0x3f, 0x7a, 0xf3, 0x1b, 0x00, 0x00, 0xff, 0xff, 0x94, 0x05, 0x3d, 0x15, 0xb4, + 0x01, 0x00, 0x00, } func (m *Params) Marshal() (dAtA []byte, err error) { @@ -151,6 +272,90 @@ func (m *Params) MarshalToSizedBuffer(dAtA []byte) (int, error) { return len(dAtA) - i, nil } +func (m *Forwarding) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *Forwarding) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *Forwarding) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.Hops) > 0 { + for iNdEx := len(m.Hops) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.Hops[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintTransfer(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x12 + } + } + if m.Unwind { + i-- + if m.Unwind { + dAtA[i] = 1 + } else { + dAtA[i] = 0 + } + i-- + dAtA[i] = 0x8 + } + return len(dAtA) - i, nil +} + +func (m *Hop) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *Hop) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *Hop) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.ChannelId) > 0 { + i -= len(m.ChannelId) + copy(dAtA[i:], m.ChannelId) + i = encodeVarintTransfer(dAtA, i, uint64(len(m.ChannelId))) + i-- + dAtA[i] = 0x12 + } + if len(m.PortId) > 0 { + i -= len(m.PortId) + copy(dAtA[i:], m.PortId) + i = encodeVarintTransfer(dAtA, i, uint64(len(m.PortId))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + func encodeVarintTransfer(dAtA []byte, offset int, v uint64) int { offset -= sovTransfer(v) base := offset @@ -177,6 +382,41 @@ func (m *Params) Size() (n int) { return n } +func (m *Forwarding) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.Unwind { + n += 2 + } + if len(m.Hops) > 0 { + for _, e := range m.Hops { + l = e.Size() + n += 1 + l + sovTransfer(uint64(l)) + } + } + return n +} + +func (m *Hop) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.PortId) + if l > 0 { + n += 1 + l + sovTransfer(uint64(l)) + } + l = len(m.ChannelId) + if l > 0 { + n += 1 + l + sovTransfer(uint64(l)) + } + return n +} + func sovTransfer(x uint64) (n int) { return (math_bits.Len64(x|1) + 6) / 7 } @@ -273,6 +513,224 @@ func (m *Params) Unmarshal(dAtA []byte) error { } return nil } +func (m *Forwarding) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTransfer + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: Forwarding: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: Forwarding: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Unwind", wireType) + } + var v int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTransfer + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + m.Unwind = bool(v != 0) + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Hops", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTransfer + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthTransfer + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthTransfer + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Hops = append(m.Hops, Hop{}) + if err := m.Hops[len(m.Hops)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipTransfer(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthTransfer + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *Hop) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTransfer + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: Hop: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: Hop: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field PortId", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTransfer + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthTransfer + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthTransfer + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.PortId = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ChannelId", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTransfer + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthTransfer + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthTransfer + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.ChannelId = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipTransfer(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthTransfer + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func skipTransfer(dAtA []byte) (n int, err error) { l := len(dAtA) iNdEx := 0 diff --git a/modules/apps/transfer/types/transfer_authorization.go b/modules/apps/transfer/types/transfer_authorization.go index 01bcafb48c6..7b73d064a1b 100644 --- a/modules/apps/transfer/types/transfer_authorization.go +++ b/modules/apps/transfer/types/transfer_authorization.go @@ -49,7 +49,11 @@ func (a TransferAuthorization) Accept(goCtx context.Context, msg proto.Message) index := getAllocationIndex(*msgTransfer, a.Allocations) if index == allocationNotFound { - return authz.AcceptResponse{}, errorsmod.Wrapf(ibcerrors.ErrNotFound, "requested port and channel allocation does not exist") + return authz.AcceptResponse{}, errorsmod.Wrap(ibcerrors.ErrNotFound, "requested port and channel allocation does not exist") + } + + if err := validateForwarding(msgTransfer.Forwarding, a.Allocations[index].AllowedForwarding); err != nil { + return authz.AcceptResponse{}, err } ctx := sdk.UnwrapSDKContext(goCtx) @@ -58,8 +62,7 @@ func (a TransferAuthorization) Accept(goCtx context.Context, msg proto.Message) return authz.AcceptResponse{}, errorsmod.Wrap(ibcerrors.ErrInvalidAddress, "not allowed receiver address for transfer") } - err := validateMemo(ctx, msgTransfer.Memo, a.Allocations[index].AllowedPacketData) - if err != nil { + if err := validateMemo(ctx, msgTransfer.Memo, a.Allocations[index].AllowedPacketData); err != nil { return authz.AcceptResponse{}, err } @@ -143,6 +146,14 @@ func (a TransferAuthorization) ValidateBasic() error { } found[allocation.AllowList[i]] = true } + + for i := 0; i < len(allocation.AllowedForwarding); i++ { + for _, hop := range allocation.AllowedForwarding[i].Hops { + if err := hop.Validate(); err != nil { + return errorsmod.Wrap(err, "invalid forwarding hop") + } + } + } } return nil @@ -166,6 +177,38 @@ func isAllowedAddress(ctx sdk.Context, receiver string, allowedAddrs []string) b return false } +// validateForwarding performs the validation of forwarding info. +func validateForwarding(forwarding Forwarding, allowedForwarding []AllowedForwarding) error { + if forwarding.Unwind { + return errorsmod.Wrap(ErrInvalidForwarding, "not allowed automatic unwind") + } + + if !isAllowedForwarding(forwarding.Hops, allowedForwarding) { + return errorsmod.Wrapf(ErrInvalidForwarding, "not allowed hops %s", forwarding.Hops) + } + + return nil +} + +// isAllowedForwarding returns whether the provided slice of Hop matches one of the allowed ones. +func isAllowedForwarding(hops []Hop, allowed []AllowedForwarding) bool { + if len(hops) == 0 { + return true + } + + // We want to ensure that at least one of the Hops in "allowed" + // is equal to "hops". + // Note that we can't use slices.Contains() as that is a generic + // function that requires the type Hop to satisfy the "comparable" constraint. + for _, allowedHops := range allowed { + if slices.Equal(hops, allowedHops.Hops) { + return true + } + } + + return false +} + // validateMemo returns a nil error indicating if the memo is valid for transfer. func validateMemo(ctx sdk.Context, memo string, allowedMemos []string) error { // if the allow list is empty, then the memo must be an empty string diff --git a/modules/apps/transfer/types/transfer_authorization_test.go b/modules/apps/transfer/types/transfer_authorization_test.go index a6a02d41369..a07fca30652 100644 --- a/modules/apps/transfer/types/transfer_authorization_test.go +++ b/modules/apps/transfer/types/transfer_authorization_test.go @@ -19,6 +19,8 @@ const ( testMemo2 = `{"forward":{"channel":"channel-11","port":"transfer","receiver":"stars1twfv52yxcyykx2lcvgl42svw46hsm5dd4ww6xy","retries":2,"timeout":1712146014542131200}}` ) +var forwardingWithValidHop = []types.AllowedForwarding{{Hops: []types.Hop{validHop}}} + func (suite *TypesTestSuite) TestTransferAuthorizationAccept() { var ( msgTransfer *types.MsgTransfer @@ -99,6 +101,22 @@ func (suite *TypesTestSuite) TestTransferAuthorizationAccept() { suite.Require().Nil(res.Updated) }, }, + { + "success: empty AllowedPacketData and empty memo in forwarding path", + func() { + allowedList := []string{} + transferAuthz.Allocations[0].AllowedPacketData = allowedList + transferAuthz.Allocations[0].AllowedForwarding = forwardingWithValidHop + msgTransfer.Forwarding = types.NewForwarding(false, validHop) + }, + func(res authz.AcceptResponse, err error) { + suite.Require().NoError(err) + + suite.Require().True(res.Accept) + suite.Require().True(res.Delete) + suite.Require().Nil(res.Updated) + }, + }, { "success: AllowedPacketData allows any packet", func() { @@ -114,6 +132,22 @@ func (suite *TypesTestSuite) TestTransferAuthorizationAccept() { suite.Require().Nil(res.Updated) }, }, + { + "success: AllowedPacketData allows any packet in forwarding path", + func() { + allowedList := []string{"*"} + transferAuthz.Allocations[0].AllowedPacketData = allowedList + transferAuthz.Allocations[0].AllowedForwarding = forwardingWithValidHop + msgTransfer.Forwarding = types.NewForwarding(false, validHop) + }, + func(res authz.AcceptResponse, err error) { + suite.Require().NoError(err) + + suite.Require().True(res.Accept) + suite.Require().True(res.Delete) + suite.Require().Nil(res.Updated) + }, + }, { "success: transfer memo allowed", func() { @@ -231,6 +265,7 @@ func (suite *TypesTestSuite) TestTransferAuthorizationAccept() { suite.chainB.GetTimeoutHeight(), 0, "", + emptyForwarding, ) }, func(res authz.AcceptResponse, err error) { @@ -246,6 +281,41 @@ func (suite *TypesTestSuite) TestTransferAuthorizationAccept() { suite.Require().Len(updatedAuthz.Allocations, 1) }, }, + { + "success: allowed forwarding hops", + func() { + msgTransfer.Forwarding = types.NewForwarding(false, types.NewHop(ibctesting.MockPort, "channel-1"), types.NewHop(ibctesting.MockPort, "channel-2")) + transferAuthz.Allocations[0].AllowedForwarding = []types.AllowedForwarding{ + { + Hops: []types.Hop{ + types.NewHop(ibctesting.MockPort, "channel-1"), + types.NewHop(ibctesting.MockPort, "channel-2"), + }, + }, + } + }, + func(res authz.AcceptResponse, err error) { + suite.Require().NoError(err) + suite.Require().True(res.Accept) + }, + }, + { + "success: Allocation specify hops but msgTransfer does not have hops", + func() { + transferAuthz.Allocations[0].AllowedForwarding = []types.AllowedForwarding{ + { + Hops: []types.Hop{ + types.NewHop(ibctesting.MockPort, "channel-1"), + types.NewHop(ibctesting.MockPort, "channel-2"), + }, + }, + } + }, + func(res authz.AcceptResponse, err error) { + suite.Require().NoError(err) + suite.Require().True(res.Accept) + }, + }, { "failure: multidenom transfer spend limit is exceeded", func() { @@ -273,6 +343,7 @@ func (suite *TypesTestSuite) TestTransferAuthorizationAccept() { suite.chainB.GetTimeoutHeight(), 0, "", + emptyForwarding, ) }, func(res authz.AcceptResponse, err error) { @@ -309,6 +380,7 @@ func (suite *TypesTestSuite) TestTransferAuthorizationAccept() { suite.chainB.GetTimeoutHeight(), 0, "", + emptyForwarding, ) }, func(res authz.AcceptResponse, err error) { @@ -318,6 +390,93 @@ func (suite *TypesTestSuite) TestTransferAuthorizationAccept() { suite.Require().Nil(res.Updated) }, }, + { + "failure: allowed forwarding hops contains more hops", + func() { + msgTransfer.Forwarding = types.NewForwarding(false, + types.NewHop(ibctesting.MockPort, "channel-1"), + types.NewHop(ibctesting.MockPort, "channel-2"), + types.NewHop(ibctesting.MockPort, "channel-3"), + ) + transferAuthz.Allocations[0].AllowedForwarding = []types.AllowedForwarding{ + { + Hops: []types.Hop{ + types.NewHop(ibctesting.MockPort, "channel-1"), + types.NewHop(ibctesting.MockPort, "channel-2"), + }, + }, + } + }, + func(res authz.AcceptResponse, err error) { + suite.Require().Error(err) + suite.Require().False(res.Accept) + }, + }, + { + "failure: allowed forwarding hops contains one different hop", + func() { + msgTransfer.Forwarding = types.NewForwarding(false, + types.NewHop(ibctesting.MockPort, "channel-1"), + types.NewHop("3", "channel-3"), + ) + transferAuthz.Allocations[0].AllowedForwarding = []types.AllowedForwarding{ + { + Hops: []types.Hop{ + types.NewHop(ibctesting.MockPort, "channel-1"), + types.NewHop(ibctesting.MockPort, "channel-2"), + }, + }, + } + }, + func(res authz.AcceptResponse, err error) { + suite.Require().Error(err) + suite.Require().False(res.Accept) + }, + }, + { + "failure: allowed forwarding hops is empty but hops are present", + func() { + msgTransfer.Forwarding = types.NewForwarding(false, + types.NewHop(ibctesting.MockPort, "channel-1"), + ) + }, + func(res authz.AcceptResponse, err error) { + suite.Require().Error(err) + suite.Require().False(res.Accept) + }, + }, + { + "failure: order of hops is different", + func() { + msgTransfer.Forwarding = types.NewForwarding(false, + types.NewHop(ibctesting.MockPort, "channel-1"), + types.NewHop(ibctesting.MockPort, "channel-2"), + ) + transferAuthz.Allocations[0].AllowedForwarding = []types.AllowedForwarding{ + { + Hops: []types.Hop{ + types.NewHop(ibctesting.MockPort, "channel-2"), + types.NewHop(ibctesting.MockPort, "channel-1"), + }, + }, + } + }, + func(res authz.AcceptResponse, err error) { + suite.Require().Error(err) + suite.Require().False(res.Accept) + }, + }, + + { + "failure: unwind is not allowed", + func() { + msgTransfer.Forwarding.Unwind = true + }, + func(res authz.AcceptResponse, err error) { + suite.Require().Error(err) + suite.Require().False(res.Accept) + }, + }, } for _, tc := range testCases { @@ -349,6 +508,7 @@ func (suite *TypesTestSuite) TestTransferAuthorizationAccept() { suite.chainB.GetTimeoutHeight(), 0, "", + emptyForwarding, ) tc.malleate() @@ -390,7 +550,7 @@ func (suite *TypesTestSuite) TestTransferAuthorizationValidateBasic() { allocation := types.Allocation{ SourcePort: types.PortID, SourceChannel: "channel-1", - SpendLimit: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(100))), + SpendLimit: sdk.NewCoins(ibctesting.TestCoin), AllowList: []string{}, } @@ -412,6 +572,16 @@ func (suite *TypesTestSuite) TestTransferAuthorizationValidateBasic() { }, true, }, + { + "success: with allowed forwarding hops", + func() { + transferAuthz.Allocations[0].AllowedForwarding = []types.AllowedForwarding{ + {Hops: []types.Hop{validHop}}, + {Hops: []types.Hop{types.NewHop(types.PortID, "channel-1")}}, + } + }, + true, + }, { "empty allocations", func() { @@ -462,18 +632,38 @@ func (suite *TypesTestSuite) TestTransferAuthorizationValidateBasic() { false, }, { - name: "duplicate channel ID", - malleate: func() { + "duplicate channel ID", + func() { allocation := types.Allocation{ SourcePort: mock.PortID, SourceChannel: transferAuthz.Allocations[0].SourceChannel, - SpendLimit: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(100))), + SpendLimit: sdk.NewCoins(ibctesting.TestCoin), AllowList: []string{ibctesting.TestAccAddress}, } transferAuthz.Allocations = append(transferAuthz.Allocations, allocation) }, - expPass: false, + false, + }, + { + "fowarding hop with invalid port ID", + func() { + transferAuthz.Allocations[0].AllowedForwarding = []types.AllowedForwarding{ + {Hops: []types.Hop{validHop}}, + {Hops: []types.Hop{types.NewHop("invalid/port", ibctesting.FirstChannelID)}}, + } + }, + false, + }, + { + "fowarding hop with invalid channel ID", + func() { + transferAuthz.Allocations[0].AllowedForwarding = []types.AllowedForwarding{ + {Hops: []types.Hop{validHop}}, + {Hops: []types.Hop{types.NewHop(types.PortID, "invalid/channel")}}, + } + }, + false, }, } @@ -486,7 +676,7 @@ func (suite *TypesTestSuite) TestTransferAuthorizationValidateBasic() { { SourcePort: mock.PortID, SourceChannel: ibctesting.FirstChannelID, - SpendLimit: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(100))), + SpendLimit: sdk.NewCoins(ibctesting.TestCoin), AllowList: []string{ibctesting.TestAccAddress}, }, }, diff --git a/modules/apps/transfer/types/tx.pb.go b/modules/apps/transfer/types/tx.pb.go index b6ad9e9b57a..518107142a6 100644 --- a/modules/apps/transfer/types/tx.pb.go +++ b/modules/apps/transfer/types/tx.pb.go @@ -56,6 +56,8 @@ type MsgTransfer struct { Memo string `protobuf:"bytes,8,opt,name=memo,proto3" json:"memo,omitempty"` // tokens to be transferred Tokens []types.Coin `protobuf:"bytes,9,rep,name=tokens,proto3" json:"tokens"` + // optional forwarding information + Forwarding Forwarding `protobuf:"bytes,10,opt,name=forwarding,proto3" json:"forwarding"` } func (m *MsgTransfer) Reset() { *m = MsgTransfer{} } @@ -223,47 +225,49 @@ func init() { } var fileDescriptor_7401ed9bed2f8e09 = []byte{ - // 634 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x54, 0x3f, 0x6f, 0xd3, 0x40, - 0x14, 0x8f, 0x49, 0x1a, 0xda, 0x0b, 0x6d, 0xa9, 0x41, 0xad, 0x6b, 0x21, 0x27, 0x8a, 0xa8, 0x54, - 0x52, 0xf5, 0x4e, 0x29, 0x42, 0x45, 0x15, 0x53, 0xba, 0x30, 0x50, 0xa9, 0x58, 0x65, 0x61, 0xa9, - 0xec, 0xcb, 0xc3, 0x39, 0x35, 0xbe, 0x33, 0xbe, 0x4b, 0x04, 0x0b, 0x42, 0x4c, 0x88, 0x89, 0x8f, - 0xc0, 0xc8, 0xd8, 0x9d, 0x2f, 0xd0, 0xb1, 0x23, 0x13, 0x42, 0xed, 0xd0, 0x85, 0x0f, 0x81, 0xee, - 0x7c, 0x0e, 0x81, 0x21, 0xc0, 0x92, 0xdc, 0x7b, 0xef, 0xf7, 0xfe, 0xfc, 0x7e, 0xef, 0xce, 0x68, - 0x83, 0xc5, 0x94, 0x44, 0x59, 0x36, 0x64, 0x34, 0x52, 0x4c, 0x70, 0x49, 0x54, 0x1e, 0x71, 0xf9, - 0x02, 0x72, 0x32, 0xee, 0x12, 0xf5, 0x0a, 0x67, 0xb9, 0x50, 0xc2, 0xbd, 0xc3, 0x62, 0x8a, 0xa7, - 0x61, 0xb8, 0x84, 0xe1, 0x71, 0xd7, 0x5f, 0x89, 0x52, 0xc6, 0x05, 0x31, 0xbf, 0x45, 0x82, 0x7f, - 0x3b, 0x11, 0x89, 0x30, 0x47, 0xa2, 0x4f, 0xd6, 0xbb, 0x46, 0x85, 0x4c, 0x85, 0x24, 0xa9, 0x4c, - 0x74, 0xf9, 0x54, 0x26, 0x36, 0x10, 0xd8, 0x40, 0x1c, 0x49, 0x20, 0xe3, 0x6e, 0x0c, 0x2a, 0xea, - 0x12, 0x2a, 0x18, 0xb7, 0xf1, 0xa6, 0x1e, 0x93, 0x8a, 0x1c, 0x08, 0x1d, 0x32, 0xe0, 0x4a, 0x67, - 0x17, 0x27, 0x0b, 0xd8, 0x9a, 0xcd, 0xa3, 0x1c, 0xd6, 0x80, 0xdb, 0x5f, 0xaa, 0xa8, 0x71, 0x20, - 0x93, 0x23, 0xeb, 0x75, 0x9b, 0xa8, 0x21, 0xc5, 0x28, 0xa7, 0x70, 0x9c, 0x89, 0x5c, 0x79, 0x4e, - 0xcb, 0xd9, 0x5c, 0x08, 0x51, 0xe1, 0x3a, 0x14, 0xb9, 0x72, 0x37, 0xd0, 0x92, 0x05, 0xd0, 0x41, - 0xc4, 0x39, 0x0c, 0xbd, 0x6b, 0x06, 0xb3, 0x58, 0x78, 0xf7, 0x0b, 0xa7, 0xfb, 0x08, 0xcd, 0x29, - 0x71, 0x02, 0xdc, 0xab, 0xb6, 0x9c, 0xcd, 0xc6, 0xce, 0x3a, 0x2e, 0x58, 0x61, 0xcd, 0x0a, 0x5b, - 0x56, 0x78, 0x5f, 0x30, 0xde, 0x6b, 0x9c, 0x7d, 0x6b, 0x56, 0x3e, 0x5f, 0x9d, 0x76, 0x1c, 0xcf, - 0x09, 0x8b, 0x24, 0x77, 0x15, 0xd5, 0x25, 0xf0, 0x3e, 0xe4, 0x5e, 0xcd, 0x14, 0xb7, 0x96, 0xeb, - 0xa3, 0xf9, 0x1c, 0x28, 0xb0, 0x31, 0xe4, 0xde, 0x9c, 0x89, 0x4c, 0x6c, 0xf7, 0x09, 0x5a, 0x52, - 0x2c, 0x05, 0x31, 0x52, 0xc7, 0x03, 0x60, 0xc9, 0x40, 0x79, 0x75, 0xd3, 0xda, 0xc7, 0x7a, 0x61, - 0x5a, 0x30, 0x6c, 0x65, 0x1a, 0x77, 0xf1, 0x63, 0x83, 0xe8, 0x2d, 0x4c, 0x7a, 0x87, 0x8b, 0x36, - 0xb9, 0x88, 0xb8, 0x5b, 0x68, 0xa5, 0xac, 0xa6, 0xff, 0xa5, 0x8a, 0xd2, 0xcc, 0xbb, 0xde, 0x72, - 0x36, 0x6b, 0xe1, 0x4d, 0x1b, 0x38, 0x2a, 0xfd, 0xae, 0x8b, 0x6a, 0x29, 0xa4, 0xc2, 0x9b, 0x37, - 0x23, 0x99, 0xb3, 0xbb, 0x8b, 0xea, 0x86, 0x8b, 0xf4, 0x16, 0x5a, 0xd5, 0xd9, 0x0a, 0xd4, 0xf4, - 0x14, 0xa1, 0x85, 0xef, 0x75, 0xde, 0x7f, 0x6a, 0x56, 0xde, 0x5d, 0x9d, 0x76, 0x2c, 0xe9, 0x0f, - 0x57, 0xa7, 0x9d, 0xd5, 0x22, 0x77, 0x5b, 0xf6, 0x4f, 0xc8, 0xd4, 0xb6, 0xda, 0xbb, 0xe8, 0xd6, - 0x94, 0x19, 0x82, 0xcc, 0x04, 0x97, 0xa0, 0x65, 0x92, 0xf0, 0x72, 0x04, 0x9c, 0x82, 0xd9, 0x60, - 0x2d, 0x9c, 0xd8, 0x7b, 0x35, 0x5d, 0xbe, 0xfd, 0x06, 0x2d, 0x1f, 0xc8, 0xe4, 0x59, 0xd6, 0x8f, - 0x14, 0x1c, 0x46, 0x79, 0x94, 0x4a, 0xa3, 0x39, 0x4b, 0x38, 0xe4, 0x76, 0xe9, 0xd6, 0x72, 0x7b, - 0xa8, 0x9e, 0x19, 0x84, 0x59, 0x74, 0x63, 0xe7, 0x2e, 0x9e, 0xf5, 0x00, 0x70, 0x51, 0xad, 0xe4, - 0x54, 0x64, 0xee, 0x2d, 0xff, 0xe2, 0x64, 0x8a, 0xb6, 0xd7, 0xd1, 0xda, 0x1f, 0xfd, 0xcb, 0xe1, - 0x77, 0x7e, 0x38, 0xa8, 0x7a, 0x20, 0x13, 0x77, 0x80, 0xe6, 0x27, 0xb7, 0xf2, 0xde, 0xec, 0x9e, - 0x53, 0x1a, 0xf8, 0xdd, 0x7f, 0x86, 0x4e, 0xe4, 0x52, 0xe8, 0xc6, 0x6f, 0x4a, 0x6c, 0xff, 0xb5, - 0xc4, 0x34, 0xdc, 0x7f, 0xf0, 0x5f, 0xf0, 0xb2, 0xab, 0x3f, 0xf7, 0x56, 0xdf, 0xbb, 0xde, 0xd3, - 0xb3, 0x8b, 0xc0, 0x39, 0xbf, 0x08, 0x9c, 0xef, 0x17, 0x81, 0xf3, 0xf1, 0x32, 0xa8, 0x9c, 0x5f, - 0x06, 0x95, 0xaf, 0x97, 0x41, 0xe5, 0xf9, 0x6e, 0xc2, 0xd4, 0x60, 0x14, 0x63, 0x2a, 0x52, 0x62, - 0xbf, 0x09, 0x2c, 0xa6, 0xdb, 0x89, 0x20, 0xe3, 0x87, 0x24, 0x15, 0xfd, 0xd1, 0x10, 0xa4, 0x7e, - 0xe7, 0x53, 0xef, 0x5b, 0xbd, 0xce, 0x40, 0xc6, 0x75, 0xf3, 0xb4, 0xef, 0xff, 0x0c, 0x00, 0x00, - 0xff, 0xff, 0xfe, 0xf1, 0xb5, 0xf1, 0xd1, 0x04, 0x00, 0x00, + // 659 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x54, 0x31, 0x6f, 0x13, 0x31, + 0x14, 0xce, 0xd1, 0x34, 0xb4, 0x0e, 0x6d, 0xa9, 0x41, 0xed, 0xf5, 0x84, 0x2e, 0x51, 0x44, 0xa5, + 0x90, 0xaa, 0xb6, 0x52, 0x84, 0x8a, 0x2a, 0xa6, 0x54, 0x42, 0x0c, 0x14, 0x95, 0x53, 0x59, 0x58, + 0xaa, 0xbb, 0x8b, 0x7b, 0xb1, 0x9a, 0xb3, 0x0f, 0xdb, 0x09, 0xb0, 0x20, 0xc4, 0x84, 0x60, 0xe1, + 0x27, 0x30, 0x32, 0xf6, 0x67, 0x74, 0xec, 0xc8, 0x84, 0x50, 0x3b, 0x74, 0xe1, 0x47, 0x20, 0xfb, + 0x7c, 0xe1, 0x60, 0x08, 0xb0, 0xdc, 0xf9, 0xbd, 0xf7, 0xbd, 0xef, 0xbd, 0xf7, 0xf9, 0xc9, 0x60, + 0x9d, 0x46, 0x31, 0x0e, 0xb3, 0x6c, 0x48, 0xe3, 0x50, 0x51, 0xce, 0x24, 0x56, 0x22, 0x64, 0xf2, + 0x88, 0x08, 0x3c, 0xee, 0x62, 0xf5, 0x0a, 0x65, 0x82, 0x2b, 0x0e, 0x6f, 0xd1, 0x28, 0x46, 0x65, + 0x18, 0x2a, 0x60, 0x68, 0xdc, 0xf5, 0x96, 0xc3, 0x94, 0x32, 0x8e, 0xcd, 0x37, 0x4f, 0xf0, 0x6e, + 0x26, 0x3c, 0xe1, 0xe6, 0x88, 0xf5, 0xc9, 0x7a, 0x57, 0x63, 0x2e, 0x53, 0x2e, 0x71, 0x2a, 0x13, + 0x4d, 0x9f, 0xca, 0xc4, 0x06, 0x7c, 0x1b, 0x88, 0x42, 0x49, 0xf0, 0xb8, 0x1b, 0x11, 0x15, 0x76, + 0x71, 0xcc, 0x29, 0xb3, 0xf1, 0x86, 0x6e, 0x33, 0xe6, 0x82, 0xe0, 0x78, 0x48, 0x09, 0x53, 0x3a, + 0x3b, 0x3f, 0x59, 0xc0, 0xc6, 0xf4, 0x39, 0x8a, 0x66, 0x0d, 0xb8, 0xf5, 0xb1, 0x0a, 0xea, 0x7b, + 0x32, 0x39, 0xb0, 0x5e, 0xd8, 0x00, 0x75, 0xc9, 0x47, 0x22, 0x26, 0x87, 0x19, 0x17, 0xca, 0x75, + 0x9a, 0x4e, 0x7b, 0x3e, 0x00, 0xb9, 0x6b, 0x9f, 0x0b, 0x05, 0xd7, 0xc1, 0xa2, 0x05, 0xc4, 0x83, + 0x90, 0x31, 0x32, 0x74, 0xaf, 0x18, 0xcc, 0x42, 0xee, 0xdd, 0xcd, 0x9d, 0xf0, 0x01, 0x98, 0x55, + 0xfc, 0x98, 0x30, 0x77, 0xa6, 0xe9, 0xb4, 0xeb, 0x5b, 0x6b, 0x28, 0x9f, 0x0a, 0xe9, 0xa9, 0x90, + 0x9d, 0x0a, 0xed, 0x72, 0xca, 0x7a, 0xf5, 0xd3, 0x6f, 0x8d, 0xca, 0x97, 0xcb, 0x93, 0x8e, 0xe3, + 0x3a, 0x41, 0x9e, 0x04, 0x57, 0x40, 0x4d, 0x12, 0xd6, 0x27, 0xc2, 0xad, 0x1a, 0x72, 0x6b, 0x41, + 0x0f, 0xcc, 0x09, 0x12, 0x13, 0x3a, 0x26, 0xc2, 0x9d, 0x35, 0x91, 0x89, 0x0d, 0x1f, 0x83, 0x45, + 0x45, 0x53, 0xc2, 0x47, 0xea, 0x70, 0x40, 0x68, 0x32, 0x50, 0x6e, 0xcd, 0x94, 0xf6, 0x90, 0xbe, + 0x30, 0x2d, 0x18, 0xb2, 0x32, 0x8d, 0xbb, 0xe8, 0x91, 0x41, 0xf4, 0xe6, 0x27, 0xb5, 0x83, 0x05, + 0x9b, 0x9c, 0x47, 0xe0, 0x06, 0x58, 0x2e, 0xd8, 0xf4, 0x5f, 0xaa, 0x30, 0xcd, 0xdc, 0xab, 0x4d, + 0xa7, 0x5d, 0x0d, 0xae, 0xdb, 0xc0, 0x41, 0xe1, 0x87, 0x10, 0x54, 0x53, 0x92, 0x72, 0x77, 0xce, + 0xb4, 0x64, 0xce, 0x70, 0x1b, 0xd4, 0xcc, 0x2c, 0xd2, 0x9d, 0x6f, 0xce, 0x4c, 0x57, 0xa0, 0xaa, + 0xbb, 0x08, 0x2c, 0x1c, 0x3e, 0x01, 0xe0, 0x88, 0x8b, 0x97, 0xa1, 0xe8, 0x53, 0x96, 0xb8, 0xc0, + 0xcc, 0xd0, 0x46, 0xd3, 0x96, 0x0e, 0x3d, 0x9c, 0xe0, 0x2d, 0x57, 0x89, 0x61, 0xa7, 0xf3, 0xfe, + 0x73, 0xa3, 0xf2, 0xee, 0xf2, 0xa4, 0x63, 0x45, 0xfc, 0x70, 0x79, 0xd2, 0x59, 0xc9, 0x7b, 0xd9, + 0x94, 0xfd, 0x63, 0x5c, 0xba, 0xfd, 0xd6, 0x36, 0xb8, 0x51, 0x32, 0x03, 0x22, 0x33, 0xce, 0x24, + 0xd1, 0xb2, 0x4b, 0xf2, 0x62, 0x44, 0x58, 0x4c, 0xcc, 0x46, 0x54, 0x83, 0x89, 0xbd, 0x53, 0xd5, + 0xf4, 0xad, 0x37, 0x60, 0x69, 0x4f, 0x26, 0xcf, 0xb2, 0x7e, 0xa8, 0xc8, 0x7e, 0x28, 0xc2, 0x54, + 0x9a, 0x3b, 0xa4, 0x09, 0x23, 0xc2, 0x2e, 0x91, 0xb5, 0x60, 0x0f, 0xd4, 0x32, 0x83, 0x30, 0x8b, + 0x53, 0xdf, 0xba, 0x3d, 0x7d, 0xb6, 0x9c, 0xad, 0xd0, 0x28, 0xcf, 0xdc, 0x59, 0xfa, 0x35, 0x93, + 0x21, 0x6d, 0xad, 0x81, 0xd5, 0x3f, 0xea, 0x17, 0xcd, 0x6f, 0xfd, 0x70, 0xc0, 0xcc, 0x9e, 0x4c, + 0xe0, 0x00, 0xcc, 0x4d, 0xb6, 0xfc, 0xce, 0xf4, 0x9a, 0x25, 0x0d, 0xbc, 0xee, 0x3f, 0x43, 0x27, + 0x72, 0x29, 0x70, 0xed, 0x37, 0x25, 0x36, 0xff, 0x4a, 0x51, 0x86, 0x7b, 0xf7, 0xfe, 0x0b, 0x5e, + 0x54, 0xf5, 0x66, 0xdf, 0xea, 0x3d, 0xee, 0x3d, 0x3d, 0x3d, 0xf7, 0x9d, 0xb3, 0x73, 0xdf, 0xf9, + 0x7e, 0xee, 0x3b, 0x9f, 0x2e, 0xfc, 0xca, 0xd9, 0x85, 0x5f, 0xf9, 0x7a, 0xe1, 0x57, 0x9e, 0x6f, + 0x27, 0x54, 0x0d, 0x46, 0x11, 0x8a, 0x79, 0x8a, 0xed, 0x1b, 0x43, 0xa3, 0x78, 0x33, 0xe1, 0x78, + 0x7c, 0x1f, 0xa7, 0xbc, 0x3f, 0x1a, 0x12, 0xa9, 0xdf, 0x8d, 0xd2, 0x7b, 0xa1, 0x5e, 0x67, 0x44, + 0x46, 0x35, 0xf3, 0x54, 0xdc, 0xfd, 0x19, 0x00, 0x00, 0xff, 0xff, 0xaf, 0x48, 0x04, 0x1e, 0x21, + 0x05, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -406,6 +410,16 @@ func (m *MsgTransfer) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + { + size, err := m.Forwarding.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintTx(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x52 if len(m.Tokens) > 0 { for iNdEx := len(m.Tokens) - 1; iNdEx >= 0; iNdEx-- { { @@ -624,6 +638,8 @@ func (m *MsgTransfer) Size() (n int) { n += 1 + l + sovTx(uint64(l)) } } + l = m.Forwarding.Size() + n += 1 + l + sovTx(uint64(l)) return n } @@ -977,6 +993,39 @@ func (m *MsgTransfer) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex + case 10: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Forwarding", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthTx + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthTx + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.Forwarding.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipTx(dAtA[iNdEx:]) diff --git a/modules/capability/go.mod b/modules/capability/go.mod index 7d6667fa0af..f9986cb710d 100644 --- a/modules/capability/go.mod +++ b/modules/capability/go.mod @@ -12,7 +12,7 @@ require ( cosmossdk.io/log v1.3.1 cosmossdk.io/math v1.3.0 cosmossdk.io/store v1.1.0 - github.com/cometbft/cometbft v0.38.7 + github.com/cometbft/cometbft v0.38.8 github.com/cosmos/cosmos-db v1.0.2 github.com/cosmos/cosmos-sdk v0.50.7 github.com/cosmos/gogoproto v1.5.0 diff --git a/modules/capability/go.sum b/modules/capability/go.sum index 25fae62960b..9bfabd22706 100644 --- a/modules/capability/go.sum +++ b/modules/capability/go.sum @@ -111,8 +111,8 @@ github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZ github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= -github.com/cometbft/cometbft v0.38.7 h1:ULhIOJ9+LgSy6nLekhq9ae3juX3NnQUMMPyVdhZV6Hk= -github.com/cometbft/cometbft v0.38.7/go.mod h1:HIyf811dFMI73IE0F7RrnY/Fr+d1+HuJAgtkEpQjCMY= +github.com/cometbft/cometbft v0.38.8 h1:XyJ9Cu3xqap6xtNxiemrO8roXZ+KS2Zlu7qQ0w1trvU= +github.com/cometbft/cometbft v0.38.8/go.mod h1:xOoGZrtUT+A5izWfHSJgl0gYZUE7lu7Z2XIS1vWG/QQ= github.com/cometbft/cometbft-db v0.9.1 h1:MIhVX5ja5bXNHF8EYrThkG9F7r9kSfv8BX4LWaxWJ4M= github.com/cometbft/cometbft-db v0.9.1/go.mod h1:iliyWaoV0mRwBJoizElCwwRA9Tf7jZJOURcRZF9m60U= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= @@ -373,6 +373,8 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= diff --git a/modules/light-clients/08-wasm/go.mod b/modules/light-clients/08-wasm/go.mod index 052d0944022..277b5f11670 100644 --- a/modules/light-clients/08-wasm/go.mod +++ b/modules/light-clients/08-wasm/go.mod @@ -24,7 +24,7 @@ require ( cosmossdk.io/x/tx v0.13.3 cosmossdk.io/x/upgrade v0.1.3 github.com/CosmWasm/wasmvm/v2 v2.0.1 - github.com/cometbft/cometbft v0.38.7 + github.com/cometbft/cometbft v0.38.8 github.com/cosmos/cosmos-db v1.0.2 github.com/cosmos/cosmos-sdk v0.50.7 github.com/cosmos/gogoproto v1.5.0 @@ -125,6 +125,7 @@ require ( github.com/hashicorp/go-safetemp v1.0.0 // indirect github.com/hashicorp/go-version v1.6.0 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/yamux v0.1.1 // indirect github.com/hdevalence/ed25519consensus v0.1.0 // indirect diff --git a/modules/light-clients/08-wasm/go.sum b/modules/light-clients/08-wasm/go.sum index 7d67ad44497..210046de1ec 100644 --- a/modules/light-clients/08-wasm/go.sum +++ b/modules/light-clients/08-wasm/go.sum @@ -335,8 +335,8 @@ github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZ github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= -github.com/cometbft/cometbft v0.38.7 h1:ULhIOJ9+LgSy6nLekhq9ae3juX3NnQUMMPyVdhZV6Hk= -github.com/cometbft/cometbft v0.38.7/go.mod h1:HIyf811dFMI73IE0F7RrnY/Fr+d1+HuJAgtkEpQjCMY= +github.com/cometbft/cometbft v0.38.8 h1:XyJ9Cu3xqap6xtNxiemrO8roXZ+KS2Zlu7qQ0w1trvU= +github.com/cometbft/cometbft v0.38.8/go.mod h1:xOoGZrtUT+A5izWfHSJgl0gYZUE7lu7Z2XIS1vWG/QQ= github.com/cometbft/cometbft-db v0.9.1 h1:MIhVX5ja5bXNHF8EYrThkG9F7r9kSfv8BX4LWaxWJ4M= github.com/cometbft/cometbft-db v0.9.1/go.mod h1:iliyWaoV0mRwBJoizElCwwRA9Tf7jZJOURcRZF9m60U= github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg= @@ -683,6 +683,8 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= diff --git a/proto/ibc/applications/interchain_accounts/host/v1/tx.proto b/proto/ibc/applications/interchain_accounts/host/v1/tx.proto index 22e8c9b3567..29cdb088c8f 100644 --- a/proto/ibc/applications/interchain_accounts/host/v1/tx.proto +++ b/proto/ibc/applications/interchain_accounts/host/v1/tx.proto @@ -47,7 +47,7 @@ message MsgModuleQuerySafe { string signer = 1; // requests defines the module safe queries to execute. - repeated QueryRequest requests = 2; + repeated QueryRequest requests = 2 [(gogoproto.nullable) = false]; } // MsgModuleQuerySafeResponse defines the response for Msg/ModuleQuerySafe diff --git a/proto/ibc/applications/transfer/v1/authz.proto b/proto/ibc/applications/transfer/v1/authz.proto index 5c4b71d3479..c8f9ca1cc66 100644 --- a/proto/ibc/applications/transfer/v1/authz.proto +++ b/proto/ibc/applications/transfer/v1/authz.proto @@ -7,6 +7,7 @@ option go_package = "github.com/cosmos/ibc-go/v8/modules/apps/transfer/types"; import "cosmos_proto/cosmos.proto"; import "gogoproto/gogo.proto"; import "cosmos/base/v1beta1/coin.proto"; +import "ibc/applications/transfer/v1/transfer.proto"; // Allocation defines the spend limit for a particular port and channel message Allocation { @@ -22,6 +23,15 @@ message Allocation { // allow list of memo strings, an empty list prohibits all memo strings; // a list only with "*" permits any memo string repeated string allowed_packet_data = 5; + // Forwarding options that are allowed. + repeated AllowedForwarding allowed_forwarding = 6 [(gogoproto.nullable) = false]; +} + +// AllowedForwarding defines which options are allowed for forwarding. +message AllowedForwarding { + // a list of allowed source port ID/channel ID pairs through which the packet is allowed to be forwarded until final + // destination + repeated ibc.applications.transfer.v1.Hop hops = 1 [(gogoproto.nullable) = false]; } // TransferAuthorization allows the grantee to spend up to spend_limit coins from diff --git a/proto/ibc/applications/transfer/v1/transfer.proto b/proto/ibc/applications/transfer/v1/transfer.proto index 15ce7a40e59..2c0b942e554 100644 --- a/proto/ibc/applications/transfer/v1/transfer.proto +++ b/proto/ibc/applications/transfer/v1/transfer.proto @@ -2,6 +2,8 @@ syntax = "proto3"; package ibc.applications.transfer.v1; +import "gogoproto/gogo.proto"; + option go_package = "github.com/cosmos/ibc-go/v8/modules/apps/transfer/types"; // Params defines the set of IBC transfer parameters. @@ -16,3 +18,20 @@ message Params { // chain. bool receive_enabled = 2; } + +// Forwarding defines a list of port ID, channel ID pairs determining the path +// through which a packet must be forwarded, and an unwind boolean indicating if +// the coin should be unwinded to its native chain before forwarding. +message Forwarding { + // optional unwinding for the token transfered + bool unwind = 1; + // optional intermediate path through which packet will be forwarded + repeated Hop hops = 2 [(gogoproto.nullable) = false]; +} + +// Hop defines a port ID, channel ID pair specifying where tokens must be forwarded +// next in a multihop transfer. +message Hop { + string port_id = 1; + string channel_id = 2; +} diff --git a/proto/ibc/applications/transfer/v1/tx.proto b/proto/ibc/applications/transfer/v1/tx.proto index 24101fff0a9..6a422d0c74d 100644 --- a/proto/ibc/applications/transfer/v1/tx.proto +++ b/proto/ibc/applications/transfer/v1/tx.proto @@ -51,6 +51,8 @@ message MsgTransfer { string memo = 8; // tokens to be transferred repeated cosmos.base.v1beta1.Coin tokens = 9 [(gogoproto.nullable) = false]; + // optional forwarding information + Forwarding forwarding = 10 [(gogoproto.nullable) = false]; } // MsgTransferResponse defines the Msg/Transfer response type. @@ -78,4 +80,4 @@ message MsgUpdateParams { // MsgUpdateParamsResponse defines the response structure for executing a // MsgUpdateParams message. -message MsgUpdateParamsResponse {} \ No newline at end of file +message MsgUpdateParamsResponse {} diff --git a/proto/ibc/applications/transfer/v2/packet.proto b/proto/ibc/applications/transfer/v2/packet.proto index e89b66356ab..162833ce4e1 100644 --- a/proto/ibc/applications/transfer/v2/packet.proto +++ b/proto/ibc/applications/transfer/v2/packet.proto @@ -6,6 +6,7 @@ option go_package = "github.com/cosmos/ibc-go/v8/modules/apps/transfer/types"; import "ibc/applications/transfer/v2/token.proto"; import "gogoproto/gogo.proto"; +import "ibc/applications/transfer/v1/transfer.proto"; // FungibleTokenPacketData defines a struct for the packet payload // See FungibleTokenPacketData spec: @@ -35,4 +36,16 @@ message FungibleTokenPacketDataV2 { string receiver = 3; // optional memo string memo = 4; + // optional forwarding information + ForwardingPacketData forwarding = 5 [(gogoproto.nullable) = false]; +} + +// ForwardingPacketData defines a list of port ID, channel ID pairs determining the path +// through which a packet must be forwarded, and the destination memo string to be used in the +// final destination of the tokens. +message ForwardingPacketData { + // optional memo consumed by final destination chain + string destination_memo = 1; + // optional intermediate path through which packet will be forwarded. + repeated ibc.applications.transfer.v1.Hop hops = 2 [(gogoproto.nullable) = false]; } diff --git a/testing/chain.go b/testing/chain.go index 663e4fe853a..6a264a6e64d 100644 --- a/testing/chain.go +++ b/testing/chain.go @@ -629,6 +629,12 @@ func (chain *TestChain) GetTimeoutHeight() clienttypes.Height { return clienttypes.NewHeight(clienttypes.ParseChainID(chain.ChainID), uint64(chain.GetContext().BlockHeight())+100) } +// GetTimeoutTimestamp is a convenience function which returns a IBC packet timeout timestamp +// to be used for testing. It returns the current block timestamp + default timestamp delta (1 hour). +func (chain *TestChain) GetTimeoutTimestamp() uint64 { + return uint64(chain.GetContext().BlockTime().UnixNano()) + DefaultTimeoutTimestampDelta +} + // DeleteKey deletes the specified key from the ibc store. func (chain *TestChain) DeleteKey(key []byte) { storeKey := chain.GetSimApp().GetKey(exported.StoreKey) diff --git a/testing/events.go b/testing/events.go index af585a87669..3e1db55ee6a 100644 --- a/testing/events.go +++ b/testing/events.go @@ -62,10 +62,21 @@ func ParseChannelIDFromEvents(events []abci.Event) (string, error) { } // ParsePacketFromEvents parses events emitted from a MsgRecvPacket and returns -// the first packet found. +// the first EventTypeSendPacket packet found. // Returns an error if no packet is found. func ParsePacketFromEvents(events []abci.Event) (channeltypes.Packet, error) { - packets, err := ParsePacketsFromEvents(events) + packets, err := ParsePacketsFromEvents(channeltypes.EventTypeSendPacket, events) + if err != nil { + return channeltypes.Packet{}, err + } + return packets[0], nil +} + +// ParseRecvPacketFromEvents parses events emitted from a MsgRecvPacket and returns +// the first EventTypeRecvPacket packet found. +// Returns an error if no packet is found. +func ParseRecvPacketFromEvents(events []abci.Event) (channeltypes.Packet, error) { + packets, err := ParsePacketsFromEvents(channeltypes.EventTypeRecvPacket, events) if err != nil { return channeltypes.Packet{}, err } @@ -75,13 +86,13 @@ func ParsePacketFromEvents(events []abci.Event) (channeltypes.Packet, error) { // ParsePacketsFromEvents parses events emitted from a MsgRecvPacket and returns // all the packets found. // Returns an error if no packet is found. -func ParsePacketsFromEvents(events []abci.Event) ([]channeltypes.Packet, error) { +func ParsePacketsFromEvents(eventType string, events []abci.Event) ([]channeltypes.Packet, error) { ferr := func(err error) ([]channeltypes.Packet, error) { return nil, fmt.Errorf("ibctesting.ParsePacketsFromEvents: %w", err) } var packets []channeltypes.Packet for _, ev := range events { - if ev.Type == channeltypes.EventTypeSendPacket { + if ev.Type == eventType { var packet channeltypes.Packet for _, attr := range ev.Attributes { switch attr.Key { @@ -174,6 +185,19 @@ func ParseProposalIDFromEvents(events []abci.Event) (uint64, error) { return 0, fmt.Errorf("proposalID event attribute not found") } +// ParsePacketSequenceFromEvents parses events emitted from MsgRecvPacket and returns the packet sequence +func ParsePacketSequenceFromEvents(events []abci.Event) (uint64, error) { + for _, event := range events { + for _, attribute := range event.Attributes { + if attribute.Key == "packet_sequence" { + return strconv.ParseUint(attribute.Value, 10, 64) + } + } + } + + return 0, fmt.Errorf("packet sequence event attribute not found") +} + // AssertEvents asserts that expected events are present in the actual events. func AssertEvents( suite *testifysuite.Suite, diff --git a/testing/events_test.go b/testing/events_test.go index a9cf524c68a..c99eceec00c 100644 --- a/testing/events_test.go +++ b/testing/events_test.go @@ -199,7 +199,7 @@ func TestParsePacketsFromEvents(t *testing.T) { } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - allPackets, err := ibctesting.ParsePacketsFromEvents(tc.events) + allPackets, err := ibctesting.ParsePacketsFromEvents(channeltypes.EventTypeSendPacket, tc.events) if tc.expectedError == "" { require.NoError(t, err) diff --git a/testing/solomachine.go b/testing/solomachine.go index 2a10fc50107..b6b3d986a22 100644 --- a/testing/solomachine.go +++ b/testing/solomachine.go @@ -6,8 +6,6 @@ import ( "github.com/stretchr/testify/require" - sdkmath "cosmossdk.io/math" - "github.com/cosmos/cosmos-sdk/codec" codectypes "github.com/cosmos/cosmos-sdk/codec/types" kmultisig "github.com/cosmos/cosmos-sdk/crypto/keys/multisig" @@ -372,12 +370,13 @@ func (solo *Solomachine) SendTransfer(chain *TestChain, portID, channelID string msgTransfer := transfertypes.NewMsgTransfer( portID, channelID, - sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(100))), + sdk.NewCoins(TestCoin), chain.SenderAccount.GetAddress().String(), chain.SenderAccount.GetAddress().String(), clienttypes.ZeroHeight(), uint64(chain.GetContext().BlockTime().Add(time.Hour).UnixNano()), "", + transfertypes.Forwarding{}, ) for _, fn := range fns { diff --git a/testing/values.go b/testing/values.go index e8e6dd006e7..d91dcb1c898 100644 --- a/testing/values.go +++ b/testing/values.go @@ -55,6 +55,8 @@ var ( // DefaultTrustLevel sets params variables used to create a TM client DefaultTrustLevel = ibctm.DefaultTrustLevel + DefaultTimeoutTimestampDelta = uint64(time.Hour.Nanoseconds()) + TestAccAddress = "cosmos17dtl0mjt3t77kpuhg2edqzjpszulwhgzuj9ljs" TestCoin = sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(100)) TestCoins = sdk.NewCoins(TestCoin)