Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
4e1cf5c
refactor: initial refactor
GitGuru7 Nov 21, 2025
71a82c3
feat: add RiskOracle
GitGuru7 Nov 25, 2025
101ad3c
feat: add timelock support for risk-steward updates
GitGuru7 Nov 25, 2025
66df110
refactor: receiver contract
GitGuru7 Nov 26, 2025
2b03837
refactor: remove unnecessary complexity and streamline implementation
GitGuru7 Nov 26, 2025
9b6f3e7
feat: add multichain updates support
GitGuru7 Nov 27, 2025
0efe2f5
feat: allow immediate execution for safe-delta updates and refactor
GitGuru7 Nov 28, 2025
857da07
feat: add CollateralFactorsRiskSteward
GitGuru7 Nov 28, 2025
d931e80
feat: add IRMRiskSteward and refactor
GitGuru7 Dec 1, 2025
b12aa63
refactor: design improvements
GitGuru7 Dec 1, 2025
f0bbad7
refactor: DestinationStewardReceiver
GitGuru7 Dec 1, 2025
5e9cefa
refactor: natspecs and minor improvements
GitGuru7 Dec 2, 2025
f02e723
fix: enforce whitelisted executors for rejection and drop LI update s…
GitGuru7 Dec 4, 2025
ae1fd50
test: add initial setup and cover main execution paths
GitGuru7 Dec 5, 2025
b2785e6
test: extend coverage to all update type
GitGuru7 Dec 5, 2025
37b89a7
test: cover failure scenarios
GitGuru7 Dec 5, 2025
fb1f0ef
test: add risk-oracle unit tests
GitGuru7 Dec 8, 2025
07145c7
feat: add fork test setup and required package upgrades
GitGuru7 Dec 8, 2025
66b0659
test: add fork test to cover main execution paths
GitGuru7 Dec 8, 2025
ae83d48
fix: improve function names and events
GitGuru7 Dec 9, 2025
c123c49
feat: add pause support
GitGuru7 Dec 9, 2025
c0f9558
fix: move auth check to external functions
GitGuru7 Dec 9, 2025
94d7f21
feat: add deployment scripts
GitGuru7 Dec 9, 2025
d264915
fix: streamline process-update
GitGuru7 Dec 9, 2025
bd71012
fix: extract modifier and update lz options
GitGuru7 Dec 9, 2025
78344c9
fix: lint error
GitGuru7 Dec 9, 2025
f5124ff
ci: fix hardhat compiler download failures
GitGuru7 Dec 9, 2025
244c349
fix: ci faliure
GitGuru7 Dec 9, 2025
25c4d6c
ci: update node version in test job
GitGuru7 Dec 9, 2025
98d9b43
fix: suppress oz upgrade-safety constructor error in tests
GitGuru7 Dec 9, 2025
bb363a6
Merge branch 'acm-risk-steward' into feat/vpd-29-risk-steward-v2
GitGuru7 Dec 10, 2025
13141c3
fix: yarn-lock
GitGuru7 Dec 10, 2025
515eec0
fix: lint
GitGuru7 Dec 10, 2025
9f3b290
fix: resolve pr comments
GitGuru7 Dec 10, 2025
0bd5888
feat: expose functions to fetch update types and their count
GitGuru7 Dec 10, 2025
a074696
refactor: execute-update to prevent reentrancy risk
GitGuru7 Dec 10, 2025
3d3461e
refactor: improvements in destination-steward-receiver
GitGuru7 Dec 10, 2025
2d81a84
refactor: allow resend-remote-update to pass optional fee
GitGuru7 Dec 10, 2025
274034f
fix: resolve comments
GitGuru7 Dec 11, 2025
675e568
refactor: remove Executable from UpdateStatus
GitGuru7 Dec 11, 2025
b6ba91b
fix: [M01] Timelock and expiration time mismatch may cause update exe…
GitGuru7 Dec 16, 2025
8349b9b
fix: [I02] [I03] Code Optimization
GitGuru7 Dec 16, 2025
687ecac
fix: [M01]
GitGuru7 Dec 16, 2025
d2ac393
fix: [VEN-1] Multiple Debounce for Cross-Chain Calls
GitGuru7 Dec 16, 2025
aedaeb0
fix: [S1] Inconfigurable REMOTE_DELAY
GitGuru7 Dec 16, 2025
9967888
fix: [S2] Missing Input Validation
GitGuru7 Dec 16, 2025
631a04f
fix: [S3] Missing renounceOwnership() Override
GitGuru7 Dec 16, 2025
1865fac
fix: [S4] [S7] RiskOracle Mappings Can Be Improved
GitGuru7 Dec 16, 2025
a2612b2
fix: [S8] Inconsistent Error Handling
GitGuru7 Dec 16, 2025
95b8efc
fix: [S9] Misaligned Comments
GitGuru7 Dec 16, 2025
fe4ea95
fix: [S6] Code Clones
GitGuru7 Dec 16, 2025
5b47b4f
fix: [VRR-10] Missing Checks
GitGuru7 Dec 22, 2025
9a46116
fix: [VRR-15] CollateralFactorsRiskSteward May Allow Unsupported Upda…
GitGuru7 Dec 22, 2025
0a55a89
fix: [VRR-16] delete Keyword Can Be Used
GitGuru7 Dec 22, 2025
f4d3fcb
fix [VRR-17] [VRR-18] Missing Redundancy Check
GitGuru7 Dec 22, 2025
9a03fcd
fix: [VRR-20] Inconsistent Expiration Logic For Remote Updates
GitGuru7 Dec 22, 2025
8a8f3e5
fix: [VRR-21] Not All Contracts Disable Renouncing Ownership
GitGuru7 Dec 22, 2025
d6d20bb
fix: [VRR-23] Incomplete/Missing Comments
GitGuru7 Dec 22, 2025
a93643d
fix: [VRR-27] Misleading Name
GitGuru7 Dec 22, 2025
9ae58f7
fix: [VRR-04] Discussion On Use Of OAppUpgradeable
GitGuru7 Dec 22, 2025
ec4ff8a
fix: [VRR-01] Array Length Is Not Cached
GitGuru7 Dec 22, 2025
c9b02ac
fix: [S4]
GitGuru7 Dec 23, 2025
d10cdfe
fix: add missing NatSpec and update custom error
GitGuru7 Dec 23, 2025
7f20b3f
fix: update natspec
GitGuru7 Dec 23, 2025
b5fade5
Merge pull request #164 from VenusProtocol/fix/vpd-29-hashdit-audit
GitGuru7 Dec 24, 2025
4a3c0b0
Merge pull request #165 from VenusProtocol/fix/vpd-29-quantstamp-audit
GitGuru7 Dec 24, 2025
0e6202d
Merge pull request #166 from VenusProtocol/fix/vpd-29-certik
fred-venus Jan 2, 2026
5cbda76
Merge pull request #167 from VenusProtocol/fix/vpd-29-quantstamp-audit
fred-venus Jan 2, 2026
d8792a2
chore: remove unused Risk Steward v1 deployments
GitGuru7 Jan 2, 2026
63102ae
feat: add risk steward deployemnts on bsctestnet and sepolia
GitGuru7 Jan 2, 2026
3ef69fe
feat: updating deployment files
GitGuru7 Jan 2, 2026
0931021
fix: redeploy CF and IRM steward on sepolia
GitGuru7 Jan 2, 2026
17d7db8
feat: updating deployment files
GitGuru7 Jan 2, 2026
0fb8a5a
feat: add risk steward deployments for bscmainnet
GitGuru7 Jan 5, 2026
097df33
feat: updating deployment files
GitGuru7 Jan 5, 2026
597cea2
revert: rollback base Sepolia deployments
GitGuru7 Jan 6, 2026
4539a76
feat: add risk steward testnet deployments on all chains
GitGuru7 Jan 6, 2026
2c0da57
feat: updating deployment files
GitGuru7 Jan 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
6 changes: 4 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,17 @@ jobs:
uses: "actions/setup-node@v3"
with:
cache: "yarn"
node-version: "lts/*"
node-version: 18

