diff --git a/contracts/interfaces/IPendlePtOracle.sol b/contracts/interfaces/IPendlePtOracle.sol index 17ba1ee0..eb1bf60b 100644 --- a/contracts/interfaces/IPendlePtOracle.sol +++ b/contracts/interfaces/IPendlePtOracle.sol @@ -3,6 +3,9 @@ pragma solidity 0.8.25; interface IPendlePtOracle { function getPtToAssetRate(address market, uint32 duration) external view returns (uint256); + + function getPtToSyRate(address market, uint32 duration) external view returns (uint256); + function getOracleState( address market, uint32 duration diff --git a/contracts/oracles/PendleOracle.sol b/contracts/oracles/PendleOracle.sol index afd78e42..0b0f5604 100644 --- a/contracts/oracles/PendleOracle.sol +++ b/contracts/oracles/PendleOracle.sol @@ -11,10 +11,28 @@ import { ensureNonzeroAddress, ensureNonzeroValue } from "@venusprotocol/solidit * @notice This oracle fetches the price of a pendle token */ contract PendleOracle is CorrelatedTokenOracle { + /// @notice Which asset to use as a base for the returned PT + /// price. Can be either a standardized yield token (SY), in + /// this case PT/SY price is returned, or the underlying + /// asset directly. Note that using PT_TO_ASSET rate assumes + /// that the yield token can be seamlessly redeemed for the + /// underlying asset. In reality, this might not always be + /// the case. For more details, see + /// https://docs.pendle.finance/Developers/Contracts/StandardizedYield + enum RateKind { + PT_TO_ASSET, + PT_TO_SY + } + /// @notice Address of the PT oracle /// @custom:oz-upgrades-unsafe-allow state-variable-immutable IPendlePtOracle public immutable PT_ORACLE; + /// @notice Whether to use PT/SY (standardized yield token) rate + /// or PT/underlying asset rate + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable + RateKind public immutable RATE_KIND; + /// @notice Address of the market /// @custom:oz-upgrades-unsafe-allow state-variable-immutable address public immutable MARKET; @@ -31,6 +49,7 @@ contract PendleOracle is CorrelatedTokenOracle { constructor( address market, address ptOracle, + RateKind rateKind, address ptToken, address underlyingToken, address resilientOracle, @@ -42,6 +61,7 @@ contract PendleOracle is CorrelatedTokenOracle { MARKET = market; PT_ORACLE = IPendlePtOracle(ptOracle); + RATE_KIND = rateKind; TWAP_DURATION = twapDuration; (bool increaseCardinalityRequired, , bool oldestObservationSatisfied) = PT_ORACLE.getOracleState( @@ -54,10 +74,13 @@ contract PendleOracle is CorrelatedTokenOracle { } /** - * @notice Fetches the amount of underlying token for 1 pendle token - * @return amount The amount of underlying token for pendle token + * @notice Fetches the amount of underlying or SY token for 1 pendle token + * @return amount The amount of underlying or SY token for pendle token */ function _getUnderlyingAmount() internal view override returns (uint256) { + if (RATE_KIND == RateKind.PT_TO_SY) { + return PT_ORACLE.getPtToSyRate(MARKET, TWAP_DURATION); + } return PT_ORACLE.getPtToAssetRate(MARKET, TWAP_DURATION); } } diff --git a/contracts/oracles/mocks/MockPendlePtOracle.sol b/contracts/oracles/mocks/MockPendlePtOracle.sol index d25d5049..1ae75bf0 100644 --- a/contracts/oracles/mocks/MockPendlePtOracle.sol +++ b/contracts/oracles/mocks/MockPendlePtOracle.sol @@ -6,6 +6,7 @@ import "@openzeppelin/contracts/access/Ownable.sol"; contract MockPendlePtOracle is IPendlePtOracle, Ownable { mapping(address => mapping(uint32 => uint256)) public ptToAssetRate; + mapping(address => mapping(uint32 => uint256)) public ptToSyRate; constructor() Ownable() {} @@ -17,12 +18,20 @@ contract MockPendlePtOracle is IPendlePtOracle, Ownable { return ptToAssetRate[market][duration]; } + function setPtToSyRate(address market, uint32 duration, uint256 rate) external onlyOwner { + ptToSyRate[market][duration] = rate; + } + + function getPtToSyRate(address market, uint32 duration) external view returns (uint256) { + return ptToSyRate[market][duration]; + } + function getOracleState( - address market, - uint32 duration + address /* market */, + uint32 /* duration */ ) external - view + pure returns (bool increaseCardinalityRequired, uint16 cardinalityRequired, bool oldestObservationSatisfied) { return (false, 0, true); diff --git a/deploy/7-deploy-pendle-oracle.ts b/deploy/7-deploy-pendle-oracle.ts index 3741c314..0e2141a9 100644 --- a/deploy/7-deploy-pendle-oracle.ts +++ b/deploy/7-deploy-pendle-oracle.ts @@ -4,6 +4,11 @@ import { HardhatRuntimeEnvironment } from "hardhat/types"; import { ADDRESSES } from "../helpers/deploymentConfig"; +enum PendleRateKind { + PT_TO_ASSET = 0, + PT_TO_SY = 1, +} + const func: DeployFunction = async ({ getNamedAccounts, deployments, @@ -32,6 +37,7 @@ const func: DeployFunction = async ({ args: [ PTweETH_26DEC2024_Market || "0x0000000000000000000000000000000000000001", ptOracleAddress, + PendleRateKind.PT_TO_ASSET, PTweETH_26DEC2024, WETH, oracle.address, diff --git a/test/PendleOracle.ts b/test/PendleOracle.ts index 98c88bd3..4d1b85f1 100644 --- a/test/PendleOracle.ts +++ b/test/PendleOracle.ts @@ -1,10 +1,15 @@ -import { smock } from "@defi-wonderland/smock"; +import { FakeContract, smock } from "@defi-wonderland/smock"; import chai from "chai"; import { parseUnits } from "ethers/lib/utils"; import { ethers } from "hardhat"; import { ADDRESSES } from "../helpers/deploymentConfig"; -import { BEP20Harness, IPendlePtOracle, ResilientOracleInterface } from "../typechain-types"; +import { + BEP20Harness, + IPendlePtOracle, + PendleOracle__factory, + ResilientOracleInterface, +} from "../typechain-types"; import { addr0000 } from "./utils/data"; const { expect } = chai; @@ -12,16 +17,21 @@ chai.use(smock.matchers); const { PTweETH_26DEC2024, PTweETH_26DEC2024_Market, PTOracle, eETH } = ADDRESSES.ethereum; const eETH_PRICE = parseUnits("3400", 18); -const PRICE_DENOMINATOR = parseUnits("1", 18); -const EETH_AMOUNT_FOR_ONE_WEETH = parseUnits("0.923601422168630818", 18); +const PT_TO_ASSET_RATE = parseUnits("0.923601422168630818", 18); +const PT_TO_SY_RATE = parseUnits("0.93", 18); const DURATION = 3600; // 1 hour +enum PendleRateKind { + PT_TO_ASSET, + PT_TO_SY, +} + describe("PendleOracle unit tests", () => { - let ptWeETHMock; - let resilientOracleMock; - let pendleOracle; - let pendleOracleFactory; - let ptOracleMock; + let ptWeETHMock: FakeContract; + let resilientOracleMock: FakeContract; + let pendleOracleFactory: PendleOracle__factory; + let ptOracleMock: FakeContract; + before(async () => { // To initialize the provider we need to hit the node with any request await ethers.getSigners(); @@ -32,7 +42,8 @@ describe("PendleOracle unit tests", () => { ptWeETHMock.decimals.returns(18); ptOracleMock = await smock.fake("IPendlePtOracle", { address: PTOracle }); - ptOracleMock.getPtToAssetRate.returns(EETH_AMOUNT_FOR_ONE_WEETH); + ptOracleMock.getPtToAssetRate.returns(PT_TO_ASSET_RATE); + ptOracleMock.getPtToSyRate.returns(PT_TO_SY_RATE); ptOracleMock.getOracleState.returns([false, 0, true]); pendleOracleFactory = await ethers.getContractFactory("PendleOracle"); @@ -44,6 +55,7 @@ describe("PendleOracle unit tests", () => { pendleOracleFactory.deploy( addr0000, ptOracleMock.address, + PendleRateKind.PT_TO_ASSET, ptWeETHMock.address, eETH, resilientOracleMock.address, @@ -56,6 +68,7 @@ describe("PendleOracle unit tests", () => { pendleOracleFactory.deploy( PTweETH_26DEC2024_Market, addr0000, + PendleRateKind.PT_TO_ASSET, ptWeETHMock.address, eETH, resilientOracleMock.address, @@ -68,6 +81,7 @@ describe("PendleOracle unit tests", () => { pendleOracleFactory.deploy( PTweETH_26DEC2024_Market, ptOracleMock.address, + PendleRateKind.PT_TO_ASSET, addr0000, eETH, resilientOracleMock.address, @@ -80,6 +94,7 @@ describe("PendleOracle unit tests", () => { pendleOracleFactory.deploy( PTweETH_26DEC2024_Market, ptOracleMock.address, + PendleRateKind.PT_TO_ASSET, ptWeETHMock.address, addr0000, resilientOracleMock.address, @@ -92,6 +107,7 @@ describe("PendleOracle unit tests", () => { pendleOracleFactory.deploy( PTweETH_26DEC2024_Market, ptOracleMock.address, + PendleRateKind.PT_TO_ASSET, ptWeETHMock.address, eETH, addr0000, @@ -104,6 +120,7 @@ describe("PendleOracle unit tests", () => { pendleOracleFactory.deploy( PTweETH_26DEC2024_Market, ptOracleMock.address, + PendleRateKind.PT_TO_ASSET, ptWeETHMock.address, eETH, resilientOracleMock.address, @@ -119,6 +136,7 @@ describe("PendleOracle unit tests", () => { pendleOracleFactory.deploy( PTweETH_26DEC2024_Market, ptOracleMock.address, + PendleRateKind.PT_TO_ASSET, ptWeETHMock.address, eETH, resilientOracleMock.address, @@ -128,26 +146,34 @@ describe("PendleOracle unit tests", () => { ptOracleMock.getOracleState.returns([false, 0, true]); }); + }); - it("should deploy contract", async () => { - pendleOracle = await pendleOracleFactory.deploy( + describe("getPrice", () => { + const deploy = (kind: PendleRateKind) => { + return pendleOracleFactory.deploy( PTweETH_26DEC2024_Market, ptOracleMock.address, + kind, ptWeETHMock.address, eETH, resilientOracleMock.address, DURATION, ); - }); - }); + }; - describe("getPrice", () => { it("revert if wstETH address is wrong", async () => { + const pendleOracle = await deploy(PendleRateKind.PT_TO_ASSET); await expect(pendleOracle.getPrice(addr0000)).to.be.revertedWithCustomError(pendleOracle, "InvalidTokenAddress"); }); - it("should get correct price", async () => { + it("should get correct price for PT_TO_ASSET rate kind", async () => { + const pendleOracle = await deploy(PendleRateKind.PT_TO_ASSET); expect(await pendleOracle.getPrice(ptWeETHMock.address)).to.equal(parseUnits("3140.2448353733447812", 18)); }); + + it("should get correct price for PT_TO_SY rate kind", async () => { + const pendleOracle = await deploy(PendleRateKind.PT_TO_SY); + expect(await pendleOracle.getPrice(ptWeETHMock.address)).to.equal(parseUnits("3162.0", 18)); + }); }); });