Skip to content

feat: Retargetter (LTV retargetting orchestrator + Morpho Blue flash-loan integration)#199

Open
maxencerb wants to merge 8 commits into
mainfrom
feat/simple-retargetter
Open

feat: Retargetter (LTV retargetting orchestrator + Morpho Blue flash-loan integration)#199
maxencerb wants to merge 8 commits into
mainfrom
feat/simple-retargetter

Conversation

@maxencerb

@maxencerb maxencerb commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

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

Contract Purpose
Retargetter Beacon-proxied orchestrator, bound to one (collateralAsset, debtAsset) pair; Solady Multicallable; flash-loan window receiver
RetargetterQuoter Stateless pure math (direction dispatch, LTV-up/LTV-down sizing, remediation delta, tick repayment model; step-by-step derivations in the contract header)
MorphoFlashLoanAdapter Stateless IFlashLoanModule adapter over Morpho Blue's zero-fee flash loans
RetargetterFactory Beacon proxy factory (wiring checklist in NatSpec)
Interfaces + libs IRetargetter, IRetargetterQuoter, IFlashLoanModule, IRequestFactory, LibStorage (ERC-7201 accessors + operation helpers), LibTransientSlot, constants, errors

Design

  • Derive, do not store: direction, consumed principal (Request PT supply), order progress and repaid status are recomputed fresh at every use. Only the operation addresses, the loan clock origin, the per-operation yield cap and the non-derivable order fields persist (four ERC-7201 namespaces split by lifecycle).
  • Trust model: fully trusted owner; semi-trusted rebalancer boxed in by guardrails G0–G6 (one operation at a time, token matching, direction checks, settlement gates, fund/module whitelists, principal cap, yield cap); untrusted everyone else.
  • No value at rest, no escape hatch: resolve (ASYNC) and window close (SYNC) require the balances of both bound assets to stay within an owner-configured residual tolerance (2^exponent per asset, one uint8 each; the zero default keeps the gate exact). The tolerance stops dust-donation griefing of settlement. There is deliberately no rescue: dust lives within the tolerance and anything above folds into the position through the full-balance rebalance sentinels.
  • Bounded capital entry: the first consume or nonzero mint authorization starts the loan clock; from tickThreshold past that origin, consume and authorizeMinting close for everyone, owner included, and the first pullRequestFunds shuts 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 (authorizeMinting passthrough) 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 push setRepaid back and wedge settlement.
  • Acknowledged limitation: the 90-day Request repayment deadline is treated as effectively infinite; if ever hit it bypasses the local repayment flow, and remediation runs through offchain arrangements delivered via governed beacon upgrades (extensive multisig + dedicated timelock). Documented at 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:

owed(t) = ptSupply + ceil(ytSupply * min(paidTicks(t) * tick, horizon) / horizon)
paidTicks(t) = max(1, floor((t + tick - threshold) / tick))

Example, LTV-up (below target):

  1. startRetargetting(pm, x, cap, fund, ...) — principal x = (τ·K·(1+Yc) − D·(1+Rb)) / (1 − τ + Yr) capped by the quoter
  2. consume(offer, sig, x) — lender funds the Request; the first consume starts the loan clock and capital entry closes tickThreshold later
  3. pullRequestFunds(max) (ends the funding round) → create(DEPOSIT x)commit() — subscribe into the fund
  4. after settlement: multicall[ unlock(), rebalance({collateral: MAX, ops: [SUPPLY MAX, BORROW owed]}), repay(), resolve() ] — supply the shares, borrow exactly what is owed, repay, settle with zero residual

Example, LTV-down (above target): borrow via Request → rebalance({REPAY x, WITHDRAW shares}) → redeem shares → repay() from proceeds → fold any surplus with rebalance({REPAY MAX})resolve().

SYNC (startSyncRetargetting) runs the whole operation atomically inside a flash loan taken through an owner-whitelisted IFlashLoanModule; the steps are a multicall payload executed inside the provider-agnostic onFlashLoan callback (authorized via a transient window flag); no Request, no persistent state:

Retargetter → adapter.flashLoan → Morpho.flashLoan → adapter.onMorphoFlashLoan
            → retargetter.onFlashLoan(payload) → [create, commit, unlock, rebalance] → repay via allowance

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:

  • Quoter unit + fuzz fixtures (incl. a guard against a mis-derived LTV-down denominator: x↓ = 2,081,260, not 2.088m; tick promotion boundaries incl. the PR feat: rebalancer Retargetter module (proposal) #195 counterexample)
  • Guardrail matrix G0–G6 (revert + owner-bypass path per guardrail), direction-rule truth table, sentinel resolution, ERC-7201 storage layout + packing assertions, end-to-end reentrancy-guard test (a role-holding fund reentering an order wrapper)
  • ASYNC + SYNC end-to-end integration (UP and DOWN, loan clock, multicall tails, drift revert, callback replay/smuggling, donation griefing + sentinel fold, deadline expiry default, fund force-end)
  • Adapter unit tests against a real local Morpho deployment
  • Handler-based invariant suite: 10 invariants x 256 runs x 64 depth (FOUNDRY_PROFILE=full forge test), incl. committed-principal-under-cap at every authorization
  • Consumption window boundaries (open at origin + 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)
  • 100% coverage (lines, statements, branches, functions) on every new src/ file

A 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 nonReentrant on the owner setters.

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.
@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@maxencerb, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 18 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a3ca2459-a552-4bb2-8ca8-8be32251aecb

📥 Commits

Reviewing files that changed from the base of the PR and between 96a18e3 and 6bf996e.

📒 Files selected for processing (23)
  • src/interfaces/manager/rebalancer/IFlashLoanModule.sol
  • src/interfaces/manager/rebalancer/IRetargetter.sol
  • src/interfaces/manager/rebalancer/IRetargetterQuoter.sol
  • src/interfaces/request/IRequestFactory.sol
  • src/libs/common/LibTransientSlot.sol
  • src/libs/manager/rebalancer/LibRetargetterConstants.sol
  • src/libs/manager/rebalancer/LibRetargetterErrors.sol
  • src/libs/manager/rebalancer/LibStorage.sol
  • src/manager/rebalancer/MorphoFlashLoanAdapter.sol
  • src/manager/rebalancer/Retargetter.sol
  • src/manager/rebalancer/RetargetterFactory.sol
  • src/manager/rebalancer/RetargetterQuoter.sol
  • test/manager/rebalancer/MorphoFlashLoanAdapter.t.sol
  • test/manager/rebalancer/Retargetter.invariant.t.sol
  • test/manager/rebalancer/Retargetter.t.sol
  • test/manager/rebalancer/RetargetterAsync.t.sol
  • test/manager/rebalancer/RetargetterBase.t.sol
  • test/manager/rebalancer/RetargetterQuoter.t.sol
  • test/manager/rebalancer/RetargetterRebalance.t.sol
  • test/manager/rebalancer/RetargetterSmoke.t.sol
  • test/manager/rebalancer/RetargetterSync.t.sol
  • test/mock/manager/rebalancer/MockRetargetterFund.sol
  • test/mock/manager/rebalancer/RetargetterHandler.sol
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/simple-retargetter

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

maxencerb added 7 commits July 2, 2026 20:22
- 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).
@maxencerb maxencerb marked this pull request as ready for review July 6, 2026 16:30
@maxencerb maxencerb self-assigned this Jul 6, 2026
@maxencerb maxencerb requested a review from maximebrugel July 6, 2026 16:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant