Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ICS20-2: Add Token Forwarding ability to ICS20-2 #1090

Merged
merged 28 commits into from
Jun 24, 2024
Merged

Conversation

AdityaSripal
Copy link
Member

Allows users to atomically route their tokens through a series of paths with a single packet.

Desired properties:

  • Atomicity: Tokens either get fully sent to final destination chain and receiver or they return to original sender chain/sender. This means that on error or timeout, the intermediate chains must revert state such that there is no difference between their current state and their state prior to the original send (modulo unrelated state changes)
  • Correctness: The token denoms and all the escrow accounts across the path must be the exact same as it would be if the tokens travelled through each hop with a separate packet

Special thanks to the @strangelove-ventures team for working on the PFM middleware which serves as a precursor to this feature

@jtieri
Copy link
Member

jtieri commented Mar 29, 2024

One important aspect of PFM is the ability to compose more complex user experiences by integrating multiple middleware in the stack together, so that you can essentially receive an ICS-20 transfer, perform some action on the intermediate chain, and then forward tokens to the destination

Example: A user wants to send tokens from a source chain to an intermediate chain. On the intermediate chain the funds will be received via OnRecvPacket, now that the funds are in the user account the tokens will be swapped in a liquidity pool for a new asset. If the swap is successful, forward the tokens received from the swap to a different account on a destination chain

With middleware this flow would involve wiring up the stack like this:

channel.OnRecvPacket
swap.OnRecvPacket
forward.OnRecvPacket
transfer.OnRecvPacket

Then when a packet comes in for an ICS-20 transfer, the swap middleware would call into the underlying app and that call would fall through to the transfer module so that the appropriate bookkeeping could take place to get funds into the user controlled account. Control would then be passed back up to the swap middleware where the swap could take place, the middleware would mutate the FungibleTokenPacketData so that the token denom and amount refer to the token that was received from the swap. Then control is passed to the forward middleware to initiate the forward.

Swap middleware

Bringing token forwarding into the ICS-20 protocol is amazing to hear, but I think preserving this type of composability for developers to build these more complicated experiences is an important aspect; otherwise, it becomes a bit less appealing.

Copy link
Contributor

@crodriguezvega crodriguezvega left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the quick turnaround with the changes, @AdityaSripal.

I did a first pass and left a few comments. I think my biggest gap of understanding at the moment is in the usage of channelForwardingAddresses (I think there might be some logic errors with that).

To help with the reasoning I tried to make a coupe of diagrams. Both go through the scenario where and error ack is written on the destination chain, but in one of them the middle hop is the source of the token and in the other one it is not.

path-forwarding-error-ack

(excalidraw link)

The diagram is not completely correct, because I think the logic to transfer/burn token on the middle hop needs to be adjusted a bit.

