Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/pendle-core-v2-public
2 changes: 1 addition & 1 deletion src/adapter/pendle/PendleOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {IPMarket} from "@pendle/core-v2/interfaces/IPMarket.sol";
import {IPPrincipalToken} from "@pendle/core-v2/interfaces/IPPrincipalToken.sol";
import {IPPYLpOracle} from "@pendle/core-v2/interfaces/IPPYLpOracle.sol";
import {IStandardizedYield} from "@pendle/core-v2/interfaces/IStandardizedYield.sol";
import {PendlePYOracleLib} from "@pendle/core-v2/oracles/PendlePYOracleLib.sol";
import {PendlePYOracleLib} from "@pendle/core-v2/oracles/PtYtLpOracle/PendlePYOracleLib.sol";
import {BaseAdapter, Errors, IPriceOracle} from "../BaseAdapter.sol";
import {ScaleUtils, Scale} from "../../lib/ScaleUtils.sol";

Expand Down
151 changes: 151 additions & 0 deletions src/adapter/pendle/PendleUnifiedOracle.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.0;

import {IPMarket} from "@pendle/core-v2/interfaces/IPMarket.sol";
import {IPPrincipalToken} from "@pendle/core-v2/interfaces/IPPrincipalToken.sol";
import {IPPYLpOracle} from "@pendle/core-v2/interfaces/IPPYLpOracle.sol";
import {IStandardizedYield} from "@pendle/core-v2/interfaces/IStandardizedYield.sol";
import {PendlePYOracleLib} from "@pendle/core-v2/oracles/PtYtLpOracle/PendlePYOracleLib.sol";
import {PendleLpOracleLib} from "@pendle/core-v2/oracles/PtYtLpOracle/PendleLpOracleLib.sol";
import {BaseAdapter, Errors, IPriceOracle} from "../BaseAdapter.sol";
import {ScaleUtils, Scale} from "../../lib/ScaleUtils.sol";
import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";

/// @title PendleUnifiedOracle
/// @custom:security-contact security@euler.xyz
/// @author Euler Labs (https://www.eulerlabs.com/)
/// @notice Adapter for Pendle PT and LP Oracle.
contract PendleUnifiedOracle is BaseAdapter, Ownable2Step {
/// @inheritdoc IPriceOracle
string public constant name = "PendleUnifiedOracle";
/// @dev The minimum length of the TWAP window.
uint32 internal constant MIN_TWAP_WINDOW = 5 minutes;
/// @dev The maximum length of the TWAP window.
uint32 internal constant MAX_TWAP_WINDOW = 60 minutes;
/// @notice The decimals of the Pendle Oracle. Fixed to 18.
uint8 internal constant FEED_DECIMALS = 18;

struct PairParams {
/// @notice The address of the Pendle market.
address pendleMarket;
/// @notice The desired length of the twap window.
uint32 twapWindow;
/// @notice The flag indicating the direction of the price. False when base/quote, true - quote/base
bool inverse;
/// @notice The PendlePYOracleLib function to call.
function (IPMarket, uint32) view returns (uint256) getRate;
/// @notice The scale factors used for decimal conversions.
Scale scale;
}

mapping(address => mapping(address => PairParams)) private _configuredPairs;

address public immutable pendleOracle;

event PairAdded(address indexed pendleMarket, address indexed base, address indexed quote, uint32 twapWindow);

constructor(address _pendleOracle) {
if (_pendleOracle == address(0)) {
revert Errors.ZeroAddress();
}

pendleOracle = _pendleOracle;
}

/// @dev The oracle can price Pendle PT,LP to SY,Asset. Whether to use SY or Asset depends on the underlying.
/// Consult https://docs.pendle.finance/Developers/Contracts/StandardizedYield#standard-sys for more information.
/// Before deploying this adapter ensure that the oracle is initialized and the observation buffer is filled.
/// Note that this adapter allows specifing any `quote` as the underlying asset.
/// @param _pendleMarket The address of the Pendle market.
/// @param _base The address of the PT or LP token.
/// @param _quote The address of the SY token or the underlying asset.
/// @param _twapWindow The desired length of the twap window.
function addPair(address _pendleMarket, address _base, address _quote, uint32 _twapWindow) external onlyOwner {
//Verify that the pair is not already initialized.
if (_configuredPairs[_base][_quote].pendleMarket != address(0)) {
revert Errors.PriceOracle_AlreadyInitialized();
}

// Verify that the TWAP window is sufficiently long.
if (_twapWindow < MIN_TWAP_WINDOW || _twapWindow > MAX_TWAP_WINDOW) {
revert Errors.PriceOracle_InvalidConfiguration();
}

// Verify that the observations buffer is adequately sized and populated.
(bool increaseCardinalityRequired,, bool oldestObservationSatisfied) =
IPPYLpOracle(pendleOracle).getOracleState(_pendleMarket, _twapWindow);
if (increaseCardinalityRequired || !oldestObservationSatisfied) {
revert Errors.PriceOracle_InvalidConfiguration();
}

(IStandardizedYield sy, IPPrincipalToken pt,) = IPMarket(_pendleMarket).readTokens();
(, address asset,) = sy.assetInfo();

PairParams memory pairParams;

if (_base == address(pt)) {
if (_quote == address(sy)) {
pairParams.getRate = PendlePYOracleLib.getPtToSyRate;
} else if (asset == _quote) {
// Pendle do not recommend to use this type of price
// https://docs.pendle.finance/Developers/Oracles/HowToIntegratePtAndLpOracle
pairParams.getRate = PendlePYOracleLib.getPtToAssetRate;
} else {
revert Errors.PriceOracle_InvalidConfiguration();
}
} else if (_base == _pendleMarket) {
if (_quote == address(sy)) {
pairParams.getRate = PendleLpOracleLib.getLpToSyRate;
} else if (asset == _quote) {
pairParams.getRate = PendleLpOracleLib.getLpToAssetRate;
} else {
revert Errors.PriceOracle_InvalidConfiguration();
}
} else {
revert Errors.PriceOracle_InvalidConfiguration();
}

pairParams.pendleMarket = _pendleMarket;
pairParams.twapWindow = _twapWindow;
pairParams.inverse = false;

// We don't need to worry about decimals base and quote decimals scaling,
// Pendle formula to access LP (rawX) in SY (rawY)
// rawY= rawX × lpToSyRate / 10^18
//
// https://docs.pendle.finance/Developers/Oracles/HowToIntegratePtAndLpOracle
pairParams.scale = ScaleUtils.calcScale(0, 0, FEED_DECIMALS);

_configuredPairs[_base][_quote] = pairParams;

pairParams.inverse = true;
_configuredPairs[_quote][_base] = pairParams;

emit PairAdded(_pendleMarket, _base, _quote, _twapWindow);
}

/// @notice Get a quote by calling the Pendle oracle.
/// @param inAmount The amount of `base` to convert.
/// @param _base The token that is being priced.
/// @param _quote The token that is the unit of account.
/// @dev Note that the quote does not include instantaneous DEX slippage.
/// @return The converted amount using the Pendle oracle.
function _getQuote(uint256 inAmount, address _base, address _quote) internal view override returns (uint256) {
PairParams memory pairParams = _configuredPairs[_base][_quote];
if (pairParams.pendleMarket == address(0)) {
revert Errors.PriceOracle_InvalidConfiguration();
}

uint256 unitPrice = pairParams.getRate(IPMarket(pairParams.pendleMarket), pairParams.twapWindow);
return ScaleUtils.calcOutAmount(inAmount, unitPrice, pairParams.scale, pairParams.inverse);
}

function getConfiguredPair(address _base, address _quote)
external
view
returns (address pendleMarket, uint32 twapWindow, bool inverse, Scale scale)
{
PairParams memory pairParams = _configuredPairs[_base][_quote];
return (pairParams.pendleMarket, pairParams.twapWindow, pairParams.inverse, pairParams.scale);
}
}
26 changes: 18 additions & 8 deletions src/adapter/pendle/PendleUniversalOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import {IPMarket} from "@pendle/core-v2/interfaces/IPMarket.sol";
import {IPPrincipalToken} from "@pendle/core-v2/interfaces/IPPrincipalToken.sol";
import {IPPYLpOracle} from "@pendle/core-v2/interfaces/IPPYLpOracle.sol";
import {IStandardizedYield} from "@pendle/core-v2/interfaces/IStandardizedYield.sol";
import {PendlePYOracleLib} from "@pendle/core-v2/oracles/PendlePYOracleLib.sol";
import {PendleLpOracleLib} from "@pendle/core-v2/oracles/PendleLpOracleLib.sol";
import {PendlePYOracleLib} from "@pendle/core-v2/oracles/PtYtLpOracle/PendlePYOracleLib.sol";
import {PendleLpOracleLib} from "@pendle/core-v2/oracles/PtYtLpOracle/PendleLpOracleLib.sol";
import {BaseAdapter, Errors, IPriceOracle} from "../BaseAdapter.sol";
import {ScaleUtils, Scale} from "../../lib/ScaleUtils.sol";

Expand Down Expand Up @@ -60,19 +60,25 @@ contract PendleUniversalOracle is BaseAdapter {
}

(IStandardizedYield sy, IPPrincipalToken pt,) = IPMarket(_pendleMarket).readTokens();
(, address asset,) = sy.assetInfo();

