From 18121380dec5cff5ec803f1088fd409e069c2c9e Mon Sep 17 00:00:00 2001 From: Cian Hatton Date: Fri, 28 Jun 2024 10:35:20 +0100 Subject: [PATCH] [FEAT] ICS20-V2 Path Forwarding (#6693) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Adding proto files for ics20-v2 (#6110) * chore: adding proto files for ics20-v2 * chore: add newline * update amount -> string (#6120) * Update MsgTransfer to accept sdk.Coins instead of sdk.Coin (#6113) * fix: allow base denom with trailing slash (#6148) * imp: add CurrentVersion, EscrowVersion (#6160) * add CurrentVersion, EscrowVersion, update tests * update hardcoded transfer channel version from interchaintest * chore: add function for converting packet data from v1 to v3 (#6116) --------- Co-authored-by: Charly * chore: implement required `FungibleTokenPacketData` v3 interface methods (#6126) * imp: `getMultiDenomFungibleTokenPacketData`to be used in packet unmarshalling/conversion (#6226) * chore: adding proto files for ics20-v2 * chore: add newline * chore: modify MsgTransfer to accept coins instead of coin * chore: reverted unintentional comment changes * chore: reverted unintentional comment changes * chore: adding test for conversion fn * chore: fix e2e linter * chore: adding docs * chore: addressing PR feedback * chore: remove duplicate import * chore: addressing PR feedback * chore: moved extration logic into internal package * chore: commented out failing test * chore: adding link to issue * chore: remove duplicate import * chore: fix linting errors * FungibleTokenPacketData interface methods + tests * linter * wip: token validation * update trace identifier validation in Token + tests * rm Printf * update with pr review * add CurrentVersion, EscrowVersion, update tests * pr review * fix e2e tests * pr review * update e2e test version * linter * update hardcoded transfer channel version from interchaintest * update hardcoded transfer channel version from interchaintest * wip packet unmarshaller * wip * wip testing * update test * linter * rm unnecessary version changes * rm unnecessary artifacts * update callbacks test * update comment * pr review * rename getMultiDenomFungibleTokenPacketData to unmarshalPacketDataBytesToICS20V2 --------- Co-authored-by: chatton Co-authored-by: Carlos Rodriguez * chore: implement version checking for channel handshake application callbacks (#6175) * add SupportedVersions array for different ics20 versions, add version checking on channel handshake application callbacks * add tests * update pr review * pr review * last few pr review nits * linter * add version counter proposing * fix missing app versino * update code + tests to return our proposed version if counterparty version is invalid * remove if statement * address review comments: return ics20-2 if counterparty version is not supported --------- Co-authored-by: Carlos Rodriguez * imp: update transfer authz implementation to account for multi denom transfers (#6252) * add transfer authz code + tests * linter * address review comments --------- Co-authored-by: Carlos Rodriguez * ics20-v2: backwards compatibility for transfer rpc and packet callbacks (#6189) * chore: adding proto files for ics20-v2 * chore: add newline * chore: modify MsgTransfer to accept coins instead of coin * chore: reverted unintentional comment changes * chore: reverted unintentional comment changes * chore: adding test for conversion fn * chore: fix e2e linter * chore: adding docs * chore: addressing PR feedback * chore: remove duplicate import * chore: addressing PR feedback * chore: moved extration logic into internal package * chore: commented out failing test * chore: adding link to issue * chore: remove duplicate import * chore: fix linting errors * FungibleTokenPacketData interface methods + tests * linter * wip: token validation * update trace identifier validation in Token + tests * rm Printf * update with pr review * pr review * linter * rm unused function: linter * wip pr feedback * fix test * pr review * lintttttt * wip: backwards compatibility for transfer rpc * implement changes for ics20-v2 packet data for onRecvPacket, onAcknowledgePacket and onTimeoutPacket * fix callbacks tests * lint --------- Co-authored-by: chatton Co-authored-by: Charly * add v3 packet proto * fix protos * fixes * test fixes * add forwardPath keys and memo check in sendTransfer * wip onRecvPacket logic * minor fixes * changes to transfer tx CLI to support multiple denoms * import renaming * onRecv logic completed * add revertInFlights function * add onAck && onTimeout logic * fix interchain accounts test * basic unit test for path forwarding * fix test unsuccessful refund from source * wip test fix * fix mbt test - need more investigation * revert test fix * add assertions * add support for async ack * wip test forwarding happy path * icsv20(path forwarding): use nil as default forwarding path when not set, use sequence in key to store forwarded packet (#6325) * use nil forwarding path as default when not set, use sequence in key to store forwarded packet * lint * add forwarding happy path tests * fix merge * Use type with V2 suffix for package data (#6330) * Adding additional comments and changing version variable names (#6345) * chore: correctly claim capability * lint * imp: change ics20 events to emit token set (#6348) * refactor: quick change to tokens event attribute * fix: various tests * lint * lint:fixeroni * imp: remove events variable in favour of direct event emission --------- Co-authored-by: DimitrisJim * imp: check length tokens array against maximum allowed (#6349) * check length of tokens array against maximum allowed * chore: add note on arbitrary selection --------- Co-authored-by: Colin Axnér <25233464+colin-axner@users.noreply.github.com> * Modify UnmarshalPacketData interface to allow additional args (#6341) * api(port)!: Allow passing of context, port and channel identifier to unmarshal packet data interface as disussed. This allows us to grab the app version in transfer and unmarshal the packet based on that instead of a hacky unmarshal v2 then v1 and whatever happens. * lint: as we do * callbacks: fix signature of UnmarshalPacketData as per changes, make refactors to hopefully simplify signatures. * chore: lint and remove some todos. * review: address feedback. * Refactor packet data unmarshalling to use specific version (#6354) * chore: specifically unmarshal v1 or v2 without brute force * chore: fix TestPacketDataUnmarshalerInterface test in transfer module * Pass dest values OnRecv, refactor GetExpectedEvents * chore: fixing TestGetCallbackData test * chore: fixed remaining tests in callbacks module * test: simplify callbacks test, revert back to previous behaviour * chore: fix test case name * chore: addressing PR feedback * chore: added docstring for unmarshalPacketDataBytesToICS20V2 --------- Co-authored-by: DimitrisJim Co-authored-by: Colin Axnér <25233464+colin-axner@users.noreply.github.com> * chore: fixing tests * imp: self review comments for ics20-v2 (#6360) * refactor: address various self review comments * revert: unnecessary change * lint * imp: self review on ics20-v2 part 2 (#6364) * refactor: apply review suggestions * imp: additional updates * refactor: make ValidateIBCDenom private * Update modules/apps/transfer/types/msgs.go Co-authored-by: Cian Hatton * apply review suggestions --------- Co-authored-by: Cian Hatton * chore: move functions from internal/denom back to trace.go (#6368) * chore: move functions from internal/denom to trace.go * lint * lint: the comeback * imp: ics20 v2 self review part 3 (#6373) * imp: self review items * apply jim's suggestion * Update modules/apps/transfer/keeper/msg_server_test.go * Update modules/apps/transfer/ibc_module.go * Update modules/apps/transfer/ibc_module.go * chore: remove duplicate test case * chore: address minor nits (#6374) * fix lint warning, add extra godocs, and some other small fixes and cleanup * fix finalReceiver address bug * wip - ack test scenario5 * add FungibleTokenPacketDataV2 test for ValidateBasic (#6398) * fix linter complaints * add test - currently faling on middle hop revert * add test comments * fixes * retrieve channel capability only if there is a previous packet in store * add missing parameter * fix: e2e build failures. * Use Transfer instead of sendTransfer when forwarding. (#6564) Pass nil as the next forwarding path if at final hop. Use consistent timeout for all hops. * lint: fix linter issues. * tests(transfer): move forwarding tests to separate file. (#6568) * chore: rename ForwardingInfo to Forwarding * Revert "chore: rename ForwardingInfo to Forwarding" This reverts commit e483b9a5fb6887c97c6f76257967b8b40bbf82cf. * nit(transfer): Mark hops as non nullable. (#6566) * nit(transfer): Mark hops as non nullable. * lint: fix additional linting issues * feat(transfer): add forwarding info validation to token packet (#6571) * feat(transfer): add forwarding info validation to token packet * Added NewForwardingInfo constructor * Removed redundant check * Clean up tests per cr comments * Back to Validate and use NewDenom * feat(transfer): add validation for forwarding info in msg transfer validate basic (#6583) * Fix and simplify reverts of forwarding state (#6574) * refactor: initial simplification attempt * refactor: further organize * fix: all tests fixed * docs: improved godocs * fix: logic and testing error * style: self suggestion * docs: suggestion * docs: spellcheck * style: suggestions * style: renamed to revertForwardedPacket * style: suggestion * docs: remove docs * docs: godoc suggestion * style: suggestion * docs: colin suggestions * chore: rename ForwardingInfo to Forwarding (#6576) * chore: rename ForwardingInfo to Forwarding * chore: rename forwarding_path field to forwarding * chore: make lint-fix * chore: rename forwarding info -> forwarding * chore: renaming fixes before merge * Refactor packet forward functions (#6575) * feat(transfer): validate forwarding memo in transfer authorization (#6591) * Add func convert token to coin ibc (#6584) * add func convert token to coin ibc * fix command: change func to ToCoin and add godoc * add unit test * Revert using ToCoin on Recv where trace manipulation occurs. Use ToCoin while forwarding. Update tests as per Carlos's review. * rename variable --------- Co-authored-by: Carlos Rodriguez Co-authored-by: DimitrisJim * transfer: Disallow a forwarding object specified with zero hops and a memo (#6599) * transfer: Disallow a forwarding object specified with zero hops and a memo. * Apply suggestions from code review Co-authored-by: Carlos Rodriguez --------- Co-authored-by: Carlos Rodriguez Co-authored-by: Gjermund Garaba * feat(transfer): move async decision and handling to the ibc module onrecv callback (#6592) * refactor: initial simplification attempt * refactor: further organize * fix: all tests fixed * chore: refactor packet forward functions * chore: use receiver address directly in msg transfer * feat(transfer): move async to ibc_module for onrecv * chore: fix linter * fix: logic and testing error * style: self suggestion * docs: suggestion * docs: spellcheck * style: suggestions * style: renamed to revertForwardedPacket * style: suggestion * docs: remove docs * Added tests for transfer OnRecv * Use new names and methods from feature branch * Apply suggestions from code review Co-authored-by: colin axnér <25233464+colin-axner@users.noreply.github.com> * Clean up test from cr feedback * move vars to beginning of function * lint * Update modules/apps/transfer/ibc_module.go Co-authored-by: Carlos Rodriguez * lint --------- Co-authored-by: srdtrk Co-authored-by: chatton Co-authored-by: colin axnér <25233464+colin-axner@users.noreply.github.com> Co-authored-by: Carlos Rodriguez Co-authored-by: DimitrisJim * chore: use NewForwarding instead of direct init (#6605) * Reduce max forwarding to 16 (#6610) * feat(transfer): use single byte ack for successful forward (#6604) Co-authored-by: Nikolas De Giorgis * chore(transfer/cli): add forwarding flag to tx cli (#6609) Co-authored-by: Carlos Rodriguez * chore(transfer): make Forwarding non-null (#6618) * chore(transfer): make Forwarding non-null * chore(transfer): always validate forwarding. * chore: restructure functions with logical ordering (#6638) * test: Add tests for OnTimeoutPacket when middle chain times out packet (#6596) * Create ontimeoutpacket test for forwarding --------- Co-authored-by: Carlos Rodriguez * feat(transfer): add ShouldBeForwarded convenience method to msg transfer (#6595) * feat(transfer): add should-be-forwarded convenience method to msg transfer * Clean up packet and transfer msg validation for forwarding logic --------- Co-authored-by: Carlos Rodriguez * disallow timeout height usage when forwarding packets (#6641) * disallow non-zero timeout height when forwarding tokens * typo * move constant * use time to create default timeout timestamp delta * Apply suggestions from code review * refactor: add GetTimeoutTimestamp helper fn to the testing pkg * lint --------- Co-authored-by: colin axnér <25233464+colin-axner@users.noreply.github.com> * nit: make set forwarded packet unexported (#6637) Co-authored-by: Gjermund Garaba Co-authored-by: colin axnér <25233464+colin-axner@users.noreply.github.com> * feat(transfer): use registered error code for error acks in token forwarding (#6648) * Add typed errors to packet forwarding * Use forward errors in tests * Make ack construction consistent * chore(transfer): emit forwarding information in events. (#6647) * chore(transfer): emit forwarding information in events. * nit: rename as per Carlos's suggestion * Fix e2e test * Refactor forwarding messages for Transfer and Packet (#6655) * feat(transfer): add unwind, refactor proto structure. gen-all * tests(transfer/types): fix test failures in types tests. * tests(transfer/keeper): fix test failures in keeper tests. * cli(transfer): fix cli usage. pending flag for unwind. * tests(callbacks): fix failing tests in callbacks. * tests(transfer/internal): fix failures in internal package. * tests(transfer): fix test failures in top level tranfer package. * tests(ica/host/keeper): fix repr of msg transfer in ica host msg execution. * lint(all): lint this bad boy * chore(transfer/types): amend validation for MsgTransfer's ShouldBeForwarded, add tests for ForwardedPacketData, minor nits. * nit(self): only pass relevant fields to create packet data; minor comment improvement. * Apply suggestions from code review Co-authored-by: Carlos Rodriguez * chore(merge): fix merge issues. * chore(proto): mention optional nature of fields. * memo: do not drop it * validation: drop requirement on memo being empty on msg transfer. --------- Co-authored-by: Carlos Rodriguez * feat: allow authz granters to specify forwarding info for token transfer (#6661) * First attempt to modify proto and validation. * Fmt * Make it compile * Proto * Add basic validation * Added tests * Fix nil check and remove redundant code. * Move "forwarding" to non-pointer * Added one test case. * Moved error and changed method name. * PR Feedback. * Add nullable=false * Added test and fixed test names * Run make proto-all * Change pointer to non-pointer * Change Yet Another Pointer * More pointers * Comments and naming * Reintroduce brace removed while merging * Remove accidentally reintroduced tests * feat: delete forwarded packet when it is not needed anymore (#6621) * Create ontimeoutpacket test for forwarding * Propagate ack on A * Refactoring * Minor changes * Added comments * Fix type name. * Gofumpt * Update modules/apps/transfer/keeper/relay_forwarding_test.go Co-authored-by: Carlos Rodriguez * Update modules/apps/transfer/keeper/relay_forwarding_test.go Co-authored-by: Carlos Rodriguez * Update modules/apps/transfer/keeper/relay_forwarding_test.go Co-authored-by: Carlos Rodriguez * Update modules/apps/transfer/keeper/relay_forwarding_test.go Co-authored-by: Carlos Rodriguez * Add godoc to test. * Changed trace construction * Update modules/apps/transfer/keeper/relay_forwarding_test.go Co-authored-by: Carlos Rodriguez * remove error msg parameter from helper function * Add test for forwarded packet * Delete packet when not needed anymore. * Move deletion of packet in a single place. * Reintroduce newline * Do not ignore error. * PR feedback. * Construct packet for B ack check. * PR feedback * Pass packet to acknowledgeforwardedpacket * revert unwanted change * Another unwanted change * Better signature and fix source/dest * Added one more test. --------- Co-authored-by: Carlos Rodriguez Co-authored-by: Gjermund Garaba * test(transfer): forwarding acknowledgment errors in middle hop (#6659) * test(transfer): forwarding where middle chaind is source for receive and send * Fix errors after merge * Finish up the test * Update some out-of-date comments * test(transfer): multi-hop ack failure with middle chain NOT being source * Fix tests after height change * Fix tests after height change * Fix test after #6586 * Rename tests to not use scenario numbers * Rename test * address self-review comments * use boolean in NewForwarding parameter * some more review comments --------- Co-authored-by: Carlos Rodriguez * test(transfer): last chain in forwarding packet is ICS20 v1 (#6622) * test(transfer): last chain in forwarding being ICS20 v1 * Finish TestForwarding_WithLastChainBeingICS20v1_Succeeds * Update CreateNewPath signature Co-authored-by: Nikolas De Giorgis * Fix PR review comments --------- Co-authored-by: Nikolas De Giorgis * refactor: rename SetupPath to SetupPaths (#6674) * chore: add flag for unwind in transfer cli (#6680) * add flag for unwind in transfer cli * update long description of cli --------- Co-authored-by: Carlos Rodriguez * feat: impl check reject transfer if len(hops) > 0 and ics20-1 (#6675) * impl check reject transfer if len(hops) > 0 and ics20-1 * add test case hops is not empty with ics20-2 * address review comments * reorder variable declaration --------- Co-authored-by: Carlos Rodriguez Co-authored-by: Gjermund Garaba * feat(transfer): add unwinding ability (#6656) * Create ontimeoutpacket test for forwarding * Propagate ack on A * Refactoring * Minor changes * Added comments * Fix type name. * Gofumpt * Update modules/apps/transfer/keeper/relay_forwarding_test.go Co-authored-by: Carlos Rodriguez * Update modules/apps/transfer/keeper/relay_forwarding_test.go Co-authored-by: Carlos Rodriguez * Update modules/apps/transfer/keeper/relay_forwarding_test.go Co-authored-by: Carlos Rodriguez * Update modules/apps/transfer/keeper/relay_forwarding_test.go Co-authored-by: Carlos Rodriguez * Add godoc to test. * Changed trace construction * Update modules/apps/transfer/keeper/relay_forwarding_test.go Co-authored-by: Carlos Rodriguez * remove error msg parameter from helper function * Add test for forwarded packet * Construct packet for B ack check. * PR feedback * feat(transfer): add unwind, refactor proto structure. gen-all * tests(transfer/types): fix test failures in types tests. * tests(transfer/keeper): fix test failures in keeper tests. * cli(transfer): fix cli usage. pending flag for unwind. * tests(callbacks): fix failing tests in callbacks. * tests(transfer/internal): fix failures in internal package. * tests(transfer): fix test failures in top level tranfer package. * tests(ica/host/keeper): fix repr of msg transfer in ica host msg execution. * lint(all): lint this bad boy * chore(transfer/types): amend validation for MsgTransfer's ShouldBeForwarded, add tests for ForwardedPacketData, minor nits. * nit(self): only pass relevant fields to create packet data; minor comment improvement. * Apply suggestions from code review Co-authored-by: Carlos Rodriguez * chore(merge): fix merge issues. * chore(proto): mention optional nature of fields. * memo: do not drop it * validation: drop requirement on memo being empty on msg transfer. * feat(transfer): add unwinding ability, wip. * Added unwind to allocation forwarding. * Add tests and move some validation * Missing import * Fixed validation and added test * PR Feedback * Return nil when returning an error. * Cleaner comment * Add test case for multiple hos --------- Co-authored-by: bznein Co-authored-by: Nikolas De Giorgis Co-authored-by: Carlos Rodriguez * fix typo * remove unnecessary wrapping of function * Revert "remove unnecessary wrapping of function" This reverts commit c2a6bc60f5d4c1a0d925cf7aba1718abcae6fec9. * fix usage of function * (chore) replace reflect.DeepEqual with slices.Equal (#6697) * Replace reflect.DeepEqual with slices.Equal * Nit formatting * chore: comment hop slicing for clarity (#6702) Co-authored-by: Carlos Rodriguez * chore: cleanup forwarding tests (#6691) * chore: cleanup forwarding tests * lint * fix --------- Co-authored-by: Nikolas De Giorgis Co-authored-by: Gjermund Garaba * chore: pull out hop validation and consolidate for transfer+packet (#6695) * chore: pull out hop validation and consolidate transfer+packet * Update modules/apps/transfer/types/forwarding_test.go Co-authored-by: Carlos Rodriguez * cr fixes --------- Co-authored-by: Carlos Rodriguez * Remove unwind field in authz (#6701) * chore: remove unwind field in authz * chore: remove duplicate test * Update modules/apps/transfer/types/transfer_authorization.go Co-authored-by: Carlos Rodriguez --------- Co-authored-by: Carlos Rodriguez * chore: add packet data validation back (#6704) * (chore) Refactor code around forwarding validation (#6706) * Refactor validation * Fixed verification logic, added two tests * Fix check for unwind * removed unneeded indirection * Update modules/apps/transfer/types/msgs.go Co-authored-by: DimitrisJim * Add docstring. --------- Co-authored-by: Gjermund Garaba Co-authored-by: DimitrisJim * use setupForwardingPaths in test * feat(transfer): allow non-cosmos-sdk AccAddress in final receiver for forwarded packets (#6709) * allow non-cosmos-sdk AccAddress for forwarded packets * cr fixes * chore: pass only hops to sendTransfer + events rename (#6703) Co-authored-by: Carlos Rodriguez * test: forwarding test that verifies forwarded memo (#6707) * test: forwarding with memo * fix test * cr fixes * chore: update godoc for relay forwarding tests * chore: use module account instead of custom forward address (#6688) * chore: use module account instead of custom forward address * pull blocked addr checker into keeper method * lint * clean up IsBlockedAddr * chore: replace continue with if/else (#6700) Co-authored-by: Nikolas De Giorgis * add changelog * add test for invalid receiver address * Update CHANGELOG.md * Update CHANGELOG.md * make getForwardedPacket private * remove auxiliary burn coins function * nit: rename func method recv args in types/forwarding.go * chore: rename ShouldBeForwarded to HasFowarding * e2e: remove template test for three chain setup. * nit: no generics silly * nit: add clarifying comment to validate basic call on msg. * nit: remove unused key. * nit: clean up cli help text. * nit: don't export is blocked address helper. * nit: docustring for e2e test and helper. * nit: improve documentation for transfer's OnRecv callback. Co-authored-by: Damian Nolan * Apply suggestions from code review Co-authored-by: Aditya <14364734+AdityaSripal@users.noreply.github.com> * chore: remove unused function * perf: allocate slice to length of packet data tokens * chore(transfer/authz): wrapf unauthorized forwarding hops * lint * Update modules/apps/transfer/types/forwarding.go Co-authored-by: DimitrisJim * Preallocate slice but keep len==0 (#6725) * imp: validate allowed forwarding hops * test: unwind fails in Transfer rpc --------- Co-authored-by: Charly Co-authored-by: Carlos Rodriguez Co-authored-by: Charly Co-authored-by: Stefano Angieri Co-authored-by: sangier <45793271+sangier@users.noreply.github.com> Co-authored-by: Colin Axnér <25233464+colin-axner@users.noreply.github.com> Co-authored-by: DimitrisJim Co-authored-by: Hastur Co-authored-by: Damian Nolan Co-authored-by: Gjermund Garaba Co-authored-by: srdtrk <59252793+srdtrk@users.noreply.github.com> Co-authored-by: Maintain Co-authored-by: srdtrk Co-authored-by: Nikolas De Giorgis Co-authored-by: Gjermund Garaba Co-authored-by: Dzung Do | Decentrio Co-authored-by: bznein Co-authored-by: Aditya <14364734+AdityaSripal@users.noreply.github.com> --- CHANGELOG.md | 1 + .../core/03-connection/connection_test.go | 2 +- e2e/tests/transfer/authz_test.go | 6 +- e2e/tests/transfer/base_test.go | 2 +- e2e/tests/transfer/forwarding_test.go | 83 +- e2e/tests/transfer/incentivized_test.go | 5 +- e2e/tests/transfer/upgradesv1_test.go | 3 +- e2e/tests/transfer/upgradesv2_test.go | 3 +- e2e/tests/upgrades/upgrade_test.go | 4 +- e2e/tests/wasm/grandpa_test.go | 2 +- e2e/testsuite/testsuite.go | 86 +- e2e/testsuite/tx.go | 3 +- .../host/keeper/relay_test.go | 8 +- modules/apps/29-fee/keeper/events_test.go | 1 + modules/apps/29-fee/transfer_test.go | 4 +- modules/apps/callbacks/ibc_middleware_test.go | 14 +- modules/apps/callbacks/replay_test.go | 1 + modules/apps/callbacks/transfer_test.go | 2 + modules/apps/transfer/client/cli/tx.go | 51 +- modules/apps/transfer/ibc_module.go | 39 +- modules/apps/transfer/ibc_module_test.go | 149 ++- .../apps/transfer/internal/events/events.go | 32 +- modules/apps/transfer/internal/packet.go | 7 +- modules/apps/transfer/internal/packet_test.go | 18 +- modules/apps/transfer/keeper/export_test.go | 20 +- modules/apps/transfer/keeper/forwarding.go | 148 +++ .../apps/transfer/keeper/invariants_test.go | 1 + modules/apps/transfer/keeper/keeper.go | 41 + modules/apps/transfer/keeper/keeper_test.go | 35 + .../apps/transfer/keeper/mbt_relay_test.go | 5 + modules/apps/transfer/keeper/msg_server.go | 42 +- .../apps/transfer/keeper/msg_server_test.go | 140 ++- modules/apps/transfer/keeper/relay.go | 191 ++- .../transfer/keeper/relay_forwarding_test.go | 1031 +++++++++++++++++ modules/apps/transfer/keeper/relay_test.go | 147 ++- modules/apps/transfer/transfer_test.go | 6 +- modules/apps/transfer/types/authz.pb.go | 310 ++++- modules/apps/transfer/types/errors.go | 3 + modules/apps/transfer/types/events.go | 1 + modules/apps/transfer/types/forwarding.go | 80 ++ .../apps/transfer/types/forwarding_test.go | 342 ++++++ modules/apps/transfer/types/keys.go | 8 + modules/apps/transfer/types/msgs.go | 56 +- modules/apps/transfer/types/msgs_test.go | 133 ++- modules/apps/transfer/types/packet.go | 24 +- modules/apps/transfer/types/packet.pb.go | 341 +++++- modules/apps/transfer/types/packet_test.go | 166 ++- modules/apps/transfer/types/token.go | 17 + modules/apps/transfer/types/token_test.go | 53 + modules/apps/transfer/types/transfer.pb.go | 488 +++++++- .../transfer/types/transfer_authorization.go | 49 +- .../types/transfer_authorization_test.go | 196 +++- modules/apps/transfer/types/tx.pb.go | 131 ++- .../ibc/applications/transfer/v1/authz.proto | 10 + .../applications/transfer/v1/transfer.proto | 19 + proto/ibc/applications/transfer/v1/tx.proto | 4 +- .../ibc/applications/transfer/v2/packet.proto | 13 + testing/chain.go | 6 + testing/events.go | 32 +- testing/events_test.go | 2 +- testing/solomachine.go | 1 + testing/values.go | 2 + 62 files changed, 4396 insertions(+), 424 deletions(-) create mode 100644 modules/apps/transfer/keeper/forwarding.go create mode 100644 modules/apps/transfer/keeper/relay_forwarding_test.go create mode 100644 modules/apps/transfer/types/forwarding.go create mode 100644 modules/apps/transfer/types/forwarding_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 370f4eaf13b..747cc70957b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,6 +73,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/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/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..386e6a61c96 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,57 @@ 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.Hop{ + PortId: channelBtoC.PortID, + ChannelId: 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 b850d2c58e8..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) 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/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/modules/apps/27-interchain-accounts/host/keeper/relay_test.go b/modules/apps/27-interchain-accounts/host/keeper/relay_test.go index 28c71b36d73..bcef1c4f900 100644 --- a/modules/apps/27-interchain-accounts/host/keeper/relay_test.go +++ b/modules/apps/27-interchain-accounts/host/keeper/relay_test.go @@ -353,6 +353,7 @@ func (suite *KeeperTestSuite) TestOnRecvPacket() { suite.chainB.GetTimeoutHeight(), 0, "", + transfertypes.Forwarding{}, ) data, err := icatypes.SerializeCosmosTx(suite.chainA.GetSimApp().AppCodec(), []proto.Message{msg}, encoding) @@ -388,6 +389,7 @@ func (suite *KeeperTestSuite) TestOnRecvPacket() { suite.chainB.GetTimeoutHeight(), 0, "", + transfertypes.Forwarding{}, ) 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/29-fee/keeper/events_test.go b/modules/apps/29-fee/keeper/events_test.go index 38b64cadd80..f9a14aa3a50 100644 --- a/modules/apps/29-fee/keeper/events_test.go +++ b/modules/apps/29-fee/keeper/events_test.go @@ -115,6 +115,7 @@ func (suite *KeeperTestSuite) TestDistributeFeeEvent() { 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(), 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/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..14d0ccb61d8 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.Hop{PortId: pairSplit[0], ChannelId: 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..c524bb42cc5 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.Hop{PortId: "transfer", ChannelId: "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..f7108295cef --- /dev/null +++ b/modules/apps/transfer/keeper/forwarding.go @@ -0,0 +1,148 @@ +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.SenderChainIsSource(prevPacket.DestinationPort, prevPacket.DestinationChannel) { + // then send it back to the escrow address + if err := k.escrowCoin(ctx, forwardingAddr, escrow, coin); err != nil { + return err + } + + continue + } + + if err := k.bankKeeper.BurnCoins(ctx, types.ModuleName, sdk.NewCoins(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..b37592d85d2 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" @@ -350,3 +351,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/msg_server.go b/modules/apps/transfer/keeper/msg_server.go index c8cc0f5b0eb..9c40125cc50 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.Hop{PortId: trace.PortId, ChannelId: 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..13b2dc72d2c 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.Hop{PortId: denom.Trace[1].PortId, ChannelId: 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.Hop{PortId: denom.Trace[3].PortId, ChannelId: denom.Trace[3].ChannelId}, + types.Hop{PortId: denom.Trace[2].PortId, ChannelId: denom.Trace[2].ChannelId}, + types.Hop{PortId: denom.Trace[1].PortId, ChannelId: 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.Hop{PortId: ibctesting.MockPort, ChannelId: "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.Hop{PortId: denom.Trace[1].PortId, ChannelId: denom.Trace[1].ChannelId}, + types.Hop{PortId: ibctesting.MockPort, ChannelId: "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..4584307c0d2 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 @@ -139,14 +148,17 @@ func (k Keeper) sendTransfer( 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 +180,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()), @@ -201,7 +213,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 +226,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,14 +344,13 @@ 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 @@ -320,7 +374,10 @@ func (k Keeper) refundPacketTokens(ctx sdk.Context, packet channeltypes.Packet, return err } - if err := k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, sender, sdk.NewCoins(coin)); err != nil { + 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)) } } @@ -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..292cd88ed5f --- /dev/null +++ b/modules/apps/transfer/keeper/relay_forwarding_test.go @@ -0,0 +1,1031 @@ +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.Hop{ + PortId: pathBtoC.EndpointA.ChannelConfig.PortID, + ChannelId: 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.Hop{ + PortId: pathBtoC.EndpointA.ChannelConfig.PortID, + ChannelId: 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.Hop{ + PortId: pathBtoC.EndpointA.ChannelConfig.PortID, + ChannelId: 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.Hop{ + PortId: pathBtoC.EndpointA.ChannelConfig.PortID, + ChannelId: 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.Hop{ + PortId: pathAtoB.EndpointB.ChannelConfig.PortID, + ChannelId: 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.Hop{ + PortId: pathAtoB.EndpointB.ChannelConfig.PortID, + ChannelId: 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.Hop{ + PortId: pathAtoB.EndpointB.ChannelConfig.PortID, + ChannelId: 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{ + { + PortId: pathBtoC.EndpointA.ChannelConfig.PortID, + ChannelId: 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) +} diff --git a/modules/apps/transfer/keeper/relay_test.go b/modules/apps/transfer/keeper/relay_test.go index 5fcd6c59c13..e9824c05ace 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.Hop{ + PortId: path.EndpointA.ChannelConfig.PortID, + ChannelId: 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.Hop{ + PortId: path.EndpointA.ChannelConfig.PortID, + ChannelId: path.EndpointA.ChannelID, + }) + }, + ibcerrors.ErrInvalidRequest, + }, } for _, tc := range testCases { @@ -156,9 +201,10 @@ func (suite *KeeperTestSuite) TestSendTransfer() { 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) @@ -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() }, - sdkerrors.ErrUnauthorized, + ibcerrors.ErrUnauthorized, + }, + { + "failure: receiver is invalid", + func() { + receiver = "invalid-address" + }, + 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, @@ -499,7 +555,7 @@ func (suite *KeeperTestSuite) TestOnRecvPacket_ReceiverIsSource() { // 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) + 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/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..7af7d339522 --- /dev/null +++ b/modules/apps/transfer/types/forwarding.go @@ -0,0 +1,80 @@ +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 +} + +// 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..357291ee735 --- /dev/null +++ b/modules/apps/transfer/types/forwarding_test.go @@ -0,0 +1,342 @@ +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.Hop{ + PortId: types.PortID, + ChannelId: 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.Hop{ + PortId: invalidShortPort, + ChannelId: ibctesting.FirstChannelID, + }, + ), + types.ErrInvalidForwarding, + }, + { + "invalid forwarding with too long hop port ID", + types.NewForwarding( + false, + types.Hop{ + PortId: invalidLongPort, + ChannelId: ibctesting.FirstChannelID, + }, + ), + types.ErrInvalidForwarding, + }, + { + "invalid forwarding with non-alpha hop port ID", + types.NewForwarding( + false, + types.Hop{ + PortId: invalidPort, + ChannelId: ibctesting.FirstChannelID, + }, + ), + types.ErrInvalidForwarding, + }, + { + "invalid forwarding with too long hop channel ID", + types.NewForwarding( + false, + types.Hop{ + PortId: types.PortID, + ChannelId: invalidLongChannel, + }, + ), + types.ErrInvalidForwarding, + }, + { + "invalid forwarding with too short hop channel ID", + types.NewForwarding( + false, + types.Hop{ + PortId: types.PortID, + ChannelId: invalidShortChannel, + }, + ), + types.ErrInvalidForwarding, + }, + { + "invalid forwarding with non-alpha hop channel ID", + types.NewForwarding( + false, + types.Hop{ + PortId: types.PortID, + ChannelId: 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.Hop{ + PortId: invalidShortPort, + ChannelId: ibctesting.FirstChannelID, + }, + ), + types.ErrInvalidForwarding, + }, + { + "invalid forwarding with too long hop port ID", + types.NewForwardingPacketData( + "", + types.Hop{ + PortId: invalidLongPort, + ChannelId: ibctesting.FirstChannelID, + }, + ), + types.ErrInvalidForwarding, + }, + { + "invalid forwarding with non-alpha hop port ID", + types.NewForwardingPacketData( + "", + types.Hop{ + PortId: invalidPort, + ChannelId: ibctesting.FirstChannelID, + }, + ), + types.ErrInvalidForwarding, + }, + { + "invalid forwarding with too long hop channel ID", + types.NewForwardingPacketData( + "", + types.Hop{ + PortId: types.PortID, + ChannelId: invalidLongChannel, + }, + ), + types.ErrInvalidForwarding, + }, + { + "invalid forwarding with too short hop channel ID", + types.NewForwardingPacketData( + "", + types.Hop{ + PortId: types.PortID, + ChannelId: invalidShortChannel, + }, + ), + types.ErrInvalidForwarding, + }, + { + "invalid forwarding with non-alpha hop channel ID", + types.NewForwardingPacketData( + "", + types.Hop{ + PortId: types.PortID, + ChannelId: 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.Hop{ + PortId: invalidShortPort, + ChannelId: ibctesting.FirstChannelID, + }, + host.ErrInvalidID, + }, + { + "invalid hop with too long port ID", + types.Hop{ + PortId: invalidLongPort, + ChannelId: ibctesting.FirstChannelID, + }, + host.ErrInvalidID, + }, + { + "invalid hop with non-alpha port ID", + types.Hop{ + PortId: invalidPort, + ChannelId: ibctesting.FirstChannelID, + }, + host.ErrInvalidID, + }, + { + "invalid hop with too long channel ID", + types.Hop{ + PortId: types.PortID, + ChannelId: invalidLongChannel, + }, + host.ErrInvalidID, + }, + { + "invalid hop with too short channel ID", + types.Hop{ + PortId: types.PortID, + ChannelId: invalidShortChannel, + }, + host.ErrInvalidID, + }, + { + "invalid hop with non-alpha channel ID", + types.Hop{ + PortId: types.PortID, + ChannelId: 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.Hop{ + PortId: types.PortID, + ChannelId: 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..2fea4f4a5b2 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.Hop{PortId: invalidPort, ChannelId: validChannel})), types.ErrInvalidForwarding}, + {"invalid forwarding info channel", types.NewMsgTransfer(validPort, validChannel, coins, sender, receiver, clienttypes.ZeroHeight(), 100, "", types.NewForwarding(false, types.Hop{PortId: validPort, ChannelId: 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..6a70b8d72a3 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,99 @@ 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.Hop{ + PortId: invalidPort, + ChannelId: "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.Hop{ + PortId: "transfer", + ChannelId: 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 +517,7 @@ func TestGetPacketSender(t *testing.T) { sender, receiver, "", + emptyForwardingPacketData, ), sender, }, @@ -394,6 +533,7 @@ func TestGetPacketSender(t *testing.T) { "", receiver, "abc", + emptyForwardingPacketData, ), "", }, @@ -423,7 +563,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 +582,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 +601,9 @@ func TestPacketDataProvider(t *testing.T) { }, sender, receiver, - `{"src_callback": "string"}`), + `{"src_callback": "string"}`, + emptyForwardingPacketData, + ), "string", }, { @@ -471,7 +617,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 +633,9 @@ func TestPacketDataProvider(t *testing.T) { }, sender, receiver, - ""), + "", + emptyForwardingPacketData, + ), nil, }, { @@ -499,7 +649,9 @@ func TestPacketDataProvider(t *testing.T) { }, sender, receiver, - "invalid"), + "invalid", + emptyForwardingPacketData, + ), nil, }, } @@ -530,6 +682,7 @@ func TestFungibleTokenPacketDataOmitEmpty(t *testing.T) { sender, receiver, "", + emptyForwardingPacketData, ), false, }, @@ -545,6 +698,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..20554327cf1 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.Hop{PortId: ibctesting.MockPort, ChannelId: "channel-1"}, types.Hop{PortId: ibctesting.MockPort, ChannelId: "channel-2"}) + transferAuthz.Allocations[0].AllowedForwarding = []types.AllowedForwarding{ + { + Hops: []types.Hop{ + {PortId: ibctesting.MockPort, ChannelId: "channel-1"}, + {PortId: ibctesting.MockPort, ChannelId: "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{ + {PortId: ibctesting.MockPort, ChannelId: "channel-1"}, + {PortId: ibctesting.MockPort, ChannelId: "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.Hop{PortId: ibctesting.MockPort, ChannelId: "channel-1"}, + types.Hop{PortId: ibctesting.MockPort, ChannelId: "channel-2"}, + types.Hop{PortId: ibctesting.MockPort, ChannelId: "channel-3"}, + ) + transferAuthz.Allocations[0].AllowedForwarding = []types.AllowedForwarding{ + { + Hops: []types.Hop{ + {PortId: ibctesting.MockPort, ChannelId: "channel-1"}, + {PortId: ibctesting.MockPort, ChannelId: "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.Hop{PortId: ibctesting.MockPort, ChannelId: "channel-1"}, + types.Hop{PortId: "3", ChannelId: "channel-3"}, + ) + transferAuthz.Allocations[0].AllowedForwarding = []types.AllowedForwarding{ + { + Hops: []types.Hop{ + {PortId: ibctesting.MockPort, ChannelId: "channel-1"}, + {PortId: ibctesting.MockPort, ChannelId: "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.Hop{PortId: ibctesting.MockPort, ChannelId: "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.Hop{PortId: ibctesting.MockPort, ChannelId: "channel-1"}, + types.Hop{PortId: ibctesting.MockPort, ChannelId: "channel-2"}, + ) + transferAuthz.Allocations[0].AllowedForwarding = []types.AllowedForwarding{ + { + Hops: []types.Hop{ + {PortId: ibctesting.MockPort, ChannelId: "channel-2"}, + {PortId: ibctesting.MockPort, ChannelId: "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() @@ -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.PortID, "channel-1"}}}, + } + }, + true, + }, { "empty allocations", func() { @@ -462,8 +632,8 @@ 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, @@ -473,7 +643,27 @@ func (suite *TypesTestSuite) TestTransferAuthorizationValidateBasic() { 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{{"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.PortID, "invalid/channel"}}}, + } + }, + false, }, } 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/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..74a127686a6 100644 --- a/testing/solomachine.go +++ b/testing/solomachine.go @@ -378,6 +378,7 @@ func (solo *Solomachine) SendTransfer(chain *TestChain, portID, channelID 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)