← back to lessons

Lesson: PT-as-Collateral Seam Hunting (Pendle PT in lending markets)

Banked: 2026-06-11 (SEAMS lane, Morpho PT-apxUSD/USDC instance) Status: methodology + negative-result calibration. WALK-CLEAN outcome.

TL;DR

When a lending market lists a Pendle PT as collateral, the trust gap is NOT one boundary, it is a 3-layer chain: lending-market-oracle -> Pendle PT feed -> SY.exchangeRate -> underlying. Most live, well-funded markets (Morpho top-TVL) wire the RECOMMENDED Pendle PendleChainlinkOracle (PT_TO_ASSET, 1800s TWAP) AND add an independent capped-ratio feed for the underlying. That stack defends a, b, c, d, e simultaneously. Expect WALK-CLEAN on the big markets; the EV lives in the long-tail / non-stable-SY markets and in the upward-exchangeRate branch.

How to map the seam fast (repeatable recipe)

  1. Morpho GraphQL blue-api.morpho.org/graphql (note: schema has NO uniqueKey; query markets(first,where:{chainId_in:[1]}){items{lltv collateralAsset{symbol address} loanAsset{symbol} oracleAddress state{collateralAssetsUsd}}}). Filter PT in collateral symbol, sort by collateralAssetsUsd. Pick the live top-TVL one.
  2. Pull the oracle source from Sourcify (sourcify.dev/server/files/any/1/<addr>) when Etherscan needs a key. The Morpho oracle is MorphoChainlinkOracleV2 -- a generic Chainlink-style wrapper, NOT a Pendle-native contract. The PT-specific logic is in baseFeed1.
  3. Decode the constructor-args (Sourcify serves constructor-args.txt). Map baseFeed1/baseFeed2/quoteFeed1/quoteFeed2 + decimals. baseFeed1 is almost always the PT feed.
  4. cast call <baseFeed1> "description()(string)" -> "Pendle Chainlink-compatible Oracle". Then read twapDuration(), baseOracleType() (enum: 0=PT_TO_SY, 1=PT_TO_ASSET, 2/3=LP), market(), factory().
  5. From the Pendle market: readTokens() -> (SY, PT, YT); expiry(); compute days-to-maturity. SY.assetInfo()(uint8,address,uint8) -> assetType (0=TOKEN/1:1 wrapper, others = yield index), asset address.

The 5 sub-classes mapped to provider code (pendle-core-v2-public)

contracts/oracles/PtYtLpOracle/PendlePYOracleLib.sol: - (a)/(e) spot/TWAP: getMarketLnImpliedRate uses market.observe(durations). Never spot. Read twapDuration() on the feed; <= 900s on a thin market is the only spot-ish risk. - (c) maturity: getPtToAssetRateRaw -> if (expiry <= block.timestamp) return PMath.ONE. The boundary is handled in the PROVIDER, so even a maturity-naive consumer is safe. Verify by vm.warp(expiry) on fork -> rate snaps to exactly 1e18, and consumer price() still resolves. - (b)/(d) depeg/insolvency: getPtToAssetRate applies *syIndex/pyIndex ONLY when syIndex < pyIndex (a DECREASE in SY.exchangeRate). A drop flows through. An UPWARD spike does NOT get this haircut (it takes the raw branch). syIndex = SY.exchangeRate() is read RAW at line ~86 -- the only unbounded external input.

Where the EV actually is (what to hunt next, NOT the big stable markets)

  1. Non-stable SY: PT-sUSDe / PT-weETH / PT-eETH style markets where SY.exchangeRate() is a within-block-movable LST/LRT index. The raw read at PendlePYOracleLib:86 is the live B-consumes-C seam there. Probe whether the SY's exchangeRate can be moved in one block (deposit/donation/reward-harvest timing).
  2. The upward branch: getPtToAssetRate does NOT haircut an exchangeRate INCREASE. If a consumer somewhere uses PT_TO_SY or composes the rate such that an inflated exchangeRate inflates collateral value, that is the gap. Stable wrappers (apxUSD, fixed 1e18) make this inert; yield indices do not.
  3. Long-tail markets wiring a NON-recommended oracle: PendleSparkLinearDiscountOracle / PendleLinearDiscountOracleWrapper (stateless, linear-discount, NO TWAP, NO market.observe). These are the ones that can be wrong: a linear-discount oracle ignores the actual AMM-implied rate and can over/under-value PT vs the market. Grep collateral oracles for description() != "Pendle Chainlink-compatible Oracle".
  4. Capped-ratio liveness: where a market adds a Chainlink-style "Capped Collateralization Ratio" feed (e.g. Apyx for apxUSD), Morpho does NOT staleness-check it (ChainlinkDataFeedLib explicitly skips staleness). If that feed stalls while backing degrades, collateral stays overvalued at the last ratio. Off-chain liveness dependency -> curator risk, not a contract bug, but worth flagging.

Negative-result calibration (so we don't re-run dead ground)

Tooling notes

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