Skip to content
Merged
Show file tree
Hide file tree
Changes from 47 commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
a6f913d
feat: added core logic
web3rover Dec 7, 2025
edf7840
fix: usd price should be 18 decimals
web3rover Dec 8, 2025
2838512
fix: handle emode groups
web3rover Dec 8, 2025
4178f93
fix: use pancakeswap v3
web3rover Dec 8, 2025
7a8bed6
fix: updated package
web3rover Dec 8, 2025
c527c47
fix: fixed pancakeswap calculation
web3rover Dec 8, 2025
02fde57
fix: optimised code
web3rover Dec 8, 2025
6114fc3
fix: added uniswap test
web3rover Dec 8, 2025
1e4da12
fix: added fork tests for deviation logic
web3rover Dec 8, 2025
5f6c5fc
fix: enable or disable markets
web3rover Dec 9, 2025
1f96225
fix: change deviation logic
web3rover Dec 9, 2025
54ccf44
fix: pause supply
web3rover Dec 9, 2025
cc2e8cc
fix: early return
web3rover Dec 9, 2025
e7b4dd2
fix: create seperate oracle contracts
web3rover Dec 10, 2025
f77d868
fix: reanmed contract
web3rover Dec 10, 2025
cffb86f
fix: move oracles to oracle repo
web3rover Dec 10, 2025
2f898bd
Revert "fix: move oracles to oracle repo"
web3rover Dec 11, 2025
cb19bbc
fix: fixed e2e tests
web3rover Dec 11, 2025
355acef
fix: fixed tests
web3rover Dec 11, 2025
0544913
feat: added unit tests
web3rover Dec 11, 2025
1fcfcf7
chore: added events and custom errors
web3rover Dec 12, 2025
91ebcb7
fix: fix LT
web3rover Dec 12, 2025
0769189
fix: fixed var name
web3rover Dec 12, 2025
8586af0
fix: optimise code
web3rover Dec 12, 2025
b10769b
fix: optimise code
web3rover Dec 12, 2025
153e950
fix: added deployments
web3rover Dec 15, 2025
375b2f2
fix: merge conflict
web3rover Dec 15, 2025
4884493
fix: added deployments
web3rover Dec 15, 2025
ae59cd3
feat: updating deployment files
web3rover Dec 15, 2025
ea31a65
fix: added imports
web3rover Dec 16, 2025
d2085c5
Merge branch 'feat/vpd-349' of github.com:VenusProtocol/venus-periphe…
web3rover Dec 16, 2025
488caf5
Merge branch 'develop' into feat/vpd-349
web3rover Dec 16, 2025
db59423
fix: remove duplicate imports
web3rover Dec 16, 2025
c347173
fix: optimise code
web3rover Dec 16, 2025
1b24e48
fix: remove unwanted error
web3rover Dec 16, 2025
7deec40
fix: optimise code
web3rover Dec 16, 2025
5588076
fix: fixed interfaces
web3rover Dec 16, 2025
a129bd7
fix: rearrange files
web3rover Dec 16, 2025
9d66b76
fix: use less vars
web3rover Dec 16, 2025
c8884a7
fix: fixed tests
web3rover Dec 16, 2025
5d5f549
fix: added reset func
web3rover Dec 16, 2025
5ac5e18
fix: fixed lint
web3rover Dec 16, 2025
e57ad40
fix: fixed decimals
web3rover Dec 16, 2025
5d443f5
fix: optimise code
web3rover Dec 17, 2025
526c91e
fix: optimise code
web3rover Dec 17, 2025
f63f361
fix: removed unused vars
web3rover Dec 17, 2025
bffb983
fix: added test case for non 18 reference token decimals
web3rover Dec 17, 2025
a6642ef
fix: deployed new contracts
web3rover Jan 6, 2026
48ef434
fix: vld-02
web3rover Jan 8, 2026
0e0a69d
fix: vld-03
web3rover Jan 8, 2026
b2150b7
fix: vld-08
web3rover Jan 8, 2026
5588f48
fix: vld-09
web3rover Jan 8, 2026
583e2d3
fix: vld-12
web3rover Jan 8, 2026
157c217
fix: vld-06
web3rover Jan 8, 2026
931bab9
fix: removed comments
web3rover Jan 8, 2026
ddcdab8
fix: removed comments
web3rover Jan 8, 2026
5340dc5
fix: vld-03
web3rover Jan 9, 2026
ab18286
fix: added comments to call resetMarketState
web3rover Jan 9, 2026
390856b
fix: m01
web3rover Jan 13, 2026
304beb5
fix: i02
web3rover Jan 13, 2026
cf89978
fix: i04
web3rover Jan 13, 2026
f133295
feat: add comment
Debugger022 Jan 16, 2026
b209db7
chore: cleanup unused code and add SPDX identifier
Debugger022 Jan 20, 2026
f1038c5
fix: unify CollateralFactorUpdated event and expand test coverage
Debugger022 Jan 30, 2026
fc47f63
Merge pull request #44 from VenusProtocol/feat/vpd-442
Debugger022 Jan 30, 2026
60151fb
Merge pull request #42 from VenusProtocol/feat/vpd-432
Debugger022 Jan 30, 2026
6886d43
Merge branch 'feat/vpd-402' into feat/vpd-349
Debugger022 Jan 30, 2026
629fc61
chore: fixed Deviation testing
Debugger022 Jan 30, 2026
f1e48df
feat: updating deployment files
Debugger022 Jan 30, 2026
aa7ffe4
chore: fixed linter error
Debugger022 Jan 30, 2026
f88054a
chore: retrigger ci
Debugger022 Jan 30, 2026
15ff9e7
fix: skip waitConfirmations on local hardhat network
Debugger022 Jan 30, 2026
5fa293e
feat: add bscmainnet deployments
Debugger022 Feb 2, 2026
e8a953d
fix: use deployer as proxy owner on non-live networks in sentinel deploy
Debugger022 Feb 2, 2026
cc6fd27
feat: updating deployment files
Debugger022 Feb 2, 2026
f60beef
feat: refactor DeviationSentinel fork tests to use mainnet contracts
Debugger022 Feb 4, 2026
ee220df
feat: add emergency pause hashdit final audit report
Debugger022 Feb 6, 2026
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ $ yarn hardhat export --network <network-name> --export ./deployments/<network-n
## Source Code Verification

In order to verify the source code of already deployed contracts, run:
`npx hardhat etherscan-verify --network <network_name>`
`npx hardhat verify --network <network-name> <contract-address> <constructor-arg1> <constructor-arg2>`

Make sure you have added `ETHERSCAN_API_KEY` in `.env` file.

Expand Down
481 changes: 481 additions & 0 deletions contracts/DeviationSentinel/DeviationSentinel.sol

Large diffs are not rendered by default.

120 changes: 120 additions & 0 deletions contracts/DeviationSentinel/Oracles/PancakeSwapOracle.sol
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 {
/// @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
/// @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;
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);
}
}
71 changes: 71 additions & 0 deletions contracts/DeviationSentinel/Oracles/SentinelOracle.sol
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 {
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) {
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

@fred-venus fred-venus Jan 20, 2026

Choose a reason for hiding this comment

The 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:
price1 * weight1 + price2 * weight2 etc..

wdty ?

Copy link
Contributor

Choose a reason for hiding this comment

The 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);
}
}
120 changes: 120 additions & 0 deletions contracts/DeviationSentinel/Oracles/UniswapOracle.sol
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;

/// @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;
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);
}
}
Loading
Loading