Skip to content
Merged
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
96 changes: 96 additions & 0 deletions packages/periphery/contracts/oracles/MarginlyCompositeOracle.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity 0.8.19;

import '@openzeppelin/contracts/access/Ownable2Step.sol';
import '@openzeppelin/contracts/utils/math/Math.sol';
import '@marginly/contracts/contracts/interfaces/IPriceOracle.sol';
import './CompositeOracle.sol';

///@dev Composition of two IPriceOracles.
/// Should be used very carefully:
/// 1) not all implementations of IPriceOracle could be combined
/// 2) both IPriceOracle implementations must have the same time settings: secondsAgo, secondsAgoLiquidation
contract MarginlyCompositeOracle is IPriceOracle, Ownable2Step {
error ZeroPrice();
error ZeroAddress();
error NotInitialized();
error PairAlreadyExists();

uint256 private constant X96ONE = 79228162514264337593543950336;

struct OracleParams {
address intermediateToken;
IPriceOracle quoteIntermediateOracle;
IPriceOracle interMediateBaseOracle;
}

mapping(address => mapping(address => OracleParams)) public getParams;

function _validatePriceOracle(IPriceOracle priceOracle, address quoteToken, address baseToken) private view {
if (priceOracle.getBalancePrice(quoteToken, baseToken) == 0) revert ZeroPrice();
if (priceOracle.getBalancePrice(baseToken, quoteToken) == 0) revert ZeroPrice();

if (priceOracle.getMargincallPrice(quoteToken, baseToken) == 0) revert ZeroPrice();
if (priceOracle.getMargincallPrice(baseToken, quoteToken) == 0) revert ZeroPrice();
}

function _getOracleParamsSafe(
address quoteToken,
address baseToken
) private view returns (OracleParams memory params) {
params = getParams[quoteToken][baseToken];
if (params.intermediateToken == address(0)) revert NotInitialized();
}

function setPair(
address quoteToken,
address intermediateToken,
address baseToken,
IPriceOracle quoteIntermediateOracle,
IPriceOracle interMediateBaseOracle
) external onlyOwner {
if (quoteToken == address(0)) revert ZeroAddress();
if (intermediateToken == address(0)) revert ZeroAddress();
if (baseToken == address(0)) revert ZeroAddress();
if (address(quoteIntermediateOracle) == address(0)) revert ZeroAddress();
if (address(interMediateBaseOracle) == address(0)) revert ZeroAddress();

OracleParams memory params = getParams[quoteToken][baseToken];
if (params.intermediateToken != address(0)) revert PairAlreadyExists();

_validatePriceOracle(quoteIntermediateOracle, quoteToken, intermediateToken);
_validatePriceOracle(interMediateBaseOracle, intermediateToken, baseToken);

getParams[quoteToken][baseToken] = OracleParams({
intermediateToken: intermediateToken,
quoteIntermediateOracle: quoteIntermediateOracle,
interMediateBaseOracle: interMediateBaseOracle
});

getParams[baseToken][quoteToken] = OracleParams({
intermediateToken: intermediateToken,
quoteIntermediateOracle: interMediateBaseOracle,
interMediateBaseOracle: quoteIntermediateOracle
});
}

function getBalancePrice(address quoteToken, address baseToken) external view override returns (uint256) {
OracleParams memory params = _getOracleParamsSafe(quoteToken, baseToken);

uint256 firstPrice = params.quoteIntermediateOracle.getBalancePrice(quoteToken, params.intermediateToken);
uint256 secondPrice = params.interMediateBaseOracle.getBalancePrice(params.intermediateToken, baseToken);
if (firstPrice == 0 || secondPrice == 0) revert ZeroPrice();

return Math.mulDiv(firstPrice, secondPrice, X96ONE);
}

function getMargincallPrice(address quoteToken, address baseToken) external view override returns (uint256) {
OracleParams memory params = _getOracleParamsSafe(quoteToken, baseToken);

uint256 firstPrice = params.quoteIntermediateOracle.getMargincallPrice(quoteToken, params.intermediateToken);
uint256 secondPrice = params.interMediateBaseOracle.getMargincallPrice(params.intermediateToken, baseToken);
if (firstPrice == 0 || secondPrice == 0) revert ZeroPrice();

return Math.mulDiv(firstPrice, secondPrice, X96ONE);
}
}
154 changes: 154 additions & 0 deletions packages/periphery/contracts/oracles/PendleMarketOracle.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity 0.8.19;

