Skip to content

Commit cf5fb09

Browse files
authored
Merge pull request #51 from Gearbox-protocol/pendle-pt-price-feed
feat: add Pendle PT price feed
2 parents 989fedf + efa28d3 commit cf5fb09

File tree

6 files changed

+185
-1
lines changed

6 files changed

+185
-1
lines changed

.github/workflows/release.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ on:
44
push:
55
branches:
66
- "main"
7+
- "pendle-pt-price-feed"
78

89
env:
910
HUSKY: 0

.releaserc.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22
"branches": [
33
{
44
"name": "main"
5+
},
6+
{
7+
"name": "pendle-pt-price-feed",
8+
"channel": "pendle",
9+
"prerelease": "pendle"
510
}
611
],
712
"plugins": [
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// SPDX-License-Identifier: MIT
2+
// Gearbox Protocol. Generalized leverage for DeFi protocols
3+
// (c) Gearbox Foundation, 2024.
4+
pragma solidity ^0.8.17;
5+
6+
interface IPendleMarket {
7+
function observe(uint32[] calldata secondsAgos) external view returns (uint216[] memory);
8+
9+
function expiry() external view returns (uint256);
10+
11+
function readTokens() external view returns (address, address, address);
12+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// SPDX-License-Identifier: MIT
2+
// Gearbox Protocol. Generalized leverage for DeFi protocols
3+
// (c) Gearbox Foundation, 2024.
4+
pragma solidity ^0.8.17;
5+
6+
interface IPendleYT {
7+
function doCacheIndexSameBlock() external view returns (bool);
8+
9+
function pyIndexLastUpdatedBlock() external view returns (uint256);
10+
11+
function pyIndexStored() external view returns (uint256);
12+
}
13+
14+
interface IPendleSY {
15+
function exchangeRate() external view returns (uint256);
16+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
// SPDX-License-Identifier: BUSL-1.1
2+
// Gearbox Protocol. Generalized leverage for DeFi protocols
3+
// (c) Gearbox Foundation, 2024.
4+
pragma solidity ^0.8.17;
5+
6+
import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
7+
8+
import {WAD, SECONDS_PER_YEAR} from "@gearbox-protocol/core-v2/contracts/libraries/Constants.sol";
9+
import {PriceFeedType} from "@gearbox-protocol/sdk-gov/contracts/PriceFeedType.sol";
10+
11+
import {IPriceFeed} from "@gearbox-protocol/core-v2/contracts/interfaces/IPriceFeed.sol";
12+
import {IPendleMarket} from "../../interfaces/pendle/IPendleMarket.sol";
13+
import {IPendleYT, IPendleSY} from "../../interfaces/pendle/IPendleTokens.sol";
14+
import {PriceFeedValidationTrait} from "@gearbox-protocol/core-v3/contracts/traits/PriceFeedValidationTrait.sol";
15+
import {SanityCheckTrait} from "@gearbox-protocol/core-v3/contracts/traits/SanityCheckTrait.sol";
16+
import {PriceFeedParams} from "../PriceFeedParams.sol";
17+
import {FixedPoint} from "../../libraries/FixedPoint.sol";
18+
import {LogExpMath} from "../../libraries/LogExpMath.sol";
19+
20+
/// @title Pendle PT price feed based on Pendle market TWAPs
21+
/// @notice The PT price is derived from the Pendle market's ln(impliedRate) TWAP:
22+
/// 1) The average implied rate is computed as (ln(IR)_(now) - ln(IR)_(now - timeWindow)) / timeWindow;
23+
/// 2) The PT to asset rate is computed as 1 / (e ^ (ln(IR)_avg * timeToExpiry / secondsPerYear));
24+
/// 3) The PT price is ptToAssetRate * assetPrice;
25+
contract PendleTWAPPTPriceFeed is IPriceFeed, PriceFeedValidationTrait, SanityCheckTrait {
26+
uint256 public constant override version = 3_00;
27+
PriceFeedType public constant override priceFeedType = PriceFeedType.PENDLE_PT_TWAP_ORACLE;
28+
uint8 public constant override decimals = 8;
29+
string public description;
30+
31+
/// @notice Indicates whether the consuming PriceOracle can skip the sanity checks. Set to `true`
32+
/// since this price feed performs sanity checks locally
33+
bool public constant override skipPriceCheck = true;
34+
35+
/// @notice Address of the pendle market where the PT is traded
36+
address public immutable market;
37+
38+
/// @notice Address of the Pendle SY connected to the PT
39+
address public immutable sy;
40+
41+
/// @notice Address of the Pendle YT connected to the YT
42+
address public immutable yt;
43+
44+
/// @notice Timestamp of the market (and PT) expiry
45+
uint256 public immutable expiry;
46+
47+
/// @notice Underlying price feed
48+
address public immutable priceFeed;
49+
uint32 public immutable stalenessPeriod;
50+
bool public immutable skipCheck;
51+
52+
/// @notice The size of the TWAP observation window
53+
uint32 public immutable twapWindow;
54+
55+
constructor(address _market, address _priceFeed, uint32 _stalenessPeriod, uint32 _twapWindow) {
56+
market = _market;
57+
expiry = IPendleMarket(_market).expiry();
58+
priceFeed = _priceFeed;
59+
stalenessPeriod = _stalenessPeriod;
60+
skipCheck = _validatePriceFeed(priceFeed, stalenessPeriod);
61+
twapWindow = _twapWindow;
62+
63+
address pt;
64+
65+
(sy, pt, yt) = IPendleMarket(_market).readTokens();
66+
67+
string memory ptName = IERC20Metadata(pt).name();
68+
69+
description = string(abi.encodePacked(ptName, " Pendle Market TWAP * ", IPriceFeed(priceFeed).description()));
70+
}
71+
72+
/// @dev Gets the ln(impliedRate) from the market TWAP
73+
function _getMarketLnImpliedRate() internal view returns (uint256) {
74+
uint32[] memory secondAgos = new uint32[](2);
75+
secondAgos[1] = twapWindow;
76+
77+
uint216[] memory cumulativeLIR = IPendleMarket(market).observe(secondAgos);
78+
79+
return (cumulativeLIR[0] - cumulativeLIR[1]) / twapWindow;
80+
}
81+
82+
/// @dev Computes the PT to asset rate from the market implied rate TWAP
83+
function _getPTToAssetRate() internal view returns (uint256) {
84+
uint256 assetToPTRate =
85+
uint256(LogExpMath.exp(int256(_getMarketLnImpliedRate() * (expiry - block.timestamp) / SECONDS_PER_YEAR)));
86+
87+
return FixedPoint.divDown(WAD, assetToPTRate);
88+
}
89+
90+
/// @dev Retrieves the current SY and YT indices
91+
function _getSYandPYIndex() internal view returns (uint256 syIndex, uint256 pyIndex) {
92+
syIndex = IPendleSY(sy).exchangeRate();
93+
uint256 pyIndexStored = IPendleYT(yt).pyIndexStored();
94+
95+
if (IPendleYT(yt).doCacheIndexSameBlock() && IPendleYT(yt).pyIndexLastUpdatedBlock() == block.number) {
96+
pyIndex = pyIndexStored;
97+
} else {
98+
pyIndex = syIndex >= pyIndexStored ? syIndex : pyIndexStored;
99+
}
100+
}
101+
102+
/// @notice Returns the USD price of the PT token with 8 decimals
103+
function latestRoundData() external view override returns (uint80, int256, uint256, uint256, uint80) {
104+
int256 answer = _getValidatedPrice(priceFeed, stalenessPeriod, skipCheck);
105+
106+
if (expiry > block.timestamp) {
107+
answer = int256(FixedPoint.mulDown(uint256(answer), _getPTToAssetRate()));
108+
}
109+
110+
(uint256 syIndex, uint256 pyIndex) = _getSYandPYIndex();
111+
112+
if (syIndex < pyIndex) {
113+
answer = int256(uint256(answer) * syIndex / pyIndex);
114+
}
115+
116+
return (0, answer, 0, 0, 0);
117+
}
118+
}

contracts/test/suites/PriceFeedDeployer.sol

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ import {
2323
GenericLPPriceFeedData,
2424
TheSamePriceFeedData,
2525
BalancerLPPriceFeedData,
26-
RedStonePriceFeedData
26+
RedStonePriceFeedData,
27+
PendlePriceFeedData
2728
} from "@gearbox-protocol/sdk-gov/contracts/PriceFeedDataLive.sol";
2829
import {PriceFeedConfig} from "@gearbox-protocol/core-v3/contracts/test/interfaces/ICreditConfig.sol";
2930
import {PriceOracleV3} from "@gearbox-protocol/core-v3/contracts/core/PriceOracleV3.sol";
@@ -49,6 +50,7 @@ import {CompositePriceFeed} from "../../oracles/CompositePriceFeed.sol";
4950
import {PriceFeedParams} from "../../oracles/PriceFeedParams.sol";
5051
import {ZeroPriceFeed} from "../../oracles/ZeroPriceFeed.sol";
5152
import {MellowLRTPriceFeed} from "../../oracles/mellow/MellowLRTPriceFeed.sol";
53+
import {PendleTWAPPTPriceFeed} from "../../oracles/pendle/PendleTWAPPTPriceFeed.sol";
5254

5355
import {IWAToken} from "../../interfaces/aave/IWAToken.sol";
5456
import {IBalancerStablePool} from "../../interfaces/balancer/IBalancerStablePool.sol";
@@ -683,6 +685,36 @@ contract PriceFeedDeployer is Test, PriceFeedDataLive {
683685
}
684686
}
685687

688+
// PENDLE PT PRICE FEEDS
689+
PendlePriceFeedData[] memory pendlePTPriceFeeds = pendlePriceFeedsByNetwork[chainId];
690+
len = pendlePTPriceFeeds.length;
691+
unchecked {
692+
for (uint256 i; i < len; ++i) {
693+
Tokens t = pendlePTPriceFeeds[i].token;
694+
address token = tokenTestSuite.addressOf(t);
695+
696+
if (token == address(0)) {
697+
continue;
698+
}
699+
700+
address underlying = tokenTestSuite.addressOf(pendlePTPriceFeeds[i].underlying);
701+
702+
address pf = address(
703+
new PendleTWAPPTPriceFeed(
704+
pendlePTPriceFeeds[i].market,
705+
priceFeeds[underlying],
706+
stalenessPeriods[underlying],
707+
pendlePTPriceFeeds[i].twapWindow
708+
)
709+
);
710+
711+
setPriceFeed(token, pf, pendlePTPriceFeeds[i].trusted, pendlePTPriceFeeds[i].reserve);
712+
713+
string memory description = string(abi.encodePacked("PRICEFEED_", tokenTestSuite.symbols(t)));
714+
vm.label(pf, description);
715+
}
716+
}
717+
686718
priceFeedConfigLength = priceFeedConfig.length;
687719
}
688720

0 commit comments

Comments
 (0)