Skip to content

Commit

Permalink
feat: add support for pendle's getPtToSyRate
Browse files Browse the repository at this point in the history
  • Loading branch information
kkirka committed Dec 11, 2024
1 parent 42da4ba commit 97d3797
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 21 deletions.
3 changes: 3 additions & 0 deletions contracts/interfaces/IPendlePtOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 25 additions & 2 deletions contracts/oracles/PendleOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -31,6 +49,7 @@ contract PendleOracle is CorrelatedTokenOracle {
constructor(
address market,
address ptOracle,
RateKind rateKind,
address ptToken,
address underlyingToken,
address resilientOracle,
Expand All @@ -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(
Expand All @@ -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);
}
}
15 changes: 12 additions & 3 deletions contracts/oracles/mocks/MockPendlePtOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,32 @@ 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() {}

function setPtToAssetRate(address market, uint32 duration, uint256 rate) external onlyOwner {
ptToAssetRate[market][duration] = rate;
}

function setPtToSyRate(address market, uint32 duration, uint256 rate) external onlyOwner {
ptToSyRate[market][duration] = rate;
}

function getPtToAssetRate(address market, uint32 duration) external view returns (uint256) {
return ptToAssetRate[market][duration];
}

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);
Expand Down
6 changes: 6 additions & 0 deletions deploy/7-deploy-pendle-oracle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import { HardhatRuntimeEnvironment } from "hardhat/types";

import { ADDRESSES } from "../helpers/deploymentConfig";

enum PendleRateKind {

Check failure on line 7 in deploy/7-deploy-pendle-oracle.ts

View workflow job for this annotation

GitHub Actions / Compile / Lint / Build

'PendleRateKind' is already declared in the upper scope on line 7 column 6
PT_TO_ASSET = 0,
PT_TO_SY = 1,
}

const func: DeployFunction = async ({
getNamedAccounts,
deployments,
Expand Down Expand Up @@ -32,6 +37,7 @@ const func: DeployFunction = async ({
args: [
PTweETH_26DEC2024_Market || "0x0000000000000000000000000000000000000001",
ptOracleAddress,
PendleRateKind.PT_TO_ASSET,
PTweETH_26DEC2024,
WETH,
oracle.address,
Expand Down
58 changes: 42 additions & 16 deletions test/PendleOracle.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,37 @@
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;
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 {

Check failure on line 24 in test/PendleOracle.ts

View workflow job for this annotation

GitHub Actions / Compile / Lint / Build

'PendleRateKind' is already declared in the upper scope on line 24 column 6
PT_TO_ASSET,
PT_TO_SY,
}

describe("PendleOracle unit tests", () => {
let ptWeETHMock;
let resilientOracleMock;
let pendleOracle;
let pendleOracleFactory;
let ptOracleMock;
let ptWeETHMock: FakeContract<BEP20Harness>;
let resilientOracleMock: FakeContract<ResilientOracleInterface>;
let pendleOracleFactory: PendleOracle__factory;
let ptOracleMock: FakeContract<IPendlePtOracle>;

before(async () => {
// To initialize the provider we need to hit the node with any request
await ethers.getSigners();
Expand All @@ -32,7 +42,8 @@ describe("PendleOracle unit tests", () => {
ptWeETHMock.decimals.returns(18);

ptOracleMock = await smock.fake<IPendlePtOracle>("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");
Expand All @@ -44,6 +55,7 @@ describe("PendleOracle unit tests", () => {
pendleOracleFactory.deploy(
addr0000,
ptOracleMock.address,
PendleRateKind.PT_TO_ASSET,
ptWeETHMock.address,
eETH,
resilientOracleMock.address,
Expand All @@ -56,6 +68,7 @@ describe("PendleOracle unit tests", () => {
pendleOracleFactory.deploy(
PTweETH_26DEC2024_Market,
addr0000,
PendleRateKind.PT_TO_ASSET,
ptWeETHMock.address,
eETH,
resilientOracleMock.address,
Expand All @@ -68,6 +81,7 @@ describe("PendleOracle unit tests", () => {
pendleOracleFactory.deploy(
PTweETH_26DEC2024_Market,
ptOracleMock.address,
PendleRateKind.PT_TO_ASSET,
addr0000,
eETH,
resilientOracleMock.address,
Expand All @@ -80,6 +94,7 @@ describe("PendleOracle unit tests", () => {
pendleOracleFactory.deploy(
PTweETH_26DEC2024_Market,
ptOracleMock.address,
PendleRateKind.PT_TO_ASSET,
ptWeETHMock.address,
addr0000,
resilientOracleMock.address,
Expand All @@ -92,6 +107,7 @@ describe("PendleOracle unit tests", () => {
pendleOracleFactory.deploy(
PTweETH_26DEC2024_Market,
ptOracleMock.address,
PendleRateKind.PT_TO_ASSET,
ptWeETHMock.address,
eETH,
addr0000,
Expand All @@ -104,6 +120,7 @@ describe("PendleOracle unit tests", () => {
pendleOracleFactory.deploy(
PTweETH_26DEC2024_Market,
ptOracleMock.address,
PendleRateKind.PT_TO_ASSET,
ptWeETHMock.address,
eETH,
resilientOracleMock.address,
Expand All @@ -119,6 +136,7 @@ describe("PendleOracle unit tests", () => {
pendleOracleFactory.deploy(
PTweETH_26DEC2024_Market,
ptOracleMock.address,
PendleRateKind.PT_TO_ASSET,
ptWeETHMock.address,
eETH,
resilientOracleMock.address,
Expand All @@ -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) => {

Check failure on line 152 in test/PendleOracle.ts

View workflow job for this annotation

GitHub Actions / Compile / Lint / Build

Unexpected block statement surrounding arrow body; move the returned value immediately after the `=>`
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));
});
});
});

0 comments on commit 97d3797

Please sign in to comment.