-
Notifications
You must be signed in to change notification settings - Fork 381
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
Conversation
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 With middleware this flow would involve wiring up the stack like this:
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 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. |
There was a problem hiding this 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.
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.
@@ -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> |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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
// 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( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nextSequence = sendFungibleTokens( | |
nextPacketSequence = sendFungibleTokens( |
There was a problem hiding this 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.
There was a problem hiding this 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
.
When doing this exercise, I noticed a couple of things that I left as comments.
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 { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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...
// 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)) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this 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 feedback @gjermundgaraba . Will mull over better naming, but if you have some suggestions feel free to share!
There was a problem hiding this 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
// we're on the last hop, we can set memo and clear | ||
// the next forwardingPath | ||
memo = forwardingPath.memo | ||
nextForwardingPath = nil |
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this 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
* simplify revert in-flight changes * typo
* ics20: use protobuf encoding for packet data of v2 * Update README.md
Co-authored-by: Damian Nolan <[email protected]>
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]>
Allows users to atomically route their tokens through a series of paths with a single packet.
Desired properties:
Special thanks to the @strangelove-ventures team for working on the PFM middleware which serves as a precursor to this feature