- name: "Install the dependencies"
run: "yarn install --immutable"
- name: "Compile the contracts and generate the TypeChain bindings"
run: "yarn typechain"

- name: "Test the contracts and generate the coverage report"
run: "npx hardhat coverage"
run: |
yarn hardhat compile
yarn hardhat coverage

- name: "Add test summary"
run: |
Expand Down
48 changes: 48 additions & 0 deletions contracts/RiskSteward/BaseRiskSteward.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// SPDX-License-Identifier: BSD-3-Clause
pragma solidity 0.8.25;

import { AccessControlledV8 } from "../Governance/AccessControlledV8.sol";
import { IRiskSteward } from "./Interfaces/IRiskSteward.sol";

/**
* @title BaseRiskSteward
* @author Venus
* @notice Abstract base contract for Risk Steward contracts providing common functionality
* @custom:security-contact https://github.com/VenusProtocol/governance-contracts#discussion
*/
abstract contract BaseRiskSteward is IRiskSteward, AccessControlledV8 {
/// @dev Max basis points i.e., 100%
uint256 internal constant MAX_BPS = 10000;

/**
* @notice The safe delta threshold in basis points.
* @notice Updates within this delta are considered safe and require no timelock. Updates exceeding this delta require timelock.
* @dev This is only used by contracts that implement safe delta checks (e.g., MarketCapsRiskSteward, CollateralFactorsRiskSteward)
*/
uint256 public safeDeltaBps;

/**
* @notice Thrown when trying to renounce ownership
*/
error RenounceOwnershipNotAllowed();

/**
* @notice Disables renounceOwnership function
* @custom:error Throws RenounceOwnershipNotAllowed
*/
function renounceOwnership() public pure override {
revert RenounceOwnershipNotAllowed();
}

/**
* @notice Checks if the difference between new and current values is within the safe delta threshold.
* @param newValue The new value to check
* @param currentValue The current value to compare against
* @return True if the difference is within the safe delta, false otherwise
*/
function _isWithinSafeDelta(uint256 newValue, uint256 currentValue) internal view returns (bool) {
uint256 diff = newValue > currentValue ? newValue - currentValue : currentValue - newValue;
uint256 maxDiff = (safeDeltaBps * currentValue) / MAX_BPS;
return diff <= maxDiff;
}
}
274 changes: 274 additions & 0 deletions contracts/RiskSteward/CollateralFactorsRiskSteward.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
// SPDX-License-Identifier: BSD-3-Clause
pragma solidity 0.8.25;

