← back to lessons

Pattern: Derived tenor (vs independent maturity input) closes the fixed-maturity boundary seam

Banked: Cycle 199 (Rheo / Size Credit, Cantina $50K bounty, commit a8e5d174). WALK-CLEAN target. Relationship: this is the defensive mirror / detection-refinement of fixed-maturity-boundary-and-fungible-credit-risk-transfer.md Pattern 1 (the Midnight t==maturity fill-vs-liquidatable seam). Status: NEW. Not previously in the bank. It explains WHY some fixed-maturity protocols are immune to the boundary seam and gives a fast classification step to avoid wasting effort.

The core distinction: maturity as INPUT vs tenor as DERIVED

The Midnight boundary seam (Blackthorn M-2) is exploitable because maturity is an independent, separately-validated input: - take() validates block.timestamp <= offer.expiry (fill path). - isLiquidatable() validates block.timestamp > obligation.maturity (liquidation path, STRICT). - When expiry == maturity (the intuitive choice), a fill AT t==maturity creates new debt that is solvent at fill-time but overdue/liquidatable one block later. Two inequalities on two independently-supplied values disagree at the boundary.

Size/Rheo immunizes the SAME seam structurally, without a special boundary check, by making tenor a DERIVED quantity funneled through ONE shared rate-curve that reverts at the boundary: - New debt (RESERVED_ID path): minTenor <= tenor <= maxTenor is enforced (SellCreditMarket/BuyCreditMarket validate), so tenor-0 debt cannot be created at all. - Existing position (transfer/sell/buy): tenor = debtPosition.dueDate - block.timestamp is COMPUTED, never user-supplied. - Transferability requires getLoanStatus == ACTIVE, i.e. block.timestamp <= dueDate (OVERDUE needs strict >). So the position is fillable up to and including t==dueDate. - But at t==dueDate, derived tenor == 0, and the shared rate resolution OfferLibrary.getUserDefinedLimitOrderAPR reverts NULL_TENOR on tenor == 0. The collection path (CollectionsManagerView.getLimitOrderAPR -> market.getUserDefined*OfferAPR) flows into the identical revert. - Net: every fill at the exact boundary reverts in the rate math. The "create unconsented debt at t==maturity, liquidate at t==maturity+1" primitive is impossible. There is no second, independent inequality to disagree with the first.

Why this is the right mental model

The seam is NOT a missing require(block.timestamp + epsilon < maturity). It is an architectural property: - INPUT maturity + two separate inequalities (fill vs liquidate) = live seam (test it). - DERIVED tenor + single shared rate function that reverts at tenor==0 (or tenor<minTenor) = closed seam (pivot elsewhere).

A protocol that derives tenor and reuses ONE pricing function for both new and existing fills cannot have the fill-path and liquidation-path inequalities silently diverge, because the fill path self-disables (reverts) exactly where the loan becomes liquidatable.

How to detect / classify on future fixed-maturity targets (fast triage)

  1. First question: is maturity an INPUT (user/maker supplies maturity/expiry/dueDate and the contract validates it), or is tenor DERIVED (dueDate - block.timestamp) and re-validated against min/maxTenor every fill?
  2. If DERIVED-and-funneled-through-one-reverting-rate-function: the boundary-seam class (Pattern 1 of the Midnight lesson) is most likely CLOSED. Confirm the rate function reverts at tenor==0 / tenor<minTenor, then DON'T spend hours testing t==maturity fills - pivot to other surfaces (accounting, liquidation incentive, vault share math, callback binding).
  3. If INPUT-with-separate-validation: the seam is potentially LIVE. Now do the full Midnight playbook - fills at t==maturity, t==maturity-1, t==maturity+1; compare strict vs non-strict inequalities between the fill path and the liquidation path; look for a state solvent-at-fill but liquidatable-next-block.
  4. Corollary check: even with derived tenor, verify the new-debt path enforces minTenor (else a 1-second tenor could still be created and matured almost immediately). Size enforces both minTenor and maxTenor.

Transfers to which protocol classes

Doctrine

Before hunting the maturity-boundary seam, classify the maturity model. A protocol that DERIVES tenor from dueDate - block.timestamp and routes both new and existing fills through a SINGLE rate function that reverts at tenor==0 has structurally closed the seam: the fill path self-disables exactly at the maturity boundary, so it can never disagree with the liquidation inequality. The seam is a property of INDEPENDENT maturity inputs with TWO inequalities, not of fixed-maturity instruments in general. This classification turns a multi-hour empirical boundary hunt into a 10-minute structural decision and reallocates effort to surfaces that are actually live.

Generated 2026-07-02 13:15:05 UTC | auto-sync /15min