From 8e734b74f931be5833c2cf2d3ad8b1d64f26285d Mon Sep 17 00:00:00 2001 From: Narayan Prusty Date: Fri, 1 Mar 2024 17:10:34 +0530 Subject: [PATCH] fix: added PendleOracle --- contracts/interfaces/IPendlePtOracle.sol | 6 ++ contracts/oracles/PendleOracle.sol | 52 +++++++++ helpers/deploymentConfig.ts | 2 +- test/PendleOracle.ts | 129 +++++++++++++++++++++++ 4 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 contracts/interfaces/IPendlePtOracle.sol create mode 100644 contracts/oracles/PendleOracle.sol create mode 100644 test/PendleOracle.ts diff --git a/contracts/interfaces/IPendlePtOracle.sol b/contracts/interfaces/IPendlePtOracle.sol new file mode 100644 index 00000000..16297600 --- /dev/null +++ b/contracts/interfaces/IPendlePtOracle.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: BSD-3-Clause +pragma solidity 0.8.13; + +interface IPendlePtOracle { + function getPtToAssetRate(address market, uint32 duration) external view returns (uint256); +} diff --git a/contracts/oracles/PendleOracle.sol b/contracts/oracles/PendleOracle.sol new file mode 100644 index 00000000..9fe2f2e4 --- /dev/null +++ b/contracts/oracles/PendleOracle.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: BSD-3-Clause +pragma solidity 0.8.13; + +import { IPendlePtOracle } from "../interfaces/IPendlePtOracle.sol"; +import { LiquidStakedTokenOracle } from "./common/LiquidStakedTokenOracle.sol"; +import { ensureNonzeroAddress, ensureNonzeroValue } from "@venusprotocol/solidity-utilities/contracts/validators.sol"; + +/** + * @title PendleOracle + * @author Venus + * @notice This oracle fetches the price of an pendle token + */ +contract PendleOracle is LiquidStakedTokenOracle { + /// @notice Address of the PT oracle + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable + IPendlePtOracle public immutable PT_ORACLE; + + /// @notice Address of the market + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable + address public immutable MARKET; + + /// @notice Twap duration for the oracle + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable + uint32 public immutable TWAP_DURATION; + + /// @notice Constructor for the implementation contract. + /// @custom:oz-upgrades-unsafe-allow constructor + constructor( + address market, + address ptOracle, + address ptToken, + address underlyingToken, + address resilientOracle, + uint32 twapDuration + ) LiquidStakedTokenOracle(ptToken, underlyingToken, resilientOracle) { + ensureNonzeroAddress(market); + ensureNonzeroAddress(ptOracle); + ensureNonzeroValue(twapDuration); + + MARKET = market; + PT_ORACLE = IPendlePtOracle(ptOracle); + TWAP_DURATION = twapDuration; + } + + /** + * @notice Fetches the amount of FRAX for 1 sFrax + * @return amount The amount of FRAX for sFrax + */ + function getUnderlyingAmount() internal view override returns (uint256) { + return PT_ORACLE.getPtToAssetRate(MARKET, TWAP_DURATION); + } +} diff --git a/helpers/deploymentConfig.ts b/helpers/deploymentConfig.ts index f09b655c..e2cd642a 100644 --- a/helpers/deploymentConfig.ts +++ b/helpers/deploymentConfig.ts @@ -118,7 +118,7 @@ export const ADDRESSES: PreconfiguredAddresses = { frxETH: "0x5e8422345238f34275888049021821e8e08caa1f", weETH: "0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee", eETH: "0x35fA164735182de50811E8e2E824cFb9B6118ac2", - PTeETH: "0xc69Ad9baB1dEE23F4605a82b3354F8E40d1E5966", + PTweETH: "0xc69Ad9baB1dEE23F4605a82b3354F8E40d1E5966", PTweETHMarket: "0xF32e58F92e60f4b0A37A69b95d642A471365EAe8", PTOracle: "0xbbd487268A295531d299c125F3e5f749884A3e30", }, diff --git a/test/PendleOracle.ts b/test/PendleOracle.ts new file mode 100644 index 00000000..1efc0907 --- /dev/null +++ b/test/PendleOracle.ts @@ -0,0 +1,129 @@ +import { 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 { addr0000 } from "./utils/data"; + +const { expect } = chai; +chai.use(smock.matchers); + +const { PTweETH, PTweETHMarket, 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 DURATION = 3600; // 1 hour + +describe("WstETHOracle unit tests", () => { + let ptWeETHMock; + let resilientOracleMock; + let pendleOracle; + let pendleOracleFactory; + let ptOracleMock; + before(async () => { + // To initialize the provider we need to hit the node with any request + await ethers.getSigners(); + resilientOracleMock = await smock.fake("ResilientOracleInterface"); + resilientOracleMock.getPrice.returns(eETH_PRICE); + + ptWeETHMock = await smock.fake("BEP20Harness", { address: PTweETH }); + ptWeETHMock.decimals.returns(18); + + ptOracleMock = await smock.fake("IPendlePtOracle", { address: PTOracle }); + ptOracleMock.getPtToAssetRate.returns(EETH_AMOUNT_FOR_ONE_WEETH); + + pendleOracleFactory = await ethers.getContractFactory("PendleOracle"); + }); + + describe("deployment", () => { + it("revert if market address is 0", async () => { + await expect( + pendleOracleFactory.deploy( + addr0000, + ptOracleMock.address, + ptWeETHMock.address, + eETH, + resilientOracleMock.address, + DURATION, + ), + ).to.be.reverted; + }); + it("revert if ptOracle address is 0", async () => { + await expect( + pendleOracleFactory.deploy( + PTweETHMarket, + addr0000, + ptWeETHMock.address, + eETH, + resilientOracleMock.address, + DURATION, + ), + ).to.be.reverted; + }); + it("revert if ptWeETH address is 0", async () => { + await expect( + pendleOracleFactory.deploy( + PTweETHMarket, + ptOracleMock.address, + addr0000, + eETH, + resilientOracleMock.address, + DURATION, + ), + ).to.be.reverted; + }); + it("revert if eETH address is 0", async () => { + await expect( + pendleOracleFactory.deploy( + PTweETHMarket, + ptOracleMock.address, + ptWeETHMock.address, + addr0000, + resilientOracleMock.address, + DURATION, + ), + ).to.be.reverted; + }); + it("revert if ResilientOracle address is 0", async () => { + await expect( + pendleOracleFactory.deploy(PTweETHMarket, ptOracleMock.address, ptWeETHMock.address, eETH, addr0000, DURATION), + ).to.be.reverted; + }); + it("revert if TWAP duration is 0", async () => { + await expect( + pendleOracleFactory.deploy( + PTweETHMarket, + ptOracleMock.address, + ptWeETHMock.address, + eETH, + resilientOracleMock.address, + 0, + ), + ).to.be.reverted; + }); + + it("should deploy contract", async () => { + pendleOracle = await pendleOracleFactory.deploy( + PTweETHMarket, + ptOracleMock.address, + ptWeETHMock.address, + eETH, + resilientOracleMock.address, + DURATION, + ); + }); + }); + + describe("getPrice", () => { + it("revert if wstETH address is wrong", async () => { + await expect(pendleOracle.getPrice(addr0000)).to.be.revertedWith("wrong token address"); + }); + + // it("should get correct price", async () => { + // const expectedPrice = STETH_USD_PRICE.mul(STETH_AMOUNT_FOR_ONE_WSTETH).div(PRICE_DENOMINATOR); + // expect(await wstETHOracle.getPrice(WSTETH)).to.equal(expectedPrice); + // }); + }); +});