// Note: we allow using any asset pricing to any token.
if (_base == address(pt)) {
if (_quote == address(sy)) {
getRate = PendlePYOracleLib.getPtToSyRate;
} else {
} else if (asset == _quote) {
// Pendle do not recommend to use this type of price
// https://docs.pendle.finance/Developers/Oracles/HowToIntegratePtAndLpOracle
getRate = PendlePYOracleLib.getPtToAssetRate;
} else {
revert Errors.PriceOracle_InvalidConfiguration();
}
} else if (_base == _pendleMarket) {
if (_quote == address(sy)) {
getRate = PendleLpOracleLib.getLpToSyRate;
} else {
} else if (asset == _quote) {
getRate = PendleLpOracleLib.getLpToAssetRate;
} else {
revert Errors.PriceOracle_InvalidConfiguration();
}
} else {
revert Errors.PriceOracle_InvalidConfiguration();
Expand All @@ -82,9 +88,13 @@ contract PendleUniversalOracle is BaseAdapter {
base = _base;
quote = _quote;
twapWindow = _twapWindow;
uint8 baseDecimals = _getDecimals(base);
uint8 quoteDecimals = _getDecimals(quote);
scale = ScaleUtils.calcScale(baseDecimals, quoteDecimals, FEED_DECIMALS);

// We don't need to worry about decimals base and quote decimals scaling,
// Pendle formula to access LP (rawX) in SY (rawY)
// rawY= rawX × lpToSyRate / 10^18
//
// https://docs.pendle.finance/Developers/Oracles/HowToIntegratePtAndLpOracle
scale = ScaleUtils.calcScale(0, 0, FEED_DECIMALS);
}

/// @notice Get a quote by calling the Pendle oracle.
Expand Down
3 changes: 3 additions & 0 deletions src/lib/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pragma solidity ^0.8.0;
/// @author Euler Labs (https://www.eulerlabs.com/)
/// @notice Collects common errors in PriceOracles.
library Errors {
error ZeroAddress();
/// @notice The external feed returned an invalid answer.
error PriceOracle_InvalidAnswer();
/// @notice The configuration parameters for the PriceOracle are invalid.
Expand All @@ -22,4 +23,6 @@ library Errors {
error PriceOracle_TooStale(uint256 staleness, uint256 maxStaleness);
/// @notice The method can only be called by the governor.
error Governance_CallerNotGovernor();
/// @notice The price oracle has already been initialized.
error PriceOracle_AlreadyInitialized();
}
4 changes: 4 additions & 0 deletions test/adapter/pendle/PendleAddresses.sol
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,7 @@ address constant PENDLE_CORN_UNIBTC_1224_SY = 0xAE754a3B4553EA2EA4794d0171a56Ac1
address constant PENDLE_CORN_SOLVBTCBBN_1224_MARKET = 0xEb4d3057738b9Ed930F451Be473C1CCC42988384;
address constant PENDLE_CORN_SOLVBTCBBN_1224_PT = 0x23e479ddcda990E8523494895759bD98cD2fDBF6;
address constant PENDLE_CORN_SOLVBTCBBN_1224_SY = 0xEC30E55B51D9518cfcf5e870BCF89c73F5708f72;

address constant PENDLE_ETHENA_SUSDE_0925_MARKET = 0xA36b60A14A1A5247912584768C6e53E1a269a9F7;
address constant PENDLE_ETHENA_SUSDE_0925_PT = 0x9F56094C450763769BA0EA9Fe2876070c0fD5F77;
address constant PENDLE_ETHENA_SUSDE_0925_SY = 0xC01cde799245a25e6EabC550b36A47F6F83cc0f1;
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.0;

import {
PENDLE_ORACLE,
PENDLE_ETHENA_SUSDE_0925_MARKET,
PENDLE_ETHENA_SUSDE_0925_PT,
PENDLE_ETHENA_SUSDE_0925_SY
} from "test/adapter/pendle/PendleAddresses.sol";
import {EETH, EBTC, USDC, USDE, WBTC} from "test/utils/EthereumAddresses.sol";
import {ForkTest} from "test/utils/ForkTest.sol";
import {PendleUnifiedOracle} from "src/adapter/pendle/PendleUnifiedOracle.sol";
import {Errors} from "src/lib/Errors.sol";

contract PendleUnifiedOracleForkTest is ForkTest {
PendleUnifiedOracle oracle;
/// @dev 1%
uint256 constant REL_PRECISION = 0.01e18;

function setUp() public {
_setUpFork(23238500);
oracle = new PendleUnifiedOracle(PENDLE_ORACLE);
}

/// @dev This market is active. 1 PT-sUSDe0925 = 0.8314 SY-sUSDe. Oracle has no slippage.
function test_GetQuote_ActiveMarket_sUSDe0925_PT_SY() public {
oracle.addPair(
PENDLE_ETHENA_SUSDE_0925_MARKET, PENDLE_ETHENA_SUSDE_0925_PT, PENDLE_ETHENA_SUSDE_0925_SY, 15 minutes
);

uint256 outAmount = oracle.getQuote(1e18, PENDLE_ETHENA_SUSDE_0925_PT, PENDLE_ETHENA_SUSDE_0925_SY);
uint256 outAmount1000 = oracle.getQuote(1000e18, PENDLE_ETHENA_SUSDE_0925_PT, PENDLE_ETHENA_SUSDE_0925_SY);
assertApproxEqRel(outAmount, 0.8314e18, REL_PRECISION);
assertEq(outAmount1000, outAmount * 1000);

uint256 outAmountInv = oracle.getQuote(outAmount, PENDLE_ETHENA_SUSDE_0925_SY, PENDLE_ETHENA_SUSDE_0925_PT);
assertEq(outAmountInv, 1e18);
uint256 outAmountInv1000 =
oracle.getQuote(outAmount1000, PENDLE_ETHENA_SUSDE_0925_SY, PENDLE_ETHENA_SUSDE_0925_PT);
assertEq(outAmountInv1000, 1000e18);
}

/// @dev This market is active. 1 PT-sUSDe0925 = 0.9911 USDe.
function test_GetQuote_ActiveMarket_sUSDe0925_PT_Asset() public {
oracle.addPair(PENDLE_ETHENA_SUSDE_0925_MARKET, PENDLE_ETHENA_SUSDE_0925_PT, USDE, 15 minutes);

uint256 outAmount = oracle.getQuote(1e18, PENDLE_ETHENA_SUSDE_0925_PT, USDE);
uint256 outAmount1000 = oracle.getQuote(1000e18, PENDLE_ETHENA_SUSDE_0925_PT, USDE);
assertApproxEqRel(outAmount, 0.9911e18, REL_PRECISION);
assertEq(outAmount1000, outAmount * 1000);

uint256 outAmountInv = oracle.getQuote(outAmount, USDE, PENDLE_ETHENA_SUSDE_0925_PT);
assertEq(outAmountInv, 1e18);
uint256 outAmountInv1000 = oracle.getQuote(outAmount1000, USDE, PENDLE_ETHENA_SUSDE_0925_PT);
assertEq(outAmountInv1000, 1000e18);
}

/// @dev This market is active. 1 LP-sUSDe0925 = 2.78426 USDe
function test_GetQuote_ActiveMarket_sUSDe0925_LP_Asset() public {
oracle.addPair(PENDLE_ETHENA_SUSDE_0925_MARKET, PENDLE_ETHENA_SUSDE_0925_MARKET, USDE, 15 minutes);

uint256 outAmount = oracle.getQuote(1e18, PENDLE_ETHENA_SUSDE_0925_MARKET, USDE);
uint256 outAmount1000 = oracle.getQuote(1000e18, PENDLE_ETHENA_SUSDE_0925_MARKET, USDE);
assertApproxEqRel(outAmount, 2.78426e18, REL_PRECISION);
assertEq(outAmount1000, outAmount * 1000);

uint256 outAmountInv = oracle.getQuote(outAmount, USDE, PENDLE_ETHENA_SUSDE_0925_MARKET);
assertEq(outAmountInv, 1e18);
uint256 outAmountInv1000 = oracle.getQuote(outAmount1000, USDE, PENDLE_ETHENA_SUSDE_0925_MARKET);
assertEq(outAmountInv1000, 1000e18);
}

/// @dev This market is active. 1 LP-sUSDe0925 = 2.3353 SY-sUSDe. Oracle has no slippage.
function test_GetQuote_ActiveMarket_sUSDe0925_LP_SY() public {
oracle.addPair(
PENDLE_ETHENA_SUSDE_0925_MARKET, PENDLE_ETHENA_SUSDE_0925_MARKET, PENDLE_ETHENA_SUSDE_0925_SY, 15 minutes
);

uint256 outAmount = oracle.getQuote(1e18, PENDLE_ETHENA_SUSDE_0925_MARKET, PENDLE_ETHENA_SUSDE_0925_SY);
uint256 outAmount1000 = oracle.getQuote(1000e18, PENDLE_ETHENA_SUSDE_0925_MARKET, PENDLE_ETHENA_SUSDE_0925_SY);
assertApproxEqRel(outAmount, 2.3353e18, REL_PRECISION);
assertEq(outAmount1000, outAmount * 1000);

uint256 outAmountInv = oracle.getQuote(outAmount, PENDLE_ETHENA_SUSDE_0925_SY, PENDLE_ETHENA_SUSDE_0925_MARKET);
assertEq(outAmountInv, 1e18);
uint256 outAmountInv1000 =
oracle.getQuote(outAmount1000, PENDLE_ETHENA_SUSDE_0925_SY, PENDLE_ETHENA_SUSDE_0925_MARKET);
assertEq(outAmountInv1000, 1000e18);
}
}