拆分时间:深入 Pendle V2 的收益代币化引擎
如果你能把 stETH 持仓中的收益剥离出来单独出售呢?不是在某个中心化交易所的衍生品,而是链上的 ERC20 代币,由一个专门设计的 AMM 定价,并在到期时通过数学公式强制收敛?
这就是 Pendle Finance V2 的核心理念。它将任何生息资产封装到标准化接口中,然后拆分为两个代币:一个代表本金,一个代表未来收益。接着它提供一个专为交易这些时间衰减工具而设计的 AMM。
我阅读了公开仓库中全部 206 个 Solidity 文件(约 21,454 行)。以下是其工作原理。
拆分机制:SY 到 PT + YT
代币化通过三层抽象实现:
底层资产(如 stETH、aUSDC)
|
v
Standardized Yield (SY) -- 将任何生息资产封装为通用接口
|
v
PT + YT -- 按当前汇率,1 SY 铸造 1 PT + 1 YTStandardized Yield (SY) 是适配器层。每个 SY 合约封装一个生息资产,并暴露统一接口:deposit()、redeem()、exchangeRate()。汇率是 SY 与底层资产的转换因子,对于正常运行的底层资产应单调不减。转换数学非常直观:
function syToAsset(uint256 exchangeRate, uint256 syAmount) internal pure returns (uint256) {
return (syAmount * exchangeRate) / ONE;
}Principal Token (PT) 是一个带有到期时间戳的简洁 ERC20。到期时,1 PT 可赎回 1 单位底层资产。到期前,它以折价交易,折价幅度反映市场的隐含收益率。PT 只能由其配对的 YT 合约铸造和销毁。
Yield Token (YT) 是复杂性所在。铸造时,YT 合约接收 SY 并同时发行 PT 和 YT:
function _calcPYToMint(uint256 amountSy, uint256 indexCurrent) internal pure returns (uint256 amountPY) {
return SYUtils.syToAsset(indexCurrent, amountSy);
}YT 持有者收取底层 SY 在购买时间和到期时间之间产生的所有收益。利息累计公式追踪每个用户的指数快照:
function _distributeInterestPrivate(address user, uint256 currentIndex) private {
uint256 prevIndex = userInterest[user].index;
uint256 principal = _YTbalance(user);
uint256 interestFromYT = (principal * (currentIndex - prevIndex))
.divDown(prevIndex * currentIndex);
userInterest[user].accrued += interestFromYT.Uint128();
userInterest[user].index = currentIndex.Uint128();
}这计算的是 principal * (1/prevIndex - 1/currentIndex) – 两个汇率快照之间累计的 SY 利息。到期后,YT 在赎回时变得无价值(只需要 PT),但用户仍可领取到期前累计的利息。所有到期后的收益流向协议金库。
AMM 设计:带时间衰减的 Logit 曲线
Pendle 不使用恒定乘积 AMM。它使用受 Notional Finance 的 fCash 模型启发的基于 logit 的汇率曲线,且仅交易 PT 对 SY。YT 交易通过路由器的闪电交换机制合成(后文详述)。
MarketMathCore.sol 中的核心定价函数:
function _getExchangeRate(
int256 totalPt, int256 totalAsset, int256 rateScalar,
int256 rateAnchor, int256 netPtToAccount
) internal pure returns (int256 exchangeRate) {
int256 numerator = totalPt.subNoNeg(netPtToAccount);
int256 proportion = numerator.divDown(totalPt + totalAsset);
int256 lnProportion = _logProportion(proportion);
exchangeRate = lnProportion.divDown(rateScalar) + rateAnchor;
}其中 _logProportion 计算 logit 函数 – ln(p / (1-p)):
function _logProportion(int256 proportion) internal pure returns (int256) {
int256 logitP = proportion.divDown(IONE - proportion);
return logitP.ln();
}这产生了一条 S 型曲线,在均衡利率附近提供集中流动性,当 PT 占比趋近 0 或 1 时有自然边界。硬性上限 MAX_MARKET_PROPORTION = 96% 防止极端的池子失衡。
关键创新是时间衰减。rateScalar 参数随到期日临近而增大:
function _getRateScalar(MarketState memory market, uint256 timeToExpiry)
internal pure returns (int256 rateScalar) {
rateScalar = (market.scalarRoot * IMPLIED_RATE_TIME.Int()) / timeToExpiry.Int();
}其中 IMPLIED_RATE_TIME = 365 days。当 timeToExpiry 趋近零时,rateScalar 趋向无穷大,这会压平汇率曲线,迫使 PT 价格向与底层资产 1:1 收敛。这就是 PT 在到期时回归面值的机制 – 不依赖治理或 keeper 机器人,纯粹通过数学实现。
市场状态在每次操作开始时加载到内存结构体中,完全在内存中操作,最后通过单次 _writeState 调用写回。这最小化了昂贵的 SSTORE 操作。存储紧密打包:totalPt(int128)+ totalSy(int128)共享一个 slot;lastLnImpliedRate(uint96)+ 预言机元数据共享另一个。
路由器:类 Diamond 代理
PendleRouterV4 只有 24 行代码。它继承 OpenZeppelin 的 Proxy,根据函数选择器分发每个调用:
contract PendleRouterV4 is Proxy, RouterStorage {
function _implementation() internal view override returns (address) {
RouterStorage.CoreStorage storage $ = _getCoreStorage();
address facet = $.selectorToFacet[msg.sig];
require(facet != address(0), "INVALID_SELECTOR");
return facet;
}
}存储使用 ERC-7201 命名空间 slot 在确定性位置,将 bytes4 选择器映射到 facet 地址。路由器委托给六个 action facet:
| Facet | 用途 |
|---|---|
ActionSwapPTV3 |
PT 与 SY 的交换 |
ActionSwapYTV3 |
YT 交换(通过闪电交换 + 铸造/赎回合成) |
ActionAddRemoveLiqV3 |
流动性管理 |
ActionMiscV3 |
杂项操作 |
ActionCallbackV3 |
交换回调处理 |
ActionSimple |
链上近似计算变体 |
YT 交换最有意思。由于市场只交易 PT/SY,购买 YT 需要多步合成:将 SY 发送到 YT 合约,从市场闪电交换 PT(PT 转到 YT 合约),从组合的 SY+PT 铸造 PY,然后在回调中结算闪电交换。卖出 YT 则相反:将 YT 发送到 YT 合约,从市场闪电交换 PT 到 YT 合约,赎回 PT+YT 获得 SY,返回 SY 以结算。所有这些通过回调机制在单笔交易中原子完成。
Reflector 辅助合约处理执行时才能确定精确输入金额的集成场景。它接收代币,将输入金额缩放到实际接收余额,将调整后的调用转发到硬编码路由器地址 0x888888888889758F76e7103c6CbF23ABbF58F946,并将剩余粉尘清扫回接收者。
预言机系统:基于隐含利率的 TWAP
预言机系统直接改编自 Uniswap V3,但追踪的不是价格,而是 lnImpliedRateCumulative – 对数隐含利率随时间的累积和:
function transform(Observation memory last, uint32 blockTimestamp, uint96 lnImpliedRate)
public pure returns (Observation memory) {
return Observation({
blockTimestamp: blockTimestamp,
lnImpliedRateCumulative: last.lnImpliedRateCumulative
+ uint216(lnImpliedRate) * (blockTimestamp - last.blockTimestamp),
initialized: true
});
}观测值存储在最多 65,535 个 slot 的环形缓冲区中,每个区块一个。任意时间段的 TWAP 为 (cumulative[now] - cumulative[now - duration]) / duration。二分搜索找到目标时间戳的相邻观测值,对中间时间戳进行线性插值。
从 TWAP 隐含利率可以推导 PT 和 YT 的价格:PT_price = 1 / e^(TWAP_lnImpliedRate * timeToExpiry / 365 days) 以及 YT_price = 1 - PT_price(因为 PT + YT = 1 个底层资产)。LP 代币估值更复杂 – 它计算一个假设交易,将池子移至预言机利率,然后按预言机价格对结果组合估值,使其具有抗操纵性。
预言机包含重入保护检查(_checkMarketReentrancy),验证市场是否处于状态修改调用中,保护预言机消费者免受只读重入攻击。
安全态势
代码库已被 6 家独立审计公司审计 9 次:Ackee、ChainSecurity、CMichel、Dedaub、Dingbats、Spearbit、0xleastwood 和 WatchPug。我们的独立分析发现 0 个严重漏洞、0 个高危漏洞、2 个中等漏洞:
| ID | 发现 | 位置 | 描述 |
|---|---|---|---|
| M-1 | Chainlink 预言机过期检查缺失 | FixedPricePTAMM.sol |
latestRoundData() 忽略了 updatedAt、answeredInRound 和 startedAt。过期或零价格可能导致跨链 PT 交换金额错误。 |
| M-2 | govExecuteMessage 治理绕过 | PendleMsgReceiveEndpointUpg.sol |
Owner 可以注入任意跨链消息,绕过 LayerZero 来源验证。无时间锁保护。被攻破的 owner 密钥可跨链伪造 vePENDLE 数据。 |
M-1 发现是一个常见的 Chainlink 集成疏忽:
(, int256 rawPrice,,,) = oracle.latestRoundData();latestRoundData() 的五个返回值都可用,但只使用了 rawPrice。修复方法很直接:检查 updatedAt > 0,强制执行最大过期窗口,验证 rawPrice > 0,并在 L2 部署时添加排序器正常运行时间检查。
除这两个发现外,安全基础扎实。每个修改状态的 external 函数都使用 nonReentrant。checks-effects-interactions 模式被一致遵循 – 状态在外部回调之前写入,回调后进行余额验证。自定义的 PendleERC20 将重入保护状态字节与 _totalSupply(uint248)打包在单个存储 slot 中,每次保护检查节省约 2,100 gas。EIP-712 类型签名配合重放保护确保限价单系统安全。所有可拥有合约使用两步所有权转移。
还发现了六个低危漏洞,包括预言机重入检查中的静默捕获(为旧版市场的前向兼容性)、Reflector 中的无限持久代币授权,以及利息计算中的理论溢出边界情况。
代码质量:B+
协议整体获得 B+ 评分。数学库经过实战检验 – LogExpMath 改编自 Balancer V2,使用 18 位小数定点精度,采用 2 的幂次分解配合 12 项 Taylor 级数。PMath 提供简洁的定点算术,并通过 PYIndex 用户自定义值类型实现类型安全。整个代码库的存储打包非常出色。
主要扣分项:NatSpec 文档稀疏(整个 /contracts/core/ 目录仅有 78 个 NatSpec 注释),公开仓库中无测试文件(测试在私有内部仓库中),代码库混合使用自定义 error 和旧式 require() 字符串。MarketMathCore.sol 文件 – 可以说是协议中最关键的代码 – 14 个函数仅有 3 个 NatSpec 注释。
| 领域 | 评估 |
|---|---|
| 架构与组织 | A- |
| 存储打包与 Gas 优化 | A |
| 安全模式 | A- |
| 数学库稳健性 | A |
| 审计覆盖 | A- |
| 可维护性 | B+ |
| 文档 | C+ |
| 测试可见性(公开仓库) | D |
总结评估
Pendle V2 是一个数学严谨的协议,围绕真正新颖的原语构建。带时间衰减的 logit-based AMM 不是任何项目的 fork – 它解决了一个真实问题(在链上为时间衰减的固定收益工具定价),采用的方法通过纯数学保证到期时的收敛,而非依赖治理。类 Diamond 的路由器保持入口点稳定,同时允许团队持续发布新的 action facet。适配了隐含利率(而非价格)的 Uniswap V3 风格 TWAP 预言机,为下游集成提供抗操纵的定价。
对于一个部署在 12+ 条 EVM 链上、拥有大量 TVL 的协议,安全态势很强。6 家公司的 9 次审计,我们独立审查中零严重发现,代码库中一致的防御模式。两个中等发现(Chainlink 过期检查、治理消息绕过)都在外围组件中,而非核心 AMM 或收益代币化逻辑。
主要差距在于透明度而非工程质量:无公开测试,最关键的数学函数文档稀疏。对于一个要求其他 DeFi 协议集成其预言机的项目,这很重要。代码是好的。展示你的工作过程会让它更好。
分析基于 pendle-core-v2-public(commit 1a9ef17,v6.5.0)。206 个 Solidity 文件,约 21,454 行代码。完整的架构、质量和安全报告可从研究团队获取。