M-1: Direct pool swaps strand unused input on partial fills
Summary:
Users lose funds when swaps partially fill, as the router pre-pulls the full input but only the consumed portion is used. The remainder is stranded — either in the router (recoverable by admin only) or in an external router (unrecoverable entirely).
Description:
The router's _pullInput() unconditionally transfers the full amountIn from the caller into the router. Multiple swap paths are then vulnerable to partial fills where the pool consumes less than the full input:
1. Direct pool swaps (UniswapV3Pool / AlgebraPool). _swapAlgebraPool() and _swapUniswapV3Pool() call pool.swap() with amountSpecified = int256(amountIn). If liquidity is insufficient, the pool partially fills. The callbacks (uniswapV3SwapCallback(), algebraSwapCallback()) pay only the actual positive delta, not the full amount. The difference remains stranded in the router, recoverable only via admin sweep() to the fee collector.
2. UniswapV4. _swapUniswapV4() builds a SWAP_EXACT_IN_SINGLE + SETTLE_ALL + TAKE_ALL action sequence. SETTLE_ALL settles based on the actual swap delta, not the full amountIn. On partial fills, Permit2 pulls only the consumed amount from the router. The unconsumed tokens remain stranded in the router after the Permit2 approval is revoked at L622-624.
3. VelodromeUniversalRouter (CL route). _swapVelodromeUniversalRouter() pushes the full amountIn to the Velodrome router via safeTransfer before executing the swap. When using the CL path (command = 0x00), the underlying Slipstream pool can partially fill like any V3-style pool. The unconsumed tokens remain stranded on the Velodrome router itself, not on the MultiSwapRouter. The MSR admin has no ability to recover these tokens — they are permanently lost to the user unless the Velodrome router has its own sweep mechanism controlled by a separate admin.
Impact:
Medium. Users permanently lose unconsumed input on partial fills. For direct pool and V4 swaps, stranded tokens remain in the router and are swept to the fee collector. For VelodromeUniversalRouter CL swaps, stranded tokens are sent to the external router and are entirely unrecoverable by the MSR admin.
Recommendation:
For direct pool swaps and V4, revert when the consumed amount differs from amountIn to prevent partial fills entirely or compute the delta and transfer back any unused tokens.
For VelodromeUniversalRouter, switch from the token push pattern to approve-call-revoke with payerIsUser = true, consistent with other router-based swaps. This keeps unconsumed tokens in the MSR where they can at least be swept.