import { RiskParameterUpdate } from "./Interfaces/IRiskOracle.sol";
import { ICorePoolVToken } from "../interfaces/ICorePoolVToken.sol";
import { ICorePoolComptroller } from "../interfaces/ICorePoolComptroller.sol";
import { IIsolatedPoolsComptroller } from "../interfaces/IIsolatedPoolsComptroller.sol";
import { IRiskStewardReceiver } from "./Interfaces/IRiskStewardReceiver.sol";
import { BaseRiskSteward } from "./BaseRiskSteward.sol";
import { ensureNonzeroAddress } from "@venusprotocol/solidity-utilities/contracts/validators.sol";

/**
* @title CollateralFactorsRiskSteward
* @author Venus
* @notice Contract that can update collateral factors and liquidation thresholds received from `RiskStewardReceiver`.
* @custom:security-contact https://github.com/VenusProtocol/governance-contracts#discussion
*/
contract CollateralFactorsRiskSteward is BaseRiskSteward {
/**
* @notice The update type for collateral factor and liquidation threshold.
*/
string public constant COLLATERAL_FACTORS = "collateralFactors";

/**
* @notice The update type key for collateral factors (keccak256 hash of COLLATERAL_FACTORS)
*/
bytes32 public constant COLLATERAL_FACTORS_KEY = keccak256(bytes(COLLATERAL_FACTORS));

/**
* @notice Address of the BNB Core Pool Comptroller.
* @dev This comptroller is specific to the BNB Core Pool, which uses a different ABI
* than isolated pools. It is used solely to detect and handle BNB Core Pool
* markets, and would not be used for remote-chain (isolated pool) deployments.
*/
ICorePoolComptroller public immutable CORE_POOL_COMPTROLLER;

/**
* @notice Address of the `RiskStewardReceiver` used to validate and dispatch incoming updates.
*/
IRiskStewardReceiver public immutable RISK_STEWARD_RECEIVER;

/**
* @dev Storage gap for upgradeability.
*/
uint256[49] private __gap;

/**
* @notice Emitted when collateral factors are updated.
*/
event CollateralFactorsUpdated(
uint256 indexed updateId,
address indexed market,
uint256 newCollateralFactor,
uint256 newLiquidationThreshold
);

/**
* @notice Emitted when the safe delta bps is updated.
*/
event SafeDeltaBpsUpdated(uint256 oldSafeDeltaBps, uint256 newSafeDeltaBps);

/**
* @notice Thrown when a `safeDeltaBps` value is greater than `MAX_BPS`.
*/
error InvalidSafeDeltaBps();

/**
* @notice Thrown when Core Pool Comptroller.setCollateralFactor fails.
*/
error SetCollateralFactorFailed(uint256 errorCode);

/**
* @notice Thrown when an invalid pool configuration is used (non-core comptroller with non-zero poolId).
*/
error InvalidPool();

/**
* @notice Thrown when an update type that is not supported is operated on.
*/
error UnsupportedUpdateType();

/**
* @notice Thrown when the update is not coming from the `RiskStewardReceiver`.
*/
error OnlyRiskStewardReceiver();

/**
* @notice Thrown when the two uint256 data length is invalid
*/
error InvalidTwoUintLength();

/**
* @notice Thrown when attempting to apply a redundant value (no-op change).
*/
error RedundantValue();

/**
* @notice Sets the immutable `CORE_POOL_COMPTROLLER` and `RISK_STEWARD_RECEIVER` addresses and disables initializers.
* @param corePoolComptroller_ The address of the Core Pool Comptroller
* @param riskStewardReceiver_ The address of the `RiskStewardReceiver`
* @custom:error Throws ZeroAddressNotAllowed if any of the addresses are zero
* @custom:oz-upgrades-unsafe-allow constructor
*/
constructor(address corePoolComptroller_, address riskStewardReceiver_) {
ensureNonzeroAddress(riskStewardReceiver_);
CORE_POOL_COMPTROLLER = ICorePoolComptroller(corePoolComptroller_);
RISK_STEWARD_RECEIVER = IRiskStewardReceiver(riskStewardReceiver_);
_disableInitializers();
}

/**
* @notice Initializes the contract as ownable and access controlled.
* @param accessControlManager_ The address of the access control manager
*/
function initialize(address accessControlManager_) external initializer {
__AccessControlled_init(accessControlManager_);
}

/**
* @notice Sets the safe delta bps.
* @param safeDeltaBps_ The new safe delta bps
* @custom:access Controlled by AccessControlManager
* @custom:event Emits SafeDeltaBpsUpdated with the old and new safe delta bps
* @custom:error Throws InvalidSafeDeltaBps if the safe delta bps is greater than MAX_BPS
* @custom:error Throws RedundantValue if the new safe delta bps is equal to the current value
*/
function setSafeDeltaBps(uint256 safeDeltaBps_) external {
_checkAccessAllowed("setSafeDeltaBps(uint256)");
if (safeDeltaBps_ > MAX_BPS) {
revert InvalidSafeDeltaBps();
}
uint256 oldSafeDeltaBps = safeDeltaBps;

if (safeDeltaBps_ == oldSafeDeltaBps) {
revert RedundantValue();
}
safeDeltaBps = safeDeltaBps_;
emit SafeDeltaBpsUpdated(oldSafeDeltaBps, safeDeltaBps_);
}

/**
* @notice Checks if an update is safe for direct execution (no timelock required).
* @param update The update to check.
* @return True if update is safe for direct execution, false if timelock is required
* @custom:error Throws UnsupportedUpdateType if the update type is not supported
* @custom:error Throws RedundantValue if the new collateral factor and liquidation threshold are unchanged
*/
function isSafeForDirectExecution(RiskParameterUpdate calldata update) external view returns (bool) {
if (update.updateTypeKey == COLLATERAL_FACTORS_KEY) {
// eMode-style updates always require timelock (not safe for direct execution)
if (update.poolId != 0) return false;

address comptroller = ICorePoolVToken(update.market).comptroller();

(uint256 newCF, uint256 newLT) = _decodeAbiEncodedTwoUint256(update.newValue);
(uint256 currCF, uint256 currLT) = _getCurrentCollateralFactors(comptroller, update.market);

// Revert on redundant updates only when both CF and LT are unchanged.
if (newCF == currCF && newLT == currLT) {
revert RedundantValue();
}

// If current values are zero, update always requires timelock
if (currCF == 0 || currLT == 0) return false;

return _isWithinSafeDelta(newCF, currCF) && _isWithinSafeDelta(newLT, currLT);
}

revert UnsupportedUpdateType();
}

/**
* @notice Applies a collateral parameter update from the `RiskStewardReceiver`.
* Delta validation and timelock checks are already performed by `RiskStewardReceiver` before execution.
* @param update RiskParameterUpdate update to apply
* @custom:access Only callable by the `RiskStewardReceiver`
* @custom:event Emits CollateralFactorsUpdated with updateId
* @custom:error Throws OnlyRiskStewardReceiver if the sender is not the `RiskStewardReceiver`
* @custom:error Throws UnsupportedUpdateType if the update type is not supported
*/
function applyUpdate(RiskParameterUpdate calldata update) external {
if (msg.sender != address(RISK_STEWARD_RECEIVER)) {
revert OnlyRiskStewardReceiver();
}

address comptroller = ICorePoolVToken(update.market).comptroller();
uint96 poolId = update.poolId;

if (update.updateTypeKey == COLLATERAL_FACTORS_KEY) {
_updateCollateralFactors(update.updateId, comptroller, update.market, poolId, update.newValue);
} else {
revert UnsupportedUpdateType();
}
}

/**
* @notice Updates the collateral factors for the given market.
* @dev Updates both collateral factor and liquidation threshold together (same setter).
* @param updateId The update ID from the Risk Oracle
* @param comptroller The comptroller address
* @param market The market to update the collateral factors for
* @param poolId The pool identifier for eMode updates (0 for regular market updates)
* @param newValue Encoded new collateral factors: `abi.encode(uint256 newCollateralFactor, uint256 newLiquidationThreshold)`
* @custom:error Throws SetCollateralFactorFailed if the core pool comptroller call to setCollateralFactor returns a non‑zero error code
* @custom:error Throws InvalidPool if a non‑core comptroller is used together with a non‑zero poolId
* @custom:event Emits CollateralFactorsUpdated with updateId
*/
function _updateCollateralFactors(
uint256 updateId,
address comptroller,
address market,
uint96 poolId,
bytes memory newValue
) internal {
(uint256 newCollateralFactor, uint256 newLiquidationThreshold) = _decodeAbiEncodedTwoUint256(newValue);

if (comptroller == address(CORE_POOL_COMPTROLLER)) {
uint256 errorCode = ICorePoolComptroller(comptroller).setCollateralFactor(
poolId,
market,
newCollateralFactor,
newLiquidationThreshold
);
if (errorCode != 0) revert SetCollateralFactorFailed(errorCode);
} else {
if (poolId != 0) revert InvalidPool();

IIsolatedPoolsComptroller(comptroller).setCollateralFactor(
market,
newCollateralFactor,
newLiquidationThreshold
);
}

emit CollateralFactorsUpdated(updateId, market, newCollateralFactor, newLiquidationThreshold);
}

/**
* @notice Returns the current collateral factors for a market on a given comptroller.
* @dev Returns both collateral factor and liquidation threshold (updated together via the same setter).
* @param comptroller The comptroller address
* @param market The market whose collateral factors are being queried
* @return currentCollateralFactor The current collateral factor
* @return currentLiquidationThreshold The current liquidation threshold
*/
function _getCurrentCollateralFactors(
address comptroller,
address market
) internal view returns (uint256 currentCollateralFactor, uint256 currentLiquidationThreshold) {
if (comptroller == address(CORE_POOL_COMPTROLLER)) {
(, currentCollateralFactor, , currentLiquidationThreshold, , , ) = ICorePoolComptroller(comptroller)
.markets(market);
} else {
(, currentCollateralFactor, currentLiquidationThreshold) = IIsolatedPoolsComptroller(comptroller).markets(
market
);
}
}

/**
* @notice Decodes ABI-encoded bytes into two uint256 values.
* @dev Expects exactly 64 bytes as produced by abi.encode(uint256,uint256).
* @param data ABI-encoded (uint256, uint256) payload
* @return a First uint256
* @return b Second uint256
* @custom:error Throws InvalidTwoUintLength if data length is not 64 bytes
*/
function _decodeAbiEncodedTwoUint256(bytes memory data) internal pure returns (uint256 a, uint256 b) {
if (data.length != 64) {
revert InvalidTwoUintLength();
}
(a, b) = abi.decode(data, (uint256, uint256));
}
}
Loading
Loading