-
Notifications
You must be signed in to change notification settings - Fork 2
[VPD-349] PriceDeviationSentinel Contract #24
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
Changes from 47 commits
a6f913d
edf7840
2838512
4178f93
7a8bed6
c527c47
02fde57
6114fc3
1e4da12
5f6c5fc
1f96225
54ccf44
cc2e8cc
e7b4dd2
f77d868
cffb86f
2f898bd
cb19bbc
355acef
0544913
1fcfcf7
91ebcb7
0769189
8586af0
b10769b
153e950
375b2f2
4884493
ae59cd3
ea31a65
d2085c5
488caf5
db59423
c347173
1b24e48
7deec40
5588076
a129bd7
9d66b76
c8884a7
5d5f549
5ac5e18
e57ad40
5d443f5
526c91e
f63f361
bffb983
a6642ef
48ef434
0e0a69d
b2150b7
5588f48
583e2d3
157c217
931bab9
ddcdab8
5340dc5
ab18286
390856b
304beb5
cf89978
f133295
b209db7
f1038c5
fc47f63
60151fb
6886d43
629fc61
f1e48df
aa7ffe4
f88054a
15ff9e7
5fa293e
e8a953d
cc6fd27
f60beef
ee220df
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| // SPDX-License-Identifier: BSD-3-Clause | ||
| pragma solidity ^0.8.25; | ||
|
|
||
| import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; | ||
| import { IPancakeV3Pool } from "../../Interfaces/IPancakeV3Pool.sol"; | ||
| import { ResilientOracleInterface } from "@venusprotocol/oracle/contracts/interfaces/OracleInterface.sol"; | ||
| import { AccessControlledV8 } from "@venusprotocol/governance-contracts/contracts/Governance/AccessControlledV8.sol"; | ||
| import { FixedPoint96 } from "../../Libraries/FixedPoint96.sol"; | ||
| import { FullMath } from "../../Libraries/FullMath.sol"; | ||
|
|
||
| /** | ||
| * @title PancakeSwapOracle | ||
| * @author Venus | ||
| * @notice Oracle contract for fetching asset prices from PancakeSwap V3 | ||
| */ | ||
| contract PancakeSwapOracle is AccessControlledV8 { | ||
GitGuru7 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| /// @notice Resilient Oracle for getting reference token prices | ||
| /// @custom:oz-upgrades-unsafe-allow state-variable-immutable | ||
| ResilientOracleInterface public immutable RESILIENT_ORACLE; | ||
|
|
||
| /// @notice Mapping of token addresses to their pool addresses | ||
| mapping(address => address) public tokenPools; | ||
Debugger022 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| /// @notice Emitted when a token's pool configuration is updated | ||
| /// @param token The token address | ||
| /// @param pool The pool address | ||
| event PoolConfigUpdated(address indexed token, address indexed pool); | ||
|
|
||
| /// @notice Thrown when a zero address is provided | ||
| error ZeroAddress(); | ||
|
|
||
| /// @notice Thrown when an invalid pool address is provided | ||
| error InvalidPool(); | ||
|
|
||
| /// @notice Thrown when token is not configured | ||
| error TokenNotConfigured(); | ||
|
|
||
| /// @notice Thrown when price calculation fails | ||
| error PriceCalculationError(); | ||
|
|
||
| /// @notice Constructor for PancakeSwapPriceOracle | ||
| /// @param resilientOracle_ Address of the resilient oracle | ||
| constructor(ResilientOracleInterface resilientOracle_) { | ||
| RESILIENT_ORACLE = resilientOracle_; | ||
|
|
||
| // Note that the contract is upgradeable. Use initialize() or reinitializers | ||
| // to set the state variables. | ||
| _disableInitializers(); | ||
| } | ||
|
|
||
| /// @notice Initialize the contract | ||
| /// @param accessControlManager_ Address of the access control manager | ||
| function initialize(address accessControlManager_) external initializer { | ||
| __AccessControlled_init(accessControlManager_); | ||
| } | ||
|
|
||
| /// @notice Set pool configuration for a token | ||
| /// @param token Address of the token | ||
| /// @param pool Address of the PancakeSwap V3 pool | ||
| /// @custom:event Emits PoolConfigUpdated event | ||
| /// @custom:error ZeroAddress is thrown when token or pool address is zero | ||
| function setPoolConfig(address token, address pool) external { | ||
| _checkAccessAllowed("setPoolConfig(address,address)"); | ||
|
|
||
| if (token == address(0) || pool == address(0)) revert ZeroAddress(); | ||
|
|
||
| tokenPools[token] = pool; | ||
| emit PoolConfigUpdated(token, pool); | ||
| } | ||
|
|
||
| /// @notice Get the price of an asset from PancakeSwap V3 | ||
| /// @param asset Address of the asset | ||
| /// @return price Price in (36 - asset decimals) format, same as ResilientOracle | ||
| /// @custom:error TokenNotConfigured is thrown when asset has no pool configured | ||
| function getPrice(address asset) external view returns (uint256 price) { | ||
| address pool = tokenPools[asset]; | ||
| if (pool == address(0)) revert TokenNotConfigured(); | ||
|
|
||
| return _getPancakeSwapV3Price(pool, asset); | ||
| } | ||
|
|
||
| /// @notice Get token price from PancakeSwap V3 pool | ||
| /// @param pool PancakeSwap V3 pool address | ||
| /// @param token Target token address | ||
| /// @return price Price in (36 - token decimals) format | ||
| function _getPancakeSwapV3Price(address pool, address token) internal view returns (uint256 price) { | ||
| IPancakeV3Pool v3Pool = IPancakeV3Pool(pool); | ||
| address token0 = v3Pool.token0(); | ||
| address token1 = v3Pool.token1(); | ||
| (uint160 sqrtPriceX96, , , , , , ) = v3Pool.slot0(); | ||
|
|
||
| uint256 priceX96 = FullMath.mulDiv(sqrtPriceX96, sqrtPriceX96, FixedPoint96.Q96); | ||
|
|
||
| address targetToken = token; | ||
fred-venus marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| address referenceToken; | ||
| bool targetIsToken0; | ||
|
|
||
| if (token == token0) { | ||
| targetIsToken0 = true; | ||
| referenceToken = token1; | ||
| } else if (token == token1) { | ||
| targetIsToken0 = false; | ||
| referenceToken = token0; | ||
| } else { | ||
| revert InvalidPool(); | ||
| } | ||
|
|
||
| uint256 referencePrice = RESILIENT_ORACLE.getPrice(referenceToken); | ||
|
|
||
| uint256 targetTokensPerReferenceToken; | ||
|
|
||
| if (targetIsToken0) { | ||
| targetTokensPerReferenceToken = FullMath.mulDiv(FixedPoint96.Q96 * (10 ** 18), 1, priceX96); | ||
| } else { | ||
| targetTokensPerReferenceToken = FullMath.mulDiv(priceX96 * (10 ** 18), 1, FixedPoint96.Q96); | ||
| } | ||
|
|
||
| price = FullMath.mulDiv(referencePrice, (10 ** 18), targetTokensPerReferenceToken); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| // SPDX-License-Identifier: BSD-3-Clause | ||
| pragma solidity ^0.8.25; | ||
|
|
||
| import { OracleInterface } from "@venusprotocol/oracle/contracts/interfaces/OracleInterface.sol"; | ||
| import { AccessControlledV8 } from "@venusprotocol/governance-contracts/contracts/Governance/AccessControlledV8.sol"; | ||
|
|
||
| /** | ||
| * @title SentinelOracle | ||
| * @author Venus | ||
| * @notice Aggregator oracle that routes price requests to appropriate DEX oracles | ||
| */ | ||
| contract SentinelOracle is AccessControlledV8 { | ||
| /// @notice Configuration for token price source | ||
| /// @param oracle Address of the DEX oracle to use for this token | ||
| struct TokenConfig { | ||
Debugger022 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| address oracle; | ||
| } | ||
|
|
||
| /// @notice Mapping of token addresses to their oracle configuration | ||
| mapping(address => TokenConfig) public tokenConfigs; | ||
|
|
||
| /// @notice Emitted when a token's oracle configuration is updated | ||
| /// @param token The token address | ||
| /// @param oracle The oracle address | ||
| event TokenOracleConfigUpdated(address indexed token, address indexed oracle); | ||
|
|
||
| /// @notice Thrown when a zero address is provided | ||
| error ZeroAddress(); | ||
|
|
||
| /// @notice Thrown when token is not configured | ||
| error TokenNotConfigured(); | ||
|
|
||
| /// @notice Constructor for PriceSentinelOracle | ||
| constructor() { | ||
| // Note that the contract is upgradeable. Use initialize() or reinitializers | ||
| // to set the state variables. | ||
| _disableInitializers(); | ||
| } | ||
|
|
||
| /// @notice Initialize the contract | ||
| /// @param accessControlManager_ Address of the access control manager | ||
| function initialize(address accessControlManager_) external initializer { | ||
| __AccessControlled_init(accessControlManager_); | ||
| } | ||
|
|
||
| /// @notice Set oracle configuration for a token | ||
| /// @param token Address of the token | ||
| /// @param oracle Address of the DEX oracle to use | ||
| /// @custom:event Emits TokenOracleConfigUpdated event | ||
| /// @custom:error ZeroAddress is thrown when token or oracle address is zero | ||
| function setTokenOracleConfig(address token, address oracle) external { | ||
| _checkAccessAllowed("setTokenOracleConfig(address,address)"); | ||
|
|
||
| if (token == address(0)) revert ZeroAddress(); | ||
| if (oracle == address(0)) revert ZeroAddress(); | ||
|
|
||
| tokenConfigs[token] = TokenConfig({ oracle: oracle }); | ||
| emit TokenOracleConfigUpdated(token, oracle); | ||
| } | ||
|
|
||
| /// @notice Get the price of an asset from the configured DEX oracle | ||
| /// @param asset Address of the asset | ||
| /// @return price Price in (36 - asset decimals) format, same as ResilientOracle | ||
| /// @custom:error TokenNotConfigured is thrown when asset has no oracle configured | ||
| function getPrice(address asset) external view returns (uint256 price) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Both the PancakeSwapOracle and UniswapOracle contracts share the same core logic, so we could consolidate them into a single common oracle contract instead of maintaining two separate ones. Additionally, we could further improve security by aggregating and comparing price data from multiple DEXs, rather than relying on a single source.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, i agree both points. That's for sure something we could do, together with what u mentioned TokenConfig has only one field, i am thinking we can have more fields: we could support amm pools array for one asset and each pool attached with a weight, and get price can simply return: wdty ?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah it's sounds good |
||
| TokenConfig memory config = tokenConfigs[asset]; | ||
| if (config.oracle == address(0)) revert TokenNotConfigured(); | ||
|
|
||
| return OracleInterface(config.oracle).getPrice(asset); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| // SPDX-License-Identifier: BSD-3-Clause | ||
| pragma solidity ^0.8.25; | ||
|
|
||
| import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; | ||
| import { IUniswapV3Pool } from "../../Interfaces/IUniswapV3Pool.sol"; | ||
| import { ResilientOracleInterface } from "@venusprotocol/oracle/contracts/interfaces/OracleInterface.sol"; | ||
| import { AccessControlledV8 } from "@venusprotocol/governance-contracts/contracts/Governance/AccessControlledV8.sol"; | ||
| import { FixedPoint96 } from "../../Libraries/FixedPoint96.sol"; | ||
| import { FullMath } from "../../Libraries/FullMath.sol"; | ||
|
|
||
| /** | ||
| * @title UniswapOracle | ||
| * @author Venus | ||
| * @notice Oracle contract for fetching asset prices from Uniswap V3 | ||
| */ | ||
| contract UniswapOracle is AccessControlledV8 { | ||
| /// @notice Resilient Oracle for getting reference token prices | ||
| /// @custom:oz-upgrades-unsafe-allow state-variable-immutable | ||
| ResilientOracleInterface public immutable RESILIENT_ORACLE; | ||
|
|
||
| /// @notice Mapping of token addresses to their pool addresses | ||
| mapping(address => address) public tokenPools; | ||
Debugger022 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| /// @notice Emitted when a token's pool configuration is updated | ||
| /// @param token The token address | ||
| /// @param pool The pool address | ||
| event PoolConfigUpdated(address indexed token, address indexed pool); | ||
|
|
||
| /// @notice Thrown when a zero address is provided | ||
| error ZeroAddress(); | ||
|
|
||
| /// @notice Thrown when an invalid pool address is provided | ||
| error InvalidPool(); | ||
|
|
||
| /// @notice Thrown when token is not configured | ||
| error TokenNotConfigured(); | ||
|
|
||
| /// @notice Thrown when price calculation fails | ||
| error PriceCalculationError(); | ||
|
|
||
| /// @notice Constructor for UniswapPriceOracle | ||
| /// @param resilientOracle_ Address of the resilient oracle | ||
| constructor(ResilientOracleInterface resilientOracle_) { | ||
| RESILIENT_ORACLE = resilientOracle_; | ||
|
|
||
| // Note that the contract is upgradeable. Use initialize() or reinitializers | ||
| // to set the state variables. | ||
| _disableInitializers(); | ||
| } | ||
|
|
||
| /// @notice Initialize the contract | ||
| /// @param accessControlManager_ Address of the access control manager | ||
| function initialize(address accessControlManager_) external initializer { | ||
| __AccessControlled_init(accessControlManager_); | ||
| } | ||
|
|
||
| /// @notice Set pool configuration for a token | ||
| /// @param token Address of the token | ||
| /// @param pool Address of the Uniswap V3 pool | ||
| /// @custom:event Emits PoolConfigUpdated event | ||
| /// @custom:error ZeroAddress is thrown when token or pool address is zero | ||
| function setPoolConfig(address token, address pool) external { | ||
| _checkAccessAllowed("setPoolConfig(address,address)"); | ||
|
|
||
| if (token == address(0) || pool == address(0)) revert ZeroAddress(); | ||
|
|
||
| tokenPools[token] = pool; | ||
| emit PoolConfigUpdated(token, pool); | ||
| } | ||
|
|
||
| /// @notice Get the price of an asset from Uniswap V3 | ||
| /// @param asset Address of the asset | ||
| /// @return price Price in (36 - asset decimals) format, same as ResilientOracle | ||
| /// @custom:error TokenNotConfigured is thrown when asset has no pool configured | ||
| function getPrice(address asset) external view returns (uint256 price) { | ||
| address pool = tokenPools[asset]; | ||
| if (pool == address(0)) revert TokenNotConfigured(); | ||
|
|
||
| return _getUniswapV3Price(pool, asset); | ||
| } | ||
|
|
||
| /// @notice Get token price from Uniswap V3 pool | ||
| /// @param pool Uniswap V3 pool address | ||
| /// @param token Target token address | ||
| /// @return price Price in (36 - token decimals) format | ||
| function _getUniswapV3Price(address pool, address token) internal view returns (uint256 price) { | ||
| IUniswapV3Pool v3Pool = IUniswapV3Pool(pool); | ||
| address token0 = v3Pool.token0(); | ||
| address token1 = v3Pool.token1(); | ||
| (uint160 sqrtPriceX96, , , , , , ) = v3Pool.slot0(); | ||
|
|
||
| uint256 priceX96 = FullMath.mulDiv(sqrtPriceX96, sqrtPriceX96, FixedPoint96.Q96); | ||
|
|
||
| address targetToken = token; | ||
fred-venus marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| address referenceToken; | ||
| bool targetIsToken0; | ||
|
|
||
| if (token == token0) { | ||
| targetIsToken0 = true; | ||
| referenceToken = token1; | ||
| } else if (token == token1) { | ||
| targetIsToken0 = false; | ||
| referenceToken = token0; | ||
| } else { | ||
| revert InvalidPool(); | ||
| } | ||
|
|
||
| uint256 referencePrice = RESILIENT_ORACLE.getPrice(referenceToken); | ||
|
|
||
| uint256 targetTokensPerReferenceToken; | ||
|
|
||
| if (targetIsToken0) { | ||
| targetTokensPerReferenceToken = FullMath.mulDiv(FixedPoint96.Q96 * (10 ** 18), 1, priceX96); | ||
| } else { | ||
| targetTokensPerReferenceToken = FullMath.mulDiv(priceX96 * (10 ** 18), 1, FixedPoint96.Q96); | ||
| } | ||
|
|
||
| price = FullMath.mulDiv(referencePrice, (10 ** 18), targetTokensPerReferenceToken); | ||
fred-venus marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.