@@ -70,9 +70,10 @@ struct RebalanceConfig {
7070/// crystallizes (or on bootstrap); on capital flows it is rebased so the pending per-share
7171/// basis is preserved (see `rebaseSnapshot`). It therefore only matches the live NAV right
7272/// after a crystallizing accrual; while the reference is held it deviates from the live
73- /// NAV by the carried (negative) pending basis. `lastCollat` is reconstructed on the fly
74- /// as `lastTotalAssets + lastDebt` rather than stored directly, which keeps the storage
75- /// layout append-only and existing integrations unchanged.
73+ /// NAV by the carried (negative) pending basis; after a seizure loss, flows convert the
74+ /// NAV drawdown against it into that carry (see `rebaseSnapshot`). `lastCollat` is
75+ /// reconstructed on the fly as `lastTotalAssets + lastDebt` rather than stored directly,
76+ /// which keeps the storage layout append-only and existing integrations unchanged.
7677/// @param ltv Loan-to-value ratio in 18-decimal fixed point (e.g., 0.86e18 = 86%).
7778/// A small buffer above the target LTV that determines how much collateral can be withdrawn.
7879/// @param virtualShareOffset Virtual shares offset for inflation attack protection, derived from debt asset decimals.
@@ -182,36 +183,44 @@ library LibStorage {
182183 /// share supply without realising a gain, so the reference must move with them: the carry
183184 /// (the negative pending basis accumulated while the reference is held, typically accrued
184185 /// debt interest under a flat collateral quote) is scaled by the share-supply change and
185- /// re-anchored on the post-flow state. Exits shed their proportional slice of the carry,
186+ /// re-anchored on the post-flow state. When the carry reads zero while NAV sits below
187+ /// the mark (a seizure leaves a NAV deficit with a non-negative levered read), the
188+ /// deficit is converted into carry form, so the high-water mark that the NAV-gain cap
189+ /// in `_pendingFees` measures against survives flows; an ordinary drawdown (nonzero
190+ /// carry) keeps the pre-existing semantics, under which a flow re-anchors the mark at
191+ /// NAV plus the levered carry only. Exits shed their proportional slice of the carry,
186192 /// deposits re-attach it to the new shares, and supply-neutral flows (rebalance, module
187193 /// changes) keep it unchanged. A holder therefore cannot shed accrued debt carry by
188194 /// exiting and re-entering, and an exit does not dump its carry slice on the stayers.
189195 ///
190- /// Called after `_accrueFees` has already crystallized any positive basis, so outside a
191- /// partial bad-debt episode the pending basis here is non-positive; `zeroFloorSub` guards
192- /// the rounding edge at exactly zero. Rounding matches `_pendingFees` (`mulDivUp` on the
193- /// scaled reference debt), so the carry is the exact complement of the fee basis and each
194- /// flow can only shrink it by rounding dust, never create a spurious positive basis.
196+ /// Called after `_accrueFees`, which crystallizes a positive basis except in two held
197+ /// states: a NAV-capped basis (seizure loss, see the cap in `_pendingFees`) and a
198+ /// performance entitlement that converts to zero fee shares. In both, the flow forgives
199+ /// the pending levered gain (for the zero-share hold the forgiven entitlement is
200+ /// bounded by one share's assets); for the seizure the converted deficit keeps the cap
201+ /// suspending fees after the flow. The held management fee accumulator is not cleared
202+ /// by the forgiveness and nets against the next crystallization. Rounding matches
203+ /// `_pendingFees` (`mulDivUp` on the scaled reference debt), so the carry is the exact
204+ /// complement of the fee basis and each flow can only shrink it by rounding dust, never
205+ /// create a spurious positive basis.
195206 ///
196207 /// Partial bad-debt episode: while some (not all) modules are excluded as bad debt, the
197- /// accrual freezes the reference instead of crystallizing (see `_pendingFees`), so a flow
198- /// here can see a positive pending basis against the reduced aggregates. `zeroFloorSub`
199- /// then clamps the carry to zero and the reference re-anchors at the post-flow visible
200- /// state; the phantom gain itself is never preserved. The re-anchor is not
201- /// direction-safe, because the pre-flow reduced aggregates are not comparable with the
202- /// frozen full-universe mark: a flow that leaves the post-flow visible LTV at or below
203- /// the frozen reference LTV under-reads the later recovery (LP-favorable; fees resume at
204- /// a genuine new high), but a flow that raises it above the mark over-reads the recovery
205- /// and can charge it as gain. The over-reading flows are a deposit with fresh borrowing
206- /// during the window, a rebalance that borrows, and, most importantly, a rescue repayment
207- /// (withdraw or rebalance) that pulls the excluded module back above water mid-flow: the
208- /// re-anchor then adopts the re-entered module's near-100% LTV. Operational rule for the
209- /// over-read: `resetPerformanceReference` does NOT correct it (the reset crystallizes a
210- /// positive pending basis before moving); instead the owner should set the performance
211- /// fee rate to zero right after such a flow, let the first accrual with a positive basis
212- /// advance the reference mintlessly (a positive basis advances even at a zero rate), and
213- /// then restore the rate. The under-read needs no action (fees resume at a genuine new
214- /// high, or an owner reset re-anchors sooner).
208+ /// accrual freezes the reference instead of crystallizing (see `_pendingFees`), so a
209+ /// flow here compares the frozen full-universe mark with reduced aggregates and the
210+ /// re-anchor is approximate; the phantom gain itself is never preserved. When the
211+ /// visible carry reads zero (the visible LTV sits below the frozen reference LTV), the
212+ /// seizure conversion measures the deficit against the reduced universe; it typically
213+ /// exceeds the post-flow debt, so the sentinel clamp writes the bootstrap sentinel and
214+ /// the first post-window accrual reseeds the reference at the state it observes without
215+ /// charging (the window recovery is forgiven, LP-favorable). With a positive visible
216+ /// carry the ordinary carry semantics re-anchor instead, which can under- or over-read
217+ /// the later recovery. Remedies for a mis-anchored window flow: an owner reset once the
218+ /// window has closed re-anchors at the live state, but it crystallizes any positive
219+ /// pending basis first, so to avoid charging a mis-read recovery the owner should
220+ /// instead set the performance fee rate to zero right after the flow, let the first
221+ /// accrual with a positive basis advance the reference mintlessly (a positive basis
222+ /// advances even at a zero rate), and then restore the rate. An under-read needs no
223+ /// action (fees resume at a genuine new high, or an owner reset re-anchors sooner).
215224 ///
216225 /// The held management fee accumulator (`heldManagementFeeAssets`, the management fees
217226 /// charged since the reference last advanced, deducted from the next positive basis) is
@@ -229,8 +238,12 @@ library LibStorage {
229238 /// Edge cases collapse to a plain snapshot of the current state (zero carry): bootstrap
230239 /// (`lastDebt == 0` sentinel), an empty vault before the flow, and a carry exceeding the
231240 /// post-flow debt (the reference debt floors at zero, which is the bootstrap sentinel, so
232- /// the excess is forgiven and the next accrual reseeds). An empty good-debt universe
233- /// after the flow instead holds the reference (see the bad-debt episode below).
241+ /// the excess is forgiven and the next accrual reseeds). The last case includes an
242+ /// oversized converted seizure deficit, e.g. a flow that unwinds debt below the seizure
243+ /// loss: the reseed then lands at the state the next accrual observes, and the recovery
244+ /// from there is charged unless the owner applies the temporary zero-rate remedy above.
245+ /// An empty good-debt universe after the flow instead holds the reference (see the
246+ /// bad-debt episode below).
234247 ///
235248 /// Bad-debt episode: whenever the flow leaves the good-debt universe empty
236249 /// (`newCollat == 0` with a live reference), there is no good-debt state to re-anchor on,
@@ -273,6 +286,16 @@ library LibStorage {
273286 uint256 refCollat = self.lastTotalAssets + refDebt;
274287 uint256 scaledRefDebt = refDebt.mulDivUp (prevCollat, refCollat);
275288 uint256 prevCarry = FixedPointMathLib.zeroFloorSub (prevDebt, scaledRefDebt);
289+ // Seizure state: a liquidation leaves NAV below the mark while the levered read is
290+ // non-negative, so `prevCarry` reads zero and the re-anchor below would drop the mark to
291+ // the post-loss trough. Carry the NAV deficit (mark minus pre-flow NAV, zero when NAV is
292+ // at or above the mark) instead: the mark then lands at `NAV + deficit`, and later flows
293+ // read the same deficit back through the standard carry path. The empty pre-flow
294+ // universe is excluded (full-episode re-anchor, see the header); the oversized-deficit
295+ // edge is covered by the sentinel clamp below (see the header's edge cases).
296+ if (prevCarry == 0 && prevCollat > 0 ) {
297+ prevCarry = FixedPointMathLib.zeroFloorSub (self.lastTotalAssets + prevDebt, prevCollat);
298+ }
276299 // Preserve the per-share carry across the supply change.
277300 carry = prevCarry.mulDiv (newSupply, prevSupply);
278301 if (carry > newDebt) carry = newDebt;
0 commit comments