[VPD-349] PriceDeviationSentinel Contract#24
Conversation
|
I am thinking should we put it governance repo like https://github.com/VenusProtocol/governance-contracts/pull/163/files |
This reverts commit cffb86f.
| /// @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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 ?
- Remove duplicate CollateralFactorUpdated event overload for isolated pools, consolidate into single event with poolId param - Rewrite DeviationSentinel unit tests with comprehensive coverage: zero-address constructor checks, emode pool iteration, CF caching/restore, deviation direction flips, and sentinel direct price scenarios - Expand PancakeSwapOracle and UniswapOracle test suites with additional edge cases - Add thorough fork test suite covering end-to-end deviation handling on BSC mainnet
[VPD-442] Hashdit Findings for Emergency Pause
[VPD-432] Certik Audit Fixes for Emergency Pause
100bd49 to
15ff9e7
Compare
Conditionally set proxy owner to deployer instead of timelock on local hardhat networks for PancakeSwapOracle and UniswapOracle, matching the pattern already used by SentinelOracle and DeviationSentinel. Also remove waitConfirmations and inline verify calls from the deploy script.
- Replace mock-based tests with actual BSC mainnet fork tests - Use deployed contract addresses from bscmainnet_addresses.json - Execute VIP-900 to set up proper permissions during test setup - Extend Chainlink staleness period for BTCB to handle fork test timing - Cache oracle price at setup to avoid staleness issues during tests - Change test asset from vBNB to vBTCB (vBNB wraps native BNB with no ERC20 underlying) - Add mocha timeout configuration (200s) for fork tests in hardhat.config.ts - Fix linter errors for unused variables
GitGuru7
left a comment
There was a problem hiding this comment.
implementation LGTM, left few nitpicks
| /// @param isTrusted Whether the keeper should be trusted | ||
| /// @custom:event Emits TrustedKeeperUpdated event | ||
| /// @custom:error ZeroAddress is thrown when keeper address is zero | ||
| function setTrustedKeeper(address keeper, bool isTrusted) external { |
There was a problem hiding this comment.
we can consider reverting when isTrusted is unchanged to avoid unnecessary storage writes and also emit the previous value for better tracking.
There was a problem hiding this comment.
applicable to other similar setters
There was a problem hiding this comment.
The current implementation is correct and functional. Your suggestions are valid optimizations:
- Reverting on unchanged value saves gas by avoiding redundant
SSTOREoperations - Emitting previous value improves off-chain tracking and event indexing
However, the current simpler approach has benefits:
- Idempotent behavior (no unexpected reverts)
- Less code complexity
- Callers don't need to check current state before calling
Since the setter will primarily be called by a whitelisted address, we can assume incorrect parameters won’t be sent. Given that the code is already deployed on mainnet, I’d suggest skipping this. @fred-venus WDYT?T?
| // so we skip restoring to avoid overwriting new pool config with zero values. | ||
| // - If storedLT is 0, skip restoration to prevent setting LT=0, which could cause immediate liquidation risk. | ||
| // This also protects against uninitialized storage for new pools. | ||
| if (storedCF == 0 && currentCF != 0) { |
There was a problem hiding this comment.
Can we consider avoiding the storedCF == 0 check and rely only on currentCF != 0 when restoring values? If a CF is updated via a VIP during the pause, should the sentinel still reset it, or is the current behavior intentional ?
There was a problem hiding this comment.
The check is correct and intentional. It distinguishes between:
storedCF == 0because never stored (new pool created during pause) → skip ifcurrentCF != 0to preserve governance's new pool config- storedCF == 0 because original CF was 0 → restore allowed if currentCF is also 0
See line 475-477 comments:
If storedCF is 0 and currentCF != 0, this pool was added after _setCollateralFactorToZero, so we skip restoring to avoid overwriting new pool config with zero values.
| event SupplyUnpaused(address indexed market); | ||
|
|
||
| /// @notice Emitted when collateral factor is updated | ||
| /// @notice Emitted when collateral factor is updated |
There was a problem hiding this comment.
| /// @notice Emitted when collateral factor is updated |
There was a problem hiding this comment.
As the code is already deployed on mainnet, I would suggest to skip this @fred-venus WDYT?
| uint256 result = CORE_POOL_COMPTROLLER.setCollateralFactor(i, address(market), storedCF, storedLT); | ||
| if (result != 0) revert ComptrollerError(result); | ||
|
|
||
| emit CollateralFactorUpdated(address(market), i, 0, storedCF); |
There was a problem hiding this comment.
On restore, the event assumes currentCF is always 0, but if governance updates the CF during the pause, this may not hold. Should we consider emitting the actual previous value instead (from currentCF) for better accuracy and traceability?
There was a problem hiding this comment.
The current behavior is intentional. The event tracks DeviationSentinel's state transitions:
- On pause: oldCF = original CF → newCF = 0 (sentinel zeroing)
- On restore: oldCF = 0 → newCF = storedCF (sentinel restoring from what it set)
If governance modifies CF during the pause, that's a separate action outside the sentinel's scope. The sentinel's event accurately reflects: "I'm restoring from 0 (what I set) to the stored value.
Does this make sense?
|
merge as vip has will be proposed soon |
|
cc @Debugger022 , once the audit report is available, feel free to create a new pr |
## 1.2.0-dev.1 (2026-02-09) * Merge branch 'develop' into feat/vpd-349 ([488caf5](488caf5)) * Merge branch 'feat/vpd-349' of github.com:VenusProtocol/venus-periphery into feat/vpd-349 ([d2085c5](d2085c5)) * Merge branch 'feat/vpd-402' into feat/vpd-349 ([6886d43](6886d43)) * Merge pull request #24 from VenusProtocol/feat/vpd-349 ([69ed443](69ed443)), closes [#24](#24) * Merge pull request #42 from VenusProtocol/feat/vpd-432 ([60151fb](60151fb)), closes [#42](#42) * Merge pull request #44 from VenusProtocol/feat/vpd-442 ([fc47f63](fc47f63)), closes [#44](#44) * feat: add bscmainnet deployments ([5fa293e](5fa293e)) * feat: add comment ([f133295](f133295)) * feat: add emergency pause hashdit final audit report ([ee220df](ee220df)) * feat: added core logic ([a6f913d](a6f913d)) * feat: added unit tests ([0544913](0544913)) * feat: refactor DeviationSentinel fork tests to use mainnet contracts ([f60beef](f60beef)) * feat: updating deployment files ([cc6fd27](cc6fd27)) * feat: updating deployment files ([f1e48df](f1e48df)) * feat: updating deployment files ([ae59cd3](ae59cd3)) * fix: added comments to call resetMarketState ([ab18286](ab18286)) * fix: added deployments ([4884493](4884493)) * fix: added deployments ([153e950](153e950)) * fix: added fork tests for deviation logic ([1e4da12](1e4da12)) * fix: added imports ([ea31a65](ea31a65)) * fix: added reset func ([5d5f549](5d5f549)) * fix: added test case for non 18 reference token decimals ([bffb983](bffb983)) * fix: added uniswap test ([6114fc3](6114fc3)) * fix: change deviation logic ([1f96225](1f96225)) * fix: create seperate oracle contracts ([e7b4dd2](e7b4dd2)) * fix: deployed new contracts ([a6642ef](a6642ef)) * fix: early return ([cc2e8cc](cc2e8cc)) * fix: enable or disable markets ([5f6c5fc](5f6c5fc)) * fix: fix LT ([91ebcb7](91ebcb7)) * fix: fixed decimals ([e57ad40](e57ad40)) * fix: fixed e2e tests ([cb19bbc](cb19bbc)) * fix: fixed interfaces ([5588076](5588076)) * fix: fixed lint ([5ac5e18](5ac5e18)) * fix: fixed pancakeswap calculation ([c527c47](c527c47)) * fix: fixed tests ([c8884a7](c8884a7)) * fix: fixed tests ([355acef](355acef)) * fix: fixed var name ([0769189](0769189)) * fix: handle emode groups ([2838512](2838512)) * fix: i02 ([304beb5](304beb5)) * fix: i04 ([cf89978](cf89978)) * fix: m01 ([390856b](390856b)) * fix: merge conflict ([375b2f2](375b2f2)) * fix: optimise code ([526c91e](526c91e)) * fix: optimise code ([5d443f5](5d443f5)) * fix: optimise code ([7deec40](7deec40)) * fix: optimise code ([c347173](c347173)) * fix: optimise code ([b10769b](b10769b)) * fix: optimise code ([8586af0](8586af0)) * fix: optimised code ([02fde57](02fde57)) * fix: pause supply ([54ccf44](54ccf44)) * fix: reanmed contract ([f77d868](f77d868)) * fix: rearrange files ([a129bd7](a129bd7)) * fix: remove duplicate imports ([db59423](db59423)) * fix: remove unwanted error ([1b24e48](1b24e48)) * fix: removed comments ([ddcdab8](ddcdab8)) * fix: removed comments ([931bab9](931bab9)) * fix: removed unused vars ([f63f361](f63f361)) * fix: skip waitConfirmations on local hardhat network ([15ff9e7](15ff9e7)) * fix: unify CollateralFactorUpdated event and expand test coverage ([f1038c5](f1038c5)) * fix: updated package ([7a8bed6](7a8bed6)) * fix: usd price should be 18 decimals ([edf7840](edf7840)) * fix: use deployer as proxy owner on non-live networks in sentinel deploy ([e8a953d](e8a953d)) * fix: use less vars ([9d66b76](9d66b76)) * fix: use pancakeswap v3 ([4178f93](4178f93)) * fix: vld-02 ([48ef434](48ef434)) * fix: vld-03 ([5340dc5](5340dc5)) * fix: vld-03 ([0e0a69d](0e0a69d)) * fix: vld-06 ([157c217](157c217)) * fix: vld-08 ([b2150b7](b2150b7)) * fix: vld-09 ([5588f48](5588f48)) * fix: vld-12 ([583e2d3](583e2d3)) * chore: added events and custom errors ([1fcfcf7](1fcfcf7)) * chore: cleanup unused code and add SPDX identifier ([b209db7](b209db7)) * chore: fixed Deviation testing ([629fc61](629fc61)) * chore: fixed linter error ([aa7ffe4](aa7ffe4)) * chore: retrigger ci ([f88054a](f88054a))
Summary
deviations are detected, and restoring them when prices normalize
Problem
Price manipulation or oracle lag can leave markets exposed. The DeviationSentinel provides an automated circuit breaker that a trusted keeper can trigger when oracle prices diverge beyond a
configured threshold.
How It Works
marketStates, sets CF to 0, and pauses supply/borrowstoredCF == 0 && currentCF != 0) and avoids restoringLT = 0to prevent liquidation riskDeviationSentinel State Conflicts with Governance Actions
A conflict arises when governance (VIP) needs to modify a market's parameters (CF, LT, pause state, or e-mode configuration) while DeviationSentinel has an active deviation state for that market.
Scenario A — Governance changes CF without calling
resetMarketStateScenario B — Governance calls
resetMarketStatebut doesn't handle pause stateresetMarketState→ clears all stored state (borrowPaused = false,cfModifiedAndSupplyPaused = false, stored CFs cleared)borrowPaused = falseandcfModifiedAndSupplyPaused = false, so it does nothingScenario C — E-mode changes (add/remove pool or market)
corePoolId()tolastPoolId()— it may try to restore a CF for a pool the market is no longer in, or skip a newly added pool, leading to incorrect stateRequired Governance Procedure
When governance needs to intervene on a market with an active deviation state, the VIP must follow this exact order:
resetMarketState(market)— Clears all stored state (pause flags, stored CFs/LTs). Note: this clears the sentinel's tracking but does not unpause on the comptrollerresetMarketStateonly clears sentinel state, governance must explicitly unpause the actions that the sentinel had pausedhandleDeviation(market)— Re-evaluates the market against the current oracle prices with a clean slateWhy This Procedure Works in Every Scenario
By explicitly unpausing and then calling
handleDeviation, the sentinel captures the fresh governance-intended state:handleDeviationstores the NEW CF values (set by governance in step 3), sets CF to 0, and re-pauses the market. When the deviation resolves later,the sentinel restores the correct governance-intended CF. ✓
handleDeviationis a no-op since no deviation is detected. The market stays unpaused with the new CF values. ✓handleDeviationiterates the updated pool list (corePoolId()tolastPoolId()) and stores the correct new pool configuration. ✓The market being re-paused when a deviation still exists is the correct behavior — the sentinel should continue protecting the market, just with the updated parameters.
Contracts Added
DeviationSentinelSentinelOraclePancakeSwapOracleUniswapOracleTest Plan
NOTE: As discussed with @fred-venus Will work on this comment in future versions.