Banked: 2026-06-11 (SEAMS lane, Morpho PT-apxUSD/USDC instance) Status: methodology + negative-result calibration. WALK-CLEAN outcome.
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.
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.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.constructor-args.txt). Map baseFeed1/baseFeed2/quoteFeed1/quoteFeed2 + decimals. baseFeed1 is almost always the PT feed.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().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.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.
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).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.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".MorphoChainlinkOracleV2 + Pendle PendleChainlinkOracle (PT_TO_ASSET, 1800s) + capped-ratio feed. This stack is CLEAN. Do not spend a full cycle re-deriving it -- map it in 15 min, confirm twapDuration>=900 and baseOracleType, fork-test the maturity warp, then move to the long-tail / non-stable-SY targets.PendleChainlinkOracle: it sets updatedAt = block.timestamp and computes live, so it can never be stale. Always check the feed TYPE before claiming staleness. (Cross-ref: this is the same shape as the earlier Morpho ERC4626-vault-rate-as-oracle seam -- the consumer's "feed keeps its promises" assumption is only as safe as the specific feed implementation behind it.)/server/files/any/<chainid>/<addr>) + cast call against ethereum-rpc.publicnode.com (publicnode and merkle.io both flake intermittently; pin --fork-block-number for determinism).uniqueKey errors, drop it; introspect with a minimal field set first.