Status: implemented
Implemented — this change has shipped.
RFC 0036: IRR-hurdled waterfall tiers — a closed-form boundary, not a nested solve
Takes up the first item RFC 0035 reserved:
until_lp_irron asplittier, refused byWF-01since 2.3.0 "until a future RFC specifies the boundary solve." This RFC specifies it — and the specification is not the one 0035 §C sketched. The sketch was a bisection on the boundary amount withxirras the objective, which nests a 200-iteration root-solve inside another root-solve and inheritsxirr's refusal cases mid-walk. The boundary has a closed form under the hurdle-rate balance identity the industry already uses, and that is what is proposed. Protocol 2.5.0 → 2.6.0 (a new capacity rule in §VIII.10 step 3; drafted against 2.3.0, the number moved under it while 0037/0038 shipped); the format version does not move.Implemented 2026-09-09 under owner-led governance (accept + implement in one change). The corpus scan the Compatibility section calls for found zero non-increasing
until_lp_emladders, so the monotone rule ships asWF-01for both hurdle kinds; noWF-04.
Summary
Allow a split tier to be capped by an LP IRR hurdle (until_lp_irr) alongside — or instead of — the equity-multiple hurdle RFC 0035 shipped. The tier pays until the LP's dated flows, including the payment being made, reach the hurdle rate; the next tier takes over. The capacity is closed-form: the LP's hurdle balance at the row date is the future value, compounded at the hurdle rate under the series' own day count, of every LP flow so far — the amount the LP must receive now for its flows to have a net present value of zero at the hurdle rate. No iteration is introduced anywhere; xirr is still computed only for the reported outcome, by the unchanged §VIII.9.3 procedure. Laddered promotes (12% → 15% → 18%) and combined hurdles (1.5x *and* 12%) follow directly. verifyWaterfall stays a verifier; the Tier-3 calc engine is untouched.
Motivation
- This is the promote structure the market actually quotes. RFC 0035 shipped equity-multiple hurdles because they are closed-form and deferred IRR hurdles as "the classic scope trap." But an LPA that reads "80/20 until the LP achieves a 12% IRR, 70/30 until 15%, 60/40 thereafter" is the common case, not the exotic one; an equity-multiple-only ladder cannot state it, so a sponsor-side underwriting still has no home for its headline promote and every host recomputes it privately — the exact gap 0035 set out to close.
- The reserved syntax is a refusal with a known design. 0035 §C and format §4.27 both name the solve and refuse the field. A reservation is meant to be short-lived; every 2.3.0–2.5.0 file that wants an IRR ladder is currently forced to either misstate its terms as a multiple or leave the waterfall out.
- The sketched design would have been wrong to build. Bisecting on the boundary amount
xwithg(x) = xirr(F ∪ {(t, lp_share·x)})as the objective (a) nests the §VIII.9.3 bisection inside another bisection — up to 200 × 200xnpvevaluations per tier per row — and (b) makes tier capacity undefined wheneverxirrrefuses (no sign change over the bracket, which is exactly the state of the LP's flows before its first distribution). The closed form has neither problem:xnpvis a finite sum with no refusal case (§VIII.9.2). - Two engines must agree on the flip. "IRR hurdle" has as many dialects as day counts: some models test the hurdle at period end, some compound the balance monthly, some solve the IRR and compare. Writing the identity down normatively is what makes the promote comparable across implementations — the RFC 0034/0035 division of labor, unchanged.
Proposed change
A. Format: §4.27 split tier gains until_lp_irr
In the tiers[].split description, replace the reserved-and-refused sentence with:
The optional
until_lp_irr(a fraction in (0, 1)) caps the tier where the LP's dated flows — every contribution and distribution to date, including this tier's payment at this row's date — reach that internal rate of return under the series' day count (Protocol §VIII.10, hurdle balance).until_lp_emanduntil_lp_irrMAY both be stated on one tier; the tier then ends only when both hurdles are met (the larger capacity governs). A capped split MUST havelp_share > 0whichever hurdle caps it. Across the ladder, successiveuntil_lp_irrvalues MUST strictly increase, as MUST successiveuntil_lp_emvalues (a later tier hurdled at or below an earlier one has capacity zero by construction and would pay nothing, silently).
The WF-01 rule list changes accordingly: remove "the reserved until_lp_irr refused"; add "until_lp_irr in (0, 1)" and "successive hurdles of one kind strictly increasing." The "Deliberately deferred" paragraph drops IRR-hurdled tiers.
Schema: until_lp_irr: { "type": ["number", "null"], "exclusiveMinimum": 0, "exclusiveMaximum": 1 } on the split variant; the reservation note is removed. No new section, no format version movement (additive field on an existing optional section — the RFC 0034 erratum precedent).
B. Protocol: §VIII.10 step 3, the capped-split capacity (normative)
Protocol 2.5.0 → 2.6.0. Replace the split bullet with:
split— paidlp_share/gp_share. A capped tier's capacity is the larger of the capacities its stated hurdles impose (both must be met before the tier ends); the final tier is unbounded.
until_lp_em:max(0, until_lp_em × lp.contributions − lp.distributions) / lp_share(unchanged from 2.3.0).until_lp_irr(=h): letFbe the LP's dated flows so far — every contribution outflow and distribution inflow appended by earlier rows and by earlier tiers of this row, at their anchor-relativet(§VIII.9.2) — andt_rowthis row'st. The LP's hurdle balance isB = −xnpv(F, h) × (1 + h) ^ t_row(§VIII.9.2's sum, accumulated in flow order; the sametvalues the outcomexirrwill use). Capacity ismax(0, B) / lp_share.B ≤ 0means the LP has already achieved the hurdle and the tier pays nothing.The hurdle test is the balance identity, not a comparison against a solved
xirr:xnpv(F ∪ {(t_row, B)}, h) = 0exactly, so a tier that fills lands the LP at the hurdle rate by construction, without iteration. The reported outcomexirris still the §VIII.9.3 procedure over the final flows; at a filled boundary the two agree to within that procedure's stopping tolerance, which the §VIII.9.4%quantum (6 dp) absorbs.
And one sentence at the end of step 3: "An implementation MUST NOT determine a tier boundary by iterating on xirr; §VIII.9.3 remains the only permitted iteration and it runs only in step 4."
Why the identity is the definition and not merely an implementation of "LP IRR ≥ h". For conventional LP flows (contributions, then distributions) the two coincide. For flows with a capital call after a distribution, xnpv(·, r) need not be monotone in r, "IRR ≥ h" can be ambiguous (multiple roots) or undefined (no root), and two engines solving for the IRR could legitimately disagree on whether the hurdle was met. The balance identity is single-valued for every finite flow list and is what LPA models mean operationally ("the LP's capital account compounds at the hurdle rate until it is paid down"). Making it the normative definition is what makes the flip deterministic; verify-irr-hurdle-interleaved-call pins exactly the case where the two readings could differ.
C. Validator: WF-01 grammar
checkWaterfall gains, on split tiers:
until_lp_irrpresent → MUST be a number in (0, 1); elseWF-01attiers[i].until_lp_irr(the reservation refusal is deleted).- A split capped by either hurdle MUST have
lp_share > 0(the existing rule, now covering both fields). - The final tier MUST be uncapped by either field (the existing rule, now covering both).
- Monotone ladders: across
splittiers in order, each stateduntil_lp_irrMUST exceed every earlier stateduntil_lp_irr, and each stateduntil_lp_emMUST exceed every earlier stateduntil_lp_em; elseWF-01at the offending tier. Compared at the rate / ratio quantum respectively.
Remediation text for each lands in BUILTIN_REMEDIATIONS under the existing WF family; no new code family, no new capability — the change slots under validate (structure) and the existing verifier surface (arithmetic) per RFC 0030.
D. What this RFC does not change
- No calc-engine surface: no grammar tokens, no builtins, no
CalcResultshape.computeWaterfallgains onecasebranch; it callsxnpvOf, which already exists and is closed-form. - No new party model, no clawback, no crystallization (still deferred, §Unresolved). No
capital_stackchange;CS-WATERFALL-UNSUPPORTEDstays. - No Excel emit (the §4.26 literals posture).
stated_outcomes/stated_scheduleshapes are unchanged — an IRR-hurdled ladder is verified by the same two comparisons.
Compatibility analysis
- Existing files. Every 2.5.0-valid file stays valid:
until_lp_irrwas refused, so no file carries it;until_lp_emsemantics are byte-for-byte the 2.3.0 rule. One tightening: the monotone-ladder rule newly refuses a non-increasinguntil_lp_emsequence. Such a ladder has a dead tier and was never meaningful, but it was legal. Implementation MUST scan the corpus and examples for one before accepting the tightening; if any exists, the rule lands foruntil_lp_irronly and theuntil_lp_emhalf becomes a warning (WF-04, new) — the CC-15/CC-16 posture. Expected result: zero hits (the only EM-hurdled document in the repo is the single-tierverify-em-hurdle-boundaryfixture). Scan result (2026-09-09): zero hits.until_lp_emappears inconformance/waterfall/ verify-em-hurdle-boundary/case.jsonandreject-capped-final-split/deal.uwx.md(one capped tier each), invalidator.waterfall.test.ts/waterfall.test.ts(one capped tier each), and nowhere underexamples/orspec/; no document carries two EM-hurdled tiers. The tightening ships asWF-01for both kinds. - Tier-1 readers: additive under
validate. Tier-2: ordinary edit policies. Tier-3: untouched by construction. Tier-4: the §4.27 prohibition already covers hurdle levels ("an agent MUST NOT invent tier terms, splits, or hurdle levels"); no new text. - Modules: no manifest change.
- Protocol skew: a 2.3.0–2.5.0 host reading a 2.6.0 file with
until_lp_irrreportsWF-01(its reservation refusal) — a visible, correct refusal rather than a silent mis-allocation. That is the §XII.4 posture and needs no shim. - Receipts: unaffected — the waterfall verdict is not a receipt output.
Conformance impact
Existing fixtures that change:
conformance/waterfall/reject-reserved-irr-hurdle/— the document becomes valid. Rename toverify-irr-hurdle-*or delete; the reservation is gone and a fixture asserting it would assert a stale refusal. Proposed: delete, since the newverify-cases cover the field andreject-irr-hurdle-out-of-rangecovers the grammar.validator.waterfall.test.ts— the "rejects the reserved until_lp_irr" case flips to the range check.
New scenarios in conformance/waterfall/ (every pinned number generated by the verifier, never hand-computed — the RFC 0034 rule; the single-shot cases are chosen so the hand check is trivial anyway):
| Scenario | Pins |
|---|---|
verify-irr-hurdle-boundary | −1,000,000 at t0, +1,150,000 at t1, equity_split 100/0, ROC → 80/20 until_lp_irr 0.12 → 60/40. The hurdled tier pays LP exactly 120,000 and GP 30,000, then nothing remains; LP xirr verifies at 0.12 (6 dp) — the boundary lands on the hurdle. |
verify-irr-hurdle-crossing | Same ladder, +2,500,000 at t1: tier 2 fills at 150,000 and hands 1,350,000 to 60/40 — LP 1,930,000, GP 570,000, stated schedule cell-for-cell. |
verify-irr-ladder | ROC → 80/20 to 10% → 70/30 to 15% → 60/40 over a five-row series; each hurdled tier's fill pinned in the schedule; LP xirr above 15%. |
verify-irr-hurdle-already-met | A series whose ROC-plus-earlier distributions already exceed the hurdle: the hurdled tier's capacity is ≤ 0, it appears in no schedule row, and the next tier absorbs the cash. |
verify-irr-hurdle-interleaved-call | A capital call after a distribution (−1,000,000, +300,000, −200,000, +1,500,000 on 2026-01-01, 2027-01-01, 2027-07-02, 2029-01-01 — t = 0, 1, 547/365, 1096/365 under act/365f), ROC → 80/20 to 12% → 60/40. Pins the balance-identity payment: the hurdled tier pays the LP 366,097.17 (F credits both ROC receipts and the mid-hold call) — the case where "solve the IRR and compare" and the identity could diverge, resolved by definition. (As implemented; the draft's 1,265,667.32 was a hand figure for a ladder without a ROC tier at exact t = 1.5 / 3, and hand figures are not pinned.) |
verify-combined-hurdles-irr-binds / -em-binds | One tier stating both until_lp_em: 1.5 and until_lp_irr: 0.12. Over a five-year hold the IRR balance (762,888.96) exceeds the 1.5x headroom (500,000) and binds; over a one-year hold the multiple binds (LP 500,000 / GP 125,000). |
verify-compound-pref-then-irr | 8% compound_annual pref → catch-up → 80/20 until 12% IRR → 60/40: the pref receipts are LP inflows in F, so the hurdle balance already credits them; promote pinned. |
reject-irr-hurdle-out-of-range | until_lp_irr: 1.2 and until_lp_irr: 0 → WF-01. |
reject-irr-hurdle-non-increasing | 15% then 12% → WF-01 at the second tier. |
reject-irr-hurdle-lp-share-zero | lp_share: 0 with until_lp_irr → WF-01. |
reject-irr-hurdle-on-final-split | The terminal tier capped by IRR → WF-01. |
Twelve directories (the combined-hurdle twin is its own scenario); one deleted: corpus 385 → 396. The verify-irr-hurdle-boundary case also serves as the regression pin for the identity itself: if an implementation ever "improves" the boundary by solving xirr, the LP xirr still reads 0.12 but the schedule cell moves by the solver's tolerance and the pinned cent disagrees.
Reference implementation
- Files:
waterfall.ts(thesplitcapacity branch;until_lp_irronWaterfallTierSplit; importxnpvOf),validator.ts(checkWaterfallrules in §C +BUILTIN_REMEDIATIONSrows),spec/schemas/section-distribution-waterfall.schema.json,spec/UW_FORMAT_SPEC_v1.md§4.27,spec/UW_PROTOCOL_v1.md§VIII.10 + version line + §XVI,protocol.ts(PROTOCOL_VERSION2.6.0),conformance/waterfall/per the table,CHANGELOG.md, the waterfall rows indocs/wiki/13-status.md,ROADMAP.md, and the RFC index. - API surface: no new exports.
WaterfallTierSplit.until_lp_irr?: number | nullis the only type change (additive). - Test plan: unit tests in
waterfall.test.tspinning the hurdle balance against a hand-worked single-shot case (B = 120,000ath = 0.12,t = 1), the "already met" zero-capacity path, the combined-hurdlemax, and the boundary-vs-xirragreement to 6 dp (asserted withtoBeon the quantized value, nottoBeCloseTo);validator.waterfall.test.tsfor each newWF-01branch; the cash-conservation property test extends unchanged to IRR-hurdled ladders (Σ tier payments = row amount); the conformance suite above. - Effort: small — one capacity branch and four grammar checks. The spec text is most of the work, as it should be.
Alternatives considered
- Bisection on the boundary amount (the 0035 §C sketch). Rejected, §Motivation: nested iteration, capacity undefined where
xirrrefuses, and 40,000xnpvevaluations per tier per row in the worst case. It also would have made the boundary depend on the §VIII.9.3 stopping tolerance — a schedule cell that moves at the twelfth decimal between implementations that both conform. - Test the hurdle on the solved IRR after each row ("did the LP reach 12% yet?"). Rejected: it is a period-end test, so the flip happens at the next row, over-paying the hurdled tier by up to a whole distribution. Wrong by the LPA's own reading and implementation-dependent in the interleaved-call case.
- Express the IRR hurdle as a
compound_annualpref tier. The identity shows they are cousins — a hurdle balance is a compound pref balance on all LP flows — but a pref tier accrues onunreturnedcapital only and pays pro-rata pari passu with the GP, while a hurdle is an LP-only cap on a split. Forcing one into the other misstates the ladder. - A separate
hurdleobject ({ kind: "irr" | "em", value }) instead of two fields. Cleaner in the abstract, but it would change the shippeduntil_lp_emshape, and the two-field form reads the way an LPA does ("until 1.5x and 12%"). Not worth a migration. - Allow non-monotone ladders and let dead tiers be. Rejected: a tier that can never pay is a misstatement of the partnership terms, and the format's posture is to refuse a structure that cannot mean what it says (the
gp_share ≤ target_promoteprecedent).
Unresolved questions
- "Any" semantics for combined hurdles. This RFC ships "both must be met" (the larger capacity). An LPA reading "until 1.5x or 12%, whichever first" would need a
hurdle_mode: "any"(the smaller capacity). Deferred until a document needs it; the field name is proposed here so a later RFC does not invent another. - GP-side hurdles (
until_gp_irr). No LPA the author has seen hurdles on the GP's return; left out rather than reserved. - Clawback / crystallization — still deferred from 0035; unchanged by this RFC, though an IRR ladder makes the clawback question more pressing (a promote paid on an interim hurdle that later un-earns itself). A terminal true-up tier remains the likely shape.
TheResolved: the scan found zero non-increasing EM ladders (see §Compatibility), so it ships asuntil_lp_emmonotonicity tightening — error or warning, decided by the corpus scan in §Compatibility.WF-01(error) for both hurdle kinds; noWF-04.
Prior art
The "hurdle balance" / "IRR hurdle account" method in standard LPA waterfall models (the LP's capital account compounding at the hurdle rate until paid down) — the operational meaning every model agrees on even when their solved-IRR tests disagree. Internally: RFC 0035 (the ladder and walk this extends; §C's reservation and sketch), RFC 0034 (xnpv, anchor-relative t, and the day-count registry the balance compounds under), RFC 0024 (why a boundary must never depend on an iterative solver's tolerance).