feat(core): set min_amount_out from the pAMM fallback amount - #402
Open
tamaralipows wants to merge 6 commits into
Open
feat(core): set min_amount_out from the pAMM fallback amount#402tamaralipows wants to merge 6 commits into
tamaralipows wants to merge 6 commits into
Conversation
`fallback_amount_out` re-simulates a route with every `propammrouter:` leg replaced by the Uniswap V3 pool the PropAMMRouter falls back to. The encoder derives `min_amount_out` from that number instead of from the pAMM quote. The contract-side retry (propeller-heads/tycho#1278) stops a stale pAMM quote from reverting the venue call, but the route-level `minAmountOut` still comes from the pAMM quote, which the fallback cannot clear. So the route reverts on `NegativeSlippage` instead of `StaleUpdate`. `FallbackFees` mirrors the router's `resolvedFee`: the per-pair override if set, else `fallbackFee`. `FallbackPoolIndex` indexes uniswap_v3 components by sorted pair and fee tier, so the quote path needs no RPC call. `FallbackAmountOut::NoFallbackPool` marks a route the caller must drop, because no pool exists at the resolved tier and the fallback would revert too. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
tamaralipows
force-pushed
the
tnl/feat/propamm-fallback-quoting
branch
2 times, most recently
from
August 6, 2026 03:54
38af81d to
c975481
Compare
`FallbackFeeFetcher` reads `fallbackFee` from the PropAMMRouter and the per-pair override for every pair the pAMM venues quote, then writes them into `SharedFallbackFees`. Governance can change both without a contract upgrade, so the tiers cannot be hardcoded. They change rarely, so a timer is enough and the quote path stays free of RPC calls. Each refresh costs one `fallbackFee` call, one `getPairs` call per venue, and one `getPairFee` call per pair. Fermi quotes about 8 pairs and Kipseli about 4. A venue that fails to answer is skipped, so one unreachable venue keeps the tiers of the others. A failed `fallbackFee` read aborts the refresh and keeps the previous tiers, because every pair without an override resolves through it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`SolverWorker` builds a `FallbackPoolIndex` when it initializes its graph, then updates it from every `MarketEvent::MarketUpdated`. A pool qualifies on its component alone — protocol system, token pair and `fee` attribute — so the index only changes when the market adds or removes a component. `apply_event` costs one lookup per added component. A full rebuild takes 97 ms over 40,000 components, so doing it per block or per quote would be visible in solve latency. State changes cannot alter whether a component qualifies, so `updated_components` are ignored. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
tamaralipows
force-pushed
the
tnl/feat/propamm-fallback-quoting
branch
3 times, most recently
from
August 6, 2026 19:50
85be9ba to
ba735e7
Compare
tamaralipows
added a commit
to propeller-heads/tycho
that referenced
this pull request
Aug 6, 2026
Adds `PropAMMFallbackExecutor` and the `propammfallback:{venue}`
protocol family. Same calldata as #1212's `PropAMMExecutor`, so both
share `PropAMMSwapEncoder`; only the call target differs.
`PropAMMExecutor` calls the venue directly, so a stale maker quote
reverts the route. That is what makes integrator simulations fail on
routes that execute fine in a Titan block. Titan's PropAMMRouter wraps
the venue call in try/catch and falls back to a single-hop Uniswap V3
pool at `resolvedFee(tokenIn, tokenOut)`.
The router address is a constant, not a constructor argument. There is
one deployment, it sits behind a UUPS proxy so upgrades keep the
address, and a wrong value would route every swap to an arbitrary
contract.
`amountOutMin` is 0 on the router call: any non-zero value makes the
Uniswap fallback revert on price for the trades it exists to rescue. The
TychoRouter's route-level `minAmountOut` is the binding check, so the
quoting side must lower it — see propeller-heads/fynd#402.
Gas: the family is ProtocolWillDebit (input transfer plus approval) and
carries `PROPAMM_FALLBACK_OVERHEAD_GAS` for the router itself.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
tamaralipows
added a commit
to propeller-heads/tycho
that referenced
this pull request
Aug 6, 2026
Adds `PropAMMFallbackExecutor` and the `propammfallback:{venue}`
protocol family. Same calldata as #1212's `PropAMMExecutor`, so both
share `PropAMMSwapEncoder`; only the call target differs.
`PropAMMExecutor` calls the venue directly, so a stale maker quote
reverts the route. That is what makes integrator simulations fail on
routes that execute fine in a Titan block. Titan's PropAMMRouter wraps
the venue call in try/catch and falls back to a single-hop Uniswap V3
pool at `resolvedFee(tokenIn, tokenOut)`.
The router address is a constant, not a constructor argument. There is
one deployment, it sits behind a UUPS proxy so upgrades keep the
address, and a wrong value would route every swap to an arbitrary
contract.
`amountOutMin` is 0 on the router call: any non-zero value makes the
Uniswap fallback revert on price for the trades it exists to rescue. The
TychoRouter's route-level `minAmountOut` is the binding check, so the
quoting side must lower it — see propeller-heads/fynd#402.
Gas: the family is ProtocolWillDebit (input transfer plus approval) and
carries `PROPAMM_FALLBACK_OVERHEAD_GAS` for the router itself.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`SolverWorker` computes `fallback_amount_out` for every route it produces and stamps it on the `Route`. The encoder reads it to derive `min_amount_out`. A route with no `propammrouter:` leg is left alone. Routes the fallback cannot price are dropped, not returned: without a fallback amount there is no justifiable `min_amount_out` for them. That covers a pair with no Uniswap V3 pool at the router's fee tier, and split routes until they are supported. `SharedFallbackFees` reaches the worker through `WorkerPoolConfig`, so every worker reads the tiers the `FallbackFeeFetcher` refreshes. The fetcher runs on Ethereum only, since the PropAMMRouter is deployed there; on other chains every pair resolves through the compiled-in default. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The encoder floors `min_amount_out` at the amount the route delivers when its pAMM legs fall back to Uniswap V3. `amount_out` in the response stays the pAMM quote; only the floor moves. Without this the contract-side fallback does not help: the route stops reverting inside the venue and starts reverting on the router's own price check instead. `EncodingOptions::cover_propamm_fallback` turns it off, and defaults to on. A caller that submits through Titan sets it to `false`: the maker's quote always lands there, so the tighter bound protects them better. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
tamaralipows
force-pushed
the
tnl/feat/propamm-fallback-quoting
branch
2 times, most recently
from
August 6, 2026 23:20
063a04a to
2dd51c9
Compare
`fallback_amount_out` replays the route with each `propammfallback:` leg pointing at its Uniswap V3 fallback pool, instead of walking the swaps itself. `replay_route` already threads split fractions and post-swap pool states, so a split route prices correctly and two legs on one fallback pool see it deplete. Split routes are no longer dropped. `FallbackAmountOut::SplitNotSupported` is replaced by `NotPriceable`, which covers any route the substituted simulation rejects. Also renames the protocol prefix to `propammfallback:`, matching propeller-heads/tycho#1278, orders the module outward-facing first, and drops the `cover_propamm_fallback` encoding option. Nobody submits pAMM routes through Titan today, so the opt-out was speculative; with it gone the wire format and the OpenAPI spec are untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
tamaralipows
force-pushed
the
tnl/feat/propamm-fallback-quoting
branch
from
August 6, 2026 23:32
2dd51c9 to
5fe4b91
Compare
tamaralipows
commented
Aug 6, 2026
Contributor
Author
There was a problem hiding this comment.
This is the file I've reviewed the least. Please use extra care here
tamaralipows
marked this pull request as ready for review
August 6, 2026 23:39
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.
Fynd side of the pAMM simulation fix. Contract side: propeller-heads/tycho#1278.
Problem
propeller-heads/tycho#1278 makes the PropAMMRouter fall back to a Uniswap V3 pool when a stale pAMM quote reverts the venue. That is not enough, because Fynd writes
min_amount_outbefore the fallback runs.The pAMM quotes 2000 USDC. The user accepts 1% slippage.
Step 3 works. Step 5 fails, so the revert moves from
StaleUpdatetoNegativeSlippage.How it works
Four steps, one per commit.
Learn which pool the router would use. The router picks it with
resolvedFee(tokenIn, tokenOut), a public view function. A background task reads that on a timer and caches it. Governance changes it rarely, and only about 12 pairs across Fermi and Kipseli need one, so the quote path never waits on RPC.Keep the candidate pools indexed. Each solver worker holds an index of Uniswap V3 pools by token pair and fee tier, built once and updated from market events. A pool qualifies on its component alone, so the index only moves when the market adds or removes one. Rebuilding it per block would cost 97 ms.
Price the fallback while solving. When a worker produces a route with a pAMM leg, it replays that route with each pAMM leg swapped for its fallback pool, and stamps the result on the route. Replay is the existing
replay_route, so split fractions and shared-pool depletion behave as they do everywhere else. A route whose fallback cannot be priced is dropped rather than quoted — with no fallback amount there is nomin_amount_outwe can justify.Use it when encoding. The encoder derives
min_amount_outfrom the stamped amount instead of the pAMM quote.amount_outin the response stays the pAMM quote; only the floor moves.No client impact
Clients change nothing.
The wire format is untouched —
clients/openapi.jsonis identical to main, so there is no field to set and no client to regenerate.Nothing emits
propammfallback:yet, so today every route reports no pAMM leg and behaviour is unchanged. Venue selection turns it on.Once on,
amount_outstill carries the pAMM quote. Onlymin_amount_outdrops, on pAMM routes, which is what lets those quotes simulate.Still open
Selecting which venues use the
propammfallback:prefix. That belongs in tycho-simulation'sPriceLevelStreamBuilder(propeller-heads/tycho#1212) and must read the router's on-chain whitelist.Limits
bopAMM does not work. The PropAMMRouter whitelist does not contain its venue, so bopAMM quotes continue to fail simulation.
A venue outside the whitelist is worse than not doing this. Every swap then executes on Uniswap V3 at a worse price than the pAMM gives.
Large trades get weak slippage protection. One pool in one fee tier moves 60 bps on 1000 WETH to USDC (about $1.87M), and 7 bps on $2M USDC to USDT.
min_amount_outalso bounds a sandwich attack, so a low floor widens it — about $11,200 of headroom on that WETH trade.Set a cap before this ships: trade size, or refuse the pAMM leg when the fallback sits more than N bps below it. Open decision.
🤖 Generated with Claude Code