[ARFC] Onboard Pendle PT tokens to Aave V3 Core Instance

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 :green_circle: The role is controlled via Timelock or Multisig wallet, which we assume is safely protected.
YELLOW :yellow_circle: 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 :red_circle: 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 :green_circle:

*owner may vary between SY deployer, Pendle’s deployer or Governance

  • Users can deposit and mint SY tokens using the deposit(tokenIn, amount) function. Any tokenIn from the getTokensIn() function can be deposited. For example, aUSDC-SY can be minted using either aUSDC or USDC, while wstETH-SY can be minted using native ETH, WETH, stETH, and wstETH.
  • Users can redeem valid tokens using the redeem(tokenOut, amount) function. Any tokenOut from the getTokensOut() function can be withdrawn. For example, aUSDC-SY shares can be redeemed for aUSDC or USDC.
  • Users can use the previewDeposit(tokenIn, amount) and previewRedeem(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 the wstETH:stETH ratio.
  • Some SY implementations inherit the abstract SYBaseWithRewards contract, which allows the SY to distribute rewards to users, while the SYBase contract does not implement rewards.
  • For SY with rewards, users can check the rewards tokens by calling the getRewardTokens() function and claim them via the claimRewards() function.
  • The SY deployer is the contract’s owner and can pause and unpause 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 :green_circle:
  • 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, and setTreasury 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 :green_circle:
  • The mint and burn are performed via the mintByYT(address, amount) and burnByYT(address, amount) functions, which have the onlyYT 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. The mintPYMulti(PTReceiver[], YTReceiver, SYAmounts[]) can also be used to mint multiple PY tokens in a batch for multiple users. The PT tokens are sent to the PTReceiver, while the YT tokens are sent to the YTReceiver.
  • 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 inherited RewardManagerAbstract contract via the _updateAndDistributeRewards(user) function. Finally, it transfers the rewards if the flag redeemRewards is positive, and the same for the redeemInterest 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 the redeemInterestAndRewardsPostExpiryForTreasury().

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 the addLiquidity() in the MarketMathCore library to calculate the amount of LP tokens based on SYAmount and PTAmount. 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 the removeLiquidity() in the MarketMathCore library to calculate the amount of SY and PT and transfers the tokens for the SYReceiver and PTReceiver, respectively.
  • Back-and-forth swaps can be made via the swapExactPtForSy(receiver, PTAmountIn) and swapSyForExactPt(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 the cardinalityRequired (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 :green_circle:
  • The rates are calculated by obtaining the exchange rate of the SY and PY tokens (syIndex and pyIndex) by calling the internal getSYandPYIndexCurrent(market) function, which calls SY.exchangeRate() for syIndex and YT.pyIndexStored() for pyIndex ensuring it is up to date otherwise it uses the max(SY.exchangeRate(), YT.pyIndexStored()) for handling the PT depeg. Then, it calls the getPtToAssetRateRaw(market, duration) that returns 1 if the maturity has been reached or calls the market.observe(duration) to compute the PT rate from the cumulative implied rate at duration.
  • (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 the market over the duration. For YT <> asset and LP <> asset, the exchange rate can be obtained by calling getYtToSyRate() and getLpToSyRate(), respectively.
  • (PT, YT, LP) <> Underlying Asset
    • The function below adjusts the exchange rate for syIndex when syIndex > pyIndex or adjusts for pyIndex, which handles insolvency cases.
    • The getPtToSyRate(market, duration) function calculates the PT <> SY exchange rate of the market over the duration. For YT <> SY and LP <> SY, the exchange rate can be obtained by calling getYtToSyRate() and getLpToSyRate(), respectively.


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.

4 Likes