import '@openzeppelin/contracts/access/Ownable2Step.sol';
import '@openzeppelin/contracts/utils/math/Math.sol';
import '@pendle/core-v2/contracts/oracles/PendlePtLpOracle.sol';
import '@pendle/core-v2/contracts/core/Market/v3/PendleMarketV3.sol';
import '@marginly/contracts/contracts/interfaces/IPriceOracle.sol';

/// @dev Oracle to get price from Pendle market Pt to Ib token
contract PendleMarketOracle is IPriceOracle, Ownable2Step {
struct OracleParams {
address pendleMarket;
address ibToken;
uint16 secondsAgo;
uint16 secondsAgoLiquidation;
uint8 ptSyDecimalsDelta;
}

uint256 private constant X96ONE = 2 ** 96;
uint8 private constant PRICE_DECIMALS = 18;

IPPtLpOracle public immutable pendle;
mapping(address => mapping(address => OracleParams)) public getParams;

error ZeroPrice();
error ZeroAddress();
error WrongValue();
error WrongIbSyDecimals();
error WrongPtAddress();
error WrongIbTokenAddress();
error PairAlreadyExist();
error UnknownPair();
error PendlePtLpOracleIsNotInitialized(uint16);

constructor(address _pendle) {
if (_pendle == address(0)) revert ZeroAddress();
pendle = IPPtLpOracle(_pendle);
}

/// @notice Create token pair oracle price params. Can be called only once per pair.
/// @param quoteToken address of IbToken or PtToken
/// @param baseToken address of PtToken or IbToken
/// @param pendleMarket Address of PendleMarket contract with PtToken and IbToken
/// @param secondsAgo Number of seconds in the past from which to calculate the time-weighted means
/// @param secondsAgoLiquidation Same as `secondsAgo`, but for liquidation case
function setPair(
address quoteToken,
address baseToken,
address pendleMarket,
uint16 secondsAgo,
uint16 secondsAgoLiquidation
) external onlyOwner {
if (secondsAgo == 0 || secondsAgoLiquidation == 0) revert WrongValue();
if (secondsAgo < secondsAgoLiquidation) revert WrongValue();
if (quoteToken == address(0) || baseToken == address(0) || pendleMarket == address(0)) {
revert ZeroAddress();
}

if (getParams[quoteToken][baseToken].pendleMarket != address(0)) revert PairAlreadyExist();

_assertOracleIsInitialized(pendleMarket, secondsAgo);

(IStandardizedYield sy, IPPrincipalToken pt, ) = PendleMarketV3(pendleMarket).readTokens();
address ibToken;
if (baseToken == address(pt)) {
ibToken = quoteToken;
} else if (quoteToken == address(pt)) {
ibToken = baseToken;
} else {
revert WrongPtAddress();
}

if (!sy.isValidTokenIn(ibToken) || !sy.isValidTokenOut(ibToken)) revert WrongIbTokenAddress();

uint8 ptDecimals = IERC20Metadata(baseToken).decimals();
uint8 syDecimals = IERC20Metadata(address(sy)).decimals();
uint8 ibDecimals = IERC20Metadata(ibToken).decimals();

//We assume that sy ib ratio is 1:1 and decimals for both tokens are equals
if (syDecimals != ibDecimals) revert WrongIbSyDecimals();

OracleParams memory oracleParams = OracleParams({
pendleMarket: pendleMarket,
ibToken: ibToken,
secondsAgo: secondsAgo,
secondsAgoLiquidation: secondsAgoLiquidation,
ptSyDecimalsDelta: PRICE_DECIMALS + ptDecimals - syDecimals
});

getParams[quoteToken][baseToken] = oracleParams;
getParams[baseToken][quoteToken] = oracleParams;
}
Comment on lines +41 to +93
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Ensure oracle is initialized for both normal and liquidation scenarios.
Currently, _assertOracleIsInitialized is only called with secondsAgo. Consider doing the same check for secondsAgoLiquidation to confirm the oracle is adequately initialized for liquidation as well.

Apply this diff after line 62 to verify the oracle’s readiness for liquidation durations:

     _assertOracleIsInitialized(pendleMarket, secondsAgo);
+    _assertOracleIsInitialized(pendleMarket, secondsAgoLiquidation);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// @notice Create token pair oracle price params. Can be called only once per pair.
/// @param quoteToken address of IbToken or PtToken
/// @param baseToken address of PtToken or IbToken
/// @param pendleMarket Address of PendleMarket contract with PtToken and IbToken
/// @param secondsAgo Number of seconds in the past from which to calculate the time-weighted means
/// @param secondsAgoLiquidation Same as `secondsAgo`, but for liquidation case
function setPair(
address quoteToken,
address baseToken,
address pendleMarket,
uint16 secondsAgo,
uint16 secondsAgoLiquidation
) external onlyOwner {
if (secondsAgo == 0 || secondsAgoLiquidation == 0) revert WrongValue();
if (secondsAgo < secondsAgoLiquidation) revert WrongValue();
if (quoteToken == address(0) || baseToken == address(0) || pendleMarket == address(0)) {
revert ZeroAddress();
}
if (getParams[quoteToken][baseToken].pendleMarket != address(0)) revert PairAlreadyExist();
_assertOracleIsInitialized(pendleMarket, secondsAgo);
(IStandardizedYield sy, IPPrincipalToken pt, ) = PendleMarketV3(pendleMarket).readTokens();
address ibToken;
if (baseToken == address(pt)) {
ibToken = quoteToken;
} else if (quoteToken == address(pt)) {
ibToken = baseToken;
} else {
revert WrongPtAddress();
}
if (!sy.isValidTokenIn(ibToken) || !sy.isValidTokenOut(ibToken)) revert WrongIbTokenAddress();
uint8 ptDecimals = IERC20Metadata(baseToken).decimals();
uint8 syDecimals = IERC20Metadata(address(sy)).decimals();
uint8 ibDecimals = IERC20Metadata(ibToken).decimals();
//We assume that sy ib ratio is 1:1 and decimals for both tokens are equals
if (syDecimals != ibDecimals) revert WrongIbSyDecimals();
OracleParams memory oracleParams = OracleParams({
pendleMarket: pendleMarket,
ibToken: ibToken,
secondsAgo: secondsAgo,
secondsAgoLiquidation: secondsAgoLiquidation,
ptSyDecimalsDelta: PRICE_DECIMALS + ptDecimals - syDecimals
});
getParams[quoteToken][baseToken] = oracleParams;
getParams[baseToken][quoteToken] = oracleParams;
}
/// @notice Create token pair oracle price params. Can be called only once per pair.
/// @param quoteToken address of IbToken or PtToken
/// @param baseToken address of PtToken or IbToken
/// @param pendleMarket Address of PendleMarket contract with PtToken and IbToken
/// @param secondsAgo Number of seconds in the past from which to calculate the time-weighted means
/// @param secondsAgoLiquidation Same as `secondsAgo`, but for liquidation case
function setPair(
address quoteToken,
address baseToken,
address pendleMarket,
uint16 secondsAgo,
uint16 secondsAgoLiquidation
) external onlyOwner {
if (secondsAgo == 0 || secondsAgoLiquidation == 0) revert WrongValue();
if (secondsAgo < secondsAgoLiquidation) revert WrongValue();
if (quoteToken == address(0) || baseToken == address(0) || pendleMarket == address(0)) {
revert ZeroAddress();
}
if (getParams[quoteToken][baseToken].pendleMarket != address(0)) revert PairAlreadyExist();
_assertOracleIsInitialized(pendleMarket, secondsAgo);
_assertOracleIsInitialized(pendleMarket, secondsAgoLiquidation);
(IStandardizedYield sy, IPPrincipalToken pt, ) = PendleMarketV3(pendleMarket).readTokens();
address ibToken;
if (baseToken == address(pt)) {
ibToken = quoteToken;
} else if (quoteToken == address(pt)) {
ibToken = baseToken;
} else {
revert WrongPtAddress();
}
if (!sy.isValidTokenIn(ibToken) || !sy.isValidTokenOut(ibToken)) revert WrongIbTokenAddress();
uint8 ptDecimals = IERC20Metadata(baseToken).decimals();
uint8 syDecimals = IERC20Metadata(address(sy)).decimals();
uint8 ibDecimals = IERC20Metadata(ibToken).decimals();
//We assume that sy ib ratio is 1:1 and decimals for both tokens are equals
if (syDecimals != ibDecimals) revert WrongIbSyDecimals();
OracleParams memory oracleParams = OracleParams({
pendleMarket: pendleMarket,
ibToken: ibToken,
secondsAgo: secondsAgo,
secondsAgoLiquidation: secondsAgoLiquidation,
ptSyDecimalsDelta: PRICE_DECIMALS + ptDecimals - syDecimals
});
getParams[quoteToken][baseToken] = oracleParams;
getParams[baseToken][quoteToken] = oracleParams;
}


/// @notice Update `secondsAgo` and `secondsAgoLiquidation` for token pair
/// @param quoteToken Quote token address, IbToken e.g. ezETH
/// @param baseToken PT token e.g. PT-ezETH-27JUN2024
/// @param secondsAgo Number of seconds in the past from which to calculate the time-weighted means
/// @param secondsAgoLiquidation Same as `secondsAgo`, but for liquidation case
function updateTwapDuration(
address quoteToken,
address baseToken,
uint16 secondsAgo,
uint16 secondsAgoLiquidation
) external onlyOwner {
if (secondsAgoLiquidation == 0) revert WrongValue();
if (secondsAgo < secondsAgoLiquidation) revert WrongValue();

OracleParams memory oracleParams = getParams[quoteToken][baseToken];
if (oracleParams.pendleMarket == address(0)) revert UnknownPair();

oracleParams.secondsAgo = secondsAgo;
oracleParams.secondsAgoLiquidation = secondsAgoLiquidation;

getParams[quoteToken][baseToken] = oracleParams;
getParams[baseToken][quoteToken] = oracleParams;
}

/// @notice Check Pendle oracle is initialized - https://docs.pendle.finance/Developers/Integration/HowToIntegratePtAndLpOracle#third-initialize-the-oracle
function _assertOracleIsInitialized(address pendleMarket, uint16 secondsAgo) private view {
(bool increaseCardinalityRequired, , bool oldestObservationSatisfied) = pendle.getOracleState(
pendleMarket,
secondsAgo
);
if (increaseCardinalityRequired) revert PendlePtLpOracleIsNotInitialized(secondsAgo);
if (!oldestObservationSatisfied) revert PendlePtLpOracleIsNotInitialized(secondsAgo);
}

function getBalancePrice(address quoteToken, address baseToken) external view returns (uint256) {
return _getPriceX96(quoteToken, baseToken, false);
}

function getMargincallPrice(address quoteToken, address baseToken) external view returns (uint256) {
return _getPriceX96(quoteToken, baseToken, true);
}

function _getPriceX96(
address quoteToken,
address baseToken,
bool isMargincallPrice
) private view returns (uint256 priceX96) {
OracleParams storage poolParams = getParams[quoteToken][baseToken];
if (poolParams.pendleMarket == address(0)) revert UnknownPair();

uint256 pendlePrice = pendle.getPtToSyRate(
poolParams.pendleMarket,
isMargincallPrice ? poolParams.secondsAgoLiquidation : poolParams.secondsAgo
);

priceX96 = poolParams.ibToken == quoteToken
? Math.mulDiv(pendlePrice, X96ONE, 10 ** poolParams.ptSyDecimalsDelta)
: Math.mulDiv(X96ONE, 10 ** poolParams.ptSyDecimalsDelta, pendlePrice);
}
}
28 changes: 28 additions & 0 deletions packages/periphery/contracts/test/MockPriceOracleV2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.19;

