Skip to content

built Built. This is a decision record, not documentation.

What is still current: Both rules are live: the matured-PT marker on the activity feed, and the redemption-index impairment factor that a matured PT is valued through. The hold-from-merge sequencing and the ledger it waits on are history.

Landed: the maturity marker in v0.53.0; the impairment factor in migration 101 and src/lib/portfolio/v2/pt-factor.ts (v0.59.0)

Header updated 2026-09-14. The body below is frozen history. All plans.

Pendle PT maturity marker and redemption-index impairment plan ​

Settled with Fred 2026-09-02. Two PRs against staging, both HELD from merge until the #715 ledger-truth campaign posts "campaign closed" (its owner has frozen src/lib/portfolio/ for the flip; see §5).

Companion, already shipped or in flight: PR #720 (matured-market history run), the 30-day PT carry runway PR (screener side, feat/pt-carry-30d-runway), issue #703 (coverage line, open), issue #725 (closed as superseded by the 30-day runway).


1. Product decisions (Fred, 2026-09-02) ​

  1. A PT position that has reached maturity must tell the user so. For any Pendle PT a tracked wallet holds at or after the PT's maturity, plain in the wallet OR as collateral inside a loop (Aave v3, SparkLend, Morpho Blue), the position row and the Activity feed carry a "matured" marker. For a loop the marker also states that funding keeps accruing against a flat collateral leg (the position bleeds until the PT is redeemed and the debt repaid).
  2. A write-down of the yield-bearing asset behind a PT must reach the PT's value, on both lines, when it is booked on-chain. Pendle passes such losses to PT holders through its redemption index (§2). Today both the market mark and the accrual (redemption) mark assume the PT pays exactly one unit of its accounting asset at maturity, so the loss only appears on the day the user redeems, as a lump.
  3. Explicitly OUT of scope (verified unnecessary against Pendle's contracts, 2026-09-02):
    • a par cap on the market mark: MarketMathCore._getExchangeRate reverts on any exchange rate below one, so the pool can never price a PT above par and the on-chain oracle we read is capped by construction. The −100% implied figure on the last day (issue #725) is an artifact of Pendle's API series, not of the pool.
    • a "shrinking discount band" near maturity: the pool caps PT at 96% of the pool (MAX_MARKET_PROPORTION) and rateScalar = scalarRoot × 365d / timeToExpiry, so the price can deviate from its glide path by at most ln(0.96/0.04) / rateScalar. For the #703 market (scalarRoot 16.58) that is ≈1.6% at 30 days, ≈0.4% at 7 days, ≈0.05% on the last day. Noise near maturity is already bounded by Pendle's own math.
    • apxUSD / apyUSD book classification: they stay in the Other view (settled).
    • the registry stays holdings-driven; no mirror of matured markets nobody held (settled).

2. The mechanism being modelled (verified on-chain, PendleYieldToken v6 / PendleMarketV7) ​

  • YT._pyIndexCurrent() = max(SY.exchangeRate(), _pyIndexStored); the stored index is written on every interaction and never decreases.
  • Redemption (_calcSyRedeemableFromPY): syToUser = amountPY / indexCurrent (in SY shares). Its value in accounting-asset terms is amountPY × SY.exchangeRate() / indexCurrent, i.e. exactly 1 per PT while the rate is at its peak, less than 1 while it sits below the peak. Post-expiry the same formula holds (postExpiry.firstPYIndex only routes the post-expiry excess to the treasury; the holder's share still uses the current max index).
  • YT holders collected interest as the rate rose and give nothing back on a fall.

Worked example (100 PT-sUSDe, accounting asset USDe; sUSDe rate 1.20 at mint):

pathsUSDe rateindexPT redeems forYT keeps
normal1.20 → 1.261.2679.4 sUSDe = 100 USDe5 USDe interest
write-down1.20 → 1.26 → 1.101.2679.4 sUSDe = 87.3 USDesame 5 USDe
recovery1.10 → 1.261.2679.4 sUSDe = 100 USDeunchanged

The impairment factor at block b: f(b) = min(1, SY.exchangeRate()@b / YT.pyIndexStored()@b), per market. Both are view calls (pendle_markets.sy_address, pendle_markets.yt_address), readable at any archive block, before and after maturity; neither reverts post-expiry (unlike the TWAP oracle). Caveat, stated in code: pyIndexStored lags the true peak if no on-chain interaction captured it, so a write-down can be slightly UNDERstated between the peak and the next interaction; never overstated.

3. Where the factor applies (both ledgers, both lines) ​

The PT→accounting-asset rate is the ONE quantity both lines are built on, so f multiplies it once and both lines move together:

  • Served ledger (v1): src/lib/portfolio/snapshot.ts ptToAssetRateAtBlock (market mark: TWAP read pre-maturity, par at/after) and the accrual/redemption path in src/lib/portfolio/assemble.ts (valueRedemption pull-to-par via pnl.ts ptRedemptionMark), plus src/lib/portfolio/readers/pendle.ts (directly held PT market mark).
  • Rebuilt ledger (v2): src/lib/portfolio/derive/marks.ts step 2 "PT rates, per (market, block)" (par at/after maturity without a read) and the redemption-rate step for PT legs.
  • Semantics: f is read in the same per-(market, block) batch as the PT rate (multicall, archive blockTag). A failed read of either leg leaves the PT mark NULL for that block, never 1, matching the existing "young-TWAP revert → null" rule; post-maturity this introduces a read where there was none, which is accepted (both calls are plain views).
  • f applies identically to a bare PT and to a PT collateral leg (the PT market attaches by asset, venue-agnostic: derive/marks.ts planMark).
  • Dislocation P&L (market − redemption) is unchanged in definition; both inputs carry f.

4. Matured marker ​

  • Read-time, derived from pendle_markets.maturity_ts + an open PT leg (qty > 0) at ts ≥ maturity_ts. No new receipt, no ledger write, no engine change: maturity is a calendar event, not a movement.
  • One shared pure function (registry row + leg → marker | null) used by BOTH the served path and the rebuilt path so the two readers cannot disagree.
  • Surfaces: the position row (plain: "Matured, redeem"; loop: "Matured, funding still accruing, unwind") and the Activity feed (one entry at the maturity timestamp per leg, synthesized at read time; it must not be counted as a flow, a capital move, or a marker on the chart).
  • The loop variant is keyed off the same leg classification the taxonomy already uses (PT pledged as same-book collateral = carry trade, §"Fixed rate assets" vs "Carry trades").

5. Sequencing and ownership ​

  • Both PRs open against staging, reviewed, then held until #715 posts "campaign closed". Reason: the campaign's proof runs on frozen inputs and src/lib/portfolio/ is its area; the factor changes PT marks, which would move figures mid-proof.
  • After the flip: prod backfill/re-mark needs are stated in each PR body under "Prod (after release)"; the impairment factor changes stored marks only if a market's rate ever sat below its index in the covered history, which the PR must measure read-only on staging and report.
  • No prod deploy or data write without Fred's explicit go.

Private documentation. creddit.xyz