概述
在构建基于 Uniswap V4 Hook 的借贷协议 Licredity 时,我们希望引入 PT 作为担保品,但此前我并没有详细了解过 PT 预言机的开发生态,所以我阅读了目前 Morpho 内几个较大使用 PT 的市场,并阅读了这些市场内的预言机实现。
本文默认读者熟悉 Morpho 的标准预言机实现,假如读者对此不熟悉,可以阅读 现代 DeFi: 最小化借贷协议 Morpho 一文。
PT-USDe-25SEP2025 / DAI
该市场本质上使用了 0x59CadA9800cc83E68Ba795c8A5982F4e6dB037eC 作为预言机,源代码如下:
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.19;
contract PendleSparkLinearDiscountOracle {
uint256 private constant SECONDS_PER_YEAR = 365 days;
uint256 private constant ONE = 1e18;
address public immutable PT;
uint256 public immutable maturity;
uint256 public immutable baseDiscountPerYear; // 100% = 1e18
constructor(address _pt, uint256 _baseDiscountPerYear) {
require(_baseDiscountPerYear <= 1e18, "invalid discount");
require(_pt != address(0), "zero address");
PT = _pt;
maturity = PTExpiry(PT).expiry();
baseDiscountPerYear = _baseDiscountPerYear;
}
function latestRoundData()
external
view
returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)
{
uint256 timeLeft = (maturity > block.timestamp) ? maturity - block.timestamp : 0;
uint256 discount = getDiscount(timeLeft);
require(discount <= ONE, "discount overflow");
return (0, int256(ONE - discount), 0, 0, 0);
}
function decimals() external pure returns (uint8) {
return 18;
}
function getDiscount(uint256 timeLeft) public view returns (uint256) {
return (timeLeft * baseDiscountPerYear) / SECONDS_PER_YEAR;
}
}
interface PTExpiry {
function expiry() external view returns (uint256);
}
直接将 PT-USDe-25SEP2025 视为折价的 USDe,并且假设 1 USDe = 1 DAI
,并没有处理 USDe 可能的脱锚问题。在 Pendle PT 内部,PT 必须实现以下接口:
interface IPPrincipalToken is IERC20Metadata {
function burnByYT(address user, uint256 amount) external;
function mintByYT(address user, uint256 amount) external;
function initialize(address _YT) external;
function SY() external view returns (address);
function YT() external view returns (address);
function factory() external view returns (address);
function expiry() external view returns (uint256);
function isExpired() external view returns (bool);
}
但需要注意的,由于在 Licredity Oracle 内部进行 maxStaleness
的预言机返回数据过期检验,所以此处我们不能直接使用 PendleSparkLinearDiscountOracle
的源代码,此处的 return (0, int256(ONE - discount), 0, 0, 0);
无法通过过期检查。
PT-USDe-25SEP2025 / USDC
MetaOracleDeviationTimelock
该预言机并没有使用 Morpho 自己的预言机实现,而是单独实现了一个符合 Morpho 接口的预言机,该预言机地址是 0xe6aBD3B78Abbb1cc1Ee76c5c3689Aa9646481Fbb
。
Morpho 要求的预言机接口为:
interface IOracle {
/// @notice Returns the price of 1 asset of collateral token quoted in 1 asset of loan token, scaled by 1e36.
/// @dev It corresponds to the price of 10**(collateral token decimals) assets of collateral token quoted in
/// 10**(loan token decimals) assets of loan token with `36 + loan token decimals - collateral token decimals`
/// decimals of precision.
function price() external view returns (uint256);
}
简单来说,Morpho 要求返回 1 单位担保品以债务资产计价的情况。在 Morpho 内部,我们在 _isHealthy
内部使用该价格:
/// @dev Returns whether the position of `borrower` in the given market `marketParams` is healthy.
/// @dev Assumes that the inputs `marketParams` and `id` match.
function _isHealthy(MarketParams memory marketParams, Id id, address borrower) internal view returns (bool) {
if (position[id][borrower].borrowShares == 0) return true;
uint256 collateralPrice = IOracle(marketParams.oracle).price();
return _isHealthy(marketParams, id, borrower, collateralPrice);
}
/// @dev Returns whether the position of `borrower` in the given market `marketParams` with the given
/// `collateralPrice` is healthy.
/// @dev Assumes that the inputs `marketParams` and `id` match.
/// @dev Rounds in favor of the protocol, so one might not be able to borrow exactly `maxBorrow` but one unit less.
function _isHealthy(MarketParams memory marketParams, Id id, address borrower, uint256 collateralPrice)
internal
view
returns (bool)
{
uint256 borrowed = uint256(position[id][borrower].borrowShares).toAssetsUp(
market[id].totalBorrowAssets, market[id].totalBorrowShares
);
uint256 maxBorrow = uint256(position[id][borrower].collateral).mulDivDown(collateralPrice, ORACLE_PRICE_SCALE)
.wMulDown(marketParams.lltv);
return maxBorrow >= borrowed;
}
继续阅读 0xe6aBD3B78Abbb1cc1Ee76c5c3689Aa9646481Fbb
源代码,该预言机实现了
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol";
import {IMetaOracleDeviationTimelock} from "./interfaces/IMetaOracleDeviationTimelock.sol";
import {IOracle} from "./interfaces/IOracle.sol";
/// @title MetaOracleDeviationTimelock
/// @author Steakhouse Financial
/// @notice A meta-oracle that selects between a primary and backup oracle based on price deviation and timelocks.
/// @dev Switches to backup if primary deviates significantly, switches back when prices reconverge.
/// MUST be initialized by calling the `initialize` function.
contract MetaOracleDeviationTimelock is IMetaOracleDeviationTimelock, Initializable {
// --- Configuration (set during initialization) ---
IOracle public primaryOracle;
IOracle public backupOracle;
uint256 public deviationThreshold; // Scaled by 1e18 (e.g., 0.01e18 for 1%)
uint256 public challengeTimelockDuration; // Duration in seconds
uint256 public healingTimelockDuration; // Duration in seconds
// --- State ---
IOracle public currentOracle; // Currently selected oracle
uint256 public challengeExpiresAt; // Timestamp when challenge period ends (0 if not challenged)
uint256 public healingExpiresAt; // Timestamp when healing period ends (0 if not healing)
/// @param _primaryOracle The primary price feed.
/// @param _backupOracle The backup price feed.
/// @param _deviationThreshold The maximum allowed relative deviation (scaled by 1e18) before a challenge can be initiated.
/// @param _challengeTimelockDuration The duration (seconds) a challenge must persist before switching to backup.
/// @param _healingTimelockDuration The duration (seconds) prices must remain converged before switching back to primary.
function initialize(
IOracle _primaryOracle,
IOracle _backupOracle,
uint256 _deviationThreshold,
uint256 _challengeTimelockDuration,
uint256 _healingTimelockDuration
) external initializer {
require(address(_primaryOracle) != address(0), "Invalid primary oracle");
require(address(_backupOracle) != address(0), "Invalid backup oracle");
require(address(_primaryOracle) != address(_backupOracle), "Oracles must be different");
require(_deviationThreshold > 0, "Deviation threshold must be positive");
primaryOracle = _primaryOracle;
backupOracle = _backupOracle;
deviationThreshold = _deviationThreshold;
challengeTimelockDuration = _challengeTimelockDuration;
healingTimelockDuration = _healingTimelockDuration;
// Check initial deviation
uint256 initialPrimaryPrice = _primaryOracle.price();
uint256 initialBackupPrice = _backupOracle.price();
uint256 initialDeviation;
if (initialBackupPrice == 0) {
initialDeviation = initialPrimaryPrice == 0 ? 0 : type(uint256).max;
} else {
uint256 diff;
if (initialPrimaryPrice >= initialBackupPrice) {
diff = initialPrimaryPrice - initialBackupPrice;
} else {
diff = initialBackupPrice - initialPrimaryPrice;
}
initialDeviation = (diff * 10**18) / initialBackupPrice;
}
require(initialDeviation <= _deviationThreshold, "MODT: Initial deviation too high");
currentOracle = _primaryOracle; // Start with the primary oracle
}
/// @inheritdoc IOracle
function price() public view returns (uint256) {
try currentOracle.price() returns (uint256 currentPrice) {
return currentPrice;
} catch {
if (isPrimary()) {
return backupOracle.price();
} else {
return primaryOracle.price();
}
}
}
/// @inheritdoc IMetaOracleDeviationTimelock
function primaryPrice() public view returns (uint256) {
return primaryOracle.price();
}
/// @inheritdoc IMetaOracleDeviationTimelock
function backupPrice() public view returns (uint256) {
return backupOracle.price();
}
/// @notice Checks if the primary oracle is currently selected.
function isPrimary() public view returns (bool) {
return currentOracle == primaryOracle;
}
/// @notice Checks if the backup oracle is currently selected.
function isBackup() public view returns (bool) {
return currentOracle == backupOracle;
}
/// @notice Checks if a challenge is currently active.
function isChallenged() public view returns (bool) {
return challengeExpiresAt > 0;
}
/// @notice Checks if a healing period is currently active.
function isHealing() public view returns (bool) {
return healingExpiresAt > 0;
}
/// @notice Calculates the absolute relative deviation between primary and backup oracles.
/// @dev Deviation is calculated as `abs(primaryPrice - backupPrice) * 1e18 / backupPrice`.
/// Returns 0 if backupPrice is 0 and primaryPrice is 0.
/// Returns type(uint256).max if backupPrice is 0 and primaryPrice is non-zero.
function getDeviation() public view returns (uint256) {
uint256 currentPrimaryPrice = primaryOracle.price();
uint256 currentBackupPrice = backupOracle.price();
if (currentBackupPrice == 0) {
return currentPrimaryPrice == 0 ? 0 : type(uint256).max;
}
uint256 diff;
if (currentPrimaryPrice >= currentBackupPrice) {
diff = currentPrimaryPrice - currentBackupPrice;
} else {
diff = currentBackupPrice - currentPrimaryPrice;
}
// Use uint256 for intermediate multiplication to avoid overflow before division
return (diff * 10**18) / currentBackupPrice;
}
/// @notice Checks if the deviation exceeds the configured threshold.
function isDeviant() public view returns (bool) {
return getDeviation() > deviationThreshold;
}
/// @notice Initiates a challenge if the primary oracle is active and deviation threshold is exceeded.
/// @dev Starts a timelock period (`challengeTimelockDuration`).
function challenge() external {
require(isPrimary(), "MODT: Must be primary oracle");
require(!isChallenged(), "MODT: Already challenged");
require(isDeviant(), "MODT: Deviation threshold not met");
challengeExpiresAt = block.timestamp + challengeTimelockDuration;
emit ChallengeStarted(challengeExpiresAt);
}
/// @notice Revokes an active challenge if the deviation is no longer present.
function revokeChallenge() external {
require(isPrimary(), "MODT: Must be primary oracle"); // Should still be primary
require(isChallenged(), "MODT: Not challenged");
require(!isDeviant(), "MODT: Deviation threshold still met");
challengeExpiresAt = 0;
emit ChallengeRevoked();
}
/// @notice Checks if the challenge has expired.
function hasChallengeExpired() public view returns (bool) {
return isChallenged() && block.timestamp >= challengeExpiresAt;
}
/// @notice Checks if the challenge can be accepted.
function canAcceptChallenge() public view returns (bool) {
return isPrimary() && isChallenged() && block.timestamp >= challengeExpiresAt && isDeviant();
}
/// @notice Accepts the challenge after the timelock expires, switching to the backup oracle.
/// @dev Requires the deviation to still be present.
function acceptChallenge() external {
require(isPrimary(), "MODT: Must be primary oracle");
require(isChallenged(), "MODT: Not challenged");
require(block.timestamp >= challengeExpiresAt, "MODT: Challenge timelock not passed");
require(isDeviant(), "MODT: Deviation resolved"); // Deviation must persist
currentOracle = backupOracle;
challengeExpiresAt = 0;
emit ChallengeAccepted(address(currentOracle));
}
/// @notice Initiates the healing process if the backup oracle is active and prices have reconverged.
/// @dev Starts a timelock period (`healingTimelockDuration`).
function heal() external {
require(isBackup(), "MODT: Must be backup oracle");
require(!isHealing(), "MODT: Already healing");
require(!isDeviant(), "MODT: Deviation threshold still met");
healingExpiresAt = block.timestamp + healingTimelockDuration;
emit HealingStarted(healingExpiresAt);
}
/// @notice Revokes an active healing process if the deviation threshold is exceeded again.
function revokeHealing() external {
require(isBackup(), "MODT: Must be backup oracle"); // Should still be backup
require(isHealing(), "MODT: Not healing");
require(isDeviant(), "MODT: Deviation threshold not met");
healingExpiresAt = 0;
emit HealingRevoked();
}
/// @notice Checks if the healing has expired.
function hasHealingExpired() public view returns (bool) {
return isHealing() && block.timestamp >= healingExpiresAt;
}
/// @notice Checks if the healing can be accepted.
function canAcceptHealing() public view returns (bool) {
return isBackup() && isHealing() && block.timestamp >= healingExpiresAt && !isDeviant();
}
/// @notice Accepts the healing after the timelock expires, switching back to the primary oracle.
/// @dev Requires the prices to still be converged (not deviant).
function acceptHealing() external {
require(isBackup(), "MODT: Must be backup oracle");
require(isHealing(), "MODT: Not healing");
require(block.timestamp >= healingExpiresAt, "MODT: Healing timelock not passed");
require(!isDeviant(), "MODT: Deviation occurred"); // Prices must remain converged
currentOracle = primaryOracle;
healingExpiresAt = 0;
emit HealingAccepted(address(currentOracle));
}
}
上述预言机的代码逻辑并不复杂。预言机内包含两个预言机,分别被称为:
primaryOracle
核心预言机,主要负责报价工作backupOracle
备用预言机,当主预言机与备用预言机的价差过大时,可以通过挑战将预言机切换为备用预言机
代码内的
currentOracle
的数值可能是primaryOracle
或是backupOracle
。另外需要注意的是,假如currentOracle
的price
函数出现问题,那么会自动切换为另一个预言机请求报价
我们可以将上述预言机视为如下状态机:
其中,challenge
开启的条件如下:
currentOracle == primaryOracle
- 当前并不属于挑战时间锁(
ChallengeTimeLock
) 内部 primaryOracle
和backupOracle
价格偏差大于最初设置
当 challenge
挑战时间锁内出现primaryOracle
和 backupOracle
价格偏差恢复的情况,可以调用 revokeChallenge
撤销挑战。当挑战时间锁完成并且此时价格偏差仍未恢复,可以调用 acceptChallenge
将 currentOracle
切换为 backupOracle
。
当然,我们也可以在 primaryOracle
与 backupOracle
报价偏差恢复后,将预言机切换为 primaryOracle
,该流程被称为 heal
过程,其过程与 challenge
基本类似,此处不再赘述。
简单来说,MetaOracleDeviationTimelock
合约允许用户设置 primaryOracle
和 backupOracle
进行报价。当 MetaOracleDeviationTimelock
的 price
函数被调用时,currentOracle
会返回报价,但假如 currentOracle
报价失败,那么会立即使用另一个预言机的报价返回。
同时,该预言机也允许随着 primaryOracle
和 backupOracle
报价价差的情况,进行预言机切换,当报价价差过大且时间锁完成时,currentOracle
会被切换为 primaryOracle
;反之,当预言机之间的价差恢复时,currentOracle
会被切换为 backupOracle
。
开发者为 PT-USDe-25SEP2025
设置了以下预言机:
primaryOracle
对应的地址是0xA5d21a73312221c1C7D94702316c02B2485036B9
backupOracle
对应的地址是0x0DE0eCde53eA9d38351b2CE0635430506c0AA330
这两个地址均部署了标准的 MorphoChainlinkOracleV2
预言机,我们会在后文分析这两个预言机的配置情况。
Primary Oracle
在 Primary Oracle 内部,开发者使用了 Morpho 的标准预言机,并只设置了 BASE_FEED_1
,设置的地址是 0x93F4E501ddcB7e8FD18f5839204AeC0234B2B2E0
。该地址内部署了 PendleChainlinkOracle
合约,该合约的源代码可以在 pendle-core-v2-public 内找到,该合约的核心部分代码如下:
function(IPMarket, uint32) internal view returns (uint256) private immutable _getRawPendlePrice;
constructor(address _market, uint32 _twapDuration, PendleOracleType _baseOracleType) {
factory = msg.sender;
market = _market;
twapDuration = _twapDuration;
baseOracleType = _baseOracleType;
(uint256 fromTokenDecimals, uint256 toTokenDecimals) = _readDecimals(_market, _baseOracleType);
(fromTokenScale, toTokenScale) = (10 ** fromTokenDecimals, 10 ** toTokenDecimals);
_getRawPendlePrice = _getRawPendlePriceFunc();
}
// =================================================================
// PRICING FUNCTIONS
// =================================================================
function _getPendleTokenPrice() internal view returns (int256) {
return _descalePrice(_getRawPendlePrice(IPMarket(market), twapDuration));
}
function _descalePrice(uint256 price) private view returns (int256 unwrappedPrice) {
return PMath.Int((price * fromTokenScale) / toTokenScale);
}
// =================================================================
// USE ONLY AT INITIALIZATION
// =================================================================
function _getRawPendlePriceFunc()
internal
view
returns (function(IPMarket, uint32) internal view returns (uint256))
{
if (baseOracleType == PendleOracleType.PT_TO_SY) {
return PendlePYOracleLib.getPtToSyRate;
} else if (baseOracleType == PendleOracleType.PT_TO_ASSET) {
return PendlePYOracleLib.getPtToAssetRate;
} else {
revert("not supported");
}
}
function _readDecimals(
address _market,
PendleOracleType _oracleType
) internal view returns (uint8 _fromDecimals, uint8 _toDecimals) {
(IStandardizedYield SY, , ) = IPMarket(_market).readTokens();
uint8 syDecimals = SY.decimals();
(, , uint8 assetDecimals) = SY.assetInfo();
if (_oracleType == PendleOracleType.PT_TO_ASSET) {
return (assetDecimals, assetDecimals);
} else if (_oracleType == PendleOracleType.PT_TO_SY) {
return (assetDecimals, syDecimals);
}
}
在以上代码内,我们可以看到一种并不常见的智能合约编程技术,即 solidity 内的函数式编程方法。在 solidity 内,实际上允许函数返回值是另一个函数。此处在构造过程中调用的 _getRawPendlePriceFunc
就是一个这种特殊函数,其返回值仍是一个函数,并且该函数被初始化到了 _getRawPendlePrice
内部。此处 _getRawPendlePrice
在 PendleOracleType.PT_TO_ASSET
情况下的实现如下:
/**
* This function returns the twap rate PT/Asset on market, but take into account the current rate of SY
This is to account for special cases where underlying asset becomes insolvent and has decreasing exchangeRate
* @param market market to get rate from
* @param duration twap duration
*/
function getPtToAssetRate(IPMarket market, uint32 duration) internal view returns (uint256) {
(uint256 syIndex, uint256 pyIndex) = getSYandPYIndexCurrent(market);
if (syIndex >= pyIndex) {
return getPtToAssetRateRaw(market, duration);
} else {
return (getPtToAssetRateRaw(market, duration) * syIndex) / pyIndex;
}
}
/// @notice returns the raw rate without taking into account whether SY is solvent
function getPtToAssetRateRaw(IPMarket market, uint32 duration) internal view returns (uint256) {
uint256 expiry = market.expiry();
if (expiry <= block.timestamp) {
return PMath.ONE;
} else {
uint256 lnImpliedRate = getMarketLnImpliedRate(market, duration);
uint256 timeToExpiry = expiry - block.timestamp;
uint256 assetToPtRate = MarketMathCore._getExchangeRateFromImpliedRate(lnImpliedRate, timeToExpiry).Uint();
return PMath.ONE.divDown(assetToPtRate);
}
}
function getMarketLnImpliedRate(IPMarket market, uint32 duration) internal view returns (uint256) {
uint32[] memory durations = new uint32[](2);
durations[0] = duration;
uint216[] memory lnImpliedRateCumulative = market.observe(durations);
return (lnImpliedRateCumulative[1] - lnImpliedRateCumulative[0]) / duration;
}
上述代码使用 getMarketLnImpliedRate
函数从 market
内读取 TWAP 数据,然后使用 (lnImpliedRateCumulative[1] - lnImpliedRateCumulative[0]) / duration
将其转化为该段时间内平均数。
Primary Oracle 存在的问题是 Pendle 预言机返回的实际上是 PT-USDe-25SEP2025 / USDe
的报价,所以 Primary Oracle 假设了 USDe
和 USDC
之间的价格不会脱锚。我们会在 Backup Oracle 内解决此问题
Backup Oracle
Backup Oracle 也使用了标准的 Morpho 预言机实现,并设置了以下参数:
BASE_FEED_1
也使用了 Pendle 的PT-USDe-25SEP2025 / USDe
报价预言机,实际上就是上文介绍的0x93F4E501ddcB7e8FD18f5839204AeC0234B2B2E0
BASE_FEED_2
使用了 Chainlink 的USDe / USD
预言机,用来处理可能的 USDe 脱锚问题,地址是0xa569d910839Ae8865Da8F8e70FfFb0cBA869F961
,返回的报价精度为 8QUOTE_FEED_1
使用了DummyFeed
,该预言机只用于修正数值精度
为什么需要 DummyFeed
修正精度? 在 MorphoChainlinkOracleV2 内存在以下代码:
SCALE_FACTOR = 10
** (
36 + quoteTokenDecimals + quoteFeed1.getDecimals() + quoteFeed2.getDecimals() - baseTokenDecimals
- baseFeed1.getDecimals() - baseFeed2.getDecimals()
) * quoteVaultConversionSample / baseVaultConversionSample;
}
在此案例中,quoteTokenDecimals
是 USDC 的精度,即 quoteTokenDecimals = 6
,而 baseTokenDecimals
是 USDe 的精度,即 baseTokenDecimals = 18
,而 baseFeed1.getDecimals
的返回值是 18,且 baseFeed2.getDecimals()
的返回值是 8。
如果不考虑 QUOTE_FEED_1
的精度,那么 SCALE_FACTOR
的计算结果是 $10^{36 + 6 - 18 - 18 - 8} = 10^{-2}$。显然,该计算由于出现负数会导致计算溢出。解决方案是设置 quoteFeed1
的 decimals
。在此案例中,我们将 quoteFeed1
的 decimals
设置为 12,由此避免计算 SCALE_FACTOR
过程中的下溢。但为了避免 quoteFeed1
影响最后的结果,我们将 quoteFeed1
的 price
设置为 1e12
。
实际上,我们可以将 quoteFeed1
设置的精度设置为大于 2 的某一个数字,该预言机存在只是为了避免计算 SCALE_FACTOR
过程中的下溢。
PT-cUSDO-20NOV2025 / USDC
对于 PT-cUSDO-20NOV2025 / USDC
市场,开发者使用了 0xCE84fD399620B7D20e18AB6009Fc8A0cdaad880f
作为该市场的预言机,该合约是一个可升级合约,其实现代码位于 0x04236557c2dac11175638a598b28a9b22fdd8d06
内部,合约被命名为 EOPendlePTFeedHybrid
。该合约的计算价格使用了 MAX(1, MIN(LinearDiscount, TWAP PTtoAsset))
公式,混合了 Pendle 的 TWAP 报价和线性折扣计算。
其中 Pendle TWAP 报价使用了如下函数获取数据:
function _getPtToAssetTWAPRate() internal view returns (uint256) {
return IPTOracle(ptOracle).getPtToAssetRate(ptMarket, twapDuration) * twapNumeratorMultiplier
/ twapDenominatorMultiplier;
}
而线性折扣计算使用了如下代码:
function _getLinearDiscountRate() internal view returns (uint256) {
uint256 timeLeft = (maturity > block.timestamp) ? maturity - block.timestamp : 0;
uint256 discount = (timeLeft * baseDiscountPerYear) / SECONDS_PER_YEAR;
if (discount > ONE) revert DiscountOverflow();
return ONE - discount;
}
AAVE PT USDe September 2025
最近 AAVE 也增加了 PT 作为担保品的选项,此处我们对 AAVE PT USDe September 2025
市场对预言机进行简单分析,该预言机的地址是 0x8B17C02d22EE7D6B8D6829ceB710A458de41E84a
。其核心代码如下:
/// @inheritdoc ICLSynchronicityPriceAdapter
function latestAnswer() external view returns (int256) {
int256 currentAssetPrice = ASSET_TO_USD_AGGREGATOR.latestAnswer();
if (currentAssetPrice <= 0) {
return 0;
}
uint256 price = (uint256(currentAssetPrice) * (PERCENTAGE_FACTOR - getCurrentDiscount())) /
PERCENTAGE_FACTOR;
return int256(price);
}
/// @inheritdoc IPendlePriceCapAdapter
function getCurrentDiscount() public view returns (uint256) {
uint256 timeToMaturity = (MATURITY > block.timestamp) ? MATURITY - block.timestamp : 0;
return (timeToMaturity * discountRatePerYear) / SECONDS_PER_YEAR;
}
与上文出现的预言机类似,该预言机也使用了简单的线性计算当前 PT 的价格,但有趣的是,AAVE 并不假设 PT 的底层资产 USDe 与 USD 不脱锚,所以引入了 ASSET_TO_USD_AGGREGATOR
的选项来处理可能的脱锚。但 AAVE 在此处的 ASSET_TO_USD_AGGREGATOR
实际上使用是 0x3E7d1eAB13ad0104d2750B8863b489D65364e32D
预言机,该预言机是 USDT / USD
预言机,也就是说 USDe 脱锚并不会导致 PT 预言机价格下降,而是 USDT 脱锚会导致 PT 预言机价格下降。
ASSET_TO_USD_AGGREGATOR
另一个有趣的特性是该预言机存在报价上限,即 USDT / USD 价格不会大于 1.04
总结
在 Morpho 内,我们可以观察到以下三种不同的预言机配置:
- 根据利率线性计算 PT 价格,SparkDAO 管理的市场基本都使用了这种类似的预言机
- 使用 Pendle 官方 TWAP 预言机作为主预言机,同时配置 TWAP 预言机和底层资产价格预言机作为备用预言机,这种方案有效避免了底层资产脱锚的风险,同时没有显著提高 gas 成本,Steakhouse 设计了这套方案
- 使用 Pendle 官方 TWAP 预言机和利率线性计算,使用较小的数值
在 AAVE 内,我们主要观察到使用了 利率线性计算 PT 价格 的预言机。
一个有趣的观察是很多预言机都没有考虑底层资产脱锚的情况,而 AAVE 奇怪的使用了 USDT 脱锚衡量 USDe 脱锚。