fix(split routing): topological order for cross-depth hops #239
Open
tamaralipows wants to merge 2 commits into
Open
fix(split routing): topological order for cross-depth hops #239tamaralipows wants to merge 2 commits into
tamaralipows wants to merge 2 commits into
Conversation
06fae40 to
4216717
Compare
f56881e to
e76c4d4
Compare
tamaralipows
commented
Jun 4, 2026
| /// ``` | ||
| /// | ||
| /// The DAI→PEPE split between Pool B and Pool C must wait until all DAI | ||
| /// has been produced (from both paths through the merged Pool A swap). |
Contributor
Author
There was a problem hiding this comment.
Docstring getting too long. Considering removing this example.
e76c4d4 to
aa3d5d6
Compare
tamaralipows
commented
Jun 4, 2026
| /// The DAI→PEPE split between Pool B and Pool C must wait until all DAI | ||
| /// has been produced (from both paths through the merged Pool A swap). | ||
| pub(crate) fn build_split_route( | ||
| paths: &[PathAllocation], |
Contributor
Author
There was a problem hiding this comment.
It's okay that we are already taking the deduplicated (after merged_shared_hops) paths here, because in-degree isn't tracking how many times Pool A appears - it's tracking how many distinct swaps produce a given token. We still have this information with the given data.
Replace naive BFS in build_split_route with Kahn's topological sort. The BFS visited tokens in discovery order, which broke when paths of different lengths converged on the same intermediate token — the shorter path caused the token to be visited before the longer path's inflows arrived. Since merge_shared_hops collapses all appearances of a shared pool hop into a single combined swap (one hop on the router, not one per path), all inflows to a token must be complete before its outgoing swap is emitted. This also saves gas: a single hop on the pool with the combined input is cheaper than multiple hops on the same pool. The topological sort guarantees this by tracking the in-degree of each token and only processing it once every upstream swap that produces it has been emitted. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
aa3d5d6 to
50b86d7
Compare
When two paths use the same pools in opposite order (e.g. path 1 uses Pool A then Pool B, path 2 uses Pool B then Pool A), merge_shared_hops collapses them into single swaps, creating a cycle in the token dependency graph. The topological sort then deadlocks — some tokens never reach in-degree zero. Detect this after the sort and return an error instead of silently producing a partial route. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
8ada7d8 to
de4682c
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem, summarized:
The original two paths:
The merged paths:
Our merging algo was emitting the USDC ─ pool_a ─▶ DAI before emitting the USDT ─▶ USDC swap. This would cause unintended consequences on our router side, since only the amount for WETH ─▶ USDC would be inputted into the final USDC ─▶ DAI swap, ignoring the USDT ─▶ USDC amount.
Related addition to the Tycho docs: propeller-heads/tycho#1068
Solution
Replace naive BFS in build_split_route with Kahn's topological sort.
Summary of Kahn's topological sort:
Consequence
The USDC ─── pool_a ──▶ DAI will now be one swap meaning we should only count gas once. The swap will be emitted only after the inflow swaps are emitted. Our router is happy.
Important Note: Unsupported edge case
The following case is still not supported, because Pool A and Pool B cause a deadlock from being swapped in opposite orders in different branches:
This is a pretty rare case and any solutions I found resulted in a massive refactor. For now, I've added a test that ensures this results in an error. Let me know if you agree or if you think this is worth spending time to support.