-
Notifications
You must be signed in to change notification settings - Fork 11
added composite oracle #187
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
b70806e
added composite oracle
rudewalt 59592a1
fix: unit test, prepare fresh oracle before calling updateAll
rudewalt e961947
update comment
rudewalt faadb57
Merge branch 'main' of github.com:eq-lab/marginly into feature/compos…
rudewalt 326face
Merge branch 'main' of github.com:eq-lab/marginly into feature/compos…
rudewalt 48a6eb2
update int tests for composite oracle, added PendleMarketAdapter from…
rudewalt cffa950
update test cases for composite oracle
rudewalt 749d667
fixes in unit tests
rudewalt File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
96 changes: 96 additions & 0 deletions
96
packages/periphery/contracts/oracles/MarginlyCompositeOracle.sol
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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
154
packages/periphery/contracts/oracles/PendleMarketOracle.sol
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
| } | ||
|
|
||
| /// @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); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| } | ||
rudewalt marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| 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]; | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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,
_assertOracleIsInitializedis only called withsecondsAgo. Consider doing the same check forsecondsAgoLiquidationto 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