feat: Retargetter (LTV retargetting orchestrator + Morpho Blue flash-loan integration)#199
feat: Retargetter (LTV retargetting orchestrator + Morpho Blue flash-loan integration)#199maxencerb wants to merge 8 commits into
Conversation
LTV retargetting orchestrator composing Requests (bridge loans), funds (subscription/redemption) and PositionManager rebalancing, with an ASYNC (Request-funded) and a SYNC (flash-loan window) entry point, a stateless quoter for the sizing and tick-repayment math, a Morpho Blue flash-loan adapter behind a provider-agnostic module interface, and a beacon proxy factory.
|
Warning Review limit reached
Next review available in: 18 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (23)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
- Move the ERC-7201 structs, accessors and storage-only operation helpers (checkActive, checkRequest, setOrder, order, clearOrder, checkNoPendingOrder, clearOperation) into LibStorage, following the existing LibStorage convention - Add LibTransientSlot (bytes32-slot transient load/store helpers) and use it in the Retargetter and the MorphoFlashLoanAdapter - Run the flash-loan payload through Solady's inherited _multicall instead of a hand-rolled delegatecall loop - Keep the SYNC window's position manager and fund in the operation storage (zeroed at window close) instead of transient slots, removing the dual resolution logic; ASYNC-only steps now gate on the Request being present
…dgment - Replace the exact-zero settlement gate with a per-asset tolerance stored as a power-of-two exponent (uint8 each in the config): balances strictly below 2^exponent pass, checked with a single shift; the zero default keeps the gate exact. Stops dust-donation griefing of resolve and of the flash-loan window close - Document the 90-day Request repayment deadline as an acknowledged limitation at REPAYMENT_DEADLINE_OFFSET: treated as effectively infinite, bypasses the local repayment flow if ever hit, remediated offchain through governed beacon upgrades (extensive multisig behind its own timelock), extended by upgrade if it becomes too short
The estimates struct packs into a single storage slot and most of its fields feed the quoter call, so one slot read into memory replaces a storage pointer re-reading the slot for every field. The other storage pointers stay: no other struct is both fully packed and read whole (assets reads are one-per-slot, operation and config copies would load more slots than they save).
Every sizing and pricing formula is now derived step by step above the RetargetterQuoter contract (model, notation, target conditions and their solutions, tick quantization, remediation delta, rounding directions). The interface keeps the behavioral contract only (units, rounding, revert and zero-return conditions) and points at the header, and the test fixtures no longer cite external documents.
The section numbers pointed at an untracked local document; the comments now carry the underlying rationale directly.
…unds Capital entry into an operation is now bounded three ways: the first consume or nonzero mint authorization starts the loan clock and capital only enters within tickThreshold of that origin, the first pull of funds closes the round early (revoking every pending authorization), and mint authorizations count toward the live principal cap until minted or revoked (Solady enumerable set capped at 16 accounts, pruned lazily). Repayment prices every yield token from the origin, so late capital would be overpaid and would dilute recovery on a default; the same gates make the Request's zero mint-to-repaid delay safe, now documented at startRetargetting. Along the way: pullRequestFunds accepts the full-balance sentinel, the whitelist add/remove pairs merged into boolean setters, rescue() is gone (dust lives within the residual tolerance, larger donations fold into the position through the rebalance sentinels), maxPrincipal dispatches through the quoter's new retargetPrincipal, and the shared gates live in LibStorage (checkOffer, checkConsumptionWindow, pruneNullAuthorizations, closeConsumption).
Adds the Retargetter: a small orchestrator that brings a PositionManager back to its target LTV by composing the existing Request (bridge loan), IFund (subscription/redemption) and PositionManager (rebalance) infrastructure, including the Morpho Blue flash-loan integration.
What's included
Retargetter(collateralAsset, debtAsset)pair; SoladyMulticallable; flash-loan window receiverRetargetterQuoterMorphoFlashLoanAdapterIFlashLoanModuleadapter over Morpho Blue's zero-fee flash loansRetargetterFactoryIRetargetter,IRetargetterQuoter,IFlashLoanModule,IRequestFactory,LibStorage(ERC-7201 accessors + operation helpers),LibTransientSlot, constants, errorsDesign
resolve(ASYNC) and window close (SYNC) require the balances of both bound assets to stay within an owner-configured residual tolerance (2^exponentper asset, oneuint8each; the zero default keeps the gate exact). The tolerance stops dust-donation griefing of settlement. There is deliberately norescue: dust lives within the tolerance and anything above folds into the position through the full-balance rebalance sentinels.tickThresholdpast that origin, consume andauthorizeMintingclose for everyone, owner included, and the firstpullRequestFundsshuts the round early (it revokes every pending authorization, so funds being deployed can no longer be diluted). Repayment prices every YT from the origin, so late capital would be overpaid for time it never covered and, on a default, would dilute earlier lenders' recovery. Mint authorizations (authorizeMintingpassthrough) count toward the live principal cap until minted or revoked (Solady enumerable set capped at 16 accounts, minted entries pruned lazily on write paths); revocation skips all gates and stays open after the window closes. The Request's mint-to-repaid delay is 0: the proportional yield gate + live principal cap + minimum-one-tick rule + consumption window already box in last-minute minting, and a nonzero delay would let a late authorized mint pushsetRepaidback and wedge settlement.REPAYMENT_DEADLINE_OFFSET.Two entry points
ASYNC (
startRetargetting) deploys a fresh Request (PT/YT bridge loan) and spans the fund's settlement window; repayment is trustless and tick-priced:Example, LTV-up (below target):
startRetargetting(pm, x, cap, fund, ...)— principalx = (τ·K·(1+Yc) − D·(1+Rb)) / (1 − τ + Yr)capped by the quoterconsume(offer, sig, x)— lender funds the Request; the first consume starts the loan clock and capital entry closestickThresholdlaterpullRequestFunds(max)(ends the funding round) →create(DEPOSIT x)→commit()— subscribe into the fundmulticall[ unlock(), rebalance({collateral: MAX, ops: [SUPPLY MAX, BORROW owed]}), repay(), resolve() ]— supply the shares, borrow exactly what is owed, repay, settle with zero residualExample, LTV-down (above target): borrow via Request →
rebalance({REPAY x, WITHDRAW shares})→ redeem shares →repay()from proceeds → fold any surplus withrebalance({REPAY MAX})→resolve().SYNC (
startSyncRetargetting) runs the whole operation atomically inside a flash loan taken through an owner-whitelistedIFlashLoanModule; the steps are a multicall payload executed inside the provider-agnosticonFlashLoancallback (authorized via a transient window flag); no Request, no persistent state:Works on venues that settle same-transaction (Pareto-style DEPOSIT always; REDEEM in continuous mode); anything else reverts atomically and leaves no trace.
Testing
230 new tests (220 unit/integration/fuzz/smoke + 10 invariants), all green:
x↓ = 2,081,260, not 2.088m; tick promotion boundaries incl. the PR feat: rebalancer Retargetter module (proposal) #195 counterexample)FOUNDRY_PROFILE=full forge test), incl. committed-principal-under-cap at every authorizationorigin + threshold, shut one second later, no owner bypass), mint-authorization lifecycle (cap occupancy, replace-not-add, lazy prune after broker mint, eager removal + no-op mint after revocation, set cleared at resolve)src/fileA five-lens adversarial review (design conformance x2, security x2, integration fit) with two-refuter verification per finding ran over the final code; the confirmed notes (all info/low, no guardrail or funds-safety issues) are addressed as NatSpec operator notes plus
nonReentranton the owner setters.