import '@marginly/contracts/contracts/interfaces/IPriceOracle.sol';
import '@openzeppelin/contracts/utils/math/Math.sol';

contract MockPriceOracleV2 is IPriceOracle {
uint256 private constant X96ONE = 79228162514264337593543950336;

mapping(address => mapping(address => uint256)) public balancePrices;
mapping(address => mapping(address => uint256)) public mcPrices;

function setPrice(address quoteToken, address baseToken, uint256 balancePriceX96, uint256 mcPriceX96) public {
balancePrices[quoteToken][baseToken] = balancePriceX96;
balancePrices[baseToken][quoteToken] = Math.mulDiv(X96ONE, X96ONE, balancePriceX96);

mcPrices[quoteToken][baseToken] = mcPriceX96;
mcPrices[baseToken][quoteToken] = Math.mulDiv(X96ONE, X96ONE, mcPriceX96);
}

function getBalancePrice(address quoteToken, address baseToken) external view returns (uint256) {
return balancePrices[quoteToken][baseToken];
}

function getMargincallPrice(address quoteToken, address baseToken) external view returns (uint256) {
return mcPrices[quoteToken][baseToken];
}
}
18 changes: 18 additions & 0 deletions packages/periphery/hardhat-configs/arb-fork.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import '@nomicfoundation/hardhat-toolbox';
import 'solidity-docgen';
import * as defaultConfig from './hardhat.config';

