Repository navigation
Refuse partial fills in the B2B forwarder and keeper - #1438
Merged
Merged
Conversation
_swap only checked that the router took no more than amountIn. A pool that runs out of liquidity takes less, and _settle valued that partial fill as a fill of the whole chunk, so a keeper swap could draw a vault subsidy for EURe that never left the clone and leave that EURe outside the deposit's accounting. The Sepolia sandbox pool at 0.3% liquidity showed it: a 5 EURe quote stopped at the swap limit after about 3 EURe.
QuoterV2 prices a partial fill without complaint: a hop that runs out of liquidity ends at the swap-limit price having taken only part of its input. The keeper took that output as a price and would send the swap once the shortfall fit its subsidy tier. Clones deployed before the contract check settle such a fill, so the keeper refuses the quote itself; the route is skipped and the chunk waits as no_route.
✅ Deploy Preview for vortexfi canceled.
|
✅ Deploy Preview for vrtx-dashboard canceled.
|
✅ Deploy Preview for vortex-sandbox ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
Since quoteRouteOutput throws on a partial fill, the depth check dropped a route's healthy minSwapAmount quote whenever its perSwapCap quote ran out of liquidity, and a pool thin at cap on every route logged an error-level abort with no mainnet/sandbox split. A size the route cannot take whole now counts as unbounded impact, so it reaches the existing cap warning or DEPTH BELOW FLOOR verdict.
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.
Why
The live thin-liquidity test on the sandbox turned up a gap between the keeper's quote and the forwarder's settlement. When a Uniswap pool runs out of liquidity along the path, QuoterV2 and SwapRouter02 both stop early: the swap takes only part of
amountIn. QuoterV2 still returns a number, andVortexForwarder._swaponly checked for overspend (consumed > amountIn)._settleand the keeper's projection value the fill against the wholeamountIn, so a partial fill looks like a bad price. When the missing share is within the keeper's current subsidy tier (at most about 1%), the keeper sends the swap, the vault pays the "shortfall" for EURe that never left the clone, and that EURe stays on the clone outside the deposit's accounting (the execution books the full chunk). A larger missing share defers, as it did in the test. The permissionless path pays no subsidy, so its floor check already rejects partial fills.Sepolia evidence (pool at 0.3% of its liquidity, QuoterV2
0xEd1f…2FB3): 1 and 3 EURe filled normally; 5 EURe returned 3.555 USDC with the price after the hop atMAX_SQRT_RATIO - 1, the swap limit, after about 3.2 EURe and about 17.4M gas. The keeper quoted exactly that partial fill (3,655 bps shortfall) and deferred.What changes
VortexForwarder._swap): the route must take exactlyamountIn; anything else revertsInputNotConsumed(replaces theOverspendcheck, which only covered one direction).chain.ts quoteRouteOutput): a quote whose hop ends at the swap-limit price (isPartialFill) throws, so the route counts as unquotable and the chunk waits asno_route. This also protects clones already deployed from the old implementation, such as the sandbox's.perSwapCap-sized swap gives the existing cap warning, and one that cannot fillminSwapAmountgives DEPTH BELOW FLOOR (error on mainnet, warning on the sandbox), withpartial fillin the detail line instead of an impact of thousands of bps. Any other quote failure still skips the route, and the check aborts when no route quotes.Deployment
Already-deployed clones keep their implementation; the keeper guard covers them. The contract check applies to factories deployed from this source onward.
Docs
Architecture doc ("Floor on the net" and "Routes") and security spec invariant 7 describe the exact-input rule and the keeper guard; security spec Monitoring item 2 describes how the depth check classifies a partial fill.
Tests
test_swap_partialFill_revertsInsteadOfSubsidizing: a keeper swap where the router takes 990 of 1,000 EURe with a fill the vault would otherwise top up reverts, and the vault and clone balances are unchanged. Fails on the old check (the swap succeeds and the vault pays).chain.test.ts:isPartialFillflags a hop at either swap limit (including the Sepolia value above) and passes normal fills.amountIn).monitoring.test.ts: aperSwapCap-sized partial fill warns on mainnet and on the sandbox with no error line; aminSwapAmount-sized one logs DEPTH BELOW FLOOR. Both fail against the monitor before this fix.