spec/app/ics-020-fungible-token-transfer/README.md Outdated Show resolved Hide resolved
spec/app/ics-020-fungible-token-transfer/README.md Outdated Show resolved Hide resolved
spec/app/ics-020-fungible-token-transfer/README.md Outdated Show resolved Hide resolved
spec/app/ics-020-fungible-token-transfer/README.md Outdated Show resolved Hide resolved
spec/app/ics-020-fungible-token-transfer/README.md Outdated Show resolved Hide resolved
spec/app/ics-020-fungible-token-transfer/README.md Outdated Show resolved Hide resolved
spec/app/ics-020-fungible-token-transfer/README.md Outdated Show resolved Hide resolved
@@ -92,6 +93,7 @@ The fungible token transfer bridge module tracks escrow addresses associated wit
```typescript
interface ModuleState {
channelEscrowAddresses: Map<Identifier, string>
channelForwardingAddresses: Map<Identifier, string>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this actually needed? It seems like we could manage by using channelEscrowAddresses only?

Copy link
Contributor

@sangier sangier Apr 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless I am missing something about the reasoning behind the separation

channelEscrowAddresses: Map<Identifier, string>
channelForwardingAddresses: Map<Identifier, string>

I agree we could use directly channelEscrowAddresses. The channelEscrowAddresses mapping is populated during onChanOpenInit at line 157 or onChainOpenTry at line 181. That means we could potentially access this mapping at line 331 using the packet.destintionChannel as identifier.
Instead the channelForwardingAddresses is never set. Thus the read we do at line 331 may be illegal.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the only reason for using this is a logical separation, I think I would be in favour of simply using the escrow address. Having this other set of addresses adds to the overall complexity without adding a huge amount of value IMO.

And like @damiannolan is linking, the existing escrow address has already been reasoned about in terms of collisions.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The forwarding address is mainly just used as pass through storage based on how ics20 is constructed in the non-forwarding case. I think the spec just needs to specify that this address should be an address the transfer module controls. I don't think it should reuse the escrow addresses

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless I am missing something, I can't think of a reason why you need channel isolated forwarding addresses cosmos/ibc-go#6561 (comment)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason I decided to split it off to a separate address is just an additional security blanket. The balance of the escrow addresses is security critical. If the balance ends up in an invalid state, that can be a very painful issue to unwind.

It should be completely fine if there's no bugs. But if there's a bug in forwarding, i figure having a separate account might make the impact a bit more isolated.

Of course it is receiving from and sending back to the escrow addresses, so if there's issues there it could still be a problem for escrow account balances.

I'm ok either way. I just figured to start out with a separate account for clarity and avoiding having the escrow account representing more than one thing semantically.

We can use the same account as well as @colin-axner suggests

spec/app/ics-020-fungible-token-transfer/README.md Outdated Show resolved Hide resolved
spec/app/ics-020-fungible-token-transfer/README.md Outdated Show resolved Hide resolved
// send the tokens we received above to the next port and channel
// on the forwarding path
// and reduce the forwardingPath by the first element
nextSequence = sendFungibleTokens(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
nextSequence = sendFungibleTokens(
nextPacketSequence = sendFungibleTokens(

Copy link
Contributor

@sangier sangier left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello guys and thank you for the great work!

I have done a first review pass and left some comments and suggestions.
Overall, I found a bit complicated reasoning by hand on certain specific scenarios, thus I agree having quint spec would be much helpful to ensure certain properties are respected.

To analyse the function flows I derived with the help of GPT4 and this app.code2flow website some flowcharts diagrams that I repost here for reference in case they can be useful for others.

SendFungibleTokens

onRecvPacket

OnAckPacket

OnTimeout

revertInFlightChanges

Copy link
Contributor

@crodriguezvega crodriguezvega left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These diagrams helped me personally to reason through the flow of an error acknowledgement with a middle hop for the 4 scenarios that are handled in revertInFlightChanges.

path-forwarding-4
(excalidraw link)

When doing this exercise, I noticed a couple of things that I left as comments.

spec/app/ics-020-fungible-token-transfer/README.md Outdated Show resolved Hide resolved
spec/app/ics-020-fungible-token-transfer/README.md Outdated Show resolved Hide resolved
Co-authored-by: Carlos Rodriguez <[email protected]>
if len(forwardingPath.hops) > 0 {
//check that next channel supports token forwarding
channel = provableStore.get(channelPath(forwardingPath.hops[0].portID, forwardingPath.hops[0].channelID))
if channel.version != "ics20-2" && len(forwardingPath.hops) > 1 {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does all the previous minting / transfer need to be reverted here, or is it implicit?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it's kinda implicit: if an error ack is returned then the datagram handler should not commit any state changes. But I agree, that maybe it would be good to mention this explicitly in the specs (maybe in ICS26? I cannot remember from the top of my head if we already do that...

@crodriguezvega crodriguezvega mentioned this pull request Jun 5, 2024
24 tasks
// the packet timed-out, so refund the tokens
refundTokens(packet)
// check if the packet sent is from a previously forwarded packet
prevPacket = privateStore.get(packetForwardPath(packet.sourcePort, packet.sourceChannel, packet.sequence))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have an observation on the naming here from reading this quite fresh:
It took me quite a bit of time and mental gymnastic to get the packets straight in my head here (to be fair, this could just be because I am not so used to this).
In particular, I found it tricky to map all these names to the right context:

  • Forwarded packet
  • Previous packet
  • Sent packet
  • Received packet
  • Packet (in the context of onTimeout, onAck, OnRecv)

At first I thought it was an error that the timeout packet was sent in to revertInFlightChanges as sentPacket, because "surely the forwarded packet, aka prevPacket was the one we sent". But after rereading, I realized that forwardedPacket was not actually received packet. So it is correct, of course, but with all these names it was not so easy to make a mental model of the different packets.

Not sure what the best way to deal with this is, but perhaps there is a way we could tweak the names to make it a bit easier to grasp on first read.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is good feedback; thanks, @gjermundgaraba. Maybe a diagram would also help; I can try to draw something in the next couple of days.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the feedback @gjermundgaraba . Will mull over better naming, but if you have some suggestions feel free to share!

@cosmos cosmos deleted a comment from Vedat7711 Jun 5, 2024
Copy link
Contributor

@chatton chatton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking great! Had just a few small questions/comments

spec/app/ics-020-fungible-token-transfer/README.md Outdated Show resolved Hide resolved
spec/app/ics-020-fungible-token-transfer/README.md Outdated Show resolved Hide resolved
spec/app/ics-020-fungible-token-transfer/README.md Outdated Show resolved Hide resolved
// we're on the last hop, we can set memo and clear
// the next forwardingPath
memo = forwardingPath.memo
nextForwardingPath = nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stylistic nit here, rather than re-assigning to nil and using that as a special value, I think it would be a bit nicer to always have a non nil type here, and have something like a forwardingPath.hasNext() or forwardingPath.lastHop() or some sort of other mechanism which examines the state of the hops and can be used.

@@ -92,6 +93,7 @@ The fungible token transfer bridge module tracks escrow addresses associated wit
```typescript
interface ModuleState {
channelEscrowAddresses: Map<Identifier, string>
channelForwardingAddresses: Map<Identifier, string>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the only reason for using this is a logical separation, I think I would be in favour of simply using the escrow address. Having this other set of addresses adds to the overall complexity without adding a huge amount of value IMO.

And like @damiannolan is linking, the existing escrow address has already been reasoned about in terms of collisions.

currentTime() + DefaultHopTimeoutPeriod,
)
// store packet for future sending ack
privateStore.set(packetForwardPath(forwardingPath.hops[0].portID, forwardingPath.hops[0].channelID, packetSequence), packet)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only store packetID, storing the other packet fields in unnecessary

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to change the function signature of writeAcknowledgement which I didn't want to do in this PR. But agree we should probs do for release

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the acknowledgement handler needs the full packet. The relayers currently grab the packet from the writeAcknowledgement event emissions. So we would need to think a bit more about how to handle relaying in the async ack case.

Copy link
Contributor

@colin-axner colin-axner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving my general approval. Spec needs updating based on latest implementation developments. Happy to see those applied in whatever fashion necessary

crodriguezvega and others added 8 commits June 19, 2024 20:36
* ics20: use protobuf encoding for packet data of v2

* Update README.md
Co-authored-by: Cian Hatton <[email protected]>
Co-authored-by: Carlos Rodriguez <[email protected]>
* nit: rename ForwardingInfo to Forwarding

* use Denom and Trace types

* Apply suggestions from code review

Co-authored-by: Aditya <[email protected]>

* comments

---------

Co-authored-by: Aditya <[email protected]>
@AdityaSripal AdityaSripal merged commit 80f3c1f into main Jun 24, 2024
3 checks passed
@AdityaSripal AdityaSripal deleted the aditya/forwarding branch June 24, 2024 14:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: No status
Development

Successfully merging this pull request may close these issues.

9 participants