const config = {
...defaultConfig.default,
networks: {
hardhat: {
forking: {
enabled: true,
url: 'https://rpc.ankr.com/arbitrum',
blockNumber: 221775851,
},
},
},
};

export default config;
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ const config = {
hardhat: {
forking: {
enabled: true,
url: 'https://arb1.arbitrum.io/rpc',
blockNumber: 203690137,
url: 'https://rpc.ankr.com/eth',
blockNumber: 21814800,
},
},
},
Expand Down
3 changes: 2 additions & 1 deletion packages/periphery/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"compile": "hardhat compile --config ./hardhat-configs/hardhat.config.ts",
"test": "UPDATE_SNAPSHOT=1 REPORT_GAS=true hardhat test ./test/*.spec.ts --config ./hardhat-configs/hardhat.config.ts",
"test:coverage": "UPDATE_SNAPSHOT=1 REPORT_GAS=true hardhat coverage --testfiles './test/*.spec.ts'",
"test:int": "REPORT_GAS=true hardhat test --config ./hardhat-configs/fork.config.ts ./test/int/**/*.spec.ts",
"test:int-arb": "REPORT_GAS=true hardhat test --config ./hardhat-configs/arb-fork.config.ts ./test/int/arb/**/*.spec.ts",
"test:int-eth": "REPORT_GAS=true hardhat test --config ./hardhat-configs/eth-fork.config.ts ./test/int/eth/*.spec.ts",
"lint:write": "prettier --write ./contracts ./test",
"docgen": "hardhat docgen"
},
Expand Down
Loading