Pendle PT (asset class) analysis
Summary
This is a technical analysis of all the Pendle PT asset-class smart contracts and main dependencies.
Disclosure: This is not an exhaustive security review of the asset like the ones done by the Pendle Team, but an analysis from an Aave technical service provider on different aspects we consider critical to review before a new type of listing. Consequently, like with any security review, this is not an absolute statement that the asset is flawless, only that, in our opinion, we don’t see significant problems with its integration with Aave, apart from different trust points.
Analysis
Pendle is a permissionless yield tokenization and trading protocol that allows splitting a yield-bearing token into two tokens: PT (Principal Token), which is the principal of the underlying yield-bearing token, and YT (Yield Token), which is the yield generated by the underlying yield-bearing token with different expiration dates.
The asset is based on a standard wrapper (ERC-5115) called SY (Standardized Yield) to separate the yield from the principal. This wrapper is an extension of ERC-20 with basic functionality for deposits, withdrawals, and transfers. Pendle uses SY as the main interface for minting PT and YT, and trading through Pendle’s AMM pools.
For the context of this analysis, our focus has been on the following aspects, critical for the correct and secure integration with Aave:
- Mechanism to update the exchange rate of the SY for the underlying asset.
- Access control (ownerships, admin roles) and nature of the entities involved.
- Any miscellaneous aspect of the code we can consider important.
- A recommendation of pricing strategy to be used in the integration of the asset <> Aave.
Criticality | Description |
---|---|
CRITICAL | It can compromise the system, leading to loss of funds if misused or exploited. e.g. proxy admin, default admin |
HIGH | It can control several parts of the system with some risk of losing funds. e.g., general owners that can set critical addresses. |
MEDIUM | It can cause system malfunctions and/or minor financial losses if misused or exploited. e.g., fee setter, fee recipient addresses |
LOW | It can cause system malfunctions but non-critical parts and or financial losses. e.g., updating descriptions or certain non-critical parameters. |
Risk | Description |
---|---|
GREEN ![]() |
The role is controlled via Timelock or Multisig wallet, which we assume is safely protected. |
YELLOW ![]() |
The role is controlled via a Multisig or EOA, which could expose the system and users to some risk depending on the actions it can control. |
RED ![]() |
The role is controlled via an EOA, representing risks for the system and users. |
General points
- FUNGIBILITY. PTs are zero-coupon bonds, fungible within the same maturity but not directly between distinct maturities. The Pendle’s AMM also provides a solution for swapping with other PTs of the same and different class and against other tokens.
- Buying/selling mechanism. Users can mint PT by depositing a yield-bearing asset and redeeming it after maturity for the underlying. Also, it’s possible to buy and sell through Pendle’s AMM anytime.
The Pools are composed of PT-SY, so swapping PT is straightforward. There’s also an auto-router for seamless swaps with any major asset. - Price mechanism. There is a built-in TWAP Oracle library. The PT oracle stores the cumulative logarithm of the interest rate implied by PT/asset pricing (called implied APY). From this, the geometric mean of Implied APY is calculated, which is used to derive the mean PT price.
- Upgradeability. The contracts related to the creation of PY contracts and AMM market are non-upgradeable. The SY contracts are asset-specific, so some may be Transparent Proxy contracts. The pyYtLpOracle is a Transparent Proxy upgradeable by the governance multi-sig 2-of-4. Aave will not be exposed to the pyYtLpOracle, which can be classified as a peripheral contract that fetches cumulative implied rates from each PY market.
- Security. Pendle V2 contracts have been audited by 8 auditors, with 3 of the top 10 renowned C4 auditors. The audits can be found in their GitHub repo here.
Contracts
The following is a non-exhaustive overview of the main smart contracts involved with Pendle Principal Tokens.
Pendle SY - Standardized Yield (SY)
The SY is an abstract contract that wraps and maintains a 1:1
ratio with any yield-bearing token. It implements the standard EIP-5115 interface, an extension of the ERC20 that includes deposit withdrawals and transfers, and provides the exchange rate of the ybToken and its underlying token. Each SY implementation is asset-specific, meaning the SY contract needs to manage deposits and withdrawals differently internally. For example, the management of deposits and withdrawals for the wstETH-SY
differs from that of the aUSDC-SY
.
Permission Owner | functions | Criticality | Risk |
---|---|---|---|
*owner: Pendle’s Governance or Pendle’s Deployer | pause, unpause | HIGH | ![]() |
*owner may vary between SY deployer, Pendle’s deployer or Governance
- Users can deposit and mint SY tokens using the
deposit(tokenIn, amount)
function. AnytokenIn
from thegetTokensIn()
function can be deposited. For example,aUSDC-SY
can be minted using eitheraUSDC
orUSDC
, whilewstETH-SY
can be minted using nativeETH
,WETH
,stETH
, andwstETH
. - Users can redeem valid tokens using the
redeem(tokenOut, amount)
function. AnytokenOut
from thegetTokensOut()
function can be withdrawn. For example, aUSDC-SY shares can be redeemed foraUSDC
orUSDC
. - Users can use the
previewDeposit(tokenIn, amount)
andpreviewRedeem(tokenOut, amount)
to estimate their deposits and withdrawals based on their chosen tokens. - The
exchangeRate()
function returns the ratio between the yield-bearing token and its underlying asset. For example,wstETH-SY.getExchangeRate()
returns thewstETH:stETH
ratio. - Some SY implementations inherit the abstract
SYBaseWithRewards
contract, which allows the SY to distribute rewards to users, while theSYBase
contract does not implement rewards. - For SY with rewards, users can check the rewards tokens by calling the
getRewardTokens()
function and claim them via theclaimRewards()
function. - The SY deployer is the contract’s owner and can
pause
andunpause
the contract. In Pendle’s deployments repo, some SY contracts have their ownership transferred to Pendle’s governance right after the deployment, but this needs to be verified before listing new PTs.
PendleYieldContractFactory - PY Factory
The PendleYieldContractFactory is the contract that creates both the PT and YT contracts from valid SY contracts. It’s a permissionless contract where anyone can create PY contracts.
Permission Owner | functions | Criticality | Risk |
---|---|---|---|
owner: Safe 2-of-4 | setExpiryDivisor, setInterestFeeRate, setRewardFeeRate, setTreasury, pay. transferOwnership | MEDIUM | ![]() |
- PY contracts are created via the
factory.createYieldContract(SY, expiry)
function by passing a valid SY contract and expiration date. - The owner can call the
setExpiryDivisor
,setInterestFeeRate
,setRewardFeeRate
, andsetTreasury
functions to set the expiry divisor, interest and reward fees, and treasury address, respectively. The expiry divisor validates the expiry timestamp, and the interest and reward fees, and the treasury address are later used in the YT contract.
PendlePrincipalToken - PT
PT represents the principal value of the underlying yield-bearing asset. At maturity, the PT token can be redeemed at 1:1 for the underlying asset. It is an ERC20 contract with custom implementations from the Pendle team, such as built-in reentrancy protection. Every PT token is minted with a YT token, and PTs can only be minted by the YT contract and before its maturity.
Permission Owner | functions | Criticality | Risk |
---|---|---|---|
correspondent YT contract | mintByYT, burnByYT | HIGH | ![]() |
- The mint and burn are performed via the
mintByYT(address, amount)
andburnByYT(address, amount)
functions, which have theonlyYT
modifier. Only the YT contract can call them. - There is an
isExpired()
function that returns whether the PT is already mature and can be redeemed 1:1 for the underlying asset.
PendleYieldToken - YT
YT represents the yield of the underlying yield-bearing asset. By holding the YT token, the user can claim the yield + reward tokens generated by the underlying yield-bearing asset up to maturity. After the expiration, the yield stops accruing, and the YT price becomes $0. It is an ERC20 contract using the custom PendleERC20
implementation, where users can mint and redeem SY tokens in the form of PY tokens (PT + YT).
Permission Owner | functions | Criticality | Risk |
---|---|---|---|
No access control | - | - | - |
- Users can mint PY tokens by calling the
mintPY(PTReceiver, YTReceiver)
. First, they must send the SY token to the contract, which will be minted at a 1:1 ratio. ThemintPYMulti(PTReceiver[], YTReceiver, SYAmounts[])
can also be used to mint multiple PY tokens in a batch for multiple users. The PT tokens are sent to thePTReceiver
, while the YT tokens are sent to theYTReceiver
. - Users can redeem PT and YT in SY by calling the
redeemPY(SYReceiver)
function. They must send PT and YT to the YT contract first or only PT if maturity has been reached. Internally, it calls the_redeemPY()
function, which burns the PT and YT tokens (YT only before maturity) and calls the_calcSyRedeemableFromPY()
function to calculate the SY equivalent of the PY amount on the current index or in the first index after maturity, and then finalizes by transferring the SY to the user and adding any interest accrued post maturity to the treasury. - The
redeemPYMulti((PTReceiver[], YTReceiver, PYAmounts[])
can also be used to redeem multiple PY tokens in a batch for multiple users. - Rewards and interest are paid in SY and distributed among YT holders before maturity. After that, YT holders do not receive any yield. They can claim the yield + rewards by calling the
redeemDueInterestAndRewards(address, bool redeemInterest, bool redeemRewards)
function. Internally, it accrues the rewards for the user in the inheritedRewardManagerAbstract
contract via the_updateAndDistributeRewards(user)
function. Finally, it transfers the rewards if the flagredeemRewards
is positive, and the same for theredeemInterest
flag, which will first_distributeInterest(user)
and then transfer the accumulated interest in SY tokens. - After maturity, interest and rewards are sent to the Pendle’s
treasury
via theredeemInterestAndRewardsPostExpiryForTreasury()
.
PendleMarket
PendleMarket is the contract that allows the creation of SY <> PT liquidity pools of the same type before maturity. Users can add or remove liquidity for LP tokens and swap directly between SY <> PT, where the LP holders receive swap fees. YT can be traded via flashswaps (via a router contract). It uses Pendle’s Math library MarketMathCore
**for the swaps and liquidity provision logic and an OracleLib
library representing the built-in oracle for the LP tokens.
Permission Owner | functions | Criticality | Risk |
---|---|---|---|
No access control | - | - | - |
- Users can add liquidity via the
mint(address, SYAmount, PTAmount)
function. This function internally calls theaddLiquidity()
in theMarketMathCore
library to calculate the amount of LP tokens based onSYAmount
andPTAmount
. The tokens must be transferred to this contract before the LP tokens are minted. - To remove liquidity, users call the
burn(SYReceiver, PTReceiver, LPAmount)
function. This function calls theremoveLiquidity()
in theMarketMathCore
library to calculate the amount of SY and PT and transfers the tokens for theSYReceiver
andPTReceiver
, respectively. - Back-and-forth swaps can be made via the
swapExactPtForSy(receiver, PTAmountIn)
andswapSyForExactPt(receiver, PTAmountOut)
functions. - LP holders can collect rewards accrued via the
redeemRewards()
. The market contract implements the PendleGauge contract, which is responsible for distributing PENDLE tokens to the LP holders. - There is an
observe(secondsAgo[])
function that returns the cumulative implied rates at specified time intervals. These cumulative implied rates can be used to calculate the Time-Weighted Average Price (TWAP) implied rate over a given interval by dividing the difference between two cumulative rates by the time elapsed. - To observe the cumulative implied rates, the oracle needs to be initialized in the market by calling the
increaseObservationsCardinalityNext(cardinalityRequired)
function, passing thecardinalityRequired
(number of blocks) for the specified TWAP duration. - It is important to mention that the market is safeguarded against any kind of donation attacks, given its usage of internal accounting when dealing with the PT and SY tokens.
PendlePYLpOracle - Oracle
The Pendle Oracle contract provides the exchange rate between its tokens involved in the protocol, such as PT, YT, LP to SY, and PT, YT, and LP to the underlying asset based on the TWAP prices. It is an OZ Transparent Proxy that uses the PendlePYOracleLib
**and PendleLpOracleLib
to fetch rates from the PendleMarket contract and handle potential PT depeg scenarios and SY solvency.
Permission Owner | functions | Criticality | Risk |
---|---|---|---|
owner: Safe 2-of-4 | setBlockCycleNumerator | HIGH | ![]() |
- The rates are calculated by obtaining the exchange rate of the SY and PY tokens (
syIndex
andpyIndex
) by calling the internalgetSYandPYIndexCurrent(market)
function, which callsSY.exchangeRate()
forsyIndex
andYT.pyIndexStored()
forpyIndex
ensuring it is up to date otherwise it uses themax(SY.exchangeRate(), YT.pyIndexStored())
for handling the PT depeg. Then, it calls thegetPtToAssetRateRaw(market, duration)
that returns1
if the maturity has been reached or calls themarket.observe(duration)
to compute the PT rate from the cumulative implied rate atduration
. - (PT, YT, LP) <> Underlying Asset
- The functions below handle cases where
syIndex < pyIndex
(insolvency) by returning the TWAP exchange rate multiplied by the SY exchange rate. - The
getPtToAssetRate(market, duration)
function calculates the PT <> underlying asset exchange rate of themarket
over theduration
. For YT <> asset and LP <> asset, the exchange rate can be obtained by callinggetYtToSyRate()
andgetLpToSyRate()
, respectively.
- The functions below handle cases where
- (PT, YT, LP) <> Underlying Asset
- The function below adjusts the exchange rate for
syIndex
whensyIndex > pyIndex
or adjusts forpyIndex
, which handles insolvency cases. - The
getPtToSyRate(market, duration)
function calculates the PT <> SY exchange rate of themarket
over theduration
. For YT <> SY and LP <> SY, the exchange rate can be obtained by callinggetYtToSyRate()
andgetLpToSyRate()
, respectively.
- The function below adjusts the exchange rate for
Miscellaneous
Introducing Principal Tokens as collateral into Aave can bring benefits such as increased liquidity with this new category of fixed-rate assets, similar to bonds with maturity. However, it also introduces several complexities regarding post-expiration management and pricing correctly until maturity, which the DAO must evaluate and consider before any listing.
PTs are traded at a discount to the underlying asset and gradually move to 1:1 parity at the expiration date, where they stop appreciating, and they must be redeemed for the underlying asset. By listing a PT token on Aave, as soon as it reaches maturity, the asset can be frozen or simply stay as a wrapper of the underlying with a 1:1 price.
To automate the management of PTs, we suggest a steward contract that can freeze the market so as not to allow further deposits after they reach maturity.
Asset pricing
Pricing PT has been extensively discussed in the forum, with different alternatives presented by Chaos and Llama risk also participating in the conversation. For different reasons, each suggestion has its pros and cons, and overall, the community showed no comfort with such approaches.
As we commented on the pricing options thread, we think the pricing algorithm proposed by Chaos Labs is the most efficient. However, we respect the arguments and decision of the community to not proceed with it, and designed a slightly different approach: what we call the dynamic linear discount rate oracle.
As presented by Chaos Labs on the pricing thread, one of the pricing methodologies out there is based on a linear discount rate forrmula: this rate is configured when the asset is listed, and by calculating the remaining time to maturity, it applies that rate to discount the price from par (1:1). This approach is, even if not representing how the market of PTs behave (volatile enough during lifetime), tries to approximate it based of the principle that the further a PT is from maturity, the higher the discount (lower the price), and at maturity, it becomes 1:1, as the underlying is fully redeemable.
Our dynamic linear discount rate oracle follows the same principle, but the main difference is that the discount itself is configurable during the lifetime of the PT.
For example, maybe at the very beginning of the maturity, the risk modelling indicates that due to high volatility, a conservative approach is to have a 40% discount rate, linearly increasing the price as time passes at a 40% “rhythm”.
But after 1 month, maybe the volatility profile of the underlying changed heavily, or the expected returns on the YT have substancially decreased, which means that becomes natural to reduce the discount rate itself to let’s say 20%.
Going more to the technical details, the adapter oracle contract and the way it should be configured have some extra limitations:
- At a listing stage, the risk team should recommend a maximum discount rate. This is a conservative limit above which the dynamic discount configuration can’t go. The rationale is to protect the oracle layer itself from unexpected (big) discount rate changes.
- In parallel, a discount rate is configured at listing time, which reflects a slightly closer discount for exactly the current market conditions of the PT, considering also the risk parameters on Aave (LT, LB). This is the dynamic configuration that later on will be updated if required.
- Aside from the max protections, the dynamic change must be regulated via a risk steward, highly constrained to not allow change on the rate of discount more than X% %, every 2-3 days.
- Additionally, depending on the underlying, the feed uses under the hood CAPO with Chainlink, or plainly Chainlink.
This approach for pricing seems less prone to manipulation. However, we must highlight that the risk partners should evaluate and understand how the base discount should be configured. Further monitoring must be made so it can be adjusted along the PT maturity based on market implications to reduce possible price discrepancies on Aave compared to other markets.
The dynamic linear discount oracle can be found here, and has been reviewed by @Certora.
Procedure to follow when listing PT instances
It is important to highlight that this analysis is meant to provide a general understanding of the Principal Tokens asset class and not a specific asset analysis like we have been doing for any asset listed on Aave.
However, aside from the risk analysis side of each PT instance listing, it is possible to already comment on “light” technical aspects that should be verified on each PT instance listing:
- We should analyze the SY contract implementation and whether it inherits the logic from the base SY contract implementation, because some can use custom implementations reflecting unique aspects of the protocol/underlying yield-bearing token.
- Check that the PT was deployed by the YieldContractFactory, where the PT and YT codes from the factory are immutable and were well audited with no issues so far.
- The high-level permissions and access control of the related contracts, e.g., who the SY contract owner is.
- Same procedure we have been doing for LRTs and LSTs is to check whether the underlying assets are already present on Aave, which reflects the security that we already evaluated. If the underlying of the PT suggested is not yet on Aave, an extra analysis must be made of it, too.
- Like LRT/LSTs, the price feed coupled in the linear discount price adapter should be the same as its underlying asset on Aave, given that they are correlated assets.
- Evaluating how long the time to maturity is is a must for listing new PTs due to the duration risks that could quickly change the price of the PT, where if the expected yield of the yield-bearing token increases, the YT will be tradable at a higher price while decreasing the PT price significantly. Longer maturities are less predictable in terms of expected yields, which means that, for Aave, a “good” PT should likely have its yield side within a range that will not expose the protocol to extreme volatility in the PT price.
- Right now, we don’t see relevant cases for borrowing PT, as we expected that users would exchange their PTs for the referent underlying asset after maturity. Given its implications, any listing suggested as borrowable must be carefully evaluated, and if possible, avoided.
Conclusion
We think Principal Tokens doesn’t have any problem in terms of integration with Aave. However, some procedures addressed in the topics above must be followed before listing specific PT instances.
Additionally, we recommend on the governance side to have an ARFC for approving the pricing approach.