Skip to content
Closed
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
68 changes: 68 additions & 0 deletions contracts/SentinelOracle.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// SPDX-License-Identifier: BSD-3-Clause
pragma solidity ^0.8.25;

import { OracleInterface } from "./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 {
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
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
function getPrice(address asset) external view returns (uint256 price) {
TokenConfig memory config = tokenConfigs[asset];
if (config.oracle == address(0)) revert TokenNotConfigured();

return OracleInterface(config.oracle).getPrice(asset);
}
}
23 changes: 23 additions & 0 deletions contracts/interfaces/IPancakeV3Pool.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.25;

interface IPancakeV3Pool {
function token0() external view returns (address);

function token1() external view returns (address);

function fee() external view returns (uint24);

function slot0()
external
view
returns (
uint160 sqrtPriceX96,
int24 tick,
uint16 observationIndex,
uint16 observationCardinality,
uint16 observationCardinalityNext,
uint32 feeProtocol,
bool unlocked
);
}
20 changes: 20 additions & 0 deletions contracts/interfaces/IUniswapV3Pool.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
pragma solidity ^0.8.25;

interface IUniswapV3Pool {
function slot0()
external
view
returns (
uint160 sqrtPriceX96,
int24 tick,
uint16 observationIndex,
uint16 observationCardinality,
uint16 observationCardinalityNext,
uint8 feeProtocol,
bool unlocked
);

function token0() external view returns (address);

function token1() external view returns (address);
}
6 changes: 6 additions & 0 deletions contracts/libraries/FixedPoint96.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
pragma solidity ^0.8.25;

library FixedPoint96 {
uint8 internal constant RESOLUTION = 96;
uint256 internal constant Q96 = 0x1000000000000000000000000;
}
50 changes: 50 additions & 0 deletions contracts/libraries/FullMath.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
pragma solidity ^0.8.25;

library FullMath {
function mulDiv(uint256 a, uint256 b, uint256 denominator) internal pure returns (uint256 result) {
uint256 prod0;
uint256 prod1;
assembly {

Check warning on line 7 in contracts/libraries/FullMath.sol

View workflow job for this annotation

GitHub Actions / Compile / Lint / Build

Avoid to use inline assembly. It is acceptable only in rare cases
let mm := mulmod(a, b, not(0))
prod0 := mul(a, b)
prod1 := sub(sub(mm, prod0), lt(mm, prod0))
}

if (prod1 == 0) {
require(denominator > 0);

Check warning on line 14 in contracts/libraries/FullMath.sol

View workflow job for this annotation

GitHub Actions / Compile / Lint / Build

Use Custom Errors instead of require statements

Check warning on line 14 in contracts/libraries/FullMath.sol

View workflow job for this annotation

GitHub Actions / Compile / Lint / Build

Provide an error message for require
assembly {

Check warning on line 15 in contracts/libraries/FullMath.sol

View workflow job for this annotation

GitHub Actions / Compile / Lint / Build

Avoid to use inline assembly. It is acceptable only in rare cases
result := div(prod0, denominator)
}
return result;
}

require(denominator > prod1);

Check warning on line 21 in contracts/libraries/FullMath.sol

View workflow job for this annotation

GitHub Actions / Compile / Lint / Build

Provide an error message for require

uint256 remainder;
assembly {
remainder := mulmod(a, b, denominator)
prod1 := sub(prod1, gt(remainder, prod0))
prod0 := sub(prod0, remainder)
}

uint256 twos = denominator & (~denominator + 1);
assembly {
denominator := div(denominator, twos)
prod0 := div(prod0, twos)
twos := add(div(sub(0, twos), twos), 1)
}

prod0 |= prod1 * twos;

uint256 inverse = (3 * denominator) ^ 2;
inverse *= 2 - denominator * inverse;
inverse *= 2 - denominator * inverse;
inverse *= 2 - denominator * inverse;
inverse *= 2 - denominator * inverse;
inverse *= 2 - denominator * inverse;
inverse *= 2 - denominator * inverse;

result = prod0 * inverse;
return result;
}
}
137 changes: 137 additions & 0 deletions contracts/oracles/PancakeSwapOracle.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// 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 "../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 {
/// @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;

/// @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
function setPoolConfig(address token, address pool) external {
_checkAccessAllowed("setPoolConfig(address,address)");

if (token == address(0)) revert ZeroAddress();
if (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
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) {
if (pool == address(0)) revert InvalidPool();

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;
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);
uint8 targetDecimals = IERC20Metadata(targetToken).decimals();
uint8 referenceDecimals = IERC20Metadata(referenceToken).decimals();
uint8 targetPriceDecimals = 36 - targetDecimals;

uint256 targetTokensPerReferenceToken;

if (targetIsToken0) {
targetTokensPerReferenceToken = FullMath.mulDiv(FixedPoint96.Q96 * (10 ** 18), 1, priceX96);
} else {
targetTokensPerReferenceToken = FullMath.mulDiv(priceX96 * (10 ** 18), 1, FixedPoint96.Q96);
}

// Calculate intermediate price in 18 decimals
price = FullMath.mulDiv(
referencePrice * (10 ** targetDecimals),
(10 ** 18),
targetTokensPerReferenceToken * (10 ** referenceDecimals)
);

// Convert from 18 decimals to target price decimals
if (targetPriceDecimals != 18) {
if (targetPriceDecimals > 18) {
price = price * (10 ** (targetPriceDecimals - 18));
} else {
price = price / (10 ** (18 - targetPriceDecimals));
}
}
}
}
Loading
Loading