From b70806ecc6e98c750b10150ba42087e856987b50 Mon Sep 17 00:00:00 2001 From: rudewalt Date: Wed, 19 Jun 2024 11:19:19 +0300 Subject: [PATCH 1/6] added composite oracle --- .../oracles/MarginlyCompositeOracle.sol | 88 +++++++++++++++++++ .../contracts/test/MockPriceOracleV2.sol | 28 ++++++ .../periphery/hardhat-configs/fork.config.ts | 4 +- .../test/MarginlyCompositionOracle.spec.ts | 87 ++++++++++++++++++ .../periphery/test/UniswapV2Oracle.spec.ts | 2 +- .../others/MarginlyCompositeOracle.spec.ts | 82 +++++++++++++++++ packages/periphery/test/shared/fixtures.ts | 61 +++++++++++++ 7 files changed, 349 insertions(+), 3 deletions(-) create mode 100644 packages/periphery/contracts/oracles/MarginlyCompositeOracle.sol create mode 100644 packages/periphery/contracts/test/MockPriceOracleV2.sol create mode 100644 packages/periphery/test/MarginlyCompositionOracle.spec.ts create mode 100644 packages/periphery/test/int/others/MarginlyCompositeOracle.spec.ts diff --git a/packages/periphery/contracts/oracles/MarginlyCompositeOracle.sol b/packages/periphery/contracts/oracles/MarginlyCompositeOracle.sol new file mode 100644 index 00000000..53ff2a11 --- /dev/null +++ b/packages/periphery/contracts/oracles/MarginlyCompositeOracle.sol @@ -0,0 +1,88 @@ +// 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 +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 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(); + + if (quoteIntermediateOracle.getBalancePrice(quoteToken, intermediateToken) == 0) revert ZeroPrice(); + if (quoteIntermediateOracle.getBalancePrice(intermediateToken, quoteToken) == 0) revert ZeroPrice(); + + if (quoteIntermediateOracle.getMargincallPrice(quoteToken, intermediateToken) == 0) revert ZeroPrice(); + if (quoteIntermediateOracle.getMargincallPrice(intermediateToken, quoteToken) == 0) revert ZeroPrice(); + + if (interMediateBaseOracle.getBalancePrice(intermediateToken, baseToken) == 0) revert ZeroPrice(); + if (interMediateBaseOracle.getBalancePrice(baseToken, intermediateToken) == 0) revert ZeroPrice(); + + if (interMediateBaseOracle.getMargincallPrice(intermediateToken, baseToken) == 0) revert ZeroPrice(); + if (interMediateBaseOracle.getMargincallPrice(baseToken, intermediateToken) == 0) revert ZeroPrice(); + + 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 = getParams[quoteToken][baseToken]; + if (params.intermediateToken == address(0)) revert NotInitialized(); + + 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 = getParams[quoteToken][baseToken]; + if (params.intermediateToken == address(0)) revert NotInitialized(); + + 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); + } +} diff --git a/packages/periphery/contracts/test/MockPriceOracleV2.sol b/packages/periphery/contracts/test/MockPriceOracleV2.sol new file mode 100644 index 00000000..e814855e --- /dev/null +++ b/packages/periphery/contracts/test/MockPriceOracleV2.sol @@ -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]; + } +} diff --git a/packages/periphery/hardhat-configs/fork.config.ts b/packages/periphery/hardhat-configs/fork.config.ts index c622df95..59101e21 100644 --- a/packages/periphery/hardhat-configs/fork.config.ts +++ b/packages/periphery/hardhat-configs/fork.config.ts @@ -8,8 +8,8 @@ const config = { hardhat: { forking: { enabled: true, - url: 'https://arb1.arbitrum.io/rpc', - blockNumber: 203690137, + url: 'https://rpc.ankr.com/arbitrum', + blockNumber: 221775851, }, }, }, diff --git a/packages/periphery/test/MarginlyCompositionOracle.spec.ts b/packages/periphery/test/MarginlyCompositionOracle.spec.ts new file mode 100644 index 00000000..26a00692 --- /dev/null +++ b/packages/periphery/test/MarginlyCompositionOracle.spec.ts @@ -0,0 +1,87 @@ +import { expect } from 'chai'; +import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; +import { ZERO_ADDRESS, createEmptyMarginlyCompositeOracle, createMarginlyCompositeOracle } from './shared/fixtures'; + +describe('MarginlyCompositeOracle', () => { + it('should fail when zero address passed', async () => { + const oracle = await loadFixture(createEmptyMarginlyCompositeOracle); + const quote = '0x0000000000000000000000000000000000000001'; + const interm = '0x0000000000000000000000000000000000000002'; + const base = '0x0000000000000000000000000000000000000003'; + const oracle1 = '0x0000000000000000000000000000000000000004'; + + await expect(oracle.setPair(ZERO_ADDRESS, interm, base, ZERO_ADDRESS, ZERO_ADDRESS)).to.be.revertedWithCustomError( + oracle, + 'ZeroAddress' + ); + + await expect(oracle.setPair(quote, ZERO_ADDRESS, base, ZERO_ADDRESS, ZERO_ADDRESS)).to.be.revertedWithCustomError( + oracle, + 'ZeroAddress' + ); + + await expect(oracle.setPair(quote, interm, ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS)).to.be.revertedWithCustomError( + oracle, + 'ZeroAddress' + ); + + await expect(oracle.setPair(quote, interm, base, ZERO_ADDRESS, oracle1)).to.be.revertedWithCustomError( + oracle, + 'ZeroAddress' + ); + + await expect(oracle.setPair(quote, interm, base, oracle1, ZERO_ADDRESS)).to.be.revertedWithCustomError( + oracle, + 'ZeroAddress' + ); + }); + + it('should return price weth/usdc with arb intermediate', async () => { + const { oracle, quoteToken: usdc, baseToken: weth } = await loadFixture(createMarginlyCompositeOracle); + + const balancePrice = await oracle.getBalancePrice(usdc.address, weth.address); + console.log(balancePrice); + + const mcPrice = await oracle.getMargincallPrice(usdc.address, weth.address); + console.log(mcPrice); + }); + + it('should return price usdc/weth with usdc intermediate', async () => { + const { oracle, quoteToken: usdc, baseToken: weth } = await loadFixture(createMarginlyCompositeOracle); + + const balancePrice = await oracle.getBalancePrice(weth.address, usdc.address); + console.log(balancePrice); + + const mcPrice = await oracle.getMargincallPrice(weth.address, usdc.address); + console.log(mcPrice); + }); + + it('should fail when pair not initialized', async () => { + const { + oracle, + quoteToken: usdc, + baseToken: weth, + intermediateToken: arb, + } = await loadFixture(createMarginlyCompositeOracle); + + await expect(oracle.getBalancePrice(arb.address, usdc.address)).to.be.revertedWithCustomError( + oracle, + 'NotInitialized' + ); + + await expect(oracle.getBalancePrice(usdc.address, arb.address)).to.be.revertedWithCustomError( + oracle, + 'NotInitialized' + ); + + await expect(oracle.getMargincallPrice(arb.address, usdc.address)).to.be.revertedWithCustomError( + oracle, + 'NotInitialized' + ); + + await expect(oracle.getMargincallPrice(usdc.address, arb.address)).to.be.revertedWithCustomError( + oracle, + 'NotInitialized' + ); + }); +}); diff --git a/packages/periphery/test/UniswapV2Oracle.spec.ts b/packages/periphery/test/UniswapV2Oracle.spec.ts index 5e351041..7026e8a2 100644 --- a/packages/periphery/test/UniswapV2Oracle.spec.ts +++ b/packages/periphery/test/UniswapV2Oracle.spec.ts @@ -293,7 +293,7 @@ describe('UniswapV2Oracle prices', () => { }); it('updateAll should work with update', async () => { - const { oracle } = await loadFixture(createUniswapV2OracleWithPairs); + const { oracle } = await createUniswapV2OracleWithPairs(); const timestamp = await time.latest(); const targetObservationIndex = await oracle.observationIndexOf(timestamp + 60); diff --git a/packages/periphery/test/int/others/MarginlyCompositeOracle.spec.ts b/packages/periphery/test/int/others/MarginlyCompositeOracle.spec.ts new file mode 100644 index 00000000..d53adc43 --- /dev/null +++ b/packages/periphery/test/int/others/MarginlyCompositeOracle.spec.ts @@ -0,0 +1,82 @@ +import { ethers } from 'hardhat'; +import { + AlgebraTickOracle, + MarginlyCompositeOracle, + UniswapV3TickOracle, +} from '../../../typechain-types/contracts/oracles'; +import { getDecimalsDiff, printPrices } from '../../shared/common'; + +describe('Composite oracle weETH/ARB with uniswapV3 weETH/WETH and uniswapV3 ARB/WETH', () => { + const uniswapFactory = '0x1F98431c8aD98523631AE4a59f267346ea31F984'; + const weETH = '0x35751007a407ca6FEFfE80b3cB397736D2cf4dbe'; + const weth = '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1'; + const arb = '0x912CE59144191C1204E64559FE8253a0e49E6548'; + + let uniswapV3Oracle: UniswapV3TickOracle; + let compositeOracle: MarginlyCompositeOracle; + + before(async () => { + const factory = await ethers.getContractFactory('UniswapV3TickOracle'); + uniswapV3Oracle = await factory.deploy(uniswapFactory); + await uniswapV3Oracle.setOptions(weth, weETH, 1800, 5, 100); + await uniswapV3Oracle.setOptions(weETH, weth, 1800, 5, 100); + await uniswapV3Oracle.setOptions(arb, weth, 1800, 5, 500); + await uniswapV3Oracle.setOptions(weth, arb, 1800, 5, 500); + + compositeOracle = await (await ethers.getContractFactory('MarginlyCompositeOracle')).deploy(); + await compositeOracle.setPair(arb, weth, weETH, uniswapV3Oracle.address, uniswapV3Oracle.address); + }); + + it('weETH/arb price', async () => { + const balancePrice = await compositeOracle.getBalancePrice(arb, weETH); + const mcPrice = await compositeOracle.getMargincallPrice(arb, weETH); + + const decimalsDiff = await getDecimalsDiff(arb, weETH); + printPrices(balancePrice, mcPrice, decimalsDiff); + }); +}); + +describe('Composite oracle wbtc/arb with uniswapV3 abr/weth and algebra-camelot weth/wbtc', () => { + const uniswapFactory = '0x1F98431c8aD98523631AE4a59f267346ea31F984'; + const camelotFactory = '0x1a3c9B1d2F0529D97f2afC5136Cc23e58f1FD35B'; + const weth = '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1'; + const wbtc = '0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f'; + const arb = '0x912CE59144191C1204E64559FE8253a0e49E6548'; + + let uniswapV3Oracle: UniswapV3TickOracle; + let camelotOracle: AlgebraTickOracle; + let compositeOracle: MarginlyCompositeOracle; + + before(async () => { + uniswapV3Oracle = await (await ethers.getContractFactory('UniswapV3TickOracle')).deploy(uniswapFactory); + await uniswapV3Oracle.setOptions(arb, weth, 1800, 5, 500); + await uniswapV3Oracle.setOptions(weth, arb, 1800, 5, 500); + + camelotOracle = await (await ethers.getContractFactory('AlgebraTickOracle')).deploy(camelotFactory); + await camelotOracle.setOptions(weth, wbtc, 1800, 5); + await camelotOracle.setOptions(wbtc, weth, 1800, 5); + + compositeOracle = await (await ethers.getContractFactory('MarginlyCompositeOracle')).deploy(); + await compositeOracle.setPair(arb, weth, wbtc, uniswapV3Oracle.address, camelotOracle.address); + }); + + it('wbtc/arb price', async () => { + const wbtcWethBalancePrice = await camelotOracle.getBalancePrice(weth, wbtc); + const wbtcWethMcPrice = await camelotOracle.getBalancePrice(weth, wbtc); + + console.log('wbtc/weth'); + printPrices(wbtcWethBalancePrice, wbtcWethMcPrice, await getDecimalsDiff(weth, wbtc)); + + const wethArbBalancePrice = await uniswapV3Oracle.getBalancePrice(arb, weth); + const wethArbMcPrice = await uniswapV3Oracle.getBalancePrice(arb, weth); + + console.log('weth/arb'); + printPrices(wethArbBalancePrice, wethArbMcPrice, await getDecimalsDiff(arb, weth)); + + const wbtcArbBalancePrice = await compositeOracle.getBalancePrice(arb, wbtc); + const wbtcArbMcPrice = await compositeOracle.getMargincallPrice(arb, wbtc); + + console.log('wbtc/arb'); + printPrices(wbtcArbBalancePrice, wbtcArbMcPrice, await getDecimalsDiff(arb, wbtc)); + }); +}); diff --git a/packages/periphery/test/shared/fixtures.ts b/packages/periphery/test/shared/fixtures.ts index 37f9a35f..884e192f 100644 --- a/packages/periphery/test/shared/fixtures.ts +++ b/packages/periphery/test/shared/fixtures.ts @@ -15,6 +15,8 @@ import { IPriceOracle, PendleMarketV3, PendlePtLpOracle, + MockPriceOracleV2, + MarginlyCompositeOracle, IPPtLpOracle, IPMarketV3, TestUniswapV2Factory, @@ -1126,3 +1128,62 @@ export async function createCurveCaseCrvUsdUsdc(): Promise { + const compositeOracle = await (await ethers.getContractFactory('MarginlyCompositeOracle')).deploy(); + return compositeOracle; +} + +type MarginlyCompositeOracleData = { + oracle: MarginlyCompositeOracle; + quoteToken: TokenInfo; + intermediateToken: TokenInfo; + baseToken: TokenInfo; + quoteIntermediateOracle: MockPriceOracleV2; + baseIntermediateOracle: MockPriceOracleV2; +}; + +export async function createMarginlyCompositeOracle(): Promise { + const compositeOracle = await createEmptyMarginlyCompositeOracle(); + const usdc: TokenInfo = { + address: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', + symbol: 'USDC', + decimals: 6, + }; + + const arb: TokenInfo = { + address: '0x912CE59144191C1204E64559FE8253a0e49E6548', + symbol: 'ARB', + decimals: 18, + }; + + const weth = { + address: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1', + symbol: 'WETH', + decimals: 18, + }; + + const oracle = await (await ethers.getContractFactory('MockPriceOracleV2')).deploy(); + { + const mcPrice = BigNumber.from('70757347443141900'); // arb/usdc = 0.89 + const balancePrice = BigNumber.from('74757347443141900'); // arb/usdc = 0.94 + await oracle.setPrice(usdc.address, arb.address, balancePrice, mcPrice); + } + + { + const mcPrice = BigNumber.from('293713709653472000000000000000000'); // wteh/abr = 3 707,188 + const balancePrice = BigNumber.from('306017264062733000000000000000000'); // wteh/abr = 3 862,480 + await oracle.setPrice(arb.address, weth.address, balancePrice, mcPrice); + } + + await compositeOracle.setPair(usdc.address, arb.address, weth.address, oracle.address, oracle.address); + + return { + oracle: compositeOracle, + quoteToken: usdc, + intermediateToken: arb, + baseToken: weth, + quoteIntermediateOracle: oracle, + baseIntermediateOracle: oracle, + }; +} From 59592a14ca4d0a97047b1b9d552be7b286002f26 Mon Sep 17 00:00:00 2001 From: rudewalt Date: Wed, 19 Jun 2024 15:08:40 +0300 Subject: [PATCH 2/6] fix: unit test, prepare fresh oracle before calling updateAll --- packages/periphery/test/UniswapV2Oracle.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/periphery/test/UniswapV2Oracle.spec.ts b/packages/periphery/test/UniswapV2Oracle.spec.ts index 7026e8a2..81fd335c 100644 --- a/packages/periphery/test/UniswapV2Oracle.spec.ts +++ b/packages/periphery/test/UniswapV2Oracle.spec.ts @@ -231,7 +231,7 @@ describe('UniswapV2Oracle prices', () => { }); it('updateAll should work', async () => { - const { oracle } = await loadFixture(createUniswapV2OracleWithPairs); + const { oracle } = await createUniswapV2OracleWithPairs(); const granularity = await oracle.granularity(); //expect all observations are empty From e961947c13336d0e46a3fcab02ecc11a5cf63b5c Mon Sep 17 00:00:00 2001 From: rudewalt Date: Tue, 25 Jun 2024 13:30:25 +0300 Subject: [PATCH 3/6] update comment --- .../periphery/contracts/oracles/MarginlyCompositeOracle.sol | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/periphery/contracts/oracles/MarginlyCompositeOracle.sol b/packages/periphery/contracts/oracles/MarginlyCompositeOracle.sol index 53ff2a11..81e5258e 100644 --- a/packages/periphery/contracts/oracles/MarginlyCompositeOracle.sol +++ b/packages/periphery/contracts/oracles/MarginlyCompositeOracle.sol @@ -6,7 +6,10 @@ import '@openzeppelin/contracts/utils/math/Math.sol'; import '@marginly/contracts/contracts/interfaces/IPriceOracle.sol'; import './CompositeOracle.sol'; -///@dev Composition of two IPriceOracles +///@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(); From 48a6eb2d399003526490d51ac2133b07d44a9758 Mon Sep 17 00:00:00 2001 From: rudewalt Date: Mon, 10 Feb 2025 13:05:14 +0300 Subject: [PATCH 4/6] update int tests for composite oracle, added PendleMarketAdapter from pendle branch --- .../contracts/oracles/PendleMarketOracle.sol | 154 ++++++++++++++++++ .../{fork.config.ts => arb-fork.config.ts} | 0 .../hardhat-configs/eth-fork.config.ts | 18 ++ packages/periphery/package.json | 3 +- .../{ => arb}/others/AlgebraOracle.spec.ts | 4 +- .../{ => arb}/others/ChainlinkOracle.spec.ts | 4 +- .../int/{ => arb}/others/CurveOracle.spec.ts | 4 +- .../others/MarginlyCompositeOracle.spec.ts | 6 +- .../int/{ => arb}/others/PythOracle.spec.ts | 4 +- .../{ => arb}/others/UniswapV2Oracle.spec.ts | 4 +- .../int/{ => arb}/pendle/PendleOracle.spec.ts | 0 .../test/int/{ => arb}/pendle/common.ts | 0 .../int/eth/MarginlyCompositeOracle.spec.ts | 68 ++++++++ 13 files changed, 256 insertions(+), 13 deletions(-) create mode 100644 packages/periphery/contracts/oracles/PendleMarketOracle.sol rename packages/periphery/hardhat-configs/{fork.config.ts => arb-fork.config.ts} (100%) create mode 100644 packages/periphery/hardhat-configs/eth-fork.config.ts rename packages/periphery/test/int/{ => arb}/others/AlgebraOracle.spec.ts (97%) rename packages/periphery/test/int/{ => arb}/others/ChainlinkOracle.spec.ts (95%) rename packages/periphery/test/int/{ => arb}/others/CurveOracle.spec.ts (97%) rename packages/periphery/test/int/{ => arb}/others/MarginlyCompositeOracle.spec.ts (95%) rename packages/periphery/test/int/{ => arb}/others/PythOracle.spec.ts (93%) rename packages/periphery/test/int/{ => arb}/others/UniswapV2Oracle.spec.ts (98%) rename packages/periphery/test/int/{ => arb}/pendle/PendleOracle.spec.ts (100%) rename packages/periphery/test/int/{ => arb}/pendle/common.ts (100%) create mode 100644 packages/periphery/test/int/eth/MarginlyCompositeOracle.spec.ts diff --git a/packages/periphery/contracts/oracles/PendleMarketOracle.sol b/packages/periphery/contracts/oracles/PendleMarketOracle.sol new file mode 100644 index 00000000..8f4cdcb4 --- /dev/null +++ b/packages/periphery/contracts/oracles/PendleMarketOracle.sol @@ -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); + } +} diff --git a/packages/periphery/hardhat-configs/fork.config.ts b/packages/periphery/hardhat-configs/arb-fork.config.ts similarity index 100% rename from packages/periphery/hardhat-configs/fork.config.ts rename to packages/periphery/hardhat-configs/arb-fork.config.ts diff --git a/packages/periphery/hardhat-configs/eth-fork.config.ts b/packages/periphery/hardhat-configs/eth-fork.config.ts new file mode 100644 index 00000000..7e6296a5 --- /dev/null +++ b/packages/periphery/hardhat-configs/eth-fork.config.ts @@ -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/eth', + blockNumber: 21814800, + }, + }, + }, +}; + +export default config; diff --git a/packages/periphery/package.json b/packages/periphery/package.json index 8ee8bc44..d5e9e603 100644 --- a/packages/periphery/package.json +++ b/packages/periphery/package.json @@ -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" }, diff --git a/packages/periphery/test/int/others/AlgebraOracle.spec.ts b/packages/periphery/test/int/arb/others/AlgebraOracle.spec.ts similarity index 97% rename from packages/periphery/test/int/others/AlgebraOracle.spec.ts rename to packages/periphery/test/int/arb/others/AlgebraOracle.spec.ts index 347778a1..107b8034 100644 --- a/packages/periphery/test/int/others/AlgebraOracle.spec.ts +++ b/packages/periphery/test/int/arb/others/AlgebraOracle.spec.ts @@ -1,6 +1,6 @@ import { ethers } from 'hardhat'; -import { AlgebraTickOracle, AlgebraTickOracleDouble } from '../../../typechain-types/contracts/oracles'; -import { getDecimalsDiff, printPrices } from '../../shared/common'; +import { AlgebraTickOracle, AlgebraTickOracleDouble } from '../../../../typechain-types/contracts/oracles'; +import { getDecimalsDiff, printPrices } from '../../../shared/common'; describe('Camelot oracle (AlgebraTickOracle)', () => { let oracle: AlgebraTickOracle; diff --git a/packages/periphery/test/int/others/ChainlinkOracle.spec.ts b/packages/periphery/test/int/arb/others/ChainlinkOracle.spec.ts similarity index 95% rename from packages/periphery/test/int/others/ChainlinkOracle.spec.ts rename to packages/periphery/test/int/arb/others/ChainlinkOracle.spec.ts index 0be65bcc..cc1c5857 100644 --- a/packages/periphery/test/int/others/ChainlinkOracle.spec.ts +++ b/packages/periphery/test/int/arb/others/ChainlinkOracle.spec.ts @@ -1,8 +1,8 @@ import { ethers } from 'hardhat'; import bn from 'bignumber.js'; import { BigNumber } from 'ethers'; -import { ChainlinkOracle } from '../../../typechain-types/contracts/oracles'; -import { getDecimalsDiff, printPrices } from '../../shared/common'; +import { ChainlinkOracle } from '../../../../typechain-types/contracts/oracles'; +import { getDecimalsDiff, printPrices } from '../../../shared/common'; describe('ChainlinkOracle', () => { let oracle: ChainlinkOracle; diff --git a/packages/periphery/test/int/others/CurveOracle.spec.ts b/packages/periphery/test/int/arb/others/CurveOracle.spec.ts similarity index 97% rename from packages/periphery/test/int/others/CurveOracle.spec.ts rename to packages/periphery/test/int/arb/others/CurveOracle.spec.ts index 4170c8d3..965c22f7 100644 --- a/packages/periphery/test/int/others/CurveOracle.spec.ts +++ b/packages/periphery/test/int/arb/others/CurveOracle.spec.ts @@ -1,8 +1,8 @@ import { expect } from 'chai'; import { BigNumber } from 'ethers'; -import { createCurveCaseCrvUsdUsdc, createCurveCaseFrxEthWeth, CurveOracleCaseParams } from '../../shared/fixtures'; +import { createCurveCaseCrvUsdUsdc, createCurveCaseFrxEthWeth, CurveOracleCaseParams } from '../../../shared/fixtures'; import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; -import { oneX96 } from '../pendle/common'; +import { oneX96 } from '../../pendle/common'; import { ethers } from 'hardhat'; async function fetchCurvePrices( diff --git a/packages/periphery/test/int/others/MarginlyCompositeOracle.spec.ts b/packages/periphery/test/int/arb/others/MarginlyCompositeOracle.spec.ts similarity index 95% rename from packages/periphery/test/int/others/MarginlyCompositeOracle.spec.ts rename to packages/periphery/test/int/arb/others/MarginlyCompositeOracle.spec.ts index d53adc43..f7e5355e 100644 --- a/packages/periphery/test/int/others/MarginlyCompositeOracle.spec.ts +++ b/packages/periphery/test/int/arb/others/MarginlyCompositeOracle.spec.ts @@ -2,9 +2,11 @@ import { ethers } from 'hardhat'; import { AlgebraTickOracle, MarginlyCompositeOracle, + PendleMarketOracle, + PythOracle, UniswapV3TickOracle, -} from '../../../typechain-types/contracts/oracles'; -import { getDecimalsDiff, printPrices } from '../../shared/common'; +} from '../../../../typechain-types/contracts/oracles'; +import { getDecimalsDiff, printPrices } from '../../../shared/common'; describe('Composite oracle weETH/ARB with uniswapV3 weETH/WETH and uniswapV3 ARB/WETH', () => { const uniswapFactory = '0x1F98431c8aD98523631AE4a59f267346ea31F984'; diff --git a/packages/periphery/test/int/others/PythOracle.spec.ts b/packages/periphery/test/int/arb/others/PythOracle.spec.ts similarity index 93% rename from packages/periphery/test/int/others/PythOracle.spec.ts rename to packages/periphery/test/int/arb/others/PythOracle.spec.ts index aa50b47c..3ad3dc9a 100644 --- a/packages/periphery/test/int/others/PythOracle.spec.ts +++ b/packages/periphery/test/int/arb/others/PythOracle.spec.ts @@ -1,6 +1,6 @@ import { ethers } from 'hardhat'; -import { PythOracle } from '../../../typechain-types/contracts/oracles'; -import { getDecimalsDiff, printPrices } from '../../shared/common'; +import { PythOracle } from '../../../../typechain-types/contracts/oracles'; +import { getDecimalsDiff, printPrices } from '../../../shared/common'; describe('PythOracle', () => { let oracle: PythOracle; diff --git a/packages/periphery/test/int/others/UniswapV2Oracle.spec.ts b/packages/periphery/test/int/arb/others/UniswapV2Oracle.spec.ts similarity index 98% rename from packages/periphery/test/int/others/UniswapV2Oracle.spec.ts rename to packages/periphery/test/int/arb/others/UniswapV2Oracle.spec.ts index 751e1275..609e7727 100644 --- a/packages/periphery/test/int/others/UniswapV2Oracle.spec.ts +++ b/packages/periphery/test/int/arb/others/UniswapV2Oracle.spec.ts @@ -1,9 +1,9 @@ import { ethers } from 'hardhat'; import { time, setBalance } from '@nomicfoundation/hardhat-network-helpers'; -import { UniswapV2Oracle } from '../../../typechain-types/contracts/oracles'; +import { UniswapV2Oracle } from '../../../../typechain-types/contracts/oracles'; import { parseEther, parseUnits } from 'ethers/lib/utils'; import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; -import { printPrices } from '../../shared/common'; +import { printPrices } from '../../../shared/common'; async function initSystem( uniswapV2Factory: string, diff --git a/packages/periphery/test/int/pendle/PendleOracle.spec.ts b/packages/periphery/test/int/arb/pendle/PendleOracle.spec.ts similarity index 100% rename from packages/periphery/test/int/pendle/PendleOracle.spec.ts rename to packages/periphery/test/int/arb/pendle/PendleOracle.spec.ts diff --git a/packages/periphery/test/int/pendle/common.ts b/packages/periphery/test/int/arb/pendle/common.ts similarity index 100% rename from packages/periphery/test/int/pendle/common.ts rename to packages/periphery/test/int/arb/pendle/common.ts diff --git a/packages/periphery/test/int/eth/MarginlyCompositeOracle.spec.ts b/packages/periphery/test/int/eth/MarginlyCompositeOracle.spec.ts new file mode 100644 index 00000000..0506db23 --- /dev/null +++ b/packages/periphery/test/int/eth/MarginlyCompositeOracle.spec.ts @@ -0,0 +1,68 @@ +import { ethers } from 'hardhat'; +import { MarginlyCompositeOracle, PendleMarketOracle, PythOracle } from '../../../typechain-types/contracts/oracles'; +import { getDecimalsDiff, printPrices } from '../../shared/common'; + +describe.only('Composite oracle PT-wstUSR/USR with Pendle for PT-wstUSR/wstUSR, Pyth wstUSR/USR', () => { + //https://docs.pyth.network/home/pyth-token/pyth-token-addresses + const pythContractAddress = '0x4305fb66699c3b2702d4d05cf36551390a4c69c6'; + const priceFeedId = '0xb74c2bc175c2dab850ce5a5451608501c293fe8410cb4aba7449dd1c355ab706'; + const wstUsrAddress = '0x1202f5c7b4b9e47a1a484e8b270be34dbbc75055'; + const usrAddress = '0x66a1e37c9b0eaddca17d3662d6c05f4decf3e110'; + const ptWstUsr27Mar2025 = '0xa8c8861b5ccf8cce0ade6811cd2a7a7d3222b0b8'; + const pendleMarketAddress = '0x353d0b2efb5b3a7987fb06d30ad6160522d08426'; + const pendlePtLpOracleAddress = '0x9a9fa8338dd5e5b2188006f1cd2ef26d921650c2'; + const pythMaxPriceAge = 1800; + const pendleMarketOracleSecondsAgo = 1800; + const pendleMarketOracleSecondsAgoLiquidation = 15; + + let pythOracle: PythOracle; + let pendleMarketOracle: PendleMarketOracle; + let compositeOracle: MarginlyCompositeOracle; + + before(async () => { + pythOracle = await (await ethers.getContractFactory('PythOracle')).deploy(pythContractAddress); + await pythOracle.setPair(usrAddress, wstUsrAddress, priceFeedId, pythMaxPriceAge); + + pendleMarketOracle = await (await ethers.getContractFactory('PendleMarketOracle')).deploy(pendlePtLpOracleAddress); + await pendleMarketOracle.setPair( + wstUsrAddress, + ptWstUsr27Mar2025, + pendleMarketAddress, + pendleMarketOracleSecondsAgo, + pendleMarketOracleSecondsAgoLiquidation + ); + + compositeOracle = await (await ethers.getContractFactory('MarginlyCompositeOracle')).deploy(); + await compositeOracle.setPair( + usrAddress, + wstUsrAddress, + ptWstUsr27Mar2025, + pythOracle.address, + pendleMarketOracle.address + ); + }); + + it('pt-wstUSR-27Mar2025/USR price', async () => { + const ptWsturWstUsrBalancePrice = await pendleMarketOracle.getBalancePrice(wstUsrAddress, ptWstUsr27Mar2025); + const ptWsturWstUsrMCPrice = await pendleMarketOracle.getMargincallPrice(wstUsrAddress, ptWstUsr27Mar2025); + + console.log('pt-wstUSR-27Mar2025/wstUSR'); + printPrices( + ptWsturWstUsrBalancePrice, + ptWsturWstUsrMCPrice, + await getDecimalsDiff(wstUsrAddress, ptWstUsr27Mar2025) + ); + + const wstUsr_usrPrice = await pythOracle.getBalancePrice(usrAddress, wstUsrAddress); + const wstUsr_usrPriceMcPrice = await pythOracle.getMargincallPrice(usrAddress, wstUsrAddress); + + console.log('wstUSR/USR'); + printPrices(wstUsr_usrPrice, wstUsr_usrPriceMcPrice, await getDecimalsDiff(usrAddress, wstUsrAddress)); + + const wbtcArbBalancePrice = await compositeOracle.getBalancePrice(usrAddress, ptWstUsr27Mar2025); + const wbtcArbMcPrice = await compositeOracle.getMargincallPrice(usrAddress, ptWstUsr27Mar2025); + + console.log('pt-wstUSR-27Mar2025/USR'); + printPrices(wbtcArbBalancePrice, wbtcArbMcPrice, await getDecimalsDiff(usrAddress, ptWstUsr27Mar2025)); + }); +}); From cffa9503f84fbf933cf8519563e991d8948d95eb Mon Sep 17 00:00:00 2001 From: rudewalt Date: Tue, 18 Feb 2025 14:00:55 +0300 Subject: [PATCH 5/6] update test cases for composite oracle --- .../oracles/MarginlyCompositeOracle.sol | 35 ++++++----- .../int/eth/MarginlyCompositeOracle.spec.ts | 58 +++++++++++++++++++ 2 files changed, 78 insertions(+), 15 deletions(-) diff --git a/packages/periphery/contracts/oracles/MarginlyCompositeOracle.sol b/packages/periphery/contracts/oracles/MarginlyCompositeOracle.sol index 81e5258e..80fd039a 100644 --- a/packages/periphery/contracts/oracles/MarginlyCompositeOracle.sol +++ b/packages/periphery/contracts/oracles/MarginlyCompositeOracle.sol @@ -26,6 +26,22 @@ contract MarginlyCompositeOracle is IPriceOracle, Ownable2Step { mapping(address => mapping(address => OracleParams)) public getParams; + function _validateOracle(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, @@ -42,17 +58,8 @@ contract MarginlyCompositeOracle is IPriceOracle, Ownable2Step { OracleParams memory params = getParams[quoteToken][baseToken]; if (params.intermediateToken != address(0)) revert PairAlreadyExists(); - if (quoteIntermediateOracle.getBalancePrice(quoteToken, intermediateToken) == 0) revert ZeroPrice(); - if (quoteIntermediateOracle.getBalancePrice(intermediateToken, quoteToken) == 0) revert ZeroPrice(); - - if (quoteIntermediateOracle.getMargincallPrice(quoteToken, intermediateToken) == 0) revert ZeroPrice(); - if (quoteIntermediateOracle.getMargincallPrice(intermediateToken, quoteToken) == 0) revert ZeroPrice(); - - if (interMediateBaseOracle.getBalancePrice(intermediateToken, baseToken) == 0) revert ZeroPrice(); - if (interMediateBaseOracle.getBalancePrice(baseToken, intermediateToken) == 0) revert ZeroPrice(); - - if (interMediateBaseOracle.getMargincallPrice(intermediateToken, baseToken) == 0) revert ZeroPrice(); - if (interMediateBaseOracle.getMargincallPrice(baseToken, intermediateToken) == 0) revert ZeroPrice(); + _validateOracle(quoteIntermediateOracle, quoteToken, intermediateToken); + _validateOracle(interMediateBaseOracle, intermediateToken, baseToken); getParams[quoteToken][baseToken] = OracleParams({ intermediateToken: intermediateToken, @@ -68,8 +75,7 @@ contract MarginlyCompositeOracle is IPriceOracle, Ownable2Step { } function getBalancePrice(address quoteToken, address baseToken) external view override returns (uint256) { - OracleParams memory params = getParams[quoteToken][baseToken]; - if (params.intermediateToken == address(0)) revert NotInitialized(); + OracleParams memory params = _getOracleParamsSafe(quoteToken, baseToken); uint256 firstPrice = params.quoteIntermediateOracle.getBalancePrice(quoteToken, params.intermediateToken); uint256 secondPrice = params.interMediateBaseOracle.getBalancePrice(params.intermediateToken, baseToken); @@ -79,8 +85,7 @@ contract MarginlyCompositeOracle is IPriceOracle, Ownable2Step { } function getMargincallPrice(address quoteToken, address baseToken) external view override returns (uint256) { - OracleParams memory params = getParams[quoteToken][baseToken]; - if (params.intermediateToken == address(0)) revert NotInitialized(); + OracleParams memory params = _getOracleParamsSafe(quoteToken, baseToken); uint256 firstPrice = params.quoteIntermediateOracle.getMargincallPrice(quoteToken, params.intermediateToken); uint256 secondPrice = params.interMediateBaseOracle.getMargincallPrice(params.intermediateToken, baseToken); diff --git a/packages/periphery/test/int/eth/MarginlyCompositeOracle.spec.ts b/packages/periphery/test/int/eth/MarginlyCompositeOracle.spec.ts index 0506db23..cb26ddc7 100644 --- a/packages/periphery/test/int/eth/MarginlyCompositeOracle.spec.ts +++ b/packages/periphery/test/int/eth/MarginlyCompositeOracle.spec.ts @@ -1,6 +1,7 @@ import { ethers } from 'hardhat'; import { MarginlyCompositeOracle, PendleMarketOracle, PythOracle } from '../../../typechain-types/contracts/oracles'; import { getDecimalsDiff, printPrices } from '../../shared/common'; +import { CurveOracle } from '../../../typechain-types'; describe.only('Composite oracle PT-wstUSR/USR with Pendle for PT-wstUSR/wstUSR, Pyth wstUSR/USR', () => { //https://docs.pyth.network/home/pyth-token/pyth-token-addresses @@ -66,3 +67,60 @@ describe.only('Composite oracle PT-wstUSR/USR with Pendle for PT-wstUSR/wstUSR, printPrices(wbtcArbBalancePrice, wbtcArbMcPrice, await getDecimalsDiff(usrAddress, ptWstUsr27Mar2025)); }); }); + +describe.only('Composite oracle Spectra PT-wstUSR/USR, Pyth wstUSR/USR', () => { + //https://docs.pyth.network/home/pyth-token/pyth-token-addresses + const pythContractAddress = '0x4305fb66699c3b2702d4d05cf36551390a4c69c6'; + const priceFeedId = '0xb74c2bc175c2dab850ce5a5451608501c293fe8410cb4aba7449dd1c355ab706'; + const wstUsrAddress = '0x1202f5c7b4b9e47a1a484e8b270be34dbbc75055'; + const usrAddress = '0x66a1e37c9b0eaddca17d3662d6c05f4decf3e110'; + const ptWstUsr26Jun2025 = '0x4a977653c58cfd82d42fd706cf68a0c1b6d0ca56'; + const spectraPool = '0x16d050778b6599ce94993d2ff83f8da7136421a9'; + const pythMaxPriceAge = 1800; + + let pythOracle: PythOracle; + let curveOracle: CurveOracle; + let compositeOracle: MarginlyCompositeOracle; + + before(async () => { + pythOracle = await (await ethers.getContractFactory('PythOracle')).deploy(pythContractAddress); + await pythOracle.setPair(usrAddress, wstUsrAddress, priceFeedId, pythMaxPriceAge); + + curveOracle = await (await ethers.getContractFactory('CurveOracle')).deploy(); + await curveOracle.addPool(spectraPool, wstUsrAddress, ptWstUsr26Jun2025, false); + await curveOracle.addPool(spectraPool, ptWstUsr26Jun2025, wstUsrAddress, false); + + compositeOracle = await (await ethers.getContractFactory('MarginlyCompositeOracle')).deploy(); + await compositeOracle.setPair( + usrAddress, + wstUsrAddress, + ptWstUsr26Jun2025, + pythOracle.address, + curveOracle.address + ); + }); + + it('pt-wstUSR-27Mar2025/USR price', async () => { + const ptWsturWstUsrBalancePrice = await curveOracle.getBalancePrice(wstUsrAddress, ptWstUsr26Jun2025); + const ptWsturWstUsrMCPrice = await curveOracle.getMargincallPrice(wstUsrAddress, ptWstUsr26Jun2025); + + console.log('pt-wstUSR-27Mar2025/wstUSR'); + printPrices( + ptWsturWstUsrBalancePrice, + ptWsturWstUsrMCPrice, + await getDecimalsDiff(wstUsrAddress, ptWstUsr26Jun2025) + ); + + const wstUsr_usrPrice = await pythOracle.getBalancePrice(usrAddress, wstUsrAddress); + const wstUsr_usrPriceMcPrice = await pythOracle.getMargincallPrice(usrAddress, wstUsrAddress); + + console.log('wstUSR/USR'); + printPrices(wstUsr_usrPrice, wstUsr_usrPriceMcPrice, await getDecimalsDiff(usrAddress, wstUsrAddress)); + + const wbtcArbBalancePrice = await compositeOracle.getBalancePrice(usrAddress, ptWstUsr26Jun2025); + const wbtcArbMcPrice = await compositeOracle.getMargincallPrice(usrAddress, ptWstUsr26Jun2025); + + console.log('pt-wstUSR-27Mar2025/USR'); + printPrices(wbtcArbBalancePrice, wbtcArbMcPrice, await getDecimalsDiff(usrAddress, ptWstUsr26Jun2025)); + }); +}); From 749d667f35cb137b959694b437de960cb03abd93 Mon Sep 17 00:00:00 2001 From: rudewalt Date: Tue, 18 Feb 2025 16:55:53 +0300 Subject: [PATCH 6/6] fixes in unit tests --- .../periphery/contracts/oracles/MarginlyCompositeOracle.sol | 6 +++--- packages/periphery/test/PendleOracle.spec.ts | 3 +-- packages/periphery/test/shared/fixtures.ts | 4 +++- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/periphery/contracts/oracles/MarginlyCompositeOracle.sol b/packages/periphery/contracts/oracles/MarginlyCompositeOracle.sol index 80fd039a..27a02a4c 100644 --- a/packages/periphery/contracts/oracles/MarginlyCompositeOracle.sol +++ b/packages/periphery/contracts/oracles/MarginlyCompositeOracle.sol @@ -26,7 +26,7 @@ contract MarginlyCompositeOracle is IPriceOracle, Ownable2Step { mapping(address => mapping(address => OracleParams)) public getParams; - function _validateOracle(IPriceOracle priceOracle, address quoteToken, address baseToken) private view { + 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(); @@ -58,8 +58,8 @@ contract MarginlyCompositeOracle is IPriceOracle, Ownable2Step { OracleParams memory params = getParams[quoteToken][baseToken]; if (params.intermediateToken != address(0)) revert PairAlreadyExists(); - _validateOracle(quoteIntermediateOracle, quoteToken, intermediateToken); - _validateOracle(interMediateBaseOracle, intermediateToken, baseToken); + _validatePriceOracle(quoteIntermediateOracle, quoteToken, intermediateToken); + _validatePriceOracle(interMediateBaseOracle, intermediateToken, baseToken); getParams[quoteToken][baseToken] = OracleParams({ intermediateToken: intermediateToken, diff --git a/packages/periphery/test/PendleOracle.spec.ts b/packages/periphery/test/PendleOracle.spec.ts index 53e7de83..afec1228 100644 --- a/packages/periphery/test/PendleOracle.spec.ts +++ b/packages/periphery/test/PendleOracle.spec.ts @@ -1,8 +1,7 @@ import { expect } from 'chai'; import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; -import { createPendleUnitTestCase } from './shared/fixtures'; +import { createPendleUnitTestCase, one, oneX96 } from './shared/fixtures'; import { ethers } from 'hardhat'; -import { one, oneX96 } from './int/pendle/common'; describe('PendleOracle prices before maturity', () => { it('getBalancePrice', async () => { diff --git a/packages/periphery/test/shared/fixtures.ts b/packages/periphery/test/shared/fixtures.ts index 884e192f..ce4775fa 100644 --- a/packages/periphery/test/shared/fixtures.ts +++ b/packages/periphery/test/shared/fixtures.ts @@ -35,9 +35,11 @@ import { UniswapV3TickOracle, UniswapV3TickOracleDouble, } from '../../typechain-types'; -import { one, oneX96 } from '../int/pendle/common'; import { BigNumber, ContractFactory } from 'ethers'; +export const oneX96 = BigNumber.from(2).pow(96); +export const one = BigNumber.from(10).pow(18); + export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; //Some random addresses for testing purposes