Partial Rebalancing#72
Merged
Merged
Conversation
Adds a swap helper that honors a slippage floor but, instead of reverting when the full size would breach it, halves the input (and the floor, which scales linearly with it) and retries until a size clears or the input is exhausted. Returns the input swapped and output received, or (0,0) if no size cleared. This is the primitive partial rebalancing builds on: a smaller swap faces both a proportionally-smaller oracle-derived floor and less price impact, so it can clear a bound the full size could not. Uniform per-unit costs (flat fee / spot divergence) still yield (0,0). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both rebalance legs now swap best-effort under the slippage floor instead of reverting when the swap that would reach the re-entry target is too large to clear it. The lever leg borrows the full target slice, swaps the largest feasible fraction, and repays the unswapped remainder so no idle loan token lingers; the delever leg sells the largest feasible fraction of yield and repays the realized loan token. The position lands partway to the target and converges over successive rebalances as price impact shrinks. When the slippage is a uniform per-unit cost no smaller trade can escape (a flat fee or oracle/pool spot divergence), the rebalance is now a swap-free no-op rather than a revert; the unchanged health factor is surfaced via the emitted Rebalanced/VaultState events. Updates the two slippage tests that asserted the old revert behavior to assert the new uniform-slippage no-op. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds MockCpmmSwapRouter, a constant-product swap mock whose average price degrades with trade size, so a swap that breaches the slippage floor at full size clears it at a reduced size — the property partial rebalancing needs and that the flat-rate MockSwapRouter cannot express. New tests assert the partial path on a shallow pool: a partial delever and a partial lever each make progress toward (but fall short of) the re-entry target with no idle loan token, and successive delevers converge the position back into the band. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Architecture: note that rebalance partial-fills the largest feasible fraction under the per-trade slippage floor instead of reverting, leaving the per-trade extraction cap unchanged. Rebalancer: replace the slippage transient-failure row with a partial-fill/converge row, since a slippage-bounded rebalance no longer fails the EVM call. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaces the try/catch halving search with the canonical Uniswap V3 mechanism: each rebalance swap carries a sqrtPriceLimitX96 derived from the yield oracle discounted by maxSlippageBps, so the pool natively fills only up to that marginal-price bound and partial-fills (instead of reverting) when the swap to the re-entry target is too large. A single bounded swap replaces up to ~10 reverting probes — gas-cheaper, precise, and the documented-standard approach. Mechanics: - SwapLib.swapExactInToLimit: amountOutMinimum=0 + a price limit; the pool stops at the bound and leaves unspent input with the caller. - FCMVault._oracleSqrtPriceLimitX96 converts the oracle price (decimals already baked into the 1e36-scaled yield oracle) into the pool's sqrt(token1/token0)*2**96 space and applies the slippage factor in sqrt space; _yieldDebtSwapLimit reads the pool's live slot0 and skips the swap (no-op) when spot is already past the bound or the limit is out of TickMath range, avoiding an SPL revert. - Lever borrows the full slice, swaps bounded by the limit, and repays the unspent loan; delever sells bounded by the limit and repays the realized loan. New yieldDebtPool immutable (plumbed via InitParams + DeployVault from the existing config) supplies slot0. Semantics change: the bound is now on marginal price (price impact) relative to the oracle; the pool's fixed LP fee is a separate known cost and is not part of the bound (matching Uniswap's own model). Tests: MockUniswapV3Pool (settable slot0) + MockCpmmSwapRouter reworked to honor sqrtPriceLimitX96 (partial fill, consumes only used input). Fee-based slippage tests replaced with price-based skip/loosen tests; partial-progress and convergence tests retained; a harness test asserts the oracle-> sqrtPriceLimit conversion directly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Update the sandwich-attack and rebalancer-failure sections to describe the marginal-price bound: the pool partial-fills up to a sqrtPriceLimitX96 derived from the oracle, skips when spot is already past the bound, and the LP fee is a separate cost not included in the bound. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Drop test_Rebalance_PartialDeleverConvergesOverTicks and repair three missing-semicolon syntax errors (FCMVault swap-limit side checks, MockCpmmSwapRouter require) that were blocking the build. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add _priceX192(num, den) and _sqrtPriceX96(num, den) so price expressions read as plain fractions (e.g. _sqrtPriceX96(99, 100) for the 1% floor) instead of inline Math.sqrt(Math.mulDiv(...)) with bit-shift constants. Route _setPoolPrice and all raw sites through them; values unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
jribbink
approved these changes
Jul 6, 2026
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.
This PR modifies rebalancing to support performing as much of a rebalancing swap as is possible without breaching price impact limits,